sharpemu/tests/SharpEmu.Libs.Tests/VideoOut/VulkanGuestImageCpuSyncPoli...

171 lines
5.9 KiB
C#

// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.Libs.VideoOut;
using Xunit;
namespace SharpEmu.Libs.Tests.VideoOut;
/// <summary>
/// Guards the two policy decisions that let a guest CPU rewrite reach a host
/// image that the GPU also renders into. Both used to be answered by proxies
/// that silently excluded real surfaces: a 1920x1080 arming cap (which left
/// every 4K target un-invalidated) and an IsCpuBacked latch (which flips false
/// the first time an address is used as a render target and never flips back).
/// The Vulkan device code around them cannot be unit-tested without a device,
/// so the decisions themselves are exercised here.
/// </summary>
public sealed class VulkanGuestImageCpuSyncPolicyTests
{
private const uint Rgba8Format = 10;
private const uint Rgba16FFormat = 11;
private const uint Rgba32FFormat = 14;
[Theory]
// 4K UI sheets are the whole point of the change: under the old
// resolution cap none of these were ever armed.
[InlineData(Rgba8Format, 3840u, 2160u)]
[InlineData(Rgba16FFormat, 3840u, 2160u)]
[InlineData(Rgba32FFormat, 3840u, 2160u)]
// Supersampled and unusual-aspect targets above 1080p in one axis only.
[InlineData(Rgba8Format, 2560u, 1440u)]
[InlineData(Rgba8Format, 1920u, 2160u)]
[InlineData(Rgba8Format, 3840u, 1080u)]
public void OversizedTargetsRemainEligibleForWriteTracking(
uint format,
uint width,
uint height)
{
var byteCount = VulkanVideoPresenter.GetGuestImageByteCount(
format,
width,
height,
depth: 1);
Assert.True(VulkanVideoPresenter.ShouldTrackGuestImageWrites(byteCount));
}
[Fact]
public void SubHdTargetsRemainEligibleForWriteTracking()
{
var byteCount = VulkanVideoPresenter.GetGuestImageByteCount(
Rgba8Format,
1280u,
720u,
depth: 1);
Assert.True(VulkanVideoPresenter.ShouldTrackGuestImageWrites(byteCount));
}
[Fact]
public void EmptyExtentIsNotTracked()
{
Assert.False(VulkanVideoPresenter.ShouldTrackGuestImageWrites(0));
}
[Fact]
public void ExtentBeyondTheReUploadBudgetIsNotTracked()
{
// Arming beyond the budget the flip/acquire sync path is willing to
// read back can only cost faults; it can never produce a re-upload.
Assert.True(
VulkanVideoPresenter.ShouldTrackGuestImageWrites(
VulkanVideoPresenter.MaxTrackedGuestImageBytes));
Assert.False(
VulkanVideoPresenter.ShouldTrackGuestImageWrites(
VulkanVideoPresenter.MaxTrackedGuestImageBytes + 1));
}
[Fact]
public void VolumeDepthCountsAgainstTheTrackingBudget()
{
// A resolution cap could not see volume depth at all. 512^3 RGBA8 is
// 512 MiB of backing memory behind a "512x512" surface.
var byteCount = VulkanVideoPresenter.GetGuestImageByteCount(
Rgba8Format,
512u,
512u,
depth: 512u);
Assert.False(VulkanVideoPresenter.ShouldTrackGuestImageWrites(byteCount));
}
[Fact]
public void CpuBackedImageAlwaysRefreshes()
{
Assert.True(
VulkanVideoPresenter.ShouldRefreshGuestImageFromCpu(
isCpuBacked: true,
textureWriteGeneration: -1,
hasUploadedGeneration: false,
uploadedGeneration: 0));
}
[Fact]
public void RenderTargetLatchNoLongerBlocksAnObservedCpuWrite()
{
// The surface was rendered into (IsCpuBacked latched false) and the
// guest CPU then rewrote its backing memory, so the parse thread
// shipped fresh texels carrying a newer generation than the upload.
Assert.True(
VulkanVideoPresenter.ShouldRefreshGuestImageFromCpu(
isCpuBacked: false,
textureWriteGeneration: 3,
hasUploadedGeneration: true,
uploadedGeneration: 2));
}
[Fact]
public void ObservedCpuWriteRefreshesEvenWithNoRecordedUpload()
{
// The render-target recreate/retain paths drop the recorded upload
// generation; a tracked CPU write must still win.
Assert.True(
VulkanVideoPresenter.ShouldRefreshGuestImageFromCpu(
isCpuBacked: false,
textureWriteGeneration: 1,
hasUploadedGeneration: false,
uploadedGeneration: 0));
}
[Fact]
public void GpuFeedbackSurfaceKeepsItsLiveImage()
{
// Tracked but never CPU-written: generation zero. Render-into-then-
// sample must not be overwritten with guest memory.
Assert.False(
VulkanVideoPresenter.ShouldRefreshGuestImageFromCpu(
isCpuBacked: false,
textureWriteGeneration: 0,
hasUploadedGeneration: false,
uploadedGeneration: 0));
}
[Fact]
public void UntrackedSurfaceKeepsItsLiveImage()
{
// -1 is the "no tracker generation" sentinel the parse thread ships
// when the range is not tracked (or tracking is disabled entirely,
// as on Windows).
Assert.False(
VulkanVideoPresenter.ShouldRefreshGuestImageFromCpu(
isCpuBacked: false,
textureWriteGeneration: -1,
hasUploadedGeneration: false,
uploadedGeneration: 0));
}
[Fact]
public void AlreadyUploadedGenerationDoesNotRefreshAgain()
{
// The upload recorded this exact generation, so the host image is
// current: re-uploading every draw would restage the whole surface.
Assert.False(
VulkanVideoPresenter.ShouldRefreshGuestImageFromCpu(
isCpuBacked: false,
textureWriteGeneration: 4,
hasUploadedGeneration: true,
uploadedGeneration: 4));
}
}