using System; using System.Collections.Generic; using AcDream.App.Rendering.Gpu; using AcDream.App.Rendering.Gpu.Vk; using Silk.NET.Vulkan; namespace AcDream.App.Tests.Rendering.Gpu.Vk; /// /// Campaign V slice V6a — the allocator's arithmetic (plan §4.2/§4.3). /// /// Every decision the Vulkan allocator makes is reachable from here: where a /// suballocation lands inside a block, whether releasing one lets the next reuse /// its bytes, when a request gets a block of its own, and which heap a residency /// class picks. That is the whole reason the free list and the pool carry no /// Vulkan handles — an allocator's real failure modes are arithmetic, and /// arithmetic does not need a GPU to be wrong. /// public sealed class VulkanMemoryModelTests { // ── free list ──────────────────────────────────────────────────────────── [Fact] public void FreeList_AllocatesFromTheFrontAndTracksUse() { var block = new VulkanMemoryBlockFreeList(1024); Assert.True(block.TryAllocate(256, 1, out ulong first)); Assert.True(block.TryAllocate(256, 1, out ulong second)); Assert.Equal(0ul, first); Assert.Equal(256ul, second); Assert.Equal(512ul, block.UsedBytes); Assert.Equal(512ul, block.FreeBytes); } [Fact] public void FreeList_HonoursAlignmentAndChargesThePaddingToTheAllocation() { var block = new VulkanMemoryBlockFreeList(1024); Assert.True(block.TryAllocate(1, 1, out _)); Assert.True(block.TryAllocate(16, 256, out ulong aligned)); Assert.Equal(256ul, aligned); // 1 byte of payload, then 255 bytes of padding plus 16 of payload. Assert.Equal(272ul, block.UsedBytes); } [Fact] public void FreeList_ReleasingAnAlignedAllocationReturnsItsPaddingToo() { var block = new VulkanMemoryBlockFreeList(1024); Assert.True(block.TryAllocate(1, 1, out _)); Assert.True(block.TryAllocate(16, 256, out ulong aligned)); block.Free(aligned, 16); // Only the leading 1-byte allocation is still out. Assert.Equal(1ul, block.UsedBytes); Assert.Equal(1023ul, block.FreeBytes); } [Fact] public void FreeList_CoalescesNeighboursSoTheBlockDoesNotFragmentAway() { var block = new VulkanMemoryBlockFreeList(1024); Assert.True(block.TryAllocate(256, 1, out ulong a)); Assert.True(block.TryAllocate(256, 1, out ulong b)); Assert.True(block.TryAllocate(256, 1, out ulong c)); block.Free(a, 256); block.Free(c, 256); // Two ranges, not three: releasing c already merged with the block's // untouched tail. Assert.Equal(2, block.FreeRangeCount); block.Free(b, 256); // Everything is back and it is ONE range, not three: a block that // coalesced badly would still report the right free byte count while // being unable to satisfy a large allocation ever again. Assert.Equal(1, block.FreeRangeCount); Assert.Equal(1024ul, block.LargestFreeBytes); Assert.Equal(0ul, block.UsedBytes); } [Fact] public void FreeList_RefusesAnAllocationLargerThanTheLargestHole() { var block = new VulkanMemoryBlockFreeList(1024); Assert.True(block.TryAllocate(600, 1, out _)); Assert.False(block.TryAllocate(600, 1, out _)); Assert.True(block.TryAllocate(400, 1, out _)); } [Fact] public void FreeList_DoubleReleaseIsRejectedRatherThanCorruptingTheAccounting() { var block = new VulkanMemoryBlockFreeList(1024); Assert.True(block.TryAllocate(256, 1, out ulong offset)); block.Free(offset, 256); Assert.Throws(() => block.Free(offset, 256)); } // ── pool policy ────────────────────────────────────────────────────────── [Fact] public void Pool_AsksForANewBlockBeforeItHasOne() { var pool = new VulkanMemoryTypePool(memoryTypeIndex: 0, blockSizeBytes: 4096, dedicatedThresholdBytes: 2048); Assert.False(pool.TryAllocate(64, 1, out _)); int index = pool.AddBlock(pool.BlockCapacityFor(64), dedicated: false); Assert.Equal(0, index); Assert.True(pool.TryAllocate(64, 1, out VulkanMemoryRange range)); Assert.Equal(0, range.BlockIndex); Assert.False(range.IsDedicated); } [Fact] public void Pool_GivesALargeRequestAWholeBlockOfItsOwn() { var pool = new VulkanMemoryTypePool(memoryTypeIndex: 0, blockSizeBytes: 4096, dedicatedThresholdBytes: 2048); int shared = pool.AddBlock(4096, dedicated: false); Assert.Equal(0, shared); // 3000 >= the 2048 threshold, so it must not be placed in the shared // block even though the shared block would fit it — taking that space // would strand the remaining kilobyte. Assert.False(pool.TryAllocate(3000, 1, out _)); Assert.Equal(3000ul, pool.BlockCapacityFor(3000)); int dedicated = pool.AddBlock(3000, dedicated: true); VulkanMemoryRange range = pool.AllocateWholeBlock(dedicated, 3000); Assert.True(range.IsDedicated); Assert.Equal(0ul, range.OffsetBytes); Assert.Equal(2, pool.LiveBlockCount); } [Fact] public void Pool_FreeingADedicatedRangeRetiresItsBlockButKeepsSharedOnes() { var pool = new VulkanMemoryTypePool(memoryTypeIndex: 0, blockSizeBytes: 4096, dedicatedThresholdBytes: 2048); int shared = pool.AddBlock(4096, dedicated: false); Assert.True(pool.TryAllocate(64, 1, out VulkanMemoryRange small)); int dedicatedBlock = pool.AddBlock(3000, dedicated: true); VulkanMemoryRange large = pool.AllocateWholeBlock(dedicatedBlock, 3000); Assert.True(pool.Free(large)); // dedicated block is now free device memory Assert.False(pool.Free(small)); // shared block is kept for reuse Assert.Equal(0, shared); Assert.Equal(1, pool.LiveBlockCount); } [Fact] public void Pool_ReusesASharedBlockAfterItsAllocationsAreReleased() { var pool = new VulkanMemoryTypePool(memoryTypeIndex: 3, blockSizeBytes: 1024, dedicatedThresholdBytes: 4096); pool.AddBlock(1024, dedicated: false); Assert.True(pool.TryAllocate(1024, 1, out VulkanMemoryRange whole)); Assert.False(pool.TryAllocate(16, 1, out _)); pool.Free(whole); Assert.True(pool.TryAllocate(16, 1, out VulkanMemoryRange reused)); Assert.Equal(0, reused.BlockIndex); Assert.Equal(1, pool.LiveBlockCount); } // ── heap selection ─────────────────────────────────────────────────────── private static readonly MemoryPropertyFlags DeviceLocal = MemoryPropertyFlags.DeviceLocalBit; private static readonly MemoryPropertyFlags HostCoherent = MemoryPropertyFlags.HostVisibleBit | MemoryPropertyFlags.HostCoherentBit; private static readonly MemoryPropertyFlags ReBar = MemoryPropertyFlags.DeviceLocalBit | MemoryPropertyFlags.HostVisibleBit | MemoryPropertyFlags.HostCoherentBit; private static readonly MemoryPropertyFlags HostCached = MemoryPropertyFlags.HostVisibleBit | MemoryPropertyFlags.HostCoherentBit | MemoryPropertyFlags.HostCachedBit; [Fact] public void Selection_HostWritablePrefersResizableBarOverPlainHostMemory() { List types = [DeviceLocal, HostCoherent, ReBar]; uint? chosen = VulkanMemoryTypeSelection.Choose(types, 0b111, GpuMemoryResidency.HostWritable); // Index 2 is the device-local AND host-visible type: writing per-frame // data straight into memory the GPU reads is the campaign's CPU win. Assert.Equal(2u, chosen); } [Fact] public void Selection_HostWritableFallsBackWhenNoResizableBarTypeExists() { List types = [DeviceLocal, HostCoherent]; Assert.Equal(1u, VulkanMemoryTypeSelection.Choose(types, 0b11, GpuMemoryResidency.HostWritable)); } [Fact] public void Selection_RespectsTheResourcesAllowedTypeMask() { List types = [ReBar, HostCoherent]; // Bit 0 is masked out even though it is the preferred type. Assert.Equal(1u, VulkanMemoryTypeSelection.Choose(types, 0b10, GpuMemoryResidency.HostWritable)); } [Fact] public void Selection_DeviceLocalPrefersDeviceLocalButAcceptsAnythingRatherThanFailing() { List deviceLocalPresent = [HostCoherent, DeviceLocal]; Assert.Equal(1u, VulkanMemoryTypeSelection.Choose(deviceLocalPresent, 0b11, GpuMemoryResidency.DeviceLocal)); List unifiedMemory = [HostCoherent]; Assert.Equal(0u, VulkanMemoryTypeSelection.Choose(unifiedMemory, 0b1, GpuMemoryResidency.DeviceLocal)); } [Fact] public void Selection_HostReadablePrefersCachedMemoryForTheReadbackItExistsFor() { List types = [HostCoherent, HostCached]; Assert.Equal(1u, VulkanMemoryTypeSelection.Choose(types, 0b11, GpuMemoryResidency.HostReadable)); } [Fact] public void Selection_AnswersNullWhenNothingIsUsableRatherThanGuessing() { List types = [DeviceLocal]; Assert.Null(VulkanMemoryTypeSelection.Choose(types, 0b1, GpuMemoryResidency.HostWritable)); } [Fact] public void Selection_NamesWhichResidenciesMustBeMappable() { Assert.False(VulkanMemoryTypeSelection.RequiresMapping(GpuMemoryResidency.DeviceLocal)); Assert.True(VulkanMemoryTypeSelection.RequiresMapping(GpuMemoryResidency.HostWritable)); Assert.True(VulkanMemoryTypeSelection.RequiresMapping(GpuMemoryResidency.HostReadable)); } }