[Tests] Cover vertex descriptor bounds and oversized image readbacks

This commit is contained in:
ParantezTech 2026-10-05 18:39:42 +03:00
parent ebfc9b0887
commit c9119da5a8
4 changed files with 101 additions and 10 deletions

View File

@ -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];

View File

@ -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

View File

@ -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();
}
}

View File

@ -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) },