diff --git a/src/SharpEmu.Libs/Agc/AgcExports.cs b/src/SharpEmu.Libs/Agc/AgcExports.cs index a7d163fe..a18aaa7e 100644 --- a/src/SharpEmu.Libs/Agc/AgcExports.cs +++ b/src/SharpEmu.Libs/Agc/AgcExports.cs @@ -6488,6 +6488,32 @@ public static partial class AgcExports TraceAstroTitlePixelGlobalProbe(pixelEvaluation); } + // Patch BufferFormat from the attrib table onto the V# before host + // vertex input. IR discovery often keeps a stale float format from the + // unpatched sharp — that turns UI glyphs into gradient triangles. + // Match by stride+offset (not bare base address) so interleaved streams + // keep loading-video bindings intact. + if (exportEvaluation.VertexInputs is { Count: > 0 } discoveredInputs && + AgcVertexMetadata.TryGetVertexTableRegisters( + ctx, + exportShaderAddress, + exportShaderHeader, + out var vertexTables)) + { + var merged = AgcVertexMetadata.MergeVertexInputsFromMetadata( + ctx, + exportEvaluation.ScalarRegisters, + vertexTables, + discoveredInputs); + if (!ReferenceEquals(merged, discoveredInputs)) + { + TraceAgcShader( + $"agc.vertex_metadata_format es=0x{exportShaderAddress:X16} " + + $"count={merged.Count}"); + exportEvaluation = exportEvaluation with { VertexInputs = merged }; + } + } + // Every bound color target the shader exports to. Deferred renderers // draw a multi-render-target G-buffer (up to eight slots) in one pass. // Fall back to slot 0 if we cannot match any export to a bound target. @@ -7269,6 +7295,7 @@ public static partial class AgcExports Mix(input.NumberFormat); Mix(input.Stride); Mix(input.OffsetBytes); + Mix(input.PerInstance ? 1u : 0u); } } @@ -8277,7 +8304,8 @@ public static partial class AgcExports binding.OffsetBytes, binding.Data, binding.DataLength, - binding.DataPooled); + binding.DataPooled, + binding.PerInstance); } return buffers; diff --git a/src/SharpEmu.Libs/Agc/AgcVertexMetadata.cs b/src/SharpEmu.Libs/Agc/AgcVertexMetadata.cs new file mode 100644 index 00000000..8537ef27 --- /dev/null +++ b/src/SharpEmu.Libs/Agc/AgcVertexMetadata.cs @@ -0,0 +1,788 @@ +// Copyright (C) 2026 SharpEmu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +using SharpEmu.HLE; +using SharpEmu.ShaderCompiler; + +namespace SharpEmu.Libs.Agc; + +/// +/// AGC embedded vertex metadata. Locates +/// PtrVertexBufferTable / PtrVertexAttribDescTable and builds authoritative +/// attribute layouts that draw translation merges onto IR-discovered fetches. +/// +internal static class AgcVertexMetadata +{ + private const ushort IllegalDirectOffset = 0xFFFF; + private const ulong ShaderUserDataOffset = 0x08; + private const ulong ShaderInputSemanticsOffset = 0x30; + private const ulong ShaderNumInputSemanticsOffset = 0x50; + + internal enum AgcDirectResourceType : uint + { + PtrVertexBufferTable = 8, + PtrVertexAttribDescTable = 10, + Last = PtrVertexAttribDescTable, + } + + internal readonly record struct VertexTableRegisters( + int VertexBufferReg, + int VertexAttribReg, + uint InputSemanticsCount, + ulong InputSemanticsAddress); + + /// + /// One AGC attrib-table resource. + /// Representation: is the V# base; attribute byte + /// offset is applied as (Vulkan bind offset), + /// not folded into the base — avoids double-counting when the IR prolog + /// already bumped the sharp address. + /// + internal readonly record struct MetadataVertexResource( + uint Location, + uint Semantic, + uint HardwareMapping, + uint SizeInElements, + ulong SharpBase, + uint Stride, + uint OffsetBytes, + uint DataFormat, + uint NumberFormat, + uint ComponentCount, + bool PerInstance); + + /// + /// Reads AGC user-data direct-resource offsets for the ES header mapped to + /// . Returns false when the header is + /// unknown or the tables are absent (attribute-less clears). + /// + internal static bool TryGetVertexTableRegisters( + CpuContext ctx, + ulong shaderCodeAddress, + ulong shaderHeaderAddress, + out VertexTableRegisters registers) + { + registers = new VertexTableRegisters(-1, -1, 0, 0); + if (shaderHeaderAddress == 0 || + !TryReadUInt64(ctx, shaderHeaderAddress + ShaderUserDataOffset, out var userDataAddress) || + userDataAddress == 0) + { + return false; + } + + // ShaderUserData layout: + // 0x00: uint16_t* direct_resource_offset + // 0x08: sharp_resource_offset[4] + // 0x28: eud_size_dw, srt_size_dw + // 0x2C: direct_resource_count + if (!TryReadUInt64(ctx, userDataAddress, out var directResourceOffset) || + !TryReadUInt16(ctx, userDataAddress + 0x2C, out var directResourceCount)) + { + return false; + } + + var maxTypes = (uint)AgcDirectResourceType.Last + 1u; + if (directResourceCount > maxTypes || directResourceOffset == 0) + { + return false; + } + + var vertexBufferReg = -1; + var vertexAttribReg = -1; + for (uint type = 0; type < directResourceCount; type++) + { + if (!TryReadUInt16( + ctx, + directResourceOffset + (type * sizeof(ushort)), + out var reg) || + reg == IllegalDirectOffset) + { + continue; + } + + switch ((AgcDirectResourceType)type) + { + case AgcDirectResourceType.PtrVertexBufferTable: + vertexBufferReg = reg; + break; + case AgcDirectResourceType.PtrVertexAttribDescTable: + vertexAttribReg = reg; + break; + } + } + + if (vertexBufferReg < 0 || vertexAttribReg < 0) + { + return false; + } + + if (!TryReadUInt64( + ctx, + shaderHeaderAddress + ShaderInputSemanticsOffset, + out var inputSemanticsAddress) || + !TryReadUInt32( + ctx, + shaderHeaderAddress + ShaderNumInputSemanticsOffset, + out var inputSemanticsCount) || + inputSemanticsCount == 0 || + inputSemanticsAddress == 0) + { + return false; + } + + registers = new VertexTableRegisters( + vertexBufferReg, + vertexAttribReg, + inputSemanticsCount, + inputSemanticsAddress); + return true; + } + + /// + /// Builds attrib resources from AGC input_semantics + tables. + /// ShaderSemantic packing: + /// bits [7:0] semantic → attrib table index + /// bits [15:8] hardware_mapping → VGPR destination + /// bits [19:16] size_in_elements + /// + internal static bool TryBuildVertexResourcesFromMetadata( + CpuContext ctx, + IReadOnlyList scalarRegisters, + VertexTableRegisters tables, + out IReadOnlyList resources) + { + resources = Array.Empty(); + if (tables.VertexAttribReg < 0 || + tables.VertexBufferReg < 0 || + tables.VertexAttribReg + 1 >= scalarRegisters.Count || + tables.VertexBufferReg + 1 >= scalarRegisters.Count || + tables.InputSemanticsCount == 0) + { + return false; + } + + var attribTable = + ((ulong)scalarRegisters[tables.VertexAttribReg + 1] << 32) | + scalarRegisters[tables.VertexAttribReg]; + var bufferTable = + ((ulong)scalarRegisters[tables.VertexBufferReg + 1] << 32) | + scalarRegisters[tables.VertexBufferReg]; + if (attribTable == 0 || bufferTable == 0) + { + return false; + } + + var built = new List((int)tables.InputSemanticsCount); + for (uint i = 0; i < tables.InputSemanticsCount; i++) + { + if (!TryReadUInt32( + ctx, + tables.InputSemanticsAddress + (i * sizeof(uint)), + out var semanticWord)) + { + return false; + } + + // Attrib index is semantic bits [7:0], not hardware_mapping. + var semantic = semanticWord & 0xFFu; + var hardwareMapping = (semanticWord >> 8) & 0xFFu; + var sizeInElements = (semanticWord >> 16) & 0xFu; + if (!TryReadUInt32(ctx, attribTable + (semantic * sizeof(uint)), out var attribWord)) + { + return false; + } + + // Attrib dword: buffer index [4:0], format [13:5], offset [25:14], fetch [26]. + var bufferIndex = attribWord & 0x1Fu; + var format = (attribWord >> 5) & 0x1FFu; + var offset = (attribWord >> 14) & 0xFFFu; + var fetchIndex = (attribWord >> 26) & 0x1u; + var sharpAddress = bufferTable + (bufferIndex * 16u); + if (!TryReadUInt32(ctx, sharpAddress, out var sharp0) || + !TryReadUInt32(ctx, sharpAddress + 4, out var sharp1)) + { + return false; + } + + var sharpBase = sharp0 | ((ulong)(sharp1 & 0xFFFFu) << 32); + var stride = (sharp1 >> 16) & 0x3FFFu; + if (sharpBase == 0 || stride == 0) + { + continue; + } + + var fallbackComponents = sizeInElements != 0 ? sizeInElements : 4u; + var (dataFormat, numberFormat, components) = + MapAttribFormat(format, fallbackComponents); + built.Add(new MetadataVertexResource( + Location: i, + Semantic: semantic, + HardwareMapping: hardwareMapping, + SizeInElements: sizeInElements, + SharpBase: sharpBase, + Stride: stride, + OffsetBytes: offset, + DataFormat: dataFormat, + NumberFormat: numberFormat, + ComponentCount: components, + PerInstance: fetchIndex != 0)); + } + + if (built.Count == 0) + { + return false; + } + + resources = built; + return true; + } + + /// + /// Patch IR-discovered fetches from the attrib table onto the V# format/offset. + /// Prefer 1:1 Location pairing when counts match on one interleaved stream + /// (GTA UI glyphs). Otherwise match by stride + byte offset. Never rebases + /// BaseAddress/Data/Location/Pc/PerInstance. + /// + internal static IReadOnlyList MergeVertexInputsFromMetadata( + CpuContext ctx, + IReadOnlyList scalarRegisters, + VertexTableRegisters tables, + IReadOnlyList discovered) + { + if (discovered.Count == 0 || + !TryBuildVertexResourcesFromMetadata( + ctx, + scalarRegisters, + tables, + out var resources)) + { + return discovered; + } + + if (TryMergeByLocationPairing(discovered, resources, out var paired)) + { + return paired; + } + + var merged = new List(discovered.Count); + var usedResources = new bool[resources.Count]; + var changed = false; + foreach (var input in discovered) + { + if (!TryMatchMetadataResource(input, resources, usedResources, out var resource, out var fillOffset)) + { + merged.Add(input); + continue; + } + + var refined = ApplyMetadataFormat(input, resource, fillOffset); + changed |= refined != input; + merged.Add(refined); + } + + return changed ? merged : discovered; + } + + /// + /// When discovery and metadata describe the same interleaved stream with + /// equal attribute counts, pair by sorted Location (semantic order). + /// Keeps each binding's Pc/Location for SPIR-V; overlays format + offset. + /// + private static bool TryMergeByLocationPairing( + IReadOnlyList discovered, + IReadOnlyList resources, + out IReadOnlyList merged) + { + merged = discovered; + if (discovered.Count != resources.Count || discovered.Count == 0) + { + return false; + } + + var orderedInputs = discovered.OrderBy(static input => input.Location).ToArray(); + var orderedResources = resources.OrderBy(static resource => resource.Location).ToArray(); + var streamBase = orderedResources[0].SharpBase; + var streamStride = orderedResources[0].Stride; + for (var index = 0; index < orderedResources.Length; index++) + { + var resource = orderedResources[index]; + var input = orderedInputs[index]; + if (resource.SharpBase != streamBase || + resource.Stride != streamStride || + (input.Stride != 0 && input.Stride != streamStride) || + !IsSameVertexStream(input, resource)) + { + return false; + } + } + + var byPc = new Dictionary(discovered.Count); + var changed = false; + for (var index = 0; index < orderedInputs.Length; index++) + { + var input = orderedInputs[index]; + var resource = orderedResources[index]; + var fillOffset = input.BaseAddress == resource.SharpBase || + IsAddressInsideCapturedSpan(input, resource.SharpBase); + var refined = ApplyMetadataFormat(input, resource, fillOffset); + changed |= refined != input; + byPc[input.Pc] = refined; + } + + if (!changed) + { + return false; + } + + var result = new Gen5VertexInputBinding[discovered.Count]; + for (var index = 0; index < discovered.Count; index++) + { + result[index] = byPc[discovered[index].Pc]; + } + + merged = result; + return true; + } + + private static Gen5VertexInputBinding ApplyMetadataFormat( + Gen5VertexInputBinding input, + MetadataVertexResource resource, + bool fillOffsetBytes) + { + var components = input.ComponentCount != 0 && + input.ComponentCount < resource.ComponentCount + ? input.ComponentCount + : resource.ComponentCount; + + return input with + { + DataFormat = resource.DataFormat, + NumberFormat = resource.NumberFormat, + ComponentCount = components, + OffsetBytes = fillOffsetBytes ? resource.OffsetBytes : input.OffsetBytes, + }; + } + + /// + /// Legacy entry point — forwards to . + /// + internal static IReadOnlyList RefineVertexInputs( + CpuContext ctx, + IReadOnlyList scalarRegisters, + VertexTableRegisters tables, + IReadOnlyList discovered) => + MergeVertexInputsFromMetadata(ctx, scalarRegisters, tables, discovered); + + /// + /// Collects SBufferLoad / SLoad PCs that read the AGC attrib or buffer + /// tables (embedded-fetch prolog). Those loads are executed on the + /// CPU during scalar evaluation; once vertex inputs are bound they must + /// not run again as live SSBOs on the GPU. + /// + internal static HashSet CollectFetchPrologPcs( + Gen5ShaderProgram program, + VertexTableRegisters tables) + { + var pcs = new HashSet(); + if (tables.VertexAttribReg < 0 || tables.VertexBufferReg < 0) + { + return pcs; + } + + var tableRegs = new HashSet + { + (uint)tables.VertexAttribReg, + (uint)tables.VertexAttribReg + 1u, + (uint)tables.VertexBufferReg, + (uint)tables.VertexBufferReg + 1u, + }; + + foreach (var instruction in program.Instructions) + { + var isScalarLoad = + instruction.Opcode.StartsWith("SBufferLoad", StringComparison.Ordinal) || + instruction.Opcode.StartsWith("SLoad", StringComparison.Ordinal); + if (!isScalarLoad) + { + continue; + } + + // SMEM loads encode the scalar base pointer in Sources[0]. + if (instruction.Sources.Count > 0 && + instruction.Sources[0] is + { + Kind: Gen5OperandKind.ScalarRegister, + Value: var scalarBase, + } && + tableRegs.Contains(scalarBase)) + { + pcs.Add(instruction.Pc); + continue; + } + + if (instruction.Control is Gen5BufferMemoryControl buffer && + tableRegs.Contains(buffer.ScalarResource)) + { + pcs.Add(instruction.Pc); + } + } + + return pcs; + } + + private static bool TryMatchMetadataResource( + Gen5VertexInputBinding input, + IReadOnlyList resources, + bool[] usedResources, + out MetadataVertexResource resource, + out bool fillOffsetBytes) + { + resource = default; + fillOffsetBytes = false; + var bestScore = int.MinValue; + var bestIndex = -1; + var bestFillOffset = false; + for (var index = 0; index < resources.Count; index++) + { + if (usedResources[index]) + { + continue; + } + + var candidate = resources[index]; + if (candidate.Stride != 0 && + input.Stride != 0 && + candidate.Stride != input.Stride) + { + continue; + } + + if (!IsSameVertexStream(input, candidate)) + { + continue; + } + + var attrAddress = candidate.SharpBase + candidate.OffsetBytes; + var score = int.MinValue; + var fillOffset = false; + + // Post-capture interleaved: shared BaseAddress, distinct OffsetBytes. + if (input.OffsetBytes == candidate.OffsetBytes && + (input.BaseAddress == candidate.SharpBase || + IsAddressInsideCapturedSpan(input, candidate.SharpBase))) + { + score = 400; + } + // IR prolog baked attrib offset into the V# base. + else if (input.BaseAddress == attrAddress) + { + score = 350; + } + // Discovery never saw the attrib offset — only safe when this + // resource's offset uniquely identifies it among unused entries. + else if (input.BaseAddress == candidate.SharpBase && + input.OffsetBytes == 0 && + candidate.OffsetBytes != 0 && + IsUniqueUnusedOffset(resources, usedResources, candidate.OffsetBytes, index)) + { + score = 300; + fillOffset = true; + } + else if (input.BaseAddress == candidate.SharpBase && + input.OffsetBytes == 0 && + candidate.OffsetBytes == 0) + { + score = 250; + } + + if (score > bestScore) + { + bestScore = score; + bestIndex = index; + bestFillOffset = fillOffset; + } + } + + // Require an offset-aware match. Bare SharpBase ties (score 250) are + // only accepted when a single unused resource remains for that stream. + if (bestIndex < 0 || bestScore < 300) + { + if (bestIndex < 0 || bestScore < 250) + { + return false; + } + + var unusedSameStream = 0; + for (var index = 0; index < resources.Count; index++) + { + if (!usedResources[index] && IsSameVertexStream(input, resources[index])) + { + unusedSameStream++; + } + } + + if (unusedSameStream != 1) + { + return false; + } + } + + usedResources[bestIndex] = true; + resource = resources[bestIndex]; + fillOffsetBytes = bestFillOffset; + return true; + } + + private static bool IsSameVertexStream( + Gen5VertexInputBinding input, + MetadataVertexResource resource) + { + if (input.BaseAddress == resource.SharpBase || + input.BaseAddress == resource.SharpBase + resource.OffsetBytes) + { + return true; + } + + return IsAddressInsideCapturedSpan(input, resource.SharpBase); + } + + private static bool IsAddressInsideCapturedSpan( + Gen5VertexInputBinding input, + ulong address) => + input.DataLength > 0 && + address >= input.BaseAddress && + address < input.BaseAddress + (ulong)input.DataLength; + + private static bool IsUniqueUnusedOffset( + IReadOnlyList resources, + bool[] usedResources, + uint offsetBytes, + int candidateIndex) + { + for (var index = 0; index < resources.Count; index++) + { + if (index == candidateIndex || usedResources[index]) + { + continue; + } + + if (resources[index].OffsetBytes == offsetBytes) + { + return false; + } + } + + return true; + } + + /// + /// Attrib-table format + /// fields are VertexAttribFormat; V# / Vulkan paths need BufferFormat. + /// Unknown values pass through (already BufferFormat). + /// + private static uint VertexAttribFormatToBufferFormat(uint format) => + format switch + { + 0 => 0, // Invalid + 4 => 1, // k8UNorm + 8 => 2, // k8SNorm + 12 => 3, // k8UScaled + 16 => 4, // k8SScaled + 20 => 5, // k8UInt + 24 => 6, // k8SInt + 28 => 7, // k16UNorm + 32 => 8, // k16SNorm + 36 => 9, // k16UScaled + 40 => 10, // k16SScaled + 44 => 11, // k16UInt + 48 => 12, // k16SInt + 52 => 13, // k16Float + 57 => 14, // k8_8UNorm + 61 => 15, // k8_8SNorm + 65 => 16, // k8_8UScaled + 69 => 17, // k8_8SScaled + 73 => 18, // k8_8UInt + 77 => 19, // k8_8SInt + 80 => 20, // k32UInt + 84 => 21, // k32SInt + 88 => 22, // k32Float + 93 => 23, // k16_16UNorm + 97 => 24, // k16_16SNorm + 101 => 25, // k16_16UScaled + 105 => 26, // k16_16SScaled + 109 => 27, // k16_16UInt + 113 => 28, // k16_16SInt + 117 => 29, // k16_16Float + 122 => 30, // k11_11_10UNorm + 126 => 31, + 130 => 32, + 134 => 33, + 138 => 34, + 142 => 35, + 146 => 36, + 150 => 37, // k10_11_11UNorm + 154 => 38, + 158 => 39, + 162 => 40, + 166 => 41, + 170 => 42, + 174 => 43, + 179 => 44, // k2_10_10_10UNorm + 183 => 45, + 187 => 46, + 191 => 47, + 195 => 48, + 199 => 49, + 203 => 50, // k10_10_10_2UNorm + 207 => 51, + 211 => 52, + 215 => 53, + 219 => 54, + 223 => 55, + 227 => 56, // k8_8_8_8UNorm + 231 => 57, + 235 => 58, + 239 => 59, + 243 => 60, + 247 => 61, + 249 => 62, // k32_32UInt + 253 => 63, + 257 => 64, // k32_32Float + 263 => 65, // k16_16_16_16UNorm + 267 => 66, + 271 => 67, + 275 => 68, + 279 => 69, + 283 => 70, + 287 => 71, // k16_16_16_16Float + 290 => 72, // k32_32_32UInt + 294 => 73, + 298 => 74, + 303 => 75, // k32_32_32_32UInt + 307 => 76, + 311 => 77, // k32_32_32_32Float + _ => format, + }; + + /// + /// Maps Prospero attrib-table formats onto GNM (DataFormat, NumberFormat, + /// Components) for ToVkVertexFormat. Accepts VertexAttribFormat + /// or BufferFormat (pass-through). NumberFormat: 0 Unorm, 1 SNorm, + /// 2 UScaled, 3 SScaled, 4 UInt, 5 SInt, 7 Float. + /// + private static (uint DataFormat, uint NumberFormat, uint Components) MapAttribFormat( + uint attribFormat, + uint fallbackComponents) + { + // Prospero VertexAttribFormat quirks before BufferFormat conversion. + if (attribFormat == 113) + { + return (14, 7, 4); // R32G32B32A32_SFLOAT + } + + if (attribFormat == 121) + { + return (5, 7, 2); // R16G16_SFLOAT + } + + var bufferFormat = VertexAttribFormatToBufferFormat(attribFormat); + + // Prospero::BufferFormat numeric values (gpu_defs.h). + return bufferFormat switch + { + 1 => (1, 0, 1), // k8UNorm + 2 => (1, 1, 1), // k8SNorm + 3 => (1, 2, 1), // k8UScaled + 4 => (1, 3, 1), // k8SScaled + 5 => (1, 4, 1), // k8UInt + 6 => (1, 5, 1), // k8SInt + 7 => (2, 0, 1), // k16UNorm + 8 => (2, 1, 1), // k16SNorm + 9 => (2, 2, 1), // k16UScaled + 10 => (2, 3, 1), // k16SScaled + 11 => (2, 4, 1), // k16UInt + 12 => (2, 5, 1), // k16SInt + 13 => (2, 7, 1), // k16Float + 14 => (3, 0, 2), // k8_8UNorm + 15 => (3, 1, 2), // k8_8SNorm + 16 => (3, 2, 2), // k8_8UScaled + 17 => (3, 3, 2), // k8_8SScaled + 18 => (3, 4, 2), // k8_8UInt + 19 => (3, 5, 2), // k8_8SInt + 20 => (4, 4, 1), // k32UInt + 21 => (4, 5, 1), // k32SInt + 22 => (4, 7, 1), // k32Float + 23 => (5, 0, 2), // k16_16UNorm + 24 => (5, 1, 2), // k16_16SNorm + 25 => (5, 2, 2), // k16_16UScaled + 26 => (5, 3, 2), // k16_16SScaled + 27 => (5, 4, 2), // k16_16UInt + 28 => (5, 5, 2), // k16_16SInt + 29 => (5, 7, 2), // k16_16Float + 50 => (9, 0, 4), // k10_10_10_2UNorm + 51 => (9, 1, 4), // k10_10_10_2SNorm + 56 => (10, 0, 4), // k8_8_8_8UNorm + 57 => (10, 1, 4), // k8_8_8_8SNorm + 58 => (10, 2, 4), // k8_8_8_8UScaled + 59 => (10, 3, 4), // k8_8_8_8SScaled + 60 => (10, 4, 4), // k8_8_8_8UInt + 61 => (10, 5, 4), // k8_8_8_8SInt + 62 => (11, 4, 2), // k32_32UInt + 63 => (11, 5, 2), // k32_32SInt + 64 => (11, 7, 2), // k32_32Float + 65 => (12, 0, 4), // k16_16_16_16UNorm + 66 => (12, 1, 4), // k16_16_16_16SNorm + 67 => (12, 2, 4), // k16_16_16_16UScaled + 68 => (12, 3, 4), // k16_16_16_16SScaled + 69 => (12, 4, 4), // k16_16_16_16UInt + 70 => (12, 5, 4), // k16_16_16_16SInt + 71 => (12, 7, 4), // k16_16_16_16Float + 72 => (13, 4, 3), // k32_32_32UInt + 73 => (13, 5, 3), // k32_32_32SInt + 74 => (13, 7, 3), // k32_32_32Float + 75 => (14, 4, 4), // k32_32_32_32UInt + 76 => (14, 5, 4), // k32_32_32_32SInt + 77 => (14, 7, 4), // k32_32_32_32Float + _ => (14, 7, Math.Clamp(fallbackComponents, 1u, 4u)), + }; + } + + private static bool TryReadUInt16(CpuContext ctx, ulong address, out ushort value) + { + Span buffer = stackalloc byte[2]; + if (!ctx.Memory.TryRead(address, buffer)) + { + value = 0; + return false; + } + + value = System.Buffers.Binary.BinaryPrimitives.ReadUInt16LittleEndian(buffer); + return true; + } + + private static bool TryReadUInt32(CpuContext ctx, ulong address, out uint value) + { + Span buffer = stackalloc byte[4]; + if (!ctx.Memory.TryRead(address, buffer)) + { + value = 0; + return false; + } + + value = System.Buffers.Binary.BinaryPrimitives.ReadUInt32LittleEndian(buffer); + return true; + } + + private static bool TryReadUInt64(CpuContext ctx, ulong address, out ulong value) + { + Span buffer = stackalloc byte[8]; + if (!ctx.Memory.TryRead(address, buffer)) + { + value = 0; + return false; + } + + value = System.Buffers.Binary.BinaryPrimitives.ReadUInt64LittleEndian(buffer); + return true; + } +} diff --git a/src/SharpEmu.Libs/Gpu/GuestGpuTypes.cs b/src/SharpEmu.Libs/Gpu/GuestGpuTypes.cs index 91a2ea3f..ab18d9a0 100644 --- a/src/SharpEmu.Libs/Gpu/GuestGpuTypes.cs +++ b/src/SharpEmu.Libs/Gpu/GuestGpuTypes.cs @@ -76,7 +76,8 @@ internal sealed record GuestVertexBuffer( uint OffsetBytes, byte[] Data, int Length, - bool Pooled); + bool Pooled, + bool PerInstance = false); internal sealed record GuestIndexBuffer( byte[] Data, diff --git a/src/SharpEmu.Libs/Gpu/Metal/MetalVideoPresenter.Draws.cs b/src/SharpEmu.Libs/Gpu/Metal/MetalVideoPresenter.Draws.cs index 86740ef0..ffcef9d8 100644 --- a/src/SharpEmu.Libs/Gpu/Metal/MetalVideoPresenter.Draws.cs +++ b/src/SharpEmu.Libs/Gpu/Metal/MetalVideoPresenter.Draws.cs @@ -1225,8 +1225,11 @@ internal static partial class MetalVideoPresenter ? vertexBuffer.Stride : Math.Max(vertexBuffer.ComponentCount, 1) * 4; MetalNative.Send(layout, MetalNative.Selector("setStride:"), (nint)stride); - // MTLVertexStepFunction.PerVertex = 1. - MetalNative.Send(layout, MetalNative.Selector("setStepFunction:"), 1); + // MTLVertexStepFunction: PerVertex = 1, PerInstance = 2. + MetalNative.Send( + layout, + MetalNative.Selector("setStepFunction:"), + vertexBuffer.PerInstance ? 2 : 1); } return descriptor; diff --git a/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.cs b/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.cs index e2ecd73e..f1f50bc4 100644 --- a/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.cs +++ b/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.cs @@ -3170,6 +3170,7 @@ internal static unsafe class VulkanVideoPresenter public uint NumberFormat; public uint Stride; public uint OffsetBytes; + public bool PerInstance; } private const Format DepthFormat = Format.D32Sfloat; @@ -6658,33 +6659,47 @@ internal static unsafe class VulkanVideoPresenter PName = entryPoint, }; - var vertexBindingDescriptions = - new VertexInputBindingDescription[resources.VertexBuffers.Length]; + // One Vulkan binding per unique host buffer and input rate + // (fetch_index). Attributes share that binding with + // Offset = OffsetBytes. + var bindingByBuffer = new Dictionary<(ulong Handle, bool PerInstance), uint>(); + var vertexBindingList = new List(); var vertexAttributeDescriptions = new VertexInputAttributeDescription[resources.VertexBuffers.Length]; for (var index = 0; index < resources.VertexBuffers.Length; index++) { var vertexBuffer = resources.VertexBuffers[index]; - vertexBindingDescriptions[index] = new VertexInputBindingDescription + var bufferKey = (vertexBuffer.Buffer.Handle, vertexBuffer.PerInstance); + if (!bindingByBuffer.TryGetValue(bufferKey, out var bindingIndex)) { - Binding = (uint)index, - Stride = vertexBuffer.Stride == 0 - ? Math.Max(vertexBuffer.ComponentCount, 1) * sizeof(float) - : vertexBuffer.Stride, - InputRate = VertexInputRate.Vertex, - }; + bindingIndex = (uint)vertexBindingList.Count; + bindingByBuffer[bufferKey] = bindingIndex; + vertexBindingList.Add(new VertexInputBindingDescription + { + Binding = bindingIndex, + Stride = vertexBuffer.Stride == 0 + ? Math.Max(vertexBuffer.ComponentCount, 1) * sizeof(float) + : vertexBuffer.Stride, + InputRate = vertexBuffer.PerInstance + ? VertexInputRate.Instance + : VertexInputRate.Vertex, + }); + } + vertexAttributeDescriptions[index] = new VertexInputAttributeDescription { Location = vertexBuffer.Location, - Binding = (uint)index, + Binding = bindingIndex, Format = ToVkVertexFormat( vertexBuffer.DataFormat, vertexBuffer.NumberFormat, vertexBuffer.ComponentCount), - Offset = 0, + Offset = vertexBuffer.OffsetBytes, }; } + var vertexBindingDescriptions = vertexBindingList.ToArray(); + fixed (VertexInputBindingDescription* vertexBindingPointerBase = vertexBindingDescriptions) fixed (VertexInputAttributeDescription* vertexAttributePointerBase = vertexAttributeDescriptions) { @@ -9280,6 +9295,7 @@ internal static unsafe class VulkanVideoPresenter NumberFormat = guestBuffer.NumberFormat, Stride = guestBuffer.Stride, OffsetBytes = guestBuffer.OffsetBytes, + PerInstance = guestBuffer.PerInstance, }; } @@ -9297,6 +9313,7 @@ internal static unsafe class VulkanVideoPresenter NumberFormat = guestBuffer.NumberFormat, Stride = guestBuffer.Stride, OffsetBytes = guestBuffer.OffsetBytes, + PerInstance = guestBuffer.PerInstance, }; private VkBuffer CreateHostBuffer( @@ -9398,21 +9415,28 @@ internal static unsafe class VulkanVideoPresenter private static Format ToVkVertexFormat( uint dataFormat, uint numberFormat, - uint componentCount) => - (dataFormat, numberFormat) switch + uint componentCount) + { + var format = (dataFormat, numberFormat) switch { (1, 0) => Format.R8Unorm, (1, 1) => Format.R8SNorm, + (1, 2) => Format.R8Uscaled, + (1, 3) => Format.R8Sscaled, (1, 4) => Format.R8Uint, (1, 5) => Format.R8Sint, (1, 9) => Format.R8Srgb, (2, 0) => Format.R16Unorm, (2, 1) => Format.R16SNorm, + (2, 2) => Format.R16Uscaled, + (2, 3) => Format.R16Sscaled, (2, 4) => Format.R16Uint, (2, 5) => Format.R16Sint, (2, 7) => Format.R16Sfloat, (3, 0) => Format.R8G8Unorm, (3, 1) => Format.R8G8SNorm, + (3, 2) => Format.R8G8Uscaled, + (3, 3) => Format.R8G8Sscaled, (3, 4) => Format.R8G8Uint, (3, 5) => Format.R8G8Sint, (3, 9) => Format.R8G8Srgb, @@ -9467,6 +9491,9 @@ internal static unsafe class VulkanVideoPresenter (14, 4) => Format.R32G32B32A32Uint, (14, 5) => Format.R32G32B32A32Sint, (14, 7) => Format.R32G32B32A32Sfloat, + // Prospero VertexAttribFormat quirks also seen as buffer formats. + (113, _) => Format.R32G32B32A32Sfloat, + (121, _) => Format.R16G16Sfloat, (16, 0) => Format.B5G6R5UnormPack16, (17, 0) => Format.R5G5B5A1UnormPack16, (19, 0) => Format.R4G4B4A4UnormPack16, @@ -9474,6 +9501,38 @@ internal static unsafe class VulkanVideoPresenter _ => ToVkFloatVertexFormat(componentCount), }; + return NarrowVkVertexFormat(format, componentCount); + } + + /// + /// Narrow a sharp's full VkFormat to the component count the VS fetch + /// actually consumes. + /// + private static Format NarrowVkVertexFormat(Format format, uint usedComponents) + { + if (usedComponents == 0) + { + return format; + } + + return (format, usedComponents) switch + { + (Format.R32G32B32A32Sfloat, 1) => Format.R32Sfloat, + (Format.R32G32B32A32Sfloat, 2) => Format.R32G32Sfloat, + (Format.R32G32B32A32Sfloat, 3) => Format.R32G32B32Sfloat, + (Format.R32G32B32Sfloat, 1) => Format.R32Sfloat, + (Format.R32G32B32Sfloat, 2) => Format.R32G32Sfloat, + (Format.R16G16B16A16Sfloat, 1) => Format.R16Sfloat, + (Format.R16G16B16A16Sfloat, 2) => Format.R16G16Sfloat, + (Format.R8G8B8A8Unorm, 1) => Format.R8Unorm, + (Format.R8G8B8A8Unorm, 2) => Format.R8G8Unorm, + (Format.R8G8B8A8SNorm, 2) => Format.R8G8SNorm, + (Format.R8G8B8A8Uint, 1) => Format.R8Uint, + (Format.R8G8B8A8Uint, 2) => Format.R8G8Uint, + _ => format, + }; + } + private static Format ToVkFloatVertexFormat(uint componentCount) => componentCount switch { diff --git a/src/SharpEmu.ShaderCompiler/Gen5ShaderIr.cs b/src/SharpEmu.ShaderCompiler/Gen5ShaderIr.cs index d00a6e89..19c01505 100644 --- a/src/SharpEmu.ShaderCompiler/Gen5ShaderIr.cs +++ b/src/SharpEmu.ShaderCompiler/Gen5ShaderIr.cs @@ -312,7 +312,8 @@ public sealed record Gen5VertexInputBinding( uint OffsetBytes, byte[] Data, int DataLength, - bool DataPooled); + bool DataPooled, + bool PerInstance = false); public sealed record Gen5ShaderEvaluation( IReadOnlyList InitialScalarRegisters, diff --git a/src/SharpEmu.ShaderCompiler/Gen5ShaderScalarEvaluator.cs b/src/SharpEmu.ShaderCompiler/Gen5ShaderScalarEvaluator.cs index 369ecf1b..6c5e9a37 100644 --- a/src/SharpEmu.ShaderCompiler/Gen5ShaderScalarEvaluator.cs +++ b/src/SharpEmu.ShaderCompiler/Gen5ShaderScalarEvaluator.cs @@ -883,8 +883,11 @@ public static class Gen5ShaderScalarEvaluator Gen5ShaderInstruction instruction, Gen5BufferMemoryControl control, BufferDescriptor descriptor) => + // AGC embedded fetch is BufferLoadFormat/TBufferLoadFormat with idxen. + // offen is allowed: the constant/scalar offset folds into OffsetBytes + // (UI glyph shaders use this shape). Rejecting offen left those loads + // as live SSBOs and dropped vertex attributes. control.IndexEnabled && - !control.OffsetEnabled && control.DwordCount is >= 1 and <= 4 && descriptor.BaseAddress != 0 && descriptor.Stride != 0 && diff --git a/tests/SharpEmu.Libs.Tests/Agc/AgcVertexMetadataTests.cs b/tests/SharpEmu.Libs.Tests/Agc/AgcVertexMetadataTests.cs new file mode 100644 index 00000000..32d1703e --- /dev/null +++ b/tests/SharpEmu.Libs.Tests/Agc/AgcVertexMetadataTests.cs @@ -0,0 +1,293 @@ +// Copyright (C) 2026 SharpEmu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +using System.Buffers.Binary; +using SharpEmu.HLE; +using SharpEmu.Libs.Agc; +using SharpEmu.ShaderCompiler; +using Xunit; + +namespace SharpEmu.Libs.Tests.Agc; + +/// +/// Coverage for AGC attrib-table → BufferFormat merge and semantic indexing. +/// +public sealed class AgcVertexMetadataTests +{ + [Fact] + public void BuildVertexResources_UsesSemanticNotHardwareMappingAsAttribIndex() + { + // input_semantics[0]: semantic=1, hardware_mapping=4, size=2 + // If hardware_mapping were wrongly used as the attrib index, we'd read + // attrib[4] instead of attrib[1] and get the wrong format/offset. + const ulong memoryBase = 0x1_0000_0000; + var memory = new FakeCpuMemory(memoryBase, 0x2000); + var ctx = new CpuContext(memory, Generation.Gen5); + + const ulong semanticsAddress = memoryBase + 0x100; + const ulong attribTable = memoryBase + 0x200; + const ulong bufferTable = memoryBase + 0x300; + const ulong sharpBase = memoryBase + 0x800; + + // ShaderSemantic word: semantic=1, hw_mapping=4, size_in_elements=2 + WriteUInt32(memory, semanticsAddress, 1u | (4u << 8) | (2u << 16)); + + // attrib[0] unused garbage + WriteUInt32(memory, attribTable, 0xDEAD_BEEFu); + // attrib[1]: buffer=0, format=k16_16Float(29), offset=8, fetch=0 + WriteUInt32(memory, attribTable + 4, 0u | (29u << 5) | (8u << 14)); + + // V# at buffer table[0]: base=sharpBase, stride=16 + WriteUInt32(memory, bufferTable, (uint)(sharpBase & 0xFFFF_FFFFUL)); + WriteUInt32( + memory, + bufferTable + 4, + (uint)(sharpBase >> 32) | (16u << 16)); + + var scalars = new uint[32]; + scalars[8] = (uint)(attribTable & 0xFFFF_FFFFUL); + scalars[9] = (uint)(attribTable >> 32); + scalars[10] = (uint)(bufferTable & 0xFFFF_FFFFUL); + scalars[11] = (uint)(bufferTable >> 32); + + var tables = new AgcVertexMetadata.VertexTableRegisters( + VertexBufferReg: 10, + VertexAttribReg: 8, + InputSemanticsCount: 1, + InputSemanticsAddress: semanticsAddress); + + Assert.True( + AgcVertexMetadata.TryBuildVertexResourcesFromMetadata( + ctx, + scalars, + tables, + out var resources)); + Assert.Single(resources); + Assert.Equal(1u, resources[0].Semantic); + Assert.Equal(4u, resources[0].HardwareMapping); + Assert.Equal(8u, resources[0].OffsetBytes); + Assert.Equal(5u, resources[0].DataFormat); // R16G16 + Assert.Equal(7u, resources[0].NumberFormat); // Float + Assert.Equal(2u, resources[0].ComponentCount); + Assert.Equal(sharpBase, resources[0].SharpBase); + Assert.False(resources[0].PerInstance); + } + + [Fact] + public void MergeVertexInputs_OverlaysFormatWithoutRebasingCapture() + { + const ulong memoryBase = 0x1_0000_0000; + var memory = new FakeCpuMemory(memoryBase, 0x2000); + var ctx = new CpuContext(memory, Generation.Gen5); + + const ulong semanticsAddress = memoryBase + 0x100; + const ulong attribTable = memoryBase + 0x200; + const ulong bufferTable = memoryBase + 0x300; + const ulong sharpBase = memoryBase + 0x800; + + WriteUInt32(memory, semanticsAddress, 0u | (0u << 8) | (4u << 16)); + // format k8_8_8_8UNorm(56), offset=12 + WriteUInt32(memory, attribTable, 0u | (56u << 5) | (12u << 14)); + WriteUInt32(memory, bufferTable, (uint)(sharpBase & 0xFFFF_FFFFUL)); + WriteUInt32(memory, bufferTable + 4, (uint)(sharpBase >> 32) | (16u << 16)); + + var scalars = new uint[32]; + scalars[4] = (uint)(attribTable & 0xFFFF_FFFFUL); + scalars[5] = (uint)(attribTable >> 32); + scalars[6] = (uint)(bufferTable & 0xFFFF_FFFFUL); + scalars[7] = (uint)(bufferTable >> 32); + + var tables = new AgcVertexMetadata.VertexTableRegisters( + VertexBufferReg: 6, + VertexAttribReg: 4, + InputSemanticsCount: 1, + InputSemanticsAddress: semanticsAddress); + + var data = new byte[64]; + var discovered = new[] + { + new Gen5VertexInputBinding( + Pc: 0x40, + Location: 0, + ComponentCount: 4, + DataFormat: 14, // wrong IR guess + NumberFormat: 7, + BaseAddress: sharpBase, + Stride: 16, + OffsetBytes: 0, + Data: data, + DataLength: data.Length, + DataPooled: false), + }; + + var merged = AgcVertexMetadata.MergeVertexInputsFromMetadata( + ctx, + scalars, + tables, + discovered); + Assert.Single(merged); + Assert.Equal(0u, merged[0].Location); + Assert.Equal(sharpBase, merged[0].BaseAddress); + Assert.Same(data, merged[0].Data); + Assert.Equal(10u, merged[0].DataFormat); // RGBA8 + Assert.Equal(0u, merged[0].NumberFormat); // Unorm + Assert.Equal(12u, merged[0].OffsetBytes); + Assert.Equal(0x40u, merged[0].Pc); + } + + [Fact] + public void MergeVertexInputs_AcceptsVertexAttribFormatEnums() + { + // Attrib tables store VertexAttribFormat (227 = rgba8 unorm), not + // BufferFormat (56). Without conversion the format patch is a no-op. + const ulong memoryBase = 0x1_0000_0000; + var memory = new FakeCpuMemory(memoryBase, 0x2000); + var ctx = new CpuContext(memory, Generation.Gen5); + + const ulong semanticsAddress = memoryBase + 0x100; + const ulong attribTable = memoryBase + 0x200; + const ulong bufferTable = memoryBase + 0x300; + const ulong sharpBase = memoryBase + 0x800; + + WriteUInt32(memory, semanticsAddress, 0u | (0u << 8) | (4u << 16)); + WriteUInt32(memory, attribTable, 0u | (227u << 5) | (12u << 14)); // VertexAttribFormat + WriteUInt32(memory, bufferTable, (uint)(sharpBase & 0xFFFF_FFFFUL)); + WriteUInt32(memory, bufferTable + 4, (uint)(sharpBase >> 32) | (16u << 16)); + + var scalars = new uint[32]; + scalars[4] = (uint)(attribTable & 0xFFFF_FFFFUL); + scalars[5] = (uint)(attribTable >> 32); + scalars[6] = (uint)(bufferTable & 0xFFFF_FFFFUL); + scalars[7] = (uint)(bufferTable >> 32); + + var tables = new AgcVertexMetadata.VertexTableRegisters( + VertexBufferReg: 6, + VertexAttribReg: 4, + InputSemanticsCount: 1, + InputSemanticsAddress: semanticsAddress); + + var data = new byte[64]; + var discovered = new[] + { + new Gen5VertexInputBinding( + 0x40, 0, 4, 14, 7, sharpBase, 16, 12, data, data.Length, false), + }; + + var merged = AgcVertexMetadata.MergeVertexInputsFromMetadata( + ctx, + scalars, + tables, + discovered); + Assert.Equal(10u, merged[0].DataFormat); + Assert.Equal(0u, merged[0].NumberFormat); + Assert.Equal(12u, merged[0].OffsetBytes); + } + + [Fact] + public void MergeVertexInputs_MatchesInterleavedAttrsByOffsetNotBareBase() + { + // Both attributes share SharpBase. Matching by base alone would assign + // the color format to position (video/UI regression). + const ulong memoryBase = 0x1_0000_0000; + var memory = new FakeCpuMemory(memoryBase, 0x2000); + var ctx = new CpuContext(memory, Generation.Gen5); + + const ulong semanticsAddress = memoryBase + 0x100; + const ulong attribTable = memoryBase + 0x200; + const ulong bufferTable = memoryBase + 0x300; + const ulong sharpBase = memoryBase + 0x800; + + // semantic0 → pos float4 @0; semantic1 → color rgba8 @12 + WriteUInt32(memory, semanticsAddress, 0u | (0u << 8) | (4u << 16)); + WriteUInt32(memory, semanticsAddress + 4, 1u | (4u << 8) | (4u << 16)); + WriteUInt32(memory, attribTable, 0u | (77u << 5) | (0u << 14)); // k32_32_32_32Float + WriteUInt32(memory, attribTable + 4, 0u | (56u << 5) | (12u << 14)); // rgba8unorm @12 + WriteUInt32(memory, bufferTable, (uint)(sharpBase & 0xFFFF_FFFFUL)); + WriteUInt32(memory, bufferTable + 4, (uint)(sharpBase >> 32) | (16u << 16)); + + var scalars = new uint[32]; + scalars[4] = (uint)(attribTable & 0xFFFF_FFFFUL); + scalars[5] = (uint)(attribTable >> 32); + scalars[6] = (uint)(bufferTable & 0xFFFF_FFFFUL); + scalars[7] = (uint)(bufferTable >> 32); + + var tables = new AgcVertexMetadata.VertexTableRegisters( + VertexBufferReg: 6, + VertexAttribReg: 4, + InputSemanticsCount: 2, + InputSemanticsAddress: semanticsAddress); + + var data = new byte[64]; + var discovered = new[] + { + new Gen5VertexInputBinding( + 0x40, 0, 4, 14, 7, sharpBase, 16, 0, data, data.Length, false), + new Gen5VertexInputBinding( + 0x80, 1, 4, 14, 7, sharpBase, 16, 12, data, data.Length, false), + }; + + var merged = AgcVertexMetadata.MergeVertexInputsFromMetadata( + ctx, + scalars, + tables, + discovered); + Assert.Equal(2, merged.Count); + Assert.Equal(0u, merged[0].OffsetBytes); + Assert.Equal(12u, merged[1].OffsetBytes); + Assert.Equal(0u, merged[1].NumberFormat); // Unorm color, not float + Assert.Equal(10u, merged[1].DataFormat); // RGBA8 + Assert.Equal(sharpBase, merged[0].BaseAddress); + Assert.Equal(sharpBase, merged[1].BaseAddress); + Assert.Same(data, merged[0].Data); + } + + [Fact] + public void CollectFetchPrologPcs_FindsSBufferLoadsFromTableRegisters() + { + var tables = new AgcVertexMetadata.VertexTableRegisters( + VertexBufferReg: 10, + VertexAttribReg: 8, + InputSemanticsCount: 1, + InputSemanticsAddress: 1); + + var program = new Gen5ShaderProgram( + 0, + [ + new Gen5ShaderInstruction( + 0x10, + Gen5ShaderEncoding.Smem, + "SBufferLoadDword", + Words: [], + Sources: [Gen5Operand.Scalar(8)], + Destinations: [Gen5Operand.Scalar(20)], + new Gen5ScalarMemoryControl(1, 0, null)), + new Gen5ShaderInstruction( + 0x20, + Gen5ShaderEncoding.Smem, + "SBufferLoadDword", + Words: [], + Sources: [Gen5Operand.Scalar(12)], + Destinations: [Gen5Operand.Scalar(24)], + new Gen5ScalarMemoryControl(1, 0, null)), + new Gen5ShaderInstruction( + 0x30, + Gen5ShaderEncoding.Sopp, + "SEndpgm", + Words: [], + Sources: [], + Destinations: [], + null), + ]); + + var pcs = AgcVertexMetadata.CollectFetchPrologPcs(program, tables); + Assert.Contains(0x10u, pcs); + Assert.DoesNotContain(0x20u, pcs); + } + + private static void WriteUInt32(FakeCpuMemory memory, ulong address, uint value) + { + Span bytes = stackalloc byte[4]; + BinaryPrimitives.WriteUInt32LittleEndian(bytes, value); + Assert.True(memory.TryWrite(address, bytes)); + } +}