From 081760be3f778570714655ab96cce1f17604ce7c Mon Sep 17 00:00:00 2001 From: Dafenx Date: Wed, 15 Jul 2026 01:41:39 +0200 Subject: [PATCH] [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> --- .github/workflows/workflow.yml | 3 + src/SharpEmu.Libs/Agc/AgcExports.cs | 118 +++- src/SharpEmu.Libs/Agc/Gen5ShaderIr.cs | 5 + src/SharpEmu.Libs/Agc/Gen5SpirvTranslator.cs | 131 +++- .../VideoOut/VulkanVideoPresenter.cs | 666 +++++++++++++----- tools/SharpEmu.Tools.ShaderDump/Program.cs | 157 ++++- 6 files changed, 818 insertions(+), 262 deletions(-) diff --git a/.github/workflows/workflow.yml b/.github/workflows/workflow.yml index 1213ae5..4eb7b47 100644 --- a/.github/workflows/workflow.yml +++ b/.github/workflows/workflow.yml @@ -100,6 +100,9 @@ jobs: - name: Build solution run: dotnet build SharpEmu.slnx -c Release --no-restore + - name: Validate synthetic shaders + run: dotnet run --project tools/SharpEmu.Tools.ShaderDump/SharpEmu.Tools.ShaderDump.csproj -c Release -- artifacts/shader-dump + - name: Publish win-x64 CLI run: dotnet publish src/SharpEmu.CLI/SharpEmu.CLI.csproj -c Release -r win-x64 --self-contained true --no-restore -p:PublishDir="${env:PUBLISH_DIR}" diff --git a/src/SharpEmu.Libs/Agc/AgcExports.cs b/src/SharpEmu.Libs/Agc/AgcExports.cs index 57d17bd..4d15503 100644 --- a/src/SharpEmu.Libs/Agc/AgcExports.cs +++ b/src/SharpEmu.Libs/Agc/AgcExports.cs @@ -157,7 +157,7 @@ public static class AgcExports private static readonly HashSet _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. /// 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() + .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 registers, - RenderTargetDescriptor target) + IReadOnlyList 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)) { diff --git a/src/SharpEmu.Libs/Agc/Gen5ShaderIr.cs b/src/SharpEmu.Libs/Agc/Gen5ShaderIr.cs index 602eb56..d0a5d58 100644 --- a/src/SharpEmu.Libs/Agc/Gen5ShaderIr.cs +++ b/src/SharpEmu.Libs/Agc/Gen5ShaderIr.cs @@ -49,6 +49,11 @@ internal enum Gen5PixelOutputKind Sint, } +internal readonly record struct Gen5PixelOutputBinding( + uint GuestSlot, + uint HostLocation, + Gen5PixelOutputKind Kind); + internal enum Gen5SpirvStage { Vertex, diff --git a/src/SharpEmu.Libs/Agc/Gen5SpirvTranslator.cs b/src/SharpEmu.Libs/Agc/Gen5SpirvTranslator.cs index ea257d5..cb19c0a 100644 --- a/src/SharpEmu.Libs/Agc/Gen5SpirvTranslator.cs +++ b/src/SharpEmu.Libs/Agc/Gen5SpirvTranslator.cs @@ -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 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 _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 _interfaces = []; private readonly Dictionary _pixelInputs = []; + private readonly Dictionary _pixelOutputs = []; private readonly Dictionary _vertexOutputs = []; private readonly Dictionary _vertexInputsByPc = []; private readonly List _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 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), diff --git a/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.cs b/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.cs index ac3d89b..ee47c7a 100644 --- a/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.cs +++ b/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.cs @@ -104,14 +104,17 @@ internal readonly record struct VulkanGuestBlendState( } internal sealed record VulkanGuestRenderState( - VulkanGuestBlendState Blend, + IReadOnlyList 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 Targets, bool PublishTarget); internal sealed record VulkanComputeGuestDispatch( @@ -463,20 +473,70 @@ internal static unsafe class VulkanVideoPresenter VulkanGuestIndexBuffer? indexBuffer = null, IReadOnlyList? 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 textures, + IReadOnlyList globalMemoryBuffers, + uint attributeCount, + IReadOnlyList targets, + byte[]? vertexSpirv = null, + uint vertexCount = 3, + uint instanceCount = 1, + uint primitiveType = 4, + VulkanGuestIndexBuffer? indexBuffer = null, + IReadOnlyList? 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 GetTraceImages( TranslatedDrawResources resources, - GuestImageResource? renderTarget = null) + IReadOnlyList? renderTargets = null) { var images = new HashSet(); - 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 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 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 formats, + IReadOnlyList 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) diff --git a/tools/SharpEmu.Tools.ShaderDump/Program.cs b/tools/SharpEmu.Tools.ShaderDump/Program.cs index 4769634..6ec1e69 100644 --- a/tools/SharpEmu.Tools.ShaderDump/Program.cs +++ b/tools/SharpEmu.Tools.ShaderDump/Program.cs @@ -6,7 +6,7 @@ // Feeds hand-assembled Gen5 (gfx10) instruction words through the real // decode -> SPIR-V pipeline (Gen5ShaderTranslator / Gen5SpirvTranslator, via // reflection so no emulator source changes are required) and writes the -// resulting vertex and compute SPIR-V blobs to disk. The blobs can then be +// resulting vertex, pixel, and compute SPIR-V blobs to disk. The blobs can then be // checked with spirv-val / spirv-dis. // // Programs that contain buffer_store_dword automatically get a single @@ -14,7 +14,7 @@ // guestBuffers[0] (descriptor set 0, binding 0). // // Each program carries an expectation: ExpectTranslate=true programs must -// decode and emit both stages; ExpectTranslate=false programs pin a decode +// decode and emit the requested stages; ExpectTranslate=false programs pin a decode // failure that must stay loud. Any unexpected outcome makes the tool exit // non-zero, so it can gate scripts/CI. // @@ -43,6 +43,63 @@ const ulong ProgramAddress = 0x100000; 0xD56C0005, 0x00020501, // v_mul_hi_i32 v5, v1, v2 0xBF810000, // s_endpgm ]), + ("mrt", true, [ + 0x7E0002FF, 0x3F800000, // v_mov_b32 v0, 1.0f + 0x7E0202FF, 0x00000000, // v_mov_b32 v1, 0.0f + 0x7E0402FF, 0x00000000, // v_mov_b32 v2, 0.0f + 0x7E0602FF, 0x3F800000, // v_mov_b32 v3, 1.0f + 0x7E0802FF, 0x00000001, // v_mov_b32 v4, 1u + 0x7E0A02FF, 0x00000002, // v_mov_b32 v5, 2u + 0x7E0C02FF, 0x00000003, // v_mov_b32 v6, 3u + 0x7E0E02FF, 0x00000004, // v_mov_b32 v7, 4u + 0x7E1002FF, 0xFFFFFFFF, // v_mov_b32 v8, -1 + 0x7E1202FF, 0x00000002, // v_mov_b32 v9, 2 + 0x7E1402FF, 0xFFFFFFFD, // v_mov_b32 v10, -3 + 0x7E1602FF, 0x00000004, // v_mov_b32 v11, 4 + 0xF800000F, 0x03020100, // exp mrt0 v0, v1, v2, v3 + 0xF800003F, 0x07060504, // exp mrt3 v4, v5, v6, v7 + 0xF800086F, 0x0B0A0908, // exp mrt6 v8, v9, v10, v11 done + 0xBF810000, // s_endpgm + ]), + ("mrt-float2", true, [ + 0x7E0002FF, 0x3F800000, // v_mov_b32 v0, 1.0f + 0x7E0202FF, 0x3E800000, // v_mov_b32 v1, 0.25f + 0x7E0402FF, 0x3E800000, // v_mov_b32 v2, 0.25f + 0x7E0602FF, 0x3F000000, // v_mov_b32 v3, 0.5f + 0xF800000F, 0x03020100, // exp mrt0 v0, v1, v2, v3 + 0xF800081F, 0x03020100, // exp mrt1 v0, v1, v2, v3 done + 0xBF810000, // s_endpgm + ]), + ("mrt8", true, [ + 0x7E0002FF, 0x3F800000, // v_mov_b32 v0, 1.0f + 0x7E0202FF, 0x00000000, // v_mov_b32 v1, 0.0f + 0x7E0402FF, 0x00000000, // v_mov_b32 v2, 0.0f + 0x7E0602FF, 0x3F800000, // v_mov_b32 v3, 1.0f + 0xF800000F, 0x03020100, // exp mrt0 v0, v1, v2, v3 + 0xF800001F, 0x03020100, // exp mrt1 v0, v1, v2, v3 + 0xF800002F, 0x03020100, // exp mrt2 v0, v1, v2, v3 + 0xF800003F, 0x03020100, // exp mrt3 v0, v1, v2, v3 + 0xF800004F, 0x03020100, // exp mrt4 v0, v1, v2, v3 + 0xF800005F, 0x03020100, // exp mrt5 v0, v1, v2, v3 + 0xF800006F, 0x03020100, // exp mrt6 v0, v1, v2, v3 + 0xF800087F, 0x03020100, // exp mrt7 v0, v1, v2, v3 done + 0xBF810000, // s_endpgm + ]), + ("mrt-partial", true, [ + 0x7E0002FF, 0x3F4CCCCD, // v_mov_b32 v0, 0.8f + 0x7E0202FF, 0x3F333333, // v_mov_b32 v1, 0.7f + 0xF8000803, 0x03020100, // exp mrt0 v0, v1, off, off done + 0xBF810000, // s_endpgm + ]), + ("mrt-partial-merge", true, [ + 0x7E0002FF, 0x3DCCCCCD, // v_mov_b32 v0, 0.1f + 0x7E0202FF, 0x3E4CCCCD, // v_mov_b32 v1, 0.2f + 0x7E0C02FF, 0x3E99999A, // v_mov_b32 v6, 0.3f + 0x7E0E02FF, 0x3ECCCCCD, // v_mov_b32 v7, 0.4f + 0xF8000003, 0x03020100, // exp mrt0 v0, v1, off, off + 0xF800080C, 0x07060504, // exp mrt0 off, off, v6, v7 done + 0xBF810000, // s_endpgm + ]), ("sopp-hints", true, [ 0xBFA10001, // s_clause 0x1 0xBFA30000, // s_waitcnt_depctr 0x0 @@ -102,10 +159,18 @@ var imageBindingType = assembly.GetType("SharpEmu.Libs.Agc.Gen5ImageBinding") ?? throw new InvalidOperationException("Gen5ImageBinding not found"); var globalBindingType = assembly.GetType("SharpEmu.Libs.Agc.Gen5GlobalMemoryBinding") ?? throw new InvalidOperationException("Gen5GlobalMemoryBinding not found"); +var pixelOutputBindingType = assembly.GetType("SharpEmu.Libs.Agc.Gen5PixelOutputBinding") + ?? throw new InvalidOperationException("Gen5PixelOutputBinding not found"); +var pixelOutputKindType = assembly.GetType("SharpEmu.Libs.Agc.Gen5PixelOutputKind") + ?? throw new InvalidOperationException("Gen5PixelOutputKind not found"); var tryCompile = spirvTranslator.GetMethod( "TryCompileVertexShader", BindingFlags.Public | BindingFlags.Static) ?? throw new InvalidOperationException("Gen5SpirvTranslator.TryCompileVertexShader not found"); +var tryCompilePixel = spirvTranslator.GetMethods(BindingFlags.Public | BindingFlags.Static) + .Single(method => + method.Name == "TryCompilePixelShader" && + method.GetParameters()[2].ParameterType.IsGenericType); var tryCompileCompute = spirvTranslator.GetMethod( "TryCompileComputeShader", BindingFlags.Public | BindingFlags.Static) @@ -230,6 +295,94 @@ foreach (var (name, expectTranslate, words) in testPrograms) failures++; Console.WriteLine($"[{name}] compute emit: FAILED ({computeArgs[6]})"); } + + if (name.StartsWith("mrt", StringComparison.Ordinal)) + { + (uint GuestSlot, uint HostLocation, string Kind)[] outputSpecs = name switch + { + "mrt" => new (uint GuestSlot, uint HostLocation, string Kind)[] + { + (0, 0, "Float"), + (3, 1, "Uint"), + (6, 2, "Sint"), + }, + "mrt-float2" => [(0, 0, "Float"), (1, 1, "Float")], + "mrt8" => Enumerable.Range(0, 8) + .Select(index => ((uint)index, (uint)index, "Float")) + .ToArray(), + _ => [(0, 0, "Float")], + }; + var pixelOutputs = Array.CreateInstance(pixelOutputBindingType, outputSpecs.Length); + for (var index = 0; index < outputSpecs.Length; index++) + { + var spec = outputSpecs[index]; + pixelOutputs.SetValue( + Activator.CreateInstance( + pixelOutputBindingType, + spec.GuestSlot, + spec.HostLocation, + Enum.Parse(pixelOutputKindType, spec.Kind)), + index); + } + + var pixelArgs = PadWithDefaults( + tryCompilePixel, + [state, evaluation, pixelOutputs, null, null]); + if ((bool)tryCompilePixel.Invoke( + null, + BindingFlags.OptionalParamBinding, + null, + pixelArgs, + null)!) + { + var shader = pixelArgs[3]!; + var spirv = (byte[])shader.GetType().GetProperty("Spirv")!.GetValue(shader)!; + var path = Path.Combine(outputDirectory, $"{name}-ps.spv"); + File.WriteAllBytes(path, spirv); + Console.WriteLine($"[{name}] pixel emit: success, {spirv.Length} bytes -> {path}"); + } + else + { + failures++; + Console.WriteLine($"[{name}] pixel emit: FAILED ({pixelArgs[4]})"); + } + + if (name == "mrt") + { + var invalidOutputs = Array.CreateInstance(pixelOutputBindingType, 2); + invalidOutputs.SetValue( + Activator.CreateInstance( + pixelOutputBindingType, + 0u, + 0u, + Enum.Parse(pixelOutputKindType, "Float")), + 0); + invalidOutputs.SetValue( + Activator.CreateInstance( + pixelOutputBindingType, + 3u, + 7u, + Enum.Parse(pixelOutputKindType, "Float")), + 1); + var invalidPixelArgs = PadWithDefaults( + tryCompilePixel, + [state, evaluation, invalidOutputs, null, null]); + if ((bool)tryCompilePixel.Invoke( + null, + BindingFlags.OptionalParamBinding, + null, + invalidPixelArgs, + null)!) + { + failures++; + Console.WriteLine("[mrt] FAILED: sparse host locations were accepted"); + } + else + { + Console.WriteLine($"[mrt] sparse host locations rejected as expected ({invalidPixelArgs[4]})"); + } + } + } } Console.WriteLine(failures == 0