namespace Ghost.RenderGraph.Concept; /// /// Represents a physical memory allocation that can be shared by multiple transient resources /// internal class PhysicalResourceAllocation { public int AllocationId { get; } public ulong SizeInBytes { get; } public ulong OffsetInBytes { get; } public string DebugName { get; } public List AliasedResources { get; } = new(); public PhysicalResourceAllocation(int allocationId, ulong sizeInBytes, ulong offsetInBytes, string debugName) { AllocationId = allocationId; SizeInBytes = sizeInBytes; OffsetInBytes = offsetInBytes; DebugName = debugName; } } /// /// Manages memory allocation and aliasing for transient resources /// internal class ResourceAllocator { private readonly List _allocations = new(); private int _allocationIdCounter = 0; public IReadOnlyList Allocations => _allocations; /// /// Allocate physical memory for resources, enabling aliasing where possible /// public void AllocateResources( IReadOnlyList resourceLifetimes, List passes) { Console.WriteLine("\n[RG] ===== RESOURCE ALIASING ANALYSIS ====="); // Separate imported and transient resources // Sort by SIZE FIRST (descending), then by FIRST USE (ascending) // This allows smaller resources (A, B) to alias into larger resources' (C) space // Example: C=10MB[1..2], A=4MB[0..1], B=6MB[0..1] → Allocate C first, then A and B alias into C's space var transientResources = resourceLifetimes .Where(lt => !lt.Handle.IsImported && lt.FirstUse != int.MaxValue) .OrderByDescending(lt => GetResourceSize(lt.Handle)) .ThenBy(lt => lt.FirstUse) .ToList(); if (!transientResources.Any()) { Console.WriteLine("No transient resources to allocate."); return; } // Track which allocation slots are occupied at each pass var allocationSlots = new List(); foreach (var resource in transientResources) { var size = GetResourceSize(resource.Handle); var alignment = GetResourceAlignment(resource.Handle); // Find an existing allocation slot that: // 1. Is large enough // 2. Has no lifetime overlap // 3. Matches resource type (texture/buffer) AllocationSlot? reuseSlot = null; foreach (var slot in allocationSlots) { if (CanAlias(slot, resource, size, alignment)) { reuseSlot = slot; break; } } if (reuseSlot != null) { // Reuse existing allocation - find offset within the allocation ulong offsetInAllocation = reuseSlot.FindFreeOffset(size, alignment, resource); reuseSlot.AddResource(resource, offsetInAllocation, size); Console.WriteLine($"[ALIAS] '{resource.Handle.Name}' aliases with '{reuseSlot.Allocation.DebugName}' " + $"(heap offset: {reuseSlot.Allocation.OffsetInBytes}, resource offset: {offsetInAllocation}, size: {size} bytes, " + $"lifetime: [{resource.FirstUse}..{resource.LastUse}])"); } else { // Create new allocation // Calculate heap offset (simulated - in real D3D12MA this would be the actual heap offset) ulong heapOffset = allocationSlots.Count > 0 ? allocationSlots.Max(s => s.Allocation.OffsetInBytes + s.Allocation.SizeInBytes) : 0; var allocation = new PhysicalResourceAllocation( _allocationIdCounter++, size, offsetInBytes: heapOffset, $"Physical_{resource.Handle.Type}_{_allocationIdCounter}"); var newSlot = new AllocationSlot(allocation, resource.Handle.Type); newSlot.AddResource(resource, 0, size); // Offset 0 within this new allocation allocationSlots.Add(newSlot); Console.WriteLine($"[ALLOC] '{resource.Handle.Name}' gets new allocation '{allocation.DebugName}' " + $"(heap offset: {heapOffset}, size: {size} bytes, lifetime: [{resource.FirstUse}..{resource.LastUse}])"); } } _allocations.AddRange(allocationSlots.Select(s => s.Allocation)); // Print summary Console.WriteLine($"\n[RG] Memory Statistics:"); var totalWithoutAliasing = transientResources.Sum(r => (long)GetResourceSize(r.Handle)); var totalWithAliasing = _allocations.Sum(a => (long)a.SizeInBytes); var savedMemory = totalWithoutAliasing - totalWithAliasing; var savingPercentage = totalWithoutAliasing > 0 ? (savedMemory * 100.0 / totalWithoutAliasing) : 0; Console.WriteLine($" Total memory without aliasing: {FormatBytes(totalWithoutAliasing)}"); Console.WriteLine($" Total memory with aliasing: {FormatBytes(totalWithAliasing)}"); Console.WriteLine($" Memory saved: {FormatBytes(savedMemory)} ({savingPercentage:F1}%)"); Console.WriteLine($" Allocations: {_allocations.Count} physical allocations for {transientResources.Count} resources"); } private bool CanAlias(AllocationSlot slot, ResourceLifetime resource, ulong requiredSize, ulong requiredAlignment) { // Must be same resource type if (slot.ResourceType != resource.Handle.Type) return false; // Must be large enough if (slot.Allocation.SizeInBytes < requiredSize) return false; // Check for lifetime overlap with any resource in this slot foreach (var existingResource in slot.Resources) { if (LifetimesOverlap(existingResource, resource)) return false; } return true; } private bool LifetimesOverlap(ResourceLifetime a, ResourceLifetime b) { // Two resources overlap if their lifetimes intersect return !(a.LastUse < b.FirstUse || b.LastUse < a.FirstUse); } private ulong GetResourceSize(RenderGraphResourceHandle handle) { return handle switch { RenderGraphTextureHandle texture => CalculateTextureSize(texture.Descriptor), RenderGraphBufferHandle buffer => (ulong)buffer.Descriptor.SizeInBytes, _ => 0 }; } private ulong GetResourceAlignment(RenderGraphResourceHandle handle) { // In a real implementation, this would query D3D12_RESOURCE_ALLOCATION_INFO return handle switch { RenderGraphTextureHandle => 65536, // 64KB texture alignment (typical) RenderGraphBufferHandle => 256, // 256 byte buffer alignment _ => 256 }; } private ulong CalculateTextureSize(TextureDescriptor desc) { // Simplified size calculation var bytesPerPixel = desc.Format switch { TextureFormat.RGBA8 => 4, TextureFormat.RGBA16F => 8, TextureFormat.RGBA32F => 16, TextureFormat.Depth32F => 4, TextureFormat.R32Uint => 4, _ => 4 }; return (ulong)(desc.Width * desc.Height * bytesPerPixel); } private string FormatBytes(long bytes) { if (bytes < 1024) return $"{bytes} B"; if (bytes < 1024 * 1024) return $"{bytes / 1024.0:F2} KB"; return $"{bytes / (1024.0 * 1024.0):F2} MB"; } public PhysicalResourceAllocation? GetAllocation(RenderGraphResourceHandle handle) { return _allocations.FirstOrDefault(a => a.AliasedResources.Any(r => r.Id == handle.Id)); } private class AllocationSlot { public PhysicalResourceAllocation Allocation { get; } public ResourceType ResourceType { get; } public List Resources { get; } = new(); // Track occupied regions within this allocation: (offset, size, lifetime) private readonly List<(ulong Offset, ulong Size, ResourceLifetime Resource)> _occupiedRegions = new(); public AllocationSlot(PhysicalResourceAllocation allocation, ResourceType resourceType) { Allocation = allocation; ResourceType = resourceType; } /// /// Find a free offset within this allocation that can fit the required size /// and doesn't conflict with active resources /// public ulong FindFreeOffset(ulong requiredSize, ulong alignment, ResourceLifetime newResource) { // If no resources yet, return 0 if (_occupiedRegions.Count == 0) { return 0; } // Sort regions by offset var sortedRegions = _occupiedRegions.OrderBy(r => r.Offset).ToList(); // Try to fit at the beginning (offset 0) ulong candidateOffset = 0; bool fitsAtStart = true; foreach (var region in sortedRegions) { // Check if this region overlaps with our candidate position if (region.Offset < requiredSize) { // Check lifetime - if no overlap, we can still use this space if (LifetimesOverlap(region.Resource, newResource)) { fitsAtStart = false; break; } } } if (fitsAtStart) { return AlignUp(0, alignment); } // Try gaps between regions for (int i = 0; i < sortedRegions.Count; i++) { var current = sortedRegions[i]; // Skip if current region's lifetime overlaps with new resource if (LifetimesOverlap(current.Resource, newResource)) { continue; } // Try placing after this region candidateOffset = AlignUp(current.Offset + current.Size, alignment); // Check if it fits before the next region (or end of allocation) ulong nextRegionStart = (i + 1 < sortedRegions.Count) ? sortedRegions[i + 1].Offset : Allocation.SizeInBytes; if (candidateOffset + requiredSize <= nextRegionStart) { // Check no lifetime conflicts with any regions in this range bool hasConflict = false; for (int j = i + 1; j < sortedRegions.Count; j++) { var other = sortedRegions[j]; if (other.Offset < candidateOffset + requiredSize) { if (LifetimesOverlap(other.Resource, newResource)) { hasConflict = true; break; } } } if (!hasConflict) { return candidateOffset; } } } // Try placing at the end if (sortedRegions.Count > 0) { var last = sortedRegions[^1]; if (!LifetimesOverlap(last.Resource, newResource)) { candidateOffset = AlignUp(last.Offset + last.Size, alignment); if (candidateOffset + requiredSize <= Allocation.SizeInBytes) { return candidateOffset; } } } // No space found - caller should create new allocation return 0; } private bool LifetimesOverlap(ResourceLifetime a, ResourceLifetime b) { return !(a.LastUse < b.FirstUse || b.LastUse < a.FirstUse); } private ulong AlignUp(ulong value, ulong alignment) { return (value + alignment - 1) / alignment * alignment; } public void AddResource(ResourceLifetime resource, ulong offsetInAllocation, ulong size) { Resources.Add(resource); _occupiedRegions.Add((offsetInAllocation, size, resource)); Allocation.AliasedResources.Add(resource.Handle); } } }