VisuTwin Canvas
C++ 3D Engine — Metal Backend
Loading...
Searching...
No Matches
metalTextureStream.cpp
Go to the documentation of this file.
1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2025-2026 Arnis Lektauers
3//
4// Triple-buffered texture streaming for real-time data ingestion.
5//
7
8#include <cassert>
9#include <string>
10#include <spdlog/spdlog.h>
11
12namespace visutwin::canvas
13{
14 MetalTextureStream::MetalTextureStream(MTL::Device* device, const Descriptor& desc)
15 : _device(device), _desc(desc)
16 {
17 assert(device && "MetalTextureStream requires a valid MTL::Device");
18 assert(desc.width > 0 && desc.height > 0 && "Texture dimensions must be positive");
19
20 // Create texture descriptor for all 3 slots.
21 // StorageModeShared: Apple Silicon unified memory, no blit needed.
22 // WriteCombined: bypasses CPU cache for write-only streaming pattern.
23 auto* texDesc = MTL::TextureDescriptor::texture2DDescriptor(
24 desc.format, desc.width, desc.height, /*mipmapped=*/false);
25
26 texDesc->setStorageMode(MTL::StorageModeShared);
27 texDesc->setUsage(MTL::TextureUsageShaderRead);
28
29 if (desc.writeCombined) {
30 texDesc->setCpuCacheMode(MTL::CPUCacheModeWriteCombined);
31 }
32
33 for (int i = 0; i < kNumSlots; ++i) {
34 _textures[i] = device->newTexture(texDesc);
35 if (_textures[i]) {
36 auto labelStr = std::string(desc.label) + "_" + std::to_string(i);
37 _textures[i]->setLabel(
38 NS::String::string(labelStr.c_str(), NS::UTF8StringEncoding));
39 }
40 }
41
42 texDesc->release();
43
44 // Semaphore with initial count = kNumSlots: all 3 slots are initially free.
45 _slotSemaphore = dispatch_semaphore_create(kNumSlots);
46
47 spdlog::info("[MetalTextureStream] Created: {}x{} {} ({} slots, {:.1f} MB total)",
48 desc.width, desc.height,
49 desc.format == MTL::PixelFormatBGRA8Unorm ? "BGRA8" :
50 desc.format == MTL::PixelFormatRGBA8Unorm ? "RGBA8" :
51 desc.format == MTL::PixelFormatRGBA16Float ? "RGBA16F" :
52 desc.format == MTL::PixelFormatR32Float ? "R32F" : "other",
54 static_cast<double>(kNumSlots * desc.width * desc.height * 4) / (1024.0 * 1024.0));
55 }
56
58 {
59 for (auto& tex : _textures) {
60 if (tex) {
61 tex->release();
62 tex = nullptr;
63 }
64 }
65
66 spdlog::info("[MetalTextureStream] Destroyed: {} frames published, {} dropped",
67 _publishCount.load(std::memory_order_relaxed),
68 _dropCount.load(std::memory_order_relaxed));
69 }
70
71 // ── Producer API ───────────────────────────────────────────────────
72
74 {
75 // Block if all 3 slots are in-flight (GPU back-pressure).
76 // This matches MetalUniformRingBuffer::beginFrame().
77 dispatch_semaphore_wait(_slotSemaphore, DISPATCH_TIME_FOREVER);
78
79 const int idx = _writeIndex.load(std::memory_order_acquire);
80 return _textures[idx];
81 }
82
83 void MetalTextureStream::writeRegion(const void* data, size_t bytesPerRow,
84 MTL::Region region, uint32_t mipLevel)
85 {
86 assert(data && "writeRegion: data must not be null");
87
88 const int idx = _writeIndex.load(std::memory_order_acquire);
89 _textures[idx]->replaceRegion(region, mipLevel, 0, data, bytesPerRow, 0);
90 }
91
93 {
94 // Atomically swap write and ready slots.
95 // If the consumer hasn't picked up the previous ready frame, it gets
96 // overwritten ("latest-frame-wins" policy). Track that as a drop.
97 const int oldWrite = _writeIndex.load(std::memory_order_acquire);
98 const int oldReady = _readyIndex.load(std::memory_order_acquire);
99
100 _readyIndex.store(oldWrite, std::memory_order_release);
101 _writeIndex.store(oldReady, std::memory_order_release);
102
103 _publishCount.fetch_add(1, std::memory_order_relaxed);
104 _hasPublished = true;
105 }
106
107 void MetalTextureStream::publishExternal(MTL::Texture* externalTexture)
108 {
109 assert(externalTexture && "publishExternal: texture must not be null");
110 _externalReady.store(externalTexture, std::memory_order_release);
111 _publishCount.fetch_add(1, std::memory_order_relaxed);
112 _hasPublished = true;
113 }
114
115 // ── Consumer API ───────────────────────────────────────────────────
116
118 {
119 // Check for externally-published texture first (CVMetalTexture path).
120 auto* ext = _externalReady.exchange(nullptr, std::memory_order_acq_rel);
121 if (ext) {
122 return ext;
123 }
124
125 if (!_hasPublished) {
126 return nullptr;
127 }
128
129 // Atomically swap ready and read slots.
130 const int oldReady = _readyIndex.load(std::memory_order_acquire);
131 const int oldRead = _readIndex.load(std::memory_order_acquire);
132
133 _readIndex.store(oldReady, std::memory_order_release);
134 _readyIndex.store(oldRead, std::memory_order_release);
135
136 return _textures[_readIndex.load(std::memory_order_acquire)];
137 }
138
139 void MetalTextureStream::endFrame(MTL::CommandBuffer* commandBuffer)
140 {
141 assert(commandBuffer && "endFrame: commandBuffer must not be null");
142
143 // Register completion handler: when GPU finishes this frame's command
144 // buffer, signal the semaphore to release one slot for the producer.
145 // Same pattern as MetalUniformRingBuffer::endFrame().
146 dispatch_semaphore_t sem = _slotSemaphore;
147 commandBuffer->addCompletedHandler(^(MTL::CommandBuffer*) {
148 dispatch_semaphore_signal(sem);
149 });
150 }
151
152} // namespace visutwin::canvas
MetalTextureStream(MTL::Device *device, const Descriptor &desc)
void endFrame(MTL::CommandBuffer *commandBuffer)
void publishExternal(MTL::Texture *externalTexture)
void writeRegion(const void *data, size_t bytesPerRow, MTL::Region region, uint32_t mipLevel=0)
bool writeCombined
CPU write-only optimization (bypass cache).