diff --git a/src/SharpEmu.Libs/Agc/AgcVertexMetadata.cs b/src/SharpEmu.Libs/Agc/AgcVertexMetadata.cs index 8537ef27..ae60d4ec 100644 --- a/src/SharpEmu.Libs/Agc/AgcVertexMetadata.cs +++ b/src/SharpEmu.Libs/Agc/AgcVertexMetadata.cs @@ -238,10 +238,12 @@ internal static class AgcVertexMetadata } /// - /// Patch IR-discovered fetches from the attrib table onto the V# format/offset. + /// Patch IR-discovered fetches from the attrib table onto the V# layout. /// 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. + /// (GTA UI glyphs). Otherwise match by the effective captured byte offset. + /// Never rebases BaseAddress/Data/Location/Pc or overwrites a discovered + /// offset: metadata may refine the format, stride and instance rate only + /// after both address keys independently resolve to the same attribute. /// internal static IReadOnlyList MergeVertexInputsFromMetadata( CpuContext ctx, @@ -269,13 +271,13 @@ internal static class AgcVertexMetadata var changed = false; foreach (var input in discovered) { - if (!TryMatchMetadataResource(input, resources, usedResources, out var resource, out var fillOffset)) + if (!TryMatchMetadataResource(input, resources, usedResources, out var resource)) { merged.Add(input); continue; } - var refined = ApplyMetadataFormat(input, resource, fillOffset); + var refined = ApplyMetadataFormat(input, resource); changed |= refined != input; merged.Add(refined); } @@ -286,7 +288,7 @@ internal static class AgcVertexMetadata /// /// 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. + /// Keeps each binding's Pc/Location/address for SPIR-V and overlays layout. /// private static bool TryMergeByLocationPairing( IReadOnlyList discovered, @@ -299,34 +301,36 @@ internal static class AgcVertexMetadata return false; } - var orderedInputs = discovered.OrderBy(static input => input.Location).ToArray(); + var orderedInputs = discovered + .Select(static (input, originalIndex) => (Input: input, OriginalIndex: originalIndex)) + .OrderBy(static entry => entry.Input.Location) + .ThenBy(static entry => entry.OriginalIndex) + .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]; + var input = orderedInputs[index].Input; if (resource.SharpBase != streamBase || resource.Stride != streamStride || - (input.Stride != 0 && input.Stride != streamStride) || - !IsSameVertexStream(input, resource)) + !TryGetMetadataOffset(input, resource, out var resolvedOffset) || + resolvedOffset != input.OffsetBytes) { return false; } } - var byPc = new Dictionary(discovered.Count); + var result = discovered.ToArray(); var changed = false; for (var index = 0; index < orderedInputs.Length; index++) { - var input = orderedInputs[index]; + var input = orderedInputs[index].Input; var resource = orderedResources[index]; - var fillOffset = input.BaseAddress == resource.SharpBase || - IsAddressInsideCapturedSpan(input, resource.SharpBase); - var refined = ApplyMetadataFormat(input, resource, fillOffset); + var refined = ApplyMetadataFormat(input, resource); changed |= refined != input; - byPc[input.Pc] = refined; + result[orderedInputs[index].OriginalIndex] = refined; } if (!changed) @@ -334,20 +338,13 @@ internal static class AgcVertexMetadata 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) + MetadataVertexResource resource) { var components = input.ComponentCount != 0 && input.ComponentCount < resource.ComponentCount @@ -359,7 +356,8 @@ internal static class AgcVertexMetadata DataFormat = resource.DataFormat, NumberFormat = resource.NumberFormat, ComponentCount = components, - OffsetBytes = fillOffsetBytes ? resource.OffsetBytes : input.OffsetBytes, + Stride = resource.Stride, + PerInstance = resource.PerInstance, }; } @@ -434,14 +432,11 @@ internal static class AgcVertexMetadata Gen5VertexInputBinding input, IReadOnlyList resources, bool[] usedResources, - out MetadataVertexResource resource, - out bool fillOffsetBytes) + out MetadataVertexResource resource) { 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]) @@ -450,100 +445,64 @@ internal static class AgcVertexMetadata } var candidate = resources[index]; - if (candidate.Stride != 0 && - input.Stride != 0 && - candidate.Stride != input.Stride) + if (!TryGetMetadataOffset(input, candidate, out var resolvedOffset) || + resolvedOffset != input.OffsetBytes) { continue; } - if (!IsSameVertexStream(input, candidate)) - { - continue; - } + // The effective captured offset (including any base rebasing done + // while coalescing adjacent vertex streams) is the primary key. + var score = 400; - 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))) + // Discovery can carry a stale inferred stride (notably 32 for a + // real stride-40 interleaved layout). Prefer a matching stride + // when candidates are otherwise equivalent, but do not reject an + // unambiguous metadata match: the V# descriptor is authoritative. + if (input.Stride == candidate.Stride) { - 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; + score += 25; } 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) { - 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; - } + return false; } usedResources[bestIndex] = true; resource = resources[bestIndex]; - fillOffsetBytes = bestFillOffset; return true; } - private static bool IsSameVertexStream( + private static bool TryGetMetadataOffset( Gen5VertexInputBinding input, - MetadataVertexResource resource) + MetadataVertexResource resource, + out uint offsetBytes) { - if (input.BaseAddress == resource.SharpBase || - input.BaseAddress == resource.SharpBase + resource.OffsetBytes) + offsetBytes = input.OffsetBytes; + if (resource.SharpBase < input.BaseAddress || + (!IsAddressInsideCapturedSpan(input, resource.SharpBase) && + resource.SharpBase != input.BaseAddress)) { - return true; + return false; } - return IsAddressInsideCapturedSpan(input, resource.SharpBase); + var relativeBase = resource.SharpBase - input.BaseAddress; + var resolvedOffset = relativeBase + resource.OffsetBytes; + if (resolvedOffset > uint.MaxValue) + { + return false; + } + + offsetBytes = (uint)resolvedOffset; + return true; } private static bool IsAddressInsideCapturedSpan( @@ -553,28 +512,6 @@ internal static class AgcVertexMetadata 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. diff --git a/tests/SharpEmu.Libs.Tests/Agc/AgcVertexMetadataTests.cs b/tests/SharpEmu.Libs.Tests/Agc/AgcVertexMetadataTests.cs index 32d1703e..88006c88 100644 --- a/tests/SharpEmu.Libs.Tests/Agc/AgcVertexMetadataTests.cs +++ b/tests/SharpEmu.Libs.Tests/Agc/AgcVertexMetadataTests.cs @@ -74,7 +74,7 @@ public sealed class AgcVertexMetadataTests } [Fact] - public void MergeVertexInputs_OverlaysFormatWithoutRebasingCapture() + public void MergeVertexInputs_OverlaysLayoutWithoutRebasingCapture() { const ulong memoryBase = 0x1_0000_0000; var memory = new FakeCpuMemory(memoryBase, 0x2000); @@ -114,7 +114,7 @@ public sealed class AgcVertexMetadataTests NumberFormat: 7, BaseAddress: sharpBase, Stride: 16, - OffsetBytes: 0, + OffsetBytes: 12, Data: data, DataLength: data.Length, DataPooled: false), @@ -135,6 +135,137 @@ public sealed class AgcVertexMetadataTests Assert.Equal(0x40u, merged[0].Pc); } + [Fact] + public void MergeVertexInputs_MetadataCorrectsStaleStride40() + { + 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 | (56u << 5) | (12u << 14)); + WriteUInt32(memory, bufferTable, (uint)(sharpBase & 0xFFFF_FFFFUL)); + WriteUInt32(memory, bufferTable + 4, (uint)(sharpBase >> 32) | (40u << 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[160]; + var discovered = new[] + { + new Gen5VertexInputBinding( + 0x40, 0, 4, 14, 7, sharpBase, 32, 12, data, data.Length, false), + }; + + var merged = AgcVertexMetadata.MergeVertexInputsFromMetadata( + ctx, + scalars, + tables, + discovered); + + Assert.Single(merged); + Assert.Equal(40u, merged[0].Stride); + Assert.Equal(12u, merged[0].OffsetBytes); + Assert.Equal(sharpBase, merged[0].BaseAddress); + Assert.Same(data, merged[0].Data); + Assert.Equal(0x40u, merged[0].Pc); + } + + [Fact] + public void MergeVertexInputs_ConflictingMetadataOffsetDoesNotMoveBinding() + { + 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 | (56u << 5) | (12u << 14)); + WriteUInt32(memory, bufferTable, (uint)(sharpBase & 0xFFFF_FFFFUL)); + WriteUInt32(memory, bufferTable + 4, (uint)(sharpBase >> 32) | (40u << 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 original = new Gen5VertexInputBinding( + 0x40, 0, 4, 14, 7, sharpBase, 32, 0, new byte[160], 160, false); + var merged = AgcVertexMetadata.MergeVertexInputsFromMetadata( + ctx, + scalars, + tables, + [original]); + + Assert.Same(original, Assert.Single(merged)); + } + + [Fact] + public void MergeVertexInputs_UsesOffsetRelativeToCapturedBase() + { + 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 capturedBase = memoryBase + 0x7F8; + const ulong sharpBase = memoryBase + 0x800; + + WriteUInt32(memory, semanticsAddress, 0u | (0u << 8) | (4u << 16)); + WriteUInt32(memory, attribTable, 0u | (56u << 5) | (12u << 14)); + WriteUInt32(memory, bufferTable, (uint)(sharpBase & 0xFFFF_FFFFUL)); + WriteUInt32(memory, bufferTable + 4, (uint)(sharpBase >> 32) | (40u << 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[160]; + var merged = AgcVertexMetadata.MergeVertexInputsFromMetadata( + ctx, + scalars, + tables, + [new Gen5VertexInputBinding( + 0x40, 0, 4, 14, 7, capturedBase, 32, 20, data, data.Length, false)]); + + Assert.Equal(40u, Assert.Single(merged).Stride); + Assert.Equal(20u, merged[0].OffsetBytes); + Assert.Equal(capturedBase, merged[0].BaseAddress); + Assert.Same(data, merged[0].Data); + } + [Fact] public void MergeVertexInputs_AcceptsVertexAttribFormatEnums() {