From f4f36b558f731198e6b2ed86dad449318985285d Mon Sep 17 00:00:00 2001
From: frangametv <50166684+frangametv@users.noreply.github.com>
Date: Mon, 24 Aug 2026 14:49:36 +0200
Subject: [PATCH] fix(agc): refine vertex layouts without rebasing captured
inputs (#841)
---
src/SharpEmu.Libs/Agc/AgcVertexMetadata.cs | 171 ++++++------------
.../Agc/AgcVertexMetadataTests.cs | 135 +++++++++++++-
2 files changed, 187 insertions(+), 119 deletions(-)
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()
{