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