Compare commits
13 Commits
1016_part3
...
alpha_rebu
| Author | SHA1 | Date |
|---|---|---|
|
|
e275750490 | |
|
|
d74cd30faa | |
|
|
aed8be3568 | |
|
|
537ecb83e0 | |
|
|
776c97cebc | |
|
|
5ebf35fee3 | |
|
|
346f1787e0 | |
|
|
a9351f4caa | |
|
|
172f4e5764 | |
|
|
63147d0cbd | |
|
|
1e790e6aa5 | |
|
|
86ddc25b4f | |
|
|
55ef42f3c5 |
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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]!);
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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}");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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}");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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)})"),
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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};");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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!;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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}");
|
||||
|
|
|
|||
|
|
@ -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)));
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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}");
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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]);
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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)]
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
Loading…
Reference in New Issue