Compare commits
2 Commits
1016_part3
...
xnetcat/ho
| Author | SHA1 | Date |
|---|---|---|
|
|
c284720e39 | |
|
|
ad92a314ae |
|
|
@ -435,7 +435,12 @@ public sealed partial class GpuCommandInterpreter
|
|||
throw _host.Fatal($"The occlusion-counter dump is invalid: index=0x{eventIndex:X8} address=0x{eventAddress:X16}.");
|
||||
}
|
||||
|
||||
// Until real sample queries are implemented, conservatively report visibility.
|
||||
if (_host.TryRecordOcclusionCounterDump(eventAddress))
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
// Without host sample queries, conservatively report visibility.
|
||||
// A one-sample delta can disappear when the guest scales or reduces the counts.
|
||||
const ulong visibleCounterStep = 0x2FFFFFF;
|
||||
const ulong readyBit = 1UL << 63;
|
||||
|
|
|
|||
|
|
@ -105,6 +105,10 @@ public interface ICommandStreamHost
|
|||
|
||||
void FillBuffer(ulong address, ulong size, uint value, bool isGds);
|
||||
|
||||
// Records a PIXEL_PIPE_STAT_DUMP occlusion counter dump with real sample counts; false when
|
||||
// the host cannot count samples and the interpreter must publish a conservative result.
|
||||
bool TryRecordOcclusionCounterDump(ulong address) => false;
|
||||
|
||||
void CopyBuffer(ulong destination, ulong source, ulong size, bool destinationIsGds, bool sourceIsGds);
|
||||
|
||||
// A satisfied wait packet or a command-processor memory write: CPU writes released by the
|
||||
|
|
|
|||
|
|
@ -0,0 +1,43 @@
|
|||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace SharpEmu.Libs.Gpu.Rendering;
|
||||
|
||||
// The 256-byte block a guest occlusion query reads: per depth block, a begin counter at
|
||||
// 16 * db and an end counter at 16 * db + 8, each valid once bit 63 is set. The guest sums
|
||||
// end - begin over the depth blocks, so host segment k is published as depth block k's count.
|
||||
internal static class OcclusionResultBlock
|
||||
{
|
||||
public const int Bytes = 256;
|
||||
public const int DepthBlocks = 16;
|
||||
public const ulong ReadyBit = 1UL << 63;
|
||||
|
||||
// Reported when the samples could not be counted, so the guest keeps the object visible.
|
||||
public const ulong ConservativeCount = 0x00FF_FFFF;
|
||||
|
||||
public static byte[] Build(int segmentCount, bool conservative, ReadOnlySpan<ulong> counts)
|
||||
{
|
||||
if (segmentCount is < 0 or > DepthBlocks || (!conservative && counts.Length < segmentCount))
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(segmentCount));
|
||||
}
|
||||
|
||||
var block = new byte[Bytes];
|
||||
var slots = MemoryMarshal.Cast<byte, ulong>(block.AsSpan());
|
||||
slots.Fill(ReadyBit);
|
||||
if (conservative)
|
||||
{
|
||||
slots[1] = ReadyBit | ConservativeCount;
|
||||
return block;
|
||||
}
|
||||
|
||||
for (var segment = 0; segment < segmentCount; segment++)
|
||||
{
|
||||
slots[segment * 2 + 1] = ReadyBit | (counts[segment] & (ReadyBit - 1));
|
||||
}
|
||||
|
||||
return block;
|
||||
}
|
||||
}
|
||||
|
|
@ -308,6 +308,7 @@ internal static unsafe partial class VulkanVideoPresenter
|
|||
{
|
||||
using var relayScope = RenderPhaseProfile.MeasureDetail(RenderPhaseProfile.Phase.QueueRelay);
|
||||
_relay.RunPendingCommands();
|
||||
DrainOcclusionWrites();
|
||||
}
|
||||
|
||||
public void BeginSubmission(int queueId, ulong submissionId, object? geometrySnapshots)
|
||||
|
|
|
|||
|
|
@ -841,7 +841,9 @@ internal static unsafe partial class VulkanVideoPresenter
|
|||
DepthBiasClamp = supportedFeatures.DepthBiasClamp,
|
||||
SampleRateShading = supportedFeatures.SampleRateShading,
|
||||
SamplerAnisotropy = supportedFeatures.SamplerAnisotropy,
|
||||
OcclusionQueryPrecise = supportedFeatures.OcclusionQueryPrecise,
|
||||
};
|
||||
_supportsPreciseOcclusion = supportedFeatures.OcclusionQueryPrecise;
|
||||
|
||||
if (!supportedFeatures.SampleRateShading)
|
||||
{
|
||||
|
|
@ -997,13 +999,19 @@ internal static unsafe partial class VulkanVideoPresenter
|
|||
SType = StructureType.PhysicalDeviceShaderAtomicInt64Features,
|
||||
PNext = &addressFeatures,
|
||||
};
|
||||
var hostQueryResetFeatures = new PhysicalDeviceHostQueryResetFeatures
|
||||
{
|
||||
SType = StructureType.PhysicalDeviceHostQueryResetFeatures,
|
||||
PNext = &atomicInt64Features,
|
||||
};
|
||||
var featuresQuery = new PhysicalDeviceFeatures2
|
||||
{
|
||||
SType = StructureType.PhysicalDeviceFeatures2,
|
||||
PNext = &atomicInt64Features,
|
||||
PNext = &hostQueryResetFeatures,
|
||||
};
|
||||
_vk.GetPhysicalDeviceFeatures2(_physicalDevice, &featuresQuery);
|
||||
var supportsTimelineSemaphore = timelineSemaphoreFeatures.TimelineSemaphore;
|
||||
_supportsHostQueryReset = hostQueryResetFeatures.HostQueryReset;
|
||||
var supportsBufferDeviceAddress = addressFeatures.BufferDeviceAddress;
|
||||
var supportsSharedInt64Atomics = atomicInt64Features.ShaderSharedInt64Atomics;
|
||||
var supportsMaintenance8 = maintenance8Features.Maintenance8;
|
||||
|
|
@ -1152,6 +1160,16 @@ internal static unsafe partial class VulkanVideoPresenter
|
|||
PNext = &timelineSemaphoreFeatures,
|
||||
};
|
||||
void* renderingChain = &addressFeatures;
|
||||
if (_supportsHostQueryReset)
|
||||
{
|
||||
hostQueryResetFeatures = new PhysicalDeviceHostQueryResetFeatures
|
||||
{
|
||||
SType = StructureType.PhysicalDeviceHostQueryResetFeatures,
|
||||
HostQueryReset = true,
|
||||
PNext = renderingChain,
|
||||
};
|
||||
renderingChain = &hostQueryResetFeatures;
|
||||
}
|
||||
if (supportsSharedInt64Atomics)
|
||||
{
|
||||
atomicInt64Features = new PhysicalDeviceShaderAtomicInt64Features
|
||||
|
|
|
|||
|
|
@ -59,6 +59,7 @@ internal static unsafe partial class VulkanVideoPresenter
|
|||
if (_device.Handle != 0)
|
||||
{
|
||||
_scheduler.Dispose();
|
||||
DestroyOcclusionPool();
|
||||
_deviceInfo.Slabs.Destroy();
|
||||
DestroyGuestPipelineCaches();
|
||||
if (_pipelineCache.Handle != 0)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,272 @@
|
|||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Runtime.InteropServices;
|
||||
using SharpEmu.Libs.Gpu.Buffers;
|
||||
using SharpEmu.Libs.Gpu.Rendering;
|
||||
using SharpEmu.Libs.Gpu.Scheduling;
|
||||
using SharpEmu.Libs.Gpu.Vulkan;
|
||||
using Silk.NET.Vulkan;
|
||||
|
||||
namespace SharpEmu.Libs.VideoOut;
|
||||
|
||||
internal static unsafe partial class VulkanVideoPresenter
|
||||
{
|
||||
private sealed partial class Presenter
|
||||
{
|
||||
// Guest occlusion queries are a pair of PIXEL_PIPE_STAT_DUMP events into one 256-byte block:
|
||||
// the begin dump writes each depth block's sample counter to block + 16 * db, the end dump to
|
||||
// block + 8 + 16 * db, and the guest sums end - begin over the depth blocks once every slot
|
||||
// carries the ready bit. The host counts the draws in between with Vulkan occlusion queries.
|
||||
// A query is split at every rendering scope boundary (Vulkan queries cannot span scopes), and
|
||||
// segment k is published as depth block k's count, so the guest's own sum adds the segments.
|
||||
// Publishing is recorded after the scope that ended the query, so it never splits a scope.
|
||||
private const int OcclusionBlockBytes = OcclusionResultBlock.Bytes;
|
||||
private const int OcclusionDepthBlocks = OcclusionResultBlock.DepthBlocks;
|
||||
private const uint OcclusionPoolSize = 4096;
|
||||
|
||||
private bool _supportsPreciseOcclusion;
|
||||
private bool _supportsHostQueryReset;
|
||||
private QueryPool _occlusionPool;
|
||||
private readonly Stack<uint> _freeOcclusionQueries = new();
|
||||
private OcclusionQuery? _openOcclusion;
|
||||
private uint? _activeOcclusionSegment;
|
||||
private readonly List<OcclusionQuery> _pendingOcclusionResults = new();
|
||||
|
||||
private sealed class OcclusionQuery(ulong block)
|
||||
{
|
||||
public ulong Block { get; } = block;
|
||||
|
||||
public List<uint> Segments { get; } = new();
|
||||
|
||||
// More scopes than depth blocks, or no free query: publish a visible result.
|
||||
public bool Conservative { get; set; }
|
||||
}
|
||||
|
||||
// Opt-in while an intermittent false "occluded" result is open: in Silent Hill: The Short
|
||||
// Message the title-menu character is sometimes culled for seconds at a time. Without it the
|
||||
// interpreter keeps publishing a conservative visible result.
|
||||
private static readonly bool HostOcclusionQueries =
|
||||
Environment.GetEnvironmentVariable("SHARPEMU_HOST_OCCLUSION_QUERIES") == "1";
|
||||
|
||||
// False when the device cannot count samples; the interpreter then publishes a visible result.
|
||||
public bool TryRecordOcclusionCounterDump(ulong address)
|
||||
{
|
||||
if (!HostOcclusionQueries || !_supportsPreciseOcclusion || !_supportsHostQueryReset || (address & 7) != 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (address & 0xF)
|
||||
{
|
||||
case 0:
|
||||
BeginOcclusionQuery(address);
|
||||
return true;
|
||||
case 8:
|
||||
EndOcclusionQuery(address - 8);
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private void EnsureOcclusionPool()
|
||||
{
|
||||
if (_occlusionPool.Handle != 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var create = new QueryPoolCreateInfo
|
||||
{
|
||||
SType = StructureType.QueryPoolCreateInfo,
|
||||
QueryType = QueryType.Occlusion,
|
||||
QueryCount = OcclusionPoolSize,
|
||||
};
|
||||
Check(_vk.CreateQueryPool(_device, &create, null, out _occlusionPool), "vkCreateQueryPool(occlusion)");
|
||||
_vk.ResetQueryPool(_device, _occlusionPool, 0, OcclusionPoolSize);
|
||||
for (var index = OcclusionPoolSize; index > 0; index--)
|
||||
{
|
||||
_freeOcclusionQueries.Push(index - 1);
|
||||
}
|
||||
}
|
||||
|
||||
private void BeginOcclusionQuery(ulong block)
|
||||
{
|
||||
EnsureOcclusionPool();
|
||||
if (_openOcclusion is { } previous)
|
||||
{
|
||||
// A begin without an end: close the previous query so its slots still become ready.
|
||||
EndOcclusionQuery(previous.Block);
|
||||
}
|
||||
|
||||
_openOcclusion = new OcclusionQuery(block);
|
||||
if (_renderingActive)
|
||||
{
|
||||
BeginOcclusionSegment(new CommandBuffer(_scheduler.Current.Handle));
|
||||
}
|
||||
}
|
||||
|
||||
private void EndOcclusionQuery(ulong block)
|
||||
{
|
||||
EnsureOcclusionPool();
|
||||
var query = _openOcclusion;
|
||||
if (query is null || query.Block != block)
|
||||
{
|
||||
// An end without its begin: there is nothing to count, so report the block visible.
|
||||
query = new OcclusionQuery(block) { Conservative = true };
|
||||
}
|
||||
else
|
||||
{
|
||||
EndOcclusionSegment(new CommandBuffer(_scheduler.Current.Handle));
|
||||
_openOcclusion = null;
|
||||
}
|
||||
|
||||
_pendingOcclusionResults.Add(query);
|
||||
if (!_renderingActive)
|
||||
{
|
||||
PublishOcclusionResults(BeginBatchedGuestCommands());
|
||||
}
|
||||
}
|
||||
|
||||
// Called right after a rendering scope begins.
|
||||
private void BeginOcclusionSegment(CommandBuffer command)
|
||||
{
|
||||
if (_openOcclusion is not { } query || _activeOcclusionSegment is not null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (query.Segments.Count >= OcclusionDepthBlocks || !_freeOcclusionQueries.TryPop(out var index))
|
||||
{
|
||||
query.Conservative = true;
|
||||
return;
|
||||
}
|
||||
|
||||
query.Segments.Add(index);
|
||||
_vk.CmdBeginQuery(command, _occlusionPool, index, QueryControlFlags.PreciseBit);
|
||||
_activeOcclusionSegment = index;
|
||||
}
|
||||
|
||||
// Called right before a rendering scope ends.
|
||||
private void EndOcclusionSegment(CommandBuffer command)
|
||||
{
|
||||
if (_activeOcclusionSegment is not { } index)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_vk.CmdEndQuery(command, _occlusionPool, index);
|
||||
_activeOcclusionSegment = null;
|
||||
}
|
||||
|
||||
// Records the results of every ended query; must run outside a scope. The counts land in the
|
||||
// download ring, and once the GPU retires them the 256-byte blocks are written to guest
|
||||
// memory on this worker, the way a command-processor WRITE_DATA would, so the buffer cache
|
||||
// refreshes any GPU copy itself and the blocks never become GPU-owned memory.
|
||||
private void PublishOcclusionResults(CommandBuffer command)
|
||||
{
|
||||
if (_pendingOcclusionResults.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var queries = _pendingOcclusionResults.ToArray();
|
||||
_pendingOcclusionResults.Clear();
|
||||
var bytes = (ulong)queries.Length * OcclusionDepthBlocks * sizeof(ulong);
|
||||
var ring = _bufferCache.GetUtilityBuffer(GpuBufferUsage.Download);
|
||||
GpuBuffer download = ring;
|
||||
// This can run inside submission's EndRendering hook. A ring wrap must not
|
||||
// recursively submit that command buffer, nor leave us recording into a
|
||||
// submitted handle. Overflow storage survives until its completion callback.
|
||||
var overflow = !ring.TryMap(bytes, out var offset, sizeof(ulong), allowWait: false);
|
||||
if (overflow)
|
||||
{
|
||||
download = new GpuBuffer(_deviceInfo, _scheduler, GpuBufferUsage.Download,
|
||||
0, BufferUsageFlags.TransferDstBit, bytes);
|
||||
offset = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
ring.Commit();
|
||||
}
|
||||
for (var index = 0; index < queries.Length; index++)
|
||||
{
|
||||
var segments = queries[index].Segments;
|
||||
for (var segment = 0; segment < segments.Count; segment++)
|
||||
{
|
||||
_vk.CmdCopyQueryPoolResults(command, _occlusionPool, segments[segment], 1, download.Handle,
|
||||
offset + ((ulong)index * OcclusionDepthBlocks + (ulong)segment) * sizeof(ulong), sizeof(ulong),
|
||||
QueryResultFlags.Result64Bit | QueryResultFlags.ResultWaitBit);
|
||||
}
|
||||
}
|
||||
|
||||
var barrier = new MemoryBarrier2
|
||||
{
|
||||
SType = StructureType.MemoryBarrier2,
|
||||
SrcAccessMask = AccessFlags2.TransferWriteBit,
|
||||
DstAccessMask = AccessFlags2.HostReadBit,
|
||||
};
|
||||
VulkanSynchronization.PipelineBarrier(_vk, command, PipelineStageFlags.TransferBit, PipelineStageFlags.HostBit, 0, 1, &barrier, 0, null, 0, null);
|
||||
foreach (var query in queries)
|
||||
{
|
||||
foreach (var segment in query.Segments)
|
||||
{
|
||||
// Query commands execute in order, so the reset follows the copy above.
|
||||
_vk.CmdResetQueryPool(command, _occlusionPool, segment, 1);
|
||||
_freeOcclusionQueries.Push(segment);
|
||||
}
|
||||
}
|
||||
|
||||
_scheduler.QueuePriorityCompletionAction(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
download.Invalidate(offset, bytes);
|
||||
var counts = MemoryMarshal.Cast<byte, ulong>(download.Mapped.Slice((int)offset, (int)bytes));
|
||||
for (var index = 0; index < queries.Length; index++)
|
||||
{
|
||||
_occlusionWrites.Enqueue((queries[index].Block, BuildOcclusionBlock(queries[index],
|
||||
counts.Slice(index * OcclusionDepthBlocks, OcclusionDepthBlocks))));
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (overflow) download.Dispose();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static byte[] BuildOcclusionBlock(OcclusionQuery query, ReadOnlySpan<ulong> counts) =>
|
||||
OcclusionResultBlock.Build(query.Segments.Count, query.Conservative, counts);
|
||||
|
||||
private readonly System.Collections.Concurrent.ConcurrentQueue<(ulong Block, byte[] Data)> _occlusionWrites = new();
|
||||
|
||||
// Runs on the command worker before every packet.
|
||||
private void DrainOcclusionWrites()
|
||||
{
|
||||
while (_occlusionWrites.TryDequeue(out var write))
|
||||
{
|
||||
if (!_guestMemory.TryWrite(write.Block, write.Data))
|
||||
{
|
||||
throw SubmissionScheduler.Fatal($"The occlusion result cannot be written: block=0x{write.Block:X16}.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void DestroyOcclusionPool()
|
||||
{
|
||||
if (_occlusionPool.Handle != 0)
|
||||
{
|
||||
_vk.DestroyQueryPool(_device, _occlusionPool, null);
|
||||
_occlusionPool = default;
|
||||
}
|
||||
|
||||
_freeOcclusionQueries.Clear();
|
||||
_pendingOcclusionResults.Clear();
|
||||
_occlusionWrites.Clear();
|
||||
_openOcclusion = null;
|
||||
_activeOcclusionSegment = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -869,6 +869,7 @@ internal static unsafe partial class VulkanVideoPresenter
|
|||
_renderingScopesBegun++;
|
||||
_renderingActive = true;
|
||||
_renderingState = state;
|
||||
BeginOcclusionSegment(command);
|
||||
}
|
||||
|
||||
public void EndRendering()
|
||||
|
|
@ -881,6 +882,7 @@ internal static unsafe partial class VulkanVideoPresenter
|
|||
_renderingActive = false;
|
||||
_renderingState = default;
|
||||
var command = new CommandBuffer(_scheduler.Current.Handle);
|
||||
EndOcclusionSegment(command);
|
||||
_vk.CmdEndRendering(command);
|
||||
foreach (var (sourceStages, destinationStages, barriers) in _barriersAfterRendering)
|
||||
{
|
||||
|
|
@ -893,6 +895,7 @@ internal static unsafe partial class VulkanVideoPresenter
|
|||
}
|
||||
|
||||
_barriersAfterRendering.Clear();
|
||||
PublishOcclusionResults(command);
|
||||
if (_globalBarrierAfterRendering)
|
||||
{
|
||||
_globalBarrierAfterRendering = false;
|
||||
|
|
|
|||
|
|
@ -29,6 +29,20 @@ internal sealed class RecordingCommandStreamHost : ICommandStreamHost
|
|||
|
||||
public List<string> Calls { get; } = new();
|
||||
|
||||
// When set, the host counts occlusion samples itself and records each dump address here.
|
||||
public List<ulong>? OcclusionDumps { get; set; }
|
||||
|
||||
public bool TryRecordOcclusionCounterDump(ulong address)
|
||||
{
|
||||
if (OcclusionDumps is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
OcclusionDumps.Add(address);
|
||||
return true;
|
||||
}
|
||||
|
||||
public List<EndOfPipeWrite> EndOfPipeWrites { get; } = new();
|
||||
|
||||
// Satisfied waits and command-processor memory writes reported to the host.
|
||||
|
|
|
|||
|
|
@ -337,6 +337,20 @@ public sealed class GpuCommandInterpreterLabelTests
|
|||
Assert.Equal((1UL << 63) | 0x2FFFFFFUL, runner.Host.ReadQword(Label));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EventWrite_OcclusionDumpUsesHostSampleCountsWhenAvailable()
|
||||
{
|
||||
var runner = new StreamRunner();
|
||||
runner.Host.OcclusionDumps = new List<ulong>();
|
||||
|
||||
runner.Run(StreamRunner.Packet(PacketOpcode.EventWrite, 0x39 | (1u << 8), StreamRunner.Low(Label), StreamRunner.High(Label)));
|
||||
runner.Run(StreamRunner.Packet(PacketOpcode.EventWrite, 0x39 | (1u << 8), StreamRunner.Low(Label + 8), StreamRunner.High(Label + 8)));
|
||||
|
||||
Assert.Equal(new[] { Label, Label + 8 }, runner.Host.OcclusionDumps);
|
||||
Assert.Empty(runner.Host.Calls);
|
||||
Assert.Equal(0UL, runner.Host.ReadQword(Label));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FlipMarkers_PrepareRecordAndFlush()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -0,0 +1,58 @@
|
|||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Buffers.Binary;
|
||||
using SharpEmu.Libs.Gpu.Rendering;
|
||||
using Xunit;
|
||||
|
||||
namespace SharpEmu.Libs.Tests.Gpu.Rendering;
|
||||
|
||||
public sealed class OcclusionResultBlockTests
|
||||
{
|
||||
private static ulong Slot(byte[] block, int depthBlock, bool end) =>
|
||||
BinaryPrimitives.ReadUInt64LittleEndian(block.AsSpan(depthBlock * 16 + (end ? 8 : 0)));
|
||||
|
||||
// What the guest computes: the sum of end - begin over every ready depth block.
|
||||
private static ulong GuestSum(byte[] block)
|
||||
{
|
||||
ulong sum = 0;
|
||||
for (var depthBlock = 0; depthBlock < OcclusionResultBlock.DepthBlocks; depthBlock++)
|
||||
{
|
||||
var begin = Slot(block, depthBlock, end: false);
|
||||
var end = Slot(block, depthBlock, end: true);
|
||||
Assert.NotEqual(0UL, begin & OcclusionResultBlock.ReadyBit);
|
||||
Assert.NotEqual(0UL, end & OcclusionResultBlock.ReadyBit);
|
||||
sum += (end & ~OcclusionResultBlock.ReadyBit) - (begin & ~OcclusionResultBlock.ReadyBit);
|
||||
}
|
||||
|
||||
return sum;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SegmentsAddUpThroughTheGuestSum()
|
||||
{
|
||||
var block = OcclusionResultBlock.Build(3, false, [10, 0, 32]);
|
||||
|
||||
Assert.Equal(42UL, GuestSum(block));
|
||||
Assert.Equal(OcclusionResultBlock.ReadyBit | 10, Slot(block, 0, end: true));
|
||||
Assert.Equal(OcclusionResultBlock.ReadyBit | 32, Slot(block, 2, end: true));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AQueryWithoutDrawsReportsZeroSamples()
|
||||
{
|
||||
Assert.Equal(0UL, GuestSum(OcclusionResultBlock.Build(0, false, [])));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AnUncountedQueryStaysVisible()
|
||||
{
|
||||
Assert.Equal(OcclusionResultBlock.ConservativeCount, GuestSum(OcclusionResultBlock.Build(0, true, [])));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MoreSegmentsThanDepthBlocksAreRejected()
|
||||
{
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => OcclusionResultBlock.Build(17, false, new ulong[17]));
|
||||
}
|
||||
}
|
||||
|
|
@ -79,6 +79,7 @@ internal sealed class PresenterUnderTest : IDisposable
|
|||
"_deferredGuestImageVersionDestroys",
|
||||
"_pipelineEntries", "_shaderModules", "_shaderModuleSpirvBytes",
|
||||
"_preparedTextures", "_barriersAfterRendering", "_feedbackSnapshotPool",
|
||||
"_occlusionWrites", "_pendingOcclusionResults", "_freeOcclusionQueries",
|
||||
})
|
||||
{
|
||||
var field = PresenterType.GetField(name, InstanceMembers)!;
|
||||
|
|
|
|||
|
|
@ -40,6 +40,38 @@ public sealed unsafe partial class RenderHostDeviceTests : IClassFixture<Headles
|
|||
private static bool DefersGlobalBarriers =>
|
||||
(bool)PresenterUnderTest.PresenterType.GetField("DeferGlobalBarriers", System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.NonPublic)!.GetValue(null)!;
|
||||
|
||||
[Theory]
|
||||
[InlineData(false)]
|
||||
[InlineData(true)]
|
||||
public void OcclusionReadbackDoesNotSubmitWhileEndingRendering(bool exhaustRing)
|
||||
{
|
||||
if (!Ready()) return;
|
||||
using var presenter = new PresenterUnderTest(_vulkan!);
|
||||
var address = presenter.Harness.MapBacked(0x10000, ReadWrite);
|
||||
presenter.Run(() =>
|
||||
{
|
||||
var ring = presenter.Harness.Cache.GetUtilityBuffer(GpuBufferUsage.Download);
|
||||
if (exhaustRing)
|
||||
{
|
||||
Assert.True(ring.TryMap(ring.Size, out _));
|
||||
ring.Commit();
|
||||
}
|
||||
var queryType = PresenterUnderTest.PresenterType.GetNestedType("OcclusionQuery",
|
||||
System.Reflection.BindingFlags.NonPublic)!;
|
||||
var query = Activator.CreateInstance(queryType, [address])!;
|
||||
queryType.GetProperty("Conservative")!.SetValue(query, true);
|
||||
presenter.GetField<System.Collections.IList>("_pendingOcclusionResults").Add(query);
|
||||
var tick = presenter.Harness.Scheduler.CurrentTick;
|
||||
presenter.InvokeMethod("PublishOcclusionResults", presenter.Command);
|
||||
Assert.Equal(tick, presenter.Harness.Scheduler.CurrentTick);
|
||||
presenter.Harness.Scheduler.Finish();
|
||||
presenter.Harness.Scheduler.WaitForAllPriorityOperations();
|
||||
((ICommandStreamHost)presenter.Instance).RunPendingCommands();
|
||||
});
|
||||
Assert.Equal(OcclusionResultBlock.Build(0, true, []), presenter.Harness.Read(address, 256));
|
||||
presenter.Harness.Shutdown();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GlobalBarrierEndsDynamicRendering()
|
||||
{
|
||||
|
|
|
|||
Loading…
Reference in New Issue