714 lines
30 KiB
C#
714 lines
30 KiB
C#
// Copyright (C) 2026 SharpEmu Emulator Project
|
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
|
|
using System.Buffers.Binary;
|
|
using SharpEmu.Libs.Gpu.GpuCommands;
|
|
using SharpEmu.Libs.Gpu.GpuCommands.Registers;
|
|
using SharpEmu.Libs.Gpu.Images;
|
|
using SharpEmu.Libs.Gpu.Scheduling;
|
|
using SharpEmu.Libs.VideoOut;
|
|
using Silk.NET.Vulkan;
|
|
using ResourceSnapshot = SharpEmu.ShaderCompiler.Resources.ResourceSnapshot;
|
|
|
|
namespace SharpEmu.Libs.Gpu.Rendering;
|
|
|
|
public enum GuestPrimitiveType : uint
|
|
{
|
|
None = 0,
|
|
PointList = 1,
|
|
LineList = 2,
|
|
LineStrip = 3,
|
|
TriangleList = 4,
|
|
TriangleFan = 5,
|
|
TriangleStrip = 6,
|
|
RectangleList = 7,
|
|
RectangleListLegacy = 17,
|
|
QuadListLegacy = 19,
|
|
Polygon = 21,
|
|
}
|
|
|
|
public enum GuestIndexType : uint
|
|
{
|
|
Index16 = 0,
|
|
Index32 = 1,
|
|
Index8 = 2,
|
|
}
|
|
|
|
// Resolves draw and dispatch state from the register banks and records it through the host.
|
|
public sealed partial class RenderExecutor
|
|
{
|
|
// Prospero uses both the full NGG mask and compact primitive-shader masks
|
|
// when the task/mesh front end has already folded the upper enable bits
|
|
// away. Little Nightmares also emits 0x2030 for a primitive path with no
|
|
// geometry shader; the remaining bits describe the fixed NGG setup.
|
|
// GS_W32_EN (bit 22) and VS_W32_EN (bit 23) only select the wave size;
|
|
// graphics stages always compile as wave32, so they do not change the path.
|
|
private const uint VgtShaderStagesWaveSizeBits = (1u << 22) | (1u << 23);
|
|
|
|
private static bool IsPrimitiveShaderStageMask(uint stages) =>
|
|
(stages & ~VgtShaderStagesWaveSizeBits) is 0x02002000 or 0x00002000 or 0x00002030;
|
|
private const uint MaxOutputPerSubgroupLimit = 0x40;
|
|
private static int _geometryWarningShown;
|
|
|
|
private readonly IRenderHost _host;
|
|
private readonly IShaderPipelineProvider _pipelines;
|
|
private readonly bool _strictDrawResources;
|
|
private readonly HashSet<(ulong ShaderHash, ImageType ImageType, ImageViewType ViewType)> _reportedDrawImageTypeMismatches = [];
|
|
|
|
public RenderExecutor(IRenderHost host, IShaderPipelineProvider pipelines)
|
|
: this(host, pipelines, Environment.GetEnvironmentVariable("SHARPEMU_STRICT_COMPUTE") != "0")
|
|
{
|
|
}
|
|
|
|
internal RenderExecutor(IRenderHost host, IShaderPipelineProvider pipelines, bool strictDrawResources)
|
|
{
|
|
_host = host;
|
|
_pipelines = pipelines;
|
|
_strictDrawResources = strictDrawResources;
|
|
}
|
|
|
|
private readonly record struct DrawCall(string Name, RecordedOperation Operation, uint Count, uint InstanceCount, uint FirstInstance);
|
|
|
|
// IndirectArgumentsAddress: the GPU reads the indexed draw's counts from guest memory there.
|
|
private readonly record struct DrawEmission(bool Indexed, int VertexOffset, uint FirstVertex, uint FirstInstance, ulong IndirectArgumentsAddress = 0);
|
|
|
|
private readonly record struct IndexSource(bool Enabled, ulong Address, byte[]? HostData, ulong Size, IndexType Type);
|
|
|
|
[System.Runtime.CompilerServices.InlineArray(RenderingState.ColorAttachmentCapacity)]
|
|
private struct ColorTargetStates
|
|
{
|
|
private ColorTargetState _element0;
|
|
}
|
|
|
|
private struct DrawState
|
|
{
|
|
public ColorTargetStates Colors;
|
|
public uint ColorCount;
|
|
public DepthAttachmentState Depth;
|
|
public bool PixelActive;
|
|
public RenderingState Rendering;
|
|
public GraphicsPrograms Programs;
|
|
|
|
public static DrawState Create() => new() { PixelActive = true, Programs = null! };
|
|
}
|
|
|
|
private static ReadOnlySpan<ColorTargetState> BoundColors(ref DrawState state) =>
|
|
((ReadOnlySpan<ColorTargetState>)state.Colors)[..(int)state.ColorCount];
|
|
|
|
public void DrawIndexed(ulong submitId, RegisterBanks banks, in DrawIndexedArguments arguments)
|
|
{
|
|
if (arguments.IndirectArgumentsAddress != 0 && !CanDrawIndirectOnGpu(banks, in arguments))
|
|
{
|
|
DrawIndexedWithCpuArguments(submitId, banks, in arguments);
|
|
return;
|
|
}
|
|
|
|
DrawIndexedCore(submitId, banks, in arguments);
|
|
}
|
|
|
|
// The GPU reads the arguments of an indirect draw unless the draw is emulated from
|
|
// its counts: strips (metadata clear quads), legacy primitives, 8-bit indices and
|
|
// restart indices converted on the CPU.
|
|
private static bool CanDrawIndirectOnGpu(RegisterBanks banks, in DrawIndexedArguments arguments)
|
|
{
|
|
switch ((GuestPrimitiveType)banks.UserConfig.PrimitiveType)
|
|
{
|
|
case GuestPrimitiveType.PointList:
|
|
case GuestPrimitiveType.LineList:
|
|
case GuestPrimitiveType.LineStrip:
|
|
case GuestPrimitiveType.TriangleList:
|
|
case GuestPrimitiveType.TriangleFan:
|
|
case GuestPrimitiveType.Polygon:
|
|
case GuestPrimitiveType.RectangleList:
|
|
break;
|
|
default:
|
|
return false;
|
|
}
|
|
|
|
var index32 = (GuestIndexType)arguments.IndexTypeAndSize == GuestIndexType.Index32;
|
|
if (!index32 && (GuestIndexType)arguments.IndexTypeAndSize != GuestIndexType.Index16)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if ((banks.UserConfig.PrimitiveResetControl & 0x1) != 0)
|
|
{
|
|
var indexMask = index32 ? uint.MaxValue : 0xFFFFu;
|
|
if ((banks.Context.PrimitiveResetIndex & indexMask) != indexMask)
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return (ulong)arguments.IndexCount * (index32 ? 4ul : 2ul) <= MaxIndirectIndexBufferBytes;
|
|
}
|
|
|
|
private const ulong MaxIndirectIndexBufferBytes = 64ul << 20;
|
|
|
|
// Reads the indirect arguments now and draws from them, as the interpreter would have.
|
|
private void DrawIndexedWithCpuArguments(ulong submitId, RegisterBanks banks, in DrawIndexedArguments arguments)
|
|
{
|
|
Span<byte> bytes = stackalloc byte[(int)IndexedIndirectArgumentsSize];
|
|
if (!_host.TryReadGuest(arguments.IndirectArgumentsAddress, bytes))
|
|
{
|
|
throw _host.Fatal($"The indirect draw arguments are unreadable: address=0x{arguments.IndirectArgumentsAddress:X16}.");
|
|
}
|
|
|
|
var words = System.Runtime.InteropServices.MemoryMarshal.Cast<byte, uint>(bytes);
|
|
var elementSize = (GuestIndexType)arguments.IndexTypeAndSize switch
|
|
{
|
|
GuestIndexType.Index16 => 2ul,
|
|
GuestIndexType.Index32 => 4ul,
|
|
_ => 1ul,
|
|
};
|
|
var resolved = arguments with
|
|
{
|
|
IndexCount = arguments.UnboundedIndexBuffer ? words[0] : Math.Min(words[0], arguments.IndexCount),
|
|
InstanceCount = words[1],
|
|
IndexAddress = arguments.IndexAddress + (words[2] * elementSize),
|
|
BaseVertex = unchecked((int)words[3]),
|
|
FirstInstance = words[4],
|
|
IndirectArgumentsAddress = 0,
|
|
UnboundedIndexBuffer = false,
|
|
};
|
|
if (resolved.IndexCount == 0 || resolved.InstanceCount == 0)
|
|
{
|
|
_host.ResetBindings();
|
|
return;
|
|
}
|
|
|
|
DrawIndexedCore(submitId, banks, in resolved);
|
|
}
|
|
|
|
private void DrawIndexedCore(ulong submitId, RegisterBanks banks, in DrawIndexedArguments arguments)
|
|
{
|
|
using var profileScope = RenderPhaseProfile.MeasureDetail(RenderPhaseProfile.Phase.DrawExecutor);
|
|
if (!_host.IsRecording)
|
|
{
|
|
throw _host.Fatal("An indexed draw has no recording command buffer.");
|
|
}
|
|
|
|
if (arguments.OffsetSource == DrawOffsetSource.Packet && arguments.FirstInstance != 0)
|
|
{
|
|
throw _host.Fatal($"A packet indexed draw carries a first instance: firstInstance={arguments.FirstInstance}.");
|
|
}
|
|
|
|
_host.RunPendingOperations();
|
|
var userConfig = banks.UserConfig;
|
|
var shader = banks.Shader;
|
|
_host.SetDebugInformation(RecordedOperation.DrawIndex, submitId, arguments.IndexCount, 0, 1, arguments.InstanceCount, arguments.IndexAddress);
|
|
if (arguments.IndexCount == 0 || arguments.InstanceCount == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (ConsumesColorMetadataOperation(banks.Context) || TryDepthStencilCopy(banks.Context))
|
|
{
|
|
_host.ResetBindings();
|
|
return;
|
|
}
|
|
|
|
if (!HasValidVertexShader(shader) || IsUnsupportedGeometryStage(banks))
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (RenderTrace.Enabled)
|
|
{
|
|
RenderTrace.Write(
|
|
$"DrawIndexed submit={submitId} indexTypeAndSize=0x{arguments.IndexTypeAndSize:X8} count=0x{arguments.IndexCount:X8} " +
|
|
$"indexAddress=0x{arguments.IndexAddress:X16} instances=0x{arguments.InstanceCount:X8} baseVertex=0x{(uint)arguments.BaseVertex:X8} firstInstance=0x{arguments.FirstInstance:X8}");
|
|
}
|
|
|
|
ValidateDrawRegisters(banks);
|
|
if (!ResolveTopology(userConfig, autoDraw: false, out var topology))
|
|
{
|
|
return;
|
|
}
|
|
|
|
var primitiveRestart = ResolvePrimitiveRestart(banks, topology, arguments.IndexTypeAndSize);
|
|
IndexType indexType;
|
|
ulong indexSize;
|
|
var expandIndex8 = false;
|
|
switch ((GuestIndexType)arguments.IndexTypeAndSize)
|
|
{
|
|
case GuestIndexType.Index16:
|
|
indexType = IndexType.Uint16;
|
|
indexSize = 2ul * arguments.IndexCount;
|
|
break;
|
|
case GuestIndexType.Index32:
|
|
indexType = IndexType.Uint32;
|
|
indexSize = 4ul * arguments.IndexCount;
|
|
break;
|
|
case GuestIndexType.Index8:
|
|
indexType = IndexType.Uint16;
|
|
indexSize = arguments.IndexCount;
|
|
expandIndex8 = true;
|
|
break;
|
|
default:
|
|
throw _host.Fatal($"The index type and size is unknown: indexTypeAndSize={arguments.IndexTypeAndSize}.");
|
|
}
|
|
|
|
var draw = new DrawCall("DrawIndexed", RecordedOperation.DrawIndex, arguments.IndexCount, arguments.InstanceCount, arguments.FirstInstance);
|
|
byte[]? expandedIndices = null;
|
|
if (expandIndex8)
|
|
{
|
|
expandedIndices = ExpandIndex8(arguments.IndexAddress, arguments.IndexCount, primitiveRestart, banks.Context.PrimitiveResetIndex);
|
|
}
|
|
else if (primitiveRestart)
|
|
{
|
|
var indexMask = indexType == IndexType.Uint16 ? 0xFFFFu : uint.MaxValue;
|
|
var restartIndex = banks.Context.PrimitiveResetIndex & indexMask;
|
|
if (restartIndex != indexMask)
|
|
{
|
|
expandedIndices = ConvertRestartIndices(arguments.IndexAddress, arguments.IndexCount, indexType, restartIndex);
|
|
indexType = IndexType.Uint32;
|
|
}
|
|
}
|
|
|
|
var indexSource = new IndexSource(
|
|
true,
|
|
arguments.IndexAddress,
|
|
expandedIndices,
|
|
expandedIndices is null ? indexSize : (ulong)expandedIndices.Length,
|
|
indexType);
|
|
var state = DrawState.Create();
|
|
if (!TryResolveDrawTargets(banks, in draw, ref state))
|
|
{
|
|
_host.ResetBindings();
|
|
return;
|
|
}
|
|
|
|
if (arguments.IndirectArgumentsAddress != 0 && state.ColorCount == 0 && !state.Depth.HasTarget)
|
|
{
|
|
// A targetless draw may be retained and replayed later; it needs its counts.
|
|
DrawIndexedWithCpuArguments(submitId, banks, in arguments);
|
|
return;
|
|
}
|
|
|
|
ResolveShaderPrograms(banks, ref state);
|
|
if (!ApplyProgramAdaptations(banks, in draw, ref state, new TargetlessDrawArguments(submitId, true, arguments, default)))
|
|
{
|
|
_host.ResetBindings();
|
|
return;
|
|
}
|
|
|
|
TraceDrawState(submitId, banks, in draw, in state);
|
|
var indirect = arguments.OffsetSource == DrawOffsetSource.IndirectArguments;
|
|
var vertexOffset = indirect
|
|
? arguments.BaseVertex
|
|
: ResolveVertexOffset(userConfig.IndexOffset, state.Programs.VertexInput) + arguments.BaseVertex;
|
|
var emission = new DrawEmission(
|
|
true,
|
|
vertexOffset,
|
|
0,
|
|
indirect ? arguments.FirstInstance : ResolveInstanceOffset(state.Programs.VertexInput),
|
|
arguments.IndirectArgumentsAddress);
|
|
RecordDraw(submitId, banks, in draw, ref state, topology, in emission, in indexSource, primitiveRestart, setBindDebug: true, setAutoDebug: false);
|
|
_host.ResetBindings();
|
|
}
|
|
|
|
public void DrawAuto(ulong submitId, RegisterBanks banks, in DrawAutoArguments arguments)
|
|
{
|
|
using var profileScope = RenderPhaseProfile.MeasureDetail(RenderPhaseProfile.Phase.DrawExecutor);
|
|
if (!_host.IsRecording)
|
|
{
|
|
throw _host.Fatal("An automatic draw has no recording command buffer.");
|
|
}
|
|
|
|
if (arguments.OffsetSource == DrawOffsetSource.Packet && arguments.FirstInstance != 0)
|
|
{
|
|
throw _host.Fatal($"A packet automatic draw carries a first instance: firstInstance={arguments.FirstInstance}.");
|
|
}
|
|
|
|
_host.RunPendingOperations();
|
|
var userConfig = banks.UserConfig;
|
|
var shader = banks.Shader;
|
|
_host.SetDebugInformation(RecordedOperation.DrawIndexAuto, submitId, arguments.VertexCount, 0, arguments.FirstVertex, arguments.InstanceCount, arguments.FirstInstance);
|
|
if (arguments.VertexCount == 0 || arguments.InstanceCount == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (ConsumesColorMetadataOperation(banks.Context) || TryDepthStencilCopy(banks.Context))
|
|
{
|
|
_host.ResetBindings();
|
|
return;
|
|
}
|
|
|
|
if (!HasValidVertexShader(shader) || IsUnsupportedGeometryStage(banks))
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (RenderTrace.Enabled)
|
|
{
|
|
RenderTrace.Write(
|
|
$"DrawAuto submit={submitId} count=0x{arguments.VertexCount:X8} instances=0x{arguments.InstanceCount:X8} " +
|
|
$"firstVertex=0x{arguments.FirstVertex:X8} firstInstance=0x{arguments.FirstInstance:X8}");
|
|
}
|
|
|
|
ValidateDrawRegisters(banks);
|
|
var draw = new DrawCall("DrawAuto", RecordedOperation.DrawIndexAuto, arguments.VertexCount, arguments.InstanceCount, arguments.FirstInstance);
|
|
var state = DrawState.Create();
|
|
if (!TryResolveDrawTargets(banks, in draw, ref state))
|
|
{
|
|
_host.ResetBindings();
|
|
return;
|
|
}
|
|
|
|
if (!ResolveTopology(userConfig, autoDraw: true, out var topology))
|
|
{
|
|
TraceDrawDisposition(banks, in draw, "no-primitive-topology");
|
|
_host.ResetBindings();
|
|
return;
|
|
}
|
|
|
|
ResolveShaderPrograms(banks, ref state);
|
|
var vertexInput = state.Programs.VertexInput;
|
|
var pixelInput = state.Programs.PixelInput;
|
|
var rectangleList = topology == PrimitiveTopology.PatchList;
|
|
if (rectangleList && vertexInput.Buffers.Length == 0 && vertexInput.Stage.Program?.ParameterExportMask == 0 && pixelInput.InputCount != 0)
|
|
{
|
|
if (RenderTrace.Enabled)
|
|
{
|
|
RenderTrace.Write(
|
|
$"DrawAuto skipped a rectangle list with no vertex parameter exports and pixel inputs: pixelInputs={pixelInput.InputCount} " +
|
|
$"pixel=0x{shader.Pixel.Address:X16} export=0x{shader.Vertex.ExportAddress:X16} geometry=0x{shader.Vertex.GeometryAddress:X16}");
|
|
}
|
|
|
|
_host.ResetBindings();
|
|
return;
|
|
}
|
|
|
|
if (!ApplyProgramAdaptations(banks, in draw, ref state, new TargetlessDrawArguments(submitId, false, default, arguments)))
|
|
{
|
|
_host.ResetBindings();
|
|
return;
|
|
}
|
|
|
|
TraceDrawState(submitId, banks, in draw, in state);
|
|
var indirect = arguments.OffsetSource == DrawOffsetSource.IndirectArguments;
|
|
var vertexOffset = indirect
|
|
? (int)arguments.FirstVertex
|
|
: ResolveVertexOffset(userConfig.IndexOffset, vertexInput) + (int)arguments.FirstVertex;
|
|
var emission = new DrawEmission(
|
|
false,
|
|
0,
|
|
(uint)vertexOffset,
|
|
indirect ? arguments.FirstInstance : ResolveInstanceOffset(vertexInput));
|
|
RecordDraw(submitId, banks, in draw, ref state, topology, in emission, default, primitiveRestart: false, setBindDebug: false, setAutoDebug: true);
|
|
_host.ResetBindings();
|
|
}
|
|
|
|
private byte[] ConvertRestartIndices(ulong indexAddress, uint indexCount, IndexType indexType, uint restartIndex)
|
|
{
|
|
var sourceStride = indexType == IndexType.Uint16 ? sizeof(ushort) : sizeof(uint);
|
|
var source = new byte[checked((int)indexCount * sourceStride)];
|
|
if (indexAddress == 0 || !_host.TryReadGuest(indexAddress, source))
|
|
{
|
|
throw _host.Fatal($"The restart index data is unreadable: address=0x{indexAddress:X16} count={indexCount}.");
|
|
}
|
|
|
|
// Widen 16-bit indices so their maximum value remains an ordinary vertex.
|
|
var converted = new byte[checked((int)indexCount * sizeof(uint))];
|
|
for (var index = 0; index < (int)indexCount; index++)
|
|
{
|
|
var value = sourceStride == sizeof(ushort)
|
|
? BinaryPrimitives.ReadUInt16LittleEndian(source.AsSpan(index * sourceStride))
|
|
: BinaryPrimitives.ReadUInt32LittleEndian(source.AsSpan(index * sourceStride));
|
|
if (value == uint.MaxValue)
|
|
{
|
|
throw _host.Fatal("A custom restart index conflicts with a 32-bit vertex index of 0xFFFFFFFF.");
|
|
}
|
|
|
|
BinaryPrimitives.WriteUInt32LittleEndian(converted.AsSpan(index * sizeof(uint)),
|
|
value == restartIndex ? uint.MaxValue : value);
|
|
}
|
|
|
|
return converted;
|
|
}
|
|
|
|
private byte[] ExpandIndex8(ulong indexAddress, uint indexCount, bool primitiveRestart, uint restartIndex)
|
|
{
|
|
if (indexAddress == 0)
|
|
{
|
|
throw _host.Fatal($"An 8-bit index draw has no index address: count={indexCount}.");
|
|
}
|
|
|
|
var source = new byte[indexCount];
|
|
if (!_host.TryReadGuest(indexAddress, source))
|
|
{
|
|
throw _host.Fatal($"The 8-bit index data is unreadable: address=0x{indexAddress:X16} count={indexCount}.");
|
|
}
|
|
|
|
var expanded = new byte[indexCount * 2];
|
|
for (var i = 0; i < indexCount; i++)
|
|
{
|
|
var value = source[i];
|
|
ushort index = primitiveRestart && value == (restartIndex & 0xFF) ? (ushort)0xFFFF : value;
|
|
expanded[i * 2] = (byte)index;
|
|
expanded[(i * 2) + 1] = (byte)(index >> 8);
|
|
}
|
|
|
|
return expanded;
|
|
}
|
|
|
|
// Vertex offset: the index offset register, or the embedded fetch scalar when that register is zero.
|
|
public static int ResolveVertexOffset(uint indexOffset, VertexInputInfo vertexInput)
|
|
{
|
|
if (indexOffset != 0 || !vertexInput.FetchEmbedded)
|
|
{
|
|
return (int)indexOffset;
|
|
}
|
|
|
|
var program = vertexInput.Stage.Program ?? throw SubmissionScheduler.Fatal("The embedded fetch vertex input has no program.");
|
|
return (int)ReadUserDataScalar(program, vertexInput.Stage.Resources, program.VertexOffsetScalarRegister);
|
|
}
|
|
|
|
public static uint ResolveInstanceOffset(VertexInputInfo vertexInput)
|
|
{
|
|
if (!vertexInput.FetchEmbedded)
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
var program = vertexInput.Stage.Program ?? throw SubmissionScheduler.Fatal("The embedded fetch vertex input has no program.");
|
|
return ReadUserDataScalar(program, vertexInput.Stage.Resources, program.InstanceOffsetScalarRegister);
|
|
}
|
|
|
|
private static uint ReadUserDataScalar(ShaderProgramInfo program, ResourceSnapshot resources, int scalarRegister)
|
|
{
|
|
if (scalarRegister >= (int)program.UserDataBase)
|
|
{
|
|
var index = (uint)scalarRegister - program.UserDataBase;
|
|
if (index < resources.UserData.Length)
|
|
{
|
|
return resources.UserData[index];
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
private static bool HasValidVertexShader(ShaderProgramRegisters shader) => shader.Vertex.ExportAddress != 0;
|
|
|
|
private static bool IsKnownGeometryOutputPrimitiveType(uint value) => value <= 4;
|
|
|
|
// Only the plain vertex path and the primitive-shader vertex path with default geometry state run.
|
|
private static bool IsUnsupportedGeometryStage(RegisterBanks banks)
|
|
{
|
|
var context = banks.Context;
|
|
var shaderInterface = context.ShaderInterface;
|
|
var vertex = banks.Shader.Vertex;
|
|
var stages = context.ShaderStages;
|
|
var primitiveShaderVertexPath =
|
|
IsPrimitiveShaderStageMask(stages) && vertex.ExportAddress != 0 &&
|
|
(vertex.GeometryAddress == 0 ||
|
|
(shaderInterface.GeometryMaxVerticesOut == 0 && IsKnownGeometryOutputPrimitiveType(shaderInterface.GeometryOutputPrimitiveType)));
|
|
var unsupportedStageMask = stages != 0 && !IsPrimitiveShaderStageMask(stages);
|
|
var unsupportedGeometryStage = vertex.ExportAddress != 0 && vertex.GeometryAddress != 0 && !primitiveShaderVertexPath;
|
|
var geometryRegisters = !primitiveShaderVertexPath &&
|
|
((shaderInterface.PrimitiveShaderSubgroupControl != 0 && shaderInterface.PrimitiveShaderSubgroupControl != 1) ||
|
|
shaderInterface.GeometryMaxVerticesOut != 0 ||
|
|
!IsKnownGeometryOutputPrimitiveType(shaderInterface.GeometryOutputPrimitiveType) ||
|
|
shaderInterface.MaxOutputPerSubgroup > MaxOutputPerSubgroupLimit);
|
|
if (!unsupportedStageMask && !unsupportedGeometryStage && !geometryRegisters)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (Interlocked.Exchange(ref _geometryWarningShown, 1) == 0)
|
|
{
|
|
Console.Error.WriteLine(
|
|
"Warning: the title uses unsupported graphics pipelines; some draw calls were skipped. " +
|
|
$"stages=0x{stages:X8} subgroup=0x{shaderInterface.PrimitiveShaderSubgroupControl:X8} " +
|
|
$"maxOutput=0x{shaderInterface.MaxOutputPerSubgroup:X8} " +
|
|
$"maxVerticesOut=0x{shaderInterface.GeometryMaxVerticesOut:X8} " +
|
|
$"outputPrimitive=0x{shaderInterface.GeometryOutputPrimitiveType:X8} " +
|
|
$"export=0x{vertex.ExportAddress:X16} geometry=0x{vertex.GeometryAddress:X16} " +
|
|
$"unsupportedMask={unsupportedStageMask} unsupportedGeometry={unsupportedGeometryStage} " +
|
|
$"unsupportedRegisters={geometryRegisters}.");
|
|
}
|
|
|
|
if (RenderTrace.Enabled && RenderTrace.GeometryStageSkip())
|
|
{
|
|
var geometryControl = banks.UserConfig.GeometryEngineControl;
|
|
RenderTrace.Write(
|
|
$"Skipping an unsupported geometry stage draw: stages=0x{stages:X8} primitiveGroup=0x{geometryControl.PrimitiveGroupSize:X4} " +
|
|
$"vertexGroup=0x{geometryControl.VertexGroupSize:X4} subgroupControl=0x{shaderInterface.PrimitiveShaderSubgroupControl:X8} " +
|
|
$"maxOutput=0x{shaderInterface.MaxOutputPerSubgroup:X8} maxVerticesOut=0x{shaderInterface.GeometryMaxVerticesOut:X8} " +
|
|
$"outputPrimitive=0x{shaderInterface.GeometryOutputPrimitiveType:X8} export=0x{vertex.ExportAddress:X16} geometry=0x{vertex.GeometryAddress:X16}");
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private static bool PixelShaderHasDepthOrCoverageSideEffects(ShaderInterfaceRegisters shaderInterface)
|
|
{
|
|
var control = shaderInterface.DepthShaderControl;
|
|
// The export format describes data; it does not enable shader execution.
|
|
return control.KillEnable || control.DepthExportEnable ||
|
|
control.MaskExportEnable || control.DualExportEnable || control.ExecuteOnNoop;
|
|
}
|
|
|
|
private static bool HasActivePixelShader(RegisterBanks banks)
|
|
{
|
|
var context = banks.Context;
|
|
var shaderInterface = context.ShaderInterface;
|
|
var hasColorOutput = (context.RenderTargetMask & shaderInterface.ColorShaderMask) != 0;
|
|
return banks.Shader.Pixel.Address != 0 && (hasColorOutput || PixelShaderHasDepthOrCoverageSideEffects(shaderInterface));
|
|
}
|
|
|
|
private const byte ColorModeEliminateFastClear = 2;
|
|
private const byte ColorModeResolve = 3;
|
|
private const byte ColorModeFmaskDecompress = 5;
|
|
private const byte ColorModeDccDecompress = 6;
|
|
|
|
// These modes run color metadata operations; the shader output is not a normal draw.
|
|
public static bool ConsumesColorMetadataOperation(ContextRegisters context)
|
|
{
|
|
var mode = context.ColorControl.Mode;
|
|
return mode is ColorModeEliminateFastClear or ColorModeFmaskDecompress or ColorModeDccDecompress;
|
|
}
|
|
|
|
public bool ResolveTopology(UserConfigRegisters userConfig, bool autoDraw, out PrimitiveTopology topology)
|
|
{
|
|
topology = PrimitiveTopology.PointList;
|
|
switch ((GuestPrimitiveType)userConfig.PrimitiveType)
|
|
{
|
|
case GuestPrimitiveType.None:
|
|
return false;
|
|
case GuestPrimitiveType.PointList:
|
|
topology = PrimitiveTopology.PointList;
|
|
break;
|
|
case GuestPrimitiveType.LineList:
|
|
topology = PrimitiveTopology.LineList;
|
|
break;
|
|
case GuestPrimitiveType.LineStrip:
|
|
topology = PrimitiveTopology.LineStrip;
|
|
break;
|
|
case GuestPrimitiveType.TriangleList:
|
|
topology = PrimitiveTopology.TriangleList;
|
|
break;
|
|
case GuestPrimitiveType.TriangleFan:
|
|
case GuestPrimitiveType.Polygon:
|
|
topology = PrimitiveTopology.TriangleFan;
|
|
break;
|
|
case GuestPrimitiveType.TriangleStrip:
|
|
topology = PrimitiveTopology.TriangleStrip;
|
|
break;
|
|
case GuestPrimitiveType.RectangleList:
|
|
topology = PrimitiveTopology.PatchList;
|
|
break;
|
|
case GuestPrimitiveType.RectangleListLegacy:
|
|
if (!autoDraw)
|
|
{
|
|
throw _host.Fatal($"The primitive type is unknown for an indexed draw: primitiveType={userConfig.PrimitiveType}.");
|
|
}
|
|
|
|
topology = PrimitiveTopology.TriangleStrip;
|
|
break;
|
|
case GuestPrimitiveType.QuadListLegacy:
|
|
topology = PrimitiveTopology.TriangleFan;
|
|
break;
|
|
default:
|
|
throw _host.Fatal($"The primitive type is unknown: primitiveType={userConfig.PrimitiveType}.");
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
public bool ResolvePrimitiveRestart(RegisterBanks banks, PrimitiveTopology topology, uint indexTypeAndSize)
|
|
{
|
|
var userConfig = banks.UserConfig;
|
|
var control = userConfig.PrimitiveResetControl;
|
|
if ((control & ~0x3u) != 0)
|
|
{
|
|
throw _host.Fatal($"The primitive reset control has unsupported bits: control=0x{control:X8}.");
|
|
}
|
|
|
|
if ((control & 0x1) == 0)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
switch ((GuestPrimitiveType)userConfig.PrimitiveType)
|
|
{
|
|
case GuestPrimitiveType.LineStrip:
|
|
case GuestPrimitiveType.TriangleFan:
|
|
case GuestPrimitiveType.TriangleStrip:
|
|
break;
|
|
default:
|
|
return false;
|
|
}
|
|
|
|
if (topology is not (PrimitiveTopology.LineStrip or PrimitiveTopology.TriangleStrip or PrimitiveTopology.TriangleFan))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var indexMask = (GuestIndexType)indexTypeAndSize switch
|
|
{
|
|
GuestIndexType.Index8 => 0xFFu,
|
|
GuestIndexType.Index16 => 0xFFFFu,
|
|
GuestIndexType.Index32 => 0xFFFF_FFFFu,
|
|
_ => throw _host.Fatal($"The index type and size is unknown: indexTypeAndSize={indexTypeAndSize}."),
|
|
};
|
|
var resetIndex = banks.Context.PrimitiveResetIndex;
|
|
if ((control & 0x2) != 0 && (resetIndex & ~indexMask) != 0)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private void ResolveShaderPrograms(RegisterBanks banks, ref DrawState state)
|
|
{
|
|
using var profileScope = RenderPhaseProfile.MeasureDetail(RenderPhaseProfile.Phase.DrawProgramResolution);
|
|
var inputs = GraphicsProgramInputs.Create();
|
|
Span<ColorComponentMap> targetExportMapping = inputs.Mapping;
|
|
foreach (ref readonly var color in BoundColors(ref state))
|
|
{
|
|
targetExportMapping[(int)color.Slot] = color.Resolution.ExportMapping;
|
|
}
|
|
|
|
inputs.PixelActive = state.PixelActive;
|
|
inputs.DepthBound = state.Depth.HasTarget;
|
|
if (Prefetch is { } prefetch && prefetch.TryTakeGraphics(banks, in inputs, out var prefetched))
|
|
{
|
|
state.Programs = prefetched;
|
|
return;
|
|
}
|
|
|
|
state.Programs = ResolveGraphicsPrograms(banks, in inputs);
|
|
}
|
|
|
|
private static void TraceDrawState(ulong submitId, RegisterBanks banks, in DrawCall draw, in DrawState state)
|
|
{
|
|
if (!RenderTrace.Enabled)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var shader = banks.Shader;
|
|
var colorAddress = state.ColorCount != 0 ? state.Colors[0].Resolution.BaseAddress : 0;
|
|
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} " +
|
|
$"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}");
|
|
}
|
|
}
|