412 lines
15 KiB
C#
412 lines
15 KiB
C#
// Copyright (C) 2026 SharpEmu Emulator Project
|
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
|
|
using SharpEmu.Libs.Gpu.GpuCommands.Registers;
|
|
using SharpEmu.Libs.Gpu.Images;
|
|
using SharpEmu.Libs.Gpu.Scheduling;
|
|
using SharpEmu.Libs.VideoOut;
|
|
using Silk.NET.Vulkan;
|
|
|
|
namespace SharpEmu.Libs.Gpu.Rendering;
|
|
|
|
public sealed partial class RenderExecutor
|
|
{
|
|
private const uint TransientIndexAlignment = 16;
|
|
|
|
private struct VertexBufferRange
|
|
{
|
|
public ulong BaseAddress;
|
|
public ulong RequestedEnd;
|
|
public ulong AcquiredEnd;
|
|
public BufferBinding Binding;
|
|
|
|
public readonly ulong RequestedSize => RequestedEnd - BaseAddress;
|
|
}
|
|
|
|
private readonly record struct PreparedIndexBuffer(BufferBinding Binding, ulong Size, IndexType Type);
|
|
|
|
// Merges the vertex ranges, obtains one host buffer per merged range and offsets every slot into it.
|
|
private BufferBinding[] AcquireVertexBuffers(VertexInputInfo vertexInput)
|
|
{
|
|
using var profileScope = RenderPhaseProfile.MeasureDetail(RenderPhaseProfile.Phase.DrawVertexBufferAcquisition);
|
|
var buffers = vertexInput.Buffers;
|
|
if (buffers.Length > VertexInputInfo.MaxBuffers)
|
|
{
|
|
throw _host.Fatal($"The vertex input has too many buffers: count={buffers.Length} max={VertexInputInfo.MaxBuffers}.");
|
|
}
|
|
|
|
Span<VertexBufferRange> ranges = stackalloc VertexBufferRange[VertexInputInfo.MaxBuffers];
|
|
var rangeCount = 0;
|
|
foreach (ref readonly var vertex in buffers.AsSpan())
|
|
{
|
|
var size = vertex.Size;
|
|
if (size == 0)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (vertex.Address == 0 || size > ulong.MaxValue - vertex.Address)
|
|
{
|
|
throw _host.Fatal($"The vertex buffer range is invalid: address=0x{vertex.Address:X16} size=0x{size:X16}.");
|
|
}
|
|
|
|
ranges[rangeCount++] = new VertexBufferRange { BaseAddress = vertex.Address, RequestedEnd = vertex.Address + size };
|
|
}
|
|
|
|
ranges[..rangeCount].Sort(static (left, right) => left.BaseAddress.CompareTo(right.BaseAddress));
|
|
Span<VertexBufferRange> merged = stackalloc VertexBufferRange[VertexInputInfo.MaxBuffers];
|
|
var mergedCount = 0;
|
|
for (var i = 0; i < rangeCount; i++)
|
|
{
|
|
ref readonly var range = ref ranges[i];
|
|
if (mergedCount != 0 && merged[mergedCount - 1].RequestedEnd >= range.BaseAddress)
|
|
{
|
|
merged[mergedCount - 1].RequestedEnd = Math.Max(merged[mergedCount - 1].RequestedEnd, range.RequestedEnd);
|
|
continue;
|
|
}
|
|
|
|
merged[mergedCount++] = new VertexBufferRange { BaseAddress = range.BaseAddress, RequestedEnd = range.RequestedEnd };
|
|
}
|
|
|
|
for (var i = 0; i < mergedCount; i++)
|
|
{
|
|
ref var range = ref merged[i];
|
|
var size = _host.ClampMappedSize(range.BaseAddress, range.RequestedSize);
|
|
range.AcquiredEnd = range.BaseAddress + size;
|
|
range.Binding = _host.ObtainBuffer(range.BaseAddress, size, isWritten: false);
|
|
}
|
|
|
|
var prepared = new BufferBinding[buffers.Length];
|
|
BufferBinding? nullBuffer = null;
|
|
for (var slot = 0; slot < buffers.Length; slot++)
|
|
{
|
|
ref readonly var vertex = ref buffers[slot];
|
|
if (vertex.Size == 0)
|
|
{
|
|
nullBuffer ??= _host.NullBuffer;
|
|
prepared[slot] = nullBuffer.Value;
|
|
continue;
|
|
}
|
|
|
|
var found = -1;
|
|
for (var i = 0; i < mergedCount; i++)
|
|
{
|
|
if (vertex.Address >= merged[i].BaseAddress && vertex.Address < merged[i].AcquiredEnd)
|
|
{
|
|
found = i;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (found < 0)
|
|
{
|
|
throw _host.Fatal($"The vertex buffer address is outside the acquired range: address=0x{vertex.Address:X16}.");
|
|
}
|
|
|
|
ref readonly var owner = ref merged[found];
|
|
prepared[slot] = new BufferBinding(owner.Binding.Handle, owner.Binding.Offset + vertex.Address - owner.BaseAddress);
|
|
}
|
|
|
|
return prepared;
|
|
}
|
|
|
|
private PreparedIndexBuffer AcquireIndexBuffer(in IndexSource source)
|
|
{
|
|
using var profileScope = RenderPhaseProfile.MeasureDetail(RenderPhaseProfile.Phase.DrawIndexBufferAcquisition);
|
|
if (!source.Enabled)
|
|
{
|
|
return default;
|
|
}
|
|
|
|
if (source.Size == 0)
|
|
{
|
|
throw _host.Fatal($"The index buffer is empty: address=0x{source.Address:X16} type={(int)source.Type}.");
|
|
}
|
|
|
|
var binding = source.HostData is { } hostData
|
|
? _host.UploadTransient(hostData, TransientIndexAlignment)
|
|
: _host.ObtainBuffer(source.Address, source.Size, isWritten: false);
|
|
return new PreparedIndexBuffer(binding, source.Size, source.Type);
|
|
}
|
|
|
|
// A written buffer resource with an address and a footprint needs a barrier after the stage.
|
|
// Device-address accesses are not split into reads and writes, so they count as stores.
|
|
private bool DrawWritesMemory(ShaderStageResources stage) =>
|
|
stage.Program is { } program &&
|
|
(program.UsesDeviceAddresses || WritesStorageImage(program) || HasBufferWrites(stage));
|
|
|
|
private bool HasBufferWrites(ShaderStageResources stage)
|
|
{
|
|
var program = stage.Program ?? throw _host.Fatal("A shader stage has no program.");
|
|
var descriptors = stage.Resources.Buffers;
|
|
if (descriptors.Length != program.Buffers.Length)
|
|
{
|
|
throw _host.Fatal($"The buffer descriptor count does not match the program: descriptors={descriptors.Length} program={program.Buffers.Length}.");
|
|
}
|
|
|
|
for (var i = 0; i < program.Buffers.Length; i++)
|
|
{
|
|
if (!program.Buffers[i].Written)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (descriptors[i].Length < 4)
|
|
{
|
|
throw _host.Fatal($"A written buffer descriptor is too short: index={i} words={descriptors[i].Length}.");
|
|
}
|
|
|
|
var descriptor = BufferDescriptorWords.From(descriptors[i]);
|
|
var footprint = descriptor.Footprint() ?? throw _host.Fatal(
|
|
$"The written buffer footprint overflows: index={i} stride={descriptor.Stride} records={descriptor.RecordCount}.");
|
|
if (descriptor.Address != 0 && footprint != 0)
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private void SetDrawDebugPhase(ulong submitId, in DrawCall draw, uint phase) =>
|
|
_host.SetDebugInformation(draw.Operation, submitId, phase, draw.Count, 0, draw.InstanceCount, draw.FirstInstance);
|
|
|
|
private IPreparedBindings PrepareBindings(in ShaderStageResources stage)
|
|
{
|
|
var bindings = _host.PrepareBindings(stage);
|
|
NoteSampledTextures(stage);
|
|
return bindings;
|
|
}
|
|
|
|
private static void NoteSampledTextures(in ShaderStageResources stage)
|
|
{
|
|
if (stage.Program is not { } program)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var descriptors = stage.Resources.Images;
|
|
for (var index = 0; index < program.Images.Length && index < descriptors.Length; index++)
|
|
{
|
|
if (program.Images[index].Class == ImageResourceClass.Sampled)
|
|
{
|
|
MipStatistics.Shared.NoteSampled(new TextureDescriptorWords(descriptors[index]));
|
|
}
|
|
}
|
|
}
|
|
|
|
// Binds everything the draw needs inside one preparation scope, then records it.
|
|
private void RecordDraw(
|
|
ulong submitId,
|
|
RegisterBanks banks,
|
|
in DrawCall draw,
|
|
ref DrawState state,
|
|
PrimitiveTopology topology,
|
|
in DrawEmission emission,
|
|
in IndexSource indexSource,
|
|
bool primitiveRestart,
|
|
bool setBindDebug,
|
|
bool setAutoDebug)
|
|
{
|
|
using var profileScope = RenderPhaseProfile.MeasureDetail(RenderPhaseProfile.Phase.DrawResourcePreparation);
|
|
var context = banks.Context;
|
|
var vertexInput = state.Programs.VertexInput;
|
|
var pixelInput = state.Programs.PixelInput;
|
|
using var preparation = _host.BeginPreparation();
|
|
IPreparedBindings vertexBindings;
|
|
IPreparedBindings? pixelBindings;
|
|
try
|
|
{
|
|
vertexBindings = PrepareBindings(vertexInput.Stage);
|
|
pixelBindings = state.PixelActive ? PrepareBindings(pixelInput.Stage) : null;
|
|
}
|
|
catch (DrawImageTypeMismatchException rejection)
|
|
{
|
|
if (_strictDrawResources)
|
|
{
|
|
throw _host.Fatal(rejection.Message);
|
|
}
|
|
|
|
if (_reportedDrawImageTypeMismatches.Add(rejection.WarningKey))
|
|
{
|
|
Console.Error.WriteLine($"[GPU][WARN][DRAW_SKIPPED] {rejection.Message} " +
|
|
"The draw was not executed. Images and FPS can be incorrect. Set SHARPEMU_STRICT_COMPUTE=1 to stop on this failure.");
|
|
}
|
|
|
|
return;
|
|
}
|
|
var vertexProgram = vertexInput.Stage.Program ?? throw _host.Fatal("The vertex stage has no program.");
|
|
var pixelProgram = pixelBindings is null ? null : pixelInput.Stage.Program ?? throw _host.Fatal("The pixel stage has no program.");
|
|
if (vertexProgram.UsesDeviceAddresses || (pixelProgram?.UsesDeviceAddresses ?? false))
|
|
{
|
|
_host.PrepareDeviceAddresses();
|
|
}
|
|
|
|
_host.BindResources(vertexBindings);
|
|
if (pixelBindings is not null)
|
|
{
|
|
_host.BindResources(pixelBindings);
|
|
}
|
|
|
|
var vertexBuffers = AcquireVertexBuffers(vertexInput);
|
|
var indexBuffer = AcquireIndexBuffer(in indexSource);
|
|
var indirectArguments = emission.IndirectArgumentsAddress != 0
|
|
? _host.ObtainBuffer(emission.IndirectArgumentsAddress, IndexedIndirectArgumentsSize, isWritten: false)
|
|
: default;
|
|
DropUnwrittenColorTargets(context, ref state, pixelProgram);
|
|
state.Rendering = AcquireAttachments(ref state);
|
|
// Nothing after the pipeline touches guest memory.
|
|
var pipeline = _pipelines.CreateGraphicsPipeline(
|
|
BoundColors(ref state),
|
|
in state.Depth,
|
|
vertexInput,
|
|
state.PixelActive ? pixelInput : null,
|
|
context,
|
|
in state.Rendering,
|
|
topology,
|
|
primitiveRestart,
|
|
state.Programs.DisableBlending,
|
|
state.Programs.Vertex,
|
|
state.Programs.Pixel);
|
|
if (setBindDebug)
|
|
{
|
|
SetDrawDebugPhase(submitId, in draw, 0x100);
|
|
}
|
|
|
|
if (setAutoDebug)
|
|
{
|
|
SetDrawDebugPhase(submitId, in draw, 0x200);
|
|
}
|
|
|
|
_host.BindVertexBuffers(vertexBuffers, vertexInput);
|
|
|
|
if (pixelBindings is not null && setAutoDebug)
|
|
{
|
|
SetDrawDebugPhase(submitId, in draw, 0x300);
|
|
}
|
|
|
|
Span<IPreparedBindings> stages = pixelBindings is null ? [vertexBindings] : [vertexBindings, pixelBindings];
|
|
_host.CommitBindings(PipelineBindPoint.Graphics, in pipeline, stages);
|
|
if (indexBuffer.Size != 0)
|
|
{
|
|
_host.BindIndexBuffer(indexBuffer.Binding, indexBuffer.Type);
|
|
}
|
|
|
|
_host.SetDynamicState(BuildDynamicState(context, in state));
|
|
if (setAutoDebug)
|
|
{
|
|
SetDrawDebugPhase(submitId, in draw, 0x400);
|
|
}
|
|
|
|
if (DrawWritesMemory(vertexInput.Stage) || (pixelBindings is not null && DrawWritesMemory(pixelInput.Stage)))
|
|
{
|
|
_host.PrepareMemoryWritingDraw();
|
|
}
|
|
|
|
_host.BeginRendering(in state.Rendering);
|
|
_host.BindPipeline(PipelineBindPoint.Graphics, in pipeline);
|
|
if (setAutoDebug)
|
|
{
|
|
SetDrawDebugPhase(submitId, in draw, 0x500);
|
|
}
|
|
|
|
if (emission.IndirectArgumentsAddress != 0)
|
|
{
|
|
// Uploads and shader writes end with barriers to all commands, so the
|
|
// indirect read sees them.
|
|
_host.DrawIndexedIndirect(indirectArguments);
|
|
}
|
|
else
|
|
{
|
|
EmitDraw(banks.UserConfig, vertexInput, in draw, in emission);
|
|
}
|
|
if (setAutoDebug)
|
|
{
|
|
SetDrawDebugPhase(submitId, in draw, 0x600);
|
|
}
|
|
|
|
var writeStages = PipelineStageFlags.None;
|
|
if (HasBufferWrites(vertexInput.Stage))
|
|
{
|
|
writeStages |= PipelineStageFlags.VertexShaderBit;
|
|
}
|
|
|
|
if (state.PixelActive && HasBufferWrites(pixelInput.Stage))
|
|
{
|
|
writeStages |= PipelineStageFlags.FragmentShaderBit;
|
|
}
|
|
|
|
if (writeStages != PipelineStageFlags.None)
|
|
{
|
|
_host.EndRendering();
|
|
_host.ShaderWriteBarrier(writeStages);
|
|
}
|
|
|
|
if (setAutoDebug)
|
|
{
|
|
SetDrawDebugPhase(submitId, in draw, 0x700);
|
|
}
|
|
}
|
|
|
|
private const ulong IndexedIndirectArgumentsSize = 20;
|
|
|
|
private void EmitDraw(UserConfigRegisters userConfig, VertexInputInfo vertexInput, in DrawCall draw, in DrawEmission emission)
|
|
{
|
|
switch ((GuestPrimitiveType)userConfig.PrimitiveType)
|
|
{
|
|
case GuestPrimitiveType.PointList:
|
|
case GuestPrimitiveType.LineList:
|
|
case GuestPrimitiveType.LineStrip:
|
|
case GuestPrimitiveType.TriangleList:
|
|
case GuestPrimitiveType.TriangleFan:
|
|
case GuestPrimitiveType.TriangleStrip:
|
|
case GuestPrimitiveType.Polygon:
|
|
case GuestPrimitiveType.RectangleList:
|
|
if (emission.Indexed)
|
|
{
|
|
_host.DrawIndexed(draw.Count, draw.InstanceCount, 0, emission.VertexOffset, emission.FirstInstance);
|
|
}
|
|
else
|
|
{
|
|
_host.Draw(draw.Count, draw.InstanceCount, emission.FirstVertex, emission.FirstInstance);
|
|
}
|
|
|
|
break;
|
|
case GuestPrimitiveType.RectangleListLegacy:
|
|
if (emission.Indexed)
|
|
{
|
|
throw _host.Fatal($"The primitive type is unknown for an indexed draw: primitiveType={userConfig.PrimitiveType}.");
|
|
}
|
|
|
|
if (draw.Count != 3 || vertexInput.Buffers.Length != 0)
|
|
{
|
|
throw _host.Fatal($"A legacy rectangle list needs three vertices and no vertex buffers: count={draw.Count} buffers={vertexInput.Buffers.Length}.");
|
|
}
|
|
|
|
_host.Draw(4, draw.InstanceCount, emission.FirstVertex, emission.FirstInstance);
|
|
break;
|
|
case GuestPrimitiveType.QuadListLegacy:
|
|
if ((draw.Count & 0x3) != 0)
|
|
{
|
|
throw _host.Fatal($"A legacy quad list count is not a multiple of four: count={draw.Count}.");
|
|
}
|
|
|
|
for (var i = 0u; i < draw.Count; i += 4)
|
|
{
|
|
if (emission.Indexed)
|
|
{
|
|
_host.DrawIndexed(4, draw.InstanceCount, i, emission.VertexOffset, emission.FirstInstance);
|
|
}
|
|
else
|
|
{
|
|
_host.Draw(4, draw.InstanceCount, i + emission.FirstVertex, emission.FirstInstance);
|
|
}
|
|
}
|
|
|
|
break;
|
|
default:
|
|
throw _host.Fatal($"The primitive type is unknown: primitiveType={userConfig.PrimitiveType}.");
|
|
}
|
|
}
|
|
}
|