diff --git a/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.Exceptions.cs b/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.Exceptions.cs
index 17ebcfd..c590886 100644
--- a/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.Exceptions.cs
+++ b/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.Exceptions.cs
@@ -365,7 +365,13 @@ public sealed partial class DirectExecutionBackend
Console.Error.WriteLine("[LOADER][ERROR] - Guest code accessed unmapped memory");
Console.Error.WriteLine("[LOADER][ERROR] - Need to implement HLE for this NID");
byte[] code = new byte[16];
- if (TryReadHostBytes(rip, code))
+ DumpHostRegion("RIP", rip);
+ // Guest code can be mapped execute-only, which makes every
+ // host-side read of it fail. The guest memory view still
+ // resolves those pages, so prefer it here: otherwise the
+ // whole call-site report below is silently skipped on
+ // exactly the faults that need it.
+ if (TryReadGuestOrHostBytes(rip, code))
{
Console.Error.WriteLine("[LOADER][INFO] Code at RIP: " + BitConverter.ToString(code).Replace("-", " "));
if (code[0] == 100)
@@ -383,12 +389,12 @@ public sealed partial class DirectExecutionBackend
Console.Error.WriteLine($"[LOADER][INFO] RSP: 0x{rsp:X16} (mod 16 = {rsp % 16})");
}
byte[] before = new byte[16];
- if (rip > 16 && TryReadHostBytes(rip - 16, before))
+ if (rip > 16 && TryReadGuestOrHostBytes(rip - 16, before))
{
Console.Error.WriteLine("[LOADER][INFO] Code before RIP: " + BitConverter.ToString(before).Replace("-", " "));
}
byte[] window = new byte[64];
- if (rip > 32 && TryReadHostBytes(rip - 32, window))
+ if (rip > 32 && TryReadGuestOrHostBytes(rip - 32, window))
{
Console.Error.WriteLine("[LOADER][INFO] Code window [RIP-0x20..]: " + BitConverter.ToString(window).Replace("-", " "));
}
@@ -405,7 +411,7 @@ public sealed partial class DirectExecutionBackend
continue;
}
byte[] callSiteWindow = new byte[48];
- if (TryReadHostBytes(candidate - 24, callSiteWindow))
+ if (TryReadGuestOrHostBytes(candidate - 24, callSiteWindow))
{
Console.Error.WriteLine(
$"[LOADER][INFO] Stack guest-code candidate [rsp+0x{stackIndex * 8:X2}]=0x{candidate:X16}, bytes [-0x18..]: " +
@@ -417,6 +423,11 @@ public sealed partial class DirectExecutionBackend
{
Console.Error.WriteLine("[LOADER][ERROR] Could not read code at RIP");
}
+ // The faulting instruction is the first thing an access
+ // violation report has to answer, so decode it through guest
+ // memory as well: that path still works when the host code
+ // page refuses a plain read.
+ DumpGuestInstructionStream("fault", rip, 4);
DumpRecentImportTrace();
DumpGuestDisasmDiagnostics(rip, rbp, rsp);
DumpGuestRegisterWindowDiagnostics(
@@ -1463,6 +1474,44 @@ public sealed partial class DirectExecutionBackend
return true;
}
+ ///
+ /// Reads guest bytes for a crash report, preferring the guest memory view
+ /// over a raw host read. Guest code pages are commonly committed
+ /// PAGE_EXECUTE, which no host read can satisfy.
+ ///
+ private bool TryReadGuestOrHostBytes(ulong address, byte[] buffer)
+ {
+ if (_cpuContext is { } context)
+ {
+ try
+ {
+ if (context.Memory.TryRead(address, buffer))
+ {
+ return true;
+ }
+ }
+ catch
+ {
+ // Fall through to the host reader.
+ }
+ }
+
+ return TryReadHostBytes(address, buffer);
+ }
+
+ private unsafe static void DumpHostRegion(string name, ulong address)
+ {
+ if (VirtualQuery((void*)address, out var mbi, (nuint)sizeof(MEMORY_BASIC_INFORMATION64)) == 0)
+ {
+ Console.Error.WriteLine($"[LOADER][INFO] {name} region: address=0x{address:X16}");
+ return;
+ }
+
+ Console.Error.WriteLine(
+ $"[LOADER][INFO] {name} region: base=0x{mbi.BaseAddress:X16} size=0x{mbi.RegionSize:X16} " +
+ $"state=0x{mbi.State:X08} protect=0x{mbi.Protect:X08} type=0x{mbi.Type:X08}");
+ }
+
private unsafe static bool IsReadableHostRange(ulong address, int byteCount, bool allowExecuteOnly = false)
{
if (byteCount < 0 || !IsCanonicalUserAddress(address))
diff --git a/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.cs b/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.cs
index b707afc..3af1142 100644
--- a/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.cs
+++ b/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.cs
@@ -151,6 +151,8 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
private static readonly ulong GuestThreadTlsBaseAddress = OperatingSystem.IsWindows() ? 0x7FFE_0000_0000UL : 0x6FFE_0000_0000UL;
private const ulong GuestThreadStackSize = 0x0020_0000UL;
+ private const ulong GuestThreadStackHeadroom = 0x0010_0000UL;
+ private const ulong GuestThreadStackSizeLimit = 0x0400_0000UL;
private const ulong GuestThreadTlsSize = 0x0001_0000UL;
@@ -285,7 +287,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
private readonly List _importHandlerTrampolines = new List();
- private const int GuestContextTransferFrameQwords = 20;
+ private const int GuestContextTransferFrameQwords = 22;
private readonly object _guestContextTransferStubGate = new();
@@ -1833,7 +1835,11 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
private static bool IsHlePreferredNid(string nid)
{
- return string.Equals(nid, "QrZZdJ8XsX0", StringComparison.Ordinal) ||
+ // The shipped libc memset body contains a PS5-specific int 45h path
+ // which is not a Windows host instruction. Use the bounds-aware HLE
+ // implementation instead of redirecting this import to LLE.
+ return string.Equals(nid, "8zTFvBIAIN8", StringComparison.Ordinal) ||
+ string.Equals(nid, "QrZZdJ8XsX0", StringComparison.Ordinal) ||
string.Equals(nid, "Q3VBxCXhUHs", StringComparison.Ordinal);
}
@@ -2140,6 +2146,13 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
error = $"guest context transfer slot is not writable at 0x{target.Rsp - sizeof(ulong):X16}";
return false;
}
+ ulong stackBottom = 0;
+ ulong stackTop = 0;
+ if (OperatingSystem.IsWindows() && !TryGetGuestStackBounds(target.Rsp, out stackBottom, out stackTop))
+ {
+ error = $"guest context transfer target rsp=0x{target.Rsp:X16} has no committed host stack mapping";
+ return false;
+ }
transferStub = GetOrCreateGuestContextTransferStub();
if (transferStub == 0)
@@ -2176,6 +2189,11 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
frame[17] = target.Mxcsr == 0 ? 0x1F80u : target.Mxcsr;
frame[18] = target.FpuControlWord == 0 ? 0x037Fu : target.FpuControlWord;
frame[19] = target.RestoreFullFpuState ? 1u : 0u;
+ // The TEB stores NT_TIB.StackBase (the high address) at +0x08 and
+ // NT_TIB.StackLimit (the low address) at +0x10. Keep the frame in that
+ // same order so the transfer stub can store the slots straight through.
+ frame[20] = stackTop;
+ frame[21] = stackBottom;
return true;
}
@@ -2243,6 +2261,12 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
}
Emit(0x49); Emit(0x89); Emit(0xC3); // mov r11, rax
+ // Fiber switches replace the guest stack without passing through a
+ // guest-entry stub, so keep the Windows TEB bounds in sync here.
+ EmitLoadFromR11Disp32(10, 160); // r10 = target stack top
+ EmitStackBound(code, ref offset, 10, 8, store: true);
+ EmitLoadFromR11Disp32(10, 168); // r10 = target stack bottom
+ EmitStackBound(code, ref offset, 10, 16, store: true);
// A new >=3.50 fiber receives the SDK-defined MXCSR verbatim. A
// resumed fiber follows _sceFiberLongJmp: preserve status bits 0-5
// while restoring the saved control bits.
@@ -5342,7 +5366,21 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
error = "creator context memory is not backed by IVirtualMemory";
return false;
}
- if (!TryMapGuestThreadRegion(virtualMemory, GuestThreadStackBaseAddress, GuestThreadStackSize, ProgramHeaderFlags.Read | ProgramHeaderFlags.Write, out var stackBase, out error))
+ var requestedStackSize = request.StackSize;
+ if (requestedStackSize > GuestThreadStackSizeLimit)
+ {
+ requestedStackSize = GuestThreadStackSizeLimit;
+ }
+
+ var stackSize = requestedStackSize == 0
+ ? GuestThreadStackSize
+ : AlignUp(requestedStackSize + GuestThreadStackHeadroom, 0x1000UL);
+ if (stackSize < GuestThreadStackSize)
+ {
+ stackSize = GuestThreadStackSize;
+ }
+
+ if (!TryMapGuestThreadRegion(virtualMemory, GuestThreadStackBaseAddress, stackSize, ProgramHeaderFlags.Read | ProgramHeaderFlags.Write, out var stackBase, out error))
{
return false;
}
@@ -5359,7 +5397,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
FsBase = tlsBase,
GsBase = tlsBase,
};
- context[CpuRegister.Rsp] = stackBase + GuestThreadStackSize - sizeof(ulong);
+ context[CpuRegister.Rsp] = stackBase + stackSize - sizeof(ulong);
context[CpuRegister.Rdi] = request.Argument;
context[CpuRegister.Rsi] = 0;
context[CpuRegister.Rdx] = 0;
@@ -5382,7 +5420,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
AffinityMask = request.AffinityMask,
Context = context,
StackBase = stackBase,
- StackSize = GuestThreadStackSize,
+ StackSize = stackSize,
State = GuestThreadRunState.Ready,
};
error = null;
@@ -6774,6 +6812,15 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
MarkExecutionProgress();
continue;
}
+ if (AreAllLiveGuestThreadsExpectedlyBlocked())
+ {
+ Console.Error.WriteLine(
+ $"[LOADER][WARN] No import progress for {stallWatchdogSeconds}s, but every live guest thread is parked in an expected blocking primitive; continuing.");
+ LogStallWatchdogSnapshot();
+ Console.Error.Flush();
+ MarkExecutionProgress();
+ continue;
+ }
if (Interlocked.Exchange(ref _stallWatchdogTriggered, 1) != 0)
{
continue;
@@ -6841,6 +6888,41 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
return false;
}
+ private bool AreAllLiveGuestThreadsExpectedlyBlocked()
+ {
+ using (LockGate("AreAllLiveGuestThreadsExpectedlyBlocked"))
+ {
+ var sawBlockedThread = false;
+ foreach (var thread in _guestThreads.Values)
+ {
+ if (thread.State is GuestThreadRunState.Exited or GuestThreadRunState.Faulted)
+ {
+ continue;
+ }
+
+ if (thread.State != GuestThreadRunState.Blocked)
+ {
+ return false;
+ }
+
+ if (Volatile.Read(ref thread.LastImportNid) is not (
+ "Op8TBGY5KHg" or // pthread_cond_wait
+ "27bAgiJmOh0" or // pthread_cond_timedwait
+ "WKAXJ4XBPQ4" or // scePthreadCondWait
+ "BmMjYxmew1w" or // scePthreadCondTimedwait
+ "fzyMKs9kim0" or // sceKernelWaitEqueue
+ "Zxa0VhQVTsk")) // sceKernelWaitSema
+ {
+ return false;
+ }
+
+ sawBlockedThread = true;
+ }
+
+ return sawBlockedThread;
+ }
+ }
+
private void StopStallWatchdog()
{
_stallWatchdogStop = true;
diff --git a/src/SharpEmu.Core/Loader/GuestRedZonePatcher.cs b/src/SharpEmu.Core/Loader/GuestRedZonePatcher.cs
index e48c32e..d541b3c 100644
--- a/src/SharpEmu.Core/Loader/GuestRedZonePatcher.cs
+++ b/src/SharpEmu.Core/Loader/GuestRedZonePatcher.cs
@@ -20,6 +20,15 @@ internal static class GuestRedZonePatcher
private const ulong AllocationAlignment = 0x10000;
private const ulong MaximumRelativeJumpDistance = 0x7FFF_FFFF;
+ internal enum SpanRefusal
+ {
+ None,
+ ControlFlow,
+ BranchTargetAfter,
+ StackAfter,
+ TooShort,
+ }
+
public static PatchResult Patch(
IVirtualMemory memory,
PhysicalVirtualMemory physicalMemory,
@@ -58,7 +67,8 @@ internal static class GuestRedZonePatcher
if (sites.Count == 0)
{
Console.Error.WriteLine(
- $"[LOADER] {hostName} red-zone scan: functions={scan.Functions} red_zone={scan.RedZoneFunctions} sites=0.");
+ $"[LOADER] {hostName} red-zone scan: functions={scan.Functions} red_zone={scan.RedZoneFunctions} " +
+ $"sites=0 unrelocatable={scan.UnrelocatableSites}.");
return scan;
}
@@ -83,14 +93,28 @@ internal static class GuestRedZonePatcher
var trampolineEnd = trampolineBase + requiredBytes;
var patched = 0;
var failed = 0;
+ var previousEnd = 0UL;
foreach (var site in sites)
{
+ // Sites are produced in increasing address order. Patching two that
+ // overlap would write one jump over another and send the trampoline
+ // into whatever follows, so refuse rather than corrupt guest code.
+ if (site.Address < previousEnd)
+ {
+ Console.Error.WriteLine(
+ $"[LOADER][WARN] {hostName} red-zone site 0x{site.Address:X16} overlaps the previous one " +
+ $"ending at 0x{previousEnd:X16}; left unpatched.");
+ failed++;
+ continue;
+ }
+
if (!TryPatchSite(memory, site, ref trampolineCursor, trampolineEnd, splitVectorStores, rewriteSha))
{
failed++;
continue;
}
+ previousEnd = site.Address + (ulong)site.ByteLength;
patched++;
}
@@ -99,11 +123,19 @@ internal static class GuestRedZonePatcher
CandidateSites = sites.Count,
PatchedSites = patched,
FailedSites = failed,
+ UnrelocatableSites = scan.UnrelocatableSites,
+ ControlFlowRefusals = scan.ControlFlowRefusals,
+ BranchTargetRefusals = scan.BranchTargetRefusals,
+ StackRefusals = scan.StackRefusals,
+ TooShortRefusals = scan.TooShortRefusals,
TrampolineBytes = trampolineCursor - trampolineBase,
};
Console.Error.WriteLine(
$"[LOADER] {hostName} red-zone patch: functions={result.Functions} red_zone={result.RedZoneFunctions} " +
$"sites={result.PatchedSites}/{result.CandidateSites} failed={result.FailedSites} " +
+ $"unrelocatable={result.UnrelocatableSites} " +
+ $"(control_flow={result.ControlFlowRefusals} branch_target_after={result.BranchTargetRefusals} " +
+ $"stack_after={result.StackRefusals} too_short={result.TooShortRefusals}) " +
$"rosetta_vector_stores={result.VectorStoreCount} sha_rewrites={result.ShaInstructionCount} " +
$"trampolines=0x{trampolineBase:X16}+0x{result.TrampolineBytes:X}.");
return result;
@@ -124,6 +156,8 @@ internal static class GuestRedZonePatcher
var redZoneFunctionCount = 0;
var instructionCount = 0;
var vectorStoreCount = 0;
+ var unrelocatableSites = 0;
+ var refusalCounts = new int[5];
var shaInstructionCount = 0;
foreach (var header in programHeaders)
@@ -174,6 +208,7 @@ internal static class GuestRedZonePatcher
functionCount++;
instructionCount += decoded.Count;
var usesRedZone = protectRedZone && decoded.Any(static entry => UsesRedZone(entry.Instruction));
+
if (!usesRedZone && !splitVectorStores && !rewriteSha)
{
continue;
@@ -184,19 +219,43 @@ internal static class GuestRedZonePatcher
redZoneFunctionCount++;
}
var branchTargets = CollectBranchTargets(decoded);
+
+ var lastSiteEnd = 0UL;
+
for (var instructionIndex = 0; instructionIndex < decoded.Count; instructionIndex++)
{
var instruction = decoded[instructionIndex].Instruction;
var isShaSite = rewriteSha && ShaInstructionRewrite.CanRewrite(instruction);
- if ((!isShaSite &&
- ((!usesRedZone && !(splitVectorStores && RosettaVectorStorePatch.RequiresStoreSplit(instruction))) ||
- !IsFaultableGuestMemoryInstruction(instruction))) ||
- !TryBuildPatchSpan(decoded, instructionIndex, branchTargets, out var span))
+ if (!isShaSite &&
+ ((!usesRedZone && !(splitVectorStores && RosettaVectorStorePatch.RequiresStoreSplit(instruction))) ||
+ !IsFaultableGuestMemoryInstruction(instruction)))
{
continue;
}
+ // A backward span reaches instructions that an earlier site
+ // may already have replaced with its own jump. Two jumps
+ // written over each other corrupt control flow, so a span is
+ // only usable when it starts at or after the end of the last
+ // one. Forward spans cannot overlap by construction; the
+ // check costs nothing and documents the invariant.
+ var backward = false;
+ if (!TryBuildPatchSpan(decoded, instructionIndex, branchTargets, out var span, out var refusal))
+ {
+ backward = TryBuildEnclosingPatchSpan(decoded, instructionIndex, branchTargets, out span);
+ }
+
+ if ((!backward && refusal != SpanRefusal.None) || span.Address < lastSiteEnd)
+ {
+ // Refused spans never enter the site list, so they are
+ // invisible in FailedSites. Count them separately.
+ unrelocatableSites++;
+ refusalCounts[(int)refusal]++;
+ continue;
+ }
+
sites.Add(span);
+ lastSiteEnd = span.Address + (ulong)span.ByteLength;
if (splitVectorStores)
{
vectorStoreCount += span.Instructions.Count(static instruction => RosettaVectorStorePatch.RequiresStoreSplit(instruction));
@@ -205,7 +264,13 @@ internal static class GuestRedZonePatcher
{
shaInstructionCount += span.Instructions.Count(static instruction => ShaInstructionRewrite.CanRewrite(instruction));
}
- instructionIndex += span.Instructions.Count - 1;
+
+ // A backward span already ends at the current instruction, so
+ // only a forward one consumes the instructions that follow.
+ if (!backward)
+ {
+ instructionIndex += span.Instructions.Count - 1;
+ }
}
}
}
@@ -217,6 +282,11 @@ internal static class GuestRedZonePatcher
Instructions = instructionCount,
CandidateSites = sites.Count,
VectorStoreCount = vectorStoreCount,
+ UnrelocatableSites = unrelocatableSites,
+ ControlFlowRefusals = refusalCounts[(int)SpanRefusal.ControlFlow],
+ BranchTargetRefusals = refusalCounts[(int)SpanRefusal.BranchTargetAfter],
+ StackRefusals = refusalCounts[(int)SpanRefusal.StackAfter],
+ TooShortRefusals = refusalCounts[(int)SpanRefusal.TooShort],
ShaInstructionCount = shaInstructionCount,
};
return sites;
@@ -274,20 +344,199 @@ internal static class GuestRedZonePatcher
return targets;
}
+ ///
+ /// Runs the enclosing-span search over a raw instruction range, so the
+ /// behaviour can be pinned on the byte sequences that occur in real guest
+ /// code rather than on a mocked decode.
+ ///
+ internal static bool TryBuildEnclosingSpan(
+ byte[] code,
+ ulong baseAddress,
+ ulong faultAddress,
+ out ulong spanAddress,
+ out int spanLength,
+ out int coreStart,
+ out int coreCount)
+ {
+ spanAddress = 0;
+ spanLength = 0;
+ coreStart = 0;
+ coreCount = 0;
+
+ var decoded = DecodeFunction(code, baseAddress, baseAddress, baseAddress + (ulong)code.Length);
+ var faultIndex = -1;
+ for (var index = 0; index < decoded.Count; index++)
+ {
+ if (decoded[index].Instruction.IP == faultAddress)
+ {
+ faultIndex = index;
+ break;
+ }
+ }
+
+ if (faultIndex < 0)
+ {
+ return false;
+ }
+
+ if (!TryBuildEnclosingPatchSpan(decoded, faultIndex, CollectBranchTargets(decoded), out var site))
+ {
+ return false;
+ }
+
+ spanAddress = site.Address;
+ spanLength = site.ByteLength;
+ coreStart = site.CoreStart;
+ coreCount = site.CoreCount;
+ return true;
+ }
+
+ ///
+ /// Finds the faultable instructions of a span. The RSP shift has to bracket
+ /// all of them and nothing that reads or writes through RSP, because the
+ /// shift would move such an access by 128 bytes.
+ ///
+ private static bool TryComputeShiftedCore(IList instructions, out int coreStart, out int coreCount)
+ {
+ coreStart = -1;
+ var coreEnd = -1;
+ for (var index = 0; index < instructions.Count; index++)
+ {
+ if (!IsFaultableGuestMemoryInstruction(instructions[index]))
+ {
+ continue;
+ }
+
+ if (coreStart < 0)
+ {
+ coreStart = index;
+ }
+
+ coreEnd = index;
+ }
+
+ if (coreStart < 0)
+ {
+ coreCount = 0;
+ return false;
+ }
+
+ for (var index = coreStart; index <= coreEnd; index++)
+ {
+ if (TouchesStackPointer(instructions[index]))
+ {
+ coreCount = 0;
+ return false;
+ }
+ }
+
+ coreCount = coreEnd - coreStart + 1;
+ return true;
+ }
+
+ ///
+ /// Builds a span that ends at the faulting instruction, taking the
+ /// bytes it needs from the instructions before it. The instruction after the
+ /// site is never overwritten, so branches into it keep working - which is
+ /// what the forward search cannot offer when that instruction is a branch
+ /// target or touches RSP.
+ ///
+ private static bool TryBuildEnclosingPatchSpan(
+ IReadOnlyList decoded,
+ int faultIndex,
+ IReadOnlySet branchTargets,
+ out PatchSite site)
+ {
+ site = default;
+ var faulting = decoded[faultIndex].Instruction;
+ if (faulting.FlowControl != FlowControl.Next || TouchesStackPointer(faulting))
+ {
+ return false;
+ }
+
+ // Starting before the faulting instruction only works when nothing
+ // branches directly to it: such a branch would land inside the new jump.
+ if (branchTargets.Contains(faulting.IP))
+ {
+ return false;
+ }
+
+ var byteLength = faulting.Length;
+ for (var startIndex = faultIndex - 1; startIndex >= 0; startIndex--)
+ {
+ var candidate = decoded[startIndex].Instruction;
+ if (candidate.FlowControl != FlowControl.Next)
+ {
+ return false;
+ }
+
+ byteLength += candidate.Length;
+
+ // A branch target on the first instruction is fine: it lands on the
+ // jump that replaces the span. One in the middle is not, so stop as
+ // soon as the span is long enough rather than growing past it.
+ if (byteLength < MinimumJumpBytes)
+ {
+ if (branchTargets.Contains(candidate.IP))
+ {
+ return false;
+ }
+
+ continue;
+ }
+
+ var instructions = new List(faultIndex - startIndex + 1);
+ for (var index = startIndex; index <= faultIndex; index++)
+ {
+ instructions.Add(decoded[index].Instruction);
+ }
+
+ if (!TryComputeShiftedCore(instructions, out var coreStart, out var coreCount))
+ {
+ return false;
+ }
+
+ site = new PatchSite(candidate.IP, byteLength, instructions, coreStart, coreCount);
+ return true;
+ }
+
+ return false;
+ }
+
private static bool TryBuildPatchSpan(
IReadOnlyList decoded,
int startIndex,
IReadOnlySet branchTargets,
- out PatchSite site)
+ out PatchSite site) =>
+ TryBuildPatchSpan(decoded, startIndex, branchTargets, out site, out _);
+
+ private static bool TryBuildPatchSpan(
+ IReadOnlyList decoded,
+ int startIndex,
+ IReadOnlySet branchTargets,
+ out PatchSite site,
+ out SpanRefusal refusal)
{
+ refusal = SpanRefusal.None;
var instructions = new List(3);
var byteLength = 0;
for (var index = startIndex; index < decoded.Count && byteLength < MinimumJumpBytes; index++)
{
var instruction = decoded[index].Instruction;
- if (instruction.FlowControl != FlowControl.Next ||
- (index != startIndex && branchTargets.Contains(instruction.IP)) ||
- TouchesStackPointer(instruction))
+ if (instruction.FlowControl != FlowControl.Next)
+ {
+ refusal = SpanRefusal.ControlFlow;
+ }
+ else if (index != startIndex && branchTargets.Contains(instruction.IP))
+ {
+ refusal = SpanRefusal.BranchTargetAfter;
+ }
+ else if (TouchesStackPointer(instruction))
+ {
+ refusal = SpanRefusal.StackAfter;
+ }
+
+ if (refusal != SpanRefusal.None)
{
site = default;
return false;
@@ -299,11 +548,19 @@ internal static class GuestRedZonePatcher
if (byteLength < MinimumJumpBytes)
{
+ refusal = SpanRefusal.TooShort;
site = default;
return false;
}
- site = new PatchSite(decoded[startIndex].Instruction.IP, byteLength, instructions);
+ if (!TryComputeShiftedCore(instructions, out var coreStart, out var coreCount))
+ {
+ refusal = SpanRefusal.TooShort;
+ site = default;
+ return false;
+ }
+
+ site = new PatchSite(decoded[startIndex].Instruction.IP, byteLength, instructions, coreStart, coreCount);
return true;
}
@@ -385,6 +642,30 @@ internal static class GuestRedZonePatcher
return false;
}
+ private static bool TryEncodeSegment(
+ IList instructions,
+ int start,
+ int count,
+ ulong address,
+ out byte[]? encoded)
+ {
+ encoded = null;
+ if (count <= 0)
+ {
+ return true;
+ }
+
+ var writer = new ListCodeWriter();
+ var segment = instructions.Skip(start).Take(count).ToList();
+ if (!BlockEncoder.TryEncode(64, new InstructionBlock(writer, segment, address), out _, out _, BlockEncoderOptions.None))
+ {
+ return false;
+ }
+
+ encoded = writer.ToArray();
+ return true;
+ }
+
private static bool TryPatchSite(
IVirtualMemory memory,
PatchSite site,
@@ -393,9 +674,25 @@ internal static class GuestRedZonePatcher
bool splitVectorStores,
bool rewriteSha)
{
+ // A span that starts before the faulting instruction may relocate an
+ // RSP-relative access, which must run before the shift or it would read
+ // 128 bytes away. Emit such a span as prefix / shifted core / suffix.
+ // A forward span starts at the faulting instruction and never steals a
+ // stack-dependent one, so it keeps the original single-block shape.
+ byte[]? prefix = null;
+ if (site.CoreStart > 0 &&
+ !TryEncodeSegment(site.Instructions, 0, site.CoreStart, trampolineCursor, out prefix))
+ {
+ return false;
+ }
+
+ var prefixLength = prefix?.Length ?? 0;
var writer = new ListCodeWriter();
- var relocatedAddress = trampolineCursor + 5;
- var instructions = splitVectorStores ? RosettaVectorStorePatch.SplitVectorStores(site.Instructions) : site.Instructions;
+ var relocatedAddress = trampolineCursor + (ulong)prefixLength + 5;
+ var core = prefix is null
+ ? site.Instructions
+ : site.Instructions.Skip(site.CoreStart).Take(site.CoreCount).ToList();
+ var instructions = splitVectorStores ? RosettaVectorStorePatch.SplitVectorStores(core) : core;
if (rewriteSha)
{
instructions = ShaInstructionRewrite.Expand(instructions);
@@ -407,21 +704,22 @@ internal static class GuestRedZonePatcher
}
var relocated = writer.ToArray();
- var trampolineLength = checked(5 + relocated.Length + 8 + 5);
+ var trampolineLength = checked(prefixLength + 5 + relocated.Length + 8 + 5);
if (trampolineCursor > trampolineEnd || (ulong)trampolineLength > trampolineEnd - trampolineCursor)
{
return false;
}
var trampoline = new byte[trampolineLength];
+ prefix?.CopyTo(trampoline, 0);
// lea rsp,[rsp-128] -- LEA preserves guest flags.
- trampoline[0] = 0x48;
- trampoline[1] = 0x8D;
- trampoline[2] = 0x64;
- trampoline[3] = 0x24;
- trampoline[4] = 0x80;
- relocated.CopyTo(trampoline, 5);
- var restoreOffset = 5 + relocated.Length;
+ trampoline[prefixLength] = 0x48;
+ trampoline[prefixLength + 1] = 0x8D;
+ trampoline[prefixLength + 2] = 0x64;
+ trampoline[prefixLength + 3] = 0x24;
+ trampoline[prefixLength + 4] = 0x80;
+ relocated.CopyTo(trampoline, prefixLength + 5);
+ var restoreOffset = prefixLength + 5 + relocated.Length;
// lea rsp,[rsp+128]
trampoline[restoreOffset] = 0x48;
trampoline[restoreOffset + 1] = 0x8D;
@@ -560,7 +858,19 @@ internal static class GuestRedZonePatcher
private readonly record struct DecodedInstruction(Instruction Instruction);
- private readonly record struct PatchSite(ulong Address, int ByteLength, IList Instructions);
+ ///
+ /// A run of guest instructions relocated into one trampoline.
+ /// and delimit the
+ /// faultable instructions the RSP shift must bracket. Instructions outside
+ /// that core run unshifted, which is what lets a span start before the
+ /// faulting instruction and still relocate an RSP-relative one correctly.
+ ///
+ private readonly record struct PatchSite(
+ ulong Address,
+ int ByteLength,
+ IList Instructions,
+ int CoreStart,
+ int CoreCount);
internal readonly record struct PatchResult
{
@@ -580,6 +890,22 @@ internal static class GuestRedZonePatcher
public int FailedSites { get; init; }
+ ///
+ /// Faultable instructions inside a red-zone function that no trampoline
+ /// span could cover. These stay unprotected and are not counted by
+ /// , which only tracks sites that were selected
+ /// and then failed to patch.
+ ///
+ public int UnrelocatableSites { get; init; }
+
+ public int ControlFlowRefusals { get; init; }
+
+ public int BranchTargetRefusals { get; init; }
+
+ public int StackRefusals { get; init; }
+
+ public int TooShortRefusals { get; init; }
+
public ulong TrampolineBytes { get; init; }
}
diff --git a/src/SharpEmu.HLE/GpuMemory/CpuDirtySummary.cs b/src/SharpEmu.HLE/GpuMemory/CpuDirtySummary.cs
new file mode 100644
index 0000000..0703365
--- /dev/null
+++ b/src/SharpEmu.HLE/GpuMemory/CpuDirtySummary.cs
@@ -0,0 +1,31 @@
+// Copyright (C) 2026 SharpEmu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+namespace SharpEmu.HLE.GpuMemory;
+
+// One bit per tracked block: set while the block has at least one CPU-dirty page.
+// Regions update their bit under their own lock whenever their CPU-dirty mask
+// changes, so a clear bit lets a reader skip the block without taking any lock.
+// A reader that must observe a CPU write is ordered after it by the guest's own
+// submission, which happens after the write fault published the bit.
+public sealed class CpuDirtySummary
+{
+ private const int WordBits = 64;
+ private readonly long[] _words = new long[(TrackerLayout.BlockCount + WordBits - 1) / WordBits];
+
+ public void Set(ulong block, bool dirty)
+ {
+ var bit = 1L << (int)(block % WordBits);
+ ref var word = ref _words[block / WordBits];
+ if (dirty)
+ {
+ Interlocked.Or(ref word, bit);
+ }
+ else
+ {
+ Interlocked.And(ref word, ~bit);
+ }
+ }
+
+ public bool IsDirty(ulong block) => (Volatile.Read(ref _words[block / WordBits]) & (1L << (int)(block % WordBits))) != 0;
+}
diff --git a/src/SharpEmu.HLE/GpuMemory/GuestPageTracker.cs b/src/SharpEmu.HLE/GpuMemory/GuestPageTracker.cs
index be7b5b8..e234511 100644
--- a/src/SharpEmu.HLE/GpuMemory/GuestPageTracker.cs
+++ b/src/SharpEmu.HLE/GpuMemory/GuestPageTracker.cs
@@ -16,12 +16,18 @@ public sealed class GuestPageTracker
private readonly PageGuard _pages;
private readonly TrackedRegion?[] _regions = new TrackedRegion?[TrackerLayout.BlockCount];
private readonly object _regionGate = new();
+ private readonly CpuDirtySummary _cpuDirtySummary = new();
public GuestPageTracker(PageGuard pages) => _pages = pages;
public bool HasCpuDirtyPages(ulong vaddr, ulong size)
{
RejectUploadCallbackReentry();
+ if (IsKnownCpuClean(vaddr, size))
+ {
+ return false;
+ }
+
return VisitRegions(vaddr, size, create: true, (region, offset, bytes) =>
{
using var _ = region.Lock.Hold();
@@ -341,6 +347,23 @@ 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.
+ private bool IsKnownCpuClean(ulong vaddr, ulong size)
+ {
+ ValidateRange(vaddr, size);
+ var last = (vaddr + size - 1) / BlockBytes;
+ for (var index = vaddr / BlockBytes; index <= last; index++)
+ {
+ if (Volatile.Read(ref _regions[index]) == null || _cpuDirtySummary.IsDirty(index))
+ {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
// Visits (region, offset, bytes) per 4 MiB chunk; a true result stops the walk early.
private bool VisitRegions(ulong vaddr, ulong size, bool create, Func visit)
{
@@ -379,7 +402,7 @@ public sealed class GuestPageTracker
return existing;
}
- var created = new TrackedRegion(_pages, index * BlockBytes);
+ var created = new TrackedRegion(_pages, index * BlockBytes, _cpuDirtySummary);
Volatile.Write(ref _regions[index], created);
return created;
}
diff --git a/src/SharpEmu.HLE/GpuMemory/TrackedRegion.cs b/src/SharpEmu.HLE/GpuMemory/TrackedRegion.cs
index ae2f6b9..8e4d87a 100644
--- a/src/SharpEmu.HLE/GpuMemory/TrackedRegion.cs
+++ b/src/SharpEmu.HLE/GpuMemory/TrackedRegion.cs
@@ -17,8 +17,9 @@ public sealed class TrackedRegion
private PageMask _recentCpuUploads;
private PageMask _repeatedCpuWrites;
private PageMask _hotCpuWrites;
+ private readonly CpuDirtySummary? _summary;
- public TrackedRegion(PageGuard pages, ulong baseAddress)
+ public TrackedRegion(PageGuard pages, ulong baseAddress, CpuDirtySummary? summary = null)
{
if (baseAddress % BlockBytes != 0)
{
@@ -26,10 +27,12 @@ public sealed class TrackedRegion
}
_pages = pages;
+ _summary = summary;
BaseAddress = baseAddress;
_cpuDirty.Fill();
_writable.Fill();
_readable.Fill();
+ _summary?.Set(BaseAddress / BlockBytes, dirty: true);
}
public RegionLock Lock { get; } = new();
@@ -205,8 +208,10 @@ public sealed class TrackedRegion
private ref PageMask GetDirtyMask(WriteOrigin side) => ref side == WriteOrigin.Cpu ? ref _cpuDirty : ref _gpuDirty;
+ // Every change to the CPU-dirty mask ends here, so it also publishes the block summary.
private void UpdateCpuProtection(bool track)
{
+ _summary?.Set(BaseAddress / BlockBytes, _cpuDirty.Any);
var mask = _cpuDirty ^ _writable;
_writable = _cpuDirty;
if (mask.None)
diff --git a/src/SharpEmu.HLE/GuestThreadExecution.cs b/src/SharpEmu.HLE/GuestThreadExecution.cs
index 595de45..b7b2a81 100644
--- a/src/SharpEmu.HLE/GuestThreadExecution.cs
+++ b/src/SharpEmu.HLE/GuestThreadExecution.cs
@@ -12,7 +12,8 @@ public readonly record struct GuestThreadStartRequest(
ulong AttributeAddress,
string Name,
int Priority,
- ulong AffinityMask);
+ ulong AffinityMask,
+ ulong StackSize = 0);
public readonly record struct GuestThreadSnapshot(
ulong ThreadHandle,
diff --git a/src/SharpEmu.Libs/Agc/AgcExports.ShaderState.cs b/src/SharpEmu.Libs/Agc/AgcExports.ShaderState.cs
index 8bb0251..b629263 100644
--- a/src/SharpEmu.Libs/Agc/AgcExports.ShaderState.cs
+++ b/src/SharpEmu.Libs/Agc/AgcExports.ShaderState.cs
@@ -31,7 +31,10 @@ public static partial class AgcExports
private const ulong ShaderNumShRegistersOffset = 0x5C;
private const int ShaderStructBytes = 0x60;
private const uint MaximumDeclaredShaderSizeBytes = 1024 * 1024;
- private const int MaximumEmbeddedFusedScanBytes = 64 * 1024;
+ // Embedded GS/HS front/back descriptors can sit beyond the first 64 KiB of
+ // the upload. Scan the full shader object size accepted by AGC so the
+ // continuation is registered before resource planning sees the front half.
+ private const int MaximumEmbeddedFusedScanBytes = 1024 * 1024;
private const ulong FusedShaderImageAlignment = 4;
private const byte ComputeShaderType = 0;
private const byte PsShaderType = 1;
@@ -166,27 +169,6 @@ public static partial class AgcExports
return false;
}
- var upload = new byte[MaximumEmbeddedFusedScanBytes];
- var bytesRead = 0;
- const int readChunkBytes = 4 * 1024;
- while (bytesRead < upload.Length)
- {
- var chunkLength = Math.Min(readChunkBytes, upload.Length - bytesRead);
- if (!ctx.Memory.TryRead(
- entryCodeAddress + (ulong)bytesRead,
- upload.AsSpan(bytesRead, chunkLength)))
- {
- break;
- }
-
- bytesRead += chunkLength;
- }
-
- if (bytesRead < ShaderStructBytes)
- {
- return false;
- }
-
var requiredContinuationType = entryType == GsFrontShaderType
? GsBackShaderType
: HsBackShaderType;
@@ -201,57 +183,95 @@ public static partial class AgcExports
ulong bestCodeAddress = 0;
ulong bestHeaderAddress = 0;
var bestDistance = ulong.MaxValue;
- for (var offset = 0;
- offset <= bytesRead - ShaderStructBytes;
- offset += sizeof(uint))
+
+ bool ScanForContinuation(ulong scanAddress)
{
- var descriptor = upload.AsSpan(offset, ShaderStructBytes);
- if (BinaryPrimitives.ReadUInt32LittleEndian(descriptor) != ShaderFileHeader ||
- BinaryPrimitives.ReadUInt32LittleEndian(descriptor[sizeof(uint)..]) != ShaderVersion ||
- descriptor[(int)ShaderTypeOffset] != requiredContinuationType)
+ var upload = new byte[MaximumEmbeddedFusedScanBytes];
+ var bytesRead = 0;
+ const int readChunkBytes = 4 * 1024;
+ while (bytesRead < upload.Length)
{
- continue;
+ var chunkLength = Math.Min(readChunkBytes, upload.Length - bytesRead);
+ if (!ctx.Memory.TryRead(
+ scanAddress + (ulong)bytesRead,
+ upload.AsSpan(bytesRead, chunkLength)))
+ {
+ break;
+ }
+
+ bytesRead += chunkLength;
}
- var continuationCodeAddress = BinaryPrimitives.ReadUInt64LittleEndian(
- descriptor[(int)ShaderCodeOffset..]);
- var continuationSize = BinaryPrimitives.ReadUInt32LittleEndian(
- descriptor[(int)ShaderSizeOffset..]);
- if (continuationCodeAddress <= entryCodeAddress ||
- continuationCodeAddress - entryCodeAddress > uint.MaxValue ||
- !IsValidDeclaredShaderSize(continuationSize) ||
- !CanReadShaderRange(ctx, continuationCodeAddress, continuationSize))
+ if (bytesRead < ShaderStructBytes)
{
- continue;
+ return false;
}
- var continuationSpecialsAddress = BinaryPrimitives.ReadUInt64LittleEndian(
- descriptor[(int)ShaderSpecialsOffset..]);
- if (entrySpecialsAddress != 0 && continuationSpecialsAddress != 0)
+ var found = false;
+ for (var offset = 0;
+ offset <= bytesRead - ShaderStructBytes;
+ offset += sizeof(uint))
{
- if (!TryReadUInt32(
- ctx,
- entrySpecialsAddress + ShaderSpecialVgtShaderStagesEnOffset + sizeof(uint),
- out var entryStages) ||
- !TryReadUInt32(
- ctx,
- continuationSpecialsAddress + ShaderSpecialVgtShaderStagesEnOffset + sizeof(uint),
- out var continuationStages) ||
- ((entryStages ^ continuationStages) & waveSizeBit) != 0)
+ var descriptor = upload.AsSpan(offset, ShaderStructBytes);
+ if (BinaryPrimitives.ReadUInt32LittleEndian(descriptor) != ShaderFileHeader ||
+ BinaryPrimitives.ReadUInt32LittleEndian(descriptor[sizeof(uint)..]) != ShaderVersion ||
+ descriptor[(int)ShaderTypeOffset] != requiredContinuationType)
{
continue;
}
+
+ var continuationCodeAddress = BinaryPrimitives.ReadUInt64LittleEndian(
+ descriptor[(int)ShaderCodeOffset..]);
+ var continuationSize = BinaryPrimitives.ReadUInt32LittleEndian(
+ descriptor[(int)ShaderSizeOffset..]);
+ if (continuationCodeAddress <= entryCodeAddress ||
+ continuationCodeAddress - entryCodeAddress > uint.MaxValue ||
+ !IsValidDeclaredShaderSize(continuationSize) ||
+ !CanReadShaderRange(ctx, continuationCodeAddress, continuationSize))
+ {
+ continue;
+ }
+
+ var continuationSpecialsAddress = BinaryPrimitives.ReadUInt64LittleEndian(
+ descriptor[(int)ShaderSpecialsOffset..]);
+ if (entrySpecialsAddress != 0 && continuationSpecialsAddress != 0)
+ {
+ if (!TryReadUInt32(
+ ctx,
+ entrySpecialsAddress + ShaderSpecialVgtShaderStagesEnOffset + sizeof(uint),
+ out var entryStages) ||
+ !TryReadUInt32(
+ ctx,
+ continuationSpecialsAddress + ShaderSpecialVgtShaderStagesEnOffset + sizeof(uint),
+ out var continuationStages) ||
+ ((entryStages ^ continuationStages) & waveSizeBit) != 0)
+ {
+ continue;
+ }
+ }
+
+ var distance = continuationCodeAddress - entryCodeAddress;
+ if (distance >= bestDistance)
+ {
+ continue;
+ }
+
+ bestDistance = distance;
+ bestCodeAddress = continuationCodeAddress;
+ bestHeaderAddress = scanAddress + (ulong)offset;
+ found = true;
}
- var distance = continuationCodeAddress - entryCodeAddress;
- if (distance >= bestDistance)
- {
- continue;
- }
+ return found;
+ }
- bestDistance = distance;
- bestCodeAddress = continuationCodeAddress;
- bestHeaderAddress = entryCodeAddress + (ulong)offset;
+ // Most uploads place the second descriptor beside the code. Some
+ // titles keep both descriptors in the header allocation instead, so
+ // inspect that region as a fallback.
+ ScanForContinuation(entryCodeAddress);
+ if (bestHeaderAddress == 0)
+ {
+ ScanForContinuation(entryHeaderAddress);
}
if (bestHeaderAddress == 0)
@@ -422,6 +442,13 @@ public static partial class AgcExports
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
+ // Both halves share one user-data block, so the fused USER_SGPR count is
+ // the larger of the two.
+ if (!MergeFusedUserScalarCount(ctx, frontRegistersAddress, frontRegisterCount, fusedRegistersAddress, registerCount))
+ {
+ return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
+ }
+
for (var occurrence = 0; occurrence < 2; occurrence++)
{
if (!TryFindShaderRegister(ctx, fusedRegistersAddress, registerCount, SpiShaderPgmChksumGs, occurrence, out var fusedEntry) ||
@@ -1216,6 +1243,27 @@ public static partial class AgcExports
(frontType == GsFrontShaderType && backType == GsBackShaderType) ||
(frontType == HsFrontShaderType && backType == HsBackShaderType);
+ private static uint UserScalarCount(uint rsrc2) => ((rsrc2 >> 1) & 0x1Fu) | (((rsrc2 >> 27) & 1u) << 5);
+
+ private static bool MergeFusedUserScalarCount(CpuContext ctx, ulong frontRegisters, int frontCount, ulong fusedRegisters, int fusedCount)
+ {
+ if (!TryFindShaderRegister(ctx, frontRegisters, frontCount, SpiShaderPgmRsrc2Gs, 0, out var frontEntry) ||
+ !TryFindShaderRegister(ctx, fusedRegisters, fusedCount, SpiShaderPgmRsrc2Gs, 0, out var fusedEntry))
+ {
+ return true;
+ }
+
+ if (!TryReadUInt32(ctx, frontEntry + sizeof(uint), out var front) ||
+ !TryReadUInt32(ctx, fusedEntry + sizeof(uint), out var fused))
+ {
+ return false;
+ }
+
+ var count = Math.Max(UserScalarCount(front), UserScalarCount(fused));
+ var merged = (fused & ~((0x1Fu << 1) | (1u << 27))) | ((count & 0x1Fu) << 1) | ((count >> 5) << 27);
+ return merged == fused || TryWriteUInt32(ctx, fusedEntry + sizeof(uint), merged);
+ }
+
private static bool TryFindShaderRegister(
CpuContext ctx,
ulong registersAddress,
diff --git a/src/SharpEmu.Libs/Agc/AgcExports.cs b/src/SharpEmu.Libs/Agc/AgcExports.cs
index e7af284..60f29fe 100644
--- a/src/SharpEmu.Libs/Agc/AgcExports.cs
+++ b/src/SharpEmu.Libs/Agc/AgcExports.cs
@@ -99,6 +99,7 @@ public static partial class AgcExports
private const uint SpiShaderPgmLoGs = 0x88;
private const uint SpiShaderPgmHiGs = 0x89;
private const uint SpiShaderPgmRsrc1Gs = 0x8A;
+ private const uint SpiShaderPgmRsrc2Gs = 0x8B;
private const uint SpiShaderPgmChksumGs = 0x80;
private const uint SpiShaderPgmChksumPs = 0x06;
private const uint SpiPsInputEna = 0x1B3;
diff --git a/src/SharpEmu.Libs/Ampr/AmprExports.cs b/src/SharpEmu.Libs/Ampr/AmprExports.cs
index e940a1d..355762f 100644
--- a/src/SharpEmu.Libs/Ampr/AmprExports.cs
+++ b/src/SharpEmu.Libs/Ampr/AmprExports.cs
@@ -965,6 +965,8 @@ public static class AmprExports
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
+ private static int _unknownReadFileIdWarnings;
+
private static int CompleteReadFileRecord(CpuContext ctx, ulong commandBuffer, ReadFileCommand command)
{
var fileId = command.FileId;
@@ -982,6 +984,13 @@ public static class AmprExports
if (!AmprFileRegistry.TryGetHostPath(fileId, out hostPath))
{
+ // An unknown id stalls the guest's stream silently, so warn even without tracing.
+ if (Interlocked.Increment(ref _unknownReadFileIdWarnings) <= 16)
+ {
+ Console.Error.WriteLine(
+ $"[LOADER][WARN] ampr.read_unknown_file_id id=0x{fileId:X8} size=0x{size:X} offset=0x{fileOffset:X}");
+ }
+
TraceAmprRead(ctx, commandBuffer, fileId, destination, size, fileOffset, bytesRead: 0, hostPath, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
}
diff --git a/src/SharpEmu.Libs/Ampr/AmprFileRegistry.cs b/src/SharpEmu.Libs/Ampr/AmprFileRegistry.cs
index f24fe97..3e77db0 100644
--- a/src/SharpEmu.Libs/Ampr/AmprFileRegistry.cs
+++ b/src/SharpEmu.Libs/Ampr/AmprFileRegistry.cs
@@ -38,6 +38,9 @@ internal static class AmprFileRegistry
private static readonly object _resolvedFileGate = new();
private static readonly Dictionary _resolvedIdsByPath = new(HostFsPath.Comparer);
private static readonly ConcurrentDictionary _resolvedPathsById = new();
+ // Ids already handed to the guest; alias publishes must not re-poison them.
+ private static readonly ConcurrentDictionary _aprResolvedPathsById = new();
+ private static readonly ConcurrentDictionary _loggedAprCollisionIds = new();
// Resolved handles use a separate range from the 31-bit compatibility hashes.
// Never reuse a handle while queued reads can still refer to it.
private static uint _nextResolvedId = 0x80000000;
@@ -67,24 +70,52 @@ internal static class AmprFileRegistry
public static uint RegisterAprResolvedPath(string guestPath, string hostPath)
{
- if (TryGetApp0Relative(guestPath, out var relative) && relative.Length != 0)
- {
- RegisterApp0Relative(relative, hostPath);
- }
-
// APR file ids are part of the guest ABI: ResolveFilepathsToIds returns
// the 31-bit FNV-1a hash of the guest path. Keep the collision-safe
// process-local handles used by Register() separate from this path so a
// title can compare resolved ids with ids baked into its asset tables.
+ // A hash shared by two files (13 /app0/ pairs in Demon's Souls) falls back to a handle.
var fileId = ComputeFileId(guestPath);
- PublishCompatibilityPath(fileId, hostPath, AliasRank.Resolved);
- return fileId;
+ lock (_resolvedFileGate)
+ {
+ if (_aprResolvedPathsById.TryGetValue(fileId, out var claimedPath))
+ {
+ if (IsSameHostFile(claimedPath, hostPath))
+ return fileId;
+ }
+ else if (!_hostPathsById.TryGetValue(fileId, out var indexed) ||
+ (!indexed.Ambiguous && IsSameHostFile(indexed.HostPath, hostPath)))
+ {
+ if (TryGetApp0Relative(guestPath, out var relative) && relative.Length != 0)
+ {
+ RegisterApp0Relative(relative, hostPath);
+ }
+
+ PublishCompatibilityPath(fileId, hostPath, AliasRank.Resolved);
+ _aprResolvedPathsById[fileId] = hostPath;
+ return fileId;
+ }
+ }
+
+ var handle = Register(guestPath, hostPath);
+ if (_loggedAprCollisionIds.TryAdd(fileId, 0))
+ {
+ Console.Error.WriteLine(
+ $"[LOADER][WARN] ampr.apr_id_collision id=0x{fileId:X8} path='{guestPath}' handle=0x{handle:X8}");
+ }
+
+ return handle;
}
+ private static bool IsSameHostFile(string left, string right) =>
+ HostFsPath.Comparer.Equals(Path.GetFullPath(left), Path.GetFullPath(right));
+
public static bool TryGetHostPath(uint id, out string hostPath)
{
if ((id & 0x80000000) != 0)
return _resolvedPathsById.TryGetValue(id, out hostPath!);
+ if (_aprResolvedPathsById.TryGetValue(id, out hostPath!))
+ return true;
if (_hostPathsById.TryGetValue(id, out var entry) && !entry.Ambiguous)
{
hostPath = entry.HostPath;
@@ -101,8 +132,10 @@ internal static class AmprFileRegistry
lock (_indexGate)
{
_hostPathsById.Clear();
+ _loggedAprCollisionIds.Clear();
lock (_resolvedFileGate)
{
+ _aprResolvedPathsById.Clear();
_resolvedIdsByPath.Clear();
_resolvedPathsById.Clear();
_nextResolvedId = 0x80000000;
@@ -417,7 +450,23 @@ internal static class AmprFileRegistry
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"SharpEmu",
"ampr-index");
- Directory.CreateDirectory(cacheDir);
+ try
+ {
+ Directory.CreateDirectory(cacheDir);
+ var probePath = Path.Combine(cacheDir, $".write-test-{Environment.ProcessId}");
+ using (File.Create(probePath)) { }
+ File.Delete(probePath);
+ }
+ catch (Exception exception) when (exception is UnauthorizedAccessException or IOException)
+ {
+ // Sandboxed launches and mitigated child processes can lose access
+ // to the profile AppData directory. Keep the index beside the
+ // executable so the next launch can still reuse it.
+ cacheDir = Path.Combine(AppContext.BaseDirectory, "user", "ampr-index");
+ Directory.CreateDirectory(cacheDir);
+ Console.Error.WriteLine(
+ $"[LOADER][WARN] ampr.app0_index_cache_fallback path={cacheDir}: {exception.Message}");
+ }
// Distinct roots must not share a cache file. Folding case is only
// correct where the host filesystem folds it too.
diff --git a/src/SharpEmu.Libs/Gpu/Buffers/GuestBufferCache.cs b/src/SharpEmu.Libs/Gpu/Buffers/GuestBufferCache.cs
index 4e1e683..7ac5d65 100644
--- a/src/SharpEmu.Libs/Gpu/Buffers/GuestBufferCache.cs
+++ b/src/SharpEmu.Libs/Gpu/Buffers/GuestBufferCache.cs
@@ -497,18 +497,23 @@ public sealed unsafe class GuestBufferCache : IGuestBufferStore, IDisposable
var index = _registry.FindFirstOverlappingIndex(guestAddress);
for (; index < _registry.RegisteredCount && _registry.GetRegisteredAddress(index) < end; index++)
{
- var buffer = _registry.GetBuffer(_registry.GetRegisteredIdentifier(index));
+ var identifier = _registry.GetRegisteredIdentifier(index);
+ var buffer = _registry.GetBuffer(identifier);
var start = Math.Max(buffer.CpuAddress, guestAddress);
var finish = Math.Min(buffer.CpuAddress + buffer.Size, end);
if (start < finish)
{
if (GuestGpuMemoryHook.Traces(start, finish - start))
GuestGpuMemoryHook.Trace(start, finish - start,
- $"device-address-touch buffer={_registry.GetRegisteredIdentifier(index)} submission_tick={_scheduler.CurrentTick} collection_tick={_retirementPolicy.CurrentTick}");
+ $"device-address-touch buffer={identifier} submission_tick={_scheduler.CurrentTick} collection_tick={_retirementPolicy.CurrentTick}");
// Clean buffers remain in use through their device addresses.
- TouchBuffer(buffer);
+ TouchBuffer(identifier);
// Device-address reads reuse persistent buffers; track writes after each upload.
- _ = SynchronizeBuffer(buffer, start, finish - start, false, false, preserveCpuWriteHotPages: false);
+ // A range without CPU-dirty pages has nothing to upload (the same early exit
+ // SynchronizeBuffer takes for this read-only call), and the block summary
+ // answers that without a lock for the common all-clean case.
+ if (_tracker.HasCpuDirtyPages(start, finish - start))
+ _ = SynchronizeBuffer(buffer, start, finish - start, false, false, preserveCpuWriteHotPages: false);
}
}
}
@@ -1052,7 +1057,7 @@ public sealed unsafe class GuestBufferCache : IGuestBufferStore, IDisposable
BufferUploadProfile.Record(guestAddress, size, copies.Count, totalSize, hotBytes, elapsedTicks);
}
- if (isTexelBuffer && !isWritten)
+ if (isTexelBuffer)
{
return RequireImageCache().TrySynchronizeBufferFromImage(buffer, guestAddress, size);
}
diff --git a/src/SharpEmu.Libs/Gpu/GpuCommands/GpuCommandInterpreter.Events.cs b/src/SharpEmu.Libs/Gpu/GpuCommands/GpuCommandInterpreter.Events.cs
index 4516ef0..47af475 100644
--- a/src/SharpEmu.Libs/Gpu/GpuCommands/GpuCommandInterpreter.Events.cs
+++ b/src/SharpEmu.Libs/Gpu/GpuCommands/GpuCommandInterpreter.Events.cs
@@ -190,12 +190,27 @@ public sealed partial class GpuCommandInterpreter
SubmitFlipWithInterrupt(payload[4], payload[5], Address(payload[1], payload[2]), payload[3]);
break;
default:
- throw _host.Fatal($"The marker is unknown: offset=0x{packet.Offset:X5} marker=0x{markerId:X} address=0x{packet.PacketAddress:X16}.");
+ // A marker id this interpreter has no side effect for (profiling/debug annotations such as
+ // 0x6EC and 0x80C in Demon's Souls). The packet carries no work, so it is consumed; the id is
+ // reported once so a marker that later turns out to matter is still visible.
+ NoteUnknownMarker(markerId, packet);
+ break;
}
return packet.Length - 1;
}
+ private static readonly System.Collections.Concurrent.ConcurrentDictionary _reportedUnknownMarkers = new();
+
+ private static void NoteUnknownMarker(uint markerId, in PacketContext packet)
+ {
+ if (_reportedUnknownMarkers.TryAdd(markerId, true))
+ {
+ Console.Error.WriteLine(
+ $"[LOADER][WARN] command_stream.marker_ignored marker=0x{markerId:X} offset=0x{packet.Offset:X5} address=0x{packet.PacketAddress:X16}");
+ }
+ }
+
internal uint PushMarkerPacket(in PacketContext packet, ReadOnlySpan payload) => packet.Length - 1;
internal uint PopMarkerPacket(in PacketContext packet, ReadOnlySpan payload) => packet.Length - 1;
diff --git a/src/SharpEmu.Libs/Gpu/Images/CachedImage.cs b/src/SharpEmu.Libs/Gpu/Images/CachedImage.cs
index 398d061..7bf81b4 100644
--- a/src/SharpEmu.Libs/Gpu/Images/CachedImage.cs
+++ b/src/SharpEmu.Libs/Gpu/Images/CachedImage.cs
@@ -68,6 +68,7 @@ public sealed unsafe partial class CachedImage : IDisposable
private bool _maybeHashValid;
private bool _gpuModified;
private bool _bufferModified;
+ private bool _bufferHoldsGpuContents;
public ImageDescription Description;
public readonly ImageBacking Backing = new();
@@ -130,9 +131,10 @@ public sealed unsafe partial class CachedImage : IDisposable
Backing.Usage = create.Usage;
var vk = device.Vk;
- if (vk.CreateImage(device.Device, &create, null, out Backing.Handle) != Result.Success)
+ var createResult = vk.CreateImage(device.Device, &create, null, out Backing.Handle);
+ if (createResult != Result.Success)
{
- throw CreateFailure(create);
+ throw CreateFailure(create, "vkCreateImage", createResult, 0);
}
vk.GetImageMemoryRequirements(device.Device, Backing.Handle, out var requirements);
@@ -158,21 +160,33 @@ public sealed unsafe partial class CachedImage : IDisposable
}
}
- if (allocated != Result.Success || vk.BindImageMemory(device.Device, Backing.Handle, Backing.Memory, 0) != Result.Success)
+ if (allocated != Result.Success)
{
vk.DestroyImage(device.Device, Backing.Handle, null);
device.FreeMemory(Backing.Memory);
Backing.Handle = default;
Backing.Memory = default;
- throw CreateFailure(create);
+ throw CreateFailure(create, "vkAllocateMemory", allocated, requirements.Size);
+ }
+
+ var bindResult = vk.BindImageMemory(device.Device, Backing.Handle, Backing.Memory, 0);
+ if (bindResult != Result.Success)
+ {
+ vk.DestroyImage(device.Device, Backing.Handle, null);
+ device.FreeMemory(Backing.Memory);
+ Backing.Handle = default;
+ Backing.Memory = default;
+ throw CreateFailure(create, "vkBindImageMemory", bindResult, requirements.Size);
}
Backing.AllocationSize = requirements.Size;
}
- private static Exception CreateFailure(in ImageCreateInfo create) =>
+ private static Exception CreateFailure(in ImageCreateInfo create, string operation, Result result, ulong requiredBytes) =>
SubmissionScheduler.Fatal(
- $"The image could not be created: extent={create.Extent.Width}x{create.Extent.Height}x{create.Extent.Depth} format={(int)create.Format} layers={create.ArrayLayers} levels={create.MipLevels}.");
+ $"The image could not be created: operation={operation} result={result} required_bytes={requiredBytes} " +
+ $"extent={create.Extent.Width}x{create.Extent.Height}x{create.Extent.Depth} format={create.Format}({(int)create.Format}) " +
+ $"layers={create.ArrayLayers} levels={create.MipLevels} usage=0x{(uint)create.Usage:X} flags=0x{(uint)create.Flags:X}.");
internal static bool TrySelectSupportedImageConfiguration(IImageFormatSupport device, ref ImageCreateInfo configuration, bool allowCompressedImageFallback)
{
@@ -351,12 +365,21 @@ public sealed unsafe partial class CachedImage : IDisposable
public bool IsGpuModified => _gpuModified;
- public void MarkGpuModified() => _gpuModified = true;
+ public void MarkGpuModified()
+ {
+ _gpuModified = true;
+ _bufferHoldsGpuContents = false;
+ }
public void ClearGpuModified() => _gpuModified = false;
public bool IsBufferModified => _bufferModified;
+ // The guest buffer cache holds this image's current GPU contents.
+ public bool BufferHoldsGpuContents => _bufferHoldsGpuContents;
+
+ public void MarkBufferHoldsGpuContents() => _bufferHoldsGpuContents = true;
+
public void MarkBufferModified() => _bufferModified = true;
public void ClearBufferModified() => _bufferModified = false;
diff --git a/src/SharpEmu.Libs/Gpu/Images/GuestImageCache.Overlap.cs b/src/SharpEmu.Libs/Gpu/Images/GuestImageCache.Overlap.cs
index 0b31a83..1e6bbf5 100644
--- a/src/SharpEmu.Libs/Gpu/Images/GuestImageCache.Overlap.cs
+++ b/src/SharpEmu.Libs/Gpu/Images/GuestImageCache.Overlap.cs
@@ -349,6 +349,11 @@ public sealed partial class GuestImageCache
case ImageRole.Texture:
recreate |= requested.IsDepth && !cachedInfo.IsDepth;
recreate |= rawD16Texture;
+ // A color texture that reads neither the depth nor the stencil plane
+ // reinterprets the memory as color, so its bytes move to a color image.
+ recreate |= cachedInfo.IsDepth && !requested.IsDepth &&
+ !ViewFormatRules.IsDepthCompatible(requested.PixelFormat) &&
+ !ViewFormatRules.IsStencilViewFormat(requested.PixelFormat);
break;
case ImageRole.StorageImage:
case ImageRole.ColorTarget:
@@ -440,23 +445,15 @@ public sealed partial class GuestImageCache
ref var cachedInfo = ref cached.Description;
var currentTick = _scheduler.CurrentTick;
var safeToDelete = currentTick - Math.Min(currentTick, cached.LastAccessTick) > TicksBeforeRemoval;
+ var requestedBlock = requested.BytesPerBlock * requested.Samples;
+ var cachedBlock = cachedInfo.BytesPerBlock * cachedInfo.Samples;
+ var requestedBlockExtent = requested.BlockExtent;
+ var cachedBlockExtent = cachedInfo.BlockExtent;
- if (requested.Data.Address == cachedInfo.Data.Address)
+ if (requested.Data.Address == cachedInfo.Data.Address &&
+ requestedBlockExtent.Width == cachedBlockExtent.Width && requestedBlockExtent.Height == cachedBlockExtent.Height &&
+ requestedBlock == cachedBlock)
{
- var requestedBlock = requested.BytesPerBlock * requested.Samples;
- var cachedBlock = cachedInfo.BytesPerBlock * cachedInfo.Samples;
- var requestedBlockExtent = requested.BlockExtent;
- var cachedBlockExtent = cachedInfo.BlockExtent;
- if (requestedBlockExtent.Width != cachedBlockExtent.Width || requestedBlockExtent.Height != cachedBlockExtent.Height || requestedBlock != cachedBlock)
- {
- if (safeToDelete)
- {
- ReleaseImage(cachedImageIdentifier);
- }
-
- return new OverlapResolution(mergedImageIdentifier);
- }
-
var depthImageIdentifier = ResolveDepthOverlap(requested, role, cachedImageIdentifier);
if (depthImageIdentifier.IsValid)
{
@@ -509,50 +506,32 @@ public sealed partial class GuestImageCache
$"type={(uint)requested.Type}/{(uint)cachedInfo.Type} tile={(uint)requested.TileMode}/{(uint)cachedInfo.TileMode}.");
}
- if (requested.Data.Address > cachedInfo.Data.Address)
+ var requestedMip = requested.FindMatchingMipLevel(cachedInfo);
+ if (requestedMip >= 0)
{
- var mip = requested.FindMatchingMipLevel(cachedInfo);
- if (mip >= 0)
- {
- var layer = requested.FindMatchingArraySlice(cachedInfo, mip);
- if (layer >= 0)
- {
- return new OverlapResolution(cachedImageIdentifier, mip, layer);
- }
- }
-
- if (safeToDelete)
- {
- ReleaseImage(cachedImageIdentifier);
- }
-
- return new OverlapResolution(ResourceSlotIdentifier.Invalid);
+ var layer = requested.FindMatchingArraySlice(cachedInfo, requestedMip);
+ return new OverlapResolution(cachedImageIdentifier, requestedMip, layer);
}
var containedMip = cachedInfo.FindMatchingMipLevel(requested);
if (containedMip >= 0)
{
var containedLayer = cachedInfo.FindMatchingArraySlice(requested, containedMip);
- if (containedLayer >= 0)
+ if (!mergedImageIdentifier.IsValid)
{
- if (cached.Binding.IsTarget)
- {
- cached.Binding.NeedsRebind = true;
- if (mergedImageIdentifier.IsValid)
- {
- _slots[mergedImageIdentifier].Binding.IsTarget = true;
- }
-
- ReleaseImage(cachedImageIdentifier);
- return new OverlapResolution(mergedImageIdentifier);
- }
-
- if (mergedImageIdentifier.IsValid)
- {
- CopyIntoMip(mergedImageIdentifier, cachedImageIdentifier, (uint)containedMip, (uint)containedLayer);
- ReleaseImage(cachedImageIdentifier);
- }
+ return new OverlapResolution(GrowImage(requested, cachedImageIdentifier));
}
+
+ cached.Binding.NeedsRebind |= cached.Binding.IsBound || cached.Binding.IsTarget;
+ _slots[mergedImageIdentifier].Binding.IsTarget |= cached.Binding.IsTarget;
+ CopyIntoMip(mergedImageIdentifier, cachedImageIdentifier, (uint)containedMip, (uint)containedLayer);
+ ReleaseImage(cachedImageIdentifier);
+ return new OverlapResolution(mergedImageIdentifier);
+ }
+
+ if (requested.Data.Address >= cachedInfo.Data.Address && safeToDelete)
+ {
+ ReleaseImage(cachedImageIdentifier);
}
return new OverlapResolution(mergedImageIdentifier);
@@ -572,7 +551,17 @@ public sealed partial class GuestImageCache
}
PopulateFromGuest(expandedImageIdentifier, ImageRequest.Refresh(description, UploadRole(source)), "expand");
- CopyWholeImage(expandedImageIdentifier, sourceImageIdentifier);
+ var mip = source.Description.FindMatchingMipLevel(description);
+ var layer = source.Description.FindMatchingArraySlice(description, mip);
+ if (layer >= 0)
+ {
+ CopyIntoMip(expandedImageIdentifier, sourceImageIdentifier, (uint)mip, (uint)layer);
+ }
+ else
+ {
+ CopyWholeImage(expandedImageIdentifier, sourceImageIdentifier);
+ }
+
ReleaseImage(sourceImageIdentifier);
return expandedImageIdentifier;
}
diff --git a/src/SharpEmu.Libs/Gpu/Images/GuestImageCache.Transfers.cs b/src/SharpEmu.Libs/Gpu/Images/GuestImageCache.Transfers.cs
index 0255c37..08a4fd9 100644
--- a/src/SharpEmu.Libs/Gpu/Images/GuestImageCache.Transfers.cs
+++ b/src/SharpEmu.Libs/Gpu/Images/GuestImageCache.Transfers.cs
@@ -653,6 +653,7 @@ public sealed unsafe partial class GuestImageCache
}
DownloadToBuffer(image, buffer, bufferOffset, copySize, plan);
+ image.MarkBufferHoldsGpuContents();
return true;
}
diff --git a/src/SharpEmu.Libs/Gpu/Images/GuestPixelFormats.cs b/src/SharpEmu.Libs/Gpu/Images/GuestPixelFormats.cs
index ea973fb..50a0f04 100644
--- a/src/SharpEmu.Libs/Gpu/Images/GuestPixelFormats.cs
+++ b/src/SharpEmu.Libs/Gpu/Images/GuestPixelFormats.cs
@@ -315,6 +315,7 @@ public static class GuestPixelFormats
new(GuestPixelFormat.Bits16Float, 2, 0, 2, true, false),
new(GuestPixelFormat.Bits8_8UNorm, 2, 0, 2, true, false),
new(GuestPixelFormat.Bits8_8SNorm, 2, 0, 2, true, false),
+ new(GuestPixelFormat.Bits8_8UScaled, 2, 0, 2, true, false),
new(GuestPixelFormat.Bits8_8UInt, 2, 0, 2, true, true),
new(GuestPixelFormat.Bits8_8SInt, 2, 0, 2, true, false, true),
new(GuestPixelFormat.Bits32UInt, 4, 0, 4, true, true),
@@ -325,6 +326,9 @@ public static class GuestPixelFormats
new(GuestPixelFormat.Bits16_16UInt, 4, 0, 4, true, true),
new(GuestPixelFormat.Bits16_16SInt, 4, 0, 4, true, false, true),
new(GuestPixelFormat.Bits16_16Float, 4, 0, 4, true, false),
+ // Vulkan has no normalized 11:11:10 format. Keep the guest packing sampled
+ // through the native 32-bit packed-float view, matching the host backend.
+ new(GuestPixelFormat.Bits11_11_10UNorm, 4, 0, 4, true, false),
new(GuestPixelFormat.Bits11_11_10UInt, 4, 0, 4, true, true),
new(GuestPixelFormat.Bits11_11_10Float, 4, 0, 4, true, false),
new(GuestPixelFormat.Bits10_10_10_2UNorm, 4, 0, 4, true, false),
@@ -347,8 +351,8 @@ public static class GuestPixelFormats
new(GuestPixelFormat.Bits32_32_32_32UInt, 16, 0, 16, true, true),
new(GuestPixelFormat.Bits32_32_32_32SInt, 16, 0, 16, true, false, true),
new(GuestPixelFormat.Bits32_32_32_32Float, 16, 0, 16, true, false),
- new(GuestPixelFormat.Bits8Srgb, 1, 0, 0, true, false),
- new(GuestPixelFormat.Bits8_8Srgb, 2, 0, 0, true, false),
+ new(GuestPixelFormat.Bits8Srgb, 1, 0, 1, true, false),
+ new(GuestPixelFormat.Bits8_8Srgb, 2, 0, 2, true, false),
new(GuestPixelFormat.Bits8_8_8_8Srgb, 4, 0, 4, true, false),
new(GuestPixelFormat.Bits9_9_9_5Float, 4, 0, 0, true, false),
new(GuestPixelFormat.Bits5_6_5UNorm, 2, 0, 2, true, false),
@@ -432,6 +436,7 @@ public static class GuestPixelFormats
(GuestPixelFormat.Bits16Float, Format.R16Sfloat),
(GuestPixelFormat.Bits8_8UNorm, Format.R8G8Unorm),
(GuestPixelFormat.Bits8_8SNorm, Format.R8G8SNorm),
+ (GuestPixelFormat.Bits8_8UScaled, Format.R8G8Uscaled),
(GuestPixelFormat.Bits8_8UInt, Format.R8G8Uint),
(GuestPixelFormat.Bits8_8SInt, Format.R8G8Sint),
(GuestPixelFormat.Bits32UInt, Format.R32Uint),
@@ -442,6 +447,7 @@ public static class GuestPixelFormats
(GuestPixelFormat.Bits16_16UInt, Format.R16G16Uint),
(GuestPixelFormat.Bits16_16SInt, Format.R16G16Sint),
(GuestPixelFormat.Bits16_16Float, Format.R16G16Sfloat),
+ (GuestPixelFormat.Bits11_11_10UNorm, Format.B10G11R11UfloatPack32),
(GuestPixelFormat.Bits11_11_10Float, Format.B10G11R11UfloatPack32),
(GuestPixelFormat.Bits10_10_10_2UNorm, Format.A2B10G10R10UnormPack32),
(GuestPixelFormat.Bits10_10_10_2UInt, Format.A2B10G10R10UintPack32),
diff --git a/src/SharpEmu.Libs/Gpu/Images/ImageDescription.cs b/src/SharpEmu.Libs/Gpu/Images/ImageDescription.cs
index 3c45d77..ce58daa 100644
--- a/src/SharpEmu.Libs/Gpu/Images/ImageDescription.cs
+++ b/src/SharpEmu.Libs/Gpu/Images/ImageDescription.cs
@@ -131,8 +131,29 @@ public struct ImageDescription
// Return the matching mip level in the containing image, or -1 if none matches.
public readonly int FindMatchingMipLevel(in ImageDescription container)
{
- if (!IsCompatible(container) || TileMode != container.TileMode || Resources.Levels != 1 ||
- container.Resources.Layers == 0 || container.Resources.Levels > MaxLevels)
+ if (container.Resources.Levels > MaxLevels)
+ {
+ return -1;
+ }
+
+ for (uint level = 0; level < container.Resources.Levels; level++)
+ {
+ if (FindMatchingArraySlice(container, (int)level) >= 0)
+ {
+ return (int)level;
+ }
+ }
+
+ return -1;
+ }
+
+ // Return the matching slice in the containing mip level, or -1 if none matches.
+ public readonly int FindMatchingArraySlice(in ImageDescription container, int mip)
+ {
+ if (!IsCompatible(container) || TileMode != container.TileMode || Type != container.Type || IsVolume ||
+ Resources.Levels != 1 || Resources.Layers == 0 || container.Resources.Layers == 0 || mip < 0 ||
+ (uint)mip >= container.Resources.Levels || container.Resources.Levels > MaxLevels ||
+ !IsValidRange(Data) || !IsValidRange(container.Data) || Data.Address < container.Data.Address || Data.End > container.Data.End)
{
return -1;
}
@@ -143,95 +164,38 @@ public struct ImageDescription
return -1;
}
- var mip = -1;
- for (uint level = 0; level < container.Resources.Levels; level++)
- {
- var layout = container.MipLayout[(int)level];
- if (layout.Size == 0 || layout.Size % container.Resources.Layers != 0 || container.Data.Address > ulong.MaxValue - layout.Offset)
- {
- continue;
- }
-
- var mipBase = container.Data.Address + layout.Offset;
- var sliceSize = layout.Size / container.Resources.Layers;
- if (sliceSize == 0 || mipBase > ulong.MaxValue - layout.Size)
- {
- continue;
- }
-
- var mipEnd = mipBase + layout.Size;
- if (Data.Address >= mipBase && Data.Address < mipEnd && (Data.Address - mipBase) % sliceSize == 0)
- {
- mip = (int)level;
- break;
- }
- }
-
- if (mip < 0)
- {
- return -1;
- }
-
- if (Extent.Width != Math.Max(container.Extent.Width >> mip, 1) || Extent.Height != Math.Max(container.Extent.Height >> mip, 1))
- {
- return -1;
- }
-
- var mipDepth = Math.Max(container.Extent.Depth >> mip, 1);
- if (container.Type == GuestImageType.Color3D && Type == GuestImageType.Color2D)
- {
- if (Resources.Layers != mipDepth)
- {
- return -1;
- }
- }
- else if (Type != container.Type)
- {
- return -1;
- }
-
- return mip;
- }
-
- // Return the matching slice in the containing mip level, or -1 if none matches.
- public readonly int FindMatchingArraySlice(in ImageDescription container, int mip)
- {
- if (!IsCompatible(container) || Type != container.Type || mip < 0 || (uint)mip >= container.Resources.Levels ||
- container.Resources.Levels > MaxLevels || container.Resources.Layers == 0 || Data.Size == 0)
- {
- return -1;
- }
-
- if (Extent.Width != Math.Max(container.Extent.Width >> mip, 1) || Extent.Height != Math.Max(container.Extent.Height >> mip, 1))
- {
- return -1;
- }
-
var layout = container.MipLayout[mip];
- if (layout.Size == 0 || layout.Size % container.Resources.Layers != 0 || container.Data.Address > ulong.MaxValue - layout.Offset)
+ if (Extent.Width != Math.Max(container.Extent.Width >> mip, 1) || Extent.Height != Math.Max(container.Extent.Height >> mip, 1) ||
+ Extent.Depth != container.Extent.Depth || MipLayout[0].Pitch != layout.Pitch || MipLayout[0].Height != layout.Height ||
+ layout.Size == 0 || layout.Size % container.Resources.Layers != 0 ||
+ container.Data.Size % container.Resources.Layers != 0 || Data.Size % Resources.Layers != 0)
{
return -1;
}
- var sliceSize = layout.Size / container.Resources.Layers;
- if (sliceSize == 0 || Data.Size % sliceSize != 0)
+ // PS5 array slices contain the whole reversed mip chain, rather than
+ // storing all slices of one mip contiguously.
+ var sliceStride = container.Data.Size / container.Resources.Layers;
+ var childStride = Data.Size / Resources.Layers;
+ if (childStride != layout.Size / container.Resources.Layers || layout.Offset >= sliceStride ||
+ childStride > sliceStride - layout.Offset || (Resources.Layers > 1 && childStride != sliceStride))
{
return -1;
}
- var mipBase = container.Data.Address + layout.Offset;
- if (Data.Address < mipBase)
+ var addressDelta = Data.Address - container.Data.Address;
+ if (addressDelta < layout.Offset || (addressDelta - layout.Offset) % sliceStride != 0)
{
return -1;
}
- var delta = Data.Address - mipBase;
- if (delta % Data.Size != 0 || delta / Data.Size > int.MaxValue)
+ var layer = (addressDelta - layout.Offset) / sliceStride;
+ if (layer > int.MaxValue || layer >= container.Resources.Layers || Resources.Layers > container.Resources.Layers - layer)
{
return -1;
}
- return (int)(delta / Data.Size);
+ return (int)layer;
}
public readonly bool IsSupportedDepthTarget
diff --git a/src/SharpEmu.Libs/Gpu/Images/ImageRequestBuilders.ColorTarget.cs b/src/SharpEmu.Libs/Gpu/Images/ImageRequestBuilders.ColorTarget.cs
index 816d7fc..1be295f 100644
--- a/src/SharpEmu.Libs/Gpu/Images/ImageRequestBuilders.ColorTarget.cs
+++ b/src/SharpEmu.Libs/Gpu/Images/ImageRequestBuilders.ColorTarget.cs
@@ -122,6 +122,17 @@ public static partial class ImageRequestBuilders
}
var depth = volume ? words.Depth + 1 : 1;
+ // Some Prospero titles encode a full 3D attachment with SliceMax as
+ // the exclusive depth (one past the last slice). The hardware accepts
+ // that form, while Vulkan needs the actual slice count for a 2D-array
+ // view. Normalize only this exact one-past range; other invalid views
+ // remain rejected below.
+ if (volume && view.BaseLayer == 0 && view.LayerCount == depth + 1)
+ {
+ view = new TargetViewRange(0, depth, depth);
+ Console.Error.WriteLine(
+ $"[LOADER][INFO] normalized an exclusive 3D color-target slice range to depth={depth}");
+ }
var tileMode = words.TileMode;
var standard4 = tileMode == GuestTileMode.Standard4KB;
var standard64 = tileMode == GuestTileMode.Standard64KB;
diff --git a/src/SharpEmu.Libs/Gpu/Images/ImageRequestBuilders.Texture.cs b/src/SharpEmu.Libs/Gpu/Images/ImageRequestBuilders.Texture.cs
index 9212e10..82e9a4f 100644
--- a/src/SharpEmu.Libs/Gpu/Images/ImageRequestBuilders.Texture.cs
+++ b/src/SharpEmu.Libs/Gpu/Images/ImageRequestBuilders.Texture.cs
@@ -240,7 +240,8 @@ public static partial class ImageRequestBuilders
{
pixelFormat = depthFormat.DepthAttachmentFormat;
}
- var storageViewFormat = storage && format == GuestPixelFormat.Bits32SInt ? Format.R32Uint : ViewFormatRules.SrgbStorageFormat(pixelFormat);
+ // Atomic storage images are declared as UINT in SPIR-V, including float atomics.
+ var storageViewFormat = storage && (shape.Atomic || format == GuestPixelFormat.Bits32SInt) ? Format.R32Uint : ViewFormatRules.SrgbStorageFormat(pixelFormat);
var viewFormat = storage && storageViewFormat != Format.Undefined ? storageViewFormat : pixelFormat;
var blockBytes = GuestPixelFormats.BlockCompressedBytes(format);
var description = ImageDescription.Create();
diff --git a/src/SharpEmu.Libs/Gpu/Images/SamplerStore.cs b/src/SharpEmu.Libs/Gpu/Images/SamplerStore.cs
index c2f8e20..466023e 100644
--- a/src/SharpEmu.Libs/Gpu/Images/SamplerStore.cs
+++ b/src/SharpEmu.Libs/Gpu/Images/SamplerStore.cs
@@ -42,28 +42,30 @@ public enum SamplerBorderColor : uint
FromTable = 3,
}
-// One Vulkan sampler per distinct guest sampler descriptor, created on first use.
+// One Vulkan sampler per distinct guest sampler descriptor and view numeric class,
+// created on first use. The guest border color has no numeric class; Vulkan's has to
+// match the sampled view, and integer views cannot be filtered.
public sealed unsafe class SamplerStore : IDisposable
{
private readonly GpuDeviceInfo _device;
- private readonly Dictionary<(uint, uint, uint, uint), Sampler> _samplers = new();
+ private readonly Dictionary<(uint, uint, uint, uint, bool), Sampler> _samplers = new();
private readonly object _gate = new();
public SamplerStore(GpuDeviceInfo device) => _device = device;
public int Count => _samplers.Count;
- public Sampler GetSampler(in SamplerDescriptorWords words)
+ public Sampler GetSampler(in SamplerDescriptorWords words, bool integerView)
{
lock (_gate)
{
- var key = (words[0], words[1], words[2], words[3]);
+ var key = (words[0], words[1], words[2], words[3], integerView);
if (_samplers.TryGetValue(key, out var existing))
{
return existing;
}
- var sampler = CreateSampler(words);
+ var sampler = CreateSampler(words, integerView);
_samplers.Add(key, sampler);
return sampler;
}
@@ -96,10 +98,10 @@ public sealed unsafe class SamplerStore : IDisposable
_ => throw SubmissionScheduler.Fatal($"The sampler clamp mode is unknown: clamp={clamp}."),
};
- private Sampler CreateSampler(in SamplerDescriptorWords words)
+ private Sampler CreateSampler(in SamplerDescriptorWords words, bool integerView)
{
- var magnify = words.MagnifyFilter;
- var minify = words.MinifyFilter;
+ var magnify = integerView ? (uint)SamplerFilter.Point : words.MagnifyFilter;
+ var minify = integerView ? (uint)SamplerFilter.Point : words.MinifyFilter;
var anisotropic = IsAnisotropic(magnify) || IsAnisotropic(minify);
var anisotropyRatio = 1.0f;
if (anisotropic)
@@ -117,6 +119,11 @@ public sealed unsafe class SamplerStore : IDisposable
}
var mipFilter = (SamplerMipFilter)words.MipFilter;
+ if (integerView && mipFilter == SamplerMipFilter.Linear)
+ {
+ mipFilter = SamplerMipFilter.Point;
+ }
+
var minLod = 0.0f;
var maxLod = 0.0f;
if (mipFilter != SamplerMipFilter.None)
@@ -125,26 +132,21 @@ public sealed unsafe class SamplerStore : IDisposable
maxLod = words.MaxLod / 256.0f;
}
- BorderColor border;
- switch ((SamplerBorderColor)words.BorderColorType)
+ var borderType = (SamplerBorderColor)words.BorderColorType;
+ if (borderType == SamplerBorderColor.FromTable)
{
- case SamplerBorderColor.TransparentBlack:
- border = BorderColor.IntTransparentBlack;
- break;
- case SamplerBorderColor.OpaqueBlack:
- border = BorderColor.IntOpaqueBlack;
- break;
- case SamplerBorderColor.OpaqueWhite:
- border = BorderColor.IntOpaqueWhite;
- break;
- case SamplerBorderColor.FromTable:
- Console.Error.WriteLine($"[LOADER][WARN] A table border color is approximated as transparent black: index={words.BorderColorIndex}.");
- border = BorderColor.IntTransparentBlack;
- break;
- default:
- throw SubmissionScheduler.Fatal($"The border color type is unknown: type={words.BorderColorType}.");
+ Console.Error.WriteLine($"[LOADER][WARN] A table border color is approximated as transparent black: index={words.BorderColorIndex}.");
+ borderType = SamplerBorderColor.TransparentBlack;
}
+ var border = borderType switch
+ {
+ SamplerBorderColor.TransparentBlack => integerView ? BorderColor.IntTransparentBlack : BorderColor.FloatTransparentBlack,
+ SamplerBorderColor.OpaqueBlack => integerView ? BorderColor.IntOpaqueBlack : BorderColor.FloatOpaqueBlack,
+ SamplerBorderColor.OpaqueWhite => integerView ? BorderColor.IntOpaqueWhite : BorderColor.FloatOpaqueWhite,
+ _ => throw SubmissionScheduler.Fatal($"The border color type is unknown: type={words.BorderColorType}."),
+ };
+
var info = new SamplerCreateInfo
{
SType = StructureType.SamplerCreateInfo,
diff --git a/src/SharpEmu.Libs/Gpu/Images/TargetRegisterWords.cs b/src/SharpEmu.Libs/Gpu/Images/TargetRegisterWords.cs
index 592d979..1b3fbb9 100644
--- a/src/SharpEmu.Libs/Gpu/Images/TargetRegisterWords.cs
+++ b/src/SharpEmu.Libs/Gpu/Images/TargetRegisterWords.cs
@@ -106,10 +106,10 @@ public readonly record struct DisplaySurfaceWords(
bool Compressed);
// The shape the shader module was compiled with; the view type must match it.
-public readonly record struct ShaderImageShape(bool Volume, bool Arrayed, bool Cube, bool Storage, bool DynamicMip, TextureNumericClass NumericClass, bool OneDimensional = false, bool R128 = false, bool Multisampled = false, bool DepthCompare = false)
+public readonly record struct ShaderImageShape(bool Volume, bool Arrayed, bool Cube, bool Storage, bool DynamicMip, TextureNumericClass NumericClass, bool OneDimensional = false, bool R128 = false, bool Multisampled = false, bool DepthCompare = false, bool Atomic = false)
{
public ShaderImageShape(bool volume, bool arrayed, bool storage, bool dynamicMip, TextureNumericClass numericClass)
- : this(volume, arrayed, false, storage, dynamicMip, numericClass, false, false, false)
+ : this(volume, arrayed, false, storage, dynamicMip, numericClass, false, false, false, false, false)
{
}
}
diff --git a/src/SharpEmu.Libs/Gpu/Images/TileGeometry.Blocks.cs b/src/SharpEmu.Libs/Gpu/Images/TileGeometry.Blocks.cs
index 3d1e646..4485662 100644
--- a/src/SharpEmu.Libs/Gpu/Images/TileGeometry.Blocks.cs
+++ b/src/SharpEmu.Libs/Gpu/Images/TileGeometry.Blocks.cs
@@ -173,63 +173,57 @@ public static partial class TileGeometry
switch (bytesPerElement)
{
case 1:
- offset ^= (y << 2) & 0x0008;
- offset ^= (y << 4) & 0x0010;
- offset ^= (y << 3) & 0x00a0;
- offset ^= (y << 5) & 0x0f00;
- offset ^= (y << 6) & 0x1000;
- offset ^= (y << 7) & 0x4000;
- offset ^= x & 0x0007;
- offset ^= (x << 3) & 0x0040;
- offset ^= (x << 5) & 0x0300;
- offset ^= (x << 4) & 0x0400;
- offset ^= (x << 6) & 0x0800;
- offset ^= (x << 7) & 0x2000;
- offset ^= (x << 8) & 0x8000;
+ offset ^= Bit(x, 0, 0) ^ Bit(x, 1, 1) ^ Bit(x, 2, 2) ^ Bit(x, 3, 3);
+ offset ^= Bit(y, 0, 4) ^ Bit(y, 1, 5) ^ Bit(y, 2, 6) ^ Bit(y, 3, 7);
+ offset ^= Bit(x, 7, 8) ^ Bit(y, 4, 8) ^ Bit(y, 7, 8);
+ offset ^= Bit(x, 4, 9) ^ Bit(y, 4, 9);
+ offset ^= Bit(x, 6, 10) ^ Bit(y, 5, 10);
+ offset ^= Bit(x, 5, 11) ^ Bit(y, 6, 11);
+ offset ^= Bit(x, 6, 12) ^ Bit(y, 6, 13);
+ offset ^= Bit(x, 7, 14) ^ Bit(y, 8, 14);
+ offset ^= Bit(x, 8, 15) ^ Bit(y, 7, 15);
return offset;
case 2:
- offset ^= (y << 4) & 0x0070;
- offset ^= (y << 5) & 0x0f00;
- offset ^= (y << 8) & 0x5000;
- offset ^= (x << 1) & 0x000e;
- offset ^= (x << 4) & 0x0480;
- offset ^= (x << 5) & 0x0300;
- offset ^= (x << 6) & 0x0800;
- offset ^= (x << 7) & 0x2000;
- offset ^= (x << 8) & 0x8000;
+ offset ^= Bit(x, 0, 1) ^ Bit(x, 1, 2) ^ Bit(x, 2, 3);
+ offset ^= Bit(y, 0, 4) ^ Bit(y, 1, 5) ^ Bit(y, 2, 6) ^ Bit(x, 3, 7);
+ offset ^= Bit(x, 7, 8) ^ Bit(y, 4, 8) ^ Bit(y, 7, 8);
+ offset ^= Bit(x, 4, 9) ^ Bit(y, 4, 9);
+ offset ^= Bit(x, 6, 10) ^ Bit(y, 5, 10);
+ offset ^= Bit(x, 5, 11) ^ Bit(y, 6, 11);
+ offset ^= Bit(y, 3, 12) ^ Bit(x, 6, 13);
+ offset ^= Bit(x, 7, 14) ^ Bit(y, 7, 14);
+ offset ^= Bit(x, 8, 15) ^ Bit(y, 6, 15);
return offset;
case 4:
- offset ^= (y << 4) & 0x0070;
- offset ^= (y << 5) & 0x0f00;
- offset ^= (y << 9) & 0x1000;
- offset ^= (y << 8) & 0x4000;
- offset ^= (x << 2) & 0x000c;
- offset ^= (x << 5) & 0x0380;
- offset ^= (x << 4) & 0x0400;
- offset ^= (x << 6) & 0x0800;
- offset ^= (x << 9) & 0xa000;
+ offset ^= Bit(x, 0, 2) ^ Bit(x, 1, 3);
+ offset ^= Bit(y, 0, 4) ^ Bit(y, 1, 5) ^ Bit(x, 2, 6) ^ Bit(y, 2, 7);
+ offset ^= Bit(x, 7, 8) ^ Bit(y, 4, 8) ^ Bit(y, 7, 8);
+ offset ^= Bit(x, 4, 9) ^ Bit(y, 4, 9);
+ offset ^= Bit(x, 6, 10) ^ Bit(y, 5, 10);
+ offset ^= Bit(x, 5, 11) ^ Bit(y, 6, 11);
+ offset ^= Bit(x, 3, 12) ^ Bit(y, 3, 13);
+ offset ^= Bit(x, 6, 14) ^ Bit(y, 7, 14);
+ offset ^= Bit(x, 7, 15) ^ Bit(y, 6, 15);
return offset;
case 8:
- offset ^= (y << 4) & 0x0010;
- offset ^= (y << 6) & 0x0080;
- offset ^= (y << 5) & 0x0f00;
- offset ^= (y << 10) & 0x5000;
- offset ^= (x << 3) & 0x0008;
- offset ^= (x << 4) & 0x0460;
- offset ^= (x << 5) & 0x0300;
- offset ^= (x << 6) & 0x0800;
- offset ^= (x << 10) & 0x2000;
- offset ^= (x << 9) & 0x8000;
+ offset ^= Bit(x, 0, 3) ^ Bit(y, 0, 4) ^ Bit(x, 1, 5) ^ Bit(x, 2, 6) ^ Bit(y, 1, 7);
+ offset ^= Bit(x, 7, 8) ^ Bit(y, 4, 8) ^ Bit(y, 7, 8);
+ offset ^= Bit(x, 4, 9) ^ Bit(y, 4, 9);
+ offset ^= Bit(x, 6, 10) ^ Bit(y, 5, 10);
+ offset ^= Bit(x, 5, 11) ^ Bit(y, 6, 11);
+ offset ^= Bit(y, 2, 12) ^ Bit(x, 3, 13);
+ offset ^= Bit(x, 7, 14) ^ Bit(y, 3, 14);
+ offset ^= Bit(x, 6, 15) ^ Bit(y, 6, 15);
return offset;
case 16:
- offset ^= (x << 4) & 0x0410;
- offset ^= (x << 5) & 0x0340;
- offset ^= (x << 6) & 0x0800;
- offset ^= (x << 11) & 0xa000;
- offset ^= (y << 5) & 0x0f20;
- offset ^= (y << 6) & 0x0080;
- offset ^= (y << 10) & 0x1000;
- offset ^= (y << 11) & 0x4000;
+ offset ^= Bit(x, 0, 4) ^ Bit(y, 0, 5) ^ Bit(x, 1, 6) ^ Bit(y, 1, 7);
+ offset ^= Bit(x, 7, 8) ^ Bit(y, 4, 8) ^ Bit(y, 7, 8);
+ offset ^= Bit(x, 4, 9) ^ Bit(y, 4, 9);
+ offset ^= Bit(x, 6, 10) ^ Bit(y, 5, 10);
+ offset ^= Bit(x, 5, 11) ^ Bit(y, 6, 11);
+ offset ^= Bit(x, 2, 12) ^ Bit(y, 2, 13);
+ offset ^= Bit(x, 6, 14) ^ Bit(y, 3, 14);
+ offset ^= Bit(x, 3, 15) ^ Bit(y, 6, 15);
return offset;
default:
throw SubmissionScheduler.Fatal($"The render-target tile block does not support this element size: bytes={bytesPerElement}.");
diff --git a/src/SharpEmu.Libs/Gpu/Images/ViewFormatRules.cs b/src/SharpEmu.Libs/Gpu/Images/ViewFormatRules.cs
index f2a81c3..bd51d5c 100644
--- a/src/SharpEmu.Libs/Gpu/Images/ViewFormatRules.cs
+++ b/src/SharpEmu.Libs/Gpu/Images/ViewFormatRules.cs
@@ -130,6 +130,19 @@ public static class ViewFormatRules
_ => None,
};
+ // Integer views sample raw integers: no filtering, integer border colors.
+ public static bool IsIntegerFormat(Format format) => format is
+ Format.R8Uint or Format.R8Sint or Format.R8G8Uint or Format.R8G8Sint or
+ Format.R8G8B8A8Uint or Format.R8G8B8A8Sint or Format.B8G8R8A8Uint or Format.B8G8R8A8Sint or
+ Format.A8B8G8R8UintPack32 or Format.A8B8G8R8SintPack32 or
+ Format.A2B10G10R10UintPack32 or Format.A2B10G10R10SintPack32 or
+ Format.A2R10G10B10UintPack32 or Format.A2R10G10B10SintPack32 or
+ Format.R16Uint or Format.R16Sint or Format.R16G16Uint or Format.R16G16Sint or
+ Format.R16G16B16A16Uint or Format.R16G16B16A16Sint or
+ Format.R32Uint or Format.R32Sint or Format.R32G32Uint or Format.R32G32Sint or
+ Format.R32G32B32Uint or Format.R32G32B32Sint or Format.R32G32B32A32Uint or Format.R32G32B32A32Sint or
+ Format.S8Uint;
+
// The bytes of one texel block of a host format; 0 for a format outside the compatibility classes.
public static uint BlockBytes(Format format)
{
diff --git a/src/SharpEmu.Libs/Gpu/Metal/MetalCommandStreamHost.Rendering.cs b/src/SharpEmu.Libs/Gpu/Metal/MetalCommandStreamHost.Rendering.cs
index c84cc52..d456471 100644
--- a/src/SharpEmu.Libs/Gpu/Metal/MetalCommandStreamHost.Rendering.cs
+++ b/src/SharpEmu.Libs/Gpu/Metal/MetalCommandStreamHost.Rendering.cs
@@ -164,6 +164,9 @@ internal sealed partial class MetalCommandStreamHost : IRenderHost, IShaderPipel
bool IShaderPipelineHost.GraphicsSubgroupOperationsEnabled => true;
+ // Metal has no 64-bit workgroup atomics here; keep the non-atomic 32-bit pair.
+ bool IShaderPipelineHost.SharedInt64AtomicsEnabled => false;
+
RenderHostLimits IShaderPipelineHost.Limits => new(MaxDimension, MaxDimension, MaxDimension, MaxDimension);
SampleCountFlags IShaderPipelineHost.NoAttachmentSampleCounts =>
@@ -174,6 +177,9 @@ internal sealed partial class MetalCommandStreamHost : IRenderHost, IShaderPipel
bool IShaderPipelineHost.TryReadCleanGuestWord(ulong address, out uint word) => TryReadWord(address, out word);
+ bool IShaderPipelineHost.TryReadResidentGuestBytes(ulong address, Span destination, bool clean) =>
+ Memory.TryRead(address, destination);
+
private bool TryReadWord(ulong address, out uint word)
{
Span bytes = stackalloc byte[sizeof(uint)];
diff --git a/src/SharpEmu.Libs/Gpu/Pipelines/ConstantFillDetector.cs b/src/SharpEmu.Libs/Gpu/Pipelines/ConstantFillDetector.cs
new file mode 100644
index 0000000..bc1f494
--- /dev/null
+++ b/src/SharpEmu.Libs/Gpu/Pipelines/ConstantFillDetector.cs
@@ -0,0 +1,68 @@
+// Copyright (C) 2026 SharpEmu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+using SharpEmu.ShaderCompiler;
+
+namespace SharpEmu.Libs.Gpu.Pipelines;
+
+// A compute program that stores one dword, read from a constant buffer, into every
+// record of a buffer: the element index is group * 64 + thread.
+// v_lshl_add_u32 v0, s, 6, v0
+// s_buffer_load_dword s, s[:+3], 0
+// s_waitcnt
+// v_mov_b32 v1, s
+// buffer_store_format_x v1, v0, s[:+3], 0 idxen
+// s_endpgm
+// Unreal clears DCC metadata this way, so the dispatch must reach the image cache as a
+// clear; the metadata is not emulated and the stores alone would leave the image stale.
+public sealed record ConstantFill(uint GroupScalarRegister, uint DestinationScalarResource, uint SourceScalarResource);
+
+public static class ConstantFillDetector
+{
+ private const uint Shift64 = 134; // inline constant 6
+ private const uint ZeroConstant = 128;
+ private const uint NullScalar = 125;
+
+ public static ConstantFill? Detect(Gen5ShaderProgram program)
+ {
+ var instructions = program.Instructions.Where(instruction => instruction.Opcode != "SWaitcnt").ToArray();
+ if (instructions.Length != 5)
+ {
+ return null;
+ }
+
+ var index = instructions[0];
+ if (index.Opcode != "VLshlAddU32" || index.Destinations is not [{ Kind: Gen5OperandKind.VectorRegister } indexRegister] ||
+ index.Sources is not [{ Kind: Gen5OperandKind.ScalarRegister } group, { Kind: Gen5OperandKind.EncodedConstant, Value: Shift64 },
+ { Kind: Gen5OperandKind.VectorRegister } thread] || thread.Value != indexRegister.Value)
+ {
+ return null;
+ }
+
+ var load = instructions[1];
+ if (load.Opcode != "SBufferLoadDword" || load.Control is not Gen5ScalarMemoryControl { ImmediateOffsetBytes: 0, DynamicOffsetRegister: null } ||
+ load.Destinations is not [{ Kind: Gen5OperandKind.ScalarRegister } value] ||
+ load.Sources is not [{ Kind: Gen5OperandKind.ScalarRegister } source, { Kind: Gen5OperandKind.EncodedConstant, Value: NullScalar or ZeroConstant }])
+ {
+ return null;
+ }
+
+ var move = instructions[2];
+ if (move.Opcode != "VMovB32" || move.Destinations is not [{ Kind: Gen5OperandKind.VectorRegister } data] ||
+ move.Sources is not [{ Kind: Gen5OperandKind.ScalarRegister } moved] || moved.Value != value.Value)
+ {
+ return null;
+ }
+
+ var store = instructions[3];
+ if (store.Opcode != "BufferStoreFormatX" ||
+ store.Control is not Gen5BufferMemoryControl { DwordCount: 1, OffsetBytes: 0, IndexEnabled: true, OffsetEnabled: false, Typed: false } control ||
+ control.VectorAddress != indexRegister.Value || control.VectorData != data.Value ||
+ store.Sources is not [_, _, { Kind: Gen5OperandKind.EncodedConstant, Value: ZeroConstant }])
+ {
+ return null;
+ }
+
+ return instructions[4].Opcode == "SEndpgm" ? new ConstantFill(group.Value, control.ScalarResource, source.Value) : null;
+ }
+}
diff --git a/src/SharpEmu.Libs/Gpu/Pipelines/IShaderPipelineHost.cs b/src/SharpEmu.Libs/Gpu/Pipelines/IShaderPipelineHost.cs
index aac1816..e5f0a20 100644
--- a/src/SharpEmu.Libs/Gpu/Pipelines/IShaderPipelineHost.cs
+++ b/src/SharpEmu.Libs/Gpu/Pipelines/IShaderPipelineHost.cs
@@ -38,6 +38,9 @@ internal interface IShaderPipelineHost
bool GraphicsSubgroupOperationsEnabled { get; }
+ // 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 PerVertexPixelInputsSupported => true;
RenderHostLimits Limits { get; }
@@ -53,6 +56,11 @@ internal interface IShaderPipelineHost
// Reads one guest dword only when no GPU work may still own the range.
bool TryReadCleanGuestWord(ulong address, out uint word);
+ // Copies guest bytes the CPU already holds, without synchronizing. False when the GPU
+ // may own the range (or, for a clean read, when a clean word read would be refused);
+ // the resource cache then re-materializes instead of trusting a stale copy.
+ bool TryReadResidentGuestBytes(ulong address, Span destination, bool clean) => false;
+
// Creates the host module of one compiled permutation and returns its handle.
ulong CreateShaderModule(IGuestCompiledShader shader, ShaderStage stage, ulong hash, ulong programId);
diff --git a/src/SharpEmu.Libs/Gpu/Pipelines/PipelineCacheSignature.cs b/src/SharpEmu.Libs/Gpu/Pipelines/PipelineCacheSignature.cs
index 27d8aed..1421bc5 100644
--- a/src/SharpEmu.Libs/Gpu/Pipelines/PipelineCacheSignature.cs
+++ b/src/SharpEmu.Libs/Gpu/Pipelines/PipelineCacheSignature.cs
@@ -43,19 +43,44 @@ public static class PipelineCacheSignature
public static bool TryUnwrap(string signature, ReadOnlySpan file, out byte[] payload)
{
payload = [];
- var prefix = Encoding.ASCII.GetBytes(signature);
- if (file.Length < prefix.Length + sizeof(ulong) || file.Length > int.MaxValue)
+ if (file.Length > int.MaxValue)
{
return false;
}
- if (!file[..prefix.Length].SequenceEqual(prefix))
+ var newline = file.IndexOf((byte)'\n');
+ if (newline < 0)
{
return false;
}
- var expectedHash = BinaryPrimitives.ReadUInt64LittleEndian(file[prefix.Length..]);
- var data = file[(prefix.Length + sizeof(ulong))..];
+ var actualSignature = Encoding.ASCII.GetString(file[..(newline + 1)]);
+ if (!string.Equals(actualSignature, signature, StringComparison.Ordinal))
+ {
+ // Pipeline binaries are keyed by the Vulkan device and driver;
+ // the SharpEmu build version only controls which shader records
+ // may be present. A newer build can safely reuse an older driver
+ // cache because Vulkan validates each pipeline key and ignores
+ // stale records. Keep the cache when the hardware suffix matches.
+ var expectedParts = signature.TrimEnd('\n').Split(':');
+ var actualParts = actualSignature.TrimEnd('\n').Split(':');
+ if (expectedParts.Length != actualParts.Length ||
+ expectedParts.Length < 6 ||
+ !string.Equals(expectedParts[0], actualParts[0], StringComparison.Ordinal) ||
+ !expectedParts[2..].SequenceEqual(actualParts[2..], StringComparer.Ordinal))
+ {
+ return false;
+ }
+ }
+
+ var hashOffset = newline + 1;
+ if (file.Length < hashOffset + sizeof(ulong))
+ {
+ return false;
+ }
+
+ var expectedHash = BinaryPrimitives.ReadUInt64LittleEndian(file[hashOffset..]);
+ var data = file[(hashOffset + sizeof(ulong))..];
if (XxHash3.HashToUInt64(data) != expectedHash)
{
return false;
diff --git a/src/SharpEmu.Libs/Gpu/Pipelines/ShaderPermutationDump.cs b/src/SharpEmu.Libs/Gpu/Pipelines/ShaderPermutationDump.cs
index d2143c5..a74faff 100644
--- a/src/SharpEmu.Libs/Gpu/Pipelines/ShaderPermutationDump.cs
+++ b/src/SharpEmu.Libs/Gpu/Pipelines/ShaderPermutationDump.cs
@@ -15,7 +15,7 @@ internal static class ShaderPermutationDump
private static long _sequence;
public static Action? CreateFailureCapture(ShaderSource source) =>
- CompiledShaderDump.ShouldWrite(source.Address) ? CreateEnabledFailureCapture(source) : null;
+ CompiledShaderDump.ShouldWrite(source.Address, source.Hash) ? CreateEnabledFailureCapture(source) : null;
// Keep captured shader state out of the disabled per-draw path.
private static Action CreateEnabledFailureCapture(ShaderSource source) =>
@@ -32,7 +32,7 @@ internal static class ShaderPermutationDump
uint pushDataCursor,
ulong programId)
{
- if (!CompiledShaderDump.ShouldWrite(source.Address)) return null;
+ if (!CompiledShaderDump.ShouldWrite(source.Address, source.Hash)) return null;
try
{
var sequence = Interlocked.Increment(ref _sequence);
@@ -90,7 +90,7 @@ internal static class ShaderPermutationDump
public static void WriteIndirectImageFailure(ShaderSource source, IndirectImageFailure failure)
{
- if (!CompiledShaderDump.ShouldWrite(source.Address)) return;
+ if (!CompiledShaderDump.ShouldWrite(source.Address, source.Hash)) return;
try
{
var sequence = Interlocked.Increment(ref _sequence);
diff --git a/src/SharpEmu.Libs/Gpu/Pipelines/ShaderPipelineCache.cs b/src/SharpEmu.Libs/Gpu/Pipelines/ShaderPipelineCache.cs
index 8660f20..43a8a09 100644
--- a/src/SharpEmu.Libs/Gpu/Pipelines/ShaderPipelineCache.cs
+++ b/src/SharpEmu.Libs/Gpu/Pipelines/ShaderPipelineCache.cs
@@ -384,6 +384,7 @@ internal sealed partial class ShaderPipelineCache : IShaderPipelineProvider
RenderTrace.Write(
$"PipelineCache create graphics vertex=0x{vertexProgram.Id:X16} pixel=0x{key.PixelProgramId:X16} colors={key.Rendering.ColorCount} " +
$"depth={description.StaticParameters.WithDepth} samples={description.StaticParameters.Samples}");
+
}
var created = _host.CreateGraphicsPipeline(description);
@@ -443,7 +444,15 @@ internal sealed partial class ShaderPipelineCache : IShaderPipelineProvider
// host output would otherwise write an undefined value (e.g. depth-only passes that
// leave a color target bound and export only to the null target).
var exported = pixelStage is null || ((pixelStage.PixelColorExportMasks >> (int)(color.Slot * 4)) & 0xFu) != 0;
- parameters.SetColorMask(index, exported ? color.Resolution.ExportMapping.ApplyMask(context.RenderTargetMaskForSlot(color.Slot)) : 0);
+ var colorMask = exported ? color.Resolution.ExportMapping.ApplyMask(context.RenderTargetMaskForSlot(color.Slot)) : 0;
+ parameters.SetColorMask(index, colorMask);
+ if (RenderTrace.Enabled && RenderTrace.Pipeline())
+ {
+ RenderTrace.Write(
+ $"PipelineCache output slot={color.Slot} guestMask=0x{context.RenderTargetMaskForSlot(color.Slot):X} " +
+ $"exported={(exported ? 1 : 0)} shaderMask=0x{pixelStage?.PixelColorExportMasks ?? 0:X8} " +
+ $"mapping=0x{color.Resolution.ExportMapping.Packed:X2} hostMask=0x{colorMask:X} format={(int)format}");
+ }
}
var withDepth = depth.HasTarget;
diff --git a/src/SharpEmu.Libs/Gpu/Pipelines/ShaderPlanningDump.cs b/src/SharpEmu.Libs/Gpu/Pipelines/ShaderPlanningDump.cs
index 2debf3e..7f73a85 100644
--- a/src/SharpEmu.Libs/Gpu/Pipelines/ShaderPlanningDump.cs
+++ b/src/SharpEmu.Libs/Gpu/Pipelines/ShaderPlanningDump.cs
@@ -67,7 +67,7 @@ internal static class ShaderPlanningDump
private static void Write(ShaderSource source, string suffix, Action write)
{
- if (!CompiledShaderDump.ShouldWrite(source.Address)) return;
+ if (!CompiledShaderDump.ShouldWrite(source.Address, source.Hash)) return;
try
{
var path = $"{CompiledShaderDump.GetBasePath(source.Label, source.Address, source.Hash)}.{suffix}";
diff --git a/src/SharpEmu.Libs/Gpu/Pipelines/ShaderProgramCache.cs b/src/SharpEmu.Libs/Gpu/Pipelines/ShaderProgramCache.cs
index ef29e5f..0f8c87b 100644
--- a/src/SharpEmu.Libs/Gpu/Pipelines/ShaderProgramCache.cs
+++ b/src/SharpEmu.Libs/Gpu/Pipelines/ShaderProgramCache.cs
@@ -85,6 +85,7 @@ internal sealed class ProgramSourceEntry
public required ShaderResourcePlan Plan { get; init; }
public required Gen5ShaderProgram Program { get; init; }
public required bool HasBitwiseExclusiveOr { get; init; }
+ public ConstantFill? ConstantFill { get; init; }
public EmbeddedVertexFetchPlan? EmbeddedFetch { get; init; }
public ShaderVertexInput[] VertexInputs { get; init; } = [];
public List Permutations { get; } = new(8);
@@ -102,8 +103,14 @@ internal sealed class ShaderProgramCache
private readonly Dictionary _programs = new();
private readonly Dictionary<(ulong Hash, uint CodeSize), Gen5ShaderProgram> _decoded = new();
private readonly List _staticState = new(StageStaticKey.MaxWords);
+ // Draws that re-bind unchanged resources reuse the last materialization.
+ // SHARPEMU_RESOURCE_CACHE=0 materializes every draw, for A/B comparisons.
+ private readonly ResourceMaterializationCache? _materializations =
+ Environment.GetEnvironmentVariable("SHARPEMU_RESOURCE_CACHE") == "0" ? null : new();
private ulong _nextProgramId;
+ public ResourceMaterializationCache? Materializations => _materializations;
+
public ShaderProgramCache(CpuContext context, IGuestGpuBackend compiler, IShaderPipelineHost host)
{
_context = context;
@@ -211,9 +218,15 @@ internal sealed class ShaderProgramCache
var captureIndirectImageFailure = ShaderPermutationDump.CreateFailureCapture(source);
using (RenderPhaseProfile.MeasureDetail(RenderPhaseProfile.Phase.ResourceMaterialization))
{
- if (!ResourceMaterializer.Materialize(entry.Plan, inputs, ref snapshot, ref specialization, out var materializationFailure, captureIndirectImageFailure))
+ // Failure capture needs the full walk, so a dump run bypasses the cache.
+ var materialized = _materializations is not null && captureIndirectImageFailure is null
+ ? _materializations.Materialize(entry.Plan, inputs, _host.TryReadResidentGuestBytes, ref snapshot, ref specialization,
+ out var materializationFailure)
+ : ResourceMaterializer.Materialize(entry.Plan, inputs, ref snapshot, ref specialization, out materializationFailure,
+ captureIndirectImageFailure);
+ if (!materialized)
{
- var message = $"The shader resources could not be materialized: stage={source.Label} hash=0x{source.Hash:X16} shader=0x{source.Address:X16}.";
+ var message = $"The shader resources could not be materialized: stage={source.Label} hash=0x{source.Hash:X16} shader=0x{source.Address:X16} reason={materializationFailure}.";
if (materializationFailure is ResourceMaterializationFailure.IncompatibleImageCandidates or ResourceMaterializationFailure.ImageCapacityExceeded)
throw new ShaderProgramRejectedException(message);
throw SubmissionScheduler.Fatal(message);
@@ -283,7 +296,7 @@ internal sealed class ShaderProgramCache
private ProgramSourceEntry CreateEntry(ShaderSource source, StageCompileOptions options)
{
var program = Decode(source);
- var dumpPlanning = CompiledShaderDump.ShouldWrite(source.Address);
+ var dumpPlanning = CompiledShaderDump.ShouldWrite(source.Address, source.Hash);
if (dumpPlanning) ShaderPlanningDump.WriteInput(source, program);
EmbeddedVertexFetchPlan? fetch = null;
ShaderVertexInput[] vertexInputs = [];
@@ -326,6 +339,7 @@ internal sealed class ShaderProgramCache
Plan = plan,
Program = program,
HasBitwiseExclusiveOr = exclusiveOr,
+ ConstantFill = source.Stage == ShaderStage.Compute ? ConstantFillDetector.Detect(program) : null,
EmbeddedFetch = fetch,
VertexInputs = vertexInputs,
};
@@ -521,6 +535,7 @@ internal sealed class ShaderProgramCache
BindingLayout layout)
{
var enableGraphicsSubgroups = _host.GraphicsSubgroupOperationsEnabled;
+ var sharedInt64Atomics = _host.SharedInt64AtomicsEnabled;
switch (source.Stage)
{
case ShaderStage.Vertex:
@@ -532,6 +547,7 @@ internal sealed class ShaderProgramCache
TraceDeviceAddressFaults = SharpEmu.HLE.GpuMemory.GuestGpuMemoryHook.TraceEnabled,
ScratchDwords = info.ScratchDwords,
EnableGraphicsSubgroupOperations = enableGraphicsSubgroups,
+ SupportsSharedInt64Atomics = sharedInt64Atomics,
RequiredVertexOutputCount = options.RequiredVertexOutputCount,
VertexInputs = entry.VertexInputs,
PositionExportControl = info.PositionExportControl,
@@ -557,6 +573,7 @@ internal sealed class ShaderProgramCache
TraceDeviceAddressFaults = SharpEmu.HLE.GpuMemory.GuestGpuMemoryHook.TraceEnabled,
ScratchDwords = info.ScratchDwords,
EnableGraphicsSubgroupOperations = enableGraphicsSubgroups,
+ SupportsSharedInt64Atomics = sharedInt64Atomics,
PixelOutputs = options.PixelOutputs,
PixelInputEnable = options.PixelInputEnable,
PixelCustomInterpolationMask = info.CustomInterpolationMask,
@@ -574,6 +591,7 @@ internal sealed class ShaderProgramCache
WaveSize = info.WaveSize,
TraceDeviceAddressFaults = SharpEmu.HLE.GpuMemory.GuestGpuMemoryHook.TraceEnabled,
ScratchDwords = info.ScratchDwords,
+ SupportsSharedInt64Atomics = sharedInt64Atomics,
ComputeSystemRegisters = options.ComputeSystemRegisters,
LocalSizeX = Math.Max(info.ThreadsX, 1),
LocalSizeY = Math.Max(info.ThreadsY, 1),
@@ -637,6 +655,7 @@ internal sealed class ShaderProgramCache
InstanceOffsetScalarRegister = entry.EmbeddedFetch?.InstanceOffsetScalarRegister ?? ShaderProgramInfo.NoScalarRegister,
UsesDeviceAddresses = info.UsesDeviceAddresses,
HasBitwiseExclusiveOr = entry.HasBitwiseExclusiveOr,
+ ConstantFill = entry.ConstantFill,
Buffers = buffers,
Images = images,
SamplerCount = info.Samplers.Count,
@@ -652,7 +671,7 @@ internal sealed class ShaderProgramCache
// Writes each compiled module and its decoded listing when the dump switch is on.
internal static class CompiledShaderDump
{
- internal static bool ShouldWrite(ulong shaderAddress)
+ internal static bool ShouldWrite(ulong shaderAddress, ulong shaderHash)
{
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_DUMP_SPIRV"), "1", StringComparison.Ordinal))
{
@@ -675,6 +694,27 @@ internal static class CompiledShaderDump
}
}
+ var hashFilter = Environment.GetEnvironmentVariable("SHARPEMU_DUMP_SPIRV_HASH");
+ if (!string.IsNullOrWhiteSpace(hashFilter))
+ {
+ foreach (var filter in hashFilter.Split(new[] { ',', ';' }, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries))
+ {
+ var span = filter.AsSpan();
+ if (span.StartsWith("0x", StringComparison.OrdinalIgnoreCase))
+ {
+ span = span[2..];
+ }
+
+ if (ulong.TryParse(span, System.Globalization.NumberStyles.HexNumber, System.Globalization.CultureInfo.InvariantCulture, out var filteredHash) &&
+ shaderHash == filteredHash)
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
return true;
}
@@ -692,7 +732,7 @@ internal static class CompiledShaderDump
public static void Write(string stage, ulong shaderAddress, ulong hash, IGuestCompiledShader shader, Gen5ShaderProgram program)
{
- if (shader.Payload.Length == 0 || !ShouldWrite(shaderAddress)) return;
+ if (shader.Payload.Length == 0 || !ShouldWrite(shaderAddress, hash)) return;
var basePath = GetBasePath(stage, shaderAddress, hash);
File.WriteAllBytes($"{basePath}.{shader.PayloadFileExtension}", shader.Payload);
var lines = new List(program.Instructions.Count + 2)
diff --git a/src/SharpEmu.Libs/Gpu/Pipelines/VertexInputResolver.cs b/src/SharpEmu.Libs/Gpu/Pipelines/VertexInputResolver.cs
index 24e532c..4265201 100644
--- a/src/SharpEmu.Libs/Gpu/Pipelines/VertexInputResolver.cs
+++ b/src/SharpEmu.Libs/Gpu/Pipelines/VertexInputResolver.cs
@@ -41,13 +41,17 @@ public static class VertexInputResolver
var attributes = new List();
VertexInputBuffer[] buffers = [];
- if (metadata.VertexAttributeRegister >= 0)
+ // A shader without input semantics never reads its vertex tables, which may be null.
+ if (metadata.VertexAttributeRegister >= 0 && metadata.InputSemantics.Length != 0)
{
var attributeTable = ReadTablePointer(userData, metadata.VertexAttributeRegister);
var bufferTable = ReadTablePointer(userData, metadata.VertexBufferRegister);
if (attributeTable == 0 || bufferTable == 0)
{
- throw SubmissionScheduler.Fatal($"The vertex table pointer is null: shader=0x{shader.CodeAddress:X16} attributes=0x{attributeTable:X16} buffers=0x{bufferTable:X16}.");
+ throw SubmissionScheduler.Fatal(
+ $"The vertex table pointer is null: shader=0x{shader.CodeAddress:X16} attributes=0x{attributeTable:X16} buffers=0x{bufferTable:X16} " +
+ $"attributeRegister={metadata.VertexAttributeRegister} bufferRegister={metadata.VertexBufferRegister} " +
+ $"user_data=[{string.Join(",", userData.ToArray().Select(word => $"{word:X8}"))}].");
}
attributes = ApplySemantics(context, metadata.InputSemantics, attributeTable, bufferTable, shader.CodeAddress);
diff --git a/src/SharpEmu.Libs/Gpu/Rendering/RenderExecutor.Compute.cs b/src/SharpEmu.Libs/Gpu/Rendering/RenderExecutor.Compute.cs
index c32885e..c94f419 100644
--- a/src/SharpEmu.Libs/Gpu/Rendering/RenderExecutor.Compute.cs
+++ b/src/SharpEmu.Libs/Gpu/Rendering/RenderExecutor.Compute.cs
@@ -3,6 +3,7 @@
using SharpEmu.Libs.Gpu.GpuCommands.Registers;
using SharpEmu.Libs.Gpu.Scheduling;
+using SharpEmu.ShaderCompiler.Vulkan;
using Silk.NET.Vulkan;
using ResourceSnapshot = SharpEmu.ShaderCompiler.Resources.ResourceSnapshot;
@@ -18,6 +19,7 @@ public sealed partial class RenderExecutor
private const uint DispatchInitiatorKnownMask = DispatchInitiatorBaseBits | DispatchInitiatorModifierBits;
private const uint ImageClearDispatchInitiator = 0x61;
private const uint ImageClearWaveSize = 64;
+ private const uint Format32UInt = 20;
private const uint ImageClearStride = 16;
private const uint ImageClearUserDataCount = 8;
@@ -82,6 +84,12 @@ public sealed partial class RenderExecutor
return;
}
+ if (indirectArgumentsAddress == 0 && TryConsumeConstantFill(input, groupsX, groupsY, groupsZ))
+ {
+ _host.ResetBindings();
+ return;
+ }
+
if (indirectArgumentsAddress == 0 && TryConsumeImageClear(input, groupsX, groupsY, groupsZ, dispatchInitiator))
{
_host.ResetBindings();
@@ -143,9 +151,25 @@ public sealed partial class RenderExecutor
}
_host.BindPipeline(PipelineBindPoint.Compute, in pipeline);
+ // The shader's local workgroup axes may have been remapped at compile time
+ // (see Gen5SpirvTranslator.ComputeWorkgroupAxisOrder) so the largest NUM_THREAD
+ // axis lands on a physical axis Vulkan actually allows it on. The dispatch group
+ // counts must be permuted the same way, or vkCmdDispatch would hand group counts
+ // for the wrong physical axis to the pipeline it built with the remapped sizes.
+ var axisOrder = Gen5SpirvTranslator.ComputeWorkgroupAxisOrder(
+ input.ThreadsX,
+ input.ThreadsY,
+ input.ThreadsZ);
+ var logicalGroups = new[] { groupsX, groupsY, groupsZ };
+ var physicalGroups = new uint[3];
+ for (var logical = 0; logical < 3; logical++)
+ {
+ physicalGroups[axisOrder[logical]] = logicalGroups[logical];
+ }
+
if (indirectArgumentsAddress == 0 || !_host.TryDispatchIndirect(indirectArgumentsAddress))
{
- _host.Dispatch(groupsX, groupsY, groupsZ);
+ _host.Dispatch(physicalGroups[0], physicalGroups[1], physicalGroups[2]);
}
_host.ShaderAccessBarrier();
}
@@ -276,6 +300,46 @@ public sealed partial class RenderExecutor
return new ComputeImageClear(descriptor, clear, size);
}
+ // A constant fill that covers a whole image or DCC metadata becomes a clear. Anything
+ // else, including a fill of plain buffer memory, runs as the guest wrote it.
+ private bool TryConsumeConstantFill(ComputeInputInfo input, uint groupsX, uint groupsY, uint groupsZ)
+ {
+ var program = input.Stage.Program!;
+ if (program.ConstantFill is not { } fill || program.UserDataBase != 0)
+ {
+ return false;
+ }
+
+ var userData = input.Stage.Resources.UserData;
+ if (fill.GroupScalarRegister != (uint)input.WorkgroupRegister || !input.GroupIdX || input.GroupIdY || input.GroupIdZ ||
+ input.ThreadsX != ImageClearWaveSize || input.ThreadsY != 1 || input.ThreadsZ != 1 || groupsY != 1 || groupsZ != 1 ||
+ fill.DestinationScalarResource + 4 > userData.Length || fill.SourceScalarResource + 4 > userData.Length)
+ {
+ return false;
+ }
+
+ var destination = BufferDescriptorWords.From(userData.AsSpan((int)fill.DestinationScalarResource, 4).ToArray());
+ var source = BufferDescriptorWords.From(userData.AsSpan((int)fill.SourceScalarResource, 4).ToArray());
+ Span valueBytes = stackalloc byte[sizeof(uint)];
+ if (destination.Format != Format32UInt || destination.Stride != sizeof(uint) || destination.SwizzleEnabled || destination.AddThreadId ||
+ (ulong)groupsX * input.ThreadsX != destination.RecordCount || source.RecordCount == 0 ||
+ !_host.TryReadGuest(source.Address, valueBytes))
+ {
+ return false;
+ }
+
+ var value = System.Buffers.Binary.BinaryPrimitives.ReadUInt32LittleEndian(valueBytes);
+ var size = (ulong)destination.RecordCount * sizeof(uint);
+ var consumed = _host.TryClearImageFromBuffer(destination.Address, size, value) ||
+ _host.TryAbsorbDccFill(destination.Address, size, value);
+ if (RenderTrace.Enabled && RenderTrace.MetadataClear())
+ {
+ RenderTrace.Write($"Constant fill: shader=0x{program.Hash:X16} address=0x{destination.Address:X16} size=0x{size:X} value=0x{value:X8} consumed={consumed}");
+ }
+
+ return consumed;
+ }
+
private bool TryConsumeImageClear(ComputeInputInfo input, uint groupsX, uint groupsY, uint groupsZ, uint dispatchInitiator)
{
if (TryDecodeImageClear(input, groupsX, groupsY, groupsZ, dispatchInitiator) is not { } clear)
diff --git a/src/SharpEmu.Libs/Gpu/Rendering/RenderExecutor.cs b/src/SharpEmu.Libs/Gpu/Rendering/RenderExecutor.cs
index ddfcaaa..57b7eb7 100644
--- a/src/SharpEmu.Libs/Gpu/Rendering/RenderExecutor.cs
+++ b/src/SharpEmu.Libs/Gpu/Rendering/RenderExecutor.cs
@@ -37,7 +37,16 @@ public enum GuestIndexType : uint
// Resolves draw and dispatch state from the register banks and records it through the host.
public sealed partial class RenderExecutor
{
- private const uint PrimitiveShaderStageMask = 0x02002000;
+ // Prospero uses both the full NGG mask and compact primitive-shader masks
+ // when the task/mesh front end has already folded the upper enable bits
+ // away. Little Nightmares also emits 0x2030 for a primitive path with no
+ // geometry shader; the remaining bits describe the fixed NGG setup.
+ // GS_W32_EN (bit 22) and VS_W32_EN (bit 23) only select the wave size;
+ // graphics stages always compile as wave32, so they do not change the path.
+ private const uint VgtShaderStagesWaveSizeBits = (1u << 22) | (1u << 23);
+
+ private static bool IsPrimitiveShaderStageMask(uint stages) =>
+ (stages & ~VgtShaderStagesWaveSizeBits) is 0x02002000 or 0x00002000 or 0x00002030;
private const uint MaxOutputPerSubgroupLimit = 0x40;
private static int _geometryWarningShown;
@@ -400,15 +409,16 @@ public sealed partial class RenderExecutor
var vertex = banks.Shader.Vertex;
var stages = context.ShaderStages;
var primitiveShaderVertexPath =
- stages == PrimitiveShaderStageMask && vertex.ExportAddress != 0 &&
- shaderInterface.GeometryMaxVerticesOut == 0 && IsKnownGeometryOutputPrimitiveType(shaderInterface.GeometryOutputPrimitiveType);
- var unsupportedStageMask = stages != 0 && stages != PrimitiveShaderStageMask;
+ IsPrimitiveShaderStageMask(stages) && vertex.ExportAddress != 0 &&
+ (vertex.GeometryAddress == 0 ||
+ (shaderInterface.GeometryMaxVerticesOut == 0 && IsKnownGeometryOutputPrimitiveType(shaderInterface.GeometryOutputPrimitiveType)));
+ var unsupportedStageMask = stages != 0 && !IsPrimitiveShaderStageMask(stages);
var unsupportedGeometryStage = vertex.ExportAddress != 0 && vertex.GeometryAddress != 0 && !primitiveShaderVertexPath;
- var geometryRegisters =
- (shaderInterface.PrimitiveShaderSubgroupControl != 0 && shaderInterface.PrimitiveShaderSubgroupControl != 1) ||
- shaderInterface.GeometryMaxVerticesOut != 0 ||
- !IsKnownGeometryOutputPrimitiveType(shaderInterface.GeometryOutputPrimitiveType) ||
- shaderInterface.MaxOutputPerSubgroup > MaxOutputPerSubgroupLimit;
+ var geometryRegisters = !primitiveShaderVertexPath &&
+ ((shaderInterface.PrimitiveShaderSubgroupControl != 0 && shaderInterface.PrimitiveShaderSubgroupControl != 1) ||
+ shaderInterface.GeometryMaxVerticesOut != 0 ||
+ !IsKnownGeometryOutputPrimitiveType(shaderInterface.GeometryOutputPrimitiveType) ||
+ shaderInterface.MaxOutputPerSubgroup > MaxOutputPerSubgroupLimit);
if (!unsupportedStageMask && !unsupportedGeometryStage && !geometryRegisters)
{
return false;
@@ -416,7 +426,15 @@ public sealed partial class RenderExecutor
if (Interlocked.Exchange(ref _geometryWarningShown, 1) == 0)
{
- Console.Error.WriteLine("Warning: the title uses unsupported graphics pipelines; some draw calls were skipped.");
+ Console.Error.WriteLine(
+ "Warning: the title uses unsupported graphics pipelines; some draw calls were skipped. " +
+ $"stages=0x{stages:X8} subgroup=0x{shaderInterface.PrimitiveShaderSubgroupControl:X8} " +
+ $"maxOutput=0x{shaderInterface.MaxOutputPerSubgroup:X8} " +
+ $"maxVerticesOut=0x{shaderInterface.GeometryMaxVerticesOut:X8} " +
+ $"outputPrimitive=0x{shaderInterface.GeometryOutputPrimitiveType:X8} " +
+ $"export=0x{vertex.ExportAddress:X16} geometry=0x{vertex.GeometryAddress:X16} " +
+ $"unsupportedMask={unsupportedStageMask} unsupportedGeometry={unsupportedGeometryStage} " +
+ $"unsupportedRegisters={geometryRegisters}.");
}
if (RenderTrace.Enabled && RenderTrace.GeometryStageSkip())
diff --git a/src/SharpEmu.Libs/Gpu/Rendering/ShaderStageResources.cs b/src/SharpEmu.Libs/Gpu/Rendering/ShaderStageResources.cs
index caddd5a..4da0543 100644
--- a/src/SharpEmu.Libs/Gpu/Rendering/ShaderStageResources.cs
+++ b/src/SharpEmu.Libs/Gpu/Rendering/ShaderStageResources.cs
@@ -52,6 +52,7 @@ public class ShaderProgramInfo
public int InstanceOffsetScalarRegister { get; init; } = NoScalarRegister;
public bool UsesDeviceAddresses { get; init; }
public bool HasBitwiseExclusiveOr { get; init; }
+ public Pipelines.ConstantFill? ConstantFill { get; init; }
public BufferResourceInfo[] Buffers { get; init; } = [];
public ImageResourceInfo[] Images { get; init; } = [];
public int SamplerCount { get; init; }
diff --git a/src/SharpEmu.Libs/Ime/ImeExports.cs b/src/SharpEmu.Libs/Ime/ImeExports.cs
index 7235731..84d92cd 100644
--- a/src/SharpEmu.Libs/Ime/ImeExports.cs
+++ b/src/SharpEmu.Libs/Ime/ImeExports.cs
@@ -33,6 +33,24 @@ public static class ImeExports
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
+ [SysAbiExport(
+ Nid = "VkqLPArfFdc",
+ ExportName = "sceImeKeyboardGetInfo",
+ Target = Generation.Gen4 | Generation.Gen5,
+ LibraryName = "libSceIme")]
+ public static int ImeKeyboardGetInfo(CpuContext ctx)
+ {
+ var infoAddress = ctx[CpuRegister.Rsi];
+ if (infoAddress != 0)
+ {
+ Span info = stackalloc byte[0x88];
+ _ = ctx.Memory.TryWrite(infoAddress, info);
+ }
+
+ ctx[CpuRegister.Rax] = 0;
+ return (int)OrbisGen2Result.ORBIS_GEN2_OK;
+ }
+
[SysAbiExport(
Nid = "dKadqZFgKKQ",
ExportName = "sceImeKeyboardGetResourceId",
diff --git a/src/SharpEmu.Libs/Kernel/KernelEventFlagCompatExports.cs b/src/SharpEmu.Libs/Kernel/KernelEventFlagCompatExports.cs
index d993c96..cb1edc7 100644
--- a/src/SharpEmu.Libs/Kernel/KernelEventFlagCompatExports.cs
+++ b/src/SharpEmu.Libs/Kernel/KernelEventFlagCompatExports.cs
@@ -60,8 +60,16 @@ public static class KernelEventFlagCompatExports
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
- if (!TryReadNullTerminatedUtf8(ctx, nameAddress, MaxEventFlagNameLength + 1, out var name))
+ // Event flag names are commonly stored in executable/read-only image
+ // pages. Use the kernel compatibility reader so those pages can use
+ // its guarded host-read fallback when the guest mapping's protection
+ // metadata temporarily rejects a normal TryRead call.
+ if (!KernelMemoryCompatExports.TryReadNullTerminatedUtf8(
+ ctx, nameAddress, MaxEventFlagNameLength + 1, out var name))
{
+ Console.Error.WriteLine(
+ $"[LOADER][WARN] event_flag name read failed address=0x{nameAddress:X16} " +
+ $"range={ctx.Memory.DescribeReadRange(nameAddress, (ulong)(MaxEventFlagNameLength + 1))}");
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
@@ -78,8 +86,11 @@ public static class KernelEventFlagCompatExports
Bits = initialPattern,
};
- if (!ctx.TryWriteUInt64(outAddress, handle))
+ if (!KernelMemoryCompatExports.TryWriteUInt64Compat(ctx, outAddress, handle))
{
+ Console.Error.WriteLine(
+ $"[LOADER][WARN] event_flag handle write failed address=0x{outAddress:X16} " +
+ $"range={ctx.Memory.DescribeReadRange(outAddress, sizeof(ulong))}");
_eventFlags.TryRemove(handle, out _);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
diff --git a/src/SharpEmu.Libs/Kernel/KernelExports.cs b/src/SharpEmu.Libs/Kernel/KernelExports.cs
index a935ef5..e32c545 100644
--- a/src/SharpEmu.Libs/Kernel/KernelExports.cs
+++ b/src/SharpEmu.Libs/Kernel/KernelExports.cs
@@ -11,6 +11,8 @@ public static class KernelExports
private static readonly object _cxaGate = new();
private static readonly List _cxaDestructors = new();
private static readonly object _coredumpGate = new();
+ private const string RepeatedGuestAssertion = "Assertion failed: 0";
+ private static int _repeatedGuestAssertionCount;
private static ulong _coredumpHandler;
private static ulong _coredumpHandlerContext;
@@ -30,7 +32,8 @@ public static class KernelExports
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
-
+ // KytyPS5 libKernel.cpp KernelGetOperationMode: *mode = 2 (PS5 base console, not Pro),
+ // *submode = 0 (none). Games poll this at high frequency.
[SysAbiExport(
Nid = "NH6xARDOVv8",
ExportName = "sceKernelGetOperationMode",
@@ -38,8 +41,19 @@ public static class KernelExports
LibraryName = "libKernel")]
public static int KernelGetOperationMode(CpuContext ctx)
{
- // SCE_KERNEL_MODE_RELEASE = 0. Reporting retail/release mode keeps guest
- // code from taking devkit/tool-only branches we do not emulate.
+ var modeAddress = ctx[CpuRegister.Rdi];
+ var submodeAddress = ctx[CpuRegister.Rsi];
+
+ if (modeAddress != 0 && !ctx.TryWriteUInt32(modeAddress, 2))
+ {
+ return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
+ }
+
+ if (submodeAddress != 0 && !ctx.TryWriteUInt32(submodeAddress, 0))
+ {
+ return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
+ }
+
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
@@ -223,7 +237,8 @@ public static class KernelExports
ctx,
attrAddress,
out var priority,
- out var affinityMask);
+ out var affinityMask,
+ out var requestedStackSize);
KernelPthreadExtendedCompatExports.RegisterThreadStart(
threadHandle,
name,
@@ -252,7 +267,8 @@ public static class KernelExports
attrAddress,
name,
priority,
- affinityMask);
+ affinityMask,
+ requestedStackSize);
if (!scheduler.TryStartThread(ctx, request, out var error))
{
Console.Error.WriteLine(
@@ -402,19 +418,42 @@ public static class KernelExports
ulong fmtPtr = ctx[CpuRegister.Rdi];
string fmt = ReadCString(ctx, fmtPtr, 4096);
string outStr = KernelMemoryCompatExports.FormatStringFromVarArgs(ctx, fmt, firstGpArgIndex: 1);
- if (outStr.EndsWith('\n') || outStr.EndsWith('\r'))
+ var byteCount = System.Text.Encoding.UTF8.GetByteCount(outStr);
+ if (ShouldWriteGuestPrintf(outStr))
{
- Console.Write($"[DEBUG][PRINF] {outStr}");
- }
- else
- {
- Console.WriteLine($"[DEBUG][PRINF] {outStr}");
+ if (outStr.EndsWith('\n') || outStr.EndsWith('\r'))
+ {
+ Console.Write($"[DEBUG][PRINF] {outStr}");
+ }
+ else
+ {
+ Console.WriteLine($"[DEBUG][PRINF] {outStr}");
+ }
}
- ctx[CpuRegister.Rax] = (ulong)System.Text.Encoding.UTF8.GetByteCount(outStr);
+ ctx[CpuRegister.Rax] = (ulong)byteCount;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
+ // Demon\'s Souls can print this same guest-side diagnostic thousands of times during
+ // bootstrap. Printing it does not convey new state and serializes the emulation threads,
+ // but printf must still report its original byte count to the guest.
+ private static bool ShouldWriteGuestPrintf(string text)
+ {
+ if (!string.Equals(text.TrimEnd('\r', '\n'), RepeatedGuestAssertion, StringComparison.Ordinal))
+ {
+ return true;
+ }
+
+ var count = Interlocked.Increment(ref _repeatedGuestAssertionCount);
+ if (count == 2)
+ {
+ Console.WriteLine("[DEBUG][PRINF] Further repeated 'Assertion failed: 0' messages are suppressed.");
+ }
+
+ return count == 1;
+ }
+
[SysAbiExport(
Nid = "EMutwaQ34Jo",
ExportName = "perror",
@@ -521,4 +560,5 @@ public static class KernelExports
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
+
}
diff --git a/src/SharpEmu.Libs/Kernel/KernelMemoryCompatExports.BackedMappings.cs b/src/SharpEmu.Libs/Kernel/KernelMemoryCompatExports.BackedMappings.cs
index 37fd893..e563f75 100644
--- a/src/SharpEmu.Libs/Kernel/KernelMemoryCompatExports.BackedMappings.cs
+++ b/src/SharpEmu.Libs/Kernel/KernelMemoryCompatExports.BackedMappings.cs
@@ -13,6 +13,16 @@ public static partial class KernelMemoryCompatExports
private const int MemoryAccessDenied = unchecked((int)0x8002000D);
private const int MemoryFault = unchecked((int)0x8002000E);
private const int MemoryInvalidArgument = unchecked((int)0x80020016);
+ // Titles use sceKernelReserveVirtualRange as an address-space reservation
+ // primitive. Some of them ask for hundreds of GiB even though only small
+ // portions are ever mapped. Windows cannot always create one contiguous
+ // placeholder that large after the executable and runtime are loaded.
+ private const ulong SparseReservationThreshold = 0x10_0000_0000UL;
+ // macOS reserves 0x10_0000_0000..0x70_0000_0000 for graphics memory (see
+ // DefaultMapSearchBase), so sparse reservations start above that window there.
+ private static ulong SparseReservationBase =>
+ OperatingSystem.IsMacOS() ? 0x70_0000_0000UL : 0x40_0000_0000UL;
+ private const ulong UserAddressLimit = 0xFC_0000_0000UL;
private static FlexibleBackingPool _flexibleBacking =
new(GuestMemoryLayout.FlexibleOffset, GuestMemoryLayout.FlexibleBytes);
private static IGuestBackedSpace? _backingOwner;
@@ -249,7 +259,9 @@ public static partial class KernelMemoryCompatExports
{
if (requested >= 0x10_0000_0000UL && requested < 0xFC_0000_0000UL &&
Interlocked.Exchange(ref _prtMapWarning, 1) == 0)
- Console.Error.WriteLine("[LOADER][WARN] Shared mapping cannot replace the reserved aperture.");
+ Console.Error.WriteLine(
+ $"[LOADER][WARN] Shared mapping cannot replace the reserved aperture: address=0x{requested:X16} size=0x{length:X} flags=0x{flags:X} " +
+ $"regions=[{string.Join(",", GetMappingSlices(requested, length).Select(region => $"0x{region.Address:X}+0x{region.Length:X}:{(region.IsReserved ? "reserved" : region.IsDirect ? "direct" : region.IsFlexible ? "flexible" : "other")}:backed={space.IsBackedView(region.Address)}"))}].");
return false;
}
address = requested;
@@ -335,7 +347,10 @@ public static partial class KernelMemoryCompatExports
if (pointer == 0 || !IsValidMapRange(length, alignment))
return MemoryInvalidArgument;
if (!ctx.TryReadUInt64(pointer, out var requested))
+ {
+ Console.Error.WriteLine($"[LOADER][WARN] reserve_virtual pointer read failed pointer=0x{pointer:X16} size=0x{length:X}");
return MemoryFault;
+ }
alignment = alignment == 0 ? OrbisPageSize : alignment;
if ((flags & OrbisKernelMapFixed) != 0 &&
(requested == 0 || !IsAligned(requested, OrbisPageSize) || requested > ulong.MaxValue - length))
@@ -343,15 +358,107 @@ public static partial class KernelMemoryCompatExports
lock (_memoryGate)
{
var space = ResolveBackingSpace(ctx);
- if (space is null || !TrySelectBackingAddress(space, requested, length, alignment, flags, out var address, reuseReservation: false))
+ var address = 0UL;
+ var sparseSelected = (flags & OrbisKernelMapFixed) == 0 &&
+ TrySelectSparseReservationAddress(requested, length, alignment, flags, out address);
+ var selected = sparseSelected;
+ if (!selected && space is not null)
+ {
+ selected = TrySelectBackingAddress(space, requested, length, alignment, flags,
+ out address, reuseReservation: false);
+ }
+
+ // A sparse reservation is only a guest mapping-table entry and
+ // intentionally does not need a host backing space yet. The host
+ // range is acquired later when a subrange is committed or mapped.
+ if (!selected || (!sparseSelected && space is null))
+ {
+ Console.Error.WriteLine($"[LOADER][WARN] reserve_virtual address selection failed requested=0x{requested:X16} size=0x{length:X} sparse={sparseSelected} backing={space is not null}");
return MemoryNoSpace;
+ }
+ if (ShouldTraceDirectMemory())
+ Console.Error.WriteLine($"[LOADER][TRACE] reserve_virtual selected address=0x{address:X16} requested=0x{requested:X16} size=0x{length:X} sparse={sparseSelected}");
ReplaceMappedRegionRangeLocked(new MappedRegion(address, length, 0, false, false, 0, IsReserved: true));
if (ShouldTraceDirectMemory())
Console.Error.WriteLine($"[LOADER][TRACE] reserve_virtual address=0x{address:X} requested=0x{requested:X} size=0x{length:X} flags=0x{flags:X}");
- return ctx.TryWriteUInt64(pointer, address) ? 0 : MemoryFault;
+ if (!ctx.TryWriteUInt64(pointer, address))
+ {
+ Console.Error.WriteLine($"[LOADER][WARN] reserve_virtual pointer write failed pointer=0x{pointer:X16} address=0x{address:X16}");
+ return MemoryFault;
+ }
+ return 0;
}
}
+ // Keep very large virtual reservations in the guest mapping table without
+ // requiring one equally large host placeholder. Actual backed mappings still
+ // reserve their individual host ranges when they are committed below this
+ // reservation. Fixed reservations never take this path: they must preserve
+ // the exact host address semantics of the original request.
+ private static bool TrySelectSparseReservationAddress(
+ ulong requested,
+ ulong length,
+ ulong alignment,
+ ulong flags,
+ out ulong address)
+ {
+ address = 0;
+ if ((flags & OrbisKernelMapFixed) != 0 || length < SparseReservationThreshold ||
+ length > UserAddressLimit || alignment == 0)
+ {
+ return false;
+ }
+
+ var candidate = requested == 0
+ ? SparseReservationBase
+ : Math.Max(requested, SparseReservationBase);
+ candidate = AlignUp(candidate, alignment);
+ if (candidate == 0)
+ {
+ return false;
+ }
+
+ // The requested address is a hint for a non-fixed reservation. If the
+ // hint is too high to contain the complete range, restart from the
+ // sparse aperture instead of treating the hint as a lower bound.
+ if (candidate > UserAddressLimit - length)
+ {
+ candidate = AlignUp(SparseReservationBase, alignment);
+ if (candidate == 0 || candidate > UserAddressLimit - length)
+ return false;
+ }
+
+ foreach (var region in _mappedRegions.Values)
+ {
+ if (region.Address >= candidate + length)
+ {
+ break;
+ }
+
+ var regionEnd = region.Address + region.Length;
+ if (regionEnd <= candidate)
+ {
+ continue;
+ }
+
+ candidate = AlignUp(regionEnd, alignment);
+ if (candidate == 0 || candidate > UserAddressLimit - length)
+ {
+ return false;
+ }
+ }
+
+ if (candidate == 0 || candidate > UserAddressLimit - length ||
+ _mappedRegions.Values.Any(region =>
+ region.Address < candidate + length && candidate < region.Address + region.Length))
+ {
+ return false;
+ }
+
+ address = candidate;
+ return true;
+ }
+
private static bool HasPhysicalSpan(ulong start, ulong length)
=> _directAllocations.ContainsAllocatedRange(start, length);
diff --git a/src/SharpEmu.Libs/Kernel/KernelMemoryCompatExports.cs b/src/SharpEmu.Libs/Kernel/KernelMemoryCompatExports.cs
index 5b32193..fd59b42 100644
--- a/src/SharpEmu.Libs/Kernel/KernelMemoryCompatExports.cs
+++ b/src/SharpEmu.Libs/Kernel/KernelMemoryCompatExports.cs
@@ -2064,6 +2064,13 @@ public static partial class KernelMemoryCompatExports
}
}
+ [SysAbiExport(
+ Nid = "VAzswvTOCzI",
+ ExportName = "unlink",
+ Target = Generation.Gen4 | Generation.Gen5,
+ LibraryName = "libKernel")]
+ public static int PosixUnlink(CpuContext ctx) => KernelUnlink(ctx);
+
[SysAbiExport(
Nid = "1-LFLmRFxxM",
ExportName = "sceKernelMkdir",
@@ -2281,6 +2288,16 @@ public static partial class KernelMemoryCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
}
+ // A host movie watchdog may have closed the decoder while the guest
+ // still owns its Bink file descriptor. Return EOF so the guest closes
+ // that descriptor and proceeds to the next movie instead of waiting
+ // forever on a stale native decoder.
+ if (HostMovieBridge.ShouldForceGuestMovieEof(stream.Name))
+ {
+ ctx[CpuRegister.Rax] = 0;
+ return (int)OrbisGen2Result.ORBIS_GEN2_OK;
+ }
+
long positionBefore;
try
{
diff --git a/src/SharpEmu.Libs/Kernel/KernelPthreadCompatExports.cs b/src/SharpEmu.Libs/Kernel/KernelPthreadCompatExports.cs
index c731b1c..3cea481 100644
--- a/src/SharpEmu.Libs/Kernel/KernelPthreadCompatExports.cs
+++ b/src/SharpEmu.Libs/Kernel/KernelPthreadCompatExports.cs
@@ -39,6 +39,11 @@ public static class KernelPthreadCompatExports
private static readonly Dictionary _condStates = new();
private static readonly Dictionary _onceGates = new();
private static readonly Dictionary _condAttrStates = new();
+ // Only these exact allocation bases are owned by the pthread compatibility
+ // layer. Guest pthread objects are commonly stored inside reusable blocks;
+ // never pass one of those storage addresses to TryFreeGuestMemory when a
+ // stale handle is destroyed.
+ private static readonly ConcurrentDictionary _opaqueAllocations = new();
private static readonly bool _tracePthreads =
string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_PTHREADS"), "1", StringComparison.Ordinal);
private static readonly bool _tracePthreadConds =
@@ -283,7 +288,11 @@ public static class KernelPthreadCompatExports
public static int PthreadYield(CpuContext ctx)
{
_ = ctx;
- Thread.Yield();
+ if (!Thread.Yield())
+ {
+ Thread.Sleep(0);
+ }
+
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
@@ -294,6 +303,13 @@ public static class KernelPthreadCompatExports
LibraryName = "libKernel")]
public static int PosixPthreadYield(CpuContext ctx) => PthreadYield(ctx);
+ [SysAbiExport(
+ Nid = "6XG4B33N09g",
+ ExportName = "sched_yield",
+ Target = Generation.Gen4 | Generation.Gen5,
+ LibraryName = "libKernel")]
+ public static int SchedYield(CpuContext ctx) => PthreadYield(ctx);
+
[SysAbiExport(
Nid = "GBUY7ywdULE",
ExportName = "scePthreadRename",
@@ -1696,6 +1712,8 @@ public static class KernelPthreadCompatExports
return false;
}
+ _opaqueAllocations[address] = 0;
+
Span initialData = stackalloc byte[size];
initialData.Clear();
if (ctx.Memory.TryWrite(address, initialData))
@@ -1710,7 +1728,9 @@ public static class KernelPthreadCompatExports
private static void FreeOpaqueObject(CpuContext ctx, ulong address)
{
- if (address != 0 && ctx.Memory is IGuestMemoryAllocator allocator)
+ if (address != 0 &&
+ _opaqueAllocations.TryRemove(address, out _) &&
+ ctx.Memory is IGuestMemoryAllocator allocator)
{
_ = allocator.TryFreeGuestMemory(address);
}
diff --git a/src/SharpEmu.Libs/Kernel/KernelPthreadExtendedCompatExports.cs b/src/SharpEmu.Libs/Kernel/KernelPthreadExtendedCompatExports.cs
index 5d0bc4f..1a09063 100644
--- a/src/SharpEmu.Libs/Kernel/KernelPthreadExtendedCompatExports.cs
+++ b/src/SharpEmu.Libs/Kernel/KernelPthreadExtendedCompatExports.cs
@@ -20,6 +20,7 @@ public static class KernelPthreadExtendedCompatExports
private const ulong NativeGuestStackSize = 0x20_0000UL;
private const ulong NativeGuestStackStride = 0x100_0000UL;
private const int DefaultInheritSched = 4;
+ private const int DefaultSoloSched = 0;
private const int DefaultSchedPolicy = 1;
private const int DefaultSchedPriority = DefaultThreadPriority;
private const ulong SyntheticRwlockHandleBase = 0x00006003_0000_0000;
@@ -44,12 +45,14 @@ public static class KernelPthreadExtendedCompatExports
CpuContext ctx,
ulong attrAddress,
out int priority,
- out ulong affinityMask)
+ out ulong affinityMask,
+ out ulong stackSize)
{
if (attrAddress == 0)
{
priority = DefaultThreadPriority;
affinityMask = DefaultThreadAffinityMask;
+ stackSize = DefaultStackSize;
return;
}
@@ -59,6 +62,7 @@ public static class KernelPthreadExtendedCompatExports
var attributes = GetOrCreateAttrStateLocked(resolvedAddress);
priority = attributes.SchedPriority;
affinityMask = attributes.AffinityMask;
+ stackSize = attributes.StackSize;
}
}
@@ -189,7 +193,8 @@ public static class KernelPthreadExtendedCompatExports
ulong GuardSize,
int InheritSched,
int SchedPolicy,
- int SchedPriority)
+ int SchedPriority,
+ int SoloSched)
{
public static PthreadAttrState Default =>
new(
@@ -200,7 +205,8 @@ public static class KernelPthreadExtendedCompatExports
DefaultGuardSize,
DefaultInheritSched,
DefaultSchedPolicy,
- DefaultSchedPriority);
+ DefaultSchedPriority,
+ DefaultSoloSched);
}
[SysAbiExport(
@@ -860,6 +866,59 @@ public static class KernelPthreadExtendedCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
+ [SysAbiExport(
+ Nid = "Dk6FC-TI+7Q",
+ ExportName = "scePthreadAttrSetsolosched",
+ Target = Generation.Gen4 | Generation.Gen5,
+ LibraryName = "libKernel")]
+ public static int PthreadAttrSetsolosched(CpuContext ctx)
+ {
+ var attrAddress = ctx[CpuRegister.Rdi];
+ var soloSched = unchecked((int)ctx[CpuRegister.Rsi]);
+ if (attrAddress == 0)
+ {
+ return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
+ }
+
+ lock (_stateGate)
+ {
+ var state = GetOrCreateAttrStateLocked(attrAddress);
+ _attrStates[attrAddress] = state with { SoloSched = soloSched };
+ }
+
+ ctx[CpuRegister.Rax] = 0;
+ return (int)OrbisGen2Result.ORBIS_GEN2_OK;
+ }
+
+ [SysAbiExport(
+ Nid = "9RnL-m0+diQ",
+ ExportName = "scePthreadAttrGetsolosched",
+ Target = Generation.Gen4 | Generation.Gen5,
+ LibraryName = "libKernel")]
+ public static int PthreadAttrGetsolosched(CpuContext ctx)
+ {
+ var attrAddress = ctx[CpuRegister.Rdi];
+ var outSoloSchedAddress = ctx[CpuRegister.Rsi];
+ if (attrAddress == 0 || outSoloSchedAddress == 0)
+ {
+ return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
+ }
+
+ PthreadAttrState state;
+ lock (_stateGate)
+ {
+ state = GetOrCreateAttrStateLocked(attrAddress);
+ }
+
+ if (!TryWriteInt32(ctx, outSoloSchedAddress, state.SoloSched))
+ {
+ return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
+ }
+
+ ctx[CpuRegister.Rax] = 0;
+ return (int)OrbisGen2Result.ORBIS_GEN2_OK;
+ }
+
///
/// The POSIX-named alias of . libKernel
/// exports the same routine under two NIDs; middleware compiled against the
diff --git a/src/SharpEmu.Libs/Kernel/KernelSemaphoreCompatExports.cs b/src/SharpEmu.Libs/Kernel/KernelSemaphoreCompatExports.cs
index 5eacd84..5dfe103 100644
--- a/src/SharpEmu.Libs/Kernel/KernelSemaphoreCompatExports.cs
+++ b/src/SharpEmu.Libs/Kernel/KernelSemaphoreCompatExports.cs
@@ -52,7 +52,8 @@ public static class KernelSemaphoreCompatExports
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
- if (!TryReadNullTerminatedUtf8(ctx, nameAddress, MaxSemaphoreNameLength, out var name))
+ if (!KernelMemoryCompatExports.TryReadNullTerminatedUtf8(
+ ctx, nameAddress, MaxSemaphoreNameLength, out var name))
{
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
diff --git a/src/SharpEmu.Libs/Media/HostMovieBridge.cs b/src/SharpEmu.Libs/Media/HostMovieBridge.cs
index bc8ac84..e91c310 100644
--- a/src/SharpEmu.Libs/Media/HostMovieBridge.cs
+++ b/src/SharpEmu.Libs/Media/HostMovieBridge.cs
@@ -42,6 +42,7 @@ internal static class HostMovieBridge
private static byte[]? _frameBuffer;
private static bool _frameBufferPresented;
private static MediaFramePlayback? _playback;
+ private static Timer? _playbackWatchdog;
private static long _frameSerial;
private static uint _presentationWidth = MaxHostVideoWidth;
private static uint _presentationHeight = MaxHostVideoHeight;
@@ -287,6 +288,7 @@ internal static class HostMovieBridge
_frameBuffer = GC.AllocateUninitializedArray(GetFrameBufferLength(info));
_frameBufferPresented = false;
FillDummyFrame(_frameBuffer, info.Width, info.Height);
+ ArmPlaybackWatchdogLocked(hostPath);
Console.Error.WriteLine(
"[LOADER][INFO] Bink dummy attached: " + Path.GetFileName(hostPath) + " " +
info.Width + "x" + info.Height + ".");
@@ -301,6 +303,53 @@ internal static class HostMovieBridge
_activePath = hostPath;
_activeInfo = info;
_playback = new MediaFramePlayback(decoder);
+ ArmPlaybackWatchdogLocked(hostPath);
+ }
+
+ private static void ArmPlaybackWatchdogLocked(string hostPath)
+ {
+ _playbackWatchdog?.Dispose();
+ var timeout = TryReadBinkInfo(hostPath, out var info)
+ ? GetPlaybackWatchdogTimeout(info)
+ : TimeSpan.FromSeconds(150);
+ _playbackWatchdog = new Timer(
+ static state =>
+ {
+ var path = (string)state!;
+ lock (Gate)
+ {
+ if (!string.Equals(_activePath, path, StringComparison.OrdinalIgnoreCase))
+ {
+ return;
+ }
+
+ Console.Error.WriteLine(
+ "[LOADER][WARN] Bink2 host watchdog expired for " +
+ Path.GetFileName(path) + "; advancing to the next movie.");
+ TimedOutMoviePaths.Add(path);
+ CloseActiveLocked();
+ AttachNextQueuedMovieLocked();
+ }
+ },
+ hostPath,
+ timeout,
+ Timeout.InfiniteTimeSpan);
+ }
+
+ // Must outlast the whole movie: Demon's Souls ships a 171 s intro and 756 s credits.
+ internal static TimeSpan GetPlaybackWatchdogTimeout(Bink2MovieInfo info)
+ {
+ if (info.FrameCount == 0 ||
+ info.FramesPerSecondNumerator == 0 ||
+ info.FramesPerSecondDenominator == 0)
+ {
+ return TimeSpan.FromSeconds(150);
+ }
+
+ var duration = (double)info.FrameCount *
+ info.FramesPerSecondDenominator /
+ info.FramesPerSecondNumerator;
+ return TimeSpan.FromSeconds(Math.Max(90, duration + 60));
}
internal static bool TryReadBinkInfo(string path, out Bink2MovieInfo info)
@@ -319,6 +368,7 @@ internal static class HostMovieBridge
info = new Bink2MovieInfo(
BinaryPrimitives.ReadUInt32LittleEndian(header.Slice(0x14, 4)),
BinaryPrimitives.ReadUInt32LittleEndian(header.Slice(0x18, 4)),
+ BinaryPrimitives.ReadUInt32LittleEndian(header.Slice(8, 4)),
BinaryPrimitives.ReadUInt32LittleEndian(header.Slice(0x1C, 4)),
BinaryPrimitives.ReadUInt32LittleEndian(header.Slice(0x20, 4)));
return info.FramesPerSecondNumerator != 0 &&
@@ -349,6 +399,8 @@ internal static class HostMovieBridge
private static void CloseActiveLocked()
{
+ _playbackWatchdog?.Dispose();
+ _playbackWatchdog = null;
_playback?.Dispose();
_playback = null;
_activePath = null;
@@ -364,6 +416,7 @@ internal static class HostMovieBridge
[StructLayout(LayoutKind.Sequential)]
internal readonly struct Bink2MovieInfo
{
+ public readonly uint FrameCount;
public readonly uint Width;
public readonly uint Height;
public readonly uint FramesPerSecondNumerator;
@@ -372,14 +425,25 @@ internal static class HostMovieBridge
internal Bink2MovieInfo(
uint width,
uint height,
+ uint frameCount,
uint framesPerSecondNumerator,
uint framesPerSecondDenominator)
{
Width = width;
Height = height;
+ FrameCount = frameCount;
FramesPerSecondNumerator = framesPerSecondNumerator;
FramesPerSecondDenominator = framesPerSecondDenominator;
}
+
+ internal Bink2MovieInfo(
+ uint width,
+ uint height,
+ uint framesPerSecondNumerator,
+ uint framesPerSecondDenominator)
+ : this(width, height, 0, framesPerSecondNumerator, framesPerSecondDenominator)
+ {
+ }
}
private enum MovieMode
@@ -393,6 +457,16 @@ internal static class HostMovieBridge
private static readonly Queue PendingMoviePaths = new();
private static readonly HashSet PendingMoviePathSet =
new(StringComparer.OrdinalIgnoreCase);
+ private static readonly HashSet TimedOutMoviePaths =
+ new(StringComparer.OrdinalIgnoreCase);
+
+ internal static bool ShouldForceGuestMovieEof(string hostPath)
+ {
+ lock (Gate)
+ {
+ return TimedOutMoviePaths.Contains(hostPath);
+ }
+ }
private static void AttachNextQueuedMovieLocked()
{
while (PendingMoviePaths.Count > 0)
@@ -475,6 +549,7 @@ internal static class HostMovieBridge
{
lock (Gate)
{
+ TimedOutMoviePaths.Remove(hostPath);
if (PendingMoviePathSet.Remove(hostPath))
{
var retained = PendingMoviePaths
diff --git a/src/SharpEmu.Libs/Network/HttpExports.cs b/src/SharpEmu.Libs/Network/HttpExports.cs
index 9c01e0e..06f8a15 100644
--- a/src/SharpEmu.Libs/Network/HttpExports.cs
+++ b/src/SharpEmu.Libs/Network/HttpExports.cs
@@ -18,7 +18,12 @@ public static partial class HttpExports
private sealed record HttpContext(int NetMemoryId, int SslContextId, ulong PoolSize);
- private sealed record HttpTemplate(int ContextId, ulong UserAgentAddress, int HttpVersion, bool AutoProxyConfig);
+ private sealed record HttpTemplate(
+ int ContextId,
+ ulong UserAgentAddress,
+ int HttpVersion,
+ bool AutoProxyConfig,
+ uint ConnectTimeoutMicroseconds = 30_000_000);
[SysAbiExport(
Nid = "A9cVMUtEp4Y",
@@ -78,6 +83,30 @@ public static partial class HttpExports
: ctx.SetReturn(HttpErrorInvalidId);
}
+ [SysAbiExport(
+ Nid = "0S9tTH0uqTU",
+ ExportName = "sceHttpSetConnectTimeOut",
+ Target = Generation.Gen4 | Generation.Gen5,
+ LibraryName = "libSceHttp")]
+ public static int HttpSetConnectTimeOut(CpuContext ctx)
+ {
+ var id = unchecked((int)ctx[CpuRegister.Rdi]);
+ var timeoutMicroseconds = unchecked((uint)ctx[CpuRegister.Rsi]);
+ if (timeoutMicroseconds == 0)
+ {
+ return ctx.SetReturn(HttpErrorInvalidValue);
+ }
+
+ if (!Templates.TryGetValue(id, out var template))
+ {
+ return ctx.SetReturn(HttpErrorInvalidId);
+ }
+
+ Templates[id] = template with { ConnectTimeoutMicroseconds = timeoutMicroseconds };
+ TraceHttp("set_connect_timeout", id, timeoutMicroseconds, 0, 0, 0);
+ return ctx.SetReturn(0);
+ }
+
[SysAbiExport(
Nid = "Ik-KpLTlf7Q",
ExportName = "sceHttpTerm",
diff --git a/src/SharpEmu.Libs/Network/NetExports.cs b/src/SharpEmu.Libs/Network/NetExports.cs
index 1ecb14a..2f01884 100644
--- a/src/SharpEmu.Libs/Network/NetExports.cs
+++ b/src/SharpEmu.Libs/Network/NetExports.cs
@@ -25,6 +25,7 @@ public static class NetExports
private const int NetErrnoAddressInUse = 48;
private const int NetErrnoNotInitialized = 200;
private const int MaxNameLength = 256;
+ private static ReadOnlySpan OfflineMacAddress => [0x02, 0x53, 0x48, 0x41, 0x52, 0x50];
private static readonly ConcurrentDictionary _pools = new();
private static readonly ConcurrentDictionary _resolvers = new();
@@ -117,6 +118,24 @@ public static class NetExports
return ctx.SetReturn(0);
}
+ [SysAbiExport(
+ Nid = "6Oc0bLsIYe0",
+ ExportName = "sceNetGetMacAddress",
+ Target = Generation.Gen4 | Generation.Gen5,
+ LibraryName = "libSceNet")]
+ public static int NetGetMacAddress(CpuContext ctx)
+ {
+ var destinationAddress = ctx[CpuRegister.Rdi];
+ var flags = unchecked((int)ctx[CpuRegister.Rsi]);
+ if (destinationAddress == 0 || flags != 0 || !ctx.Memory.TryWrite(destinationAddress, OfflineMacAddress))
+ {
+ return SetNetError(ctx, NetErrorInvalidArgument, NetErrnoInvalidArgument);
+ }
+
+ TraceNet("get_mac_address", 0, destinationAddress, unchecked((ulong)flags), 0);
+ return ctx.SetReturn(0);
+ }
+
[SysAbiExport(
Nid = "Q4qBuN-c0ZM",
ExportName = "sceNetSocket",
diff --git a/src/SharpEmu.Libs/Np/NpEntitlementAccessExports.cs b/src/SharpEmu.Libs/Np/NpEntitlementAccessExports.cs
index 21f42db..5c60943 100644
--- a/src/SharpEmu.Libs/Np/NpEntitlementAccessExports.cs
+++ b/src/SharpEmu.Libs/Np/NpEntitlementAccessExports.cs
@@ -23,15 +23,22 @@ public static class NpEntitlementAccessExports
private const int NpEntitlementAccessErrorParameter = unchecked((int)0x817D0002);
private const int NpEntitlementAccessErrorNoEntitlement = unchecked((int)0x817D0007);
+ private const int EntitlementKeySize = 16;
- // Offline add-on entitlements titles query through NpEntitlementAccess.
+ // Offline entitlements queried by titles through NpEntitlementAccess.
// GTA V Enhanced (PPSA04264) gates Story Mode on these three labels; without
// them the frontend offers "Buy GTAV Story Mode" despite a full dump.
+ // Ghost of Yotei (PPSA26344) separately checks its application and base
+ // game entitlements before it creates the frontend. These represent the
+ // locally installed main package only; optional editions and add-ons remain
+ // unowned.
private static readonly AddcontEntitlement[] OwnedAddcontEntitlements =
[
new("85y-je", PackageTypePsal, DownloadStatusInstalled),
new("5d5c48", PackageTypePsal, DownloadStatusInstalled),
new("_mtqu6", PackageTypePsal, DownloadStatusInstalled),
+ new("GHOST2APP0000000", PackageTypePsal, DownloadStatusInstalled),
+ new("GHOST2BASE000000", PackageTypePsal, DownloadStatusInstalled),
];
private readonly record struct AddcontEntitlement(
@@ -193,6 +200,40 @@ public static class NpEntitlementAccessExports
return ctx.SetReturn(NpEntitlementAccessErrorNoEntitlement);
}
+ [SysAbiExport(
+ Nid = "5LiMEPuW0DQ",
+ ExportName = "sceNpEntitlementAccessGetEntitlementKey",
+ Target = Generation.Gen4 | Generation.Gen5,
+ LibraryName = "libSceNpEntitlementAccess")]
+ public static int NpEntitlementAccessGetEntitlementKey(CpuContext ctx)
+ {
+ var labelAddress = ctx[CpuRegister.Rsi];
+ var keyAddress = ctx[CpuRegister.Rdx];
+ if (labelAddress == 0 || keyAddress == 0)
+ {
+ return ctx.SetReturn(NpEntitlementAccessErrorParameter);
+ }
+
+ Span labelBytes = stackalloc byte[EntitlementLabelSize];
+ if (!ctx.Memory.TryRead(labelAddress, labelBytes))
+ {
+ return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
+ }
+
+ var label = ReadEntitlementLabel(labelBytes);
+ Span key = stackalloc byte[EntitlementKeySize];
+ key.Clear();
+ if (!ctx.Memory.TryWrite(keyAddress, key))
+ {
+ return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
+ }
+
+ return ctx.SetReturn(OwnedAddcontEntitlements.Any(entitlement =>
+ string.Equals(label, entitlement.Label, StringComparison.Ordinal))
+ ? (int)OrbisGen2Result.ORBIS_GEN2_OK
+ : NpEntitlementAccessErrorNoEntitlement);
+ }
+
private static bool TryWriteAddcontEntitlementInfo(
CpuContext ctx,
ulong address,
diff --git a/src/SharpEmu.Libs/Np/NpManagerExports.cs b/src/SharpEmu.Libs/Np/NpManagerExports.cs
index ab43b2c..4f69258 100644
--- a/src/SharpEmu.Libs/Np/NpManagerExports.cs
+++ b/src/SharpEmu.Libs/Np/NpManagerExports.cs
@@ -11,6 +11,7 @@ public static class NpManagerExports
private const int NpTitleIdSize = 16;
private const int NpTitleSecretSize = 128;
private const int NpErrorInvalidArgument = unchecked((int)0x80550003);
+ private static int _nextNpRequestId;
[SysAbiExport(
Nid = "3Zl8BePTh9Y",
@@ -23,6 +24,48 @@ public static class NpManagerExports
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
+ [SysAbiExport(Nid = "eiqMCt9UshI", ExportName = "sceNpCreateAsyncRequest", Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceNpManager")]
+ public static int NpCreateAsyncRequest(CpuContext ctx) => SetReturn(ctx, NextRequestId());
+
+ [SysAbiExport(Nid = "KfGZg2y73oM", ExportName = "sceNpCheckNpReachability", Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceNpManager")]
+ public static int NpCheckNpReachability(CpuContext ctx) =>
+ SetReturn(ctx, unchecked((int)ctx[CpuRegister.Rdi]) > 0 ? (int)OrbisGen2Result.ORBIS_GEN2_OK : NpErrorInvalidArgument);
+
+ [SysAbiExport(Nid = "GpLQDNKICac", ExportName = "sceNpCreateRequest", Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceNpManager")]
+ public static int NpCreateRequest(CpuContext ctx) => SetReturn(ctx, NextRequestId());
+
+ [SysAbiExport(Nid = "OzKvTvg3ZYU", ExportName = "sceNpAbortRequest", Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceNpManager")]
+ public static int NpAbortRequest(CpuContext ctx) => SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_OK);
+
+ [SysAbiExport(Nid = "uqcPJLWL08M", ExportName = "sceNpPollAsync", Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceNpManager")]
+ public static int NpPollAsync(CpuContext ctx)
+ {
+ var requestId = unchecked((int)ctx[CpuRegister.Rdi]);
+ var resultAddress = ctx[CpuRegister.Rsi];
+ if (requestId <= 0)
+ {
+ return SetReturn(ctx, NpErrorInvalidArgument);
+ }
+
+ if (resultAddress != 0)
+ {
+ Span result = stackalloc byte[sizeof(int)];
+ BinaryPrimitives.WriteInt32LittleEndian(result, 0);
+ if (!ctx.Memory.TryWrite(resultAddress, result))
+ {
+ return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
+ }
+ }
+
+ return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_OK);
+ }
+
+ private static int NextRequestId()
+ {
+ var id = Interlocked.Increment(ref _nextNpRequestId) & 0x7FFF_FFFF;
+ return id == 0 ? 1 : id;
+ }
+
[SysAbiExport(
Nid = "S7QTn72PrDw",
ExportName = "sceNpDeleteRequest",
diff --git a/src/SharpEmu.Libs/Np/NpSessionSignalingExports.cs b/src/SharpEmu.Libs/Np/NpSessionSignalingExports.cs
index 015d259..49eb507 100644
--- a/src/SharpEmu.Libs/Np/NpSessionSignalingExports.cs
+++ b/src/SharpEmu.Libs/Np/NpSessionSignalingExports.cs
@@ -7,6 +7,8 @@ namespace SharpEmu.Libs.Np;
public static class NpSessionSignalingExports
{
+ private static int _nextContextId;
+
[SysAbiExport(
Nid = "ysmw6J-P8Ak",
ExportName = "sceNpSessionSignalingInitialize",
@@ -17,4 +19,16 @@ public static class NpSessionSignalingExports
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
+
+ [SysAbiExport(
+ Nid = "aBuX0PX-T7I",
+ ExportName = "sceNpSessionSignalingCreateContext2",
+ Target = Generation.Gen5,
+ LibraryName = "libSceNpSessionSignaling")]
+ public static int NpSessionSignalingCreateContext2(CpuContext ctx)
+ {
+ var contextId = Interlocked.Increment(ref _nextContextId);
+ ctx[CpuRegister.Rax] = unchecked((ulong)contextId);
+ return contextId;
+ }
}
diff --git a/src/SharpEmu.Libs/Np/NpWebApi2Exports.cs b/src/SharpEmu.Libs/Np/NpWebApi2Exports.cs
index a497fe4..ba07186 100644
--- a/src/SharpEmu.Libs/Np/NpWebApi2Exports.cs
+++ b/src/SharpEmu.Libs/Np/NpWebApi2Exports.cs
@@ -134,6 +134,13 @@ public static class NpWebApi2Exports
return Interlocked.Increment(ref _nextPushEventHandle);
}
+ [SysAbiExport(
+ Nid = "QafxeZM3WK4",
+ ExportName = "sceNpWebApi2PushEventDeletePushContext",
+ Target = Generation.Gen4 | Generation.Gen5,
+ LibraryName = "libSceNpWebApi2")]
+ public static int NpWebApi2PushEventDeletePushContext(CpuContext ctx) => ctx.SetReturn(0);
+
private static bool IsValidLibraryContextId(int libraryContextId)
{
if (libraryContextId <= 0 || libraryContextId >= 0x8000)
diff --git a/src/SharpEmu.Libs/Pad/PadExports.cs b/src/SharpEmu.Libs/Pad/PadExports.cs
index 6668fed..b6225a5 100644
--- a/src/SharpEmu.Libs/Pad/PadExports.cs
+++ b/src/SharpEmu.Libs/Pad/PadExports.cs
@@ -652,6 +652,32 @@ public static class PadExports
return ctx.SetReturn(0);
}
+ [SysAbiExport(
+ Nid = "rIZnR6eSpvk",
+ ExportName = "scePadResetOrientation",
+ Target = Generation.Gen4 | Generation.Gen5,
+ LibraryName = "libScePad")]
+ public static int PadResetOrientation(CpuContext ctx)
+ {
+ var handle = unchecked((int)ctx[CpuRegister.Rdi]);
+ return IsPrimaryPadHandle(handle)
+ ? ctx.SetReturn(0)
+ : ctx.SetReturn(OrbisPadErrorInvalidHandle);
+ }
+
+ [SysAbiExport(
+ Nid = "fCWdlnmB1Ks",
+ ExportName = "scePadIsRemoteController",
+ Target = Generation.Gen4 | Generation.Gen5,
+ LibraryName = "libScePad")]
+ public static int PadIsRemoteController(CpuContext ctx)
+ {
+ var handle = unchecked((int)ctx[CpuRegister.Rdi]);
+ return IsPrimaryPadHandle(handle)
+ ? ctx.SetReturn(0)
+ : ctx.SetReturn(OrbisPadErrorInvalidHandle);
+ }
+
private static bool WriteNeutralPadData(CpuContext ctx, ulong dataAddress)
{
Span data = stackalloc byte[PadDataSize];
diff --git a/src/SharpEmu.Libs/VideoOut/RenderPhaseProfile.cs b/src/SharpEmu.Libs/VideoOut/RenderPhaseProfile.cs
index 1b92cfa..5a27739 100644
--- a/src/SharpEmu.Libs/VideoOut/RenderPhaseProfile.cs
+++ b/src/SharpEmu.Libs/VideoOut/RenderPhaseProfile.cs
@@ -464,6 +464,7 @@ internal static class RenderPhaseProfile
ReportImageUploads();
BufferUploadProfile.Report();
+ Console.Error.WriteLine(SharpEmu.ShaderCompiler.Resources.ResourceMaterializationCache.TakeReport());
SharpEmu.ShaderCompiler.Resources.ResourceMaterializationProfile.WriteReport();
SharpEmu.Libs.Diagnostics.AgcRegisterPacketProfile.WriteReport();
SharpEmu.HLE.GpuMemory.GpuMemoryAccessProfile.WriteReport();
diff --git a/src/SharpEmu.Libs/VideoOut/VulkanStorageFeedbackTargetResolver.cs b/src/SharpEmu.Libs/VideoOut/VulkanStorageFeedbackTargetResolver.cs
new file mode 100644
index 0000000..bc0fdc6
--- /dev/null
+++ b/src/SharpEmu.Libs/VideoOut/VulkanStorageFeedbackTargetResolver.cs
@@ -0,0 +1,53 @@
+// Copyright (C) 2026 SharpEmu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+using SharpEmu.Libs.Gpu;
+
+namespace SharpEmu.Libs.VideoOut;
+
+// Keeps a storage image out of the Vulkan color-attachment list when the guest
+// disabled color writes. A storage image with active color writes still needs a
+// pass split; silently binding both views would make the result undefined.
+internal static class VulkanStorageFeedbackTargetResolver
+{
+ public static bool TryResolve(
+ IReadOnlyList targets,
+ IReadOnlyList textures,
+ IReadOnlyList blends,
+ out GuestRenderTarget[] resolvedTargets,
+ out bool usedCompatibilityAttachment,
+ out ulong unsupportedAddress)
+ {
+ if (targets.Count != blends.Count)
+ {
+ throw new ArgumentException(
+ "color attachment and blend-state counts must match",
+ nameof(blends));
+ }
+
+ resolvedTargets = targets.ToArray();
+ usedCompatibilityAttachment = false;
+ unsupportedAddress = 0;
+
+ for (var targetIndex = 0; targetIndex < targets.Count; targetIndex++)
+ {
+ var target = targets[targetIndex];
+ if (target.Address == 0 || !textures.Any(texture =>
+ texture.IsStorage && texture.Address == target.Address))
+ {
+ continue;
+ }
+
+ if (targets.Count != 1 || blends[targetIndex].WriteMask != 0)
+ {
+ unsupportedAddress = target.Address;
+ return false;
+ }
+
+ resolvedTargets[targetIndex] = target with { Address = 0 };
+ usedCompatibilityAttachment = true;
+ }
+
+ return true;
+ }
+}
diff --git a/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.CachedImages.cs b/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.CachedImages.cs
index 11208bf..ae527a9 100644
--- a/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.CachedImages.cs
+++ b/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.CachedImages.cs
@@ -14,6 +14,18 @@ using VkBuffer = Silk.NET.Vulkan.Buffer;
internal static unsafe partial class VulkanVideoPresenter
{
+ private readonly record struct FeedbackSnapshotKey(
+ Format Format,
+ uint Width,
+ uint Height,
+ uint MipLevels,
+ uint Layers,
+ uint Samples,
+ ImageAspectFlags Aspect);
+
+ private const int FeedbackSnapshotPoolMaxEntriesPerKey = 2;
+ private const ulong FeedbackSnapshotPoolMaxBytes = 256UL * 1024 * 1024;
+
// Display buffers the guest registered; the only image addresses a flip may capture.
private static readonly HashSet _knownDisplayBuffers = new();
@@ -115,12 +127,17 @@ internal static unsafe partial class VulkanVideoPresenter
public uint DestinationSelect;
// One view per mip level for a storage image the program indexes by mip; empty otherwise.
public ImageView[] MipViews = [];
+ // A draw-local copy used when the guest samples a depth aspect while the
+ // same image is attached for writes. It retires with the submission.
+ public bool IsFeedbackSnapshot;
}
private sealed partial class Presenter
{
private readonly List _trackedImageBindings = new();
private static int _guestImageDumpSequence;
+ private readonly Dictionary> _feedbackSnapshotPool = [];
+ private ulong _feedbackSnapshotPoolBytes;
// Shader images for work without color targets: resolve, acquire, transition and release the bindings.
private TextureResource[] ResolveDrawTextures(IReadOnlyList textures)
@@ -514,10 +531,10 @@ internal static unsafe partial class VulkanVideoPresenter
};
}
- private Sampler ResolveSampler(in GuestSampler sampler)
+ private Sampler ResolveSampler(in GuestSampler sampler, Format viewFormat)
{
Span words = stackalloc uint[4] { sampler.Word0, sampler.Word1, sampler.Word2, sampler.Word3 };
- return _samplerStore.GetSampler(new SamplerDescriptorWords(words));
+ return _samplerStore.GetSampler(new SamplerDescriptorWords(words), ViewFormatRules.IsIntegerFormat(viewFormat));
}
// Views for every cached binding after the targets are bound; a stale image is found again first.
@@ -571,12 +588,12 @@ internal static unsafe partial class VulkanVideoPresenter
binding.Image = image.Backing.Handle;
if (!binding.IsStorage)
{
- binding.Sampler = ResolveSampler(binding.SamplerState);
+ binding.Sampler = ResolveSampler(binding.SamplerState, binding.Request.View.Format);
}
}
}
- private CachedImage AcquireStencilStorage(TextureResource binding, CachedImage attachment)
+ private CachedImage AcquireStencilStorage(TextureResource binding, CachedImage attachment, bool writeBack = true)
{
var description = binding.Request.Description;
if (description.Data.Address != attachment.Description.Stencil.Address || description.Data.Size > attachment.Description.Stencil.Size ||
@@ -586,7 +603,7 @@ internal static unsafe partial class VulkanVideoPresenter
throw SubmissionScheduler.Fatal("The stencil storage request does not cover its attachment's stencil layout.");
}
- if (_hasBoundDepth && _boundDepth.Image == binding.ImageIdentifier &&
+ if (writeBack && _hasBoundDepth && _boundDepth.Image == binding.ImageIdentifier &&
(_boundDepthLoadState.StencilTestEnabled || _boundDepthLoadState.StencilClearEnabled))
{
throw SubmissionScheduler.Fatal("A stencil storage write cannot share an active stencil attachment.");
@@ -595,7 +612,16 @@ internal static unsafe partial class VulkanVideoPresenter
var preparation = RequirePreparation();
var stencilImages = preparation.StencilStorageImages ??= new();
if (stencilImages.TryGetValue(attachment, out var storage))
+ {
+ if (writeBack)
+ {
+ preparation.StencilStorageWriteBackImages ??= new();
+ preparation.StencilStorageWriteBackImages.Add(attachment);
+ storage.Binding.ShaderWrite = true;
+ }
+
return storage;
+ }
var sampledRequest = binding.Request with
{
@@ -603,10 +629,19 @@ internal static unsafe partial class VulkanVideoPresenter
View = binding.Request.View with { Usage = ImageUsageFlags.SampledBit },
};
_imageCache.AcquireTextureView(binding.ImageIdentifier, sampledRequest);
- _imageCache.MarkGpuWritten(binding.ImageIdentifier);
+ if (writeBack)
+ {
+ _imageCache.MarkGpuWritten(binding.ImageIdentifier);
+ }
+
storage = attachment.CreateStencilStorageImage();
stencilImages.Add(attachment, storage);
- storage.Binding.ShaderWrite = true;
+ storage.Binding.ShaderWrite = writeBack;
+ if (writeBack)
+ {
+ preparation.StencilStorageWriteBackImages ??= new();
+ preparation.StencilStorageWriteBackImages.Add(attachment);
+ }
attachment.CopyStencilStorage(storage, _bufferCache.GetUtilityBuffer(GpuBufferUsage.DeviceLocal), writeBack: false);
if (_hasBoundDepth && _boundDepth.Image == binding.ImageIdentifier)
{
@@ -619,6 +654,182 @@ internal static unsafe partial class VulkanVideoPresenter
return storage;
}
+ private CachedImage CreateDepthFeedbackSnapshot(CachedImage source, ImageAspectFlags aspect)
+ {
+ if (source.Backing.ImageType != ImageType.Type2D || source.Backing.MipLevels == 0 ||
+ source.Backing.Samples == 0)
+ {
+ throw SubmissionScheduler.Fatal(
+ $"A depth feedback snapshot needs a 2D image: type={source.Backing.ImageType} " +
+ $"mips={source.Backing.MipLevels} samples={source.Backing.Samples}.");
+ }
+
+ var key = new FeedbackSnapshotKey(
+ source.Backing.Format,
+ source.Backing.Extent.Width,
+ source.Backing.Extent.Height,
+ source.Backing.MipLevels,
+ source.Backing.Layers,
+ source.Backing.Samples,
+ ImageAspectFlags.None);
+ var snapshot = RentFeedbackSnapshot(key);
+ if (snapshot is null)
+ {
+ var description = ImageDescription.Create();
+ description.PixelFormat = source.Backing.Format;
+ description.GuestFormat = source.Description.GuestFormat;
+ description.Type = GuestImageType.Color2D;
+ description.Extent = new Extent3D(source.Backing.Extent.Width, source.Backing.Extent.Height, 1);
+ description.Resources = new SubresourceCount(source.Backing.MipLevels, source.Backing.Layers);
+ description.BytesPerBlock = source.Description.BytesPerBlock == 0 ? 1 : source.Description.BytesPerBlock;
+ description.Samples = source.Backing.Samples;
+ // The snapshot is host-owned. Keeping guest placement empty prevents
+ // the image cache from treating it as guest memory that needs upload.
+ description.Pitch = 0;
+ description.TileMode = GuestTileMode.Linear;
+ snapshot = new CachedImage(_deviceInfo, _scheduler, _guestBacking, description);
+ }
+ var range = new SubresourceRange(0, source.Backing.MipLevels, 0, source.Backing.Layers);
+ if ((aspect & (ImageAspectFlags.DepthBit | ImageAspectFlags.StencilBit)) != 0)
+ {
+ snapshot.CopyDepthStencilFrom(
+ source,
+ range,
+ source.Backing.Extent,
+ aspect & (ImageAspectFlags.DepthBit | ImageAspectFlags.StencilBit));
+ var depthView = _boundDepth.Target.Target.Request.View;
+ var writes = _boundDepthLoadState.AttachmentWriteAspects(_boundDepth.Target.Target.Format);
+ source.Transition(
+ _boundDepthLayout,
+ AccessFlags.DepthStencilAttachmentReadBit |
+ (writes != 0 ? AccessFlags.DepthStencilAttachmentWriteBit : 0),
+ new SubresourceRange(depthView.BaseLevel, depthView.LevelCount, depthView.BaseLayer, depthView.LayerCount),
+ BeginBatchedGuestCommands());
+ snapshot.Transition(
+ ImageLayout.DepthStencilReadOnlyOptimal,
+ AccessFlags.ShaderReadBit,
+ range,
+ BeginBatchedGuestCommands());
+ }
+ else
+ {
+ snapshot.CopyFrom(source);
+ snapshot.Transition(
+ ImageLayout.ShaderReadOnlyOptimal,
+ AccessFlags.ShaderReadBit,
+ range,
+ BeginBatchedGuestCommands());
+ }
+
+ var preparation = RequirePreparation();
+ preparation.FeedbackSnapshots ??= new();
+ preparation.FeedbackSnapshots.Add(snapshot);
+ return snapshot;
+ }
+
+ private CachedImage? RentFeedbackSnapshot(FeedbackSnapshotKey key)
+ {
+ if (!_feedbackSnapshotPool.TryGetValue(key, out var entries) || !entries.TryPop(out var snapshot))
+ {
+ return null;
+ }
+
+ _feedbackSnapshotPoolBytes -= snapshot.Backing.AllocationSize;
+ if (entries.Count == 0)
+ {
+ _feedbackSnapshotPool.Remove(key);
+ }
+
+ return snapshot;
+ }
+
+ private void RetireFeedbackSnapshot(CachedImage snapshot)
+ {
+ var key = new FeedbackSnapshotKey(
+ snapshot.Backing.Format,
+ snapshot.Backing.Extent.Width,
+ snapshot.Backing.Extent.Height,
+ snapshot.Backing.MipLevels,
+ snapshot.Backing.Layers,
+ snapshot.Backing.Samples,
+ ImageAspectFlags.None);
+ var allocation = snapshot.Backing.AllocationSize;
+ _feedbackSnapshotPool.TryGetValue(key, out var entries);
+ if (entries is null && _feedbackSnapshotPoolBytes + allocation <= FeedbackSnapshotPoolMaxBytes)
+ {
+ entries = new Stack();
+ _feedbackSnapshotPool[key] = entries;
+ }
+
+ if (entries is not null && entries.Count < FeedbackSnapshotPoolMaxEntriesPerKey &&
+ _feedbackSnapshotPoolBytes + allocation <= FeedbackSnapshotPoolMaxBytes)
+ {
+ entries.Push(snapshot);
+ _feedbackSnapshotPoolBytes += allocation;
+ return;
+ }
+
+ snapshot.Dispose();
+ }
+
+ private void DestroyFeedbackSnapshotPool()
+ {
+ foreach (var entries in _feedbackSnapshotPool.Values)
+ {
+ foreach (var snapshot in entries)
+ {
+ snapshot.Dispose();
+ }
+ }
+
+ _feedbackSnapshotPool.Clear();
+ _feedbackSnapshotPoolBytes = 0;
+ }
+
+ // Vulkan core does not permit a sampled depth aspect to alias a depth
+ // attachment that the same draw may write. Preserve the guest's
+ // read-before-write semantics with a private copy for this draw.
+ private void PrepareDepthFeedback(TextureResource[] bindings)
+ {
+ if (!_hasBoundDepth)
+ {
+ return;
+ }
+
+ var depthImage = _imageCache.GetImage(_boundDepth.Image);
+ var attachmentView = _boundDepth.Target.Target.Request.View;
+ var writes = _boundDepthLoadState.AttachmentWriteAspects(_boundDepth.Target.Target.Format);
+ var writableDepth = writes & ImageAspectFlags.DepthBit;
+ if (writableDepth == 0)
+ {
+ return;
+ }
+
+ foreach (var binding in bindings)
+ {
+ if (binding.IsHostMovie || binding.IsStorage || binding.CachedImage is not { } image ||
+ !ReferenceEquals(image, depthImage) || !ViewsOverlap(binding.Request.View, attachmentView))
+ {
+ continue;
+ }
+
+ var sampledAspect = ViewFormatRules.IsStencilViewFormat(binding.Request.View.Format)
+ ? ImageAspectFlags.StencilBit
+ : ImageAspectFlags.DepthBit;
+ if ((sampledAspect & writableDepth) == 0)
+ {
+ continue;
+ }
+
+ var snapshot = CreateDepthFeedbackSnapshot(image, sampledAspect);
+ binding.CachedImage = snapshot;
+ binding.Image = snapshot.Backing.Handle;
+ binding.View = snapshot.GetOrCreateView(binding.Request.View);
+ binding.MipViews = [];
+ binding.IsFeedbackSnapshot = true;
+ }
+ }
+
// The layout each binding reads through; a target read by its own draw uses the general layout.
private void RecordTextureTransitions(TextureResource[] bindings) =>
RecordDrawTextureTransitions(bindings, null, GuestDepthState.Default);
diff --git a/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.Descriptors.cs b/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.Descriptors.cs
index c3a273a..143755c 100644
--- a/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.Descriptors.cs
+++ b/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.Descriptors.cs
@@ -95,7 +95,8 @@ internal static unsafe partial class VulkanVideoPresenter
OneDimensional: image.Dimension is ImageDimension.Dim1D or ImageDimension.Dim1DArray,
R128: image.R128,
Multisampled: image.Dimension is ImageDimension.Dim2DMsaa or ImageDimension.Dim2DMsaaArray,
- DepthCompare: image.DepthCompare);
+ DepthCompare: image.DepthCompare,
+ Atomic: image.Atomic);
// Render-state discovery for one shader image; the view is acquired later with the draw.
private TextureResource ResolveImageBinding(ImageResource image, uint[] words, ShaderProgramInfo program, int index)
@@ -114,6 +115,20 @@ internal static unsafe partial class VulkanVideoPresenter
imageIdentifier = ImageRequestBuilders.ValidateTextureOwner(_imageCache, imageIdentifier, resolution);
BindImage(imageIdentifier, storage);
var descriptor = new TextureDescriptorWords(words);
+ if (ShouldTraceTextureBindings())
+ {
+ var cached = _imageCache.GetImage(imageIdentifier);
+ var description = cached.Description;
+ Console.Error.WriteLine(
+ $"TextureBinding stage={program.Stage} hash=0x{program.Hash:X16} index={index} " +
+ $"address=0x{new TextureDescriptorWords(words).BaseAddress:X16} " +
+ $"descriptor={descriptor.BaseAddress:X16} size={descriptor.Width + 1}x{descriptor.Height + 1} " +
+ $"format={(uint)descriptor.Format} tile={(uint)descriptor.TileMode} " +
+ $"image=0x{description.Data.Address:X16} size=0x{description.Data.Size:X} " +
+ $"extent={description.Extent.Width}x{description.Extent.Height} pitch={description.Pitch} " +
+ $"guestFormat={(uint)description.GuestFormat} imageTile={(uint)description.TileMode} " +
+ $"backing={cached.Backing.Extent.Width}x{cached.Backing.Extent.Height} format={cached.Backing.Format}");
+ }
return new TextureResource
{
Address = descriptor.BaseAddress,
@@ -128,7 +143,31 @@ internal static unsafe partial class VulkanVideoPresenter
}
// Compare bits stay only on depth-compare samplers; a forced point sampler drops its filters.
- private Sampler ResolveSampler(SamplerResource sampler, uint[] words, ShaderProgramInfo program, int index, ShaderStageResources stage)
+ // A sampler takes the numeric class of the views it samples; integer only when every
+ // paired view is integer, since a float view needs a float border and filtering.
+ private static bool SamplesIntegerViews(ShaderResourceInfo info, TextureResource[] images, int sampler)
+ {
+ var paired = false;
+ foreach (var pair in info.SampledPairs)
+ {
+ if (pair.Sampler != sampler || pair.Image >= images.Length || images[pair.Image].IsHostMovie)
+ {
+ continue;
+ }
+
+ if (!ViewFormatRules.IsIntegerFormat(images[pair.Image].Request.View.Format))
+ {
+ return false;
+ }
+
+ paired = true;
+ }
+
+ return paired;
+ }
+
+ private Sampler ResolveSampler(SamplerResource sampler, uint[] words, ShaderProgramInfo program, int index, ShaderStageResources stage,
+ bool integerView)
{
if (words.Length < 4)
{
@@ -164,7 +203,7 @@ internal static unsafe partial class VulkanVideoPresenter
$"user_data=[{string.Join(",", stage.Resources.UserData.Select(word => $"{word:X8}"))}]");
}
- return _samplerStore.GetSampler(descriptor);
+ return _samplerStore.GetSampler(descriptor, integerView);
}
// The guest textures the movie path matches; built only while a decoded frame is active.
@@ -216,7 +255,8 @@ internal static unsafe partial class VulkanVideoPresenter
descriptors.Samplers = new Sampler[info.Samplers.Count];
for (var index = 0; index < info.Samplers.Count; index++)
{
- descriptors.Samplers[index] = ResolveSampler(info.Samplers[index], snapshot.Samplers[index], program, index, stage);
+ descriptors.Samplers[index] = ResolveSampler(info.Samplers[index], snapshot.Samplers[index], program, index, stage,
+ SamplesIntegerViews(info, descriptors.Images, index));
}
var shaderData = new uint[layout.ShaderDataDwordCount];
@@ -852,6 +892,7 @@ internal static unsafe partial class VulkanVideoPresenter
{
DebugName = bindPoint == PipelineBindPoint.Compute ? "SharpEmu dispatch" : "SharpEmu draw",
Textures = textures,
+ FeedbackSnapshots = preparation.FeedbackSnapshots?.ToArray() ?? [],
OverflowBuffers = preparation.OverflowBuffers.Count == 0 ? null : preparation.OverflowBuffers.ToArray(),
});
preparation.OverflowBuffers.Clear();
diff --git a/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.Device.Setup.cs b/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.Device.Setup.cs
index e84876f..f638a4f 100644
--- a/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.Device.Setup.cs
+++ b/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.Device.Setup.cs
@@ -50,6 +50,9 @@ internal static unsafe partial class VulkanVideoPresenter
private string? _pipelineCachePath;
private bool _pipelineCacheDirty;
private long _lastPipelineCacheSaveTick;
+ // Reading a mature driver cache can itself take several seconds. Keep the
+ // render thread focused on pipeline warm-up; teardown still saves eagerly.
+ private const long PipelineCacheCheckpointIntervalMs = 300_000;
private Queue _queue;
private uint _queueFamilyIndex;
@@ -669,6 +672,7 @@ internal static unsafe partial class VulkanVideoPresenter
VertexPipelineStoresAndAtomics = supportedFeatures.VertexPipelineStoresAndAtomics,
FragmentStoresAndAtomics = supportedFeatures.FragmentStoresAndAtomics,
ShaderInt64 = supportedFeatures.ShaderInt64,
+ ShaderFloat64 = supportedFeatures.ShaderFloat64,
ShaderImageGatherExtended = supportedFeatures.ShaderImageGatherExtended,
ShaderStorageImageExtendedFormats = supportedFeatures.ShaderStorageImageExtendedFormats,
ShaderStorageImageReadWithoutFormat = supportedFeatures.ShaderStorageImageReadWithoutFormat,
@@ -699,6 +703,15 @@ internal static unsafe partial class VulkanVideoPresenter
"The device lacks the shaderInt64 feature, which the device-address programs need.");
}
+ if (!supportedFeatures.ShaderFloat64)
+ {
+ Console.Error.WriteLine(
+ "[LOADER][WARN] GPU does not support shaderFloat64 " +
+ "translated shaders using double precision will fail.");
+ }
+
+
+
if (!supportedFeatures.VertexPipelineStoresAndAtomics || !supportedFeatures.FragmentStoresAndAtomics)
{
Console.Error.WriteLine(
@@ -786,19 +799,33 @@ internal static unsafe partial class VulkanVideoPresenter
SType = StructureType.PhysicalDeviceBufferDeviceAddressFeatures,
PNext = &timelineSemaphoreFeatures,
};
+ var atomicInt64Features = new PhysicalDeviceShaderAtomicInt64Features
+ {
+ SType = StructureType.PhysicalDeviceShaderAtomicInt64Features,
+ PNext = &addressFeatures,
+ };
var featuresQuery = new PhysicalDeviceFeatures2
{
SType = StructureType.PhysicalDeviceFeatures2,
- PNext = &addressFeatures,
+ PNext = &atomicInt64Features,
};
_vk.GetPhysicalDeviceFeatures2(_physicalDevice, &featuresQuery);
var supportsTimelineSemaphore = timelineSemaphoreFeatures.TimelineSemaphore;
var supportsBufferDeviceAddress = addressFeatures.BufferDeviceAddress;
+ var supportsSharedInt64Atomics = atomicInt64Features.ShaderSharedInt64Atomics;
var supportsMaintenance8 = maintenance8Features.Maintenance8;
var supportsRobustBufferAccess2 = robustness2Features.RobustBufferAccess2;
var supportsRobustImageAccess2 = robustness2Features.RobustImageAccess2;
var supportsNullDescriptor = robustness2Features.NullDescriptor;
var supportsRobustness2 = supportsRobustImageAccess2 || supportsNullDescriptor;
+ SetSharedInt64AtomicsCapability(supportsSharedInt64Atomics);
+ if (!supportsSharedInt64Atomics)
+ {
+ Console.Error.WriteLine(
+ "[LOADER][WARN] GPU does not support shaderSharedInt64Atomics " +
+ "64-bit LDS atomics fall back to a non-atomic pair of 32-bit atomics.");
+ }
+
_vk.GetPhysicalDeviceProperties(_physicalDevice, out var deviceProperties);
var deviceName = SilkMarshal.PtrToString((nint)deviceProperties.DeviceName) ?? "unknown";
RequireRenderingFeature(vulkan13Features.DynamicRendering, "Vulkan 1.3 dynamicRendering", deviceName);
@@ -911,6 +938,17 @@ internal static unsafe partial class VulkanVideoPresenter
PNext = &timelineSemaphoreFeatures,
};
void* renderingChain = &addressFeatures;
+ if (supportsSharedInt64Atomics)
+ {
+ atomicInt64Features = new PhysicalDeviceShaderAtomicInt64Features
+ {
+ SType = StructureType.PhysicalDeviceShaderAtomicInt64Features,
+ ShaderSharedInt64Atomics = true,
+ PNext = renderingChain,
+ };
+ renderingChain = &atomicInt64Features;
+ }
+
if (_supportsFragmentShaderBarycentric)
{
barycentricFeatures.PNext = renderingChain;
@@ -1143,7 +1181,7 @@ internal static unsafe partial class VulkanVideoPresenter
// frame, so saving after every slow creation compounds a warm-up
// hitch into a multi-minute stall. Coalesce all creations into one
// periodic snapshot; shutdown still forces a final save.
- if (Environment.TickCount64 - _lastPipelineCacheSaveTick >= 30_000)
+ if (Environment.TickCount64 - _lastPipelineCacheSaveTick >= PipelineCacheCheckpointIntervalMs)
{
SavePipelineCache(force: false);
}
diff --git a/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.Device.Teardown.cs b/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.Device.Teardown.cs
index bca9f04..cd3fa85 100644
--- a/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.Device.Teardown.cs
+++ b/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.Device.Teardown.cs
@@ -29,6 +29,7 @@ internal static unsafe partial class VulkanVideoPresenter
SavePipelineCache(force: true);
DrainFrameSlots();
CollectCompletedGuestSubmissions(waitForOldest: false);
+ DestroyFeedbackSnapshotPool();
DestroyRenderPipelines();
_descriptorHeap.Dispose();
_imageCache.Dispose();
diff --git a/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.Draws.Recording.cs b/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.Draws.Recording.cs
index ed3a3de..24b12e0 100644
--- a/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.Draws.Recording.cs
+++ b/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.Draws.Recording.cs
@@ -90,6 +90,11 @@ internal static unsafe partial class VulkanVideoPresenter
Environment.GetEnvironmentVariable("SHARPEMU_LOG_AGC_SHADER"),
"1",
StringComparison.Ordinal);
+ private static readonly bool _traceTextureBindingsEnabled =
+ string.Equals(
+ Environment.GetEnvironmentVariable("SHARPEMU_TRACE_TEXTURE_BINDINGS"),
+ "1",
+ StringComparison.Ordinal);
private static readonly uint _traceGuestImageWidth =
uint.TryParse(
Environment.GetEnvironmentVariable("SHARPEMU_TRACE_GUEST_IMAGE_WIDTH"),
@@ -259,6 +264,9 @@ internal static unsafe partial class VulkanVideoPresenter
private static bool ShouldTraceVulkanResources() =>
_traceVulkanResourcesEnabled;
+ private static bool ShouldTraceTextureBindings() =>
+ _traceTextureBindingsEnabled;
+
private void RecycleSubmissionUploads(SubmissionUploadResources resources)
{
using var profileScope = RenderPhaseProfile.MeasureDetail(RenderPhaseProfile.Phase.ResourceDestroy);
@@ -270,6 +278,11 @@ internal static unsafe partial class VulkanVideoPresenter
}
}
+ foreach (var snapshot in resources.FeedbackSnapshots)
+ {
+ RetireFeedbackSnapshot(snapshot);
+ }
+
foreach (var (buffer, memory) in resources.OverflowBuffers ?? [])
{
RecycleHostBuffer(buffer, memory);
diff --git a/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.HostMovie.cs b/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.HostMovie.cs
index ad4a40e..b5aa427 100644
--- a/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.HostMovie.cs
+++ b/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.HostMovie.cs
@@ -137,8 +137,12 @@ internal static unsafe partial class VulkanVideoPresenter
{
using var profileScope = RenderPhaseProfile.MeasureDetail(RenderPhaseProfile.Phase.MovieFramePolling);
if (!HostMovieBridge.TryDecodeNextFrame(
- advanceClock: _hostMovieLumaTextureAddress != 0 &&
- _hostMovieChromaTextureAddress != 0,
+ // Once the host bridge owns a movie, advance its clock on
+ // every presenter tick. Texture discovery can lag behind
+ // the first decoded frame; tying playback to those two
+ // addresses leaves the guest stuck forever on attract
+ // movies when Vulkan selects a different binding layout.
+ advanceClock: true,
out var pixels,
out var width,
out var height,
diff --git a/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.Pipelines.cs b/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.Pipelines.cs
index b286fc9..d8f66d1 100644
--- a/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.Pipelines.cs
+++ b/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.Pipelines.cs
@@ -66,6 +66,7 @@ internal static unsafe partial class VulkanVideoPresenter
bool IShaderPipelineHost.GraphicsSubgroupOperationsEnabled => GraphicsSubgroupOperationsEnabled;
+ bool IShaderPipelineHost.SharedInt64AtomicsEnabled => SharedInt64AtomicsEnabled;
bool IShaderPipelineHost.PerVertexPixelInputsSupported => _supportsPerVertexPixelInputs;
RenderHostLimits IShaderPipelineHost.Limits => _renderHostLimits;
@@ -77,6 +78,16 @@ internal static unsafe partial class VulkanVideoPresenter
{
using var profile = ResourceMaterializationProfile.Measure(ResourceMaterializationProfile.Phase.GuestRead);
word = 0;
+ // Resource planning can inspect a dynamic descriptor before the draw has
+ // supplied a valid guest address. Do not pass an invalid range to the
+ // page tracker: it treats that as an emulator invariant violation and
+ // terminates the process. A failed read lets the materializer reject or
+ // specialize the source normally.
+ if (!_guestMemory.CanRead(address, sizeof(uint)))
+ {
+ return false;
+ }
+
if (!_bufferCache.TrySynchronizeCpuRead(address, sizeof(uint),
SharpEmu.HLE.GuestMemory.GuestMemoryProfile.ReadbackSource.ShaderResourceRead))
{
@@ -98,6 +109,11 @@ internal static unsafe partial class VulkanVideoPresenter
{
using var profile = ResourceMaterializationProfile.Measure(ResourceMaterializationProfile.Phase.CleanGuestRead);
word = 0;
+ if (!_guestMemory.CanRead(address, sizeof(uint)))
+ {
+ return false;
+ }
+
if (_bufferCache.HasGpuDirtyPages(address, sizeof(uint)) ||
_bufferCache.HasGpuDirtyBytes(address, sizeof(uint)) ||
_imageCache.HasGpuModifiedImageBytes(address, sizeof(uint)))
@@ -115,6 +131,19 @@ internal static unsafe partial class VulkanVideoPresenter
return true;
}
+ // The same ownership rules as the two word readers, checked once for a whole range.
+ public bool TryReadResidentGuestBytes(ulong address, Span destination, bool clean)
+ {
+ var size = (ulong)destination.Length;
+ if (_bufferCache.HasGpuDirtyPages(address, size) ||
+ (clean && (_bufferCache.HasGpuDirtyBytes(address, size) || _imageCache.HasGpuModifiedImageBytes(address, size))))
+ {
+ return false;
+ }
+
+ return _guestMemory.TryRead(address, destination);
+ }
+
public ulong CreateShaderModule(IGuestCompiledShader shader, ShaderStage stage, ulong hash, ulong programId)
{
using var profileScope = RenderPhaseProfile.MeasureDetail(RenderPhaseProfile.Phase.ProgramCompile);
diff --git a/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.Rendering.cs b/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.Rendering.cs
index 2f3d3da..bce1b6a 100644
--- a/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.Rendering.cs
+++ b/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.Rendering.cs
@@ -50,6 +50,10 @@ internal static unsafe partial class VulkanVideoPresenter
public Dictionary? StencilStorageImages { get; set; }
+ public HashSet? StencilStorageWriteBackImages { get; set; }
+
+ public List? FeedbackSnapshots { get; set; }
+
public bool Committed { get; set; }
public bool CommandsRecorded { get; set; }
@@ -69,7 +73,8 @@ internal static unsafe partial class VulkanVideoPresenter
{
try
{
- if (CommandsRecorded)
+ if (CommandsRecorded && StencilStorageWriteBackImages is not null &&
+ StencilStorageWriteBackImages.Contains(attachment))
attachment.CopyStencilStorage(storage, owner._bufferCache.GetUtilityBuffer(GpuBufferUsage.DeviceLocal), writeBack: true);
}
finally
@@ -79,6 +84,14 @@ internal static unsafe partial class VulkanVideoPresenter
}
}
+ if (!Committed && FeedbackSnapshots is { } feedbackSnapshots)
+ {
+ foreach (var snapshot in feedbackSnapshots)
+ {
+ snapshot.Dispose();
+ }
+ }
+
if (Committed)
{
return;
@@ -367,6 +380,40 @@ internal static unsafe partial class VulkanVideoPresenter
? entry
: throw SubmissionScheduler.Fatal($"The pipeline handle is unknown: pipeline={pipeline.Pipeline} layout={pipeline.Layout}.");
+ // Vulkan cannot sample a stencil aspect while the same aspect is attached for writes.
+ // Use a per-draw storage copy for read-only stencil feedback and keep the attachment authoritative.
+ private void PrepareStencilFeedback(TextureResource[] bindings)
+ {
+ if (!_hasBoundDepth)
+ {
+ return;
+ }
+
+ var depthImage = _imageCache.GetImage(_boundDepth.Image);
+ var attachmentView = _boundDepth.Target.Target.Request.View;
+ var writes = _boundDepthLoadState.AttachmentWriteAspects(_boundDepth.Target.Target.Format);
+ if ((writes & ImageAspectFlags.StencilBit) == 0)
+ {
+ return;
+ }
+
+ foreach (var binding in bindings)
+ {
+ if (binding.IsHostMovie || binding.CachedImage is not { } image ||
+ !ReferenceEquals(image, depthImage) || !ViewFormatRules.IsStencilViewFormat(binding.Request.View.Format) ||
+ !ViewsOverlap(binding.Request.View, attachmentView))
+ {
+ continue;
+ }
+
+ var storage = AcquireStencilStorage(binding, image, writeBack: false);
+ binding.CachedImage = storage;
+ binding.Image = storage.Backing.Handle;
+ binding.View = storage.GetOrCreateView(binding.Request.View with { Aspect = ImageAspectFlags.ColorBit });
+ binding.MipViews = [];
+ }
+ }
+
// The layout each sampled image reads through; a target read by its own draw uses the general layout.
private void RecordStageTextureTransitions(TextureResource[] bindings)
@@ -404,6 +451,11 @@ internal static unsafe partial class VulkanVideoPresenter
}
}
+ // Create feedback copies after load clears have been materialized;
+ // otherwise a shader would sample the pre-clear contents.
+ PrepareDepthFeedback(bindings);
+ PrepareStencilFeedback(bindings);
+
var command = BeginBatchedGuestCommands();
foreach (var binding in bindings)
{
diff --git a/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.cs b/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.cs
index c549231..8fd8978 100644
--- a/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.cs
+++ b/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.cs
@@ -9,6 +9,7 @@ using SharpEmu.Libs.AvPlayer;
using SharpEmu.Libs.Media;
using SharpEmu.Libs.Gpu;
using SharpEmu.Libs.Gpu.GpuCommands;
+using SharpEmu.Libs.Gpu.Images;
using SharpEmu.ShaderCompiler;
using SharpEmu.ShaderCompiler.Vulkan;
using Silk.NET.Vulkan;
@@ -79,6 +80,15 @@ internal static unsafe partial class VulkanVideoPresenter
private static int _nativeSubgroupSize;
private static int _nativeSubgroupShaderStages;
+ private static int _sharedInt64AtomicsSupported;
+
+ // True when the device supports shaderSharedInt64Atomics, so LDS 64-bit
+ // atomics can be emitted as real 64-bit atomics.
+ internal static bool SharedInt64AtomicsEnabled =>
+ Volatile.Read(ref _sharedInt64AtomicsSupported) != 0;
+
+ private static void SetSharedInt64AtomicsCapability(bool supported) =>
+ Volatile.Write(ref _sharedInt64AtomicsSupported, supported ? 1 : 0);
internal static bool GraphicsSubgroupOperationsEnabled =>
VulkanGraphicsSubgroupPolicy.ShouldUseNativeGraphicsSubgroups(
@@ -233,6 +243,9 @@ internal static unsafe partial class VulkanVideoPresenter
{
public string DebugName = "SharpEmu translated";
public TextureResource[] Textures = [];
+ // Draw-local feedback copies stay alive until the GPU has consumed
+ // the descriptors that reference them.
+ public CachedImage[] FeedbackSnapshots = [];
// Host buffers that took uploads the stream ring could not hold; recycled with the draw.
public (VkBuffer Buffer, DeviceMemory Memory)[]? OverflowBuffers;
}
diff --git a/src/SharpEmu.ShaderCompiler.Metal/Gen5MslTranslator.Alu.cs b/src/SharpEmu.ShaderCompiler.Metal/Gen5MslTranslator.Alu.cs
index 936be1d..fb3dd60 100644
--- a/src/SharpEmu.ShaderCompiler.Metal/Gen5MslTranslator.Alu.cs
+++ b/src/SharpEmu.ShaderCompiler.Metal/Gen5MslTranslator.Alu.cs
@@ -203,7 +203,7 @@ public static partial class Gen5MslTranslator
// form is fma(src0, src1, src2) exactly like the SPIR-V translator.
"VFmaF32" or "VMadF32" or "VMadAkF32" or "VMadMkF32" or "VFmaAkF32" or "VFmaMkF32" =>
FloatResult(instruction, $"fma({F(instruction, 0)}, {F(instruction, 1)}, {F(instruction, 2)})"),
- "VFmaF16" => Float16Result(
+ "VFmaF16" or "VFmaMkF16" or "VFmaAkF16" => Float16Result(
instruction,
destination,
$"fma({F16(instruction, 0)}, {F16(instruction, 1)}, {F16(instruction, 2)})"),
@@ -216,6 +216,17 @@ public static partial class Gen5MslTranslator
"VFractF32" => FloatResult(instruction, $"fract({F(instruction, 0)})"),
"VSqrtF32" => FloatResult(instruction, $"sqrt({F(instruction, 0)})"),
"VRsqF32" => FloatResult(instruction, $"rsqrt({F(instruction, 0)})"),
+ "VRcpF16" => Float16Result(instruction, destination, $"(1.0f / {F16(instruction, 0)})"),
+ "VSqrtF16" => Float16Result(instruction, destination, $"sqrt({F16(instruction, 0)})"),
+ "VLogF16" => Float16Result(instruction, destination, $"log2({F16(instruction, 0)})"),
+ "VExpF16" => Float16Result(instruction, destination, $"exp2({F16(instruction, 0)})"),
+ "VFloorF16" => Float16Result(instruction, destination, $"floor({F16(instruction, 0)})"),
+ "VCeilF16" => Float16Result(instruction, destination, $"ceil({F16(instruction, 0)})"),
+ "VTruncF16" => Float16Result(instruction, destination, $"trunc({F16(instruction, 0)})"),
+ "VRndneF16" => Float16Result(instruction, destination, $"rint({F16(instruction, 0)})"),
+ "VFractF16" => Float16Result(instruction, destination, $"fract({F16(instruction, 0)})"),
+ "VSinF16" => Float16Result(instruction, destination, $"sin({F16(instruction, 0)} * {TauLiteral})"),
+ "VCosF16" => Float16Result(instruction, destination, $"cos({F16(instruction, 0)} * {TauLiteral})"),
"VRsqF16" => Float16Result(
instruction,
destination,
@@ -228,6 +239,12 @@ public static partial class Gen5MslTranslator
"VCosF32" => FloatResult(instruction, $"cos({F(instruction, 0)} * {TauLiteral})"),
"VLdexpF32" =>
FloatResult(instruction, $"ldexp({F(instruction, 0)}, as_type({RawSource(instruction, 1)}))"),
+ "VMin3F16" => Float16Result(instruction, destination,
+ $"fmin(fmin({F16(instruction, 0)}, {F16(instruction, 1)}), {F16(instruction, 2)})"),
+ "VMax3F16" => Float16Result(instruction, destination,
+ $"fmax(fmax({F16(instruction, 0)}, {F16(instruction, 1)}), {F16(instruction, 2)})"),
+ "VMed3F16" => Float16Result(instruction, destination,
+ $"fmax(fmin({F16(instruction, 0)}, {F16(instruction, 1)}), fmin(fmax({F16(instruction, 0)}, {F16(instruction, 1)}), {F16(instruction, 2)}))"),
"VMin3F32" =>
FloatResult(instruction, $"fmin(fmin({F(instruction, 0)}, {F(instruction, 1)}), {F(instruction, 2)})"),
"VMax3F32" =>
@@ -422,8 +439,10 @@ public static partial class Gen5MslTranslator
}
case "VSubbU32":
case "VSubbrevU32":
+ case "VSubCoCiU32":
+ case "VSubrevCoCiU32":
{
- var reverse = instruction.Opcode == "VSubbrevU32";
+ var reverse = instruction.Opcode is "VSubbrevU32" or "VSubrevCoCiU32";
var left = Temp("uint", RawSource(instruction, reverse ? 1 : 0));
var right = Temp("uint", RawSource(instruction, reverse ? 0 : 1));
var borrowIn = instruction.Sources.Count > 2
@@ -1107,7 +1126,18 @@ public static partial class Gen5MslTranslator
return true;
}
+ if (instruction.Opcode == "SFlbitI32B64")
+ {
+ var wide = Temp("ulong", RawSource64(instruction, 0));
+ var result = Temp(
+ "uint",
+ $"{wide} == 0ul ? 0xFFFFFFFFu : (uint)clz({wide})");
+ StoreScalar(destination, result);
+ return true;
+ }
+
if (instruction.Opcode.EndsWith("B64", StringComparison.Ordinal) ||
+ instruction.Opcode == "SAshrI64" ||
instruction.Opcode is "SBfeU64" or "SBfeI64")
{
return TryEmitScalar64(instruction, destination, out error);
@@ -1240,6 +1270,14 @@ public static partial class Gen5MslTranslator
resultExpression = $"({left} - {right})";
sccStatement = $"(((({left} ^ {right})) & ({left} ^ RESULT)) >> 31) != 0u";
break;
+ case "SAddF32":
+ resultExpression = $"as_type(as_type({left}) + as_type({right}))";
+ sccStatement = string.Empty;
+ break;
+ case "SSubF32":
+ resultExpression = $"as_type(as_type({left}) - as_type({right}))";
+ sccStatement = string.Empty;
+ break;
case "SAddcU32":
{
var partial = Temp("uint", $"{left} + {right}");
@@ -1479,6 +1517,16 @@ public static partial class Gen5MslTranslator
out string error)
{
error = string.Empty;
+
+ // S_SWAPPC_B64 is a dynamic call/return operation. The Gen5
+ // translator has already linearized the reachable shader body,
+ // so there is no host program counter to exchange here. Its
+ // single pair operand must not be treated as a binary 64-bit op.
+ if (instruction.Opcode == "SSwappcB64")
+ {
+ return true;
+ }
+
var left = Temp("ulong", RawSource64(instruction, 0));
if (instruction.Opcode.EndsWith("SaveexecB64", StringComparison.Ordinal))
{
@@ -1536,12 +1584,15 @@ public static partial class Gen5MslTranslator
value = $"({quadAny} * 0xFul)";
break;
}
- case "SLshlB64" or "SLshrB64":
+ case "SLshlB64" or "SLshrB64" or "SAshrI64":
{
var shift = Temp("uint", $"({RawSource(instruction, 1)}) & 63u");
- value = instruction.Opcode == "SLshlB64"
- ? $"({left} << {shift})"
- : $"({left} >> {shift})";
+ value = instruction.Opcode switch
+ {
+ "SLshlB64" => $"({left} << {shift})",
+ "SLshrB64" => $"({left} >> {shift})",
+ _ => $"(ulong)(as_type({left}) >> {shift})",
+ };
break;
}
case "SBfmB64":
diff --git a/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.Alu.cs b/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.Alu.cs
index d9e5f95..3a41b8d 100644
--- a/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.Alu.cs
+++ b/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.Alu.cs
@@ -243,6 +243,169 @@ public static partial class Gen5SpirvTranslator
case "VCvtOffF32I4":
result = EmitCvtOffF32I4(instruction);
break;
+ case "VCvtF64I32":
+ case "VCvtF64U32":
+ {
+ var source = GetRawSource(instruction, 0);
+ var converted = _module.AddInstruction(
+ instruction.Opcode == "VCvtF64I32"
+ ? SpirvOp.ConvertSToF
+ : SpirvOp.ConvertUToF,
+ DoubleType(),
+ instruction.Opcode == "VCvtF64I32"
+ ? Bitcast(_intType, source)
+ : source);
+ if (!TryStoreDoubleResult(
+ instruction,
+ destination,
+ converted,
+ out result,
+ out error))
+ {
+ return false;
+ }
+
+ break;
+ }
+ case "VCvtF32F64":
+ {
+ if (!TryGetDoubleSource(instruction, 0, out var source, out error))
+ {
+ return false;
+ }
+
+ result = Bitcast(
+ _uintType,
+ _module.AddInstruction(SpirvOp.FConvert, _floatType, source));
+ break;
+ }
+ case "VRcpF64":
+ {
+ if (!TryGetDoubleSource(instruction, 0, out var source, out error))
+ {
+ return false;
+ }
+
+ var reciprocal = _module.AddInstruction(
+ SpirvOp.FDiv,
+ DoubleType(),
+ Double(1.0),
+ source);
+ if (!TryStoreDoubleResult(
+ instruction,
+ destination,
+ reciprocal,
+ out result,
+ out error))
+ {
+ return false;
+ }
+
+ break;
+ }
+ case "VRsqF64":
+ {
+ if (!TryGetDoubleSource(instruction, 0, out var source, out error))
+ {
+ return false;
+ }
+
+ var root = Ext(31, DoubleType(), source);
+ var reciprocal = _module.AddInstruction(
+ SpirvOp.FDiv,
+ DoubleType(),
+ Double(1.0),
+ root);
+ if (!TryStoreDoubleResult(
+ instruction,
+ destination,
+ reciprocal,
+ out result,
+ out error))
+ {
+ return false;
+ }
+
+ break;
+ }
+ case "VSqrtF64":
+ {
+ if (!TryGetDoubleSource(instruction, 0, out var source, out error))
+ {
+ return false;
+ }
+
+ if (!TryStoreDoubleResult(
+ instruction,
+ destination,
+ Ext(31, DoubleType(), source),
+ out result,
+ out error))
+ {
+ return false;
+ }
+
+ break;
+ }
+ case "VMulF64":
+ {
+ if (!TryGetDoubleSource(instruction, 0, out var left, out error))
+ {
+ return false;
+ }
+
+ if (!TryGetDoubleSource(instruction, 1, out var right, out error))
+ {
+ return false;
+ }
+
+ var product = _module.AddInstruction(
+ SpirvOp.FMul,
+ DoubleType(),
+ left,
+ right);
+ if (!TryStoreDoubleResult(
+ instruction,
+ destination,
+ product,
+ out result,
+ out error))
+ {
+ return false;
+ }
+
+ break;
+ }
+ case "VFmaF64":
+ {
+ if (!TryGetDoubleSource(instruction, 0, out var first, out error))
+ {
+ return false;
+ }
+
+ if (!TryGetDoubleSource(instruction, 1, out var second, out error))
+ {
+ return false;
+ }
+
+ if (!TryGetDoubleSource(instruction, 2, out var third, out error))
+ {
+ return false;
+ }
+
+ var fused = Ext(50, DoubleType(), first, second, third);
+ if (!TryStoreDoubleResult(
+ instruction,
+ destination,
+ fused,
+ out result,
+ out error))
+ {
+ return false;
+ }
+
+ break;
+ }
case "VCvtPkU8F32":
{
var converted = _module.AddInstruction(
@@ -295,6 +458,39 @@ public static partial class Gen5SpirvTranslator
instruction,
Ext(32, _floatType, GetFloatSource(instruction, 0)));
break;
+ case "VRcpF16":
+ result = EmitFloat16Result(instruction, destination, _module.AddInstruction(SpirvOp.FDiv, _floatType, Float(1), GetFloat16Source(instruction, 0)));
+ break;
+ case "VSqrtF16":
+ result = EmitFloat16Result(instruction, destination, Ext(31, _floatType, GetFloat16Source(instruction, 0)));
+ break;
+ case "VLogF16":
+ result = EmitFloat16Result(instruction, destination, Ext(30, _floatType, GetFloat16Source(instruction, 0)));
+ break;
+ case "VExpF16":
+ result = EmitFloat16Result(instruction, destination, Ext(29, _floatType, GetFloat16Source(instruction, 0)));
+ break;
+ case "VFloorF16":
+ result = EmitFloat16Result(instruction, destination, Ext(8, _floatType, GetFloat16Source(instruction, 0)));
+ break;
+ case "VCeilF16":
+ result = EmitFloat16Result(instruction, destination, Ext(9, _floatType, GetFloat16Source(instruction, 0)));
+ break;
+ case "VTruncF16":
+ result = EmitFloat16Result(instruction, destination, Ext(3, _floatType, GetFloat16Source(instruction, 0)));
+ break;
+ case "VRndneF16":
+ result = EmitFloat16Result(instruction, destination, Ext(2, _floatType, GetFloat16Source(instruction, 0)));
+ break;
+ case "VFractF16":
+ result = EmitFloat16Result(instruction, destination, Ext(10, _floatType, GetFloat16Source(instruction, 0)));
+ break;
+ case "VSinF16":
+ result = EmitFloat16Result(instruction, destination, Ext(13, _floatType, _module.AddInstruction(SpirvOp.FMul, _floatType, GetFloat16Source(instruction, 0), Float(MathF.Tau))));
+ break;
+ case "VCosF16":
+ result = EmitFloat16Result(instruction, destination, Ext(14, _floatType, _module.AddInstruction(SpirvOp.FMul, _floatType, GetFloat16Source(instruction, 0), Float(MathF.Tau))));
+ break;
case "VRsqF16":
result = EmitFloat16Result(
instruction,
@@ -395,6 +591,20 @@ public static partial class Gen5SpirvTranslator
case "VMaxF16":
result = EmitFloat16ExtBinary(instruction, destination, 40);
break;
+ case "VFmaF16":
+ case "VFmaMkF16":
+ case "VFmaAkF16":
+ {
+ var fusedBits = EmitPackedF16FusedMultiplyAdd(
+ GetFloat16Source(instruction, 0),
+ GetFloat16Source(instruction, 1),
+ GetFloat16Source(instruction, 2));
+ result = EmitFloat16Result(
+ instruction,
+ destination,
+ Bitcast(_floatType, fusedBits));
+ break;
+ }
case "VMadF32":
case "VFmaF32":
case "VMadMkF32":
@@ -410,17 +620,6 @@ public static partial class Gen5SpirvTranslator
GetFloatSource(instruction, 1),
GetFloatSource(instruction, 2)));
break;
- case "VFmaF16":
- result = EmitFloat16Result(
- instruction,
- destination,
- Ext(
- 50,
- _floatType,
- GetFloat16Source(instruction, 0),
- GetFloat16Source(instruction, 1),
- GetFloat16Source(instruction, 2)));
- break;
case "VMacF32":
case "VFmacF32":
{
@@ -435,6 +634,47 @@ public static partial class Gen5SpirvTranslator
addend));
break;
}
+ case "VDot2cF32F16":
+ {
+ // The two packed half pairs are widened before the
+ // products are accumulated in f32. The c form clamps
+ // the final dot product to the normalized [0, 1] range.
+ var left = GetRawSource(instruction, 0);
+ var right = GetRawSource(instruction, 1);
+ var lowProduct = _module.AddInstruction(
+ SpirvOp.FMul,
+ _floatType,
+ EmitPackedF16Half(left, highLane: false),
+ EmitPackedF16Half(right, highLane: false));
+ var highProduct = _module.AddInstruction(
+ SpirvOp.FMul,
+ _floatType,
+ EmitPackedF16Half(left, highLane: true),
+ EmitPackedF16Half(right, highLane: true));
+ var dot = _module.AddInstruction(SpirvOp.FAdd, _floatType, lowProduct, highProduct);
+ result = EmitFloatResult(
+ instruction,
+ Bitcast(_floatType, EmitClampToUnitInterval(Bitcast(_uintType, dot))));
+ break;
+ }
+ case "VMin3F16":
+ case "VMax3F16":
+ case "VMed3F16":
+ {
+ // Every f16 value widens to f32 exactly, and min/max pick one
+ // input, so the f32 result narrows back without rounding.
+ var a = GetFloat16Source(instruction, 0);
+ var b = GetFloat16Source(instruction, 1);
+ var c = GetFloat16Source(instruction, 2);
+ var value = instruction.Opcode switch
+ {
+ "VMin3F16" => Ext(37, _floatType, Ext(37, _floatType, a, b), c),
+ "VMax3F16" => Ext(40, _floatType, Ext(40, _floatType, a, b), c),
+ _ => Ext(40, _floatType, Ext(37, _floatType, a, b), Ext(37, _floatType, Ext(40, _floatType, a, b), c)),
+ };
+ result = EmitFloat16Result(instruction, destination, value);
+ break;
+ }
case "VMin3F32":
result = EmitFloatTernaryExt(instruction, 37);
break;
@@ -671,6 +911,19 @@ public static partial class Gen5SpirvTranslator
BitwiseAnd(GetRawSource(instruction, 0), lowBitsMask)));
break;
}
+ case "VLshlrevB64":
+ {
+ // D.u64 = S1.u64 << (S0.u32 & 0x3F).
+ var shiftAmount = _module.AddInstruction(
+ SpirvOp.UConvert,
+ _ulongType,
+ BitwiseAnd(GetRawSource(instruction, 0), UInt(63)));
+ var shifted = ShiftLeftLogical64(GetRawSource64(instruction, 1), shiftAmount);
+ result = Narrow(shifted);
+ StoreV(destination + 1, Narrow(ShiftRightLogical64(
+ shifted, _module.Constant64(_ulongType, 32))));
+ break;
+ }
case "VBfmB32":
{
var width = BitwiseAnd(GetRawSource(instruction, 0), UInt(31));
@@ -682,6 +935,30 @@ public static partial class Gen5SpirvTranslator
result = ShiftLeftLogical(lowMask, GetRawSource(instruction, 1));
break;
}
+ case "VMadI32I24":
+ {
+ // D = S0 * S1 + S2 with S0 and S1 taken as signed 24-bit
+ // values; the product wraps in 32 bits before the add.
+ var signedLeft = _module.AddInstruction(
+ SpirvOp.BitFieldSExtract,
+ _intType,
+ Bitcast(_intType, GetRawSource(instruction, 0)),
+ UInt(0),
+ UInt(24));
+ var signedRight = _module.AddInstruction(
+ SpirvOp.BitFieldSExtract,
+ _intType,
+ Bitcast(_intType, GetRawSource(instruction, 1)),
+ UInt(0),
+ UInt(24));
+ var product = _module.AddInstruction(
+ SpirvOp.IMul,
+ _uintType,
+ Bitcast(_uintType, signedLeft),
+ Bitcast(_uintType, signedRight));
+ result = IAdd(product, GetRawSource(instruction, 2));
+ break;
+ }
case "VMadU32U24":
{
var left = BitwiseAnd(
@@ -1156,6 +1433,15 @@ public static partial class Gen5SpirvTranslator
return false;
}
+ break;
+ case "VPkAddI16":
+ result = EmitPackedI16Arithmetic(instruction, subtract: false);
+ break;
+ case "VPkSubI16":
+ result = EmitPackedI16Arithmetic(instruction, subtract: true);
+ break;
+ case "VPkFmacF16":
+ result = EmitPackedF16Fmac(instruction, destination);
break;
case "VFmaMixF32":
case "VFmaMixloF16":
@@ -1166,6 +1452,66 @@ public static partial class Gen5SpirvTranslator
}
break;
+ case "VAddNcI16":
+ case "VSubNcU16":
+ case "VSubNcI16":
+ case "VLshrrevB16":
+ case "VLshlrevB16":
+ case "VAshrrevI16":
+ case "VMaxU16":
+ case "VMinU16":
+ case "VMaxI16":
+ case "VMinI16":
+ {
+ if (instruction.Control is Gen5Vop3Control { Clamp: true })
+ {
+ error = $"{instruction.Opcode}: clamped 16-bit integer results are not supported";
+ return false;
+ }
+
+ var signed = instruction.Opcode.EndsWith("I16", StringComparison.Ordinal);
+ var left = GetInt16Source(instruction, 0, signed);
+ var right = GetInt16Source(instruction, 1, signed);
+ uint value;
+ switch (instruction.Opcode)
+ {
+ case "VAddNcU16":
+ case "VAddNcI16":
+ value = _module.AddInstruction(SpirvOp.IAdd, _uintType, left, right);
+ break;
+ case "VSubNcU16":
+ case "VSubNcI16":
+ value = _module.AddInstruction(SpirvOp.ISub, _uintType, left, right);
+ break;
+ case "VLshrrevB16":
+ value = ShiftRightLogical(right, BitwiseAnd(left, UInt(15)));
+ break;
+ case "VLshlrevB16":
+ value = ShiftLeftLogical(right, BitwiseAnd(left, UInt(15)));
+ break;
+ case "VAshrrevI16":
+ value = ShiftRightArithmetic(right, BitwiseAnd(left, UInt(15)));
+ break;
+ case "VMaxU16":
+ value = Ext(41, _uintType, left, right);
+ break;
+ case "VMinU16":
+ value = Ext(38, _uintType, left, right);
+ break;
+ default:
+ value = Bitcast(
+ _uintType,
+ Ext(
+ instruction.Opcode == "VMaxI16" ? 42u : 39u,
+ _intType,
+ Bitcast(_intType, left),
+ Bitcast(_intType, right)));
+ break;
+ }
+
+ result = EmitInt16Result(instruction, destination, value);
+ break;
+ }
default:
error = $"unsupported vector opcode {instruction.Opcode}";
return false;
@@ -1297,6 +1643,37 @@ public static partial class Gen5SpirvTranslator
return true;
}
+ // V_PK_ADD/SUB_I16 operates on the selected 16-bit halves of src0 and
+ // src1. The packed integer result is modulo 16 bits in each lane; the
+ // signed and unsigned forms therefore share the same bit-level
+ // implementation.
+ private uint EmitPackedI16Arithmetic(Gen5ShaderInstruction instruction, bool subtract)
+ {
+ var control = (Gen5Vop3pControl)instruction.Control!;
+ var left = GetRawSource(instruction, 0);
+ var right = GetRawSource(instruction, 1);
+ uint Arithmetic(uint a, uint b) => subtract ? ISubU(a, b) : IAdd(a, b);
+
+ uint SelectHalf(uint value, uint mask, int source)
+ {
+ return ((mask >> source) & 1) != 0
+ ? ShiftRightLogical(value, UInt(16))
+ : value;
+ }
+
+ var low = BitwiseAnd(
+ Arithmetic(
+ BitwiseAnd(SelectHalf(left, control.OpSelMask, 0), UInt(0xFFFF)),
+ BitwiseAnd(SelectHalf(right, control.OpSelMask, 1), UInt(0xFFFF))),
+ UInt(0xFFFF));
+ var high = BitwiseAnd(
+ Arithmetic(
+ BitwiseAnd(SelectHalf(left, control.OpSelHiMask, 0), UInt(0xFFFF)),
+ BitwiseAnd(SelectHalf(right, control.OpSelHiMask, 1), UInt(0xFFFF))),
+ UInt(0xFFFF));
+ return BitwiseOr(low, ShiftLeftLogical(high, UInt(16)));
+ }
+
// V_FMA_MIX_F32 / _MIXLO_F16 / _MIXHI_F16 (VOP3P opcodes 0x20 / 0x21 /
// 0x22). Unlike the packed v_pk_* ops these compute a single f32
// fma(a, b, c): each of the three sources is *independently* read as
@@ -1436,6 +1813,37 @@ public static partial class Gen5SpirvTranslator
return EmitFloatToHalf(value);
}
+ // V_PK_FMAC_F16 (VOP2 0x3C): each result half is a fused f16 multiply-add
+ // whose addend is the matching half of the old destination, so the packed
+ // register is read as D = S0 * S1 + D for both lanes at once.
+ private uint EmitPackedF16Fmac(
+ Gen5ShaderInstruction instruction,
+ uint destination)
+ {
+ var accumulator = LoadV(destination);
+ var low = EmitPackedF16FmacLane(instruction, accumulator, highLane: false);
+ var high = EmitPackedF16FmacLane(instruction, accumulator, highLane: true);
+ return BitwiseOr(low, ShiftLeftLogical(high, UInt(16)));
+ }
+
+ private uint EmitPackedF16FmacLane(
+ Gen5ShaderInstruction instruction,
+ uint accumulator,
+ bool highLane)
+ {
+ var left = EmitPackedF16Half(GetRawSource(instruction, 0), highLane);
+ var right = EmitPackedF16Half(GetRawSource(instruction, 1), highLane);
+ var addend = EmitPackedF16Half(accumulator, highLane);
+ return EmitFloatToHalf(
+ EmitPackedF16FusedMultiplyAdd(left, right, addend));
+ }
+
+ private uint EmitPackedF16Half(uint raw, bool highLane)
+ {
+ var half = highLane ? ShiftRightLogical(raw, UInt(16)) : raw;
+ return Bitcast(_floatType, EmitHalfToFloat(half));
+ }
+
// Saturates an f32 bit pattern to [0, 1] the way the VOP3P clamp modifier
// does: below 0 (and NaN, since the ordered compare is false for it) becomes
// 0, above 1 becomes 1. Ordered compares match the hardware's NaN-to-zero
@@ -1527,6 +1935,22 @@ public static partial class Gen5SpirvTranslator
int index,
bool highLane)
{
+ var operand = instruction.Sources[index];
+ if (operand.Kind is Gen5OperandKind.EncodedConstant or Gen5OperandKind.LiteralConstant)
+ {
+ // An inline constant or literal is a single f32 value, not a
+ // packed register: both lanes take the exactly-converted f16 of
+ // the same constant, so there is no half to select.
+ var constant = GetFloatSource(instruction, index);
+ var constantNegate = highLane ? control.NegHiMask : control.NegLoMask;
+ if (((constantNegate >> index) & 1) != 0)
+ {
+ constant = _module.AddInstruction(SpirvOp.FNegate, _floatType, constant);
+ }
+
+ return constant;
+ }
+
var raw = GetRawSource(instruction, index);
var selectMask = highLane ? control.OpSelHiMask : control.OpSelMask;
var half = ((selectMask >> index) & 1) != 0
@@ -1869,6 +2293,70 @@ public static partial class Gen5SpirvTranslator
condition = _module.AddInstruction(operation, _boolType, left, right);
}
+ else if (opcode.EndsWith("U64", StringComparison.Ordinal))
+ {
+ if (opcode is "VCmpFU64" or "VCmpxFU64")
+ {
+ condition = _module.ConstantBool(false);
+ }
+ else if (opcode is "VCmpTU64" or "VCmpxTU64")
+ {
+ condition = _module.ConstantBool(true);
+ }
+ else
+ {
+ var wideLeft = GetRawSource64(instruction, 0);
+ var wideRight = GetRawSource64(instruction, 1);
+ var operation = opcode switch
+ {
+ "VCmpEqU64" or "VCmpxEqU64" => SpirvOp.IEqual,
+ "VCmpNeU64" or "VCmpxNeU64" => SpirvOp.INotEqual,
+ "VCmpLtU64" or "VCmpxLtU64" => SpirvOp.ULessThan,
+ "VCmpLeU64" or "VCmpxLeU64" => SpirvOp.ULessThanEqual,
+ "VCmpGtU64" or "VCmpxGtU64" => SpirvOp.UGreaterThan,
+ "VCmpGeU64" or "VCmpxGeU64" => SpirvOp.UGreaterThanEqual,
+ _ => SpirvOp.Nop,
+ };
+ if (operation == SpirvOp.Nop)
+ {
+ error = $"unsupported u64 compare {opcode}";
+ return false;
+ }
+
+ condition = _module.AddInstruction(operation, _boolType, wideLeft, wideRight);
+ }
+ }
+ else if (opcode.EndsWith("U16", StringComparison.Ordinal) || opcode.EndsWith("I16", StringComparison.Ordinal))
+ {
+ // VOPC 16-bit integer comparisons consume the low half of each VGPR.
+ // Signed comparisons sign-extend that half before the SPIR-V operation.
+ var signed = opcode.EndsWith("I16", StringComparison.Ordinal);
+ var left = GetInteger16Source(instruction, 0, signed);
+ var right = GetInteger16Source(instruction, 1, signed);
+ var operation = opcode switch
+ {
+ "VCmpEqI16" or "VCmpxEqI16" or
+ "VCmpEqU16" or "VCmpxEqU16" => SpirvOp.IEqual,
+ "VCmpNeI16" or "VCmpxNeI16" or
+ "VCmpNeU16" or "VCmpxNeU16" => SpirvOp.INotEqual,
+ "VCmpLtI16" or "VCmpxLtI16" => SpirvOp.SLessThan,
+ "VCmpLeI16" or "VCmpxLeI16" => SpirvOp.SLessThanEqual,
+ "VCmpGtI16" or "VCmpxGtI16" => SpirvOp.SGreaterThan,
+ "VCmpGeI16" or "VCmpxGeI16" => SpirvOp.SGreaterThanEqual,
+ "VCmpLtU16" or "VCmpxLtU16" => SpirvOp.ULessThan,
+ "VCmpLeU16" or "VCmpxLeU16" => SpirvOp.ULessThanEqual,
+ "VCmpGtU16" or "VCmpxGtU16" => SpirvOp.UGreaterThan,
+ "VCmpGeU16" or "VCmpxGeU16" => SpirvOp.UGreaterThanEqual,
+ _ => SpirvOp.Nop,
+ };
+ if (operation == SpirvOp.Nop)
+ {
+ error = $"unsupported 16-bit integer compare {opcode}";
+ return false;
+ }
+
+ condition = _module.AddInstruction(operation, _boolType, left, right);
+ }
else if (opcode is not ("VCmpClassF32" or "VCmpxClassF32"))
{
var compare64 = opcode.EndsWith("U64", StringComparison.Ordinal) ||
@@ -2116,13 +2604,62 @@ public static partial class Gen5SpirvTranslator
return true;
}
+ if (instruction.Opcode == "SFlbitI32B64")
+ {
+ var wide = GetRawSource64(instruction, 0);
+ var low = _module.AddInstruction(
+ SpirvOp.UConvert,
+ _uintType,
+ wide);
+ var high = _module.AddInstruction(
+ SpirvOp.UConvert,
+ _uintType,
+ ShiftRightLogical64(
+ wide,
+ _module.Constant64(_ulongType, 32)));
+ var highClz = _module.AddInstruction(
+ SpirvOp.ISub,
+ _uintType,
+ UInt(31),
+ Ext(74, _uintType, high));
+ var lowClz = _module.AddInstruction(
+ SpirvOp.ISub,
+ _uintType,
+ UInt(31),
+ Ext(74, _uintType, low));
+ var lowResult = IAdd(lowClz, UInt(32));
+ var leadingZeroResult = _module.AddInstruction(
+ SpirvOp.Select,
+ _uintType,
+ IsNotZero(high),
+ highClz,
+ _module.AddInstruction(
+ SpirvOp.Select,
+ _uintType,
+ IsNotZero(low),
+ lowResult,
+ UInt(uint.MaxValue)));
+ StoreS(destination, leadingZeroResult);
+ return true;
+ }
+
if (instruction.Opcode.EndsWith("B64", StringComparison.Ordinal) ||
+ instruction.Opcode == "SAshrI64" ||
instruction.Opcode is "SWqmB64" or "SBfeU64" or "SBfeI64")
{
return TryEmitScalar64(instruction, destination, out error);
}
var left = GetRawSource(instruction, 0);
+ if (instruction.Opcode == "SBitreplicateB64B32")
+ {
+ // S_BITREPLICATE_B64_B32 broadcasts the source dword into both
+ // halves of its 64-bit SGPR destination and does not update SCC.
+ StoreS(destination, left);
+ StoreS(destination + 1, left);
+ return true;
+ }
+
if (instruction.Opcode.EndsWith("SaveexecB32", StringComparison.Ordinal))
{
var oldExec64 = BooleanToWaveMask(Load(_boolType, _exec));
@@ -2227,8 +2764,21 @@ public static partial class Gen5SpirvTranslator
result = Ext(73, _uintType, left);
StoreS(destination, result);
return true;
+ case "SFlbitI32B32":
+ {
+ // Count leading zero bits, 0xFFFFFFFF when the source is zero.
+ var msb = Ext(74, _uintType, left);
+ var clz = _module.AddInstruction(SpirvOp.ISub, _uintType, UInt(31), msb);
+ result = _module.AddInstruction(
+ SpirvOp.Select, _uintType, IsNotZero(left), clz, UInt(0xFFFFFFFFu));
+ StoreS(destination, result);
+ return true;
+ }
case "SBitset0B32":
case "SBitset1B32":
+ // S_BITSET*_B32 is a read-modify-write of the destination:
+ // it sets the single bit selected by SSRC0[4:0]. Neither
+ // form writes SCC.
result = _module.AddInstruction(
SpirvOp.BitFieldInsert,
_uintType,
@@ -2281,6 +2831,24 @@ public static partial class Gen5SpirvTranslator
right);
Store(_scc, SignedSubOverflow(left, right, result));
break;
+ case "SAddF32":
+ result = Bitcast(
+ _uintType,
+ _module.AddInstruction(
+ SpirvOp.FAdd,
+ _floatType,
+ Bitcast(_floatType, left),
+ Bitcast(_floatType, right)));
+ break;
+ case "SSubF32":
+ result = Bitcast(
+ _uintType,
+ _module.AddInstruction(
+ SpirvOp.FSub,
+ _floatType,
+ Bitcast(_floatType, left),
+ Bitcast(_floatType, right)));
+ break;
case "SAddcU32":
{
var carryIn = _module.AddInstruction(
@@ -2362,6 +2930,33 @@ public static partial class Gen5SpirvTranslator
left,
right);
break;
+ case "SMulHiI32":
+ {
+ var wideLeft = _module.AddInstruction(
+ SpirvOp.SConvert,
+ _longType,
+ Bitcast(_intType, left));
+ var wideRight = _module.AddInstruction(
+ SpirvOp.SConvert,
+ _longType,
+ Bitcast(_intType, right));
+ var product = _module.AddInstruction(
+ SpirvOp.IMul,
+ _longType,
+ wideLeft,
+ wideRight);
+ result = Bitcast(
+ _uintType,
+ _module.AddInstruction(
+ SpirvOp.SConvert,
+ _intType,
+ _module.AddInstruction(
+ SpirvOp.ShiftRightArithmetic,
+ _longType,
+ product,
+ _module.Constant64(_longType, 32))));
+ break;
+ }
case "SMulHiU32":
{
var product = _module.AddInstruction(
@@ -2710,6 +3305,43 @@ public static partial class Gen5SpirvTranslator
out string error)
{
error = string.Empty;
+
+ // S_SETPC_B64 and S_SWAPPC_B64 are dynamic call/return operations. The Gen5
+ // translator has already linearized the reachable shader body,
+ // so there is no host program counter to exchange here. Keep
+ // the instruction as a control-flow marker and continue with the
+ // linearized body; unlike the binary scalar operations it has a
+ // single pair operand (s[dst:dst+1] <- s[src:src+1]).
+ if (instruction.Opcode is "SSetpcB64" or "SSwappcB64")
+ {
+ return true;
+ }
+
+ // S_BITSET0/1_B64 use a 32-bit bit index and update the 64-bit
+ // value already held at the destination; they do not have a
+ // scalar 64-bit source operand.
+ if (instruction.Opcode is "SBitset0B64" or "SBitset1B64")
+ {
+ var bitIndex = Widen(BitwiseAnd(GetRawSource(instruction, 0), UInt(63)));
+ var selected = ShiftLeftLogical64(
+ _module.Constant64(_ulongType, 1),
+ bitIndex);
+ var current = LoadS64(destination);
+ var updated = instruction.Opcode == "SBitset1B64"
+ ? _module.AddInstruction(
+ SpirvOp.BitwiseOr,
+ _ulongType,
+ current,
+ selected)
+ : _module.AddInstruction(
+ SpirvOp.BitwiseAnd,
+ _ulongType,
+ current,
+ _module.AddInstruction(SpirvOp.Not, _ulongType, selected));
+ StoreS64(destination, updated);
+ return true;
+ }
+
var left = GetRawSource64(instruction, 0);
if (instruction.Opcode.EndsWith("SaveexecB64", StringComparison.Ordinal))
{
@@ -2782,13 +3414,20 @@ public static partial class Gen5SpirvTranslator
return false;
}
+ if (!_emulateWave64 && _waveLaneCount != 64)
+ {
+ newExec = BitwiseAnd(
+ newExec,
+ _module.Constant64(_ulongType, 0xFFFF_FFFFUL));
+ }
+
StoreS64(destination, oldExec);
StoreS64(126, newExec);
Store(_scc, IsNotZero64(newExec));
return true;
}
- if (instruction.Opcode is "SLshlB64" or "SLshrB64")
+ if (instruction.Opcode is "SLshlB64" or "SLshrB64" or "SAshrI64")
{
if (instruction.Sources.Count < 2)
{
@@ -2800,9 +3439,12 @@ public static partial class Gen5SpirvTranslator
SpirvOp.UConvert,
_ulongType,
GetRawSource(instruction, 1));
- var shiftedValue = instruction.Opcode == "SLshlB64"
- ? ShiftLeftLogical64(left, shift)
- : ShiftRightLogical64(left, shift);
+ var shiftedValue = instruction.Opcode switch
+ {
+ "SLshlB64" => ShiftLeftLogical64(left, shift),
+ "SLshrB64" => ShiftRightLogical64(left, shift),
+ _ => ShiftRightArithmetic64(left, shift),
+ };
StoreS64(destination, shiftedValue);
Store(_scc, IsNotZero64(shiftedValue));
return true;
@@ -3443,6 +4085,17 @@ public static partial class Gen5SpirvTranslator
sourceAllowsWrite));
}
+ private uint GetInteger16Source(
+ Gen5ShaderInstruction instruction,
+ int sourceIndex,
+ bool signed)
+ {
+ var value = BitwiseAnd(GetRawSource(instruction, sourceIndex), UInt(0xFFFF));
+ return signed
+ ? ShiftRightArithmetic(ShiftLeftLogical(value, UInt(16)), UInt(16))
+ : value;
+ }
+
private uint GetFloat16Source(
Gen5ShaderInstruction instruction,
int sourceIndex)
@@ -3594,6 +4247,12 @@ public static partial class Gen5SpirvTranslator
high);
}
+ if (operand.Kind == Gen5OperandKind.EncodedConstant &&
+ Gen5InlineConstants.IsAperture(operand.Value))
+ {
+ return _module.Constant64(_ulongType, Gen5InlineConstants.DecodeAperture64(operand.Value));
+ }
+
// Scalar inline negative constants are signed immediates. B64
// consumers sign-extend them, so -1 denotes a full 64-bit mask.
if (operand.Kind == Gen5OperandKind.EncodedConstant &&
@@ -3631,6 +4290,117 @@ public static partial class Gen5SpirvTranslator
_module.AddInstruction(SpirvOp.UConvert, _uintType, high));
}
+ private uint DoubleType()
+ {
+ _module.AddCapability(SpirvCapability.Float64);
+ return _module.TypeFloat(64);
+ }
+
+ private uint Double(double value) =>
+ _module.Constant64(
+ DoubleType(),
+ unchecked((ulong)BitConverter.DoubleToInt64Bits(value)));
+
+ // FP64 operands live in a register pair. The gfx10 inline-constant
+ // and literal encodings follow different expansion rules than the
+ // integer 64-bit forms, so refuse them instead of reading the wrong
+ // bits as a double.
+ private bool TryGetDoubleSource(
+ Gen5ShaderInstruction instruction,
+ int sourceIndex,
+ out uint value,
+ out string error)
+ {
+ error = string.Empty;
+ value = 0;
+ var operand = instruction.Sources[sourceIndex];
+ if (operand.Kind != Gen5OperandKind.VectorRegister &&
+ operand.Kind != Gen5OperandKind.ScalarRegister)
+ {
+ error = $"FP64 source {sourceIndex} of {instruction.Opcode} is not a register pair";
+ return false;
+ }
+
+ value = Bitcast(DoubleType(), GetRawSource64(instruction, sourceIndex));
+
+ var absoluteMask = 0u;
+ var negateMask = 0u;
+ switch (instruction.Control)
+ {
+ case Gen5Vop3Control vop3:
+ absoluteMask = vop3.AbsoluteMask;
+ negateMask = vop3.NegateMask;
+ break;
+ case Gen5SdwaControl sdwa:
+ absoluteMask = sdwa.AbsoluteMask;
+ negateMask = sdwa.NegateMask;
+ break;
+ }
+
+ if ((absoluteMask & (1u << sourceIndex)) != 0)
+ {
+ value = Ext(4, DoubleType(), value);
+ }
+
+ if ((negateMask & (1u << sourceIndex)) != 0)
+ {
+ value = _module.AddInstruction(SpirvOp.FNegate, DoubleType(), value);
+ }
+
+ return true;
+ }
+
+ // Applies the VOP3 output modifier and clamp to a double result, then
+ // splits it across the destination register pair. The low word is
+ // returned for the caller's common destination store; the high word is
+ // stored here so both words share the same EXEC guard.
+ private bool TryStoreDoubleResult(
+ Gen5ShaderInstruction instruction,
+ uint destination,
+ uint doubleValue,
+ out uint low,
+ out string error)
+ {
+ error = string.Empty;
+ low = 0;
+ var outputModifier = 0u;
+ var clamp = false;
+ switch (instruction.Control)
+ {
+ case Gen5Vop3Control control:
+ outputModifier = control.OutputModifier;
+ clamp = control.Clamp;
+ break;
+ case null:
+ break;
+ default:
+ error = $"FP64 output control {instruction.Control.GetType().Name} is not implemented in {instruction.Opcode}";
+ return false;
+ }
+
+ var type = DoubleType();
+ doubleValue = outputModifier switch
+ {
+ 1 => _module.AddInstruction(SpirvOp.FMul, type, doubleValue, Double(2.0)),
+ 2 => _module.AddInstruction(SpirvOp.FMul, type, doubleValue, Double(4.0)),
+ 3 => _module.AddInstruction(SpirvOp.FMul, type, doubleValue, Double(0.5)),
+ _ => doubleValue,
+ };
+ if (clamp)
+ {
+ doubleValue = Ext(43, type, doubleValue, Double(0.0), Double(1.0));
+ }
+
+ var bits = Bitcast(_ulongType, doubleValue);
+ var high = _module.AddInstruction(
+ SpirvOp.UConvert,
+ _uintType,
+ ShiftRightLogical64(bits, _module.Constant64(_ulongType, 32)));
+ StoreV(destination + 1, high);
+ low = _module.AddInstruction(SpirvOp.UConvert, _uintType, bits);
+ return true;
+ }
+
private uint EmitFloat16Binary(
Gen5ShaderInstruction instruction,
uint destination,
@@ -4015,6 +4785,24 @@ public static partial class Gen5SpirvTranslator
return result;
}
+ // A lane selector reduced to the guest wave: bits [5:0] for wave64, [4:0] for wave32.
+ private uint GuestLaneSelect(uint selector) =>
+ BitwiseAnd(selector, UInt(_waveLaneCount == 64 ? 63u : RdnaWaveLaneCount - 1));
+
+ // An emulated wave64 spans two host subgroups, so a subgroup broadcast cannot reach
+ // the other half. The selected guest lane publishes through workgroup scratch, like
+ // BroadcastFirstWave64Active; the trailing barrier keeps the next use from racing.
+ private uint BroadcastWave64Lane(uint value, uint lane)
+ {
+ EmitConditional(
+ _module.AddInstruction(SpirvOp.IEqual, _boolType, GuestWaveLane(), lane),
+ () => Store(WaveBroadcastScratchPointer(), value));
+ EmitWave64Barrier();
+ var result = Load(_uintType, WaveBroadcastScratchPointer());
+ EmitWave64Barrier();
+ return result;
+ }
+
private uint BroadcastFirstWave64Active(uint value)
{
var lane = GuestWaveLane();
@@ -4215,6 +5003,30 @@ public static partial class Gen5SpirvTranslator
UInt(0));
}
+ private uint GetInt16Source(Gen5ShaderInstruction instruction, int sourceIndex, bool signed)
+ {
+ var raw = GetRawSource(instruction, sourceIndex, applySdwaIntegerModifiers: false);
+ if (instruction.Control is Gen5Vop3Control control && (control.OperandSelect & (1u << sourceIndex)) != 0)
+ {
+ raw = ShiftRightLogical(raw, UInt(16));
+ }
+
+ var half = BitwiseAnd(raw, UInt(0xFFFF));
+ return signed
+ ? ShiftRightArithmetic(ShiftLeftLogical(half, UInt(16)), UInt(16))
+ : half;
+ }
+
+ private uint EmitInt16Result(Gen5ShaderInstruction instruction, uint destination, uint value)
+ {
+ var control = instruction.Control as Gen5Vop3Control;
+ var half = BitwiseAnd(value, UInt(0xFFFF));
+ var current = LoadV(destination);
+ return ((control?.OperandSelect ?? 0) & 8) != 0
+ ? BitwiseOr(BitwiseAnd(current, UInt(0x0000_FFFF)), ShiftLeftLogical(half, UInt(16)))
+ : BitwiseOr(BitwiseAnd(current, UInt(0xFFFF_0000)), half);
+ }
+
private uint EmitFloat16Result(
Gen5ShaderInstruction instruction,
uint destination,
diff --git a/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.RayIntersect.cs b/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.RayIntersect.cs
new file mode 100644
index 0000000..0e2bedf
--- /dev/null
+++ b/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.RayIntersect.cs
@@ -0,0 +1,324 @@
+// Copyright (C) 2026 SharpEmu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+using SharpEmu.ShaderCompiler;
+
+namespace SharpEmu.ShaderCompiler.Vulkan;
+
+// IMAGE_BVH_INTERSECT_RAY tests one ray against one BVH node; the guest shader owns the
+// traversal loop and stack. This is the GFX10 (RT IP 1.1) node test as AMD's GPURT
+// software fallback defines it (IntersectCommon.hlsl, image_bvh64_intersect_ray_base),
+// with node layouts from GPURT's gfx10 TriangleNode1_0 and BoxNode1_0.
+public static partial class Gen5SpirvTranslator
+{
+ private sealed partial class CompilationContext
+ {
+ private const uint InvalidNode = 0xFFFF_FFFF;
+
+ // Node pointer: the byte offset divided by eight above bits 2:0, which hold the type.
+ private const uint NodeTypeTriangle1 = 1;
+ private const uint NodeTypeBoxFloat16 = 4;
+ private const uint NodeTypeBoxFloat32 = 5;
+
+ // Triangle node: four float3 vertices, then flags/indices, the triangle id at dword 15.
+ private const uint TriangleIdDword = 15;
+ private const int TriangleIdBitStride = 8;
+ private const int TriangleIdISourceShift = 0;
+ private const int TriangleIdJSourceShift = 2;
+
+ // Box nodes: four child pointers, then per child a float3 min/max pair (six floats)
+ // or six packed halves (three dwords).
+ private const uint BoxBoundsDword = 4;
+
+ private uint[]? _rayResult;
+
+ private void EmitRayIntersect(Gen5RayIntersectControl ray, bool bvh64)
+ {
+ _rayResult ??= DeclareRayResult();
+ EmitExecConditional(() => EmitRayIntersectActive(ray, bvh64));
+ }
+
+ private uint[] DeclareRayResult()
+ {
+ var result = new uint[Gen5RayIntersectControl.ResultDwords];
+ for (var component = 0; component < result.Length; component++)
+ {
+ result[component] = _module.AddGlobalVariable(_privateUintPointer, SpirvStorageClass.Private, UInt(InvalidNode));
+ _module.AddName(result[component], $"rayIntersectResult{component}");
+ _interfaces.Add(result[component]);
+ }
+
+ return result;
+ }
+
+ private void EmitRayIntersectActive(Gen5RayIntersectControl ray, bool bvh64)
+ {
+ var result = _rayResult!;
+ var nodeLow = LoadV(ray.GetAddressRegister(0));
+ var nodePointer = bvh64 ? Pair64(nodeLow, LoadV(ray.GetAddressRegister(1))) : Widen(nodeLow);
+ var nodeType = BitwiseAnd(nodeLow, UInt(7));
+ var nodeIndex = ShiftRightLogical64(nodePointer, ULong(3));
+
+ // The BVH T#: base_address[39:0] in 256-byte units, box_grow_value [62:55],
+ // box_sort_en [63], size [105:64] (the last valid 64-byte node index),
+ // triangle_return_mode [120].
+ var word0 = LoadS(ray.ScalarResource);
+ var word1 = LoadS(ray.ScalarResource + 1);
+ var word2 = LoadS(ray.ScalarResource + 2);
+ var word3 = LoadS(ray.ScalarResource + 3);
+ var baseUnits = Pair64(word0, BitwiseAnd(word1, UInt(0xFF)));
+ var bvhBase = ShiftLeftLogical64(baseUnits, ULong(8));
+ var lastNode = Pair64(word2, BitwiseAnd(word3, UInt(0x3FF)));
+ var boxGrow = BitwiseAnd(ShiftRightLogical(word1, UInt(23)), UInt(0xFF));
+ var boxSort = IsNotZero(BitwiseAnd(word1, UInt(0x8000_0000)));
+ var barycentrics = IsNotZero(BitwiseAnd(word3, UInt(1u << 24)));
+ var nodeAddress = IAdd64(bvhBase, ShiftLeftLogical64(And64(nodePointer, ULong(~7ul)), ULong(3)));
+
+ var present = _module.AddInstruction(SpirvOp.INotEqual, _boolType, baseUnits, ULong(0));
+ var inRange = _module.AddInstruction(SpirvOp.ULessThanEqual, _boolType, nodeIndex, lastNode);
+ // A float32 box node spans two 64-byte units.
+ var wideInRange = _module.AddInstruction(SpirvOp.ULessThan, _boolType, nodeIndex, lastNode);
+ var valid = LogicalAnd(present, inRange);
+ var isTriangle = LogicalAnd(valid, _module.AddInstruction(SpirvOp.ULessThanEqual, _boolType, nodeType, UInt(NodeTypeTriangle1)));
+ var isBox16 = LogicalAnd(valid, _module.AddInstruction(SpirvOp.IEqual, _boolType, nodeType, UInt(NodeTypeBoxFloat16)));
+ var isBox32 = LogicalAnd(LogicalAnd(present, wideInRange),
+ _module.AddInstruction(SpirvOp.IEqual, _boolType, nodeType, UInt(NodeTypeBoxFloat32)));
+
+ var extent = Bitcast(_floatType, LoadV(ray.GetAddressRegister(bvh64 ? 2 : 1)));
+ var origin = new uint[3];
+ for (var axis = 0; axis < 3; axis++)
+ origin[axis] = Bitcast(_floatType, LoadV(ray.GetAddressRegister((bvh64 ? 3 : 2) + axis)));
+ var direction = LoadRayVector(ray, bvh64, inverse: false);
+ var inverseDirection = LoadRayVector(ray, bvh64, inverse: true);
+
+ // Other node types (user nodes 6 and 7, unused 2 and 3), a null BVH or an
+ // out-of-range node return four invalid dwords.
+ foreach (var variable in result)
+ Store(variable, UInt(InvalidNode));
+ EmitConditional(isTriangle, () =>
+ {
+ var values = EmitRayTriangle(nodeAddress, nodeType, barycentrics, origin, direction);
+ for (var component = 0; component < values.Length; component++)
+ Store(result[component], values[component]);
+ });
+ // Each box format reads only its own node bytes.
+ void EmitBoxes(bool fp16)
+ {
+ var children = EmitRayBoxes(nodeAddress, fp16, boxGrow, boxSort, extent, origin, inverseDirection);
+ for (var component = 0; component < children.Length; component++)
+ Store(result[component], children[component]);
+ }
+
+ EmitConditional(isBox16, () => EmitBoxes(fp16: true));
+ EmitConditional(isBox32, () => EmitBoxes(fp16: false));
+
+ for (var component = 0u; component < Gen5RayIntersectControl.ResultDwords; component++)
+ StoreV(ray.VectorData + component, Load(_uintType, result[component]));
+ }
+
+ // Direction and inverse direction follow the origin; A16 packs both as six halves
+ // into three dwords.
+ private uint[] LoadRayVector(Gen5RayIntersectControl ray, bool bvh64, bool inverse)
+ {
+ var first = bvh64 ? 6 : 5;
+ var values = new uint[3];
+ if (!ray.A16)
+ {
+ for (var axis = 0; axis < 3; axis++)
+ values[axis] = Bitcast(_floatType, LoadV(ray.GetAddressRegister(first + (inverse ? 3 : 0) + axis)));
+ return values;
+ }
+
+ for (var axis = 0; axis < 3; axis++)
+ {
+ var half = (inverse ? 3 : 0) + axis;
+ var word = LoadV(ray.GetAddressRegister(first + half / 2));
+ var bits = half % 2 == 0 ? BitwiseAnd(word, UInt(0xFFFF)) : ShiftRightLogical(word, UInt(16));
+ values[axis] = Bitcast(_floatType, EmitHalfToFloat(bits));
+ }
+
+ return values;
+ }
+
+ private uint LoadNodeDword(uint nodeAddress, uint dword) =>
+ LoadDeviceDword(IAdd64(nodeAddress, ULong(dword * 4ul)));
+
+ private uint LoadNodeFloat(uint nodeAddress, uint dword) =>
+ Bitcast(_floatType, LoadNodeDword(nodeAddress, dword));
+
+ // fast_intersect_triangle and SwizzleBarycentrics. Node type 0 tests (v0, v1, v2),
+ // type 1 tests (v1, v3, v2). A miss returns t_num = +inf over t_denom = 1. The
+ // barycentric return mode yields the i/j numerators the builder's rotation maps
+ // back; the other mode yields the triangle id and a hit flag.
+ private uint[] EmitRayTriangle(uint nodeAddress, uint nodeType, uint barycentrics,
+ IReadOnlyList origin, IReadOnlyList direction)
+ {
+ var second = _module.AddInstruction(SpirvOp.IEqual, _boolType, nodeType, UInt(NodeTypeTriangle1));
+ var v1 = new uint[3];
+ var v2 = new uint[3];
+ var v3 = new uint[3];
+ for (var axis = 0u; axis < 3; axis++)
+ {
+ var first = LoadNodeFloat(nodeAddress, axis);
+ var vertex1 = LoadNodeFloat(nodeAddress, 3 + axis);
+ var vertex2 = LoadNodeFloat(nodeAddress, 6 + axis);
+ var vertex3 = LoadNodeFloat(nodeAddress, 9 + axis);
+ v1[axis] = SelectF(second, vertex1, first);
+ v2[axis] = SelectF(second, vertex3, vertex1);
+ v3[axis] = vertex2;
+ }
+
+ var e1 = Subtract(v2, v1);
+ var e2 = Subtract(v3, v1);
+ var e3 = Subtract(origin, v1);
+ var s1 = Cross(direction, e2);
+ var s2 = Cross(e3, e1);
+ var tNum = Dot(e2, s2);
+ var tDenom = Dot(s1, e1);
+ var iNum = Dot(e3, s1);
+ var jNum = Dot(direction, s2);
+
+ var t = FDiv(tNum, tDenom);
+ var u = FDiv(iNum, tDenom);
+ var v = FDiv(jNum, tDenom);
+ var zero = Float(0f);
+ var one = Float(1f);
+ var missed = Any(
+ FCompare(SpirvOp.FOrdLessThan, u, zero),
+ FCompare(SpirvOp.FOrdGreaterThan, u, one),
+ FCompare(SpirvOp.FOrdLessThan, v, zero),
+ FCompare(SpirvOp.FOrdGreaterThan, FAdd(u, v), one),
+ FCompare(SpirvOp.FOrdLessThan, t, zero));
+ tNum = SelectF(missed, Float(float.PositiveInfinity), tNum);
+ tDenom = SelectF(missed, one, tDenom);
+
+ var triangleId = LoadNodeDword(nodeAddress, TriangleIdDword);
+ var shift = ShiftLeftLogical(nodeType, UInt(3));
+ uint Barycentric(int sourceShift)
+ {
+ var source = BitwiseAnd(ShiftRightLogical(triangleId, IAdd(shift, UInt((uint)sourceShift))), UInt(3));
+ return SelectF(_module.AddInstruction(SpirvOp.IEqual, _boolType, source, UInt(1)), iNum,
+ SelectF(_module.AddInstruction(SpirvOp.IEqual, _boolType, source, UInt(2)), jNum,
+ FSub(FSub(tDenom, iNum), jNum)));
+ }
+
+ var hit = LogicalNot(missed);
+ return
+ [
+ Bitcast(_uintType, tNum),
+ Bitcast(_uintType, tDenom),
+ SelectU(barycentrics, Bitcast(_uintType, Barycentric(TriangleIdISourceShift)), triangleId),
+ SelectU(barycentrics, Bitcast(_uintType, Barycentric(TriangleIdJSourceShift)), SelectU(hit, UInt(1), UInt(0))),
+ ];
+ }
+
+ // IntersectNodeBvh4 with fast_intersect_bbox: slab test clipped to [0, extent], a
+ // NaN interval misses, box_grow_value widens the exit time by that many 2^-24
+ // steps, and the optional sort orders hit children by entry time.
+ private uint[] EmitRayBoxes(uint nodeAddress, bool fp16, uint boxGrow, uint boxSort, uint extent,
+ IReadOnlyList origin, IReadOnlyList inverseDirection)
+ {
+ var zero = Float(0f);
+ var growFactor = FAdd(Float(1f), FMul(
+ _module.AddInstruction(SpirvOp.ConvertUToF, _floatType, boxGrow), Float(5.960464478e-8f)));
+ var children = new uint[4];
+ var keys = new uint[4];
+ for (var child = 0u; child < 4; child++)
+ {
+ var pointer = LoadNodeDword(nodeAddress, child);
+ var enter = Float(float.NegativeInfinity);
+ var leave = Float(float.PositiveInfinity);
+ for (var axis = 0u; axis < 3; axis++)
+ {
+ var min = BoxBound(nodeAddress, fp16, child, axis);
+ var max = BoxBound(nodeAddress, fp16, child, axis + 3);
+ var planeMin = FMul(FSub(min, origin[(int)axis]), inverseDirection[(int)axis]);
+ var planeMax = FMul(FSub(max, origin[(int)axis]), inverseDirection[(int)axis]);
+ var positive = FCompare(SpirvOp.FOrdGreaterThanEqual, inverseDirection[(int)axis], zero);
+ var near = SelectF(positive, planeMin, planeMax);
+ var far = SelectF(positive, planeMax, planeMin);
+ // HLSL max3/min3: NaN operands propagate, so the NaN check below sees them.
+ enter = axis == 0 ? near : NanMax(enter, near);
+ leave = axis == 0 ? far : NanMin(leave, far);
+ }
+
+ var nan = _module.AddInstruction(SpirvOp.LogicalOr, _boolType,
+ _module.AddInstruction(SpirvOp.IsNan, _boolType, enter),
+ _module.AddInstruction(SpirvOp.IsNan, _boolType, leave));
+ var minT = SelectF(nan, Float(float.PositiveInfinity), Ext(40, _floatType, enter, zero));
+ var maxT = SelectF(nan, Float(float.NegativeInfinity), Ext(37, _floatType, leave, extent));
+ var hit = FCompare(SpirvOp.FOrdLessThanEqual, minT, FMul(maxT, growFactor));
+ children[child] = SelectU(hit, pointer, UInt(InvalidNode));
+ keys[child] = minT;
+ }
+
+ // The hardware sorting network; an invalid child always sinks.
+ var sorted = (uint[])children.Clone();
+ (int, int)[] network = [(0, 2), (1, 3), (0, 1), (2, 3), (1, 2)];
+ foreach (var (a, b) in network)
+ {
+ var bValid = _module.AddInstruction(SpirvOp.INotEqual, _boolType, sorted[b], UInt(InvalidNode));
+ var aInvalid = _module.AddInstruction(SpirvOp.IEqual, _boolType, sorted[a], UInt(InvalidNode));
+ var swap = _module.AddInstruction(SpirvOp.LogicalOr, _boolType,
+ LogicalAnd(bValid, FCompare(SpirvOp.FOrdLessThan, keys[b], keys[a])), aInvalid);
+ (sorted[a], sorted[b]) = (SelectU(swap, sorted[b], sorted[a]), SelectU(swap, sorted[a], sorted[b]));
+ (keys[a], keys[b]) = (SelectF(swap, keys[b], keys[a]), SelectF(swap, keys[a], keys[b]));
+ }
+
+ for (var child = 0; child < 4; child++)
+ children[child] = SelectU(boxSort, sorted[child], children[child]);
+ return children;
+ }
+
+ // Bound 0-2 is the minimum, 3-5 the maximum of one child box.
+ private uint BoxBound(uint nodeAddress, bool fp16, uint child, uint bound)
+ {
+ var index = child * 6 + bound;
+ if (!fp16)
+ return LoadNodeFloat(nodeAddress, BoxBoundsDword + index);
+
+ var packed = LoadNodeDword(nodeAddress, BoxBoundsDword + index / 2);
+ var bits = index % 2 == 0 ? BitwiseAnd(packed, UInt(0xFFFF)) : ShiftRightLogical(packed, UInt(16));
+ return Bitcast(_floatType, EmitHalfToFloat(bits));
+ }
+
+ private uint NanMax(uint left, uint right) => SelectF(
+ _module.AddInstruction(SpirvOp.LogicalOr, _boolType,
+ _module.AddInstruction(SpirvOp.IsNan, _boolType, left),
+ FCompare(SpirvOp.FOrdGreaterThan, left, right)), left, right);
+
+ private uint NanMin(uint left, uint right) => SelectF(
+ _module.AddInstruction(SpirvOp.LogicalOr, _boolType,
+ _module.AddInstruction(SpirvOp.IsNan, _boolType, left),
+ FCompare(SpirvOp.FOrdLessThan, left, right)), left, right);
+
+ private uint[] Subtract(IReadOnlyList left, IReadOnlyList right) =>
+ [FSub(left[0], right[0]), FSub(left[1], right[1]), FSub(left[2], right[2])];
+
+ private uint[] Cross(IReadOnlyList a, IReadOnlyList b) =>
+ [
+ FSub(FMul(a[1], b[2]), FMul(a[2], b[1])),
+ FSub(FMul(a[2], b[0]), FMul(a[0], b[2])),
+ FSub(FMul(a[0], b[1]), FMul(a[1], b[0])),
+ ];
+
+ private uint Dot(IReadOnlyList a, IReadOnlyList b) =>
+ FAdd(FAdd(FMul(a[0], b[0]), FMul(a[1], b[1])), FMul(a[2], b[2]));
+
+ private uint Any(params uint[] conditions)
+ {
+ var result = conditions[0];
+ for (var index = 1; index < conditions.Length; index++)
+ result = _module.AddInstruction(SpirvOp.LogicalOr, _boolType, result, conditions[index]);
+ return result;
+ }
+
+ private uint FCompare(SpirvOp op, uint left, uint right) => _module.AddInstruction(op, _boolType, left, right);
+ private uint FAdd(uint left, uint right) => _module.AddInstruction(SpirvOp.FAdd, _floatType, left, right);
+ private uint FSub(uint left, uint right) => _module.AddInstruction(SpirvOp.FSub, _floatType, left, right);
+ private uint FMul(uint left, uint right) => _module.AddInstruction(SpirvOp.FMul, _floatType, left, right);
+ private uint FDiv(uint left, uint right) => _module.AddInstruction(SpirvOp.FDiv, _floatType, left, right);
+ private uint SelectF(uint condition, uint whenTrue, uint whenFalse) =>
+ _module.AddInstruction(SpirvOp.Select, _floatType, condition, whenTrue, whenFalse);
+ }
+}
diff --git a/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.Resources.cs b/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.Resources.cs
index 2ccf92c..34df42b 100644
--- a/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.Resources.cs
+++ b/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.Resources.cs
@@ -115,6 +115,7 @@ public static partial class Gen5SpirvTranslator
_localSizeX = Math.Max(request.LocalSizeX, 1);
_localSizeY = Math.Max(request.LocalSizeY, 1);
_localSizeZ = Math.Max(request.LocalSizeZ, 1);
+ _physicalAxisOfLogical = ComputeWorkgroupAxisOrder(_localSizeX, _localSizeY, _localSizeZ);
_emulateWave64 = _stage == Gen5SpirvStage.Compute && _waveLaneCount == 64 && (ulong)_localSizeX * _localSizeY * _localSizeZ == 64;
_requiredVertexOutputCount = request.RequiredVertexOutputCount;
_pixelInputEnable = request.PixelInputEnable;
@@ -322,31 +323,41 @@ public static partial class Gen5SpirvTranslator
}
var isStorage = resourceClass == ImageResourceClass.Storage;
- var kind = numericClass switch
- {
- ImageNumericClass.Uint => ImageComponentKind.Uint,
- ImageNumericClass.Sint => ImageComponentKind.Sint,
- _ => ImageComponentKind.Float,
- };
+ // Every atomic image (integer or float) is declared with the R32ui
+ // format below, so its OpTypeImage Sampled Type must be uint too -
+ // VUID-StandaloneSpirv-Image-04965 requires the two to match. The
+ // float atomics (Fmin/Fmax) still operate on the real float bits;
+ // they just get there through a bitcast + integer compare-exchange
+ // (see Gen5SpirvTranslator.cs's ImageAtomicFmax/Fmin case) instead
+ // of through the declared image/sampled type.
+ var kind = atomic
+ ? ImageComponentKind.Uint
+ : numericClass switch
+ {
+ ImageNumericClass.Uint => ImageComponentKind.Uint,
+ ImageNumericClass.Sint => ImageComponentKind.Sint,
+ _ => ImageComponentKind.Float,
+ };
var componentType = kind switch
{
ImageComponentKind.Sint => _intType,
ImageComponentKind.Uint => _uintType,
_ => _floatType,
};
+ // The image request builder binds guest 1D textures through 1D views,
+ // so the shader declaration must be 1D as well.
var spirvDimension = dimension switch
{
ImageDimension.Dim1D or ImageDimension.Dim1DArray => SpirvImageDim.Dim1D,
ImageDimension.Dim3D => SpirvImageDim.Dim3D,
_ => SpirvImageDim.Dim2D,
};
- var arrayed = dimension is ImageDimension.Dim1DArray or ImageDimension.Dim2DArray or ImageDimension.Dim2DMsaaArray;
- var multisampled = dimension is ImageDimension.Dim2DMsaa or ImageDimension.Dim2DMsaaArray;
if (spirvDimension == SpirvImageDim.Dim1D)
{
_module.AddCapability(isStorage ? SpirvCapability.Image1D : SpirvCapability.Sampled1D);
}
-
+ var arrayed = dimension is ImageDimension.Dim1DArray or ImageDimension.Dim2DArray or ImageDimension.Dim2DMsaaArray;
+ var multisampled = dimension is ImageDimension.Dim2DMsaa or ImageDimension.Dim2DMsaaArray;
var format = atomic ? SpirvImageFormat.R32ui : SpirvImageFormat.Unknown;
if (isStorage && !atomic)
{
@@ -772,16 +783,44 @@ public static partial class Gen5SpirvTranslator
}
else if (entry.Kind == MemoryResourceKind.ScalarBuffer)
{
- if (entry.Resource == MemoryAccessInfo.NoResource)
+ if (entry.Resource != MemoryAccessInfo.NoResource)
{
- error = "scalar buffer load has no dense buffer";
- return false;
+ var bindingIndex = (int)entry.Resource;
+ var byteAddress = IAdd(dynamicOffset, UInt(unchecked((uint)control.ImmediateOffsetBytes + (uint)component * sizeof(uint))));
+ byteAddress = ApplyGuestBufferByteBias(bindingIndex, byteAddress);
+ value = LoadBufferWord(bindingIndex, ShiftRightLogical(byteAddress, UInt(2)));
}
+ else
+ {
+ // A runtime SRT V#: its SGPRs hold the descriptor, so build the
+ // guest address from the base and go through the device-address
+ // page table, bounded by the descriptor's stride and record count.
+ if (instruction.Sources.Count == 0 || instruction.Sources[0].Kind != Gen5OperandKind.ScalarRegister)
+ {
+ error = "runtime scalar buffer load has no scalar base";
+ return false;
+ }
- var bindingIndex = (int)entry.Resource;
- var byteAddress = IAdd(dynamicOffset, UInt(unchecked((uint)control.ImmediateOffsetBytes + (uint)component * sizeof(uint))));
- byteAddress = ApplyGuestBufferByteBias(bindingIndex, byteAddress);
- value = LoadBufferWord(bindingIndex, ShiftRightLogical(byteAddress, UInt(2)));
+ var baseRegister = instruction.Sources[0].Value;
+ var descriptorWord1 = LoadS(baseRegister + 1);
+ var stride = BitwiseAnd(ShiftRightLogical(descriptorWord1, UInt(16)), UInt(0x3FFF));
+ var records = LoadS(baseRegister + 2);
+ var size = _module.AddInstruction(
+ SpirvOp.Select,
+ _uintType,
+ _module.AddInstruction(SpirvOp.IEqual, _boolType, stride, UInt(0)),
+ records,
+ _module.AddInstruction(SpirvOp.IMul, _uintType, stride, records));
+ var baseAddress = Pair64(LoadS(baseRegister), BitwiseAnd(descriptorWord1, UInt(0xFFFF)));
+ var byteOffset = IAdd(dynamicOffset, UInt(unchecked((uint)control.ImmediateOffsetBytes + (uint)component * sizeof(uint))));
+ var address = And64(IAdd64(baseAddress, Widen(byteOffset)), ULong(DeviceAddressMask & ~3ul));
+ var inRange = _module.AddInstruction(
+ SpirvOp.ULessThan,
+ _boolType,
+ ShiftRightLogical(byteOffset, UInt(2)),
+ ShiftRightLogical(size, UInt(2)));
+ value = LoadBoundedDeviceDword(address, inRange);
+ }
}
else
{
@@ -846,6 +885,11 @@ public static partial class Gen5SpirvTranslator
address = IAdd64(address, SignedOffset64(control.OffsetBytes));
var entry = request.Memory[memoryIndex];
+ if (control.UsesFlatAddress && entry.AddressSpace != FlatAddressSpace.Global)
+ {
+ return TryEmitFlatLocalMemory(instruction, control, memoryOpcode, entry.AddressSpace, address, out error);
+ }
+
var writes = entry.Access is MemoryAccess.Write or MemoryAccess.Atomic;
var accessBytes = Math.Max((entry.DataBits + 7) / 8, 1u) * Math.Max(entry.DataDwords, 1u);
var allowed = writes ? IsWrittenAccessAllowed(memoryIndex, address, accessBytes) : _module.ConstantBool(true);
@@ -927,6 +971,100 @@ public static partial class Gen5SpirvTranslator
return true;
}
+ // Routes each lane of an aperture-derived FLAT access like the hardware: LDS or
+ // scratch at the low dword when the high dword is in an aperture, otherwise a
+ // global access through the page table.
+ private bool TryEmitFlatLocalMemory(
+ Gen5ShaderInstruction instruction,
+ Gen5GlobalMemoryControl control,
+ string memoryOpcode,
+ FlatAddressSpace addressSpace,
+ uint address,
+ out string error)
+ {
+ error = string.Empty;
+ var store = memoryOpcode.StartsWith("GlobalStoreDword", StringComparison.Ordinal);
+ var load = memoryOpcode.StartsWith("GlobalLoadDword", StringComparison.Ordinal);
+ if (!store && !load)
+ {
+ error = $"{instruction.Opcode} through the {addressSpace} flat aperture is not implemented";
+ return false;
+ }
+
+ var canShare = addressSpace is FlatAddressSpace.Shared or FlatAddressSpace.SharedOrPrivate;
+ var canPrivate = addressSpace is FlatAddressSpace.Private or FlatAddressSpace.SharedOrPrivate;
+ if (canShare && _lds == 0)
+ {
+ error = "a shared flat aperture access has no LDS storage";
+ return false;
+ }
+
+ if (canPrivate && (_scratch == 0 || _scratchDwordCount == 0))
+ {
+ error = "a private flat aperture access has no scratch storage";
+ return false;
+ }
+
+ var offset = _module.AddInstruction(SpirvOp.UConvert, _uintType, address);
+ var aperture = _module.AddInstruction(
+ SpirvOp.UConvert,
+ _uintType,
+ ShiftRightLogical64(address, _module.Constant64(_ulongType, 32 + Gen5InlineConstants.ApertureShift)));
+ var isShared = _module.AddInstruction(SpirvOp.IEqual, _boolType, aperture,
+ UInt(Gen5InlineConstants.SharedApertureHigh >> Gen5InlineConstants.ApertureShift));
+ var isPrivate = _module.AddInstruction(SpirvOp.IEqual, _boolType, aperture,
+ UInt(Gen5InlineConstants.PrivateApertureHigh >> Gen5InlineConstants.ApertureShift));
+
+ void Shared()
+ {
+ for (uint index = 0; index < control.DwordCount; index++)
+ {
+ var pointer = LdsPointer(offset, index * sizeof(uint));
+ if (store)
+ Store(pointer, LoadV(control.SourceVectorRegister + index));
+ else
+ StoreV(control.DestinationVectorRegister + index, Load(_uintType, pointer));
+ }
+ }
+
+ void Private()
+ {
+ for (uint index = 0; index < control.DwordCount; index++)
+ {
+ var pointer = ScratchPointer(offset, index * sizeof(uint));
+ if (store)
+ Store(pointer, LoadV(control.SourceVectorRegister + index));
+ else
+ StoreV(control.DestinationVectorRegister + index, Load(_uintType, pointer));
+ }
+ }
+
+ void Global()
+ {
+ var aligned = And64(address, ULong(~3ul));
+ for (uint index = 0; index < control.DwordCount; index++)
+ {
+ var dwordAddress = index == 0 ? aligned : IAdd64(aligned, ULong((ulong)index * sizeof(uint)));
+ if (store)
+ StoreDeviceDword(dwordAddress, LoadV(control.SourceVectorRegister + index), _module.ConstantBool(true));
+ else
+ StoreV(control.DestinationVectorRegister + index, LoadDeviceDword(dwordAddress));
+ }
+ }
+
+ // Inactive lanes must not touch any of the three storages.
+ EmitExecConditional(() =>
+ {
+ if (canShare && canPrivate)
+ EmitConditional(isShared, Shared, () => EmitConditional(isPrivate, Private, Global));
+ else if (canShare)
+ EmitConditional(isShared, Shared, Global);
+ else
+ EmitConditional(isPrivate, Private, Global);
+ });
+ return true;
+ }
+
private bool TryEmitScratchMemory(
Gen5ShaderInstruction instruction,
Gen5GlobalMemoryControl control,
@@ -942,6 +1080,10 @@ public static partial class Gen5SpirvTranslator
var address = control.ScalarAddress < 125
? LoadS(control.ScalarAddress)
: LoadV(control.VectorAddress);
+ if (control.DynamicOffsetRegister is { } dynamicOffsetRegister)
+ {
+ address = IAdd(address, LoadS(dynamicOffsetRegister));
+ }
if (control.OffsetBytes != 0)
{
address = IAdd(address, UInt(unchecked((uint)control.OffsetBytes)));
@@ -961,7 +1103,9 @@ public static partial class Gen5SpirvTranslator
{
Store(
ScratchPointer(address, index * sizeof(uint)),
- LoadV(control.SourceVectorRegister + index));
+ control.SourceIsScalar
+ ? LoadS(control.SourceVectorRegister + index)
+ : LoadV(control.SourceVectorRegister + index));
}
});
return true;
@@ -1085,6 +1229,160 @@ public static partial class Gen5SpirvTranslator
return true;
}
+ // BufferLoweringStrategy.PhysicalStorageBuffer: a raw buffer access whose
+ // V# was read from a runtime scalar address. The descriptor dwords live in
+ // the SGPRs named by the instruction, so build the guest address from the
+ // runtime base and go through the device-address page table.
+ // num_records/stride bound the access, so a null or empty descriptor binds
+ // nothing, matching the static path's range check. Formatted/typed/atomic
+ // accesses never reach here; the chooser escalates them to
+ // BoundedCandidateTable.
+ private bool TryEmitPhysicalStorageBufferMemory(
+ Gen5ShaderInstruction instruction,
+ Gen5BufferMemoryControl control,
+ out string error)
+ {
+ error = string.Empty;
+ if (control.Typed || instruction.Opcode.Contains("Format", StringComparison.Ordinal))
+ {
+ error = $"runtime buffer descriptor does not support the formatted access {instruction.Opcode}";
+ return false;
+ }
+
+ if (instruction.Opcode.StartsWith("BufferAtomic", StringComparison.Ordinal))
+ {
+ error = $"runtime buffer descriptor does not support the atomic access {instruction.Opcode}";
+ return false;
+ }
+
+ if (instruction.Sources.Count < 2 ||
+ instruction.Sources[1].Kind != Gen5OperandKind.ScalarRegister)
+ {
+ error = "runtime buffer descriptor has no scalar resource base";
+ return false;
+ }
+
+ var srsrc = instruction.Sources[1].Value;
+ var scalarOffset = instruction.Sources.Count > 2
+ ? GetRawSource(instruction, 2)
+ : UInt(0);
+ var vectorIndex = control.IndexEnabled ? LoadV(control.VectorAddress) : UInt(0);
+ var vectorOffset = control.OffsetEnabled
+ ? LoadV(control.VectorAddress + (control.IndexEnabled ? 1u : 0u))
+ : UInt(0);
+
+ var descriptorWord1 = LoadS(srsrc + 1);
+ var stride = BitwiseAnd(ShiftRightLogical(descriptorWord1, UInt(16)), UInt(0x3FFF));
+ var byteOffset = IAdd(UInt(unchecked((uint)control.OffsetBytes)), scalarOffset);
+ byteOffset = IAdd(byteOffset, vectorOffset);
+ byteOffset = IAdd(
+ byteOffset,
+ _module.AddInstruction(SpirvOp.IMul, _uintType, vectorIndex, stride));
+
+ // size = stride == 0 ? num_records : stride * num_records, in bytes.
+ var records = LoadS(srsrc + 2);
+ var size = _module.AddInstruction(
+ SpirvOp.Select,
+ _uintType,
+ _module.AddInstruction(SpirvOp.IEqual, _boolType, stride, UInt(0)),
+ records,
+ _module.AddInstruction(SpirvOp.IMul, _uintType, stride, records));
+
+ var dwordOffset = ShiftRightLogical(byteOffset, UInt(2));
+ var dwordSize = ShiftRightLogical(size, UInt(2));
+ var baseAddress = Pair64(
+ LoadS(srsrc),
+ BitwiseAnd(descriptorWord1, UInt(0xFFFF)));
+ var address = IAdd64(baseAddress, Widen(byteOffset));
+
+ uint InRange(uint index)
+ {
+ var dword = index == 0 ? dwordOffset : IAdd(dwordOffset, UInt(index));
+ return _module.AddInstruction(SpirvOp.ULessThan, _boolType, dword, dwordSize);
+ }
+
+ if (instruction.Opcode.StartsWith("BufferStoreDword", StringComparison.Ordinal) ||
+ instruction.Opcode.StartsWith("BufferStoreByte", StringComparison.Ordinal) ||
+ instruction.Opcode.StartsWith("BufferStoreShort", StringComparison.Ordinal))
+ {
+ EmitExecConditional(() =>
+ {
+ if (TryGetSubdwordStoreInfo(instruction.Opcode, out var byteCount, out var sourceShift))
+ {
+ StoreDeviceBytes(
+ address,
+ LoadV(control.VectorData),
+ byteCount,
+ sourceShift,
+ InRange(0));
+ return;
+ }
+
+ for (uint index = 0; index < control.DwordCount; index++)
+ {
+ StoreDeviceDword(
+ index == 0 ? address : IAdd64(address, ULong((ulong)index * sizeof(uint))),
+ LoadV(control.VectorData + index),
+ InRange(index));
+ }
+ });
+ return true;
+ }
+
+ if (TryGetSubdwordLoadInfo(
+ instruction.Opcode,
+ out var loadByteCount,
+ out var signExtend,
+ out var d16,
+ out var d16High))
+ {
+ StoreV(
+ control.VectorData,
+ LoadSubdwordDeviceValue(
+ address,
+ LoadV(control.VectorData),
+ loadByteCount,
+ signExtend,
+ d16,
+ d16High));
+ return true;
+ }
+
+ if (!instruction.Opcode.StartsWith("BufferLoad", StringComparison.Ordinal) &&
+ !instruction.Opcode.StartsWith("TBufferLoad", StringComparison.Ordinal))
+ {
+ error = $"unsupported runtime buffer opcode {instruction.Opcode}";
+ return false;
+ }
+
+ for (uint index = 0; index < control.DwordCount; index++)
+ {
+ StoreV(
+ control.VectorData + index,
+ LoadBoundedDeviceDword(
+ index == 0 ? address : IAdd64(address, ULong((ulong)index * sizeof(uint))),
+ InRange(index)));
+ }
+
+ return true;
+ }
+
+ private uint LoadBoundedDeviceDword(uint address64, uint inRange)
+ {
+ var (pointer, valid) = ResolveDeviceAddress(address64);
+ Store(_deviceWordScratch, UInt(0));
+ EmitConditional(LogicalAnd(inRange, valid), () =>
+ Store(
+ _deviceWordScratch,
+ _module.AddInstruction(
+ SpirvOp.Load,
+ _uintType,
+ DeviceWordPointer(pointer),
+ 2u,
+ 4u)));
+ return Load(_uintType, _deviceWordScratch);
+ }
+
// ---- images ----
// An access over several descriptors takes one constant-element case per descriptor:
diff --git a/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.cs b/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.cs
index 81cf6fe..4c4a7b9 100644
--- a/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.cs
+++ b/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.cs
@@ -23,6 +23,38 @@ public static partial class Gen5SpirvTranslator
uint numberType) =>
CompilationContext.DecodeStorageImageFormat(dataFormat, numberType);
+ // Vulkan only guarantees maxComputeWorkGroupSize >= (1024, 1024, 64) and
+ // maxComputeWorkGroupInvocations >= 1024, but a GNM NUM_THREAD layout can put its
+ // largest axis on Z (real PS5 hardware has no such asymmetry between axes). Remap
+ // logical GNM axes onto physical SPIR-V/dispatch axes so the largest logical size
+ // always lands on the physical axis with the most headroom (X), and the smallest
+ // lands on Z, which is the only axis with a tight guaranteed limit. This is a pure
+ // function of the three thread-group sizes so the host dispatch path (which permutes
+ // vkCmdDispatch's group counts the same way) can recompute it independently without
+ // any shader-compiler metadata plumbing.
+ //
+ // Returns physicalAxisOfLogical: physicalAxisOfLogical[logical 0=X/1=Y/2=Z] is the
+ // physical axis (0=X/1=Y/2=Z) that logical axis is remapped onto.
+ public static int[] ComputeWorkgroupAxisOrder(uint sizeX, uint sizeY, uint sizeZ)
+ {
+ if (sizeZ <= 64)
+ {
+ return [0, 1, 2];
+ }
+
+ var sizes = new[] { sizeX, sizeY, sizeZ };
+ var byDescendingSize = new[] { 0, 1, 2 };
+ Array.Sort(byDescendingSize, (a, b) => sizes[b].CompareTo(sizes[a]));
+
+ var physicalAxisOfLogical = new int[3];
+ for (var physical = 0; physical < 3; physical++)
+ {
+ physicalAxisOfLogical[byDescendingSize[physical]] = physical;
+ }
+
+ return physicalAxisOfLogical;
+ }
+
private sealed partial class CompilationContext
{
private const int ScalarRegisterCount = 128;
@@ -80,6 +112,13 @@ public static partial class Gen5SpirvTranslator
private readonly uint _localSizeX;
private readonly uint _localSizeY;
private readonly uint _localSizeZ;
+
+ // Vulkan only guarantees maxComputeWorkGroupSize >= (1024, 1024, 64), but a GNM
+ // NUM_THREAD layout can put its largest axis on Z (real hardware has no such asymmetry).
+ // physicalAxisOfLogical[logical 0=X/1=Y/2=Z] gives which physical SPIR-V axis (and which
+ // vkCmdDispatch group-count slot) that logical axis is remapped onto, so the largest size
+ // always lands on the physical axis with the most headroom.
+ private readonly int[] _physicalAxisOfLogical;
private readonly uint _pixelInputEnable;
private readonly uint _pixelInputAddress;
private readonly uint[] _pixelInputCntl;
@@ -122,6 +161,7 @@ public static partial class Gen5SpirvTranslator
private uint _storageUintPointer;
private uint _lds;
private uint _ldsElementPointer;
+ private uint _lds64ElementPointer;
private uint _ldsDwordMask;
private uint _scratch;
private uint _scratchElementPointer;
@@ -172,11 +212,11 @@ public static partial class Gen5SpirvTranslator
ImageComponentKind ComponentKind,
bool IsStorage,
bool Arrayed,
- bool Cube,
- bool Multisampled,
- SpirvImageDim Dimension,
- uint ConversionFormat,
- uint ShaderSwizzle,
+ bool Cube = false,
+ bool Multisampled = false,
+ SpirvImageDim Dimension = SpirvImageDim.Dim2D,
+ uint ConversionFormat = 0,
+ uint ShaderSwizzle = 0,
int EmulatedCompareFunction = -1);
private readonly record struct SpirvVertexInput(
@@ -428,12 +468,19 @@ public static partial class Gen5SpirvTranslator
}
else if (_stage == Gen5SpirvStage.Compute)
{
+ var logicalSizes = new[] { _localSizeX, _localSizeY, _localSizeZ };
+ var physicalSizes = new uint[3];
+ for (var logical = 0; logical < 3; logical++)
+ {
+ physicalSizes[_physicalAxisOfLogical[logical]] = logicalSizes[logical];
+ }
+
_module.AddExecutionMode(
main,
SpirvExecutionMode.LocalSize,
- _localSizeX,
- _localSizeY,
- _localSizeZ);
+ physicalSizes[0],
+ physicalSizes[1],
+ physicalSizes[2]);
}
var attributeCount = _stage == Gen5SpirvStage.Vertex
@@ -576,7 +623,9 @@ public static partial class Gen5SpirvTranslator
private void DeclareScratch()
{
if (!_request.Program.Instructions.Any(static instruction =>
- instruction.Opcode.StartsWith("Scratch", StringComparison.Ordinal)))
+ instruction.Opcode.StartsWith("Scratch", StringComparison.Ordinal)) &&
+ !_request.Memory.Entries.Any(static memory =>
+ memory.AddressSpace is FlatAddressSpace.Private or FlatAddressSpace.SharedOrPrivate))
{
return;
}
@@ -661,6 +710,7 @@ public static partial class Gen5SpirvTranslator
var ldsArrayType = _module.TypeArray(_uintType, dwordCount);
var ldsPointer = _module.TypePointer(storageClass, ldsArrayType);
_ldsElementPointer = _module.TypePointer(storageClass, _uintType);
+ _lds64ElementPointer = _module.TypePointer(storageClass, _ulongType);
_lds = storageClass == SpirvStorageClass.Workgroup
? _module.AddGlobalVariable(ldsPointer, storageClass)
: _module.AddGlobalVariable(
@@ -855,6 +905,8 @@ public static partial class Gen5SpirvTranslator
for (var index = 0; index < attributes.Length; index++)
{
var attribute = attributes[index];
+ // V_INTERP_MOV reads one vertex of the primitive, so its attribute is a
+ // per-vertex array rather than an interpolated value.
var variable = _module.AddGlobalVariable(
_perVertexAttributes.Contains(attribute)
? _module.TypePointer(SpirvStorageClass.Input, _module.TypeArray(_vec4Type, 3))
@@ -1074,18 +1126,23 @@ public static partial class Gen5SpirvTranslator
var invocationInBounds = _module.ConstantBool(true);
for (uint component = 0; component < 3; component++)
{
+ // gl_LocalInvocationId/gl_WorkGroupId are indexed by the physical SPIR-V
+ // axis, which can differ from the logical GNM axis when the workgroup was
+ // remapped to respect the device's tighter Z-axis limit (see
+ // ComputeWorkgroupAxisOrder).
+ var physicalComponent = (uint)_physicalAxisOfLogical[component];
var localComponent = _module.AddInstruction(
SpirvOp.CompositeExtract,
_uintType,
localId,
- component);
+ physicalComponent);
StoreV(component, localComponent, guardWithExec: false);
var groupComponent = _module.AddInstruction(
SpirvOp.CompositeExtract,
_uintType,
workGroupId,
- component);
+ physicalComponent);
var localSize = component switch
{
0 => _localSizeX,
@@ -1120,15 +1177,15 @@ public static partial class Gen5SpirvTranslator
StoreComputeSystemRegister(
registers.WorkGroupXRegister,
workGroupId,
- 0);
+ (uint)_physicalAxisOfLogical[0]);
StoreComputeSystemRegister(
registers.WorkGroupYRegister,
workGroupId,
- 1);
+ (uint)_physicalAxisOfLogical[1]);
StoreComputeSystemRegister(
registers.WorkGroupZRegister,
workGroupId,
- 2);
+ (uint)_physicalAxisOfLogical[2]);
if (registers.ThreadGroupSizeRegister is { } sizeRegister)
{
StoreS(
@@ -1172,6 +1229,8 @@ public static partial class Gen5SpirvTranslator
{
if ((_pixelInputAddress & (1u << bit)) != 0)
{
+ // Shaders that interpolate by hand (P1/P2 on per-vertex attributes) read
+ // the I/J barycentrics from these registers.
if (_barycentricInputs.TryGetValue(bit, out var barycentricInput))
{
var coordinates = LoadBarycentricCoordinates(bit, barycentricInput);
@@ -1182,6 +1241,7 @@ public static partial class Gen5SpirvTranslator
StoreV(vgpr + component, Bitcast(_uintType, coordinate), guardWithExec: false);
}
}
+
vgpr += dwordCount;
}
}
@@ -1433,8 +1493,11 @@ public static partial class Gen5SpirvTranslator
{
if (_stage == Gen5SpirvStage.Compute)
{
+ // s_waitcnt vmcnt(0) + s_barrier also publishes buffer and image
+ // stores to the workgroup: AcquireRelease over uniform, workgroup
+ // and image memory.
var workgroup = UInt(2);
- var semantics = UInt(0x108);
+ var semantics = UInt(0x8 | 0x40 | 0x100 | 0x800);
_module.AddStatement(
SpirvOp.ControlBarrier,
workgroup,
@@ -1461,7 +1524,8 @@ public static partial class Gen5SpirvTranslator
if (instruction.Control is Gen5RayIntersectControl rayIntersect)
{
- EmitRayIntersectMiss(rayIntersect);
+ _deviceAddressInstructionPc = instruction.Pc;
+ EmitRayIntersect(rayIntersect, instruction.Opcode == "ImageBvh64IntersectRay");
return true;
}
@@ -1540,6 +1604,8 @@ public static partial class Gen5SpirvTranslator
case "DsAppend":
case "DsConsume":
return TryEmitDataShareWaveCounter(instruction, control, out error);
+ case "DsMskorB32":
+ return TryEmitDataShareAtomic(instruction, control, out error);
case "DsWriteB32":
case "DsWriteAddtidB32":
{
@@ -1575,6 +1641,92 @@ public static partial class Gen5SpirvTranslator
GetRawSource(instruction, 2));
return true;
}
+ case "DsAddU64":
+ case "DsOrB64":
+ {
+ if (instruction.Sources.Count < 3)
+ {
+ error = "missing LDS 64-bit atomic source";
+ return false;
+ }
+
+ var atomicAddress = GetRawSource(instruction, 0);
+ var isOr = instruction.Opcode == "DsOrB64";
+ // The LDS allocation is a uint array. Reinterpreting its
+ // pointer as ulong with OpBitcast is invalid SPIR-V, so use
+ // the dword fallback even when native 64-bit atomics exist.
+ // The pair is not atomic against concurrent 64-bit use.
+ var lowPointer = LdsPointer(
+ atomicAddress,
+ control.SingleOffsetBytes);
+ var highPointer = LdsPointer(
+ atomicAddress,
+ control.SingleOffsetBytes + sizeof(uint));
+ EmitExecConditional(() =>
+ {
+ var lowValue = GetRawSource(instruction, 1);
+ var highValue = GetRawSource(instruction, 2);
+ if (isOr)
+ {
+ // OR is bitwise-independent: no dword influences the
+ // other, so two 32-bit ORs are the 64-bit OR.
+ EmitAtomic(
+ SpirvOp.AtomicOr,
+ _uintType,
+ lowPointer,
+ scope: 2,
+ semantics: 0x108,
+ value: () => lowValue,
+ comparator: () => lowValue);
+ EmitAtomic(
+ SpirvOp.AtomicOr,
+ _uintType,
+ highPointer,
+ scope: 2,
+ semantics: 0x108,
+ value: () => highValue,
+ comparator: () => highValue);
+ return;
+ }
+
+ // ADD needs the carry out of the low dword folded into
+ // the high one. Each lane detects its own wrap from the
+ // pre-add value its atomic returned and adds exactly one
+ // carry, so the high dword ends up with the true count of
+ // wraps however the lanes interleave.
+ var originalLow = EmitAtomic(
+ SpirvOp.AtomicIAdd,
+ _uintType,
+ lowPointer,
+ scope: 2,
+ semantics: 0x108,
+ value: () => lowValue,
+ comparator: () => lowValue);
+ var sumLow = IAdd(originalLow, lowValue);
+ var wrapped = _module.AddInstruction(
+ SpirvOp.ULessThan,
+ _boolType,
+ sumLow,
+ originalLow);
+ var carry = _module.AddInstruction(
+ SpirvOp.Select,
+ _uintType,
+ wrapped,
+ UInt(1),
+ UInt(0));
+ var highWithCarry = IAdd(highValue, carry);
+ EmitAtomic(
+ SpirvOp.AtomicIAdd,
+ _uintType,
+ highPointer,
+ scope: 2,
+ semantics: 0x108,
+ value: () => highWithCarry,
+ comparator: () => highWithCarry);
+ });
+
+ return true;
+ }
case "DsWriteB96":
case "DsWriteB128":
{
@@ -1898,6 +2050,40 @@ public static partial class Gen5SpirvTranslator
out string error)
{
error = string.Empty;
+ if (instruction.Opcode == "DsMskorB32")
+ {
+ if (instruction.Sources.Count < 3)
+ {
+ error = "missing LDS masked-OR source";
+ return false;
+ }
+
+ var maskedPointer = LdsPointer(
+ GetRawSource(instruction, 0),
+ control.SingleOffsetBytes);
+ EmitExecConditional(() =>
+ {
+ var original = Load(_uintType, maskedPointer);
+ var updated = BitwiseOr(
+ BitwiseAnd(
+ original,
+ _module.AddInstruction(
+ SpirvOp.Not,
+ _uintType,
+ GetRawSource(instruction, 1))),
+ GetRawSource(instruction, 2));
+ EmitAtomic(
+ SpirvOp.AtomicCompareExchange,
+ _uintType,
+ maskedPointer,
+ scope: 2,
+ semantics: 0x108,
+ value: () => updated,
+ comparator: () => original);
+ });
+ return true;
+ }
+
if (instruction.Opcode is "DsMinF32" or "DsMaxF32")
{
if (instruction.Sources.Count < 3)
@@ -2208,6 +2394,13 @@ public static partial class Gen5SpirvTranslator
out string error)
{
error = string.Empty;
+ if (instruction.Opcode is "BufferWbinvl1" or "BufferWbinvl1Vol")
+ {
+ // The host storage path is already coherent; the guest instruction
+ // invalidates a cache level and does not access the buffer itself.
+ return true;
+ }
+
if (control.Typed && instruction.Opcode.Contains("D16", StringComparison.Ordinal))
{
error = $"unsupported buffer opcode {instruction.Opcode}";
@@ -2225,23 +2418,65 @@ public static partial class Gen5SpirvTranslator
return TryEmitDeviceDescriptorBufferMemory(instruction, control, out error);
}
- int bindingIndex;
- uint stride;
- uint descriptorWord3;
- uint descriptorFormat;
+ if (_request.Memory.TryGetIndex(instruction.Pc, 0, out var accessMemoryIndex))
{
- // The dense buffer, its stride and its format come from the specialization.
- if (!TryResolveLayoutBuffer(instruction.Pc, out bindingIndex, out var specialized))
+ var accessMemory = _request.Memory[accessMemoryIndex];
+ if (accessMemory.PlanningOnly)
{
- error = "missing buffer-memory binding";
- return false;
+ // The access belongs to the linearized tail of a fused
+ // shader. Its descriptor is supplied by the continuation
+ // object and is intentionally absent from this plan; keep
+ // the dead linear instruction side-effect free.
+ return true;
}
- stride = UInt(specialized.PackedStride & 0x3FFF);
- descriptorFormat = specialized.DescriptorFormat;
- descriptorWord3 = UInt((specialized.DescriptorFormat << 12) | (specialized.DescriptorSwizzle & 0xFFF));
+ var strategy = accessMemory.BufferDescriptor?.ChooseStrategy(
+ control.Typed,
+ accessMemory.Formatted,
+ accessMemory.Access == MemoryAccess.Atomic)
+ ?? BufferLoweringStrategy.NativeBinding;
+ if (strategy == BufferLoweringStrategy.PhysicalStorageBuffer)
+ {
+ return TryEmitPhysicalStorageBufferMemory(instruction, control, out error);
+ }
+
+ if (strategy == BufferLoweringStrategy.BoundedCandidateTable)
+ {
+ return TryEmitBoundedCandidateTableMemory(instruction, control, accessMemoryIndex, out error);
+ }
}
+ if (!TryResolveLayoutBuffer(instruction.Pc, out var bindingIndex, out _))
+ {
+ error = "missing buffer-memory binding";
+ return false;
+ }
+
+ return EmitResolvedBufferMemory(instruction, control, bindingIndex, out error);
+ }
+
+ // One buffer operation against a resolved candidate binding: the dense binding path
+ // and every arm of a bounded candidate table share it, so the operation itself
+ // stays independent of how its descriptor was selected.
+ private bool EmitResolvedBufferMemory(
+ Gen5ShaderInstruction instruction,
+ Gen5BufferMemoryControl control,
+ int bindingIndex,
+ out string error)
+ {
+ error = string.Empty;
+ var info = _request.Resources.Info;
+ if (bindingIndex < 0 || bindingIndex >= info.Buffers.Count)
+ {
+ error = $"buffer binding {bindingIndex} is out of range";
+ return false;
+ }
+
+ var specialized = info.Buffers[bindingIndex];
+ var stride = UInt(specialized.PackedStride & 0x3FFF);
+ var descriptorFormat = specialized.DescriptorFormat;
+ var descriptorWord3 = UInt((specialized.DescriptorFormat << 12) | (specialized.DescriptorSwizzle & 0xFFF));
+
var scalarOffset = instruction.Sources.Count > 2
? GetRawSource(instruction, 2)
: UInt(0);
@@ -2261,6 +2496,44 @@ public static partial class Gen5SpirvTranslator
byteAddress = ApplyGuestBufferByteBias(bindingIndex, byteAddress);
var dwordAddress = ShiftRightLogical(byteAddress, UInt(2));
+ if (instruction.Opcode is "BufferAtomicSwapX2" or "BufferAtomicOrX2")
+ {
+ EmitExecConditional(() =>
+ {
+ var firstInRange = IsBufferWordInRange(bindingIndex, dwordAddress);
+ var secondAddress = IAdd(dwordAddress, UInt(1));
+ var secondInRange = IsBufferWordInRange(bindingIndex, secondAddress);
+ EmitConditional(LogicalAnd(firstInRange, secondInRange), () =>
+ {
+ var atomicOp = instruction.Opcode == "BufferAtomicSwapX2"
+ ? SpirvOp.AtomicExchange
+ : SpirvOp.AtomicOr;
+ var first = EmitAtomic(
+ atomicOp,
+ _uintType,
+ BufferWordPointer(bindingIndex, dwordAddress),
+ scope: 1,
+ semantics: 0x48,
+ value: () => LoadV(control.VectorData),
+ comparator: () => UInt(0));
+ var second = EmitAtomic(
+ atomicOp,
+ _uintType,
+ BufferWordPointer(bindingIndex, secondAddress),
+ scope: 1,
+ semantics: 0x48,
+ value: () => LoadV(control.VectorData + 1),
+ comparator: () => UInt(0));
+ if (control.Glc)
+ {
+ StoreV(control.VectorData, first);
+ StoreV(control.VectorData + 1, second);
+ }
+ });
+ });
+ return true;
+ }
+
if (instruction.Opcode.StartsWith("BufferAtomic", StringComparison.Ordinal))
{
var atomicSuffix = instruction.Opcode["BufferAtomic".Length..];
@@ -2481,6 +2754,114 @@ public static partial class Gen5SpirvTranslator
return true;
}
+ // BufferLoweringStrategy.BoundedCandidateTable: a runtime V# whose descriptors cannot
+ // be reconstructed from its raw words. The runtime V# sits in the scalar resource
+ // registers; its base-address dword is the probe key into the flattened candidate
+ // mapping, and each arm binds a native candidate and runs the ordinary buffer op.
+ private bool TryEmitBoundedCandidateTableMemory(
+ Gen5ShaderInstruction instruction,
+ Gen5BufferMemoryControl control,
+ int memoryIndex,
+ out string error)
+ {
+ error = string.Empty;
+ if (!_request.BufferCandidateTableByMemoryIndex.TryGetValue(memoryIndex, out var table))
+ {
+ // A formatted load needs a statically known Vulkan view format. Most
+ // runtime V#s prove a bounded SRT candidate set above, but a few Yotei
+ // material-lookup paths merge descriptors through control flow and have
+ // no finite, safe candidate table. Do not reinterpret an arbitrary
+ // device address with a guessed format: a null read is the defined
+ // fallback, matching the bindless-image fallback in ResourceTracker.
+ if (instruction.Opcode.StartsWith("BufferLoadFormat", StringComparison.Ordinal))
+ {
+ for (uint index = 0; index < control.DwordCount; index++)
+ {
+ StoreV(control.VectorData + index, UInt(0));
+ }
+
+ return true;
+ }
+
+ error = $"runtime buffer descriptor has no candidate table for {instruction.Opcode}";
+ return false;
+ }
+
+ if (table.CandidateCount == 0)
+ {
+ error = "runtime buffer descriptor candidate table is empty";
+ return false;
+ }
+
+ if (table.CandidateCount == 1)
+ {
+ return EmitResolvedBufferMemory(instruction, control, (int)table.FirstCandidate, out error);
+ }
+
+ if (!HasFlattenedTable)
+ {
+ error = "runtime buffer descriptor candidate table without a flattened table binding";
+ return false;
+ }
+
+ if (instruction.Sources.Count < 2 || instruction.Sources[1].Kind != Gen5OperandKind.ScalarRegister)
+ {
+ error = "runtime buffer descriptor has no scalar resource base";
+ return false;
+ }
+
+ var probeKey = LoadS(instruction.Sources[1].Value);
+ var selector = SelectBufferCandidate(table, probeKey);
+ var emitted = true;
+ var caseError = string.Empty;
+ for (uint index = 0; index < table.CandidateCount && emitted; index++)
+ {
+ var candidate = table.FirstCandidate + index;
+ EmitConditional(_module.AddInstruction(SpirvOp.IEqual, _boolType, selector, UInt(index)), () =>
+ {
+ if (!EmitResolvedBufferMemory(instruction, control, (int)candidate, out caseError))
+ {
+ emitted = false;
+ }
+ });
+ }
+
+ error = caseError;
+ return emitted;
+ }
+
+ // Searches the sorted base-address mapping of a candidate table for the runtime V#'s
+ // probe key; the result is the candidate-local index, candidate 0 when absent.
+ private uint SelectBufferCandidate(BufferCandidateTableUse table, uint key)
+ {
+ var mapping = UInt(table.MappingOffset);
+ var count = LoadFlattenedWord(mapping);
+ var low = UInt(0);
+ var high = count;
+ for (uint iteration = 0; iteration < table.SearchIterations; iteration++)
+ {
+ var span = _module.AddInstruction(SpirvOp.ISub, _uintType, high, low);
+ var middle = IAdd(low, ShiftRightLogical(span, UInt(1)));
+ var probeSlot = IAdd(IAdd(mapping, UInt(1)), ShiftLeftLogical(middle, UInt(1)));
+ var probeKey = LoadFlattenedWord(probeSlot);
+ var moveUp = LogicalAnd(
+ _module.AddInstruction(SpirvOp.ULessThan, _boolType, probeKey, key),
+ _module.AddInstruction(SpirvOp.ULessThan, _boolType, low, high));
+ low = _module.AddInstruction(SpirvOp.Select, _uintType, moveUp, IAdd(middle, UInt(1)), low);
+ high = _module.AddInstruction(SpirvOp.Select, _uintType, moveUp, high, middle);
+ }
+
+ var foundSlot = IAdd(IAdd(mapping, UInt(1)), ShiftLeftLogical(low, UInt(1)));
+ var found = LogicalAnd(
+ _module.AddInstruction(SpirvOp.ULessThan, _boolType, low, count),
+ _module.AddInstruction(SpirvOp.IEqual, _boolType, LoadFlattenedWord(foundSlot), key));
+ var mapped = LoadFlattenedWord(IAdd(foundSlot, UInt(1)));
+ var inRange = LogicalAnd(
+ found,
+ _module.AddInstruction(SpirvOp.ULessThan, _boolType, mapped, UInt(table.CandidateCount)));
+ return _module.AddInstruction(SpirvOp.Select, _uintType, inRange, mapped, UInt(0));
+ }
+
private bool TryEmitDeviceDescriptorBufferMemory(
Gen5ShaderInstruction instruction,
Gen5BufferMemoryControl control,
@@ -3695,8 +4076,10 @@ public static partial class Gen5SpirvTranslator
}
// Compare-exchange loop: the merge runs on the observed word until the
- // exchange succeeds.
- private void EmitAtomicWordUpdate(uint pointer, Func merge)
+ // exchange succeeds. Returns the observed (pre-exchange) word at the
+ // point the loop exits, which is a valid use here because it is
+ // defined by the OpPhi in `header`, and `header` dominates `mergeLabel`.
+ private uint EmitAtomicWordUpdate(uint pointer, Func merge)
{
var preheader = _module.AllocateId();
var header = _module.AllocateId();
@@ -3733,6 +4116,7 @@ public static partial class Gen5SpirvTranslator
_module.AddLabel(continueLabel);
_module.AddStatement(SpirvOp.Branch, header);
_module.AddLabel(mergeLabel);
+ return observed;
}
private static bool TryGetSubdwordLoadInfo(
@@ -3833,48 +4217,12 @@ public static partial class Gen5SpirvTranslator
return true;
}
- // BVH nodes are not traversed yet: every ray misses the node it is tested against.
- // The node type sits in the low three bits of the node pointer. A box node (4-7)
- // returns four invalid child pointers, a triangle node (0-3) returns t = +inf over
- // a denominator of 1, so the guest's traversal loop ends with no hit.
- private void EmitRayIntersectMiss(Gen5RayIntersectControl ray)
- {
- var nodeType = BitwiseAnd(LoadV(ray.GetAddressRegister(0)), UInt(7));
- var isTriangle = _module.AddInstruction(SpirvOp.ULessThan, _boolType, nodeType, UInt(4));
- uint[] triangleMiss = [0x7F800000u, 0x3F800000u, 0u, 0u];
- for (var component = 0u; component < Gen5RayIntersectControl.ResultDwords; component++)
- {
- var value = _module.AddInstruction(
- SpirvOp.Select,
- _uintType,
- isTriangle,
- UInt(triangleMiss[component]),
- UInt(0xFFFFFFFFu));
- StoreV(ray.VectorData + component, value);
- }
- }
-
private bool TryEmitImage(
Gen5ShaderInstruction instruction,
Gen5ImageControl image,
out string error)
{
error = string.Empty;
- if (instruction.Opcode is "ImageBvhIntersectRay" or "ImageBvh64IntersectRay")
- {
- // The host path does not expose Vulkan ray-query or an
- // acceleration-structure descriptor for GFX10's raw BVH
- // texture. Return a deterministic miss instead of rejecting
- // the complete compute shader. The instruction always writes
- // four DWORD result registers.
- for (uint component = 0; component < 4; component++)
- {
- StoreV(image.VectorData + component, UInt(0));
- }
-
- return true;
- }
-
SpirvImageResource resource;
uint imageObject;
uint dstSelect;
@@ -3954,7 +4302,8 @@ public static partial class Gen5SpirvTranslator
resource.IsStorage || resource.Multisampled
? [queryImage]
: [queryImage, LoadImageIntegerAddress(image, 0)]);
- var levels = !resource.IsStorage && !resource.Multisampled
+ // NVIDIA's compiler crashes on an unused level query of a 3D image.
+ var levels = (image.Dmask & 0x8u) != 0 && !resource.IsStorage && !resource.Multisampled
? _module.AddInstruction(
SpirvOp.ImageQueryLevels,
_uintType,
@@ -4097,6 +4446,50 @@ public static partial class Gen5SpirvTranslator
return false;
}
+ // IMAGE_ATOMIC_FMIN/FMAX target float-format storage images and
+ // have no native SPIR-V storage-image atomic without pulling in
+ // SPV_EXT_shader_atomic_float_min_max. Lower them as a
+ // compare-and-swap loop on the underlying bit pattern instead
+ // (same technique used by other float-atomic emulations that
+ // avoid that extension dependency).
+ if (instruction.Opcode is "ImageAtomicFmax" or "ImageAtomicFmin")
+ {
+ var isMax = instruction.Opcode == "ImageAtomicFmax";
+ var floatCoordinateCount = ImageCoordinateComponentCount(resource);
+ var floatCoordinates = BuildIntegerCoordinates(
+ image,
+ 0,
+ floatCoordinateCount);
+ EmitExecConditional(() =>
+ {
+ var pointer = _module.AddInstruction(
+ SpirvOp.ImageTexelPointer,
+ _module.TypePointer(SpirvStorageClass.Image, _uintType),
+ resource.Variable,
+ floatCoordinates,
+ UInt(0));
+ var srcBits = Bitcast(_uintType, LoadV(image.VectorData));
+ var old = EmitAtomicWordUpdate(pointer, observed =>
+ {
+ var oldFloat = Bitcast(_floatType, observed);
+ var srcFloat = Bitcast(_floatType, srcBits);
+ var pickSrc = _module.AddInstruction(
+ isMax ? SpirvOp.FOrdLessThan : SpirvOp.FOrdGreaterThan,
+ _boolType,
+ oldFloat,
+ srcFloat);
+ var chosen = _module.AddInstruction(
+ SpirvOp.Select, _floatType, pickSrc, srcFloat, oldFloat);
+ return Bitcast(_uintType, chosen);
+ });
+ if (image.Glc)
+ {
+ StoreV(image.VectorData, old);
+ }
+ });
+ return true;
+ }
+
if (resource.ComponentKind == ImageComponentKind.Float ||
!TryGetAtomicOp(instruction.Opcode["ImageAtomic".Length..], out var atomicOp))
{
@@ -4903,6 +5296,12 @@ public static partial class Gen5SpirvTranslator
? _intType
: _module.TypeVector(_intType, componentCount);
+ private uint IntegerTypeForComponents(uint componentCount) =>
+ componentCount == 1 ? _intType : _module.TypeVector(_intType, componentCount);
+
+ private uint FloatTypeForComponents(uint componentCount) =>
+ componentCount == 1 ? _floatType : _module.TypeVector(_floatType, componentCount);
+
private uint BuildFloatCoordinates(
Gen5ImageControl image,
int start,
@@ -6143,9 +6542,8 @@ public static partial class Gen5SpirvTranslator
value = 0;
var packedSource = exportInstruction.Sources[component >> 1];
var tracePackedExport =
- Environment.GetEnvironmentVariable(
- "SHARPEMU_TRACE_PACKED_EXPORT") == "1" &&
- _request.Program.Address == 0x0000000500781200ul;
+ Environment.GetEnvironmentVariable("SHARPEMU_TRACE_PACKED_EXPORT") == "1" &&
+ TraceShaderAddressMatches("SHARPEMU_TRACE_PACKED_EXPORT_ADDRESS");
if (tracePackedExport)
{
Console.Error.WriteLine(
@@ -6261,6 +6659,28 @@ public static partial class Gen5SpirvTranslator
return false;
}
+ private bool TraceShaderAddressMatches(string environmentVariable)
+ {
+ var filter = Environment.GetEnvironmentVariable(environmentVariable);
+ if (string.IsNullOrWhiteSpace(filter))
+ {
+ return true;
+ }
+
+ var span = filter.AsSpan();
+ if (span.StartsWith("0x", StringComparison.OrdinalIgnoreCase))
+ {
+ span = span[2..];
+ }
+
+ return ulong.TryParse(
+ span,
+ System.Globalization.NumberStyles.HexNumber,
+ System.Globalization.CultureInfo.InvariantCulture,
+ out var address) &&
+ _request.Program.Address == address;
+ }
+
private uint GetPixelOutputType(Gen5PixelOutputKind kind) =>
kind switch
{
@@ -6536,6 +6956,15 @@ public static partial class Gen5SpirvTranslator
left,
BitwiseAnd64(right, _module.Constant64(_ulongType, 63)));
+ private uint ShiftRightArithmetic64(uint left, uint right) =>
+ Bitcast(
+ _ulongType,
+ _module.AddInstruction(
+ SpirvOp.ShiftRightArithmetic,
+ _longType,
+ Bitcast(_longType, left),
+ BitwiseAnd64(right, _module.Constant64(_ulongType, 63))));
+
private uint BitwiseAnd(uint left, uint right) =>
_module.AddInstruction(SpirvOp.BitwiseAnd, _uintType, left, right);
@@ -6845,7 +7274,9 @@ public static partial class Gen5SpirvTranslator
private bool UsesLds() =>
_request.Program.Instructions.Any(instruction =>
- instruction.Control is Gen5DataShareControl);
+ instruction.Control is Gen5DataShareControl) ||
+ _request.Memory.Entries.Any(static memory =>
+ memory.AddressSpace is FlatAddressSpace.Shared or FlatAddressSpace.SharedOrPrivate);
private bool UsesSubgroupShuffle() =>
_request.Program.Instructions.Any(instruction =>
diff --git a/src/SharpEmu.ShaderCompiler.Vulkan/SpirvModuleBuilder.cs b/src/SharpEmu.ShaderCompiler.Vulkan/SpirvModuleBuilder.cs
index 4fcd834..485a002 100644
--- a/src/SharpEmu.ShaderCompiler.Vulkan/SpirvModuleBuilder.cs
+++ b/src/SharpEmu.ShaderCompiler.Vulkan/SpirvModuleBuilder.cs
@@ -197,6 +197,7 @@ public enum SpirvCapability : uint
Float16 = 9,
Float64 = 10,
Int64 = 11,
+ Int64Atomics = 12,
Int16 = 22,
ImageGatherExtended = 25,
StorageImageExtendedFormats = 49,
@@ -207,6 +208,8 @@ public enum SpirvCapability : uint
GroupNonUniformVote = 62,
GroupNonUniformBallot = 64,
GroupNonUniformShuffle = 65,
+ ShaderLayer = 5253,
+ ShaderViewportIndex = 5254,
ShaderViewportIndexLayerExt = 5254,
RuntimeDescriptorArray = 5302,
PhysicalStorageBufferAddresses = 5347,
diff --git a/src/SharpEmu.ShaderCompiler.Vulkan/TilerShaders.cs b/src/SharpEmu.ShaderCompiler.Vulkan/TilerShaders.cs
index 269e038..0662b24 100644
--- a/src/SharpEmu.ShaderCompiler.Vulkan/TilerShaders.cs
+++ b/src/SharpEmu.ShaderCompiler.Vulkan/TilerShaders.cs
@@ -201,14 +201,55 @@ public static class TilerShaders
_ => [X(6, 0x0c0), X(7, 0x200), X(8, 0x800), X(9, 0x2000), X(10, 0x8000), Y(4, 0x030), Y(6, 0x100), Y(7, 0x400), Y(8, 0x1000), Y(9, 0x4000)],
};
- private static Term[] RenderTargetTerms(uint bytes) => bytes switch
+ private readonly record struct AddressBit(Axis Source, byte Bit);
+
+ private static AddressBit BX(byte bit) => new(Axis.X, bit);
+
+ private static AddressBit BY(byte bit) => new(Axis.Y, bit);
+
+ private static AddressBit[] BXY(byte x, byte y) => [BX(x), BY(y)];
+
+ // PS5 Oberon RB+ R_X addressing. The destination bit index is the array
+ // index. Keep this in lockstep with GnmTiling.RbPlus64KRenderX and
+ // TileGeometry.RenderTargetOffset.
+ private static readonly AddressBit[][][] RenderTargetAddressBits =
+ [
+ [
+ [BX(0)], [BX(1)], [BX(2)], [BX(3)], [BY(0)], [BY(1)], [BY(2)], [BY(3)],
+ BXY(7, 4).Concat([BY(7)]).ToArray(), BXY(4, 4), BXY(6, 5), BXY(5, 6), [BX(6)], [BY(6)], BXY(7, 8), BXY(8, 7),
+ ],
+ [
+ [], [BX(0)], [BX(1)], [BX(2)], [BY(0)], [BY(1)], [BY(2)], [BX(3)],
+ BXY(7, 4).Concat([BY(7)]).ToArray(), BXY(4, 4), BXY(6, 5), BXY(5, 6), [BY(3)], [BX(6)], BXY(7, 7), BXY(8, 6),
+ ],
+ [
+ [], [], [BX(0)], [BX(1)], [BY(0)], [BY(1)], [BX(2)], [BY(2)],
+ BXY(7, 4).Concat([BY(7)]).ToArray(), BXY(4, 4), BXY(6, 5), BXY(5, 6), [BX(3)], [BY(3)], BXY(6, 7), BXY(7, 6),
+ ],
+ [
+ [], [], [], [BX(0)], [BY(0)], [BX(1)], [BX(2)], [BY(1)],
+ BXY(7, 4).Concat([BY(7)]).ToArray(), BXY(4, 4), BXY(6, 5), BXY(5, 6), [BY(2)], [BX(3)], BXY(7, 3), BXY(6, 6),
+ ],
+ [
+ [], [], [], [], [BX(0)], [BY(0)], [BX(1)], [BY(1)],
+ BXY(7, 4).Concat([BY(7)]).ToArray(), BXY(4, 4), BXY(6, 5), BXY(5, 6), [BX(2)], [BY(2)], BXY(6, 3), BXY(3, 6),
+ ],
+ ];
+
+ private static uint RenderTargetOffset(ShaderModuleContext shaderModule, uint x, uint y, uint bytes)
{
- 1 => [Y(2, 0x008), Y(4, 0x010), Y(3, 0x0a0), Y(5, 0xf00), Y(6, 0x1000), Y(7, 0x4000), X(0, 7), X(3, 0x040), X(5, 0x300), X(4, 0x400), X(6, 0x800), X(7, 0x2000), X(8, 0x8000)],
- 2 => [Y(4, 0x070), Y(5, 0xf00), Y(8, 0x5000), X(1, 0x00e), X(4, 0x480), X(5, 0x300), X(6, 0x800), X(7, 0x2000), X(8, 0x8000)],
- 4 => [Y(4, 0x070), Y(5, 0xf00), Y(9, 0x1000), Y(8, 0x4000), X(2, 0x00c), X(5, 0x380), X(4, 0x400), X(6, 0x800), X(9, 0xa000)],
- 8 => [Y(4, 0x010), Y(6, 0x080), Y(5, 0xf00), Y(10, 0x5000), X(3, 0x008), X(4, 0x460), X(5, 0x300), X(6, 0x800), X(10, 0x2000), X(9, 0x8000)],
- _ => [X(4, 0x410), X(5, 0x340), X(6, 0x800), X(11, 0xa000), Y(5, 0xf20), Y(6, 0x080), Y(10, 0x1000), Y(11, 0x4000)],
- };
+ uint offset = shaderModule.GetUnsignedConstant(0);
+ var table = RenderTargetAddressBits[Log2(bytes)];
+ for (uint destination = 0; destination < table.Length; destination++)
+ {
+ foreach (var bit in table[destination])
+ {
+ offset = shaderModule.Xor(offset, Bit(shaderModule, bit.Source == Axis.X ? x : y, bit.Bit, destination));
+ }
+ }
+
+ return offset;
+ }
private static Term[] DepthTerms(uint bytes) => bytes switch
{
@@ -306,7 +347,7 @@ public static class TilerShaders
return shaderModule.Xor(Standard64KB3DOffset(shaderModule, x, y, z, bytes), delta);
}
case TilerBlockShape.RenderTarget64KB:
- return shaderModule.Xor(XorTerms(shaderModule, x, y, z, RenderTargetTerms(bytes)), ZSpread(shaderModule, z));
+ return shaderModule.Xor(RenderTargetOffset(shaderModule, x, y, bytes), ZSpread(shaderModule, z));
case TilerBlockShape.Depth64KB:
return shaderModule.Xor(ZSpread(shaderModule, z), XorTerms(shaderModule, x, y, z, DepthTerms(bytes)));
default:
diff --git a/src/SharpEmu.ShaderCompiler/Gen5InlineConstants.cs b/src/SharpEmu.ShaderCompiler/Gen5InlineConstants.cs
index 672d80b..ece91a8 100644
--- a/src/SharpEmu.ShaderCompiler/Gen5InlineConstants.cs
+++ b/src/SharpEmu.ShaderCompiler/Gen5InlineConstants.cs
@@ -9,8 +9,52 @@ namespace SharpEmu.ShaderCompiler;
///
public static class Gen5InlineConstants
{
+ public const uint SharedBase = 235;
+ public const uint SharedLimit = 236;
+ public const uint PrivateBase = 237;
+ public const uint PrivateLimit = 238;
+
+ // The PS5 FLAT apertures, as title shaders hard-code them: a high dword of
+ // 0x7xxxxxxx selects LDS and 0x8xxxxxxx scratch. Guest device addresses fit in
+ // 40 bits and never reach them.
+ public const uint SharedApertureHigh = 0x7000_0000;
+ public const uint PrivateApertureHigh = 0x8000_0000;
+
+ // Each aperture is the window of high dwords sharing its top nibble.
+ public const int ApertureShift = 28;
+
+ public static bool IsSharedApertureHigh(uint high) => high >> ApertureShift == SharedApertureHigh >> ApertureShift;
+
+ public static bool IsPrivateApertureHigh(uint high) => high >> ApertureShift == PrivateApertureHigh >> ApertureShift;
+
+ public static bool IsAperture(uint encoded) => encoded is >= SharedBase and <= PrivateLimit;
+
+ public static bool IsSharedAperture(uint encoded) => encoded is SharedBase or SharedLimit;
+
+ // The 64-bit aperture value: a base has a zero low dword, a limit spans the
+ // whole 32-bit offset range. A 32-bit read returns the high dword.
+ public static ulong DecodeAperture64(uint encoded)
+ {
+ var high = IsSharedAperture(encoded) ? SharedApertureHigh : PrivateApertureHigh;
+ var low = encoded is SharedLimit or PrivateLimit ? uint.MaxValue : 0u;
+ return ((ulong)high << 32) | low;
+ }
+
public static bool TryDecode(uint encoded, out uint value)
{
+ if (IsAperture(encoded))
+ {
+ value = (uint)(DecodeAperture64(encoded) >> 32);
+ return true;
+ }
+
+ // POPS_EXITING_WAVE_ID: no primitive-ordered pixel shading is modeled.
+ if (encoded == 239)
+ {
+ value = 0;
+ return true;
+ }
+
if (encoded == 125)
{
value = 0;
diff --git a/src/SharpEmu.ShaderCompiler/Gen5ShaderIr.cs b/src/SharpEmu.ShaderCompiler/Gen5ShaderIr.cs
index 52d5116..fb48f84 100644
--- a/src/SharpEmu.ShaderCompiler/Gen5ShaderIr.cs
+++ b/src/SharpEmu.ShaderCompiler/Gen5ShaderIr.cs
@@ -160,7 +160,11 @@ public readonly record struct Gen5Operand(Gen5OperandKind Kind, uint Value)
return new(Gen5OperandKind.LiteralConstant, literal.Value);
}
- if (encoded <= 105 || encoded is 106 or 107 or 124 or 126 or 127)
+ // 108..123 are the temporary SGPRs (TTMP0..TTMP15). They use the
+ // same scalar register file as ordinary SGPRs; treating them as
+ // encoded constants loses values in shader continuations that spill
+ // through a temporary register.
+ if (encoded <= 105 || encoded is 106 or 107 or (>= 108 and <= 123) or 124 or 126 or 127)
{
return Scalar(encoded);
}
@@ -223,7 +227,9 @@ public sealed record Gen5GlobalMemoryControl(
int OffsetBytes,
bool Glc,
bool Slc,
- bool UsesFlatAddress = false) : Gen5InstructionControl;
+ bool UsesFlatAddress = false,
+ uint? DynamicOffsetRegister = null,
+ bool SourceIsScalar = false) : Gen5InstructionControl;
// A typed access carries the unified format from the instruction; a formatted
// untyped access reads the descriptor format when it executes.
diff --git a/src/SharpEmu.ShaderCompiler/Gen5ShaderTranslator.cs b/src/SharpEmu.ShaderCompiler/Gen5ShaderTranslator.cs
index d54f618..e4c1f3f 100644
--- a/src/SharpEmu.ShaderCompiler/Gen5ShaderTranslator.cs
+++ b/src/SharpEmu.ShaderCompiler/Gen5ShaderTranslator.cs
@@ -14,7 +14,10 @@ public static partial class Gen5ShaderTranslator
{
private static int _dppVectorsValidated;
- private const int MaxInstructions = 16384;
+ // Some shipped Gen5 shaders exceed 64 KiB of encoded instructions before
+ // reaching S_ENDPGM. Keep the malformed-shader guard, but size it to the
+ // largest declared shader object accepted by the decoder (1 MiB).
+ private const int MaxInstructions = (int)(MaximumDeclaredShaderSizeBytes / sizeof(uint));
private const ulong ShaderSizeOffset = 0x44;
private const uint MaximumDeclaredShaderSizeBytes = 1024 * 1024;
private static readonly ConditionalWeakTable