Astrobot / Demon's soul performance improvement (#955)
* [Gpu] Astro Bot performance: 14 -> ~60 fps at the title scene - Defer RELEASE_MEM command-buffer flushes instead of submitting per packet - Resolve indexed indirect draws and dispatches on the GPU - Async buffer readbacks on a dedicated transfer/compute queue - Pool image backings, flip snapshots and tiler resources - One-frame command-stream run-ahead - Lock-free backed-range and image page-owner checks on CPU writes - Scalar value cache with generation-stamped reset; memoized invariant phis - Shader compiler: structured forward-only CFGs with function-scope registers, EXEC-full guard elision, bounded V_MOVREL offsets - Trivial-leaf import dispatch and strchr/strrchr fast paths - SHARPEMU_GUEST_REFRESH_RATE option * [Tests] Fix full-suite flaky tests - Red-zone patcher: when every fixed trampoline candidate is taken, look for any free host range within relative-jump reach (PhysicalVirtualMemory.EnumerateFreeHostRanges); the test places its image in a probed free hole - Kernel memory tests: probe a free PRT-area address instead of the fixed 0x1026C00000; the reservation-extent test only rejects picks inside the reservation - Release checks tolerate another thread reusing the freed range - Allocation tests measure on a dedicated thread and keep the least allocating of several rounds (AllocationMeasurement.SteadyState) * [Gpu] Astro Bot: structured loops, buffer slabs, pad motion - Shader: emit natural loops as structured SPIR-V loops (OpLoopMerge) instead of the block dispatcher, and merge SCC-uniform if/else joins as selects so descriptors picked on a branch stay evaluable per draw. - Shader: decode V_FFBH_I32 and V_MIN3_F16; read the DS GDS bit from bit 17 (GFX10) and route GDS append/consume through shared atomics. - Gpu: sub-allocate small device-local buffers from shared memory slabs. - Pad: report motion data by default (Astro Bot never imports scePadSetMotionSensorState), convert acceleration from m/s^2 to G, and add scePadResetOrientation / scePadSetAngularVelocityDeadbandState. * [CPU] Stop pinning guest threads to host cores by default Guest affinity packed Demon's Souls' 13 spinning BPE job workers onto 10 host lanes and put its renderer next to them at equal priority, so the renderer and lock holders waited out whole scheduler quanta. Intro 10.6 -> 44.7 fps, menus 11.6 -> 47.5 fps. SHARPEMU_GUEST_AFFINITY=1 restores pinning. * [Media] Convert host movie frames to YUV on the decoder thread The render thread converted every decoded Bink frame from BGRA back to YUV 4:2:0 (6.7 ms per frame). The decoder thread now produces the planes and the presenter uploads them as-is. PS Studios logo 33 -> ~50 fps, attract movie 44 -> 60 fps. * [Agc] Accept a zero destination in sceAgcDcbWriteData Demon's Souls emits WRITE_DATA with address 0 into a template and patches it after copying. Returning null made the title patch a relocated null pointer, which wrote a stack-derived value into its label and was then parsed as an unknown NOP marker (GPU FATAL). * [Loader] Patch short red-zone faulting accesses next to branches A faulting access shorter than a jump could not be relocated when followed by a branch or a branch target, so a handled fault let Windows overwrite the red zone. Spans may now end with a direct branch (run after the stack pointer is restored) or start up to three instructions earlier. Fixes a Demon's Souls crash after the new-game intro movie. * [GPU] Astro Bot rendering fixes: compute subgroups, DCC clears, PS inputs, texture MIN_LOD - Compute: pin non-emulated compute pipelines to a 32-lane subgroup (VK_EXT_subgroup_size_control) so wave64 groups other than 64 invocations keep lanes 32..63 active on AMD (striped froxel volume). - Shader translator: 64-bit AND for the wave32 S_*_SAVEEXEC_B64 mask. - PS inputs: slots that read one VS parameter share one host input; a smooth/flat slot next to a V_INTERP_MOV slot is rebuilt from the per-vertex values (black save cards, NaN normals, black props). - Render targets: drop unwritten color targets of depth passes from both the attachments and the pixel outputs, keeping locations aligned. - DCC: materialize pending zero fast clears before shader access so a later attachment bind does not wipe compute writes. - Textures: honour the T# MIN_LOD with VK_EXT_image_view_min_lod so streamed textures never sample unloaded mips (blown-out stadium sky). - Device: enable VK_KHR_maintenance5 for point lists without PointSize. * [Audio][GPU] ATRAC9 independent mono streams, BC storage fallback, 32-bit bit counts - LibAtrac9: decode config header 0x30 streams (PS5 ambisonics), where every channel is an independent mono stream interleaved frame by frame. - Images: when the driver refuses storage usage on a block-compressed image, compute writes go through an uncompressed replacement of the same memory. - SPIR-V: count 64-bit masks with two 32-bit OpBitCount, which Vulkan requires without maintenance9. - Agc: drop the indirect-draw emit debug log; report the draw modifier in the unsupported indirect draw error. * [GPU] Partial texture-pool uploads and cheaper per-draw preparation - A CPU write into a large layered texture (Demon's Souls' 256-layer BC3 pools) re-uploaded the whole image, ~750 ms of GPU detiling in one frame. Each (mip, layer) piece's guest bytes are hashed at upload; a refresh uploads only the pieces that changed. - Device-address preparation touched and checked every registered buffer for each dispatch. Recency touches now run once per retirement tick, and uploads only visit buffers in blocks the tracker does not know to be clean; hot pages stay dirty, so they are still re-uploaded every time. - Resource materialization cache: when the only changed words are ones the flattened table alone reads (per-frame inline constants), only the table is evaluated again. - A full batch no longer splits an open render pass before 512 draws. - Lock-free lookups for shader headers, device format properties and the idle host movie bridge; the SPIR-V dump toggle is read once per program cache. * Demon soul - fix texture * Delete runahead tests * Screcrow fix for nvidia GPU
This commit is contained in:
parent
d0dd3fe102
commit
c076513bd6
|
|
@ -182,7 +182,8 @@ These variables apply to guest memory and guest code execution.
|
|||
| `SHARPEMU_IGNORE_STACK_CHK` | `1` | Recovers from a guest stack-protector failure call (NID `Ou3iL1abvng`) when a UD2 instruction follows the call. The emulator sets the return address to the function epilogue 20 bytes before. It does this only when the code bytes match the known pattern. | `DirectExecutionBackend.Imports.cs` |
|
||||
| `SHARPEMU_LAZY_RESERVE_PRIME_MB` | number | Sets the number of megabytes that the emulator commits at the start of each reserve-only memory region. The default is 64 and the maximum is 4096. The value 0 turns the initial commit off. | `PhysicalVirtualMemory.cs` |
|
||||
| `SHARPEMU_NATIVE_WORKER_MAX_CONCURRENT` | number | Sets the maximum number of native guest worker runs that execute at the same time. The default is 2. The emulator limits the value to the range 1 to 64. | `DirectExecutionBackend.NativeWorker.cs`, `DirectExecutionBackend.cs` |
|
||||
| `SHARPEMU_RESERVED_HOST_LANES` | number | Set the number of host logical processors that guest thread affinity does not use. The emulator keeps these processors for its own threads. The default is three eighths of the processor count, with a minimum of 2. | `DirectExecutionBackend.cs` |
|
||||
| `SHARPEMU_GUEST_AFFINITY` | `1` | Applies the guest thread affinity to the host threads. The default leaves host threads unpinned, because pinning let spinning guest workers starve the render thread. | `DirectExecutionBackend.cs` |
|
||||
| `SHARPEMU_RESERVED_HOST_LANES` | number | Set the number of host logical processors that guest thread affinity does not use. The emulator keeps these processors for its own threads. The default is three eighths of the processor count, with a minimum of 2. It has an effect only with `SHARPEMU_GUEST_AFFINITY=1`. | `DirectExecutionBackend.cs` |
|
||||
| `SHARPEMU_TSC_FREQ_HZ` | number (Hz, minimum 1000000) | Sets the TSC frequency that the kernel library reports to the guest. When unset, the emulator uses the calibrated RDTSC frequency, then the CPUID frequency, then the host stopwatch frequency. | `KernelRuntimeCompatExports.cs` |
|
||||
| `SHARPEMU_WATCH_BULK_DEST_HI` | hex number | Limits the `SHARPEMU_WATCH_BULK_TORN` scan to writes whose upper 32 address bits equal this value. When unset, the scan uses the direct-memory address band. | `GuestWriteWatch.cs` |
|
||||
| `SHARPEMU_WATCH_BULK_TORN` | `1` | Examines each aligned 8-byte value in managed bulk writes to guest memory. It reports torn values and shifted pointer values with a stack trace. It writes a maximum of 64 reports for each kind. | `GuestWriteWatch.cs` |
|
||||
|
|
|
|||
|
|
@ -114,7 +114,7 @@ public sealed partial class DirectExecutionBackend
|
|||
|
||||
ulong rip = ReadCtxU64(contextRecord, 248);
|
||||
ulong rsp = ReadCtxU64(contextRecord, 152);
|
||||
if (TryRecoverGuestInt41(exceptionCode, contextRecord, rip))
|
||||
if (MayBeGuestInt41(exceptionRecord) && TryRecoverGuestInt41(exceptionCode, contextRecord, rip))
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
|
@ -619,6 +619,16 @@ public sealed partial class DirectExecutionBackend
|
|||
return (ulong)(_workerAbortStack + (nint)WorkerAbortStackSize - 0x100) & ~0xFUL;
|
||||
}
|
||||
|
||||
// On Windows an INT n the guest may not execute raises a general-protection fault,
|
||||
// reported as an access violation whose fault address is all ones; a page fault
|
||||
// carries the data address instead. GPU-tracked and lazily committed pages fault
|
||||
// thousands of times per frame, so skip the opcode probe (a VirtualQuery and a
|
||||
// read of the faulting instruction) for them.
|
||||
private static unsafe bool MayBeGuestInt41(EXCEPTION_RECORD* exceptionRecord) =>
|
||||
!OperatingSystem.IsWindows() ||
|
||||
exceptionRecord->NumberParameters < 2 ||
|
||||
exceptionRecord->ExceptionInformation[1] == ulong.MaxValue;
|
||||
|
||||
private unsafe bool TryRecoverGuestInt41(uint exceptionCode, void* contextRecord, ulong rip)
|
||||
{
|
||||
if (!_ignoreGuestInt41 || exceptionCode != 3221225477u || rip < 0x10000)
|
||||
|
|
|
|||
|
|
@ -185,6 +185,11 @@ public sealed partial class DirectExecutionBackend
|
|||
return hotMemoryResult;
|
||||
}
|
||||
|
||||
if (importStubEntry.IsTrivialLeaf && !_disableTrivialLeafDispatch)
|
||||
{
|
||||
return DispatchTrivialLeaf(cpuContext, in importStubEntry, argPackPtr, num);
|
||||
}
|
||||
|
||||
if (importStubEntry.IsLeaf &&
|
||||
TryDispatchLeafImport(cpuContext, importStubEntry, argPackPtr, num, out var leafResult))
|
||||
{
|
||||
|
|
@ -1494,6 +1499,68 @@ public sealed partial class DirectExecutionBackend
|
|||
return true;
|
||||
}
|
||||
|
||||
private static readonly bool _disableTrivialLeafDispatch = string.Equals(
|
||||
Environment.GetEnvironmentVariable("SHARPEMU_DISABLE_TRIVIAL_LEAF"), "1", StringComparison.Ordinal);
|
||||
|
||||
// Tiny exports called millions of times per second, where the ~250ns of full
|
||||
// import bookkeeping costs more than the export itself. Each one reads only
|
||||
// argument registers (RDI..R9) and thread-local state, returns in RAX, never
|
||||
// blocks, yields, calls back into the guest or reads the guest stack.
|
||||
private static bool IsTrivialLeafImport(string nid) =>
|
||||
nid is
|
||||
"0-KXaS70xy4" or // pthread_getspecific
|
||||
"eoht7mQOCmo" or // scePthreadGetspecific
|
||||
"EI-5-jlq2dE" or // scePthreadGetthreadid
|
||||
"3eqs37G74-s" or // pthread_getthreadid_np
|
||||
"BNowx2l588E" or // sceKernelGetProcessTimeCounterFrequency
|
||||
"fgxnMeTNUtY" or // sceKernelGetProcessTimeCounter
|
||||
"4J2sUJmuHZQ" or // sceKernelGetProcessTime
|
||||
"-2IRUCO--PM" or // sceKernelReadTsc
|
||||
"1j3S3n-tTW4" or // sceKernelGetTscFrequency
|
||||
"ob5xAW4ln-0" or // strchr
|
||||
"9yDWMxEFdJU" or // strrchr
|
||||
"V++UgBtQhn0"; // sceAgcGetDataPacketPayloadAddress
|
||||
|
||||
private unsafe ulong DispatchTrivialLeaf(
|
||||
CpuContext cpuContext,
|
||||
in ImportStubEntry importStubEntry,
|
||||
nint argPackPtr,
|
||||
long dispatchIndex)
|
||||
{
|
||||
// Polling loops on the host main thread use the time queries as loop-guard
|
||||
// boundaries; keep resetting the pattern so skipping the guard here cannot
|
||||
// turn a legitimate time-polling loop into a forced exit.
|
||||
if (importStubEntry.IsLoopGuardBoundary && !GuestThreadExecution.IsGuestThread)
|
||||
{
|
||||
ResetImportLoopPattern();
|
||||
}
|
||||
|
||||
cpuContext.Rip = importStubEntry.Address;
|
||||
cpuContext[CpuRegister.Rdi] = *(ulong*)argPackPtr;
|
||||
cpuContext[CpuRegister.Rsi] = *(ulong*)(argPackPtr + 8);
|
||||
cpuContext[CpuRegister.Rdx] = *(ulong*)(argPackPtr + 16);
|
||||
cpuContext[CpuRegister.Rcx] = *(ulong*)(argPackPtr + 24);
|
||||
cpuContext[CpuRegister.R8] = *(ulong*)(argPackPtr + 32);
|
||||
cpuContext[CpuRegister.R9] = *(ulong*)(argPackPtr + 40);
|
||||
cpuContext.ClearRaxWriteFlag();
|
||||
var returnValue = importStubEntry.Export!.Function(cpuContext);
|
||||
if (!cpuContext.WasRaxWritten)
|
||||
{
|
||||
cpuContext[CpuRegister.Rax] = unchecked((ulong)returnValue);
|
||||
}
|
||||
|
||||
if (returnValue != (int)OrbisGen2Result.ORBIS_GEN2_OK &&
|
||||
ShouldLogImportResult(importStubEntry.Nid, (OrbisGen2Result)returnValue))
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][WARN] Import#{dispatchIndex} result: {(OrbisGen2Result)returnValue} ({importStubEntry.Nid}) " +
|
||||
$"rdi=0x{cpuContext[CpuRegister.Rdi]:X16} rsi=0x{cpuContext[CpuRegister.Rsi]:X16} " +
|
||||
$"ret=0x{*(ulong*)(argPackPtr + 96):X16}");
|
||||
}
|
||||
|
||||
return cpuContext[CpuRegister.Rax];
|
||||
}
|
||||
|
||||
private static bool IsNoBlockLeafImport(string nid) =>
|
||||
nid is
|
||||
"8aI7R7WaOlc" or // sceAmprCommandBufferConstructor
|
||||
|
|
|
|||
|
|
@ -53,6 +53,10 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||
|
||||
public bool IsNoBlockLeaf { get; }
|
||||
|
||||
// Argument-register-only exports that never block or touch the guest
|
||||
// stack; DispatchImport runs them without the full import bookkeeping.
|
||||
public bool IsTrivialLeaf { get; }
|
||||
|
||||
public bool SuppressStrlenTrace { get; }
|
||||
|
||||
public bool IsLoopGuardBoundary { get; }
|
||||
|
|
@ -74,6 +78,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||
Export = export;
|
||||
IsLeaf = isLeaf;
|
||||
IsNoBlockLeaf = isNoBlockLeaf;
|
||||
IsTrivialLeaf = export is not null && IsTrivialLeafImport(nid);
|
||||
SuppressStrlenTrace = suppressStrlenTrace;
|
||||
IsLoopGuardBoundary = isLoopGuardBoundary;
|
||||
NidHash = nidHash;
|
||||
|
|
@ -5650,7 +5655,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||
|
||||
private static ulong MapGuestThreadAffinity(ulong guestAffinityMask)
|
||||
{
|
||||
if (guestAffinityMask == 0 || guestAffinityMask == ulong.MaxValue)
|
||||
if (guestAffinityMask == 0 || guestAffinityMask == ulong.MaxValue || !GuestAffinityEnabled)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
|
@ -5710,9 +5715,20 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Host lanes kept away from guest threads. Measured on a 16-lane host with
|
||||
/// Demon's Souls: reserving 0/4/6/8 lanes gave 6.08/6.78/7.20/5.62 fps, so
|
||||
/// the useful range is a bit over a third of the machine — too few and the
|
||||
/// Guest affinity is not applied to host threads unless
|
||||
/// SHARPEMU_GUEST_AFFINITY=1. A console title pins one spinning worker per
|
||||
/// dedicated core; on a shared host, pinning traps its renderer and any lock
|
||||
/// holder on a lane next to a busy spinner of equal priority, where it waits
|
||||
/// out whole scheduler quanta. Measured with Demon's Souls on a 16-lane host:
|
||||
/// intro 10.6 → 44.7 fps and menus 11.6 → 47.5 fps with pinning off.
|
||||
/// </summary>
|
||||
private static readonly bool GuestAffinityEnabled =
|
||||
Environment.GetEnvironmentVariable("SHARPEMU_GUEST_AFFINITY") == "1";
|
||||
|
||||
/// <summary>
|
||||
/// Host lanes kept away from pinned guest threads. Measured on a 16-lane host
|
||||
/// with Demon's Souls: reserving 0/4/6/8 lanes gave 6.08/6.78/7.20/5.62 fps,
|
||||
/// so the useful range is a bit over a third of the machine — too few and the
|
||||
/// emulator is crowded out, too many and the guest cannot make progress.
|
||||
/// </summary>
|
||||
private static readonly int EmulatorReservedLanes =
|
||||
|
|
|
|||
|
|
@ -53,6 +53,9 @@ public sealed class TrackedCpuMemory : ICpuMemory, ITrackedCpuMemory, IGuestMemo
|
|||
|
||||
public bool CanRead(ulong address, ulong size) => _inner.CanRead(address, size);
|
||||
|
||||
public bool TryScanCString(ulong address, byte needle, bool findLast, ulong maxLength, out ulong match) =>
|
||||
_inner.TryScanCString(address, needle, findLast, maxLength, out match);
|
||||
|
||||
public bool TryAllocateGuestMemory(ulong size, ulong alignment, out ulong address)
|
||||
{
|
||||
if (_inner is IGuestMemoryAllocator allocator)
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ internal static class GuestRedZonePatcher
|
|||
{
|
||||
private const int GuestRedZoneBytes = 128;
|
||||
private const int MinimumJumpBytes = 5;
|
||||
private const int EstimatedTrampolineBytesPerSite = 48;
|
||||
private const int EstimatedTrampolineBytesPerSite = 64;
|
||||
private const ulong PageSize = 0x1000;
|
||||
private const ulong AllocationAlignment = 0x10000;
|
||||
private const ulong MaximumRelativeJumpDistance = 0x7FFF_FFFF;
|
||||
|
|
@ -519,10 +519,22 @@ internal static class GuestRedZonePatcher
|
|||
{
|
||||
refusal = SpanRefusal.None;
|
||||
var instructions = new List<Instruction>(3);
|
||||
Instruction? terminalBranch = null;
|
||||
var byteLength = 0;
|
||||
for (var index = startIndex; index < decoded.Count && byteLength < MinimumJumpBytes; index++)
|
||||
{
|
||||
var instruction = decoded[index].Instruction;
|
||||
if (index != startIndex &&
|
||||
!branchTargets.Contains(instruction.IP) &&
|
||||
IsRelocatableDirectBranch(instruction))
|
||||
{
|
||||
// A short access followed by its branch (TEST mem; JZ) closes the
|
||||
// span with that branch; the trampoline runs it once RSP is restored.
|
||||
terminalBranch = instruction;
|
||||
byteLength += instruction.Length;
|
||||
break;
|
||||
}
|
||||
|
||||
if (instruction.FlowControl != FlowControl.Next)
|
||||
{
|
||||
refusal = SpanRefusal.ControlFlow;
|
||||
|
|
@ -560,10 +572,15 @@ internal static class GuestRedZonePatcher
|
|||
return false;
|
||||
}
|
||||
|
||||
site = new PatchSite(decoded[startIndex].Instruction.IP, byteLength, instructions, coreStart, coreCount);
|
||||
site = new PatchSite(decoded[startIndex].Instruction.IP, byteLength, instructions, coreStart, coreCount, terminalBranch);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool IsRelocatableDirectBranch(in Instruction instruction) =>
|
||||
instruction.FlowControl is FlowControl.ConditionalBranch or FlowControl.UnconditionalBranch &&
|
||||
instruction.OpCount == 1 &&
|
||||
instruction.GetOpKind(0) is OpKind.NearBranch16 or OpKind.NearBranch32 or OpKind.NearBranch64;
|
||||
|
||||
internal static bool UsesRedZone(in Instruction instruction)
|
||||
{
|
||||
if (!HasMemoryOperand(instruction) || instruction.MemoryBase != Register.RSP)
|
||||
|
|
@ -704,7 +721,20 @@ internal static class GuestRedZonePatcher
|
|||
}
|
||||
|
||||
var relocated = writer.ToArray();
|
||||
var trampolineLength = checked(prefixLength + 5 + relocated.Length + 8 + 5);
|
||||
byte[]? branch = null;
|
||||
if (site.TerminalBranch is { } terminalBranch &&
|
||||
!TryEncodeSegment(
|
||||
[terminalBranch],
|
||||
0,
|
||||
1,
|
||||
trampolineCursor + (ulong)prefixLength + 5 + (ulong)relocated.Length + 8,
|
||||
out branch))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var branchLength = branch?.Length ?? 0;
|
||||
var trampolineLength = checked(prefixLength + 5 + relocated.Length + 8 + branchLength + 5);
|
||||
if (trampolineCursor > trampolineEnd || (ulong)trampolineLength > trampolineEnd - trampolineCursor)
|
||||
{
|
||||
return false;
|
||||
|
|
@ -729,7 +759,10 @@ internal static class GuestRedZonePatcher
|
|||
trampoline[restoreOffset + 5] = 0;
|
||||
trampoline[restoreOffset + 6] = 0;
|
||||
trampoline[restoreOffset + 7] = 0;
|
||||
var returnJumpOffset = restoreOffset + 8;
|
||||
// LEA leaves the flags alone, so a conditional branch still sees the
|
||||
// result of the relocated compare.
|
||||
branch?.CopyTo(trampoline, restoreOffset + 8);
|
||||
var returnJumpOffset = restoreOffset + 8 + branchLength;
|
||||
if (!TryWriteRelativeJump(
|
||||
trampoline.AsSpan(returnJumpOffset, 5),
|
||||
trampolineCursor + (ulong)returnJumpOffset,
|
||||
|
|
@ -812,6 +845,34 @@ internal static class GuestRedZonePatcher
|
|||
}
|
||||
}
|
||||
|
||||
// The image can sit among dense host allocations (the runtime's own heaps) where
|
||||
// every fixed step above is taken. Look for any free range in reach instead.
|
||||
var low = Math.Max(maximumSite > MaximumRelativeJumpDistance ? maximumSite - MaximumRelativeJumpDistance : 0, AllocationAlignment);
|
||||
var highExclusive = minimumSite <= ulong.MaxValue - MaximumRelativeJumpDistance - 1
|
||||
? minimumSite + MaximumRelativeJumpDistance + 1
|
||||
: ulong.MaxValue;
|
||||
foreach (var candidate in memory.EnumerateFreeHostRanges(low, highExclusive, requiredBytes, AllocationAlignment))
|
||||
{
|
||||
if (!CanReach(candidate, minimumSite) ||
|
||||
!CanReach(candidate + requiredBytes - 1, maximumSite))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
address = memory.AllocateAt(candidate, requiredBytes, executable: true, allowAlternative: false);
|
||||
if (address == candidate)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
catch (InvalidOperationException)
|
||||
{
|
||||
// Taken since it was listed; keep looking.
|
||||
}
|
||||
}
|
||||
|
||||
address = 0;
|
||||
return false;
|
||||
}
|
||||
|
|
@ -870,7 +931,8 @@ internal static class GuestRedZonePatcher
|
|||
int ByteLength,
|
||||
IList<Instruction> Instructions,
|
||||
int CoreStart,
|
||||
int CoreCount);
|
||||
int CoreCount,
|
||||
Instruction? TerminalBranch = null);
|
||||
|
||||
internal readonly record struct PatchResult
|
||||
{
|
||||
|
|
|
|||
|
|
@ -371,6 +371,33 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||
return true;
|
||||
}
|
||||
|
||||
// Aligned starts of the free host ranges in [low, highExclusive) that can hold size
|
||||
// bytes, from the host's own region map. The ranges are free when listed; another
|
||||
// thread may take one before the caller allocates it.
|
||||
public IEnumerable<ulong> EnumerateFreeHostRanges(ulong low, ulong highExclusive, ulong size, ulong alignment)
|
||||
{
|
||||
var address = low;
|
||||
while (address < highExclusive && _hostMemory.Query(address, out var info) && info.RegionSize != 0)
|
||||
{
|
||||
var regionEnd = info.BaseAddress + info.RegionSize;
|
||||
if (regionEnd <= address)
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
|
||||
if (info.State == HostRegionState.Free)
|
||||
{
|
||||
var start = (address + alignment - 1) & ~(alignment - 1);
|
||||
if (start >= address && start + size >= start && start + size <= Math.Min(regionEnd, highExclusive))
|
||||
{
|
||||
yield return start;
|
||||
}
|
||||
}
|
||||
|
||||
address = regionEnd;
|
||||
}
|
||||
}
|
||||
|
||||
public string DescribeAddressForDiagnostics(ulong address)
|
||||
{
|
||||
if (!_hostMemory.Query(address, out var info))
|
||||
|
|
@ -1054,6 +1081,13 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||
|
||||
public bool IsBackedRange(ulong address, ulong size)
|
||||
{
|
||||
// A backed range confirmed from the view snapshot needs no lock; a miss (partly
|
||||
// unbacked, or a view being remapped) takes the locked path.
|
||||
if (_backedSpace is { } backed && !_disposed && backed.IsBackedWithoutLock(address, size))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
_gate.EnterReadLock();
|
||||
try
|
||||
{
|
||||
|
|
@ -1784,6 +1818,15 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||
|
||||
public bool TryRead(ulong virtualAddress, Span<byte> destination)
|
||||
{
|
||||
// Backed guest memory is the shared backing itself, and the backing's view table
|
||||
// is readable without a lock; only other memory needs _gate to find its region.
|
||||
// Descriptor evaluation alone reads guest words ~40 at a time per draw here.
|
||||
if (!destination.IsEmpty && _backedSpace is { } backed && !_disposed &&
|
||||
backed.TryReadSingleView(virtualAddress, destination))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var requiresExclusiveAccess = false;
|
||||
_gate.EnterReadLock();
|
||||
try
|
||||
|
|
@ -1851,6 +1894,85 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||
}
|
||||
}
|
||||
|
||||
public bool TryScanCString(ulong address, byte needle, bool findLast, ulong maxLength, out ulong match)
|
||||
{
|
||||
match = 0;
|
||||
_gate.EnterReadLock();
|
||||
try
|
||||
{
|
||||
// Scan page by page in place, with the same region and protection
|
||||
// checks TryRead applies before its copy. Anything TryRead would
|
||||
// handle specially (backed views, lazy commit, protection changes)
|
||||
// is left to the caller's copying path.
|
||||
ulong offset = 0;
|
||||
while (offset < maxLength)
|
||||
{
|
||||
var current = address + offset;
|
||||
if (current < address)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var length = Math.Min(PageSize - (current & (PageSize - 1)), maxLength - offset);
|
||||
var region = FindRegion(current, length);
|
||||
if (region is null ||
|
||||
region.IsReservedOnly ||
|
||||
IsBackedSpan(region, current, length) ||
|
||||
!TryResolveRegionOffset(current, length, region, out var regionOffset))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var hostAddress = region.VirtualAddress + regionOffset;
|
||||
if (!CanReadWithoutProtectionChange(hostAddress, length, region))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var bytes = new ReadOnlySpan<byte>((void*)hostAddress, (int)length);
|
||||
if (!findLast)
|
||||
{
|
||||
// One pass that stops at the needle or the terminator,
|
||||
// whichever comes first.
|
||||
var stop = bytes.IndexOfAny(needle, (byte)0);
|
||||
if (stop >= 0)
|
||||
{
|
||||
match = bytes[stop] == needle ? current + (ulong)stop : 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
offset += length;
|
||||
continue;
|
||||
}
|
||||
|
||||
var nulIndex = bytes.IndexOf((byte)0);
|
||||
var searched = nulIndex >= 0 ? bytes[..(nulIndex + 1)] : bytes;
|
||||
var found = findLast ? searched.LastIndexOf(needle) : searched.IndexOf(needle);
|
||||
if (found >= 0)
|
||||
{
|
||||
match = current + (ulong)found;
|
||||
if (!findLast)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (nulIndex >= 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
offset += length;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_gate.ExitReadLock();
|
||||
}
|
||||
}
|
||||
|
||||
public bool TryCompare(ulong virtualAddress, ReadOnlySpan<byte> expected) =>
|
||||
TryCompare(virtualAddress, expected, out var equal) && equal;
|
||||
|
||||
|
|
|
|||
|
|
@ -45,6 +45,31 @@ public sealed class GuestPageTracker
|
|||
});
|
||||
}
|
||||
|
||||
// Lock-free pre-check for hot readers: only the GPU queue thread sets GPU-dirty bits,
|
||||
// so on that thread a clear answer is exact; a stale dirty answer merely sends the
|
||||
// caller to HasGpuDirtyPages. Allocation- and lock-free.
|
||||
public bool MayHaveGpuDirtyPages(ulong vaddr, ulong size)
|
||||
{
|
||||
ValidateRange(vaddr, size);
|
||||
var remaining = size;
|
||||
var index = vaddr / BlockBytes;
|
||||
var offset = vaddr % BlockBytes;
|
||||
while (remaining != 0)
|
||||
{
|
||||
var bytes = Math.Min(BlockBytes - offset, remaining);
|
||||
if (Volatile.Read(ref _regions[index]) is { } region && region.IsModified(WriteOrigin.Gpu, offset, bytes))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
remaining -= bytes;
|
||||
offset = 0;
|
||||
index++;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool IsCpuWriteHotRange(ulong vaddr, ulong size)
|
||||
{
|
||||
RejectUploadCallbackReentry();
|
||||
|
|
@ -116,28 +141,52 @@ public sealed class GuestPageTracker
|
|||
// Removes protection from a range; a GPU-dirty region flushes through onFlush without the lock.
|
||||
public bool InvalidateRegion(ulong vaddr, ulong size, Action onFlush)
|
||||
{
|
||||
RejectUploadCallbackReentry();
|
||||
var tracked = false;
|
||||
VisitRegions(vaddr, size, create: false, (region, offset, bytes) =>
|
||||
var tracked = InvalidateRegion(vaddr, size, out var needsGpuFlush);
|
||||
if (needsGpuFlush)
|
||||
{
|
||||
tracked = true;
|
||||
bool shouldFlush;
|
||||
using (region.Lock.Hold())
|
||||
onFlush();
|
||||
}
|
||||
|
||||
return tracked;
|
||||
}
|
||||
|
||||
// Marks the CPU write in every region whose bytes the GPU has not modified and
|
||||
// reports whether any region still holds GPU data the caller must download first
|
||||
// (one download of the whole range covers them all). Allocation-free: guest
|
||||
// command writes land here once per dword.
|
||||
public bool InvalidateRegion(ulong vaddr, ulong size, out bool needsGpuFlush)
|
||||
{
|
||||
RejectUploadCallbackReentry();
|
||||
ValidateRange(vaddr, size);
|
||||
var tracked = false;
|
||||
needsGpuFlush = false;
|
||||
var remaining = size;
|
||||
var index = vaddr / BlockBytes;
|
||||
var offset = vaddr % BlockBytes;
|
||||
while (remaining != 0)
|
||||
{
|
||||
var bytes = Math.Min(BlockBytes - offset, remaining);
|
||||
if (Volatile.Read(ref _regions[index]) is { } region)
|
||||
{
|
||||
shouldFlush = region.IsModified(WriteOrigin.Gpu, offset, bytes);
|
||||
if (!shouldFlush)
|
||||
tracked = true;
|
||||
using (region.Lock.Hold())
|
||||
{
|
||||
region.MarkCpuWrite(region.BaseAddress + offset, bytes);
|
||||
if (region.IsModified(WriteOrigin.Gpu, offset, bytes))
|
||||
{
|
||||
needsGpuFlush = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
region.MarkCpuWrite(region.BaseAddress + offset, bytes);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldFlush)
|
||||
{
|
||||
onFlush();
|
||||
}
|
||||
remaining -= bytes;
|
||||
offset = 0;
|
||||
index++;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
return tracked;
|
||||
}
|
||||
|
||||
|
|
@ -349,6 +398,49 @@ public sealed class GuestPageTracker
|
|||
|
||||
// True when every block of the range is tracked and has no CPU-dirty page. A missing
|
||||
// region is not known clean: the precise path creates it, fully dirty.
|
||||
// Visits the maximal runs of the range whose 4 MiB blocks may hold CPU-dirty pages: a block
|
||||
// with no region yet starts all dirty, and a region's summary bit is set while any of its
|
||||
// pages is dirty. Known-clean blocks are skipped without a lock, the same test
|
||||
// HasCpuDirtyPages starts with, so a caller that only uploads dirty pages loses nothing.
|
||||
public void ForEachPossiblyCpuDirtyRange(ulong vaddr, ulong size, Action<ulong, ulong> visit)
|
||||
{
|
||||
if (size == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!new GuestSpan(vaddr, size).IsValid)
|
||||
{
|
||||
// Outside the tracked space nothing is known clean; the caller decides as before.
|
||||
visit(vaddr, size);
|
||||
return;
|
||||
}
|
||||
|
||||
var end = vaddr + size;
|
||||
var last = (end - 1) / BlockBytes;
|
||||
var runStart = 0UL;
|
||||
var inRun = false;
|
||||
for (var index = vaddr / BlockBytes; index <= last; index++)
|
||||
{
|
||||
var possiblyDirty = Volatile.Read(ref _regions[index]) == null || _cpuDirtySummary.IsDirty(index);
|
||||
if (possiblyDirty && !inRun)
|
||||
{
|
||||
runStart = Math.Max(vaddr, index * BlockBytes);
|
||||
inRun = true;
|
||||
}
|
||||
else if (!possiblyDirty && inRun)
|
||||
{
|
||||
visit(runStart, index * BlockBytes - runStart);
|
||||
inRun = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (inRun)
|
||||
{
|
||||
visit(runStart, end - runStart);
|
||||
}
|
||||
}
|
||||
|
||||
private bool IsKnownCpuClean(ulong vaddr, ulong size)
|
||||
{
|
||||
ValidateRange(vaddr, size);
|
||||
|
|
|
|||
|
|
@ -64,10 +64,15 @@ public sealed class GuestSpaceOwner : IDisposable
|
|||
|
||||
public bool TryReadBacking(ulong address, Span<byte> data) => _views.TryReadBacking(address, data);
|
||||
|
||||
// Reads through a view that covers the whole range without any lock; false otherwise.
|
||||
public bool TryReadSingleView(ulong address, Span<byte> data) => _views.TryReadSingleView(address, data);
|
||||
|
||||
public bool TryCopyBacking(ulong destination, ulong source, ulong size) => _views.TryCopyBacking(destination, source, size);
|
||||
|
||||
public bool IsBacked(ulong address, ulong size) => _views.Contains(address, size);
|
||||
|
||||
public bool IsBackedWithoutLock(ulong address, ulong size) => _views.ContainsWithoutLock(address, size);
|
||||
|
||||
public bool IsRestoredView(ulong address) => _views.IsRestoredView(address);
|
||||
|
||||
public bool TryReserveAddressRange(ulong address, ulong size)
|
||||
|
|
|
|||
|
|
@ -21,6 +21,15 @@ public sealed unsafe class SharedBackingViews : IDisposable
|
|||
private readonly SortedList<ulong, ViewRecord> _views = new();
|
||||
private bool _disposed;
|
||||
|
||||
// Guest command writes and their reads by the render thread reach TryWriteBacking and
|
||||
// TryReadBacking millions of times per second, a few bytes at a time, while views
|
||||
// change rarely. Single-view accesses search this immutable copy of _views without
|
||||
// the lock; every change to _views republishes it under the lock.
|
||||
private ViewRecord[] _snapshot = [];
|
||||
|
||||
// Single-view accesses in flight; Dispose waits for them before releasing the alias.
|
||||
private int _activeAccesses;
|
||||
|
||||
public SharedBackingViews(IHostViewMemory host, ulong size)
|
||||
{
|
||||
_host = host;
|
||||
|
|
@ -49,6 +58,19 @@ public sealed unsafe class SharedBackingViews : IDisposable
|
|||
|
||||
public bool TryWriteBacking(ulong address, ReadOnlySpan<byte> data)
|
||||
{
|
||||
if (TryAccessSingleView(address, (ulong)data.Length, out var target))
|
||||
{
|
||||
try
|
||||
{
|
||||
data.CopyTo(new Span<byte>((void*)target, data.Length));
|
||||
return true;
|
||||
}
|
||||
finally
|
||||
{
|
||||
Interlocked.Decrement(ref _activeAccesses);
|
||||
}
|
||||
}
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
if (IsAvailable && TryFindRecord(address, (ulong)data.Length, out var record))
|
||||
|
|
@ -77,8 +99,40 @@ public sealed unsafe class SharedBackingViews : IDisposable
|
|||
}
|
||||
}
|
||||
|
||||
// The lock-free single-view read only; false (with nothing read) for anything else.
|
||||
public bool TryReadSingleView(ulong address, Span<byte> data)
|
||||
{
|
||||
if (!TryAccessSingleView(address, (ulong)data.Length, out var source))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
new ReadOnlySpan<byte>((void*)source, data.Length).CopyTo(data);
|
||||
return true;
|
||||
}
|
||||
finally
|
||||
{
|
||||
Interlocked.Decrement(ref _activeAccesses);
|
||||
}
|
||||
}
|
||||
|
||||
public bool TryReadBacking(ulong address, Span<byte> data)
|
||||
{
|
||||
if (TryAccessSingleView(address, (ulong)data.Length, out var source))
|
||||
{
|
||||
try
|
||||
{
|
||||
new ReadOnlySpan<byte>((void*)source, data.Length).CopyTo(data);
|
||||
return true;
|
||||
}
|
||||
finally
|
||||
{
|
||||
Interlocked.Decrement(ref _activeAccesses);
|
||||
}
|
||||
}
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
// A read inside one mapping needs no temporary segment list.
|
||||
|
|
@ -194,6 +248,7 @@ public sealed unsafe class SharedBackingViews : IDisposable
|
|||
}
|
||||
|
||||
_views[address] = new ViewRecord(address, size, offset, protection) { WasRestored = wasRestored };
|
||||
PublishSnapshot();
|
||||
}
|
||||
|
||||
failure = HostViewFailure.None;
|
||||
|
|
@ -268,6 +323,7 @@ public sealed unsafe class SharedBackingViews : IDisposable
|
|||
{
|
||||
old = record;
|
||||
_views.RemoveAt(index);
|
||||
PublishSnapshot();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -346,6 +402,49 @@ public sealed unsafe class SharedBackingViews : IDisposable
|
|||
}
|
||||
}
|
||||
|
||||
// Contains over the published snapshot, without the lock: descriptor validation asks
|
||||
// this for every bound buffer, and the lock is contended by guest threads. A true answer
|
||||
// is as current as a locked one would be once the lock were released.
|
||||
public bool ContainsWithoutLock(ulong address, ulong size)
|
||||
{
|
||||
if (size == 0 || ulong.MaxValue - address < size || Volatile.Read(ref _disposed))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var snapshot = Volatile.Read(ref _snapshot);
|
||||
var end = address + size;
|
||||
var current = address;
|
||||
while (current < end)
|
||||
{
|
||||
var low = 0;
|
||||
var high = snapshot.Length - 1;
|
||||
var found = -1;
|
||||
while (low <= high)
|
||||
{
|
||||
var middle = low + ((high - low) >> 1);
|
||||
if (snapshot[middle].Address <= current)
|
||||
{
|
||||
found = middle;
|
||||
low = middle + 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
high = middle - 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (found < 0 || current >= snapshot[found].Address + snapshot[found].Size)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
current = Math.Min(end, snapshot[found].Address + snapshot[found].Size);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool Contains(ulong address, ulong size)
|
||||
{
|
||||
if (size == 0 || ulong.MaxValue - address < size)
|
||||
|
|
@ -385,6 +484,15 @@ public sealed unsafe class SharedBackingViews : IDisposable
|
|||
_disposed = true;
|
||||
views = new List<ViewRecord>(_views.Values);
|
||||
_views.Clear();
|
||||
PublishSnapshot();
|
||||
}
|
||||
|
||||
// A lock-free access that found a view before the snapshot emptied still copies
|
||||
// through the alias; let it finish before the alias is released.
|
||||
var spin = new SpinWait();
|
||||
while (Volatile.Read(ref _activeAccesses) != 0)
|
||||
{
|
||||
spin.SpinOnce();
|
||||
}
|
||||
|
||||
foreach (var view in views)
|
||||
|
|
@ -495,6 +603,62 @@ public sealed unsafe class SharedBackingViews : IDisposable
|
|||
return true;
|
||||
}
|
||||
|
||||
// Must be called under _lock after every change to _views.
|
||||
private void PublishSnapshot()
|
||||
{
|
||||
var snapshot = new ViewRecord[_views.Count];
|
||||
_views.Values.CopyTo(snapshot, 0);
|
||||
Volatile.Write(ref _snapshot, snapshot);
|
||||
}
|
||||
|
||||
// Resolves an access that lies inside one view to its alias address and registers
|
||||
// it as in flight; the caller must decrement _activeAccesses when done. Anything
|
||||
// else (spanning views, unmapped, disposed) is left to the locked path.
|
||||
private bool TryAccessSingleView(ulong address, ulong size, out ulong target)
|
||||
{
|
||||
target = 0;
|
||||
if (size == 0 || ulong.MaxValue - address < size)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
Interlocked.Increment(ref _activeAccesses);
|
||||
var snapshot = Volatile.Read(ref _snapshot);
|
||||
var low = 0;
|
||||
var high = snapshot.Length - 1;
|
||||
var found = -1;
|
||||
while (low <= high)
|
||||
{
|
||||
var middle = low + ((high - low) >> 1);
|
||||
if (snapshot[middle].Address <= address)
|
||||
{
|
||||
found = middle;
|
||||
low = middle + 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
high = middle - 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (found >= 0 && !Volatile.Read(ref _disposed) && _backing != null)
|
||||
{
|
||||
var record = snapshot[found];
|
||||
if (address + size <= record.Address + record.Size)
|
||||
{
|
||||
var offset = record.Offset + address - record.Address;
|
||||
if (IsWithinBacking(offset, size))
|
||||
{
|
||||
target = AliasBase + offset;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Interlocked.Decrement(ref _activeAccesses);
|
||||
return false;
|
||||
}
|
||||
|
||||
private bool IsWithinBacking(ulong offset, ulong size) =>
|
||||
size != 0 && offset < Size && size <= Size - offset;
|
||||
|
||||
|
|
@ -503,6 +667,7 @@ public sealed unsafe class SharedBackingViews : IDisposable
|
|||
lock (_lock)
|
||||
{
|
||||
_views[record.Address] = record;
|
||||
PublishSnapshot();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -23,6 +23,16 @@ public interface ICpuMemory
|
|||
|
||||
bool TryCopy(ulong destinationAddress, ulong sourceAddress, ulong length) => false;
|
||||
|
||||
// Finds the first (or last) byte equal to needle in the NUL-terminated string
|
||||
// at address, the terminator included, scanning mapped memory in place.
|
||||
// match is 0 when absent. False means this memory cannot scan directly and
|
||||
// the caller should read the string through TryRead instead.
|
||||
bool TryScanCString(ulong address, byte needle, bool findLast, ulong maxLength, out ulong match)
|
||||
{
|
||||
match = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
// True when the whole range is mapped guest memory; no bytes are copied.
|
||||
bool CanRead(ulong address, ulong size) => false;
|
||||
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ namespace LibAtrac9
|
|||
public int SampleRateIndex { get; }
|
||||
/// <summary>
|
||||
/// A 3-bit value specifying one of 6 substream channel mappings.
|
||||
/// Always 0 for streams with independent mono channels.
|
||||
/// </summary>
|
||||
public int ChannelConfigIndex { get; }
|
||||
/// <summary>
|
||||
|
|
@ -70,6 +71,12 @@ namespace LibAtrac9
|
|||
/// The number of samples in one superframe.
|
||||
/// </summary>
|
||||
public int SuperframeSamples { get; }
|
||||
/// <summary>
|
||||
/// Non-zero for streams whose config header is 0x30: every channel is
|
||||
/// an independent mono stream with this many bytes per superframe,
|
||||
/// and the channels are interleaved frame by frame.
|
||||
/// </summary>
|
||||
public int IndependentBlockSuperframeBytes { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Reads ATRAC9 configuration data and calculates the stream parameters from it.
|
||||
|
|
@ -82,16 +89,33 @@ namespace LibAtrac9
|
|||
throw new InvalidDataException("Config data must be 4 bytes long");
|
||||
}
|
||||
|
||||
ReadConfigData(configData, out int a, out int b, out int c, out int d);
|
||||
ReadConfigData(configData, out int a, out int b, out int c, out int d, out int independentChannels);
|
||||
SampleRateIndex = a;
|
||||
ChannelConfigIndex = b;
|
||||
FrameBytes = c;
|
||||
SuperframeIndex = d;
|
||||
ConfigData = configData;
|
||||
|
||||
if (independentChannels > 0)
|
||||
{
|
||||
// The frame size field holds one channel's share; the frame as
|
||||
// a whole carries one mono block per channel.
|
||||
ChannelConfig = ChannelConfig.CreateIndependentMono(independentChannels);
|
||||
IndependentBlockSuperframeBytes = c << SuperframeIndex;
|
||||
FrameBytes = c * independentChannels;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (ChannelConfigIndex >= Tables.ChannelConfig.Length)
|
||||
{
|
||||
throw new InvalidDataException("ATRAC9 Config Data is invalid");
|
||||
}
|
||||
|
||||
ChannelConfig = Tables.ChannelConfig[ChannelConfigIndex];
|
||||
FrameBytes = c;
|
||||
}
|
||||
|
||||
FramesPerSuperframe = 1 << SuperframeIndex;
|
||||
SuperframeBytes = FrameBytes << SuperframeIndex;
|
||||
ChannelConfig = Tables.ChannelConfig[ChannelConfigIndex];
|
||||
|
||||
ChannelCount = ChannelConfig.ChannelCount;
|
||||
SampleRate = Tables.SampleRates[SampleRateIndex];
|
||||
|
|
@ -101,21 +125,36 @@ namespace LibAtrac9
|
|||
SuperframeSamples = FrameSamples * FramesPerSuperframe;
|
||||
}
|
||||
|
||||
private static void ReadConfigData(byte[] configData, out int sampleRateIndex, out int channelConfigIndex, out int frameBytes, out int superframeIndex)
|
||||
private static void ReadConfigData(byte[] configData, out int sampleRateIndex, out int channelConfigIndex, out int frameBytes, out int superframeIndex, out int independentChannels)
|
||||
{
|
||||
var reader = new BitReader(configData);
|
||||
|
||||
int header = reader.ReadInt(8);
|
||||
sampleRateIndex = reader.ReadInt(4);
|
||||
channelConfigIndex = reader.ReadInt(3);
|
||||
channelConfigIndex = 0;
|
||||
independentChannels = 0;
|
||||
if (header == IndependentMonoHeader)
|
||||
{
|
||||
// PS5 multichannel streams (ambisonics: 4, 9 or 16 channels)
|
||||
// replace the 3-bit channel mapping with a 6-bit channel count.
|
||||
independentChannels = reader.ReadInt(6) + 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
channelConfigIndex = reader.ReadInt(3);
|
||||
}
|
||||
|
||||
int validationBit = reader.ReadInt(1);
|
||||
frameBytes = reader.ReadInt(11) + 1;
|
||||
superframeIndex = reader.ReadInt(2);
|
||||
|
||||
if (header != 0xFE || validationBit != 0)
|
||||
if ((header != StandardHeader && header != IndependentMonoHeader) || validationBit != 0)
|
||||
{
|
||||
throw new InvalidDataException("ATRAC9 Config Data is invalid");
|
||||
}
|
||||
}
|
||||
|
||||
private const int StandardHeader = 0xFE;
|
||||
private const int IndependentMonoHeader = 0x30;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ namespace LibAtrac9
|
|||
|
||||
public bool FirstInSuperframe { get; set; }
|
||||
public bool ReuseBandParams { get; set; }
|
||||
public int SuperframeBytesUsed { get; set; }
|
||||
|
||||
public int BandCount { get; set; }
|
||||
public int StereoBand { get; set; }
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
// SPDX-License-Identifier: MIT
|
||||
#nullable disable
|
||||
using System;
|
||||
|
||||
namespace LibAtrac9
|
||||
{
|
||||
/// <summary>
|
||||
|
|
@ -17,6 +19,13 @@ namespace LibAtrac9
|
|||
}
|
||||
}
|
||||
|
||||
internal static ChannelConfig CreateIndependentMono(int channelCount)
|
||||
{
|
||||
var blockTypes = new BlockType[channelCount];
|
||||
Array.Fill(blockTypes, BlockType.Mono);
|
||||
return new ChannelConfig(blockTypes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The number of blocks or substreams in the ATRAC9 stream
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -10,12 +10,57 @@ namespace LibAtrac9
|
|||
{
|
||||
public static void UnpackFrame(BitReader reader, Frame frame)
|
||||
{
|
||||
if (frame.Config.IndependentBlockSuperframeBytes != 0)
|
||||
{
|
||||
UnpackIndependentMonoFrame(reader, frame);
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (Block block in frame.Blocks)
|
||||
{
|
||||
UnpackBlock(reader, block);
|
||||
}
|
||||
}
|
||||
|
||||
// Each channel is a standalone mono superframe with a fixed byte budget,
|
||||
// interleaved frame by frame. Blocks of the earlier frames are packed
|
||||
// back to back; in the last frame every block gets whatever is left of
|
||||
// its channel's budget, so those slots end in padding.
|
||||
private static void UnpackIndependentMonoFrame(BitReader reader, Frame frame)
|
||||
{
|
||||
int budget = frame.Config.IndependentBlockSuperframeBytes;
|
||||
bool lastFrame = frame.FrameIndex == frame.Config.FramesPerSuperframe - 1;
|
||||
int slotStart = reader.Position / 8;
|
||||
|
||||
foreach (Block block in frame.Blocks)
|
||||
{
|
||||
if (frame.FrameIndex == 0)
|
||||
{
|
||||
block.SuperframeBytesUsed = 0;
|
||||
}
|
||||
|
||||
int usedBefore = block.SuperframeBytesUsed;
|
||||
if (lastFrame)
|
||||
{
|
||||
reader.Position = slotStart * 8;
|
||||
}
|
||||
|
||||
int start = reader.Position;
|
||||
UnpackBlock(reader, block);
|
||||
block.SuperframeBytesUsed += (reader.Position - start) / 8;
|
||||
if (block.SuperframeBytesUsed > budget)
|
||||
{
|
||||
throw new InvalidDataException();
|
||||
}
|
||||
|
||||
if (lastFrame)
|
||||
{
|
||||
slotStart += budget - usedBefore;
|
||||
reader.Position = slotStart * 8;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void UnpackBlock(BitReader reader, Block block)
|
||||
{
|
||||
ReadBlockHeader(reader, block);
|
||||
|
|
|
|||
|
|
@ -546,8 +546,11 @@ public static partial class AgcExports
|
|||
|
||||
var increment = (uint)(incrementRaw & 0xFF);
|
||||
var writeConfirm = (uint)(writeConfirmRaw & 0xFF);
|
||||
// A zero destination is legal: titles emit the packet into a template
|
||||
// and set the address later with sceAgcWriteDataPatchSetAddressOrOffset.
|
||||
// Rejecting it returned null, and patching that relocated null pointer
|
||||
// wrote garbage into the label the packet was meant to clear.
|
||||
if (commandBufferAddress == 0 ||
|
||||
destinationAddress == 0 ||
|
||||
dataAddress == 0 ||
|
||||
dwordCount > 0x3FFD)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -184,24 +184,6 @@ public static partial class AgcExports
|
|||
var commandBufferAddress = ctx[CpuRegister.Rdi];
|
||||
var dataOffset = (uint)ctx[CpuRegister.Rsi];
|
||||
var drawModifier = ctx[CpuRegister.Rdx];
|
||||
var emit = Interlocked.Increment(ref _indirectDrawEmitCount);
|
||||
|
||||
if (emit <= 12 || emit % 250 == 0)
|
||||
{
|
||||
var rcx = ctx[CpuRegister.Rcx];
|
||||
var dump = string.Empty;
|
||||
for (var word = 0; word < 8; word++)
|
||||
{
|
||||
dump += TryReadUInt32(ctx, rcx + dataOffset + ((ulong)word * 4), out var raw)
|
||||
? $" {raw}"
|
||||
: " ?";
|
||||
}
|
||||
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][WARN] agc.emit_indirect#{emit} buf=0x{commandBufferAddress:X16} " +
|
||||
$"off=0x{dataOffset:X} rdx=0x{ctx[CpuRegister.Rdx]:X} rcx=0x{rcx:X} " +
|
||||
$"r8=0x{ctx[CpuRegister.R8]:X} rcx_words:{dump}");
|
||||
}
|
||||
|
||||
if (commandBufferAddress == 0)
|
||||
{
|
||||
|
|
@ -242,7 +224,7 @@ public static partial class AgcExports
|
|||
|
||||
TraceAgc(
|
||||
$"agc.dcb_draw_indirect buf=0x{commandBufferAddress:X16} " +
|
||||
$"draw=0x{drawCommand:X16} offset=0x{dataOffset:X}");
|
||||
$"draw=0x{drawCommand:X16} offset=0x{dataOffset:X} modifier=0x{drawModifier:X}");
|
||||
|
||||
return ReturnPointer(ctx, drawCommand);
|
||||
}
|
||||
|
|
@ -474,6 +456,5 @@ public static partial class AgcExports
|
|||
}
|
||||
#pragma warning restore SHEM006
|
||||
|
||||
private static long _indirectDrawEmitCount;
|
||||
private static long _indirectDrawEmitRejectCount;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,13 +10,8 @@ public static partial class AgcExports
|
|||
{
|
||||
// This partial exposes the shader headers the create export registered.
|
||||
|
||||
internal static ulong GetShaderHeaderAddress(ulong codeAddress)
|
||||
{
|
||||
lock (_submitTraceGate)
|
||||
{
|
||||
return _shaderHeadersByCode.TryGetValue(codeAddress, out var header) ? header : 0;
|
||||
}
|
||||
}
|
||||
internal static ulong GetShaderHeaderAddress(ulong codeAddress) =>
|
||||
_shaderHeadersByCode.TryGetValue(codeAddress, out var header) ? header : 0;
|
||||
|
||||
internal static ShaderHeaderRegistry CreateShaderHeaderRegistry(CpuContext context) =>
|
||||
new(
|
||||
|
|
|
|||
|
|
@ -128,10 +128,7 @@ public static partial class AgcExports
|
|||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
lock (_submitTraceGate)
|
||||
{
|
||||
_shaderHeadersByCode[codeAddress] = headerAddress;
|
||||
}
|
||||
_shaderHeadersByCode[codeAddress] = headerAddress;
|
||||
|
||||
TryRegisterEmbeddedFusedProgram(ctx, codeAddress, headerAddress);
|
||||
|
||||
|
|
|
|||
|
|
@ -201,10 +201,10 @@ public static partial class AgcExports
|
|||
private const ulong CommandBufferCallbackOffset = 0x20;
|
||||
private const ulong CommandBufferUserDataOffset = 0x28;
|
||||
private const ulong CommandBufferReservedDwOffset = 0x30;
|
||||
private static readonly object _submitTraceGate = new();
|
||||
// Every PM4 opcode with no handler, logged once with its first two
|
||||
// payload dwords as a possible target address.
|
||||
private static readonly Dictionary<ulong, ulong> _shaderHeadersByCode = new();
|
||||
// Read by the render thread for every draw; written by guest threads creating shaders.
|
||||
private static readonly System.Collections.Concurrent.ConcurrentDictionary<ulong, ulong> _shaderHeadersByCode = new();
|
||||
private static readonly bool _traceAgc = string.Equals(
|
||||
Environment.GetEnvironmentVariable("SHARPEMU_LOG_AGC"),
|
||||
"1",
|
||||
|
|
|
|||
|
|
@ -9,7 +9,8 @@ using VkBuffer = Silk.NET.Vulkan.Buffer;
|
|||
|
||||
namespace SharpEmu.Libs.Gpu.Buffers;
|
||||
|
||||
// One Vulkan buffer with its own dedicated allocation, mapped when host-visible.
|
||||
// One Vulkan buffer, mapped when host-visible. Small buffers share slab chunks (GpuMemorySlabs);
|
||||
// larger ones get a dedicated allocation.
|
||||
public unsafe class GpuBuffer : IDisposable
|
||||
{
|
||||
public const BufferUsageFlags ReadFlags =
|
||||
|
|
@ -28,8 +29,12 @@ public unsafe class GpuBuffer : IDisposable
|
|||
private readonly ulong _deviceAddress;
|
||||
private VkBuffer _handle;
|
||||
private DeviceMemory _memory;
|
||||
private readonly ulong _memoryOffset;
|
||||
private readonly GpuMemorySlabs.Block? _slab;
|
||||
|
||||
public GpuBuffer(GpuDeviceInfo device, SubmissionScheduler scheduler, GpuBufferUsage usage, ulong cpuAddress, BufferUsageFlags flags, ulong size)
|
||||
// allowSlab: the caller records on the scheduler's current command buffer, which clears a recycled slab block.
|
||||
public GpuBuffer(GpuDeviceInfo device, SubmissionScheduler scheduler, GpuBufferUsage usage, ulong cpuAddress, BufferUsageFlags flags, ulong size,
|
||||
bool allowSlab = false)
|
||||
{
|
||||
if (size == 0)
|
||||
{
|
||||
|
|
@ -43,14 +48,23 @@ public unsafe class GpuBuffer : IDisposable
|
|||
Size = size;
|
||||
|
||||
var vk = device.Vk;
|
||||
var bufferInfo = new BufferCreateInfo
|
||||
var sharedFamilies = device.SharedQueueFamilies;
|
||||
fixed (uint* families = sharedFamilies)
|
||||
{
|
||||
SType = StructureType.BufferCreateInfo,
|
||||
Size = size,
|
||||
Usage = flags,
|
||||
SharingMode = SharingMode.Exclusive,
|
||||
};
|
||||
RequireSuccess(vk.CreateBuffer(device.Device, &bufferInfo, null, out _handle), "vkCreateBuffer");
|
||||
// Buffers carry no layout, so concurrent sharing lets the readback queue copy
|
||||
// them without queue-family ownership transfers at no cost to other queues.
|
||||
var bufferInfo = new BufferCreateInfo
|
||||
{
|
||||
SType = StructureType.BufferCreateInfo,
|
||||
Size = size,
|
||||
Usage = flags,
|
||||
SharingMode = sharedFamilies is { Length: > 1 } ? SharingMode.Concurrent : SharingMode.Exclusive,
|
||||
QueueFamilyIndexCount = sharedFamilies is { Length: > 1 } ? (uint)sharedFamilies.Length : 0,
|
||||
PQueueFamilyIndices = sharedFamilies is { Length: > 1 } ? families : null,
|
||||
};
|
||||
RequireSuccess(vk.CreateBuffer(device.Device, &bufferInfo, null, out _handle), "vkCreateBuffer");
|
||||
}
|
||||
|
||||
vk.GetBufferMemoryRequirements(device.Device, _handle, out var requirements);
|
||||
var withAddress = (flags & BufferUsageFlags.ShaderDeviceAddressBit) != 0;
|
||||
var flagsInfo = new MemoryAllocateFlagsInfo
|
||||
|
|
@ -67,23 +81,43 @@ public unsafe class GpuBuffer : IDisposable
|
|||
|
||||
// The best-scored type first; a full heap falls through to the next candidate.
|
||||
var result = Result.ErrorOutOfDeviceMemory;
|
||||
foreach (var candidate in RankMemoryTypes(device, requirements.MemoryTypeBits, usage))
|
||||
var candidates = RankMemoryTypes(device, requirements.MemoryTypeBits, usage).ToArray();
|
||||
if (allowSlab && GpuMemorySlabs.Fits(requirements))
|
||||
{
|
||||
allocateInfo.MemoryTypeIndex = candidate;
|
||||
result = device.AllocateMemory(allocateInfo, out _memory);
|
||||
if (result == Result.Success)
|
||||
foreach (var candidate in candidates)
|
||||
{
|
||||
break;
|
||||
if (device.Slabs.TryAllocate(candidate, withAddress, requirements, out var block))
|
||||
{
|
||||
_slab = block;
|
||||
_memory = block.Memory;
|
||||
_memoryOffset = block.Offset;
|
||||
_mapped = block.Mapped;
|
||||
allocateInfo.MemoryTypeIndex = candidate;
|
||||
result = Result.Success;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (_slab is null)
|
||||
{
|
||||
foreach (var candidate in candidates)
|
||||
{
|
||||
allocateInfo.MemoryTypeIndex = candidate;
|
||||
result = device.AllocateMemory(allocateInfo, out _memory);
|
||||
if (result == Result.Success)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RequireSuccess(result, $"vkAllocateMemory({usage}, 0x{size:X} bytes)");
|
||||
RequireSuccess(vk.BindBufferMemory(device.Device, _handle, _memory, 0), "vkBindBufferMemory");
|
||||
_allocationSize = requirements.Size;
|
||||
|
||||
RequireSuccess(vk.BindBufferMemory(device.Device, _handle, _memory, _memoryOffset), "vkBindBufferMemory");
|
||||
_allocationSize = _slab?.Size ?? requirements.Size;
|
||||
var properties = device.GetMemoryTypeFlags(allocateInfo.MemoryTypeIndex);
|
||||
IsCoherent = (properties & MemoryPropertyFlags.HostCoherentBit) != 0;
|
||||
if ((properties & MemoryPropertyFlags.HostVisibleBit) != 0)
|
||||
if (_slab is null && (properties & MemoryPropertyFlags.HostVisibleBit) != 0)
|
||||
{
|
||||
void* pointer;
|
||||
RequireSuccess(vk.MapMemory(device.Device, _memory, 0, Vk.WholeSize, 0, &pointer), "vkMapMemory");
|
||||
|
|
@ -99,6 +133,12 @@ public unsafe class GpuBuffer : IDisposable
|
|||
throw SubmissionScheduler.Fatal("The buffer device address is unavailable.");
|
||||
}
|
||||
}
|
||||
|
||||
// A dedicated allocation arrives zeroed; keep that for a block another buffer used.
|
||||
if (_slab is { Recycled: true } && (Size & ~3UL) != 0)
|
||||
{
|
||||
Fill(0, Size & ~3UL, 0);
|
||||
}
|
||||
}
|
||||
|
||||
public VkBuffer Handle => _handle;
|
||||
|
|
@ -128,6 +168,20 @@ public unsafe class GpuBuffer : IDisposable
|
|||
|
||||
public void AddStreamScore(int score) => StreamScore += score;
|
||||
|
||||
// The highest scheduler tick whose command buffer may write this buffer on the GPU.
|
||||
// A readback of it only has to wait for that tick, not for all queued work.
|
||||
public ulong LastGpuWriteTick { get; private set; }
|
||||
|
||||
// Called by every path that records a GPU write into this buffer.
|
||||
public void NoteGpuWrite()
|
||||
{
|
||||
var tick = _scheduler.CurrentTick;
|
||||
if (tick > LastGpuWriteTick)
|
||||
{
|
||||
LastGpuWriteTick = tick;
|
||||
}
|
||||
}
|
||||
|
||||
public void Write(ulong offset, ReadOnlySpan<byte> source)
|
||||
{
|
||||
if (_mapped == null || offset > Size || (ulong)source.Length > Size - offset)
|
||||
|
|
@ -189,6 +243,7 @@ public unsafe class GpuBuffer : IDisposable
|
|||
}
|
||||
|
||||
command.EndRendering();
|
||||
NoteGpuWrite();
|
||||
var vk = _device.Vk;
|
||||
var native = new CommandBuffer(command.Handle);
|
||||
var before = stackalloc BufferMemoryBarrier2[2];
|
||||
|
|
@ -216,6 +271,7 @@ public unsafe class GpuBuffer : IDisposable
|
|||
|
||||
var command = _scheduler.Current;
|
||||
command.EndRendering();
|
||||
NoteGpuWrite();
|
||||
var vk = _device.Vk;
|
||||
var native = new CommandBuffer(command.Handle);
|
||||
var before = CreateBarrier(offset, size, MemoryAccess, AccessFlags.TransferWriteBit);
|
||||
|
|
@ -237,7 +293,15 @@ public unsafe class GpuBuffer : IDisposable
|
|||
}
|
||||
|
||||
_device.Vk.DestroyBuffer(_device.Device, _handle, null);
|
||||
_device.FreeMemory(_memory);
|
||||
if (_slab is { } slab)
|
||||
{
|
||||
_device.Slabs.Release(slab);
|
||||
}
|
||||
else
|
||||
{
|
||||
_device.FreeMemory(_memory);
|
||||
}
|
||||
|
||||
_handle = default;
|
||||
_memory = default;
|
||||
}
|
||||
|
|
@ -280,7 +344,7 @@ public unsafe class GpuBuffer : IDisposable
|
|||
{
|
||||
SType = StructureType.MappedMemoryRange,
|
||||
Memory = _memory,
|
||||
Offset = begin,
|
||||
Offset = _memoryOffset + begin,
|
||||
Size = end - begin,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,12 +10,16 @@ namespace SharpEmu.Libs.Gpu.Buffers;
|
|||
public sealed unsafe class GpuDeviceInfo : IImageFormatSupport
|
||||
{
|
||||
private PhysicalDeviceMemoryProperties _memoryProperties;
|
||||
private readonly Dictionary<Format, FormatProperties> _formatProperties = new();
|
||||
private readonly Dictionary<(Format, ImageType, ImageTiling, ImageUsageFlags, ImageCreateFlags), (Result Result, ImageFormatProperties Properties)> _imageFormatProperties = new();
|
||||
private readonly object _gate = new();
|
||||
// Queried for every draw target from the render thread and from other threads; a hit takes no lock.
|
||||
// Two threads may query the same missing key once each; the driver answers identically.
|
||||
private readonly System.Collections.Concurrent.ConcurrentDictionary<Format, FormatProperties> _formatProperties = new();
|
||||
private readonly System.Collections.Concurrent.ConcurrentDictionary<(Format, ImageType, ImageTiling, ImageUsageFlags, ImageCreateFlags), (Result Result, ImageFormatProperties Properties)> _imageFormatProperties = new();
|
||||
private int _liveAllocations;
|
||||
private int _peakAllocations;
|
||||
|
||||
// VK_EXT_image_view_min_lod is enabled, so a view can clamp to a texture descriptor's MIN_LOD.
|
||||
public bool ImageViewMinLodSupported { get; init; }
|
||||
|
||||
public GpuDeviceInfo(Vk vk, PhysicalDevice physicalDevice, Device device)
|
||||
{
|
||||
Vk = vk;
|
||||
|
|
@ -29,10 +33,18 @@ public sealed unsafe class GpuDeviceInfo : IImageFormatSupport
|
|||
MaxStorageBufferRange = properties.Limits.MaxStorageBufferRange;
|
||||
MaxMemoryAllocationCount = properties.Limits.MaxMemoryAllocationCount;
|
||||
MaxComputeWorkGroupCount = (properties.Limits.MaxComputeWorkGroupCount[0], properties.Limits.MaxComputeWorkGroupCount[1], properties.Limits.MaxComputeWorkGroupCount[2]);
|
||||
Slabs = new GpuMemorySlabs(this);
|
||||
}
|
||||
|
||||
// Shared chunks the small buffers are carved from; freed at device teardown.
|
||||
internal GpuMemorySlabs Slabs { get; }
|
||||
|
||||
public Vk Vk { get; }
|
||||
|
||||
// Queue families that may access every buffer; set before the first buffer is
|
||||
// created when a second queue family (the async readback queue) reads them.
|
||||
public uint[]? SharedQueueFamilies { get; set; }
|
||||
|
||||
public PhysicalDevice PhysicalDevice { get; }
|
||||
|
||||
public Device Device { get; }
|
||||
|
|
@ -65,33 +77,27 @@ public sealed unsafe class GpuDeviceInfo : IImageFormatSupport
|
|||
|
||||
public FormatProperties GetFormatProperties(Format format)
|
||||
{
|
||||
lock (_gate)
|
||||
if (!_formatProperties.TryGetValue(format, out var properties))
|
||||
{
|
||||
if (!_formatProperties.TryGetValue(format, out var properties))
|
||||
{
|
||||
Vk.GetPhysicalDeviceFormatProperties(PhysicalDevice, format, out properties);
|
||||
_formatProperties[format] = properties;
|
||||
}
|
||||
|
||||
return properties;
|
||||
Vk.GetPhysicalDeviceFormatProperties(PhysicalDevice, format, out properties);
|
||||
_formatProperties[format] = properties;
|
||||
}
|
||||
|
||||
return properties;
|
||||
}
|
||||
|
||||
public bool TryGetImageFormatProperties(Format format, ImageType type, ImageTiling tiling, ImageUsageFlags usage, ImageCreateFlags flags, out ImageFormatProperties properties)
|
||||
{
|
||||
lock (_gate)
|
||||
var key = (format, type, tiling, usage, flags);
|
||||
if (!_imageFormatProperties.TryGetValue(key, out var entry))
|
||||
{
|
||||
var key = (format, type, tiling, usage, flags);
|
||||
if (!_imageFormatProperties.TryGetValue(key, out var entry))
|
||||
{
|
||||
var result = Vk.GetPhysicalDeviceImageFormatProperties(PhysicalDevice, format, type, tiling, usage, flags, out var found);
|
||||
entry = (result, found);
|
||||
_imageFormatProperties[key] = entry;
|
||||
}
|
||||
|
||||
properties = entry.Properties;
|
||||
return entry.Result == Result.Success;
|
||||
var result = Vk.GetPhysicalDeviceImageFormatProperties(PhysicalDevice, format, type, tiling, usage, flags, out var found);
|
||||
entry = (result, found);
|
||||
_imageFormatProperties[key] = entry;
|
||||
}
|
||||
|
||||
properties = entry.Properties;
|
||||
return entry.Result == Result.Success;
|
||||
}
|
||||
|
||||
// Every device-memory allocation goes through here so the live count stays exact.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,171 @@
|
|||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Numerics;
|
||||
using Silk.NET.Vulkan;
|
||||
|
||||
namespace SharpEmu.Libs.Gpu.Buffers;
|
||||
|
||||
// Small buffers take a power-of-two block out of a shared, persistently mapped chunk
|
||||
// instead of a dedicated allocation. Games stream constants through fresh ranges every
|
||||
// frame, and the buffer cache created and freed a device allocation for each of them;
|
||||
// vkAllocateMemory/vkMapMemory/vkFreeMemory then cost ~15% of the render thread in busy
|
||||
// Astro Bot scenes, and every live allocation counts against maxMemoryAllocationCount.
|
||||
internal sealed unsafe class GpuMemorySlabs
|
||||
{
|
||||
public const ulong MinBlockSize = 4096;
|
||||
public const ulong MaxBlockSize = 1024 * 1024;
|
||||
private const ulong ChunkSize = 8 * 1024 * 1024;
|
||||
private static readonly int ClassCount = BitOperations.Log2(MaxBlockSize) - BitOperations.Log2(MinBlockSize) + 1;
|
||||
|
||||
private readonly GpuDeviceInfo _device;
|
||||
private readonly object _gate = new();
|
||||
private readonly Dictionary<(uint MemoryType, bool DeviceAddress), Pool> _pools = [];
|
||||
private readonly List<DeviceMemory> _chunks = [];
|
||||
private readonly List<Block> _quarantine = [];
|
||||
private int _foreignReaders;
|
||||
|
||||
public GpuMemorySlabs(GpuDeviceInfo device) => _device = device;
|
||||
|
||||
// Recycled blocks still hold their previous buffer's bytes; fresh ones come zeroed with their chunk.
|
||||
public readonly record struct Block(DeviceMemory Memory, ulong Offset, ulong Size, nint MappedAddress, uint MemoryType, bool DeviceAddress, bool Recycled)
|
||||
{
|
||||
public byte* Mapped => (byte*)MappedAddress;
|
||||
}
|
||||
|
||||
private sealed class Pool
|
||||
{
|
||||
public readonly Stack<(DeviceMemory Memory, ulong Offset, nint Mapped, bool Recycled)>[] Free =
|
||||
Enumerable.Range(0, ClassCount).Select(_ => new Stack<(DeviceMemory, ulong, nint, bool)>()).ToArray();
|
||||
}
|
||||
|
||||
// A readback on the second queue copies from buffers the render thread may retire (and
|
||||
// recycle) while the copy is still pending. A dedicated allocation kept its old bytes
|
||||
// after vkFreeMemory, which hid that race; a recycled block does not. Released blocks
|
||||
// therefore wait until no readback that might have picked their buffer is running.
|
||||
public ForeignReadScope BeginForeignRead()
|
||||
{
|
||||
Interlocked.Increment(ref _foreignReaders);
|
||||
return new ForeignReadScope(this);
|
||||
}
|
||||
|
||||
public readonly struct ForeignReadScope(GpuMemorySlabs owner) : IDisposable
|
||||
{
|
||||
public void Dispose() => Interlocked.Decrement(ref owner._foreignReaders);
|
||||
}
|
||||
|
||||
public static bool Fits(in MemoryRequirements requirements) =>
|
||||
requirements.Size <= MaxBlockSize && requirements.Alignment <= MaxBlockSize;
|
||||
|
||||
public bool TryAllocate(uint memoryType, bool deviceAddress, in MemoryRequirements requirements, out Block block)
|
||||
{
|
||||
block = default;
|
||||
var blockSize = Math.Max(MinBlockSize, BitOperations.RoundUpToPowerOf2(Math.Max(requirements.Size, requirements.Alignment)));
|
||||
if (blockSize > MaxBlockSize)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var sizeClass = BitOperations.Log2(blockSize) - BitOperations.Log2(MinBlockSize);
|
||||
lock (_gate)
|
||||
{
|
||||
if (_quarantine.Count != 0 && Volatile.Read(ref _foreignReaders) == 0)
|
||||
{
|
||||
foreach (var released in _quarantine)
|
||||
{
|
||||
var releasedClass = BitOperations.Log2(released.Size) - BitOperations.Log2(MinBlockSize);
|
||||
_pools[(released.MemoryType, released.DeviceAddress)].Free[releasedClass]
|
||||
.Push((released.Memory, released.Offset, released.MappedAddress, true));
|
||||
}
|
||||
|
||||
_quarantine.Clear();
|
||||
}
|
||||
|
||||
if (!_pools.TryGetValue((memoryType, deviceAddress), out var pool))
|
||||
{
|
||||
pool = new Pool();
|
||||
_pools.Add((memoryType, deviceAddress), pool);
|
||||
}
|
||||
|
||||
var free = pool.Free[sizeClass];
|
||||
if (free.Count == 0 && !TryAddChunk(memoryType, deviceAddress, blockSize, free))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var (memory, offset, mapped, recycled) = free.Pop();
|
||||
block = new Block(memory, offset, blockSize, mapped, memoryType, deviceAddress, recycled);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public void Release(in Block block)
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
_quarantine.Add(block);
|
||||
}
|
||||
}
|
||||
|
||||
// Frees every chunk; only valid once no buffer still uses one (device teardown).
|
||||
public void Destroy()
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
foreach (var chunk in _chunks)
|
||||
{
|
||||
_device.FreeMemory(chunk);
|
||||
}
|
||||
|
||||
_chunks.Clear();
|
||||
_pools.Clear();
|
||||
_quarantine.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
private bool TryAddChunk(uint memoryType, bool deviceAddress, ulong blockSize, Stack<(DeviceMemory, ulong, nint, bool)> free)
|
||||
{
|
||||
var flagsInfo = new MemoryAllocateFlagsInfo
|
||||
{
|
||||
SType = StructureType.MemoryAllocateFlagsInfo,
|
||||
Flags = MemoryAllocateFlags.DeviceAddressBit,
|
||||
};
|
||||
var allocateInfo = new MemoryAllocateInfo
|
||||
{
|
||||
SType = StructureType.MemoryAllocateInfo,
|
||||
PNext = deviceAddress ? &flagsInfo : null,
|
||||
AllocationSize = ChunkSize,
|
||||
MemoryTypeIndex = memoryType,
|
||||
};
|
||||
if (_device.AllocateMemory(allocateInfo, out var memory) != Result.Success)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
byte* mapped = null;
|
||||
if ((_device.GetMemoryTypeFlags(memoryType) & MemoryPropertyFlags.HostVisibleBit) != 0)
|
||||
{
|
||||
void* pointer;
|
||||
if (_device.Vk.MapMemory(_device.Device, memory, 0, Vk.WholeSize, 0, &pointer) != Result.Success)
|
||||
{
|
||||
_device.FreeMemory(memory);
|
||||
return false;
|
||||
}
|
||||
|
||||
mapped = (byte*)pointer;
|
||||
}
|
||||
|
||||
_chunks.Add(memory);
|
||||
// Push in reverse so blocks come out in address order.
|
||||
for (var offset = ChunkSize - blockSize; ; offset -= blockSize)
|
||||
{
|
||||
free.Push((memory, offset, mapped == null ? 0 : (nint)(mapped + offset), false));
|
||||
if (offset == 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
@ -126,10 +126,9 @@ public sealed unsafe class GuestBufferCache : IGuestBufferStore, IDisposable
|
|||
// A CPU write fault: true when the range is tracked and any GPU data reached guest memory.
|
||||
bool IGuestBufferStore.MarkCpuWrite(ulong address, ulong size)
|
||||
{
|
||||
var completed = true;
|
||||
var tracked = _tracker.InvalidateRegion(address, size,
|
||||
() => completed &= ReadMemoryOrAwaitShutdown(address, size, isWrite: true,
|
||||
GuestMemoryProfile.ReadbackSource.CpuWriteInvalidation));
|
||||
var tracked = _tracker.InvalidateRegion(address, size, out var needsGpuFlush);
|
||||
var completed = !needsGpuFlush || ReadMemoryOrAwaitShutdown(address, size, isWrite: true,
|
||||
GuestMemoryProfile.ReadbackSource.CpuWriteInvalidation);
|
||||
if (GuestGpuMemoryHook.Traces(address, size))
|
||||
GuestGpuMemoryHook.Trace(address, size, $"buffer-write tracked={tracked} completed={completed}");
|
||||
return tracked && completed;
|
||||
|
|
@ -139,7 +138,12 @@ public sealed unsafe class GuestBufferCache : IGuestBufferStore, IDisposable
|
|||
TrySynchronizeCpuRead(address, size, GuestMemoryProfile.ReadbackSource.CpuReadSynchronization);
|
||||
|
||||
public bool TrySynchronizeCpuRead(ulong address, ulong size, GuestMemoryProfile.ReadbackSource source) =>
|
||||
!_tracker.HasGpuDirtyPages(address, size) || ReadMemoryOrAwaitShutdown(address, size, isWrite: false, source);
|
||||
!_tracker.MayHaveGpuDirtyPages(address, size) ||
|
||||
!_tracker.HasGpuDirtyPages(address, size) ||
|
||||
ReadMemoryOrAwaitShutdown(address, size, isWrite: false, source);
|
||||
|
||||
// Lock-free on the GPU queue thread; see GuestPageTracker.MayHaveGpuDirtyPages.
|
||||
public bool MayHaveGpuDirtyPages(ulong address, ulong size) => _tracker.MayHaveGpuDirtyPages(address, size);
|
||||
|
||||
// A CPU read fault: GPU-dirty pages download through the worker first.
|
||||
public bool DownloadToCpu(ulong address, ulong size)
|
||||
|
|
@ -235,6 +239,7 @@ public sealed unsafe class GuestBufferCache : IGuestBufferStore, IDisposable
|
|||
_ = SynchronizeBuffer(buffer, guestAddress, size, isWritten, isTexelBuffer, preserveCpuWriteHotPages: false);
|
||||
if (isWritten)
|
||||
{
|
||||
buffer.NoteGpuWrite();
|
||||
_gpuModifiedRanges.Add(guestAddress, size);
|
||||
}
|
||||
|
||||
|
|
@ -474,15 +479,44 @@ public sealed unsafe class GuestBufferCache : IGuestBufferStore, IDisposable
|
|||
public void ProcessFaultBuffer() => _faults.ProcessFaultBuffer();
|
||||
|
||||
// Uploads every mapped range before a BDA draw; the fault pass runs at the next collection.
|
||||
// Every device-address program prepares all GPU-mapped memory; visiting each registered
|
||||
// buffer per dispatch cost ~17 % of the Demon's Souls render thread. The same work is done
|
||||
// in two cheaper parts: the recency touch, a no-op after the first one in a retirement tick,
|
||||
// runs once per tick (and when the mapping changes; new buffers register with the current
|
||||
// tick), and uploads only visit buffers in blocks the tracker does not know to be clean.
|
||||
// Hot pages stay dirty and writable, so every preparation still re-uploads them.
|
||||
private ulong _bdaTouchTick = ulong.MaxValue;
|
||||
private ulong _bdaTouchMapping;
|
||||
|
||||
public void PrepareBda(IEnumerable<GuestSpan> mapped)
|
||||
{
|
||||
var traceAddress = GuestGpuMemoryHook.TraceAddress;
|
||||
var traceCovered = false;
|
||||
foreach (var span in mapped)
|
||||
if (traceAddress != 0)
|
||||
{
|
||||
if (traceAddress != 0 && GuestGpuMemoryHook.Traces(span.Address, span.Size))
|
||||
traceCovered = true;
|
||||
SynchronizeBuffersInRange(span.Address, span.Size);
|
||||
foreach (var span in mapped)
|
||||
{
|
||||
if (traceAddress != 0 && GuestGpuMemoryHook.Traces(span.Address, span.Size))
|
||||
traceCovered = true;
|
||||
SynchronizeBuffersInRange(span.Address, span.Size);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var spans = mapped as IReadOnlyCollection<GuestSpan> ?? mapped.ToList();
|
||||
var mapping = (ulong)spans.Count;
|
||||
foreach (var span in spans)
|
||||
mapping = (mapping ^ span.Address ^ (span.Size << 17)) * 0x100000001B3UL;
|
||||
if (_retirementPolicy.CurrentTick != _bdaTouchTick || mapping != _bdaTouchMapping)
|
||||
{
|
||||
foreach (var span in spans)
|
||||
TouchBuffersInRange(span.Address, span.Size);
|
||||
_bdaTouchTick = _retirementPolicy.CurrentTick;
|
||||
_bdaTouchMapping = mapping;
|
||||
}
|
||||
|
||||
foreach (var span in spans)
|
||||
_tracker.ForEachPossiblyCpuDirtyRange(span.Address, span.Size, _uploadDirtyBuffersInRange ??= UploadDirtyBuffersInRange);
|
||||
}
|
||||
|
||||
if (traceAddress != 0)
|
||||
|
|
@ -491,6 +525,31 @@ public sealed unsafe class GuestBufferCache : IGuestBufferStore, IDisposable
|
|||
_faultProcessPending = true;
|
||||
}
|
||||
|
||||
private Action<ulong, ulong>? _uploadDirtyBuffersInRange;
|
||||
|
||||
private void TouchBuffersInRange(ulong guestAddress, ulong size)
|
||||
{
|
||||
var end = guestAddress + size;
|
||||
var index = _registry.FindFirstOverlappingIndex(guestAddress);
|
||||
for (; index < _registry.RegisteredCount && _registry.GetRegisteredAddress(index) < end; index++)
|
||||
TouchBuffer(_registry.GetRegisteredIdentifier(index));
|
||||
}
|
||||
|
||||
// The upload half of SynchronizeBuffersInRange, for a range that may hold CPU-dirty pages.
|
||||
private void UploadDirtyBuffersInRange(ulong guestAddress, ulong size)
|
||||
{
|
||||
var end = guestAddress + size;
|
||||
var index = _registry.FindFirstOverlappingIndex(guestAddress);
|
||||
for (; index < _registry.RegisteredCount && _registry.GetRegisteredAddress(index) < end; index++)
|
||||
{
|
||||
var buffer = _registry.GetBuffer(_registry.GetRegisteredIdentifier(index));
|
||||
var start = Math.Max(buffer.CpuAddress, guestAddress);
|
||||
var finish = Math.Min(buffer.CpuAddress + buffer.Size, end);
|
||||
if (start < finish && _tracker.HasCpuDirtyPages(start, finish - start))
|
||||
_ = SynchronizeBuffer(buffer, start, finish - start, false, false, preserveCpuWriteHotPages: false);
|
||||
}
|
||||
}
|
||||
|
||||
public void SynchronizeBuffersInRange(ulong guestAddress, ulong size)
|
||||
{
|
||||
var end = guestAddress + size;
|
||||
|
|
@ -536,6 +595,7 @@ public sealed unsafe class GuestBufferCache : IGuestBufferStore, IDisposable
|
|||
|
||||
public void RunGarbageCollector()
|
||||
{
|
||||
using var foreignRead = _device.Slabs.BeginForeignRead();
|
||||
ProcessPendingFaultBuffer();
|
||||
if (!_retirementPolicy.TryBeginCollection(_registry.RegisteredBytes, out var retirement))
|
||||
{
|
||||
|
|
@ -604,6 +664,7 @@ public sealed unsafe class GuestBufferCache : IGuestBufferStore, IDisposable
|
|||
_scheduler.Finish();
|
||||
}
|
||||
|
||||
using var foreignRead = _device.Slabs.BeginForeignRead();
|
||||
var copies = new List<DownloadPiece>();
|
||||
var dirtyBuffers = new List<ResourceSlotIdentifier>();
|
||||
foreach (var bufferIdentifier in _registry.SnapshotRegisteredIdentifiers())
|
||||
|
|
@ -699,33 +760,14 @@ public sealed unsafe class GuestBufferCache : IGuestBufferStore, IDisposable
|
|||
private void ReadMemoryOnGpu(ulong guestAddress, ulong size, bool isWrite, GuestMemoryProfile.ReadbackSource source)
|
||||
{
|
||||
using var readbackScope = GuestMemoryProfile.Measure(GuestMemoryProfile.Operation.BufferReadback);
|
||||
using var foreignRead = _device.Slabs.BeginForeignRead();
|
||||
var readbackStarted = GuestMemoryProfile.ReadbackDetailsEnabled ? System.Diagnostics.Stopwatch.GetTimestamp() : 0;
|
||||
if (isWrite && !IsRegionRegistered(guestAddress, size))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var buffer = _registry.GetBuffer(FindBuffer(guestAddress, size));
|
||||
|
||||
// Widen nearby CPU reads so they share one GPU drain.
|
||||
const ulong windowSize = 512 * 1024;
|
||||
var bufferEnd = buffer.CpuAddress + buffer.Size;
|
||||
var windowBegin = Math.Max(guestAddress & ~(windowSize - 1), buffer.CpuAddress);
|
||||
var windowEnd = Math.Min(Math.Max(windowBegin + windowSize, guestAddress + size), bufferEnd);
|
||||
|
||||
var copies = new List<DownloadPiece>();
|
||||
_tracker.ForEachDownloadRange(
|
||||
windowBegin,
|
||||
windowEnd - windowBegin,
|
||||
clear: false,
|
||||
(address, bytes) => _tracker.ValidateGpuDirtyPages(_gpuModifiedRanges, address, bytes, "memory invalidation"),
|
||||
(address, bytes) =>
|
||||
{
|
||||
foreach (var range in _gpuModifiedRanges.GetOverlappingRanges(address, bytes))
|
||||
{
|
||||
copies.Add(new DownloadPiece(buffer, buffer.Offset(range.Address), range.Address, range.Size));
|
||||
}
|
||||
});
|
||||
var copies = CollectReadbackWindow(guestAddress, size, out var windowBegin, out var windowEnd);
|
||||
if (copies.Count != 0)
|
||||
{
|
||||
DownloadBufferMemory(copies);
|
||||
|
|
@ -746,6 +788,32 @@ public sealed unsafe class GuestBufferCache : IGuestBufferStore, IDisposable
|
|||
}
|
||||
}
|
||||
|
||||
// The GPU-modified ranges to download for a read of the range, widened to a window so
|
||||
// nearby CPU reads share one GPU drain.
|
||||
private List<DownloadPiece> CollectReadbackWindow(ulong guestAddress, ulong size, out ulong windowBegin, out ulong windowEnd)
|
||||
{
|
||||
var buffer = _registry.GetBuffer(FindBuffer(guestAddress, size));
|
||||
const ulong windowSize = 512 * 1024;
|
||||
var bufferEnd = buffer.CpuAddress + buffer.Size;
|
||||
windowBegin = Math.Max(guestAddress & ~(windowSize - 1), buffer.CpuAddress);
|
||||
windowEnd = Math.Min(Math.Max(windowBegin + windowSize, guestAddress + size), bufferEnd);
|
||||
|
||||
var copies = new List<DownloadPiece>();
|
||||
_tracker.ForEachDownloadRange(
|
||||
windowBegin,
|
||||
windowEnd - windowBegin,
|
||||
clear: false,
|
||||
(address, bytes) => _tracker.ValidateGpuDirtyPages(_gpuModifiedRanges, address, bytes, "memory invalidation"),
|
||||
(address, bytes) =>
|
||||
{
|
||||
foreach (var range in _gpuModifiedRanges.GetOverlappingRanges(address, bytes))
|
||||
{
|
||||
copies.Add(new DownloadPiece(buffer, buffer.Offset(range.Address), range.Address, range.Size));
|
||||
}
|
||||
});
|
||||
return copies;
|
||||
}
|
||||
|
||||
private void CollectDirtyPieces(GpuBuffer buffer, List<DownloadPiece> copies, string operation)
|
||||
{
|
||||
_tracker.ForEachDownloadRange(
|
||||
|
|
@ -852,9 +920,24 @@ public sealed unsafe class GuestBufferCache : IGuestBufferStore, IDisposable
|
|||
}
|
||||
}
|
||||
|
||||
// Set when a second queue can copy readbacks; null keeps every readback on the main queue.
|
||||
internal VulkanAsyncReadback? AsyncReadback { get; set; }
|
||||
|
||||
private const ulong AsyncReadbackLimit = 64UL << 20;
|
||||
|
||||
// Packs pieces into the download ring, waits for the copy, then writes each through the backing alias.
|
||||
private void DownloadBufferMemory(List<DownloadPiece> copies)
|
||||
{
|
||||
if (TryDownloadAsync(copies))
|
||||
{
|
||||
foreach (var copy in copies)
|
||||
{
|
||||
_gpuModifiedRanges.Remove(copy.Address, copy.Size);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
var batch = new List<PlannedDownload>();
|
||||
var planner = new BufferDownloadBatchPlanner(_download.Size);
|
||||
foreach (var piece in copies)
|
||||
|
|
@ -889,6 +972,58 @@ public sealed unsafe class GuestBufferCache : IGuestBufferStore, IDisposable
|
|||
}
|
||||
}
|
||||
|
||||
// Copies the pieces on the readback queue after only the tick that last wrote their
|
||||
// buffers, instead of appending the copy to the main queue and draining all of it.
|
||||
private bool TryDownloadAsync(List<DownloadPiece> copies)
|
||||
{
|
||||
if (AsyncReadback is not { } readback || copies.Count == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var waitTick = 0UL;
|
||||
var total = 0UL;
|
||||
foreach (var copy in copies)
|
||||
{
|
||||
var written = copy.Buffer.LastGpuWriteTick;
|
||||
// A GPU-modified range with no recorded writer: keep the conservative path.
|
||||
if (written == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
waitTick = Math.Max(waitTick, written);
|
||||
total += copy.Size;
|
||||
}
|
||||
|
||||
if (total > AsyncReadbackLimit)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// The last writer is still in the buffer being recorded; submit it (without
|
||||
// waiting) so the readback queue has a signal to wait for.
|
||||
if (waitTick >= _scheduler.CurrentTick)
|
||||
{
|
||||
_scheduler.Flush();
|
||||
}
|
||||
|
||||
var pieces = new ReadbackPiece[copies.Count];
|
||||
for (var index = 0; index < pieces.Length; index++)
|
||||
{
|
||||
pieces[index] = new ReadbackPiece(copies[index].Buffer, copies[index].SourceOffset, copies[index].Size);
|
||||
}
|
||||
|
||||
readback.Read(pieces, waitTick, (index, bytes) =>
|
||||
{
|
||||
if (!_backing.TryWriteBacking(copies[index].Address, bytes))
|
||||
{
|
||||
throw SubmissionScheduler.Fatal($"Could not write the required direct backing: addr=0x{copies[index].Address:X16} size=0x{(ulong)bytes.Length:X16}");
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
private void FlushDownloads(List<PlannedDownload> batch, ulong packedSize)
|
||||
{
|
||||
if (!_download.TryMap(packedSize, out var baseOffset, BufferDownloadBatchPlanner.Alignment))
|
||||
|
|
@ -978,7 +1113,7 @@ public sealed unsafe class GuestBufferCache : IGuestBufferStore, IDisposable
|
|||
|
||||
var bufferIdentifier = _registry.AllocateBuffer(new GpuBuffer(
|
||||
_device, _scheduler, GpuBufferUsage.DeviceLocal, overlap.Begin,
|
||||
GpuBuffer.AllFlags | BufferUsageFlags.ShaderDeviceAddressBit, overlap.End - overlap.Begin), overlap.Begin, overlap.End - overlap.Begin);
|
||||
GpuBuffer.AllFlags | BufferUsageFlags.ShaderDeviceAddressBit, overlap.End - overlap.Begin, allowSlab: true), overlap.Begin, overlap.End - overlap.Begin);
|
||||
foreach (var oldId in overlapping)
|
||||
{
|
||||
MergeOverlappingBuffer(bufferIdentifier, oldId, !overlap.HasStreamLeap);
|
||||
|
|
@ -1016,6 +1151,7 @@ public sealed unsafe class GuestBufferCache : IGuestBufferStore, IDisposable
|
|||
preserveCpuWriteHotPages);
|
||||
if (source != null)
|
||||
{
|
||||
buffer.NoteGpuWrite();
|
||||
var command = _scheduler.Current;
|
||||
command.EndRendering();
|
||||
var native = new CommandBuffer(command.Handle);
|
||||
|
|
@ -1059,7 +1195,13 @@ public sealed unsafe class GuestBufferCache : IGuestBufferStore, IDisposable
|
|||
|
||||
if (isTexelBuffer)
|
||||
{
|
||||
return RequireImageCache().TrySynchronizeBufferFromImage(buffer, guestAddress, size);
|
||||
var copiedFromImage = RequireImageCache().TrySynchronizeBufferFromImage(buffer, guestAddress, size);
|
||||
if (copiedFromImage)
|
||||
{
|
||||
buffer.NoteGpuWrite();
|
||||
}
|
||||
|
||||
return copiedFromImage;
|
||||
}
|
||||
|
||||
return false;
|
||||
|
|
|
|||
|
|
@ -197,14 +197,10 @@ public sealed class CommandStreamQueue
|
|||
Monitor.PulseAll(_gate);
|
||||
}
|
||||
|
||||
// Marks the frame boundary; from another thread it first waits for the queue to drain.
|
||||
// Marks the frame boundary and drains all pending GPU work before returning.
|
||||
public IdleOutcome Done()
|
||||
{
|
||||
var outcome = IdleOutcome.Completed;
|
||||
if (_processingThread != Thread.CurrentThread)
|
||||
{
|
||||
outcome = WaitForIdle();
|
||||
}
|
||||
var outcome = _processingThread == Thread.CurrentThread ? IdleOutcome.Completed : WaitForIdle();
|
||||
|
||||
lock (_gate)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -110,6 +110,8 @@ public sealed class CommandSubmission
|
|||
|
||||
public PacketCursorStack Commands { get; } = new();
|
||||
|
||||
// Its place in the order every queue was fed; the frame run-ahead waits by it.
|
||||
|
||||
public bool ResetInterpreter { get; internal set; }
|
||||
|
||||
public bool Started { get; internal set; }
|
||||
|
|
|
|||
|
|
@ -114,6 +114,18 @@ public sealed partial class GpuCommandInterpreter
|
|||
return;
|
||||
}
|
||||
|
||||
if (_host.ResolvesIndirectDrawOnGpu && IndexTypeAndSize is 0 or 1 && TryGetGpuIndexRange(out var gpuIndexCount))
|
||||
{
|
||||
// The host reads the five arguments on the GPU. It draws from the whole index
|
||||
// range; the arguments pick the indices. The host falls back to reading them
|
||||
// itself when the draw needs the counts.
|
||||
_deferredInstanceCountAddress = argumentsAddress + 4;
|
||||
_host.DrawIndexed(SubmitId, new DrawIndexedArguments(
|
||||
packetAddress, opcode, gpuIndexCount, IndexBaseAddress, IndexTypeAndSize, 1, 0, 0,
|
||||
DrawOffsetSource.IndirectArguments, argumentsAddress, UnboundedIndexBuffer: IndexBufferSize == 0));
|
||||
return;
|
||||
}
|
||||
|
||||
var indexCountPerInstance = ReadDword(argumentsAddress);
|
||||
var indexedInstanceCount = ReadDword(argumentsAddress + 4);
|
||||
var startIndex = ReadDword(argumentsAddress + 8);
|
||||
|
|
@ -122,9 +134,71 @@ public sealed partial class GpuCommandInterpreter
|
|||
var indexAddress = IndexBaseAddress + (startIndex * IndexElementSize);
|
||||
var indexCount = IndexBufferSize != 0 ? Math.Min(indexCountPerInstance, IndexBufferSize) : indexCountPerInstance;
|
||||
InstanceCount = indexedInstanceCount;
|
||||
if (IndexBufferSize == 0 && IndexTypeAndSize is 0 or 1)
|
||||
{
|
||||
LearnIndexExtent(startIndex, indexCountPerInstance);
|
||||
}
|
||||
|
||||
DrawIndexed(packetAddress, opcode, indexCount, indexAddress, indexedInstanceCount, unchecked((int)baseVertex), indexedStartInstance, DrawOffsetSource.IndirectArguments);
|
||||
}
|
||||
|
||||
// An index buffer without a size (INDEX_BUFFER_SIZE never set) has no bound the GPU
|
||||
// path could bind. The extent the draws from an index base have used so far, rounded
|
||||
// up to the end of its 16 KiB page, stands in for it; every 1024th such draw reads its
|
||||
// arguments on the CPU again so the extent can grow.
|
||||
private const ulong IndexExtentPageSize = 0x4000;
|
||||
private const ulong MaxIndexExtentBytes = 64ul << 20;
|
||||
private const int MaxIndexExtents = 4096;
|
||||
private readonly Dictionary<ulong, ulong> _indexExtents = new();
|
||||
private uint _indexExtentDraws;
|
||||
|
||||
private bool TryGetGpuIndexRange(out uint indexCount)
|
||||
{
|
||||
if (IndexBufferSize != 0)
|
||||
{
|
||||
indexCount = IndexBufferSize;
|
||||
return true;
|
||||
}
|
||||
|
||||
indexCount = 0;
|
||||
if ((++_indexExtentDraws & 1023) == 0 || !_indexExtents.TryGetValue(IndexBaseAddress, out var extentBytes))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
indexCount = (uint)(extentBytes / IndexElementSize);
|
||||
return indexCount != 0;
|
||||
}
|
||||
|
||||
private void LearnIndexExtent(uint startIndex, uint indexCount)
|
||||
{
|
||||
if (indexCount == 0 || IndexBaseAddress == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var end = IndexBaseAddress + (((ulong)startIndex + indexCount) * IndexElementSize);
|
||||
var extentBytes = ((end + IndexExtentPageSize - 1) & ~(IndexExtentPageSize - 1)) - IndexBaseAddress;
|
||||
if (extentBytes > MaxIndexExtentBytes)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (_indexExtents.TryGetValue(IndexBaseAddress, out var known))
|
||||
{
|
||||
if (known >= extentBytes)
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
else if (_indexExtents.Count >= MaxIndexExtents)
|
||||
{
|
||||
_indexExtents.Clear();
|
||||
}
|
||||
|
||||
_indexExtents[IndexBaseAddress] = extentBytes;
|
||||
}
|
||||
|
||||
internal void DispatchDirect(uint groupsX, uint groupsY, uint groupsZ, uint dispatchInitiator, ulong indirectArgumentsAddress = 0) =>
|
||||
_host.DispatchDirect(SubmitId, groupsX, groupsY, groupsZ, dispatchInitiator, indirectArgumentsAddress);
|
||||
|
||||
|
|
@ -135,7 +209,22 @@ public sealed partial class GpuCommandInterpreter
|
|||
throw _host.Fatal($"The indirect dispatch arguments base is zero: offset=0x{dataOffset:X}.");
|
||||
}
|
||||
|
||||
var argumentsAddress = DispatchIndirectArgumentsBase + dataOffset;
|
||||
DispatchIndirectAt(DispatchIndirectArgumentsBase + dataOffset, dispatchInitiator);
|
||||
}
|
||||
|
||||
private const uint DispatchInitiatorUseThreadDimensions = 1u << 5;
|
||||
|
||||
// Group counts written by an earlier shader are only in GPU memory; reading them
|
||||
// here waits for the GPU. When the host dispatches indirectly they stay there.
|
||||
// Thread-unit dispatches still need the counts to convert them to groups.
|
||||
private void DispatchIndirectAt(ulong argumentsAddress, uint dispatchInitiator)
|
||||
{
|
||||
if (_host.ResolvesIndirectDispatchOnGpu && (dispatchInitiator & DispatchInitiatorUseThreadDimensions) == 0)
|
||||
{
|
||||
DispatchDirect(0, 0, 0, dispatchInitiator, argumentsAddress);
|
||||
return;
|
||||
}
|
||||
|
||||
DispatchDirect(ReadDword(argumentsAddress), ReadDword(argumentsAddress + 4), ReadDword(argumentsAddress + 8), dispatchInitiator, argumentsAddress);
|
||||
}
|
||||
|
||||
|
|
@ -271,7 +360,7 @@ public sealed partial class GpuCommandInterpreter
|
|||
throw _host.Fatal($"The indirect dispatch arguments address is zero: address=0x{packet.PacketAddress:X16}.");
|
||||
}
|
||||
|
||||
DispatchDirect(ReadDword(argumentsAddress), ReadDword(argumentsAddress + 4), ReadDword(argumentsAddress + 8), payload[2], argumentsAddress);
|
||||
DispatchIndirectAt(argumentsAddress, payload[2]);
|
||||
return 3;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -260,6 +260,23 @@ public sealed partial class GpuCommandInterpreter
|
|||
}
|
||||
}
|
||||
|
||||
// Release-memory completions stay in the recording command buffer instead of
|
||||
// submitting it at every packet: labels are already written to guest memory,
|
||||
// only the interrupt waits for the GPU, and RunSlice submits the buffer when
|
||||
// the slice ends or blocks, so every deferred interrupt still reaches the GPU.
|
||||
// Submitting per packet produced ~430 tiny submissions per Astro Bot frame.
|
||||
// SHARPEMU_DEFER_RELEASE_FLUSH=0 restores the per-packet submit.
|
||||
private static readonly bool DeferReleaseFlush = !string.Equals(
|
||||
Environment.GetEnvironmentVariable("SHARPEMU_DEFER_RELEASE_FLUSH"), "0", StringComparison.Ordinal);
|
||||
|
||||
private void FlushReleaseMemory()
|
||||
{
|
||||
if (!DeferReleaseFlush)
|
||||
{
|
||||
_host.Flush();
|
||||
}
|
||||
}
|
||||
|
||||
internal void QueueInterrupt(uint interruptContextId) =>
|
||||
_host.RecordEndOfPipe(new EndOfPipeWrite(EndOfPipeWriteKind.InterruptOnly, SubmitId, EventId: InterruptEventId, ContextId: interruptContextId));
|
||||
|
||||
|
|
@ -327,7 +344,7 @@ public sealed partial class GpuCommandInterpreter
|
|||
if (dataSelection == 1)
|
||||
{
|
||||
WriteEndOfPipe(false, cachePolicy, 0, 0x2F, cacheAction, 6, 2, destination, (uint)value, interruptSelector, interruptContextId);
|
||||
_host.Flush();
|
||||
FlushReleaseMemory();
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -341,7 +358,7 @@ public sealed partial class GpuCommandInterpreter
|
|||
WriteEndOfPipe(false, cachePolicy, 0, 0x2F, cacheAction, 6, 1, destination, (uint)value, interruptSelector, interruptContextId);
|
||||
if (interruptSelector == 1)
|
||||
{
|
||||
_host.Flush();
|
||||
FlushReleaseMemory();
|
||||
}
|
||||
|
||||
return;
|
||||
|
|
@ -371,7 +388,7 @@ public sealed partial class GpuCommandInterpreter
|
|||
case 2:
|
||||
case 4:
|
||||
QueueInterrupt(interruptContextId);
|
||||
_host.Flush();
|
||||
FlushReleaseMemory();
|
||||
return;
|
||||
default:
|
||||
throw _host.Fatal($"The release-memory interrupt selector is unknown: selector={interruptSelector}.");
|
||||
|
|
|
|||
|
|
@ -64,8 +64,30 @@ public sealed partial class GpuCommandInterpreter
|
|||
|
||||
public ulong DispatchIndirectArgumentsBase { get; private set; }
|
||||
|
||||
// Persistent draw state: indirect draws update it for later draws.
|
||||
public uint InstanceCount { get; private set; } = 1;
|
||||
// Persistent draw state: indirect draws update it for later draws. An indirect draw
|
||||
// resolved on the GPU leaves the count in guest memory; it is read only if a later
|
||||
// draw needs it.
|
||||
public uint InstanceCount
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_deferredInstanceCountAddress != 0)
|
||||
{
|
||||
_instanceCount = ReadDword(_deferredInstanceCountAddress);
|
||||
_deferredInstanceCountAddress = 0;
|
||||
}
|
||||
|
||||
return _instanceCount;
|
||||
}
|
||||
private set
|
||||
{
|
||||
_instanceCount = value;
|
||||
_deferredInstanceCountAddress = 0;
|
||||
}
|
||||
}
|
||||
|
||||
private uint _instanceCount = 1;
|
||||
private ulong _deferredInstanceCountAddress;
|
||||
|
||||
public uint DrawIndexOffset { get; private set; }
|
||||
|
||||
|
|
|
|||
|
|
@ -21,7 +21,14 @@ public readonly record struct DrawIndexedArguments(
|
|||
uint InstanceCount,
|
||||
int BaseVertex,
|
||||
uint FirstInstance,
|
||||
DrawOffsetSource OffsetSource);
|
||||
DrawOffsetSource OffsetSource,
|
||||
// Nonzero when the counts are still in guest memory at this address, laid out as
|
||||
// (indexCount, instanceCount, firstIndex, vertexOffset, firstInstance). IndexCount is
|
||||
// then the index buffer size and IndexAddress its base.
|
||||
ulong IndirectArgumentsAddress = 0,
|
||||
// The guest set no index buffer size: IndexCount is only the range seen so far and
|
||||
// must not clamp the count read from the arguments.
|
||||
bool UnboundedIndexBuffer = false);
|
||||
|
||||
// Arguments of a non-indexed draw as the packet stream describes them.
|
||||
public readonly record struct DrawAutoArguments(
|
||||
|
|
@ -121,6 +128,14 @@ public interface ICommandStreamHost
|
|||
|
||||
void DispatchDirect(ulong submitId, uint groupsX, uint groupsY, uint groupsZ, uint dispatchInitiator, ulong indirectArgumentsAddress = 0);
|
||||
|
||||
// True when an indirect dispatch in workgroup units reads its group counts on the
|
||||
// GPU, so the interpreter need not wait for the GPU to read them back first.
|
||||
bool ResolvesIndirectDispatchOnGpu => false;
|
||||
|
||||
// True when an indexed indirect draw may be handed over with its arguments still in
|
||||
// guest memory (DrawIndexedArguments.IndirectArgumentsAddress).
|
||||
bool ResolvesIndirectDrawOnGpu => false;
|
||||
|
||||
// Called when a queue reset packet clears the processor.
|
||||
void OnQueueReset(int queueId);
|
||||
|
||||
|
|
|
|||
|
|
@ -36,6 +36,12 @@ public sealed unsafe partial class CachedImage
|
|||
{
|
||||
var barriers = new List<ImageMemoryBarrier2>();
|
||||
PipelineStageFlags sourceStages = 0;
|
||||
if ((access & (WriteAccess | AccessFlags.ColorAttachmentWriteBit | AccessFlags.DepthStencilAttachmentWriteBit)) != 0)
|
||||
{
|
||||
// Any GPU write, the guest upload included, leaves contents the piece hashes do not describe.
|
||||
_guestPieceHashes = null;
|
||||
}
|
||||
|
||||
if (range is { } && Description.IsVolume)
|
||||
{
|
||||
range = range.Value with { BaseLayer = 0, LayerCount = 1 };
|
||||
|
|
|
|||
|
|
@ -62,6 +62,8 @@ public sealed unsafe partial class CachedImage : IDisposable
|
|||
private readonly GpuDeviceInfo _device;
|
||||
private readonly SubmissionScheduler _scheduler;
|
||||
private readonly IGuestBackedSpace _guestBacking;
|
||||
private readonly ImageBackingPool? _pool;
|
||||
private ImageBackingPool.Key _poolKey;
|
||||
private ulong _maybeCpuHash;
|
||||
private bool _cpuDirty;
|
||||
private bool _maybeCpuDirty;
|
||||
|
|
@ -83,11 +85,13 @@ public sealed unsafe partial class CachedImage : IDisposable
|
|||
public ulong LastAccessTick;
|
||||
public int RecencyEntryIndex;
|
||||
|
||||
public CachedImage(GpuDeviceInfo device, SubmissionScheduler scheduler, IGuestBackedSpace guestBacking, in ImageDescription description)
|
||||
public CachedImage(GpuDeviceInfo device, SubmissionScheduler scheduler, IGuestBackedSpace guestBacking, in ImageDescription description,
|
||||
ImageBackingPool? pool = null)
|
||||
{
|
||||
_device = device;
|
||||
_scheduler = scheduler;
|
||||
_guestBacking = guestBacking;
|
||||
_pool = pool;
|
||||
Description = description;
|
||||
Description.Validate();
|
||||
_cpuDirty = !ImageDescription.IsEmptyRange(Description.Data) && Description.Metadata.Compression == DisplayCompression.Uncompressed;
|
||||
|
|
@ -130,6 +134,12 @@ public sealed unsafe partial class CachedImage : IDisposable
|
|||
Backing.Flags = create.Flags;
|
||||
Backing.Usage = create.Usage;
|
||||
|
||||
_poolKey = ImageBackingPool.KeyOf(create);
|
||||
if (_pool is not null && _pool.TryTake(_poolKey, out Backing.Handle, out Backing.Memory, out Backing.AllocationSize))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var vk = device.Vk;
|
||||
var createResult = vk.CreateImage(device.Device, &create, null, out Backing.Handle);
|
||||
if (createResult != Result.Success)
|
||||
|
|
@ -199,6 +209,21 @@ public sealed unsafe partial class CachedImage : IDisposable
|
|||
return true;
|
||||
}
|
||||
|
||||
// Some drivers (AMDVLK) refuse storage usage on block-compressed images even
|
||||
// with extended usage. Storage writes to such an image go through an
|
||||
// uncompressed replacement instead (GuestImageCache.ReplaceCompressedForStorage).
|
||||
if ((configuration.Flags & ImageCreateFlags.CreateBlockTexelViewCompatibleBit) != 0 &&
|
||||
(configuration.Usage & ImageUsageFlags.StorageBit) != 0)
|
||||
{
|
||||
var withoutStorage = configuration;
|
||||
withoutStorage.Usage &= ~ImageUsageFlags.StorageBit;
|
||||
if (SupportsImageConfiguration(device, withoutStorage))
|
||||
{
|
||||
configuration = withoutStorage;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!allowCompressedImageFallback || (configuration.Flags & ImageCreateFlags.CreateBlockTexelViewCompatibleBit) == 0)
|
||||
{
|
||||
return false;
|
||||
|
|
@ -274,7 +299,18 @@ public sealed unsafe partial class CachedImage : IDisposable
|
|||
usage |= ImageUsageFlags.ColorAttachmentBit;
|
||||
}
|
||||
|
||||
var storageFormat = ViewFormatRules.SrgbStorageFormat(description.PixelFormat);
|
||||
// Compressed images are written by compute shaders (GPU texture
|
||||
// encoders) through an uncompressed block view; with extended usage the
|
||||
// image may carry storage usage when that block view format supports it.
|
||||
// TrySelectSupportedImageConfiguration drops it again if the driver refuses.
|
||||
var storageFormat = GuestPixelFormats.BlockCompressedBytes(description.GuestFormat) != 0
|
||||
? ViewFormatRules.BlockBytes(description.PixelFormat) switch
|
||||
{
|
||||
8 => Format.R32G32Uint,
|
||||
16 => Format.R32G32B32A32Uint,
|
||||
_ => Format.Undefined,
|
||||
}
|
||||
: ViewFormatRules.SrgbStorageFormat(description.PixelFormat);
|
||||
var storageFeatures = storageFormat == Format.Undefined ? features : device.GetFormatProperties(storageFormat).OptimalTilingFeatures;
|
||||
if (description.Samples == 1 && (storageFeatures & FormatFeatureFlags.StorageImageBit) != 0)
|
||||
{
|
||||
|
|
@ -363,12 +399,26 @@ public sealed unsafe partial class CachedImage : IDisposable
|
|||
LastCpuWriteSize = 0;
|
||||
}
|
||||
|
||||
// Guest-byte hashes of each tile transfer piece at the last upload from guest memory; null
|
||||
// once the image holds anything else, so a refresh falls back to a full upload.
|
||||
private ulong[]? _guestPieceHashes;
|
||||
private GuestSpan _guestPieceRange;
|
||||
|
||||
internal ulong[]? GuestPieceHashes => _guestPieceHashes != null && _guestPieceRange == Description.Data ? _guestPieceHashes : null;
|
||||
|
||||
internal void SetGuestPieceHashes(ulong[]? hashes)
|
||||
{
|
||||
_guestPieceHashes = hashes;
|
||||
_guestPieceRange = Description.Data;
|
||||
}
|
||||
|
||||
public bool IsGpuModified => _gpuModified;
|
||||
|
||||
public void MarkGpuModified()
|
||||
{
|
||||
_gpuModified = true;
|
||||
_bufferHoldsGpuContents = false;
|
||||
_guestPieceHashes = null;
|
||||
}
|
||||
|
||||
public void ClearGpuModified() => _gpuModified = false;
|
||||
|
|
@ -509,9 +559,15 @@ public sealed unsafe partial class CachedImage : IDisposable
|
|||
}
|
||||
}
|
||||
|
||||
var minLod = new ImageViewMinLodCreateInfoEXT
|
||||
{
|
||||
SType = StructureType.ImageViewMinLodCreateInfoExt,
|
||||
MinLod = normalized.MinLod,
|
||||
};
|
||||
var usage = new ImageViewUsageCreateInfo
|
||||
{
|
||||
SType = StructureType.ImageViewUsageCreateInfo,
|
||||
PNext = normalized.MinLod > 0 && _device.ImageViewMinLodSupported ? &minLod : null,
|
||||
Usage = isStorage ? image.Usage : image.Usage & ~ImageUsageFlags.StorageBit,
|
||||
};
|
||||
var create = new ImageViewCreateInfo
|
||||
|
|
@ -547,8 +603,12 @@ public sealed unsafe partial class CachedImage : IDisposable
|
|||
Views.Clear();
|
||||
if (Backing.Exists)
|
||||
{
|
||||
_device.Vk.DestroyImage(_device.Device, Backing.Handle, null);
|
||||
_device.FreeMemory(Backing.Memory);
|
||||
if (_pool is null || !_pool.TryReturn(_poolKey, Backing.Handle, Backing.Memory, Backing.AllocationSize))
|
||||
{
|
||||
_device.Vk.DestroyImage(_device.Device, Backing.Handle, null);
|
||||
_device.FreeMemory(Backing.Memory);
|
||||
}
|
||||
|
||||
Backing.Handle = default;
|
||||
Backing.Memory = default;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Numerics;
|
||||
using System.Runtime.InteropServices;
|
||||
using SharpEmu.Libs.Gpu.Buffers;
|
||||
using SharpEmu.Libs.Gpu.Scheduling;
|
||||
|
|
@ -146,6 +147,21 @@ public sealed unsafe class GpuTiler : IDisposable
|
|||
DestroyPipeline(_d32ToD16);
|
||||
DestroyPipeline(_swapBgra16);
|
||||
_pools.Dispose();
|
||||
lock (_scratchGate)
|
||||
{
|
||||
_scratchDisposed = true;
|
||||
foreach (var pooled in _scratchPool.Values)
|
||||
{
|
||||
foreach (var buffer in pooled)
|
||||
{
|
||||
buffer.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
_scratchPool.Clear();
|
||||
_scratchPooledBytes = 0;
|
||||
}
|
||||
|
||||
vk.DestroyPipelineLayout(_device.Device, _pipelineLayout, null);
|
||||
vk.DestroyDescriptorSetLayout(_device.Device, _descriptorLayout, null);
|
||||
_pipelineLayout = default;
|
||||
|
|
@ -170,20 +186,69 @@ public sealed unsafe class GpuTiler : IDisposable
|
|||
|
||||
private ulong StorageAlignment => Math.Max(_device.MinStorageBufferOffsetAlignment, 4);
|
||||
|
||||
// The scratch stays alive through the current tick; a completion action frees it.
|
||||
private GpuBuffer AllocateScratch(ulong size)
|
||||
// Image uploads and downloads that tile or swap need a scratch every time, several
|
||||
// per frame; creating and allocating a Vulkan buffer each time cost ~4 ms per frame.
|
||||
// Scratches are pooled by power-of-two capacity and return to the pool when their tick
|
||||
// completes, so the GPU is done with them before anyone else records into them.
|
||||
private const int ScratchMinimumShift = 16;
|
||||
private const int ScratchBuffersPerSize = 8;
|
||||
private const ulong ScratchPoolBudget = 512UL << 20;
|
||||
private readonly object _scratchGate = new();
|
||||
private readonly Dictionary<int, Stack<GpuBuffer>> _scratchPool = new();
|
||||
private ulong _scratchPooledBytes;
|
||||
|
||||
// The scratch stays alive through the current tick; the caller sees exactly `size` bytes.
|
||||
private TilerBufferSpan AllocateScratch(ulong size)
|
||||
{
|
||||
if (size == 0)
|
||||
{
|
||||
throw SubmissionScheduler.Fatal("The tiler scratch size is zero.");
|
||||
}
|
||||
|
||||
var buffer = new GpuBuffer(_device, _scheduler, GpuBufferUsage.DeviceLocal, 0,
|
||||
BufferUsageFlags.StorageBufferBit | BufferUsageFlags.TransferSrcBit | BufferUsageFlags.TransferDstBit, size);
|
||||
_scheduler.QueueCompletionAction(buffer.Dispose);
|
||||
return buffer;
|
||||
var shift = Math.Max(ScratchMinimumShift, 64 - BitOperations.LeadingZeroCount(size - 1));
|
||||
GpuBuffer? buffer = null;
|
||||
lock (_scratchGate)
|
||||
{
|
||||
if (_scratchPool.TryGetValue(shift, out var pooled) && pooled.TryPop(out var reused))
|
||||
{
|
||||
buffer = reused;
|
||||
_scratchPooledBytes -= buffer.Size;
|
||||
}
|
||||
}
|
||||
|
||||
buffer ??= new GpuBuffer(_device, _scheduler, GpuBufferUsage.DeviceLocal, 0,
|
||||
BufferUsageFlags.StorageBufferBit | BufferUsageFlags.TransferSrcBit | BufferUsageFlags.TransferDstBit,
|
||||
shift >= 63 ? size : 1UL << shift);
|
||||
_scheduler.QueueCompletionAction(() => ReturnScratch(shift, buffer));
|
||||
return new TilerBufferSpan(buffer.Handle, 0, size);
|
||||
}
|
||||
|
||||
private void ReturnScratch(int shift, GpuBuffer buffer)
|
||||
{
|
||||
lock (_scratchGate)
|
||||
{
|
||||
if (!_scratchDisposed && _scratchPooledBytes + buffer.Size <= ScratchPoolBudget)
|
||||
{
|
||||
if (!_scratchPool.TryGetValue(shift, out var pooled))
|
||||
{
|
||||
pooled = new Stack<GpuBuffer>();
|
||||
_scratchPool[shift] = pooled;
|
||||
}
|
||||
|
||||
if (pooled.Count < ScratchBuffersPerSize)
|
||||
{
|
||||
pooled.Push(buffer);
|
||||
_scratchPooledBytes += buffer.Size;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
buffer.Dispose();
|
||||
}
|
||||
|
||||
private bool _scratchDisposed;
|
||||
|
||||
private Pipeline CreateComputePipeline(byte[] spirv, string operation)
|
||||
{
|
||||
var vk = _device.Vk;
|
||||
|
|
@ -537,8 +602,8 @@ public sealed unsafe class GpuTiler : IDisposable
|
|||
var dispatches = new List<TransferDispatch>();
|
||||
Prepare(false, tiledCapacity, linearCapacity, transfers, sourceBase, 0, dispatches);
|
||||
var scratch = AllocateScratch((linearCapacity + 3) & ~3UL);
|
||||
Record(false, tiled, tiledOffset, tiledCapacity, scratch.Handle, 0, scratch.Size, dispatches, true);
|
||||
return new TilerBufferSpan(scratch.Handle, 0, linearCapacity);
|
||||
Record(false, tiled, tiledOffset, tiledCapacity, scratch.Buffer, 0, scratch.Size, dispatches, true);
|
||||
return new TilerBufferSpan(scratch.Buffer, 0, linearCapacity);
|
||||
}
|
||||
|
||||
public void Tile(VkBuffer linear, ulong linearOffset, ulong linearCapacity, VkBuffer tiled, ulong tiledOffset, ulong tiledCapacity, ReadOnlySpan<TileTransfer> transfers)
|
||||
|
|
@ -562,8 +627,8 @@ public sealed unsafe class GpuTiler : IDisposable
|
|||
var dispatches = new List<TransferDispatch>();
|
||||
Prepare(true, tiledCapacity, linearCapacity, transfers, 0, targetBase, dispatches);
|
||||
var linear = AllocateScratch((linearCapacity + 3) & ~3UL);
|
||||
image.DownloadToBuffer(regions, linear.Handle, 0, linear.Size);
|
||||
var source = new TilerBufferSpan(linear.Handle, 0, linear.Size);
|
||||
image.DownloadToBuffer(regions, linear.Buffer, 0, linear.Size);
|
||||
var source = linear;
|
||||
if (swap == ColorChannelSwap.SwapBgra16)
|
||||
{
|
||||
source = SwapBgra16(source);
|
||||
|
|
@ -572,11 +637,7 @@ public sealed unsafe class GpuTiler : IDisposable
|
|||
Record(true, source.Buffer, source.Offset, linearCapacity, tiled, tiledOffset, tiledCapacity, dispatches, false);
|
||||
}
|
||||
|
||||
public TilerBufferSpan GetScratchBuffer(ulong size)
|
||||
{
|
||||
var scratch = AllocateScratch((size + 3) & ~3UL);
|
||||
return new TilerBufferSpan(scratch.Handle, 0, scratch.Size);
|
||||
}
|
||||
public TilerBufferSpan GetScratchBuffer(ulong size) => AllocateScratch((size + 3) & ~3UL);
|
||||
|
||||
private StorageBinding BindStorage(TilerBufferSpan buffer, ulong size)
|
||||
{
|
||||
|
|
@ -784,8 +845,7 @@ public sealed unsafe class GpuTiler : IDisposable
|
|||
throw SubmissionScheduler.Fatal($"The BGRA16 swap input size is invalid: size={input.Size}.");
|
||||
}
|
||||
|
||||
var output = AllocateScratch(input.Size);
|
||||
var result = new TilerBufferSpan(output.Handle, 0, output.Size);
|
||||
var result = AllocateScratch(input.Size);
|
||||
SwapBgra16(input, result, (uint)(input.Size / 8));
|
||||
return result;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -566,6 +566,26 @@ public sealed partial class GuestImageCache
|
|||
return expandedImageIdentifier;
|
||||
}
|
||||
|
||||
// A compute shader writes the blocks of a compressed image through an
|
||||
// uncompressed view, but the driver could not give that image storage usage.
|
||||
// The blocks move to an uncompressed image of the same memory; the next
|
||||
// compressed request copies them back (the IsBlock branch of ResolveOverlap).
|
||||
private ResourceSlotIdentifier ReplaceCompressedForStorage(in ImageDescription requested, ResourceSlotIdentifier cachedImageIdentifier)
|
||||
{
|
||||
var replacementImageIdentifier = InsertImage(requested);
|
||||
var replacement = _slots[replacementImageIdentifier];
|
||||
var cached = _slots[cachedImageIdentifier];
|
||||
replacement.Uses = cached.Uses;
|
||||
if (cached.Binding.IsBound || cached.Binding.IsTarget)
|
||||
{
|
||||
cached.Binding.NeedsRebind = true;
|
||||
}
|
||||
|
||||
CopyWholeImage(replacementImageIdentifier, cachedImageIdentifier);
|
||||
ReleaseImage(cachedImageIdentifier);
|
||||
return replacementImageIdentifier;
|
||||
}
|
||||
|
||||
private void AssociateStencilRange(ResourceSlotIdentifier depthImageIdentifier, GuestSpan stencil)
|
||||
{
|
||||
if (!ImageDescription.IsValidRange(stencil))
|
||||
|
|
|
|||
|
|
@ -0,0 +1,156 @@
|
|||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.IO.Hashing;
|
||||
using SharpEmu.HLE.GpuMemory;
|
||||
using SharpEmu.Libs.Kernel;
|
||||
using Silk.NET.Vulkan;
|
||||
|
||||
namespace SharpEmu.Libs.Gpu.Images;
|
||||
|
||||
// Large layered textures are pools the game streams into one slot at a time. One CPU write
|
||||
// makes the whole image dirty, and re-uploading a 256-layer BC3 array (~350 MB) cost
|
||||
// ~570 ms of GPU detiling from host memory in Demon's Souls. Each (level, layer) piece's
|
||||
// guest bytes are hashed before an upload, so a later refresh transfers only the pieces
|
||||
// whose bytes changed.
|
||||
public sealed partial class GuestImageCache
|
||||
{
|
||||
private const ulong PieceHashMinimumImageSize = 16UL << 20;
|
||||
private const int PieceHashMinimumPieces = 16;
|
||||
private const ulong PieceRunMergeGap = 1UL << 20;
|
||||
private const int PieceHashChunkBytes = 1 << 20;
|
||||
private const ulong PieceLinearAlignment = 256;
|
||||
|
||||
// Hashes are only worth their cost on large, many-piece textures uploaded from guest memory.
|
||||
private static ColorTransferPlan? PieceHashPlan(CachedImage image, in ImageRequest request)
|
||||
{
|
||||
ref readonly var info = ref image.Description;
|
||||
if (request.Role is not (ImageRole.Texture or ImageRole.StorageImage) || info.IsVolume || info.IsDepth ||
|
||||
info.Data.Size < PieceHashMinimumImageSize)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var plan = PlanColorTransfer(image, request.Role, TransferDirection.Upload);
|
||||
if (!plan.Valid || !plan.Tiled || plan.SwapBgra16 || plan.Tiles.Count < PieceHashMinimumPieces || plan.Tiles.Count != plan.Regions.Count)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return plan;
|
||||
}
|
||||
|
||||
// Null when a piece's guest bytes cannot be read; the caller then keeps no hashes.
|
||||
private ulong[]? HashGuestPieces(in GuestSpan data, List<TileTransfer> tiles)
|
||||
{
|
||||
var hashes = new ulong[tiles.Count];
|
||||
var failed = 0;
|
||||
var options = new ParallelOptions { MaxDegreeOfParallelism = Math.Max(1, Environment.ProcessorCount / 2) };
|
||||
var address = data.Address;
|
||||
Parallel.For(0, tiles.Count, options,
|
||||
() => new byte[PieceHashChunkBytes],
|
||||
(index, state, chunk) =>
|
||||
{
|
||||
var tile = tiles[index];
|
||||
var hasher = new XxHash3();
|
||||
for (ulong done = 0; done < tile.TiledSize;)
|
||||
{
|
||||
var length = (int)Math.Min((ulong)chunk.Length, tile.TiledSize - done);
|
||||
var span = chunk.AsSpan(0, length);
|
||||
var pieceAddress = address + tile.TiledOffset + done;
|
||||
if (!_backing.TryReadBacking(pieceAddress, span) && !KernelMemoryCompatExports.TryReadPrtBacking(_backing, pieceAddress, span))
|
||||
{
|
||||
Interlocked.Exchange(ref failed, 1);
|
||||
state.Stop();
|
||||
return chunk;
|
||||
}
|
||||
|
||||
hasher.Append(span);
|
||||
done += (ulong)length;
|
||||
}
|
||||
|
||||
hashes[index] = hasher.GetCurrentHashAsUInt64();
|
||||
return chunk;
|
||||
},
|
||||
_ => { });
|
||||
return failed != 0 ? null : hashes;
|
||||
}
|
||||
|
||||
// True when the refresh was handled by transferring only the changed pieces.
|
||||
private bool TryUploadChangedPieces(CachedImage image, in ImageRequest request)
|
||||
{
|
||||
var previous = image.GuestPieceHashes;
|
||||
if (previous == null || image.IsBufferModified || image.IsGpuModified)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
ref readonly var info = ref image.Description;
|
||||
var plan = PieceHashPlan(image, request);
|
||||
if (plan == null || plan.Tiles.Count != previous.Length || _bufferCache.HasGpuDirtyBytes(info.Data.Address, info.Data.Size))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Hash before staging: a write racing the copy then shows up as a changed piece next time.
|
||||
var hashes = HashGuestPieces(info.Data, plan.Tiles);
|
||||
if (hashes == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var changed = new List<int>();
|
||||
for (var index = 0; index < hashes.Length; index++)
|
||||
{
|
||||
if (hashes[index] != previous[index])
|
||||
{
|
||||
changed.Add(index);
|
||||
}
|
||||
}
|
||||
|
||||
changed.Sort((left, right) => plan.Tiles[left].TiledOffset.CompareTo(plan.Tiles[right].TiledOffset));
|
||||
for (var start = 0; start < changed.Count;)
|
||||
{
|
||||
var runBegin = plan.Tiles[changed[start]].TiledOffset;
|
||||
var runEnd = runBegin + plan.Tiles[changed[start]].TiledSize;
|
||||
var end = start + 1;
|
||||
while (end < changed.Count && plan.Tiles[changed[end]].TiledOffset <= runEnd + PieceRunMergeGap)
|
||||
{
|
||||
var tile = plan.Tiles[changed[end]];
|
||||
runEnd = Math.Max(runEnd, tile.TiledOffset + tile.TiledSize);
|
||||
end++;
|
||||
}
|
||||
|
||||
UploadPieceRun(image, plan, changed.GetRange(start, end - start), runBegin, runEnd);
|
||||
start = end;
|
||||
}
|
||||
|
||||
image.SetGuestPieceHashes(hashes);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Stages one contiguous tiled range and detiles its pieces into a compact linear scratch.
|
||||
private void UploadPieceRun(CachedImage image, ColorTransferPlan plan, List<int> pieces, ulong runBegin, ulong runEnd)
|
||||
{
|
||||
var tiles = new List<TileTransfer>(pieces.Count);
|
||||
var regions = new List<BufferImageCopy>(pieces.Count);
|
||||
ulong linear = 0;
|
||||
foreach (var index in pieces)
|
||||
{
|
||||
linear = (linear + PieceLinearAlignment - 1) & ~(PieceLinearAlignment - 1);
|
||||
var tile = plan.Tiles[index];
|
||||
tile.TiledOffset -= runBegin;
|
||||
tile.LinearOffset = linear;
|
||||
var region = plan.Regions[index];
|
||||
region.BufferOffset = linear;
|
||||
tiles.Add(tile);
|
||||
regions.Add(region);
|
||||
linear += tile.LinearSize;
|
||||
}
|
||||
|
||||
var runSize = runEnd - runBegin;
|
||||
var (source, sourceOffset) = _bufferCache.ObtainBufferForImage(image.Description.Data.Address + runBegin, runSize);
|
||||
var detiled = _tiler.Detile(source.Handle, sourceOffset, runSize, linear, System.Runtime.InteropServices.CollectionsMarshal.AsSpan(tiles));
|
||||
UploadRegions(image, regions, detiled);
|
||||
}
|
||||
}
|
||||
|
|
@ -368,10 +368,40 @@ public sealed partial class GuestImageCache
|
|||
return false;
|
||||
}
|
||||
|
||||
// Most CPU writes (AGC command building, labels) touch no image page. Checking
|
||||
// the page owners without the lock keeps those writes from spinning behind the
|
||||
// render thread, which holds the lock for most of a frame. The check sees the
|
||||
// same state a locked call made before the write would.
|
||||
if (!MayOwnPages(address, size))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
using var held = _lock.Hold();
|
||||
return InvalidateAliases(address, size);
|
||||
}
|
||||
|
||||
private const ulong MaxLockFreeOwnerPages = 64;
|
||||
|
||||
private bool MayOwnPages(ulong address, ulong size)
|
||||
{
|
||||
if (!ImagePageOwnerTable.TryGetPageRange(address, size, out var first, out var lastExclusive) ||
|
||||
lastExclusive - first > MaxLockFreeOwnerPages)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
for (var page = first; page < lastExclusive; page++)
|
||||
{
|
||||
if (_pageOwners.Find(page) is { IsEmpty: false })
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public void InvalidateMemory(ulong address, ulong size)
|
||||
{
|
||||
GpuMemoryAccessProfile.CountImageCpuWrite();
|
||||
|
|
|
|||
|
|
@ -349,10 +349,25 @@ public sealed unsafe partial class GuestImageCache
|
|||
? (image.IsCpuDirty ? "buffer-and-cpu-dirty" : "buffer-dirty")
|
||||
: (image.IsMaybeCpuDirty ? "maybe-cpu-dirty" : "cpu-dirty");
|
||||
var sourceStarted = measureUpload ? System.Diagnostics.Stopwatch.GetTimestamp() : 0;
|
||||
var (source, sourceOffset) = _bufferCache.ObtainBufferForImage(image.Description.Data.Address, image.Description.Data.Size);
|
||||
var sourceFinished = measureUpload ? System.Diagnostics.Stopwatch.GetTimestamp() : 0;
|
||||
dataImported = true;
|
||||
UploadFromBuffer(image, request, source, sourceOffset);
|
||||
long sourceFinished;
|
||||
if (TryUploadChangedPieces(image, request))
|
||||
{
|
||||
sourceFinished = measureUpload ? System.Diagnostics.Stopwatch.GetTimestamp() : 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Hash before staging: a write racing the copy then shows up as a changed piece next time.
|
||||
var piecePlan = !image.IsBufferModified && !_bufferCache.HasGpuDirtyBytes(image.Description.Data.Address, image.Description.Data.Size)
|
||||
? PieceHashPlan(image, request)
|
||||
: null;
|
||||
var pieceHashes = piecePlan != null ? HashGuestPieces(image.Description.Data, piecePlan.Tiles) : null;
|
||||
var (source, sourceOffset) = _bufferCache.ObtainBufferForImage(image.Description.Data.Address, image.Description.Data.Size);
|
||||
sourceFinished = measureUpload ? System.Diagnostics.Stopwatch.GetTimestamp() : 0;
|
||||
UploadFromBuffer(image, request, source, sourceOffset);
|
||||
image.SetGuestPieceHashes(pieceHashes);
|
||||
}
|
||||
|
||||
if (measureUpload)
|
||||
{
|
||||
RenderPhaseProfile.RecordImageUpload(image.Description, reason,
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ public sealed unsafe partial class GuestImageCache : IGuestImageCache, IGuestIma
|
|||
private readonly GuestBufferCache _bufferCache;
|
||||
private readonly IGuestBackedSpace _backing;
|
||||
private readonly SlotTable<CachedImage> _slots = new();
|
||||
private readonly ImageBackingPool? _backingPool;
|
||||
private readonly ImagePageOwnerTable _pageOwners = new();
|
||||
private readonly Dictionary<Format, ResourceSlotIdentifier> _nullImages = new();
|
||||
private RecencyQueue<ResourceSlotIdentifier> _recencyQueue = new();
|
||||
|
|
@ -51,6 +52,7 @@ public sealed unsafe partial class GuestImageCache : IGuestImageCache, IGuestIma
|
|||
_readbackLinearImages = readbackLinearImages;
|
||||
_blit = new ColorToMultisampleDepthBlit(device, scheduler);
|
||||
_tiler = new GpuTiler(device, scheduler, bufferCache.GetUtilityBuffer(GpuBufferUsage.Stream));
|
||||
_backingPool = ImageBackingPool.Enabled ? new ImageBackingPool(device) : null;
|
||||
}
|
||||
|
||||
public ulong TotalUsedMemory => _totalUsedMemory;
|
||||
|
|
@ -95,6 +97,7 @@ public sealed unsafe partial class GuestImageCache : IGuestImageCache, IGuestIma
|
|||
|
||||
_disposed = true;
|
||||
_slots.ForEach((_, image) => image.Dispose());
|
||||
_backingPool?.Dispose();
|
||||
_tiler.Dispose();
|
||||
_blit.Dispose();
|
||||
}
|
||||
|
|
@ -175,6 +178,12 @@ public sealed unsafe partial class GuestImageCache : IGuestImageCache, IGuestIma
|
|||
? GuestImageType.Color1D : GuestImageType.Color2D;
|
||||
result = GrowImage(replacement, result);
|
||||
}
|
||||
else if (request.Role == ImageRole.StorageImage && viewMip < 0 && viewLayer < 0 &&
|
||||
resolved.Description.IsBlock && !request.Description.IsBlock &&
|
||||
(resolved.Backing.Usage & ImageUsageFlags.StorageBit) == 0)
|
||||
{
|
||||
result = ReplaceCompressedForStorage(request.Description, result);
|
||||
}
|
||||
}
|
||||
|
||||
if (!result.IsValid)
|
||||
|
|
@ -463,7 +472,7 @@ public sealed unsafe partial class GuestImageCache : IGuestImageCache, IGuestIma
|
|||
private ResourceSlotIdentifier InsertImage(in ImageDescription description)
|
||||
{
|
||||
using var profileScope = RenderPhaseProfile.MeasureDetail(RenderPhaseProfile.Phase.ImageCreate);
|
||||
var imageIdentifier = _slots.Insert(new CachedImage(_device, _scheduler, _backing, description));
|
||||
var imageIdentifier = _slots.Insert(new CachedImage(_device, _scheduler, _backing, description, _backingPool));
|
||||
if (!ImageDescription.IsEmptyRange(description.Data))
|
||||
{
|
||||
AddToIndex(imageIdentifier);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,148 @@
|
|||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using SharpEmu.Libs.Gpu.Buffers;
|
||||
using Silk.NET.Vulkan;
|
||||
|
||||
namespace SharpEmu.Libs.Gpu.Images;
|
||||
|
||||
// Keeps the Vulkan image and memory of retired cached images so an image created with the
|
||||
// same parameters reuses them instead of calling vkCreateImage and vkAllocateMemory again.
|
||||
// Astro Bot turns one surface into a depth target and back into a color image every frame,
|
||||
// which otherwise creates and frees three images a frame. A retired image is returned only
|
||||
// after the GPU finished with it (the cache erases images on tick completion), and a reused
|
||||
// image starts again from the undefined layout, so no contents or hazards carry over.
|
||||
public sealed unsafe class ImageBackingPool : IDisposable
|
||||
{
|
||||
private const int MaxPerKey = 4;
|
||||
private const ulong MaxPooledBytes = 256UL << 20;
|
||||
|
||||
public readonly record struct Key(
|
||||
Format Format,
|
||||
ImageType Type,
|
||||
Extent3D Extent,
|
||||
uint Layers,
|
||||
uint MipLevels,
|
||||
SampleCountFlags Samples,
|
||||
ImageCreateFlags Flags,
|
||||
ImageUsageFlags Usage);
|
||||
|
||||
private readonly record struct Entry(Image Handle, DeviceMemory Memory, ulong Size);
|
||||
|
||||
private readonly GpuDeviceInfo _device;
|
||||
private readonly Dictionary<Key, Stack<Entry>> _entries = new();
|
||||
private readonly LinkedList<(Key Key, Entry Entry)> _order = new();
|
||||
private ulong _pooledBytes;
|
||||
private bool _disposed;
|
||||
|
||||
public ImageBackingPool(GpuDeviceInfo device) => _device = device;
|
||||
|
||||
// SHARPEMU_IMAGE_BACKING_POOL=0 destroys every retired image as before.
|
||||
public static readonly bool Enabled = !string.Equals(
|
||||
Environment.GetEnvironmentVariable("SHARPEMU_IMAGE_BACKING_POOL"), "0", StringComparison.Ordinal);
|
||||
|
||||
internal static Key KeyOf(in ImageCreateInfo create) =>
|
||||
new(create.Format, create.ImageType, create.Extent, create.ArrayLayers, create.MipLevels, create.Samples, create.Flags, create.Usage);
|
||||
|
||||
public bool TryTake(in Key key, out Image handle, out DeviceMemory memory, out ulong size)
|
||||
{
|
||||
if (_entries.TryGetValue(key, out var stack) && stack.TryPop(out var entry))
|
||||
{
|
||||
RemoveOrder(key, entry);
|
||||
_pooledBytes -= entry.Size;
|
||||
(handle, memory, size) = (entry.Handle, entry.Memory, entry.Size);
|
||||
return true;
|
||||
}
|
||||
|
||||
handle = default;
|
||||
memory = default;
|
||||
size = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
// False when the pool is full for this key or disposed; the caller then destroys it.
|
||||
public bool TryReturn(in Key key, Image handle, DeviceMemory memory, ulong size)
|
||||
{
|
||||
if (_disposed || size > MaxPooledBytes)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!_entries.TryGetValue(key, out var stack))
|
||||
{
|
||||
stack = new Stack<Entry>();
|
||||
_entries.Add(key, stack);
|
||||
}
|
||||
|
||||
if (stack.Count >= MaxPerKey)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var entry = new Entry(handle, memory, size);
|
||||
stack.Push(entry);
|
||||
_order.AddLast((key, entry));
|
||||
_pooledBytes += size;
|
||||
while (_pooledBytes > MaxPooledBytes && _order.First is { } oldest)
|
||||
{
|
||||
_order.RemoveFirst();
|
||||
var (oldestKey, oldestEntry) = oldest.Value;
|
||||
RemoveFromStack(oldestKey, oldestEntry);
|
||||
_pooledBytes -= oldestEntry.Size;
|
||||
Destroy(oldestEntry);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void RemoveOrder(Key key, Entry entry)
|
||||
{
|
||||
for (var node = _order.Last; node is not null; node = node.Previous)
|
||||
{
|
||||
if (node.Value.Key == key && node.Value.Entry == entry)
|
||||
{
|
||||
_order.Remove(node);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void RemoveFromStack(Key key, Entry entry)
|
||||
{
|
||||
if (!_entries.TryGetValue(key, out var stack))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var kept = stack.Where(item => item != entry).Reverse().ToArray();
|
||||
stack.Clear();
|
||||
foreach (var item in kept)
|
||||
{
|
||||
stack.Push(item);
|
||||
}
|
||||
}
|
||||
|
||||
private void Destroy(Entry entry)
|
||||
{
|
||||
_device.Vk.DestroyImage(_device.Device, entry.Handle, null);
|
||||
_device.FreeMemory(entry.Memory);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_disposed = true;
|
||||
foreach (var (_, entry) in _order)
|
||||
{
|
||||
Destroy(entry);
|
||||
}
|
||||
|
||||
_order.Clear();
|
||||
_entries.Clear();
|
||||
_pooledBytes = 0;
|
||||
}
|
||||
}
|
||||
|
|
@ -118,7 +118,7 @@ public static partial class ImageRequestBuilders
|
|||
var usage = shape.Storage ? ImageUsageFlags.StorageBit : ImageUsageFlags.SampledBit;
|
||||
if (shape.Volume)
|
||||
{
|
||||
return new ImageViewDescription(format, ImageViewType.Type3D, ImageAspectFlags.ColorBit, descriptor.BaseLevel, viewLevels, 0, 1, mapping, usage);
|
||||
return WithMinLod(new ImageViewDescription(format, ImageViewType.Type3D, ImageAspectFlags.ColorBit, descriptor.BaseLevel, viewLevels, 0, 1, mapping, usage), descriptor, shape);
|
||||
}
|
||||
|
||||
var baseLayer = descriptor.BaseArray;
|
||||
|
|
@ -131,7 +131,21 @@ public static partial class ImageRequestBuilders
|
|||
var type = shape.OneDimensional
|
||||
? shape.Arrayed ? ImageViewType.Type1DArray : ImageViewType.Type1D
|
||||
: shape.Arrayed ? ImageViewType.Type2DArray : ImageViewType.Type2D;
|
||||
return new ImageViewDescription(format, type, ImageAspectFlags.ColorBit, descriptor.BaseLevel, viewLevels, baseLayer, layerCount, mapping, usage);
|
||||
return WithMinLod(new ImageViewDescription(format, type, ImageAspectFlags.ColorBit, descriptor.BaseLevel, viewLevels, baseLayer, layerCount, mapping, usage), descriptor, shape);
|
||||
}
|
||||
|
||||
// MIN_LOD (4.8 fixed point, absolute mip levels) keeps sampling off mips a streamed texture has not
|
||||
// loaded yet; ignoring it sampled the unloaded mip 0 of Astro Bot's stadium sky and bloom spread its
|
||||
// garbage HDR values over the screen. Storage views address levels explicitly and are not clamped.
|
||||
private static ImageViewDescription WithMinLod(ImageViewDescription view, in TextureDescriptorWords descriptor, in ShaderImageShape shape)
|
||||
{
|
||||
var minLod = descriptor.MinLod / 256f;
|
||||
if (shape.Storage || minLod <= view.BaseLevel)
|
||||
{
|
||||
return view;
|
||||
}
|
||||
|
||||
return view with { MinLod = Math.Min(minLod, view.BaseLevel + view.LevelCount - 1) };
|
||||
}
|
||||
|
||||
public static uint DestinationSwizzle(in TextureDescriptorWords descriptor) =>
|
||||
|
|
@ -149,8 +163,39 @@ public static partial class ImageRequestBuilders
|
|||
|
||||
private static bool IsMultisampledTexture(GuestImageType type) => type is GuestImageType.Color2DMsaa or GuestImageType.Color2DMsaaArray;
|
||||
|
||||
private readonly record struct TextureKey(uint W0, uint W1, uint W2, uint W3, uint W4, uint W5, uint W6, uint W7, ShaderImageShape Shape);
|
||||
|
||||
private const int TextureCacheLimit = 8192;
|
||||
|
||||
// Every texture binding of every draw resolves its T# here, and the tiled layout math
|
||||
// allocates several arrays each time; the result depends only on the words and shape
|
||||
// and is a pure value, so it is memoised per thread.
|
||||
[ThreadStatic]
|
||||
private static Dictionary<TextureKey, TextureRequestResolution>? _textureCache;
|
||||
|
||||
// Builds the request for a sampled or storage texture; the words are the eight T# dwords.
|
||||
public static TextureRequestResolution Texture(ReadOnlySpan<uint> words, in ShaderImageShape shape)
|
||||
{
|
||||
Span<uint> key = stackalloc uint[8];
|
||||
words[..Math.Min(words.Length, 8)].CopyTo(key);
|
||||
var cacheKey = new TextureKey(key[0], key[1], key[2], key[3], key[4], key[5], key[6], key[7], shape);
|
||||
var cache = _textureCache ??= new Dictionary<TextureKey, TextureRequestResolution>();
|
||||
if (cache.TryGetValue(cacheKey, out var cached))
|
||||
{
|
||||
return cached;
|
||||
}
|
||||
|
||||
var resolution = BuildTexture(key, shape);
|
||||
if (cache.Count >= TextureCacheLimit)
|
||||
{
|
||||
cache.Clear();
|
||||
}
|
||||
|
||||
cache[cacheKey] = resolution;
|
||||
return resolution;
|
||||
}
|
||||
|
||||
private static TextureRequestResolution BuildTexture(ReadOnlySpan<uint> words, in ShaderImageShape shape)
|
||||
{
|
||||
Span<uint> padded = stackalloc uint[8];
|
||||
words[..Math.Min(words.Length, 8)].CopyTo(padded);
|
||||
|
|
|
|||
|
|
@ -20,15 +20,20 @@ public readonly record struct ImageViewDescription(
|
|||
{
|
||||
public static ImageViewDescription Default => new(Format.Undefined, ImageViewType.Type2D, ImageAspectFlags.ColorBit, 0, 1, 0, 1, default, ImageUsageFlags.SampledBit);
|
||||
|
||||
// The texture descriptor's MIN_LOD in absolute mip levels; 0 leaves the view unclamped.
|
||||
// Streamed textures raise it until their larger mips are resident.
|
||||
public float MinLod { get; init; }
|
||||
|
||||
public bool Equals(ImageViewDescription other) =>
|
||||
Format == other.Format && Type == other.Type && Aspect == other.Aspect &&
|
||||
BaseLevel == other.BaseLevel && LevelCount == other.LevelCount &&
|
||||
BaseLayer == other.BaseLayer && LayerCount == other.LayerCount &&
|
||||
Mapping.R == other.Mapping.R && Mapping.G == other.Mapping.G &&
|
||||
Mapping.B == other.Mapping.B && Mapping.A == other.Mapping.A && Usage == other.Usage;
|
||||
Mapping.B == other.Mapping.B && Mapping.A == other.Mapping.A && Usage == other.Usage &&
|
||||
MinLod == other.MinLod;
|
||||
|
||||
public override int GetHashCode() =>
|
||||
HashCode.Combine(Format, Type, Aspect, BaseLevel, LevelCount, BaseLayer, LayerCount, HashCode.Combine(Mapping.R, Mapping.G, Mapping.B, Mapping.A, Usage));
|
||||
HashCode.Combine(Format, Type, Aspect, BaseLevel, LevelCount, BaseLayer, LayerCount, HashCode.Combine(Mapping.R, Mapping.G, Mapping.B, Mapping.A, Usage, MinLod));
|
||||
}
|
||||
|
||||
// Which host formats may view which image formats, and which guest swizzles are legal.
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ internal interface IShaderPipelineHost
|
|||
// The device supports shaderSharedInt64Atomics, so LDS 64-bit atomics can be
|
||||
// emitted as real 64-bit atomics instead of a non-atomic 32-bit pair.
|
||||
bool SharedInt64AtomicsEnabled { get; }
|
||||
bool ExecGuardElisionEnabled => true;
|
||||
bool PerVertexPixelInputsSupported => true;
|
||||
|
||||
RenderHostLimits Limits { get; }
|
||||
|
|
|
|||
|
|
@ -60,10 +60,19 @@ public static class ShaderIdentity
|
|||
return declaredHash;
|
||||
}
|
||||
|
||||
var hash = new XxHash3();
|
||||
// Shaders without a declared hash are rehashed on every draw, so the hasher and
|
||||
// the code buffer are reused per thread instead of allocated per call.
|
||||
var hash = _hasher ??= new XxHash3();
|
||||
hash.Reset();
|
||||
foreach (var (address, sizeBytes) in ranges)
|
||||
{
|
||||
var code = new byte[sizeBytes];
|
||||
var buffer = _codeBuffer;
|
||||
if (buffer is null || buffer.Length < sizeBytes)
|
||||
{
|
||||
buffer = _codeBuffer = new byte[Math.Max(sizeBytes, 4096u)];
|
||||
}
|
||||
|
||||
var code = buffer.AsSpan(0, (int)sizeBytes);
|
||||
if (!memory.TryRead(address, code))
|
||||
{
|
||||
throw Scheduling.SubmissionScheduler.Fatal($"The shader code is unreadable: label={label} shader=0x{address:X16} size=0x{sizeBytes:X8}.");
|
||||
|
|
@ -74,4 +83,10 @@ public static class ShaderIdentity
|
|||
|
||||
return hash.GetCurrentHashAsUInt64();
|
||||
}
|
||||
|
||||
[ThreadStatic]
|
||||
private static XxHash3? _hasher;
|
||||
|
||||
[ThreadStatic]
|
||||
private static byte[]? _codeBuffer;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -76,7 +76,8 @@ internal sealed partial class ShaderPipelineCache : IShaderPipelineProvider
|
|||
ShaderInterfaceRegisters shaderInterface,
|
||||
ContextRegisters context,
|
||||
ReadOnlySpan<ColorComponentMap> targetExportMapping,
|
||||
bool pixelActive)
|
||||
bool pixelActive,
|
||||
bool depthBound)
|
||||
{
|
||||
using var profileScope = RenderPhaseProfile.MeasureDetail(RenderPhaseProfile.Phase.ProgramPreparation);
|
||||
var vertexSource = PrepareSource(
|
||||
|
|
@ -109,7 +110,8 @@ internal sealed partial class ShaderPipelineCache : IShaderPipelineProvider
|
|||
throw SubmissionScheduler.Fatal($"The pixel program declares too many interpolators: shader=0x{pixel.Address:X16} count={inputCount}.");
|
||||
}
|
||||
|
||||
pixelOutputs = ResolveBoundTargets(context, out var outputModes, out var outputMappings);
|
||||
pixelOutputs = ResolveBoundTargets(context, targetExportMapping, depthBound ? pixelProgram.PixelColorExportMasks : null,
|
||||
out var outputModes, out var outputMappings);
|
||||
pixelInfo = PixelStageInputResolver.Resolve(_context, pixelSource.Registered, shaderInterface, outputModes, outputMappings, inputCount);
|
||||
}
|
||||
|
||||
|
|
@ -219,7 +221,11 @@ internal sealed partial class ShaderPipelineCache : IShaderPipelineProvider
|
|||
}
|
||||
|
||||
// The bound colour slots in order; each output mode names the kind the pixel program exports.
|
||||
private Gen5PixelOutputBinding[] ResolveBoundTargets(ContextRegisters context, out byte[] outputModes, out ColorComponentMap[] outputMappings)
|
||||
// With a depth target bound, a slot the draw never writes is dropped here and from the
|
||||
// attachments alike (RenderExecutor.DropUnwrittenColorTargets): unwrittenExportMasks is the
|
||||
// pixel program's export mask then, null otherwise.
|
||||
private Gen5PixelOutputBinding[] ResolveBoundTargets(ContextRegisters context, ReadOnlySpan<ColorComponentMap> targetExportMapping,
|
||||
uint? unwrittenExportMasks, out byte[] outputModes, out ColorComponentMap[] outputMappings)
|
||||
{
|
||||
outputModes = new byte[PixelInputInfo.TargetCount];
|
||||
outputMappings = new ColorComponentMap[PixelInputInfo.TargetCount];
|
||||
|
|
@ -236,6 +242,12 @@ internal sealed partial class ShaderPipelineCache : IShaderPipelineProvider
|
|||
continue;
|
||||
}
|
||||
|
||||
if (unwrittenExportMasks is { } exportMasks &&
|
||||
RenderExecutor.IsUnwrittenColorTarget(context, slot, targetExportMapping[(int)slot], exportMasks))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var words = context.ColorTargets[slot];
|
||||
if (!_host.TryResolveColorOutput((uint)words.Layout, (uint)words.NumberType, (uint)words.Order, out var kind, out var mapping))
|
||||
{
|
||||
|
|
|
|||
|
|
@ -111,6 +111,10 @@ internal sealed class ShaderProgramCache
|
|||
|
||||
public ResourceMaterializationCache? Materializations => _materializations;
|
||||
|
||||
// Read once: the draw path asked the environment on every draw and dispatch
|
||||
// (~4 % of the Demon's Souls render thread) for a debug dump that is almost never on.
|
||||
private readonly bool _spirvDumpEnabled = string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_DUMP_SPIRV"), "1", StringComparison.Ordinal);
|
||||
|
||||
public ShaderProgramCache(CpuContext context, IGuestGpuBackend compiler, IShaderPipelineHost host)
|
||||
{
|
||||
_context = context;
|
||||
|
|
@ -215,7 +219,7 @@ internal sealed class ShaderProgramCache
|
|||
|
||||
var snapshot = new ResourceSnapshot();
|
||||
var specialization = new ResourceSpecialization();
|
||||
var captureIndirectImageFailure = ShaderPermutationDump.CreateFailureCapture(source);
|
||||
var captureIndirectImageFailure = _spirvDumpEnabled ? ShaderPermutationDump.CreateFailureCapture(source) : null;
|
||||
using (RenderPhaseProfile.MeasureDetail(RenderPhaseProfile.Phase.ResourceMaterialization))
|
||||
{
|
||||
// Failure capture needs the full walk, so a dump run bypasses the cache.
|
||||
|
|
@ -589,6 +593,7 @@ internal sealed class ShaderProgramCache
|
|||
return new ShaderCompileRequest(entry.Plan, resources, layout)
|
||||
{
|
||||
WaveSize = info.WaveSize,
|
||||
EnableExecGuardElision = info.WaveSize != 64 || _host.ExecGuardElisionEnabled,
|
||||
TraceDeviceAddressFaults = SharpEmu.HLE.GpuMemory.GuestGpuMemoryHook.TraceEnabled,
|
||||
ScratchDwords = info.ScratchDwords,
|
||||
SupportsSharedInt64Atomics = sharedInt64Atomics,
|
||||
|
|
|
|||
|
|
@ -143,6 +143,10 @@ public interface IRenderHost
|
|||
|
||||
void DrawIndexed(uint indexCount, uint instanceCount, uint firstIndex, int vertexOffset, uint firstInstance);
|
||||
|
||||
// One indexed draw whose counts the GPU reads from the buffer (VkDrawIndexedIndirectCommand layout).
|
||||
void DrawIndexedIndirect(BufferBinding arguments) =>
|
||||
throw new NotSupportedException("The render host does not draw from indirect arguments.");
|
||||
|
||||
void Dispatch(uint groupsX, uint groupsY, uint groupsZ);
|
||||
|
||||
// Executes a dispatch from three uint32 group counts in guest memory when the backend can
|
||||
|
|
|
|||
|
|
@ -60,7 +60,8 @@ public interface IShaderPipelineProvider
|
|||
ShaderInterfaceRegisters shaderInterface,
|
||||
ContextRegisters context,
|
||||
ReadOnlySpan<ColorComponentMap> targetExportMapping,
|
||||
bool pixelActive);
|
||||
bool pixelActive,
|
||||
bool depthBound);
|
||||
|
||||
PipelineHandle CreateGraphicsPipeline(
|
||||
ReadOnlySpan<ColorTargetState> colors,
|
||||
|
|
|
|||
|
|
@ -220,6 +220,10 @@ public sealed partial class RenderExecutor
|
|||
|
||||
var vertexBuffers = AcquireVertexBuffers(vertexInput);
|
||||
var indexBuffer = AcquireIndexBuffer(in indexSource);
|
||||
var indirectArguments = emission.IndirectArgumentsAddress != 0
|
||||
? _host.ObtainBuffer(emission.IndirectArgumentsAddress, IndexedIndirectArgumentsSize, isWritten: false)
|
||||
: default;
|
||||
DropUnwrittenColorTargets(context, ref state, pixelProgram);
|
||||
state.Rendering = AcquireAttachments(ref state);
|
||||
// Nothing after the pipeline touches guest memory.
|
||||
var pipeline = _pipelines.CreateGraphicsPipeline(
|
||||
|
|
@ -271,7 +275,16 @@ public sealed partial class RenderExecutor
|
|||
SetDrawDebugPhase(submitId, in draw, 0x500);
|
||||
}
|
||||
|
||||
EmitDraw(banks.UserConfig, vertexInput, in draw, in emission);
|
||||
if (emission.IndirectArgumentsAddress != 0)
|
||||
{
|
||||
// Uploads and shader writes end with barriers to all commands, so the
|
||||
// indirect read sees them.
|
||||
_host.DrawIndexedIndirect(indirectArguments);
|
||||
}
|
||||
else
|
||||
{
|
||||
EmitDraw(banks.UserConfig, vertexInput, in draw, in emission);
|
||||
}
|
||||
if (setAutoDebug)
|
||||
{
|
||||
SetDrawDebugPhase(submitId, in draw, 0x600);
|
||||
|
|
@ -300,6 +313,8 @@ public sealed partial class RenderExecutor
|
|||
}
|
||||
}
|
||||
|
||||
private const ulong IndexedIndirectArgumentsSize = 20;
|
||||
|
||||
private void EmitDraw(UserConfigRegisters userConfig, VertexInputInfo vertexInput, in DrawCall draw, in DrawEmission emission)
|
||||
{
|
||||
switch ((GuestPrimitiveType)userConfig.PrimitiveType)
|
||||
|
|
|
|||
|
|
@ -171,6 +171,39 @@ public sealed partial class RenderExecutor
|
|||
|
||||
private static bool IsSupportedSampleCount(uint samples) => samples is 1 or 2 or 4 or 8;
|
||||
|
||||
// A depth pass can leave a color target bound that it never writes (target mask 0, or a
|
||||
// slot the pixel program does not export). Hardware bounds such a draw only by the
|
||||
// scissor, but a host attachment also bounds the render area: a 1024x1024 color target
|
||||
// left bound by Astro Bot's depth clear kept the clear out of most of a 1080p depth buffer.
|
||||
// The pixel outputs drop the same slots (ShaderPipelineCache.ResolveBoundTargets), so host
|
||||
// locations stay dense and aligned with the remaining attachments.
|
||||
private static void DropUnwrittenColorTargets(ContextRegisters context, ref DrawState state, ShaderProgramInfo? pixelProgram)
|
||||
{
|
||||
if (!state.Depth.HasTarget)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var exportMasks = pixelProgram?.PixelColorExportMasks ?? uint.MaxValue;
|
||||
var kept = 0u;
|
||||
for (var i = 0; i < state.ColorCount; i++)
|
||||
{
|
||||
ref readonly var target = ref state.Colors[i];
|
||||
if (IsUnwrittenColorTarget(context, target.Slot, target.Resolution.ExportMapping, exportMasks))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
state.Colors[(int)kept++] = target;
|
||||
}
|
||||
|
||||
state.ColorCount = kept;
|
||||
}
|
||||
|
||||
internal static bool IsUnwrittenColorTarget(ContextRegisters context, uint slot, ColorComponentMap exportMapping, uint pixelColorExportMasks) =>
|
||||
exportMapping.ApplyMask(context.RenderTargetMaskForSlot(slot)) == 0 ||
|
||||
((pixelColorExportMasks >> (int)(slot * 4)) & 0xFu) == 0;
|
||||
|
||||
// Acquires every attachment through the host and assembles the rendering scope.
|
||||
private RenderingState AcquireAttachments(ref DrawState state)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -69,7 +69,8 @@ public sealed partial class RenderExecutor
|
|||
|
||||
private readonly record struct DrawCall(string Name, RecordedOperation Operation, uint Count, uint InstanceCount, uint FirstInstance);
|
||||
|
||||
private readonly record struct DrawEmission(bool Indexed, int VertexOffset, uint FirstVertex, uint FirstInstance);
|
||||
// IndirectArgumentsAddress: the GPU reads the indexed draw's counts from guest memory there.
|
||||
private readonly record struct DrawEmission(bool Indexed, int VertexOffset, uint FirstVertex, uint FirstInstance, ulong IndirectArgumentsAddress = 0);
|
||||
|
||||
private readonly record struct IndexSource(bool Enabled, ulong Address, byte[]? HostData, ulong Size, IndexType Type);
|
||||
|
||||
|
|
@ -95,6 +96,91 @@ public sealed partial class RenderExecutor
|
|||
((ReadOnlySpan<ColorTargetState>)state.Colors)[..(int)state.ColorCount];
|
||||
|
||||
public void DrawIndexed(ulong submitId, RegisterBanks banks, in DrawIndexedArguments arguments)
|
||||
{
|
||||
if (arguments.IndirectArgumentsAddress != 0 && !CanDrawIndirectOnGpu(banks, in arguments))
|
||||
{
|
||||
DrawIndexedWithCpuArguments(submitId, banks, in arguments);
|
||||
return;
|
||||
}
|
||||
|
||||
DrawIndexedCore(submitId, banks, in arguments);
|
||||
}
|
||||
|
||||
// The GPU reads the arguments of an indirect draw unless the draw is emulated from
|
||||
// its counts: strips (metadata clear quads), legacy primitives, 8-bit indices and
|
||||
// restart indices converted on the CPU.
|
||||
private static bool CanDrawIndirectOnGpu(RegisterBanks banks, in DrawIndexedArguments arguments)
|
||||
{
|
||||
switch ((GuestPrimitiveType)banks.UserConfig.PrimitiveType)
|
||||
{
|
||||
case GuestPrimitiveType.PointList:
|
||||
case GuestPrimitiveType.LineList:
|
||||
case GuestPrimitiveType.LineStrip:
|
||||
case GuestPrimitiveType.TriangleList:
|
||||
case GuestPrimitiveType.TriangleFan:
|
||||
case GuestPrimitiveType.Polygon:
|
||||
case GuestPrimitiveType.RectangleList:
|
||||
break;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
|
||||
var index32 = (GuestIndexType)arguments.IndexTypeAndSize == GuestIndexType.Index32;
|
||||
if (!index32 && (GuestIndexType)arguments.IndexTypeAndSize != GuestIndexType.Index16)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if ((banks.UserConfig.PrimitiveResetControl & 0x1) != 0)
|
||||
{
|
||||
var indexMask = index32 ? uint.MaxValue : 0xFFFFu;
|
||||
if ((banks.Context.PrimitiveResetIndex & indexMask) != indexMask)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return (ulong)arguments.IndexCount * (index32 ? 4ul : 2ul) <= MaxIndirectIndexBufferBytes;
|
||||
}
|
||||
|
||||
private const ulong MaxIndirectIndexBufferBytes = 64ul << 20;
|
||||
|
||||
// Reads the indirect arguments now and draws from them, as the interpreter would have.
|
||||
private void DrawIndexedWithCpuArguments(ulong submitId, RegisterBanks banks, in DrawIndexedArguments arguments)
|
||||
{
|
||||
Span<byte> bytes = stackalloc byte[(int)IndexedIndirectArgumentsSize];
|
||||
if (!_host.TryReadGuest(arguments.IndirectArgumentsAddress, bytes))
|
||||
{
|
||||
throw _host.Fatal($"The indirect draw arguments are unreadable: address=0x{arguments.IndirectArgumentsAddress:X16}.");
|
||||
}
|
||||
|
||||
var words = System.Runtime.InteropServices.MemoryMarshal.Cast<byte, uint>(bytes);
|
||||
var elementSize = (GuestIndexType)arguments.IndexTypeAndSize switch
|
||||
{
|
||||
GuestIndexType.Index16 => 2ul,
|
||||
GuestIndexType.Index32 => 4ul,
|
||||
_ => 1ul,
|
||||
};
|
||||
var resolved = arguments with
|
||||
{
|
||||
IndexCount = arguments.UnboundedIndexBuffer ? words[0] : Math.Min(words[0], arguments.IndexCount),
|
||||
InstanceCount = words[1],
|
||||
IndexAddress = arguments.IndexAddress + (words[2] * elementSize),
|
||||
BaseVertex = unchecked((int)words[3]),
|
||||
FirstInstance = words[4],
|
||||
IndirectArgumentsAddress = 0,
|
||||
UnboundedIndexBuffer = false,
|
||||
};
|
||||
if (resolved.IndexCount == 0 || resolved.InstanceCount == 0)
|
||||
{
|
||||
_host.ResetBindings();
|
||||
return;
|
||||
}
|
||||
|
||||
DrawIndexedCore(submitId, banks, in resolved);
|
||||
}
|
||||
|
||||
private void DrawIndexedCore(ulong submitId, RegisterBanks banks, in DrawIndexedArguments arguments)
|
||||
{
|
||||
using var profileScope = RenderPhaseProfile.MeasureDetail(RenderPhaseProfile.Phase.DrawExecutor);
|
||||
if (!_host.IsRecording)
|
||||
|
|
@ -193,6 +279,13 @@ public sealed partial class RenderExecutor
|
|||
return;
|
||||
}
|
||||
|
||||
if (arguments.IndirectArgumentsAddress != 0 && state.ColorCount == 0 && !state.Depth.HasTarget)
|
||||
{
|
||||
// A targetless draw may be retained and replayed later; it needs its counts.
|
||||
DrawIndexedWithCpuArguments(submitId, banks, in arguments);
|
||||
return;
|
||||
}
|
||||
|
||||
ResolveShaderPrograms(banks, ref state);
|
||||
if (!ApplyProgramAdaptations(banks, in draw, ref state, new TargetlessDrawArguments(submitId, true, arguments, default)))
|
||||
{
|
||||
|
|
@ -209,7 +302,8 @@ public sealed partial class RenderExecutor
|
|||
true,
|
||||
vertexOffset,
|
||||
0,
|
||||
indirect ? arguments.FirstInstance : ResolveInstanceOffset(state.Programs.VertexInput));
|
||||
indirect ? arguments.FirstInstance : ResolveInstanceOffset(state.Programs.VertexInput),
|
||||
arguments.IndirectArgumentsAddress);
|
||||
RecordDraw(submitId, banks, in draw, ref state, topology, in emission, in indexSource, primitiveRestart, setBindDebug: true, setAutoDebug: false);
|
||||
_host.ResetBindings();
|
||||
}
|
||||
|
|
@ -588,7 +682,8 @@ public sealed partial class RenderExecutor
|
|||
context.ShaderInterface,
|
||||
context,
|
||||
targetExportMapping,
|
||||
state.PixelActive);
|
||||
state.PixelActive,
|
||||
state.Depth.HasTarget);
|
||||
}
|
||||
|
||||
[System.Runtime.CompilerServices.InlineArray(RenderingState.ColorAttachmentCapacity)]
|
||||
|
|
|
|||
|
|
@ -0,0 +1,222 @@
|
|||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using SharpEmu.Libs.Gpu.Buffers;
|
||||
using SharpEmu.Libs.Gpu.Scheduling;
|
||||
using Silk.NET.Vulkan;
|
||||
using VkSemaphore = Silk.NET.Vulkan.Semaphore;
|
||||
|
||||
namespace SharpEmu.Libs.Gpu.Vulkan;
|
||||
|
||||
public readonly record struct ReadbackPiece(GpuBuffer Source, ulong SourceOffset, ulong Size);
|
||||
|
||||
// Copies GPU-written buffer ranges back to the host on a second queue. The copy waits
|
||||
// on the main queue's timeline for the tick that last wrote the sources only, so a
|
||||
// guest read of one GPU-produced value no longer waits behind every later draw that
|
||||
// is still queued on the main queue (a readback there has to go to the queue's tail).
|
||||
internal sealed unsafe class VulkanAsyncReadback : IDisposable
|
||||
{
|
||||
private const ulong Alignment = 16;
|
||||
|
||||
private readonly GpuDeviceInfo _device;
|
||||
private readonly SubmissionScheduler _scheduler;
|
||||
private readonly Queue _queue;
|
||||
private readonly CommandPool _pool;
|
||||
private readonly CommandBuffer _command;
|
||||
private readonly VkSemaphore _timeline;
|
||||
private readonly object _gate = new();
|
||||
private ulong _signaled;
|
||||
private GpuBuffer? _staging;
|
||||
|
||||
public VulkanAsyncReadback(GpuDeviceInfo device, SubmissionScheduler scheduler, Queue queue, uint queueFamilyIndex)
|
||||
{
|
||||
_device = device;
|
||||
_scheduler = scheduler;
|
||||
_queue = queue;
|
||||
var vk = device.Vk;
|
||||
var poolInfo = new CommandPoolCreateInfo
|
||||
{
|
||||
SType = StructureType.CommandPoolCreateInfo,
|
||||
QueueFamilyIndex = queueFamilyIndex,
|
||||
Flags = CommandPoolCreateFlags.TransientBit | CommandPoolCreateFlags.ResetCommandBufferBit,
|
||||
};
|
||||
Require(vk.CreateCommandPool(device.Device, &poolInfo, null, out _pool), "vkCreateCommandPool(readback)");
|
||||
var allocateInfo = new CommandBufferAllocateInfo
|
||||
{
|
||||
SType = StructureType.CommandBufferAllocateInfo,
|
||||
CommandPool = _pool,
|
||||
Level = CommandBufferLevel.Primary,
|
||||
CommandBufferCount = 1,
|
||||
};
|
||||
CommandBuffer command;
|
||||
Require(vk.AllocateCommandBuffers(device.Device, &allocateInfo, &command), "vkAllocateCommandBuffers(readback)");
|
||||
_command = command;
|
||||
var typeInfo = new SemaphoreTypeCreateInfo
|
||||
{
|
||||
SType = StructureType.SemaphoreTypeCreateInfo,
|
||||
SemaphoreType = SemaphoreType.Timeline,
|
||||
};
|
||||
var semaphoreInfo = new SemaphoreCreateInfo { SType = StructureType.SemaphoreCreateInfo, PNext = &typeInfo };
|
||||
Require(vk.CreateSemaphore(device.Device, &semaphoreInfo, null, out _timeline), "vkCreateSemaphore(readback)");
|
||||
}
|
||||
|
||||
// Copies the pieces once the main-queue tick has completed and hands each one's bytes
|
||||
// to consume, in order. The caller must have submitted waitTick already.
|
||||
public void Read(ReadOnlySpan<ReadbackPiece> pieces, ulong waitTick, ReadbackConsumer consume)
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
var total = 0UL;
|
||||
foreach (var piece in pieces)
|
||||
{
|
||||
total = AlignUp(total, Alignment) + piece.Size;
|
||||
}
|
||||
|
||||
var staging = EnsureStaging(total);
|
||||
var vk = _device.Vk;
|
||||
Require(vk.ResetCommandBuffer(_command, 0), "vkResetCommandBuffer(readback)");
|
||||
var beginInfo = new CommandBufferBeginInfo
|
||||
{
|
||||
SType = StructureType.CommandBufferBeginInfo,
|
||||
Flags = CommandBufferUsageFlags.OneTimeSubmitBit,
|
||||
};
|
||||
Require(vk.BeginCommandBuffer(_command, &beginInfo), "vkBeginCommandBuffer(readback)");
|
||||
var offset = 0UL;
|
||||
foreach (var piece in pieces)
|
||||
{
|
||||
offset = AlignUp(offset, Alignment);
|
||||
var region = new BufferCopy(piece.SourceOffset, offset, piece.Size);
|
||||
vk.CmdCopyBuffer(_command, piece.Source.Handle, staging.Handle, 1, ®ion);
|
||||
offset += piece.Size;
|
||||
}
|
||||
|
||||
// Make the copied bytes available to the host once the queue signals.
|
||||
var toHost = new MemoryBarrier2
|
||||
{
|
||||
SType = StructureType.MemoryBarrier2,
|
||||
SrcStageMask = PipelineStageFlags2.CopyBit,
|
||||
SrcAccessMask = AccessFlags2.TransferWriteBit,
|
||||
DstStageMask = PipelineStageFlags2.HostBit,
|
||||
DstAccessMask = AccessFlags2.HostReadBit,
|
||||
};
|
||||
var dependency = new DependencyInfo
|
||||
{
|
||||
SType = StructureType.DependencyInfo,
|
||||
MemoryBarrierCount = 1,
|
||||
PMemoryBarriers = &toHost,
|
||||
};
|
||||
vk.CmdPipelineBarrier2(_command, &dependency);
|
||||
Require(vk.EndCommandBuffer(_command), "vkEndCommandBuffer(readback)");
|
||||
|
||||
var signalValue = ++_signaled;
|
||||
// The semaphore wait orders the copies after every write the main queue made
|
||||
// up to waitTick and makes those writes visible to them.
|
||||
var wait = new SemaphoreSubmitInfo
|
||||
{
|
||||
SType = StructureType.SemaphoreSubmitInfo,
|
||||
Semaphore = new VkSemaphore(_scheduler.Timeline.Handle),
|
||||
Value = waitTick,
|
||||
StageMask = PipelineStageFlags2.AllCommandsBit,
|
||||
};
|
||||
var signal = new SemaphoreSubmitInfo
|
||||
{
|
||||
SType = StructureType.SemaphoreSubmitInfo,
|
||||
Semaphore = _timeline,
|
||||
Value = signalValue,
|
||||
StageMask = PipelineStageFlags2.AllCommandsBit,
|
||||
};
|
||||
var commandInfo = new CommandBufferSubmitInfo
|
||||
{
|
||||
SType = StructureType.CommandBufferSubmitInfo,
|
||||
CommandBuffer = _command,
|
||||
DeviceMask = 1,
|
||||
};
|
||||
var submit = new SubmitInfo2
|
||||
{
|
||||
SType = StructureType.SubmitInfo2,
|
||||
WaitSemaphoreInfoCount = waitTick == 0 ? 0u : 1u,
|
||||
PWaitSemaphoreInfos = &wait,
|
||||
CommandBufferInfoCount = 1,
|
||||
PCommandBufferInfos = &commandInfo,
|
||||
SignalSemaphoreInfoCount = 1,
|
||||
PSignalSemaphoreInfos = &signal,
|
||||
};
|
||||
Require(vk.QueueSubmit2(_queue, 1, &submit, default), "vkQueueSubmit2(readback)");
|
||||
|
||||
var semaphore = _timeline;
|
||||
var waitInfo = new SemaphoreWaitInfo
|
||||
{
|
||||
SType = StructureType.SemaphoreWaitInfo,
|
||||
SemaphoreCount = 1,
|
||||
PSemaphores = &semaphore,
|
||||
PValues = &signalValue,
|
||||
};
|
||||
using (VideoOut.RenderPhaseProfile.MeasureDetail(VideoOut.RenderPhaseProfile.Phase.GpuCompletionWait))
|
||||
{
|
||||
Require(vk.WaitSemaphores(_device.Device, &waitInfo, ulong.MaxValue), "vkWaitSemaphores(readback)");
|
||||
}
|
||||
|
||||
offset = 0;
|
||||
for (var index = 0; index < pieces.Length; index++)
|
||||
{
|
||||
offset = AlignUp(offset, Alignment);
|
||||
var size = pieces[index].Size;
|
||||
staging.Invalidate(offset, size);
|
||||
|
||||
consume(index, staging.Mapped.Slice((int)offset, (int)size));
|
||||
offset += size;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public delegate void ReadbackConsumer(int index, ReadOnlySpan<byte> bytes);
|
||||
|
||||
private GpuBuffer EnsureStaging(ulong size)
|
||||
{
|
||||
if (_staging is { } existing && existing.Size >= size)
|
||||
{
|
||||
return existing;
|
||||
}
|
||||
|
||||
_staging?.Dispose();
|
||||
var capacity = Math.Max(size, 1UL << 20);
|
||||
_staging = new GpuBuffer(_device, _scheduler, GpuBufferUsage.Download, 0, BufferUsageFlags.TransferDstBit, capacity);
|
||||
return _staging;
|
||||
}
|
||||
|
||||
private static ulong AlignUp(ulong value, ulong alignment) => (value + alignment - 1) / alignment * alignment;
|
||||
|
||||
private static void Require(Result result, string operation)
|
||||
{
|
||||
if (result != Result.Success)
|
||||
{
|
||||
throw SubmissionScheduler.Fatal($"{operation} failed: {result}.");
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
var vk = _device.Vk;
|
||||
lock (_gate)
|
||||
{
|
||||
if (_signaled != 0)
|
||||
{
|
||||
var semaphore = _timeline;
|
||||
var value = _signaled;
|
||||
var waitInfo = new SemaphoreWaitInfo
|
||||
{
|
||||
SType = StructureType.SemaphoreWaitInfo,
|
||||
SemaphoreCount = 1,
|
||||
PSemaphores = &semaphore,
|
||||
PValues = &value,
|
||||
};
|
||||
_ = vk.WaitSemaphores(_device.Device, &waitInfo, ulong.MaxValue);
|
||||
}
|
||||
|
||||
_staging?.Dispose();
|
||||
_staging = null;
|
||||
vk.DestroySemaphore(_device.Device, _timeline, null);
|
||||
vk.DestroyCommandPool(_device.Device, _pool, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -7414,18 +7414,108 @@ public static partial class KernelMemoryCompatExports
|
|||
|
||||
// The terminator counts as part of the scanned range, so strchr(s, '\0')
|
||||
// returns a pointer to the string's null byte just like a native libc.
|
||||
if (!TryScanCString(ctx, address, needle, findLast: false, out var match))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
|
||||
ctx[CpuRegister.Rax] = match;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[ThreadStatic]
|
||||
private static byte[]? _cStringScanChunk;
|
||||
|
||||
// strchr/strrchr run hundreds of thousands of times per second in Astro
|
||||
// Bot's XML loader. Mapped guest memory is searched in place; otherwise
|
||||
// page-bounded chunks are copied out and searched with the vectorised span
|
||||
// helpers. A chunk that cannot be read whole is rescanned byte by byte, so
|
||||
// a string ending just before an unmapped byte behaves as before.
|
||||
private static bool TryScanCString(CpuContext ctx, ulong address, byte needle, bool findLast, out ulong match)
|
||||
{
|
||||
const int pageSize = 4096;
|
||||
const int firstChunkSize = 256;
|
||||
const ulong scanLimit = 1_048_576;
|
||||
if (ctx.Memory.TryScanCString(address, needle, findLast, scanLimit, out match))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
match = 0;
|
||||
Span<byte> firstChunk = stackalloc byte[firstChunkSize];
|
||||
ulong offset = 0;
|
||||
while (offset < scanLimit)
|
||||
{
|
||||
var current = address + offset;
|
||||
var pageRemaining = pageSize - (int)(current & (pageSize - 1));
|
||||
var length = (int)Math.Min((ulong)Math.Min(pageRemaining, offset == 0 ? firstChunkSize : pageSize), scanLimit - offset);
|
||||
var chunk = offset == 0
|
||||
? firstChunk[..length]
|
||||
: (_cStringScanChunk ??= new byte[pageSize]).AsSpan(0, length);
|
||||
if (!TryReadCompat(ctx, current, chunk))
|
||||
{
|
||||
return TryScanCStringBytewise(ctx, current, needle, findLast, scanLimit - offset, ref match);
|
||||
}
|
||||
|
||||
if (!findLast)
|
||||
{
|
||||
var stop = chunk.IndexOfAny(needle, (byte)0);
|
||||
if (stop >= 0)
|
||||
{
|
||||
match = chunk[stop] == needle ? current + (ulong)stop : 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
offset += (ulong)length;
|
||||
continue;
|
||||
}
|
||||
|
||||
var nulIndex = chunk.IndexOf((byte)0);
|
||||
var searched = nulIndex >= 0 ? chunk[..(nulIndex + 1)] : chunk;
|
||||
var found = findLast ? searched.LastIndexOf(needle) : searched.IndexOf(needle);
|
||||
if (found >= 0)
|
||||
{
|
||||
match = current + (ulong)found;
|
||||
if (!findLast)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (nulIndex >= 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
offset += (ulong)length;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryScanCStringBytewise(
|
||||
CpuContext ctx,
|
||||
ulong address,
|
||||
byte needle,
|
||||
bool findLast,
|
||||
ulong remaining,
|
||||
ref ulong match)
|
||||
{
|
||||
Span<byte> current = stackalloc byte[1];
|
||||
for (ulong index = 0; index < 1_048_576; index++)
|
||||
for (ulong index = 0; index < remaining; index++)
|
||||
{
|
||||
if (!TryReadCompat(ctx, address + index, current))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (current[0] == needle)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = address + index;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
match = address + index;
|
||||
if (!findLast)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (current[0] == 0)
|
||||
|
|
@ -7434,8 +7524,7 @@ public static partial class KernelMemoryCompatExports
|
|||
}
|
||||
}
|
||||
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
return true;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
|
|
@ -7452,29 +7541,12 @@ public static partial class KernelMemoryCompatExports
|
|||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
|
||||
ulong match = 0;
|
||||
var found = false;
|
||||
Span<byte> current = stackalloc byte[1];
|
||||
for (ulong index = 0; index < 1_048_576; index++)
|
||||
if (!TryScanCString(ctx, address, needle, findLast: true, out var match))
|
||||
{
|
||||
if (!TryReadCompat(ctx, address + index, current))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
|
||||
if (current[0] == needle)
|
||||
{
|
||||
match = address + index;
|
||||
found = true;
|
||||
}
|
||||
|
||||
if (current[0] == 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
|
||||
ctx[CpuRegister.Rax] = found ? match : 0;
|
||||
ctx[CpuRegister.Rax] = match;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -12,8 +12,9 @@ namespace SharpEmu.Libs.Media;
|
|||
///
|
||||
/// Such a game never imports libSceVideodec or sceAvPlayer, so no HLE export
|
||||
/// can see its movie frames. Kernel file opens identify the active movie and
|
||||
/// the presenter requests BGRA frames from <see cref="FfmpegVideoDecoder"/> —
|
||||
/// the same decoder sceAvPlayer uses, so every format is handled in one place.
|
||||
/// the presenter requests frames from <see cref="FfmpegVideoDecoder"/> — the
|
||||
/// same decoder sceAvPlayer uses, so every format is handled in one place.
|
||||
/// Frames reach the presenter as <see cref="HostMovieYuv420"/> planes.
|
||||
/// </summary>
|
||||
internal static class HostMovieBridge
|
||||
{
|
||||
|
|
@ -46,6 +47,9 @@ internal static class HostMovieBridge
|
|||
private static long _frameSerial;
|
||||
private static uint _presentationWidth = MaxHostVideoWidth;
|
||||
private static uint _presentationHeight = MaxHostVideoHeight;
|
||||
// TimedOutMoviePaths.Count, readable without Gate: every guest file read asks
|
||||
// whether its file timed out, and almost none ever does.
|
||||
private static int _timedOutMovieCount;
|
||||
|
||||
internal static bool IsHostPlaybackActive
|
||||
{
|
||||
|
|
@ -132,6 +136,20 @@ internal static class HostMovieBridge
|
|||
out long frameSerial,
|
||||
out string hostPath)
|
||||
{
|
||||
// The render thread asks before every draw and dispatch. With no movie attached
|
||||
// it must not queue on Gate behind guest file reads (Demon's Souls: ~15 % of the
|
||||
// render thread blocked here). A movie attached meanwhile is picked up next call.
|
||||
if (Volatile.Read(ref _playback) is null && Volatile.Read(ref _frameBuffer) is null)
|
||||
{
|
||||
pixels = [];
|
||||
width = 0;
|
||||
height = 0;
|
||||
advanced = false;
|
||||
frameSerial = Volatile.Read(ref _frameSerial);
|
||||
hostPath = string.Empty;
|
||||
return false;
|
||||
}
|
||||
|
||||
lock (Gate)
|
||||
{
|
||||
pixels = [];
|
||||
|
|
@ -230,7 +248,7 @@ internal static class HostMovieBridge
|
|||
return;
|
||||
}
|
||||
|
||||
AttachPlaybackLocked(hostPath, info, source);
|
||||
AttachPlaybackLocked(hostPath, info, new HostMovieYuv420Decoder(source));
|
||||
Console.Error.WriteLine(
|
||||
"[LOADER][INFO] Bink2 bridge attached: " + Path.GetFileName(hostPath) + " " +
|
||||
info.Width + "x" + info.Height + " @ " +
|
||||
|
|
@ -285,9 +303,12 @@ internal static class HostMovieBridge
|
|||
CloseActiveLocked();
|
||||
_activePath = hostPath;
|
||||
_activeInfo = info;
|
||||
_frameBuffer = GC.AllocateUninitializedArray<byte>(GetFrameBufferLength(info));
|
||||
var bgra = GC.AllocateUninitializedArray<byte>(GetFrameBufferLength(info));
|
||||
FillDummyFrame(bgra, info.Width, info.Height);
|
||||
_frameBuffer = GC.AllocateUninitializedArray<byte>(
|
||||
HostMovieYuv420.FrameLength(info.Width, info.Height));
|
||||
HostMovieYuv420.ConvertFromBgra(bgra, info.Width, info.Height, _frameBuffer);
|
||||
_frameBufferPresented = false;
|
||||
FillDummyFrame(_frameBuffer, info.Width, info.Height);
|
||||
ArmPlaybackWatchdogLocked(hostPath);
|
||||
Console.Error.WriteLine(
|
||||
"[LOADER][INFO] Bink dummy attached: " + Path.GetFileName(hostPath) + " " +
|
||||
|
|
@ -327,6 +348,7 @@ internal static class HostMovieBridge
|
|||
"[LOADER][WARN] Bink2 host watchdog expired for " +
|
||||
Path.GetFileName(path) + "; advancing to the next movie.");
|
||||
TimedOutMoviePaths.Add(path);
|
||||
Volatile.Write(ref _timedOutMovieCount, TimedOutMoviePaths.Count);
|
||||
CloseActiveLocked();
|
||||
AttachNextQueuedMovieLocked();
|
||||
}
|
||||
|
|
@ -462,6 +484,11 @@ internal static class HostMovieBridge
|
|||
|
||||
internal static bool ShouldForceGuestMovieEof(string hostPath)
|
||||
{
|
||||
if (Volatile.Read(ref _timedOutMovieCount) == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
lock (Gate)
|
||||
{
|
||||
return TimedOutMoviePaths.Contains(hostPath);
|
||||
|
|
@ -550,6 +577,7 @@ internal static class HostMovieBridge
|
|||
lock (Gate)
|
||||
{
|
||||
TimedOutMoviePaths.Remove(hostPath);
|
||||
Volatile.Write(ref _timedOutMovieCount, TimedOutMoviePaths.Count);
|
||||
if (PendingMoviePathSet.Remove(hostPath))
|
||||
{
|
||||
var retained = PendingMoviePaths
|
||||
|
|
|
|||
|
|
@ -0,0 +1,167 @@
|
|||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace SharpEmu.Libs.Media;
|
||||
|
||||
/// <summary>
|
||||
/// Planar YUV 4:2:0 layout of a host movie frame: a full-resolution luma plane
|
||||
/// followed by one interleaved chroma pair per 2x2 block. It is the layout the
|
||||
/// guest's Bink shaders sample, so the presenter uploads the planes as-is.
|
||||
/// </summary>
|
||||
internal static unsafe class HostMovieYuv420
|
||||
{
|
||||
internal static int LumaLength(uint width, uint height) =>
|
||||
checked((int)((ulong)width * height));
|
||||
|
||||
internal static int ChromaLength(uint width, uint height) =>
|
||||
checked((int)((ulong)((width + 1) / 2) * ((height + 1) / 2) * 2));
|
||||
|
||||
internal static int FrameLength(uint width, uint height) =>
|
||||
checked(LumaLength(width, height) + ChromaLength(width, height));
|
||||
|
||||
/// <summary>
|
||||
/// Converts a BGRA frame into <paramref name="destination"/>: the luma
|
||||
/// plane, then the chroma plane. Each chroma pair averages its 2x2 block,
|
||||
/// clipped at odd edges.
|
||||
/// </summary>
|
||||
internal static void ConvertFromBgra(
|
||||
ReadOnlySpan<byte> bgra,
|
||||
uint width,
|
||||
uint height,
|
||||
Span<byte> destination)
|
||||
{
|
||||
if (width == 0 || height == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var lumaLength = LumaLength(width, height);
|
||||
if (bgra.Length < (long)lumaLength * 4 ||
|
||||
destination.Length < FrameLength(width, height))
|
||||
{
|
||||
throw new ArgumentException("Host movie frame buffers are too small.");
|
||||
}
|
||||
|
||||
fixed (byte* source = bgra, luma = destination)
|
||||
{
|
||||
ConvertRowPairs(source, (int)width, (int)height, luma, luma + lumaLength);
|
||||
}
|
||||
}
|
||||
|
||||
private static void ConvertRowPairs(
|
||||
byte* bgra,
|
||||
int width,
|
||||
int height,
|
||||
byte* luma,
|
||||
byte* chroma)
|
||||
{
|
||||
var chromaWidth = (width + 1) / 2;
|
||||
var rowPairs = (height + 1) / 2;
|
||||
for (var pair = 0; pair < rowPairs; pair++)
|
||||
{
|
||||
var y = pair * 2;
|
||||
var hasSecondRow = y + 1 < height;
|
||||
var row0 = bgra + (nint)y * width * 4;
|
||||
var row1 = row0 + (nint)width * 4;
|
||||
var luma0 = luma + (nint)y * width;
|
||||
var luma1 = luma0 + width;
|
||||
var chromaRow = chroma + (nint)pair * chromaWidth * 2;
|
||||
for (var x = 0; x < width; x += 2)
|
||||
{
|
||||
var hasSecondColumn = x + 1 < width;
|
||||
var pixel = row0 + x * 4;
|
||||
int blue = pixel[0], green = pixel[1], red = pixel[2];
|
||||
luma0[x] = LumaFromRgb(red, green, blue);
|
||||
var samples = 1;
|
||||
if (hasSecondColumn)
|
||||
{
|
||||
AccumulatePixel(pixel + 4, luma0 + x + 1, ref red, ref green, ref blue);
|
||||
samples++;
|
||||
}
|
||||
|
||||
if (hasSecondRow)
|
||||
{
|
||||
pixel = row1 + x * 4;
|
||||
AccumulatePixel(pixel, luma1 + x, ref red, ref green, ref blue);
|
||||
samples++;
|
||||
if (hasSecondColumn)
|
||||
{
|
||||
AccumulatePixel(pixel + 4, luma1 + x + 1, ref red, ref green, ref blue);
|
||||
samples++;
|
||||
}
|
||||
}
|
||||
|
||||
red /= samples;
|
||||
green /= samples;
|
||||
blue /= samples;
|
||||
var destination = chromaRow + x;
|
||||
destination[0] = ClampByte(
|
||||
((128 * red - 116 * green - 12 * blue + 128) >> 8) + 128);
|
||||
destination[1] = ClampByte(
|
||||
((-29 * red - 99 * green + 128 * blue + 128) >> 8) + 128);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static void AccumulatePixel(
|
||||
byte* pixel,
|
||||
byte* lumaDestination,
|
||||
ref int red,
|
||||
ref int green,
|
||||
ref int blue)
|
||||
{
|
||||
int b = pixel[0], g = pixel[1], r = pixel[2];
|
||||
*lumaDestination = LumaFromRgb(r, g, b);
|
||||
red += r;
|
||||
green += g;
|
||||
blue += b;
|
||||
}
|
||||
|
||||
// The weights sum to 256, so the result never exceeds 255.
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static byte LumaFromRgb(int red, int green, int blue) =>
|
||||
(byte)((54 * red + 183 * green + 19 * blue + 128) >> 8);
|
||||
|
||||
private static byte ClampByte(int value) => (byte)Math.Clamp(value, 0, 255);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts each decoded BGRA frame to <see cref="HostMovieYuv420"/> planes on
|
||||
/// the playback's decoder thread, so the render thread only copies them.
|
||||
/// </summary>
|
||||
internal sealed class HostMovieYuv420Decoder : IMediaFrameDecoder
|
||||
{
|
||||
private readonly IMediaFrameDecoder _inner;
|
||||
private readonly byte[] _bgra;
|
||||
|
||||
internal HostMovieYuv420Decoder(IMediaFrameDecoder inner)
|
||||
{
|
||||
_inner = inner;
|
||||
_bgra = GC.AllocateUninitializedArray<byte>(
|
||||
checked((int)((ulong)inner.Width * inner.Height * 4)));
|
||||
}
|
||||
|
||||
public uint Width => _inner.Width;
|
||||
|
||||
public uint Height => _inner.Height;
|
||||
|
||||
public uint FramesPerSecondNumerator => _inner.FramesPerSecondNumerator;
|
||||
|
||||
public uint FramesPerSecondDenominator => _inner.FramesPerSecondDenominator;
|
||||
|
||||
public bool TryDecodeNextFrame(Span<byte> destination)
|
||||
{
|
||||
if (!_inner.TryDecodeNextFrame(_bgra))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
HostMovieYuv420.ConvertFromBgra(_bgra, Width, Height, destination);
|
||||
return true;
|
||||
}
|
||||
|
||||
public void Dispose() => _inner.Dispose();
|
||||
}
|
||||
|
|
@ -23,6 +23,7 @@ public static class PadExports
|
|||
private const int PrimaryPadHandle = 1;
|
||||
private const int ControllerInformationSize = 0x1C;
|
||||
private const int PadDataSize = 0x78;
|
||||
private const float StandardGravity = 9.80665f;
|
||||
|
||||
// Monster Truck Championship reads pad state with handle 0 before it opens a pad,
|
||||
// and rejecting that leaves its controller/FFB init path polling forever. After a
|
||||
|
|
@ -44,7 +45,9 @@ public static class PadExports
|
|||
private static PadState _cachedInputState;
|
||||
|
||||
private static bool _initialized;
|
||||
private static int _motionSensorEnabled;
|
||||
// Motion data is reported until a title turns it off: Astro Bot reads it for
|
||||
// shake/tilt without ever importing scePadSetMotionSensorState.
|
||||
private static int _motionSensorEnabled = 1;
|
||||
private static int _controlsAnnouncementLogged;
|
||||
|
||||
[SysAbiExport(
|
||||
|
|
@ -182,6 +185,19 @@ public static class PadExports
|
|||
: ctx.SetReturn(OrbisPadErrorInvalidHandle);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "r44mAxdSG+U",
|
||||
ExportName = "scePadSetAngularVelocityDeadbandState",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libScePad")]
|
||||
public static int PadSetAngularVelocityDeadbandState(CpuContext ctx)
|
||||
{
|
||||
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
return IsPrimaryPadHandle(handle)
|
||||
? ctx.SetReturn(0)
|
||||
: ctx.SetReturn(OrbisPadErrorInvalidHandle);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "gjP9-KQzoUk",
|
||||
ExportName = "scePadGetControllerInformation",
|
||||
|
|
@ -676,9 +692,10 @@ public static class PadExports
|
|||
BinaryPrimitives.WriteSingleLittleEndian(data[0x18..], 1.0f);
|
||||
if (Volatile.Read(ref _motionSensorEnabled) != 0 && input.Motion.Available)
|
||||
{
|
||||
BinaryPrimitives.WriteSingleLittleEndian(data[0x1C..], input.Motion.AccelerationX);
|
||||
BinaryPrimitives.WriteSingleLittleEndian(data[0x20..], input.Motion.AccelerationY);
|
||||
BinaryPrimitives.WriteSingleLittleEndian(data[0x24..], input.Motion.AccelerationZ);
|
||||
// Host acceleration is m/s^2 (SDL); ScePadData.acceleration is in G.
|
||||
BinaryPrimitives.WriteSingleLittleEndian(data[0x1C..], input.Motion.AccelerationX / StandardGravity);
|
||||
BinaryPrimitives.WriteSingleLittleEndian(data[0x20..], input.Motion.AccelerationY / StandardGravity);
|
||||
BinaryPrimitives.WriteSingleLittleEndian(data[0x24..], input.Motion.AccelerationZ / StandardGravity);
|
||||
BinaryPrimitives.WriteSingleLittleEndian(data[0x28..], input.Motion.AngularVelocityX);
|
||||
BinaryPrimitives.WriteSingleLittleEndian(data[0x2C..], input.Motion.AngularVelocityY);
|
||||
BinaryPrimitives.WriteSingleLittleEndian(data[0x30..], input.Motion.AngularVelocityZ);
|
||||
|
|
|
|||
|
|
@ -49,6 +49,14 @@ public static partial class VideoOutExports
|
|||
private const int MaxLatencyHistoryEntries = 256;
|
||||
private const ulong SceVideoOutOutputModeDefault = 1;
|
||||
private const ulong SceVideoOutOutputMode119_88Hz = 0xF;
|
||||
// SHARPEMU_GUEST_REFRESH_RATE=<Hz> paces the emulated display (vblanks and flips) at
|
||||
// that rate instead of 60 Hz. A game that flips every vblank then runs faster than 60 fps;
|
||||
// one whose logic counts frames also runs faster.
|
||||
private static readonly uint GuestRefreshRate =
|
||||
uint.TryParse(Environment.GetEnvironmentVariable("SHARPEMU_GUEST_REFRESH_RATE"), out var rate) && rate is >= 30 and <= 1000
|
||||
? rate
|
||||
: 60;
|
||||
|
||||
private const ulong SceVideoOutRefreshRate59_94Hz = 3;
|
||||
private const ulong SceVideoOutRefreshRate119_88Hz = 13;
|
||||
private const ulong SceVideoOutPixelFormatA8R8G8B8Srgb = 0x80000000;
|
||||
|
|
@ -239,7 +247,7 @@ public static partial class VideoOutExports
|
|||
public int CurrentBuffer { get; set; } = -1;
|
||||
public uint OutputWidth { get; set; } = 1920;
|
||||
public uint OutputHeight { get; set; } = 1080;
|
||||
public uint RefreshRate { get; set; } = 60;
|
||||
public uint RefreshRate { get; set; } = GuestRefreshRate;
|
||||
public ulong OutputMode { get; set; } = SceVideoOutOutputModeDefault;
|
||||
public float Gamma { get; set; } = 1.0f;
|
||||
public Dictionary<long, long> LatencyStartPoints { get; } = new();
|
||||
|
|
|
|||
|
|
@ -64,6 +64,8 @@ internal static unsafe partial class VulkanVideoPresenter
|
|||
public Format Format;
|
||||
public Image Image;
|
||||
public DeviceMemory Memory;
|
||||
// Made by CreateGuestFlipSnapshot: its image goes back to the snapshot pool.
|
||||
public bool FromSnapshotPool;
|
||||
}
|
||||
|
||||
// A cached color target bound to one draw or resolve.
|
||||
|
|
@ -1011,7 +1013,7 @@ internal static unsafe partial class VulkanVideoPresenter
|
|||
}
|
||||
}
|
||||
|
||||
private (VkBuffer Buffer, DeviceMemory Memory) CreateTextureStagingBuffer(byte[] pixels, string debugName)
|
||||
private (VkBuffer Buffer, DeviceMemory Memory) CreateTextureStagingBuffer(ReadOnlySpan<byte> pixels, string debugName)
|
||||
{
|
||||
var buffer = CreateHostBuffer(pixels, BufferUsageFlags.TransferSrcBit, out var memory, out _);
|
||||
SetDebugName(ObjectType.Buffer, buffer.Handle, debugName);
|
||||
|
|
@ -1109,6 +1111,13 @@ internal static unsafe partial class VulkanVideoPresenter
|
|||
|
||||
private void DestroyGuestImage(GuestImageResource resource)
|
||||
{
|
||||
if (resource.FromSnapshotPool && resource.Image.Handle != 0 && ReturnFlipSnapshot(resource))
|
||||
{
|
||||
resource.Image = default;
|
||||
resource.Memory = default;
|
||||
return;
|
||||
}
|
||||
|
||||
if (resource.Image.Handle != 0)
|
||||
{
|
||||
_vk.DestroyImage(_device, resource.Image, null);
|
||||
|
|
|
|||
|
|
@ -456,6 +456,20 @@ internal static unsafe partial class VulkanVideoPresenter
|
|||
}
|
||||
}
|
||||
|
||||
// RenderExecutor records indirect workgroup dispatches with vkCmdDispatchIndirect.
|
||||
// SHARPEMU_CPU_INDIRECT_DISPATCH=1 reads the counts back on the CPU as before.
|
||||
public bool ResolvesIndirectDispatchOnGpu => !_cpuIndirectDispatch;
|
||||
|
||||
// RenderExecutor records indexed indirect draws with vkCmdDrawIndexedIndirect.
|
||||
// SHARPEMU_CPU_INDIRECT_DRAW=1 reads the arguments back on the CPU as before.
|
||||
public bool ResolvesIndirectDrawOnGpu => !_cpuIndirectDraw;
|
||||
|
||||
private static readonly bool _cpuIndirectDraw = string.Equals(
|
||||
Environment.GetEnvironmentVariable("SHARPEMU_CPU_INDIRECT_DRAW"), "1", StringComparison.Ordinal);
|
||||
|
||||
private static readonly bool _cpuIndirectDispatch = string.Equals(
|
||||
Environment.GetEnvironmentVariable("SHARPEMU_CPU_INDIRECT_DISPATCH"), "1", StringComparison.Ordinal);
|
||||
|
||||
public void DispatchDirect(ulong submitId, uint groupsX, uint groupsY, uint groupsZ, uint dispatchInitiator, ulong indirectArgumentsAddress = 0)
|
||||
{
|
||||
using var translationScope = RenderPhaseProfile.MeasureDetail(RenderPhaseProfile.Phase.CommandDispatchTranslation);
|
||||
|
|
|
|||
|
|
@ -516,6 +516,12 @@ internal static unsafe partial class VulkanVideoPresenter
|
|||
throw SubmissionScheduler.Fatal($"A device-address range is outside the cache: handle={range.Handle} base=0x{range.Base:X16} size=0x{range.Size:X} hash=0x{program.Hash:X16}.");
|
||||
}
|
||||
|
||||
// Unmapped read-only pointers resolve to zero through the page table.
|
||||
if (!range.Written && !_guestMemory.CanRead(range.Base, 1))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var size = ClampMappedSize(range.Base, range.Size);
|
||||
if (range.Written)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ internal static unsafe partial class VulkanVideoPresenter
|
|||
private DebugUtilsMessengerEXT _debugMessenger;
|
||||
private ExtDebugUtils? _debugUtils;
|
||||
private PhysicalDevice _physicalDevice;
|
||||
private uint _physicalDeviceVendorId;
|
||||
private uint _maxComputeWorkGroupCountX;
|
||||
private uint _maxComputeWorkGroupCountY;
|
||||
private uint _maxComputeWorkGroupCountZ;
|
||||
|
|
@ -557,6 +558,7 @@ internal static unsafe partial class VulkanVideoPresenter
|
|||
|
||||
LoadComputeDeviceLimits();
|
||||
_vk.GetPhysicalDeviceProperties(_physicalDevice, out var selected);
|
||||
_physicalDeviceVendorId = selected.VendorID;
|
||||
_maxColorAttachments = selected.Limits.MaxColorAttachments;
|
||||
var selectedName = SilkMarshal.PtrToString((nint)selected.DeviceName) ?? "unknown";
|
||||
Console.Error.WriteLine(
|
||||
|
|
@ -592,6 +594,12 @@ internal static unsafe partial class VulkanVideoPresenter
|
|||
};
|
||||
_vk.GetPhysicalDeviceProperties2(_physicalDevice, &properties2);
|
||||
SetNativeSubgroupCapabilities(subgroup.SubgroupSize, subgroup.SupportedStages);
|
||||
_canRequireComputeSubgroup32 =
|
||||
subgroup.SubgroupSize != RdnaSubgroupSize &&
|
||||
subgroupSizeControl.MinSubgroupSize <= RdnaSubgroupSize &&
|
||||
subgroupSizeControl.MaxSubgroupSize >= RdnaSubgroupSize &&
|
||||
(subgroupSizeControl.RequiredSubgroupSizeStages & ShaderStageFlags.ComputeBit) != 0;
|
||||
_maxComputeWorkgroupSubgroups = subgroupSizeControl.MaxComputeWorkgroupSubgroups;
|
||||
_maxPushDescriptors = pushDescriptorProperties.MaxPushDescriptors;
|
||||
_noAttachmentSampleCounts = properties.Limits.FramebufferNoAttachmentsSampleCounts;
|
||||
_maxComputeWorkGroupCountX = properties.Limits.MaxComputeWorkGroupCount[0];
|
||||
|
|
@ -647,20 +655,80 @@ internal static unsafe partial class VulkanVideoPresenter
|
|||
$"mode={graphicsSubgroupMode} compute_subgroups=unchanged");
|
||||
}
|
||||
|
||||
// A second queue family for buffer readbacks (see VulkanAsyncReadback): a transfer-only
|
||||
// family first, then a compute family. SHARPEMU_ASYNC_READBACK=0 keeps one queue.
|
||||
private uint? _readbackQueueFamilyIndex;
|
||||
private Queue _readbackQueue;
|
||||
|
||||
private uint? SelectReadbackQueueFamily()
|
||||
{
|
||||
if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_ASYNC_READBACK"), "0", StringComparison.Ordinal))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
uint count = 0;
|
||||
_vk.GetPhysicalDeviceQueueFamilyProperties(_physicalDevice, &count, null);
|
||||
var families = new QueueFamilyProperties[count];
|
||||
fixed (QueueFamilyProperties* pointer = families)
|
||||
{
|
||||
_vk.GetPhysicalDeviceQueueFamilyProperties(_physicalDevice, &count, pointer);
|
||||
}
|
||||
|
||||
uint? compute = null;
|
||||
for (uint index = 0; index < count; index++)
|
||||
{
|
||||
var flags = families[index].QueueFlags;
|
||||
if (index == _queueFamilyIndex || families[index].QueueCount == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if ((flags & QueueFlags.TransferBit) != 0 && (flags & (QueueFlags.GraphicsBit | QueueFlags.ComputeBit)) == 0)
|
||||
{
|
||||
return index;
|
||||
}
|
||||
|
||||
if ((flags & QueueFlags.ComputeBit) != 0 && (flags & QueueFlags.GraphicsBit) == 0)
|
||||
{
|
||||
compute ??= index;
|
||||
}
|
||||
}
|
||||
|
||||
return compute;
|
||||
}
|
||||
|
||||
private bool _supportsFragmentShaderBarycentric;
|
||||
private bool _supportsPerVertexPixelInputs;
|
||||
private const string FragmentShaderBarycentricExtensionName = "VK_KHR_fragment_shader_barycentric";
|
||||
private const string Maintenance5ExtensionName = "VK_KHR_maintenance5";
|
||||
private const string ImageViewMinLodExtensionName = "VK_EXT_image_view_min_lod";
|
||||
private bool _supportsImageViewMinLod;
|
||||
|
||||
private void CreateDevice()
|
||||
{
|
||||
var priority = 1.0f;
|
||||
var queueInfo = new DeviceQueueCreateInfo
|
||||
var queueInfos = stackalloc DeviceQueueCreateInfo[2];
|
||||
queueInfos[0] = new DeviceQueueCreateInfo
|
||||
{
|
||||
SType = StructureType.DeviceQueueCreateInfo,
|
||||
QueueFamilyIndex = _queueFamilyIndex,
|
||||
QueueCount = 1,
|
||||
PQueuePriorities = &priority,
|
||||
};
|
||||
var queueInfoCount = 1u;
|
||||
_readbackQueueFamilyIndex = SelectReadbackQueueFamily();
|
||||
if (_readbackQueueFamilyIndex is { } readbackFamily)
|
||||
{
|
||||
queueInfos[1] = new DeviceQueueCreateInfo
|
||||
{
|
||||
SType = StructureType.DeviceQueueCreateInfo,
|
||||
QueueFamilyIndex = readbackFamily,
|
||||
QueueCount = 1,
|
||||
PQueuePriorities = &priority,
|
||||
};
|
||||
queueInfoCount = 2;
|
||||
}
|
||||
_vk.GetPhysicalDeviceFeatures(_physicalDevice, out var supportedFeatures);
|
||||
_supportsIndependentBlend = supportedFeatures.IndependentBlend;
|
||||
_supportsDepthBiasClamp = supportedFeatures.DepthBiasClamp;
|
||||
|
|
@ -756,6 +824,40 @@ internal static unsafe partial class VulkanVideoPresenter
|
|||
_supportsFragmentShaderBarycentric = barycentricFeatures.FragmentShaderBarycentric;
|
||||
}
|
||||
|
||||
// With maintenance5 a vertex program that writes no PointSize draws 1-pixel points. The
|
||||
// guest sizes points from PA_SU_POINT_SIZE, which is not modelled; without the feature a
|
||||
// point-list pipeline whose program does not export a size is invalid.
|
||||
var maintenance5Features = new PhysicalDeviceMaintenance5FeaturesKHR
|
||||
{
|
||||
SType = StructureType.PhysicalDeviceMaintenance5FeaturesKhr,
|
||||
};
|
||||
var supportsMaintenance5 = false;
|
||||
if (IsDeviceExtensionAvailable(Maintenance5ExtensionName))
|
||||
{
|
||||
var maintenance5Query = new PhysicalDeviceFeatures2
|
||||
{
|
||||
SType = StructureType.PhysicalDeviceFeatures2,
|
||||
PNext = &maintenance5Features,
|
||||
};
|
||||
_vk.GetPhysicalDeviceFeatures2(_physicalDevice, &maintenance5Query);
|
||||
supportsMaintenance5 = maintenance5Features.Maintenance5;
|
||||
}
|
||||
|
||||
var imageViewMinLodFeatures = new PhysicalDeviceImageViewMinLodFeaturesEXT
|
||||
{
|
||||
SType = StructureType.PhysicalDeviceImageViewMinLodFeaturesExt,
|
||||
};
|
||||
if (IsDeviceExtensionAvailable(ImageViewMinLodExtensionName))
|
||||
{
|
||||
var imageViewMinLodQuery = new PhysicalDeviceFeatures2
|
||||
{
|
||||
SType = StructureType.PhysicalDeviceFeatures2,
|
||||
PNext = &imageViewMinLodFeatures,
|
||||
};
|
||||
_vk.GetPhysicalDeviceFeatures2(_physicalDevice, &imageViewMinLodQuery);
|
||||
_supportsImageViewMinLod = imageViewMinLodFeatures.MinLod;
|
||||
}
|
||||
|
||||
// MoltenVK exposes the barycentric builtins, but SPIRV-Cross rejects PerVertexKHR inputs.
|
||||
_supportsPerVertexPixelInputs = _supportsFragmentShaderBarycentric &&
|
||||
!IsDeviceExtensionAvailable(PortabilitySubsetExtensionName);
|
||||
|
|
@ -818,6 +920,7 @@ internal static unsafe partial class VulkanVideoPresenter
|
|||
var supportsRobustImageAccess2 = robustness2Features.RobustImageAccess2;
|
||||
var supportsNullDescriptor = robustness2Features.NullDescriptor;
|
||||
var supportsRobustness2 = supportsRobustImageAccess2 || supportsNullDescriptor;
|
||||
_canRequireComputeSubgroup32 &= vulkan13Features.SubgroupSizeControl;
|
||||
SetSharedInt64AtomicsCapability(supportsSharedInt64Atomics);
|
||||
if (!supportsSharedInt64Atomics)
|
||||
{
|
||||
|
|
@ -862,9 +965,11 @@ internal static unsafe partial class VulkanVideoPresenter
|
|||
var depthClipEnableExtension = (byte*)SilkMarshal.StringToPtr(DepthClipEnableExtensionName);
|
||||
var barycentricExtension = (byte*)SilkMarshal.StringToPtr(FragmentShaderBarycentricExtensionName);
|
||||
var viewportIndexLayerExtension = (byte*)SilkMarshal.StringToPtr("VK_EXT_shader_viewport_index_layer");
|
||||
var maintenance5Extension = (byte*)SilkMarshal.StringToPtr(Maintenance5ExtensionName);
|
||||
var imageViewMinLodExtension = (byte*)SilkMarshal.StringToPtr(ImageViewMinLodExtensionName);
|
||||
try
|
||||
{
|
||||
var extensions = stackalloc byte*[12];
|
||||
var extensions = stackalloc byte*[14];
|
||||
var extensionCount = 0u;
|
||||
extensions[extensionCount++] = swapchainExtension;
|
||||
extensions[extensionCount++] = pushDescriptorExtension;
|
||||
|
|
@ -896,6 +1001,16 @@ internal static unsafe partial class VulkanVideoPresenter
|
|||
extensions[extensionCount++] = maintenance8Extension;
|
||||
}
|
||||
|
||||
if (supportsMaintenance5)
|
||||
{
|
||||
extensions[extensionCount++] = maintenance5Extension;
|
||||
}
|
||||
|
||||
if (_supportsImageViewMinLod)
|
||||
{
|
||||
extensions[extensionCount++] = imageViewMinLodExtension;
|
||||
}
|
||||
|
||||
if (supportsRobustness2)
|
||||
{
|
||||
extensions[extensionCount++] = robustness2Extension;
|
||||
|
|
@ -954,6 +1069,26 @@ internal static unsafe partial class VulkanVideoPresenter
|
|||
barycentricFeatures.PNext = renderingChain;
|
||||
renderingChain = &barycentricFeatures;
|
||||
}
|
||||
if (supportsMaintenance5)
|
||||
{
|
||||
maintenance5Features = new PhysicalDeviceMaintenance5FeaturesKHR
|
||||
{
|
||||
SType = StructureType.PhysicalDeviceMaintenance5FeaturesKhr,
|
||||
Maintenance5 = true,
|
||||
PNext = renderingChain,
|
||||
};
|
||||
renderingChain = &maintenance5Features;
|
||||
}
|
||||
if (_supportsImageViewMinLod)
|
||||
{
|
||||
imageViewMinLodFeatures = new PhysicalDeviceImageViewMinLodFeaturesEXT
|
||||
{
|
||||
SType = StructureType.PhysicalDeviceImageViewMinLodFeaturesExt,
|
||||
MinLod = true,
|
||||
PNext = renderingChain,
|
||||
};
|
||||
renderingChain = &imageViewMinLodFeatures;
|
||||
}
|
||||
if (_supportsDepthClipEnable)
|
||||
{
|
||||
depthClipEnableFeatures = new PhysicalDeviceDepthClipEnableFeaturesEXT
|
||||
|
|
@ -992,6 +1127,7 @@ internal static unsafe partial class VulkanVideoPresenter
|
|||
SType = StructureType.PhysicalDeviceVulkan13Features,
|
||||
DynamicRendering = true,
|
||||
Synchronization2 = true,
|
||||
SubgroupSizeControl = _canRequireComputeSubgroup32,
|
||||
PNext = renderingChain,
|
||||
};
|
||||
var features2 = new PhysicalDeviceFeatures2
|
||||
|
|
@ -1004,8 +1140,8 @@ internal static unsafe partial class VulkanVideoPresenter
|
|||
{
|
||||
SType = StructureType.DeviceCreateInfo,
|
||||
PNext = &features2,
|
||||
QueueCreateInfoCount = 1,
|
||||
PQueueCreateInfos = &queueInfo,
|
||||
QueueCreateInfoCount = queueInfoCount,
|
||||
PQueueCreateInfos = queueInfos,
|
||||
EnabledExtensionCount = extensionCount,
|
||||
PpEnabledExtensionNames = extensions,
|
||||
};
|
||||
|
|
@ -1016,6 +1152,8 @@ internal static unsafe partial class VulkanVideoPresenter
|
|||
{
|
||||
SilkMarshal.Free((nint)swapchainExtension);
|
||||
SilkMarshal.Free((nint)maintenance8Extension);
|
||||
SilkMarshal.Free((nint)maintenance5Extension);
|
||||
SilkMarshal.Free((nint)imageViewMinLodExtension);
|
||||
SilkMarshal.Free((nint)robustness2Extension);
|
||||
SilkMarshal.Free((nint)portabilitySubsetExtension);
|
||||
SilkMarshal.Free((nint)colorWriteEnableExtension);
|
||||
|
|
@ -1027,7 +1165,13 @@ internal static unsafe partial class VulkanVideoPresenter
|
|||
}
|
||||
|
||||
_vk.GetDeviceQueue(_device, _queueFamilyIndex, 0, out _queue);
|
||||
_deviceInfo = new GpuDeviceInfo(_vk, _physicalDevice, _device);
|
||||
_deviceInfo = new GpuDeviceInfo(_vk, _physicalDevice, _device) { ImageViewMinLodSupported = _supportsImageViewMinLod };
|
||||
if (_readbackQueueFamilyIndex is { } readbackQueueFamily)
|
||||
{
|
||||
_vk.GetDeviceQueue(_device, readbackQueueFamily, 0, out _readbackQueue);
|
||||
_deviceInfo.SharedQueueFamilies = [_queueFamilyIndex, readbackQueueFamily];
|
||||
Console.Error.WriteLine($"[LOADER][INFO] Vulkan async readback queue: family={readbackQueueFamily} (main family={_queueFamilyIndex})");
|
||||
}
|
||||
CreateScheduler();
|
||||
CreateBufferCache();
|
||||
CreateImageCache();
|
||||
|
|
|
|||
|
|
@ -34,6 +34,8 @@ internal static unsafe partial class VulkanVideoPresenter
|
|||
_descriptorHeap.Dispose();
|
||||
_imageCache.Dispose();
|
||||
_samplerStore.Dispose();
|
||||
_bufferCache.AsyncReadback?.Dispose();
|
||||
_bufferCache.AsyncReadback = null;
|
||||
_bufferCache.Dispose();
|
||||
PerfOverlay.SetGuestCacheStatistics(0, 0, _deviceInfo.LiveAllocations, _deviceInfo.PeakAllocations);
|
||||
_hostBufferPool.Dispose();
|
||||
|
|
@ -46,6 +48,7 @@ internal static unsafe partial class VulkanVideoPresenter
|
|||
{
|
||||
DestroyGuestImage(deferredVersion.Image);
|
||||
}
|
||||
DestroyFlipSnapshotPool();
|
||||
DestroySwapchainResources();
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][INFO] vk.device_memory live_allocations={_deviceInfo.LiveAllocations} " +
|
||||
|
|
@ -53,6 +56,7 @@ internal static unsafe partial class VulkanVideoPresenter
|
|||
if (_device.Handle != 0)
|
||||
{
|
||||
_scheduler.Dispose();
|
||||
_deviceInfo.Slabs.Destroy();
|
||||
if (_pipelineCache.Handle != 0)
|
||||
{
|
||||
_vk.DestroyPipelineCache(_device, _pipelineCache, null);
|
||||
|
|
|
|||
|
|
@ -14,7 +14,13 @@ internal static unsafe partial class VulkanVideoPresenter
|
|||
{
|
||||
// This partial owns guest GPU submission execution and lifetime.
|
||||
|
||||
private const int MaxInFlightGuestSubmissions = 8;
|
||||
// Batched submissions each carry up to a frame's worth of work, so a cap of
|
||||
// 8 made draw preparation wait on the GPU once per frame; 32 keeps the CPU
|
||||
// ahead. SHARPEMU_MAX_INFLIGHT_SUBMISSIONS overrides it.
|
||||
private static readonly int MaxInFlightGuestSubmissions =
|
||||
int.TryParse(Environment.GetEnvironmentVariable("SHARPEMU_MAX_INFLIGHT_SUBMISSIONS"), out var inFlight) && inFlight > 0
|
||||
? inFlight
|
||||
: 32;
|
||||
// Scheduler ticks: the last submitted tick and the highest tick known retired.
|
||||
private ulong _submitTimeline;
|
||||
private ulong _completedTimeline;
|
||||
|
|
|
|||
|
|
@ -117,12 +117,9 @@ internal static unsafe partial class VulkanVideoPresenter
|
|||
private uint _hostMovieChromaImageDstSelect;
|
||||
private bool _hostMovieChromaImageInitialized;
|
||||
private byte[]? _hostMovieFramePixels;
|
||||
private byte[]? _hostMovieLumaPixels;
|
||||
private byte[]? _hostMovieChromaPixels;
|
||||
private uint _hostMovieFrameWidth;
|
||||
private uint _hostMovieFrameHeight;
|
||||
private long _hostMovieFrameSerial;
|
||||
private long _hostMovieConvertedFrameSerial = -1;
|
||||
private long _hostMovieLumaUploadedFrameSerial = -1;
|
||||
private long _hostMovieChromaUploadedFrameSerial = -1;
|
||||
private string? _hostMovieFramePath;
|
||||
|
|
@ -167,7 +164,6 @@ internal static unsafe partial class VulkanVideoPresenter
|
|||
_hostMovieChromaTextureAddress = 0;
|
||||
_hostMovieLumaDstSelect = 0;
|
||||
_hostMovieChromaDstSelect = 0;
|
||||
_hostMovieConvertedFrameSerial = -1;
|
||||
_hostMovieLumaUploadedFrameSerial = -1;
|
||||
_hostMovieChromaUploadedFrameSerial = -1;
|
||||
}
|
||||
|
|
@ -303,9 +299,15 @@ internal static unsafe partial class VulkanVideoPresenter
|
|||
GuestDrawTexture texture,
|
||||
int plane)
|
||||
{
|
||||
EnsureHostMovieYuvFrame();
|
||||
var isLuma = plane == 0;
|
||||
var pixels = isLuma ? _hostMovieLumaPixels! : _hostMovieChromaPixels!;
|
||||
var frame = _hostMovieFramePixels ??
|
||||
throw new InvalidOperationException("Host movie frame is unavailable.");
|
||||
var lumaLength = HostMovieYuv420.LumaLength(_hostMovieFrameWidth, _hostMovieFrameHeight);
|
||||
var pixels = isLuma
|
||||
? frame.AsSpan(0, lumaLength)
|
||||
: frame.AsSpan(
|
||||
lumaLength,
|
||||
HostMovieYuv420.ChromaLength(_hostMovieFrameWidth, _hostMovieFrameHeight));
|
||||
var width = isLuma
|
||||
? _hostMovieFrameWidth
|
||||
: (_hostMovieFrameWidth + 1) / 2;
|
||||
|
|
@ -352,93 +354,6 @@ internal static unsafe partial class VulkanVideoPresenter
|
|||
};
|
||||
}
|
||||
|
||||
private void EnsureHostMovieYuvFrame()
|
||||
{
|
||||
if (_hostMovieConvertedFrameSerial == _hostMovieFrameSerial)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var bgra = _hostMovieFramePixels ??
|
||||
throw new InvalidOperationException("Host movie frame is unavailable.");
|
||||
var width = checked((int)_hostMovieFrameWidth);
|
||||
var height = checked((int)_hostMovieFrameHeight);
|
||||
var chromaWidth = (width + 1) / 2;
|
||||
var chromaHeight = (height + 1) / 2;
|
||||
if (_hostMovieLumaPixels?.Length != width * height)
|
||||
{
|
||||
_hostMovieLumaPixels = GC.AllocateUninitializedArray<byte>(width * height);
|
||||
}
|
||||
if (_hostMovieChromaPixels?.Length != chromaWidth * chromaHeight * 2)
|
||||
{
|
||||
_hostMovieChromaPixels =
|
||||
GC.AllocateUninitializedArray<byte>(chromaWidth * chromaHeight * 2);
|
||||
}
|
||||
|
||||
ConvertBgraToYuv420(
|
||||
bgra,
|
||||
width,
|
||||
height,
|
||||
_hostMovieLumaPixels,
|
||||
_hostMovieChromaPixels);
|
||||
_hostMovieConvertedFrameSerial = _hostMovieFrameSerial;
|
||||
}
|
||||
|
||||
internal static void ConvertBgraToYuv420(
|
||||
ReadOnlySpan<byte> bgra,
|
||||
int width,
|
||||
int height,
|
||||
Span<byte> luma,
|
||||
Span<byte> chroma)
|
||||
{
|
||||
var chromaWidth = (width + 1) / 2;
|
||||
for (var y = 0; y < height; y++)
|
||||
{
|
||||
for (var x = 0; x < width; x++)
|
||||
{
|
||||
var source = (y * width + x) * 4;
|
||||
var b = bgra[source];
|
||||
var g = bgra[source + 1];
|
||||
var r = bgra[source + 2];
|
||||
luma[y * width + x] = ClampByte(
|
||||
(54 * r + 183 * g + 19 * b + 128) >> 8);
|
||||
}
|
||||
}
|
||||
|
||||
for (var y = 0; y < height; y += 2)
|
||||
{
|
||||
for (var x = 0; x < width; x += 2)
|
||||
{
|
||||
var red = 0;
|
||||
var green = 0;
|
||||
var blue = 0;
|
||||
var samples = 0;
|
||||
for (var sampleY = y; sampleY < Math.Min(y + 2, height); sampleY++)
|
||||
{
|
||||
for (var sampleX = x; sampleX < Math.Min(x + 2, width); sampleX++)
|
||||
{
|
||||
var source = (sampleY * width + sampleX) * 4;
|
||||
blue += bgra[source];
|
||||
green += bgra[source + 1];
|
||||
red += bgra[source + 2];
|
||||
samples++;
|
||||
}
|
||||
}
|
||||
|
||||
red /= samples;
|
||||
green /= samples;
|
||||
blue /= samples;
|
||||
var destination = ((y / 2) * chromaWidth + x / 2) * 2;
|
||||
chroma[destination] = ClampByte(
|
||||
((128 * red - 116 * green - 12 * blue + 128) >> 8) + 128);
|
||||
chroma[destination + 1] = ClampByte(
|
||||
((-29 * red - 99 * green + 128 * blue + 128) >> 8) + 128);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static byte ClampByte(int value) => (byte)Math.Clamp(value, 0, 255);
|
||||
|
||||
private void EnsureHostMovieImages(
|
||||
uint lumaWidth,
|
||||
uint lumaHeight,
|
||||
|
|
|
|||
|
|
@ -64,9 +64,26 @@ internal static unsafe partial class VulkanVideoPresenter
|
|||
// Wave64 compute runs natively when the device's subgroup is that wide; smaller devices emulate it.
|
||||
bool IShaderPipelineHost.ComputeWave64Supported => Volatile.Read(ref _nativeSubgroupSize) >= 64;
|
||||
|
||||
// Only a 64-invocation wave64 workgroup is translated for either host subgroup width. Every
|
||||
// other compute translation maps a guest wave to 32-lane host subgroups, and a 64-lane host
|
||||
// subgroup (AMD's default) left lanes 32..63 inactive: a wave64 8x8x8 group lost rows 4..7.
|
||||
private const uint RdnaSubgroupSize = 32;
|
||||
private bool _canRequireComputeSubgroup32;
|
||||
private uint _maxComputeWorkgroupSubgroups;
|
||||
|
||||
private bool RequiresComputeSubgroup32(ComputeInputInfo input)
|
||||
{
|
||||
var invocations = (ulong)Math.Max(input.ThreadsX, 1) * Math.Max(input.ThreadsY, 1) * Math.Max(input.ThreadsZ, 1);
|
||||
return _canRequireComputeSubgroup32 &&
|
||||
!(input.WaveSize == 64 && invocations == 64) &&
|
||||
invocations <= (ulong)_maxComputeWorkgroupSubgroups * RdnaSubgroupSize;
|
||||
}
|
||||
|
||||
bool IShaderPipelineHost.GraphicsSubgroupOperationsEnabled => GraphicsSubgroupOperationsEnabled;
|
||||
|
||||
bool IShaderPipelineHost.SharedInt64AtomicsEnabled => SharedInt64AtomicsEnabled;
|
||||
// NVIDIA's compiler rejects the elided-EXEC wave64 compute module with NVVM error 3.
|
||||
bool IShaderPipelineHost.ExecGuardElisionEnabled => _physicalDeviceVendorId != NvidiaVendorId;
|
||||
bool IShaderPipelineHost.PerVertexPixelInputsSupported => _supportsPerVertexPixelInputs;
|
||||
|
||||
RenderHostLimits IShaderPipelineHost.Limits => _renderHostLimits;
|
||||
|
|
@ -114,7 +131,7 @@ internal static unsafe partial class VulkanVideoPresenter
|
|||
return false;
|
||||
}
|
||||
|
||||
if (_bufferCache.HasGpuDirtyPages(address, sizeof(uint)) ||
|
||||
if ((_bufferCache.MayHaveGpuDirtyPages(address, sizeof(uint)) && _bufferCache.HasGpuDirtyPages(address, sizeof(uint))) ||
|
||||
_bufferCache.HasGpuDirtyBytes(address, sizeof(uint)) ||
|
||||
_imageCache.HasGpuModifiedImageBytes(address, sizeof(uint)))
|
||||
{
|
||||
|
|
@ -776,9 +793,15 @@ internal static unsafe partial class VulkanVideoPresenter
|
|||
Pipeline pipeline;
|
||||
try
|
||||
{
|
||||
var requiredSubgroupSize = new PipelineShaderStageRequiredSubgroupSizeCreateInfo
|
||||
{
|
||||
SType = StructureType.PipelineShaderStageRequiredSubgroupSizeCreateInfo,
|
||||
RequiredSubgroupSize = RdnaSubgroupSize,
|
||||
};
|
||||
var stageInfo = new PipelineShaderStageCreateInfo
|
||||
{
|
||||
SType = StructureType.PipelineShaderStageCreateInfo,
|
||||
PNext = RequiresComputeSubgroup32(description.Input) ? &requiredSubgroupSize : null,
|
||||
Stage = ShaderStageFlags.ComputeBit,
|
||||
Module = computeModule,
|
||||
PName = entryPoint,
|
||||
|
|
@ -789,7 +812,8 @@ internal static unsafe partial class VulkanVideoPresenter
|
|||
Stage = stageInfo,
|
||||
Layout = layout,
|
||||
};
|
||||
Check(_vk.CreateComputePipelines(_device, _pipelineCache, 1, &pipelineInfo, null, out pipeline), "vkCreateComputePipelines(rendering)");
|
||||
Check(_vk.CreateComputePipelines(_device, _pipelineCache, 1, &pipelineInfo, null, out pipeline),
|
||||
$"vkCreateComputePipelines(rendering) hash=0x{description.Stage.Hash:X16}");
|
||||
MarkPipelineCacheDirty();
|
||||
Interlocked.Increment(ref _perfPipelineCreations);
|
||||
SetDebugName(ObjectType.Pipeline, pipeline.Handle, $"SharpEmu compute cs=0x{description.Stage.Hash:X16}");
|
||||
|
|
|
|||
|
|
@ -709,8 +709,62 @@ internal static unsafe partial class VulkanVideoPresenter
|
|||
return destination;
|
||||
}
|
||||
// Captures the display surface through the store in queue order; presentation reads the copy.
|
||||
// Flip snapshots come and go every frame at the same few sizes. A retired one (its
|
||||
// frame slot waited, so the GPU is done with it) is kept for the next flip of the
|
||||
// same format and size instead of another vkCreateImage and vkAllocateMemory; the
|
||||
// copy into it starts again from the undefined layout.
|
||||
private const int MaxPooledFlipSnapshots = 4;
|
||||
private readonly Dictionary<(Format Format, uint Width, uint Height), Stack<(Image Image, DeviceMemory Memory)>> _flipSnapshotPool = new();
|
||||
|
||||
private bool ReturnFlipSnapshot(GuestImageResource resource)
|
||||
{
|
||||
var key = (resource.Format, resource.Width, resource.Height);
|
||||
if (!_flipSnapshotPool.TryGetValue(key, out var pooled))
|
||||
{
|
||||
pooled = new Stack<(Image, DeviceMemory)>();
|
||||
_flipSnapshotPool.Add(key, pooled);
|
||||
}
|
||||
|
||||
if (pooled.Count >= MaxPooledFlipSnapshots)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
pooled.Push((resource.Image, resource.Memory));
|
||||
return true;
|
||||
}
|
||||
|
||||
private void DestroyFlipSnapshotPool()
|
||||
{
|
||||
foreach (var pooled in _flipSnapshotPool.Values)
|
||||
{
|
||||
while (pooled.TryPop(out var entry))
|
||||
{
|
||||
_vk.DestroyImage(_device, entry.Image, null);
|
||||
_deviceInfo.FreeMemory(entry.Memory);
|
||||
}
|
||||
}
|
||||
|
||||
_flipSnapshotPool.Clear();
|
||||
}
|
||||
|
||||
private GuestImageResource CreateGuestFlipSnapshot(Format format, uint width, uint height, ulong address, long version)
|
||||
{
|
||||
if (_flipSnapshotPool.TryGetValue((format, width, height), out var pooled) && pooled.TryPop(out var reused))
|
||||
{
|
||||
return new GuestImageResource
|
||||
{
|
||||
Address = address,
|
||||
FlipVersion = version,
|
||||
Width = width,
|
||||
Height = height,
|
||||
Format = format,
|
||||
Image = reused.Image,
|
||||
Memory = reused.Memory,
|
||||
FromSnapshotPool = true,
|
||||
};
|
||||
}
|
||||
|
||||
var imageInfo = new ImageCreateInfo
|
||||
{
|
||||
SType = StructureType.ImageCreateInfo,
|
||||
|
|
@ -756,6 +810,7 @@ internal static unsafe partial class VulkanVideoPresenter
|
|||
Format = format,
|
||||
Image = image,
|
||||
Memory = memory,
|
||||
FromSnapshotPool = true,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -28,6 +28,10 @@ internal static unsafe partial class VulkanVideoPresenter
|
|||
private const string DepthClipControlExtensionName = "VK_EXT_depth_clip_control";
|
||||
private const string DepthClipEnableExtensionName = "VK_EXT_depth_clip_enable";
|
||||
private const int DrawsPerBatch = 64;
|
||||
// A full batch does not split an open render pass: ending it there flushed and
|
||||
// restarted Demon's Souls' 1440p G-buffer pass (five targets + depth) every 64 draws.
|
||||
// The pass still ends at this cap so a long pass cannot hold the batch forever.
|
||||
private const int DrawsPerBatchInRenderPass = 512;
|
||||
private const uint SingleRectangleVertexCount = 4;
|
||||
|
||||
private static void RequireRenderingFeature(bool supported, string feature, string deviceName)
|
||||
|
|
@ -360,7 +364,7 @@ internal static unsafe partial class VulkanVideoPresenter
|
|||
// Acquire the decoded frame before any stage selects its movie texture planes.
|
||||
PumpHostMovieFrame();
|
||||
|
||||
if (_batchDrawCount >= DrawsPerBatch)
|
||||
if (_batchDrawCount >= (_renderingActive ? DrawsPerBatchInRenderPass : DrawsPerBatch))
|
||||
{
|
||||
EndRendering();
|
||||
FlushBatchedGuestCommands();
|
||||
|
|
@ -451,6 +455,8 @@ internal static unsafe partial class VulkanVideoPresenter
|
|||
}
|
||||
}
|
||||
|
||||
RecordSampledColorMetadataClears(bindings);
|
||||
|
||||
// Create feedback copies after load clears have been materialized;
|
||||
// otherwise a shader would sample the pre-clear contents.
|
||||
PrepareDepthFeedback(bindings);
|
||||
|
|
@ -525,6 +531,45 @@ internal static unsafe partial class VulkanVideoPresenter
|
|||
(ulong)attachment.BaseLayer < (ulong)sampled.BaseLayer + sampled.LayerCount;
|
||||
|
||||
// Clears the depth view with a transfer so the draw can sample the cleared image.
|
||||
// A DCC fast clear stays pending until the surface binds as a color target. A shader that
|
||||
// samples or writes the surface first must see the cleared contents, and the later bind must
|
||||
// not clear over its writes: Astro Bot's save-slot cards were drawn by compute into a
|
||||
// fast-cleared target, then wiped black by the deferred clear. Only the zero clear code is
|
||||
// materialized here; the register clear color is known only when the surface is a target.
|
||||
private void RecordSampledColorMetadataClears(TextureResource[] bindings)
|
||||
{
|
||||
const byte DccClearToZero = 0x00;
|
||||
foreach (var binding in bindings)
|
||||
{
|
||||
if (binding.IsHostMovie || binding.CachedImage is not { } image || image.Description.Metadata.Kind != MetadataKind.Dcc)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var metadataAddress = image.Description.Metadata.Range.Address;
|
||||
var view = binding.Request.View;
|
||||
for (var layer = view.BaseLayer; layer < view.BaseLayer + view.LayerCount; layer++)
|
||||
{
|
||||
if (!_imageCache.IsMetadataCleared(metadataAddress, layer, out var metadataValue) || (byte)metadataValue != DccClearToZero)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
EndRendering();
|
||||
var command = BeginBatchedGuestCommands();
|
||||
var range = new SubresourceRange(0, 1, layer, 1);
|
||||
image.Transition(ImageLayout.TransferDstOptimal, AccessFlags.TransferWriteBit, range, command);
|
||||
var vkRange = new ImageSubresourceRange(ImageAspectFlags.ColorBit, 0, 1, layer, 1);
|
||||
var clearValue = default(ClearColorValue);
|
||||
_vk.CmdClearColorImage(command, image.Backing.Handle, ImageLayout.TransferDstOptimal, &clearValue, 1, &vkRange);
|
||||
if (!_imageCache.SetMetadataSlice(metadataAddress, layer, false))
|
||||
{
|
||||
throw SubmissionScheduler.Fatal($"The DCC clear state could not be consumed: metadata=0x{metadataAddress:X16} layer={layer}.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void RecordSampledDepthClear(CachedImage image, in ImageViewDescription view, Format format)
|
||||
{
|
||||
var aspects = (_boundDepthLoadState.DepthClearEnabled ? ImageAspectFlags.DepthBit : 0) |
|
||||
|
|
@ -848,6 +893,22 @@ internal static unsafe partial class VulkanVideoPresenter
|
|||
CountDraw();
|
||||
}
|
||||
|
||||
void IRenderHost.DrawIndexedIndirect(BufferBinding arguments)
|
||||
{
|
||||
using var profileScope = RenderPhaseProfile.MeasureDetail(RenderPhaseProfile.Phase.DrawRecording);
|
||||
var command = BeginBatchedGuestCommands();
|
||||
if (_boundGraphicsPipeline is { RectangleList: true } entry)
|
||||
{
|
||||
BindRectangleListVariant(entry, strip: false, command);
|
||||
}
|
||||
|
||||
_gpuCommandProfile?.WriteMarker(command, VulkanCommandProfile.IntervalKind.Preparation);
|
||||
_vk.CmdDrawIndexedIndirect(command, new VkBuffer(arguments.Handle), arguments.Offset, 1, 20);
|
||||
_gpuCommandProfile?.WriteMarker(command, VulkanCommandProfile.IntervalKind.DrawIndexed,
|
||||
_boundGraphicsPipeline?.Id ?? 0, 0, 0);
|
||||
CountDraw();
|
||||
}
|
||||
|
||||
public void Dispatch(uint groupsX, uint groupsY, uint groupsZ)
|
||||
{
|
||||
using var profileScope = RenderPhaseProfile.MeasureDetail(RenderPhaseProfile.Phase.DrawRecording);
|
||||
|
|
|
|||
|
|
@ -80,6 +80,10 @@ internal static unsafe partial class VulkanVideoPresenter
|
|||
_guestBacking = backing;
|
||||
_bufferCache = new GuestBufferCache(_deviceInfo, _scheduler, _relay, memory.Pages, guest, backing);
|
||||
_bufferCache.StreamOffsetAlignment = Math.Max(_bufferCache.StreamOffsetAlignment, GuestStorageBufferOffsetAlignment);
|
||||
if (_readbackQueueFamilyIndex is { } readbackFamily)
|
||||
{
|
||||
_bufferCache.AsyncReadback = new Gpu.Vulkan.VulkanAsyncReadback(_deviceInfo, _scheduler, _readbackQueue, readbackFamily);
|
||||
}
|
||||
}
|
||||
|
||||
// The image store follows the buffer store; readback of linear images stays off.
|
||||
|
|
|
|||
|
|
@ -118,6 +118,21 @@ public static partial class Gen5SpirvTranslator
|
|||
var oldValue = LoadV(destination);
|
||||
var sourceValue = GetRawSource(instruction, 0);
|
||||
var selectedLane = BitwiseAnd(GetRawSource(instruction, 1), UInt(_waveLaneCount - 1));
|
||||
if (_laneSpillSlots.Count != 0)
|
||||
{
|
||||
foreach (var ((slotRegister, slotLane), variable) in _laneSpillSlots)
|
||||
{
|
||||
if (slotRegister != destination)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var isSlotLane = _module.AddInstruction(SpirvOp.IEqual, _boolType, selectedLane, UInt(slotLane));
|
||||
Store(variable, _module.AddInstruction(
|
||||
SpirvOp.Select, _uintType, isSlotLane, sourceValue, Load(_uintType, variable)));
|
||||
}
|
||||
}
|
||||
|
||||
var isTargetLane = _module.AddInstruction(
|
||||
SpirvOp.IEqual,
|
||||
_boolType,
|
||||
|
|
@ -736,6 +751,31 @@ public static partial class Gen5SpirvTranslator
|
|||
UInt(uint.MaxValue));
|
||||
break;
|
||||
}
|
||||
case "VFfbhI32":
|
||||
{
|
||||
// First bit that differs from the sign bit, counted from bit 31; ~0 for 0 and -1.
|
||||
var source = GetRawSource(instruction, 0);
|
||||
var msb = Bitcast(
|
||||
_uintType,
|
||||
Ext(74, _intType, Bitcast(_intType, source)));
|
||||
var position = _module.AddInstruction(
|
||||
SpirvOp.ISub,
|
||||
_uintType,
|
||||
UInt(31),
|
||||
msb);
|
||||
var found = _module.AddInstruction(
|
||||
SpirvOp.INotEqual,
|
||||
_boolType,
|
||||
msb,
|
||||
UInt(uint.MaxValue));
|
||||
result = _module.AddInstruction(
|
||||
SpirvOp.Select,
|
||||
_uintType,
|
||||
found,
|
||||
position,
|
||||
UInt(uint.MaxValue));
|
||||
break;
|
||||
}
|
||||
case "VFfblB32":
|
||||
result = Bitcast(
|
||||
_uintType,
|
||||
|
|
@ -1568,6 +1608,12 @@ public static partial class Gen5SpirvTranslator
|
|||
}
|
||||
|
||||
var m0 = LoadS(M0ScalarRegister);
|
||||
_moveRelativeOffsets ??= Ir.Gen5MoveRelativeOffsets.Analyze(_request.Program);
|
||||
if (_moveRelativeOffsets.TryGetValue(instruction.Pc, out var offsets))
|
||||
{
|
||||
return TryEmitBoundedMoveRelative(instruction, destination, m0, offsets, out error);
|
||||
}
|
||||
|
||||
uint sourceOffset;
|
||||
uint destinationOffset;
|
||||
if (instruction.Opcode == "VMovrelsd2B32")
|
||||
|
|
@ -1610,6 +1656,62 @@ public static partial class Gen5SpirvTranslator
|
|||
return true;
|
||||
}
|
||||
|
||||
private IReadOnlyDictionary<uint, uint[]>? _moveRelativeOffsets;
|
||||
|
||||
// A relative move whose M0 values are known picks among those registers with
|
||||
// constant register numbers, so the VGPR array is never indexed at run time and
|
||||
// the driver can keep it in hardware registers instead of scratch memory.
|
||||
private bool TryEmitBoundedMoveRelative(
|
||||
Gen5ShaderInstruction instruction,
|
||||
uint destination,
|
||||
uint m0,
|
||||
uint[] offsets,
|
||||
out string error)
|
||||
{
|
||||
error = string.Empty;
|
||||
uint value;
|
||||
if (instruction.Opcode == "VMovreldB32")
|
||||
{
|
||||
value = GetRawSource(instruction, 0);
|
||||
}
|
||||
else
|
||||
{
|
||||
var source = instruction.Sources[0];
|
||||
if (source.Kind != Gen5OperandKind.VectorRegister)
|
||||
{
|
||||
error = $"{instruction.Opcode} source must be a vector register";
|
||||
return false;
|
||||
}
|
||||
|
||||
value = LoadV((source.Value + offsets[0]) & (VectorRegisterCount - 1));
|
||||
for (var index = 1; index < offsets.Length; index++)
|
||||
{
|
||||
value = _module.AddInstruction(
|
||||
SpirvOp.Select,
|
||||
_uintType,
|
||||
_module.AddInstruction(SpirvOp.IEqual, _boolType, m0, UInt(offsets[index])),
|
||||
LoadV((source.Value + offsets[index]) & (VectorRegisterCount - 1)),
|
||||
value);
|
||||
}
|
||||
}
|
||||
|
||||
if (instruction.Opcode == "VMovrelsB32")
|
||||
{
|
||||
StoreV(destination, value);
|
||||
return true;
|
||||
}
|
||||
|
||||
var active = Load(_boolType, _exec);
|
||||
foreach (var offset in offsets)
|
||||
{
|
||||
var register = (destination + offset) & (VectorRegisterCount - 1);
|
||||
var selected = LogicalAnd(active, _module.AddInstruction(SpirvOp.IEqual, _boolType, m0, UInt(offset)));
|
||||
StoreV(register, _module.AddInstruction(SpirvOp.Select, _uintType, selected, value, LoadV(register)), guardWithExec: false);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Packed f16 (VOP3P) arithmetic. Each source register holds two f16 values,
|
||||
// one per result lane. Every f16<->f32 conversion is done with the explicit
|
||||
// integer sequences below (EmitHalfToFloat / EmitFloatToHalf) instead of
|
||||
|
|
@ -2561,14 +2663,15 @@ public static partial class Gen5SpirvTranslator
|
|||
|
||||
if (instruction.Opcode == "SBcnt1I32B64")
|
||||
{
|
||||
var wideCount = _module.AddInstruction(
|
||||
SpirvOp.BitCount,
|
||||
_ulongType,
|
||||
GetRawSource64(instruction, 0));
|
||||
var bitCountResult = _module.AddInstruction(
|
||||
SpirvOp.UConvert,
|
||||
_uintType,
|
||||
wideCount);
|
||||
// Vulkan only allows OpBitCount on 32-bit operands without
|
||||
// maintenance9, so count each half separately.
|
||||
var wide = GetRawSource64(instruction, 0);
|
||||
var bitCountResult = IAdd(
|
||||
_module.AddInstruction(SpirvOp.BitCount, _uintType, Narrow(wide)),
|
||||
_module.AddInstruction(
|
||||
SpirvOp.BitCount,
|
||||
_uintType,
|
||||
Narrow(ShiftRightLogical64(wide, _module.Constant64(_ulongType, 32)))));
|
||||
StoreS(destination, bitCountResult);
|
||||
Store(_scc, IsNotZero(bitCountResult));
|
||||
return true;
|
||||
|
|
@ -3416,7 +3519,7 @@ public static partial class Gen5SpirvTranslator
|
|||
|
||||
if (!_emulateWave64 && _waveLaneCount != 64)
|
||||
{
|
||||
newExec = BitwiseAnd(
|
||||
newExec = BitwiseAnd64(
|
||||
newExec,
|
||||
_module.Constant64(_ulongType, 0xFFFF_FFFFUL));
|
||||
}
|
||||
|
|
@ -3847,7 +3950,15 @@ public static partial class Gen5SpirvTranslator
|
|||
}
|
||||
|
||||
var targetLane = IAdd(BitwiseAnd(lane, UInt(0xFFFF_FFF8)), selector);
|
||||
targetLane = BitwiseAnd(targetLane, UInt(31));
|
||||
// Ensure target lane is properly constrained to wave size (32 or 64)
|
||||
if (_waveLaneCount == 64)
|
||||
{
|
||||
targetLane = BitwiseAnd(targetLane, UInt(63));
|
||||
}
|
||||
else
|
||||
{
|
||||
targetLane = BitwiseAnd(targetLane, UInt(31));
|
||||
}
|
||||
var shuffled = ShuffleLane(value, targetLane);
|
||||
if (control.FetchInactive)
|
||||
{
|
||||
|
|
@ -3880,7 +3991,10 @@ public static partial class Gen5SpirvTranslator
|
|||
inRange,
|
||||
targetLane,
|
||||
lane);
|
||||
safeTarget = BitwiseAnd(safeTarget, UInt(31));
|
||||
// Mask to guest wave size (32 or 64 lanes) — on Radeon hardware, DPP
|
||||
// operations are limited to a single half-wave for some encodings, so we
|
||||
// must not clamp wave64 lanes to 31; use the full lane mask instead.
|
||||
safeTarget = BitwiseAnd(safeTarget, UInt(_waveLaneCount == 64 ? 63u : 31u));
|
||||
var shuffled = ShuffleLane(value, safeTarget);
|
||||
|
||||
var sourceAvailable = inRange;
|
||||
|
|
@ -4921,7 +5035,13 @@ public static partial class Gen5SpirvTranslator
|
|||
}
|
||||
else
|
||||
{
|
||||
// Fallback: no subgroup ops, read current lane's value.
|
||||
// Fallback: no subgroup ops, read current lane's value, or the
|
||||
// spill slot that V_WRITELANE filled for the selected lane.
|
||||
if (instruction.Sources[0].Kind == Gen5OperandKind.VectorRegister)
|
||||
{
|
||||
sourceValue = SelectLaneSpillSlot(instruction.Sources[0].Value, selectedLane, sourceValue);
|
||||
}
|
||||
|
||||
StoreS(destination, sourceValue);
|
||||
}
|
||||
|
||||
|
|
@ -4979,7 +5099,10 @@ public static partial class Gen5SpirvTranslator
|
|||
}
|
||||
|
||||
var targetLane = IAdd(rowBase, selector);
|
||||
targetLane = BitwiseAnd(targetLane, UInt(31));
|
||||
// Mask to guest wave size — on Radeon hardware DPP is limited to a
|
||||
// single half-wave for some encodings, but we must not clamp wave64
|
||||
// lanes to 31; use the full lane mask instead.
|
||||
targetLane = BitwiseAnd(targetLane, UInt(_waveLaneCount == 64 ? 63u : 31u));
|
||||
var shuffled = ShuffleLane(value, targetLane);
|
||||
var fetchInactive = (control.OperandSelect & 1) != 0;
|
||||
if (fetchInactive)
|
||||
|
|
|
|||
|
|
@ -11,6 +11,9 @@ public static partial class Gen5SpirvTranslator
|
|||
{
|
||||
private readonly HashSet<uint> _perVertexAttributes = [];
|
||||
private readonly HashSet<uint> _flatParameterAttributes = [];
|
||||
// Slots that share a per-vertex input with a V_INTERP_MOV slot; true for a flat slot.
|
||||
private readonly Dictionary<uint, bool> _perVertexSourcedAttributes = [];
|
||||
private uint _perspectiveBarycentric;
|
||||
private readonly Dictionary<int, uint> _barycentricInputs = [];
|
||||
private uint _interpolationSampleId;
|
||||
private const uint InterpolateAtCentroid = 76;
|
||||
|
|
@ -90,6 +93,53 @@ public static partial class Gen5SpirvTranslator
|
|||
}
|
||||
}
|
||||
|
||||
// The perspective barycentrics a smooth slot sharing a per-vertex input is rebuilt with.
|
||||
private void DeclarePerspectiveBarycentric()
|
||||
{
|
||||
foreach (var bit in new[] { 0, 1, 2 })
|
||||
{
|
||||
if (_barycentricInputs.TryGetValue(bit, out var existing))
|
||||
{
|
||||
_perspectiveBarycentric = existing;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
_perspectiveBarycentric = _module.AddGlobalVariable(
|
||||
_module.TypePointer(SpirvStorageClass.Input, _vec3Type), SpirvStorageClass.Input);
|
||||
_module.AddDecoration(_perspectiveBarycentric, SpirvDecoration.BuiltIn, (uint)SpirvBuiltIn.BaryCoordKhr);
|
||||
_interfaces.Add(_perspectiveBarycentric);
|
||||
}
|
||||
|
||||
// A smooth or flat slot read from a per-vertex input: the vertices weighted by the
|
||||
// perspective barycentrics, or vertex 0 (the provoking vertex) for a flat slot.
|
||||
private void EmitPerVertexSourcedInterpolation(Gen5InterpolationControl interpolation, uint input, uint destination, bool flat)
|
||||
{
|
||||
uint LoadVertex(uint vertex)
|
||||
{
|
||||
var pointer = _module.AddInstruction(SpirvOp.AccessChain,
|
||||
_module.TypePointer(SpirvStorageClass.Input, _floatType),
|
||||
input, UInt(vertex), UInt(interpolation.Channel));
|
||||
return Load(_floatType, pointer);
|
||||
}
|
||||
|
||||
var value = LoadVertex(0);
|
||||
if (!flat)
|
||||
{
|
||||
var barycentric = Load(_vec3Type, _perspectiveBarycentric);
|
||||
value = _module.AddInstruction(SpirvOp.FMul, _floatType, value,
|
||||
_module.AddInstruction(SpirvOp.CompositeExtract, _floatType, barycentric, 0));
|
||||
for (uint vertex = 1; vertex < 3; vertex++)
|
||||
{
|
||||
var weight = _module.AddInstruction(SpirvOp.CompositeExtract, _floatType, barycentric, vertex);
|
||||
value = _module.AddInstruction(SpirvOp.FAdd, _floatType, value,
|
||||
_module.AddInstruction(SpirvOp.FMul, _floatType, LoadVertex(vertex), weight));
|
||||
}
|
||||
}
|
||||
|
||||
StoreV(destination, Bitcast(_uintType, value));
|
||||
}
|
||||
|
||||
private uint LoadBarycentricCoordinates(int bit, uint variable) => bit switch
|
||||
{
|
||||
0 or 4 => _module.AddInstruction(SpirvOp.ExtInst, _vec3Type, _glsl, InterpolateAtSample,
|
||||
|
|
|
|||
|
|
@ -155,6 +155,10 @@ public static partial class Gen5SpirvTranslator
|
|||
private uint _programCounter;
|
||||
private uint _programActive;
|
||||
private uint _iterationGuard;
|
||||
// One-lane waves only have lane 0 of each VGPR. Other lanes that the program spills
|
||||
// SGPRs into with V_WRITELANE and reads back with V_READLANE live here, keyed by
|
||||
// (VGPR, lane).
|
||||
private readonly Dictionary<(uint Register, uint Lane), uint> _laneSpillSlots = new();
|
||||
private uint _globalBuffers;
|
||||
private uint _gfx10BufferFormatTable;
|
||||
private uint _storageBlockPointer;
|
||||
|
|
@ -272,8 +276,9 @@ public static partial class Gen5SpirvTranslator
|
|||
}
|
||||
}
|
||||
|
||||
DeclareModule();
|
||||
var blocks = BuildBasicBlocks(_request.Program.Instructions);
|
||||
_functionScopeState = StructuredForwardBlocks && blocks.Count != 0 && TryBuildLoopRegions(blocks, out _loopLatchByHeader);
|
||||
DeclareModule();
|
||||
if (blocks.Count == 0)
|
||||
{
|
||||
error = "shader contains no executable blocks";
|
||||
|
|
@ -285,6 +290,10 @@ public static partial class Gen5SpirvTranslator
|
|||
var main = _module.BeginFunction(_voidType, functionType);
|
||||
_module.AddName(main, "main");
|
||||
_module.AddLabel();
|
||||
if (_functionScopeState)
|
||||
{
|
||||
DeclareRegisterFiles();
|
||||
}
|
||||
if (_stage == Gen5SpirvStage.Pixel &&
|
||||
Environment.GetEnvironmentVariable(
|
||||
"SHARPEMU_FORCE_TITLE_EARLY_COLOR") == "1" &&
|
||||
|
|
@ -326,6 +335,23 @@ public static partial class Gen5SpirvTranslator
|
|||
caseLabels[index] = _module.AllocateId();
|
||||
}
|
||||
|
||||
if (_functionScopeState)
|
||||
{
|
||||
// Each invocation visits its blocks in program order, except for the back
|
||||
// edges of natural loops. Emitting the blocks in order, each guarded by "this
|
||||
// is the next block", and every loop as a structured SPIR-V loop runs exactly
|
||||
// what the dispatcher loop would. The driver then sees ifs and loops and can
|
||||
// keep only the live guest registers in hardware registers, where the
|
||||
// dispatcher keeps nearly all of them live and pays a switch per block.
|
||||
if (!TryEmitStructuredRange(blocks, 0, blocks.Count - 1, -1, out error))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
_module.AddStatement(SpirvOp.Branch, loopMerge);
|
||||
}
|
||||
else
|
||||
{
|
||||
_module.AddStatement(SpirvOp.Branch, loopHeader);
|
||||
_module.AddLabel(loopHeader);
|
||||
// Check before the first block can write from an inactive invocation.
|
||||
|
|
@ -387,6 +413,8 @@ public static partial class Gen5SpirvTranslator
|
|||
active,
|
||||
loopHeader,
|
||||
loopMerge);
|
||||
}
|
||||
|
||||
_module.AddLabel(loopMerge);
|
||||
if (_stage == Gen5SpirvStage.Pixel &&
|
||||
Environment.GetEnvironmentVariable(
|
||||
|
|
@ -541,74 +569,24 @@ public static partial class Gen5SpirvTranslator
|
|||
|
||||
var scalarArrayType = _module.TypeArray(_uintType, ScalarRegisterCount);
|
||||
var vectorArrayType = _module.TypeArray(_uintType, VectorRegisterCount);
|
||||
var privateScalarArrayPointer =
|
||||
_module.TypePointer(SpirvStorageClass.Private, scalarArrayType);
|
||||
var privateVectorArrayPointer =
|
||||
_module.TypePointer(SpirvStorageClass.Private, vectorArrayType);
|
||||
_scalarRegisters = _module.AddGlobalVariable(
|
||||
privateScalarArrayPointer,
|
||||
SpirvStorageClass.Private,
|
||||
_module.ConstantNull(scalarArrayType));
|
||||
_vectorRegisters = _module.AddGlobalVariable(
|
||||
privateVectorArrayPointer,
|
||||
SpirvStorageClass.Private,
|
||||
_module.ConstantNull(vectorArrayType));
|
||||
_scc = _module.AddGlobalVariable(
|
||||
_privateBoolPointer,
|
||||
SpirvStorageClass.Private,
|
||||
_module.ConstantBool(false));
|
||||
_vcc = _module.AddGlobalVariable(
|
||||
_privateBoolPointer,
|
||||
SpirvStorageClass.Private,
|
||||
_module.ConstantBool(false));
|
||||
_exec = _module.AddGlobalVariable(
|
||||
_privateBoolPointer,
|
||||
SpirvStorageClass.Private,
|
||||
_module.ConstantBool(true));
|
||||
_reachedPixelExport = _module.AddGlobalVariable(
|
||||
_privateBoolPointer,
|
||||
SpirvStorageClass.Private,
|
||||
_module.ConstantBool(false));
|
||||
if (_usesPixelValidMask)
|
||||
// The register files are variables of main, declared when main begins (see
|
||||
// DeclareRegisterFiles). As Private globals, a file the driver fully promotes to
|
||||
// registers stays behind as an unused .bss global, and AMD's driver crashes
|
||||
// linking two stages that both carry one.
|
||||
_scalarArrayType = scalarArrayType;
|
||||
_vectorArrayType = vectorArrayType;
|
||||
var registerStorage = _functionScopeState ? SpirvStorageClass.Function : SpirvStorageClass.Private;
|
||||
_functionUintPointer = _module.TypePointer(registerStorage, _uintType);
|
||||
_functionVec2Pointer = _module.TypePointer(registerStorage, _vec2Type);
|
||||
// The per-invocation state (SCC, VCC, EXEC, the block dispatcher's counters) also
|
||||
// lives in main: an unused Private global in a fragment shader crashes AMD's
|
||||
// driver when it links the stages (seen with the dispatcher guard of a shader
|
||||
// that no longer needs the dispatcher).
|
||||
_functionBoolPointer = _module.TypePointer(registerStorage, _boolType);
|
||||
if (!_functionScopeState)
|
||||
{
|
||||
_pixelValidMaskActive = _module.AddGlobalVariable(
|
||||
_privateBoolPointer,
|
||||
SpirvStorageClass.Private,
|
||||
_module.ConstantBool(true));
|
||||
DeclareRegisterFiles();
|
||||
}
|
||||
_programCounter = _module.AddGlobalVariable(
|
||||
_privateUintPointer,
|
||||
SpirvStorageClass.Private,
|
||||
_module.Constant(_uintType, 0));
|
||||
_programActive = _module.AddGlobalVariable(
|
||||
_privateBoolPointer,
|
||||
SpirvStorageClass.Private,
|
||||
_module.ConstantBool(true));
|
||||
if (_maxDispatcherSteps > 0)
|
||||
{
|
||||
_iterationGuard = _module.AddGlobalVariable(
|
||||
_privateUintPointer,
|
||||
SpirvStorageClass.Private,
|
||||
_module.Constant(_uintType, 0));
|
||||
_interfaces.Add(_iterationGuard);
|
||||
_module.AddName(_iterationGuard, "pcGuard");
|
||||
}
|
||||
|
||||
_interfaces.Add(_scalarRegisters);
|
||||
_interfaces.Add(_vectorRegisters);
|
||||
_interfaces.Add(_scc);
|
||||
_interfaces.Add(_vcc);
|
||||
_interfaces.Add(_exec);
|
||||
_interfaces.Add(_reachedPixelExport);
|
||||
if (_pixelValidMaskActive != 0)
|
||||
{
|
||||
_interfaces.Add(_pixelValidMaskActive);
|
||||
_module.AddName(_pixelValidMaskActive, "pixelValidMaskActive");
|
||||
}
|
||||
_interfaces.Add(_programCounter);
|
||||
_interfaces.Add(_programActive);
|
||||
_module.AddName(_scalarRegisters, "sgpr");
|
||||
_module.AddName(_vectorRegisters, "vgpr");
|
||||
|
||||
{
|
||||
DeclareLayoutBindings();
|
||||
|
|
@ -898,10 +876,68 @@ public static partial class Gen5SpirvTranslator
|
|||
.Distinct()
|
||||
.Order()
|
||||
.ToArray();
|
||||
DeclareInterpolationParameters();
|
||||
// Several PS input slots may read one VS parameter (the guest compiler emits a
|
||||
// slot per use). Relocating a duplicate left it on a location the vertex program
|
||||
// never writes, so it read zero: Astro Bot's save-slot card normals decoded to NaN
|
||||
// and a machine's albedo came out black. Slots of one kind share one host input;
|
||||
// when a V_INTERP_MOV slot is among them, the others read the per-vertex values
|
||||
// (interpolated with the barycentrics, or vertex 0 for a flat slot).
|
||||
var shared = new Dictionary<uint, uint>();
|
||||
bool IsFlatSlot(uint attribute) =>
|
||||
((attribute < (uint)_pixelInputCntl.Length ? _pixelInputCntl[attribute] : 0u) & 0x400u) != 0 ||
|
||||
_flatParameterAttributes.Contains(attribute);
|
||||
foreach (var group in attributes.GroupBy(attribute =>
|
||||
(attribute < (uint)_pixelInputCntl.Length ? _pixelInputCntl[attribute] : attribute) & 0x1Fu))
|
||||
{
|
||||
uint? perVertexOwner = null;
|
||||
foreach (var attribute in group)
|
||||
{
|
||||
if (_perVertexAttributes.Contains(attribute))
|
||||
{
|
||||
perVertexOwner = attribute;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
var owners = new Dictionary<bool, uint>();
|
||||
foreach (var attribute in group)
|
||||
{
|
||||
if (perVertexOwner is { } perVertex)
|
||||
{
|
||||
if (attribute != perVertex)
|
||||
{
|
||||
shared.Add(attribute, perVertex);
|
||||
if (!_perVertexAttributes.Contains(attribute))
|
||||
{
|
||||
_perVertexSourcedAttributes.Add(attribute, IsFlatSlot(attribute));
|
||||
}
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
var flat = IsFlatSlot(attribute);
|
||||
if (owners.TryGetValue(flat, out var owner))
|
||||
{
|
||||
shared.Add(attribute, owner);
|
||||
}
|
||||
else
|
||||
{
|
||||
owners.Add(flat, attribute);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (_perVertexSourcedAttributes.ContainsValue(false))
|
||||
{
|
||||
DeclarePerspectiveBarycentric();
|
||||
}
|
||||
|
||||
attributes = attributes.Where(attribute => !shared.ContainsKey(attribute)).ToArray();
|
||||
var locations = Gen5PixelInputMapping.ResolveLocations(
|
||||
_pixelInputCntl,
|
||||
attributes);
|
||||
DeclareInterpolationParameters();
|
||||
for (var index = 0; index < attributes.Length; index++)
|
||||
{
|
||||
var attribute = attributes[index];
|
||||
|
|
@ -934,6 +970,11 @@ public static partial class Gen5SpirvTranslator
|
|||
_interfaces.Add(variable);
|
||||
}
|
||||
|
||||
foreach (var (attribute, owner) in shared)
|
||||
{
|
||||
_pixelInputs.Add(attribute, _pixelInputs[owner]);
|
||||
}
|
||||
|
||||
_fragCoordInput = _module.AddGlobalVariable(
|
||||
inputVec4Pointer,
|
||||
SpirvStorageClass.Input);
|
||||
|
|
@ -1078,6 +1119,29 @@ public static partial class Gen5SpirvTranslator
|
|||
StoreS64(106, _module.Constant64(_ulongType, 0));
|
||||
StoreS64(126, _module.Constant64(_ulongType, 1));
|
||||
}
|
||||
|
||||
// Primitive (NGG) vertex programs rebuild EXEC from the merged wave info in s3:
|
||||
// vertex count in bits 0-7, primitive count in bits 8-15. Left at zero, EXEC
|
||||
// becomes all 64 lanes, and a waterfall loop then waits forever for lanes that
|
||||
// do not exist to clear their bits (Astro Bot intro_next froze the GPU this way).
|
||||
if (_stage == Gen5SpirvStage.Vertex && _request.UserDataBase > 3)
|
||||
{
|
||||
uint laneCount;
|
||||
if (_subgroupInvocationIdInput == 0)
|
||||
{
|
||||
laneCount = UInt(1);
|
||||
}
|
||||
else
|
||||
{
|
||||
var lanes = BooleanToWaveMask(_module.ConstantBool(true));
|
||||
laneCount = IAdd(
|
||||
_module.AddInstruction(SpirvOp.BitCount, _uintType, Narrow(lanes)),
|
||||
_module.AddInstruction(SpirvOp.BitCount, _uintType, Narrow(ShiftRightLogical64(lanes, _module.Constant64(_ulongType, 32)))));
|
||||
}
|
||||
|
||||
StoreS(3, BitwiseOr(laneCount, ShiftLeftLogical(laneCount, UInt(8))));
|
||||
}
|
||||
|
||||
Store(_programCounter, UInt(0));
|
||||
Store(_programActive, _module.ConstantBool(true));
|
||||
|
||||
|
|
@ -1324,7 +1388,10 @@ public static partial class Gen5SpirvTranslator
|
|||
}
|
||||
}
|
||||
|
||||
if (!TryEmitInstruction(instruction, out error))
|
||||
_execKnownFull = IsExecKnownFull(instruction.Pc);
|
||||
var emitted = TryEmitInstruction(instruction, out error);
|
||||
_execKnownFull = false;
|
||||
if (!emitted)
|
||||
{
|
||||
error = $"pc=0x{instruction.Pc:X} {instruction.Opcode}: {error}";
|
||||
return false;
|
||||
|
|
@ -1414,6 +1481,143 @@ public static partial class Gen5SpirvTranslator
|
|||
return true;
|
||||
}
|
||||
|
||||
// SHARPEMU_STRUCTURED_FORWARD_BLOCKS=0 always emits the block dispatcher loop.
|
||||
private static readonly bool StructuredForwardBlocks = !string.Equals(
|
||||
Environment.GetEnvironmentVariable("SHARPEMU_STRUCTURED_FORWARD_BLOCKS"), "0", StringComparison.Ordinal);
|
||||
|
||||
// The natural loops of the program: each block targeted by a backward branch heads a
|
||||
// loop that ends at the last block branching back to it. Loops must nest; a program
|
||||
// whose loops overlap keeps the block dispatcher.
|
||||
private Dictionary<int, int> _loopLatchByHeader = [];
|
||||
|
||||
private bool TryBuildLoopRegions(IReadOnlyList<ShaderBlock> blocks, out Dictionary<int, int> latchByHeader)
|
||||
{
|
||||
latchByHeader = [];
|
||||
for (var index = 0; index < blocks.Count; index++)
|
||||
{
|
||||
var terminator = _request.Program.Instructions[blocks[index].EndIndex - 1];
|
||||
if (terminator.Opcode != "SBranch" && !terminator.Opcode.StartsWith("SCbranch", StringComparison.Ordinal))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!TryGetBranchTargetPc(terminator, out var targetPc))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (IsExitBranchTarget(_request.Program.Instructions, targetPc))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!TryFindBlock(blocks, targetPc, out var target))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (target <= index)
|
||||
{
|
||||
latchByHeader[target] = latchByHeader.TryGetValue(target, out var latch) ? Math.Max(latch, index) : index;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var (outerHeader, outerLatch) in latchByHeader)
|
||||
{
|
||||
foreach (var (innerHeader, innerLatch) in latchByHeader)
|
||||
{
|
||||
if (outerHeader < innerHeader && innerHeader <= outerLatch && innerLatch > outerLatch)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Emits blocks first..last in order; a nested loop header opens a structured loop.
|
||||
private bool TryEmitStructuredRange(IReadOnlyList<ShaderBlock> blocks, int first, int last, int enclosingHeader, out string error)
|
||||
{
|
||||
error = string.Empty;
|
||||
for (var index = first; index <= last;)
|
||||
{
|
||||
if (index != enclosingHeader && _loopLatchByHeader.TryGetValue(index, out var latch))
|
||||
{
|
||||
if (!TryEmitStructuredLoop(blocks, index, latch, out error))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
index = latch + 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
var isNext = _module.AddInstruction(
|
||||
SpirvOp.LogicalAnd,
|
||||
_boolType,
|
||||
Load(_boolType, _programActive),
|
||||
_module.AddInstruction(SpirvOp.IEqual, _boolType, Load(_uintType, _programCounter), UInt((uint)index)));
|
||||
var blockBody = _module.AllocateId();
|
||||
var blockMerge = _module.AllocateId();
|
||||
_module.AddStatement(SpirvOp.SelectionMerge, blockMerge, 0);
|
||||
_module.AddStatement(SpirvOp.BranchConditional, isNext, blockBody, blockMerge);
|
||||
_module.AddLabel(blockBody);
|
||||
if (!TryEmitBlock(blocks, index, out error))
|
||||
{
|
||||
error = $"block=0x{blocks[index].StartPc:X}: {error}";
|
||||
return false;
|
||||
}
|
||||
|
||||
_module.AddStatement(SpirvOp.Branch, blockMerge);
|
||||
_module.AddLabel(blockMerge);
|
||||
index++;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// One natural loop: the body runs its blocks in order and repeats while the next block
|
||||
// is the header. An exit leaves the program counter past the loop (or at an outer
|
||||
// header), so the guards after the loop pick up where the invocation continues.
|
||||
private bool TryEmitStructuredLoop(IReadOnlyList<ShaderBlock> blocks, int header, int latch, out string error)
|
||||
{
|
||||
var loopHeader = _module.AllocateId();
|
||||
var loopBody = _module.AllocateId();
|
||||
var loopContinue = _module.AllocateId();
|
||||
var loopMerge = _module.AllocateId();
|
||||
_module.AddStatement(SpirvOp.Branch, loopHeader);
|
||||
_module.AddLabel(loopHeader);
|
||||
_module.AddStatement(SpirvOp.LoopMerge, loopMerge, loopContinue, 0);
|
||||
_module.AddStatement(SpirvOp.Branch, loopBody);
|
||||
_module.AddLabel(loopBody);
|
||||
if (!TryEmitStructuredRange(blocks, header, latch, header, out error))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
_module.AddStatement(SpirvOp.Branch, loopContinue);
|
||||
_module.AddLabel(loopContinue);
|
||||
var again = _module.AddInstruction(
|
||||
SpirvOp.LogicalAnd,
|
||||
_boolType,
|
||||
Load(_boolType, _programActive),
|
||||
_module.AddInstruction(SpirvOp.IEqual, _boolType, Load(_uintType, _programCounter), UInt((uint)header)));
|
||||
if (_maxDispatcherSteps > 0)
|
||||
{
|
||||
// The dispatcher's safety valve, counted per loop iteration: a mistranslated
|
||||
// exit ends the invocation instead of wedging the GPU queue.
|
||||
var steps = IAdd(Load(_uintType, _iterationGuard), UInt(1));
|
||||
Store(_iterationGuard, steps);
|
||||
var withinLimit = _module.AddInstruction(SpirvOp.ULessThan, _boolType, steps, UInt((uint)_maxDispatcherSteps));
|
||||
again = _module.AddInstruction(SpirvOp.LogicalAnd, _boolType, again, withinLimit);
|
||||
}
|
||||
|
||||
_module.AddStatement(SpirvOp.BranchConditional, again, loopHeader, loopMerge);
|
||||
_module.AddLabel(loopMerge);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static string FormatBlockStarts(IReadOnlyList<ShaderBlock> blocks)
|
||||
{
|
||||
const int maxBlocks = 32;
|
||||
|
|
@ -1913,14 +2117,6 @@ public static partial class Gen5SpirvTranslator
|
|||
var active = Load(_boolType, _exec);
|
||||
|
||||
var activeMask = BooleanToWaveMask(active);
|
||||
var activeCount64 = _module.AddInstruction(
|
||||
SpirvOp.BitCount,
|
||||
_ulongType,
|
||||
activeMask);
|
||||
var activeCount = _module.AddInstruction(
|
||||
SpirvOp.UConvert,
|
||||
_uintType,
|
||||
activeCount64);
|
||||
var activeLow = _module.AddInstruction(
|
||||
SpirvOp.UConvert,
|
||||
_uintType,
|
||||
|
|
@ -1931,6 +2127,10 @@ public static partial class Gen5SpirvTranslator
|
|||
ShiftRightLogical64(
|
||||
activeMask,
|
||||
_module.Constant64(_ulongType, 32)));
|
||||
// OpBitCount needs a 32-bit operand without maintenance9.
|
||||
var activeCount = IAdd(
|
||||
_module.AddInstruction(SpirvOp.BitCount, _uintType, activeLow),
|
||||
_module.AddInstruction(SpirvOp.BitCount, _uintType, activeHigh));
|
||||
var firstLane = _module.AddInstruction(
|
||||
SpirvOp.Select,
|
||||
_uintType,
|
||||
|
|
@ -2356,6 +2556,12 @@ public static partial class Gen5SpirvTranslator
|
|||
return TryEmitInterpolationParameter(instruction, interpolation, input, destination, out error);
|
||||
}
|
||||
|
||||
if (_perVertexSourcedAttributes.TryGetValue(interpolation.Attribute, out var flatSource))
|
||||
{
|
||||
EmitPerVertexSourcedInterpolation(interpolation, input, destination, flatSource);
|
||||
return true;
|
||||
}
|
||||
|
||||
var vector = Load(_vec4Type, input);
|
||||
var component = _module.AddInstruction(
|
||||
SpirvOp.CompositeExtract,
|
||||
|
|
@ -6769,10 +6975,156 @@ public static partial class Gen5SpirvTranslator
|
|||
UInt(0),
|
||||
dwordAddress);
|
||||
|
||||
private uint _scalarArrayType;
|
||||
private uint _vectorArrayType;
|
||||
private uint _functionUintPointer;
|
||||
private uint _functionVec2Pointer;
|
||||
private uint _functionBoolPointer;
|
||||
|
||||
// True when main has no dispatcher loop: the register files and state then live in
|
||||
// main (see DeclareModule). A shader that keeps the dispatcher keeps them as Private
|
||||
// globals, which the driver leaves in memory instead of promoting 600-odd registers
|
||||
// across the loop (that promotion makes pipeline compiles take minutes).
|
||||
private bool _functionScopeState;
|
||||
|
||||
// Declares the register files and the per-invocation state: in main's first block
|
||||
// (Function variables must open it) or as Private globals.
|
||||
private void DeclareRegisterFiles()
|
||||
{
|
||||
uint Variable(uint valueType, uint initializer)
|
||||
{
|
||||
if (_functionScopeState)
|
||||
{
|
||||
return _module.AddFunctionVariable(_module.TypePointer(SpirvStorageClass.Function, valueType), initializer);
|
||||
}
|
||||
|
||||
var variable = _module.AddGlobalVariable(
|
||||
_module.TypePointer(SpirvStorageClass.Private, valueType),
|
||||
SpirvStorageClass.Private,
|
||||
initializer);
|
||||
_interfaces.Add(variable);
|
||||
return variable;
|
||||
}
|
||||
|
||||
_scalarRegisters = Variable(_scalarArrayType, _module.ConstantNull(_scalarArrayType));
|
||||
_vectorRegisters = Variable(_vectorArrayType, _module.ConstantNull(_vectorArrayType));
|
||||
if (_functionScopeState)
|
||||
{
|
||||
// A Private one is declared on first use instead; see PackedHalfRegisters.
|
||||
var packedArrayType = _module.TypeArray(_vec2Type, VectorRegisterCount);
|
||||
_packedHalfRegisters = Variable(packedArrayType, _module.ConstantNull(packedArrayType));
|
||||
_module.AddName(_packedHalfRegisters, "vgprPackedHalf");
|
||||
}
|
||||
|
||||
_scc = Variable(_boolType, _module.ConstantBool(false));
|
||||
_vcc = Variable(_boolType, _module.ConstantBool(false));
|
||||
_exec = Variable(_boolType, _module.ConstantBool(true));
|
||||
_reachedPixelExport = Variable(_boolType, _module.ConstantBool(false));
|
||||
if (_usesPixelValidMask)
|
||||
{
|
||||
_pixelValidMaskActive = Variable(_boolType, _module.ConstantBool(true));
|
||||
_module.AddName(_pixelValidMaskActive, "pixelValidMaskActive");
|
||||
}
|
||||
|
||||
_programCounter = Variable(_uintType, _module.Constant(_uintType, 0));
|
||||
_programActive = Variable(_boolType, _module.ConstantBool(true));
|
||||
if (_maxDispatcherSteps > 0)
|
||||
{
|
||||
_iterationGuard = Variable(_uintType, _module.Constant(_uintType, 0));
|
||||
_module.AddName(_iterationGuard, "pcGuard");
|
||||
}
|
||||
|
||||
_module.AddName(_scalarRegisters, "sgpr");
|
||||
_module.AddName(_vectorRegisters, "vgpr");
|
||||
|
||||
foreach (var slot in FindLaneSpillSlots())
|
||||
{
|
||||
var variable = Variable(_uintType, _module.Constant(_uintType, 0));
|
||||
_module.AddName(variable, $"v{slot.Register}_lane{slot.Lane}");
|
||||
_laneSpillSlots.Add(slot, variable);
|
||||
}
|
||||
}
|
||||
|
||||
// Without subgroup operations each invocation is a one-lane wave, so a V_WRITELANE to
|
||||
// lane N > 0 used to be dropped and V_READLANE returned lane 0. Compilers spill SGPRs
|
||||
// (EXEC included) to VGPR lanes like this; restoring the wrong EXEC made waterfall
|
||||
// loops spin until the dispatcher guard. Only VGPRs that are also read back get slots.
|
||||
private List<(uint Register, uint Lane)> FindLaneSpillSlots()
|
||||
{
|
||||
var slots = new List<(uint Register, uint Lane)>();
|
||||
if (UsesSubgroupOperations())
|
||||
{
|
||||
return slots;
|
||||
}
|
||||
|
||||
var readRegisters = _request.Program.Instructions
|
||||
.Where(static instruction => instruction.Opcode == "VReadlaneB32" &&
|
||||
instruction.Sources.Count > 0 &&
|
||||
instruction.Sources[0].Kind == Gen5OperandKind.VectorRegister)
|
||||
.Select(static instruction => instruction.Sources[0].Value)
|
||||
.ToHashSet();
|
||||
foreach (var instruction in _request.Program.Instructions)
|
||||
{
|
||||
if (instruction.Opcode == "VWritelaneB32" &&
|
||||
TryGetVectorDestination(instruction, out var register) &&
|
||||
readRegisters.Contains(register) &&
|
||||
TryGetConstantLane(instruction, out var lane) &&
|
||||
lane != 0 &&
|
||||
!slots.Contains((register, lane)))
|
||||
{
|
||||
slots.Add((register, lane));
|
||||
}
|
||||
}
|
||||
|
||||
return slots;
|
||||
}
|
||||
|
||||
// The lane select operand (source 1) of V_READLANE/V_WRITELANE when it is a constant.
|
||||
private bool TryGetConstantLane(Gen5ShaderInstruction instruction, out uint lane)
|
||||
{
|
||||
lane = 0;
|
||||
if (instruction.Sources.Count < 2)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var operand = instruction.Sources[1];
|
||||
uint value;
|
||||
if (operand.Kind == Gen5OperandKind.LiteralConstant)
|
||||
{
|
||||
value = operand.Value;
|
||||
}
|
||||
else if (operand.Kind != Gen5OperandKind.EncodedConstant ||
|
||||
!TryDecodeInlineConstant(operand.Value, out value))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
lane = value & (_waveLaneCount - 1);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Folds the spill slots of register into value: selected lane == slot lane reads the slot.
|
||||
private uint SelectLaneSpillSlot(uint register, uint selectedLane, uint value)
|
||||
{
|
||||
foreach (var ((slotRegister, slotLane), variable) in _laneSpillSlots)
|
||||
{
|
||||
if (slotRegister != register)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var isSlotLane = _module.AddInstruction(SpirvOp.IEqual, _boolType, selectedLane, UInt(slotLane));
|
||||
value = _module.AddInstruction(SpirvOp.Select, _uintType, isSlotLane, Load(_uintType, variable), value);
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
private uint ScalarPointer(uint register) =>
|
||||
_module.AddInstruction(
|
||||
SpirvOp.AccessChain,
|
||||
_privateUintPointer,
|
||||
_functionUintPointer,
|
||||
_scalarRegisters,
|
||||
UInt(register));
|
||||
|
||||
|
|
@ -6786,7 +7138,7 @@ public static partial class Gen5SpirvTranslator
|
|||
private uint VectorPointer(uint register) =>
|
||||
_module.AddInstruction(
|
||||
SpirvOp.AccessChain,
|
||||
_privateUintPointer,
|
||||
_functionUintPointer,
|
||||
_vectorRegisters,
|
||||
UInt(register));
|
||||
|
||||
|
|
@ -6798,7 +7150,7 @@ public static partial class Gen5SpirvTranslator
|
|||
private uint DynamicVectorPointer(uint registerIndex) =>
|
||||
_module.AddInstruction(
|
||||
SpirvOp.AccessChain,
|
||||
_privateUintPointer,
|
||||
_functionUintPointer,
|
||||
_vectorRegisters,
|
||||
BitwiseAnd(registerIndex, UInt(VectorRegisterCount - 1)));
|
||||
|
||||
|
|
@ -6808,6 +7160,12 @@ public static partial class Gen5SpirvTranslator
|
|||
private void StoreVDynamic(uint registerIndex, uint value)
|
||||
{
|
||||
var pointer = DynamicVectorPointer(registerIndex);
|
||||
if (_execKnownFull)
|
||||
{
|
||||
Store(pointer, value);
|
||||
return;
|
||||
}
|
||||
|
||||
value = _module.AddInstruction(
|
||||
SpirvOp.Select,
|
||||
_uintType,
|
||||
|
|
@ -6820,13 +7178,13 @@ public static partial class Gen5SpirvTranslator
|
|||
private uint PackedHalfPointer(uint register) =>
|
||||
_module.AddInstruction(
|
||||
SpirvOp.AccessChain,
|
||||
_privateVec2Pointer,
|
||||
_functionVec2Pointer,
|
||||
PackedHalfRegisters(),
|
||||
UInt(register));
|
||||
|
||||
// Declared on first use. AMD's compiler keeps an unused 4 KiB private array as a
|
||||
// named .bss global: two stages then fail to link, and the driver copies the
|
||||
// NOBITS section as file data and reads past the end of the ELF.
|
||||
// Declared on first use when Private. AMD's compiler keeps an unused 4 KiB private
|
||||
// array as a named .bss global: two stages then fail to link, and the driver copies
|
||||
// the NOBITS section as file data and reads past the end of the ELF.
|
||||
private uint PackedHalfRegisters()
|
||||
{
|
||||
if (_packedHalfRegisters != 0)
|
||||
|
|
@ -6861,9 +7219,30 @@ public static partial class Gen5SpirvTranslator
|
|||
}
|
||||
}
|
||||
|
||||
// SHARPEMU_EXEC_GUARD_ELISION=0 guards every vector register write with EXEC again.
|
||||
private static readonly bool ExecGuardElision = !string.Equals(
|
||||
Environment.GetEnvironmentVariable("SHARPEMU_EXEC_GUARD_ELISION"), "0", StringComparison.Ordinal);
|
||||
|
||||
private IReadOnlySet<uint>? _execFullPcs;
|
||||
|
||||
// Set while an instruction that starts with every EXEC lane set is emitted: its vector
|
||||
// register writes need no EXEC guard, so the old values need not stay live until them.
|
||||
private bool _execKnownFull;
|
||||
|
||||
private bool IsExecKnownFull(uint pc)
|
||||
{
|
||||
if (!ExecGuardElision || !_request.EnableExecGuardElision)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
_execFullPcs ??= Ir.Gen5ExecFullAnalysis.Analyze(_request.Program, wave32: _waveLaneCount == 32);
|
||||
return _execFullPcs.Contains(pc);
|
||||
}
|
||||
|
||||
private void StoreV(uint register, uint value, bool guardWithExec = true)
|
||||
{
|
||||
if (guardWithExec)
|
||||
if (guardWithExec && !_execKnownFull)
|
||||
{
|
||||
var active = Load(_boolType, _exec);
|
||||
var oldValue = LoadV(register);
|
||||
|
|
@ -6880,6 +7259,12 @@ public static partial class Gen5SpirvTranslator
|
|||
|
||||
private void StorePackedHalf(uint register, uint value)
|
||||
{
|
||||
if (_execKnownFull)
|
||||
{
|
||||
Store(PackedHalfPointer(register), value);
|
||||
return;
|
||||
}
|
||||
|
||||
var active = Load(_boolType, _exec);
|
||||
if (Environment.GetEnvironmentVariable(
|
||||
"SHARPEMU_FORCE_PACKED_STORE_EXEC_VALUES") == "1" &&
|
||||
|
|
|
|||
|
|
@ -868,6 +868,7 @@ public static partial class Gen5ShaderTranslator
|
|||
0x38 => "VBfrevB32",
|
||||
0x39 => "VFfbhU32",
|
||||
0x3A => "VFfblB32",
|
||||
0x3B => "VFfbhI32",
|
||||
0x42 => "VMovreldB32",
|
||||
0x43 => "VMovrelsB32",
|
||||
0x44 => "VMovrelsdB32",
|
||||
|
|
|
|||
|
|
@ -203,6 +203,10 @@ public static class Gfx10UnifiedFormat
|
|||
|
||||
// Legacy data-format layouts: a byte offset for naturally aligned components, a bit
|
||||
// offset inside the first dword for packed fields. Ordered by component, then format.
|
||||
// AMD names packed formats from the most significant field, so 2_10_10_10 (unified
|
||||
// format 50) is R10G10B10A2 with X in bits 0..9, as the image path maps it
|
||||
// (A2B10G10R10); decoding X from the 2-bit field garbled Demon's Souls' packed
|
||||
// vertex normals and tangents (their W is the handedness sign, 0 or 3).
|
||||
public static IReadOnlyList<(uint Component, uint DataFormat, uint ByteOffset, uint BitOffset, uint BitCount)> ComponentLayouts { get; } =
|
||||
[
|
||||
(0, 1, 0, 0, 8), // 8
|
||||
|
|
@ -213,7 +217,7 @@ public static class Gfx10UnifiedFormat
|
|||
(0, 6, 0, 0, 10), // 10_11_11
|
||||
(0, 7, 0, 0, 11), // 11_11_10
|
||||
(0, 8, 0, 0, 10), // 10_10_10_2
|
||||
(0, 9, 0, 0, 2), // 2_10_10_10
|
||||
(0, 9, 0, 0, 10), // 2_10_10_10
|
||||
(0, 10, 0, 0, 8), // 8_8_8_8
|
||||
(0, 11, 0, 0, 32), // 32_32
|
||||
(0, 12, 0, 0, 16), // 16_16_16_16
|
||||
|
|
@ -224,7 +228,7 @@ public static class Gfx10UnifiedFormat
|
|||
(1, 6, 0, 10, 11),
|
||||
(1, 7, 0, 11, 11),
|
||||
(1, 8, 0, 10, 10),
|
||||
(1, 9, 0, 2, 10),
|
||||
(1, 9, 0, 10, 10),
|
||||
(1, 10, 1, 0, 8),
|
||||
(1, 11, 4, 0, 32),
|
||||
(1, 12, 2, 0, 16),
|
||||
|
|
@ -233,13 +237,13 @@ public static class Gfx10UnifiedFormat
|
|||
(2, 6, 0, 21, 11),
|
||||
(2, 7, 0, 22, 10),
|
||||
(2, 8, 0, 20, 10),
|
||||
(2, 9, 0, 12, 10),
|
||||
(2, 9, 0, 20, 10),
|
||||
(2, 10, 2, 0, 8),
|
||||
(2, 12, 4, 0, 16),
|
||||
(2, 13, 8, 0, 32),
|
||||
(2, 14, 8, 0, 32),
|
||||
(3, 8, 0, 30, 2),
|
||||
(3, 9, 0, 22, 10),
|
||||
(3, 9, 0, 30, 2),
|
||||
(3, 10, 3, 0, 8),
|
||||
(3, 12, 6, 0, 16),
|
||||
(3, 14, 12, 0, 32),
|
||||
|
|
|
|||
|
|
@ -0,0 +1,257 @@
|
|||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
namespace SharpEmu.ShaderCompiler.Ir;
|
||||
|
||||
// The instructions that run with every lane of EXEC set. EXEC starts full; it stays full
|
||||
// through S_WQM of a full EXEC and comes back when a saved copy of a full EXEC is moved or
|
||||
// ORed back into it. Any other EXEC write (V_CMPX, S_AND_SAVEEXEC, ...) makes it unknown,
|
||||
// and a join is full only when every path into it is. A backend can skip the EXEC guard of
|
||||
// a vector register write there: no lane keeps its old value, so the old value need not
|
||||
// stay live until the write.
|
||||
public static class Gen5ExecFullAnalysis
|
||||
{
|
||||
private const uint ExecLow = 126;
|
||||
private const uint ExecHigh = 127;
|
||||
|
||||
private readonly struct State(bool reached, bool execFull, UInt128 fullCopies)
|
||||
{
|
||||
public readonly bool Reached = reached;
|
||||
public readonly bool ExecFull = execFull;
|
||||
// SGPRs 0..127 holding (part of) a copy of a full EXEC.
|
||||
public readonly UInt128 FullCopies = fullCopies;
|
||||
|
||||
public static State Meet(State left, State right) =>
|
||||
!left.Reached ? right
|
||||
: !right.Reached ? left
|
||||
: new State(true, left.ExecFull && right.ExecFull, left.FullCopies & right.FullCopies);
|
||||
|
||||
public bool SameAs(State other) =>
|
||||
Reached == other.Reached && ExecFull == other.ExecFull && FullCopies == other.FullCopies;
|
||||
}
|
||||
|
||||
// Pcs of the instructions that start with a full EXEC.
|
||||
public static IReadOnlySet<uint> Analyze(Gen5ShaderProgram program, bool wave32)
|
||||
{
|
||||
var instructions = program.Instructions;
|
||||
var result = new HashSet<uint>();
|
||||
if (instructions.Count == 0)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
var indexByPc = new Dictionary<uint, int>(instructions.Count);
|
||||
for (var index = 0; index < instructions.Count; index++)
|
||||
{
|
||||
if (instructions[index].Opcode is "SSetpcB64" or "SSwappcB64" or "SRfeB64")
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
indexByPc[instructions[index].Pc] = index;
|
||||
}
|
||||
|
||||
var states = new State[instructions.Count];
|
||||
states[0] = new State(true, true, 0);
|
||||
var pending = new Queue<int>();
|
||||
var queued = new bool[instructions.Count];
|
||||
pending.Enqueue(0);
|
||||
queued[0] = true;
|
||||
while (pending.TryDequeue(out var index))
|
||||
{
|
||||
queued[index] = false;
|
||||
var instruction = instructions[index];
|
||||
var output = Transfer(instruction, states[index], wave32);
|
||||
void Flow(int successor)
|
||||
{
|
||||
var merged = State.Meet(states[successor], output);
|
||||
if (!merged.SameAs(states[successor]))
|
||||
{
|
||||
states[successor] = merged;
|
||||
if (!queued[successor])
|
||||
{
|
||||
queued[successor] = true;
|
||||
pending.Enqueue(successor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (Gen5IrBranchResolver.IsTerminator(instruction))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (Gen5IrBranchResolver.Instance.TryGetBranchTarget(instruction, out var targetPc))
|
||||
{
|
||||
if (!indexByPc.TryGetValue(targetPc, out var target))
|
||||
{
|
||||
// A branch out of the decoded range: give up on the whole program.
|
||||
return new HashSet<uint>();
|
||||
}
|
||||
|
||||
Flow(target);
|
||||
if (Gen5IrBranchResolver.IsUnconditionalBranch(instruction))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (index + 1 < instructions.Count)
|
||||
{
|
||||
Flow(index + 1);
|
||||
}
|
||||
}
|
||||
|
||||
for (var index = 0; index < instructions.Count; index++)
|
||||
{
|
||||
if (states[index].Reached && states[index].ExecFull)
|
||||
{
|
||||
result.Add(instructions[index].Pc);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static State Transfer(Gen5ShaderInstruction instruction, State input, bool wave32)
|
||||
{
|
||||
if (!input.Reached)
|
||||
{
|
||||
return input;
|
||||
}
|
||||
|
||||
var execFull = input.ExecFull;
|
||||
var copies = input.FullCopies;
|
||||
var opcode = instruction.Opcode;
|
||||
var sources = instruction.Sources;
|
||||
var destinations = instruction.Destinations;
|
||||
var wide = opcode.StartsWith('S') &&
|
||||
(opcode.EndsWith("B64", StringComparison.Ordinal) ||
|
||||
opcode.EndsWith("U64", StringComparison.Ordinal) ||
|
||||
opcode.EndsWith("I64", StringComparison.Ordinal));
|
||||
|
||||
// Registers written, with EXEC treated separately.
|
||||
UInt128 written = 0;
|
||||
var writesExec = false;
|
||||
foreach (var destination in destinations)
|
||||
{
|
||||
if (destination.Kind != Gen5OperandKind.ScalarRegister)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
written |= Bit(destination.Value);
|
||||
if (wide)
|
||||
{
|
||||
written |= Bit(destination.Value + 1);
|
||||
}
|
||||
}
|
||||
|
||||
var scalarDestination = instruction.Control switch
|
||||
{
|
||||
Gen5Vop3Control control => control.ScalarDestination,
|
||||
Gen5SdwaControl control => control.ScalarDestination,
|
||||
_ => null,
|
||||
};
|
||||
if (scalarDestination is { } sdst)
|
||||
{
|
||||
written |= Bit(sdst) | Bit(sdst + 1);
|
||||
}
|
||||
|
||||
if (opcode.StartsWith("VCmp", StringComparison.Ordinal) && scalarDestination is null)
|
||||
{
|
||||
// VOPC compares write VCC, V_CMPX writes EXEC.
|
||||
written |= opcode.StartsWith("VCmpx", StringComparison.Ordinal)
|
||||
? Bit(ExecLow) | Bit(ExecHigh)
|
||||
: Bit(106) | Bit(107);
|
||||
}
|
||||
|
||||
if (opcode.StartsWith('V') && opcode.Contains("Co", StringComparison.Ordinal) && scalarDestination is null)
|
||||
{
|
||||
// VOP2 carry forms write VCC without listing it.
|
||||
written |= Bit(106) | Bit(107);
|
||||
}
|
||||
|
||||
if (opcode.Contains("Saveexec", StringComparison.Ordinal) || opcode.Contains("Wrexec", StringComparison.Ordinal) ||
|
||||
opcode.StartsWith("SMovrel", StringComparison.Ordinal))
|
||||
{
|
||||
// These also write EXEC, or registers chosen at run time.
|
||||
written |= Bit(ExecLow) | Bit(ExecHigh);
|
||||
if (opcode.StartsWith("SMovrel", StringComparison.Ordinal))
|
||||
{
|
||||
return new State(true, false, 0);
|
||||
}
|
||||
}
|
||||
|
||||
writesExec = (written & (Bit(ExecLow) | Bit(ExecHigh))) != 0;
|
||||
var nextCopies = copies & ~written;
|
||||
var nextExecFull = execFull;
|
||||
if (writesExec)
|
||||
{
|
||||
nextExecFull = false;
|
||||
var onlyExecDestination = destinations.Count == 1 && destinations[0] is { Kind: Gen5OperandKind.ScalarRegister, Value: ExecLow };
|
||||
// In wave64 a 32-bit write leaves EXEC_HI as it was.
|
||||
if (onlyExecDestination && scalarDestination is null && (wide || wave32))
|
||||
{
|
||||
switch (opcode)
|
||||
{
|
||||
case "SWqmB64" or "SWqmB32" when sources.Count == 1 && IsExec(sources[0]):
|
||||
nextExecFull = execFull;
|
||||
break;
|
||||
case "SMovB64" or "SMovB32" when sources.Count == 1:
|
||||
nextExecFull = IsFullCopy(sources[0], copies, execFull, wide);
|
||||
break;
|
||||
case "SOrB64" or "SOrB32" when sources.Count == 2:
|
||||
nextExecFull = IsFullCopy(sources[0], copies, execFull, wide) ||
|
||||
IsFullCopy(sources[1], copies, execFull, wide);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (execFull && opcode is "SMovB64" or "SMovB32" && destinations.Count == 1 &&
|
||||
destinations[0].Kind == Gen5OperandKind.ScalarRegister && sources.Count == 1 && IsExec(sources[0]))
|
||||
{
|
||||
// A saved copy of the full EXEC.
|
||||
nextCopies |= Bit(destinations[0].Value);
|
||||
if (wide)
|
||||
{
|
||||
nextCopies |= Bit(destinations[0].Value + 1);
|
||||
}
|
||||
}
|
||||
|
||||
if (opcode.Contains("Saveexec", StringComparison.Ordinal) && execFull && destinations.Count >= 1 &&
|
||||
destinations[0].Kind == Gen5OperandKind.ScalarRegister && destinations[0].Value < ExecLow)
|
||||
{
|
||||
// The saved destination receives the (full) EXEC from before the instruction.
|
||||
nextCopies |= Bit(destinations[0].Value);
|
||||
if (wide)
|
||||
{
|
||||
nextCopies |= Bit(destinations[0].Value + 1);
|
||||
}
|
||||
}
|
||||
|
||||
return new State(true, nextExecFull, nextCopies);
|
||||
}
|
||||
|
||||
private static bool IsExec(Gen5Operand operand) =>
|
||||
operand.Kind == Gen5OperandKind.ScalarRegister && operand.Value == ExecLow;
|
||||
|
||||
// True when the operand is a full EXEC or a saved copy of one.
|
||||
private static bool IsFullCopy(Gen5Operand operand, UInt128 copies, bool execFull, bool wide)
|
||||
{
|
||||
if (operand.Kind != Gen5OperandKind.ScalarRegister)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (operand.Value == ExecLow)
|
||||
{
|
||||
return execFull;
|
||||
}
|
||||
|
||||
var mask = Bit(operand.Value) | (wide ? Bit(operand.Value + 1) : 0);
|
||||
return (copies & mask) == mask;
|
||||
}
|
||||
|
||||
private static UInt128 Bit(uint register) => register < 128 ? UInt128.One << (int)register : 0;
|
||||
}
|
||||
|
|
@ -0,0 +1,309 @@
|
|||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
namespace SharpEmu.ShaderCompiler.Ir;
|
||||
|
||||
// The values M0 can hold at each V_MOVRELS/V_MOVRELD/V_MOVRELSD, when they follow from
|
||||
// straight-line scalar code (bit-field extracts, masks, shifts, adds and multiplies by
|
||||
// constants). A backend that knows the few registers a relative move can reach selects
|
||||
// among them with constant register numbers; a register file indexed at run time keeps
|
||||
// the whole file out of hardware registers.
|
||||
public static class Gen5MoveRelativeOffsets
|
||||
{
|
||||
private const uint M0Register = 124;
|
||||
private const int MaxValues = 64;
|
||||
private const int MaxDepth = 8;
|
||||
private const int MaxScanSteps = 4096;
|
||||
|
||||
// Maps the pc of every resolved relative move to its sorted M0 values; a move that
|
||||
// is missing from the map could not be bounded.
|
||||
public static IReadOnlyDictionary<uint, uint[]> Analyze(Gen5ShaderProgram program)
|
||||
{
|
||||
var result = new Dictionary<uint, uint[]>();
|
||||
var instructions = program.Instructions;
|
||||
var hasRelativeMove = false;
|
||||
foreach (var instruction in instructions)
|
||||
{
|
||||
if (IsBoundedRelativeMove(instruction.Opcode))
|
||||
{
|
||||
hasRelativeMove = true;
|
||||
}
|
||||
|
||||
// A computed jump can land anywhere; no straight-line reasoning holds.
|
||||
if (instruction.Opcode is "SSetpcB64" or "SSwappcB64" or "SRfeB64")
|
||||
{
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasRelativeMove)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
// Branch target pc -> index of the single branch reaching it, or -1 when several
|
||||
// branches do.
|
||||
var targets = new Dictionary<uint, int>();
|
||||
for (var index = 0; index < instructions.Count; index++)
|
||||
{
|
||||
if (Gen5IrBranchResolver.Instance.TryGetBranchTarget(instructions[index], out var target))
|
||||
{
|
||||
targets[target] = targets.ContainsKey(target) ? -1 : index;
|
||||
}
|
||||
}
|
||||
|
||||
for (var index = 0; index < instructions.Count; index++)
|
||||
{
|
||||
if (!IsBoundedRelativeMove(instructions[index].Opcode))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var values = new HashSet<uint>();
|
||||
if (TryResolveRegister(instructions, targets, index, M0Register, 0, values))
|
||||
{
|
||||
var sorted = values.ToArray();
|
||||
Array.Sort(sorted);
|
||||
result[instructions[index].Pc] = sorted;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static bool IsBoundedRelativeMove(string opcode) =>
|
||||
opcode is "VMovrelsB32" or "VMovreldB32" or "VMovrelsdB32";
|
||||
|
||||
// The values the scalar register holds when the instruction at index executes.
|
||||
private static bool TryResolveRegister(
|
||||
IReadOnlyList<Gen5ShaderInstruction> instructions,
|
||||
Dictionary<uint, int> targets,
|
||||
int index,
|
||||
uint register,
|
||||
int depth,
|
||||
HashSet<uint> values)
|
||||
{
|
||||
if (depth > MaxDepth)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var steps = 0;
|
||||
for (var at = index - 1; at >= 0; at--)
|
||||
{
|
||||
if (++steps > MaxScanSteps)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// A join: follow its one predecessor, or give up when two paths meet.
|
||||
if (targets.TryGetValue(instructions[at + 1].Pc, out var branch))
|
||||
{
|
||||
var fallsThrough = !Gen5IrBranchResolver.IsUnconditionalBranch(instructions[at]) &&
|
||||
!Gen5IrBranchResolver.IsTerminator(instructions[at]);
|
||||
if (branch < 0 || fallsThrough)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
at = branch;
|
||||
continue;
|
||||
}
|
||||
|
||||
var instruction = instructions[at];
|
||||
if (!Writes(instruction, register, out var exact))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
return exact && TryEvaluate(instructions, targets, at, instruction, depth, values);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// exact: the instruction's only effect on the register is its listed destination.
|
||||
private static bool Writes(Gen5ShaderInstruction instruction, uint register, out bool exact)
|
||||
{
|
||||
exact = false;
|
||||
var wide = instruction.Opcode.StartsWith('S') &&
|
||||
(instruction.Opcode.EndsWith("B64", StringComparison.Ordinal) ||
|
||||
instruction.Opcode.EndsWith("U64", StringComparison.Ordinal) ||
|
||||
instruction.Opcode.EndsWith("I64", StringComparison.Ordinal));
|
||||
foreach (var destination in instruction.Destinations)
|
||||
{
|
||||
if (destination.Kind != Gen5OperandKind.ScalarRegister)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (destination.Value == register)
|
||||
{
|
||||
exact = !wide && instruction.Destinations.Count == 1;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (wide && destination.Value + 1 == register)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
var scalarDestination = instruction.Control switch
|
||||
{
|
||||
Gen5Vop3Control control => control.ScalarDestination,
|
||||
Gen5SdwaControl control => control.ScalarDestination,
|
||||
_ => null,
|
||||
};
|
||||
if (scalarDestination is { } written && (written == register || written + 1 == register))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// Vector compares and carries write VCC or EXEC without listing them; on gfx10 a
|
||||
// V_CMPX writes EXEC only.
|
||||
if (instruction.Opcode.StartsWith("VCmpx", StringComparison.Ordinal))
|
||||
{
|
||||
return register is 126 or 127;
|
||||
}
|
||||
|
||||
if (register is 106 or 107 &&
|
||||
instruction.Opcode.StartsWith('V') &&
|
||||
(instruction.Opcode.StartsWith("VCmp", StringComparison.Ordinal) ||
|
||||
instruction.Opcode.Contains("Co", StringComparison.Ordinal) ||
|
||||
instruction.Opcode.Contains("Div", StringComparison.Ordinal)))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// Relative scalar moves and loads write registers picked at run time.
|
||||
return instruction.Opcode.StartsWith("SMovrel", StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
private static bool TryEvaluate(
|
||||
IReadOnlyList<Gen5ShaderInstruction> instructions,
|
||||
Dictionary<uint, int> targets,
|
||||
int index,
|
||||
Gen5ShaderInstruction instruction,
|
||||
int depth,
|
||||
HashSet<uint> values)
|
||||
{
|
||||
var sources = instruction.Sources;
|
||||
switch (instruction.Opcode)
|
||||
{
|
||||
case "SMovB32" when sources.Count == 1:
|
||||
return TryResolveOperand(instructions, targets, index, sources[0], depth, values);
|
||||
case "SBfeU32" when sources.Count == 2 && TryConstant(sources[1], out var field):
|
||||
{
|
||||
var width = (int)((field >> 16) & 0x7F);
|
||||
if (width > 6)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
for (var value = 0u; value < 1u << width; value++)
|
||||
{
|
||||
values.Add(value);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
case "SAndB32" when sources.Count == 2:
|
||||
{
|
||||
if (!TryConstant(sources[1], out var mask) && !TryConstant(sources[0], out mask))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (mask >= MaxValues)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
for (var value = 0u; value <= mask; value++)
|
||||
{
|
||||
if ((value & ~mask) == 0)
|
||||
{
|
||||
values.Add(value);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
case "SLshlB32" when sources.Count == 2 && TryConstant(sources[1], out var shift):
|
||||
return TryCombine(instructions, targets, index, sources[0], depth, values, value => value << (int)(shift & 31));
|
||||
case "SMulI32" or "SAddU32" or "SAddI32" when sources.Count == 2:
|
||||
{
|
||||
var multiply = instruction.Opcode == "SMulI32";
|
||||
if (TryConstant(sources[1], out var right))
|
||||
{
|
||||
return TryCombine(instructions, targets, index, sources[0], depth, values,
|
||||
value => multiply ? value * right : value + right);
|
||||
}
|
||||
|
||||
if (TryConstant(sources[0], out var left))
|
||||
{
|
||||
return TryCombine(instructions, targets, index, sources[1], depth, values,
|
||||
value => multiply ? value * left : value + left);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryCombine(
|
||||
IReadOnlyList<Gen5ShaderInstruction> instructions,
|
||||
Dictionary<uint, int> targets,
|
||||
int index,
|
||||
Gen5Operand operand,
|
||||
int depth,
|
||||
HashSet<uint> values,
|
||||
Func<uint, uint> apply)
|
||||
{
|
||||
var inputs = new HashSet<uint>();
|
||||
if (!TryResolveOperand(instructions, targets, index, operand, depth, inputs))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach (var input in inputs)
|
||||
{
|
||||
values.Add(unchecked(apply(input)));
|
||||
}
|
||||
|
||||
return values.Count <= MaxValues;
|
||||
}
|
||||
|
||||
private static bool TryResolveOperand(
|
||||
IReadOnlyList<Gen5ShaderInstruction> instructions,
|
||||
Dictionary<uint, int> targets,
|
||||
int index,
|
||||
Gen5Operand operand,
|
||||
int depth,
|
||||
HashSet<uint> values)
|
||||
{
|
||||
if (TryConstant(operand, out var constant))
|
||||
{
|
||||
values.Add(constant);
|
||||
return true;
|
||||
}
|
||||
|
||||
return operand.Kind == Gen5OperandKind.ScalarRegister &&
|
||||
TryResolveRegister(instructions, targets, index, operand.Value, depth + 1, values) &&
|
||||
values.Count <= MaxValues;
|
||||
}
|
||||
|
||||
private static bool TryConstant(Gen5Operand operand, out uint value)
|
||||
{
|
||||
value = 0;
|
||||
return operand.Kind switch
|
||||
{
|
||||
Gen5OperandKind.LiteralConstant => (value = operand.Value) == operand.Value,
|
||||
Gen5OperandKind.EncodedConstant => Gen5InlineConstants.TryDecode(operand.Value, out value),
|
||||
_ => false,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -36,6 +36,7 @@ public sealed class ResourceMaterializationCache
|
|||
public long Hits { get; private set; }
|
||||
public long Misses { get; private set; }
|
||||
public long Uncacheable { get; private set; }
|
||||
public long TableRefreshes { get; private set; }
|
||||
|
||||
// Process-wide counters since the previous call, for the periodic render report.
|
||||
public static string TakeReport()
|
||||
|
|
@ -57,14 +58,26 @@ public sealed class ResourceMaterializationCache
|
|||
out ResourceMaterializationFailure failure)
|
||||
{
|
||||
var key = KeyOf(plan, inputs);
|
||||
if (TryFind(key, plan, inputs, out var cached) && Validate(cached, residentReader))
|
||||
if (TryFind(key, plan, inputs, out var cached))
|
||||
{
|
||||
Hits++;
|
||||
Interlocked.Increment(ref _totalHits);
|
||||
snapshot = cached.Snapshot;
|
||||
specialization = cached.Specialization;
|
||||
failure = default;
|
||||
return true;
|
||||
if (Validate(cached, residentReader))
|
||||
{
|
||||
Hits++;
|
||||
Interlocked.Increment(ref _totalHits);
|
||||
snapshot = cached.Snapshot;
|
||||
specialization = cached.Specialization;
|
||||
failure = default;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (TryRefreshTable(key, cached, plan, inputs, residentReader, out var refreshed))
|
||||
{
|
||||
TableRefreshes++;
|
||||
snapshot = refreshed.Snapshot;
|
||||
specialization = refreshed.Specialization;
|
||||
failure = default;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
Misses++;
|
||||
|
|
@ -77,6 +90,7 @@ public sealed class ResourceMaterializationCache
|
|||
ReadMemory = recorder.Wrap(inputs.ReadMemory, clean: false),
|
||||
ReadCleanMemory = recorder.Wrap(inputs.ReadCleanMemory, clean: true),
|
||||
ComputeState = inputs.ComputeState,
|
||||
TablePhase = recorder.SetTablePhase,
|
||||
};
|
||||
if (!ResourceMaterializer.Materialize(plan, recording, ref snapshot, ref specialization, out failure))
|
||||
return false;
|
||||
|
|
@ -92,6 +106,121 @@ public sealed class ResourceMaterializationCache
|
|||
return true;
|
||||
}
|
||||
|
||||
// Most stale entries in Demon's Souls differ only in words the shader reads through scalar
|
||||
// loads (inline constants rewritten every frame); the descriptors, device ranges and
|
||||
// specialization are unchanged. When every changed word was read only while evaluating the
|
||||
// flattened table, only the table is evaluated again. It is refused when the plan's
|
||||
// specialization reads or extends the table (indirect or candidate tables), when the table
|
||||
// now reads a word the entry did not validate, or when a word moved between the two reads.
|
||||
private bool TryRefreshTable(ulong key, Entry cached, ShaderResourcePlan plan, ResourceRuntimeInputs inputs,
|
||||
ResidentGuestBytesReader residentReader, out Entry refreshed)
|
||||
{
|
||||
refreshed = null!;
|
||||
if (!cached.TableRefreshable)
|
||||
return false;
|
||||
|
||||
var current = new byte[cached.Bytes.Length];
|
||||
for (var index = 0; index < cached.RangeAddresses.Length; index++)
|
||||
{
|
||||
if (!residentReader(cached.RangeAddresses[index], current.AsSpan(cached.RangeOffsets[index], cached.RangeLengths[index]), cached.RangeClean[index]))
|
||||
return false;
|
||||
}
|
||||
|
||||
var changed = false;
|
||||
for (var offset = 0; offset < current.Length; offset += sizeof(uint))
|
||||
{
|
||||
if (current.AsSpan(offset, sizeof(uint)).SequenceEqual(cached.Bytes.AsSpan(offset, sizeof(uint))))
|
||||
continue;
|
||||
if (!cached.WordTableOnly[offset / sizeof(uint)])
|
||||
return false;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (!changed)
|
||||
return false;
|
||||
|
||||
var recorder = new ReadRecorder();
|
||||
var recording = new ResourceRuntimeInputs
|
||||
{
|
||||
UserData = inputs.UserData,
|
||||
ShaderBase = inputs.ShaderBase,
|
||||
ReadMemory = recorder.Wrap(inputs.ReadMemory, clean: false),
|
||||
ReadCleanMemory = recorder.Wrap(inputs.ReadCleanMemory, clean: true),
|
||||
ComputeState = inputs.ComputeState,
|
||||
};
|
||||
var cachedTable = cached.Snapshot.FlattenedResourceTable;
|
||||
if (!ResourceMaterializer.TryEvaluateTable(plan, recording, out var table) || recorder.Failed || table.Length != cachedTable.Length)
|
||||
return false;
|
||||
|
||||
foreach (var (address, word, _, _) in recorder.Reads)
|
||||
{
|
||||
if (!TryFindWord(cached, address, out var offset) ||
|
||||
System.Buffers.Binary.BinaryPrimitives.ReadUInt32LittleEndian(current.AsSpan(offset, sizeof(uint))) != word)
|
||||
return false;
|
||||
}
|
||||
|
||||
// The written device-address slots come from reads validated unchanged above.
|
||||
foreach (var slot in plan.WrittenRangeSlotByHandle.Values)
|
||||
cachedTable.AsSpan((int)slot, ShaderResourcePlan.WrittenRangeDwordCount).CopyTo(table.AsSpan((int)slot));
|
||||
|
||||
var previous = cached.Snapshot;
|
||||
refreshed = new Entry
|
||||
{
|
||||
Plan = cached.Plan,
|
||||
UserData = cached.UserData,
|
||||
ShaderBase = cached.ShaderBase,
|
||||
ComputeState = cached.ComputeState,
|
||||
RangeAddresses = cached.RangeAddresses,
|
||||
RangeOffsets = cached.RangeOffsets,
|
||||
RangeLengths = cached.RangeLengths,
|
||||
RangeClean = cached.RangeClean,
|
||||
WordTableOnly = cached.WordTableOnly,
|
||||
TableRefreshable = true,
|
||||
Bytes = current,
|
||||
Snapshot = new ResourceSnapshot
|
||||
{
|
||||
Buffers = previous.Buffers,
|
||||
Images = previous.Images,
|
||||
Samplers = previous.Samplers,
|
||||
FlattenedResourceTable = table,
|
||||
UserData = previous.UserData,
|
||||
DeviceAddressRanges = previous.DeviceAddressRanges,
|
||||
},
|
||||
Specialization = cached.Specialization,
|
||||
};
|
||||
Store(key, refreshed);
|
||||
return true;
|
||||
}
|
||||
|
||||
// The byte offset of a recorded dword in the entry's bytes, found by its address.
|
||||
private static bool TryFindWord(Entry entry, ulong address, out int offset)
|
||||
{
|
||||
offset = 0;
|
||||
var addresses = entry.RangeAddresses;
|
||||
int low = 0, high = addresses.Length - 1, found = -1;
|
||||
while (low <= high)
|
||||
{
|
||||
var middle = (low + high) >>> 1;
|
||||
if (addresses[middle] <= address)
|
||||
{
|
||||
found = middle;
|
||||
low = middle + 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
high = middle - 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (found < 0)
|
||||
return false;
|
||||
var delta = address - addresses[found];
|
||||
if (delta % sizeof(uint) != 0 || delta >= (ulong)entry.RangeLengths[found])
|
||||
return false;
|
||||
offset = entry.RangeOffsets[found] + (int)delta;
|
||||
return true;
|
||||
}
|
||||
|
||||
private static ulong KeyOf(ShaderResourcePlan plan, ResourceRuntimeInputs inputs)
|
||||
{
|
||||
var hash = new HashCode();
|
||||
|
|
@ -158,6 +287,10 @@ public sealed class ResourceMaterializationCache
|
|||
public required int[] RangeOffsets { get; init; }
|
||||
public required int[] RangeLengths { get; init; }
|
||||
public required bool[] RangeClean { get; init; }
|
||||
// Per recorded dword: read only while the flattened table was evaluated.
|
||||
public required bool[] WordTableOnly { get; init; }
|
||||
// The table has the plan's plain layout, so no specialization read or extended it.
|
||||
public required bool TableRefreshable { get; init; }
|
||||
public required byte[] Bytes { get; init; }
|
||||
public required ResourceSnapshot Snapshot { get; init; }
|
||||
public required ResourceSpecialization Specialization { get; init; }
|
||||
|
|
@ -176,10 +309,15 @@ public sealed class ResourceMaterializationCache
|
|||
|
||||
private sealed class ReadRecorder
|
||||
{
|
||||
private readonly List<(ulong Address, uint Word, bool Clean)> _reads = new();
|
||||
private readonly List<(ulong Address, uint Word, bool Clean, bool Table)> _reads = new();
|
||||
private bool _inTable;
|
||||
|
||||
public bool Failed { get; private set; }
|
||||
|
||||
public List<(ulong Address, uint Word, bool Clean, bool Table)> Reads => _reads;
|
||||
|
||||
public void SetTablePhase(bool inTable) => _inTable = inTable;
|
||||
|
||||
public GuestWordReader? Wrap(GuestWordReader? inner, bool clean)
|
||||
{
|
||||
if (inner is null)
|
||||
|
|
@ -192,7 +330,7 @@ public sealed class ResourceMaterializationCache
|
|||
return false;
|
||||
}
|
||||
|
||||
_reads.Add((address, word, clean));
|
||||
_reads.Add((address, word, clean, _inTable));
|
||||
return true;
|
||||
};
|
||||
}
|
||||
|
|
@ -206,16 +344,20 @@ public sealed class ResourceMaterializationCache
|
|||
var lengths = new List<int>();
|
||||
var clean = new List<bool>();
|
||||
var bytes = new List<byte>(_reads.Count * sizeof(uint));
|
||||
var tableOnly = new List<bool>(_reads.Count);
|
||||
ulong end = 0;
|
||||
var previous = ulong.MaxValue;
|
||||
foreach (var (address, word, wordClean) in _reads)
|
||||
foreach (var (address, word, wordClean, table) in _reads)
|
||||
{
|
||||
if (address == previous)
|
||||
{
|
||||
clean[^1] |= wordClean;
|
||||
tableOnly[^1] &= table;
|
||||
continue;
|
||||
}
|
||||
|
||||
tableOnly.Add(table);
|
||||
|
||||
previous = address;
|
||||
if (addresses.Count == 0 || address != end)
|
||||
{
|
||||
|
|
@ -247,6 +389,9 @@ public sealed class ResourceMaterializationCache
|
|||
RangeOffsets = [.. offsets],
|
||||
RangeLengths = [.. lengths],
|
||||
RangeClean = [.. clean],
|
||||
WordTableOnly = [.. tableOnly],
|
||||
TableRefreshable = snapshot.FlattenedResourceTable.Length ==
|
||||
plan.TableReads.Count + plan.WrittenRangeCount * ShaderResourcePlan.WrittenRangeDwordCount,
|
||||
Bytes = [.. bytes],
|
||||
Snapshot = snapshot,
|
||||
Specialization = specialization,
|
||||
|
|
|
|||
|
|
@ -111,6 +111,12 @@ public static class ResourceMaterializer
|
|||
return true;
|
||||
}
|
||||
|
||||
// Evaluates only the flattened table, laid out as a full materialization lays it out
|
||||
// before specialization; the written device-address slots are left zero for the caller.
|
||||
public static bool TryEvaluateTable(ShaderResourcePlan plan, ResourceRuntimeInputs inputs, out uint[] table) =>
|
||||
RuntimeValueEvaluator.EvaluateSources(plan, [], inputs, plan.CleanFlatSlots, evaluateTable: true, out _, out table, out _,
|
||||
additionalTableWords: checked(plan.WrittenRangeCount * ShaderResourcePlan.WrittenRangeDwordCount));
|
||||
|
||||
// ---- snapshot ----
|
||||
|
||||
private static bool MaterializeSnapshot(ShaderResourcePlan plan, ResourceRuntimeInputs inputs,
|
||||
|
|
@ -1176,6 +1182,35 @@ public static class ResourceMaterializer
|
|||
}
|
||||
}
|
||||
|
||||
// ApplyTo appends a depth-compare copy of every sampler shared by ordinary and
|
||||
// depth-reference sampling, after the point samplers; the snapshot needs the same words there.
|
||||
var compareUsage = new byte[ShaderResourceInfo.MaxSamplers];
|
||||
foreach (var pair in info.SampledPairs)
|
||||
{
|
||||
var image = info.Images[(int)pair.Image];
|
||||
var specialized = images[(int)pair.Image];
|
||||
var sampler = RequiresPointSampler(specialized.NumericClass, specialized.ConversionFormat)
|
||||
? samplerPlan.PointSampler[pair.Sampler]
|
||||
: pair.Sampler;
|
||||
var depthCompare = image.DepthCompare && specialized.EmulatedCompareFunction < 0;
|
||||
compareUsage[sampler] |= depthCompare ? (byte)2 : (byte)1;
|
||||
}
|
||||
|
||||
for (var index = 0; index < snapshot.Samplers.Length && index < compareUsage.Length; index++)
|
||||
{
|
||||
if (compareUsage[index] == 3)
|
||||
{
|
||||
if (snapshot.Samplers.Length >= ShaderResourceInfo.MaxSamplers)
|
||||
{
|
||||
return Fail("specialized sampler layout exceeds its resource limit");
|
||||
}
|
||||
|
||||
Array.Resize(ref snapshot.Samplers, snapshot.Samplers.Length + 1);
|
||||
snapshot.Samplers[^1] = snapshot.Samplers[index];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Bounded runtime V# candidates are appended after the plan's buffers; each draw
|
||||
// owns their words and the run-time key mapping sits in the flattened table.
|
||||
var baseBufferCount = buffers.Count;
|
||||
|
|
|
|||
|
|
@ -18,6 +18,10 @@ public sealed class ResourceRuntimeInputs
|
|||
public GuestWordReader? ReadCleanMemory { get; init; }
|
||||
public ComputeSelectorState? ComputeState { get; init; }
|
||||
|
||||
// Told true before the flattened table's words are evaluated and false after, so a reader
|
||||
// wrapper can tell the words only the table reads from those the descriptors depend on.
|
||||
public Action<bool>? TablePhase { get; init; }
|
||||
|
||||
public ResourceRuntimeInputs WithReader(GuestWordReader? reader) => new()
|
||||
{
|
||||
UserData = UserData,
|
||||
|
|
@ -25,6 +29,7 @@ public sealed class ResourceRuntimeInputs
|
|||
ReadMemory = reader,
|
||||
ReadCleanMemory = ReadCleanMemory,
|
||||
ComputeState = ComputeState,
|
||||
TablePhase = TablePhase,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ internal sealed class RuntimeEvaluationScratch : IDisposable
|
|||
private RuntimeEvaluationScratch? _nextAvailable;
|
||||
private bool _rented;
|
||||
|
||||
internal Dictionary<ScalarValue, ulong> Values { get; } = [];
|
||||
internal ScalarValueCache Values { get; } = new();
|
||||
internal List<ScalarValue> Visiting { get; } = [];
|
||||
|
||||
internal static RuntimeEvaluationScratch Rent()
|
||||
|
|
@ -31,7 +31,7 @@ internal sealed class RuntimeEvaluationScratch : IDisposable
|
|||
public void Dispose()
|
||||
{
|
||||
if (!_rented) return;
|
||||
Values.Clear();
|
||||
Values.Reset();
|
||||
Visiting.Clear();
|
||||
_rented = false;
|
||||
_nextAvailable = _available;
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ public sealed class RuntimeValueEvaluator
|
|||
private readonly IReadOnlyList<byte> _cleanFlatSlots;
|
||||
private readonly RuntimeValueEvaluator? _cleanEvaluator;
|
||||
private readonly ScalarValue? _activeMask;
|
||||
private readonly Dictionary<ScalarValue, ulong> _cache;
|
||||
private readonly ScalarValueCache _cache;
|
||||
private readonly List<ScalarValue> _visiting;
|
||||
|
||||
public RuntimeValueEvaluator(
|
||||
|
|
@ -25,7 +25,7 @@ public sealed class RuntimeValueEvaluator
|
|||
IReadOnlyList<byte>? cleanFlatSlots = null,
|
||||
RuntimeValueEvaluator? cleanEvaluator = null,
|
||||
ScalarValue? activeMask = null)
|
||||
: this(plan, inputs, cleanFlatSlots, cleanEvaluator, activeMask, [], [])
|
||||
: this(plan, inputs, cleanFlatSlots, cleanEvaluator, activeMask, new ScalarValueCache(), [])
|
||||
{
|
||||
}
|
||||
|
||||
|
|
@ -46,7 +46,7 @@ public sealed class RuntimeValueEvaluator
|
|||
IReadOnlyList<byte>? cleanFlatSlots,
|
||||
RuntimeValueEvaluator? cleanEvaluator,
|
||||
ScalarValue? activeMask,
|
||||
Dictionary<ScalarValue, ulong> cache,
|
||||
ScalarValueCache cache,
|
||||
List<ScalarValue> visiting)
|
||||
{
|
||||
_cache = cache;
|
||||
|
|
@ -373,16 +373,24 @@ public sealed class RuntimeValueEvaluator
|
|||
if (evaluateTable)
|
||||
{
|
||||
flattened = new uint[checked(plan.TableReads.Count + additionalTableWords)];
|
||||
foreach (var read in plan.TableReads)
|
||||
inputs.TablePhase?.Invoke(true);
|
||||
try
|
||||
{
|
||||
var clean = read.FlatOffset < cleanFlatSlots.Count && cleanFlatSlots[(int)read.FlatOffset] != 0;
|
||||
var selected = clean ? cleanEvaluator : evaluator;
|
||||
if (read.FlatOffset >= plan.TableReads.Count || !selected.Evaluate(read.Value, out var word))
|
||||
foreach (var read in plan.TableReads)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
var clean = read.FlatOffset < cleanFlatSlots.Count && cleanFlatSlots[(int)read.FlatOffset] != 0;
|
||||
var selected = clean ? cleanEvaluator : evaluator;
|
||||
if (read.FlatOffset >= plan.TableReads.Count || !selected.Evaluate(read.Value, out var word))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
flattened[(int)read.FlatOffset] = word;
|
||||
flattened[(int)read.FlatOffset] = word;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
inputs.TablePhase?.Invoke(false);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -232,6 +232,22 @@ public static class ScalarValueEquivalence
|
|||
return true;
|
||||
}
|
||||
|
||||
// A loop re-reads loop-invariant registers through phis that only merge the
|
||||
// entry value with themselves; compare the value they carry. Otherwise every
|
||||
// descriptor load inside a loop looks new, and a shader that reloads the same
|
||||
// three samplers 104 times exceeds the sampler table (Astro Bot 0xD97F248195E22298).
|
||||
left = ResolveInvariant(memory, left);
|
||||
right = ResolveInvariant(memory, right);
|
||||
if (left.IsUndefined || right.IsUndefined)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (ReferenceEquals(left, right))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (left.Kind != right.Kind || left.Type != right.Type || left.Operation != right.Operation ||
|
||||
left.Operands.Length != right.Operands.Length)
|
||||
{
|
||||
|
|
@ -287,6 +303,48 @@ public static class ScalarValueEquivalence
|
|||
return true;
|
||||
}
|
||||
|
||||
private static readonly System.Runtime.CompilerServices.ConditionalWeakTable<ScalarValue, StrongBox> ResolvedPhis = new();
|
||||
|
||||
[System.ThreadStatic]
|
||||
private static HashSet<ScalarValue>? _resolvingPhis;
|
||||
|
||||
private sealed class StrongBox(ScalarValue value)
|
||||
{
|
||||
public readonly ScalarValue Value = value;
|
||||
}
|
||||
|
||||
// The invariant value of a phi, or the phi itself when it merges different values
|
||||
// (or while that phi is already being resolved further up the stack).
|
||||
private static ScalarValue ResolveInvariant(MemoryAccessTable memory, ScalarValue value)
|
||||
{
|
||||
if (value.Kind != ScalarValueKind.Phi)
|
||||
{
|
||||
return value;
|
||||
}
|
||||
|
||||
if (ResolvedPhis.TryGetValue(value, out var cached))
|
||||
{
|
||||
return cached.Value;
|
||||
}
|
||||
|
||||
var resolving = _resolvingPhis ??= [];
|
||||
if (!resolving.Add(value))
|
||||
{
|
||||
return value;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var resolved = ResolveInvariantPhi(memory, value) ?? value;
|
||||
ResolvedPhis.AddOrUpdate(value, new StrongBox(resolved));
|
||||
return resolved;
|
||||
}
|
||||
finally
|
||||
{
|
||||
resolving.Remove(value);
|
||||
}
|
||||
}
|
||||
|
||||
// A phi whose non-phi operands are all equivalent resolves to that value; a phi that
|
||||
// merges different values resolves to null.
|
||||
public static ScalarValue? ResolveInvariantPhi(MemoryAccessTable memory, ScalarValue value)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,100 @@
|
|||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
namespace SharpEmu.ShaderCompiler.Resources;
|
||||
|
||||
// Evaluated values by ScalarValue.Id, reset once per draw. An open-addressing table whose
|
||||
// slots carry the generation that filled them, so a reset only bumps the generation: a
|
||||
// Dictionary.Clear zeroed its whole capacity on every draw.
|
||||
internal sealed class ScalarValueCache
|
||||
{
|
||||
private int[] _ids = new int[64];
|
||||
private uint[] _generations = new uint[64];
|
||||
private ulong[] _values = new ulong[64];
|
||||
private uint _generation = 1;
|
||||
private int _count;
|
||||
|
||||
public int Count => _count;
|
||||
|
||||
public int Capacity => _ids.Length;
|
||||
|
||||
public bool TryGetValue(ScalarValue key, out ulong value)
|
||||
{
|
||||
var mask = _ids.Length - 1;
|
||||
for (var slot = Slot(key.Id, mask); _generations[slot] == _generation; slot = (slot + 1) & mask)
|
||||
{
|
||||
if (_ids[slot] == key.Id)
|
||||
{
|
||||
value = _values[slot];
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
value = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
public ulong this[ScalarValue key]
|
||||
{
|
||||
set
|
||||
{
|
||||
if ((_count + 1) * 2 > _ids.Length)
|
||||
{
|
||||
Grow();
|
||||
}
|
||||
|
||||
Insert(key.Id, value);
|
||||
}
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
_count = 0;
|
||||
if (++_generation == 0)
|
||||
{
|
||||
Array.Clear(_generations);
|
||||
_generation = 1;
|
||||
}
|
||||
}
|
||||
|
||||
private void Insert(int id, ulong value)
|
||||
{
|
||||
var mask = _ids.Length - 1;
|
||||
var slot = Slot(id, mask);
|
||||
for (; _generations[slot] == _generation; slot = (slot + 1) & mask)
|
||||
{
|
||||
if (_ids[slot] == id)
|
||||
{
|
||||
_values[slot] = value;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
_generations[slot] = _generation;
|
||||
_ids[slot] = id;
|
||||
_values[slot] = value;
|
||||
_count++;
|
||||
}
|
||||
|
||||
private void Grow()
|
||||
{
|
||||
var ids = _ids;
|
||||
var generations = _generations;
|
||||
var values = _values;
|
||||
var generation = _generation;
|
||||
_ids = new int[ids.Length * 2];
|
||||
_generations = new uint[ids.Length * 2];
|
||||
_values = new ulong[ids.Length * 2];
|
||||
_generation = 1;
|
||||
_count = 0;
|
||||
for (var index = 0; index < ids.Length; index++)
|
||||
{
|
||||
if (generations[index] == generation)
|
||||
{
|
||||
Insert(ids[index], values[index]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static int Slot(int id, int mask) => (int)(((uint)id * 2654435769u) >> 7) & mask;
|
||||
}
|
||||
|
|
@ -186,6 +186,8 @@ public sealed partial class ScalarValueGraph
|
|||
return merged;
|
||||
}
|
||||
|
||||
_uniformJoin = pending ? null : FindUniformJoin(block, visited);
|
||||
|
||||
for (var register = 0; register < ScalarRegisterCount; register++)
|
||||
{
|
||||
merged.Scalars[register] = MergeSlot(block, register, visited, pending, state => state.Scalars[register]);
|
||||
|
|
@ -233,6 +235,90 @@ public sealed partial class ScalarValueGraph
|
|||
return merged;
|
||||
}
|
||||
|
||||
// The join of an if or if/else on SCC: every lane of the wave took the same side, so
|
||||
// a value merged there is a select on the branch condition rather than an opaque phi.
|
||||
// Shaders pick descriptors this way (a sampler reloaded when a constant-buffer flag is
|
||||
// set); as a select the host can still evaluate it per draw.
|
||||
private (ScalarValue Taken, int TakenPredecessor)? _uniformJoin;
|
||||
|
||||
private (ScalarValue Taken, int TakenPredecessor)? FindUniformJoin(int block, List<(int Block, RegisterState State)> visited)
|
||||
{
|
||||
var controlFlow = _graph.ControlFlow;
|
||||
var predecessors = controlFlow.Predecessors[block];
|
||||
if (visited.Count != 2 || predecessors.Count != 2 || visited.Any(entry => entry.Block < 0) ||
|
||||
controlFlow.LoopHeaders.Contains(block))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
int SinglePredecessor(int candidate) =>
|
||||
controlFlow.Predecessors[candidate].Count == 1 && controlFlow.Successors[candidate].Count == 1 &&
|
||||
controlFlow.Successors[candidate][0] == block
|
||||
? controlFlow.Predecessors[candidate][0]
|
||||
: -1;
|
||||
|
||||
var first = predecessors[0];
|
||||
var second = predecessors[1];
|
||||
int branch;
|
||||
if (SinglePredecessor(first) == second)
|
||||
{
|
||||
branch = second;
|
||||
}
|
||||
else if (SinglePredecessor(second) == first)
|
||||
{
|
||||
branch = first;
|
||||
}
|
||||
else if (SinglePredecessor(first) is var head && head >= 0 && head == SinglePredecessor(second))
|
||||
{
|
||||
branch = head;
|
||||
}
|
||||
else
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (branch == block || _exit[branch] is not { } branchExit ||
|
||||
!TryGetTerminator(branch, out var terminator) ||
|
||||
terminator.Opcode is not ("SCbranchScc0" or "SCbranchScc1") ||
|
||||
!_blockByPc.TryGetValue(BranchTarget(terminator), out var targetBlock))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// The predecessor reached through the taken edge: the join itself (triangle) is
|
||||
// entered from the branch block, otherwise it is the arm that starts at the target.
|
||||
var takenPredecessor = targetBlock == block ? branch : targetBlock;
|
||||
if (takenPredecessor != first && takenPredecessor != second)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var taken = terminator.Opcode == "SCbranchScc1"
|
||||
? branchExit.Scc
|
||||
: Unary(ScalarOperation.LogicalNot, branchExit.Scc);
|
||||
// A constant condition only comes from the modelled initial SCC, which the hardware
|
||||
// leaves undefined; keep that join an opaque phi.
|
||||
return taken.IsUndefined || taken.IsConstant ? null : (taken, takenPredecessor);
|
||||
}
|
||||
|
||||
private bool TryGetTerminator(int block, out Gen5ShaderInstruction terminator)
|
||||
{
|
||||
var range = _graph.ControlFlow.Blocks[block];
|
||||
terminator = null!;
|
||||
foreach (var instruction in _program.Instructions)
|
||||
{
|
||||
if (instruction.Pc >= range.StartPc && instruction.Pc < range.EndPc)
|
||||
{
|
||||
terminator = instruction;
|
||||
}
|
||||
}
|
||||
|
||||
return terminator is not null;
|
||||
}
|
||||
|
||||
private static uint BranchTarget(Gen5ShaderInstruction branch) =>
|
||||
(uint)(branch.Pc + 4 + (short)(branch.Words[0] & 0xFFFF) * 4);
|
||||
|
||||
// Equal incoming values pass through; differing or still unknown ones meet in a
|
||||
// phi owned by this block and register. Any undefined input stays undefined.
|
||||
private ScalarValue MergeSlot(
|
||||
|
|
@ -260,6 +346,13 @@ public sealed partial class ScalarValueGraph
|
|||
return first;
|
||||
}
|
||||
|
||||
if (_uniformJoin is { } join)
|
||||
{
|
||||
var taken = read(visited.First(entry => entry.Block == join.TakenPredecessor).State);
|
||||
var other = read(visited.First(entry => entry.Block != join.TakenPredecessor).State);
|
||||
return _graph.Select(join.Taken, taken, other);
|
||||
}
|
||||
|
||||
if (!_phis.TryGetValue((block, slot), out var phi))
|
||||
{
|
||||
phi = _graph.Phi(block, first.Type);
|
||||
|
|
|
|||
|
|
@ -48,7 +48,14 @@ public sealed partial class ScalarValueGraph
|
|||
|
||||
public bool Equivalent(ScalarValue left, ScalarValue right) => ScalarValueEquivalence.Equivalent(Memory, left, right);
|
||||
|
||||
public ScalarValue? ResolveInvariantPhi(ScalarValue value) => ScalarValueEquivalence.ResolveInvariantPhi(Memory, value);
|
||||
// The graph is complete once built, so a phi's invariant value never changes; the
|
||||
// resource evaluator asks for it on every draw.
|
||||
private readonly System.Collections.Concurrent.ConcurrentDictionary<ScalarValue, ScalarValue?> _invariantPhis = new();
|
||||
|
||||
public ScalarValue? ResolveInvariantPhi(ScalarValue value) =>
|
||||
value.Kind != ScalarValueKind.Phi
|
||||
? value
|
||||
: _invariantPhis.GetOrAdd(value, static (phi, memory) => ScalarValueEquivalence.ResolveInvariantPhi(memory, phi), Memory);
|
||||
|
||||
public static ScalarValueGraph Build(Gen5ShaderProgram program, uint userDataBase, uint userDataCount,
|
||||
IReadOnlySet<uint>? fixedFunctionVertexLoads = null, uint waveSize = 64)
|
||||
|
|
|
|||
|
|
@ -157,6 +157,7 @@ public sealed class ShaderCompileRequest
|
|||
public IReadOnlyDictionary<int, uint> WrittenRangeSlotByMemoryIndex { get; }
|
||||
|
||||
public uint WaveSize { get; init; } = 32;
|
||||
public bool EnableExecGuardElision { get; init; } = true;
|
||||
public uint ScratchDwords { get; init; }
|
||||
public bool EnableGraphicsSubgroupOperations { get; init; } = true;
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,82 @@
|
|||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Buffers.Binary;
|
||||
using SharpEmu.HLE;
|
||||
using SharpEmu.Libs.Agc;
|
||||
using Xunit;
|
||||
|
||||
namespace SharpEmu.Libs.Tests.Agc;
|
||||
|
||||
public sealed class AgcWriteDataTests
|
||||
{
|
||||
private const ulong BaseAddress = 0x2_3000_0000;
|
||||
private const ulong CommandBufferAddress = BaseAddress + 0x100;
|
||||
private const ulong PacketAddress = BaseAddress + 0x400;
|
||||
private const ulong DataAddress = BaseAddress + 0x800;
|
||||
private const ulong StackAddress = BaseAddress + 0xC00;
|
||||
private const ulong LabelAddress = BaseAddress + 0xE00;
|
||||
|
||||
// Demon's Souls builds WRITE_DATA with a zero address into a template, copies
|
||||
// it into the ring and only then patches the destination to its label.
|
||||
[Fact]
|
||||
public void DcbWriteData_WithZeroDestination_EmitsPacketThatPatchTargets()
|
||||
{
|
||||
var memory = new FakeCpuMemory(BaseAddress, 0x1000);
|
||||
var ctx = new CpuContext(memory, Generation.Gen5);
|
||||
WriteUInt64(memory, CommandBufferAddress + 0x10, PacketAddress);
|
||||
WriteUInt64(memory, CommandBufferAddress + 0x18, PacketAddress + 0x100);
|
||||
WriteUInt32(memory, DataAddress, 0);
|
||||
WriteUInt64(memory, StackAddress + 8, 0);
|
||||
WriteUInt64(memory, StackAddress + 16, 1);
|
||||
WriteUInt32(memory, LabelAddress, 0x5A5A_5A5A);
|
||||
|
||||
ctx[CpuRegister.Rdi] = CommandBufferAddress;
|
||||
ctx[CpuRegister.Rsi] = 4;
|
||||
ctx[CpuRegister.Rdx] = 0;
|
||||
ctx[CpuRegister.Rcx] = 0;
|
||||
ctx[CpuRegister.R8] = DataAddress;
|
||||
ctx[CpuRegister.R9] = 1;
|
||||
ctx[CpuRegister.Rsp] = StackAddress;
|
||||
AgcExports.DcbWriteData(ctx);
|
||||
|
||||
Assert.Equal(PacketAddress, ctx[CpuRegister.Rax]);
|
||||
Assert.Equal(0xC0031054u, ReadUInt32(memory, PacketAddress));
|
||||
Assert.Equal(0UL, ReadUInt64(memory, PacketAddress + 8));
|
||||
|
||||
ctx[CpuRegister.Rdi] = PacketAddress;
|
||||
ctx[CpuRegister.Rsi] = LabelAddress;
|
||||
AgcExports.WriteDataPatchSetAddressOrOffset(ctx);
|
||||
|
||||
Assert.Equal(LabelAddress, ReadUInt64(memory, PacketAddress + 8));
|
||||
Assert.Equal(0x5A5A_5A5Au, ReadUInt32(memory, LabelAddress));
|
||||
}
|
||||
|
||||
private static uint ReadUInt32(ICpuMemory memory, ulong address)
|
||||
{
|
||||
Span<byte> buffer = stackalloc byte[sizeof(uint)];
|
||||
Assert.True(memory.TryRead(address, buffer));
|
||||
return BinaryPrimitives.ReadUInt32LittleEndian(buffer);
|
||||
}
|
||||
|
||||
private static ulong ReadUInt64(ICpuMemory memory, ulong address)
|
||||
{
|
||||
Span<byte> buffer = stackalloc byte[sizeof(ulong)];
|
||||
Assert.True(memory.TryRead(address, buffer));
|
||||
return BinaryPrimitives.ReadUInt64LittleEndian(buffer);
|
||||
}
|
||||
|
||||
private static void WriteUInt32(ICpuMemory memory, ulong address, uint value)
|
||||
{
|
||||
Span<byte> buffer = stackalloc byte[sizeof(uint)];
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(buffer, value);
|
||||
Assert.True(memory.TryWrite(address, buffer));
|
||||
}
|
||||
|
||||
private static void WriteUInt64(ICpuMemory memory, ulong address, ulong value)
|
||||
{
|
||||
Span<byte> buffer = stackalloc byte[sizeof(ulong)];
|
||||
BinaryPrimitives.WriteUInt64LittleEndian(buffer, value);
|
||||
Assert.True(memory.TryWrite(address, buffer));
|
||||
}
|
||||
}
|
||||
|
|
@ -23,11 +23,14 @@ public sealed class Gen5MoveRelativeSpirvTests
|
|||
// [15:8]=op(0x03), [7:0]=ssrc0. m0 is SGPR 124, inline constant 2 is 130.
|
||||
private const uint SMovM0 = 0xBE800000u | (124u << 16) | (0x03u << 8) | 130u;
|
||||
|
||||
// s_mov_b32 m0, s3: a value the translator cannot bound.
|
||||
private const uint SMovM0FromUserData = 0xBE800000u | (124u << 16) | (0x03u << 8) | 3u;
|
||||
|
||||
[Fact]
|
||||
public void MovrelsB32_ReadsTheSourceRegisterThroughADynamicIndex()
|
||||
{
|
||||
// s_mov_b32 m0, 2 ; v_movrels_b32 v5, v3 -> v5 = vgpr[3 + m0]
|
||||
var spirv = Compile([SMovM0, Vop1 | (5u << 17) | (0x43u << 9) | (256u + 3u)]);
|
||||
// s_mov_b32 m0, s3 ; v_movrels_b32 v5, v3 -> v5 = vgpr[3 + m0]
|
||||
var spirv = Compile([SMovM0FromUserData, Vop1 | (5u << 17) | (0x43u << 9) | (256u + 3u)]);
|
||||
|
||||
Assert.True(
|
||||
HasDynamicVectorRegisterAccess(spirv),
|
||||
|
|
@ -37,8 +40,8 @@ public sealed class Gen5MoveRelativeSpirvTests
|
|||
[Fact]
|
||||
public void MovreldB32_WritesTheDestinationRegisterThroughADynamicIndex()
|
||||
{
|
||||
// s_mov_b32 m0, 2 ; v_movreld_b32 v5, v3 -> vgpr[5 + m0] = v3
|
||||
var spirv = Compile([SMovM0, Vop1 | (5u << 17) | (0x42u << 9) | (256u + 3u)]);
|
||||
// s_mov_b32 m0, s3 ; v_movreld_b32 v5, v3 -> vgpr[5 + m0] = v3
|
||||
var spirv = Compile([SMovM0FromUserData, Vop1 | (5u << 17) | (0x42u << 9) | (256u + 3u)]);
|
||||
|
||||
Assert.True(
|
||||
HasDynamicVectorRegisterAccess(spirv),
|
||||
|
|
@ -48,8 +51,8 @@ public sealed class Gen5MoveRelativeSpirvTests
|
|||
[Fact]
|
||||
public void MovrelsdB32_TranslatesWithoutDroppingShader()
|
||||
{
|
||||
// s_mov_b32 m0, 2 ; v_movrelsd_b32 v5, v3 -> vgpr[5 + m0] = vgpr[3 + m0]
|
||||
var spirv = Compile([SMovM0, Vop1 | (5u << 17) | (0x44u << 9) | (256u + 3u)]);
|
||||
// s_mov_b32 m0, s3 ; v_movrelsd_b32 v5, v3 -> vgpr[5 + m0] = vgpr[3 + m0]
|
||||
var spirv = Compile([SMovM0FromUserData, Vop1 | (5u << 17) | (0x44u << 9) | (256u + 3u)]);
|
||||
|
||||
Assert.True(
|
||||
HasDynamicVectorRegisterAccess(spirv),
|
||||
|
|
@ -68,6 +71,21 @@ public sealed class Gen5MoveRelativeSpirvTests
|
|||
"V_MOVRELSD_2_B32 must index the VGPR array with a computed index");
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(0x43u)]
|
||||
[InlineData(0x42u)]
|
||||
[InlineData(0x44u)]
|
||||
public void RelativeMovesWithAKnownM0_UseConstantRegisterNumbers(uint opcode)
|
||||
{
|
||||
// s_mov_b32 m0, 2 lets the translator pick the register directly, so the VGPR
|
||||
// array is never indexed at run time.
|
||||
var spirv = Compile([SMovM0, Vop1 | (5u << 17) | (opcode << 9) | (256u + 3u)]);
|
||||
|
||||
Assert.False(
|
||||
HasDynamicVectorRegisterAccess(spirv),
|
||||
"a relative move with a known M0 must not index the VGPR array at run time");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MovrelsB32_RejectsANonVectorSource()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -234,9 +234,11 @@ public sealed class Gen5ShaderAtomicDecodeTests
|
|||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(0xD8F83412u, "DsAppend")]
|
||||
[InlineData(0xD8F43412u, "DsConsume")]
|
||||
public void DsWaveCounter_UsesM0AndReturnsOldValue(uint word, string opcode)
|
||||
[InlineData(0xD8F83412u, "DsAppend", false)]
|
||||
[InlineData(0xD8FA3412u, "DsAppend", true)]
|
||||
[InlineData(0xD8F43412u, "DsConsume", false)]
|
||||
[InlineData(0xD8F63412u, "DsConsume", true)]
|
||||
public void DsWaveCounter_UsesM0AndReturnsOldValue(uint word, string opcode, bool global)
|
||||
{
|
||||
var instruction = DecodeSingle(word, 0x07000000);
|
||||
|
||||
|
|
@ -247,7 +249,7 @@ public sealed class Gen5ShaderAtomicDecodeTests
|
|||
Assert.Equal(0x12u, control.Offset0);
|
||||
Assert.Equal(0x34u, control.Offset1);
|
||||
Assert.Equal(0x3412u, control.SingleOffsetBytes);
|
||||
Assert.False(control.Gds);
|
||||
Assert.Equal(global, control.Gds);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -288,6 +290,17 @@ public sealed class Gen5ShaderAtomicDecodeTests
|
|||
Assert.Equal(new[] { Gen5Operand.Vector(1) }, instruction.Destinations);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VFfbhI32_DecodesVop1Opcode3B()
|
||||
{
|
||||
// V_FFBH_I32 v1, v2 (Astro Bot vertex program 0x7D308CDE1E9F2E5A).
|
||||
var instruction = DecodeSingle(0x7E027702);
|
||||
|
||||
Assert.Equal("VFfbhI32", instruction.Opcode);
|
||||
Assert.Equal(new[] { Gen5Operand.Vector(2) }, instruction.Sources);
|
||||
Assert.Equal(new[] { Gen5Operand.Vector(1) }, instruction.Destinations);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VCmpxClassF32_DecodesVopcOpcode98()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -115,24 +115,35 @@ public sealed class Gen5SpirvAtomicTranslationTests
|
|||
Assert.Contains((ushort)SpirvOp.ULessThan, opcodes);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DataShareWaveCounters_InVertexStageUseWaveCountAndBroadcast()
|
||||
[Theory]
|
||||
[InlineData(false)]
|
||||
[InlineData(true)]
|
||||
public void DataShareWaveCounters_InVertexStageUseWaveCountAndBroadcast(bool global)
|
||||
{
|
||||
var gdsBit = global ? 1u << 17 : 0u;
|
||||
var opcodes = CompileVertexOpcodes(
|
||||
[
|
||||
0xD8F80014, 0x07000000,
|
||||
0xD8F40014, 0x08000000,
|
||||
0xD8F80014 | gdsBit, 0x07000000,
|
||||
0xD8F40014 | gdsBit, 0x08000000,
|
||||
]);
|
||||
|
||||
// Private graphics storage applies the counter once per active wave.
|
||||
// GDS uses shared atomics; graphics LDS uses private storage.
|
||||
// Each active lane receives the value from before the update.
|
||||
Assert.Contains((ushort)SpirvOp.GroupNonUniformBallot, opcodes);
|
||||
Assert.Contains((ushort)SpirvOp.BitCount, opcodes);
|
||||
Assert.Contains((ushort)SpirvOp.GroupNonUniformShuffle, opcodes);
|
||||
Assert.Contains((ushort)SpirvOp.IAdd, opcodes);
|
||||
Assert.Contains((ushort)SpirvOp.ISub, opcodes);
|
||||
Assert.DoesNotContain((ushort)SpirvOp.AtomicIAdd, opcodes);
|
||||
Assert.DoesNotContain((ushort)SpirvOp.AtomicISub, opcodes);
|
||||
if (global)
|
||||
{
|
||||
Assert.Contains((ushort)SpirvOp.AtomicIAdd, opcodes);
|
||||
Assert.Contains((ushort)SpirvOp.AtomicISub, opcodes);
|
||||
}
|
||||
else
|
||||
{
|
||||
Assert.Contains((ushort)SpirvOp.ISub, opcodes);
|
||||
Assert.DoesNotContain((ushort)SpirvOp.AtomicIAdd, opcodes);
|
||||
Assert.DoesNotContain((ushort)SpirvOp.AtomicISub, opcodes);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
|
|||
|
|
@ -0,0 +1,40 @@
|
|||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Runtime.ExceptionServices;
|
||||
|
||||
namespace SharpEmu.Libs.Tests;
|
||||
|
||||
internal static class AllocationMeasurement
|
||||
{
|
||||
// Bytes allocated by the least-allocating of several rounds, measured on a fresh thread.
|
||||
// Test-runner work cannot enter the sample there, and a one-off runtime allocation
|
||||
// (tiered recompilation on a busy machine) lands in one round only, while storage the
|
||||
// measured code allocates shows in every round.
|
||||
public static long SteadyState(Action? warmup, Action round, int rounds = 5)
|
||||
{
|
||||
var least = long.MaxValue;
|
||||
ExceptionDispatchInfo? failure = null;
|
||||
var thread = new Thread(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
warmup?.Invoke();
|
||||
for (var index = 0; index < rounds && least != 0; index++)
|
||||
{
|
||||
var before = GC.GetAllocatedBytesForCurrentThread();
|
||||
round();
|
||||
least = Math.Min(least, GC.GetAllocatedBytesForCurrentThread() - before);
|
||||
}
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
failure = ExceptionDispatchInfo.Capture(exception);
|
||||
}
|
||||
});
|
||||
thread.Start();
|
||||
thread.Join();
|
||||
failure?.Throw();
|
||||
return least;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using LibAtrac9;
|
||||
using Xunit;
|
||||
|
||||
namespace SharpEmu.Libs.Tests.Audio;
|
||||
|
||||
public sealed class Atrac9ConfigTests
|
||||
{
|
||||
[Fact]
|
||||
public void StandardStereoConfigIsParsed()
|
||||
{
|
||||
var config = new Atrac9Config([0xFE, 0x74, 0x0B, 0xF0]);
|
||||
|
||||
Assert.Equal(2, config.ChannelCount);
|
||||
Assert.Equal(48000, config.SampleRate);
|
||||
Assert.Equal(96, config.FrameBytes);
|
||||
Assert.Equal(384, config.SuperframeBytes);
|
||||
Assert.Equal(1024, config.SuperframeSamples);
|
||||
Assert.Equal(0, config.IndependentBlockSuperframeBytes);
|
||||
}
|
||||
|
||||
// Demon's Souls ships first-, second- and third-order ambisonic streams
|
||||
// (4, 9 and 16 channels) with a 0x30 config header.
|
||||
[Theory]
|
||||
[InlineData(new byte[] { 0x30, 0x70, 0xC1, 0x7E }, 4, 1536)]
|
||||
[InlineData(new byte[] { 0x30, 0x72, 0x01, 0x7E }, 9, 3456)]
|
||||
[InlineData(new byte[] { 0x30, 0x73, 0xC1, 0x7E }, 16, 6144)]
|
||||
public void IndependentMonoConfigIsParsed(byte[] configData, int channels, int superframeBytes)
|
||||
{
|
||||
var config = new Atrac9Config(configData);
|
||||
|
||||
Assert.Equal(channels, config.ChannelCount);
|
||||
Assert.Equal(channels, config.ChannelConfig.BlockCount);
|
||||
Assert.All(config.ChannelConfig.BlockTypes, type => Assert.Equal(BlockType.Mono, type));
|
||||
Assert.Equal(48000, config.SampleRate);
|
||||
Assert.Equal(4, config.FramesPerSuperframe);
|
||||
Assert.Equal(1024, config.SuperframeSamples);
|
||||
Assert.Equal(superframeBytes, config.SuperframeBytes);
|
||||
Assert.Equal(384, config.IndependentBlockSuperframeBytes);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(new byte[] { 0x31, 0x73, 0xC1, 0x7E })]
|
||||
[InlineData(new byte[] { 0xFE, 0x7C, 0x0B, 0xF0 })]
|
||||
[InlineData(new byte[] { 0xFE, 0x75, 0x0B, 0xF0 })]
|
||||
public void InvalidConfigIsRejected(byte[] configData)
|
||||
{
|
||||
Assert.Throws<InvalidDataException>(() => new Atrac9Config(configData));
|
||||
}
|
||||
}
|
||||
|
|
@ -74,8 +74,11 @@ public sealed class WindowsThreadContextCaptureTests
|
|||
Assert.NotEqual((nuint)0, HostMemory.Query((void*)address, out var before));
|
||||
Assert.Equal(HostMemory.PAGE_EXECUTE_READ, before.Protect);
|
||||
code.Dispose();
|
||||
// Another thread may reuse the freed range at once; only the original executable
|
||||
// allocation still being there is a leak.
|
||||
Assert.NotEqual((nuint)0, HostMemory.Query((void*)address, out var after));
|
||||
Assert.Equal(HostMemory.MEM_FREE_STATE, after.State);
|
||||
Assert.False(after.State == HostMemory.MEM_COMMIT && after.Protect == HostMemory.PAGE_EXECUTE_READ &&
|
||||
after.AllocationBase == (ulong)address);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
|
|||
|
|
@ -33,6 +33,54 @@ public sealed class GpuBufferTests : IClassFixture<HeadlessVulkanFixture>
|
|||
Assert.True(buffer.Mapped.IsEmpty);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SlabBuffers_ShareAChunkAndRecycleZeroedOnlyAfterForeignReads()
|
||||
{
|
||||
if (_vulkan is null) return;
|
||||
using var worker = new CacheWorker(_vulkan);
|
||||
var info = _vulkan.DeviceInfo;
|
||||
const BufferUsageFlags flags = GpuBuffer.AllFlags | BufferUsageFlags.ShaderDeviceAddressBit;
|
||||
using var download = new GpuBuffer(info, worker.Scheduler, GpuBufferUsage.Download, 0, GpuBuffer.AllFlags, 0x1000);
|
||||
GpuBuffer? recycled = null;
|
||||
worker.Run(() =>
|
||||
{
|
||||
worker.Scheduler.Begin(new SubmissionContext());
|
||||
var first = new GpuBuffer(info, worker.Scheduler, GpuBufferUsage.DeviceLocal, 0, flags, 0x1000, allowSlab: true);
|
||||
var allocations = info.LiveAllocations;
|
||||
using var second = new GpuBuffer(info, worker.Scheduler, GpuBufferUsage.DeviceLocal, 0, flags, 0x1000, allowSlab: true);
|
||||
Assert.Equal(allocations, info.LiveAllocations);
|
||||
first.Fill(0, 0x1000, 0xA5A5A5A5);
|
||||
worker.Scheduler.Finish();
|
||||
|
||||
// A readback may still copy from the released block; it must not be handed out yet.
|
||||
var firstAddress = first.DeviceAddress;
|
||||
using var during = new ForeignReadBuffers();
|
||||
using (info.Slabs.BeginForeignRead())
|
||||
{
|
||||
first.Dispose();
|
||||
during.Buffer = new GpuBuffer(info, worker.Scheduler, GpuBufferUsage.DeviceLocal, 0, flags, 0x1000, allowSlab: true);
|
||||
Assert.NotEqual(firstAddress, during.Buffer.DeviceAddress);
|
||||
}
|
||||
|
||||
recycled = new GpuBuffer(info, worker.Scheduler, GpuBufferUsage.DeviceLocal, 0, flags, 0x1000, allowSlab: true);
|
||||
Assert.Equal(firstAddress, recycled.DeviceAddress);
|
||||
download.CopyFrom(worker.Scheduler.Current, recycled, 0, 0, 0x1000, AccessFlags.MemoryWriteBit, AccessFlags.None, AccessFlags.MemoryReadBit, AccessFlags.HostReadBit);
|
||||
worker.Scheduler.Finish();
|
||||
download.Invalidate(0, 0x1000);
|
||||
});
|
||||
|
||||
Assert.All(download.Mapped.ToArray(), value => Assert.Equal(0, value));
|
||||
recycled!.Dispose();
|
||||
worker.Run(() => worker.Scheduler.Shutdown());
|
||||
}
|
||||
|
||||
private sealed class ForeignReadBuffers : IDisposable
|
||||
{
|
||||
public GpuBuffer? Buffer;
|
||||
|
||||
public void Dispose() => Buffer?.Dispose();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FillAndCopyFrom_ReachTheHostThroughADownloadBuffer()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -59,6 +59,42 @@ public sealed class CommandStreamQueueTests
|
|||
Assert.Equal(IdleOutcome.Completed, queue.WaitForIdle());
|
||||
}
|
||||
|
||||
private static uint[] ReleaseMemoryInterrupt(ulong label, uint value) =>
|
||||
StreamRunner.Packet(
|
||||
PacketOpcode.ReleaseMemory,
|
||||
0x28u | (5u << 8),
|
||||
(2u << 24) | (1u << 29),
|
||||
StreamRunner.Low(label), StreamRunner.High(label),
|
||||
value, 0,
|
||||
0);
|
||||
|
||||
// Release-memory packets do not submit on their own; the slice end does, so the
|
||||
// interrupt they queue always reaches the GPU once the slice is over.
|
||||
[Fact]
|
||||
public void ReleaseMemoryInterrupt_IsSubmittedAtTheEndOfTheSlice()
|
||||
{
|
||||
var (host, queue) = NewQueue();
|
||||
Enqueue(host, queue, Graphics, 1, ReleaseMemoryInterrupt(Label, 5), CreateInstanceCountPacket(2), ReleaseMemoryInterrupt(Label, 6));
|
||||
|
||||
Assert.Equal(SliceResult.Completed, queue.ProcessOne());
|
||||
|
||||
Assert.Equal(new[] { "begin 0 1", "eop Interrupt32", "eop Interrupt32", "gc", "flush" }, host.Calls);
|
||||
}
|
||||
|
||||
// A slice that blocks after a release still submits it, so a wait that depends on
|
||||
// the guest reacting to that interrupt cannot hold the interrupt back.
|
||||
[Fact]
|
||||
public void ReleaseMemoryInterrupt_IsSubmittedWhenTheSliceBlocks()
|
||||
{
|
||||
var (host, queue) = NewQueue();
|
||||
Enqueue(host, queue, Graphics, 1, ReleaseMemoryInterrupt(Label, 5), WaitEqual(Label + 8, 1));
|
||||
|
||||
Assert.Equal(SliceResult.Progressed, queue.ProcessOne());
|
||||
|
||||
var release = host.Calls.IndexOf("eop Interrupt32");
|
||||
Assert.True(release >= 0 && host.Calls.IndexOf("flush") > release, string.Join(", ", host.Calls));
|
||||
}
|
||||
|
||||
// A video-out export flip captures after the draws submitted before it, never ahead of them.
|
||||
[Fact]
|
||||
public void FlipPreparation_RunsAfterTheGraphicsSubmissionsBeforeIt()
|
||||
|
|
@ -335,4 +371,5 @@ public sealed class CommandStreamQueueTests
|
|||
Assert.Equal(SliceResult.Completed, queue.ProcessOne());
|
||||
Assert.Equal(2UL, queue.GetInterpreter(0).SubmitId);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -169,7 +169,13 @@ internal sealed class RecordingCommandStreamHost : ICommandStreamHost
|
|||
}
|
||||
|
||||
public void DispatchDirect(ulong submitId, uint groupsX, uint groupsY, uint groupsZ, uint dispatchInitiator, ulong indirectArgumentsAddress = 0) =>
|
||||
Calls.Add($"dispatch {submitId} {groupsX} {groupsY} {groupsZ} {dispatchInitiator:X}");
|
||||
Calls.Add(indirectArgumentsAddress == 0
|
||||
? $"dispatch {submitId} {groupsX} {groupsY} {groupsZ} {dispatchInitiator:X}"
|
||||
: $"dispatch {submitId} {groupsX} {groupsY} {groupsZ} {dispatchInitiator:X} @{indirectArgumentsAddress:X}");
|
||||
|
||||
public bool ResolvesIndirectDispatchOnGpu { get; set; }
|
||||
|
||||
public bool ResolvesIndirectDrawOnGpu { get; set; }
|
||||
|
||||
public void OnQueueReset(int queueId) => Calls.Add($"queue_reset {queueId}");
|
||||
|
||||
|
|
|
|||
|
|
@ -126,6 +126,61 @@ public sealed class GpuCommandInterpreterDrawTests
|
|||
Assert.Contains("arguments base is zero", new StreamRunner().RunExpectingFatal(StreamRunner.Packet(PacketOpcode.DrawIndirect, 0, 0, 0, 2)).Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IndexedIndirectDraws_LeaveArgumentsOnTheGpuWhenTheHostCan()
|
||||
{
|
||||
var runner = new StreamRunner();
|
||||
runner.Host.ResolvesIndirectDrawOnGpu = true;
|
||||
runner.Host.WriteWords(Arguments + 16, new uint[] { 50, 2, 4, 7, 1 });
|
||||
|
||||
runner.Run(
|
||||
SetBase(Arguments, dispatch: false),
|
||||
StreamRunner.Packet(PacketOpcode.IndexBase, StreamRunner.Low(Indices), StreamRunner.High(Indices)),
|
||||
StreamRunner.Packet(PacketOpcode.IndexType, 0),
|
||||
StreamRunner.Packet(PacketOpcode.IndexBufferSize, 40),
|
||||
StreamRunner.Packet(PacketOpcode.DrawIndexIndirect, 16, 0, 0, 0x22));
|
||||
|
||||
var indexed = Assert.Single(runner.Host.IndexedDraws);
|
||||
Assert.Equal((40u, Indices, Arguments + 16, DrawOffsetSource.IndirectArguments),
|
||||
(indexed.IndexCount, indexed.IndexAddress, indexed.IndirectArgumentsAddress, indexed.OffsetSource));
|
||||
Assert.DoesNotContain(runner.Host.GuestReads, address => address >= Arguments + 16 && address < Arguments + 36);
|
||||
|
||||
// A later draw that inherits the instance count reads it then.
|
||||
runner.Run(StreamRunner.Packet(PacketOpcode.DrawIndexAuto, 5, 0));
|
||||
Assert.Equal(2u, runner.Host.AutoDraws[0].InstanceCount);
|
||||
|
||||
// 8-bit indices are expanded on the CPU, so their counts are read at once.
|
||||
runner.Run(
|
||||
StreamRunner.Packet(PacketOpcode.IndexType, 2),
|
||||
StreamRunner.Packet(PacketOpcode.DrawIndexIndirect, 16, 0, 0, 0x22));
|
||||
Assert.Equal(0ul, runner.Host.IndexedDraws[1].IndirectArgumentsAddress);
|
||||
Assert.Equal(40u, runner.Host.IndexedDraws[1].IndexCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UnsizedIndexedIndirectDraws_LearnTheIndexRangeBeforeUsingTheGpu()
|
||||
{
|
||||
var runner = new StreamRunner();
|
||||
runner.Host.ResolvesIndirectDrawOnGpu = true;
|
||||
runner.Host.WriteWords(Arguments + 16, new uint[] { 3, 1, 2, 0, 0 });
|
||||
var draw = new[]
|
||||
{
|
||||
SetBase(Arguments, dispatch: false),
|
||||
StreamRunner.Packet(PacketOpcode.IndexBase, StreamRunner.Low(Indices), StreamRunner.High(Indices)),
|
||||
StreamRunner.Packet(PacketOpcode.IndexType, 0),
|
||||
StreamRunner.Packet(PacketOpcode.DrawIndexIndirect, 16, 0, 0, 0x22),
|
||||
};
|
||||
|
||||
runner.Run(draw);
|
||||
runner.Run(draw);
|
||||
|
||||
Assert.Equal(2, runner.Host.IndexedDraws.Count);
|
||||
Assert.Equal((3u, Indices + 4, 0ul), (runner.Host.IndexedDraws[0].IndexCount, runner.Host.IndexedDraws[0].IndexAddress, runner.Host.IndexedDraws[0].IndirectArgumentsAddress));
|
||||
var learned = runner.Host.IndexedDraws[1];
|
||||
Assert.Equal((Arguments + 16, Indices, true), (learned.IndirectArgumentsAddress, learned.IndexAddress, learned.UnboundedIndexBuffer));
|
||||
Assert.Equal((uint)(((Indices + 10 + 0x3FFF) & ~0x3FFFul) - Indices) / 2, learned.IndexCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IndirectMulti_LoopsOverTheStrideAndTheCountAddress()
|
||||
{
|
||||
|
|
@ -155,11 +210,36 @@ public sealed class GpuCommandInterpreterDrawTests
|
|||
StreamRunner.Packet(PacketOpcode.DispatchIndirect, 0, 0x41),
|
||||
StreamRunner.Packet(PacketOpcode.DispatchIndirect, StreamRunner.Low(Arguments), StreamRunner.High(Arguments), 0x43));
|
||||
|
||||
Assert.Equal(new[] { "dispatch 0 1 2 3 41", "dispatch 0 4 5 6 41", "dispatch 0 4 5 6 43" }, runner.Host.Calls);
|
||||
Assert.Equal(new[] { "dispatch 0 1 2 3 41", $"dispatch 0 4 5 6 41 @{Arguments:X}", $"dispatch 0 4 5 6 43 @{Arguments:X}" }, runner.Host.Calls);
|
||||
Assert.Contains("indirect dispatch arguments base is zero", new StreamRunner().RunExpectingFatal(StreamRunner.Packet(PacketOpcode.DispatchIndirect, 0, 0x41)).Message);
|
||||
Assert.Contains("set-base packet is not supported", runner.RunExpectingFatal(StreamRunner.Packet(PacketOpcode.SetBase, 2, 0, 0)).Message);
|
||||
}
|
||||
|
||||
// A host that dispatches indirectly on the GPU gets the arguments address without the
|
||||
// counts, so the interpreter never waits for a GPU readback of them. Thread-unit
|
||||
// dispatches (initiator bit 5) still read them to convert threads into groups.
|
||||
[Fact]
|
||||
public void IndirectDispatches_LeaveWorkgroupCountsOnTheGpuWhenTheHostCan()
|
||||
{
|
||||
var runner = new StreamRunner();
|
||||
runner.Host.ResolvesIndirectDispatchOnGpu = true;
|
||||
runner.Host.WriteWords(Arguments, new uint[] { 4, 5, 6 });
|
||||
|
||||
runner.Run(
|
||||
SetBase(Arguments, dispatch: true),
|
||||
StreamRunner.Packet(PacketOpcode.DispatchIndirect, 0, 0x41),
|
||||
StreamRunner.Packet(PacketOpcode.DispatchIndirect, StreamRunner.Low(Arguments), StreamRunner.High(Arguments), 0x41));
|
||||
|
||||
Assert.Equal(new[] { $"dispatch 0 0 0 0 41 @{Arguments:X}", $"dispatch 0 0 0 0 41 @{Arguments:X}" }, runner.Host.Calls);
|
||||
Assert.DoesNotContain(Arguments, runner.Host.GuestReads);
|
||||
|
||||
runner.Host.Calls.Clear();
|
||||
runner.Run(
|
||||
SetBase(Arguments, dispatch: true),
|
||||
StreamRunner.Packet(PacketOpcode.DispatchIndirect, 0, 0x61));
|
||||
Assert.Equal(new[] { $"dispatch 0 4 5 6 61 @{Arguments:X}" }, runner.Host.Calls);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IndexCountPackets_SetTheBufferSize()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -166,18 +166,21 @@ public sealed class GpuCommandInterpreterLabelTests
|
|||
Assert.Equal(0x22u, runner.Host.ReadDword(Label + 4));
|
||||
}
|
||||
|
||||
// Release-memory packets leave their completions in the recording buffer; the
|
||||
// command stream queue submits it when the slice ends or blocks (see
|
||||
// CommandStreamQueueTests), so none of these expect a per-packet flush.
|
||||
[Fact]
|
||||
public void ReleaseMemoryNative_DataSelectionOne_WritesFlushesAndFollowsTheGcrBarrierRule()
|
||||
public void ReleaseMemoryNative_DataSelectionOne_WritesAndFollowsTheGcrBarrierRule()
|
||||
{
|
||||
var runner = new StreamRunner();
|
||||
|
||||
runner.Run(ReleaseMemoryNative(0x28, 5, 0, 0, 1, 0, Label, 0x77, 0));
|
||||
Assert.Equal(new[] { "eop Write32", "flush" }, runner.Host.Calls);
|
||||
Assert.Equal(new[] { "eop Write32" }, runner.Host.Calls);
|
||||
Assert.Equal(0x77u, runner.Host.ReadDword(Label));
|
||||
|
||||
runner.Host.Calls.Clear();
|
||||
runner.Run(ReleaseMemoryNative(0x28, 5, 1u << 9, 0, 1, 2, Label, 0x78, 9));
|
||||
Assert.Equal(new[] { "barrier", "eop InterruptWriteBack32", "flush" }, runner.Host.Calls);
|
||||
Assert.Equal(new[] { "barrier", "eop InterruptWriteBack32" }, runner.Host.Calls);
|
||||
Assert.Equal(9u, runner.Host.EndOfPipeWrites[^1].ContextId);
|
||||
}
|
||||
|
||||
|
|
@ -188,16 +191,16 @@ public sealed class GpuCommandInterpreterLabelTests
|
|||
runner.Host.WriteDword(Label, 5);
|
||||
|
||||
runner.Run(ReleaseMemoryNative(0x28, 5, 0, 0, 0, 2, Label, 1, 0));
|
||||
Assert.Equal(new[] { "eop InterruptOnly", "flush" }, runner.Host.Calls);
|
||||
Assert.Equal(new[] { "eop InterruptOnly" }, runner.Host.Calls);
|
||||
|
||||
runner.Host.Calls.Clear();
|
||||
runner.Run(ReleaseMemoryNative(0x04, 5, 0, 0, 1, 4, Label, 1, 0));
|
||||
Assert.Equal(new[] { "barrier", "eop InterruptOnly", "flush" }, runner.Host.Calls);
|
||||
Assert.Equal(new[] { "barrier", "eop InterruptOnly" }, runner.Host.Calls);
|
||||
Assert.Equal(5u, runner.Host.ReadDword(Label));
|
||||
|
||||
runner.Host.Calls.Clear();
|
||||
runner.Run(ReleaseMemoryNative(0x28, 5, 0, 0, 1, 3, Label, 1, 0));
|
||||
Assert.Equal(new[] { "eop Write32", "flush" }, runner.Host.Calls);
|
||||
Assert.Equal(new[] { "eop Write32" }, runner.Host.Calls);
|
||||
Assert.Equal(1u, runner.Host.ReadDword(Label));
|
||||
}
|
||||
|
||||
|
|
@ -217,13 +220,13 @@ public sealed class GpuCommandInterpreterLabelTests
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public void ReleaseMemoryNative_GdsSelectionFlushesOnlyForSelectorOne()
|
||||
public void ReleaseMemoryNative_GdsSelectionInterruptsOnlyForSelectorOne()
|
||||
{
|
||||
var runner = new StreamRunner(queueId: 2);
|
||||
runner.Host.Gds[0] = 0x42;
|
||||
|
||||
runner.Run(ReleaseMemoryNative(0x28, 5, 0, 0, 5, 1, Label, 1u << 16, 0));
|
||||
Assert.Equal(new[] { "synchronize", "read_gds 0 1", "eop GdsWrite32", $"interrupt {GpuCommandInterpreter.ComputeQueueBase + 1} 0", "flush" }, runner.Host.Calls);
|
||||
Assert.Equal(new[] { "synchronize", "read_gds 0 1", "eop GdsWrite32", $"interrupt {GpuCommandInterpreter.ComputeQueueBase + 1} 0" }, runner.Host.Calls);
|
||||
|
||||
runner.Host.Calls.Clear();
|
||||
runner.Run(ReleaseMemoryNative(0x28, 5, 0, 0, 5, 0, Label, 1u << 16, 0));
|
||||
|
|
@ -253,7 +256,7 @@ public sealed class GpuCommandInterpreterLabelTests
|
|||
runner.Host.Calls.Clear();
|
||||
runner.Run(ReleaseMemoryWrapped(0x2F, 1u << 9, 1, 0, Label, 0x9, 0));
|
||||
Assert.Equal(0x9u, runner.Host.ReadDword(Label));
|
||||
Assert.Equal(new[] { "barrier", "eop WriteBack32", "flush" }, runner.Host.Calls);
|
||||
Assert.Equal(new[] { "barrier", "eop WriteBack32" }, runner.Host.Calls);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -263,7 +266,7 @@ public sealed class GpuCommandInterpreterLabelTests
|
|||
runner.Host.WriteDword(Label, 4);
|
||||
|
||||
runner.Run(ReleaseMemoryWrapped(0x28, 0, 1, 5, Label, 4, 1));
|
||||
Assert.Equal(new[] { "eop InterruptOnly", "flush" }, runner.Host.Calls);
|
||||
Assert.Equal(new[] { "eop InterruptOnly" }, runner.Host.Calls);
|
||||
Assert.Equal(4u, runner.Host.ReadDword(Label));
|
||||
|
||||
runner.Host.Calls.Clear();
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ public sealed class CachedImageFormatSupportTests
|
|||
ImageCreateFlags.CreateBlockTexelViewCompatibleBit;
|
||||
private const ImageUsageFlags SampledUsage = ImageUsageFlags.TransferSrcBit | ImageUsageFlags.TransferDstBit | ImageUsageFlags.SampledBit;
|
||||
|
||||
private sealed class TestImageFormatSupport(bool supportsBlockViews, bool supportsSampling = true, SampleCountFlags supportedSampleCounts = SampleCountFlags.Count1Bit) : IImageFormatSupport
|
||||
private sealed class TestImageFormatSupport(bool supportsBlockViews, bool supportsSampling = true, SampleCountFlags supportedSampleCounts = SampleCountFlags.Count1Bit, bool supportsCompressedStorage = true) : IImageFormatSupport
|
||||
{
|
||||
public int FormatQueryCount { get; private set; }
|
||||
|
||||
|
|
@ -24,7 +24,7 @@ public sealed class CachedImageFormatSupportTests
|
|||
FormatQueryCount++;
|
||||
properties = new ImageFormatProperties { SampleCounts = supportedSampleCounts };
|
||||
return (flags & ImageCreateFlags.CreateBlockTexelViewCompatibleBit) != 0
|
||||
? supportsBlockViews
|
||||
? supportsBlockViews && (supportsCompressedStorage || (usage & ImageUsageFlags.StorageBit) == 0)
|
||||
: supportsSampling && (usage & ImageUsageFlags.StorageBit) == 0;
|
||||
}
|
||||
}
|
||||
|
|
@ -53,7 +53,7 @@ public sealed class CachedImageFormatSupportTests
|
|||
|
||||
Assert.True(CachedImage.TrySelectSupportedImageConfiguration(device, ref configuration, allowCompressedImageFallback: true));
|
||||
|
||||
Assert.Equal(2, device.FormatQueryCount);
|
||||
Assert.Equal(3, device.FormatQueryCount);
|
||||
Assert.Equal(OriginalFlags & ~ImageCreateFlags.CreateBlockTexelViewCompatibleBit, configuration.Flags);
|
||||
Assert.Equal(SampledUsage, configuration.Usage);
|
||||
Assert.Equal(format, configuration.Format);
|
||||
|
|
@ -83,7 +83,7 @@ public sealed class CachedImageFormatSupportTests
|
|||
var configuration = CreateCompressedImageConfiguration(Format.BC1RgbaUnormBlock);
|
||||
|
||||
Assert.False(CachedImage.TrySelectSupportedImageConfiguration(device, ref configuration, allowCompressedImageFallback: false));
|
||||
Assert.Equal(1, device.FormatQueryCount);
|
||||
Assert.Equal(2, device.FormatQueryCount);
|
||||
Assert.Equal(OriginalFlags, configuration.Flags);
|
||||
Assert.Equal(SampledUsage | ImageUsageFlags.StorageBit, configuration.Usage);
|
||||
}
|
||||
|
|
@ -101,6 +101,20 @@ public sealed class CachedImageFormatSupportTests
|
|||
Assert.Equal(SampledUsage | ImageUsageFlags.StorageBit, configuration.Usage);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(false)]
|
||||
[InlineData(true)]
|
||||
public void CompressedImageDropsOnlyStorageWhenDriverRefusesIt(bool allowCompressedImageFallback)
|
||||
{
|
||||
var device = new TestImageFormatSupport(supportsBlockViews: true, supportsCompressedStorage: false);
|
||||
var configuration = CreateCompressedImageConfiguration(Format.BC5UnormBlock);
|
||||
|
||||
Assert.True(CachedImage.TrySelectSupportedImageConfiguration(device, ref configuration, allowCompressedImageFallback));
|
||||
Assert.Equal(2, device.FormatQueryCount);
|
||||
Assert.Equal(OriginalFlags, configuration.Flags);
|
||||
Assert.Equal(SampledUsage, configuration.Usage);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UncompressedImageDoesNotUseCompressedFallback()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -264,21 +264,30 @@ public sealed class GpuTilerTests : IClassFixture<HeadlessVulkanFixture>
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public void Detile_ReleasesTheScratchAfterTheTick()
|
||||
// Scratches go back to a pool when their tick completes instead of being freed, so
|
||||
// the next tick's scratches of the same sizes reuse them without new allocations.
|
||||
public void Detile_ReusesTheScratchAfterTheTick()
|
||||
{
|
||||
if (!GatePrerequisites.Ready(_vulkan)) return;
|
||||
using var harness = new ImageTestHarness(_vulkan);
|
||||
var tilerCase = TilerCases.Create(TileBlockKind.Standard4KB, 4, TilerCases.Shape.Blocks, 77)!;
|
||||
var tiled = harness.Upload(tilerCase.Tiled);
|
||||
var baseline = harness.Device.LiveAllocations;
|
||||
harness.Run(() =>
|
||||
for (var round = 0; round < 3; round++)
|
||||
{
|
||||
harness.Tiler.Detile(tiled.Handle, 0, tilerCase.Transfer.TiledSize, tilerCase.Transfer.LinearSize, new[] { tilerCase.Transfer });
|
||||
harness.Tiler.GetScratchBuffer(64);
|
||||
harness.Run(() =>
|
||||
{
|
||||
var linear = harness.Tiler.Detile(tiled.Handle, 0, tilerCase.Transfer.TiledSize, tilerCase.Transfer.LinearSize, new[] { tilerCase.Transfer });
|
||||
var scratch = harness.Tiler.GetScratchBuffer(64);
|
||||
Assert.Equal(tilerCase.Transfer.LinearSize, linear.Size);
|
||||
Assert.Equal(64UL, scratch.Size);
|
||||
Assert.NotEqual(linear.Buffer.Handle, scratch.Buffer.Handle);
|
||||
Assert.Equal(baseline + 2, harness.Device.LiveAllocations);
|
||||
harness.Scheduler.Finish();
|
||||
});
|
||||
Assert.Equal(baseline + 2, harness.Device.LiveAllocations);
|
||||
harness.Scheduler.Finish();
|
||||
});
|
||||
Assert.Equal(baseline, harness.Device.LiveAllocations);
|
||||
}
|
||||
|
||||
harness.AssertNoValidationMessages();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -35,11 +35,14 @@ public sealed class ImageQueryResultsTests
|
|||
[InlineData(16)]
|
||||
public void InlineQueriesDoNotAllocate(int count)
|
||||
{
|
||||
for (var warmup = 0; warmup < 100; warmup++) Sum(count);
|
||||
var before = GC.GetAllocatedBytesForCurrentThread();
|
||||
var sum = 0UL;
|
||||
for (var iteration = 0; iteration < 1000; iteration++) sum += Sum(count);
|
||||
var allocated = GC.GetAllocatedBytesForCurrentThread() - before;
|
||||
var allocated = AllocationMeasurement.SteadyState(
|
||||
() => { for (var warmup = 0; warmup < 100; warmup++) Sum(count); },
|
||||
() =>
|
||||
{
|
||||
sum = 0;
|
||||
for (var iteration = 0; iteration < 1000; iteration++) sum += Sum(count);
|
||||
});
|
||||
Assert.Equal(0, allocated);
|
||||
Assert.Equal((ulong)(count * (count - 1) / 2) * 1000, sum);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -137,7 +137,7 @@ public sealed class MetalRenderHostTests : IDisposable
|
|||
private ShaderProgram _pixelProgram;
|
||||
private ShaderProgram _computeProgram;
|
||||
|
||||
public GraphicsPrograms GetGraphicsPrograms(VertexStageRegisters vertex, PixelStageRegisters pixel, ShaderInterfaceRegisters shaderInterface, ContextRegisters context, ReadOnlySpan<ColorComponentMap> targetExportMapping, bool pixelActive)
|
||||
public GraphicsPrograms GetGraphicsPrograms(VertexStageRegisters vertex, PixelStageRegisters pixel, ShaderInterfaceRegisters shaderInterface, ContextRegisters context, ReadOnlySpan<ColorComponentMap> targetExportMapping, bool pixelActive, bool depthBound)
|
||||
{
|
||||
if (!_vertexProgram.IsValid)
|
||||
{
|
||||
|
|
@ -413,7 +413,7 @@ public sealed class MetalRenderHostTests : IDisposable
|
|||
{
|
||||
var executor = Executor(out var provider);
|
||||
var input = provider.GetGraphicsPrograms(_banks.Shader.Vertex, _banks.Shader.Pixel,
|
||||
_banks.Context.ShaderInterface, _banks.Context, [], true).VertexInput;
|
||||
_banks.Context.ShaderInterface, _banks.Context, [], true, false).VertexInput;
|
||||
var original = Floats(-1f, -1f, 3f, -1f, -1f, 3f);
|
||||
Assert.True(_memory.TryWrite(VertexBase, original));
|
||||
if (changedLayout) input.Attributes[0] = input.Attributes[0] with { OffsetBytes = 4 };
|
||||
|
|
@ -440,7 +440,7 @@ public sealed class MetalRenderHostTests : IDisposable
|
|||
var executor = Executor(out var provider);
|
||||
provider.ReuseGraphicsPipeline = true;
|
||||
var first = provider.GetGraphicsPrograms(_banks.Shader.Vertex, _banks.Shader.Pixel,
|
||||
_banks.Context.ShaderInterface, _banks.Context, [], true).VertexInput;
|
||||
_banks.Context.ShaderInterface, _banks.Context, [], true, false).VertexInput;
|
||||
Assert.True(_memory.TryWrite(VertexBase, new byte[24]));
|
||||
executor.DrawAuto(1, _banks, new DrawAutoArguments(0, 0, 3, 1, 0, 0, DrawOffsetSource.Packet));
|
||||
var address = VertexBase + 64;
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue