VisuTwin Canvas
C++ 3D Engine — Metal Backend
Loading...
Searching...
No Matches
metalTexture.cpp
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 08.08.2025.
5//
6
7#include "metalTexture.h"
8
9#include <algorithm>
10#include <vector>
11#include <spdlog/spdlog.h>
12#include "metalGraphicsDevice.h"
13
15{
16 namespace
17 {
18 MTL::PixelFormat pixelFormatToMetal(const PixelFormat format)
19 {
20 switch (format) {
22 return MTL::PixelFormatRGBA8Unorm;
24 return MTL::PixelFormatRGBA8Unorm;
26 return MTL::PixelFormatRGBA16Float;
28 return MTL::PixelFormatRGBA32Float;
30 return MTL::PixelFormatR32Float;
32 return MTL::PixelFormatR8Unorm;
34 return MTL::PixelFormatRG8Unorm;
36 return MTL::PixelFormatDepth16Unorm;
38 return MTL::PixelFormatDepth32Float;
40 return MTL::PixelFormatDepth32Float_Stencil8;
41 default:
42 return MTL::PixelFormatRGBA8Unorm;
43 }
44 }
45
46 uint32_t bytesPerPixel(const PixelFormat format)
47 {
48 switch (format) {
50 return 1;
52 return 2;
54 return 3;
56 return 4;
58 return 4;
60 return 8;
62 return 16;
63 default:
64 return 4;
65 }
66 }
67 }
68
69 MetalTexture::MetalTexture(Texture* texture) : _texture(texture)
70 {
71 }
72
74 {
75 if (_descriptor) {
76 _descriptor->release();
77 _descriptor = nullptr;
78 }
79 if (_metalTexture && _ownsTexture) {
80 _metalTexture->release();
81 _metalTexture = nullptr;
82 }
83 }
84
85 void MetalTexture::setExternalTexture(MTL::Texture* externalTexture)
86 {
87 if (_metalTexture && _ownsTexture) {
88 _metalTexture->release();
89 }
90 _metalTexture = externalTexture;
91 _ownsTexture = false;
92 }
93
95 {
96 assert(_texture->width() > 0 && _texture->height() > 0);
97
98 _descriptor = MTL::TextureDescriptor::alloc()->init();
99 _descriptor->setPixelFormat(pixelFormatToMetal(_texture->format()));
100 _descriptor->setWidth(_texture->width());
101 _descriptor->setHeight(_texture->height());
102 _descriptor->setMipmapLevelCount(std::max(1u, _texture->getNumLevels()));
103 MTL::TextureUsage usage = MTL::TextureUsageShaderRead | MTL::TextureUsageRenderTarget;
104 if (_texture->storage()) {
105 usage = usage | MTL::TextureUsageShaderWrite;
106 }
107 _descriptor->setUsage(usage);
108 _descriptor->setStorageMode(MTL::StorageModeShared);
109
110 if (_texture->isCubemap()) {
111 _descriptor->setTextureType(MTL::TextureTypeCube);
112 } else if (_texture->isVolume()) {
113 _descriptor->setTextureType(MTL::TextureType3D);
114 _descriptor->setDepth(_texture->depth());
115 }
116
117 _metalTexture = device->raw()->newTexture(_descriptor);
118 }
119
121 {
122 // If the CPU-side dimensions have changed (e.g. after Texture::resize()), the
123 // existing Metal texture object has stale dimensions. Recreate it so that render
124 // target color/depth buffers actually allocate GPU memory at the new size.
125 if (_metalTexture &&
126 (_metalTexture->width() != _texture->width() ||
127 _metalTexture->height() != _texture->height() ||
128 (_texture->isVolume() && _metalTexture->depth() != _texture->depth()))) {
129 auto* metalDevice = dynamic_cast<MetalGraphicsDevice*>(device);
130 if (metalDevice) {
131 if (_descriptor) { _descriptor->release(); _descriptor = nullptr; }
132 _metalTexture->release(); _metalTexture = nullptr;
133 create(metalDevice);
134 }
135 _texture->setNeedsUpload(false);
136 _texture->setNeedsMipmapsUpload(false);
137 return;
138 }
139
140 if (_texture->needsUpload() || _texture->needsMipmapsUpload())
141 {
142 uploadData(device);
143 _texture->setNeedsUpload(false);
144 _texture->setNeedsMipmapsUpload(false);
145 }
146 }
147
149 {
150 (void)device;
151 if (!_descriptor || !_metalTexture) {
152 spdlog::warn("Texture upload skipped: Metal texture is not initialized");
153 return;
154 }
155 if (!_texture->hasLevels())
156 {
157 return;
158 }
159
160 bool anyUploads = false;
161 bool anyLevelMissing = false;
162 uint32_t requiredMipLevels = _texture->getNumLevels();
163
164 for (uint32_t mipLevel = 0; mipLevel < requiredMipLevels; mipLevel++)
165 {
166 auto* mipObject = _texture->getLevel(mipLevel);
167
168 if (mipObject)
169 {
170 if (_texture->isCubemap())
171 {
172 // Handle cubemap faces
173 for (uint32_t face = 0; face < 6; face++)
174 {
175 auto* faceSource = _texture->getFaceData(mipLevel, face);
176 if (faceSource)
177 {
178 const auto sourceSize = _texture->getLevelDataSize(mipLevel, face);
179 uploadRawImage(faceSource, sourceSize, mipLevel, face);
180 anyUploads = true;
181 }
182 else
183 {
184 anyLevelMissing = true;
185 }
186 }
187 }
188 else if (_texture->isVolume())
189 {
190 const auto sourceSize = _texture->getLevelDataSize(mipLevel);
191 uploadVolumeData(mipObject, sourceSize, mipLevel);
192 anyUploads = true;
193 }
194 else if (_texture->isArray())
195 {
196 // Handle texture arrays
197 for (uint32_t index = 0; index < _texture->getArrayLength(); index++)
198 {
199 if (auto* arraySource = _texture->getArrayData(mipLevel, index))
200 {
201 const auto sourceSize = _texture->getLevelDataSize(mipLevel);
202 uploadRawImage(arraySource, sourceSize, mipLevel, index);
203 anyUploads = true;
204 }
205 }
206 }
207 else
208 {
209 // Handle 2D texture
210 const auto sourceSize = _texture->getLevelDataSize(mipLevel);
211 uploadRawImage(mipObject, sourceSize, mipLevel, 0);
212 anyUploads = true;
213 }
214 }
215 else
216 {
217 anyLevelMissing = true;
218 }
219 }
220 }
221
222 void MetalTexture::uploadRawImage(void* imageData, size_t imageDataSize, uint32_t mipLevel, uint32_t index) const
223 {
224 if (!imageData) {
225 return;
226 }
227 const auto descriptorMipLevels = std::max(1u, static_cast<uint32_t>(_descriptor->mipmapLevelCount()));
228 if (mipLevel >= descriptorMipLevels) {
229 spdlog::warn("Texture upload skipped: mip level {} out of descriptor range {}", mipLevel, descriptorMipLevels);
230 return;
231 }
232
233 const auto mipWidth = std::max(1u, static_cast<uint32_t>(_descriptor->width()) >> mipLevel);
234 const auto mipHeight = std::max(1u, static_cast<uint32_t>(_descriptor->height()) >> mipLevel);
235 const auto bpp = bytesPerPixel(_texture->format());
236 const auto expectedSize = static_cast<size_t>(mipWidth) * static_cast<size_t>(mipHeight) * bpp;
237 if (imageDataSize > 0 && imageDataSize < expectedSize) {
238 spdlog::warn("Texture upload skipped: level data size {} smaller than expected {}", imageDataSize, expectedSize);
239 return;
240 }
241
242 const MTL::Region region = MTL::Region(0, 0, 0, mipWidth, mipHeight, 1);
243 // For cubemaps, 'index' is the face/slice index (0-5)
244 const NS::UInteger slice = _texture->isCubemap() ? index : 0;
245
246 if (_texture->format() == PixelFormat::PIXELFORMAT_RGB8) {
247 // Metal does not expose a 24-bit RGB8 texture format for sampling.
248 // Expand source RGB data to RGBA when uploading into RGBA8 textures.
249 const size_t pixelCount = static_cast<size_t>(mipWidth) * static_cast<size_t>(mipHeight);
250 std::vector<uint8_t> expanded(pixelCount * 4u);
251 const auto* src = static_cast<const uint8_t*>(imageData);
252 for (size_t i = 0; i < pixelCount; ++i) {
253 const size_t srcOffset = i * 3u;
254 const size_t dstOffset = i * 4u;
255 expanded[dstOffset + 0u] = src[srcOffset + 0u];
256 expanded[dstOffset + 1u] = src[srcOffset + 1u];
257 expanded[dstOffset + 2u] = src[srcOffset + 2u];
258 expanded[dstOffset + 3u] = 255u;
259 }
260
261 const NS::UInteger bytesPerRow = 4u * mipWidth;
262 _metalTexture->replaceRegion(region, mipLevel, slice, expanded.data(), bytesPerRow, 0);
263 return;
264 }
265
266 const NS::UInteger bytesPerRow = bpp * mipWidth;
267 _metalTexture->replaceRegion(region, mipLevel, slice, imageData, bytesPerRow, 0);
268 }
269
270 void MetalTexture::uploadVolumeData(void* imageData, size_t imageDataSize, uint32_t mipLevel) const
271 {
272 if (!imageData) {
273 return;
274 }
275 const auto descriptorMipLevels = std::max(1u, static_cast<uint32_t>(_descriptor->mipmapLevelCount()));
276 if (mipLevel >= descriptorMipLevels) {
277 spdlog::warn("Volume texture upload skipped: mip level {} out of descriptor range {}", mipLevel, descriptorMipLevels);
278 return;
279 }
280
281 const auto mipWidth = std::max(1u, static_cast<uint32_t>(_descriptor->width()) >> mipLevel);
282 const auto mipHeight = std::max(1u, static_cast<uint32_t>(_descriptor->height()) >> mipLevel);
283 const auto mipDepth = std::max(1u, static_cast<uint32_t>(_descriptor->depth()) >> mipLevel);
284 const auto bpp = bytesPerPixel(_texture->format());
285
286 const auto expectedSize = static_cast<size_t>(mipWidth) * mipHeight * mipDepth * bpp;
287 if (imageDataSize > 0 && imageDataSize < expectedSize) {
288 spdlog::warn("Volume texture upload skipped: data size {} < expected {}", imageDataSize, expectedSize);
289 return;
290 }
291
292 const MTL::Region region = MTL::Region(0, 0, 0, mipWidth, mipHeight, mipDepth);
293 const NS::UInteger bytesPerRow = static_cast<NS::UInteger>(bpp) * mipWidth;
294 const NS::UInteger bytesPerImage = bytesPerRow * mipHeight;
295
296 _metalTexture->replaceRegion(region, mipLevel, 0, imageData, bytesPerRow, bytesPerImage);
297 }
298
300 {
301 // Clear samplers to force recreation
302 for (auto* sampler : _samplers) {
303 sampler->release();
304 }
305 _samplers.clear();
306 }
307}
Abstract GPU interface for resource creation, state management, and draw submission.
GPU texture resource supporting 2D, cubemap, volume, and array formats with mipmap management.
Definition texture.h:57
PixelFormat format() const
Definition texture.h:97
bool isCubemap() const
Definition texture.h:85
void uploadImmediate(GraphicsDevice *device) override
void setExternalTexture(MTL::Texture *externalTexture)
void uploadData(GraphicsDevice *device)
void create(MetalGraphicsDevice *device)
void propertyChanged(uint32_t flag) override