From a030cb5a5d2d4368c824a08d0f130461659ed9e5 Mon Sep 17 00:00:00 2001 From: Berk Date: Sun, 19 Jul 2026 00:31:50 +0300 Subject: [PATCH] Gpu runtime stalls (#410) * [runtime] restore default GC mode * [cpu] add string leaf stubs * [ampr] allow concurrent reads * [bink] keep guest decode path * [kernel] streamline host memory access * [shader] add scalar memory fallback * [gpu] bound guest data pool * [gpu] reduce queue stalls * [video] stabilize guest resources * revert lock file --- docs/bink2-bridge.md | 6 +- src/SharpEmu.CLI/Program.cs | 5 - src/SharpEmu.CLI/packages.lock.json | 6 +- .../Native/DirectExecutionBackend.Imports.cs | 4 + .../Cpu/Native/DirectExecutionBackend.cs | 48 ++ src/SharpEmu.Libs/Agc/AgcExports.cs | 99 ++-- .../Agc/AgcShaderCompilerHooks.cs | 2 +- src/SharpEmu.Libs/Ampr/AmprExports.cs | 27 +- src/SharpEmu.Libs/Bink/Bink2MovieBridge.cs | 23 +- src/SharpEmu.Libs/Gpu/GuestDataPool.cs | 123 ++++- .../Kernel/KernelMemoryCompatExports.cs | 85 ++- src/SharpEmu.Libs/VideoOut/PerfOverlay.cs | 17 +- .../VideoOut/VulkanVideoPresenter.cs | 496 ++++++++++++------ .../Gen5ShaderScalarEvaluator.cs | 3 +- .../Agc/Gen5ScalarMemoryFallbackTests.cs | 99 ++++ 15 files changed, 768 insertions(+), 275 deletions(-) create mode 100644 tests/SharpEmu.Libs.Tests/Agc/Gen5ScalarMemoryFallbackTests.cs diff --git a/docs/bink2-bridge.md b/docs/bink2-bridge.md index 6ea35a57..e2ea7369 100644 --- a/docs/bink2-bridge.md +++ b/docs/bink2-bridge.md @@ -14,9 +14,9 @@ available, presents its decoded BGRA frames at the normal guest-flip boundary. This preserves the game's own timing and lets the host Vulkan presenter display the movie without trying to execute the PS5-specific Bink GPU decode path. -Without an adapter, Bink movies are skipped by default: their open call returns -not-found so games that mark cinematics as optional progress to their next -state instead of waiting on an empty Bink GPU texture. +Without an adapter, Bink files remain visible to the guest and the game's +statically linked decoder runs normally. Set SHARPEMU_BINK_MODE=skip only when +explicitly testing a title whose cinematics are optional. Set SHARPEMU_BINK_MODE=dummy to retain the open and show a built-in, non-decoded placeholder frame. This requires no SDK, but is a visual diagnostic diff --git a/src/SharpEmu.CLI/Program.cs b/src/SharpEmu.CLI/Program.cs index 3dc2fe54..97108573 100644 --- a/src/SharpEmu.CLI/Program.cs +++ b/src/SharpEmu.CLI/Program.cs @@ -45,11 +45,6 @@ internal static partial class Program [STAThread] private static int Main(string[] args) { - // Avoid blocking full collections while guest and render threads are - // running, and establish the GC mode before the runtime reserves the - // fixed guest address-space window. - System.Runtime.GCSettings.LatencyMode = System.Runtime.GCLatencyMode.SustainedLowLatency; - try { return Run(args); diff --git a/src/SharpEmu.CLI/packages.lock.json b/src/SharpEmu.CLI/packages.lock.json index 9d62b12a..f41e08b8 100644 --- a/src/SharpEmu.CLI/packages.lock.json +++ b/src/SharpEmu.CLI/packages.lock.json @@ -4,9 +4,9 @@ "net10.0": { "Microsoft.NET.ILLink.Tasks": { "type": "Direct", - "requested": "[10.0.8, )", - "resolved": "10.0.8", - "contentHash": "dVbSXGIFNR5nZcv2tOLoWI+a9T4jtFd77IYjuND+QVe360qWgAF7H0WtoopYhRw/+SgpGUTyrkrh+65+ClNnfw==" + "requested": "[10.0.3, )", + "resolved": "10.0.3", + "contentHash": "0B6nZyCHWXnvmlB559oduOspVdNOnpNXPjhpWVMovLPAsDVG7A4jJR9rzECf67JUzxP8/ee/wA8clwIzJcWNFA==" }, "Avalonia.Angle.Windows.Natives": { "type": "Transitive", diff --git a/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.Imports.cs b/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.Imports.cs index db18bcf9..978828cc 100644 --- a/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.Imports.cs +++ b/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.Imports.cs @@ -1410,6 +1410,8 @@ public sealed partial class DirectExecutionBackend "eE4Szl8sil8" or // sceKernelAprSubmitCommandBuffer "qvMUCyyaCSI" or // sceKernelAprSubmitCommandBufferAndGetId "Q2V+iqvjgC0" or // vsnprintf + "AV6ipCNa4Rw" or // strcasecmp + "viiwFMaNamA" or // strstr "q1cHNfGycLI" or // scePadRead "xk0AcarP3V4" or // scePadOpen "yH17Q6NWtVg" or // sceUserServiceGetEvent @@ -1571,6 +1573,8 @@ public sealed partial class DirectExecutionBackend "WkkeywLJcgU" or // wcslen "Ovb2dSJOAuE" or // strcmp "aesyjrHVWy4" or // strncmp + "AV6ipCNa4Rw" or // strcasecmp + "viiwFMaNamA" or // strstr "pNtJdE3x49E" or // wcscmp "fV2xHER+bKE" or // wcscoll "E8wCoUEbfzk" or // wcsncmp diff --git a/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.cs b/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.cs index cac25765..cdd7aca0 100644 --- a/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.cs +++ b/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.cs @@ -1408,6 +1408,54 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I 0x75, 0xE7, 0xC3, ], + "AV6ipCNa4Rw" => + [ + 0x0F, 0xB6, 0x07, + 0x0F, 0xB6, 0x16, + 0x8D, 0x48, 0xBF, + 0x83, 0xF9, 0x19, + 0x77, 0x03, + 0x83, 0xC0, 0x20, + 0x8D, 0x4A, 0xBF, + 0x83, 0xF9, 0x19, + 0x77, 0x03, + 0x83, 0xC2, 0x20, + 0x29, 0xD0, + 0x75, 0x0C, + 0x85, 0xD2, + 0x74, 0x08, + 0x48, 0xFF, 0xC7, + 0x48, 0xFF, 0xC6, + 0xEB, 0xD4, + 0xC3, + ], + "viiwFMaNamA" => + [ + 0x0F, 0xB6, 0x16, + 0x84, 0xD2, + 0x74, 0x2D, + 0x0F, 0xB6, 0x07, + 0x84, 0xC0, + 0x74, 0x2A, + 0x38, 0xD0, + 0x75, 0x1D, + 0x4C, 0x8D, 0x47, 0x01, + 0x4C, 0x8D, 0x4E, 0x01, + 0x41, 0x0F, 0xB6, 0x09, + 0x84, 0xC9, + 0x74, 0x12, + 0x41, 0x38, 0x08, + 0x75, 0x08, + 0x49, 0xFF, 0xC0, + 0x49, 0xFF, 0xC1, + 0xEB, 0xEB, + 0x48, 0xFF, 0xC7, + 0xEB, 0xD3, + 0x48, 0x89, 0xF8, + 0xC3, + 0x31, 0xC0, + 0xC3, + ], "pNtJdE3x49E" or "fV2xHER+bKE" => [ 0x0F, 0xB7, 0x07, diff --git a/src/SharpEmu.Libs/Agc/AgcExports.cs b/src/SharpEmu.Libs/Agc/AgcExports.cs index 39419e44..a79a1e26 100644 --- a/src/SharpEmu.Libs/Agc/AgcExports.cs +++ b/src/SharpEmu.Libs/Agc/AgcExports.cs @@ -568,6 +568,8 @@ public static partial class AgcExports public ulong WorkSequence { get; set; } public ulong SubmissionSequence { get; set; } public bool WaitMonitorRunning { get; set; } + public object WaitMonitorSignalGate { get; } = new(); + public long WaitMonitorSignalVersion { get; set; } } private readonly record struct RegisteredAgcResource( @@ -3313,24 +3315,11 @@ public static partial class AgcExports length, op, out var dispatch, - out var indirectDimsRetryAddress)) + out _)) { state.FrameDispatchCount++; ObserveComputeDispatch(ctx, gpuState, state, dispatch); } - else if (indirectDimsRetryAddress != 0 && - HandleSubmittedIndirectDimsWait( - ctx, - state, - commandAddress, - currentAddress, - offset, - dwordCount, - indirectDimsRetryAddress, - tracePackets)) - { - return true; // suspend until the producer computes the dims - } } if (op == ItNop && @@ -3668,27 +3657,11 @@ public static partial class AgcExports void CompleteAndWake() { CompleteLabelProducer(producer); - if (GpuWaitRegistry.Count == 0) + lock (gpuState.WaitMonitorSignalGate) { - return; + gpuState.WaitMonitorSignalVersion++; + Monitor.Pulse(gpuState.WaitMonitorSignalGate); } - - // Resuming a DCB can enqueue another compute dispatch and wait for - // it. Never do that reentrantly on the Vulkan render thread. - ThreadPool.UnsafeQueueUserWorkItem( - static state => - { - var (resumeContext, resumeGpuState) = state; - lock (resumeGpuState.Gate) - { - DrainResumableDcbs( - resumeContext, - resumeGpuState, - tracePackets: _traceAgc); - } - }, - (ctx, gpuState), - preferLocal: false); } void ApplyAndQueueCompletion() @@ -4867,38 +4840,45 @@ public static partial class AgcExports SubmittedGpuState gpuState) { var delayMilliseconds = 1; + long observedSignal; + lock (gpuState.WaitMonitorSignalGate) + { + observedSignal = gpuState.WaitMonitorSignalVersion; + } + while (true) { - var madeProgress = false; + int resumed; + int remaining; lock (gpuState.Gate) { - var before = GpuWaitRegistry.CountForMemory(ctx.Memory); - if (before == 0) - { - gpuState.WaitMonitorRunning = false; - return; - } - - DrainResumableDcbs(ctx, gpuState, tracePackets: _traceAgc); - var after = GpuWaitRegistry.CountForMemory(ctx.Memory); - madeProgress = after < before; - if (madeProgress) + resumed = DrainResumableDcbs(ctx, gpuState, tracePackets: _traceAgc); + remaining = GpuWaitRegistry.CountForMemory(ctx.Memory); + if (_traceAgc && resumed != 0) { Console.Error.WriteLine( - $"[LOADER][TRACE] agc.wait_monitor_resumed count={before - after} " + - $"remaining={after}"); + $"[LOADER][TRACE] agc.wait_monitor_resumed count={resumed} " + + $"remaining={remaining}"); } - if (after == 0) + if (remaining == 0) { gpuState.WaitMonitorRunning = false; return; } } - delayMilliseconds = madeProgress + delayMilliseconds = resumed != 0 ? 1 : Math.Min(delayMilliseconds * 2, 16); - Thread.Sleep(delayMilliseconds); + lock (gpuState.WaitMonitorSignalGate) + { + if (gpuState.WaitMonitorSignalVersion == observedSignal) + { + Monitor.Wait(gpuState.WaitMonitorSignalGate, delayMilliseconds); + } + + observedSignal = gpuState.WaitMonitorSignalVersion; + } } } @@ -4952,16 +4932,17 @@ public static partial class AgcExports // guest memory (labels are advanced by ReleaseMem/WriteData/DmaData packets // or direct CPU writes) and resumes the ones now satisfied. A resumed DCB // can itself write labels that unblock others, so loop to a fixed point. - private static void DrainResumableDcbs( + private static int DrainResumableDcbs( CpuContext ctx, SubmittedGpuState gpuState, bool tracePackets) { if (!_gpuWaitSuspendEnabled) { - return; + return 0; } + var resumedCount = 0; for (var pass = 0; pass < 256; pass++) { var woken = GpuWaitRegistry.CollectSatisfied(ctx.Memory, (address, is64Bit) => @@ -5039,7 +5020,7 @@ public static partial class AgcExports } } - return; + return resumedCount; } if (woken is not null) @@ -5047,9 +5028,12 @@ public static partial class AgcExports foreach (var waiter in woken) { ResumeSuspendedDcb(ctx, gpuState, waiter, tracePackets); + resumedCount++; } } } + + return resumedCount; } private static void ResumeSuspendedDcb( @@ -8894,7 +8878,7 @@ public static partial class AgcExports out _); var globalMemoryBuffers = CreateTranslatedComputeGlobalBuffers(evaluation); - var workSequence = GuestGpu.Current.SubmitComputeDispatch( + GuestGpu.Current.SubmitComputeDispatch( shaderAddress, computeShader, textures, @@ -8913,12 +8897,9 @@ public static partial class AgcExports dispatch.ThreadCountX, dispatch.ThreadCountY, dispatch.ThreadCountZ); + // Vulkan queue order keeps dependent dispatches coherent. CPU visibility is + // published by explicit PM4 release/write actions instead of per dispatch. gpuDispatch = true; - if (writesGlobalMemory && - !GuestGpu.Current.WaitForGuestWork(workSequence)) - { - computeError = $"global-write-sync-timeout sequence={workSequence}"; - } } } diff --git a/src/SharpEmu.Libs/Agc/AgcShaderCompilerHooks.cs b/src/SharpEmu.Libs/Agc/AgcShaderCompilerHooks.cs index bb6699df..bb08b944 100644 --- a/src/SharpEmu.Libs/Agc/AgcShaderCompilerHooks.cs +++ b/src/SharpEmu.Libs/Agc/AgcShaderCompilerHooks.cs @@ -27,7 +27,7 @@ internal static class AgcShaderCompilerHooks internal static void Install() { Gen5ShaderScalarEvaluator.FallbackMemoryReader = - KernelMemoryCompatExports.TryReadTrackedLibcHeap; + KernelMemoryCompatExports.TryReadShaderGuestMemory; Gen5ShaderScalarEvaluator.GlobalMemoryPool = GuestDataPool.Shared; } diff --git a/src/SharpEmu.Libs/Ampr/AmprExports.cs b/src/SharpEmu.Libs/Ampr/AmprExports.cs index 0dc0cfe0..e9202bcc 100644 --- a/src/SharpEmu.Libs/Ampr/AmprExports.cs +++ b/src/SharpEmu.Libs/Ampr/AmprExports.cs @@ -6,6 +6,7 @@ using SharpEmu.Libs.Kernel; using System.Buffers; using System.Buffers.Binary; using System.Collections.Concurrent; +using Microsoft.Win32.SafeHandles; namespace SharpEmu.Libs.Ampr; @@ -43,17 +44,17 @@ public static class AmprExports { public CachedHostFile(string path) { - Stream = new FileStream( + Handle = File.OpenHandle( path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete, - bufferSize: 1024 * 1024, FileOptions.RandomAccess); + Length = RandomAccess.GetLength(Handle); } - public object Gate { get; } = new(); - public FileStream Stream { get; } + public SafeFileHandle Handle { get; } + public long Length { get; } } [SysAbiExport( @@ -735,13 +736,7 @@ public static class AmprExports return openResult; } - long fileLength; - lock (cachedFile.Gate) - { - fileLength = cachedFile.Stream.Length; - } - - if (fileOffset >= (ulong)fileLength) + if (fileOffset >= (ulong)cachedFile.Length) { return (int)OrbisGen2Result.ORBIS_GEN2_OK; } @@ -760,12 +755,10 @@ public static class AmprExports } var request = (int)Math.Min((ulong)buffer.Length, size - bytesRead); - int read; - lock (cachedFile.Gate) - { - cachedFile.Stream.Position = unchecked((long)absoluteOffset); - read = cachedFile.Stream.Read(buffer, 0, request); - } + var read = RandomAccess.Read( + cachedFile.Handle, + buffer.AsSpan(0, request), + unchecked((long)absoluteOffset)); if (read <= 0) { diff --git a/src/SharpEmu.Libs/Bink/Bink2MovieBridge.cs b/src/SharpEmu.Libs/Bink/Bink2MovieBridge.cs index d3d5ead1..21699847 100644 --- a/src/SharpEmu.Libs/Bink/Bink2MovieBridge.cs +++ b/src/SharpEmu.Libs/Bink/Bink2MovieBridge.cs @@ -29,10 +29,9 @@ internal static class Bink2MovieBridge private static bool _availabilityReported; /// - /// Returns true when the guest should receive a normal "file not found" - /// result for a Bink movie. This is the safe default without a decoder: - /// games that treat movies as optional fall through to their next state - /// rather than submitting an empty Bink GPU texture forever. + /// Returns true only when movie skipping was explicitly requested. Without + /// a host adapter the guest must be allowed to run the Bink implementation + /// statically linked into its executable. /// internal static bool ShouldSkipGuestMovie(string hostPath) => hostPath.EndsWith(".bk2", StringComparison.OrdinalIgnoreCase) && @@ -53,12 +52,18 @@ internal static class Bink2MovieBridge return; } - if (ResolveMode() == MovieMode.Dummy) + var mode = ResolveMode(); + if (mode == MovieMode.Dummy) { AttachDummyMovieLocked(hostPath); return; } + if (mode != MovieMode.Native) + { + return; + } + var adapter = GetAdapterLocked(); if (adapter is null) { @@ -165,16 +170,15 @@ internal static class Bink2MovieBridge return MovieMode.Skip; } - // With no SDK adapter present, returning "not found" makes optional - // cinematics advance. Supplying either an explicit path or the normal - // side-by-side adapter enables native playback automatically. + // Prefer the optional host adapter when one is supplied. Otherwise let + // the game's statically linked Bink implementation consume the file. if (!string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable("SHARPEMU_BINK2_BRIDGE")) || EnumerateAdapterCandidates().Any(File.Exists)) { return MovieMode.Native; } - return MovieMode.Skip; + return MovieMode.Guest; } private static void AttachDummyMovieLocked(string hostPath) @@ -335,6 +339,7 @@ internal static class Bink2MovieBridge private enum MovieMode { + Guest, Skip, Dummy, Native, diff --git a/src/SharpEmu.Libs/Gpu/GuestDataPool.cs b/src/SharpEmu.Libs/Gpu/GuestDataPool.cs index d885bdc9..a9d395dc 100644 --- a/src/SharpEmu.Libs/Gpu/GuestDataPool.cs +++ b/src/SharpEmu.Libs/Gpu/GuestDataPool.cs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: GPL-2.0-or-later using System.Buffers; +using System.Numerics; namespace SharpEmu.Libs.Gpu; @@ -15,7 +16,125 @@ namespace SharpEmu.Libs.Gpu; /// internal static class GuestDataPool { - public static ArrayPool Shared { get; } = ArrayPool.Create( + public static ArrayPool Shared { get; } = new BoundedByteArrayPool( maxArrayLength: 16 * 1024 * 1024, - maxArraysPerBucket: 96); + maxCachedBytes: 256UL * 1024 * 1024, + maxArraysPerBucket: 8); + + public static void Trim() => ((BoundedByteArrayPool)Shared).Trim(); + + private sealed class BoundedByteArrayPool : ArrayPool + { + private readonly object _gate = new(); + private readonly int _maxArrayLength; + private readonly ulong _maxCachedBytes; + private readonly int _maxArraysPerBucket; + private readonly Dictionary> _cachedByBucket = []; + private readonly HashSet _leases = + new(System.Collections.Generic.ReferenceEqualityComparer.Instance); + private ulong _cachedBytes; + + public BoundedByteArrayPool( + int maxArrayLength, + ulong maxCachedBytes, + int maxArraysPerBucket) + { + ArgumentOutOfRangeException.ThrowIfNegativeOrZero(maxArrayLength); + ArgumentOutOfRangeException.ThrowIfZero(maxCachedBytes); + ArgumentOutOfRangeException.ThrowIfNegativeOrZero(maxArraysPerBucket); + _maxArrayLength = maxArrayLength; + _maxCachedBytes = maxCachedBytes; + _maxArraysPerBucket = maxArraysPerBucket; + } + + public override byte[] Rent(int minimumLength) + { + ArgumentOutOfRangeException.ThrowIfNegative(minimumLength); + var length = GetAllocationLength(minimumLength); + byte[]? array = null; + lock (_gate) + { + if (length <= _maxArrayLength && + _cachedByBucket.TryGetValue(length, out var bucket) && + bucket.TryPop(out array)) + { + _cachedBytes -= (ulong)array.LongLength; + } + + array ??= new byte[length]; + _leases.Add(array); + } + + return array; + } + + public override void Return(byte[] array, bool clearArray = false) + { + ArgumentNullException.ThrowIfNull(array); + lock (_gate) + { + if (!_leases.Remove(array)) + { + return; + } + } + + if (clearArray) + { + Array.Clear(array); + } + + lock (_gate) + { + if (array.Length > _maxArrayLength || + !IsBucketLength(array.Length) || + (ulong)array.LongLength > _maxCachedBytes - + Math.Min(_cachedBytes, _maxCachedBytes)) + { + return; + } + + if (!_cachedByBucket.TryGetValue(array.Length, out var bucket)) + { + bucket = new Stack(); + _cachedByBucket.Add(array.Length, bucket); + } + + if (bucket.Count >= _maxArraysPerBucket) + { + return; + } + + bucket.Push(array); + _cachedBytes += (ulong)array.LongLength; + } + } + + public void Trim() + { + lock (_gate) + { + _cachedByBucket.Clear(); + _cachedBytes = 0; + } + } + + private int GetAllocationLength(int minimumLength) + { + if (minimumLength <= 16) + { + return 16; + } + + if (minimumLength > _maxArrayLength) + { + return minimumLength; + } + + return checked((int)BitOperations.RoundUpToPowerOf2((uint)minimumLength)); + } + + private static bool IsBucketLength(int length) => + length >= 16 && (length & (length - 1)) == 0; + } } diff --git a/src/SharpEmu.Libs/Kernel/KernelMemoryCompatExports.cs b/src/SharpEmu.Libs/Kernel/KernelMemoryCompatExports.cs index dac33c6e..e1863d9e 100644 --- a/src/SharpEmu.Libs/Kernel/KernelMemoryCompatExports.cs +++ b/src/SharpEmu.Libs/Kernel/KernelMemoryCompatExports.cs @@ -6010,7 +6010,7 @@ public static partial class KernelMemoryCompatExports return highWaterMark; } - private static bool TryReadHostMemory(ulong address, Span destination) + private static unsafe bool TryReadHostMemory(ulong address, Span destination) { if (destination.IsEmpty || !IsHostRangeAccessible(address, (ulong)destination.Length, writeAccess: false)) { @@ -6019,9 +6019,7 @@ public static partial class KernelMemoryCompatExports try { - var temporary = new byte[destination.Length]; - Marshal.Copy((nint)address, temporary, 0, temporary.Length); - temporary.AsSpan().CopyTo(destination); + new ReadOnlySpan((void*)address, destination.Length).CopyTo(destination); return true; } catch @@ -6061,6 +6059,41 @@ public static partial class KernelMemoryCompatExports return false; } + internal static bool TryReadShaderGuestMemory( + ulong address, + Span destination) + { + if (destination.IsEmpty) + { + return true; + } + + if (TryReadTrackedLibcHeap(address, destination)) + { + return true; + } + + var length = (ulong)destination.Length; + lock (_memoryGate) + { + if (TryFindVirtualQueryRegionLocked( + address, + findNext: false, + out var region) && + length <= region.Length && + address >= region.Address && + length <= region.Address + region.Length - address) + { + return TryReadHostMemory(address, destination); + } + } + + // Direct execution uses guest virtual addresses as host virtual addresses. + // Some native mmap paths predate _mappedRegions tracking, so retain the same + // committed/readable-page fallback used by the libc compatibility layer. + return TryReadHostMemory(address, destination); + } + internal static bool TryReadTrackedLibcHeapGpuAlias( ulong packedAddress, Span destination) @@ -6340,7 +6373,7 @@ public static partial class KernelMemoryCompatExports return value != 0 && (value & (value - 1)) == 0; } - private static bool TryWriteHostMemory(ulong address, ReadOnlySpan source) + private static unsafe bool TryWriteHostMemory(ulong address, ReadOnlySpan source) { if (source.IsEmpty || !IsHostRangeAccessible(address, (ulong)source.Length, writeAccess: true)) { @@ -6349,8 +6382,7 @@ public static partial class KernelMemoryCompatExports try { - var temporary = source.ToArray(); - Marshal.Copy(temporary, 0, (nint)address, temporary.Length); + source.CopyTo(new Span((void*)address, source.Length)); return true; } catch @@ -6377,20 +6409,37 @@ public static partial class KernelMemoryCompatExports return false; } - if (!TryQueryHostPage(address, out var startInfo) || !HasRequiredProtection(startInfo.Protect, writeAccess)) - { - return false; - } - var endAddress = address + length - 1; - if (endAddress == address) + var currentAddress = address; + while (currentAddress <= endAddress) { - return true; - } + if (!TryQueryHostPage(currentAddress, out var info) || + !HasRequiredProtection(info.Protect, writeAccess)) + { + return false; + } - if (!TryQueryHostPage(endAddress, out var endInfo) || !HasRequiredProtection(endInfo.Protect, writeAccess)) - { - return false; + var regionBase = unchecked((ulong)info.BaseAddress); + var regionSize = (ulong)info.RegionSize; + if (regionSize == 0 || + regionBase > currentAddress || + ulong.MaxValue - regionBase < regionSize) + { + return false; + } + + var regionEnd = regionBase + regionSize; + if (regionEnd <= currentAddress) + { + return false; + } + + if (regionEnd > endAddress) + { + return true; + } + + currentAddress = regionEnd; } return true; diff --git a/src/SharpEmu.Libs/VideoOut/PerfOverlay.cs b/src/SharpEmu.Libs/VideoOut/PerfOverlay.cs index ea757c50..c2c1b422 100644 --- a/src/SharpEmu.Libs/VideoOut/PerfOverlay.cs +++ b/src/SharpEmu.Libs/VideoOut/PerfOverlay.cs @@ -36,6 +36,7 @@ public static class PerfOverlay private static long _presentedInWindow; private static long _submittedInWindow; private static long _drawsInWindow; + private static long _guestBufferCacheBytes; // Refreshed once per second so per-frame fills never allocate. private static long _statsWindowStart = Stopwatch.GetTimestamp(); @@ -74,11 +75,8 @@ public static class PerfOverlay if (last != 0) { var milliseconds = (now - last) * 1000.0 / Stopwatch.Frequency; - if (milliseconds < 1000.0) - { - _frameMilliseconds[_frameHistoryIndex] = milliseconds; - _frameHistoryIndex = (_frameHistoryIndex + 1) % FrameHistorySize; - } + _frameMilliseconds[_frameHistoryIndex] = milliseconds; + _frameHistoryIndex = (_frameHistoryIndex + 1) % FrameHistorySize; } } @@ -88,6 +86,9 @@ public static class PerfOverlay /// Called per translated draw/dispatch executed. public static void RecordDraw() => Interlocked.Increment(ref _drawsInWindow); + public static void SetGuestBufferCacheBytes(ulong bytes) => + Interlocked.Exchange(ref _guestBufferCacheBytes, checked((long)bytes)); + /// /// Rasterizes the panel into a BGRA byte span of PanelWidth x PanelHeight. /// Runs on the render thread. @@ -165,7 +166,7 @@ public static class PerfOverlay Environment.ProcessorCount; _lastCpuTime = cpuTime; - var drawsPerFrame = _fps > 0.5 ? _drawsPerSecond / _fps : 0; + var drawsPerFrame = _fps > 0 ? _drawsPerSecond / _fps : 0; var sessionStart = Interlocked.Read(ref _sessionStartTimestamp); var elapsedSeconds = sessionStart == 0 ? 0L @@ -176,7 +177,9 @@ public static class PerfOverlay _line1 = $"FPS {_fps:0.0} FLIP {_submittedFps:0.0} {_averageFrameMs:0.0} MS"; _line2 = $"DRAWS {_drawsPerSecond:0}/S {drawsPerFrame:0}/F Q {pendingWork}+{inFlightSubmissions}"; _line3 = $"ALLOC {_allocatedMbPerSecond:0.0} MB/S GC {_gen0PerWindow}/{_gen1PerWindow}/{_gen2PerWindow}"; - _line4 = $"CPU {_cpuPercent:0}% HEAP {GC.GetTotalMemory(false) / (1024 * 1024)} MB F1 HIDE"; + var heapMb = GC.GetTotalMemory(false) / (1024 * 1024); + var guestBufferMb = Interlocked.Read(ref _guestBufferCacheBytes) / (1024 * 1024); + _line4 = $"MEM {heapMb}M BUF {guestBufferMb}M CPU {_cpuPercent:0}%"; _line5 = $"TIME {elapsedHours:00}:{elapsedMinutes:00}:{elapsedRemainingSeconds:00}"; } } diff --git a/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.cs b/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.cs index 94af4c00..b7a4684e 100644 --- a/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.cs +++ b/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.cs @@ -2412,7 +2412,9 @@ internal static unsafe class VulkanVideoPresenter } } - private static bool TryTakeGuestWork(out PendingGuestWork work) + private static bool TryTakeGuestWork( + out PendingGuestWork work, + HashSet? excludedQueues = null) { lock (_gate) { @@ -2425,6 +2427,14 @@ internal static unsafe class VulkanVideoPresenter } var queueName = _pendingGuestQueueSchedule[_pendingGuestQueueCursor]; + if (excludedQueues?.Contains(queueName) == true) + { + _pendingGuestQueueCursor = + (_pendingGuestQueueCursor + 1) % _pendingGuestQueueSchedule.Count; + queuesToProbe--; + continue; + } + if (!_pendingGuestWorkByQueue.TryGetValue(queueName, out var queue) || queue.First is not { } first) { @@ -2466,6 +2476,32 @@ internal static unsafe class VulkanVideoPresenter } } + private static bool RequeueGuestWorkFront(in PendingGuestWork work) + { + lock (_gate) + { + if (_closed) + { + return false; + } + + if (!_pendingGuestWorkByQueue.TryGetValue(work.Queue.Name, out var queue)) + { + queue = new LinkedList(); + _pendingGuestWorkByQueue.Add(work.Queue.Name, queue); + _pendingGuestQueueSchedule.Add(work.Queue.Name); + } + + // TryTakeGuestWork removes only the item count. Payload ownership + // remains live until CompleteGuestWork, so requeueing must not add + // the retained-byte total a second time. + queue.AddFirst(work); + _pendingGuestWorkCount++; + System.Threading.Monitor.PulseAll(_gate); + return true; + } + } + private static void CompleteGuestWork(in PendingGuestWork pending) { SharpEmu.HLE.GuestImageWriteTracker.FlushPendingDiagnostics(); @@ -2875,11 +2911,14 @@ internal static unsafe class VulkanVideoPresenter DescriptorSetLayout DescriptorSetLayout, PipelineLayout PipelineLayout); - private readonly record struct DirtyGuestBufferRange(ulong Offset, ulong Length); + private readonly record struct DirtyGuestBufferRange( + ulong Offset, + ulong Length, + string QueueName, + ulong Timeline); private sealed class GuestBufferAllocation { - public string QueueName = VulkanGuestQueueIdentity.Default.Name; public ulong BaseAddress; public ulong Size; public VkBuffer Buffer; @@ -2890,8 +2929,6 @@ internal static unsafe class VulkanVideoPresenter public List DirtyRanges { get; } = []; } - private const string SharedReadOnlyGuestBufferQueue = "shared.readonly"; - private sealed class TranslatedDrawResources { public string DebugName = "SharpEmu translated"; @@ -4795,7 +4832,9 @@ internal static unsafe class VulkanVideoPresenter MarkGuestBufferDirty( allocation, globalBuffer.GuestOffset, - globalBuffer.GuestSize); + globalBuffer.GuestSize, + _activeGuestQueue.Name, + _submitTimeline); } } } @@ -4991,7 +5030,7 @@ internal static unsafe class VulkanVideoPresenter } } - private void WaitForActiveGuestQueueSubmissionsForCpuVisibility() + private bool TryMakeActiveGuestQueueSubmissionsCpuVisible() { FlushBatchedGuestCommands(); if (!_lastSubmittedTimelineByGuestQueue.TryGetValue( @@ -4999,7 +5038,7 @@ internal static unsafe class VulkanVideoPresenter out var targetTimeline) || targetTimeline <= _completedTimeline) { - return; + return true; } PendingGuestSubmission? target = null; @@ -5019,27 +5058,77 @@ internal static unsafe class VulkanVideoPresenter $"{targetTimeline} (completed={_completedTimeline})."); } - var waitStart = System.Diagnostics.Stopwatch.GetTimestamp(); var fence = target.Fence; - Check( - _vk.WaitForFences(_device, 1, &fence, true, ulong.MaxValue), - $"vkWaitForFences(queue visibility: {_activeGuestQueue.Name})"); + var status = _vk.GetFenceStatus(_device, fence); + if (status == Result.NotReady) + { + return false; + } + + if (status == Result.ErrorDeviceLost) + { + _deviceLost = true; + return true; + } + + Check(status, $"vkGetFenceStatus(queue visibility: {_activeGuestQueue.Name})"); CollectCompletedGuestSubmissions(waitForOldest: false); - var waitedMs = (System.Diagnostics.Stopwatch.GetTimestamp() - waitStart) * - 1000.0 / System.Diagnostics.Stopwatch.Frequency; if (_traceVulkanShaderEnabled) { TraceVulkanShader( $"vk.queue_visibility queue={_activeGuestQueue.Name} " + $"submission={_activeGuestQueue.SubmissionId} " + - $"target_timeline={targetTimeline} completed_timeline={_completedTimeline} " + - $"waited_ms={waitedMs:F3}"); + $"target_timeline={targetTimeline} completed_timeline={_completedTimeline}"); } + + return true; } - private void ExecuteOrderedGuestAction(VulkanOrderedGuestAction work) + private void WaitForGuestBufferAllocationForCpuVisibility( + GuestBufferAllocation allocation) { - WaitForActiveGuestQueueSubmissionsForCpuVisibility(); + if (IsGuestBufferAllocationReferencedByOpenBatch(allocation)) + { + FlushBatchedGuestCommands(); + } + + var targetTimeline = allocation.LastUseTimeline; + if (targetTimeline <= _completedTimeline) + { + return; + } + + PendingGuestSubmission? target = null; + foreach (var submission in _pendingGuestSubmissions) + { + if (submission.Timeline == targetTimeline) + { + target = submission; + break; + } + } + + if (target is null) + { + throw new InvalidOperationException( + $"Guest buffer 0x{allocation.BaseAddress:X16} lost pending timeline " + + $"{targetTimeline} (completed={_completedTimeline})."); + } + + var fence = target.Fence; + Check( + _vk.WaitForFences(_device, 1, &fence, true, ulong.MaxValue), + $"vkWaitForFences(buffer visibility: 0x{allocation.BaseAddress:X16})"); + CollectCompletedGuestSubmissions(waitForOldest: false); + } + + private bool TryExecuteOrderedGuestAction(VulkanOrderedGuestAction work) + { + if (!TryMakeActiveGuestQueueSubmissionsCpuVisible()) + { + return false; + } + WriteBackAllDirtyGuestBuffers(_activeGuestQueue.Name); work.Action(); if (_traceVulkanShaderEnabled) @@ -5049,6 +5138,8 @@ internal static unsafe class VulkanVideoPresenter $"submission={_activeGuestQueue.SubmissionId} " + $"work_sequence={_activeGuestWorkSequence} name='{work.DebugName}'"); } + + return true; } private void ExecuteOrderedGuestFlip(VulkanOrderedGuestFlip work) @@ -8186,9 +8277,6 @@ internal static unsafe class VulkanVideoPresenter var size = (ulong)Math.Max(guestBuffer.Length, sizeof(uint)); var endAddress = checked(guestBuffer.BaseAddress + size); GuestBufferAllocation? allocation = null; - var allocationPriority = -1; - // This runs for every bound global buffer. Preserve the previous - // stable queue preference without allocating LINQ sort state. foreach (var candidate in _guestBufferAllocations) { if (candidate.BaseAddress > guestBuffer.BaseAddress || @@ -8197,22 +8285,8 @@ internal static unsafe class VulkanVideoPresenter continue; } - var candidatePriority = string.Equals( - candidate.QueueName, - _activeGuestQueue.Name, - StringComparison.Ordinal) - ? 2 - : string.Equals( - candidate.QueueName, - SharedReadOnlyGuestBufferQueue, - StringComparison.Ordinal) - ? 1 - : 0; - if (candidatePriority > allocationPriority) - { - allocation = candidate; - allocationPriority = candidatePriority; - } + allocation = candidate; + break; } if (allocation is null) @@ -8247,11 +8321,7 @@ internal static unsafe class VulkanVideoPresenter var shadow = allocation.Shadow.AsSpan(checked((int)guestOffset), guestBuffer.Length); if (!source.SequenceEqual(shadow)) { - var sharedReadOnly = string.Equals( - allocation.QueueName, - SharedReadOnlyGuestBufferQueue, - StringComparison.Ordinal); - if (sharedReadOnly && + if (!guestBuffer.Writable && (allocation.LastUseTimeline > _completedTimeline || IsGuestBufferAllocationReferencedByOpenBatch(allocation))) { @@ -8265,14 +8335,8 @@ internal static unsafe class VulkanVideoPresenter // an in-flight shader access. Retire prior users, publish their // dirty ranges to guest memory, then upload the current guest // bytes (which may be newer than the parser's captured array). - if (!sharedReadOnly) - { - // Writable aliases are private to one logical guest queue. - // Retiring unrelated queues here recreates the global FIFO - // and turns routine buffer refreshes into queue-wide stalls. - WaitForActiveGuestQueueSubmissionsForCpuVisibility(); - WriteBackAllDirtyGuestBuffers(_activeGuestQueue.Name); - } + WaitForGuestBufferAllocationForCpuVisibility(allocation); + WriteBackAllDirtyGuestBuffers(); // Populate the cached shadow copy first and write it out to the // mapped allocation in one pass. The mapped memory is // HOST_VISIBLE|HOST_COHERENT (write-combined on most drivers), @@ -8418,7 +8482,7 @@ internal static unsafe class VulkanVideoPresenter return; } - var ranges = new List<(ulong Start, ulong End, bool Writable)>(buffers.Count); + var ranges = new List<(ulong Start, ulong End)>(buffers.Count); foreach (var buffer in buffers) { if (buffer.BaseAddress == 0) @@ -8432,8 +8496,7 @@ internal static unsafe class VulkanVideoPresenter var paddedEnd = checked(buffer.BaseAddress + size + 3) & ~3UL; ranges.Add(( alignedStart, - paddedEnd, - buffer.Writable && buffer.WriteBackToGuest)); + paddedEnd)); } if (ranges.Count == 0) @@ -8442,7 +8505,7 @@ internal static unsafe class VulkanVideoPresenter } ranges.Sort(static (left, right) => left.Start.CompareTo(right.Start)); - var merged = new List<(ulong Start, ulong End, bool Writable)>(ranges.Count); + var merged = new List<(ulong Start, ulong End)>(ranges.Count); foreach (var range in ranges) { if (merged.Count == 0 || range.Start > merged[^1].End) @@ -8454,25 +8517,18 @@ internal static unsafe class VulkanVideoPresenter var previous = merged[^1]; merged[^1] = ( previous.Start, - Math.Max(previous.End, range.End), - previous.Writable || range.Writable); + Math.Max(previous.End, range.End)); } foreach (var range in merged) { - EnsureGuestBufferAllocation( - range.Start, - range.End, - range.Writable - ? _activeGuestQueue.Name - : SharedReadOnlyGuestBufferQueue); + EnsureGuestBufferAllocation(range.Start, range.End); } } private void EnsureGuestBufferAllocation( ulong requestedStart, - ulong requestedEnd, - string queueName) + ulong requestedEnd) { var start = requestedStart; var end = requestedEnd; @@ -8481,10 +8537,6 @@ internal static unsafe class VulkanVideoPresenter { overlaps = _guestBufferAllocations .Where(allocation => - string.Equals( - allocation.QueueName, - queueName, - StringComparison.Ordinal) && allocation.BaseAddress < end && start < allocation.BaseAddress + allocation.Size) .ToList(); @@ -8521,7 +8573,7 @@ internal static unsafe class VulkanVideoPresenter WriteBackAllDirtyGuestBuffers(); } - var replacement = CreateGuestBufferAllocation(start, end, queueName); + var replacement = CreateGuestBufferAllocation(start, end); foreach (var overlap in overlaps) { _guestBufferAllocations.Remove(overlap); @@ -8530,22 +8582,27 @@ internal static unsafe class VulkanVideoPresenter _guestBufferAllocations.Add(replacement); _guestBufferAllocations.Sort(static (left, right) => - { - var queueOrder = string.CompareOrdinal(left.QueueName, right.QueueName); - return queueOrder != 0 - ? queueOrder - : left.BaseAddress.CompareTo(right.BaseAddress); - }); + left.BaseAddress.CompareTo(right.BaseAddress)); + UpdateGuestBufferCacheMetric(); TraceVulkanShader( - $"vk.guest_buffer_allocation queue={queueName} " + - $"base=0x{start:X16} bytes={replacement.Size} " + + $"vk.guest_buffer_allocation base=0x{start:X16} bytes={replacement.Size} " + $"merged={overlaps.Count}"); } + private void UpdateGuestBufferCacheMetric() + { + var bytes = 0UL; + foreach (var allocation in _guestBufferAllocations) + { + bytes = checked(bytes + allocation.Size); + } + + PerfOverlay.SetGuestBufferCacheBytes(bytes); + } + private GuestBufferAllocation CreateGuestBufferAllocation( ulong start, - ulong end, - string queueName) + ulong end) { var size = checked(end - start); if (size == 0 || size > int.MaxValue) @@ -8571,7 +8628,6 @@ internal static unsafe class VulkanVideoPresenter $"SharpEmu guest VA 0x{start:X16}-0x{end:X16}"); return new GuestBufferAllocation { - QueueName = queueName, BaseAddress = start, Size = size, Buffer = buffer, @@ -9577,15 +9633,6 @@ internal static unsafe class VulkanVideoPresenter MarkSampledImagesInitialized(resources); MarkStorageImagesInitialized(resources, traceContents: false); - if (work.WritesGlobalMemory) - { - // The CPU submit thread may immediately consume an indirect - // argument written by this dispatch. Wait for the specific - // guest fences and publish only dirty ranges; a queue-wide - // idle unnecessarily serialized presentation work too. - WaitForActiveGuestQueueSubmissionsForCpuVisibility(); - WriteBackAllDirtyGuestBuffers(_activeGuestQueue.Name); - } TraceVulkanShader( $"vk.compute_dispatch groups={work.GroupCountX}x" + $"{work.GroupCountY}x{work.GroupCountZ} " + @@ -9815,7 +9862,9 @@ internal static unsafe class VulkanVideoPresenter private static void MarkGuestBufferDirty( GuestBufferAllocation allocation, ulong offset, - ulong length) + ulong length, + string queueName, + ulong timeline) { if (length == 0) { @@ -9827,6 +9876,11 @@ internal static unsafe class VulkanVideoPresenter for (var index = allocation.DirtyRanges.Count - 1; index >= 0; index--) { var existing = allocation.DirtyRanges[index]; + if (!string.Equals(existing.QueueName, queueName, StringComparison.Ordinal)) + { + continue; + } + var existingEnd = existing.Offset + existing.Length; if (end < existing.Offset || existingEnd < start) { @@ -9835,10 +9889,12 @@ internal static unsafe class VulkanVideoPresenter start = Math.Min(start, existing.Offset); end = Math.Max(end, existingEnd); + timeline = Math.Max(timeline, existing.Timeline); allocation.DirtyRanges.RemoveAt(index); } - allocation.DirtyRanges.Add(new DirtyGuestBufferRange(start, end - start)); + allocation.DirtyRanges.Add( + new DirtyGuestBufferRange(start, end - start, queueName, timeline)); } private void WriteBackAllDirtyGuestBuffers(string? queueName = null) @@ -9851,33 +9907,51 @@ internal static unsafe class VulkanVideoPresenter foreach (var allocation in _guestBufferAllocations) { - if (queueName is not null && - !string.Equals(allocation.QueueName, queueName, StringComparison.Ordinal)) - { - continue; - } - - if (allocation.DirtyRanges.Count != 0 && - allocation.LastUseTimeline > _completedTimeline) - { - // A mapped HOST_COHERENT allocation still cannot be read - // by the CPU while a shader may be writing it. Callers - // normally retire the relevant fences first; keep this - // helper fail-closed if a future path forgets to do so. - TraceVulkanShader( - $"vk.global_writeback_deferred base=0x{allocation.BaseAddress:X16} " + - $"last_use={allocation.LastUseTimeline} completed={_completedTimeline}"); - continue; - } - for (var index = allocation.DirtyRanges.Count - 1; index >= 0; index--) { var range = allocation.DirtyRanges[index]; + if ((queueName is not null && + !string.Equals(range.QueueName, queueName, StringComparison.Ordinal)) || + range.Timeline > _completedTimeline) + { + continue; + } + if (range.Length == 0 || range.Length > int.MaxValue) { continue; } + var rangeEnd = checked(range.Offset + range.Length); + var overlapsInFlightWrite = false; + for (var otherIndex = 0; + otherIndex < allocation.DirtyRanges.Count; + otherIndex++) + { + if (otherIndex == index) + { + continue; + } + + var other = allocation.DirtyRanges[otherIndex]; + if (other.Timeline <= _completedTimeline) + { + continue; + } + + var otherEnd = checked(other.Offset + other.Length); + if (range.Offset < otherEnd && other.Offset < rangeEnd) + { + overlapsInFlightWrite = true; + break; + } + } + + if (overlapsInFlightWrite) + { + continue; + } + var mappedBytes = new ReadOnlySpan( (void*)(allocation.Mapped + checked((nint)range.Offset)), checked((int)range.Length)); @@ -9907,6 +9981,7 @@ internal static unsafe class VulkanVideoPresenter const int pageSize = 4096; const int unreadableMergeGap = 16; var livePageBuffer = GuestDataPool.Shared.Rent(pageSize); + var mappedPageBuffer = GuestDataPool.Shared.Rent(pageSize); var pageRuns = new List<(int Start, int Length)>(64); try { @@ -9915,35 +9990,49 @@ internal static unsafe class VulkanVideoPresenter pageStart += pageSize) { var pageEnd = Math.Min(pageStart + pageSize, mappedBytes.Length); - pageRuns.Clear(); - var cursor = pageStart; - while (cursor < pageEnd) + var pageLength = pageEnd - pageStart; + var mappedPageSource = mappedBytes.Slice(pageStart, pageLength); + var shadowPage = shadowBytes.Slice(pageStart, pageLength); + if (mappedPageSource.SequenceEqual(shadowPage)) { - while (cursor < pageEnd && - mappedBytes[cursor] == shadowBytes[cursor]) + continue; + } + + // HOST_COHERENT mappings are commonly uncached or + // write-combined on the CPU. Read each changed page + // once with a bulk copy, then perform the byte-level + // merge against ordinary cached memory. + var mappedPage = mappedPageBuffer.AsSpan(0, pageLength); + mappedPageSource.CopyTo(mappedPage); + pageRuns.Clear(); + var cursor = 0; + while (cursor < pageLength) + { + while (cursor < pageLength && + mappedPage[cursor] == shadowPage[cursor]) { cursor++; } - if (cursor == pageEnd) + if (cursor == pageLength) { break; } var runStart = cursor; - while (cursor < pageEnd && - mappedBytes[cursor] != shadowBytes[cursor]) + while (cursor < pageLength && + mappedPage[cursor] != shadowPage[cursor]) { cursor++; } var runLength = cursor - runStart; - pageRuns.Add((runStart, runLength)); + pageRuns.Add((pageStart + runStart, runLength)); changedRuns++; changedBytes += (ulong)runLength; if (firstChangedOffset < 0) { - firstChangedOffset = runStart; + firstChangedOffset = pageStart + runStart; } } @@ -9953,13 +10042,12 @@ internal static unsafe class VulkanVideoPresenter } changedPages++; - var pageLength = pageEnd - pageStart; var livePage = livePageBuffer.AsSpan(0, pageLength); if (memory.TryRead(guestAddress + (ulong)pageStart, livePage)) { foreach (var run in pageRuns) { - mappedBytes.Slice(run.Start, run.Length).CopyTo( + mappedPage.Slice(run.Start - pageStart, run.Length).CopyTo( livePage.Slice(run.Start - pageStart, run.Length)); } @@ -9967,7 +10055,7 @@ internal static unsafe class VulkanVideoPresenter { foreach (var run in pageRuns) { - mappedBytes.Slice(run.Start, run.Length).CopyTo( + mappedPage.Slice(run.Start - pageStart, run.Length).CopyTo( shadowBytes.Slice(run.Start, run.Length)); } @@ -9982,7 +10070,9 @@ internal static unsafe class VulkanVideoPresenter MarkGuestBufferDirty( allocation, range.Offset + (ulong)run.Start, - (ulong)run.Length); + (ulong)run.Length, + range.QueueName, + range.Timeline); } continue; @@ -10018,7 +10108,7 @@ internal static unsafe class VulkanVideoPresenter overlayIndex++) { var run = pageRuns[overlayIndex]; - mappedBytes.Slice(run.Start, run.Length).CopyTo( + mappedPage.Slice(run.Start - pageStart, run.Length).CopyTo( mergedLive.Slice( run.Start - mergedStart, run.Length)); @@ -10034,7 +10124,7 @@ internal static unsafe class VulkanVideoPresenter overlayIndex++) { var run = pageRuns[overlayIndex]; - mappedBytes.Slice(run.Start, run.Length).CopyTo( + mappedPage.Slice(run.Start - pageStart, run.Length).CopyTo( shadowBytes.Slice(run.Start, run.Length)); } @@ -10051,7 +10141,9 @@ internal static unsafe class VulkanVideoPresenter exactIndex++) { var run = pageRuns[exactIndex]; - var changed = mappedBytes.Slice(run.Start, run.Length); + var changed = mappedPage.Slice( + run.Start - pageStart, + run.Length); fallbackWrites++; if (memory.TryWrite( guestAddress + (ulong)run.Start, @@ -10068,7 +10160,9 @@ internal static unsafe class VulkanVideoPresenter MarkGuestBufferDirty( allocation, range.Offset + (ulong)run.Start, - (ulong)run.Length); + (ulong)run.Length, + range.QueueName, + range.Timeline); } } } @@ -10077,6 +10171,7 @@ internal static unsafe class VulkanVideoPresenter finally { GuestDataPool.Shared.Return(livePageBuffer); + GuestDataPool.Shared.Return(mappedPageBuffer); } var probe = mappedBytes[..Math.Min(mappedBytes.Length, 256)]; @@ -10346,6 +10441,7 @@ internal static unsafe class VulkanVideoPresenter } GuestDepthResource? depth = null; DepthFramebufferResource? depthFramebuffer = null; + var clearDepthSeparately = false; if (ShouldAttachGuestDepth( work.DepthTarget, draw.RenderState.Depth) && @@ -10373,12 +10469,26 @@ internal static unsafe class VulkanVideoPresenter depth.GuestClearDepth = effectiveDepthTarget.ClearDepth; depth.ClearDepth = effectiveDepthTarget.ClearDepth; } - if (targets.Length == 1) + clearDepthSeparately = clearDepthForDraw && + (depth.Width < firstTarget.Width || + depth.Height < firstTarget.Height); + if (targets.Length == 1 && !clearDepthSeparately) { depthFramebuffer = GetOrCreateDepthFramebuffer(firstTarget, depth); } } + if (depth is not null && !clearDepthSeparately) + { + // Guest color images may be allocated at their maximum + // resolution while the active viewport and DB surface use + // a smaller dynamic-rendering extent. Vulkan requires the + // framebuffer extent to fit every attachment. + extent = new Extent2D( + Math.Min(firstTarget.Width, depth.Width), + Math.Min(firstTarget.Height, depth.Height)); + } + if (clearDepthForDraw) { // DB_RENDER_CONTROL.DEPTH_CLEAR_ENABLE makes this a DB @@ -10412,17 +10522,18 @@ internal static unsafe class VulkanVideoPresenter var framebuffer = depthFramebuffer?.Framebuffer ?? firstTarget.Framebuffer; if (targets.Length > 1) { + var attachedDepth = clearDepthSeparately ? null : depth; (renderPass, framebuffer) = CreateRenderPassAndFramebuffer( formats, targets.Select(target => target.MipViews.Length > 0 ? target.MipViews[0] : target.View).ToArray(), - firstTarget.Width, - firstTarget.Height, + extent.Width, + extent.Height, targets.Select(target => target.Initialized || target.InitialUploadPending).ToArray(), - depth, - depth?.Initialized == true && !clearDepthForDraw); + attachedDepth, + attachedDepth?.Initialized == true && !clearDepthForDraw); transientRenderPass = renderPass; transientFramebuffer = framebuffer; } @@ -10433,8 +10544,8 @@ internal static unsafe class VulkanVideoPresenter formats, extent, targets, - hasDepthAttachment: depth is not null, - feedbackDepth: depth); + hasDepthAttachment: depth is not null && !clearDepthSeparately, + feedbackDepth: clearDepthSeparately ? null : depth); resources.TransientRenderPass = transientRenderPass; resources.TransientFramebuffer = transientFramebuffer; transientRenderPass = default; @@ -10454,6 +10565,10 @@ internal static unsafe class VulkanVideoPresenter submitted = true; BeginDebugLabel(_commandBuffer, resources.DebugName); + if (clearDepthSeparately && depth is not null) + { + RecordStandaloneGuestDepthClear(depth); + } var hasStorageImages = false; foreach (var texture in resources.Textures) { @@ -10517,6 +10632,7 @@ internal static unsafe class VulkanVideoPresenter toColorAttachments); if (depth is not null && + !clearDepthSeparately && depth.Layout == ImageLayout.ShaderReadOnlyOptimal) { var toDepthAttachment = new ImageMemoryBarrier @@ -10554,7 +10670,7 @@ internal static unsafe class VulkanVideoPresenter framebuffer, extent, colorAttachmentCount: targets.Length, - hasDepthAttachment: depth is not null, + hasDepthAttachment: depth is not null && !clearDepthSeparately, clearDepth: depth?.ClearDepth ?? 1f); RecordTranslatedDrawInPass(resources, extent); _vk.CmdEndRenderPass(_commandBuffer); @@ -10610,7 +10726,10 @@ internal static unsafe class VulkanVideoPresenter if (depth is not null) { depth.Initialized = true; - depth.Layout = ImageLayout.DepthStencilAttachmentOptimal; + if (!clearDepthSeparately) + { + depth.Layout = ImageLayout.DepthStencilAttachmentOptimal; + } if (clearDepthForDraw) { depth.InitializationSource = "guest-depth-clear"; @@ -11624,14 +11743,6 @@ internal static unsafe class VulkanVideoPresenter return existing; } - if (depth.Width < color.Width || depth.Height < color.Height) - { - throw new InvalidOperationException( - $"guest depth 0x{depth.Address:X16} extent {depth.Width}x{depth.Height} " + - $"is smaller than color target 0x{color.Address:X16} " + - $"{color.Width}x{color.Height}"); - } - var attachmentView = color.MipViews.Length > 0 ? color.MipViews[0] : color.View; var loadRenderPass = CreateDepthRenderPass( color.Format, @@ -11658,8 +11769,8 @@ internal static unsafe class VulkanVideoPresenter RenderPass = loadRenderPass, AttachmentCount = 2, PAttachments = attachments, - Width = color.Width, - Height = color.Height, + Width = Math.Min(color.Width, depth.Width), + Height = Math.Min(color.Height, depth.Height), Layers = 1, }; Check( @@ -12155,6 +12266,7 @@ internal static unsafe class VulkanVideoPresenter EvictDirtyCachedTextures(); var completedWork = 0; + HashSet? deferredOrderedQueues = null; var renderWorkDeadline = _renderWorkBudgetTicks > 0 ? System.Diagnostics.Stopwatch.GetTimestamp() + _renderWorkBudgetTicks : long.MaxValue; @@ -12172,7 +12284,7 @@ internal static unsafe class VulkanVideoPresenter break; } - if (!TryTakeGuestWork(out var pendingGuestWork)) + if (!TryTakeGuestWork(out var pendingGuestWork, deferredOrderedQueues)) { break; } @@ -12199,6 +12311,7 @@ internal static unsafe class VulkanVideoPresenter _enqueueAsImmediateQueueFollowup = true; _immediateFollowupTail = null; var work = pendingGuestWork.Work; + var deferGuestWork = false; var traceWork = ShouldTracePresentedGuestImageContentsForDiagnostics(); var workStart = traceWork ? System.Diagnostics.Stopwatch.GetTimestamp() : 0L; @@ -12226,7 +12339,7 @@ internal static unsafe class VulkanVideoPresenter ExecuteGuestImageWrite(guestImageWrite); break; case VulkanOrderedGuestAction orderedAction: - ExecuteOrderedGuestAction(orderedAction); + deferGuestWork = !TryExecuteOrderedGuestAction(orderedAction); break; case VulkanOrderedGuestFlip orderedFlip: ExecuteOrderedGuestFlip(orderedFlip); @@ -12238,12 +12351,21 @@ internal static unsafe class VulkanVideoPresenter } finally { - CompleteGuestWork(pendingGuestWork); + if (!deferGuestWork || !RequeueGuestWorkFront(pendingGuestWork)) + { + CompleteGuestWork(pendingGuestWork); + } _enqueueAsImmediateQueueFollowup = false; _immediateFollowupTail = null; Volatile.Write(ref _executingGuestWorkSequence, 0); } + if (deferGuestWork) + { + deferredOrderedQueues ??= new HashSet(StringComparer.Ordinal); + deferredOrderedQueues.Add(pendingGuestWork.Queue.Name); + } + if (workStart != 0) { var elapsedMs = (System.Diagnostics.Stopwatch.GetTimestamp() - workStart) @@ -13200,6 +13322,79 @@ internal static unsafe class VulkanVideoPresenter depth.Layout = ImageLayout.ShaderReadOnlyOptimal; } + private void RecordStandaloneGuestDepthClear(GuestDepthResource depth) + { + var depthRange = new ImageSubresourceRange( + ImageAspectFlags.DepthBit, + 0, + 1, + 0, + 1); + var sourceStage = PipelineStageFlags.TopOfPipeBit; + var sourceAccess = AccessFlags.None; + switch (depth.Layout) + { + case ImageLayout.ShaderReadOnlyOptimal: + sourceStage = + PipelineStageFlags.VertexShaderBit | + PipelineStageFlags.FragmentShaderBit | + PipelineStageFlags.ComputeShaderBit; + sourceAccess = AccessFlags.ShaderReadBit; + break; + case ImageLayout.DepthStencilAttachmentOptimal: + sourceStage = + PipelineStageFlags.EarlyFragmentTestsBit | + PipelineStageFlags.LateFragmentTestsBit; + sourceAccess = + AccessFlags.DepthStencilAttachmentReadBit | + AccessFlags.DepthStencilAttachmentWriteBit; + break; + case ImageLayout.TransferDstOptimal: + sourceStage = PipelineStageFlags.TransferBit; + sourceAccess = AccessFlags.TransferWriteBit; + break; + } + + if (depth.Layout != ImageLayout.TransferDstOptimal) + { + var toTransfer = new ImageMemoryBarrier + { + SType = StructureType.ImageMemoryBarrier, + SrcAccessMask = sourceAccess, + DstAccessMask = AccessFlags.TransferWriteBit, + OldLayout = depth.Layout, + NewLayout = ImageLayout.TransferDstOptimal, + SrcQueueFamilyIndex = Vk.QueueFamilyIgnored, + DstQueueFamilyIndex = Vk.QueueFamilyIgnored, + Image = depth.Image, + SubresourceRange = depthRange, + }; + _vk.CmdPipelineBarrier( + _commandBuffer, + sourceStage, + PipelineStageFlags.TransferBit, + 0, + 0, + null, + 0, + null, + 1, + &toTransfer); + } + + var clearValue = new ClearDepthStencilValue(depth.ClearDepth, 0); + _vk.CmdClearDepthStencilImage( + _commandBuffer, + depth.Image, + ImageLayout.TransferDstOptimal, + &clearValue, + 1, + &depthRange); + depth.Initialized = true; + depth.Layout = ImageLayout.TransferDstOptimal; + depth.InitializationSource = "guest-depth-clear"; + } + private void RecordRenderTargetFeedbackSnapshots( TranslatedDrawResources resources, PipelineStageFlags shaderStage) @@ -14965,6 +15160,7 @@ internal static unsafe class VulkanVideoPresenter DestroyGuestBufferAllocation(allocation); } _guestBufferAllocations.Clear(); + PerfOverlay.SetGuestBufferCacheBytes(0); _hostBufferPool.Dispose(); foreach (var guestImage in _guestImages.Values) { diff --git a/src/SharpEmu.ShaderCompiler/Gen5ShaderScalarEvaluator.cs b/src/SharpEmu.ShaderCompiler/Gen5ShaderScalarEvaluator.cs index 0dc2d257..7a8d21f4 100644 --- a/src/SharpEmu.ShaderCompiler/Gen5ShaderScalarEvaluator.cs +++ b/src/SharpEmu.ShaderCompiler/Gen5ShaderScalarEvaluator.cs @@ -2350,7 +2350,8 @@ public static class Gen5ShaderScalarEvaluator private static bool TryReadUInt32(CpuContext ctx, ulong address, out uint value) { Span bytes = stackalloc byte[sizeof(uint)]; - if (!ctx.Memory.TryRead(address, bytes)) + if (!ctx.Memory.TryRead(address, bytes) && + FallbackMemoryReader?.Invoke(address, bytes) != true) { value = 0; return false; diff --git a/tests/SharpEmu.Libs.Tests/Agc/Gen5ScalarMemoryFallbackTests.cs b/tests/SharpEmu.Libs.Tests/Agc/Gen5ScalarMemoryFallbackTests.cs new file mode 100644 index 00000000..63a43eb1 --- /dev/null +++ b/tests/SharpEmu.Libs.Tests/Agc/Gen5ScalarMemoryFallbackTests.cs @@ -0,0 +1,99 @@ +// 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 Xunit; + +namespace SharpEmu.Libs.Tests.Agc; + +public sealed class Gen5ScalarMemoryFallbackTests +{ + private const ulong ScalarTableAddress = 0x4_4665_4FD0; + private static readonly object FallbackReaderGate = new(); + + [Fact] + public void ScalarLoadReadsTrackedFallbackMemory() + { + var expected = new uint[] + { + 0x4665_4F70, + 0x0000_0004, + 0x4EA7_FCE0, + 0x0000_0004, + }; + var table = new byte[expected.Length * sizeof(uint)]; + for (var index = 0; index < expected.Length; index++) + { + BinaryPrimitives.WriteUInt32LittleEndian( + table.AsSpan(index * sizeof(uint), sizeof(uint)), + expected[index]); + } + + var load = new Gen5ShaderInstruction( + 0, + Gen5ShaderEncoding.Smem, + "SLoadDwordx4", + [], + [Gen5Operand.Scalar(0)], + [ + Gen5Operand.Scalar(16), + Gen5Operand.Scalar(17), + Gen5Operand.Scalar(18), + Gen5Operand.Scalar(19), + ], + new Gen5ScalarMemoryControl(4, 0, null)); + var end = new Gen5ShaderInstruction( + 8, + Gen5ShaderEncoding.Sopp, + "SEndpgm", + [], + [], + [], + null); + var state = new Gen5ShaderState( + new Gen5ShaderProgram(0, [load, end]), + [unchecked((uint)ScalarTableAddress), (uint)(ScalarTableAddress >> 32)], + null); + var ctx = new CpuContext(new FakeCpuMemory(0x1000, 0x100), Generation.Gen5); + + lock (FallbackReaderGate) + { + var previousReader = Gen5ShaderScalarEvaluator.FallbackMemoryReader; + try + { + Gen5ShaderScalarEvaluator.FallbackMemoryReader = ReadFallback; + Assert.True( + Gen5ShaderScalarEvaluator.TryEvaluate( + ctx, + state, + out var evaluation, + out var error), + error); + Assert.Equal(expected, evaluation.ScalarRegisters.Skip(16).Take(4)); + } + finally + { + Gen5ShaderScalarEvaluator.FallbackMemoryReader = previousReader; + } + } + + bool ReadFallback(ulong address, Span destination) + { + if (address < ScalarTableAddress) + { + return false; + } + + var offset = address - ScalarTableAddress; + if (offset + (ulong)destination.Length > (ulong)table.Length) + { + return false; + } + + table.AsSpan((int)offset, destination.Length).CopyTo(destination); + return true; + } + } +}