[Tests] Cover vertex descriptor bounds and oversized image readbacks
This commit is contained in:
parent
ebfc9b0887
commit
c9119da5a8
|
|
@ -421,13 +421,16 @@ public sealed partial class GuestImageCacheTests
|
|||
harness.Shutdown();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NearCapacityReadback_ReusesTheSharedDownloadRing()
|
||||
[Theory]
|
||||
[InlineData(2047u)]
|
||||
[InlineData(2048u)]
|
||||
[InlineData(4096u)]
|
||||
public void Readback_HandlesNearCapacityAndOversizedImages(uint height)
|
||||
{
|
||||
if (!GatePrerequisites.Ready(_vulkan)) return;
|
||||
const uint width = 4096, height = 2047;
|
||||
const ulong size = (ulong)width * height * 4;
|
||||
using var harness = new CacheHarness(_vulkan, backingBytes: 40UL * 1024 * 1024);
|
||||
const uint width = 4096;
|
||||
var size = (ulong)width * height * 4;
|
||||
using var harness = new CacheHarness(_vulkan, backingBytes: size + 8UL * 1024 * 1024);
|
||||
var address = harness.MapBacked(size, ReadWrite);
|
||||
var request = LinearRequest(address, size, Format.R8G8B8A8Unorm, GuestPixelFormat.Bits8_8_8_8UNorm, GuestImageType.Color2D, new Extent3D(width, height, 1), 1, 4, 1);
|
||||
ulong[] samples = [0, size / 2, size - 4];
|
||||
|
|
|
|||
|
|
@ -336,6 +336,9 @@ internal sealed unsafe class HeadlessVulkan : IDisposable
|
|||
SamplerAnisotropy = baseFeatures.SamplerAnisotropy,
|
||||
ShaderStorageImageExtendedFormats = baseFeatures.ShaderStorageImageExtendedFormats,
|
||||
ShaderInt64 = baseFeatures.ShaderInt64,
|
||||
ShaderClipDistance = baseFeatures.ShaderClipDistance,
|
||||
ShaderCullDistance = baseFeatures.ShaderCullDistance,
|
||||
RobustBufferAccess = baseFeatures.RobustBufferAccess,
|
||||
};
|
||||
var priority = 1f;
|
||||
var queueInfo = new DeviceQueueCreateInfo
|
||||
|
|
|
|||
|
|
@ -0,0 +1,84 @@
|
|||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using SharpEmu.Libs.Gpu.GpuCommands;
|
||||
using SharpEmu.Libs.Gpu.Pipelines;
|
||||
using SharpEmu.Libs.Gpu.Rendering;
|
||||
using SharpEmu.Libs.Tests.Gpu.Images;
|
||||
using SharpEmu.ShaderCompiler;
|
||||
using SharpEmu.ShaderCompiler.Resources;
|
||||
using SharpEmu.ShaderCompiler.Vulkan;
|
||||
using Xunit;
|
||||
using static SharpEmu.Libs.Tests.Gpu.Images.ImageCacheTestSupport;
|
||||
|
||||
namespace SharpEmu.Libs.Tests.VideoOut;
|
||||
|
||||
public sealed unsafe partial class RenderHostDeviceTests
|
||||
{
|
||||
[Fact]
|
||||
public void FullscreenStrip_DoesNotRasterizeTheVertexBeyondTheGuestDescriptor()
|
||||
{
|
||||
if (!Ready()) return;
|
||||
using var presenter = new PresenterUnderTest(_vulkan);
|
||||
presenter.LoadRenderingCommands();
|
||||
var harness = presenter.Harness;
|
||||
var target = harness.MapBacked(0x10000, ReadWrite);
|
||||
var vertices = harness.MapBacked(0x10000, ReadWrite);
|
||||
var words = RegisterWords.Color(target, Size, Size);
|
||||
// PLAYROOM draws four vertices from a three-record, 24-byte-stride descriptor.
|
||||
// The following record belongs to other guest data, not the fullscreen triangle.
|
||||
harness.Write(vertices, Bytes(
|
||||
-1f, -1f, 0f, 1f, 0f, 0f,
|
||||
3f, -1f, 0f, 1f, 2f, 0f,
|
||||
-1f, 3f, 0f, 1f, 0f, 2f,
|
||||
0f, 0f, BitConverter.UInt32BitsToSingle(9952), 0f, 0f, 0f));
|
||||
|
||||
var fetch = new Gen5ShaderInstruction(0, Gen5ShaderEncoding.Mubuf, "BufferLoadFormatXyzw", [], [], [],
|
||||
new Gen5BufferMemoryControl(4, 0, 0, 0, 0, true, false, false, false));
|
||||
var export = new Gen5ShaderInstruction(8, Gen5ShaderEncoding.Exp, "Exp", [],
|
||||
[Gen5Operand.Vector(0), Gen5Operand.Vector(1), Gen5Operand.Vector(2), Gen5Operand.Vector(3)], [],
|
||||
new Gen5ExportControl(12, 15, false, true, true));
|
||||
var parameter = new Gen5ShaderInstruction(16, Gen5ShaderEncoding.Exp, "Exp", [],
|
||||
[Gen5Operand.Vector(3), Gen5Operand.Vector(3), Gen5Operand.Vector(3), Gen5Operand.Vector(3)], [],
|
||||
new Gen5ExportControl(32, 15, false, true, true));
|
||||
var end = new Gen5ShaderInstruction(24, Gen5ShaderEncoding.Sopp, "SEndpgm", [], [], [], null);
|
||||
var plan = ShaderResourcePlan.Extract(new Gen5ShaderProgram(0, [fetch, export, parameter, end]), ShaderStage.Vertex, 1, 0, 0, new HashSet<uint> { 0 });
|
||||
var resources = ResourceMaterializer.ApplyTo(plan, ResourceSpecialization.Default(plan.Info));
|
||||
var request = new ShaderCompileRequest(plan, resources, BindingLayout.Allocate(resources.Info, [], false, false, false, 0))
|
||||
{
|
||||
EnableGraphicsSubgroupOperations = false,
|
||||
VertexInputs = [new ShaderVertexInput(0, 0, 4, 7, false, [])],
|
||||
};
|
||||
Assert.True(Gen5SpirvTranslator.TryCompileProgram(request, out var shader, out var error), error);
|
||||
|
||||
// Valid vertices interpolate white; an unculled out-of-bounds vertex can overwrite it.
|
||||
var fragment = new SpirvModuleBuilder();
|
||||
fragment.AddCapability(SpirvCapability.Shader);
|
||||
var voidType = fragment.TypeVoid();
|
||||
var vectorType = fragment.TypeVector(fragment.TypeFloat(32), 4);
|
||||
var input = fragment.AddGlobalVariable(fragment.TypePointer(SpirvStorageClass.Input, vectorType), SpirvStorageClass.Input);
|
||||
var output = fragment.AddGlobalVariable(fragment.TypePointer(SpirvStorageClass.Output, vectorType), SpirvStorageClass.Output);
|
||||
fragment.AddDecoration(input, SpirvDecoration.Location, 0);
|
||||
fragment.AddDecoration(output, SpirvDecoration.Location, 0);
|
||||
var main = fragment.BeginFunction(voidType, fragment.TypeFunction(voidType));
|
||||
fragment.AddLabel();
|
||||
fragment.AddStatement(SpirvOp.Store, output, fragment.AddInstruction(SpirvOp.Load, vectorType, input));
|
||||
fragment.AddStatement(SpirvOp.Return);
|
||||
fragment.EndFunction();
|
||||
fragment.AddEntryPoint(SpirvExecutionModel.Fragment, main, "main", [input, output]);
|
||||
fragment.AddExecutionMode(main, SpirvExecutionMode.OriginUpperLeft);
|
||||
var executor = new RenderExecutor(presenter.RenderHost,
|
||||
new FixedProgramProvider((IShaderPipelineHost)presenter.Instance, vertices,
|
||||
interpolationShader: fragment.Build(), vertexShader: shader.Spirv, vertexComponents: 4, vertexStride: 24));
|
||||
var banks = Banks(words);
|
||||
banks.UserConfig.PrimitiveType = 5;
|
||||
presenter.Run(() => executor.DrawAuto(1, banks,
|
||||
new DrawAutoArguments(0, 0, 4, 1, 0, 0, DrawOffsetSource.Packet)));
|
||||
presenter.Run(() => presenter.InvokeMethod("FlushBatchedGuestCommands"));
|
||||
harness.Finish();
|
||||
var pixels = harness.ReadImageBytes(TargetImage(presenter, words));
|
||||
Assert.All(Enumerable.Range(0, (int)(Size * Size)),
|
||||
index => Assert.Equal(uint.MaxValue, BitConverter.ToUInt32(pixels, index * 4)));
|
||||
harness.Shutdown();
|
||||
}
|
||||
}
|
||||
|
|
@ -143,11 +143,12 @@ public sealed unsafe partial class RenderHostDeviceTests : IClassFixture<Headles
|
|||
|
||||
// One float2 position program and one solid red pixel program over empty resource plans.
|
||||
private sealed class FixedProgramProvider(IShaderPipelineHost host, ulong vertexAddress, bool pushData = false,
|
||||
byte[]? interpolationShader = null, uint vertexCount = VertexCount) : IShaderPipelineProvider
|
||||
byte[]? interpolationShader = null, uint vertexCount = VertexCount,
|
||||
byte[]? vertexShader = null, uint vertexComponents = 2, uint vertexStride = VertexStride) : IShaderPipelineProvider
|
||||
{
|
||||
private const uint Float2Format = 64;
|
||||
private static readonly uint[] UserRegisters = [0, 1];
|
||||
private readonly ShaderProgramInfo _vertex = EmptyProgram(ShaderStageKind.Vertex, 1, fetchComponents: 2, userDataRegisters: pushData ? UserRegisters : null, pushDataStart: 2);
|
||||
private readonly ShaderProgramInfo _vertex = EmptyProgram(ShaderStageKind.Vertex, 1, fetchComponents: vertexComponents, userDataRegisters: pushData ? UserRegisters : null, pushDataStart: 2);
|
||||
private readonly ShaderProgramInfo _pixel = EmptyProgram(ShaderStageKind.Pixel, 2, userDataRegisters: pushData ? UserRegisters : null);
|
||||
private readonly ResourceSnapshot _snapshot = new() { UserData = pushData ? [0x11, 0x22] : [] };
|
||||
private ShaderProgram _vertexProgram;
|
||||
|
|
@ -179,7 +180,7 @@ public sealed unsafe partial class RenderHostDeviceTests : IClassFixture<Headles
|
|||
}
|
||||
|
||||
_vertexProgram = new ShaderProgram(1, host.CreateShaderModule(new VulkanCompiledGuestShader(
|
||||
CreatePositionVertexShader(interpolationShader is not null)), ShaderStage.Vertex, 1, 1));
|
||||
vertexShader ?? CreatePositionVertexShader(interpolationShader is not null)), ShaderStage.Vertex, 1, 1));
|
||||
_pixelProgram = new ShaderProgram(2, host.CreateShaderModule(new VulkanCompiledGuestShader(
|
||||
interpolationShader ?? SpirvFixedShaders.CreateSolidFragment(1f, 0f, 0f, 1f)), ShaderStage.Pixel, 2, 2));
|
||||
}
|
||||
|
|
@ -200,8 +201,8 @@ public sealed unsafe partial class RenderHostDeviceTests : IClassFixture<Headles
|
|||
Pixel = _pixelProgram,
|
||||
VertexInput = new VertexInputInfo
|
||||
{
|
||||
Buffers = [new VertexInputBuffer(vertexAddress, VertexStride, vertexCount)],
|
||||
Attributes = [new VertexAttributeResource(new BufferDescriptorWords((uint)vertexAddress, (uint)(vertexAddress >> 32) | (VertexStride << 16), vertexCount, Float2Format << 12), 0, 2, 0, 0, 0, 0)],
|
||||
Buffers = [new VertexInputBuffer(vertexAddress, vertexStride, vertexCount)],
|
||||
Attributes = [new VertexAttributeResource(new BufferDescriptorWords((uint)vertexAddress, (uint)(vertexAddress >> 32) | (vertexStride << 16), vertexCount, (vertexComponents == 4 ? 77u : Float2Format) << 12), 0, (int)vertexComponents, 0, 0, 0, 0)],
|
||||
Stage = new ShaderStageResources(_vertex, _snapshot),
|
||||
},
|
||||
PixelInput = new PixelInputInfo { InputCount = 0, Stage = new ShaderStageResources(_pixel, _snapshot) },
|
||||
|
|
|
|||
Loading…
Reference in New Issue