diff --git a/src/SharpEmu.Libs/Gpu/Pipelines/VertexInputResolver.cs b/src/SharpEmu.Libs/Gpu/Pipelines/VertexInputResolver.cs index 4265201..df493cc 100644 --- a/src/SharpEmu.Libs/Gpu/Pipelines/VertexInputResolver.cs +++ b/src/SharpEmu.Libs/Gpu/Pipelines/VertexInputResolver.cs @@ -360,7 +360,24 @@ public static class VertexInputResolver for (var bufferIndex = 0; bufferIndex < buffers.Count; bufferIndex++) { var buffer = buffers[bufferIndex]; - result[bufferIndex] = new VertexInputBuffer(buffer.Address, buffer.Stride, buffer.RecordCount, buffer.FetchIndex != 0); + uint minimumFetchBytes = 0; + foreach (var resourceIndex in buffer.AttributeIndices) + { + var resource = resources[resourceIndex]; + var descriptor = resource.Descriptor; + // OOB_SELECT=2 only tests for an unbound (zero-record) descriptor. + // A constant stream still reads the whole formatted element, even + // when NUM_RECORDS is smaller than that element's byte width. + if (descriptor.Stride == 0 && descriptor.OutOfBounds == 2 && descriptor.RecordCount != 0 && + VertexAttributeFormats.TryResolve(in descriptor, 4, out var format, out _)) + { + minimumFetchBytes = Math.Max(minimumFetchBytes, Images.ViewFormatRules.BlockBytes(format)); + } + } + result[bufferIndex] = new VertexInputBuffer(buffer.Address, buffer.Stride, buffer.RecordCount, buffer.FetchIndex != 0) + { + MinimumFetchBytes = minimumFetchBytes, + }; foreach (var resourceIndex in buffer.AttributeIndices) { var resource = resources[resourceIndex]; diff --git a/src/SharpEmu.Libs/Gpu/Rendering/ShaderStageResources.cs b/src/SharpEmu.Libs/Gpu/Rendering/ShaderStageResources.cs index fa55ad3..8d6aa08 100644 --- a/src/SharpEmu.Libs/Gpu/Rendering/ShaderStageResources.cs +++ b/src/SharpEmu.Libs/Gpu/Rendering/ShaderStageResources.cs @@ -100,7 +100,9 @@ public readonly record struct DispatchThreadLimits(uint X, uint Y, uint Z); // The vertex buffer words of one fetch slot as the vertex program declares them. public readonly record struct VertexInputBuffer(ulong Address, uint Stride, uint RecordCount, bool PerInstance = false) { - public ulong Size => Stride != 0 ? (ulong)Stride * RecordCount : RecordCount; + public uint MinimumFetchBytes { get; init; } + + public ulong Size => Math.Max(Stride != 0 ? (ulong)Stride * RecordCount : RecordCount, MinimumFetchBytes); } // One attribute of the vertex tables: its buffer words, the registers it fills and its buffer slot. diff --git a/tests/SharpEmu.Libs.Tests/Gpu/Pipelines/VertexInputResolverTests.cs b/tests/SharpEmu.Libs.Tests/Gpu/Pipelines/VertexInputResolverTests.cs index 63b750e..de20b9d 100644 --- a/tests/SharpEmu.Libs.Tests/Gpu/Pipelines/VertexInputResolverTests.cs +++ b/tests/SharpEmu.Libs.Tests/Gpu/Pipelines/VertexInputResolverTests.cs @@ -354,4 +354,36 @@ public sealed class VertexInputResolverTests : IDisposable Assert.Contains("records=4/5", fatal.Message); } + [Theory] + [InlineData(74u, 3, 12ul)] + [InlineData(77u, 4, 16ul)] + [InlineData(77u, 3, 16ul)] + [InlineData(64u, 2, 8ul)] + public void MergeBuffers_ConstantUncheckedStreamIncludesFullFormattedFetch(uint format, int components, ulong expectedSize) + { + var resource = Attribute(StreamBase, 0, 1, 0); + resource = resource with + { + Descriptor = resource.Descriptor with { Word3 = IdentitySelect | (format << 12) | (2u << 28) }, + RegisterCount = components, + }; + var buffer = Assert.Single(VertexInputResolver.MergeBuffers([resource], ShaderAddress)); + Assert.Equal(1u, buffer.RecordCount); + Assert.Equal(0u, buffer.Stride); + Assert.Equal(expectedSize, buffer.Size); + } + + [Theory] + [InlineData(0u, 1u)] + [InlineData(1u, 1u)] + [InlineData(3u, 1u)] + [InlineData(2u, 0u)] + public void MergeBuffers_BoundedOrUnboundConstantStreamKeepsDeclaredFootprint(uint outOfBounds, uint records) + { + var resource = Attribute(StreamBase, 0, records, 0); + resource = resource with { Descriptor = resource.Descriptor with { Word3 = resource.Descriptor.Word3 | (outOfBounds << 28) } }; + var buffer = Assert.Single(VertexInputResolver.MergeBuffers([resource], ShaderAddress)); + Assert.Equal((ulong)records, buffer.Size); + } + }