sharpemu/tests/SharpEmu.Libs.Tests/Gpu/Rendering/RenderExecutorAdaptationTes...

245 lines
10 KiB
C#

// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.Libs.Gpu.GpuCommands.Registers;
using SharpEmu.Libs.Gpu.Images;
using SharpEmu.Libs.Gpu.Rendering;
using SharpEmu.Libs.Tests.Gpu.Images;
using SharpEmu.Libs.Tests.Gpu.Scheduling;
using Xunit;
using static SharpEmu.Libs.Tests.Gpu.Rendering.RenderExecutorFixtures;
namespace SharpEmu.Libs.Tests.Gpu.Rendering;
// The draw paths kept beside the executor: solid clears, metadata-only quads, requested clears, retained draws.
[Collection(SchedulingStateCollection.Name)]
public sealed class RenderExecutorAdaptationTests : IDisposable
{
private const uint FastClearBit = 1u << 13;
private const uint DccEnabledBit = 1u << 28;
private readonly RecordingRenderHost _host = new();
private readonly FakePipelineProvider _pipelines = new();
private readonly RenderExecutor _executor;
private readonly FatalScope _fatal = new();
public RenderExecutorAdaptationTests() => _executor = new RenderExecutor(_host, _pipelines);
public void Dispose() => _fatal.Dispose();
private static BlendRegisters PremultipliedFill() => new() { Enable = true, ColorSourceFactor = 1, ColorDestinationFactor = 5, ColorFunction = 0 };
private void WriteQuad(float left, float top, float right, float bottom)
{
var bytes = new byte[4 * 12];
Span<(float X, float Y)> corners = [(left, top), (right, top), (left, bottom), (right, bottom)];
for (var index = 0; index < 4; index++)
{
BitConverter.TryWriteBytes(bytes.AsSpan(index * 12), corners[index].X);
BitConverter.TryWriteBytes(bytes.AsSpan((index * 12) + 4), corners[index].Y);
}
_host.WriteGuest(VertexBase, bytes);
}
[Fact]
public void UnavailablePrograms_SkipTheDrawAfterTheTargetsAreBound()
{
_pipelines.Graphics = new GraphicsPrograms { Available = false };
_executor.DrawAuto(1, Banks(), Auto(3));
Assert.Contains("bind_target 1", _host.Calls);
Assert.Contains("reset_bindings", _host.Calls);
Assert.DoesNotContain(_host.Calls, call => call.StartsWith("draw", StringComparison.Ordinal));
Assert.DoesNotContain(_host.Calls, call => call.StartsWith("begin_rendering", StringComparison.Ordinal));
Assert.Empty(_pipelines.PipelineRequests);
}
[Fact]
public void SolidClearPrograms_ClearTheBoundTargetsInsteadOfDrawing()
{
_pipelines.Graphics = Programs(solidClear: new SolidColorClear(0.25f, 0.5f, 0.75f, 1f));
var banks = Banks();
banks.Context.ColorTargets[1] = RegisterWords.Color(SecondColorBase, 64, 64);
banks.Context.RenderTargetMask = 0xFF;
_executor.DrawAuto(1, banks, Auto(3));
Assert.Contains("clear_targets 0,1 0.25,0.5,0.75,1", _host.Calls);
Assert.DoesNotContain(_host.Calls, call => call.StartsWith("draw", StringComparison.Ordinal));
Assert.DoesNotContain(_host.Calls, call => call.StartsWith("acquire_color", StringComparison.Ordinal));
Assert.Contains("reset_bindings", _host.Calls);
}
[Fact]
public void SolidClearPrograms_WithoutColorTargets_DrawNormally()
{
_pipelines.Graphics = Programs(solidClear: new SolidColorClear(0f, 0f, 0f, 1f));
var banks = Banks(withDepth: true);
banks.Context.RenderTargetMask = 0;
banks.Context.ColorTargets[0] = default;
_executor.DrawAuto(1, banks, Auto(3));
Assert.DoesNotContain(_host.Calls, call => call.StartsWith("clear_targets", StringComparison.Ordinal));
Assert.Contains(_host.Calls, call => call.StartsWith("draw 3", StringComparison.Ordinal));
}
[Fact]
public void FastClearEnabled_DoesNotRequestAClearForEitherTargetAcrossDraws()
{
var banks = Banks();
var first = banks.Context.ColorTargets[0];
banks.Context.ColorTargets[0] = first with { Info = first.Info | FastClearBit };
banks.Context.ColorTargets[1] = RegisterWords.Color(SecondColorBase, 64, 64);
banks.Context.RenderTargetMask = 0xFF;
_executor.DrawAuto(1, banks, Auto(3));
_host.EndRendering();
_executor.DrawAuto(2, banks, Auto(3));
Assert.DoesNotContain(_host.Calls, call => call.StartsWith("request_clear", StringComparison.Ordinal));
Assert.DoesNotContain(_host.Calls, call => call.StartsWith("clear_targets", StringComparison.Ordinal));
Assert.Contains(_host.Calls, call => call.StartsWith("draw 3", StringComparison.Ordinal));
Assert.Equal(2, _host.BegunRenderings.Count);
Assert.All(_host.BegunRenderings, rendering =>
{
Assert.False(rendering.ColorAttachments[0].IsClear);
Assert.False(rendering.ColorAttachments[1].IsClear);
});
}
[Fact]
public void TargetWithoutTheFastClearBit_RequestsNoClear()
{
_executor.DrawAuto(1, Banks(), Auto(3));
Assert.DoesNotContain(_host.Calls, call => call.StartsWith("request_clear", StringComparison.Ordinal));
}
private RegisterBanks MetadataQuadBanks()
{
var banks = Banks(PrimitiveTriangleStrip);
var words = banks.Context.ColorTargets[0];
banks.Context.ColorTargets[0] = words with { Info = words.Info | DccEnabledBit };
banks.Context.BlendControls[0] = PremultipliedFill();
_pipelines.Graphics = Programs(positionStream: new VertexPositionStream(VertexBase, 12, 0));
WriteQuad(-1f, -1f, 1f, 1f);
return banks;
}
[Fact]
public void MetadataClearQuad_IsDroppedWithoutRecording()
{
var banks = MetadataQuadBanks();
_executor.DrawAuto(1, banks, Auto(4));
Assert.DoesNotContain(_host.Calls, call => call.StartsWith("draw", StringComparison.Ordinal));
Assert.DoesNotContain(_host.Calls, call => call.StartsWith("prepare_bindings", StringComparison.Ordinal));
Assert.Contains("reset_bindings", _host.Calls);
Assert.Equal(4, _host.GuestReads);
}
[Fact]
public void MetadataClearQuad_ThatDoesNotCoverClipSpace_Draws()
{
var banks = MetadataQuadBanks();
WriteQuad(-1f, -1f, 0.5f, 1f);
_executor.DrawAuto(1, banks, Auto(4));
Assert.Contains(_host.Calls, call => call.StartsWith("draw 4", StringComparison.Ordinal));
}
[Fact]
public void MetadataClearQuad_WithANonZeroClearWord_Draws()
{
var banks = MetadataQuadBanks();
banks.Context.ColorClearWord1[0] = 1;
_executor.DrawAuto(1, banks, Auto(4));
Assert.Contains(_host.Calls, call => call.StartsWith("draw 4", StringComparison.Ordinal));
}
[Fact]
public void MetadataClearQuad_WithAnOrdinaryBlend_Draws()
{
var banks = MetadataQuadBanks();
banks.Context.BlendControls[0] = new BlendRegisters { Enable = true, ColorSourceFactor = 4, ColorDestinationFactor = 5 };
_executor.DrawAuto(1, banks, Auto(4));
Assert.Contains(_host.Calls, call => call.StartsWith("draw 4", StringComparison.Ordinal));
}
[Fact]
public void MetadataClearQuad_WithAnImageInThePixelProgram_Draws()
{
var banks = MetadataQuadBanks();
_pipelines.Graphics = Programs(
pixelStage: Stage(Program(ShaderStageKind.Pixel, images: [new ImageResourceInfo(ImageResourceClass.Sampled, false)])),
positionStream: new VertexPositionStream(VertexBase, 12, 0));
_executor.DrawAuto(1, banks, Auto(4));
Assert.Contains(_host.Calls, call => call.StartsWith("draw 4", StringComparison.Ordinal));
}
private RegisterBanks TargetlessBanks()
{
var banks = Banks();
banks.Context.RenderTargetMask = 0;
banks.Context.ColorTargets[0] = default;
banks.Context.ShaderInterface.DepthShaderControl = new DepthShaderControlRegisters { KillEnable = true };
return banks;
}
[Fact]
public void TargetlessDrawWithImages_IsRetainedWhenTheHostKeepsIt()
{
_host.RetainTargetlessDraws = true;
_pipelines.Graphics = Programs(pixelStage: Stage(Program(ShaderStageKind.Pixel, images: [new ImageResourceInfo(ImageResourceClass.Sampled, false)])));
_executor.DrawIndexed(9, TargetlessBanks(), Indexed(6));
Assert.Contains("retain_targetless 9", _host.Calls);
Assert.DoesNotContain(_host.Calls, call => call.StartsWith("draw", StringComparison.Ordinal));
var (_, _, arguments) = Assert.Single(_host.RetainedDraws);
Assert.True(arguments.Indexed);
Assert.Equal(6u, arguments.Indexed_.IndexCount);
}
[Fact]
public void TargetlessDrawWithImages_DrawsWhenTheHostDeclines()
{
_pipelines.Graphics = Programs(pixelStage: Stage(Program(ShaderStageKind.Pixel, images: [new ImageResourceInfo(ImageResourceClass.Sampled, false)])));
_executor.DrawAuto(1, TargetlessBanks(), Auto(3));
Assert.Contains("retain_targetless 1", _host.Calls);
Assert.Contains(_host.Calls, call => call.StartsWith("draw 3", StringComparison.Ordinal));
}
[Fact]
public void TargetlessDrawThatWritesAStorageImage_IsNeverRetained()
{
_host.RetainTargetlessDraws = true;
_pipelines.Graphics = Programs(pixelStage: Stage(Program(ShaderStageKind.Pixel, images: [new ImageResourceInfo(ImageResourceClass.Storage, true)])));
_executor.DrawAuto(1, TargetlessBanks(), Auto(3));
Assert.DoesNotContain(_host.Calls, call => call.StartsWith("retain_targetless", StringComparison.Ordinal));
Assert.Contains(_host.Calls, call => call.StartsWith("draw 3", StringComparison.Ordinal));
}
[Fact]
public void ConsumedComputeProgram_RecordsNothing()
{
_pipelines.Compute = new ComputeProgram { Consumed = true };
_executor.Dispatch(1, Banks(), 4, 1, 1, 0x1);
Assert.DoesNotContain(_host.Calls, call => call.StartsWith("dispatch", StringComparison.Ordinal));
Assert.DoesNotContain(_host.Calls, call => call.StartsWith("prepare_bindings", StringComparison.Ordinal));
}
[Fact]
public void UnavailableComputeProgram_SkipsTheDispatch()
{
_pipelines.Compute = new ComputeProgram { Available = false };
_executor.Dispatch(1, Banks(), 4, 1, 1, 0x1);
Assert.DoesNotContain(_host.Calls, call => call.StartsWith("dispatch", StringComparison.Ordinal));
Assert.DoesNotContain(_pipelines.Calls, call => call.StartsWith("create_compute", StringComparison.Ordinal));
}
}