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
This commit is contained in:
Berk
2026-07-19 00:31:50 +03:00
committed by GitHub
parent 336286e588
commit a030cb5a5d
15 changed files with 768 additions and 275 deletions
+3 -3
View File
@@ -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
-5
View File
@@ -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);
+3 -3
View File
@@ -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",
@@ -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
@@ -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,
+40 -59
View File
@@ -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}";
}
}
}
@@ -27,7 +27,7 @@ internal static class AgcShaderCompilerHooks
internal static void Install()
{
Gen5ShaderScalarEvaluator.FallbackMemoryReader =
KernelMemoryCompatExports.TryReadTrackedLibcHeap;
KernelMemoryCompatExports.TryReadShaderGuestMemory;
Gen5ShaderScalarEvaluator.GlobalMemoryPool =
GuestDataPool.Shared;
}
+10 -17
View File
@@ -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)
{
+14 -9
View File
@@ -29,10 +29,9 @@ internal static class Bink2MovieBridge
private static bool _availabilityReported;
/// <summary>
/// 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.
/// </summary>
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,
+121 -2
View File
@@ -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;
/// </summary>
internal static class GuestDataPool
{
public static ArrayPool<byte> Shared { get; } = ArrayPool<byte>.Create(
public static ArrayPool<byte> 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<byte>
{
private readonly object _gate = new();
private readonly int _maxArrayLength;
private readonly ulong _maxCachedBytes;
private readonly int _maxArraysPerBucket;
private readonly Dictionary<int, Stack<byte[]>> _cachedByBucket = [];
private readonly HashSet<byte[]> _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<byte[]>();
_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;
}
}
@@ -6010,7 +6010,7 @@ public static partial class KernelMemoryCompatExports
return highWaterMark;
}
private static bool TryReadHostMemory(ulong address, Span<byte> destination)
private static unsafe bool TryReadHostMemory(ulong address, Span<byte> 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<byte>((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<byte> 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<byte> destination)
@@ -6340,7 +6373,7 @@ public static partial class KernelMemoryCompatExports
return value != 0 && (value & (value - 1)) == 0;
}
private static bool TryWriteHostMemory(ulong address, ReadOnlySpan<byte> source)
private static unsafe bool TryWriteHostMemory(ulong address, ReadOnlySpan<byte> 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<byte>((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;
+10 -7
View File
@@ -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
/// <summary>Called per translated draw/dispatch executed.</summary>
public static void RecordDraw() => Interlocked.Increment(ref _drawsInWindow);
public static void SetGuestBufferCacheBytes(ulong bytes) =>
Interlocked.Exchange(ref _guestBufferCacheBytes, checked((long)bytes));
/// <summary>
/// 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}";
}
}
+346 -150
View File
@@ -2412,7 +2412,9 @@ internal static unsafe class VulkanVideoPresenter
}
}
private static bool TryTakeGuestWork(out PendingGuestWork work)
private static bool TryTakeGuestWork(
out PendingGuestWork work,
HashSet<string>? 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<PendingGuestWork>();
_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<DirtyGuestBufferRange> 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<byte>(
(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<string>? 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<string>(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)
{
@@ -2350,7 +2350,8 @@ public static class Gen5ShaderScalarEvaluator
private static bool TryReadUInt32(CpuContext ctx, ulong address, out uint value)
{
Span<byte> 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;
@@ -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<byte> 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;
}
}
}