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