Compare commits

...

16 Commits

Author SHA1 Message Date
ParantezTech 248aed8e96 [Tests] Cover DCC metadata on 2D, 3D and array textures 2026-10-06 00:56:14 +03:00
ParantezTech e1b2928b6a [GPU] Carry DCC metadata only on single-layer 2D textures 2026-10-06 00:56:14 +03:00
ParantezTech cbee451aed [GPU] Revert binding vertex buffers with their guest descriptor sizes 2026-10-06 00:56:13 +03:00
ParantezTech c9119da5a8 [Tests] Cover vertex descriptor bounds and oversized image readbacks 2026-10-05 18:39:42 +03:00
ParantezTech ebfc9b0887 [GPU] Mark sampled DCC clears through the metadata fill path 2026-10-05 18:38:08 +03:00
ParantezTech cccee2d368 [GPU] Carry DCC metadata on compressed textures 2026-10-05 18:38:08 +03:00
ParantezTech 5165b8760c [GPU] Spill oversized image readbacks to a temporary buffer 2026-10-05 18:38:08 +03:00
ParantezTech d7cbb1ca50 [GPU] Accept B10G11R11 float vertex attributes 2026-10-05 18:38:08 +03:00
ParantezTech c4b381c9d2 [GPU] Bind vertex buffers with their guest descriptor sizes 2026-10-05 18:38:08 +03:00
ParantezTech b60eceab87 [ShaderCompiler] Cull primitives with a zero position 2026-10-05 18:38:08 +03:00
ParantezTech b56d0da86d [ShaderCompiler] Reserve a clip plane for position exports 2026-10-05 18:38:07 +03:00
ParantezTech 34a2801a04 [GPU] Pass clip distance support to shader compilation 2026-10-05 18:38:07 +03:00
ParantezTech 771459afc7 [GPU] Expose host clip distance support to pipelines 2026-10-05 18:38:07 +03:00
ParantezTech 49c6b055c4 [ShaderCompiler] Add a clip distance support flag to compile requests 2026-10-05 18:38:07 +03:00
ParantezTech dddee59bd9 [Vulkan] Track shader clip distance support 2026-10-05 18:38:07 +03:00
ParantezTech 6e2933e49c [Vulkan] Enable shader clip and cull distances 2026-10-05 18:38:06 +03:00
15 changed files with 171 additions and 15 deletions

View File

@ -725,15 +725,20 @@ public sealed unsafe partial class GuestImageCache
}
var range = image.Description.Data;
var download = _bufferCache.GetUtilityBuffer(GpuBufferUsage.Download);
if (!download.TryMap(range.Size, out var offset, Math.Max(image.Description.BytesPerBlock, 4u)))
var ring = _bufferCache.GetUtilityBuffer(GpuBufferUsage.Download);
GpuBuffer download = ring;
if (ring.TryMap(range.Size, out var offset, Math.Max(image.Description.BytesPerBlock, 4u)))
{
throw SubmissionScheduler.Fatal($"The reusable download ring cannot map the image: address=0x{range.Address:X16} size=0x{range.Size:X}.");
ring.Commit();
}
else
{
download = new GpuBuffer(_device, _scheduler, GpuBufferUsage.Download, 0, GpuBuffer.AllFlags, range.Size);
}
download.Commit();
if (!_backing.TryReadBacking(range.Address, download.Mapped.Slice((int)offset, (int)range.Size)))
{
if (download != ring) download.Dispose();
return false;
}
@ -761,6 +766,8 @@ public sealed unsafe partial class GuestImageCache
throw SubmissionScheduler.Fatal($"The image readback could not be written to guest memory: address=0x{range.Address:X16} size=0x{range.Size:X}.");
}
});
// Completion actions wait for the priority readback before freeing spill buffers.
if (download != ring) _scheduler.QueueCompletionAction(download.Dispose);
return true;
}

View File

@ -300,6 +300,14 @@ public static partial class ImageRequestBuilders
description.BytesPerBlock = blockBytes != 0 ? blockBytes : GuestPixelFormats.BytesPerElement(format);
description.Samples = samples;
description.TileMode = tile;
var metadataAddress = descriptor.MetadataAddress << 8;
if (!storage && type == GuestImageType.Color2D && samples == 1 && levels == 1 && tile == GuestTileMode.RenderTarget && descriptor.MetadataCompress &&
metadataAddress != 0 && metadataAddress < TrackerLayout.SpaceBytes)
{
description.Metadata.Kind = MetadataKind.Dcc;
description.Metadata.Range = new GuestSpan(metadataAddress, 0);
}
if (samples > 1)
{
description.MipLayout[0] = new MipLevelLayout { Offset = 0, Size = size.Size, Pitch = pitch, Height = height };

View File

@ -44,6 +44,7 @@ internal interface IShaderPipelineHost
bool ExecGuardElisionEnabled => true;
ShaderPrewarmList? ShaderPrewarm => null;
bool PerVertexPixelInputsSupported => true;
bool ClipDistanceEnabled => false;
RenderHostLimits Limits { get; }

View File

@ -608,6 +608,7 @@ internal sealed class ShaderProgramCache
RequiredVertexOutputCount = options.RequiredVertexOutputCount,
VertexInputs = entry.VertexInputs,
PositionExportControl = info.PositionExportControl,
SupportsClipDistance = _host.ClipDistanceEnabled,
ClipSpace = new ShaderClipSpaceTransform(
info.ClipSpace.Enabled,
info.ClipSpace.ScaleX,

View File

@ -71,6 +71,7 @@ public static class VertexAttributeFormats
56 => (Format.R8G8B8A8Unorm, 4u),
50 => (Format.A2B10G10R10UnormPack32, 4u),
51 => (Format.A2B10G10R10SNormPack32, 4u),
36 => (Format.B10G11R11UfloatPack32, 3u),
29 => (Format.R16G16Sfloat, 2u),
28 => (Format.R16G16Sint, 2u),
27 => (Format.R16G16Uint, 2u),

View File

@ -742,6 +742,7 @@ internal static unsafe partial class VulkanVideoPresenter
_supportsIndependentBlend = supportedFeatures.IndependentBlend;
_supportsDepthBiasClamp = supportedFeatures.DepthBiasClamp;
_supportsDepthBounds = supportedFeatures.DepthBounds;
_supportsShaderClipDistance = supportedFeatures.ShaderClipDistance;
_supportsFillRectangle = IsDeviceExtensionAvailable(FillRectangleExtensionName);
var enabledFeatures = new PhysicalDeviceFeatures
{
@ -751,6 +752,8 @@ internal static unsafe partial class VulkanVideoPresenter
FragmentStoresAndAtomics = supportedFeatures.FragmentStoresAndAtomics,
ShaderInt64 = supportedFeatures.ShaderInt64,
ShaderFloat64 = supportedFeatures.ShaderFloat64,
ShaderClipDistance = supportedFeatures.ShaderClipDistance,
ShaderCullDistance = supportedFeatures.ShaderCullDistance,
ShaderImageGatherExtended = supportedFeatures.ShaderImageGatherExtended,
ShaderStorageImageExtendedFormats = supportedFeatures.ShaderStorageImageExtendedFormats,
ShaderStorageImageReadWithoutFormat = supportedFeatures.ShaderStorageImageReadWithoutFormat,

View File

@ -90,6 +90,7 @@ internal static unsafe partial class VulkanVideoPresenter
// NVIDIA's compiler rejects the elided-EXEC wave64 compute module with NVVM error 3.
bool IShaderPipelineHost.ExecGuardElisionEnabled => _physicalDeviceVendorId != NvidiaVendorId;
bool IShaderPipelineHost.PerVertexPixelInputsSupported => _supportsPerVertexPixelInputs;
bool IShaderPipelineHost.ClipDistanceEnabled => _supportsShaderClipDistance;
RenderHostLimits IShaderPipelineHost.Limits => _renderHostLimits;

View File

@ -126,6 +126,7 @@ internal static unsafe partial class VulkanVideoPresenter
private bool _supportsDepthClipControl;
private bool _supportsDepthClipEnable;
private bool _supportsDepthBounds;
private bool _supportsShaderClipDistance;
private bool _supportsFillRectangle;
private RenderHostLimits _renderHostLimits;
private IGuestBackedSpace _guestBacking = null!;
@ -602,7 +603,7 @@ internal static unsafe partial class VulkanVideoPresenter
_vk.CmdClearColorImage(command, image.Backing.Handle, ImageLayout.TransferDstOptimal, &clearValue, 1, &vkRange);
if (!tracked)
{
_bufferCache.FillBuffer(guestSlice, sliceSize, uint.MaxValue, false);
_bufferCache.FillDccMetadata(guestSlice, sliceSize, uint.MaxValue);
if (RenderTrace.Enabled && RenderTrace.MetadataClear())
{
RenderTrace.Write(

View File

@ -185,6 +185,7 @@ public static partial class Gen5SpirvTranslator
private uint _layerOutput;
private uint _viewportIndexOutput;
private uint _clipDistanceCount;
private uint _invalidPositionClipDistance = uint.MaxValue;
private uint _cullDistanceCount;
private uint _vertexIndexInput;
private uint _instanceIndexInput;
@ -6588,6 +6589,20 @@ public static partial class Gen5SpirvTranslator
outputValue,
Load(_vec4Type, outputVariable));
Store(outputVariable, outputValue);
if (export.Target == 12 && _invalidPositionClipDistance != uint.MaxValue)
{
var equal = _module.AddInstruction(
SpirvOp.FOrdEqual,
_module.TypeVector(_boolType, 4),
outputValue,
_module.ConstantNull(_vec4Type));
var invalid = _module.AddInstruction(SpirvOp.All, _boolType, equal);
// A zero position has an undefined perspective divide. Collapse its primitive
// to the remaining edge, as the guest's clipping-error cull does.
var distance = _module.AddInstruction(
SpirvOp.Select, _floatType, invalid, Float(-1f), Float(0f));
StoreDistanceConditional(_clipDistanceOutput, _invalidPositionClipDistance, distance);
}
return true;
}
@ -6696,6 +6711,14 @@ public static partial class Gen5SpirvTranslator
}
}
if (_request.SupportsClipDistance && clipCount + cullCount < 8 &&
_request.Program.Instructions.Any(static instruction =>
instruction.Control is Gen5ExportControl { Target: 12, EnableMask: not 0 }))
{
// Use a separate plane so auxiliary position exports keep their own distances.
_invalidPositionClipDistance = clipCount++;
}
if (needPointSize)
{
_pointSizeOutput = DeclareBuiltInOutput(

View File

@ -186,6 +186,7 @@ public sealed class ShaderCompileRequest
public int RequiredVertexOutputCount { get; init; }
public IReadOnlyList<ShaderVertexInput> VertexInputs { get; init; } = [];
public uint PositionExportControl { get; init; }
public bool SupportsClipDistance { get; init; } = true;
public ShaderClipSpaceTransform ClipSpace { get; init; }
// The LDS the dispatch allocates (COMPUTE_PGM_RSRC2.LDS_SIZE), 0 when unknown.

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

@ -73,6 +73,24 @@ public sealed class ImageRequestBuildersTests : IClassFixture<HeadlessVulkanFixt
Assert.Equal(ImageRole.Texture, comparison.Request.Role);
}
[Theory]
[InlineData(GuestImageType.Color2D, 1u, true)]
[InlineData(GuestImageType.Color3D, 32u, false)]
[InlineData(GuestImageType.Color2DArray, 4u, false)]
public void CompressedTexture_CarriesDccMetadataOnlyForSingleLayer2D(GuestImageType type, uint layers, bool expected)
{
const ulong metadata = 0x1_2000_0000;
var words = RegisterWords.Texture(Base, GuestPixelFormat.Bits8_8_8_8UNorm, 32, 32, type, GuestTileMode.RenderTarget, layers: layers);
words[6] |= (1u << 21) | (uint)(((metadata >> 8) & 0xFF) << 24);
words[7] = (uint)(metadata >> 16);
var shape = Sampled2D with { Volume = type == GuestImageType.Color3D, Arrayed = type == GuestImageType.Color2DArray };
var description = ImageRequestBuilders.Texture(words, shape).Request.Description;
Assert.Equal(expected ? MetadataKind.Dcc : MetadataKind.None, description.Metadata.Kind);
Assert.Equal(expected ? metadata : 0UL, description.Metadata.Range.Address);
}
private readonly HeadlessVulkan? _vulkan;
public ImageRequestBuildersTests(HeadlessVulkanFixture fixture) => _vulkan = fixture.Vulkan;

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