Alpha rebuild part2 (#893)

* [Libc] routed strcasecmp through HLE

* [Ampr] implemented command buffer streaming contract

* [Agc] completed command buffer operations

* [Kernel] added wide string formatting coverage

* [VideoOut] added output mode events and profiling fixes

* [Shader] expanded Gen5 resource and interpolation support

* [Gpu] fixed image, render, and dispatch handling

* [Memory] added guest address space pre-reservation

* [AMPR] deferred read file execution to command submission

* [SpirvTranslator] Add DsMinF32 and DsMaxF32 instructions

* [spirvtranslator] Added V_CMPX_NE_U64 instruction

* [spirvtranslator] added DsReadI8 instruction

* [test] fixed
This commit is contained in:
Berk 2026-09-21 00:29:16 +03:00 committed by GitHub
parent 9db40fcec8
commit cff4a167a6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
106 changed files with 5387 additions and 792 deletions

View File

@ -210,6 +210,13 @@ public sealed partial class DirectExecutionBackend
cpuContext[CpuRegister.R14] = *(ulong*)(argPackPtr + 80);
cpuContext[CpuRegister.R15] = *(ulong*)(argPackPtr + 88);
cpuContext[CpuRegister.Rsp] = (ulong)argPackPtr + 96uL;
cpuContext.SetImportStackArguments(
ReadImportStackArgument(argPackPtr, 0),
ReadImportStackArgument(argPackPtr, 1),
ReadImportStackArgument(argPackPtr, 2),
ReadImportStackArgument(argPackPtr, 3),
ReadImportStackArgument(argPackPtr, 4),
ReadImportStackArgument(argPackPtr, 5));
ulong value = cpuContext[CpuRegister.Rdi];
ulong value2 = cpuContext[CpuRegister.Rsi];
ulong num3 = cpuContext[CpuRegister.Rdx];
@ -1324,6 +1331,13 @@ public sealed partial class DirectExecutionBackend
cpuContext[CpuRegister.R14] = *(ulong*)(argPackPtr + 80);
cpuContext[CpuRegister.R15] = *(ulong*)(argPackPtr + 88);
cpuContext[CpuRegister.Rsp] = (ulong)argPackPtr + 96uL;
cpuContext.SetImportStackArguments(
ReadImportStackArgument(argPackPtr, 0),
ReadImportStackArgument(argPackPtr, 1),
ReadImportStackArgument(argPackPtr, 2),
ReadImportStackArgument(argPackPtr, 3),
ReadImportStackArgument(argPackPtr, 4),
ReadImportStackArgument(argPackPtr, 5));
if (_activeGuestThreadState is { } activeGuestThreadState)
{
@ -1584,7 +1598,7 @@ public sealed partial class DirectExecutionBackend
"8aI7R7WaOlc" or // sceAmprCommandBufferConstructor
"zgXifHT9ErY" or // sceVideoOutIsFlipPending
"V++UgBtQhn0" or // sceAgcGetDataPacketPayloadAddress
"qj7QZpgr9Uw" or // Gen5 graphics type-2 packet
"qj7QZpgr9Uw" or // Graphics context-state operation
"LtTouSCZjHM" or // sceAgcCbNop
"k3GhuSNmBLU" or // sceAgcCbDispatch
"UZbQjYAwwXM" or // sceAgcCbSetShRegistersDirect

View File

@ -1823,7 +1823,8 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
private static bool IsHlePreferredNid(string nid)
{
return string.Equals(nid, "QrZZdJ8XsX0", StringComparison.Ordinal) ||
string.Equals(nid, "Q3VBxCXhUHs", StringComparison.Ordinal);
string.Equals(nid, "Q3VBxCXhUHs", StringComparison.Ordinal) ||
string.Equals(nid, "AV6ipCNa4Rw", StringComparison.Ordinal);
}
private static bool IsLibcLibrary(string libraryName)

View File

@ -140,12 +140,12 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
private GuestSpaceOwner? _backedSpace;
public PhysicalVirtualMemory(IHostMemory? hostMemory = null, IHostViewMemory? viewHost = null,
ulong backingBytes = GuestMemoryLayout.BackingBytes)
ulong backingBytes = GuestMemoryLayout.BackingBytes, bool preReserveGuestAddressSpace = false)
{
_hostMemory = hostMemory ?? CrossPlatformHostMemory.Instance;
if (viewHost != null)
{
_backedSpace = new GuestSpaceOwner(viewHost, backingBytes);
_backedSpace = new GuestSpaceOwner(viewHost, backingBytes, preReserveGuestAddressSpace);
RunBackingSelfTest();
}
}

View File

@ -90,13 +90,15 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
ImportTraceLimit = Math.Max(0, options.ImportTraceLimit),
DebugHook = options.DebugHook,
};
var virtualMemory = new PhysicalVirtualMemory(
viewHost: HostViewMemory.Create(),
preReserveGuestAddressSpace: true);
var moduleManager = new ModuleManager();
// The compile-time generated registry (SharpEmu.SourceGenerators) is the sole
// registration source; content tests in SharpEmu.Libs.Tests pin its invariants.
moduleManager.RegisterExports(SharpEmu.Generated.SysAbiExportRegistry.CreateExports(Generation.Gen4 | Generation.Gen5));
moduleManager.Freeze();
var virtualMemory = new PhysicalVirtualMemory(viewHost: HostViewMemory.Create());
var gpuMemory = new GuestGpuMemory(virtualMemory);
GuestGpuMemoryHook.Attach(gpuMemory);

View File

@ -12,6 +12,8 @@ public sealed class CpuContext(ICpuMemory memory, Generation generation)
private readonly ulong[] _registers = new ulong[16];
private readonly ulong[] _xmmRegisters = new ulong[32];
private readonly ulong[] _ymmUpperRegisters = new ulong[32];
private readonly ulong[] _importStackArguments = new ulong[6];
private int _importStackArgumentCount;
private bool _raxWritten;
public ICpuMemory Memory { get; } = memory ?? throw new ArgumentNullException(nameof(memory));
@ -61,6 +63,40 @@ public sealed class CpuContext(ICpuMemory memory, Generation generation)
public bool WasRaxWritten => _raxWritten;
public void SetImportStackArguments(
ulong arg0,
ulong arg1,
ulong arg2,
ulong arg3,
ulong arg4,
ulong arg5)
{
_importStackArguments[0] = arg0;
_importStackArguments[1] = arg1;
_importStackArguments[2] = arg2;
_importStackArguments[3] = arg3;
_importStackArguments[4] = arg4;
_importStackArguments[5] = arg5;
_importStackArgumentCount = _importStackArguments.Length;
}
public void ClearImportStackArguments()
{
_importStackArgumentCount = 0;
}
public bool TryGetImportStackArgument(int index, out ulong value)
{
if ((uint)index < (uint)_importStackArgumentCount)
{
value = _importStackArguments[index];
return true;
}
value = 0;
return false;
}
public void GetXmmRegister(int registerIndex, out ulong low, out ulong high)
{
if ((uint)registerIndex >= 16)

View File

@ -19,6 +19,9 @@ public sealed class GuestSpaceOwner : IDisposable
internal static Action<string> OnFatal = message => Environment.FailFast(message);
public const ulong GuestPage = 0x4000;
private const ulong UserAddressStart = 0x10_0000_0000;
private const ulong UserAddressEnd = 0xFC_0000_0000;
private const ulong MinimumPreReservedRange = 0x0100_0000;
private readonly IHostViewMemory _host;
private readonly SharedBackingViews _views;
@ -29,13 +32,21 @@ public sealed class GuestSpaceOwner : IDisposable
private readonly List<(ulong Address, ulong Size)> _owned = new();
private bool _disposed;
public GuestSpaceOwner(IHostViewMemory host, ulong backingSize)
public GuestSpaceOwner(IHostViewMemory host, ulong backingSize, bool preReserveGuestAddressSpace = false)
{
_host = host;
Granularity = host.Granularity;
// Create lookup views before concurrent fault handlers can read the range table.
_ = _mapped.Keys;
_ = _mapped.Values;
if (preReserveGuestAddressSpace)
{
foreach (var range in _host.ReserveFreeAddressRanges(UserAddressStart, UserAddressEnd, MinimumPreReservedRange))
{
_owned.Add((range.Address, range.Size));
AddFreeRange(range.Address, range.Size);
}
}
_views = new SharedBackingViews(host, backingSize);
if (!_views.IsAvailable)
{

View File

@ -3,6 +3,8 @@
namespace SharpEmu.HLE.Host;
public readonly record struct HostAddressRange(ulong Address, ulong Size);
// The caller must own each range. The host does not check this on all platforms.
public interface IHostViewMemory
{
@ -14,6 +16,8 @@ public interface IHostViewMemory
ulong ReserveHole(ulong address, ulong size);
IReadOnlyList<HostAddressRange> ReserveFreeAddressRanges(ulong start, ulong end, ulong minimumSize) => [];
bool SplitHole(ulong address, ulong size);
bool JoinHoles(ulong address, ulong size);

View File

@ -13,6 +13,7 @@ internal sealed unsafe partial class WindowsHostViews : IHostViewMemory
private const uint MEM_COMMIT = 0x1000;
private const uint MEM_RESERVE = 0x2000;
private const uint MEM_RELEASE = 0x8000;
private const uint MEM_FREE = 0x10000;
private const uint MEM_COALESCE_PLACEHOLDERS = 0x1;
private const uint MEM_PRESERVE_PLACEHOLDER = 0x2;
private const uint MEM_REPLACE_PLACEHOLDER = 0x4000;
@ -88,6 +89,47 @@ internal sealed unsafe partial class WindowsHostViews : IHostViewMemory
return address;
}
public IReadOnlyList<HostAddressRange> ReserveFreeAddressRanges(ulong start, ulong end, ulong minimumSize)
{
var reserved = new List<HostAddressRange>();
if (start >= end || minimumSize == 0)
{
return reserved;
}
var current = start;
while (current < end)
{
if (VirtualQuery((void*)current, out var info, (nuint)sizeof(MemoryBasicInformation)) == 0)
{
break;
}
var regionEnd = Math.Min(end, info.BaseAddress + info.RegionSize);
if (info.State == MEM_FREE)
{
var reserveStart = AlignUp(Math.Max(start, info.BaseAddress), Granularity);
var reserveEnd = AlignDown(regionEnd, Granularity);
if (reserveEnd > reserveStart && reserveEnd - reserveStart >= minimumSize)
{
var size = reserveEnd - reserveStart;
if (ReserveHole(reserveStart, size) == reserveStart)
{
reserved.Add(new HostAddressRange(reserveStart, size));
}
}
}
if (regionEnd <= current)
{
break;
}
current = regionEnd;
}
return reserved;
}
public bool SplitHole(ulong address, ulong size) =>
VirtualFree((void*)address, (nuint)size, MEM_RELEASE | MEM_PRESERVE_PLACEHOLDER);
@ -212,6 +254,14 @@ internal sealed unsafe partial class WindowsHostViews : IHostViewMemory
return true;
}
private static ulong AlignUp(ulong value, ulong alignment)
{
var remainder = value % alignment;
return remainder == 0 ? value : value + (alignment - remainder);
}
private static ulong AlignDown(ulong value, ulong alignment) => value - value % alignment;
private static void ReleaseBackingObject(HostBackingObject backing)
{
UnmapViewOfFile((void*)backing.AliasBase);

View File

@ -233,6 +233,10 @@ public static partial class AgcExports
internal void RetainTargetlessDraw(RegisterBanks banks, in TargetlessDrawArguments arguments)
{
var state = RequireCurrent();
if (RenderTrace.Enabled)
{
RenderTrace.Write($"TargetlessRetention submit={arguments.SubmitId} replaced={state.RetainedTargetlessDraw is not null} previousSubmit={state.RetainedTargetlessDraw?.Arguments.SubmitId} export=0x{banks.Shader.Vertex.ExportAddress:X16} pixel=0x{banks.Shader.Pixel.Address:X16}");
}
state.RetainedTargetlessDraw = new RetainedTargetlessDraw(banks.Clone(), arguments);
}
@ -352,7 +356,7 @@ public static partial class AgcExports
}
public void Dispatch(ulong submitId, uint endX, uint endY, uint endZ, uint dispatchInitiator)
public void Dispatch(ulong submitId, uint endX, uint endY, uint endZ, uint dispatchInitiator, ulong indirectArgumentsAddress = 0)
{
var state = RequireCurrent();
if (_executor is { } executor)
@ -362,7 +366,7 @@ public static partial class AgcExports
var executorStarted = DcbParseProfile.Begin();
try
{
executor.Dispatch(submitId, banks, endX, endY, endZ, dispatchInitiator);
executor.Dispatch(submitId, banks, endX, endY, endZ, dispatchInitiator, indirectArgumentsAddress);
}
finally
{
@ -387,6 +391,10 @@ public static partial class AgcExports
if (!VideoOutExports.TryGetDisplayBufferInfo(handle, displayBufferIndex, out var displayBuffer) ||
!state.KnownColorTargets.TryGetValue(displayBuffer.Address, out var words))
{
if (RenderTrace.Enabled)
{
RenderTrace.Write($"TargetlessReplay submit={retained.Arguments.SubmitId} outcome=discarded reason=display-buffer-or-target-unavailable handle={handle} index={displayBufferIndex}");
}
return;
}
@ -394,6 +402,10 @@ public static partial class AgcExports
banks.Context.ColorTargets[0] = words;
banks.Context.RenderTargetMask = 0xF;
var arguments = retained.Arguments;
if (RenderTrace.Enabled)
{
RenderTrace.Write($"TargetlessReplay submit={arguments.SubmitId} outcome=begin destination=0x{displayBuffer.Address:X16}");
}
if (arguments.Indexed)
{
executor.DrawIndexed(arguments.SubmitId, banks, arguments.Indexed_);
@ -403,6 +415,10 @@ public static partial class AgcExports
executor.DrawAuto(arguments.SubmitId, banks, arguments.Auto);
}
if (RenderTrace.Enabled)
{
RenderTrace.Write($"TargetlessReplay submit={arguments.SubmitId} outcome=executor-returned destination=0x{displayBuffer.Address:X16}");
}
TraceAgcShader(
$"agc.deferred_composite dst=0x{displayBuffer.Address:X16} export=0x{banks.Shader.Vertex.ExportAddress:X16} " +
$"pixel=0x{banks.Shader.Pixel.Address:X16} size={displayBuffer.Width}x{displayBuffer.Height}");
@ -491,8 +507,8 @@ public static partial class AgcExports
public override void DrawAuto(ulong submitId, in DrawAutoArguments arguments) =>
Translation.DrawAuto(submitId, in arguments);
public override void DispatchDirect(ulong submitId, uint groupsX, uint groupsY, uint groupsZ, uint dispatchInitiator) =>
Translation.Dispatch(submitId, groupsX, groupsY, groupsZ, dispatchInitiator);
public override void DispatchDirect(ulong submitId, uint groupsX, uint groupsY, uint groupsZ, uint dispatchInitiator, ulong indirectArgumentsAddress = 0) =>
Translation.Dispatch(submitId, groupsX, groupsY, groupsZ, dispatchInitiator, indirectArgumentsAddress);
public override void OnQueueReset(int queueId) => Translation.QueueReset(queueId);
@ -515,7 +531,7 @@ public static partial class AgcExports
public override void DrawAuto(ulong submitId, in DrawAutoArguments arguments) =>
Translation.RecordAutoDrawState(in arguments);
public override void DispatchDirect(ulong submitId, uint groupsX, uint groupsY, uint groupsZ, uint dispatchInitiator)
public override void DispatchDirect(ulong submitId, uint groupsX, uint groupsY, uint groupsZ, uint dispatchInitiator, ulong indirectArgumentsAddress = 0)
{
Dispatches.Add((submitId, groupsX, groupsY, groupsZ, dispatchInitiator));
}

View File

@ -157,9 +157,8 @@ public static partial class AgcExports
return (int)ctx[CpuRegister.Rax];
}
// COND_EXEC gates the following execCount dwords on a 32-bit predicate in
// memory. Our submitted-packet walker skips unknown PM4 ops, so the recorded
// packet degrades to "predicate always true" — the gated commands always run.
// The interpreter reads the predicate when this packet executes.
// A zero value skips the specified number of following dwords.
[SysAbiExport(
Nid = "BIPexNBSGog",
ExportName = "sceAgcDcbCondExec",

View File

@ -10,6 +10,58 @@ public static partial class AgcExports
{
// This partial constructs AGC DMA transfer packets.
[SysAbiExport(
Nid = "1rZSWUv1IRc",
ExportName = "sceAgcDcbCopyData",
Target = Generation.Gen5,
LibraryName = "libSceAgc")]
public static int DcbCopyData(CpuContext ctx)
{
var commandBufferAddress = ctx[CpuRegister.Rdi];
var destinationSelector = (uint)(ctx[CpuRegister.Rsi] & 0xFF);
var destinationPolicy = (uint)(ctx[CpuRegister.Rdx] & 0xFF);
var destinationAddress = ctx[CpuRegister.Rcx];
var sourceSelector = (uint)(ctx[CpuRegister.R8] & 0xFF);
var sourcePolicy = (uint)(ctx[CpuRegister.R9] & 0xFF);
var stackAddress = ctx[CpuRegister.Rsp];
if (!TryReadUInt64(ctx, stackAddress + sizeof(ulong), out var sourceValue) ||
!TryReadUInt64(ctx, stackAddress + (2 * sizeof(ulong)), out var elementSizeRaw) ||
!TryReadUInt64(ctx, stackAddress + (3 * sizeof(ulong)), out var confirmWriteRaw) ||
commandBufferAddress == 0 ||
!TryAllocateCommandDwords(ctx, commandBufferAddress, 6, out var packetAddress))
{
return ReturnPointer(ctx, 0);
}
var elementSize = (uint)(elementSizeRaw & 0xFF);
var confirmWrite = (uint)(confirmWriteRaw & 0xFF);
var control =
((sourceSelector >> 1) & 0xFu) |
(((destinationSelector >> 1) & 0xFu) << 8) |
((sourcePolicy & 0x3u) << 13) |
((elementSize & 0x1u) << 16) |
((confirmWrite & 0x1u) << 20) |
((destinationPolicy & 0x3u) << 25) |
((sourceSelector & 0x1u) << 30);
if (!TryWriteUInt32(ctx, packetAddress, Pm4(6, ItCopyData, 0)) ||
!TryWriteUInt32(ctx, packetAddress + 4, control) ||
!TryWriteUInt32(ctx, packetAddress + 8, (uint)sourceValue) ||
!TryWriteUInt32(ctx, packetAddress + 12, (uint)(sourceValue >> 32)) ||
!TryWriteUInt32(ctx, packetAddress + 16, (uint)destinationAddress) ||
!TryWriteUInt32(ctx, packetAddress + 20, (uint)(destinationAddress >> 32)))
{
return ReturnPointer(ctx, 0);
}
TraceAgc(
$"agc.dcb_copy_data buf=0x{commandBufferAddress:X16} cmd=0x{packetAddress:X16} " +
$"dstSel=0x{destinationSelector:X2} srcSel=0x{sourceSelector:X2} " +
$"dst=0x{destinationAddress:X16} src=0x{sourceValue:X16} control=0x{control:X8}");
return ReturnPointer(ctx, packetAddress);
}
[SysAbiExport(
Nid = "WmAc2MEj6Io",
ExportName = "sceAgcDcbDmaData",

View File

@ -267,7 +267,7 @@ public static partial class AgcExports
public static int DriverUnregisterOwnerAndResources(CpuContext ctx)
{
var owner = (uint)ctx[CpuRegister.Rdi];
var state = _submittedGpuStates.GetValue(ctx.Memory, static _ => new SubmittedGpuState());
var state = _submittedGpuStates.GetValue(CanonicalMemory(ctx.Memory), static _ => new SubmittedGpuState());
int resources;
lock (state.Gate)
{
@ -292,7 +292,7 @@ public static partial class AgcExports
public static int DriverUnregisterAllResourcesForOwner(CpuContext ctx)
{
var owner = (uint)ctx[CpuRegister.Rdi];
var state = _submittedGpuStates.GetValue(ctx.Memory, static _ => new SubmittedGpuState());
var state = _submittedGpuStates.GetValue(CanonicalMemory(ctx.Memory), static _ => new SubmittedGpuState());
int resources;
lock (state.Gate)
{

View File

@ -198,20 +198,48 @@ public static partial class AgcExports
ExportName = "sceAgcUnknownQj7QZpgr9Uw",
Target = Generation.Gen5,
LibraryName = "libSceAgc")]
public static int UnknownQj7QZpgr9Uw(CpuContext ctx)
public static int DcbContextStateOperation(CpuContext ctx)
{
var commandBufferAddress = ctx[CpuRegister.Rdi];
if (commandBufferAddress == 0 ||
!TryAllocateCommandDwords(ctx, commandBufferAddress, 1, out var commandAddress) ||
!TryWriteUInt32(ctx, commandAddress, 0x8000_0000))
var operation = (uint)ctx[CpuRegister.Rsi];
if (commandBufferAddress == 0 || operation > (uint)ContextStateOperation.PushClear)
{
return ReturnPointer(ctx, 0);
}
TraceAgc(
$"agc.unknown_qj7 buf=0x{commandBufferAddress:X16} cmd=0x{commandAddress:X16} " +
$"arg1=0x{ctx[CpuRegister.Rsi]:X16} arg2=0x{ctx[CpuRegister.Rdx]:X16}");
return ReturnPointer(ctx, commandAddress);
ulong firstPacket = 0;
bool Append(uint dwords)
{
if (!TryAllocateCommandDwords(ctx, commandBufferAddress, dwords, out var address))
return false;
Span<byte> packet = stackalloc byte[36];
packet.Clear();
var first = firstPacket == 0;
BinaryPrimitives.WriteUInt32LittleEndian(packet, Pm4(dwords, PacketOpcode.Nop, first ? PacketCustomCode.ContextState : 0));
if (first)
BinaryPrimitives.WriteUInt32LittleEndian(packet[4..], operation);
if (!ctx.Memory.TryWrite(address, packet[..(int)(dwords * sizeof(uint))]))
return false;
if (first)
firstPacket = address;
return true;
}
bool AppendSaveRestore() =>
TryPrepareCommandDwords(ctx, commandBufferAddress, 22, false, out _) &&
Append(5) && Append(8) && Append(9);
// Keep allocation boundaries while the first packet carries the context operation.
var complete = (ContextStateOperation)operation switch
{
ContextStateOperation.Clear => Append(5),
ContextStateOperation.Push => AppendSaveRestore() && Append(3) && Append(2),
ContextStateOperation.Pop => Append(3) && AppendSaveRestore() && Append(2),
ContextStateOperation.PushClear => AppendSaveRestore() && Append(3) && Append(2) && Append(5),
_ => false,
};
TraceAgc($"agc.context_state buf=0x{commandBufferAddress:X16} operation={operation} complete={complete}");
return ReturnPointer(ctx, complete ? firstPacket : 0);
}
#pragma warning restore SHEM006

View File

@ -201,10 +201,10 @@ public static partial class AgcExports
return false;
}
// Preserve the high address byte and the full width and height fields.
// Prospero T# stores width-minus-one across 14 bits and height-minus-one in 14 bits.
var address = (((ulong)(fields[1] & 0xFFu) << 32) | fields[0]) << 8;
var width = (((fields[1] >> 30) & 0x3u) | ((fields[2] & 0x3FFFu) << 2)) + 1;
var height = ((fields[2] >> 14) & 0xFFFFu) + 1;
var width = (((fields[1] >> 30) & 0x3u) | ((fields[2] & 0x0FFFu) << 2)) + 1;
var height = ((fields[2] >> 14) & 0x3FFFu) + 1;
var unifiedFormat = (fields[1] >> 20) & 0x1FFu;
if (unifiedFormat == 0 ||
!Gfx10UnifiedFormat.TryDecode(

View File

@ -38,6 +38,7 @@ public static partial class AgcExports
private const uint ItDispatchIndirect = 0x16;
private const uint ItSetPredication = 0x20;
private const uint ItCondExec = 0x22;
private const uint ItCopyData = 0x40;
private const uint ItWaitRegMem = 0x3C;
private const uint ItIndirectBuffer = 0x3F;
private const uint ItCondWrite = 0x45;
@ -487,6 +488,9 @@ public static partial class AgcExports
}
private static bool TryAllocateCommandDwords(CpuContext ctx, ulong commandBufferAddress, uint sizeDwords, out ulong commandAddress)
=> TryPrepareCommandDwords(ctx, commandBufferAddress, sizeDwords, true, out commandAddress);
private static bool TryPrepareCommandDwords(CpuContext ctx, ulong commandBufferAddress, uint sizeDwords, bool advanceCursor, out ulong commandAddress)
{
commandAddress = 0;
if (sizeDwords == 0 ||
@ -542,7 +546,7 @@ public static partial class AgcExports
}
var nextCursor = cursorUp + ((ulong)sizeDwords * sizeof(uint));
if (!ctx.TryWriteUInt64(commandBufferAddress + CommandBufferCursorUpOffset, nextCursor))
if (advanceCursor && !ctx.TryWriteUInt64(commandBufferAddress + CommandBufferCursorUpOffset, nextCursor))
{
return false;
}

File diff suppressed because it is too large Load Diff

View File

@ -19,26 +19,43 @@ internal static class AmprFileRegistry
concurrencyLevel: Math.Max(4, Environment.ProcessorCount),
capacity: 1_048_576);
private static readonly object _indexGate = new();
private static readonly object _resolvedFileGate = new();
private static readonly Dictionary<string, uint> _resolvedIdsByPath = new(HostFsPath.Comparer);
private static readonly ConcurrentDictionary<uint, string> _resolvedPathsById = new();
private static readonly ConcurrentDictionary<uint, byte> _ambiguousCompatibilityIds = 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;
private static string? _indexedApp0Root;
private static string? _indexingApp0Root;
private static int _preloadStarted;
public static uint Register(string guestPath, string hostPath)
{
var resolvedPathKey = Path.GetFullPath(hostPath);
if (TryGetApp0Relative(guestPath, out var relative) && relative.Length != 0)
{
RegisterApp0Relative(relative, hostPath);
return ComputeFileId("$/" + relative);
}
var id = ComputeFileId(guestPath);
_hostPathsById[id] = hostPath;
return id;
lock (_resolvedFileGate)
{
if (_resolvedIdsByPath.TryGetValue(resolvedPathKey, out var existingId))
return existingId;
if (_nextResolvedId == uint.MaxValue)
throw new InvalidOperationException("APR file identifiers are exhausted.");
var resolvedId = _nextResolvedId++;
_resolvedPathsById[resolvedId] = hostPath;
_resolvedIdsByPath.Add(resolvedPathKey, resolvedId);
return resolvedId;
}
}
public static bool TryGetHostPath(uint id, out string hostPath)
{
return _hostPathsById.TryGetValue(id, out hostPath!);
if ((id & 0x80000000) != 0)
return _resolvedPathsById.TryGetValue(id, out hostPath!);
hostPath = null!;
return !_ambiguousCompatibilityIds.ContainsKey(id) && _hostPathsById.TryGetValue(id, out hostPath!);
}
/// <summary>Test hook: wipe registry state between cases.</summary>
@ -47,6 +64,13 @@ internal static class AmprFileRegistry
lock (_indexGate)
{
_hostPathsById.Clear();
_ambiguousCompatibilityIds.Clear();
lock (_resolvedFileGate)
{
_resolvedIdsByPath.Clear();
_resolvedPathsById.Clear();
_nextResolvedId = 0x80000000;
}
_indexedApp0Root = null;
_indexingApp0Root = null;
_preloadStarted = 0;
@ -257,12 +281,19 @@ internal static class AmprFileRegistry
private static void Publish(uint hash, string relative, string hostPath)
{
_hostPathsById[FnvContinueUtf8(hash, relative)] = hostPath;
PublishCompatibilityPath(NormalizeFileId(FnvContinueUtf8(hash, relative)), hostPath);
}
private static void PublishCompatibilityPath(uint id, string hostPath)
{
var existingPath = _hostPathsById.GetOrAdd(id, hostPath);
if (!HostFsPath.Comparer.Equals(existingPath, hostPath))
_ambiguousCompatibilityIds.TryAdd(id, 0);
}
internal static uint ComputeFileId(string guestPath)
{
return FnvContinueUtf8(OffsetBasis, guestPath);
return NormalizeFileId(FnvContinueUtf8(OffsetBasis, guestPath));
}
internal static IEnumerable<string> EnumerateApp0PathAliases(string guestPath)
@ -332,7 +363,8 @@ internal static class AmprFileRegistry
// Distinct roots must not share a cache file. Folding case is only
// correct where the host filesystem folds it too.
var rootKey = OperatingSystem.IsWindows() ? normalizedRoot.ToLowerInvariant() : normalizedRoot;
var rootHash = ComputeFileId(rootKey);
// Keep raw hashes for cache filenames. Guest file identifiers use 31 bits.
var rootHash = FnvContinueUtf8(OffsetBasis, rootKey);
return Path.Combine(cacheDir, $"app0-{rootHash:x8}.v{version}.idx");
}
@ -427,10 +459,11 @@ internal static class AmprFileRegistry
? normalizedRoot + entry.Relative.Replace('/', Path.DirectorySeparatorChar)
: normalizedRoot + Path.DirectorySeparatorChar +
entry.Relative.Replace('/', Path.DirectorySeparatorChar);
_hostPathsById[entry.Id0] = hostPath;
_hostPathsById[entry.Id1] = hostPath;
_hostPathsById[entry.Id2] = hostPath;
_hostPathsById[entry.Id3] = hostPath;
// Normalize cached identifiers to the guest's 31-bit range.
PublishCompatibilityPath(NormalizeFileId(entry.Id0), hostPath);
PublishCompatibilityPath(NormalizeFileId(entry.Id1), hostPath);
PublishCompatibilityPath(NormalizeFileId(entry.Id2), hostPath);
PublishCompatibilityPath(NormalizeFileId(entry.Id3), hostPath);
});
}
else
@ -539,12 +572,14 @@ internal static class AmprFileRegistry
out uint app0,
out uint bare)
{
var dollar = FnvContinueUtf8(
var dollar = NormalizeFileId(FnvContinueUtf8(
FnvContinueAscii(FnvContinueAscii(OffsetBasis, (byte)'$'), (byte)'/'),
relative);
app0Slash = FnvContinueUtf8(FnvContinueAsciiPrefix(OffsetBasis, "/app0/"u8), relative);
app0 = FnvContinueUtf8(FnvContinueAsciiPrefix(OffsetBasis, "app0/"u8), relative);
bare = FnvContinueUtf8(OffsetBasis, relative);
relative));
app0Slash = NormalizeFileId(
FnvContinueUtf8(FnvContinueAsciiPrefix(OffsetBasis, "/app0/"u8), relative));
app0 = NormalizeFileId(
FnvContinueUtf8(FnvContinueAsciiPrefix(OffsetBasis, "app0/"u8), relative));
bare = NormalizeFileId(FnvContinueUtf8(OffsetBasis, relative));
return dollar;
}
@ -571,6 +606,13 @@ internal static class AmprFileRegistry
private const uint OffsetBasis = 2166136261;
private const uint FnvPrime = 16777619;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static uint NormalizeFileId(uint hash)
{
// Keep compatibility hashes in the nonnegative signed 32-bit range.
return hash & 0x7fffffffu;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static uint FnvContinueAscii(uint hash, byte value)
{

View File

@ -125,8 +125,8 @@ public sealed partial class GpuCommandInterpreter
DrawIndexed(packetAddress, opcode, indexCount, indexAddress, indexedInstanceCount, unchecked((int)baseVertex), indexedStartInstance, DrawOffsetSource.IndirectArguments);
}
internal void DispatchDirect(uint groupsX, uint groupsY, uint groupsZ, uint dispatchInitiator) =>
_host.DispatchDirect(SubmitId, groupsX, groupsY, groupsZ, dispatchInitiator);
internal void DispatchDirect(uint groupsX, uint groupsY, uint groupsZ, uint dispatchInitiator, ulong indirectArgumentsAddress = 0) =>
_host.DispatchDirect(SubmitId, groupsX, groupsY, groupsZ, dispatchInitiator, indirectArgumentsAddress);
internal void DispatchIndirect(uint dataOffset, uint dispatchInitiator)
{
@ -136,7 +136,7 @@ public sealed partial class GpuCommandInterpreter
}
var argumentsAddress = DispatchIndirectArgumentsBase + dataOffset;
DispatchDirect(ReadDword(argumentsAddress), ReadDword(argumentsAddress + 4), ReadDword(argumentsAddress + 8), dispatchInitiator);
DispatchDirect(ReadDword(argumentsAddress), ReadDword(argumentsAddress + 4), ReadDword(argumentsAddress + 8), dispatchInitiator, argumentsAddress);
}
internal uint DrawIndexPacket(in PacketContext packet, ReadOnlySpan<uint> payload)
@ -271,7 +271,7 @@ public sealed partial class GpuCommandInterpreter
throw _host.Fatal($"The indirect dispatch arguments address is zero: address=0x{packet.PacketAddress:X16}.");
}
DispatchDirect(ReadDword(argumentsAddress), ReadDword(argumentsAddress + 4), ReadDword(argumentsAddress + 8), payload[2]);
DispatchDirect(ReadDword(argumentsAddress), ReadDword(argumentsAddress + 4), ReadDword(argumentsAddress + 8), payload[2], argumentsAddress);
return 3;
}

View File

@ -119,7 +119,7 @@ public interface ICommandStreamHost
void DrawAuto(ulong submitId, in DrawAutoArguments arguments);
void DispatchDirect(ulong submitId, uint groupsX, uint groupsY, uint groupsZ, uint dispatchInitiator);
void DispatchDirect(ulong submitId, uint groupsX, uint groupsY, uint groupsZ, uint dispatchInitiator, ulong indirectArgumentsAddress = 0);
// Called when a queue reset packet clears the processor.
void OnQueueReset(int queueId);

View File

@ -221,7 +221,7 @@ public class ManagedCommandStreamHost : ICommandStreamHost
{
}
public virtual void DispatchDirect(ulong submitId, uint groupsX, uint groupsY, uint groupsZ, uint dispatchInitiator)
public virtual void DispatchDirect(ulong submitId, uint groupsX, uint groupsY, uint groupsZ, uint dispatchInitiator, ulong indirectArgumentsAddress = 0)
{
}

View File

@ -192,6 +192,22 @@ public struct ColorControlRegisters
};
}
public struct DepthRenderOverrideRegisters
{
public bool ForceZValid;
public bool ForceZDirty;
public bool ForceStencilValid;
public bool ForceStencilDirty;
public static DepthRenderOverrideRegisters Decode(uint value) => new()
{
ForceZValid = (value & 0x2000_0000u) != 0,
ForceZDirty = (value & 0x0800_0000u) != 0,
ForceStencilValid = (value & 0x4000_0000u) != 0,
ForceStencilDirty = (value & 0x1000_0000u) != 0,
};
}
public struct ScanModeRegisters
{
public bool MultisampleEnable;
@ -348,6 +364,7 @@ public sealed class ContextRegisters
public ScreenViewportRegisters ScreenViewport = new();
public ClipControlRegisters Clip;
public ColorControlRegisters ColorControl = new();
public DepthRenderOverrideRegisters DepthRenderOverride;
public ScanModeRegisters ScanMode = new();
public SampleLocationRegisters SampleLocations = new();
public AntialiasingConfigRegisters AntialiasingConfig;

View File

@ -57,6 +57,7 @@ public sealed class RegisterBanks
public void ApplyContextState(ContextStateOperation operation)
{
var previousViewport = Context.ScreenViewport.Viewports[0];
switch (operation)
{
case ContextStateOperation.Clear:
@ -85,6 +86,13 @@ public sealed class RegisterBanks
default:
throw Fatal($"The context state operation is unknown: operation={(uint)operation}.");
}
if (Rendering.RenderTrace.Enabled)
{
var currentViewport = Context.ScreenViewport.Viewports[0];
Rendering.RenderTrace.Write(
$"ContextState operation={operation} previousZScale={previousViewport.ZScale} previousZOffset={previousViewport.ZOffset} " +
$"zScale={currentViewport.ZScale} zOffset={currentViewport.ZOffset}");
}
}
private void Push()

View File

@ -26,8 +26,19 @@ public static class RegisterWriteTable
RegisterWriters.FillUserConfig(UserConfig, UserConfigIndirect);
}
public static uint WriteContextPacket(RegisterBanks banks, in PacketContext packet, uint offset, ReadOnlySpan<uint> values) =>
WritePacket(banks, in packet, offset, values, Context, ContextIndirect, "context", emptyIsHandled: true);
public static uint WriteContextPacket(RegisterBanks banks, in PacketContext packet, uint offset, ReadOnlySpan<uint> values)
{
var consumed = WritePacket(banks, in packet, offset, values, Context, ContextIndirect, "context", emptyIsHandled: true);
if (Rendering.RenderTrace.Enabled)
{
for (var index = 0; index < values.Length && index < consumed; index++)
{
TraceDepthStateWrite(offset + (uint)index, values[index], packet.PacketAddress, "packet");
}
}
return consumed;
}
// A shader packet without values has no writer; the other banks accept it.
public static uint WriteShaderPacket(RegisterBanks banks, in PacketContext packet, uint offset, ReadOnlySpan<uint> values)
@ -44,8 +55,27 @@ public static class RegisterWriteTable
public static uint WriteUserConfigPacket(RegisterBanks banks, in PacketContext packet, uint offset, ReadOnlySpan<uint> values) =>
WritePacket(banks, in packet, offset, values, UserConfig, UserConfigIndirect, "user-config", emptyIsHandled: true);
public static void WriteContextEntry(RegisterBanks banks, uint offset, uint value, ulong tableAddress) =>
public static void WriteContextEntry(RegisterBanks banks, uint offset, uint value, ulong tableAddress)
{
WriteEntry(banks, offset, value, ContextIndirect, "context", tableAddress);
if (Rendering.RenderTrace.Enabled)
{
TraceDepthStateWrite(offset, value, tableAddress, "table");
}
}
private static void TraceDepthStateWrite(uint offset, uint value, ulong address, string source)
{
if (offset != ContextRegisterOffset.PaClClipCntl &&
offset != ContextRegisterOffset.PaClVteCntl &&
offset != ContextRegisterOffset.PaClViewportXScale + 4 &&
offset != ContextRegisterOffset.PaClViewportXScale + 5)
{
return;
}
Rendering.RenderTrace.Write($"DepthStateWrite source={source} address=0x{address:X16} register=0x{offset:X4} value=0x{value:X8}");
}
public static void WriteShaderEntry(RegisterBanks banks, uint offset, uint value, ulong tableAddress)
{

View File

@ -16,9 +16,10 @@ internal static partial class RegisterWriters
public static void FillContext(RegisterPacketWriter?[] direct, RegisterWriter?[] indirect)
{
direct[DbRenderControl] = RenderControlPacket;
direct[DbRenderOverride] = DepthRenderOverridePacket;
foreach (var offset in new[]
{
DbCountControl, DbRenderOverride, DbRenderOverride2, DbDfsmControl, DbRmiL2CacheControl, CbRmiGl2CacheControl,
DbCountControl, DbRenderOverride2, DbDfsmControl, DbRmiL2CacheControl, CbRmiGl2CacheControl,
TaBcBaseAddr, TaBcBaseAddrHi, CbDccControl, PaSuPointSize, PaSuPointMinMax, DbAlphaToMask, VgtDrawPayloadCntl,
VgtPrimitiveIdReset, PaClObjPrimIdCntl, PaSuVtxCntl, PaScFovWindowLr, PaScFovWindowTb, PaScModeCntl1,
PaScAaMaskX0Y0X1Y0, PaScAaMaskX0Y1X1Y1, PsShaderSampleExclusionMask, PaScBinnerCntl0, PaScBinnerCntl1,
@ -164,7 +165,7 @@ internal static partial class RegisterWriters
foreach (var offset in new[]
{
CbDccControl, DbCountControl, DbSResultsCompareState0, DbSResultsCompareState1, DbRenderOverride, DbRenderOverride2,
CbDccControl, DbCountControl, DbSResultsCompareState0, DbSResultsCompareState1, DbRenderOverride2,
DbDfsmControl, DbRmiL2CacheControl, CbRmiGl2CacheControl, TaBcBaseAddr, TaBcBaseAddrHi, PaSuPointSize, PaSuPointMinMax,
SpiTmpringSize, VgtDrawPayloadCntl, VgtPrimitiveIdReset, PaClObjPrimIdCntl, PaScFovWindowLr, PaScFovWindowTb, PaScFsrEnable,
FsrRecursions0, FsrRecursions1, PaScModeCntl1, PaScAaMaskX0Y0X1Y0, PaScAaMaskX0Y1X1Y1, PaSuVtxCntl, PsShaderSampleExclusionMask,
@ -175,6 +176,7 @@ internal static partial class RegisterWriters
}
indirect[SpiVsOutConfig] = static (banks, _, value) => banks.Context.ShaderInterface.VertexOutputConfiguration = value;
indirect[DbRenderOverride] = static (banks, _, value) => banks.Context.DepthRenderOverride = DepthRenderOverrideRegisters.Decode(value);
indirect[SpiShaderPosFormat] = static (banks, _, value) => banks.Context.ShaderInterface.PositionExportFormat = value;
indirect[SpiShaderIdxFormat] = static (banks, _, value) => banks.Context.ShaderInterface.IndexExportFormat = value;
indirect[PaClVsOutCntl] = static (banks, _, value) => banks.Context.ShaderInterface.VertexOutputControl = value;
@ -314,6 +316,9 @@ internal static partial class RegisterWriters
private static uint RenderControlPacket(RegisterBanks banks, in PacketContext packet, uint offset, ReadOnlySpan<uint> values) =>
SingleValue(banks, in packet, offset, values, RenderControlEntry);
private static uint DepthRenderOverridePacket(RegisterBanks banks, in PacketContext packet, uint offset, ReadOnlySpan<uint> values) =>
SingleValue(banks, in packet, offset, values, RegisterWriteTable.ContextIndirect[DbRenderOverride]!);
private static uint StencilClearPacket(RegisterBanks banks, in PacketContext packet, uint offset, ReadOnlySpan<uint> values) =>
SingleValue(banks, in packet, offset, values, RegisterWriteTable.ContextIndirect[DbStencilClear]!);

View File

@ -162,15 +162,59 @@ public sealed unsafe partial class CachedImage
return new CommandBuffer(_scheduler.Current.Handle);
}
private BufferImageCopy[]? SanitizeUploadCopies(ReadOnlySpan<BufferImageCopy> copies)
{
BufferImageCopy[]? sanitized = null;
for (var index = 0; index < copies.Length; index++)
{
var copy = copies[index];
var mip = (int)copy.ImageSubresource.MipLevel;
var width = Math.Max(Backing.Extent.Width >> mip, 1u);
var height = Math.Max(Backing.Extent.Height >> mip, 1u);
var depth = Backing.ImageType == ImageType.Type3D
? Math.Max(Backing.Extent.Depth >> mip, 1u)
: 1u;
if (copy.ImageOffset.X < 0 || copy.ImageOffset.Y < 0 || copy.ImageOffset.Z < 0 ||
(uint)copy.ImageOffset.X >= width || (uint)copy.ImageOffset.Y >= height ||
(uint)copy.ImageOffset.Z >= depth)
{
throw SubmissionScheduler.Fatal(
$"The image upload offset is outside the backing image: mip={mip} " +
$"offset={copy.ImageOffset.X},{copy.ImageOffset.Y},{copy.ImageOffset.Z} extent={width}x{height}x{depth}.");
}
var availableWidth = width - (uint)copy.ImageOffset.X;
var availableHeight = height - (uint)copy.ImageOffset.Y;
var availableDepth = depth - (uint)copy.ImageOffset.Z;
var clamped = new Extent3D(
Math.Min(copy.ImageExtent.Width, availableWidth),
Math.Min(copy.ImageExtent.Height, availableHeight),
Math.Min(copy.ImageExtent.Depth, availableDepth));
if (clamped.Width == copy.ImageExtent.Width &&
clamped.Height == copy.ImageExtent.Height &&
clamped.Depth == copy.ImageExtent.Depth)
{
continue;
}
sanitized ??= copies.ToArray();
sanitized[index].ImageExtent = clamped;
}
return sanitized;
}
public void UploadFromBuffer(ReadOnlySpan<BufferImageCopy> copies, VkBuffer buffer, ulong offset, ulong size)
{
var command = BeginTransfer(copies, buffer, size);
var sanitized = SanitizeUploadCopies(copies);
var uploadCopies = sanitized is null ? copies : sanitized.AsSpan();
var bufferBarrier = BufferBarrier(buffer, offset, size, AccessFlags.MemoryWriteBit, AccessFlags.TransferReadBit);
var (imageBarriers, sourceStages) = GetBarriers(ImageLayout.TransferDstOptimal, AccessFlags.TransferWriteBit, PipelineStageFlags.TransferBit, null);
RecordBarriers(command, sourceStages | PipelineStageFlags.AllCommandsBit, PipelineStageFlags.TransferBit, &bufferBarrier, imageBarriers);
fixed (BufferImageCopy* regions = copies)
fixed (BufferImageCopy* regions = uploadCopies)
{
_device.Vk.CmdCopyBufferToImage(command, buffer, Backing.Handle, ImageLayout.TransferDstOptimal, (uint)copies.Length, regions);
_device.Vk.CmdCopyBufferToImage(command, buffer, Backing.Handle, ImageLayout.TransferDstOptimal, (uint)uploadCopies.Length, regions);
}
bufferBarrier = BufferBarrier(buffer, offset, size, AccessFlags.TransferReadBit, MemoryAccess);
@ -331,6 +375,52 @@ public sealed unsafe partial class CachedImage
}
}
public void CopyDepthStencilFrom(CachedImage source, in SubresourceRange range, in Extent3D extent, ImageAspectFlags aspects)
{
var requested = aspects & (ImageAspectFlags.DepthBit | ImageAspectFlags.StencilBit);
var available = ViewFormatRules.FullAspects(Backing.Format);
if (requested == 0 || (requested & ~available) != 0 || source.Backing.Format != Backing.Format ||
source.Backing.ImageType != ImageType.Type2D || Backing.ImageType != ImageType.Type2D ||
source.Backing.Samples != Backing.Samples || range.LevelCount != 1 ||
range.BaseLevel >= source.Backing.MipLevels || range.BaseLevel >= Backing.MipLevels ||
range.BaseLayer >= source.Backing.Layers || range.BaseLayer >= Backing.Layers ||
range.LayerCount == 0 || range.LayerCount > source.Backing.Layers - range.BaseLayer ||
range.LayerCount > Backing.Layers - range.BaseLayer)
{
throw SubmissionScheduler.Fatal(
$"The depth/stencil copy is invalid: aspects={(uint)aspects:X} format={(int)Backing.Format} sourceFormat={(int)source.Backing.Format} " +
$"range={range.BaseLevel}+{range.LevelCount}/{range.BaseLayer}+{range.LayerCount} samples={source.Backing.Samples}->{Backing.Samples}.");
}
_scheduler.EndRendering();
var command = new CommandBuffer(_scheduler.Current.Handle);
source.Transition(ImageLayout.TransferSrcOptimal, AccessFlags.TransferReadBit, range, command);
Transition(ImageLayout.TransferDstOptimal, AccessFlags.TransferWriteBit, range, command);
ImageCopy* regions = stackalloc ImageCopy[2];
uint count = 0;
if ((requested & ImageAspectFlags.DepthBit) != 0)
{
var layers = new ImageSubresourceLayers(ImageAspectFlags.DepthBit, range.BaseLevel, range.BaseLayer, range.LayerCount);
regions[count++] = new ImageCopy { SrcSubresource = layers, DstSubresource = layers, Extent = extent };
}
if ((requested & ImageAspectFlags.StencilBit) != 0)
{
var layers = new ImageSubresourceLayers(ImageAspectFlags.StencilBit, range.BaseLevel, range.BaseLayer, range.LayerCount);
regions[count++] = new ImageCopy { SrcSubresource = layers, DstSubresource = layers, Extent = extent };
}
_device.Vk.CmdCopyImage(
command,
source.Backing.Handle,
ImageLayout.TransferSrcOptimal,
Backing.Handle,
ImageLayout.TransferDstOptimal,
count,
regions);
}
public static uint CalculateRowsPerCopy(ulong rowSize, uint rows, ulong capacity)
{
if (rowSize == 0 || rows == 0 || rowSize > capacity)

View File

@ -228,6 +228,15 @@ public sealed unsafe partial class CachedImage : IDisposable
{
flags |= ImageCreateFlags.Create2DArrayCompatibleBit;
}
else if (description.Type == GuestImageType.Color2D &&
description.Samples == 1 &&
description.Extent.Depth == 1 &&
description.Extent.Width == description.Extent.Height &&
description.Resources.Layers >= 6 &&
description.Resources.Layers % 6 == 0)
{
flags |= ImageCreateFlags.CreateCubeCompatibleBit;
}
return flags;
}

View File

@ -254,6 +254,60 @@ public sealed partial class GuestImageCache
}
}
private static bool GrowImageToLayers(ref ImageDescription info, uint layers)
{
var current = info.Resources.Layers;
if (info.Resources.Levels != 1 || current == 0 || layers <= current ||
info.MipLayout[0].Offset != 0 || info.MipLayout[0].Size != info.Data.Size)
{
return false;
}
static bool TryStretch(ref GuestSpan range, uint currentLayers, uint newLayers)
{
if (range.Size == 0)
{
return true;
}
if (range.Size % currentLayers != 0)
{
return false;
}
range = new GuestSpan(range.Address, range.Size / currentLayers * newLayers);
return true;
}
var grown = info;
if (!TryStretch(ref grown.Data, current, layers) ||
!TryStretch(ref grown.Stencil, current, layers) ||
!TryStretch(ref grown.Metadata.Range, current, layers))
{
return false;
}
grown.MipLayout[0].Size = grown.Data.Size;
grown.Resources = grown.Resources with { Layers = layers };
info = grown;
return true;
}
private static bool SameMipLayout(in ImageDescription left, in ImageDescription right)
{
for (var level = 0; level < ImageDescription.MaxLevels; level++)
{
var a = left.MipLayout[level];
var b = right.MipLayout[level];
if (a.Offset != b.Offset || a.Size != b.Size || a.Pitch != b.Pitch || a.Height != b.Height)
{
return false;
}
}
return true;
}
private ResourceSlotIdentifier ResolveDepthOverlap(in ImageDescription requested, ImageRole role, ResourceSlotIdentifier cachedImageIdentifier)
{
using var profileScope = RenderPhaseProfile.MeasureDetail(RenderPhaseProfile.Phase.ImageOverlap);
@ -322,7 +376,13 @@ public sealed partial class GuestImageCache
}
else
{
replacementInfo.Resources = SubresourceCount.Max(requested.Resources, cachedInfo.Resources);
var merged = SubresourceCount.Max(requested.Resources, cachedInfo.Resources);
if (merged.Layers > requested.Resources.Layers && !GrowImageToLayers(ref replacementInfo, merged.Layers))
{
merged = merged with { Layers = requested.Resources.Layers };
}
replacementInfo.Resources = merged;
}
replacementInfo.HtileClearMask = 0;
@ -413,7 +473,8 @@ public sealed partial class GuestImageCache
return new OverlapResolution(GrowImage(requested, cachedImageIdentifier));
}
if (requested.TileMode != cachedInfo.TileMode)
if (requested.TileMode != cachedInfo.TileMode ||
(requested.Resources == cachedInfo.Resources && !SameMipLayout(requested, cachedInfo)))
{
if (safeToDelete)
{

View File

@ -108,6 +108,15 @@ public sealed unsafe partial class GuestImageCache
plan.Layout = TextureTransferLayout.Compute(format, info.Extent.Width, info.Extent.Height, info.Resources.Levels, layers, info.TileMode, info.Data.Size, allowDepthTile, volume, owner);
plan.Regions = plan.Layout.BuildCopies();
if (info.IsDepth)
{
for (var index = 0; index < plan.Regions.Count; index++)
{
var region = plan.Regions[index];
region.ImageSubresource.AspectMask = ImageAspectFlags.DepthBit;
plan.Regions[index] = region;
}
}
plan.Tiled = plan.Layout.Surface.Description.TileMode != GuestTileMode.Linear;
if (plan.Tiled)
{

View File

@ -69,6 +69,10 @@ public static partial class ImageRequestBuilders
if (words.BaseAddress == 0 || mask == 0)
{
if (Rendering.RenderTrace.Enabled)
{
Rendering.RenderTrace.Write($"ColorAttachmentRejected reason={(words.BaseAddress == 0 ? "zero-address" : "masked-output")} address=0x{words.BaseAddress:X16} mask=0x{mask:X}");
}
return null;
}

View File

@ -61,16 +61,33 @@ public static partial class ImageRequestBuilders
private static bool HtileStencilCompatible(bool hasStencil, bool hasHtile, bool htileStencilDisabled) => !hasStencil || !hasHtile || htileStencilDisabled;
// Builds the request for the bound depth target. Null when no depth or stencil state is active.
public static DepthTargetResolution? DepthTarget(in DepthTargetWords depthWords, IImageFormatSupport device)
public static DepthTargetResolution? DepthTarget(in DepthTargetWords depthWords, IImageFormatSupport device) =>
DepthTargetCore(in depthWords, device, copyMode: false, writeBuffer: false);
// Builds the source or destination image used by DB_RENDER_OVERRIDE depth/stencil copies.
public static DepthTargetResolution? DepthTargetCopy(in DepthTargetWords depthWords, IImageFormatSupport device, bool writeBuffer) =>
DepthTargetCore(in depthWords, device, copyMode: true, writeBuffer);
private static DepthTargetResolution? DepthTargetCore(in DepthTargetWords depthWords, IImageFormatSupport device, bool copyMode, bool writeBuffer)
{
var hasStencil = depthWords.StencilFormat != GuestStencilFormat.Invalid;
var depthActive = depthWords.DepthTestEnabled || depthWords.DepthWriteEnabled || depthWords.DepthBoundsEnabled || depthWords.DepthClearEnabled || depthWords.CopyDepthToColor;
var stencilActive = hasStencil && (depthWords.StencilTestEnabled || depthWords.StencilClearEnabled || depthWords.CopyStencilToColor);
if (!depthActive && !stencilActive)
if (!copyMode)
{
return null;
var depthActive = depthWords.DepthTestEnabled || depthWords.DepthWriteEnabled || depthWords.DepthBoundsEnabled || depthWords.DepthClearEnabled || depthWords.CopyDepthToColor;
var stencilActive = hasStencil && (depthWords.StencilTestEnabled || depthWords.StencilClearEnabled || depthWords.CopyStencilToColor);
if (!depthActive && !stencilActive)
{
if (Rendering.RenderTrace.Enabled)
{
Rendering.RenderTrace.Write("DepthAttachmentRejected reason=depth-and-stencil-inactive");
}
return null;
}
}
var depthAddress = copyMode && writeBuffer ? depthWords.ZWriteBase : depthWords.ZReadBase;
var stencilAddress = copyMode && writeBuffer ? depthWords.StencilWriteBase : depthWords.StencilReadBase;
// The size register is independent state; a zero encoding alone must not produce an attachment.
var attachmentUnbound =
depthWords.DepthFormat == GuestDepthFormat.Invalid && !hasStencil && depthWords.SamplesLog2 == 0 && !depthWords.ZExpClear && !depthWords.ZPartiallyResident && depthWords.MaxMip == 0 &&
@ -79,6 +96,10 @@ public static partial class ImageRequestBuilders
depthWords.StencilWriteBase == 0 && depthWords.HtileBase == 0 && !depthWords.HtileAcceleration && depthWords.ShadingRateEncoding == 0 && depthWords.XMax == 0 && depthWords.YMax == 0;
if (attachmentUnbound)
{
if (Rendering.RenderTrace.Enabled)
{
Rendering.RenderTrace.Write("DepthAttachmentRejected reason=unbound-register-state");
}
return null;
}
@ -97,7 +118,7 @@ public static partial class ImageRequestBuilders
if (depthWords.CopyDepthToColor || depthWords.CopyStencilToColor || depthWords.CopyCentroid || depthWords.CopySample != 0 || depthWords.ZExpClear || depthWords.StencilExpClear ||
depthWords.ZPartiallyResident || depthWords.StencilPartiallyResident || depthWords.MaxMip != 0 || depthWords.ViewMipLevel != 0 || unsupportedShadingRate ||
depthWords.ZReadBase == 0 || (!depthWords.DepthWriteDisabled && depthWords.ZWriteBase != depthWords.ZReadBase) || (depthWords.ZReadBase & 0xFFFF) != 0 || depthWords.DepthCompare > 7)
depthAddress == 0 || (!copyMode && !depthWords.DepthWriteDisabled && depthWords.ZWriteBase != depthWords.ZReadBase) || (depthAddress & 0xFFFF) != 0 || depthWords.DepthCompare > 7)
{
throw SubmissionScheduler.Fatal(
$"The depth register state is not supported: zInfo=0x{depthWords.ZInfo:X8} stencilInfo=0x{depthWords.StencilInfo:X8} view=0x{depthWords.DepthView:X8} " +
@ -107,13 +128,13 @@ public static partial class ImageRequestBuilders
if (hasStencil)
{
if (depthWords.StencilFormat != GuestStencilFormat.Stencil8UInt || !HtileStencilCompatible(hasStencil, hasHtile, depthWords.HtileStencilDisabled) ||
depthWords.StencilReadBase == 0 || (!depthWords.StencilWriteDisabled && depthWords.StencilWriteBase != depthWords.StencilReadBase) || (depthWords.StencilReadBase & 0xFFFF) != 0)
stencilAddress == 0 || (!copyMode && !depthWords.StencilWriteDisabled && depthWords.StencilWriteBase != depthWords.StencilReadBase) || (stencilAddress & 0xFFFF) != 0)
{
throw SubmissionScheduler.Fatal(
$"The stencil attachment state is not supported: stencilInfo=0x{depthWords.StencilInfo:X8} htile={hasHtile} read=0x{depthWords.StencilReadBase:X16} write=0x{depthWords.StencilWriteBase:X16}.");
}
}
else if (depthWords.StencilReadBase != 0 || depthWords.StencilWriteBase != 0)
else if (!copyMode && (depthWords.StencilReadBase != 0 || depthWords.StencilWriteBase != 0))
{
throw SubmissionScheduler.Fatal($"Stencil state is set without an active stencil attachment: read=0x{depthWords.StencilReadBase:X16} write=0x{depthWords.StencilWriteBase:X16}.");
}
@ -169,20 +190,20 @@ public static partial class ImageRequestBuilders
var depthBackingSize = (ulong)depthSize.Size * view.ImageLayers;
var stencilBackingSize = (ulong)stencilSize.Size * view.ImageLayers;
var htileBackingSize = (ulong)htileSize.Size * view.ImageLayers;
if (!new GuestSpan(depthWords.ZReadBase, depthBackingSize).IsValid ||
(hasStencil && !new GuestSpan(depthWords.StencilReadBase, stencilBackingSize).IsValid) ||
if (!new GuestSpan(depthAddress, depthBackingSize).IsValid ||
(hasStencil && !new GuestSpan(stencilAddress, stencilBackingSize).IsValid) ||
(hasHtile && !new GuestSpan(depthWords.HtileBase, htileBackingSize).IsValid))
{
throw SubmissionScheduler.Fatal(
$"The layered depth backing range is invalid: depth=0x{depthWords.ZReadBase:X16}+0x{depthBackingSize:X} stencil=0x{depthWords.StencilReadBase:X16}+0x{stencilBackingSize:X} htile=0x{depthWords.HtileBase:X16}+0x{htileBackingSize:X}.");
$"The layered depth backing range is invalid: depth=0x{depthAddress:X16}+0x{depthBackingSize:X} stencil=0x{stencilAddress:X16}+0x{stencilBackingSize:X} htile=0x{depthWords.HtileBase:X16}+0x{htileBackingSize:X}.");
}
var stencilAddress = hasStencil ? depthWords.StencilReadBase : 0;
stencilAddress = hasStencil ? stencilAddress : 0;
stencilBackingSize = hasStencil ? stencilBackingSize : 0;
var htileAddress = hasHtile ? depthWords.HtileBase : 0;
htileBackingSize = hasHtile ? htileBackingSize : 0;
var description = ImageDescription.Create();
description.Data = new GuestSpan(depthWords.ZReadBase, depthBackingSize);
description.Data = new GuestSpan(depthAddress, depthBackingSize);
description.Stencil = new GuestSpan(stencilAddress, stencilBackingSize);
description.PixelFormat = format;
description.GuestFormat = rule.GuestFormat;
@ -203,7 +224,7 @@ public static partial class ImageRequestBuilders
var request = new ImageRequest(description, viewDescription, ImageRole.DepthTarget);
return new DepthTargetResolution(
request, format, width, height, samples, hasStencil, hasHtile,
depthWords.ZReadBase, depthBackingSize, stencilAddress, stencilBackingSize, htileAddress, htileBackingSize,
depthAddress, depthBackingSize, stencilAddress, stencilBackingSize, htileAddress, htileBackingSize,
depthWords.DepthClearEnabled, hasStencil && depthWords.StencilClearEnabled && !depthWords.StencilWriteDisabled, depthWords.DepthWriteDisabled, depthWords.StencilWriteDisabled);
}

View File

@ -14,8 +14,10 @@ public readonly record struct TextureRequestResolution(ImageRequest Request, boo
public static partial class ImageRequestBuilders
{
// A null descriptor binds a one-texel image of the numeric class the shader expects.
public static ImageRequest NullTexture(TextureNumericClass numericClass, bool storage)
public static ImageRequest NullTexture(in ShaderImageShape shape)
{
var numericClass = shape.NumericClass;
var storage = shape.Storage;
var (format, guestFormat) = numericClass switch
{
TextureNumericClass.Float => (Format.R32Sfloat, GuestPixelFormat.Bits32Float),
@ -26,16 +28,22 @@ public static partial class ImageRequestBuilders
var description = ImageDescription.Create();
description.PixelFormat = format;
description.GuestFormat = guestFormat;
description.Type = GuestImageType.Color2D;
description.Type = shape.Volume
? GuestImageType.Color3D
: shape.OneDimensional ? GuestImageType.Color1D : GuestImageType.Color2D;
description.Extent = new Extent3D(1, 1, 1);
description.Resources = SubresourceCount.Single;
description.BytesPerBlock = 4;
description.Samples = 1;
description.Samples = shape.Multisampled ? 4u : 1u;
description.MipLayout[0] = new MipLevelLayout { Offset = 0, Size = 0, Pitch = 1, Height = 1 };
var view = ImageViewDescription.Default with
{
Format = format,
Type = ImageViewType.Type2D,
Type = shape.Volume
? ImageViewType.Type3D
: shape.OneDimensional
? shape.Arrayed ? ImageViewType.Type1DArray : ImageViewType.Type1D
: shape.Arrayed ? ImageViewType.Type2DArray : ImageViewType.Type2D,
Aspect = ImageAspectFlags.ColorBit,
Usage = storage ? ImageUsageFlags.StorageBit : ImageUsageFlags.SampledBit,
};
@ -104,8 +112,10 @@ public static partial class ImageRequestBuilders
throw SubmissionScheduler.Fatal($"The texture base layer is outside the image: baseLayer={baseLayer} layers={imageLayers} address=0x{descriptor.BaseAddress:X16}.");
}
var type = shape.Arrayed ? ImageViewType.Type2DArray : ImageViewType.Type2D;
var layerCount = shape.Arrayed ? imageLayers - baseLayer : 1;
var type = shape.OneDimensional
? shape.Arrayed ? ImageViewType.Type1DArray : ImageViewType.Type1D
: shape.Arrayed ? ImageViewType.Type2DArray : ImageViewType.Type2D;
return new ImageViewDescription(format, type, ImageAspectFlags.ColorBit, descriptor.BaseLevel, viewLevels, baseLayer, layerCount, mapping, usage);
}
@ -115,8 +125,12 @@ public static partial class ImageRequestBuilders
// Cube maps become two-dimensional arrays; the multisample kinds keep their own type.
private static GuestImageType TextureType(GuestImageType type) => type == GuestImageType.Cube ? GuestImageType.Color2DArray : type;
// Every non-volume texture is a two-dimensional image; the translator compiles 1D textures as 2D.
private static GuestImageType TextureBaseType(GuestImageType type) => type == GuestImageType.Color3D ? GuestImageType.Color3D : GuestImageType.Color2D;
private static GuestImageType TextureBaseType(GuestImageType type) => type switch
{
GuestImageType.Color1DArray => GuestImageType.Color1D,
GuestImageType.Color2DArray or GuestImageType.Color2DMsaa or GuestImageType.Color2DMsaaArray => GuestImageType.Color2D,
_ => type,
};
private static bool IsMultisampledTexture(GuestImageType type) => type is GuestImageType.Color2DMsaa or GuestImageType.Color2DMsaaArray;
@ -126,11 +140,10 @@ public static partial class ImageRequestBuilders
Span<uint> padded = stackalloc uint[8];
words[..Math.Min(words.Length, 8)].CopyTo(padded);
var descriptor = new TextureDescriptorWords(padded);
var compactDescriptor = words.Length < 8;
var storage = shape.Storage;
if (descriptor.BaseAddress == 0)
{
var nullRequest = NullTexture(shape.NumericClass, storage);
var nullRequest = NullTexture(shape);
return new TextureRequestResolution(nullRequest, false, nullRequest.View.Format, 0);
}
@ -141,7 +154,7 @@ public static partial class ImageRequestBuilders
var lastLevel = descriptor.LastLevel;
var type = TextureType(descriptor.Type);
var multisampled = IsMultisampledTexture(type);
var maxMip = compactDescriptor ? lastLevel : descriptor.MaxMip;
var maxMip = shape.R128 ? lastLevel : descriptor.MaxMip;
var levels = multisampled ? 1 : maxMip + 1;
var dynamicStorage = storage && shape.DynamicMip;
var viewLastLevel = !multisampled && !dynamicStorage ? Math.Min(lastLevel, maxMip) : lastLevel;
@ -168,6 +181,13 @@ public static partial class ImageRequestBuilders
var volume = type == GuestImageType.Color3D;
var layered = type is GuestImageType.Color1DArray or GuestImageType.Color2DArray or GuestImageType.Color2DMsaaArray;
var imageLayers = layered ? depth : 1;
if (shape.Cube &&
(volume || multisampled || width != height || descriptor.BaseArray > descriptor.Depth ||
(descriptor.Depth - descriptor.BaseArray + 1) % 6 != 0))
{
throw SubmissionScheduler.Fatal(
$"The cubemap view is invalid: address=0x{address:X16} extent={width}x{height} layers={imageLayers} baseArray={descriptor.BaseArray} samples={samples}.");
}
uint pitch;
TileSizeAndAlignment size;
if (multisampled)
@ -195,6 +215,11 @@ public static partial class ImageRequestBuilders
}
var pixelFormat = surfaceFormat.HostFormat;
// Comparison sampling needs a depth image, even when no depth target created it.
if (shape.DepthCompare && DepthFormatRule.FindByGuestFormat(format) is { } depthFormat)
{
pixelFormat = depthFormat.DepthAttachmentFormat;
}
var storageViewFormat = storage && format == GuestPixelFormat.Bits32SInt ? Format.R32Uint : ViewFormatRules.SrgbStorageFormat(pixelFormat);
var viewFormat = storage && storageViewFormat != Format.Undefined ? storageViewFormat : pixelFormat;
var blockBytes = GuestPixelFormats.BlockCompressedBytes(format);

View File

@ -106,4 +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 Storage, bool DynamicMip, TextureNumericClass NumericClass);
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 ShaderImageShape(bool volume, bool arrayed, bool storage, bool dynamicMip, TextureNumericClass numericClass)
: this(volume, arrayed, false, storage, dynamicMip, numericClass, false, false, false)
{
}
}

View File

@ -1147,6 +1147,14 @@ internal sealed partial class MetalCommandStreamHost : IRenderHost, IShaderPipel
_ = _snapshots.TrySubmitGuestImageBlit(from, to);
}
bool IRenderHost.TryDispatchIndirect(ulong argumentsAddress) => false;
void IRenderHost.CopyDepthStencilImage(ResourceSlotIdentifier source, ResourceSlotIdentifier destination, in SubresourceRange range, in Extent3D extent, ImageAspectFlags aspects)
{
_ = (source, destination, range, extent, aspects);
throw Fatal("Depth/stencil image copies are not implemented by the Metal command stream host.");
}
bool IRenderHost.IsMetadata(ulong address)
{
_ = address;

View File

@ -333,22 +333,38 @@ internal sealed class ShaderProgramCache
private static ShaderVertexInput[] BuildVertexInputs(EmbeddedVertexFetchPlan fetch, VertexInputInfo info, ShaderSource source)
{
var inputs = new List<ShaderVertexInput>();
var aliases = new Dictionary<int, List<uint>>();
var inputByLocation = new Dictionary<int, int>();
foreach (var load in fetch.Loads)
{
if (aliases.TryGetValue(load.AttributeId, out var aliasPcs))
var location = -1;
if ((uint)load.AttributeId < (uint)info.Attributes.Length &&
info.Attributes[load.AttributeId].AttributeId == load.AttributeId)
{
aliasPcs.Add(load.Pc);
continue;
location = load.AttributeId;
}
var location = -1;
for (var index = 0; index < info.Attributes.Length; index++)
if (location < 0)
{
if (info.Attributes[index].AttributeId == load.AttributeId)
for (var index = 0; index < info.Attributes.Length; index++)
{
location = index;
break;
if (info.Attributes[index].AttributeId == load.AttributeId &&
info.Attributes[index].RegisterCount >= load.Components)
{
location = index;
break;
}
}
}
if (location < 0)
{
for (var index = 0; index < info.Attributes.Length; index++)
{
if (info.Attributes[index].AttributeId == load.AttributeId)
{
location = index;
break;
}
}
}
@ -365,9 +381,55 @@ internal sealed class ShaderProgramCache
numberFormat = decodedNumberFormat;
}
aliasPcs = [];
aliases.Add(load.AttributeId, aliasPcs);
inputs.Add(new ShaderVertexInput(load.Pc, (uint)location, load.Components, numberFormat, attribute.FetchIndex != 0, aliasPcs));
var requiredComponents = 0u;
var destinationSelect = attribute.Descriptor.DestinationSelectXYZW;
for (uint component = 0; component < load.Components; component++)
{
var selector = (destinationSelect >> (int)(component * 3)) & 0x7u;
if (selector is >= 4u and <= 7u)
{
requiredComponents = Math.Max(requiredComponents, selector - 3u);
continue;
}
if (selector is 0u or 1u)
{
continue;
}
throw SubmissionScheduler.Fatal(
$"The vertex program uses an unsupported attribute destination selector: hash=0x{source.Hash:X16} shader=0x{source.Address:X16} attribute={load.AttributeId} pc=0x{load.Pc:X} component={component} selector={selector}.");
}
if (requiredComponents == 0)
{
throw SubmissionScheduler.Fatal(
$"The vertex program fetches an attribute without a memory component: hash=0x{source.Hash:X16} shader=0x{source.Address:X16} attribute={load.AttributeId} pc=0x{load.Pc:X}.");
}
if (inputByLocation.TryGetValue(location, out var existingIndex))
{
var existing = inputs[existingIndex];
((List<uint>)existing.AliasPcs).Add(load.Pc);
inputs[existingIndex] = existing with
{
FetchComponentCount = Math.Max(existing.FetchComponentCount, load.Components),
ComponentCount = Math.Max(existing.ComponentCount, requiredComponents),
};
continue;
}
var aliasPcs = new List<uint>();
inputByLocation.Add(location, inputs.Count);
inputs.Add(new ShaderVertexInput(
load.Pc,
(uint)location,
load.Components,
requiredComponents,
numberFormat,
destinationSelect,
attribute.FetchIndex != 0,
aliasPcs));
}
return inputs.ToArray();
@ -470,6 +532,15 @@ internal sealed class ShaderProgramCache
EnableGraphicsSubgroupOperations = enableGraphicsSubgroups,
RequiredVertexOutputCount = options.RequiredVertexOutputCount,
VertexInputs = entry.VertexInputs,
PositionExportControl = info.PositionExportControl,
ClipSpace = new ShaderClipSpaceTransform(
info.ClipSpace.Enabled,
info.ClipSpace.ScaleX,
info.ClipSpace.ScaleY,
info.ClipSpace.OffsetX,
info.ClipSpace.OffsetY,
info.ClipSpace.HalfExtentX,
info.ClipSpace.HalfExtentY),
};
}
@ -486,6 +557,7 @@ internal sealed class ShaderProgramCache
EnableGraphicsSubgroupOperations = enableGraphicsSubgroups,
PixelOutputs = options.PixelOutputs,
PixelInputEnable = options.PixelInputEnable,
PixelCustomInterpolationMask = info.CustomInterpolationMask,
PixelInputAddress = options.PixelInputAddress,
PixelInputCntl = interpolators,
};

View File

@ -16,7 +16,6 @@ public sealed class VertexTableMetadata
public int VertexAttributeRegister { get; init; } = NoRegister;
public uint[] InputSemantics { get; init; } = [];
}
// One packed input semantic word of a shader header.
public readonly record struct ShaderInputSemantic(uint Word)
{
@ -77,7 +76,11 @@ public static class VertexInputResolver
private const uint DirectResourceCount = DirectResourceVertexAttributeTable + 1;
private const ulong DirectResourceOffsetField = 0x00;
private const ulong DirectResourceCountField = 0x2C;
private const uint IdentityDestinationSelect = 4 | (5 << 3) | (6 << 6) | (7 << 9);
private static uint DestinationSelectForChannels(uint channels) =>
4u |
((channels > 1 ? 5u : 0u) << 3) |
((channels > 2 ? 6u : 0u) << 6) |
((channels > 3 ? 7u : 1u) << 9);
private sealed class ResolvedBuffer
{
@ -259,10 +262,13 @@ public static class VertexInputResolver
var descriptor = BufferDescriptorWords.From(descriptorWords);
if (format != 0)
{
var bufferFormat = VertexAttributeFormat.ToBufferFormat(format);
var bufferFormat = format >> 2;
var channels = (format & 0x3u) + 1u;
descriptor = descriptor with
{
Word3 = (descriptor.Word3 & ~((0x7Fu << 12) | 0xFFFu)) | ((bufferFormat & 0x7Fu) << 12) | IdentityDestinationSelect,
Word3 = (descriptor.Word3 & ~((0x7Fu << 12) | 0xFFFu)) |
((bufferFormat & 0x7Fu) << 12) |
DestinationSelectForChannels(channels),
};
}
@ -365,30 +371,3 @@ public static class VertexInputResolver
return result;
}
}
// The vertex attribute formats of the guest tables and the buffer formats they select.
public static class VertexAttributeFormat
{
private static readonly Dictionary<uint, uint> _bufferFormats = new()
{
[0] = 0,
[4] = 1, [8] = 2, [12] = 3, [16] = 4, [20] = 5, [24] = 6,
[28] = 7, [32] = 8, [36] = 9, [40] = 10, [44] = 11, [48] = 12, [52] = 13,
[57] = 14, [61] = 15, [65] = 16, [69] = 17, [73] = 18, [77] = 19,
[80] = 20, [84] = 21, [88] = 22,
[93] = 23, [97] = 24, [101] = 25, [105] = 26, [109] = 27, [113] = 28, [117] = 29,
[122] = 30, [126] = 31, [130] = 32, [134] = 33, [138] = 34, [142] = 35, [146] = 36,
[150] = 37, [154] = 38, [158] = 39, [162] = 40, [166] = 41, [170] = 42, [174] = 43,
[179] = 44, [183] = 45, [187] = 46, [191] = 47, [195] = 48, [199] = 49,
[203] = 50, [207] = 51, [211] = 52, [215] = 53, [219] = 54, [223] = 55,
[227] = 56, [231] = 57, [235] = 58, [239] = 59, [243] = 60, [247] = 61,
[249] = 62, [253] = 63, [257] = 64,
[263] = 65, [267] = 66, [271] = 67, [275] = 68, [279] = 69, [283] = 70, [287] = 71,
[290] = 72, [294] = 73, [298] = 74,
[303] = 75, [307] = 76, [311] = 77,
};
// An unknown attribute format is kept as the buffer format value it names.
public static uint ToBufferFormat(uint attributeFormat) =>
_bufferFormats.TryGetValue(attributeFormat, out var bufferFormat) ? bufferFormat : attributeFormat;
}

View File

@ -145,6 +145,10 @@ public interface IRenderHost
void Dispatch(uint groupsX, uint groupsY, uint groupsZ);
// Executes a dispatch from three uint32 group counts in guest memory when the backend can
// consume the guest buffer directly. Backends without native indirect dispatch return false.
bool TryDispatchIndirect(ulong argumentsAddress);
// Orders the buffer writes of the given shader stages before every later access.
void ShaderWriteBarrier(PipelineStageFlags sourceStages);
@ -162,6 +166,8 @@ public interface IRenderHost
void ResolveImage(ResourceSlotIdentifier source, uint sourceMip, uint sourceLayer, ResourceSlotIdentifier destination, uint destinationMip, uint destinationLayer);
void CopyDepthStencilImage(ResourceSlotIdentifier source, ResourceSlotIdentifier destination, in SubresourceRange range, in Extent3D extent, ImageAspectFlags aspects);
bool IsMetadata(ulong address);
bool ClearMetadata(ulong address);

View File

@ -31,12 +31,14 @@ public sealed partial class RenderExecutor
if (programs.SolidClear is { } clear && state.ColorCount != 0)
{
TraceDrawDisposition(banks, in draw, "color-clear");
_host.ClearColorTargets(BoundColors(ref state), clear);
return false;
}
if (IsMetadataClearQuad(banks, in draw, ref state))
{
TraceDrawDisposition(banks, in draw, "metadata-clear-quad");
return false;
}
@ -44,12 +46,46 @@ public sealed partial class RenderExecutor
programs.PixelInput.Stage.Program is { Images.Length: > 0 } pixelProgram && !WritesStorageImage(pixelProgram) &&
_host.TryRetainTargetlessDraw(banks, programs, in arguments))
{
TraceTargetlessAttachments(banks, arguments.SubmitId);
TraceDrawDisposition(banks, in draw, "retained-targetless-draw");
return false;
}
return true;
}
private static void TraceDrawDisposition(RegisterBanks banks, in DrawCall draw, string reason)
{
if (!RenderTrace.Enabled)
{
return;
}
ref readonly var viewport = ref banks.Context.ScreenViewport.Viewports[0];
RenderTrace.Write(
$"DrawDisposition name={draw.Name} reason={reason} count={draw.Count} " +
$"export=0x{banks.Shader.Vertex.ExportAddress:X16} pixel=0x{banks.Shader.Pixel.Address:X16} " +
$"colorMode={banks.Context.ColorControl.Mode} targetMask=0x{banks.Context.RenderTargetMask:X8} " +
$"zScale={viewport.ZScale} zOffset={viewport.ZOffset}");
}
private static void TraceTargetlessAttachments(RegisterBanks banks, ulong submitId)
{
if (!RenderTrace.Enabled)
{
return;
}
var context = banks.Context;
RenderTrace.Write($"TargetlessDepth submit={submitId} registers={context.DepthTarget}");
for (var slot = 0; slot < ContextRegisters.ColorTargetCount; slot++)
{
RenderTrace.Write(
$"TargetlessColor submit={submitId} slot={slot} mask=0x{context.RenderTargetMaskForSlot((uint)slot):X} " +
$"registers={context.ColorTargets[slot]}");
}
}
private static bool WritesStorageImage(ShaderProgramInfo program)
{
foreach (var image in program.Images)

View File

@ -21,7 +21,7 @@ public sealed partial class RenderExecutor
private const uint ImageClearStride = 16;
private const uint ImageClearUserDataCount = 8;
public void Dispatch(ulong submitId, RegisterBanks banks, uint groupsX, uint groupsY, uint groupsZ, uint dispatchInitiator)
public void Dispatch(ulong submitId, RegisterBanks banks, uint groupsX, uint groupsY, uint groupsZ, uint dispatchInitiator, ulong indirectArgumentsAddress = 0)
{
if (!_host.IsRecording)
{
@ -76,13 +76,13 @@ public sealed partial class RenderExecutor
$"buffers={program.Buffers.Length} images={program.Images.Length} samplers={program.SamplerCount}");
}
if (TryConsumeMetadataClear(input))
if (indirectArgumentsAddress == 0 && TryConsumeMetadataClear(input))
{
_host.ResetBindings();
return;
}
if (TryConsumeImageClear(input, groupsX, groupsY, groupsZ, dispatchInitiator))
if (indirectArgumentsAddress == 0 && TryConsumeImageClear(input, groupsX, groupsY, groupsZ, dispatchInitiator))
{
_host.ResetBindings();
return;
@ -90,6 +90,9 @@ public sealed partial class RenderExecutor
if (useThreadDimensions)
{
// The indirect buffer carries thread counts in this mode, while Vulkan indirect
// dispatch consumes workgroup counts. Use the CPU-resolved counts after conversion.
indirectArgumentsAddress = 0;
var threadsX = groupsX;
var threadsY = groupsY;
var threadsZ = groupsZ;
@ -104,7 +107,7 @@ public sealed partial class RenderExecutor
}
}
if (groupsX == 0 || groupsY == 0 || groupsZ == 0)
if (indirectArgumentsAddress == 0 && (groupsX == 0 || groupsY == 0 || groupsZ == 0))
{
if (RenderTrace.Enabled && RenderTrace.ZeroDispatch())
{
@ -140,7 +143,10 @@ public sealed partial class RenderExecutor
}
_host.BindPipeline(PipelineBindPoint.Compute, in pipeline);
_host.Dispatch(groupsX, groupsY, groupsZ);
if (indirectArgumentsAddress == 0 || !_host.TryDispatchIndirect(indirectArgumentsAddress))
{
_host.Dispatch(groupsX, groupsY, groupsZ);
}
_host.ShaderAccessBarrier();
}

View File

@ -117,7 +117,7 @@ public sealed partial class RenderExecutor
viewportY,
viewportWidth,
viewportHeight,
viewport.ZOffset,
viewport.ZOffset - (context.Clip.DirectXClipSpace ? 0f : viewport.ZScale),
viewport.ZScale + viewport.ZOffset,
scissor,
lineWidth,

View File

@ -20,6 +20,7 @@ public sealed partial class RenderExecutor
var context = banks.Context;
if (TryResolveMultisampleColor(context))
{
TraceDrawDisposition(banks, in draw, "multisample-color-resolve");
return false;
}
@ -52,6 +53,7 @@ public sealed partial class RenderExecutor
state.PixelActive = HasActivePixelShader(banks);
if (state.ColorCount == 0 && !state.Depth.HasTarget && !state.PixelActive)
{
TraceDrawDisposition(banks, in draw, "no-framebuffer");
if (RenderTrace.Enabled && RenderTrace.FramebufferSkip())
{
RenderTrace.Write(
@ -97,6 +99,63 @@ public sealed partial class RenderExecutor
return true;
}
// DB_RENDER_OVERRIDE can turn a draw packet into a depth/stencil read-to-write copy.
private bool TryDepthStencilCopy(ContextRegisters context)
{
if (context.ColorControl.Mode != 0)
{
return false;
}
ref readonly var words = ref context.DepthTarget;
var renderOverride = context.DepthRenderOverride;
var depthCopy =
renderOverride.ForceZDirty &&
renderOverride.ForceZValid &&
words.DepthFormat != GuestDepthFormat.Invalid &&
words.ZReadBase != 0 &&
words.ZWriteBase != 0 &&
words.ZReadBase != words.ZWriteBase;
var stencilCopy =
renderOverride.ForceStencilDirty &&
renderOverride.ForceStencilValid &&
words.StencilFormat != GuestStencilFormat.Invalid &&
words.StencilReadBase != 0 &&
words.StencilWriteBase != 0 &&
words.StencilReadBase != words.StencilWriteBase;
if (!depthCopy && !stencilCopy)
{
return false;
}
var read = ImageRequestBuilders.DepthTargetCopy(in words, _host.FormatSupport, writeBuffer: false)
?? throw _host.Fatal("A depth/stencil copy has no readable depth target.");
var write = ImageRequestBuilders.DepthTargetCopy(in words, _host.FormatSupport, writeBuffer: true)
?? throw _host.Fatal("A depth/stencil copy has no writable depth target.");
var readRequest = read.Request;
var writeRequest = write.Request;
var source = _host.FindImage(ref readRequest, exactFormat: true);
var destination = _host.FindImage(ref writeRequest, exactFormat: true);
_host.BindRenderTarget(source);
_host.BindRenderTarget(destination);
if (source == destination || read.Format != write.Format)
{
throw _host.Fatal(
$"A depth/stencil copy must use distinct images with the same format: source={source.Index} destination={destination.Index} readFormat={(int)read.Format} writeFormat={(int)write.Format}.");
}
var range = new SubresourceRange(
readRequest.View.BaseLevel,
readRequest.View.LevelCount,
readRequest.View.BaseLayer,
readRequest.View.LayerCount);
var extent = new Extent3D(write.Width, write.Height, 1);
var aspects = (depthCopy ? ImageAspectFlags.DepthBit : 0) | (stencilCopy ? ImageAspectFlags.StencilBit : 0);
_host.MarkGpuWritten(destination);
_host.CopyDepthStencilImage(source, destination, in range, in extent, aspects);
return true;
}
private ColorTargetState? ResolveMultisampleSlot(ContextRegisters context, uint slot)
{
if (ColorTargetResolver.Resolve(context, slot, DrawLayerOffset, ignoreTargetMask: true, out var resolvedSlot) is not { } resolution)

View File

@ -106,7 +106,7 @@ public sealed partial class RenderExecutor
return;
}
if (ConsumesColorMetadataOperation(banks.Context))
if (ConsumesColorMetadataOperation(banks.Context) || TryDepthStencilCopy(banks.Context))
{
_host.ResetBindings();
return;
@ -226,7 +226,7 @@ public sealed partial class RenderExecutor
return;
}
if (ConsumesColorMetadataOperation(banks.Context))
if (ConsumesColorMetadataOperation(banks.Context) || TryDepthStencilCopy(banks.Context))
{
_host.ResetBindings();
return;
@ -255,6 +255,7 @@ public sealed partial class RenderExecutor
if (!ResolveTopology(userConfig, autoDraw: true, out var topology))
{
TraceDrawDisposition(banks, in draw, "no-primitive-topology");
_host.ResetBindings();
return;
}
@ -588,11 +589,17 @@ public sealed partial class RenderExecutor
var colorWidth = state.ColorCount != 0 ? state.Colors[0].Resolution.Extent.Width : 0;
var colorHeight = state.ColorCount != 0 ? state.Colors[0].Resolution.Extent.Height : 0;
var depth = state.Depth.HasTarget ? state.Depth.Target : default;
ref readonly var viewport = ref banks.Context.ScreenViewport.Viewports[0];
var clip = banks.Context.Clip;
RenderTrace.Write(
$"DrawState seq={RenderTrace.NextSequence()} submit={submitId} name={draw.Name} export=0x{shader.Vertex.ExportAddress:X16} " +
$"pixel=0x{shader.Pixel.Address:X16} pixelActive={state.PixelActive} count={draw.Count} instances={draw.InstanceCount} " +
$"colors={state.ColorCount} color=0x{colorAddress:X10}:{colorWidth}x{colorHeight} targetMask=0x{banks.Context.RenderTargetMask:X8} " +
$"depth=0x{depth.Target.DepthAddress:X10}:{depth.Target.Width}x{depth.Target.Height}:{(int)depth.Target.Format} " +
$"depthState={(depth.State.DepthTestEnabled ? 1 : 0)}/{(depth.State.DepthWriteEnabled ? 1 : 0)}/{(int)depth.State.DepthCompare}");
$"depthState={(depth.State.DepthTestEnabled ? 1 : 0)}/{(depth.State.DepthWriteEnabled ? 1 : 0)}/{(int)depth.State.DepthCompare} " +
$"viewportTransform=0x{banks.Context.ScreenViewport.TransformControl:X8} directXClip={clip.DirectXClipSpace} " +
$"clipDisabled={clip.ClipDisable} nearClipDisabled={clip.NearZClipDisable} farClipDisabled={clip.FarZClipDisable} " +
$"zScale={viewport.ZScale:R} zOffset={viewport.ZOffset:R} zBounds={viewport.MinDepth:R}/{viewport.MaxDepth:R} " +
$"hostDepthRange={viewport.ZOffset - (clip.DirectXClipSpace ? 0f : viewport.ZScale):R}/{viewport.ZScale + viewport.ZOffset:R}");
}
}

View File

@ -3,6 +3,7 @@
using SharpEmu.HLE;
using SharpEmu.Libs.Ampr;
using System.Buffers.Binary;
using System.Collections.Concurrent;
using System.Threading;
@ -16,7 +17,12 @@ public static class KernelAprCompatExports
private static readonly bool _traceApr =
string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_AMPR"), "1", StringComparison.Ordinal);
private readonly record struct AprSubmission(ulong CommandBuffer, ulong Priority, ulong ResultAddress);
private readonly record struct AprSubmission(
ulong CommandBuffer,
ulong Priority,
ulong ResultAddress,
int ExecutionResult,
uint ErrorOffset);
[SysAbiExport(
Nid = "ASoW5WE-UPo",
@ -41,20 +47,24 @@ public static class KernelAprCompatExports
submissionId = unchecked((uint)Interlocked.Increment(ref _nextSubmissionId));
}
_submittedCommandBuffers[submissionId] = new AprSubmission(commandBuffer, priority, resultAddress);
var completionResult = AmprExports.CompleteCommandBuffer(ctx, commandBuffer);
var completionResult = AmprExports.CompleteCommandBuffer(
ctx,
commandBuffer,
out var executionResult,
out var errorOffset);
if (completionResult != (int)OrbisGen2Result.ORBIS_GEN2_OK)
{
return completionResult;
}
_submittedCommandBuffers[submissionId] =
new AprSubmission(commandBuffer, priority, resultAddress, executionResult, errorOffset);
if (outSubmissionId != 0 && !ctx.TryWriteUInt32(outSubmissionId, submissionId))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
if (resultAddress != 0 && !TryWriteAprResult(ctx, resultAddress))
if (resultAddress != 0 && !TryWriteAprResult(ctx, resultAddress, executionResult, errorOffset))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
@ -80,7 +90,12 @@ public static class KernelAprCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
}
// Completion output was written when the command was submitted.
if (submission.ResultAddress != 0 &&
!TryWriteAprResult(ctx, submission.ResultAddress, submission.ExecutionResult, submission.ErrorOffset))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
TraceApr(ctx, "wait", submissionId, submission.CommandBuffer, waitArg1, waitArg2);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
@ -99,13 +114,17 @@ public static class KernelAprCompatExports
}
var submissionId = unchecked((uint)Interlocked.Increment(ref _nextSubmissionId));
_submittedCommandBuffers[submissionId] = new AprSubmission(commandBuffer, ctx[CpuRegister.Rsi], ResultAddress: 0);
var completionResult = AmprExports.CompleteCommandBuffer(ctx, commandBuffer);
var completionResult = AmprExports.CompleteCommandBuffer(
ctx,
commandBuffer,
out var executionResult,
out var errorOffset);
if (completionResult != (int)OrbisGen2Result.ORBIS_GEN2_OK)
{
return completionResult;
}
_submittedCommandBuffers[submissionId] =
new AprSubmission(commandBuffer, ctx[CpuRegister.Rsi], 0, executionResult, errorOffset);
TraceApr(ctx, "submit", submissionId, commandBuffer, ctx[CpuRegister.Rsi], 0);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
@ -126,13 +145,17 @@ public static class KernelAprCompatExports
}
var submissionId = unchecked((uint)Interlocked.Increment(ref _nextSubmissionId));
_submittedCommandBuffers[submissionId] = new AprSubmission(commandBuffer, ctx[CpuRegister.Rsi], ResultAddress: 0);
var completionResult = AmprExports.CompleteCommandBuffer(ctx, commandBuffer);
var completionResult = AmprExports.CompleteCommandBuffer(
ctx,
commandBuffer,
out var executionResult,
out var errorOffset);
if (completionResult != (int)OrbisGen2Result.ORBIS_GEN2_OK)
{
return completionResult;
}
_submittedCommandBuffers[submissionId] =
new AprSubmission(commandBuffer, ctx[CpuRegister.Rsi], 0, executionResult, errorOffset);
if (!ctx.TryWriteUInt32(outSubmissionId, submissionId))
{
@ -156,10 +179,15 @@ public static class KernelAprCompatExports
return ctx.SetReturn(0);
}
private static bool TryWriteAprResult(CpuContext ctx, ulong resultAddress)
private static bool TryWriteAprResult(
CpuContext ctx,
ulong resultAddress,
int executionResult,
uint errorOffset)
{
Span<byte> result = stackalloc byte[sizeof(ulong)];
result.Clear();
BinaryPrimitives.WriteInt32LittleEndian(result, executionResult);
BinaryPrimitives.WriteUInt32LittleEndian(result[sizeof(int)..], errorOffset);
return ctx.Memory.TryWrite(resultAddress, result);
}

View File

@ -903,6 +903,17 @@ public static class KernelEventQueueCompatExports
return true;
}
internal static bool TriggerAmprEvent(ulong handle, ulong ident, ulong data)
{
return TriggerRegisteredEvent(
handle,
ident,
KernelEventFilterAmpr,
0,
data,
preserveRegisteredUserData: true);
}
public static bool RegisterEvent(
ulong handle,
ulong ident,
@ -1279,7 +1290,9 @@ public static class KernelEventQueueCompatExports
ulong handle,
ulong ident,
short filter,
ulong userData)
ulong userData,
ulong data = 0,
bool preserveRegisteredUserData = false)
{
EventQueueState state;
lock (_eventQueueGate)
@ -1305,8 +1318,8 @@ public static class KernelEventQueueCompatExports
registration.Filter,
registration.Flags,
0,
0,
userData));
data,
preserveRegisteredUserData ? registration.UserData : userData));
}
WakeEventQueue(

View File

@ -367,8 +367,9 @@ internal static class RenderPhaseProfile
}
// Cache calls on other threads must not enter the render-thread counters.
// Check the switch first to avoid thread-local storage allocation when profiling is disabled.
internal static Scope MeasureDetail(Phase phase) =>
_scopeDepth > 0 ? Measure(phase) : default;
Enabled && _scopeDepth > 0 ? Measure(phase) : default;
internal static bool DetailMeasurementsEnabled => Enabled && _scopeDepth > 0;

View File

@ -72,6 +72,7 @@ public static partial class VideoOutExports
// sceVideoOutGetEventId (mapped below), so the exact value is internal; only
// its distinctness from the flip ident matters for GetEventId/GetEventData.
private const ulong SceVideoOutInternalEventVblank = 0x40;
private const ulong SceVideoOutInternalEventOutputMode = 0x8;
private const short OrbisKernelEventFilterVideoOut = -13;
private static readonly object _stateGate = new();
@ -239,6 +240,7 @@ public static partial class VideoOutExports
public uint OutputWidth { get; set; } = 1920;
public uint OutputHeight { get; set; } = 1080;
public uint RefreshRate { get; set; } = 60;
public ulong OutputMode { get; set; } = SceVideoOutOutputModeDefault;
public float Gamma { get; set; } = 1.0f;
public Dictionary<long, long> LatencyStartPoints { get; } = new();
public Queue<(long FlipArg, long Timestamp)> LatencyStartPointOrder { get; } = new();
@ -249,6 +251,7 @@ public static partial class VideoOutExports
public VideoOutBufferSlot[] BufferSlots { get; } = CreateBufferSlots();
public List<FlipEventRegistration> FlipEvents { get; } = new();
public List<FlipEventRegistration> VblankEvents { get; } = new();
public List<FlipEventRegistration> OutputModeEvents { get; } = new();
public long OpenTimestamp;
public long LastCpuFlipTimestamp = -1;
public int WindowTop;
@ -435,10 +438,31 @@ public static partial class VideoOutExports
if (supported < 0) return supported;
if (supported == 0) return OrbisVideoOutErrorUnavailableOutputMode;
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
// The only advertised configuration is the current default output.
return TryGetPort(handle, out _)
? (int)OrbisGen2Result.ORBIS_GEN2_OK
: OrbisVideoOutErrorInvalidHandle;
var mode = ctx[CpuRegister.Rsi];
if (!TryGetPort(handle, out var port))
{
return OrbisVideoOutErrorInvalidHandle;
}
FlipEventRegistration[] registrations;
lock (_stateGate)
{
port.OutputMode = mode;
registrations = port.OutputModeEvents.ToArray();
}
var eventHint = (mode & 0x0000_FFFF_FFFF_FFFFUL) << 16;
foreach (var registration in registrations)
{
_ = KernelEventQueueCompatExports.TriggerDisplayEvent(
registration.Equeue,
SceVideoOutInternalEventOutputMode,
OrbisKernelEventFilterVideoOut,
eventHint,
registration.UserData);
}
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
@ -884,6 +908,52 @@ public static partial class VideoOutExports
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "kmSe30JTs+E",
ExportName = "sceVideoOutAddOutputModeEvent",
Target = Generation.Gen5,
LibraryName = "libSceVideoOut")]
public static int VideoOutAddOutputModeEvent(CpuContext ctx)
{
var equeue = ctx[CpuRegister.Rdi];
var handle = unchecked((int)ctx[CpuRegister.Rsi]);
var userData = ctx[CpuRegister.Rdx];
if (!TryGetPort(handle, out var port))
{
return OrbisVideoOutErrorInvalidHandle;
}
if (!KernelEventQueueCompatExports.IsValidEqueue(equeue))
{
return OrbisVideoOutErrorInvalidEventQueue;
}
ulong outputMode;
lock (_stateGate)
{
var existingIndex = port.OutputModeEvents.FindIndex(registration => registration.Equeue == equeue);
if (existingIndex >= 0)
{
port.OutputModeEvents[existingIndex] = new FlipEventRegistration(equeue, userData);
}
else
{
port.OutputModeEvents.Add(new FlipEventRegistration(equeue, userData));
}
outputMode = port.OutputMode;
}
var eventHint = (outputMode & 0x0000_FFFF_FFFF_FFFFUL) << 16;
return KernelEventQueueCompatExports.TriggerDisplayEvent(
equeue,
SceVideoOutInternalEventOutputMode,
OrbisKernelEventFilterVideoOut,
eventHint,
userData)
? (int)OrbisGen2Result.ORBIS_GEN2_OK
: OrbisVideoOutErrorInvalidEventQueue;
}
[SysAbiExport(
Nid = "oNOQn3knW6s",
ExportName = "sceVideoOutDeleteVblankEvent",
@ -1025,6 +1095,11 @@ public static partial class VideoOutExports
return 1;
}
if (ident == SceVideoOutInternalEventOutputMode)
{
return 8;
}
return OrbisVideoOutErrorInvalidEvent;
}
@ -1050,7 +1125,9 @@ public static partial class VideoOutExports
}
if (filter != OrbisKernelEventFilterVideoOut ||
(ident != SceVideoOutInternalEventFlip && ident != SceVideoOutInternalEventVblank))
(ident != SceVideoOutInternalEventFlip &&
ident != SceVideoOutInternalEventVblank &&
ident != SceVideoOutInternalEventOutputMode))
{
return OrbisVideoOutErrorInvalidEvent;
}

View File

@ -455,13 +455,13 @@ internal static unsafe partial class VulkanVideoPresenter
}
}
public void DispatchDirect(ulong submitId, uint groupsX, uint groupsY, uint groupsZ, uint dispatchInitiator)
public void DispatchDirect(ulong submitId, uint groupsX, uint groupsY, uint groupsZ, uint dispatchInitiator, ulong indirectArgumentsAddress = 0)
{
using var translationScope = RenderPhaseProfile.MeasureDetail(RenderPhaseProfile.Phase.CommandDispatchTranslation);
var started = System.Diagnostics.Stopwatch.GetTimestamp();
try
{
_translation.Dispatch(submitId, groupsX, groupsY, groupsZ, dispatchInitiator);
_translation.Dispatch(submitId, groupsX, groupsY, groupsZ, dispatchInitiator, indirectArgumentsAddress);
}
finally
{

View File

@ -87,9 +87,14 @@ internal static unsafe partial class VulkanVideoPresenter
private static ShaderImageShape ShapeOf(ImageResource image) => new(
Volume: image.Dimension == ImageDimension.Dim3D,
Arrayed: image.Cube || image.Dimension is ImageDimension.Dim1DArray or ImageDimension.Dim2DArray or ImageDimension.Dim2DMsaaArray,
Cube: image.Cube,
Storage: image.ResourceClass == ShaderCompiler.Resources.ImageResourceClass.Storage,
DynamicMip: image.MipMode == ImageMipMode.DynamicStorage,
NumericClass: NumericClassOf(image));
NumericClass: NumericClassOf(image),
OneDimensional: image.Dimension is ImageDimension.Dim1D or ImageDimension.Dim1DArray,
R128: image.R128,
Multisampled: image.Dimension is ImageDimension.Dim2DMsaa or ImageDimension.Dim2DMsaaArray,
DepthCompare: image.DepthCompare);
// 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)

View File

@ -628,6 +628,9 @@ internal static unsafe partial class VulkanVideoPresenter
$"mode={graphicsSubgroupMode} compute_subgroups=unchanged");
}
private bool _supportsFragmentShaderBarycentric;
private const string FragmentShaderBarycentricExtensionName = "VK_KHR_fragment_shader_barycentric";
private void CreateDevice()
{
var priority = 1.0f;
@ -708,6 +711,21 @@ internal static unsafe partial class VulkanVideoPresenter
"guest BC1-BC7 textures cannot be sampled directly.");
}
var barycentricFeatures = new PhysicalDeviceFragmentShaderBarycentricFeaturesKHR
{
SType = StructureType.PhysicalDeviceFragmentShaderBarycentricFeaturesKhr,
};
if (IsDeviceExtensionAvailable(FragmentShaderBarycentricExtensionName))
{
var barycentricQuery = new PhysicalDeviceFeatures2
{
SType = StructureType.PhysicalDeviceFeatures2,
PNext = &barycentricFeatures,
};
_vk.GetPhysicalDeviceFeatures2(_physicalDevice, &barycentricQuery);
_supportsFragmentShaderBarycentric = barycentricFeatures.FragmentShaderBarycentric;
}
var depthClipEnableFeatures = new PhysicalDeviceDepthClipEnableFeaturesEXT
{
SType = StructureType.PhysicalDeviceDepthClipEnableFeaturesExt,
@ -809,15 +827,20 @@ internal static unsafe partial class VulkanVideoPresenter
var colorWriteEnableExtension = (byte*)SilkMarshal.StringToPtr(ColorWriteEnableExtensionName);
var depthClipControlExtension = (byte*)SilkMarshal.StringToPtr(DepthClipControlExtensionName);
var depthClipEnableExtension = (byte*)SilkMarshal.StringToPtr(DepthClipEnableExtensionName);
var barycentricExtension = (byte*)SilkMarshal.StringToPtr(FragmentShaderBarycentricExtensionName);
try
{
var extensions = stackalloc byte*[11];
var extensions = stackalloc byte*[12];
var extensionCount = 0u;
extensions[extensionCount++] = swapchainExtension;
extensions[extensionCount++] = pushDescriptorExtension;
extensions[extensionCount++] = dynamicRenderingExtension;
extensions[extensionCount++] = extendedDynamicStateExtension;
extensions[extensionCount++] = extendedDynamicState2Extension;
if (_supportsFragmentShaderBarycentric)
{
extensions[extensionCount++] = barycentricExtension;
}
if (supportsColorWriteEnable)
{
extensions[extensionCount++] = colorWriteEnableExtension;
@ -880,6 +903,11 @@ internal static unsafe partial class VulkanVideoPresenter
PNext = &timelineSemaphoreFeatures,
};
void* renderingChain = &addressFeatures;
if (_supportsFragmentShaderBarycentric)
{
barycentricFeatures.PNext = renderingChain;
renderingChain = &barycentricFeatures;
}
if (_supportsDepthClipEnable)
{
depthClipEnableFeatures = new PhysicalDeviceDepthClipEnableFeaturesEXT
@ -962,6 +990,7 @@ internal static unsafe partial class VulkanVideoPresenter
SilkMarshal.Free((nint)depthClipControlExtension);
SilkMarshal.Free((nint)depthClipEnableExtension);
SilkMarshal.Free((nint)pushDescriptorExtension);
SilkMarshal.Free((nint)barycentricExtension);
}
_vk.GetDeviceQueue(_device, _queueFamilyIndex, 0, out _queue);

View File

@ -11,6 +11,7 @@ using SharpEmu.Libs.Gpu.Rendering;
using SharpEmu.Libs.Gpu.Scheduling;
using SharpEmu.ShaderCompiler;
using SharpEmu.ShaderCompiler.Resources;
using SharpEmu.ShaderCompiler.Vulkan;
using Silk.NET.Core.Native;
using Silk.NET.Vulkan;
using Silk.NET.Vulkan.Extensions.KHR;
@ -228,6 +229,12 @@ internal static unsafe partial class VulkanVideoPresenter
private ShaderModule CreateShaderModule(byte[] code)
{
if (!_supportsFragmentShaderBarycentric && RequiresFragmentShaderBarycentric(code))
{
throw new NotSupportedException(
"The shader requires the fragmentShaderBarycentric device feature.");
}
string? dumpPath = null;
var dumpDirectory = Environment.GetEnvironmentVariable("SHARPEMU_SHADER_SPIRV_DUMP_DIR");
if (!string.IsNullOrWhiteSpace(dumpDirectory))
@ -263,6 +270,28 @@ internal static unsafe partial class VulkanVideoPresenter
}
}
private static bool RequiresFragmentShaderBarycentric(ReadOnlySpan<byte> code)
{
for (var offset = 20; offset + 4 <= code.Length;)
{
var header = System.Buffers.Binary.BinaryPrimitives.ReadUInt32LittleEndian(code[offset..]);
var wordCount = (int)(header >> 16);
if (wordCount == 0 || wordCount > (code.Length - offset) / 4)
{
throw new InvalidOperationException("The shader contains an invalid SPIR-V instruction.");
}
if ((header & 0xFFFF) == (uint)SpirvOp.Capability && wordCount == 2 &&
System.Buffers.Binary.BinaryPrimitives.ReadUInt32LittleEndian(code[(offset + 4)..]) ==
(uint)SpirvCapability.FragmentBarycentricKhr)
{
return true;
}
offset += wordCount * 4;
}
return false;
}
// One layout binding per descriptor binding of the stage, at the stage's native binding numbers.
private static void CollectLayoutBindings(List<DescriptorSetLayoutBinding> bindings, ShaderProgramInfo program, ShaderStage stage)
{
@ -413,7 +442,7 @@ internal static unsafe partial class VulkanVideoPresenter
throw SubmissionScheduler.Fatal($"A vertex attribute buffer is swizzled: attribute={index} hash=0x{description.VertexStage.Hash:X16}.");
}
CheckVertexSwizzle(in descriptor, resource.RegisterCount, attributeSize, index);
CheckVertexSwizzle(in descriptor, (int)usedComponents, attributeSize, index);
attributes[index] = new VertexInputAttributeDescription
{
Location = (uint)index,
@ -427,12 +456,12 @@ internal static unsafe partial class VulkanVideoPresenter
private static uint DestinationSelect(uint x, uint y = 0, uint z = 0, uint w = 0) => x | (y << 3) | (z << 6) | (w << 9);
// A destination select the fixed-function fetch cannot apply is logged once and accepted.
private static void CheckVertexSwizzle(in BufferDescriptorWords descriptor, int registerCount, uint attributeSize, int index)
private static void CheckVertexSwizzle(in BufferDescriptorWords descriptor, int fetchedComponents, uint attributeSize, int index)
{
uint swizzle;
uint expected;
var supported = true;
switch (registerCount)
switch (fetchedComponents)
{
case 1:
swizzle = descriptor.DestinationSelectX;
@ -478,14 +507,14 @@ internal static unsafe partial class VulkanVideoPresenter
break;
default:
throw SubmissionScheduler.Fatal($"A vertex attribute fills an invalid register count: attribute={index} registers={registerCount}.");
throw SubmissionScheduler.Fatal($"A vertex attribute fetch uses an invalid component count: attribute={index} components={fetchedComponents}.");
}
if (!supported && Interlocked.Exchange(ref _vertexSwizzleLines, 1) == 0)
{
Console.Error.WriteLine(
$"[LOADER][INFO] vertex_input accepted an unsupported destination select attribute={index} size={attributeSize} " +
$"registers={registerCount} swizzle=0x{swizzle:X3} expected=0x{expected:X3}");
$"components={fetchedComponents} swizzle=0x{swizzle:X3} expected=0x{expected:X3}");
}
}

View File

@ -827,6 +827,28 @@ internal static unsafe partial class VulkanVideoPresenter
CountDraw();
}
public bool TryDispatchIndirect(ulong argumentsAddress)
{
using var profileScope = RenderPhaseProfile.MeasureDetail(RenderPhaseProfile.Phase.DrawRecording);
var (buffer, offset) = _bufferCache.ObtainBuffer(argumentsAddress, 3u * sizeof(uint), false);
var command = BeginBatchedGuestCommands();
var barrier = new BufferMemoryBarrier
{
SType = StructureType.BufferMemoryBarrier,
SrcAccessMask = AccessFlags.ShaderWriteBit | AccessFlags.TransferWriteBit | AccessFlags.MemoryWriteBit,
DstAccessMask = AccessFlags.IndirectCommandReadBit,
SrcQueueFamilyIndex = Vk.QueueFamilyIgnored,
DstQueueFamilyIndex = Vk.QueueFamilyIgnored,
Buffer = buffer.Handle,
Offset = offset,
Size = 3u * sizeof(uint),
};
_vk.CmdPipelineBarrier(command, PipelineStageFlags.AllCommandsBit, PipelineStageFlags.DrawIndirectBit, 0, 0, null, 1, &barrier, 0, null);
_vk.CmdDispatchIndirect(command, buffer.Handle, offset);
CountDraw();
return true;
}
private void RecordMemoryBarrier(PipelineStageFlags sourceStages, PipelineStageFlags destinationStages, AccessFlags sourceAccess, AccessFlags destinationAccess)
{
EndRendering();
@ -917,6 +939,17 @@ internal static unsafe partial class VulkanVideoPresenter
_ = BeginBatchedGuestCommands();
}
public void CopyDepthStencilImage(ResourceSlotIdentifier source, ResourceSlotIdentifier destination, in SubresourceRange range, in Extent3D extent, ImageAspectFlags aspects)
{
EndRendering();
_imageCache.RefreshImage(source);
_imageCache.RefreshImage(destination);
var sourceImage = _imageCache.GetImage(source);
var destinationImage = _imageCache.GetImage(destination);
destinationImage.CopyDepthStencilFrom(sourceImage, in range, in extent, aspects);
_ = BeginBatchedGuestCommands();
}
public bool IsMetadata(ulong address) => _imageCache.IsMetadata(address);
public bool ClearMetadata(ulong address)

View File

@ -123,7 +123,8 @@ public sealed class Gen5MslArgumentLayout
ImageNumericClass.Sint => "sint",
_ => "float",
};
return $"{classPart}_{numericPart}_{DimensionName(dimension)}";
var dimensionPart = ImageDescriptorBinding.IsCube(kind) ? "cube" : DimensionName(dimension);
return $"{classPart}_{numericPart}_{dimensionPart}";
}
public static string DimensionName(ImageDimension dimension) => dimension switch
@ -138,9 +139,9 @@ public sealed class Gen5MslArgumentLayout
};
// The Metal texture type of an image class with the given component and access.
public static string TextureType(ImageDimension dimension, string componentType, string? access)
public static string TextureType(ImageDimension dimension, string componentType, string? access, bool cube = false)
{
var typeName = dimension switch
var typeName = cube ? "texturecube" : dimension switch
{
ImageDimension.Dim1D => "texture1d",
ImageDimension.Dim1DArray => "texture1d_array",

View File

@ -203,6 +203,10 @@ 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(
instruction,
destination,
$"fma({F16(instruction, 0)}, {F16(instruction, 1)}, {F16(instruction, 2)})"),
"VFmacF32" or "VMacF32" =>
FloatResult(instruction, $"fma({F(instruction, 0)}, {F(instruction, 1)}, as_type<float>(v[{destination}]))"),
"VFloorF32" => FloatResult(instruction, $"floor({F(instruction, 0)})"),
@ -212,6 +216,10 @@ 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)})"),
"VRsqF16" => Float16Result(
instruction,
destination,
$"rsqrt({F16(instruction, 0)})"),
"VRcpF32" or "VRcpIflagF32" => FloatResult(instruction, $"(1.0f / {F(instruction, 0)})"),
"VLogF32" => FloatResult(instruction, $"log2({F(instruction, 0)})"),
"VExpF32" => FloatResult(instruction, $"exp2({F(instruction, 0)})"),

View File

@ -650,18 +650,36 @@ public static partial class Gen5MslTranslator
for (uint component = 0; component < control.DwordCount; component++)
{
if (component >= input.ComponentCount)
var selector = (input.DestinationSelect >> (int)(component * 3)) & 0x7u;
if (selector == 0)
{
// Formatted buffer loads return zero for components that
// are not present in the resource format.
StoreVector(control.VectorData + component, "0u");
}
else if (selector == 1)
{
StoreVector(
control.VectorData + component,
input.NumberFormat is 4u or 5u ? "1u" : "0x3f800000u");
}
else if (selector is >= 4u and <= 7u)
{
var sourceComponent = selector - 4u;
if (sourceComponent >= input.ComponentCount)
{
error =
$"vertex input destination selector exceeds source components: selector={selector} components={input.ComponentCount}";
return false;
}
var value = input.ComponentCount == 1
? $"sharpemu_vin.in{input.Location}"
: $"sharpemu_vin.in{input.Location}[{sourceComponent}]";
StoreVector(control.VectorData + component, AsUInt(value));
}
else
{
var value = input.ComponentCount == 1
? $"sharpemu_vin.in{input.Location}"
: $"sharpemu_vin.in{input.Location}[{component}]";
StoreVector(control.VectorData + component, AsUInt(value));
error = $"unsupported vertex input destination selector={selector}";
return false;
}
}

View File

@ -147,6 +147,7 @@ public static partial class Gen5MslTranslator
private void DeclareImageClass(DescriptorBinding binding, ShaderResourceInfo info)
{
var (resourceClass, numericClass, dimension, atomic) = ImageDescriptorBinding.Describe(binding.Kind);
var cube = ImageDescriptorBinding.IsCube(binding.Kind);
if (resourceClass == ImageResourceClass.None)
{
throw new InvalidOperationException($"binding kind {binding.Kind} is not an image class");
@ -177,7 +178,7 @@ public static partial class Gen5MslTranslator
var multisampled = dimension is ImageDimension.Dim2DMsaa or ImageDimension.Dim2DMsaaArray;
_imageClasses[binding.Kind] = new MslImageClass(
Gen5MslArgumentLayout.ImageClassName(binding.Kind),
Gen5MslArgumentLayout.TextureType(dimension, componentKind, access),
Gen5MslArgumentLayout.TextureType(dimension, componentKind, access, cube),
componentKind,
isStorage,
multisampled,
@ -465,7 +466,8 @@ public static partial class Gen5MslTranslator
value = Temp("uint", $"sharpemu_load_device_dword({DeviceArguments}, {componentAddress})");
}
if (_indirectKeyScratch.TryGetValue(memoryIndex, out var keyScratch))
if (!request.IndirectOffsetKeyMemoryIndices.Contains(memoryIndex) &&
_indirectKeyScratch.TryGetValue(memoryIndex, out var keyScratch))
{
Line($"{keyScratch} = {value};");
}

View File

@ -332,9 +332,15 @@ public static partial class Gen5MslTranslator
continue;
}
var componentType = input.NumberFormat switch
{
4u => "uint",
5u => "int",
_ => "float",
};
var fieldType = input.ComponentCount == 1
? "float"
: $"float{input.ComponentCount}";
? componentType
: $"{componentType}{input.ComponentCount}";
source.AppendLine(
$" {fieldType} in{input.Location} [[attribute({input.Location})]];");
}
@ -1010,6 +1016,7 @@ public static partial class Gen5MslTranslator
int bindingIndex;
string stride;
string descriptorWord3;
uint descriptorFormat;
{
// The dense buffer, its stride and its format come from the specialization.
if (!TryResolveLayoutBuffer(instruction.Pc, out bindingIndex, out var specialized))
@ -1019,6 +1026,7 @@ public static partial class Gen5MslTranslator
}
stride = FormatUInt(specialized.PackedStride & 0x3FFF);
descriptorFormat = specialized.DescriptorFormat;
descriptorWord3 = FormatUInt((specialized.DescriptorFormat << 12) | (specialized.DescriptorSwizzle & 0xFFF));
}
@ -1036,9 +1044,26 @@ public static partial class Gen5MslTranslator
ApplyByteBias(
bindingIndex,
$"(0x{unchecked((uint)control.OffsetBytes):X}u + {scalarOffset} + {vectorOffset} + ({vectorIndex} * {stride}))"));
if (instruction.Opcode is "BufferStoreFormatX" or "BufferStoreFormatXy" or
"BufferStoreFormatXyz" or "BufferStoreFormatXyzw")
{
if (descriptorFormat == 0)
{
return true;
}
if (!TryEmitBufferFormatStore(bindingIndex, address, control, descriptorWord3, descriptorFormat))
{
error = $"unsupported buffer store format {descriptorFormat}";
return false;
}
return true;
}
if (control.Typed &&
instruction.Opcode.StartsWith("TBufferStore", StringComparison.Ordinal) &&
TryEmitTypedBufferFormatStore(bindingIndex, address, control, descriptorWord3))
TryEmitBufferFormatStore(bindingIndex, address, control, descriptorWord3, control.TypedFormat))
{
return true;
}
@ -1181,15 +1206,16 @@ public static partial class Gen5MslTranslator
return true;
}
// A typed store converts each register with the format's number format and places
// A formatted store converts each register with the selected number format and places
// the bits at the component's offset; all transferred components are stored or dropped.
private bool TryEmitTypedBufferFormatStore(
private bool TryEmitBufferFormatStore(
int bindingIndex,
string byteAddress,
Gen5BufferMemoryControl control,
string descriptorWord3)
string descriptorWord3,
uint unifiedFormat)
{
if (!Gfx10UnifiedFormat.TryDecode(control.TypedFormat, out var dataFormat, out var numberFormat))
if (!Gfx10UnifiedFormat.TryDecode(unifiedFormat, out var dataFormat, out var numberFormat))
{
return false;
}

View File

@ -115,26 +115,19 @@ public static partial class Gen5SpirvTranslator
break;
case "VWritelaneB32":
{
// vdst[lane(src1)] = src0
// Per-lane: if current lane == src1, write src0, else keep old value.
var oldValue = LoadV(destination);
var src0 = GetRawSource(instruction, 0);
var laneSelect = GetRawSource(instruction, 1);
var currentLane = _subgroupInvocationIdInput != 0
? BitwiseAnd(
Load(_uintType, _subgroupInvocationIdInput),
UInt(RdnaWaveLaneCount - 1))
: UInt(0);
var sourceValue = GetRawSource(instruction, 0);
var selectedLane = BitwiseAnd(GetRawSource(instruction, 1), UInt(_waveLaneCount - 1));
var isTargetLane = _module.AddInstruction(
SpirvOp.IEqual,
_boolType,
currentLane,
laneSelect);
GuestWaveLane(),
selectedLane);
result = _module.AddInstruction(
SpirvOp.Select,
_uintType,
isTargetLane,
src0,
sourceValue,
oldValue);
// Writelane writes to a specific lane regardless of exec mask.
StoreV(destination, result, guardWithExec: false);
@ -283,6 +276,12 @@ public static partial class Gen5SpirvTranslator
instruction,
Ext(32, _floatType, GetFloatSource(instruction, 0)));
break;
case "VRsqF16":
result = EmitFloat16Result(
instruction,
destination,
Ext(32, _floatType, GetFloat16Source(instruction, 0)));
break;
case "VFractF32":
result = EmitFloatResult(
instruction,
@ -392,6 +391,17 @@ 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":
{
@ -455,6 +465,12 @@ public static partial class Gen5SpirvTranslator
case "VAddU32":
result = EmitIntegerBinary(instruction, SpirvOp.IAdd);
break;
case "VAddNcU16":
result = EmitInteger16Binary(
instruction,
destination,
SpirvOp.IAdd);
break;
case "VAddcU32":
case "VAddCoCiU32":
result = EmitAddWithCarry(instruction);
@ -1189,18 +1205,6 @@ public static partial class Gen5SpirvTranslator
return false;
}
var sourceCount = instruction.Opcode == "VPkFmaF16" ? 3 : 2;
for (var index = 0; index < sourceCount; index++)
{
var source = instruction.Sources[index];
if (source.Kind is not (Gen5OperandKind.VectorRegister or Gen5OperandKind.ScalarRegister))
{
error =
$"unsupported vop3p operand {source} for {instruction.Opcode} (first slice: registers only)";
return false;
}
}
var low = EmitPackedF16Lane(instruction, control, highLane: false);
var high = EmitPackedF16Lane(instruction, control, highLane: true);
result = BitwiseOr(low, ShiftLeftLogical(high, UInt(16)));
@ -1779,8 +1783,14 @@ public static partial class Gen5SpirvTranslator
}
else if (opcode is not ("VCmpClassF32" or "VCmpxClassF32"))
{
var left = GetRawSource(instruction, 0);
var right = GetRawSource(instruction, 1);
var compare64 = opcode.EndsWith("U64", StringComparison.Ordinal) ||
opcode.EndsWith("I64", StringComparison.Ordinal);
var left = compare64
? GetRawSource64(instruction, 0)
: GetRawSource(instruction, 0);
var right = compare64
? GetRawSource64(instruction, 1)
: GetRawSource(instruction, 1);
var signed = opcode.EndsWith("I32", StringComparison.Ordinal);
if (signed)
{
@ -1794,6 +1804,7 @@ public static partial class Gen5SpirvTranslator
"VCmpEqU32" or "VCmpxEqU32" => SpirvOp.IEqual,
"VCmpNeI32" or "VCmpxNeI32" or
"VCmpNeU32" or "VCmpxNeU32" => SpirvOp.INotEqual,
"VCmpxNeU64" => SpirvOp.INotEqual,
"VCmpLtI32" or "VCmpxLtI32" => SpirvOp.SLessThan,
"VCmpLeI32" or "VCmpxLeI32" => SpirvOp.SLessThanEqual,
"VCmpGtI32" or "VCmpxGtI32" => SpirvOp.SGreaterThan,
@ -3595,6 +3606,41 @@ public static partial class Gen5SpirvTranslator
return _module.AddInstruction(operation, _uintType, left, right);
}
private uint EmitInteger16Binary(
Gen5ShaderInstruction instruction,
uint destination,
SpirvOp operation)
{
var control = instruction.Control as Gen5Vop3Control;
var left = GetRawSource(instruction, 0, applySdwaIntegerModifiers: false);
var right = GetRawSource(instruction, 1, applySdwaIntegerModifiers: false);
if ((control?.OperandSelect & 0x1) != 0)
{
left = ShiftRightLogical(left, UInt(16));
}
if ((control?.OperandSelect & 0x2) != 0)
{
right = ShiftRightLogical(right, UInt(16));
}
left = BitwiseAnd(left, UInt(0xFFFF));
right = BitwiseAnd(right, UInt(0xFFFF));
var low16 = BitwiseAnd(
_module.AddInstruction(operation, _uintType, left, right),
UInt(0xFFFF));
var current = LoadV(destination);
return (control?.OperandSelect & 0x8) != 0
? BitwiseOr(
BitwiseAnd(current, UInt(0x0000_FFFF)),
ShiftLeftLogical(low16, UInt(16)))
: BitwiseOr(
BitwiseAnd(current, UInt(0xFFFF_0000)),
low16);
}
private enum CubeCoordinate
{
Id,
@ -3946,24 +3992,35 @@ public static partial class Gen5SpirvTranslator
}
var destination = instruction.Destinations[0].Value;
var src0 = GetRawSource(instruction, 0);
var sourceValue = GetRawSource(instruction, 0);
var selectedLane = BitwiseAnd(GetRawSource(instruction, 1), UInt(_waveLaneCount - 1));
if (_subgroupInvocationIdInput != 0)
if (_emulateWave64)
{
// The selected guest lane can belong to another host subgroup.
// Read it even when the guest execution mask disables that lane.
var isSelectedLane = _module.AddInstruction(
SpirvOp.IEqual, _boolType, GuestWaveLane(), selectedLane);
EmitConditional(isSelectedLane, () => Store(WaveBroadcastScratchPointer(), sourceValue));
EmitWave64Barrier();
var broadcast = Load(_uintType, WaveBroadcastScratchPointer());
EmitWave64Barrier();
StoreS(destination, broadcast);
}
else if (_subgroupInvocationIdInput != 0)
{
// sdst = vsrc0[lane(src1)] — broadcast from the specified lane.
var laneSelect = GetRawSource(instruction, 1);
var broadcast = _module.AddInstruction(
SpirvOp.GroupNonUniformBroadcast,
_uintType,
UInt(3), // Subgroup scope
src0,
laneSelect);
UInt(3),
sourceValue,
selectedLane);
StoreS(destination, broadcast);
}
else
{
// Fallback: no subgroup ops, read current lane's value.
StoreS(destination, src0);
StoreS(destination, sourceValue);
}
return true;

View File

@ -0,0 +1,141 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.ShaderCompiler;
namespace SharpEmu.ShaderCompiler.Vulkan;
public static partial class Gen5SpirvTranslator
{
private sealed partial class CompilationContext
{
private readonly HashSet<uint> _perVertexAttributes = [];
private readonly Dictionary<int, uint> _barycentricInputs = [];
private uint _interpolationSampleId;
private const uint InterpolateAtCentroid = 76;
private const uint InterpolateAtSample = 77;
private const uint InterpolateAtOffset = 78;
private void DeclareInterpolationParameters()
{
foreach (var instruction in _request.Program.Instructions)
{
if (instruction.Opcode == "VInterpMovF32" &&
instruction.Control is Gen5InterpolationControl interpolation)
{
_perVertexAttributes.Add(interpolation.Attribute);
}
}
if (_perVertexAttributes.Count == 0)
{
return;
}
_module.AddCapability(SpirvCapability.FragmentBarycentricKhr);
_module.AddExtension("SPV_KHR_fragment_shader_barycentric");
_module.AddCapability(SpirvCapability.InterpolationFunction);
var enabledInputs = _pixelInputAddress & _pixelInputEnable;
if ((enabledInputs & (1u << 3)) != 0)
{
throw new NotSupportedException("Pull-model interpolation parameters are not supported.");
}
var variables = new Dictionary<bool, uint>();
foreach (var bit in new[] { 0, 1, 2, 4, 5, 6 })
{
if ((enabledInputs & (1u << bit)) == 0)
{
continue;
}
if (!variables.TryGetValue(bit < 4, out var variable))
{
variable = _module.AddGlobalVariable(
_module.TypePointer(SpirvStorageClass.Input, _vec3Type), SpirvStorageClass.Input);
_module.AddDecoration(variable, SpirvDecoration.BuiltIn,
(uint)(bit < 4 ? SpirvBuiltIn.BaryCoordKhr : SpirvBuiltIn.BaryCoordNoPerspKhr));
variables.Add(bit < 4, variable);
_interfaces.Add(variable);
}
_barycentricInputs.Add(bit, variable);
}
if ((enabledInputs & 0x11u) != 0)
{
_module.AddCapability(SpirvCapability.SampleRateShading);
_interpolationSampleId = _module.AddGlobalVariable(
_module.TypePointer(SpirvStorageClass.Input, _intType), SpirvStorageClass.Input);
_module.AddDecoration(_interpolationSampleId, SpirvDecoration.BuiltIn, (uint)SpirvBuiltIn.SampleId);
_module.AddDecoration(_interpolationSampleId, SpirvDecoration.Flat);
_interfaces.Add(_interpolationSampleId);
}
}
private uint LoadBarycentricCoordinates(int bit, uint variable) => bit switch
{
0 or 4 => _module.AddInstruction(SpirvOp.ExtInst, _vec3Type, _glsl, InterpolateAtSample,
variable, Load(_intType, _interpolationSampleId)),
2 or 6 => _module.AddInstruction(SpirvOp.ExtInst, _vec3Type, _glsl, InterpolateAtCentroid, variable),
_ => _module.AddInstruction(SpirvOp.ExtInst, _vec3Type, _glsl, InterpolateAtOffset,
variable, _module.ConstantNull(_vec2Type)),
};
private bool TryEmitInterpolationParameter(
Gen5ShaderInstruction instruction,
Gen5InterpolationControl interpolation,
uint input,
uint destination,
out string error)
{
error = string.Empty;
var custom = interpolation.Attribute < 32 &&
(_request.PixelCustomInterpolationMask & (1u << (int)interpolation.Attribute)) != 0;
uint LoadVertex(uint vertex)
{
var pointer = _module.AddInstruction(SpirvOp.AccessChain,
_module.TypePointer(SpirvStorageClass.Input, _floatType),
input, UInt(vertex), UInt(interpolation.Channel));
return Load(_floatType, pointer);
}
uint LoadParameter(uint mode)
{
var value = LoadVertex((mode + 1) % 3);
return !custom && mode < 2
? _module.AddInstruction(SpirvOp.FSub, _floatType, value, LoadVertex(0))
: value;
}
uint result;
if (instruction.Opcode == "VInterpMovF32")
{
var mode = instruction.Words[0] & 0xFFu;
if (mode >= 3)
{
error = "reserved interpolation parameter selector";
return false;
}
result = LoadParameter(mode);
}
else if (instruction.Opcode is "VInterpP1F32" or "VInterpP2F32")
{
var firstPhase = instruction.Opcode == "VInterpP1F32";
var source = Bitcast(_floatType, GetRawSource(instruction, 0));
var product = _module.AddInstruction(SpirvOp.FMul, _floatType,
LoadParameter(firstPhase ? 0u : 1u), source);
result = _module.AddInstruction(SpirvOp.FAdd, _floatType, product,
firstPhase ? LoadParameter(2) : Bitcast(_floatType, LoadV(destination)));
}
else
{
error = $"unsupported interpolation opcode {instruction.Opcode}";
return false;
}
StoreV(destination, Bitcast(_uintType, result));
return true;
}
}
}

View File

@ -91,6 +91,7 @@ public static partial class Gen5SpirvTranslator
ImageComponentKind Kind,
bool IsStorage,
bool Arrayed,
bool Cube,
bool Multisampled,
SpirvImageDim Dimension,
IReadOnlyList<uint> Resources);
@ -307,6 +308,7 @@ public static partial class Gen5SpirvTranslator
private void DeclareImageClass(DescriptorBinding binding, uint bindingNumber)
{
var (resourceClass, numericClass, dimension, atomic) = ImageDescriptorBinding.Describe(binding.Kind);
var cube = ImageDescriptorBinding.IsCube(binding.Kind);
if (resourceClass == ImageResourceClass.None)
{
throw new InvalidOperationException($"binding kind {binding.Kind} is not an image class");
@ -353,7 +355,7 @@ public static partial class Gen5SpirvTranslator
_module.AddDecoration(variable, SpirvDecoration.DescriptorSet, 0);
_module.AddDecoration(variable, SpirvDecoration.Binding, bindingNumber);
_interfaces.Add(variable);
_imageClasses[binding.Kind] = new LayoutImageClass(variable, imageType, elementPointer, componentType, kind, isStorage, arrayed, multisampled, spirvDimension, binding.Resources);
_imageClasses[binding.Kind] = new LayoutImageClass(variable, imageType, elementPointer, componentType, kind, isStorage, arrayed, cube, multisampled, spirvDimension, binding.Resources);
}
// ---- initial state ----
@ -721,7 +723,8 @@ public static partial class Gen5SpirvTranslator
value = LoadDeviceDword(component == 0 ? deviceAddress.Value : IAdd64(deviceAddress.Value, ULong((ulong)component * sizeof(uint))));
}
if (_indirectKeyScratch.TryGetValue(memoryIndex, out var keyScratch))
if (!request.IndirectOffsetKeyMemoryIndices.Contains(memoryIndex) &&
_indirectKeyScratch.TryGetValue(memoryIndex, out var keyScratch))
{
Store(keyScratch, value);
}
@ -921,6 +924,11 @@ public static partial class Gen5SpirvTranslator
var candidateElements = new List<(uint Resource, uint Element)>();
foreach (var candidate in candidates)
{
if (candidate >= info.Images.Count)
{
error = $"indirect candidate {candidate} is outside the image table";
return false;
}
var candidateKind = ImageDescriptorBinding.ForImage(info.Images[(int)candidate]);
if (candidateKind is null || !_imageClasses.TryGetValue(candidateKind.Value, out var candidateClass))
{
@ -978,6 +986,11 @@ public static partial class Gen5SpirvTranslator
}
var resourceIndex = (int)(fixedElement?.Resource ?? entry.Resource);
if ((uint)resourceIndex >= info.Images.Count)
{
error = $"image {resourceIndex} is outside the image table";
return false;
}
var imageInfo = info.Images[resourceIndex];
var kind = ImageDescriptorBinding.ForImage(imageInfo);
if (kind is null || !_imageClasses.TryGetValue(kind.Value, out var imageClass))
@ -986,12 +999,6 @@ public static partial class Gen5SpirvTranslator
return false;
}
if (imageClass.Multisampled)
{
error = "multisampled image access is not supported";
return false;
}
var element = imageClass.Resources.ToList().IndexOf((uint)resourceIndex);
if (element < 0)
{
@ -1052,7 +1059,11 @@ public static partial class Gen5SpirvTranslator
imageClass.Kind,
imageClass.IsStorage,
imageClass.Arrayed,
imageClass.Dimension);
imageClass.Cube,
imageClass.Multisampled,
imageClass.Dimension,
imageInfo.ConversionFormat,
imageInfo.ShaderSwizzle);
dstSelect = imageInfo.ShaderSwizzle;
return true;
}
@ -1147,6 +1158,24 @@ public static partial class Gen5SpirvTranslator
case "DsReadB32":
StoreV(instruction.Destinations[0].Value, LoadBlockWord(_globalDataShare, GlobalDataShareIndex(GetRawSource(instruction, 0), control.SingleOffsetBytes)));
return true;
case "DsReadI8":
{
var address = GetRawSource(instruction, 0);
var byteAddress = control.SingleOffsetBytes == 0
? address
: IAdd(address, UInt(control.SingleOffsetBytes));
var word = LoadBlockWord(_globalDataShare, GlobalDataShareIndex(address, control.SingleOffsetBytes));
var shift = ShiftLeftLogical(BitwiseAnd(byteAddress, UInt(3)), UInt(3));
var packed = ShiftRightLogical(word, shift);
var signedByte = _module.AddInstruction(
SpirvOp.BitFieldSExtract,
_intType,
Bitcast(_intType, packed),
UInt(0),
UInt(8));
StoreV(instruction.Destinations[0].Value, Bitcast(_uintType, signedByte));
return true;
}
case "DsReadB64":
{
var index = GlobalDataShareIndex(GetRawSource(instruction, 0), control.SingleOffsetBytes);
@ -1240,6 +1269,29 @@ public static partial class Gen5SpirvTranslator
private bool TryEmitGlobalDataShareAtomic(Gen5ShaderInstruction instruction, Gen5DataShareControl control, out string error)
{
error = string.Empty;
if (instruction.Opcode is "DsMinF32" or "DsMaxF32")
{
if (instruction.Sources.Count < 3)
{
error = $"missing GDS operands for {instruction.Opcode}";
return false;
}
var floatIndex = GlobalDataShareIndex(GetRawSource(instruction, 0), control.SingleOffsetBytes);
EmitExecConditional(() =>
{
EmitConditional(IsBlockWordInRange(_globalDataShare, floatIndex), () =>
EmitDataShareFloatAtomic(
BlockWordPointer(_globalDataShare, floatIndex),
GetRawSource(instruction, 1),
GetRawSource(instruction, 2),
instruction.Opcode == "DsMaxF32",
scope: 1,
semantics: 0x48));
});
return true;
}
var atomicOp = instruction.Opcode switch
{
"DsAddU32" or "DsAddRtnU32" => SpirvOp.AtomicIAdd,

File diff suppressed because it is too large Load Diff

View File

@ -185,6 +185,10 @@ public enum SpirvOp : ushort
public enum SpirvCapability : uint
{
Shader = 1,
InterpolationFunction = 52,
FragmentBarycentricKhr = 5284,
ClipDistance = 32,
CullDistance = 33,
SampledImageArrayDynamicIndexing = 29,
StorageImageArrayDynamicIndexing = 31,
SampleRateShading = 35,
@ -203,6 +207,8 @@ public enum SpirvCapability : uint
GroupNonUniformVote = 62,
GroupNonUniformBallot = 64,
GroupNonUniformShuffle = 65,
ShaderLayer = 5253,
ShaderViewportIndex = 5254,
RuntimeDescriptorArray = 5302,
PhysicalStorageBufferAddresses = 5347,
}
@ -243,6 +249,7 @@ public enum SpirvDecoration : uint
BuiltIn = 11,
NoPerspective = 13,
Flat = 14,
PerVertexKhr = 5285,
Location = 30,
Binding = 33,
DescriptorSet = 34,
@ -254,9 +261,16 @@ public enum SpirvDecoration : uint
public enum SpirvBuiltIn : uint
{
Position = 0,
PointSize = 1,
ClipDistance = 3,
CullDistance = 4,
Layer = 9,
ViewportIndex = 10,
VertexIndex = 42,
InstanceIndex = 43,
FragCoord = 15,
BaryCoordKhr = 5286,
BaryCoordNoPerspKhr = 5287,
FrontFacing = 17,
WorkgroupId = 26,
LocalInvocationId = 27,

View File

@ -829,6 +829,7 @@ public static partial class Gen5ShaderTranslator
0x43 => "VMovrelsB32",
0x44 => "VMovrelsdB32",
0x48 => "VMovrelsd2B32",
0x56 => "VRsqF16",
_ => string.Empty,
};
@ -1009,6 +1010,7 @@ public static partial class Gen5ShaderTranslator
0xED => "VCmpNeqF16",
0xEE => "VCmpNltF16",
0xEF => "VCmpTruF16",
0xF5 => "VCmpxNeU64",
0xF8 => "VCmpxUF16",
0xF9 => "VCmpxNgeF16",
0xFA => "VCmpxNlgF16",
@ -1087,6 +1089,8 @@ public static partial class Gen5ShaderTranslator
0x16A => "VMulHiU32",
0x16B => "VMulLoI32",
0x16C => "VMulHiI32",
0x303 => "VAddNcU16",
0x34B => "VFmaF16",
0x360 => "VReadlaneB32",
0x361 => "VWritelaneB32",
0x362 => "VLdexpF32",
@ -1193,6 +1197,8 @@ public static partial class Gen5ShaderTranslator
0x0E => "DsWrite2B32",
0x0F => "DsWrite2St64B32",
0x10 => "DsCmpstB32",
0x12 => "DsMinF32",
0x13 => "DsMaxF32",
0x20 => "DsAddRtnU32",
0x21 => "DsSubRtnU32",
0x23 => "DsIncRtnU32",
@ -1210,6 +1216,7 @@ public static partial class Gen5ShaderTranslator
0x36 => "DsReadB32",
0x37 => "DsRead2B32",
0x38 => "DsRead2St64B32",
0x39 => "DsReadI8",
0x3D => "DsConsume",
0x3E => "DsAppend",
0x4D => "DsWriteB64",
@ -1239,7 +1246,10 @@ public static partial class Gen5ShaderTranslator
out uint sizeDwords,
out string error)
{
var opcode = (word >> 18) & 0x7F;
// GFX10 MIMG uses bit 0 as opcode bit 7. Treating the opcode as only
// bits 18..24 makes the 0x80+ family (including sample-adjust forms)
// decode as the corresponding low opcode.
var opcode = ((word >> 18) & 0x7F) | ((word & 1) << 7);
name = $"{prefix}Raw{opcode:X2}";
sizeDwords = 2;
error = string.Empty;
@ -1287,7 +1297,7 @@ public static partial class Gen5ShaderTranslator
out uint sizeDwords,
out string error)
{
var opcode = (word >> 18) & 0x7F;
var opcode = ((word >> 18) & 0x7F) | ((word & 1) << 7);
name = opcode switch
{
0x00 => "BufferLoadFormatX",
@ -1333,6 +1343,9 @@ public static partial class Gen5ShaderTranslator
0x3B => "BufferAtomicXor",
0x3C => "BufferAtomicInc",
0x3D => "BufferAtomicDec",
0x3F => "BufferAtomicFmin",
0x40 => "BufferAtomicFmax",
0x5A => "BufferAtomicOrX2",
_ => $"MubufRaw{opcode:X2}",
};
sizeDwords = (extra >> 24) == 0xFF ? 3u : 2u;
@ -1347,7 +1360,7 @@ public static partial class Gen5ShaderTranslator
out string error)
{
var segment = (word >> 14) & 0x3;
var opcode = (word >> 18) & 0x7F;
var opcode = ((word >> 18) & 0x7F) | ((word & 1) << 7);
sizeDwords = 2;
error = string.Empty;
var prefix = segment switch
@ -1468,20 +1481,70 @@ public static partial class Gen5ShaderTranslator
0x1B => "ImageAtomicInc",
0x1C => "ImageAtomicDec",
0x20 => "ImageSample",
0x21 => "ImageSampleCl",
0x22 => "ImageSampleD",
0x23 => "ImageSampleDCl",
0x24 => "ImageSampleL",
0x25 => "ImageSampleB",
0x26 => "ImageSampleBCl",
0x27 => "ImageSampleLz",
0x28 => "ImageSampleC",
0x29 => "ImageSampleCCl",
0x2A => "ImageSampleCD",
0x2B => "ImageSampleCDCl",
0x2C => "ImageSampleCL",
0x2D => "ImageSampleCB",
0x2E => "ImageSampleCBCl",
0x2F => "ImageSampleCLz",
0x30 => "ImageSampleO",
0x31 => "ImageSampleClO",
0x32 => "ImageSampleDO",
0x33 => "ImageSampleDClO",
0x34 => "ImageSampleLO",
0x35 => "ImageSampleBO",
0x36 => "ImageSampleBClO",
0x37 => "ImageSampleLzO",
0x38 => "ImageSampleCO",
0x39 => "ImageSampleCClO",
0x3A => "ImageSampleCDO",
0x3B => "ImageSampleCDClO",
0x3C => "ImageSampleCLO",
0x3D => "ImageSampleCBO",
0x3E => "ImageSampleCBClO",
0x3F => "ImageSampleCLzO",
0x40 => "ImageGather4",
0x47 => "ImageGather4Lz",
0x48 => "ImageGather4C",
0x4E => "ImageGather4CBCl",
0x4F => "ImageGather4CLz",
0x57 => "ImageGather4LzO",
0x58 => "ImageGather4CO",
0x5F => "ImageGather4CLzO",
0x60 => "ImageGetLod",
0x61 => "ImageGather4H",
0x68 => "ImageSampleCd",
0x69 => "ImageSampleCdCl",
0x6A => "ImageSampleCCd",
0x6B => "ImageSampleCCdCl",
0x6C => "ImageSampleCdO",
0x6D => "ImageSampleCdClO",
0x6E => "ImageSampleCCdO",
0x6F => "ImageSampleCCdClO",
0xA0 => "ImageSampleA",
0xA1 => "ImageSampleClA",
0xA5 => "ImageSampleBA",
0xA6 => "ImageSampleBClA",
0xA8 => "ImageSampleCA",
0xA9 => "ImageSampleCClA",
0xAD => "ImageSampleCBA",
0xAE => "ImageSampleCBClA",
0xB0 => "ImageSampleAO",
0xB1 => "ImageSampleClAO",
0xB5 => "ImageSampleBAO",
0xB6 => "ImageSampleBClAO",
0xB8 => "ImageSampleCAO",
0xB9 => "ImageSampleCClAO",
0xBD => "ImageSampleCBAO",
0xBE => "ImageSampleCBClAO",
_ => string.Empty,
};
@ -1627,6 +1690,7 @@ public static partial class Gen5ShaderTranslator
{
"DsAddU32" or "DsSubU32" or "DsIncU32" or "DsDecU32" or
"DsMinI32" or "DsMaxI32" or "DsMinU32" or "DsMaxU32" or
"DsMinF32" or "DsMaxF32" or
"DsAndB32" or "DsOrB32" or "DsXorB32" or "DsCmpstB32" or
"DsAddRtnU32" or "DsSubRtnU32" or "DsIncRtnU32" or "DsDecRtnU32" or
"DsMinRtnI32" or "DsMaxRtnI32" or "DsMinRtnU32" or "DsMaxRtnU32" or
@ -2103,6 +2167,13 @@ public static partial class Gen5ShaderTranslator
Gen5Operand.Vector(vectorData0),
Gen5Operand.Vector(vectorData1),
],
// GFX10 DS_MIN/MAX_F32 use DATA0 as the replacement value and
// DATA1 as the floating-point compare operand.
"DsMinF32" or "DsMaxF32" => [
Gen5Operand.Vector(vectorAddress),
Gen5Operand.Vector(vectorData0),
Gen5Operand.Vector(vectorData1),
],
_ when IsDataShareAtomic(opcode) => [
Gen5Operand.Vector(vectorAddress),
Gen5Operand.Vector(vectorData0),
@ -2114,7 +2185,7 @@ public static partial class Gen5ShaderTranslator
"DsAppend" or "DsConsume" => [
Gen5Operand.Vector(vectorDestination),
],
"DsReadB32" or "DsReadAddtidB32" or "DsSwizzleB32" => [
"DsReadB32" or "DsReadI8" or "DsReadAddtidB32" or "DsSwizzleB32" => [
Gen5Operand.Vector(vectorDestination),
],
"DsReadB64" or "DsRead2B32" or "DsRead2St64B32" => [
@ -2272,7 +2343,7 @@ public static partial class Gen5ShaderTranslator
"BufferStoreDwordx2" => 2u,
"BufferStoreDwordx3" => 3u,
"BufferStoreDwordx4" => 4u,
"BufferAtomicCmpswap" => 2u,
"BufferAtomicCmpswap" or "BufferAtomicOrX2" => 2u,
_ when opcode.StartsWith("BufferAtomic", StringComparison.Ordinal) => 1u,
_ => 0u,
};

View File

@ -7,7 +7,7 @@ using SharpEmu.ShaderCompiler.Ir;
namespace SharpEmu.ShaderCompiler.Resources;
// The fixed binding numbers of one stage: buffers, thirty-six image classes, samplers,
// The fixed binding numbers of one stage: buffers, image classes, samplers,
// and the shared buffers. A pixel stage adds Count to every number.
public enum DescriptorBindingKind : uint
{
@ -44,9 +44,58 @@ public static class ImageDescriptorBinding
private const uint StorageFloatBinding = 22;
private const uint StorageUintBinding = 27;
private const uint AtomicUintBinding = 32;
private const uint SampledCubeFloatBinding = 37;
private const uint SampledCubeUintBinding = 38;
private const uint SampledCubeSintBinding = 39;
private const uint StorageCubeFloatBinding = 40;
private const uint StorageCubeUintBinding = 41;
private const uint AtomicCubeUintBinding = 42;
public static DescriptorBindingKind? ForImage(ImageResource image)
{
if (image.Cube)
{
if (image.Dimension is not (ImageDimension.Dim2D or ImageDimension.Dim2DArray))
{
return null;
}
if (image.ResourceClass == ImageResourceClass.Sampled)
{
if (image.Atomic)
{
return null;
}
return image.NumericClass switch
{
ImageNumericClass.Float => (DescriptorBindingKind)SampledCubeFloatBinding,
ImageNumericClass.Uint => (DescriptorBindingKind)SampledCubeUintBinding,
ImageNumericClass.Sint => (DescriptorBindingKind)SampledCubeSintBinding,
_ => null,
};
}
if (image.ResourceClass == ImageResourceClass.Storage)
{
if (image.Atomic)
{
return image.NumericClass == ImageNumericClass.Uint
? (DescriptorBindingKind)AtomicCubeUintBinding
: null;
}
return image.NumericClass switch
{
ImageNumericClass.Float => (DescriptorBindingKind)StorageCubeFloatBinding,
ImageNumericClass.Uint => (DescriptorBindingKind)StorageCubeUintBinding,
_ => null,
};
}
return null;
}
uint baseBinding;
var sampled = false;
if (image.ResourceClass == ImageResourceClass.Sampled)
@ -147,6 +196,16 @@ public static class ImageDescriptorBinding
public static ImageResourceClass ResourceClass(DescriptorBindingKind kind)
{
var value = (uint)kind;
if (value is >= SampledCubeFloatBinding and <= SampledCubeSintBinding)
{
return ImageResourceClass.Sampled;
}
if (value is >= StorageCubeFloatBinding and <= AtomicCubeUintBinding)
{
return ImageResourceClass.Storage;
}
if (value >= BindingLayout.FirstImageBinding && value < BindingLayout.FirstStorageImageBinding)
{
return ImageResourceClass.Sampled;
@ -162,6 +221,9 @@ public static class ImageDescriptorBinding
public static uint ArrayIndex(DescriptorBindingKind kind) => (uint)kind - BindingLayout.FirstImageBinding;
public static bool IsCube(DescriptorBindingKind kind) =>
(uint)kind is >= SampledCubeFloatBinding and <= AtomicCubeUintBinding;
private static readonly ImageDimension[] SampledDimensions =
[
ImageDimension.Dim1D, ImageDimension.Dim1DArray, ImageDimension.Dim2D, ImageDimension.Dim2DArray,
@ -177,6 +239,29 @@ public static class ImageDescriptorBinding
public static (ImageResourceClass ResourceClass, ImageNumericClass NumericClass, ImageDimension Dimension, bool Atomic) Describe(DescriptorBindingKind kind)
{
var index = (uint)kind;
if (index is >= SampledCubeFloatBinding and <= SampledCubeSintBinding)
{
var numericClass = index switch
{
SampledCubeFloatBinding => ImageNumericClass.Float,
SampledCubeUintBinding => ImageNumericClass.Uint,
_ => ImageNumericClass.Sint,
};
return (ImageResourceClass.Sampled, numericClass, ImageDimension.Dim2DArray, false);
}
if (index is >= StorageCubeFloatBinding and <= StorageCubeUintBinding)
{
return (ImageResourceClass.Storage,
index == StorageCubeFloatBinding ? ImageNumericClass.Float : ImageNumericClass.Uint,
ImageDimension.Dim2DArray, false);
}
if (index == AtomicCubeUintBinding)
{
return (ImageResourceClass.Storage, ImageNumericClass.Uint, ImageDimension.Dim2DArray, true);
}
if (index >= SampledFloatBinding && index < StorageFloatBinding)
{
var offset = index - SampledFloatBinding;
@ -205,7 +290,7 @@ public sealed class BindingLayout : IEquatable<BindingLayout>
{
public const uint FirstImageBinding = 1;
public const uint FirstStorageImageBinding = 22;
public const uint ImageBindingCount = 36;
public const uint ImageBindingCount = 42;
public const uint NoShaderBase = uint.MaxValue;
public const uint ShaderBaseDwordCount = 2;
private const int ScalarRegisterCount = 256;

View File

@ -260,9 +260,16 @@ public sealed class MemoryAccessTable
var atomic = opcode.StartsWith("ImageAtomic", StringComparison.Ordinal);
var store = opcode.StartsWith("ImageStore", StringComparison.Ordinal);
var sampled = opcode.StartsWith("ImageSample", StringComparison.Ordinal) ||
opcode.StartsWith("ImageGather", StringComparison.Ordinal);
opcode.StartsWith("ImageGather", StringComparison.Ordinal) ||
opcode == "ImageGetLod";
var compare = (opcode.StartsWith("ImageSampleC", StringComparison.Ordinal) && !opcode.StartsWith("ImageSampleCd", StringComparison.Ordinal)) ||
opcode.StartsWith("ImageGather4C", StringComparison.Ordinal);
var mimgOpcode = instruction.Words.Count != 0
? ((instruction.Words[0] >> 18) & 0x7F) | ((instruction.Words[0] & 1) << 7)
: 0u;
var adjust = mimgOpcode is
0xA0 or 0xA1 or 0xA5 or 0xA6 or 0xA8 or 0xA9 or 0xAD or 0xAE or
0xB0 or 0xB1 or 0xB5 or 0xB6 or 0xB8 or 0xB9 or 0xBD or 0xBE;
return new MemoryAccessInfo
{
Pc = instruction.Pc,
@ -273,7 +280,9 @@ public sealed class MemoryAccessTable
NeedsSampler = sampled,
Dmask = control.Dmask,
ImageDimension = DecodeImageDimension(control.Dimension),
ImageSampleFlags = compare ? ImageSampleFlags.Compare : ImageSampleFlags.None,
ImageSampleFlags =
(compare ? ImageSampleFlags.Compare : ImageSampleFlags.None) |
(adjust ? ImageSampleFlags.Adjust : ImageSampleFlags.None),
ImageHasMip = opcode is "ImageLoadMip" or "ImageStoreMip",
ImageR128 = instruction.Words.Count != 0 && ((instruction.Words[0] >> 15) & 1) != 0,
Glc = control.Glc,

View File

@ -146,7 +146,8 @@ public static class ResourceMaterializer
for (var candidateIndex = 0; candidateIndex < descriptors.Count; candidateIndex++)
{
var descriptor = descriptors[candidateIndex];
if (NullImageDescriptor(descriptor.Dwords) || !ValidImageDescriptor(descriptor.Dwords, image.R128))
if (NullImageDescriptor(descriptor.Dwords) || !ValidImageDescriptor(descriptor.Dwords, image.R128) ||
!ReservedImageBitsClear(descriptor.Dwords))
descriptor = DescriptorWords.Empty(8);
var existing = directTable.Descriptors.FindIndex(candidate => candidate.SameAs(descriptor));
if (existing < 0)
@ -161,12 +162,27 @@ public static class ResourceMaterializer
if (directTable.Descriptors.Count > 1) snapshot.IndirectImages.Add(directTable);
continue;
}
if (indirect.Dense)
{
if (!RuntimeValueEvaluator.EvaluateSources(plan, [indirect.HeapSource], cleanInputs, [], evaluateTable: false,
out var denseSources, out _))
return false;
if (!MaterializeDenseIndirectImage(indirect, denseSources[0], image, image.R128, inputs, out var denseTable, out failure))
return false;
snapshot.Images[imageIndex] = denseTable.Descriptors[(int)denseTable.Candidates[0]].Dwords;
if (denseTable.Descriptors.Count > 1)
{
denseTable.Resource = (uint)imageIndex;
snapshot.IndirectImages.Add(denseTable);
}
continue;
}
if (!RuntimeValueEvaluator.EvaluateSources(plan, [indirect.MaterialSource, indirect.HeapSource], cleanInputs, [], evaluateTable: false, out var tables, out _))
{
return false;
}
if (!MaterializeIndirectImage(plan, indirect, tables[0], tables[1], image.R128, inputs,
if (!MaterializeIndirectImage(plan, indirect, tables[0], tables[1], image, inputs,
captureSelectorDiagnostic, out var indirectTable, out failure))
{
return false;
@ -201,6 +217,21 @@ public static class ResourceMaterializer
private static bool NullImageDescriptor(ReadOnlySpan<uint> descriptor) =>
descriptor[0] == 0 && (descriptor[1] & 0xFF) == 0;
private static bool ReservedImageBitsClear(ReadOnlySpan<uint> descriptor)
{
ReadOnlySpan<uint> reserved =
[
0x00000000u, 0x20000000u, 0xf0003000u, 0x00000000u,
0xe000e000u, 0xf9000000u, 0x00007b00u, 0x00000000u,
];
for (var dword = 0; dword < reserved.Length; dword++)
{
if ((descriptor[dword] & reserved[dword]) != 0)
return false;
}
return true;
}
private static bool ValidImageDescriptor(ReadOnlySpan<uint> descriptor, bool r128)
{
var type = GuestImageFormat.ImageTypeOf(descriptor);
@ -215,6 +246,20 @@ public static class ResourceMaterializer
return false;
}
if (type is GuestImageFormat.ImageTypeCube or
GuestImageFormat.ImageType1DArray or
GuestImageFormat.ImageType2DArray or
GuestImageFormat.ImageType2DMsaaArray)
{
var lastSlice = descriptor[4] & 0x1FFF;
var baseSlice = (descriptor[4] >> 16) & 0x1FFF;
if (baseSlice > lastSlice)
{
return false;
}
}
if (type is GuestImageFormat.ImageType2DMsaa or GuestImageFormat.ImageType2DMsaaArray)
{
var baseLevel = (descriptor[3] >> 12) & 0xF;
@ -259,7 +304,7 @@ public static class ResourceMaterializer
IndirectImageSelector indirect,
DescriptorWords material,
DescriptorWords heap,
bool r128,
ImageResource image,
ResourceRuntimeInputs inputs,
bool captureSelectorDiagnostic,
out IndirectImageTable result,
@ -322,15 +367,7 @@ public static class ResourceMaterializer
}
var table = new IndirectImageTable
{
Keys = keys,
MaterialDescriptor = material.Dwords,
HeapDescriptor = heap.Dwords,
SelectorStride = indirect.SelectorStride,
SelectorOffset = indirect.SelectorOffset,
SelectorDiagnostic = diagnostic,
};
var probed = new List<uint[]>(keys.Count);
foreach (var key in keys)
{
var candidate = new uint[8];
@ -343,34 +380,102 @@ public static class ResourceMaterializer
}
}
if (NullImageDescriptor(candidate) || !ValidImageDescriptor(candidate, r128))
if (NullImageDescriptor(candidate) || !ValidImageDescriptor(candidate, image.R128) ||
!ReservedImageBitsClear(candidate))
{
Array.Clear(candidate);
}
var words = new DescriptorWords(candidate);
var found = table.Descriptors.FindIndex(existing => existing.SameAs(words));
if (found < 0)
{
if (table.Descriptors.Count >= ShaderResourceInfo.MaxImages)
{
failure = ResourceMaterializationFailure.ImageCapacityExceeded;
return Fail("indirect image candidates exceed the dense image resource limit");
}
table.Descriptors.Add(words);
table.Candidates.Add((uint)(table.Descriptors.Count - 1));
}
else
{
table.Candidates.Add((uint)found);
}
probed.Add(candidate);
}
if (!FinishIndirectImage(
probed,
keys,
out var table,
out failure))
{
return false;
}
table.MaterialDescriptor = material.Dwords;
table.HeapDescriptor = heap.Dwords;
table.SelectorStride = indirect.SelectorStride;
table.SelectorOffset = indirect.SelectorOffset;
table.SelectorDiagnostic = diagnostic;
result = table;
return true;
}
private static bool MaterializeDenseIndirectImage(
IndirectImageSelector indirect,
DescriptorWords heap,
ImageResource image,
bool r128,
ResourceRuntimeInputs inputs,
out IndirectImageTable result,
out ResourceMaterializationFailure failure)
{
failure = ResourceMaterializationFailure.Other;
result = new IndirectImageTable();
if (heap.DwordCount != 2 || indirect.KeyBound == 0 || indirect.KeyBound > MaxIndirectImageProbes || inputs.ReadCleanMemory is null)
return false;
var baseAddress = (((ulong)heap.Dwords[1] << 32) | heap.Dwords[0]) & AddressMask;
var probed = new List<uint[]>((int)indirect.KeyBound);
for (uint key = 0; key < indirect.KeyBound; key++)
{
var candidate = new uint[8];
var entry = (ulong)indirect.TableOffset + ((ulong)key << 5);
for (uint dword = 0; dword < 8; dword++)
{
var relative = entry + dword * sizeof(uint);
if (relative > AddressMask || baseAddress > AddressMask - relative ||
!inputs.ReadCleanMemory(baseAddress + relative, out candidate[dword]))
return false;
}
if (NullImageDescriptor(candidate) || !ValidImageDescriptor(candidate, r128) ||
!ReservedImageBitsClear(candidate))
Array.Clear(candidate);
probed.Add(candidate);
}
return FinishIndirectImage(
probed,
Enumerable.Range(0, (int)indirect.KeyBound)
.Select(index => unchecked(indirect.DynamicOffsetBase + ((uint)index << 5))),
out result,
out failure);
}
private static bool FinishIndirectImage(
IReadOnlyList<uint[]> probed,
IEnumerable<uint> keys,
out IndirectImageTable result,
out ResourceMaterializationFailure failure)
{
failure = ResourceMaterializationFailure.Other;
result = new IndirectImageTable();
result.Keys.AddRange(keys);
foreach (var candidate in probed)
{
var words = new DescriptorWords(candidate);
var found = result.Descriptors.FindIndex(existing => existing.SameAs(words));
if (found < 0)
{
if (result.Descriptors.Count >= ShaderResourceInfo.MaxImages)
{
failure = ResourceMaterializationFailure.ImageCapacityExceeded;
return false;
}
found = result.Descriptors.Count;
result.Descriptors.Add(words);
}
result.Candidates.Add((uint)found);
}
return true;
}
// ---- specialization ----
private static ImageDimension DescriptorDimension(ReadOnlySpan<uint> descriptor, ImageDimension requested)
@ -643,6 +748,7 @@ public static class ResourceMaterializer
}
var imageClass = images[(int)exemplar];
var separateSampledDimensions = info.Images[rootIndex].ResourceClass == ImageResourceClass.Sampled;
for (var candidate = 0; candidate < images.Count; candidate++)
{
var image = images[candidate];
@ -665,12 +771,12 @@ public static class ResourceMaterializer
images[candidate] = image;
}
// Sampled 2D cases use their own binding and coordinate width.
var separateDimensions = info.Images[rootIndex].ResourceClass == ImageResourceClass.Sampled &&
!image.Cube && !imageClass.Cube &&
// Each sampled candidate has its own binding and coordinate width.
var compatibleDimensions = image.Dimension == imageClass.Dimension ||
separateSampledDimensions && !image.Cube && !imageClass.Cube &&
image.Dimension is ImageDimension.Dim2D or ImageDimension.Dim2DArray &&
imageClass.Dimension is ImageDimension.Dim2D or ImageDimension.Dim2DArray;
if (image.NumericClass != imageClass.NumericClass || (!separateDimensions && image.Dimension != imageClass.Dimension) ||
if (image.NumericClass != imageClass.NumericClass || !compatibleDimensions ||
image.MipCount != imageClass.MipCount || image.ConversionFormat != imageClass.ConversionFormat ||
image.ShaderSwizzle != imageClass.ShaderSwizzle || image.Cube != imageClass.Cube)
{
@ -863,8 +969,52 @@ public static class ResourceMaterializer
{
pair.Sampler = samplerPlan.PointSampler[pair.Sampler];
}
}
info.Samplers[(int)pair.Sampler].DepthCompare |= image.DepthCompare;
// A guest sampler can be shared by ordinary sampling and depth-reference
// sampling. Vulkan bakes compareEnable into VkSampler, so those uses cannot
// share one host sampler. Split only the mixed cases; compare-only samplers
// can use their existing slot.
var compareUsage = new byte[ShaderResourceInfo.MaxSamplers];
foreach (var pair in info.SampledPairs)
{
var image = info.Images[(int)pair.Image];
compareUsage[pair.Sampler] |= image.DepthCompare ? (byte)2 : (byte)1;
}
var compareSampler = new uint[ShaderResourceInfo.MaxSamplers];
Array.Fill(compareSampler, DescriptorConstants.NoIndex);
for (var index = 0; index < info.Samplers.Count; index++)
{
if ((compareUsage[index] & 2) == 0)
{
continue;
}
if ((compareUsage[index] & 1) == 0)
{
info.Samplers[index].DepthCompare = true;
compareSampler[index] = (uint)index;
continue;
}
if (info.Samplers.Count >= ShaderResourceInfo.MaxSamplers)
{
throw new ResourcePlanException($"shader resource specialization exceeds the sampler limit: hash=0x{plan.Hash:X16}");
}
var sampler = info.Samplers[index].Clone();
sampler.DepthCompare = true;
compareSampler[index] = (uint)info.Samplers.Count;
info.Samplers.Add(sampler);
}
foreach (var pair in info.SampledPairs)
{
if (info.Images[(int)pair.Image].DepthCompare)
{
pair.Sampler = compareSampler[pair.Sampler];
}
}
var samplerByMemory = new Dictionary<int, uint>();
@ -877,9 +1027,25 @@ public static class ResourceMaterializer
}
var image = info.Images[(int)memory.Resource];
if (memory.NeedsSampler && RequiresPointSampler(image.NumericClass, image.ConversionFormat) && memory.Sampler < source.Samplers.Count)
if (!memory.NeedsSampler || memory.Sampler >= source.Samplers.Count)
{
samplerByMemory[index] = samplerPlan.PointSampler[memory.Sampler];
continue;
}
var sampler = memory.Sampler;
if (RequiresPointSampler(image.NumericClass, image.ConversionFormat))
{
sampler = samplerPlan.PointSampler[sampler];
}
if (image.DepthCompare)
{
sampler = compareSampler[sampler];
}
if (sampler != memory.Sampler)
{
samplerByMemory[index] = sampler;
}
}

View File

@ -0,0 +1,177 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
namespace SharpEmu.ShaderCompiler.Resources;
public sealed partial class ResourceTracker
{
private const uint DenseIndirectImageShift = 5;
private const uint MaxDenseIndirectImageEntries = 4096;
private bool TryMakeDenseIndirectImage(ScalarValue handle, uint pc, out IndirectImagePlan plan)
{
plan = null!;
if (handle.Kind != ScalarValueKind.ImageHandle || handle.Operands.Length != 8)
return false;
var reads = new ScalarValue[8];
var memoryIndices = new int[8];
ScalarValue? heapHandle = null;
ScalarValue? based = null;
uint tableImmediate = 0;
var canSuppressMemoryReads = true;
for (var dword = 0; dword < 8; dword++)
{
var read = handle.Operands[dword];
if (read.Kind != ScalarValueKind.ScalarAddressWord || read.MemoryIndex < 0 ||
read.MemoryIndex >= _plan.Memory.Count || !MemoryIndexBelongsTo(read.MemoryIndex, read))
return false;
var memory = _plan.Memory[read.MemoryIndex];
if (memory.Kind != MemoryResourceKind.ScalarAddress || memory.DataBits != 32 || memory.DataDwords != 1)
return false;
var offset = read.Operands[1];
uint extra = 0;
if (based is null)
{
based = offset;
}
else if (!_graph.Equivalent(offset, based))
{
if (offset.Kind != ScalarValueKind.Operation || offset.Operation != ScalarOperation.IAdd32 || offset.Operands.Length != 2)
return false;
ScalarValue inner;
if (offset.Operands[1].IsConstant)
{
extra = offset.Operands[1].ConstantU32;
inner = offset.Operands[0];
}
else if (offset.Operands[0].IsConstant)
{
extra = offset.Operands[0].ConstantU32;
inner = offset.Operands[1];
}
else
{
return false;
}
if (!_graph.Equivalent(inner, based))
return false;
}
var componentOffset = checked((uint)dword * sizeof(uint));
if ((ulong)extra + memory.Offset < componentOffset)
return false;
var immediate = extra + memory.Offset - componentOffset;
if (dword == 0)
tableImmediate = immediate;
else if (immediate != tableImmediate)
return false;
var currentHandle = read.Operands[0];
if (currentHandle.Kind != ScalarValueKind.AddressHandle ||
(heapHandle is not null && !_graph.Equivalent(currentHandle, heapHandle)))
return false;
heapHandle = currentHandle;
reads[dword] = read;
memoryIndices[dword] = read.MemoryIndex;
canSuppressMemoryReads &= HasOnlyImageConsumers(memory, handle);
}
if (based is null || heapHandle is null)
return false;
uint tableOffset = tableImmediate;
var scaled = based;
if (scaled.Kind == ScalarValueKind.Operation && scaled.Operation == ScalarOperation.IAdd32 && scaled.Operands.Length == 2)
{
if (scaled.Operands[1].IsConstant)
{
tableOffset = unchecked(tableOffset + scaled.Operands[1].ConstantU32);
scaled = scaled.Operands[0];
}
else if (scaled.Operands[0].IsConstant)
{
tableOffset = unchecked(tableOffset + scaled.Operands[0].ConstantU32);
scaled = scaled.Operands[1];
}
else
{
return false;
}
}
if (scaled.Kind != ScalarValueKind.Operation || scaled.Operation != ScalarOperation.ShiftLeft32 ||
scaled.Operands.Length != 2 || !scaled.Operands[1].IsConstant ||
scaled.Operands[1].ConstantU32 != DenseIndirectImageShift)
return false;
var key = scaled.Operands[0];
var bound = DenseKeyBound(key);
if (bound == 0)
return false;
foreach (var read in reads)
if (!UsesOnly(read, [handle]))
return false;
if (!MakeRuntimeAddressSource(heapHandle, pc, out var heapSourceIndex, out var heapSource))
return false;
var imageDwords = Enumerable.Repeat(key, 8).ToArray();
imageDwords[0] = heapSource.Dwords[0];
imageDwords[1] = heapSource.Dwords[1];
var imageSource = new DescriptorSource
{
Dwords = imageDwords,
IndirectImage = new IndirectImageSelector(0, heapSourceIndex, 0, 0, 0)
{
Dense = true,
TableOffset = tableOffset,
DynamicOffsetBase = unchecked(tableOffset - tableImmediate),
KeyBound = bound,
},
};
plan = new IndirectImagePlan
{
Handle = handle,
Source = InternSource(imageSource),
Key = reads[0],
KeyIsAddressOffset = true,
HeapSource = heapSourceIndex,
SuppressMemoryReads = canSuppressMemoryReads,
Memory = memoryIndices,
Reads = reads,
};
return true;
}
private uint DenseKeyBound(ScalarValue key)
{
if (key.Kind == ScalarValueKind.Operation && key.Operation == ScalarOperation.FindLowestBit32 &&
_graph.HasNonZeroBitScanInput(key))
return 32;
if (!_uses.TryGetValue(key, out var uses))
return 0;
foreach (var use in uses)
{
if (use.Kind != ScalarValueKind.Operation || use.Operation != ScalarOperation.ULessThan32 || use.Operands.Length != 2 ||
!ReferenceEquals(use.Operands[0], key) || !use.Operands[1].IsConstant)
continue;
var limit = use.Operands[1].ConstantU32;
if (limit is > 0 and <= MaxDenseIndirectImageEntries)
return limit;
}
return 0;
}
}

View File

@ -13,6 +13,7 @@ public sealed partial class ResourceTracker
var reads = handle.Operands;
var memoryIndices = new int[8];
var canSuppressMemoryReads = true;
for (var component = 0; component < reads.Length; component++)
{
var read = reads[component];
@ -22,7 +23,7 @@ public sealed partial class ResourceTracker
var memory = _plan.Memory[read.MemoryIndex];
if (memory.Kind != MemoryResourceKind.ScalarAddress || memory.DataBits != 32 || memory.DataDwords != 1)
return false;
if (!HasOnlyImageConsumers(memory, handle)) return false;
canSuppressMemoryReads &= HasOnlyImageConsumers(memory, handle);
memoryIndices[component] = read.MemoryIndex;
}
@ -86,6 +87,7 @@ public sealed partial class ResourceTracker
Source = InternSource(imageSource),
Key = keyRead,
KeyIsAddressOffset = true,
SuppressMemoryReads = canSuppressMemoryReads,
Memory = memoryIndices,
Reads = reads,
};

View File

@ -29,6 +29,7 @@ public sealed partial class ResourceTracker
public ScalarValue Key = null!;
public uint HeapSource;
public bool KeyIsAddressOffset;
public bool SuppressMemoryReads = true;
public int[] Memory = new int[8];
public ScalarValue[] Reads = new ScalarValue[8];
}
@ -108,14 +109,17 @@ public sealed partial class ResourceTracker
var indirectAccesses = new List<IndirectImageAccess>();
foreach (var plan in _indirectImages)
{
foreach (var index in plan.Memory)
if (plan.SuppressMemoryReads)
{
_plan.Memory[index].PlanningOnly = true;
}
foreach (var index in plan.Memory)
{
_plan.Memory[index].PlanningOnly = true;
}
foreach (var read in plan.Reads)
{
indirectReads.Add(read);
foreach (var read in plan.Reads)
{
indirectReads.Add(read);
}
}
for (var index = 0; index < _plan.Accesses.Length; index++)
@ -578,7 +582,7 @@ public sealed partial class ResourceTracker
// ---- indirect images ----
private bool IsIndirectPlanningMemory(int index) =>
_indirectImages.Any(plan => plan.Memory.Contains(index));
_indirectImages.Any(plan => plan.SuppressMemoryReads && plan.Memory.Contains(index));
private void PlanIndirectImages()
{
@ -591,7 +595,9 @@ public sealed partial class ResourceTracker
continue;
}
if (TryMakeIndirectImage(handle, memory.Pc, out var plan) || TryMakeDirectImage(handle, out plan))
if (TryMakeIndirectImage(handle, memory.Pc, out var plan) ||
TryMakeDenseIndirectImage(handle, memory.Pc, out plan) ||
TryMakeDirectImage(handle, out plan))
{
_indirectImages.Add(plan);
}
@ -635,6 +641,25 @@ public sealed partial class ResourceTracker
return true;
}
private bool MakeRuntimeAddressSource(ScalarValue handle, uint pc, out uint sourceIndex, out DescriptorSource source)
{
sourceIndex = 0;
source = null!;
if (handle.Kind != ScalarValueKind.AddressHandle)
{
return false;
}
source = MakeSource(handle, 2, false, false, pc);
if (!ValidateSource(source, out _))
{
return false;
}
sourceIndex = InternSource(source);
return true;
}
private static bool MatchMaterialOffset(ScalarValue value, out ScalarValue selector, out uint stride, out uint offset)
{
selector = null!;

View File

@ -142,7 +142,7 @@ public sealed record StageOutput(StageOutputKind Kind, uint Index, uint Location
public sealed class ShaderResourceInfo
{
public const int MaxBuffers = 32;
public const int MaxImages = 32;
public const int MaxImages = 64;
public const int MaxSamplers = 32;
public const int MaxSampledPairs = 64;
public const int NoScalarRegister = -1;
@ -185,6 +185,10 @@ public sealed record IndirectImageSelector(
{
public IndirectSelectorValues? SelectorValues { get; init; }
public IReadOnlyList<DirectImageCandidate>? DirectCandidates { get; init; }
public bool Dense { get; init; }
public uint TableOffset { get; init; }
public uint DynamicOffsetBase { get; init; }
public uint KeyBound { get; init; }
}
public sealed record DirectImageCandidate(uint Offset, uint Source);

View File

@ -126,6 +126,10 @@ public sealed class ShaderResourcePlan
foreach (var candidate in directCandidates)
plan.MarkCleanFlatSlots(plan.DescriptorSources[(int)candidate.Source], cleanSlots);
}
else if (indirect.Dense)
{
plan.MarkCleanFlatSlots(plan.DescriptorSources[(int)indirect.HeapSource], cleanSlots);
}
else
{
plan.MarkCleanFlatSlots(plan.DescriptorSources[(int)indirect.MaterialSource], cleanSlots);

View File

@ -11,10 +11,48 @@ namespace SharpEmu.ShaderCompiler;
public sealed record ShaderVertexInput(
uint Pc,
uint Location,
uint FetchComponentCount,
uint ComponentCount,
uint NumberFormat,
uint DestinationSelect,
bool PerInstance,
IReadOnlyList<uint> AliasPcs);
IReadOnlyList<uint> AliasPcs)
{
public ShaderVertexInput(
uint pc,
uint location,
uint componentCount,
uint numberFormat,
bool perInstance,
IReadOnlyList<uint> aliasPcs)
: this(
pc,
location,
componentCount,
componentCount,
numberFormat,
componentCount switch
{
1u => 4u,
2u => 4u | (5u << 3),
3u => 4u | (5u << 3) | (6u << 6),
4u => 4u | (5u << 3) | (6u << 6) | (7u << 9),
_ => 0u,
},
perInstance,
aliasPcs)
{
}
}
public readonly record struct ShaderClipSpaceTransform(
bool Enabled,
float ScaleX,
float ScaleY,
float OffsetX,
float OffsetY,
float HalfExtentX,
float HalfExtentY);
// Everything an emitter needs to compile one permutation of a program: the decoded
// program, its resource plan applied to one specialization, and the binding layout.
@ -99,11 +137,14 @@ public sealed class ShaderCompileRequest
public IReadOnlyList<Gen5PixelOutputBinding> PixelOutputs { get; init; } = [];
public uint PixelInputEnable { get; init; }
public uint PixelCustomInterpolationMask { get; init; }
public uint PixelInputAddress { get; init; }
public IReadOnlyList<uint>? PixelInputCntl { get; init; }
public int RequiredVertexOutputCount { get; init; }
public IReadOnlyList<ShaderVertexInput> VertexInputs { get; init; } = [];
public uint PositionExportControl { get; init; }
public ShaderClipSpaceTransform ClipSpace { get; init; }
public uint LocalSizeX { get; init; } = 1;
public uint LocalSizeY { get; init; } = 1;

View File

@ -0,0 +1,90 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Buffers.Binary;
using SharpEmu.HLE;
using SharpEmu.Libs.Agc;
using SharpEmu.Libs.Gpu.GpuCommands;
using SharpEmu.Libs.Tests.Gpu.GpuCommands;
using Xunit;
namespace SharpEmu.Libs.Tests.Agc;
[Collection(AgcCommandBufferChainCollection.Name)]
public sealed class AgcContextStateTests
{
private const ulong MemoryBase = 0x210000000;
private const ulong CommandBuffer = MemoryBase + 0x40;
private const ulong Commands = MemoryBase + 0x200;
private static uint[] Emit(uint operation, uint capacity, out ulong result, out ulong cursor)
{
var memory = new FakeCpuMemory(MemoryBase, 0x1000);
var context = new CpuContext(memory, Generation.Gen5);
Assert.True(context.TryWriteUInt64(CommandBuffer + 0x10, Commands));
Assert.True(context.TryWriteUInt64(CommandBuffer + 0x18, Commands + capacity * 4));
context[CpuRegister.Rdi] = CommandBuffer;
context[CpuRegister.Rsi] = operation;
AgcExports.DcbContextStateOperation(context);
result = context[CpuRegister.Rax];
Span<byte> cursorBytes = stackalloc byte[8];
Assert.True(memory.TryRead(CommandBuffer + 0x10, cursorBytes));
cursor = BinaryPrimitives.ReadUInt64LittleEndian(cursorBytes);
var bytes = new byte[(int)(cursor - Commands)];
Assert.True(memory.TryRead(Commands, bytes));
return Enumerable.Range(0, bytes.Length / 4)
.Select(index => BinaryPrimitives.ReadUInt32LittleEndian(bytes.AsSpan(index * 4))).ToArray();
}
[Theory]
[InlineData(0u, 5u)]
[InlineData(1u, 27u)]
[InlineData(2u, 27u)]
[InlineData(3u, 32u)]
public void ExportPreservesPacketSizesAndOperation(uint operation, uint expectedDwords)
{
var words = Emit(operation, expectedDwords, out var result, out var cursor);
Assert.Equal(Commands, result);
Assert.Equal(Commands + expectedDwords * 4, cursor);
Assert.Equal(operation, words[1]);
for (var index = 0; index < words.Length;)
{
Assert.Equal(PacketOpcode.Nop, PacketHeader.Opcode(words[index]));
Assert.Equal(index == 0 ? PacketCustomCode.ContextState : 0u, PacketHeader.CustomCode(words[index]));
index += (int)PacketHeader.Length(words[index]);
Assert.InRange(index, 1, words.Length);
}
}
[Fact]
public void ExportedPushClearAndPopRestoreViewportAndTargetMask()
{
var push = Emit(3, 32, out _, out _);
var pop = Emit(2, 27, out _, out _);
var runner = new StreamRunner();
runner.Run(
StreamRunner.Packet(PacketOpcode.SetContextRegister, 0x113, 0x3F800000u, 0u),
StreamRunner.Packet(PacketOpcode.SetContextRegister, 0x8E, 0xFu),
push,
StreamRunner.Packet(PacketOpcode.SetContextRegister, 0x113, 0x3F000000u, 0x3F000000u),
StreamRunner.Packet(PacketOpcode.SetContextRegister, 0x8E, 0u));
Assert.Equal(0.5f, runner.Interpreter.TypedRegisters.Context.ScreenViewport.Viewports[0].ZOffset);
runner.Run(pop);
var restored = runner.Interpreter.TypedRegisters.Context;
Assert.Equal(1f, restored.ScreenViewport.Viewports[0].ZScale);
Assert.Equal(0f, restored.ScreenViewport.Viewports[0].ZOffset);
Assert.Equal(0xFu, restored.RenderTargetMask);
Assert.False(runner.Interpreter.TypedRegisters.ContextPushed);
}
[Theory]
[InlineData(4u, 32u)]
[InlineData(1u, 21u)]
[InlineData(0u, 4u)]
public void InvalidOperationOrInsufficientSpaceReturnsNull(uint operation, uint capacity)
{
Assert.Empty(Emit(operation, capacity, out var result, out var cursor));
Assert.Equal(0UL, result);
Assert.Equal(Commands, cursor);
}
}

View File

@ -58,4 +58,34 @@ public sealed class AgcResourceOwnerTests
Assert.True(memory.TryRead(address, buffer));
return BinaryPrimitives.ReadUInt32LittleEndian(buffer);
}
[Theory]
[InlineData(false)]
[InlineData(true)]
public void UnregisterOwnerUsesTheSharedRegistryAcrossMemoryWrappers(bool nested)
{
var memory = new FakeCpuMemory(BaseAddress, 0x2000);
memory.WriteCString(NameAddress, "Owner");
var registration = new CpuContext(new MemoryWrapper(memory), Generation.Gen5);
registration[CpuRegister.Rdi] = OwnerAddress;
registration[CpuRegister.Rsi] = NameAddress;
Assert.Equal(0, AgcExports.DriverRegisterOwner(registration));
var owner = ReadUInt32(memory, OwnerAddress);
ICpuMemory removalMemory = new MemoryWrapper(memory);
if (nested)
removalMemory = new MemoryWrapper(removalMemory);
var removal = new CpuContext(removalMemory, Generation.Gen5);
removal[CpuRegister.Rdi] = owner;
Assert.Equal(0, AgcExports.DriverUnregisterOwnerAndResources(removal));
registration[CpuRegister.Rdi] = owner;
Assert.Equal((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT,
AgcExports.DriverUnregisterOwnerAndResources(registration));
}
private sealed class MemoryWrapper(ICpuMemory inner) : ICpuMemory, ICpuMemoryWrapper
{
public ICpuMemory Inner => inner;
public bool TryRead(ulong address, Span<byte> destination) => inner.TryRead(address, destination);
public bool TryWrite(ulong address, ReadOnlySpan<byte> source) => inner.TryWrite(address, source);
}
}

View File

@ -44,6 +44,35 @@ public sealed class Gen5ShaderAtomicDecodeTests
Assert.Equal(1u, control.VectorData);
}
[Theory]
[InlineData(0xE0FC4000u, "BufferAtomicFmin")]
[InlineData(0xE1004000u, "BufferAtomicFmax")]
public void BufferAtomicFloatMinMax_Decode(uint word, string opcode)
{
var instruction = DecodeSingle(word, 0x80000100);
Assert.Equal(opcode, instruction.Opcode);
var control = Assert.IsType<Gen5BufferMemoryControl>(instruction.Control);
Assert.Equal(1u, control.DwordCount);
Assert.Equal(1u, control.VectorData);
Assert.Equal(0u, control.ScalarResource);
}
[Fact]
public void BufferAtomicOrX2_UsesTwoDataRegisters()
{
// BUFFER_ATOMIC_OR_X2 v[1:2], off, s[0:3], 128 glc
var instruction = DecodeSingle(0xE1684000, 0x80000100);
Assert.Equal("BufferAtomicOrX2", instruction.Opcode);
var control = Assert.IsType<Gen5BufferMemoryControl>(instruction.Control);
Assert.Equal(2u, control.DwordCount);
Assert.Equal(1u, control.VectorData);
Assert.Equal(
new[] { Gen5Operand.Vector(1), Gen5Operand.Vector(2) },
instruction.Destinations);
}
[Fact]
public void ImageAtomicAdd_KeepsDataRegisterAsDestination()
{
@ -84,6 +113,21 @@ public sealed class Gen5ShaderAtomicDecodeTests
Assert.Equal(new[] { Gen5Operand.Vector(3) }, instruction.Destinations);
}
[Theory]
[InlineData(0xD8480004u, "DsMinF32")]
[InlineData(0xD84C0008u, "DsMaxF32")]
public void DsFloatMinMax_KeepReplacementAndCompareOperands(uint word, string opcode)
{
// DATA0 is the replacement value and DATA1 is the float compare operand.
var instruction = DecodeSingle(word, 0x00010907);
Assert.Equal(opcode, instruction.Opcode);
Assert.Equal(
new[] { Gen5Operand.Vector(7), Gen5Operand.Vector(9), Gen5Operand.Vector(1) },
instruction.Sources);
Assert.Empty(instruction.Destinations);
}
[Fact]
public void DsCmpstRtnB32_OrdersComparatorBeforeNewValue()
{
@ -111,6 +155,19 @@ public sealed class Gen5ShaderAtomicDecodeTests
Assert.Equal(0x0808u, control.SingleOffsetBytes);
}
[Fact]
public void DsReadI8_DecodesAddressAndDestination()
{
// DS_READ_I8 v5, v7 offset:3
var instruction = DecodeSingle(0xD8E40003, 0x05000007);
Assert.Equal("DsReadI8", instruction.Opcode);
Assert.Equal(new[] { Gen5Operand.Vector(7) }, instruction.Sources);
Assert.Equal(new[] { Gen5Operand.Vector(5) }, instruction.Destinations);
var control = Assert.IsType<Gen5DataShareControl>(instruction.Control);
Assert.Equal(3u, control.SingleOffsetBytes);
}
[Theory]
[InlineData(0xD8FA3412u, "DsAppend")]
[InlineData(0xD8F63412u, "DsConsume")]
@ -128,6 +185,20 @@ public sealed class Gen5ShaderAtomicDecodeTests
Assert.False(control.Gds);
}
[Fact]
public void VCmpxNeU64_DecodesVectorRegisterPairs()
{
// V_CMPX_NE_U64 v[0:1], v[3:4]. The translator consumes each encoded
// source as the low register of a 64-bit pair and updates EXEC.
var instruction = DecodeSingle(0x7DEA0700);
Assert.Equal("VCmpxNeU64", instruction.Opcode);
Assert.Equal(
new[] { Gen5Operand.Vector(0), Gen5Operand.Vector(3) },
instruction.Sources);
Assert.Empty(instruction.Destinations);
}
private static Gen5ShaderInstruction DecodeSingle(params uint[] words)
{
var memory = new FakeCpuMemory(ShaderAddress, 0x1000);

View File

@ -35,6 +35,30 @@ public sealed class Gen5SpirvAtomicTranslationTests
Assert.Contains((ushort)SpirvOp.AtomicIIncrement, opcodes);
}
[Theory]
[InlineData(0xE0FC4000u, SpirvOp.FOrdLessThan)]
[InlineData(0xE1004000u, SpirvOp.FOrdGreaterThan)]
public void BufferFloatAtomics_EmitCompareExchangeLoop(uint word, SpirvOp compareOp)
{
var opcodes = CompileComputeOpcodes(
[word, 0x80000100],
BufferDescriptorRegisters());
Assert.Contains((ushort)SpirvOp.AtomicLoad, opcodes);
Assert.Contains((ushort)SpirvOp.AtomicCompareExchange, opcodes);
Assert.Contains((ushort)compareOp, opcodes);
}
[Fact]
public void BufferAtomicOrX2_EmitsAtomicOr()
{
var opcodes = CompileComputeOpcodes(
[0xE1684000, 0x80000100],
BufferDescriptorRegisters());
Assert.Contains((ushort)SpirvOp.AtomicOr, opcodes);
}
[Fact]
public void DataShareAtomics_EmitAtomicOpcodes()
{

View File

@ -1,6 +1,9 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Buffers.Binary;
using SharpEmu.HLE;
using SharpEmu.Libs.Ampr;
using SharpEmu.Libs.Kernel;
using Xunit;
@ -14,4 +17,75 @@ public sealed class AmprCompletionEventTests
Assert.Equal(-25, KernelEventQueueCompatExports.KernelEventFilterAmpr);
Assert.Equal(-30, KernelEventQueueCompatExports.KernelEventFilterAmprSystem);
}
[Theory]
[InlineData(false, false)]
[InlineData(true, false)]
[InlineData(false, true)]
[InlineData(true, true)]
public void CompletionUsesAmprRegistrationAndPreservesPayload(bool completionCommand, bool userRegistrationOnly)
{
const ulong memoryBase = 0x100000000;
const ulong commandBuffer = memoryBase + 0x100;
const ulong eventIdentifier = 29954;
const ulong eventData = 0x10000000000003E8;
const ulong registeredUserData = 0x12345678;
var memory = new FakeCpuMemory(memoryBase, 0x4000);
var context = new CpuContext(memory, Generation.Gen5);
context[CpuRegister.Rdi] = memoryBase;
Assert.Equal(0, KernelEventQueueCompatExports.KernelCreateEqueue(context));
Assert.True(context.TryReadUInt64(memoryBase, out var queue));
try
{
context[CpuRegister.Rdi] = queue;
context[CpuRegister.Rsi] = eventIdentifier;
Assert.Equal(0, KernelEventQueueCompatExports.KernelAddUserEvent(context));
if (!userRegistrationOnly)
{
context[CpuRegister.Rdx] = registeredUserData;
Assert.Equal(0, KernelEventQueueCompatExports.KernelAddAmprEvent(context));
}
context[CpuRegister.Rdi] = commandBuffer;
context[CpuRegister.Rsi] = memoryBase + 0x200;
context[CpuRegister.Rdx] = 0x100;
Assert.Equal(0, AmprExports.CommandBufferConstructor(context));
Assert.Equal(0, AmprExports.CommandBufferSetBuffer(context));
context[CpuRegister.Rsi] = queue;
context[CpuRegister.Rdx] = eventIdentifier;
context[CpuRegister.Rcx] = eventData;
Assert.Equal(0, completionCommand
? AmprExports.CommandBufferWriteKernelEventQueueOnCompletion(context)
: AmprExports.CommandBufferWriteKernelEventQueue0400(context));
Assert.Equal(0, AmprExports.CompleteCommandBuffer(context, commandBuffer, out var result, out var errorOffset));
Assert.Equal(userRegistrationOnly ? (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND : 0, result);
Assert.Equal(0u, errorOffset);
context[CpuRegister.Rdi] = queue;
context[CpuRegister.Rsi] = memoryBase + 0x400;
context[CpuRegister.Rdx] = 1;
context[CpuRegister.Rcx] = memoryBase + 0x500;
context[CpuRegister.R8] = memoryBase + 0x600;
var waitResult = KernelEventQueueCompatExports.KernelWaitEqueue(context);
if (userRegistrationOnly)
{
Assert.Equal((int)OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT, waitResult);
return;
}
Assert.Equal(0, waitResult);
Span<byte> eventBytes = stackalloc byte[32];
Assert.True(memory.TryRead(memoryBase + 0x400, eventBytes));
Assert.Equal(eventIdentifier, BinaryPrimitives.ReadUInt64LittleEndian(eventBytes));
Assert.Equal((short)-25,
BinaryPrimitives.ReadInt16LittleEndian(eventBytes[8..]));
Assert.Equal(eventData, BinaryPrimitives.ReadUInt64LittleEndian(eventBytes[16..]));
Assert.Equal(registeredUserData, BinaryPrimitives.ReadUInt64LittleEndian(eventBytes[24..]));
}
finally
{
context[CpuRegister.Rdi] = queue;
Assert.Equal(0, KernelEventQueueCompatExports.KernelDeleteEqueue(context));
}
}
}

View File

@ -11,6 +11,34 @@ namespace SharpEmu.Libs.Tests.Ampr;
[Collection("AmprFileRegistry")]
public class AmprFileRegistryTests
{
[Theory]
[InlineData(false)]
[InlineData(true)]
public void ResolveCollidingPaths_PreservesBothHandles(bool reverseOrder)
{
AmprFileRegistry.ClearForTests();
const string firstPath = "$/assets/bce5a816.bin";
const string secondPath = "$/assets/1b9e7058.bin";
Assert.Equal(AmprFileRegistry.ComputeFileId(firstPath), AmprFileRegistry.ComputeFileId(secondPath));
var firstHost = Path.Combine(Path.GetTempPath(), "apr-first.bin");
var secondHost = Path.Combine(Path.GetTempPath(), "apr-second.bin");
if (reverseOrder)
AmprFileRegistry.Register(secondPath, secondHost);
var firstId = AmprFileRegistry.Register(firstPath, firstHost);
var secondId = AmprFileRegistry.Register(secondPath, secondHost);
Assert.NotEqual(firstId, secondId);
Assert.NotEqual(uint.MaxValue, firstId);
Assert.True(AmprFileRegistry.TryGetHostPath(firstId, out var firstResult));
Assert.True(AmprFileRegistry.TryGetHostPath(secondId, out var secondResult));
Assert.Equal(firstHost, firstResult);
Assert.Equal(secondHost, secondResult);
Assert.Equal(firstId, AmprFileRegistry.Register(firstPath, firstHost));
Assert.False(AmprFileRegistry.TryGetHostPath(AmprFileRegistry.ComputeFileId(firstPath), out _));
AmprFileRegistry.RegisterApp0RelativeForTests("assets/1b9e7058.bin", secondHost);
Assert.True(AmprFileRegistry.TryGetHostPath(firstId, out firstResult));
Assert.Equal(firstHost, firstResult);
}
[Fact]
public void ComputeFileId_matches_utf8_fnv1a()
{
@ -149,6 +177,6 @@ public class AmprFileRegistryTests
hash *= prime;
}
return hash;
return hash & 0x7fffffff;
}
}

View File

@ -49,6 +49,7 @@ public sealed class AmprWriteAddressTests
context[CpuRegister.Rdx] = 0x100;
Assert.Equal(0, AmprExports.CommandBufferConstructor(context));
Assert.Equal(0, AmprExports.CommandBufferSetBuffer(context));
context[CpuRegister.Rdi] = commandBufferAddress;
context[CpuRegister.Rsi] = watcherAddress;

View File

@ -12,6 +12,8 @@ namespace SharpEmu.Libs.Tests.Ampr;
[Collection("AmprFileRegistry")]
public sealed class AprStreamingContractTests
{
public AprStreamingContractTests() => AmprFileRegistry.ClearForTests();
[Fact]
public void ResolveStatAndReadFile_UsesSharedAprFileId()
{
@ -71,6 +73,7 @@ public sealed class AprStreamingContractTests
context[CpuRegister.Rdx] = 0x100;
Assert.Equal(0, AmprExports.CommandBufferConstructor(context));
Assert.Equal(0, AmprExports.CommandBufferSetBuffer(context));
const ulong readOffset = 2;
const ulong readSize = 4;
@ -85,16 +88,10 @@ public sealed class AprStreamingContractTests
Span<byte> destination = stackalloc byte[(int)readSize];
Assert.True(memory.TryRead(destinationAddress, destination));
Assert.Equal(new byte[(int)readSize], destination.ToArray());
Assert.Equal(0, AmprExports.CompleteCommandBuffer(context, commandBufferAddress));
Assert.True(memory.TryRead(destinationAddress, destination));
Assert.Equal(fileContents.AsSpan((int)readOffset, (int)readSize), destination);
Span<byte> record = stackalloc byte[0x30];
Assert.True(memory.TryRead(recordBufferAddress, record));
Assert.Equal(1U, BinaryPrimitives.ReadUInt32LittleEndian(record));
Assert.Equal(fileId, BinaryPrimitives.ReadUInt32LittleEndian(record[0x04..]));
Assert.Equal(destinationAddress, BinaryPrimitives.ReadUInt64LittleEndian(record[0x08..]));
Assert.Equal(readSize, BinaryPrimitives.ReadUInt64LittleEndian(record[0x10..]));
Assert.Equal(readOffset, BinaryPrimitives.ReadUInt64LittleEndian(record[0x18..]));
Assert.Equal(readSize, BinaryPrimitives.ReadUInt64LittleEndian(record[0x20..]));
}
finally
{
@ -106,6 +103,65 @@ public sealed class AprStreamingContractTests
}
}
[Fact]
public void ReadFile_UsesCapturedNativeImportStackArgument()
{
const ulong memoryBase = 0x1_0000_0000;
const ulong commandBufferAddress = memoryBase + 0x100;
const ulong recordBufferAddress = memoryBase + 0x200;
const ulong destinationAddress = memoryBase + 0x400;
const ulong fileOffset = 0x1234;
var memory = new FakeCpuMemory(memoryBase, 0x1000);
var context = new CpuContext(memory, Generation.Gen5);
context[CpuRegister.Rdi] = commandBufferAddress;
Assert.Equal(0, AmprExports.CommandBufferConstructor(context));
context[CpuRegister.Rdi] = commandBufferAddress;
context[CpuRegister.Rsi] = recordBufferAddress;
context[CpuRegister.Rdx] = 0x100;
Assert.Equal(0, AmprExports.CommandBufferSetBuffer(context));
context[CpuRegister.Rsp] = 0x7FFF_FFFF_F000;
context.SetImportStackArguments(fileOffset, 0, 0, 0, 0, 0);
context[CpuRegister.Rdi] = commandBufferAddress;
context[CpuRegister.Rcx] = 0x8000_0001;
context[CpuRegister.R8] = destinationAddress;
context[CpuRegister.R9] = 0x20;
Assert.Equal(0, AmprExports.AprCommandBufferReadFile(context));
Assert.Equal(0x14u, ReadUInt32(memory, commandBufferAddress + 4));
}
[Fact]
public void ReadFile_SequentialOffsetSentinel_DoesNotFaultDuringAppend()
{
const ulong memoryBase = 0x1_0000_0000;
const ulong commandBufferAddress = memoryBase + 0x100;
const ulong recordBufferAddress = memoryBase + 0x200;
const ulong destinationAddress = memoryBase + 0x400;
var memory = new FakeCpuMemory(memoryBase, 0x1000);
var context = new CpuContext(memory, Generation.Gen5);
context[CpuRegister.Rdi] = commandBufferAddress;
Assert.Equal(0, AmprExports.CommandBufferConstructor(context));
context[CpuRegister.Rdi] = commandBufferAddress;
context[CpuRegister.Rsi] = recordBufferAddress;
context[CpuRegister.Rdx] = 0x100;
Assert.Equal(0, AmprExports.CommandBufferSetBuffer(context));
context[CpuRegister.Rsp] = 0x7FFF_FFFF_F000;
context.SetImportStackArguments(ulong.MaxValue, 0, 0, 0, 0, 0);
context[CpuRegister.Rdi] = commandBufferAddress;
context[CpuRegister.Rcx] = 0x8000_0001;
context[CpuRegister.R8] = destinationAddress;
context[CpuRegister.R9] = 0x20;
Assert.Equal(0, AmprExports.AprCommandBufferReadFile(context));
Assert.Equal(0x18u, ReadUInt32(memory, commandBufferAddress + 4));
}
[Fact]
public void ResolveFilepathsWithPrefixToIdsAndFileSizes_CombinesPrefixAndResolvesRealFile()
{

View File

@ -14,6 +14,7 @@ public sealed class MemcpyHleRoutingTests
{
private const string MemcpyNid = "Q3VBxCXhUHs";
private const string MemsetNid = "QrZZdJ8XsX0";
private const string StrcasecmpNid = "AV6ipCNa4Rw";
private const string RdtscNid = "-2IRUCO--PM";
[Fact]
@ -34,6 +35,14 @@ public sealed class MemcpyHleRoutingTests
$"memset ({MemsetNid}) must route through HLE on every platform.");
}
[Fact]
public void IsHlePreferredNid_PrefersHleForStrcasecmp()
{
Assert.True(
InvokeIsHlePreferredNid(StrcasecmpNid),
$"strcasecmp ({StrcasecmpNid}) must route through HLE so null-argument recovery remains active.");
}
[Fact]
public void TryCreateNativeImportIntrinsic_DoesNotClaimMemcpy()
{
@ -52,6 +61,22 @@ public sealed class MemcpyHleRoutingTests
Assert.Equal(0, address);
}
[Fact]
public void TryCreateNativeImportIntrinsic_DoesNotClaimStrcasecmp()
{
if (RuntimeInformation.ProcessArchitecture != Architecture.X64)
{
return;
}
var claimed = InvokeTryCreateNativeImportIntrinsic(StrcasecmpNid, out var address);
Assert.False(
claimed,
$"strcasecmp ({StrcasecmpNid}) must fall through to the HLE trampoline so a null guest pointer does not fault in the intrinsic stub.");
Assert.Equal(0, address);
}
[Fact]
public void TryCreateNativeImportIntrinsic_StillClaimsNonHleNids()
{

View File

@ -168,7 +168,7 @@ internal sealed class RecordingCommandStreamHost : ICommandStreamHost
Calls.Add($"draw_auto {submitId} {arguments.VertexCount}");
}
public void DispatchDirect(ulong submitId, uint groupsX, uint groupsY, uint groupsZ, uint dispatchInitiator) =>
public void DispatchDirect(ulong submitId, uint groupsX, uint groupsY, uint groupsZ, uint dispatchInitiator, ulong indirectArgumentsAddress = 0) =>
Calls.Add($"dispatch {submitId} {groupsX} {groupsY} {groupsZ} {dispatchInitiator:X}");
public void OnQueueReset(int queueId) => Calls.Add($"queue_reset {queueId}");

View File

@ -67,7 +67,11 @@ public sealed class CommandStreamWorkerTests
var (host, queue, worker) = NewWorker(cancelBlockedAtStop: true);
host.WriteDword(Label, 0);
Enqueue(host, queue, 1, WaitEqual(Label, 1), CreateInstanceCountPacket(6));
var waiter = Task.Run(queue.WaitForIdle);
var waiter = Task.Factory.StartNew(
queue.WaitForIdle,
CancellationToken.None,
TaskCreationOptions.LongRunning,
TaskScheduler.Default);
Assert.Equal(IdleOutcome.Cancelled, worker.Stop());
Assert.Equal(IdleOutcome.Cancelled, await waiter.WaitAsync(TimeSpan.FromSeconds(2)));

View File

@ -151,6 +151,48 @@ public sealed class GpuCommandInterpreterMemoryTests
Assert.Single(runner.Host.Calls));
}
[Fact]
public void CopyDataBuilder_EncodesGuestArguments()
{
var runner = new StreamRunner();
var context = new CpuContext(runner.Host.Memory, Generation.Gen5);
const ulong commandBufferAddress = StreamRunner.TableAddress;
const ulong stackAddress = StreamRunner.TableAddress + 0x100;
const ulong immediateValue = 0x1122_3344_5566_7788;
runner.Host.WriteQword(commandBufferAddress + 0x10, StreamRunner.CommandAddress);
runner.Host.WriteQword(commandBufferAddress + 0x18, StreamRunner.CommandAddress + 0x100);
context[CpuRegister.Rdi] = commandBufferAddress;
context[CpuRegister.Rsi] = 2;
context[CpuRegister.Rdx] = 3;
context[CpuRegister.Rcx] = Label;
context[CpuRegister.R8] = 10;
context[CpuRegister.R9] = 2;
context[CpuRegister.Rsp] = stackAddress;
runner.Host.WriteQword(stackAddress + 8, immediateValue);
runner.Host.WriteQword(stackAddress + 16, 1);
runner.Host.WriteQword(stackAddress + 24, 1);
AgcExports.DcbCopyData(context);
var expectedControl =
((10u >> 1) & 0xFu) |
(((2u >> 1) & 0xFu) << 8) |
((2u & 0x3u) << 13) |
(1u << 16) |
(1u << 20) |
((3u & 0x3u) << 25) |
((10u & 0x1u) << 30);
Assert.Equal(StreamRunner.CommandAddress, context[CpuRegister.Rax]);
Assert.Equal(StreamRunner.CommandAddress + 24, runner.Host.ReadQword(commandBufferAddress + 0x10));
Assert.Equal(PacketHeader.Make(6, PacketOpcode.CopyData), runner.Host.ReadDword(StreamRunner.CommandAddress));
Assert.Equal(expectedControl, runner.Host.ReadDword(StreamRunner.CommandAddress + 4));
Assert.Equal(immediateValue, runner.Host.ReadQword(StreamRunner.CommandAddress + 8));
Assert.Equal(Label, runner.Host.ReadQword(StreamRunner.CommandAddress + 16));
runner.Interpreter.Process(new PacketCursorStack(), StreamRunner.CommandAddress, 6);
Assert.Equal(immediateValue, runner.Host.ReadQword(Label));
}
[Fact]
public void CopyData_MapsSelectorsToTransfers()
{

View File

@ -32,6 +32,27 @@ public sealed class ImageRequestBuildersTests : IClassFixture<HeadlessVulkanFixt
}
private static readonly ShaderImageShape Sampled2D = new(false, false, false, false, TextureNumericClass.Float);
[Theory]
[InlineData(GuestPixelFormat.Bits16UNorm, Format.D16Unorm, Format.R16Unorm)]
[InlineData(GuestPixelFormat.Bits32Float, Format.D32Sfloat, Format.R32Sfloat)]
public void ComparisonTexture_SelectsDepthWithoutChangingTheGuestLayout(
GuestPixelFormat guestFormat, Format depthFormat, Format colorFormat)
{
var words = RegisterWords.Texture(Base, guestFormat, 48, 24);
var ordinary = ImageRequestBuilders.Texture(words, Sampled2D);
var comparison = ImageRequestBuilders.Texture(words, Sampled2D with { DepthCompare = true });
Assert.Equal(colorFormat, ordinary.Request.Description.PixelFormat);
Assert.False(ordinary.Request.Description.IsDepth);
Assert.Equal(depthFormat, comparison.Request.Description.PixelFormat);
Assert.Equal(depthFormat, comparison.Request.View.Format);
Assert.True(comparison.Request.Description.IsDepth);
Assert.Equal(ordinary.Request.Description.Data, comparison.Request.Description.Data);
Assert.Equal(ordinary.Request.Description.Pitch, comparison.Request.Description.Pitch);
Assert.Equal(ordinary.Request.Description.MipLayout[0], comparison.Request.Description.MipLayout[0]);
Assert.Equal(ImageRole.Texture, comparison.Request.Role);
}
private readonly HeadlessVulkan? _vulkan;
public ImageRequestBuildersTests(HeadlessVulkanFixture fixture) => _vulkan = fixture.Vulkan;

View File

@ -65,7 +65,7 @@ public sealed class GraphicsRejectionPolicyTests
[InlineData(true, false)]
[InlineData(false, true)]
[InlineData(true, true)]
public void RejectedPixelCandidatesSkipOnlyWhenStrictModeIsOff(bool strict, bool exceedCapacity)
public void PixelCandidateValidationHonorsCapacityAndStrictMode(bool strict, bool distinctCandidates)
{
using var fatal = new FatalScope();
var guest = new PipelineTestGuest();
@ -74,11 +74,19 @@ public sealed class GraphicsRejectionPolicyTests
0xF0000108, 0x00010400, 0xBF810000]);
for (var index = 0; index < 33; index++)
guest.WriteWords(TableAddress + 312 + (ulong)index * 32,
exceedCapacity ? 0x1000u + (uint)index : 0x1000u, 20u << 20, 0,
0xFACu | ((!exceedCapacity && index == 1 ? 10u : 9u) << 28), 0, 0, 0, 0);
distinctCandidates ? 0x1000u + (uint)index : 0x1000u, 20u << 20, 0,
0xFACu | ((!distinctCandidates && index == 1 ? 10u : 9u) << 28), 0, 0, 0, 0);
var source = guest.Source(PixelAddress, ShaderStage.Pixel, [(uint)(TableAddress & uint.MaxValue), (uint)(TableAddress >> 32)]);
var options = new StageCompileOptions { PixelInfo = new PixelInputInfo() };
var cursor = 7u;
if (distinctCandidates)
{
Assert.True(guest.Programs.TryGetProgram(source, options, strict, ref cursor, out var program, out _, out var rejection), rejection);
Assert.True(program.IsValid);
Assert.Single(guest.Compiler.Requests);
Assert.Single(guest.Host.Modules);
return;
}
if (strict)
{
var failure = Assert.Throws<SchedulerFatalException>(() => guest.Programs.TryGetProgram(

View File

@ -29,6 +29,9 @@ public sealed class VertexInputResolverTests : IDisposable
private const uint BufferFormat32x2Float = 64;
private const uint BufferFormat32x4Float = 77;
private const uint IdentitySelect = 4 | (5 << 3) | (6 << 6) | (7 << 9);
private const uint X001Select = 4 | (0 << 3) | (0 << 6) | (1 << 9);
private const uint XY01Select = 4 | (5 << 3) | (0 << 6) | (1 << 9);
private const uint XYZ1Select = 4 | (5 << 3) | (6 << 6) | (1 << 9);
private readonly FatalScope _fatal = new();
private readonly FakeCpuMemory _memory = new(MemoryBase, 0x1_0000);
@ -202,7 +205,7 @@ public sealed class VertexInputResolverTests : IDisposable
Assert.Equal(16u, resource.Descriptor.Stride);
Assert.Equal(3u, resource.Descriptor.RecordCount);
Assert.Equal(BufferFormat32x2Float, resource.Descriptor.Format);
Assert.Equal(IdentitySelect, resource.Descriptor.DestinationSelectXYZW);
Assert.Equal(XY01Select, resource.Descriptor.DestinationSelectXYZW);
Assert.Equal(-1, resource.BufferIndex);
}
@ -241,15 +244,20 @@ public sealed class VertexInputResolverTests : IDisposable
Assert.Equal(BufferFormat32x2Float, resources[1].Descriptor.Format);
}
[Fact]
public void ApplySemantics_UnknownAttributeFormat_IsKeptAsTheBufferFormatValue()
[Theory]
[InlineData(88u, 22u, X001Select)]
[InlineData(257u, 64u, XY01Select)]
[InlineData(298u, 74u, XYZ1Select)]
[InlineData(311u, 77u, IdentitySelect)]
public void ApplySemantics_DecodesPackedFormatsWithChannelSwizzle(uint attributeFormat, uint bufferFormat, uint destinationSelect)
{
WriteWord(AttributeTable, AttributeWord(0, 64, 0));
WriteWord(AttributeTable, AttributeWord(0, attributeFormat, 0));
WriteBuffer(0, StreamBase, 8, 1, BufferFormat32x4Float);
var resource = Assert.Single(VertexInputResolver.ApplySemantics(_context, [SemanticWord(0, 0, 2)], AttributeTable, BufferTable, ShaderAddress));
Assert.Equal(64u, resource.Descriptor.Format);
Assert.Equal(bufferFormat, resource.Descriptor.Format);
Assert.Equal(destinationSelect, resource.Descriptor.DestinationSelectXYZW);
}
[Theory]
@ -346,13 +354,4 @@ public sealed class VertexInputResolverTests : IDisposable
Assert.Contains("records=4/5", fatal.Message);
}
[Fact]
public void AttributeFormats_MapToTheBufferFormatsOfTheTable()
{
Assert.Equal(0u, VertexAttributeFormat.ToBufferFormat(0));
Assert.Equal(1u, VertexAttributeFormat.ToBufferFormat(4));
Assert.Equal(BufferFormat32x2Float, VertexAttributeFormat.ToBufferFormat(AttributeFormat32x2Float));
Assert.Equal(BufferFormat32x4Float, VertexAttributeFormat.ToBufferFormat(311));
Assert.Equal(999u, VertexAttributeFormat.ToBufferFormat(999));
}
}

View File

@ -42,7 +42,7 @@ public sealed class RenderExecutorStateTests : IDisposable
var state = DrawAndTakeState(banks);
Assert.Equal((0f, 64f, 64f, -64f), (state.ViewportX, state.ViewportY, state.ViewportWidth, state.ViewportHeight));
Assert.Equal((0.5f, 1f), (state.ViewportMinDepth, state.ViewportMaxDepth));
Assert.Equal((0f, 1f), (state.ViewportMinDepth, state.ViewportMaxDepth));
Assert.Equal(new ScissorRectangle(0, 0, 48, 40), state.Scissor);
Assert.Equal(1f, state.LineWidth);
Assert.Equal((0.1f, 0.2f, 0.3f, 0.4f), (state.BlendRed, state.BlendGreen, state.BlendBlue, state.BlendAlpha));
@ -54,6 +54,37 @@ public sealed class RenderExecutorStateTests : IDisposable
Assert.Equal((1u, (byte)1), (state.ColorWriteCount, state.ColorWriteEnableMask));
}
[Theory]
[InlineData(false, 0.5f, 0.5f, 0f, 1f)]
[InlineData(true, 0.5f, 0.5f, 0.5f, 1f)]
[InlineData(false, -0.5f, 0.5f, 1f, 0f)]
[InlineData(true, -1f, 1f, 1f, 0f)]
[InlineData(false, 0.25f, 0.5f, 0.25f, 0.75f)]
[InlineData(true, 0.25f, 0.5f, 0.5f, 0.75f)]
[InlineData(false, 0f, 0.5f, 0.5f, 0.5f)]
[InlineData(true, 1f, 0f, 0f, 1f)]
public void DynamicState_DepthRangePreservesTheGuestTransform(
bool directXClipSpace, float depthScale, float depthOffset, float expectedMinimum, float expectedMaximum)
{
var banks = Banks();
banks.Context.Clip = ClipControlRegisters.Decode(directXClipSpace ? 1u << 19 : 0u);
banks.Context.ScreenViewport.Viewports[0].ZScale = depthScale;
banks.Context.ScreenViewport.Viewports[0].ZOffset = depthOffset;
var state = DrawAndTakeState(banks);
Assert.Equal(expectedMinimum, state.ViewportMinDepth);
Assert.Equal(expectedMaximum, state.ViewportMaxDepth);
var hostScale = directXClipSpace
? state.ViewportMaxDepth - state.ViewportMinDepth
: (state.ViewportMaxDepth - state.ViewportMinDepth) / 2f;
var hostOffset = directXClipSpace
? state.ViewportMinDepth
: (state.ViewportMaxDepth + state.ViewportMinDepth) / 2f;
Assert.Equal(depthScale, hostScale);
Assert.Equal(depthOffset, hostOffset);
}
[Fact]
public void DynamicState_ClipDisableUsesTheDeviceViewportLimit()
{

View File

@ -324,6 +324,8 @@ internal sealed class RecordingRenderHost : IRenderHost
public void Dispatch(uint groupsX, uint groupsY, uint groupsZ) => Calls.Add($"dispatch {groupsX} {groupsY} {groupsZ}");
public bool TryDispatchIndirect(ulong argumentsAddress) => false;
public void ShaderWriteBarrier(PipelineStageFlags sourceStages) => Calls.Add($"write_barrier {sourceStages}");
public void ShaderWriteHazardBarrier() => Calls.Add("write_hazard_barrier");
@ -335,7 +337,8 @@ internal sealed class RecordingRenderHost : IRenderHost
public List<(RegisterBanks Banks, GraphicsPrograms Programs, TargetlessDrawArguments Arguments)> RetainedDraws { get; } = new();
public void ClearColorTargets(ReadOnlySpan<ColorTargetState> targets, SolidColorClear clear) =>
Calls.Add($"clear_targets {string.Join(",", targets.ToArray().Select(t => t.Slot))} {clear.Red},{clear.Green},{clear.Blue},{clear.Alpha}");
Calls.Add(FormattableString.Invariant(
$"clear_targets {string.Join(",", targets.ToArray().Select(t => t.Slot))} {clear.Red},{clear.Green},{clear.Blue},{clear.Alpha}"));
public bool TryRetainTargetlessDraw(RegisterBanks banks, GraphicsPrograms programs, in TargetlessDrawArguments arguments)
@ -355,6 +358,9 @@ internal sealed class RecordingRenderHost : IRenderHost
public void ResolveImage(ResourceSlotIdentifier source, uint sourceMip, uint sourceLayer, ResourceSlotIdentifier destination, uint destinationMip, uint destinationLayer) =>
Calls.Add($"resolve {source.Index}:{sourceMip}:{sourceLayer} -> {destination.Index}:{destinationMip}:{destinationLayer}");
public void CopyDepthStencilImage(ResourceSlotIdentifier source, ResourceSlotIdentifier destination, in SubresourceRange range, in Extent3D extent, ImageAspectFlags aspects) =>
Calls.Add($"copy_depth_stencil {source.Index} -> {destination.Index} {aspects}");
public bool IsMetadata(ulong address)
{
Calls.Add($"is_metadata {address:X}");

View File

@ -208,6 +208,38 @@ public sealed class GlobalMemoryShaderTests(HeadlessVulkanFixture fixture, ITest
private const uint BoundDescriptor = (Format32x4Float << 12) | IdentitySwizzle;
private const float Half = 0.5f;
public static IEnumerable<object[]> DescriptorStoreCases()
{
foreach (var testCase in TypedStoreCases())
{
var format = (uint)testCase[0];
testCase[1] = (uint)testCase[1] == 0 ? 0u : (format << 12) | IdentitySwizzle;
yield return testCase;
}
yield return [50u, (50u << 12) | IdentitySwizzle, 7u, (uint)MemoryOffset,
Floats(1f, Half, 0f, 1f), Bytes(0x03, 0x08, 0xC0, 0xFF)];
}
[Theory]
[MemberData(nameof(DescriptorStoreCases))]
public void FormattedBufferStores_UseDescriptorConversionWithoutChangingNeighborBytes(
uint format, uint descriptorWord3, uint opcode, uint offset, uint[] sources, byte[]? expectedBytes)
{
var vulkan = fixture.Vulkan;
if (!GatePrerequisites.Ready(vulkan, shaderInt64: true)) return;
var initial = CreateInput();
for (var index = 0; index < sources.Length; index++)
WriteWord(initial, index * 4, sources[index]);
var expected = (byte[])initial.Clone();
initial.AsSpan(0, 64).CopyTo(expected.AsSpan(RegisterOutputOffset, 64));
expectedBytes?.CopyTo(expected.AsSpan((int)offset));
var shader = CompileTypedShader(format, descriptorWord3, offset, opcode, SourceRegister, typed: false);
Assert.Equal(expected, RunOnce(vulkan, shader, initial));
}
public static IEnumerable<object[]> TypedStoreCases()
{
// instruction format, descriptor word 3, opcode (4 = X .. 7 = XYZW), byte offset,
@ -424,7 +456,7 @@ public sealed class GlobalMemoryShaderTests(HeadlessVulkanFixture fixture, ITest
private static ShaderFixture CompileTypedShader(uint instructionFormat, uint descriptorWord3, uint offset) =>
CompileTypedShader(instructionFormat, descriptorWord3, offset, opcode: 3, DestinationRegister);
private static ShaderFixture CompileTypedShader(uint instructionFormat, uint descriptorWord3, uint offset, uint opcode, uint dataRegister)
private static ShaderFixture CompileTypedShader(uint instructionFormat, uint descriptorWord3, uint offset, uint opcode, uint dataRegister, bool typed = true)
{
const uint descriptorRegister = 16;
var words = new List<uint>();
@ -434,9 +466,12 @@ public sealed class GlobalMemoryShaderTests(HeadlessVulkanFixture fixture, ITest
words.Add(0x8000_0000 | ((SourceRegister + group * 4) << 8));
}
var accessPc = checked((uint)words.Count * 4);
// tbuffer_load/store_format_* v[data..], off, s[16:19], 0 format:instructionFormat offset:offset
words.Add(0xE800_0000u | (instructionFormat << 19) | (opcode << 16) | offset);
words.Add((0x80u << 24) | ((descriptorRegister / 4) << 16) | (dataRegister << 8));
// Keep the extended opcode bit clear; use the scalar operand for an odd byte offset.
words.Add(typed
? 0xE800_0000u | (instructionFormat << 19) | (opcode << 16) | offset
: 0xE000_0000u | (opcode << 18) | (offset & ~1u));
var scalarOffsetOperand = typed ? 0x80u : 0x80u + (offset & 1u);
words.Add((scalarOffsetOperand << 24) | ((descriptorRegister / 4) << 16) | (dataRegister << 8));
for (uint group = 0; group < 4; group++)
{
words.Add(0xE078_0000 | (RegisterOutputOffset + group * 16));
@ -447,8 +482,8 @@ public sealed class GlobalMemoryShaderTests(HeadlessVulkanFixture fixture, ITest
var program = DecodeProgram(words);
var access = Assert.Single(program.Instructions, instruction => instruction.Pc == accessPc);
var control = Assert.IsType<Gen5BufferMemoryControl>(access.Control);
Assert.True(control.Typed);
Assert.Equal(instructionFormat, control.TypedFormat);
Assert.Equal(typed, control.Typed);
Assert.Equal(typed ? instructionFormat : 0u, control.TypedFormat);
Assert.Equal(descriptorRegister, control.ScalarResource);
var scalars = BaseScalars();

View File

@ -42,6 +42,7 @@ internal sealed unsafe class HeadlessVulkan : IDisposable
// Dynamic rendering with the two extended dynamic state extensions, as the presenter's render host needs.
public bool SupportsDynamicRendering { get; }
public bool SupportsFragmentShaderBarycentric { get; private init; }
private static readonly string[] RenderingExtensionNames =
[
@ -284,6 +285,22 @@ internal sealed unsafe class HeadlessVulkan : IDisposable
}
}
const string barycentricExtension = "VK_KHR_fragment_shader_barycentric";
var barycentricFeatures = new PhysicalDeviceFragmentShaderBarycentricFeaturesKHR
{
SType = StructureType.PhysicalDeviceFragmentShaderBarycentricFeaturesKhr,
};
if (HasDeviceExtensions(vk, physical, [barycentricExtension]))
{
var query = new PhysicalDeviceFeatures2
{
SType = StructureType.PhysicalDeviceFeatures2,
PNext = &barycentricFeatures,
};
vk.GetPhysicalDeviceFeatures2(physical, &query);
}
var barycentric = (bool)barycentricFeatures.FragmentShaderBarycentric;
var extendedDynamicState2Features = new PhysicalDeviceExtendedDynamicState2FeaturesEXT
{
SType = StructureType.PhysicalDeviceExtendedDynamicState2FeaturesExt,
@ -359,7 +376,15 @@ internal sealed unsafe class HeadlessVulkan : IDisposable
PNext = dynamicRendering ? &dynamicRenderingFeatures : null,
};
timelineFeatures.PNext = &addressFeatures;
var deviceExtensions = dynamicRendering ? SilkMarshal.StringArrayToPtr(RenderingExtensionNames) : 0;
if (barycentric)
{
barycentricFeatures.PNext = addressFeatures.PNext;
addressFeatures.PNext = &barycentricFeatures;
}
var extensionNames = new List<string>();
if (dynamicRendering) extensionNames.AddRange(RenderingExtensionNames);
if (barycentric) extensionNames.Add(barycentricExtension);
var deviceExtensions = extensionNames.Count > 0 ? SilkMarshal.StringArrayToPtr(extensionNames.ToArray()) : 0;
var deviceInfo = new DeviceCreateInfo
{
SType = StructureType.DeviceCreateInfo,
@ -367,11 +392,11 @@ internal sealed unsafe class HeadlessVulkan : IDisposable
QueueCreateInfoCount = 1,
PQueueCreateInfos = &queueInfo,
PEnabledFeatures = &enabledFeatures,
EnabledExtensionCount = dynamicRendering ? (uint)RenderingExtensionNames.Length : 0u,
EnabledExtensionCount = (uint)extensionNames.Count,
PpEnabledExtensionNames = (byte**)deviceExtensions,
};
var deviceCreated = vk.CreateDevice(physical, &deviceInfo, null, out var device);
if (dynamicRendering)
if (deviceExtensions != 0)
{
SilkMarshal.Free(deviceExtensions);
}
@ -383,7 +408,10 @@ internal sealed unsafe class HeadlessVulkan : IDisposable
}
vk.GetDeviceQueue(device, family, 0, out var queue);
var result = new HeadlessVulkan(vk, instance, physical, device, queue, family, apiVersion, enabledFeatures, dynamicRendering);
var result = new HeadlessVulkan(vk, instance, physical, device, queue, family, apiVersion, enabledFeatures, dynamicRendering)
{
SupportsFragmentShaderBarycentric = barycentric,
};
if (validation)
{
result.RegisterDebugMessenger();

View File

@ -207,10 +207,10 @@ public sealed class LayoutImageShaderTests(HeadlessVulkanFixture fixture, ITestO
return registers;
}
private static DescriptorImageInfo SampledView(CachedImage image, uint levelCount = 1) =>
private static DescriptorImageInfo SampledView(CachedImage image, uint levelCount = 1, bool arrayed = false) =>
new()
{
ImageView = image.GetOrCreateView(ImageViewDescription.Default with { Format = image.Backing.Format, Usage = ImageUsageFlags.SampledBit, LevelCount = levelCount }),
ImageView = image.GetOrCreateView(ImageViewDescription.Default with { Format = image.Backing.Format, Usage = ImageUsageFlags.SampledBit, LevelCount = levelCount, Type = arrayed ? ImageViewType.Type2DArray : ImageViewType.Type2D }),
ImageLayout = ImageLayout.ShaderReadOnlyOptimal,
};
@ -301,7 +301,12 @@ public sealed class LayoutImageShaderTests(HeadlessVulkanFixture fixture, ITestO
[InlineData(true, false, 2.5f)]
[InlineData(false, true, 2.0f)]
[InlineData(true, true, 2.5f)]
public void IndirectImage_SelectsTheMappedCandidateOnTheDevice(bool loadMip, bool gpuDependentSelector, float expected)
[InlineData(false, false, 1.0f, 9u, 13u, 0u)]
[InlineData(false, false, 2.0f, 9u, 13u, 1u)]
[InlineData(false, false, 1.0f, 13u, 9u, 0u)]
[InlineData(false, false, 2.0f, 13u, 9u, 1u)]
public void IndirectImage_SelectsTheMappedCandidateOnTheDevice(bool loadMip, bool gpuDependentSelector, float expected,
uint firstImageType = 9, uint secondImageType = 9, uint selectedRecord = 1)
{
var vulkan = fixture.Vulkan;
if (!GatePrerequisites.Ready(vulkan))
@ -309,8 +314,10 @@ public sealed class LayoutImageShaderTests(HeadlessVulkanFixture fixture, ITestO
return;
}
var (request, snapshot) = SpirvBindingDeclarationTests.IndirectImageAfterPlainImageRequest(ResultBytes, loadMip, gpuDependentSelector);
var (registers, memory) = SpirvBindingDeclarationTests.IndirectImageAfterPlainImageInputs(ResultBytes, loadMip);
var (request, snapshot) = SpirvBindingDeclarationTests.IndirectImageAfterPlainImageRequest(ResultBytes, loadMip, gpuDependentSelector,
firstImageType, secondImageType, firstImageType == 13 || secondImageType == 13 ? 5u : 1u);
var (registers, memory) = SpirvBindingDeclarationTests.IndirectImageAfterPlainImageInputs(ResultBytes, loadMip, firstImageType, secondImageType);
registers[8] = selectedRecord;
Assert.True(Gen5SpirvTranslator.TryCompileProgram(request, out var shader, out var error), error);
using var harness = new ImageTestHarness(vulkan);
using var runner = new LayoutComputeRunner(harness, request, shader.Spirv);
@ -329,7 +336,7 @@ public sealed class LayoutImageShaderTests(HeadlessVulkanFixture fixture, ITestO
images[index] = harness.CreateImage(description);
var data = Enumerable.Repeat(texels[index], (int)(levels * levels)).Concat(levels > 1 ? [texels[index] + 0.5f] : Array.Empty<float>()).ToArray();
harness.UploadImage(images[index], MemoryMarshal.AsBytes<float>(data), ImageTestHarness.WholeImageCopies(description, 0));
infos[index] = [SampledView(images[index], levels)];
infos[index] = [SampledView(images[index], levels, request.Resources.Info.Images[index].Dimension == ImageDimension.Dim2DArray)];
}
Assert.Equal(0x20u, snapshot.Images[1][0]);

View File

@ -0,0 +1,93 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Buffers.Binary;
using SharpEmu.Libs.Gpu.Buffers;
using SharpEmu.Libs.Tests.Gpu.Images;
using SharpEmu.ShaderCompiler;
using SharpEmu.ShaderCompiler.Resources;
using SharpEmu.ShaderCompiler.Vulkan;
using Xunit;
using static SharpEmu.ShaderCompiler.Tests.Resources.ResourceTestProgram;
namespace SharpEmu.Libs.Tests.Gpu.Vulkan;
public sealed class WaveLaneTransferDeviceTests(HeadlessVulkanFixture fixture) : IClassFixture<HeadlessVulkanFixture>
{
[Theory]
[InlineData(32u, 31u, false)]
[InlineData(32u, 63u, true)]
[InlineData(64u, 31u, false)]
[InlineData(64u, 63u, false)]
[InlineData(64u, 95u, true)]
[InlineData(64u, 127u, true)]
public void ReadLane_UsesGuestWaveIndexAndIgnoresExecutionMask(uint waveSize, uint selector, bool disableExecution)
{
Run(waveSize, selector, disableExecution, writeLane: false);
}
[Theory]
[InlineData(32u, 63u)]
[InlineData(64u, 31u)]
[InlineData(64u, 63u)]
[InlineData(64u, 127u)]
public void WriteLane_ChangesOnlySelectedGuestLaneWithExecutionDisabled(uint waveSize, uint selector)
{
Run(waveSize, selector, disableExecution: true, writeLane: true);
}
private void Run(uint waveSize, uint selector, bool disableExecution, bool writeLane)
{
var vulkan = fixture.Vulkan;
if (!GatePrerequisites.Ready(vulkan, shaderInt64: true)) return;
var instructions = new List<Gen5ShaderInstruction>();
uint programCounter = 0;
void Add(Gen5ShaderInstruction instruction)
{
instructions.Add(instruction with { Pc = programCounter });
programCounter += 8;
}
Add(Vop2(0, "VLshlrevB32", 2, Operand(3), Gen5Operand.Vector(1)));
Add(Vop2(0, "VAddI32", 2, Gen5Operand.Vector(0), Gen5Operand.Vector(2)));
Add(Vop2(0, "VAddI32", 3, Operand(1000), Gen5Operand.Vector(2)));
Add(MoveScalar(0, 20, selector));
Add(MoveScalar(0, 21, 0x12345678));
if (disableExecution)
{
Add(MoveScalar(0, 126, 0));
Add(MoveScalar(0, 127, 0));
}
if (writeLane)
Add(WriteLane(0, 3, 21, 0) with { Sources = [Gen5Operand.Scalar(21), Gen5Operand.Scalar(20)] });
else
Add(ReadLane(0, 22, 3, 0) with { Sources = [Gen5Operand.Vector(3), Gen5Operand.Scalar(20)] });
Add(MoveScalar(0, 126, uint.MaxValue));
Add(MoveScalar(0, 127, uint.MaxValue));
if (!writeLane) Add(MoveVectorFromScalar(0, 3, 22));
Add(Vop2(0, "VLshlrevB32", 5, Operand(2), Gen5Operand.Vector(2)));
Add(BufferAccess(0, "BufferStoreDword", 4, vectorData: 3, offsetEnabled: true, vectorAddress: 5));
Add(EndProgram(0));
var (plan, resources, layout) = Prepare(Program([.. instructions]));
var request = new ShaderCompileRequest(plan, resources, layout)
{
LocalSizeX = 8, LocalSizeY = waveSize / 8, ThreadCountX = 8, ThreadCountY = waveSize / 8,
WaveSize = waveSize,
};
Assert.True(Gen5SpirvTranslator.TryCompileProgram(request, out var shader, out var error), error);
using var harness = new ImageTestHarness(vulkan);
using var runner = new LayoutComputeRunner(harness, request, shader.Spirv);
var output = runner.CreateBuffer(256);
var registers = new uint[256];
registers[6] = 256;
harness.Run(() => runner.Dispatch(registers,
new Dictionary<DescriptorBindingKind, GpuBuffer[]> { [DescriptorBindingKind.Buffers] = [output] }, 1));
var bytes = runner.ReadBack(output, 0, 256);
var selectedLane = selector % waveSize;
for (uint lane = 0; lane < waveSize; lane++)
{
var expected = writeLane ? (lane == selectedLane ? 0x12345678u : 1000 + lane) : 1000 + selectedLane;
Assert.Equal(expected, BinaryPrimitives.ReadUInt32LittleEndian(bytes.AsSpan((int)lane * 4)));
}
harness.AssertNoValidationMessages();
}
}

View File

@ -27,6 +27,52 @@ public sealed class KernelMemoryCompatExportsTests
private const ulong SpanStartOutAddress = GuestMemoryBase + 0x108;
private const ulong SpanSizeOutAddress = GuestMemoryBase + 0x110;
[Theory]
[InlineData(false)]
[InlineData(true)]
public void UppercaseStringConversion_ConsumesWideArgumentBetweenNarrowStrings(bool useVaList)
{
var memory = new FakeCpuMemory(GuestMemoryBase, 0x2000);
var context = new CpuContext(memory, Generation.Gen5);
memory.WriteCString(GuestMemoryBase + 0x100, "assets");
Assert.True(memory.TryWrite(GuestMemoryBase + 0x200, Encoding.Unicode.GetBytes("armor\0")));
memory.WriteCString(GuestMemoryBase + 0x300, "body");
const string format = "%s/%S_%s.bin";
string rendered;
if (useVaList)
{
var arguments = GuestMemoryBase + 0x400;
var savedRegisters = GuestMemoryBase + 0x500;
Assert.True(context.TryWriteUInt64(arguments, 48UL << 32));
Assert.True(context.TryWriteUInt64(arguments + 8, GuestMemoryBase + 0x600));
Assert.True(context.TryWriteUInt64(arguments + 16, savedRegisters));
Assert.True(context.TryWriteUInt64(savedRegisters, GuestMemoryBase + 0x100));
Assert.True(context.TryWriteUInt64(savedRegisters + 8, GuestMemoryBase + 0x200));
Assert.True(context.TryWriteUInt64(savedRegisters + 16, GuestMemoryBase + 0x300));
Assert.True(KernelMemoryCompatExports.TryFormatStringFromVaList(context, format, arguments, out rendered));
}
else
{
context[CpuRegister.Rcx] = GuestMemoryBase + 0x100;
context[CpuRegister.R8] = GuestMemoryBase + 0x200;
context[CpuRegister.R9] = GuestMemoryBase + 0x300;
rendered = KernelMemoryCompatExports.FormatStringFromVarArgs(context, format, 3);
}
Assert.Equal("assets/armor_body.bin", rendered);
}
[Theory]
[InlineData("%8.3S", " arm")]
[InlineData("%-8.3S", "arm ")]
public void UppercaseStringConversion_UsesExistingWideStringFormatting(string format, string expected)
{
var memory = new FakeCpuMemory(GuestMemoryBase, 0x1000);
var context = new CpuContext(memory, Generation.Gen5);
Assert.True(memory.TryWrite(GuestMemoryBase + 0x100, Encoding.Unicode.GetBytes("armor\0")));
context[CpuRegister.Rcx] = GuestMemoryBase + 0x100;
Assert.Equal(expected, KernelMemoryCompatExports.FormatStringFromVarArgs(context, format, 3));
}
[Fact]
public void PosixStat_MissingFileReturnsMinusOne()
{

View File

@ -45,6 +45,53 @@ public sealed class PresenterImageBindingTests : IClassFixture<HeadlessVulkanFix
public PresenterImageBindingTests(HeadlessVulkanFixture fixture) => _vulkan = fixture.Vulkan;
[Theory]
[InlineData(GuestPixelFormat.Bits16UNorm, Format.D16Unorm)]
[InlineData(GuestPixelFormat.Bits32Float, Format.D32Sfloat)]
public void ComparisonBinding_CreatesDepthImageWithoutAnAttachment(GuestPixelFormat guestFormat, Format hostFormat)
{
if (!GatePrerequisites.Ready(_vulkan)) return;
using var fatal = new FatalScope();
using var presenter = new PresenterUnderTest(_vulkan);
var harness = presenter.Harness;
var address = harness.MapBacked(0x10000, ReadWrite);
var sample = guestFormat == GuestPixelFormat.Bits16UNorm
? BitConverter.GetBytes((ushort)0x4000)
: BitConverter.GetBytes(0.25f);
var guestBytes = new byte[0x10000];
for (var offset = 0; offset < guestBytes.Length; offset += sample.Length)
sample.CopyTo(guestBytes, offset);
harness.Write(address, guestBytes);
CachedImage sampledImage = null!;
presenter.Run(() =>
{
var resource = new ImageResource
{
ResourceClass = PlanImageResourceClass.Sampled,
NumericClass = ImageNumericClass.Float,
Dimension = ImageDimension.Dim2D,
Read = true,
DepthCompare = true,
};
var words = RegisterWords.Texture(address, guestFormat, 48, 24);
words[3] = (words[3] & ~0xFFFu) | ViewFormatRules.PackDestinationSelect(4, 0, 0, 1);
var binding = presenter.InvokeMethod("ResolveImageBinding", resource, words, new ShaderProgramInfo(), 0)!;
var identifier = (ResourceSlotIdentifier)GetFieldValue(binding, "ImageIdentifier");
var request = (ImageRequest)GetFieldValue(binding, "Request");
var image = harness.Images.GetImage(identifier);
sampledImage = image;
var view = harness.Images.AcquireTextureView(identifier, request);
Assert.Equal(hostFormat, image.Backing.Format);
Assert.True(image.Description.IsDepth);
Assert.NotEqual(0UL, view.Handle);
Assert.Contains(image.Views, candidate => candidate.Description.Aspect == ImageAspectFlags.DepthBit);
presenter.RenderHost.ResetBindings();
});
var uploaded = harness.ReadImageBytes(sampledImage, ImageAspectFlags.DepthBit);
Assert.Equal(sample, uploaded.AsSpan(0, sample.Length).ToArray());
harness.Shutdown();
}
[Theory]
[InlineData(true, 1u, false)]
[InlineData(true, 2u, true)]
@ -67,7 +114,7 @@ public sealed class PresenterImageBindingTests : IClassFixture<HeadlessVulkanFix
var workingImages = new List<CachedImage>();
presenter.Run(() =>
{
var shape = new ShaderImageShape(Volume: false, Arrayed: true, Storage: true, DynamicMip: false, TextureNumericClass.Uint);
var shape = new ShaderImageShape(false, true, true, false, TextureNumericClass.Uint);
var texture = new GuestDrawTexture(stencilAddress, 64, 64, 0, 0, [], false, true,
Descriptor: RegisterWords.Texture(stencilAddress, GuestPixelFormat.Bits8UInt, 64, 64,
type: GuestImageType.Color2DArray, tile: GuestTileMode.Depth, layers: layers, baseArray: layers - 1), Shape: shape);

View File

@ -53,6 +53,7 @@ internal sealed class PresenterUnderTest : IDisposable
SetField("_instance", vulkan.Instance);
SetField("_physicalDevice", vulkan.Physical);
SetField("_device", vulkan.Device);
SetField("_supportsFragmentShaderBarycentric", vulkan.SupportsFragmentShaderBarycentric);
SetField("_deviceInfo", vulkan.DeviceInfo);
SetField("_scheduler", Harness.Scheduler);
SetField("_relay", Harness.Worker.Relay);

View File

@ -0,0 +1,115 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.HLE.Host;
using SharpEmu.Libs.Gpu.Pipelines;
using SharpEmu.Libs.Gpu.Rendering;
using SharpEmu.Libs.Gpu.Vulkan;
using SharpEmu.Libs.Tests.Gpu.Images;
using SharpEmu.ShaderCompiler;
using SharpEmu.ShaderCompiler.Resources;
using SharpEmu.ShaderCompiler.Vulkan;
using Xunit;
using static SharpEmu.Libs.Tests.Gpu.Images.ImageCacheTestSupport;
using static SharpEmu.ShaderCompiler.Tests.Resources.ResourceTestProgram;
namespace SharpEmu.Libs.Tests.VideoOut;
public sealed unsafe partial class RenderHostDeviceTests
{
[Theory]
[InlineData(0u, true, 191)]
[InlineData(1u, true, 64)]
[InlineData(2u, true, 64)]
[InlineData(0u, false, 128)]
[InlineData(1u, false, 0)]
[InlineData(2u, false, 64)]
public void InterpolationParameter_DrawReadsSelectedVertex(uint selector, bool custom, int expected)
{
VerifyInterpolationDraw(selector, custom, interpolate: false, expected);
}
[Fact]
public void InterpolationPhases_DrawUsesBarycentricRegisters()
{
VerifyInterpolationDraw(2, false, interpolate: true, expected: 96);
}
[Fact]
public void InterpolationShader_RejectsMissingDeviceFeatureBeforeModuleCreation()
{
if (!Ready()) return;
using var presenter = new PresenterUnderTest(_vulkan);
presenter.SetField("_supportsFragmentShaderBarycentric", false);
var shader = new VulkanCompiledGuestShader(CompileInterpolationFragment(2, true, false));
presenter.Run(() =>
{
var error = Assert.Throws<NotSupportedException>(() =>
((IShaderPipelineHost)presenter.Instance).CreateShaderModule(shader, ShaderStage.Pixel, 1, 1));
Assert.Contains("fragmentShaderBarycentric", error.Message);
});
presenter.Harness.Shutdown();
}
private void VerifyInterpolationDraw(uint selector, bool custom, bool interpolate, int expected)
{
if (!Ready()) return;
if (!_vulkan.SupportsFragmentShaderBarycentric)
{
Assert.False(GatePrerequisites.DeviceRequired, "The required device lacks fragmentShaderBarycentric.");
Console.Error.WriteLine("[TEST][SKIP] The device lacks fragmentShaderBarycentric.");
return;
}
using var presenter = new PresenterUnderTest(_vulkan);
presenter.LoadRenderingCommands();
var harness = presenter.Harness;
var target = harness.MapBacked(0x10000, ReadWrite);
var vertices = harness.MapBacked(0x10000, ReadWrite);
var words = RegisterWords.Color(target, Size, Size);
harness.Write(vertices, Triangle(-1f, -1f, 3f, -1f, -1f, 3f));
var provider = new FixedProgramProvider((IShaderPipelineHost)presenter.Instance, vertices,
interpolationShader: CompileInterpolationFragment(selector, custom, interpolate));
var executor = new RenderExecutor(presenter.RenderHost, provider);
presenter.Run(() => executor.DrawAuto(1, Banks(words), Draw()));
presenter.Run(() => presenter.InvokeMethod("FlushBatchedGuestCommands"));
harness.Finish();
var pixels = harness.ReadImageBytes(TargetImage(presenter, words));
var actual = Pixel(pixels, Size / 2, Size / 2);
Assert.InRange((int)(actual & 255), Math.Max(0, expected - 1), expected + 1);
Assert.Equal(255u, actual >> 24);
harness.Shutdown();
_vulkan.AssertNoValidationMessages();
}
private static byte[] CompileInterpolationFragment(uint selector, bool custom, bool interpolate)
{
var instructions = new List<Gen5ShaderInstruction>();
void Parameter(string opcode, uint source) => instructions.Add(new Gen5ShaderInstruction(
(uint)instructions.Count * 4, Gen5ShaderEncoding.Vintrp, opcode, [source],
[Gen5Operand.Vector(source)], [Gen5Operand.Vector(4)], new Gen5InterpolationControl(0, 0)));
Parameter("VInterpMovF32", selector);
if (interpolate)
{
Parameter("VInterpP1F32", 0);
Parameter("VInterpP2F32", 1);
}
instructions.Add(MoveVector((uint)instructions.Count * 4, 5, 0x3F800000));
instructions.Add(new Gen5ShaderInstruction((uint)instructions.Count * 4, Gen5ShaderEncoding.Exp,
"Exp", [], [Gen5Operand.Vector(4), Gen5Operand.Vector(4), Gen5Operand.Vector(4), Gen5Operand.Vector(5)],
[], new Gen5ExportControl(0, 15, false, true, true)));
instructions.Add(EndProgram((uint)instructions.Count * 4));
var (plan, resources, layout) = Prepare(Program([.. instructions]), ShaderStage.Pixel, userDataCount: 0);
var request = new ShaderCompileRequest(plan, resources, layout)
{
PixelInputAddress = 2,
PixelInputEnable = 2,
PixelInputCntl = [custom ? 0x400u : 0u],
PixelCustomInterpolationMask = custom ? 1u : 0u,
PixelOutputs = [new Gen5PixelOutputBinding(0, 0, Gen5PixelOutputKind.Float)],
EnableGraphicsSubgroupOperations = false,
};
Assert.True(Gen5SpirvTranslator.TryCompileProgram(request, out var shader, out var error), error);
return shader.Spirv;
}
}

View File

@ -28,7 +28,7 @@ namespace SharpEmu.Libs.Tests.VideoOut;
// The executor over the presenter's render host on a real device: each draw renders the guest bytes it was recorded with.
[Collection(SchedulingStateCollection.Name)]
public sealed unsafe class RenderHostDeviceTests : IClassFixture<HeadlessVulkanFixture>
public sealed unsafe partial class RenderHostDeviceTests : IClassFixture<HeadlessVulkanFixture>
{
private const uint Size = 64;
private const uint VertexStride = 8;
@ -116,7 +116,8 @@ public sealed unsafe class RenderHostDeviceTests : IClassFixture<HeadlessVulkanF
public RenderHostDeviceTests(HeadlessVulkanFixture fixture) => _vulkan = fixture.Vulkan;
// One float2 position program and one solid red pixel program over empty resource plans.
private sealed class FixedProgramProvider(IShaderPipelineHost host, ulong vertexAddress, bool pushData = false) : IShaderPipelineProvider
private sealed class FixedProgramProvider(IShaderPipelineHost host, ulong vertexAddress, bool pushData = false,
byte[]? interpolationShader = null) : IShaderPipelineProvider
{
private const uint Float2Format = 64;
private static readonly uint[] UserRegisters = [0, 1];
@ -151,8 +152,10 @@ public sealed unsafe class RenderHostDeviceTests : IClassFixture<HeadlessVulkanF
return;
}
_vertexProgram = new ShaderProgram(1, host.CreateShaderModule(new VulkanCompiledGuestShader(CreatePositionVertexShader()), ShaderStage.Vertex, 1, 1));
_pixelProgram = new ShaderProgram(2, host.CreateShaderModule(new VulkanCompiledGuestShader(SpirvFixedShaders.CreateSolidFragment(1f, 0f, 0f, 1f)), ShaderStage.Pixel, 2, 2));
_vertexProgram = new ShaderProgram(1, host.CreateShaderModule(new VulkanCompiledGuestShader(
CreatePositionVertexShader(interpolationShader is not null)), ShaderStage.Vertex, 1, 1));
_pixelProgram = new ShaderProgram(2, host.CreateShaderModule(new VulkanCompiledGuestShader(
interpolationShader ?? SpirvFixedShaders.CreateSolidFragment(1f, 0f, 0f, 1f)), ShaderStage.Pixel, 2, 2));
}
public GraphicsPrograms GetGraphicsPrograms(
@ -202,7 +205,7 @@ public sealed unsafe class RenderHostDeviceTests : IClassFixture<HeadlessVulkanF
}
// position = (input.xy, 0, 1)
private static byte[] CreatePositionVertexShader()
private static byte[] CreatePositionVertexShader(bool interpolationOutput = false)
{
var module = new SpirvModuleBuilder();
module.AddCapability(SpirvCapability.Shader);
@ -216,6 +219,12 @@ public sealed unsafe class RenderHostDeviceTests : IClassFixture<HeadlessVulkanF
module.AddDecoration(input, SpirvDecoration.Location, 0);
var position = module.AddGlobalVariable(outputPointer, SpirvStorageClass.Output);
module.AddDecoration(position, SpirvDecoration.BuiltIn, (uint)SpirvBuiltIn.Position);
uint parameter = 0;
if (interpolationOutput)
{
parameter = module.AddGlobalVariable(outputPointer, SpirvStorageClass.Output);
module.AddDecoration(parameter, SpirvDecoration.Location, 0);
}
var functionType = module.TypeFunction(voidType);
var main = module.BeginFunction(voidType, functionType);
module.AddLabel();
@ -225,9 +234,19 @@ public sealed unsafe class RenderHostDeviceTests : IClassFixture<HeadlessVulkanF
var composed = module.AddInstruction(
SpirvOp.CompositeConstruct, vec4Type, x, y, module.ConstantFloat(floatType, 0f), module.ConstantFloat(floatType, 1f));
module.AddStatement(SpirvOp.Store, position, composed);
if (interpolationOutput)
{
var component = module.AddInstruction(SpirvOp.FAdd, floatType,
module.AddInstruction(SpirvOp.FMul, floatType, x, module.ConstantFloat(floatType, 0.125f)),
module.ConstantFloat(floatType, 0.375f));
var output = module.AddInstruction(SpirvOp.CompositeConstruct, vec4Type,
component, component, component, component);
module.AddStatement(SpirvOp.Store, parameter, output);
}
module.AddStatement(SpirvOp.Return);
module.EndFunction();
module.AddEntryPoint(SpirvExecutionModel.Vertex, main, "main", [input, position]);
module.AddEntryPoint(SpirvExecutionModel.Vertex, main, "main",
interpolationOutput ? [input, position, parameter] : [input, position]);
return module.Build();
}

View File

@ -0,0 +1,43 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.Libs.VideoOut;
using Xunit;
namespace SharpEmu.Libs.Tests.VideoOut;
public sealed class RenderPhaseProfileTests
{
[Fact]
public void DisabledDetailMeasurement_DoesNotAllocateOnANewThread()
{
if (RenderPhaseProfile.Enabled) return;
long allocated = -1;
var thread = new Thread(() =>
{
var before = GC.GetAllocatedBytesForCurrentThread();
using (RenderPhaseProfile.MeasureDetail(RenderPhaseProfile.Phase.ImageTracking))
{
}
allocated = GC.GetAllocatedBytesForCurrentThread() - before;
}) { IsBackground = true };
thread.Start();
Assert.True(thread.Join(TimeSpan.FromSeconds(30)));
Assert.Equal(0, allocated);
}
[Fact]
public void DetailMeasurement_PreservesTheOuterScope()
{
Assert.False(RenderPhaseProfile.DetailMeasurementsEnabled);
using (RenderPhaseProfile.Measure(RenderPhaseProfile.Phase.Draw))
{
Assert.Equal(RenderPhaseProfile.Enabled, RenderPhaseProfile.DetailMeasurementsEnabled);
using (RenderPhaseProfile.MeasureDetail(RenderPhaseProfile.Phase.ImageTracking))
Assert.Equal(RenderPhaseProfile.Enabled, RenderPhaseProfile.DetailMeasurementsEnabled);
Assert.Equal(RenderPhaseProfile.Enabled, RenderPhaseProfile.DetailMeasurementsEnabled);
}
Assert.False(RenderPhaseProfile.DetailMeasurementsEnabled);
}
}

View File

@ -234,6 +234,60 @@ public sealed class VideoOutFlipRequestTests : IDisposable
}
}
[Fact]
public void OutputModeEventIsImmediatelyAvailableAndTracksConfiguration()
{
_context[CpuRegister.Rdi] = StatusAddress;
_context[CpuRegister.Rsi] = 0;
Assert.Equal(0, KernelEventQueueCompatExports.KernelCreateEqueue(_context));
var bytes = new byte[8];
Assert.True(_memory.TryRead(StatusAddress, bytes));
var queue = BinaryPrimitives.ReadUInt64LittleEndian(bytes);
try
{
const ulong userData = 0x1234_5678_9ABC_DEF0;
_context[CpuRegister.Rdi] = queue;
_context[CpuRegister.Rsi] = (ulong)_handle;
_context[CpuRegister.Rdx] = userData;
Assert.Equal(0, VideoOutExports.VideoOutAddOutputModeEvent(_context));
AssertOutputModeEvent(queue, expectedMode: 1, userData);
_context[CpuRegister.Rdi] = (ulong)_handle;
_context[CpuRegister.Rsi] = 1;
_context[CpuRegister.Rdx] = 0;
_context[CpuRegister.Rcx] = 0;
_context[CpuRegister.R8] = 0;
Assert.Equal(0, VideoOutExports.VideoOutConfigureOutput(_context));
AssertOutputModeEvent(queue, expectedMode: 1, userData);
}
finally
{
_context[CpuRegister.Rdi] = queue;
Assert.Equal(0, KernelEventQueueCompatExports.KernelDeleteEqueue(_context));
}
}
private void AssertOutputModeEvent(ulong queue, ulong expectedMode, ulong userData)
{
var timeoutAddress = MemoryBase + 0x400;
Assert.True(_memory.TryWrite(timeoutAddress, BitConverter.GetBytes(1_000_000u)));
_context[CpuRegister.Rdi] = queue;
_context[CpuRegister.Rsi] = MemoryBase + 0x200;
_context[CpuRegister.Rdx] = 1;
_context[CpuRegister.Rcx] = MemoryBase + 0x300;
_context[CpuRegister.R8] = timeoutAddress;
Assert.Equal(0, KernelEventQueueCompatExports.KernelWaitEqueue(_context));
var eventBytes = new byte[0x20];
Assert.True(_memory.TryRead(MemoryBase + 0x200, eventBytes));
Assert.Equal(8UL, BinaryPrimitives.ReadUInt64LittleEndian(eventBytes));
Assert.Equal(-13, BinaryPrimitives.ReadInt16LittleEndian(eventBytes.AsSpan(0x08)));
Assert.Equal(expectedMode, BinaryPrimitives.ReadUInt64LittleEndian(eventBytes.AsSpan(0x10)) >> 16);
Assert.Equal(userData, BinaryPrimitives.ReadUInt64LittleEndian(eventBytes.AsSpan(0x18)));
}
[Theory]
[InlineData(0)]
[InlineData(1)]

View File

@ -530,7 +530,21 @@ public sealed class MslTranslationTests
Assert.DoesNotContain("16u <= sharpemu_resources.buffer_bytes[0] && ", shader.Source, StringComparison.Ordinal);
}
private static Gen5MslShader CompileTypedBufferAccess(string opcode, uint dwordCount, uint typedFormat)
[Fact]
public void FormattedBufferStoreUsesDescriptorFormatForPackedNormals()
{
var shader = CompileTypedBufferAccess("BufferStoreFormatXyzw", 4, 0, typed: false, descriptorFormat: 50);
Assert.Contains("sharpemu_format_encode(v[4], 2u, 0u, 9u)", shader.Source, StringComparison.Ordinal);
Assert.Contains("sharpemu_format_encode(v[5], 10u, 0u, 9u)", shader.Source, StringComparison.Ordinal);
Assert.Contains("sharpemu_format_encode(v[6], 10u, 0u, 9u)", shader.Source, StringComparison.Ordinal);
Assert.Contains("sharpemu_format_encode(v[7], 10u, 0u, 9u)", shader.Source, StringComparison.Ordinal);
Assert.Contains("uint4(0xFFFFFFFFu, 0x0u, 0x0u, 0x0u)", shader.Source, StringComparison.Ordinal);
Assert.DoesNotContain("sharpemu_store_bytes(b", shader.Source, StringComparison.Ordinal);
}
private static Gen5MslShader CompileTypedBufferAccess(string opcode, uint dwordCount, uint typedFormat,
bool typed = true, uint descriptorFormat = 77)
{
var data = new Gen5Operand[dwordCount];
for (var index = 0; index < data.Length; index++)
@ -540,7 +554,7 @@ public sealed class MslTranslationTests
var access = new Gen5ShaderInstruction(
0,
Gen5ShaderEncoding.Mtbuf,
typed ? Gen5ShaderEncoding.Mtbuf : Gen5ShaderEncoding.Mubuf,
opcode,
[0, 0],
[Gen5Operand.Vector(0), Gen5Operand.Scalar(8), Gen5Operand.Source(128, null)],
@ -555,13 +569,13 @@ public sealed class MslTranslationTests
OffsetEnabled: false,
Glc: false,
Slc: false,
Typed: true,
Typed: typed,
TypedFormat: typedFormat));
var end = new Gen5ShaderInstruction(8, Gen5ShaderEncoding.Sopp, "SEndpgm", [0xBF810000], [], [], null);
var scalars = new uint[256];
scalars[8] = 0x2000;
scalars[10] = 64;
scalars[11] = 77u << 12;
scalars[11] = descriptorFormat << 12;
var shader = CompileMaterialized(new Gen5ShaderProgram(0, [access, end]), scalars[..12]);
return shader;
}

View File

@ -19,6 +19,7 @@ public sealed class Gen5Float16ArithmeticTests
{
var program = Decode(
[
0x7E00AD01, // v_rsq_f16 v0, v1
0x64000501, // v_add_f16 v0, v1, v2
0x66060B04, // v_sub_f16 v3, v4, v5
0x680C1107, // v_subrev_f16 v6, v7, v8
@ -29,7 +30,7 @@ public sealed class Gen5Float16ArithmeticTests
]);
Assert.Equal(
["VAddF16", "VSubF16", "VSubrevF16", "VMulF16", "VMaxF16", "VMinF16", "SEndpgm"],
["VRsqF16", "VAddF16", "VSubF16", "VSubrevF16", "VMulF16", "VMaxF16", "VMinF16", "SEndpgm"],
program.Instructions.Select(instruction => instruction.Opcode));
var request = ResourceTestProgram.Request(program, userDataCount: 0);
@ -45,10 +46,49 @@ public sealed class Gen5Float16ArithmeticTests
Assert.Contains((ushort)SpirvOp.FAdd, opcodes);
Assert.Contains((ushort)SpirvOp.FSub, opcodes);
Assert.Contains((ushort)SpirvOp.FMul, opcodes);
Assert.True(opcodes.Count(opcode => opcode == (ushort)SpirvOp.ExtInst) >= 2);
Assert.True(opcodes.Count(opcode => opcode == (ushort)SpirvOp.ExtInst) >= 3);
Assert.DoesNotContain((ushort)SpirvCapability.Float16, ReadCapabilities(shader.Spirv));
}
[Fact]
public void Vop3Float16FmaDecodesAndCompiles()
{
var program = Decode(
[
(0x35u << 26) | (0x34Bu << 16) | 122u,
261u | (262u << 9) | (263u << 18),
SEndpgm,
]);
Assert.Equal("VFmaF16", program.Instructions[0].Opcode);
var request = ResourceTestProgram.Request(program, userDataCount: 0);
Assert.True(
Gen5SpirvTranslator.TryCompileProgram(request, out var shader, out var error),
error);
Assert.Contains((ushort)SpirvOp.ExtInst, ReadOpcodes(shader.Spirv));
Assert.DoesNotContain((ushort)SpirvCapability.Float16, ReadCapabilities(shader.Spirv));
}
[Fact]
public void PackedFloat16MultiplyAcceptsLiteralOperand()
{
var program = Decode(
[
(0x33u << 26) | (0x10u << 16) | 1u,
0xFFu | (258u << 9),
0x00002C00u,
SEndpgm,
]);
Assert.Equal("VPkMulF16", program.Instructions[0].Opcode);
Assert.Equal(Gen5OperandKind.LiteralConstant, program.Instructions[0].Sources[0].Kind);
Assert.Equal(0x00002C00u, program.Instructions[0].Sources[0].Value);
var request = ResourceTestProgram.Request(program, userDataCount: 0);
Assert.True(
Gen5SpirvTranslator.TryCompileProgram(request, out _, out var error),
error);
}
private static Gen5ShaderProgram Decode(IReadOnlyList<uint> words)
{
var memory = new TestCpuMemory(ShaderAddress, words.Count * sizeof(uint));

Some files were not shown because too many files have changed in this diff Show More