diff --git a/src/SharpEmu.Libs/Agc/AgcExports.cs b/src/SharpEmu.Libs/Agc/AgcExports.cs index ef37f955..bd077a7a 100644 --- a/src/SharpEmu.Libs/Agc/AgcExports.cs +++ b/src/SharpEmu.Libs/Agc/AgcExports.cs @@ -142,7 +142,7 @@ public static class AgcExports private static readonly HashSet _tracedDcbSizes = new(); private static readonly HashSet<(ulong Es, ulong Ps, GuestDrawKind Kind)> _tracedShaderTranslations = new(); private static readonly HashSet<(ulong Es, ulong Ps)> _tracedShaderDecodePairs = new(); - private static readonly HashSet<(ulong Es, ulong Ps, ulong Target)> _tracedShaderDraws = new(); + private static readonly HashSet<(ulong Es, ulong Ps, ulong Target, ulong Texture, uint VertexCount)> _tracedShaderDraws = new(); private static readonly HashSet<(ulong Ps, string Error)> _tracedShaderFailures = new(); private static readonly HashSet<(int Handle, int Index, ulong Address, string Path)> _tracedDisplayBuffers = new(); private static readonly HashSet _tracedComputeShaders = new(); @@ -2788,7 +2788,7 @@ public static class AgcExports $"ps=0x{(hasPixelShader ? pixelShaderAddress : 0):X16}"); } - if (vertexCount is not (3 or 4 or 6)) + if (vertexCount == 0 || vertexCount > 1_048_576) { return; } @@ -2872,8 +2872,9 @@ public static class AgcExports lock (_submitTraceGate) { + var firstTextureAddress = translatedDraw.Textures.FirstOrDefault()?.Descriptor.Address ?? 0; if (_tracedShaderDraws.Add( - (exportShaderAddress, pixelShaderAddress, firstTarget.Address))) + (exportShaderAddress, pixelShaderAddress, firstTarget.Address, firstTextureAddress, vertexCount))) { TraceTranslatedGuestDraw( ctx, @@ -3033,6 +3034,11 @@ public static class AgcExports return false; } + TraceAgcShader( + $"agc.texture_binding ps=0x{pixelShaderAddress:X16} es=0x{exportShaderAddress:X16} " + + $"pc=0x{binding.Pc:X} op={binding.Opcode} storage={(Gen5ShaderTranslator.IsStorageImageOperation(binding.Opcode) ? 1 : 0)} " + + $"decoded={FormatTextureDescriptor(texture)} " + + $"raw={FormatShaderDwords(binding.ResourceDescriptor)} sampler={FormatShaderDwords(binding.SamplerDescriptor)}"); textures.Add( new TranslatedImageBinding( texture, @@ -3477,7 +3483,8 @@ public static class AgcExports ',', draw.VertexInputs.Select(input => $"{input.Location}:pc=0x{input.Pc:X}:0x{input.BaseAddress:X16}" + - $":stride{input.Stride}:off{input.OffsetBytes}:c{input.ComponentCount}")); + $":stride{input.Stride}:off{input.OffsetBytes}:c{input.ComponentCount}" + + $":fmt{input.DataFormat}/num{input.NumberFormat}")); var scissor = draw.RenderState.Scissor is { } drawScissor ? $"{drawScissor.X},{drawScissor.Y},{drawScissor.Width}x{drawScissor.Height}" : "full"; @@ -3550,6 +3557,8 @@ public static class AgcExports buffers[index] = new VulkanGuestVertexBuffer( binding.Location, binding.ComponentCount, + binding.DataFormat, + binding.NumberFormat, binding.BaseAddress, binding.Stride, binding.OffsetBytes, @@ -3579,7 +3588,10 @@ public static class AgcExports } var sourceWidth = descriptor.TileMode == 0 - ? Math.Max(descriptor.Width, descriptor.Pitch) + ? GetLinearTexturePitch( + Math.Max(descriptor.Width, descriptor.Pitch), + descriptor.Height, + descriptor.Format) : descriptor.Width; var sourceByteCount = GetTextureByteCount( descriptor.Format, @@ -3617,7 +3629,7 @@ public static class AgcExports IsStorage: true, MipLevels: descriptor.MipLevels, MipLevel: mipLevel, - Pitch: descriptor.Pitch, + Pitch: sourceWidth, TileMode: descriptor.TileMode, DstSelect: descriptor.DstSelect, Sampler: ToVulkanSampler(samplerDescriptor)); @@ -3663,7 +3675,7 @@ public static class AgcExports IsStorage: isStorage, MipLevels: descriptor.MipLevels, MipLevel: mipLevel, - Pitch: descriptor.Pitch, + Pitch: sourceWidth, TileMode: descriptor.TileMode, DstSelect: descriptor.DstSelect, Sampler: ToVulkanSampler(samplerDescriptor)); @@ -4326,6 +4338,28 @@ public static class AgcExports : checked(((ulong)width + 3) / 4 * (((ulong)height + 3) / 4) * blockBytes); } + private static uint GetLinearTexturePitch(uint pitch, uint height, uint format) + { + var bytesPerTexel = GetTextureBytesPerTexel(format); + if (bytesPerTexel == 0 || height == 0) + { + return pitch; + } + + var pitchAlignment = Math.Max(8UL, 64UL / bytesPerTexel); + var alignedPitch = AlignUp(pitch, pitchAlignment); + var sliceAlignment = Math.Max(64UL, 256UL / bytesPerTexel); + while ((alignedPitch * height) % sliceAlignment != 0) + { + alignedPitch += pitchAlignment; + } + + return checked((uint)alignedPitch); + } + + private static ulong AlignUp(ulong value, ulong alignment) => + (value + alignment - 1) & ~(alignment - 1); + private static void TraceShaderTranslationMiss( CpuContext ctx, SubmittedDcbState state, @@ -4594,6 +4628,10 @@ public static class AgcExports return false; } + // GFX10/RDNA2 T# layout: WIDTH is split across word1[31:30] (lo 2 bits) + // and word2[11:0] (hi 12 bits); FORMAT is the combined 9-bit field at + // word1[28:20]. Verified against Kyty's decode of the same game + // descriptors (fmt=56=8_8_8_8_UNORM, extent 1280x720, sw_mode 27). // GNM T# exposes a 38-bit baseaddr256 field, but RPCSX and the // Demon's Souls descriptors both show that only the low 32 bits are // part of the guest GPU VA. The upper baseaddr bits carry resource @@ -5367,6 +5405,12 @@ public static class AgcExports ? "none" : string.Join(',', values.Select(static value => $"{value:X8}")); + private static string FormatTextureDescriptor(TextureDescriptor descriptor) => + $"addr=0x{descriptor.Address:X16} {descriptor.Width}x{descriptor.Height} " + + $"fmt={descriptor.Format} num={descriptor.NumberType} tile={descriptor.TileMode} " + + $"type={descriptor.Type} levels={descriptor.BaseLevel}-{descriptor.LastLevel} " + + $"pitch={descriptor.Pitch} dst=0x{descriptor.DstSelect:X3}"; + private static void DumpSpirv( string stage, ulong shaderAddress, diff --git a/src/SharpEmu.Libs/Agc/Gen5ShaderIr.cs b/src/SharpEmu.Libs/Agc/Gen5ShaderIr.cs index d7ddc76d..602eb568 100644 --- a/src/SharpEmu.Libs/Agc/Gen5ShaderIr.cs +++ b/src/SharpEmu.Libs/Agc/Gen5ShaderIr.cs @@ -276,6 +276,8 @@ internal sealed record Gen5VertexInputBinding( uint Pc, uint Location, uint ComponentCount, + uint DataFormat, + uint NumberFormat, ulong BaseAddress, uint Stride, uint OffsetBytes, diff --git a/src/SharpEmu.Libs/Agc/Gen5ShaderScalarEvaluator.cs b/src/SharpEmu.Libs/Agc/Gen5ShaderScalarEvaluator.cs index 269d596a..67d79655 100644 --- a/src/SharpEmu.Libs/Agc/Gen5ShaderScalarEvaluator.cs +++ b/src/SharpEmu.Libs/Agc/Gen5ShaderScalarEvaluator.cs @@ -20,7 +20,9 @@ internal static class Gen5ShaderScalarEvaluator ulong BaseAddress, uint Stride, uint NumRecords, - ulong SizeBytes); + ulong SizeBytes, + uint NumberFormat, + uint DataFormat); public static bool TryResolveImageBindings( CpuContext ctx, @@ -400,14 +402,21 @@ internal static class Gen5ShaderScalarEvaluator return false; } + var bindingData = data; + var bindingStride = descriptor.Stride; + var bindingOffset = unchecked((uint)control.OffsetBytes + scalarOffset); + var bindingDataFormat = descriptor.DataFormat; + var bindingNumberFormat = descriptor.NumberFormat; binding = new Gen5VertexInputBinding( instruction.Pc, location, control.DwordCount, + bindingDataFormat, + bindingNumberFormat, descriptor.BaseAddress, - descriptor.Stride, - unchecked((uint)control.OffsetBytes + scalarOffset), - data); + bindingStride, + bindingOffset, + bindingData); return true; } @@ -1416,7 +1425,7 @@ internal static class Gen5ShaderScalarEvaluator word2 == 0 && word3 == 0) { - descriptor = new BufferDescriptor(0, 0, 0, 0); + descriptor = new BufferDescriptor(0, 0, 0, 0, 0, 0); return true; } @@ -1428,19 +1437,64 @@ internal static class Gen5ShaderScalarEvaluator return false; } - descriptor = new BufferDescriptor(0, 0, 0, 0); + descriptor = new BufferDescriptor(0, 0, 0, 0, 0, 0); return true; } - var baseAddress = word0 | ((ulong)(word1 & 0x0FFFu) << 32); + var baseAddress = word0 | ((ulong)(word1 & 0xFFFFu) << 32); var stride = (word1 >> 16) & 0x3FFFu; + var unifiedFormat = (word3 >> 12) & 0x7Fu; + var (dataFormat, numberFormat) = + DecodeGfx10BufferFormat(unifiedFormat); var sizeBytes = stride == 0 ? word2 : (ulong)stride * word2; - descriptor = new BufferDescriptor(baseAddress, stride, word2, sizeBytes); + descriptor = new BufferDescriptor(baseAddress, stride, word2, sizeBytes, numberFormat, dataFormat); return true; } + private static (uint DataFormat, uint NumberFormat) + DecodeGfx10BufferFormat(uint format) => + format switch + { + 0 => (0, 0), + >= 1 and <= 6 => (1, format - 1), + >= 7 and <= 13 => (2, DecodeUnifiedNumber(format - 7, 7)), + >= 14 and <= 19 => (3, format - 14), + >= 20 and <= 22 => (4, DecodeIntegerOrFloatNumber(format - 20)), + >= 23 and <= 29 => (5, DecodeUnifiedNumber(format - 23, 7)), + >= 30 and <= 36 => (6, DecodeUnifiedNumber(format - 30, 7)), + >= 37 and <= 43 => (7, DecodeUnifiedNumber(format - 37, 7)), + >= 44 and <= 49 => (8, format - 44), + >= 50 and <= 55 => (9, format - 50), + >= 56 and <= 61 => (10, format - 56), + >= 62 and <= 64 => (11, DecodeIntegerOrFloatNumber(format - 62)), + >= 65 and <= 71 => (12, DecodeUnifiedNumber(format - 65, 7)), + >= 72 and <= 74 => (13, DecodeIntegerOrFloatNumber(format - 72)), + >= 75 and <= 77 => (14, DecodeIntegerOrFloatNumber(format - 75)), + 128 => (1, 9), + 129 => (3, 9), + 130 => (10, 9), + 132 => (34, 7), + 133 => (16, 0), + 134 => (17, 0), + 135 => (18, 0), + 136 => (19, 0), + 140 => (4, 7), + _ => (0, 0), + }; + + private static uint DecodeUnifiedNumber(uint offset, uint formatCount) => + offset == formatCount - 1 ? 7u : offset; + + private static uint DecodeIntegerOrFloatNumber(uint offset) => + offset switch + { + 0 => 4, + 1 => 5, + _ => 7, + }; + private static bool TryReadUserDataScalarLoad( Gen5ShaderState state, Gen5ShaderInstruction instruction, diff --git a/src/SharpEmu.Libs/Agc/Gen5SpirvTranslator.Alu.cs b/src/SharpEmu.Libs/Agc/Gen5SpirvTranslator.Alu.cs index f2682907..21f27fc2 100644 --- a/src/SharpEmu.Libs/Agc/Gen5SpirvTranslator.Alu.cs +++ b/src/SharpEmu.Libs/Agc/Gen5SpirvTranslator.Alu.cs @@ -212,12 +212,26 @@ internal static partial class Gen5SpirvTranslator case "VSinF32": result = EmitFloatResult( instruction, - Ext(13, _floatType, GetFloatSource(instruction, 0))); + Ext( + 13, + _floatType, + _module.AddInstruction( + SpirvOp.FMul, + _floatType, + GetFloatSource(instruction, 0), + Float(MathF.Tau)))); break; case "VCosF32": result = EmitFloatResult( instruction, - Ext(14, _floatType, GetFloatSource(instruction, 0))); + Ext( + 14, + _floatType, + _module.AddInstruction( + SpirvOp.FMul, + _floatType, + GetFloatSource(instruction, 0), + Float(MathF.Tau)))); break; case "VAddF32": result = EmitFloatBinary(instruction, SpirvOp.FAdd); @@ -637,11 +651,13 @@ internal static partial class Gen5SpirvTranslator } case "VCvtPkrtzF16F32": { + var first = TruncateFloat32ForPack(GetFloatSource(instruction, 0)); + var second = TruncateFloat32ForPack(GetFloatSource(instruction, 1)); var vector = _module.AddInstruction( SpirvOp.CompositeConstruct, _vec2Type, - GetFloatSource(instruction, 0), - GetFloatSource(instruction, 1)); + first, + second); result = Ext(58, _uintType, vector); break; } @@ -2067,6 +2083,14 @@ internal static partial class Gen5SpirvTranslator return Bitcast(_uintType, value); } + private uint TruncateFloat32ForPack(uint value) + { + var raw = BitwiseAnd( + Bitcast(_uintType, value), + UInt(0xFFFF_E000)); + return Bitcast(_floatType, raw); + } + private uint Ext(uint operation, uint resultType, params uint[] operands) { var values = new uint[2 + operands.Length]; diff --git a/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.cs b/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.cs index 64dfffe9..dc7da053 100644 --- a/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.cs +++ b/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.cs @@ -50,6 +50,8 @@ internal sealed record VulkanGuestMemoryBuffer( internal sealed record VulkanGuestVertexBuffer( uint Location, uint ComponentCount, + uint DataFormat, + uint NumberFormat, ulong BaseAddress, uint Stride, uint OffsetBytes, @@ -163,6 +165,8 @@ internal static unsafe class VulkanVideoPresenter private static readonly object _gate = new(); private static readonly Queue _pendingGuestWork = new(); private static readonly Dictionary _availableGuestImages = new(); + private static readonly HashSet<(ulong Address, uint Width, uint Height)> + _tracedGuestImageSubmissions = []; private static Thread? _thread; private static Presentation? _latestPresentation; private static byte[]? _copyFragmentSpirv; @@ -558,44 +562,38 @@ internal static unsafe class VulkanVideoPresenter uint height, uint pitchInPixel) { - uint format; + var traceSubmission = false; lock (_gate) { if (_closed || - !_availableGuestImages.TryGetValue(address, out format)) + !_availableGuestImages.ContainsKey(address)) { return false; } + + traceSubmission = + _tracedGuestImageSubmissions.Add((address, width, height)); + var sequence = (_latestPresentation?.Sequence ?? 0) + 1; + _latestPresentation = new Presentation( + null, + width, + height, + sequence, + GuestDrawKind.None, + TranslatedDraw: null, + RequiredGuestWorkSequence: 0, + IsSplash: false, + GuestImageAddress: address); } - var effectivePitch = pitchInPixel == 0 ? width : pitchInPixel; - - Console.Error.WriteLine( - $"[LOADER][TRACE] vk.submit_guest_image addr=0x{address:X16} " + - $"size={width}x{height} pitch={effectivePitch}"); - - if (!TryGetCopyFragmentShader(out var fragmentSpirv)) + if (traceSubmission) { - return false; + var effectivePitch = pitchInPixel == 0 ? width : pitchInPixel; + Console.Error.WriteLine( + $"[LOADER][TRACE] vk.submit_guest_image addr=0x{address:X16} " + + $"size={width}x{height} pitch={effectivePitch}"); } - SubmitTranslatedDraw( - fragmentSpirv, - [ - new VulkanGuestDrawTexture( - address, - width, - height, - format, - NumberType: 0, - [], - IsFallback: false, - IsStorage: false), - ], - [], - width, - height, - attributeCount: 1); return true; } @@ -807,7 +805,8 @@ internal static unsafe class VulkanVideoPresenter GuestDrawKind DrawKind, VulkanTranslatedGuestDraw? TranslatedDraw, long RequiredGuestWorkSequence, - bool IsSplash); + bool IsSplash, + ulong GuestImageAddress = 0); private sealed class Presenter : IDisposable { @@ -855,11 +854,16 @@ internal static unsafe class VulkanVideoPresenter private bool _firstGuestDrawPresented; private bool _splashPresented; private bool _swapchainRecreateDeferred; + private bool _tracedPresentedSwapchain; + private bool _swapchainReadbackPending; + private int _directPresentationCount; private readonly Dictionary _guestImages = new(); private readonly HashSet<(ulong Address, uint Width, uint Height, Format Format)> _tracedTextureCacheHits = new(); private readonly HashSet<(ulong Address, uint Width, uint Height, Format Format)> _tracedTextureUploads = new(); + private readonly HashSet<(ulong Address, uint Width, uint Height, uint Format)> _dumpedTextures = new(); private readonly HashSet<(ulong Address, int Size)> _tracedGlobalBuffers = new(); private readonly HashSet _tracedGuestImageContents = new(); + private readonly Dictionary _tracedGuestWriteCounts = new(); private int _tracedVertexBufferCount; private readonly Dictionary _computePipelines = new(ReferenceEqualityComparer.Instance); @@ -922,6 +926,8 @@ internal static unsafe class VulkanVideoPresenter public ulong Size; public uint Location; public uint ComponentCount; + public uint DataFormat; + public uint NumberFormat; public uint Stride; public uint OffsetBytes; } @@ -1361,7 +1367,10 @@ internal static unsafe class VulkanVideoPresenter ImageColorSpace = surfaceFormat.ColorSpace, ImageExtent = _extent, ImageArrayLayers = 1, - ImageUsage = ImageUsageFlags.TransferDstBit | ImageUsageFlags.ColorAttachmentBit, + ImageUsage = + ImageUsageFlags.TransferDstBit | + ImageUsageFlags.TransferSrcBit | + ImageUsageFlags.ColorAttachmentBit, ImageSharingMode = SharingMode.Exclusive, PreTransform = capabilities.CurrentTransform, CompositeAlpha = compositeAlpha, @@ -2233,7 +2242,10 @@ internal static unsafe class VulkanVideoPresenter { Location = vertexBuffer.Location, Binding = (uint)index, - Format = ToVkVertexFormat(vertexBuffer.ComponentCount), + Format = ToVkVertexFormat( + vertexBuffer.DataFormat, + vertexBuffer.NumberFormat, + vertexBuffer.ComponentCount), Offset = 0, }; } @@ -2420,8 +2432,7 @@ internal static unsafe class VulkanVideoPresenter var vkFormat = GetTextureFormat(texture.Format, texture.NumberType); if (texture.Address != 0 && _guestImages.TryGetValue(texture.Address, out var guestImage) && - guestImage.Width == texture.Width && - guestImage.Height == texture.Height && + IsCompatibleGuestImageAlias(texture, guestImage) && IsCompatibleViewFormat(guestImage.Format, vkFormat) && TryGetOrCreateGuestImageView( guestImage, @@ -2440,6 +2451,16 @@ internal static unsafe class VulkanVideoPresenter $"image_format={guestImage.Format} view_format={vkFormat}"); } + if (guestImage.Width != texture.Width || + guestImage.Height != texture.Height) + { + TraceVulkanShader( + $"vk.texture_cache_alias addr=0x{texture.Address:X16} " + + $"texture={texture.Width}x{texture.Height} " + + $"image={guestImage.Width}x{guestImage.Height} " + + $"tile={texture.TileMode} format={vkFormat}"); + } + return new TextureResource { Address = texture.Address, @@ -2457,6 +2478,27 @@ internal static unsafe class VulkanVideoPresenter return CreateTextureResource(texture); } + private static bool IsCompatibleGuestImageAlias( + VulkanGuestDrawTexture texture, + GuestImageResource guestImage) + { + if (guestImage.Width == texture.Width && + guestImage.Height == texture.Height) + { + return true; + } + + if (texture.TileMode == 0 || + texture.Width == 0 || + texture.Height == 0) + { + return false; + } + + return texture.Width <= guestImage.Width && + texture.Height <= guestImage.Height; + } + [MethodImpl(MethodImplOptions.NoInlining)] private TextureResource ResolveStorageImageResource(VulkanGuestDrawTexture texture) { @@ -2673,6 +2715,7 @@ internal static unsafe class VulkanVideoPresenter var pixels = texture.RgbaPixels.Length == (int)expectedSize ? texture.RgbaPixels : CreateFallbackTexturePixels(texture.Format, rowLength, height, expectedSize); + DumpTextureUpload(texture, pixels, rowLength, width, height); var uploadPixels = texture.Format == 13 ? ExpandRgb32Pixels(pixels) : pixels; @@ -2754,6 +2797,102 @@ internal static unsafe class VulkanVideoPresenter }; } + private void DumpTextureUpload( + VulkanGuestDrawTexture texture, + byte[] pixels, + uint rowLength, + uint width, + uint height) + { + if (!string.Equals( + Environment.GetEnvironmentVariable("SHARPEMU_DUMP_TEXTURES"), + "1", + StringComparison.Ordinal) || + texture.IsFallback || + texture.IsStorage || + GetTextureBytesPerPixel(texture.Format) != 4 || + width == 0 || + height == 0 || + !_dumpedTextures.Add((texture.Address, width, height, texture.Format))) + { + return; + } + + var rowBytes = checked((int)rowLength * 4); + var visibleRowBytes = checked((int)width * 4); + if (pixels.Length < checked(rowBytes * (int)height)) + { + return; + } + + var directory = Path.Combine(AppContext.BaseDirectory, "texture-dumps"); + Directory.CreateDirectory(directory); + var path = Path.Combine( + directory, + $"tex-{texture.Address:X16}-{width}x{height}-fmt{texture.Format}-row{rowLength}.bmp"); + WriteRgbaBmp(path, pixels, rowBytes, visibleRowBytes, (int)width, (int)height); + } + + private static void WriteRgbaBmp( + string path, + byte[] rgba, + int sourceRowBytes, + int visibleRowBytes, + int width, + int height) + { + const int fileHeaderSize = 14; + const int infoHeaderSize = 40; + const int bytesPerPixel = 4; + var pixelBytes = checked(width * height * bytesPerPixel); + var fileSize = fileHeaderSize + infoHeaderSize + pixelBytes; + var output = new byte[fileSize]; + + output[0] = (byte)'B'; + output[1] = (byte)'M'; + WriteUInt32(output, 2, (uint)fileSize); + WriteUInt32(output, 10, fileHeaderSize + infoHeaderSize); + WriteUInt32(output, 14, infoHeaderSize); + WriteInt32(output, 18, width); + WriteInt32(output, 22, -height); + WriteUInt16(output, 26, 1); + WriteUInt16(output, 28, 32); + WriteUInt32(output, 34, (uint)pixelBytes); + + var destinationOffset = fileHeaderSize + infoHeaderSize; + for (var y = 0; y < height; y++) + { + var sourceOffset = y * sourceRowBytes; + for (var x = 0; x < visibleRowBytes; x += bytesPerPixel) + { + var destination = destinationOffset + y * visibleRowBytes + x; + output[destination + 0] = rgba[sourceOffset + x + 2]; + output[destination + 1] = rgba[sourceOffset + x + 1]; + output[destination + 2] = rgba[sourceOffset + x + 0]; + output[destination + 3] = rgba[sourceOffset + x + 3]; + } + } + + File.WriteAllBytes(path, output); + } + + private static void WriteUInt16(byte[] output, int offset, ushort value) + { + output[offset + 0] = (byte)value; + output[offset + 1] = (byte)(value >> 8); + } + + private static void WriteUInt32(byte[] output, int offset, uint value) + { + output[offset + 0] = (byte)value; + output[offset + 1] = (byte)(value >> 8); + output[offset + 2] = (byte)(value >> 16); + output[offset + 3] = (byte)(value >> 24); + } + + private static void WriteInt32(byte[] output, int offset, int value) => + WriteUInt32(output, offset, unchecked((uint)value)); + private Sampler CreateSampler(VulkanGuestSampler sampler) { var minLod = DecodeSamplerMipFilter(sampler) == 0 @@ -2872,6 +3011,7 @@ internal static unsafe class VulkanVideoPresenter $"vk.vertex_buffer loc={guestBuffer.Location} " + $"base=0x{guestBuffer.BaseAddress:X16} stride={guestBuffer.Stride} " + $"offset={guestBuffer.OffsetBytes} comps={guestBuffer.ComponentCount} " + + $"fmt={guestBuffer.DataFormat}/num={guestBuffer.NumberFormat} " + $"bytes={guestBuffer.Data.Length}"); } @@ -2882,6 +3022,8 @@ internal static unsafe class VulkanVideoPresenter Size = size, Location = guestBuffer.Location, ComponentCount = guestBuffer.ComponentCount, + DataFormat = guestBuffer.DataFormat, + NumberFormat = guestBuffer.NumberFormat, Stride = guestBuffer.Stride, OffsetBytes = guestBuffer.OffsetBytes, }; @@ -2931,7 +3073,83 @@ internal static unsafe class VulkanVideoPresenter _ => PrimitiveTopology.TriangleList, }; - private static Format ToVkVertexFormat(uint componentCount) => + private static Format ToVkVertexFormat( + uint dataFormat, + uint numberFormat, + uint componentCount) => + (dataFormat, numberFormat) switch + { + (1, 0) => Format.R8Unorm, + (1, 1) => Format.R8SNorm, + (1, 4) => Format.R8Uint, + (1, 5) => Format.R8Sint, + (1, 9) => Format.R8Srgb, + (2, 0) => Format.R16Unorm, + (2, 1) => Format.R16SNorm, + (2, 4) => Format.R16Uint, + (2, 5) => Format.R16Sint, + (2, 7) => Format.R16Sfloat, + (3, 0) => Format.R8G8Unorm, + (3, 1) => Format.R8G8SNorm, + (3, 4) => Format.R8G8Uint, + (3, 5) => Format.R8G8Sint, + (3, 9) => Format.R8G8Srgb, + (4, 4) => Format.R32Uint, + (4, 5) => Format.R32Sint, + (4, 7) => Format.R32Sfloat, + (5, 0) => Format.R16G16Unorm, + (5, 1) => Format.R16G16SNorm, + (5, 2) => Format.R16G16Uscaled, + (5, 3) => Format.R16G16Sscaled, + (5, 4) => Format.R16G16Uint, + (5, 5) => Format.R16G16Sint, + (5, 7) => Format.R16G16Sfloat, + (6, 7) => Format.B10G11R11UfloatPack32, + (7, 7) => Format.B10G11R11UfloatPack32, + (8, 0) => Format.A2B10G10R10UnormPack32, + (8, 1) => Format.A2B10G10R10SNormPack32, + (8, 2) => Format.A2B10G10R10UscaledPack32, + (8, 3) => Format.A2B10G10R10SscaledPack32, + (8, 4) => Format.A2B10G10R10UintPack32, + (8, 5) => Format.A2B10G10R10SintPack32, + (9, 0) => Format.A2R10G10B10UnormPack32, + (9, 1) => Format.A2R10G10B10SNormPack32, + (9, 2) => Format.A2R10G10B10UscaledPack32, + (9, 3) => Format.A2R10G10B10SscaledPack32, + (9, 4) => Format.A2R10G10B10UintPack32, + (9, 5) => Format.A2R10G10B10SintPack32, + (10, 0) => Format.R8G8B8A8Unorm, + (10, 1) => Format.R8G8B8A8SNorm, + (10, 2) => Format.R8G8B8A8Uscaled, + (10, 3) => Format.R8G8B8A8Sscaled, + (10, 4) => Format.R8G8B8A8Uint, + (10, 5) => Format.R8G8B8A8Sint, + (10, 9) => Format.R8G8B8A8Srgb, + (11, 4) => Format.R32G32Uint, + (11, 5) => Format.R32G32Sint, + (11, 7) => Format.R32G32Sfloat, + (12, 0) => Format.R16G16B16A16Unorm, + (12, 1) => Format.R16G16B16A16SNorm, + (12, 2) => Format.R16G16B16A16Uscaled, + (12, 3) => Format.R16G16B16A16Sscaled, + (12, 4) => Format.R16G16B16A16Uint, + (12, 5) => Format.R16G16B16A16Sint, + (12, 6) => Format.R16G16B16A16SNorm, + (12, 7) => Format.R16G16B16A16Sfloat, + (13, 4) => Format.R32G32B32Uint, + (13, 5) => Format.R32G32B32Sint, + (13, 7) => Format.R32G32B32Sfloat, + (14, 4) => Format.R32G32B32A32Uint, + (14, 5) => Format.R32G32B32A32Sint, + (14, 7) => Format.R32G32B32A32Sfloat, + (16, 0) => Format.B5G6R5UnormPack16, + (17, 0) => Format.R5G5B5A1UnormPack16, + (19, 0) => Format.R4G4B4A4UnormPack16, + (34, 7) => Format.E5B9G9R9UfloatPack32, + _ => ToVkFloatVertexFormat(componentCount), + }; + + private static Format ToVkFloatVertexFormat(uint componentCount) => componentCount switch { 1 => Format.R32Sfloat, @@ -3317,7 +3535,7 @@ internal static unsafe class VulkanVideoPresenter { _stagingBuffer = CreateBuffer( size, - BufferUsageFlags.TransferSrcBit, + BufferUsageFlags.TransferSrcBit | BufferUsageFlags.TransferDstBit, MemoryPropertyFlags.HostVisibleBit | MemoryPropertyFlags.HostCoherentBit, out _stagingMemory); _stagingSize = size; @@ -3588,6 +3806,27 @@ internal static unsafe class VulkanVideoPresenter _availableGuestImages[target.Address] = guestTextureFormat; } } + if (ShouldTraceGuestImageWriteForDiagnostics(target.Address)) + { + 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} " + @@ -4051,7 +4290,8 @@ internal static unsafe class VulkanVideoPresenter if (presentation.Pixels is null && presentation.DrawKind != GuestDrawKind.FullscreenBarycentric && - presentation.TranslatedDraw is null) + presentation.TranslatedDraw is null && + presentation.GuestImageAddress == 0) { return; } @@ -4074,6 +4314,28 @@ internal static unsafe class VulkanVideoPresenter } TranslatedDrawResources? translatedResources = null; + GuestImageResource? presentedGuestImage = null; + if (presentation.GuestImageAddress != 0 && + (!_guestImages.TryGetValue( + presentation.GuestImageAddress, + out presentedGuestImage) || + !presentedGuestImage.Initialized)) + { + return; + } + if (presentedGuestImage is not null) + { + _directPresentationCount++; + if (ShouldTracePresentedGuestImageContentsForDiagnostics() && + _directPresentationCount is 1 or 30 or 120) + { + Console.Error.WriteLine( + $"[LOADER][TRACE] vk.present_sample frame={_directPresentationCount} " + + $"addr=0x{presentedGuestImage.Address:X16}"); + TraceGuestImageContents(presentedGuestImage); + } + } + if (presentation.TranslatedDraw is { } translatedDraw) { try @@ -4175,6 +4437,11 @@ internal static unsafe class VulkanVideoPresenter _vk.CmdEndRenderPass(_commandBuffer); waitStage = PipelineStageFlags.ColorAttachmentOutputBit; } + else if (presentedGuestImage is not null) + { + RecordGuestImageBlit(imageIndex, presentedGuestImage); + waitStage = PipelineStageFlags.TransferBit; + } else if (translatedResources is not null) { RecordTranslatedDraw(imageIndex, translatedResources); @@ -4231,6 +4498,10 @@ internal static unsafe class VulkanVideoPresenter CheckSwapchainResult(presentResult, "vkQueuePresentKHR"); recreateAfterPresent |= presentResult == Result.SuboptimalKhr; Check(_vk.QueueWaitIdle(_queue), "vkQueueWaitIdle"); + if (_swapchainReadbackPending) + { + TraceSwapchainReadback(); + } CollectCompletedGuestSubmissions(waitForOldest: false); if (translatedResources is not null) { @@ -4259,8 +4530,11 @@ internal static unsafe class VulkanVideoPresenter { _firstGuestDrawPresented = true; Console.Error.WriteLine( - $"[LOADER][INFO] Vulkan VideoOut presented translated guest draw: " + - (presentation.TranslatedDraw is null + $"[LOADER][INFO] Vulkan VideoOut presented guest frame: " + + (presentedGuestImage is not null + ? $"image=0x{presentedGuestImage.Address:X16} " + + $"{presentedGuestImage.Width}x{presentedGuestImage.Height}" + : presentation.TranslatedDraw is null ? $"{presentation.DrawKind}" : $"shader textures={presentation.TranslatedDraw.Textures.Count}")); } @@ -4760,7 +5034,23 @@ internal static unsafe class VulkanVideoPresenter private static bool ShouldTraceGuestImageAddressForDiagnostics(ulong address) { - var addresses = Environment.GetEnvironmentVariable("SHARPEMU_TRACE_GUEST_IMAGE_ADDRS"); + return AddressListContains( + "SHARPEMU_TRACE_GUEST_IMAGE_ADDRS", + address); + } + + private static bool ShouldTraceGuestImageWriteForDiagnostics(ulong address) + { + return AddressListContains( + "SHARPEMU_TRACE_GUEST_WRITES", + address); + } + + private static bool AddressListContains( + string environmentVariable, + ulong address) + { + var addresses = Environment.GetEnvironmentVariable(environmentVariable); if (string.IsNullOrWhiteSpace(addresses)) { return false; @@ -4770,6 +5060,11 @@ internal static unsafe class VulkanVideoPresenter [',', ';', ' ', '\t'], StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)) { + if (token == "*") + { + return true; + } + var span = token.AsSpan(); if (span.StartsWith("0x", StringComparison.OrdinalIgnoreCase)) { @@ -5105,6 +5400,234 @@ internal static unsafe class VulkanVideoPresenter &toPresent); } + private void RecordGuestImageBlit( + uint imageIndex, + GuestImageResource source) + { + var traceDestination = + ShouldTracePresentedGuestImageContentsForDiagnostics() && + !_tracedPresentedSwapchain; + _tracedPresentedSwapchain |= traceDestination; + BeginDebugLabel( + _commandBuffer, + $"SharpEmu present image 0x{source.Address:X16}"); + + var sourceToTransfer = new ImageMemoryBarrier + { + SType = StructureType.ImageMemoryBarrier, + SrcAccessMask = AccessFlags.ShaderReadBit, + DstAccessMask = AccessFlags.TransferReadBit, + OldLayout = ImageLayout.ShaderReadOnlyOptimal, + NewLayout = ImageLayout.TransferSrcOptimal, + SrcQueueFamilyIndex = Vk.QueueFamilyIgnored, + DstQueueFamilyIndex = Vk.QueueFamilyIgnored, + Image = source.Image, + SubresourceRange = ColorSubresourceRange(), + }; + var destinationToTransfer = new ImageMemoryBarrier + { + SType = StructureType.ImageMemoryBarrier, + SrcAccessMask = _imageInitialized[imageIndex] + ? AccessFlags.MemoryReadBit + : 0, + DstAccessMask = AccessFlags.TransferWriteBit, + OldLayout = _imageInitialized[imageIndex] + ? ImageLayout.PresentSrcKhr + : ImageLayout.Undefined, + NewLayout = ImageLayout.TransferDstOptimal, + SrcQueueFamilyIndex = Vk.QueueFamilyIgnored, + DstQueueFamilyIndex = Vk.QueueFamilyIgnored, + Image = _swapchainImages[imageIndex], + SubresourceRange = ColorSubresourceRange(), + }; + var barriers = stackalloc ImageMemoryBarrier[2]; + barriers[0] = sourceToTransfer; + barriers[1] = destinationToTransfer; + _vk.CmdPipelineBarrier( + _commandBuffer, + PipelineStageFlags.AllCommandsBit, + PipelineStageFlags.TransferBit, + 0, + 0, + null, + 0, + null, + 2, + barriers); + + var sourceOffsets = new ImageBlit.SrcOffsetsBuffer + { + Element0 = new Offset3D(0, 0, 0), + Element1 = new Offset3D( + checked((int)source.Width), + checked((int)source.Height), + 1), + }; + var destinationOffsets = new ImageBlit.DstOffsetsBuffer + { + Element0 = new Offset3D(0, 0, 0), + Element1 = new Offset3D( + checked((int)_extent.Width), + checked((int)_extent.Height), + 1), + }; + var region = new ImageBlit + { + SrcSubresource = new ImageSubresourceLayers( + ImageAspectFlags.ColorBit, + 0, + 0, + 1), + SrcOffsets = sourceOffsets, + DstSubresource = new ImageSubresourceLayers( + ImageAspectFlags.ColorBit, + 0, + 0, + 1), + DstOffsets = destinationOffsets, + }; + _vk.CmdBlitImage( + _commandBuffer, + source.Image, + ImageLayout.TransferSrcOptimal, + _swapchainImages[imageIndex], + ImageLayout.TransferDstOptimal, + 1, + ®ion, + Filter.Nearest); + + if (traceDestination) + { + var destinationToReadback = new ImageMemoryBarrier + { + SType = StructureType.ImageMemoryBarrier, + SrcAccessMask = AccessFlags.TransferWriteBit, + DstAccessMask = AccessFlags.TransferReadBit, + OldLayout = ImageLayout.TransferDstOptimal, + NewLayout = ImageLayout.TransferSrcOptimal, + SrcQueueFamilyIndex = Vk.QueueFamilyIgnored, + DstQueueFamilyIndex = Vk.QueueFamilyIgnored, + Image = _swapchainImages[imageIndex], + SubresourceRange = ColorSubresourceRange(), + }; + _vk.CmdPipelineBarrier( + _commandBuffer, + PipelineStageFlags.TransferBit, + PipelineStageFlags.TransferBit, + 0, + 0, + null, + 0, + null, + 1, + &destinationToReadback); + + var copyRegion = new BufferImageCopy + { + ImageSubresource = new ImageSubresourceLayers + { + AspectMask = ImageAspectFlags.ColorBit, + LayerCount = 1, + }, + ImageExtent = new Extent3D(_extent.Width, _extent.Height, 1), + }; + _vk.CmdCopyImageToBuffer( + _commandBuffer, + _swapchainImages[imageIndex], + ImageLayout.TransferSrcOptimal, + _stagingBuffer, + 1, + ©Region); + _swapchainReadbackPending = true; + } + + var sourceToShaderRead = new ImageMemoryBarrier + { + SType = StructureType.ImageMemoryBarrier, + SrcAccessMask = AccessFlags.TransferReadBit, + DstAccessMask = AccessFlags.ShaderReadBit, + OldLayout = ImageLayout.TransferSrcOptimal, + NewLayout = ImageLayout.ShaderReadOnlyOptimal, + SrcQueueFamilyIndex = Vk.QueueFamilyIgnored, + DstQueueFamilyIndex = Vk.QueueFamilyIgnored, + Image = source.Image, + SubresourceRange = ColorSubresourceRange(), + }; + var destinationToPresent = new ImageMemoryBarrier + { + SType = StructureType.ImageMemoryBarrier, + SrcAccessMask = traceDestination + ? AccessFlags.TransferReadBit + : AccessFlags.TransferWriteBit, + DstAccessMask = AccessFlags.MemoryReadBit, + OldLayout = traceDestination + ? ImageLayout.TransferSrcOptimal + : ImageLayout.TransferDstOptimal, + NewLayout = ImageLayout.PresentSrcKhr, + SrcQueueFamilyIndex = Vk.QueueFamilyIgnored, + DstQueueFamilyIndex = Vk.QueueFamilyIgnored, + Image = _swapchainImages[imageIndex], + SubresourceRange = ColorSubresourceRange(), + }; + barriers[0] = sourceToShaderRead; + barriers[1] = destinationToPresent; + _vk.CmdPipelineBarrier( + _commandBuffer, + PipelineStageFlags.TransferBit, + PipelineStageFlags.AllCommandsBit, + 0, + 0, + null, + 0, + null, + 2, + barriers); + EndDebugLabel(_commandBuffer); + } + + private void TraceSwapchainReadback() + { + _swapchainReadbackPending = false; + var byteCount = checked((ulong)_extent.Width * _extent.Height * 4); + void* mapped; + Check( + _vk.MapMemory(_device, _stagingMemory, 0, byteCount, 0, &mapped), + "vkMapMemory(swapchain readback)"); + try + { + var bytes = new ReadOnlySpan(mapped, checked((int)byteCount)); + var nonzeroBytes = 0L; + var nonblackPixels = 0L; + ulong hash = 14695981039346656037UL; + for (var offset = 0; offset < bytes.Length; offset += 4) + { + var b0 = bytes[offset]; + var b1 = bytes[offset + 1]; + var b2 = bytes[offset + 2]; + var b3 = bytes[offset + 3]; + nonzeroBytes += b0 == 0 ? 0 : 1; + nonzeroBytes += b1 == 0 ? 0 : 1; + nonzeroBytes += b2 == 0 ? 0 : 1; + nonzeroBytes += b3 == 0 ? 0 : 1; + nonblackPixels += b0 != 0 || b1 != 0 || b2 != 0 ? 1 : 0; + hash = (hash ^ b0) * 1099511628211UL; + hash = (hash ^ b1) * 1099511628211UL; + hash = (hash ^ b2) * 1099511628211UL; + hash = (hash ^ b3) * 1099511628211UL; + } + + Console.Error.WriteLine( + $"[LOADER][TRACE] vk.swapchain_image size={_extent.Width}x{_extent.Height} " + + $"format={_swapchainFormat} nonzero_bytes={nonzeroBytes}/{byteCount} " + + $"nonblack_pixels={nonblackPixels}/{(ulong)_extent.Width * _extent.Height} " + + $"hash=0x{hash:X16}"); + } + finally + { + _vk.UnmapMemory(_device, _stagingMemory); + } + } + private Extent2D ChooseExtent(SurfaceCapabilitiesKHR capabilities) { if (capabilities.CurrentExtent.Width != uint.MaxValue)