[AGC/Vulkan] Support multiple render targets (#149)

* [AGC] Support multiple typed pixel outputs

Emit dense float, uint, and sint fragment outputs for sparse guest MRT slots. Preserve disabled components across partial exports, validate dense host locations, and retain the single-output compiler overload for compatibility.

* [Vulkan] Execute translated draws with multiple color attachments

Carry every active color target and its effective shader/register write mask through one Vulkan draw. Add per-attachment blending, independentBlend negotiation, device/format validation, multi-attachment synchronization, and safe image recreation after in-flight work completes.

* [ShaderDump] Add MRT edge-case coverage

Cover sparse mixed-type outputs, partial exports, merged partial exports, independent blend layouts, eight attachments, and invalid host locations. Run the synthetic shader suite in CI.

---------

Co-authored-by: Dafenx <196083014+Dafenxz0@users.noreply.github.com>
This commit is contained in:
Dafenx
2026-07-15 01:41:39 +02:00
committed by GitHub
parent e604fb606d
commit 081760be3f
6 changed files with 818 additions and 262 deletions
+478 -188
View File
@@ -104,14 +104,17 @@ internal readonly record struct VulkanGuestBlendState(
}
internal sealed record VulkanGuestRenderState(
VulkanGuestBlendState Blend,
IReadOnlyList<VulkanGuestBlendState> Blends,
VulkanGuestRect? Scissor,
VulkanGuestViewport? Viewport)
{
public static VulkanGuestRenderState Default { get; } = new(
VulkanGuestBlendState.Default,
[VulkanGuestBlendState.Default],
Scissor: null,
Viewport: null);
public VulkanGuestBlendState Blend =>
Blends.Count == 0 ? VulkanGuestBlendState.Default : Blends[0];
}
internal sealed record VulkanGuestRenderTarget(
@@ -122,6 +125,13 @@ internal sealed record VulkanGuestRenderTarget(
uint NumberType,
uint MipLevels = 1);
internal readonly record struct VulkanRenderTargetFormat(
Format Format,
Gen5PixelOutputKind OutputKind)
{
public bool IsInteger => OutputKind is Gen5PixelOutputKind.Uint or Gen5PixelOutputKind.Sint;
}
internal sealed record VulkanTranslatedGuestDraw(
byte[] VertexSpirv,
byte[] PixelSpirv,
@@ -137,7 +147,7 @@ internal sealed record VulkanTranslatedGuestDraw(
internal sealed record VulkanOffscreenGuestDraw(
VulkanTranslatedGuestDraw Draw,
VulkanGuestRenderTarget Target,
IReadOnlyList<VulkanGuestRenderTarget> Targets,
bool PublishTarget);
internal sealed record VulkanComputeGuestDispatch(
@@ -463,20 +473,70 @@ internal static unsafe class VulkanVideoPresenter
VulkanGuestIndexBuffer? indexBuffer = null,
IReadOnlyList<VulkanGuestVertexBuffer>? vertexBuffers = null,
VulkanGuestRenderState? renderState = null)
{
SubmitOffscreenTranslatedDraw(
pixelSpirv,
textures,
globalMemoryBuffers,
attributeCount,
[target],
vertexSpirv,
vertexCount,
instanceCount,
primitiveType,
indexBuffer,
vertexBuffers,
renderState);
}
public static void SubmitOffscreenTranslatedDraw(
byte[] pixelSpirv,
IReadOnlyList<VulkanGuestDrawTexture> textures,
IReadOnlyList<VulkanGuestMemoryBuffer> globalMemoryBuffers,
uint attributeCount,
IReadOnlyList<VulkanGuestRenderTarget> targets,
byte[]? vertexSpirv = null,
uint vertexCount = 3,
uint instanceCount = 1,
uint primitiveType = 4,
VulkanGuestIndexBuffer? indexBuffer = null,
IReadOnlyList<VulkanGuestVertexBuffer>? vertexBuffers = null,
VulkanGuestRenderState? renderState = null)
{
if (pixelSpirv.Length == 0 ||
target.Address == 0 ||
target.Width == 0 ||
target.Height == 0)
targets.Count == 0 ||
targets.Count > 8 ||
targets.Any(target =>
target.Address == 0 || target.Width == 0 || target.Height == 0))
{
return;
}
var firstTarget = targets[0];
if (targets.Any(target =>
target.Width != firstTarget.Width || target.Height != firstTarget.Height) ||
targets.Select(target => target.Address).Distinct().Count() != targets.Count)
{
Console.Error.WriteLine(
"[LOADER][WARN] Vulkan skipped MRT draw with mismatched dimensions or aliased targets.");
return;
}
if (ShouldTracePresentedGuestImageContentsForDiagnostics())
{
Console.Error.WriteLine(
$"[LOADER][TRACE] vk.submit_call kind=SubmitOffscreenTranslatedDraw " +
$"target=0x{target.Address:X16} {target.Width}x{target.Height} textures={textures.Count}");
$"targets={targets.Count} first=0x{firstTarget.Address:X16} " +
$"{firstTarget.Width}x{firstTarget.Height} textures={textures.Count}");
}
var effectiveRenderState = renderState ?? VulkanGuestRenderState.Default;
if (effectiveRenderState.Blends.Count == 1 && targets.Count > 1)
{
effectiveRenderState = effectiveRenderState with
{
Blends = Enumerable.Repeat(effectiveRenderState.Blends[0], targets.Count).ToArray(),
};
}
lock (_gate)
@@ -486,12 +546,15 @@ internal static unsafe class VulkanVideoPresenter
return;
}
var guestTextureFormat = GetGuestTextureFormat(
target.Format,
target.NumberType);
if (guestTextureFormat != 0)
foreach (var target in targets)
{
_availableGuestImages[target.Address] = guestTextureFormat;
var guestTextureFormat = GetGuestTextureFormat(
target.Format,
target.NumberType);
if (guestTextureFormat != 0)
{
_availableGuestImages[target.Address] = guestTextureFormat;
}
}
EnqueueGuestWorkLocked(
@@ -507,8 +570,8 @@ internal static unsafe class VulkanVideoPresenter
instanceCount,
primitiveType,
indexBuffer,
renderState ?? VulkanGuestRenderState.Default),
target,
effectiveRenderState),
targets.ToArray(),
PublishTarget: true));
}
}
@@ -550,12 +613,12 @@ internal static unsafe class VulkanVideoPresenter
4,
null,
VulkanGuestRenderState.Default),
new VulkanGuestRenderTarget(
[new VulkanGuestRenderTarget(
Address: 0,
width,
height,
Format: 12,
NumberType: 7),
NumberType: 7)],
PublishTarget: false));
}
}
@@ -785,25 +848,100 @@ internal static unsafe class VulkanVideoPresenter
return true;
}
private static uint GetGuestTextureFormat(uint format, uint numberType) =>
(format, numberType) switch
private static uint GetGuestTextureFormat(uint format, uint numberType)
{
if (!TryDecodeRenderTargetFormat(format, numberType, out var decoded))
{
(9, _) => 9,
(4, 4) => GuestFormatR32Uint,
(4, 5) => GuestFormatR32Sint,
(4, 7) => GuestFormatR32Sfloat,
(5, 4) => GuestFormatR16G16Uint,
(5, 5) => GuestFormatR16G16Sint,
(5, 7) => GuestFormatR16G16Sfloat,
(10, 4) => GuestFormatR8G8B8A8Uint,
(10, 5) => GuestFormatR8G8B8A8Sint,
(10, _) => 56,
(12, 4) => GuestFormatR16G16B16A16Uint,
(12, 5) => GuestFormatR16G16B16A16Sint,
(12, 7) => 71,
(_, 0) when IsKnownGuestTextureFormat(format) => format,
return 0;
}
return decoded.Format switch
{
Format.R8Unorm => 36,
Format.R8Uint => 49,
Format.R8G8Unorm => 3,
Format.A2R10G10B10UnormPack32 => 9,
Format.B10G11R11UfloatPack32 => 7,
Format.R32Uint => GuestFormatR32Uint,
Format.R32Sint => GuestFormatR32Sint,
Format.R32Sfloat => GuestFormatR32Sfloat,
Format.R16G16Unorm => 5,
Format.R16G16Uint => GuestFormatR16G16Uint,
Format.R16G16Sint => GuestFormatR16G16Sint,
Format.R16G16Sfloat => GuestFormatR16G16Sfloat,
Format.R32G32Sfloat => 75,
Format.R8G8B8A8Unorm => 56,
Format.R8G8B8A8Uint => GuestFormatR8G8B8A8Uint,
Format.R8G8B8A8Sint => GuestFormatR8G8B8A8Sint,
Format.R16G16B16A16Unorm => 12,
Format.R16G16B16A16Uint => GuestFormatR16G16B16A16Uint,
Format.R16G16B16A16Sint => GuestFormatR16G16B16A16Sint,
Format.R16G16B16A16Sfloat => 71,
Format.R32G32B32A32Sfloat => 14,
_ => 0,
};
}
internal static bool TryDecodeRenderTargetFormat(
uint dataFormat,
uint numberType,
out VulkanRenderTargetFormat result)
{
var format = (dataFormat, numberType) switch
{
(4, 4) => Format.R32Uint,
(4, 5) => Format.R32Sint,
(4, 7) => Format.R32Sfloat,
(5, 4) => Format.R16G16Uint,
(5, 5) => Format.R16G16Sint,
(5, 7) => Format.R16G16Sfloat,
(9, _) => Format.A2R10G10B10UnormPack32,
(10, 4) => Format.R8G8B8A8Uint,
(10, 5) => Format.R8G8B8A8Sint,
(10, _) => Format.R8G8B8A8Unorm,
(12, 4) => Format.R16G16B16A16Uint,
(12, 5) => Format.R16G16B16A16Sint,
(12, 7) => Format.R16G16B16A16Sfloat,
(GuestFormatR32Uint, _) or (20, 0) => Format.R32Uint,
(GuestFormatR32Sint, _) => Format.R32Sint,
(GuestFormatR32Sfloat, _) or (29, 0) or (4, 0) => Format.R32Sfloat,
(GuestFormatR16G16Uint, _) => Format.R16G16Uint,
(GuestFormatR16G16Sint, _) => Format.R16G16Sint,
(GuestFormatR16G16Sfloat, _) => Format.R16G16Sfloat,
(GuestFormatR8G8B8A8Uint, _) => Format.R8G8B8A8Uint,
(GuestFormatR8G8B8A8Sint, _) => Format.R8G8B8A8Sint,
(GuestFormatR16G16B16A16Uint, _) => Format.R16G16B16A16Uint,
(GuestFormatR16G16B16A16Sint, _) => Format.R16G16B16A16Sint,
(1, 0) or (36, 0) => Format.R8Unorm,
(49, 0) => Format.R8Uint,
(3, 0) => Format.R8G8Unorm,
(5, 0) => Format.R16G16Unorm,
(7, 0) => Format.B10G11R11UfloatPack32,
(12, 0) => Format.R16G16B16A16Unorm,
(13, 0) or (14, 0) => Format.R32G32B32A32Sfloat,
(22, 0) or (71, 0) => Format.R16G16B16A16Sfloat,
(56, 0) or (62, 0) or (64, 0) => Format.R8G8B8A8Unorm,
(75, 0) => Format.R32G32Sfloat,
_ => Format.Undefined,
};
if (format == Format.Undefined)
{
result = default;
return false;
}
var outputKind = format switch
{
Format.R8Uint or Format.R32Uint or Format.R16G16Uint or
Format.R8G8B8A8Uint or Format.R16G16B16A16Uint => Gen5PixelOutputKind.Uint,
Format.R32Sint or Format.R16G16Sint or Format.R8G8B8A8Sint or
Format.R16G16B16A16Sint => Gen5PixelOutputKind.Sint,
_ => Gen5PixelOutputKind.Float,
};
result = new VulkanRenderTargetFormat(format, outputKind);
return true;
}
private static bool IsKnownGuestTextureFormat(uint format) =>
format is 4 or 5 or 7 or 9 or 13 or 14 or 22 or 29 or 36 or 56 or 62 or 64 or 71;
@@ -948,6 +1086,8 @@ internal static unsafe class VulkanVideoPresenter
private DebugUtilsMessengerEXT _debugMessenger;
private ExtDebugUtils? _debugUtils;
private PhysicalDevice _physicalDevice;
private bool _supportsIndependentBlend;
private uint _maxColorAttachments;
private Device _device;
private PipelineCache _pipelineCache;
private string? _pipelineCachePath;
@@ -1024,9 +1164,9 @@ internal static unsafe class VulkanVideoPresenter
private readonly record struct GraphicsPipelineKey(
string VertexShader,
string FragmentShader,
Format RenderTargetFormat,
string RenderTargetLayout,
PrimitiveTopology Topology,
VulkanGuestBlendState Blend,
string BlendLayout,
string ResourceLayout,
string VertexLayout);
@@ -1066,9 +1206,11 @@ internal static unsafe class VulkanVideoPresenter
public uint VertexCount = 3;
public uint InstanceCount = 1;
public PrimitiveTopology Topology = PrimitiveTopology.TriangleList;
public VulkanGuestBlendState Blend = VulkanGuestBlendState.Default;
public VulkanGuestBlendState[] Blends = [VulkanGuestBlendState.Default];
public VulkanGuestRect? Scissor;
public VulkanGuestViewport? Viewport;
public RenderPass TransientRenderPass;
public Framebuffer TransientFramebuffer;
}
private sealed class TextureResource
@@ -1604,6 +1746,7 @@ internal static unsafe class VulkanVideoPresenter
}
_vk.GetPhysicalDeviceProperties(_physicalDevice, out var selected);
_maxColorAttachments = selected.Limits.MaxColorAttachments;
var selectedName = SilkMarshal.PtrToString((nint)selected.DeviceName) ?? "unknown";
Console.Error.WriteLine(
$"[LOADER][INFO] Vulkan device: {selectedName} ({selected.DeviceType})");
@@ -1650,8 +1793,10 @@ internal static unsafe class VulkanVideoPresenter
PQueuePriorities = &priority,
};
_vk.GetPhysicalDeviceFeatures(_physicalDevice, out var supportedFeatures);
_supportsIndependentBlend = supportedFeatures.IndependentBlend;
var enabledFeatures = new PhysicalDeviceFeatures
{
IndependentBlend = supportedFeatures.IndependentBlend,
VertexPipelineStoresAndAtomics = supportedFeatures.VertexPipelineStoresAndAtomics,
FragmentStoresAndAtomics = supportedFeatures.FragmentStoresAndAtomics,
ShaderInt64 = supportedFeatures.ShaderInt64,
@@ -2150,6 +2295,14 @@ internal static unsafe class VulkanVideoPresenter
}
}
private void WaitForAllGuestSubmissions()
{
while (_pendingGuestSubmissions.Count != 0)
{
CollectCompletedGuestSubmissions(waitForOldest: true);
}
}
private void CollectCompletedGuestSubmissions(bool waitForOldest)
{
if (waitForOldest && _pendingGuestSubmissions.TryPeek(out var oldest))
@@ -2193,13 +2346,15 @@ internal static unsafe class VulkanVideoPresenter
private IReadOnlyList<GuestImageResource> GetTraceImages(
TranslatedDrawResources resources,
GuestImageResource? renderTarget = null)
IReadOnlyList<GuestImageResource>? renderTargets = null)
{
var images = new HashSet<GuestImageResource>();
if (renderTarget is not null &&
ShouldTraceGuestImageContents(renderTarget))
foreach (var renderTarget in renderTargets ?? [])
{
images.Add(renderTarget);
if (ShouldTraceGuestImageContents(renderTarget))
{
images.Add(renderTarget);
}
}
foreach (var texture in resources.Textures)
@@ -2430,10 +2585,16 @@ internal static unsafe class VulkanVideoPresenter
private TranslatedDrawResources CreateTranslatedDrawResources(
VulkanTranslatedGuestDraw draw,
RenderPass renderPass,
Format renderTargetFormat,
IReadOnlyList<Format> renderTargetFormats,
Extent2D extent)
{
var vertexSpirv = draw.VertexSpirv;
if (draw.RenderState.Blends.Count != renderTargetFormats.Count)
{
throw new InvalidOperationException(
"color attachment formats and blend states must have matching counts");
}
if (vertexSpirv.Length == 0 &&
!TryCompileFullscreenVertexShader(
draw.AttributeCount,
@@ -2453,7 +2614,7 @@ internal static unsafe class VulkanVideoPresenter
VertexCount = GetDrawVertexCount(draw.PrimitiveType, draw.VertexCount, draw.IndexBuffer),
InstanceCount = Math.Max(draw.InstanceCount, 1),
Topology = GetPrimitiveTopology(draw.PrimitiveType),
Blend = draw.RenderState.Blend,
Blends = draw.RenderState.Blends.ToArray(),
Scissor = draw.RenderState.Scissor,
Viewport = draw.RenderState.Viewport,
};
@@ -2502,7 +2663,7 @@ internal static unsafe class VulkanVideoPresenter
vertexSpirv,
draw.PixelSpirv,
renderPass,
renderTargetFormat,
renderTargetFormats,
extent);
return resources;
}
@@ -2799,15 +2960,18 @@ internal static unsafe class VulkanVideoPresenter
byte[] vertexSpirv,
byte[] fragmentSpirv,
RenderPass renderPass,
Format renderTargetFormat,
IReadOnlyList<Format> renderTargetFormats,
Extent2D extent)
{
var pipelineKey = new GraphicsPipelineKey(
GetShaderDigest(vertexSpirv),
GetShaderDigest(fragmentSpirv),
renderTargetFormat,
string.Join(',', renderTargetFormats.Select(format => (uint)format)),
resources.Topology,
resources.Blend,
string.Join(';', resources.Blends.Select(blend =>
$"{(blend.Enable ? 1 : 0)}:{blend.ColorSrcFactor}:{blend.ColorDstFactor}:" +
$"{blend.ColorFunc}:{blend.AlphaSrcFactor}:{blend.AlphaDstFactor}:" +
$"{blend.AlphaFunc}:{(blend.SeparateAlphaBlend ? 1 : 0)}:{blend.WriteMask}")),
GetResourceLayoutKey(resources),
GetVertexLayoutKey(resources));
if (_graphicsPipelines.TryGetValue(pipelineKey, out var cachedPipeline))
@@ -2908,29 +3072,33 @@ internal static unsafe class VulkanVideoPresenter
SType = StructureType.PipelineMultisampleStateCreateInfo,
RasterizationSamples = SampleCountFlags.Count1Bit,
};
var colorBlendAttachment = new PipelineColorBlendAttachmentState
var colorBlendAttachments = stackalloc PipelineColorBlendAttachmentState[resources.Blends.Length];
for (var index = 0; index < resources.Blends.Length; index++)
{
BlendEnable = resources.Blend.Enable,
SrcColorBlendFactor = ToVkBlendFactor(resources.Blend.ColorSrcFactor),
DstColorBlendFactor = ToVkBlendFactor(resources.Blend.ColorDstFactor),
ColorBlendOp = ToVkBlendOp(resources.Blend.ColorFunc),
SrcAlphaBlendFactor = resources.Blend.SeparateAlphaBlend
? ToVkBlendFactor(resources.Blend.AlphaSrcFactor)
: ToVkBlendFactor(resources.Blend.ColorSrcFactor),
DstAlphaBlendFactor = resources.Blend.SeparateAlphaBlend
? ToVkBlendFactor(resources.Blend.AlphaDstFactor)
: ToVkBlendFactor(resources.Blend.ColorDstFactor),
AlphaBlendOp = resources.Blend.SeparateAlphaBlend
? ToVkBlendOp(resources.Blend.AlphaFunc)
: ToVkBlendOp(resources.Blend.ColorFunc),
ColorWriteMask =
ToVkColorWriteMask(resources.Blend.WriteMask),
};
var blend = resources.Blends[index];
colorBlendAttachments[index] = new PipelineColorBlendAttachmentState
{
BlendEnable = blend.Enable,
SrcColorBlendFactor = ToVkBlendFactor(blend.ColorSrcFactor),
DstColorBlendFactor = ToVkBlendFactor(blend.ColorDstFactor),
ColorBlendOp = ToVkBlendOp(blend.ColorFunc),
SrcAlphaBlendFactor = blend.SeparateAlphaBlend
? ToVkBlendFactor(blend.AlphaSrcFactor)
: ToVkBlendFactor(blend.ColorSrcFactor),
DstAlphaBlendFactor = blend.SeparateAlphaBlend
? ToVkBlendFactor(blend.AlphaDstFactor)
: ToVkBlendFactor(blend.ColorDstFactor),
AlphaBlendOp = blend.SeparateAlphaBlend
? ToVkBlendOp(blend.AlphaFunc)
: ToVkBlendOp(blend.ColorFunc),
ColorWriteMask = ToVkColorWriteMask(blend.WriteMask),
};
}
var colorBlend = new PipelineColorBlendStateCreateInfo
{
SType = StructureType.PipelineColorBlendStateCreateInfo,
AttachmentCount = 1,
PAttachments = &colorBlendAttachment,
AttachmentCount = (uint)resources.Blends.Length,
PAttachments = colorBlendAttachments,
};
var dynamicStateValues = stackalloc DynamicState[2];
dynamicStateValues[0] = DynamicState.Viewport;
@@ -4372,6 +4540,12 @@ internal static unsafe class VulkanVideoPresenter
: checked(((ulong)width + 3) / 4 * (((ulong)height + 3) / 4) * blockBytes);
}
private bool SupportsColorAttachment(Format format)
{
_vk.GetPhysicalDeviceFormatProperties(_physicalDevice, format, out var properties);
return (properties.OptimalTilingFeatures & FormatFeatureFlags.ColorAttachmentBit) != 0;
}
private static Format GetTextureFormat(uint format, uint numberType) =>
(format, numberType) switch
{
@@ -4434,26 +4608,6 @@ internal static unsafe class VulkanVideoPresenter
_ => Format.R8G8B8A8Unorm,
};
private static Format GetRenderTargetFormat(uint format, uint numberType) =>
(format, numberType) switch
{
(4, 4) => Format.R32Uint,
(4, 5) => Format.R32Sint,
(4, 7) => Format.R32Sfloat,
(5, 4) => Format.R16G16Uint,
(5, 5) => Format.R16G16Sint,
(5, 7) => Format.R16G16Sfloat,
(9, _) => Format.A2R10G10B10UnormPack32,
(10, 4) => Format.R8G8B8A8Uint,
(10, 5) => Format.R8G8B8A8Sint,
(10, _) => Format.R8G8B8A8Unorm,
(12, 4) => Format.R16G16B16A16Uint,
(12, 5) => Format.R16G16B16A16Sint,
(12, 7) => Format.R16G16B16A16Sfloat,
(_, 0) => GetTextureFormat(format, numberType),
_ => Format.Undefined,
};
private static bool IsBlockCompressedFormat(Format format) =>
format is Format.BC1RgbaUnormBlock or
Format.BC1RgbaSrgbBlock or
@@ -4694,47 +4848,114 @@ internal static unsafe class VulkanVideoPresenter
private void ExecuteOffscreenDraw(VulkanOffscreenGuestDraw work)
{
if (_deviceLost)
if (_deviceLost || work.Targets.Count == 0)
{
return;
}
var format = GetRenderTargetFormat(work.Target.Format, work.Target.NumberType);
if (format == Format.Undefined)
if (work.Targets.Count > _maxColorAttachments)
{
Console.Error.WriteLine(
$"[LOADER][WARN] Vulkan skipped unsupported render target " +
$"addr=0x{work.Target.Address:X16} format={work.Target.Format} " +
$"number={work.Target.NumberType}");
$"[LOADER][WARN] Vulkan skipped MRT draw requesting {work.Targets.Count} color attachments; " +
$"the selected device supports {_maxColorAttachments}.");
return;
}
var targetFormats = new VulkanRenderTargetFormat[work.Targets.Count];
for (var index = 0; index < targetFormats.Length; index++)
{
var target = work.Targets[index];
if (!TryDecodeRenderTargetFormat(target.Format, target.NumberType, out targetFormats[index]) ||
!SupportsColorAttachment(targetFormats[index].Format))
{
Console.Error.WriteLine(
$"[LOADER][WARN] Vulkan skipped MRT draw with unsupported color target " +
$"format={target.Format} number_type={target.NumberType}.");
return;
}
}
if (work.Draw.RenderState.Blends.Count != targetFormats.Length)
{
Console.Error.WriteLine(
"[LOADER][WARN] Vulkan skipped MRT draw with mismatched attachment/blend counts.");
return;
}
if (targetFormats
.Select((format, index) => format.IsInteger && work.Draw.RenderState.Blends[index].Enable)
.Any(invalid => invalid))
{
Console.Error.WriteLine(
"[LOADER][WARN] Vulkan skipped MRT draw with blending enabled for an integer attachment.");
return;
}
if (!_supportsIndependentBlend &&
work.Draw.RenderState.Blends.Skip(1).Any(blend => blend != work.Draw.RenderState.Blends[0]))
{
Console.Error.WriteLine(
"[LOADER][WARN] Vulkan skipped MRT draw requiring unsupported independentBlend.");
return;
}
var formats = targetFormats.Select(target => target.Format).ToArray();
var targetAddresses = work.Targets
.Where(target => target.Address != 0)
.Select(target => target.Address)
.ToHashSet();
if (work.Draw.Textures.Any(texture =>
texture.Address == work.Target.Address &&
texture.Address != 0))
targetAddresses.Contains(texture.Address)))
{
Console.Error.WriteLine(
$"[LOADER][WARN] Vulkan skipped render-target feedback loop " +
$"addr=0x{work.Target.Address:X16}");
$"targets={string.Join(',', targetAddresses.Select(address => $"0x{address:X16}"))}");
return;
}
var target = GetOrCreateGuestImage(work.Target, format);
var targets = new GuestImageResource[work.Targets.Count];
EnsureGuestSubmissionCapacity();
for (var index = 0; index < targets.Length; index++)
{
targets[index] = GetOrCreateGuestImage(work.Targets[index], formats[index]);
}
var firstTarget = targets[0];
TranslatedDrawResources? resources = null;
CommandBuffer commandBuffer = default;
var submitted = false;
RenderPass transientRenderPass = default;
Framebuffer transientFramebuffer = default;
try
{
EnsureGuestSubmissionCapacity();
var extent = new Extent2D(target.Width, target.Height);
var extent = new Extent2D(firstTarget.Width, firstTarget.Height);
var renderPass = firstTarget.RenderPass;
var framebuffer = firstTarget.Framebuffer;
if (targets.Length > 1)
{
(renderPass, framebuffer) = CreateRenderPassAndFramebuffer(
formats,
targets.Select(target => target.MipViews[0]).ToArray(),
firstTarget.Width,
firstTarget.Height);
transientRenderPass = renderPass;
transientFramebuffer = framebuffer;
}
resources = CreateTranslatedDrawResources(
work.Draw,
target.RenderPass,
target.Format,
renderPass,
formats,
extent);
resources.TransientRenderPass = transientRenderPass;
resources.TransientFramebuffer = transientFramebuffer;
transientRenderPass = default;
transientFramebuffer = default;
resources.DebugName =
$"SharpEmu offscreen rt=0x{work.Target.Address:X16} " +
$"{work.Target.Width}x{work.Target.Height} fmt{work.Target.Format}";
$"SharpEmu offscreen mrt={targets.Length} " +
$"first=0x{work.Targets[0].Address:X16} " +
$"{firstTarget.Width}x{firstTarget.Height}";
commandBuffer = AllocateGuestCommandBuffer();
_commandBuffer = commandBuffer;
@@ -4751,24 +4972,30 @@ internal static unsafe class VulkanVideoPresenter
RecordTextureUploads(resources, PipelineStageFlags.FragmentShaderBit);
RecordStorageImagesForWrite(resources, PipelineStageFlags.FragmentShaderBit);
var targetHasPriorContents = target.Initialized || target.InitialUploadPending;
var toColorAttachment = new ImageMemoryBarrier
var toColorAttachments = stackalloc ImageMemoryBarrier[targets.Length];
var anyPriorContents = false;
for (var index = 0; index < targets.Length; index++)
{
SType = StructureType.ImageMemoryBarrier,
SrcAccessMask = targetHasPriorContents ? AccessFlags.ShaderReadBit : 0,
DstAccessMask = AccessFlags.ColorAttachmentWriteBit,
OldLayout = targetHasPriorContents
? ImageLayout.ShaderReadOnlyOptimal
: ImageLayout.Undefined,
NewLayout = ImageLayout.ColorAttachmentOptimal,
SrcQueueFamilyIndex = Vk.QueueFamilyIgnored,
DstQueueFamilyIndex = Vk.QueueFamilyIgnored,
Image = target.Image,
SubresourceRange = ColorSubresourceRange(),
};
var hasPriorContents = targets[index].Initialized || targets[index].InitialUploadPending;
anyPriorContents |= hasPriorContents;
toColorAttachments[index] = new ImageMemoryBarrier
{
SType = StructureType.ImageMemoryBarrier,
SrcAccessMask = hasPriorContents ? AccessFlags.ShaderReadBit : 0,
DstAccessMask = AccessFlags.ColorAttachmentWriteBit,
OldLayout = hasPriorContents
? ImageLayout.ShaderReadOnlyOptimal
: ImageLayout.Undefined,
NewLayout = ImageLayout.ColorAttachmentOptimal,
SrcQueueFamilyIndex = Vk.QueueFamilyIgnored,
DstQueueFamilyIndex = Vk.QueueFamilyIgnored,
Image = targets[index].Image,
SubresourceRange = ColorSubresourceRange(),
};
}
_vk.CmdPipelineBarrier(
_commandBuffer,
targetHasPriorContents
anyPriorContents
? PipelineStageFlags.AllCommandsBit
: PipelineStageFlags.TopOfPipeBit,
PipelineStageFlags.ColorAttachmentOutputBit,
@@ -4777,28 +5004,32 @@ internal static unsafe class VulkanVideoPresenter
null,
0,
null,
1,
&toColorAttachment);
(uint)targets.Length,
toColorAttachments);
RecordTranslatedGraphicsPass(
resources,
target.RenderPass,
target.Framebuffer,
renderPass,
framebuffer,
extent);
RecordStorageImagesForRead(resources, PipelineStageFlags.FragmentShaderBit);
var toShaderRead = new ImageMemoryBarrier
var toShaderRead = stackalloc ImageMemoryBarrier[targets.Length];
for (var index = 0; index < targets.Length; index++)
{
SType = StructureType.ImageMemoryBarrier,
SrcAccessMask = AccessFlags.ColorAttachmentWriteBit,
DstAccessMask = AccessFlags.ShaderReadBit,
OldLayout = ImageLayout.ColorAttachmentOptimal,
NewLayout = ImageLayout.ShaderReadOnlyOptimal,
SrcQueueFamilyIndex = Vk.QueueFamilyIgnored,
DstQueueFamilyIndex = Vk.QueueFamilyIgnored,
Image = target.Image,
SubresourceRange = ColorSubresourceRange(),
};
toShaderRead[index] = new ImageMemoryBarrier
{
SType = StructureType.ImageMemoryBarrier,
SrcAccessMask = AccessFlags.ColorAttachmentWriteBit,
DstAccessMask = AccessFlags.ShaderReadBit,
OldLayout = ImageLayout.ColorAttachmentOptimal,
NewLayout = ImageLayout.ShaderReadOnlyOptimal,
SrcQueueFamilyIndex = Vk.QueueFamilyIgnored,
DstQueueFamilyIndex = Vk.QueueFamilyIgnored,
Image = targets[index].Image,
SubresourceRange = ColorSubresourceRange(),
};
}
_vk.CmdPipelineBarrier(
_commandBuffer,
PipelineStageFlags.ColorAttachmentOutputBit,
@@ -4808,55 +5039,70 @@ internal static unsafe class VulkanVideoPresenter
null,
0,
null,
1,
&toShaderRead);
(uint)targets.Length,
toShaderRead);
EndDebugLabel(_commandBuffer);
Check(_vk.EndCommandBuffer(_commandBuffer), "vkEndCommandBuffer(offscreen)");
SubmitGuestCommandBuffer(
commandBuffer,
resources,
GetTraceImages(resources, target));
GetTraceImages(resources, targets));
submitted = true;
target.Initialized = true;
foreach (var target in targets)
{
target.Initialized = true;
}
MarkSampledImagesInitialized(resources);
MarkStorageImagesInitialized(resources, traceContents: false);
var guestTextureFormat = VulkanVideoPresenter.GetGuestTextureFormat(
work.Target.Format,
work.Target.NumberType);
if (work.PublishTarget && guestTextureFormat != 0)
if (work.PublishTarget)
{
lock (_gate)
for (var index = 0; index < targets.Length; index++)
{
_availableGuestImages[target.Address] = guestTextureFormat;
_gpuGuestImages[target.Address] = guestTextureFormat;
var guestTextureFormat = VulkanVideoPresenter.GetGuestTextureFormat(
work.Targets[index].Format,
work.Targets[index].NumberType);
if (guestTextureFormat == 0)
{
continue;
}
lock (_gate)
{
_availableGuestImages[targets[index].Address] = guestTextureFormat;
_gpuGuestImages[targets[index].Address] = guestTextureFormat;
}
}
}
if (ShouldTraceGuestImageWriteForDiagnostics(target.Address))
foreach (var target in targets)
{
var writeCount = _tracedGuestWriteCounts.TryGetValue(
target.Address,
out var previousCount)
? previousCount + 1
: 1;
_tracedGuestWriteCounts[target.Address] = writeCount;
if (writeCount <= 3)
if (ShouldTraceGuestImageWriteForDiagnostics(target.Address))
{
_commandBuffer = _presentationCommandBuffer;
Check(
_vk.QueueWaitIdle(_queue),
"vkQueueWaitIdle(guest write trace)");
Console.Error.WriteLine(
$"[LOADER][TRACE] vk.guest_write_sample " +
$"addr=0x{target.Address:X16} write={writeCount} " +
$"ps_bytes={work.Draw.PixelSpirv.Length}");
TraceGuestImageContents(target);
var writeCount = _tracedGuestWriteCounts.TryGetValue(
target.Address,
out var previousCount)
? previousCount + 1
: 1;
_tracedGuestWriteCounts[target.Address] = writeCount;
if (writeCount <= 3)
{
_commandBuffer = _presentationCommandBuffer;
Check(
_vk.QueueWaitIdle(_queue),
"vkQueueWaitIdle(guest write trace)");
Console.Error.WriteLine(
$"[LOADER][TRACE] vk.guest_write_sample " +
$"addr=0x{target.Address:X16} write={writeCount} " +
$"ps_bytes={work.Draw.PixelSpirv.Length}");
TraceGuestImageContents(target);
}
}
}
TraceVulkanShader(
$"vk.offscreen_draw addr=0x{target.Address:X16} " +
$"size={target.Width}x{target.Height} format={target.Format} " +
$"vk.offscreen_draw mrt={targets.Length} " +
$"size={firstTarget.Width}x{firstTarget.Height} " +
$"textures={work.Draw.Textures.Count}");
}
catch (Exception exception)
@@ -4866,19 +5112,22 @@ internal static unsafe class VulkanVideoPresenter
return;
}
if (!_guestImages.TryGetValue(work.Target.Address, out var failedTarget) ||
!failedTarget.Initialized)
lock (_gate)
{
lock (_gate)
foreach (var target in work.Targets)
{
_availableGuestImages.Remove(work.Target.Address);
_gpuGuestImages.Remove(work.Target.Address);
if (!_guestImages.TryGetValue(target.Address, out var failedTarget) ||
!failedTarget.Initialized)
{
_availableGuestImages.Remove(target.Address);
_gpuGuestImages.Remove(target.Address);
}
}
}
Console.Error.WriteLine(
$"[LOADER][ERROR] Vulkan offscreen draw failed " +
$"addr=0x{work.Target.Address:X16}: {exception.Message}");
$"mrt={work.Targets.Count}: {exception.Message}");
}
finally
{
@@ -4896,6 +5145,16 @@ internal static unsafe class VulkanVideoPresenter
{
DestroyTranslatedDrawResources(resources);
}
if (transientFramebuffer.Handle != 0)
{
_vk.DestroyFramebuffer(_device, transientFramebuffer, null);
}
if (transientRenderPass.Handle != 0)
{
_vk.DestroyRenderPass(_device, transientRenderPass, null);
}
}
}
@@ -4934,6 +5193,8 @@ internal static unsafe class VulkanVideoPresenter
return existing;
}
CompletePendingPresentation(wait: true);
WaitForAllGuestSubmissions();
DestroyGuestImage(existing);
_guestImages.Remove(target.Address);
lock (_gate)
@@ -5052,35 +5313,55 @@ internal static unsafe class VulkanVideoPresenter
Format format,
ImageView attachmentView,
uint width,
uint height) =>
CreateRenderPassAndFramebuffer([format], [attachmentView], width, height);
private (RenderPass RenderPass, Framebuffer Framebuffer) CreateRenderPassAndFramebuffer(
IReadOnlyList<Format> formats,
IReadOnlyList<ImageView> attachmentViews,
uint width,
uint height)
{
var colorAttachment = new AttachmentDescription
if (formats.Count == 0 || formats.Count != attachmentViews.Count)
{
Format = format,
Samples = SampleCountFlags.Count1Bit,
LoadOp = AttachmentLoadOp.Load,
StoreOp = AttachmentStoreOp.Store,
StencilLoadOp = AttachmentLoadOp.DontCare,
StencilStoreOp = AttachmentStoreOp.DontCare,
InitialLayout = ImageLayout.ColorAttachmentOptimal,
FinalLayout = ImageLayout.ColorAttachmentOptimal,
};
var colorReference = new AttachmentReference
throw new InvalidOperationException("render target formats and views must have matching counts");
}
var colorAttachments = stackalloc AttachmentDescription[formats.Count];
var colorReferences = stackalloc AttachmentReference[formats.Count];
var views = stackalloc ImageView[formats.Count];
for (var index = 0; index < formats.Count; index++)
{
Attachment = 0,
Layout = ImageLayout.ColorAttachmentOptimal,
};
colorAttachments[index] = new AttachmentDescription
{
Format = formats[index],
Samples = SampleCountFlags.Count1Bit,
LoadOp = AttachmentLoadOp.Load,
StoreOp = AttachmentStoreOp.Store,
StencilLoadOp = AttachmentLoadOp.DontCare,
StencilStoreOp = AttachmentStoreOp.DontCare,
InitialLayout = ImageLayout.ColorAttachmentOptimal,
FinalLayout = ImageLayout.ColorAttachmentOptimal,
};
colorReferences[index] = new AttachmentReference
{
Attachment = (uint)index,
Layout = ImageLayout.ColorAttachmentOptimal,
};
views[index] = attachmentViews[index];
}
var subpass = new SubpassDescription
{
PipelineBindPoint = PipelineBindPoint.Graphics,
ColorAttachmentCount = 1,
PColorAttachments = &colorReference,
ColorAttachmentCount = (uint)formats.Count,
PColorAttachments = colorReferences,
};
var renderPassInfo = new RenderPassCreateInfo
{
SType = StructureType.RenderPassCreateInfo,
AttachmentCount = 1,
PAttachments = &colorAttachment,
AttachmentCount = (uint)formats.Count,
PAttachments = colorAttachments,
SubpassCount = 1,
PSubpasses = &subpass,
};
@@ -5088,13 +5369,12 @@ internal static unsafe class VulkanVideoPresenter
_vk.CreateRenderPass(_device, &renderPassInfo, null, out var renderPass),
"vkCreateRenderPass(offscreen)");
var attachment = attachmentView;
var framebufferInfo = new FramebufferCreateInfo
{
SType = StructureType.FramebufferCreateInfo,
RenderPass = renderPass,
AttachmentCount = 1,
PAttachments = &attachment,
AttachmentCount = (uint)formats.Count,
PAttachments = views,
Width = width,
Height = height,
Layers = 1,
@@ -5632,7 +5912,7 @@ internal static unsafe class VulkanVideoPresenter
translatedResources = CreateTranslatedDrawResources(
translatedDraw,
_renderPass,
_swapchainFormat,
[_swapchainFormat],
_extent);
if (ShouldTracePresentedGuestImageContentsForDiagnostics() &&
!_firstGuestDrawPresented)
@@ -6527,6 +6807,16 @@ internal static unsafe class VulkanVideoPresenter
private void DestroyTranslatedDrawResources(TranslatedDrawResources resources)
{
if (resources.TransientFramebuffer.Handle != 0)
{
_vk.DestroyFramebuffer(_device, resources.TransientFramebuffer, null);
}
if (resources.TransientRenderPass.Handle != 0)
{
_vk.DestroyRenderPass(_device, resources.TransientRenderPass, null);
}
foreach (var texture in resources.Textures)
{
if (texture is null)