VisuTwin Canvas
C++ 3D Engine — Metal Backend
Loading...
Searching...
No Matches
metalPaletteRingBuffer.h
Go to the documentation of this file.
1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2025-2026 Arnis Lektauers
3//
4// Created by Arnis Lektauers on 07.03.2026.
5//
6// Variable-size triple-buffered ring buffer for dynamic batch matrix palettes.
7//
8// Unlike MetalUniformRingBuffer (fixed-slot-size, 256B-aligned), this uses
9// bump allocation to handle variable-size palette data efficiently:
10// - 10 instances → 640B + padding = 768B
11// - 33 instances → 2.1KB + padding = 2.25KB
12// - 330 instances → 21KB + padding = 21.25KB
13//
14// Same triple-buffered semaphore pattern as MetalUniformRingBuffer:
15// 1. beginFrame() — wait for GPU, advance to next 256KB region
16// 2. allocate(data, size) — bump-allocate, memcpy data, return offset
17// 3. encoder->setVertexBufferOffset(offset, 6) — cheap offset-only bind
18// 4. endFrame(commandBuffer) — register GPU completion signal
19//
20#pragma once
21
22#include <Metal/Metal.hpp>
23#include <dispatch/dispatch.h>
24#include <cassert>
25#include <cstring>
26
27#include "spdlog/spdlog.h"
28
29namespace visutwin::canvas
30{
32 {
33 public:
34 static constexpr int kMaxInflightFrames = 3;
35 static constexpr size_t kAlignment = 256; // Metal constant buffer offset alignment
36 static constexpr size_t kRegionSize = 256 * 1024; // 256KB per frame region
37 // 256KB = 4096 instances × 64 bytes (float4x4). This is the total budget
38 // across ALL dynamic batches rendered in a single frame.
39
40 MetalPaletteRingBuffer(MTL::Device* device, const char* label = "PaletteRing")
41 {
42 _totalSize = kMaxInflightFrames * kRegionSize;
43 _buffer = device->newBuffer(_totalSize, MTL::ResourceStorageModeShared);
44 if (_buffer) {
45 _buffer->setLabel(NS::String::string(label, NS::UTF8StringEncoding));
46 _basePtr = static_cast<uint8_t*>(_buffer->contents());
47 }
48 _frameSemaphore = dispatch_semaphore_create(kMaxInflightFrames);
49 }
50
52 {
53 if (_buffer) {
54 _buffer->release();
55 _buffer = nullptr;
56 }
57 }
58
59 // Non-copyable, non-movable
64
70 {
71 dispatch_semaphore_wait(_frameSemaphore, DISPATCH_TIME_FOREVER);
72 _frameIndex = (_frameIndex + 1) % kMaxInflightFrames;
73 _writeOffset = 0;
74 }
75
85 [[nodiscard]] size_t allocate(const void* data, size_t size)
86 {
87 assert(data != nullptr);
88 assert(size > 0);
89
90 const size_t alignedSize = alignUp(size, kAlignment);
91 if (_writeOffset + alignedSize > kRegionSize) {
92 spdlog::warn("PaletteRingBuffer: frame allocation exceeded {}KB budget "
93 "(requested {}B at offset {})", kRegionSize / 1024, size, _writeOffset);
94 return SIZE_MAX;
95 }
96
97 const size_t absoluteOffset = static_cast<size_t>(_frameIndex) * kRegionSize + _writeOffset;
98 std::memcpy(_basePtr + absoluteOffset, data, size);
99 _writeOffset += alignedSize;
100 return absoluteOffset;
101 }
102
107 void endFrame(MTL::CommandBuffer* commandBuffer)
108 {
109 assert(commandBuffer != nullptr);
110 dispatch_semaphore_t sem = _frameSemaphore;
111 commandBuffer->addCompletedHandler(^(MTL::CommandBuffer*) {
112 dispatch_semaphore_signal(sem);
113 });
114 }
115
116 [[nodiscard]] MTL::Buffer* buffer() const { return _buffer; }
117 [[nodiscard]] size_t writeOffset() const { return _writeOffset; }
118 [[nodiscard]] size_t totalSize() const { return _totalSize; }
119
120 private:
121 static size_t alignUp(size_t value, size_t alignment)
122 {
123 return (value + alignment - 1) & ~(alignment - 1);
124 }
125
126 MTL::Buffer* _buffer = nullptr;
127 uint8_t* _basePtr = nullptr;
128 dispatch_semaphore_t _frameSemaphore = nullptr;
129
130 size_t _totalSize = 0;
131 int _frameIndex = -1; // Will become 0 on first beginFrame()
132 size_t _writeOffset = 0; // Bump pointer within current frame region
133 };
134}
MetalPaletteRingBuffer(const MetalPaletteRingBuffer &)=delete
MetalPaletteRingBuffer(MetalPaletteRingBuffer &&)=delete
MetalPaletteRingBuffer(MTL::Device *device, const char *label="PaletteRing")
MetalPaletteRingBuffer & operator=(const MetalPaletteRingBuffer &)=delete
void endFrame(MTL::CommandBuffer *commandBuffer)
MetalPaletteRingBuffer & operator=(MetalPaletteRingBuffer &&)=delete
size_t allocate(const void *data, size_t size)