Compare commits

...

3 Commits

Author SHA1 Message Date
Mathias a2241d0e83 Fix/agc zero dim and storage (#828)
* fix(videoout): promote guest images to storage usage

* fix(agc): treat zero-dimension indirect compute dispatches as valid no-ops
2026-08-18 21:20:54 +02:00
Foued Attar fe6521f617 Fix unaligned BufferLoad/GlobalLoad dword access
BufferLoadDword/x2/x3/x4, BufferStoreDword/x2/x3/x4, and their GLOBAL
counterparts were routed through LoadUnalignedBufferWord /
StoreBufferBytes, which reconstruct every dword one byte at a time
(4 bounds-checked buffer accesses per dword, each with its own
OpArrayLength + OpSelect + OpAccessChain + OpLoad/OpStore, plus
shift/mask/or reassembly on top).

The GCN ISA guarantees these opcodes are always dword-aligned - only
the byte/short/D16 variants legitimately need unaligned access, and
those already have their own dedicated path
(LoadSubdwordBufferValue / StoreBufferBytes with an explicit byte
count). The generic dword-count loop reached by every other
BufferLoad*/BufferStore*/GlobalLoad*/GlobalStore* opcode was paying
the same per-byte cost for no reason.

Route the dword-granularity path straight through the existing
LoadBufferWord / StoreBufferWord helpers (one bounds check and one
load/store per dword) instead. Measured on a compute shader with 6
BufferLoadDwordx4 instructions in its hottest basic block, this drops
GPU dispatch time for that shader from ~430-448ms to ~86-92ms (~5x)
with no change in output correctness - it is a pure translation
inefficiency fix, independent of any specific title.
2026-08-18 12:20:24 +02:00
Mathias 034ddcc092 fix(vmem): use ConcurrentDictionary for _pageProtections to prevent race corruption (#823) 2026-08-18 12:13:32 +02:00
4 changed files with 77 additions and 84 deletions
@@ -1,6 +1,7 @@
// Copyright (C) 2026 SharpEmu Emulator Project // Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later // SPDX-License-Identifier: GPL-2.0-or-later
using System.Collections.Concurrent;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using SharpEmu.Core.Loader; using SharpEmu.Core.Loader;
using SharpEmu.HLE; using SharpEmu.HLE;
@@ -18,7 +19,7 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
private readonly object _allocationSearchHintGate = new(); private readonly object _allocationSearchHintGate = new();
private readonly List<MemoryRegion> _regions = new(); private readonly List<MemoryRegion> _regions = new();
private readonly Dictionary<(ulong DesiredAddress, ulong Alignment, bool Executable), ulong> _allocationSearchHints = new(); private readonly Dictionary<(ulong DesiredAddress, ulong Alignment, bool Executable), ulong> _allocationSearchHints = new();
private readonly Dictionary<ulong, ProgramHeaderFlags> _pageProtections = new(); private readonly ConcurrentDictionary<ulong, ProgramHeaderFlags> _pageProtections = new();
private bool _disposed; private bool _disposed;
[ThreadStatic] [ThreadStatic]
+11 -8
View File
@@ -12174,15 +12174,18 @@ public static partial class AgcExports
if (dispatchEndX == 0 || dispatchEndY == 0 || dispatchEndZ == 0) if (dispatchEndX == 0 || dispatchEndY == 0 || dispatchEndZ == 0)
{ {
// Indirect dispatches read their dimensions from a guest buffer a // For indirect dispatches (both absolute and base), zero dimensions are a valid outcome
// prior GPU dispatch fills. Zero here means that producer has not run // of GPU culling passes (0 workgroups). VulkanVideoPresenter handles groupCount = 0 as a clean no-op.
// yet — signal the caller to suspend on the dims buffer and retry, if (opcode == ItDispatchIndirect || dispatchSource is "absolute-indirect" or "base-indirect")
// rather than dropping the work (which black-screens GPU-driven games
// like Astro Bot). Direct dispatches carry dims inline, so a zero is
// genuinely malformed and still rejected.
if (opcode == ItDispatchIndirect)
{ {
indirectDimsRetryAddress = dimensionsAddress; var waveCount = (initiator & (1u << 15)) != 0 ? 32u : 64u;
dispatch = new ComputeDispatch(
0, 0, 0,
0, 0, 0,
waveCount,
IsIndirect: true,
0, 0, 0);
return true;
} }
return RejectComputeDispatch( return RejectComputeDispatch(
@@ -13828,16 +13828,10 @@ internal static unsafe class VulkanVideoPresenter
existing.LogicalDepth == depth && existing.LogicalDepth == depth &&
existing.Type == type && existing.Type == type &&
existing.MipLevels == mipLevels && existing.MipLevels == mipLevels &&
(!requiresStorage || existing.SupportsStorageUsage) &&
(exactFormatMatch || (exactFormatMatch ||
(IsAliasableGuestImageFormat(existing.Format, format) && IsAliasableGuestImageFormat(existing.Format, format)))
(!requiresStorage || existing.SupportsStorageUsage))))
{ {
if (requiresStorage && !existing.SupportsStorageUsage)
{
throw new InvalidOperationException(
$"Guest image 0x{target.Address:X16} was created without storage usage.");
}
existing.IsCpuBacked = false; existing.IsCpuBacked = false;
existing.CpuContentFingerprint = 0; existing.CpuContentFingerprint = 0;
if (existing.RenderPass.Handle == 0 && if (existing.RenderPass.Handle == 0 &&
@@ -13870,14 +13864,9 @@ internal static unsafe class VulkanVideoPresenter
if (existing.Width == target.Width && if (existing.Width == target.Width &&
existing.Height == target.Height && existing.Height == target.Height &&
existing.MipLevels == mipLevels && existing.MipLevels == mipLevels &&
(!requiresStorage || existing.SupportsStorageUsage) &&
IsCompatibleViewFormat(existing.Format, format)) IsCompatibleViewFormat(existing.Format, format))
{ {
if (requiresStorage && !existing.SupportsStorageUsage)
{
throw new InvalidOperationException(
$"Guest image 0x{target.Address:X16} was created without storage usage.");
}
if (_traceGuestImageEvents) if (_traceGuestImageEvents)
{ {
Console.Error.WriteLine( Console.Error.WriteLine(
@@ -13952,10 +13941,11 @@ internal static unsafe class VulkanVideoPresenter
{ {
if (requiresStorage && !retained.SupportsStorageUsage) if (requiresStorage && !retained.SupportsStorageUsage)
{ {
throw new InvalidOperationException( // Do not reuse retained image if it lacks required storage usage
$"Retained guest image 0x{target.Address:X16} was created without storage usage."); DestroyGuestImage(retained);
} }
else
{
retained.IsCpuBacked = false; retained.IsCpuBacked = false;
retained.CpuContentFingerprint = 0; retained.CpuContentFingerprint = 0;
_guestImages.Add(target.Address, retained); _guestImages.Add(target.Address, retained);
@@ -13997,6 +13987,7 @@ internal static unsafe class VulkanVideoPresenter
return retained; return retained;
} }
}
var imageInfo = new ImageCreateInfo var imageInfo = new ImageCreateInfo
{ {
@@ -2366,17 +2366,16 @@ public static partial class Gen5SpirvTranslator
return; return;
} }
// GLOBAL_STORE/LOAD_DWORD(x2/x3/x4) are dword-aligned by the GCN ISA, so read/write dwords directly instead of the per-byte loop.
for (uint index = 0; index < control.DwordCount; index++) for (uint index = 0; index < control.DwordCount; index++)
{ {
var address = index == 0 var indexedDwordAddress = index == 0
? byteAddress ? dwordAddress
: IAdd(byteAddress, UInt(index * sizeof(uint))); : IAdd(dwordAddress, UInt(index));
StoreBufferBytes( StoreBufferWord(
bindingIndex, bindingIndex,
address, indexedDwordAddress,
LoadV(control.VectorData + index), LoadV(control.VectorData + index));
sizeof(uint),
0);
} }
}); });
return true; return true;
@@ -2404,12 +2403,12 @@ public static partial class Gen5SpirvTranslator
for (uint index = 0; index < control.DwordCount; index++) for (uint index = 0; index < control.DwordCount; index++)
{ {
var address = index == 0 var indexedDwordAddress = index == 0
? byteAddress ? dwordAddress
: IAdd(byteAddress, UInt(index * sizeof(uint))); : IAdd(dwordAddress, UInt(index));
StoreV( StoreV(
control.VectorData + index, control.VectorData + index,
LoadUnalignedBufferWord(bindingIndex, address)); LoadBufferWord(bindingIndex, indexedDwordAddress));
} }
return true; return true;
@@ -2510,17 +2509,16 @@ public static partial class Gen5SpirvTranslator
return; return;
} }
// BUFFER_STORE/LOAD_DWORD(x2/x3/x4) are dword-aligned by the GCN ISA, same as the GLOBAL case above — no per-byte reassembly needed.
for (uint index = 0; index < control.DwordCount; index++) for (uint index = 0; index < control.DwordCount; index++)
{ {
var address = index == 0 var indexedDwordAddress = index == 0
? byteAddress ? dwordAddress
: IAdd(byteAddress, UInt(index * sizeof(uint))); : IAdd(dwordAddress, UInt(index));
StoreBufferBytes( StoreBufferWord(
bindingIndex, bindingIndex,
address, indexedDwordAddress,
LoadV(control.VectorData + index), LoadV(control.VectorData + index));
sizeof(uint),
0);
} }
}); });
@@ -2576,12 +2574,12 @@ public static partial class Gen5SpirvTranslator
for (uint index = 0; index < control.DwordCount; index++) for (uint index = 0; index < control.DwordCount; index++)
{ {
var address = index == 0 var indexedDwordAddress = index == 0
? byteAddress ? dwordAddress
: IAdd(byteAddress, UInt(index * sizeof(uint))); : IAdd(dwordAddress, UInt(index));
StoreV( StoreV(
control.VectorData + index, control.VectorData + index,
LoadUnalignedBufferWord(bindingIndex, address)); LoadBufferWord(bindingIndex, indexedDwordAddress));
} }
return true; return true;