[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
+79 -39
View File
@@ -157,7 +157,7 @@ public static class AgcExports
private static readonly HashSet<uint> _tracedSubmittedDrawOpcodes = new();
private static readonly Dictionary<(ulong Ps, ulong State, Gen5PixelOutputKind Output), byte[]> _pixelSpirvCache = new();
private static readonly Dictionary<
(ulong Es, ulong EsState, ulong Ps, ulong PsState, Gen5PixelOutputKind Output, uint Attributes),
(ulong Es, ulong EsState, ulong Ps, ulong PsState, string OutputLayout, uint Attributes),
(byte[] Vertex, byte[] Pixel)> _graphicsSpirvCache = new();
private static readonly Dictionary<
(ulong Cs, ulong State, uint LocalX, uint LocalY, uint LocalZ),
@@ -3329,12 +3329,13 @@ public static class AgcExports
textures,
globalMemoryBuffers,
translatedDraw.AttributeCount,
new VulkanGuestRenderTarget(
firstTarget.Address,
firstTarget.Width,
firstTarget.Height,
firstTarget.Format,
firstTarget.NumberType),
translatedDraw.RenderTargets.Select(target =>
new VulkanGuestRenderTarget(
target.Address,
target.Width,
target.Height,
target.Format,
target.NumberType)).ToArray(),
translatedDraw.VertexSpirv,
translatedDraw.VertexCount,
translatedDraw.InstanceCount,
@@ -3462,11 +3463,34 @@ public static class AgcExports
}
var renderTargets = GetRenderTargets(state.CxRegisters)
.Where(target =>
target.Slot == 0 &&
HasPixelColorExport(pixelState, target.Slot))
.Where(target => HasPixelColorExport(pixelState, target.Slot))
.OrderBy(target => target.Slot)
.ToArray();
var outputKind = GetPixelOutputKind(renderTargets.FirstOrDefault().NumberType);
var renderTargetFormats = new VulkanRenderTargetFormat[renderTargets.Length];
for (var index = 0; index < renderTargets.Length; index++)
{
var target = renderTargets[index];
if (!VulkanVideoPresenter.TryDecodeRenderTargetFormat(
target.Format,
target.NumberType,
out renderTargetFormats[index]))
{
error =
$"unsupported color target format={target.Format} number_type={target.NumberType}";
return false;
}
}
var pixelOutputs = renderTargets
.Select((target, location) => new Gen5PixelOutputBinding(
target.Slot,
(uint)location,
renderTargetFormats[location].OutputKind))
.ToArray();
var outputLayout = string.Join(
';',
pixelOutputs.Select(output =>
$"{output.GuestSlot}:{output.HostLocation}:{(int)output.Kind}"));
var attributeCount = GetInterpolatedAttributeCount(pixelState);
var exportStateFingerprint = ComputeShaderStructureFingerprint(exportEvaluation);
var pixelStateFingerprint = ComputeShaderStructureFingerprint(pixelEvaluation);
@@ -3475,7 +3499,7 @@ public static class AgcExports
exportStateFingerprint,
pixelShaderAddress,
pixelStateFingerprint,
outputKind,
outputLayout,
attributeCount);
var totalGlobalBuffers =
pixelEvaluation.GlobalMemoryBindings.Count +
@@ -3491,7 +3515,7 @@ public static class AgcExports
if (!Gen5SpirvTranslator.TryCompilePixelShader(
pixelState,
pixelEvaluation,
outputKind,
pixelOutputs,
out var pixelShader,
out error,
globalBufferBase: 0,
@@ -3579,7 +3603,7 @@ public static class AgcExports
vertexInputs,
renderTargets,
ApplyTransparentPremultipliedFillClear(
CreateRenderState(state.CxRegisters, renderTargets.FirstOrDefault()),
CreateRenderState(state.CxRegisters, renderTargets, pixelState),
textures,
vertexInputs,
pixelEvaluation.InitialScalarRegisters));
@@ -3595,7 +3619,8 @@ public static class AgcExports
/// Chowdren resets its effect layers with an untextured transparent-black
/// fill using premultiplied blending. With One/OneMinusSrcAlpha that draw
/// is otherwise a no-op, causing fog and vignette layers to accumulate.
/// Treat precisely that draw shape as an overwrite.
/// Treat precisely that draw shape as an overwrite only when every MRT
/// attachment uses the same premultiplied blend pattern.
/// </summary>
private static VulkanGuestRenderState ApplyTransparentPremultipliedFillClear(
VulkanGuestRenderState renderState,
@@ -3607,13 +3632,7 @@ public static class AgcExports
textures.Count != 0 ||
vertexInputs.Count != 0 ||
pixelUserData.Count < 4 ||
renderState.Blend is not
{
Enable: true,
ColorSrcFactor: 1,
ColorDstFactor: 5,
ColorFunc: 0,
})
!renderState.Blends.All(IsTransparentPremultipliedFillBlend))
{
return renderState;
}
@@ -3628,10 +3647,21 @@ public static class AgcExports
return renderState with
{
Blend = renderState.Blend with { Enable = false },
Blends = renderState.Blends
.Select(blend => blend with { Enable = false })
.ToArray(),
};
}
private static bool IsTransparentPremultipliedFillBlend(VulkanGuestBlendState blend) =>
blend is
{
Enable: true,
ColorSrcFactor: 1,
ColorDstFactor: 5,
ColorFunc: 0,
};
private static VulkanGuestIndexBuffer? CreateVulkanIndexBuffer(
CpuContext ctx,
SubmittedDcbState state,
@@ -3654,19 +3684,15 @@ public static class AgcExports
: null;
}
private static Gen5PixelOutputKind GetPixelOutputKind(uint numberType) =>
numberType switch
{
4 => Gen5PixelOutputKind.Uint,
5 => Gen5PixelOutputKind.Sint,
_ => Gen5PixelOutputKind.Float,
};
private static bool HasPixelColorExport(Gen5ShaderState state, uint target) =>
state.Program.Instructions.Any(instruction =>
instruction.Control is Gen5ExportControl export &&
export.Target == target &&
export.EnableMask != 0);
GetPixelColorExportMask(state, target) != 0;
private static uint GetPixelColorExportMask(Gen5ShaderState state, uint target) =>
state.Program.Instructions
.Select(instruction => instruction.Control)
.OfType<Gen5ExportControl>()
.Where(export => export.Target == target)
.Aggregate(0u, (mask, export) => mask | (export.EnableMask & 0xFu));
private static uint GetInterpolatedAttributeCount(Gen5ShaderState state)
{
@@ -3824,11 +3850,25 @@ public static class AgcExports
private static VulkanGuestRenderState CreateRenderState(
IReadOnlyDictionary<uint, uint> registers,
RenderTargetDescriptor target)
IReadOnlyList<RenderTargetDescriptor> targets,
Gen5ShaderState pixelState)
{
if (targets.Count == 0)
{
return VulkanGuestRenderState.Default;
}
var target = targets[0];
var scissor = DecodeScissor(registers, target.Width, target.Height);
return new VulkanGuestRenderState(
DecodeBlendState(registers, target.Slot),
targets.Select(target =>
{
var blend = DecodeBlendState(registers, target.Slot);
return blend with
{
WriteMask = blend.WriteMask & GetPixelColorExportMask(pixelState, target.Slot),
};
}).ToArray(),
scissor,
DecodeViewport(registers, target.Width, target.Height, scissor));
}
@@ -3853,7 +3893,7 @@ public static class AgcExports
(control >> 24) & 0x1Fu,
(control >> 21) & 0x7u,
((control >> 29) & 1u) != 0,
writeMask == 0 ? 0xFu : writeMask);
writeMask);
}
private static VulkanGuestRect? DecodeScissor(
@@ -5191,7 +5231,7 @@ public static class AgcExports
if (Gen5SpirvTranslator.TryCompilePixelShader(
pixelState,
evaluation,
Gen5PixelOutputKind.Float,
[new(0, 0, Gen5PixelOutputKind.Float)],
out var compiledPixel,
out var compileError))
{
+5
View File
@@ -49,6 +49,11 @@ internal enum Gen5PixelOutputKind
Sint,
}
internal readonly record struct Gen5PixelOutputBinding(
uint GuestSlot,
uint HostLocation,
Gen5PixelOutputKind Kind);
internal enum Gen5SpirvStage
{
Vertex,
+98 -33
View File
@@ -19,13 +19,59 @@ internal static partial class Gen5SpirvTranslator
int globalBufferBase = 0,
int totalGlobalBufferCount = -1,
int imageBindingBase = 0,
int scalarRegisterBufferIndex = -1) =>
TryCompilePixelShader(
state,
evaluation,
[new Gen5PixelOutputBinding(0, 0, outputKind)],
out shader,
out error,
globalBufferBase,
totalGlobalBufferCount,
imageBindingBase,
scalarRegisterBufferIndex);
public static bool TryCompilePixelShader(
Gen5ShaderState state,
Gen5ShaderEvaluation evaluation,
IReadOnlyList<Gen5PixelOutputBinding> outputs,
out Gen5SpirvShader shader,
out string error,
int globalBufferBase = 0,
int totalGlobalBufferCount = -1,
int imageBindingBase = 0,
int scalarRegisterBufferIndex = -1)
{
if (outputs.Count > 8 || outputs.Any(output => output.GuestSlot > 7))
{
shader = default!;
error = "pixel outputs must contain at most eight guest slots in the 0..7 range";
return false;
}
if (outputs.Select(output => output.GuestSlot).Distinct().Count() != outputs.Count ||
outputs.Select(output => output.HostLocation).Distinct().Count() != outputs.Count)
{
shader = default!;
error = "pixel output guest slots and host locations must be unique";
return false;
}
if (!outputs
.OrderBy(output => output.HostLocation)
.Select((output, index) => output.HostLocation == (uint)index)
.All(isDense => isDense))
{
shader = default!;
error = "pixel output host locations must be dense in the 0..N-1 range";
return false;
}
var context = new CompilationContext(
Gen5SpirvStage.Pixel,
state,
evaluation,
outputKind,
outputs,
1,
1,
1,
@@ -50,7 +96,7 @@ internal static partial class Gen5SpirvTranslator
Gen5SpirvStage.Vertex,
state,
evaluation,
Gen5PixelOutputKind.Float,
[],
1,
1,
1,
@@ -74,7 +120,7 @@ internal static partial class Gen5SpirvTranslator
Gen5SpirvStage.Compute,
state,
evaluation,
Gen5PixelOutputKind.Float,
[],
Math.Max(localSizeX, 1),
Math.Max(localSizeY, 1),
Math.Max(localSizeZ, 1),
@@ -91,7 +137,7 @@ internal static partial class Gen5SpirvTranslator
private readonly Gen5SpirvStage _stage;
private readonly Gen5ShaderState _state;
private readonly Gen5ShaderEvaluation _evaluation;
private readonly Gen5PixelOutputKind _outputKind;
private readonly IReadOnlyList<Gen5PixelOutputBinding> _pixelOutputBindings;
private readonly uint _localSizeX;
private readonly uint _localSizeY;
private readonly uint _localSizeZ;
@@ -101,6 +147,7 @@ internal static partial class Gen5SpirvTranslator
private readonly int _scalarRegisterBufferIndex;
private readonly List<uint> _interfaces = [];
private readonly Dictionary<uint, uint> _pixelInputs = [];
private readonly Dictionary<uint, SpirvPixelOutput> _pixelOutputs = [];
private readonly Dictionary<uint, uint> _vertexOutputs = [];
private readonly Dictionary<uint, SpirvVertexInput> _vertexInputsByPc = [];
private readonly List<SpirvImageResource> _imageResources = [];
@@ -133,7 +180,6 @@ internal static partial class Gen5SpirvTranslator
private uint _lds;
private uint _workgroupUintPointer;
private uint _positionOutput;
private uint _pixelOutput;
private uint _vertexIndexInput;
private uint _instanceIndexInput;
private uint _fragCoordInput;
@@ -163,11 +209,16 @@ internal static partial class Gen5SpirvTranslator
uint Type,
uint ComponentCount);
private readonly record struct SpirvPixelOutput(
uint Variable,
uint Type,
Gen5PixelOutputKind Kind);
public CompilationContext(
Gen5SpirvStage stage,
Gen5ShaderState state,
Gen5ShaderEvaluation evaluation,
Gen5PixelOutputKind outputKind,
IReadOnlyList<Gen5PixelOutputBinding> pixelOutputBindings,
uint localSizeX,
uint localSizeY,
uint localSizeZ,
@@ -179,7 +230,7 @@ internal static partial class Gen5SpirvTranslator
_stage = stage;
_state = state;
_evaluation = evaluation;
_outputKind = outputKind;
_pixelOutputBindings = pixelOutputBindings;
_localSizeX = localSizeX;
_localSizeY = localSizeY;
_localSizeZ = localSizeZ;
@@ -700,19 +751,24 @@ internal static partial class Gen5SpirvTranslator
(uint)SpirvBuiltIn.FragCoord);
_interfaces.Add(_fragCoordInput);
var outputType = _outputKind switch
foreach (var binding in _pixelOutputBindings)
{
Gen5PixelOutputKind.Uint => _uvec4Type,
Gen5PixelOutputKind.Sint => _module.TypeVector(_intType, 4),
_ => _vec4Type,
};
var outputPointer =
_module.TypePointer(SpirvStorageClass.Output, outputType);
_pixelOutput = _module.AddGlobalVariable(
outputPointer,
SpirvStorageClass.Output);
_module.AddDecoration(_pixelOutput, SpirvDecoration.Location, 0);
_interfaces.Add(_pixelOutput);
var outputType = GetPixelOutputType(binding.Kind);
var outputPointer =
_module.TypePointer(SpirvStorageClass.Output, outputType);
var variable = _module.AddGlobalVariable(
outputPointer,
SpirvStorageClass.Output);
_module.AddName(variable, $"mrt{binding.GuestSlot}");
_module.AddDecoration(
variable,
SpirvDecoration.Location,
binding.HostLocation);
_pixelOutputs.Add(
binding.GuestSlot,
new SpirvPixelOutput(variable, outputType, binding.Kind));
_interfaces.Add(variable);
}
}
else
{
@@ -831,7 +887,10 @@ internal static partial class Gen5SpirvTranslator
1);
StoreV(2, Bitcast(_uintType, x), guardWithExec: false);
StoreV(3, Bitcast(_uintType, y), guardWithExec: false);
Store(_pixelOutput, _module.ConstantNull(GetPixelOutputType()));
foreach (var output in _pixelOutputs.Values)
{
Store(output.Variable, _module.ConstantNull(output.Type));
}
}
else
{
@@ -2084,21 +2143,27 @@ internal static partial class Gen5SpirvTranslator
if (_stage == Gen5SpirvStage.Pixel)
{
if (export.Target != 0)
if (!_pixelOutputs.TryGetValue(export.Target, out var output))
{
return true;
}
var outputType = GetPixelOutputType();
var values = new uint[4];
for (var component = 0; component < 4; component++)
{
var enabled = (export.EnableMask & (1u << component)) != 0;
if (!enabled)
{
values[component] = _outputKind == Gen5PixelOutputKind.Sint
? Bitcast(_intType, UInt(0))
: UInt(0);
values[component] = _module.AddInstruction(
SpirvOp.CompositeExtract,
output.Kind switch
{
Gen5PixelOutputKind.Uint => _uintType,
Gen5PixelOutputKind.Sint => _intType,
_ => _floatType,
},
Load(output.Type, output.Variable),
(uint)component);
continue;
}
@@ -2107,7 +2172,7 @@ internal static partial class Gen5SpirvTranslator
var value = LoadCompressedExportComponent(
instruction,
component);
values[component] = _outputKind switch
values[component] = output.Kind switch
{
Gen5PixelOutputKind.Uint => _module.AddInstruction(
SpirvOp.ConvertFToU,
@@ -2123,7 +2188,7 @@ internal static partial class Gen5SpirvTranslator
}
var raw = LoadV(instruction.Sources[component].Value);
values[component] = _outputKind switch
values[component] = output.Kind switch
{
Gen5PixelOutputKind.Uint => raw,
Gen5PixelOutputKind.Sint => Bitcast(_intType, raw),
@@ -2133,15 +2198,15 @@ internal static partial class Gen5SpirvTranslator
var vector = _module.AddInstruction(
SpirvOp.CompositeConstruct,
outputType,
output.Type,
values);
vector = _module.AddInstruction(
SpirvOp.Select,
outputType,
output.Type,
Load(_boolType, _exec),
vector,
Load(outputType, _pixelOutput));
Store(_pixelOutput, vector);
Load(output.Type, output.Variable));
Store(output.Variable, vector);
return true;
}
@@ -2209,8 +2274,8 @@ internal static partial class Gen5SpirvTranslator
(uint)(component & 1));
}
private uint GetPixelOutputType() =>
_outputKind switch
private uint GetPixelOutputType(Gen5PixelOutputKind kind) =>
kind switch
{
Gen5PixelOutputKind.Uint => _uvec4Type,
Gen5PixelOutputKind.Sint => _module.TypeVector(_intType, 4),
+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)