mirror of
https://github.com/par274/sharpemu.git
synced 2026-07-22 10:56:20 +08:00
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:
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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}";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user