fix(agc): refine vertex layouts without rebasing captured inputs (#841)

This commit is contained in:
frangametv
2026-08-24 14:49:36 +02:00
committed by GitHub
parent 2b8ef7d8fa
commit f4f36b558f
2 changed files with 187 additions and 119 deletions
+54 -117
View File
@@ -238,10 +238,12 @@ internal static class AgcVertexMetadata
} }
/// <summary> /// <summary>
/// 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 /// Prefer 1:1 Location pairing when counts match on one interleaved stream
/// (GTA UI glyphs). Otherwise match by stride + byte offset. Never rebases /// (GTA UI glyphs). Otherwise match by the effective captured byte offset.
/// BaseAddress/Data/Location/Pc/PerInstance. /// 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.
/// </summary> /// </summary>
internal static IReadOnlyList<Gen5VertexInputBinding> MergeVertexInputsFromMetadata( internal static IReadOnlyList<Gen5VertexInputBinding> MergeVertexInputsFromMetadata(
CpuContext ctx, CpuContext ctx,
@@ -269,13 +271,13 @@ internal static class AgcVertexMetadata
var changed = false; var changed = false;
foreach (var input in discovered) 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); merged.Add(input);
continue; continue;
} }
var refined = ApplyMetadataFormat(input, resource, fillOffset); var refined = ApplyMetadataFormat(input, resource);
changed |= refined != input; changed |= refined != input;
merged.Add(refined); merged.Add(refined);
} }
@@ -286,7 +288,7 @@ internal static class AgcVertexMetadata
/// <summary> /// <summary>
/// When discovery and metadata describe the same interleaved stream with /// When discovery and metadata describe the same interleaved stream with
/// equal attribute counts, pair by sorted Location (semantic order). /// 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.
/// </summary> /// </summary>
private static bool TryMergeByLocationPairing( private static bool TryMergeByLocationPairing(
IReadOnlyList<Gen5VertexInputBinding> discovered, IReadOnlyList<Gen5VertexInputBinding> discovered,
@@ -299,34 +301,36 @@ internal static class AgcVertexMetadata
return false; 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 orderedResources = resources.OrderBy(static resource => resource.Location).ToArray();
var streamBase = orderedResources[0].SharpBase; var streamBase = orderedResources[0].SharpBase;
var streamStride = orderedResources[0].Stride; var streamStride = orderedResources[0].Stride;
for (var index = 0; index < orderedResources.Length; index++) for (var index = 0; index < orderedResources.Length; index++)
{ {
var resource = orderedResources[index]; var resource = orderedResources[index];
var input = orderedInputs[index]; var input = orderedInputs[index].Input;
if (resource.SharpBase != streamBase || if (resource.SharpBase != streamBase ||
resource.Stride != streamStride || resource.Stride != streamStride ||
(input.Stride != 0 && input.Stride != streamStride) || !TryGetMetadataOffset(input, resource, out var resolvedOffset) ||
!IsSameVertexStream(input, resource)) resolvedOffset != input.OffsetBytes)
{ {
return false; return false;
} }
} }
var byPc = new Dictionary<uint, Gen5VertexInputBinding>(discovered.Count); var result = discovered.ToArray();
var changed = false; var changed = false;
for (var index = 0; index < orderedInputs.Length; index++) for (var index = 0; index < orderedInputs.Length; index++)
{ {
var input = orderedInputs[index]; var input = orderedInputs[index].Input;
var resource = orderedResources[index]; var resource = orderedResources[index];
var fillOffset = input.BaseAddress == resource.SharpBase || var refined = ApplyMetadataFormat(input, resource);
IsAddressInsideCapturedSpan(input, resource.SharpBase);
var refined = ApplyMetadataFormat(input, resource, fillOffset);
changed |= refined != input; changed |= refined != input;
byPc[input.Pc] = refined; result[orderedInputs[index].OriginalIndex] = refined;
} }
if (!changed) if (!changed)
@@ -334,20 +338,13 @@ internal static class AgcVertexMetadata
return false; return false;
} }
var result = new Gen5VertexInputBinding[discovered.Count];
for (var index = 0; index < discovered.Count; index++)
{
result[index] = byPc[discovered[index].Pc];
}
merged = result; merged = result;
return true; return true;
} }
private static Gen5VertexInputBinding ApplyMetadataFormat( private static Gen5VertexInputBinding ApplyMetadataFormat(
Gen5VertexInputBinding input, Gen5VertexInputBinding input,
MetadataVertexResource resource, MetadataVertexResource resource)
bool fillOffsetBytes)
{ {
var components = input.ComponentCount != 0 && var components = input.ComponentCount != 0 &&
input.ComponentCount < resource.ComponentCount input.ComponentCount < resource.ComponentCount
@@ -359,7 +356,8 @@ internal static class AgcVertexMetadata
DataFormat = resource.DataFormat, DataFormat = resource.DataFormat,
NumberFormat = resource.NumberFormat, NumberFormat = resource.NumberFormat,
ComponentCount = components, ComponentCount = components,
OffsetBytes = fillOffsetBytes ? resource.OffsetBytes : input.OffsetBytes, Stride = resource.Stride,
PerInstance = resource.PerInstance,
}; };
} }
@@ -434,14 +432,11 @@ internal static class AgcVertexMetadata
Gen5VertexInputBinding input, Gen5VertexInputBinding input,
IReadOnlyList<MetadataVertexResource> resources, IReadOnlyList<MetadataVertexResource> resources,
bool[] usedResources, bool[] usedResources,
out MetadataVertexResource resource, out MetadataVertexResource resource)
out bool fillOffsetBytes)
{ {
resource = default; resource = default;
fillOffsetBytes = false;
var bestScore = int.MinValue; var bestScore = int.MinValue;
var bestIndex = -1; var bestIndex = -1;
var bestFillOffset = false;
for (var index = 0; index < resources.Count; index++) for (var index = 0; index < resources.Count; index++)
{ {
if (usedResources[index]) if (usedResources[index])
@@ -450,100 +445,64 @@ internal static class AgcVertexMetadata
} }
var candidate = resources[index]; var candidate = resources[index];
if (candidate.Stride != 0 && if (!TryGetMetadataOffset(input, candidate, out var resolvedOffset) ||
input.Stride != 0 && resolvedOffset != input.OffsetBytes)
candidate.Stride != input.Stride)
{ {
continue; continue;
} }
if (!IsSameVertexStream(input, candidate)) // The effective captured offset (including any base rebasing done
{ // while coalescing adjacent vertex streams) is the primary key.
continue; var score = 400;
}
var attrAddress = candidate.SharpBase + candidate.OffsetBytes; // Discovery can carry a stale inferred stride (notably 32 for a
var score = int.MinValue; // real stride-40 interleaved layout). Prefer a matching stride
var fillOffset = false; // when candidates are otherwise equivalent, but do not reject an
// unambiguous metadata match: the V# descriptor is authoritative.
// Post-capture interleaved: shared BaseAddress, distinct OffsetBytes. if (input.Stride == candidate.Stride)
if (input.OffsetBytes == candidate.OffsetBytes &&
(input.BaseAddress == candidate.SharpBase ||
IsAddressInsideCapturedSpan(input, candidate.SharpBase)))
{ {
score = 400; score += 25;
}
// 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) if (score > bestScore)
{ {
bestScore = score; bestScore = score;
bestIndex = index; bestIndex = index;
bestFillOffset = fillOffset;
} }
} }
// Require an offset-aware match. Bare SharpBase ties (score 250) are if (bestIndex < 0)
// only accepted when a single unused resource remains for that stream.
if (bestIndex < 0 || bestScore < 300)
{ {
if (bestIndex < 0 || bestScore < 250) return false;
{
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; usedResources[bestIndex] = true;
resource = resources[bestIndex]; resource = resources[bestIndex];
fillOffsetBytes = bestFillOffset;
return true; return true;
} }
private static bool IsSameVertexStream( private static bool TryGetMetadataOffset(
Gen5VertexInputBinding input, Gen5VertexInputBinding input,
MetadataVertexResource resource) MetadataVertexResource resource,
out uint offsetBytes)
{ {
if (input.BaseAddress == resource.SharpBase || offsetBytes = input.OffsetBytes;
input.BaseAddress == resource.SharpBase + resource.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( private static bool IsAddressInsideCapturedSpan(
@@ -553,28 +512,6 @@ internal static class AgcVertexMetadata
address >= input.BaseAddress && address >= input.BaseAddress &&
address < input.BaseAddress + (ulong)input.DataLength; address < input.BaseAddress + (ulong)input.DataLength;
private static bool IsUniqueUnusedOffset(
IReadOnlyList<MetadataVertexResource> 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;
}
/// <summary> /// <summary>
/// Attrib-table format /// Attrib-table format
/// fields are VertexAttribFormat; V# / Vulkan paths need BufferFormat. /// fields are VertexAttribFormat; V# / Vulkan paths need BufferFormat.
@@ -74,7 +74,7 @@ public sealed class AgcVertexMetadataTests
} }
[Fact] [Fact]
public void MergeVertexInputs_OverlaysFormatWithoutRebasingCapture() public void MergeVertexInputs_OverlaysLayoutWithoutRebasingCapture()
{ {
const ulong memoryBase = 0x1_0000_0000; const ulong memoryBase = 0x1_0000_0000;
var memory = new FakeCpuMemory(memoryBase, 0x2000); var memory = new FakeCpuMemory(memoryBase, 0x2000);
@@ -114,7 +114,7 @@ public sealed class AgcVertexMetadataTests
NumberFormat: 7, NumberFormat: 7,
BaseAddress: sharpBase, BaseAddress: sharpBase,
Stride: 16, Stride: 16,
OffsetBytes: 0, OffsetBytes: 12,
Data: data, Data: data,
DataLength: data.Length, DataLength: data.Length,
DataPooled: false), DataPooled: false),
@@ -135,6 +135,137 @@ public sealed class AgcVertexMetadataTests
Assert.Equal(0x40u, merged[0].Pc); 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] [Fact]
public void MergeVertexInputs_AcceptsVertexAttribFormatEnums() public void MergeVertexInputs_AcceptsVertexAttribFormatEnums()
{ {