载入中...
搜索中...
未找到
GraphicsTexture.cpp
浏览该文件的文档.
1// Vulkan backend implementation — texture creation, upload and reload.
2//
3// Split out of Graphics2D.cpp (pure move; shared helpers live in
4// GraphicsInternal.h). Keep the include list tight: the embedded shader
5// .inc arrays are unused here and would trip -Wunused-const-variable under
6// strict-warning CI builds.
7
11
12#include "common/Diagnostic.h"
13#include "common/Exception.h"
14#include "common/Resource.h"
15#include "common/Result.h"
16#include "image/Image.h"
17#include "image/ImageData.h"
18#include "zeroerr/assert.h"
19
20#include <algorithm>
21#include <array>
22#include <cstdint>
23#include <cstring>
24#include <limits>
25#include <memory>
26#include <string>
27#include <vector>
28
29#if __has_include("graphics/shaders/reflection_probe_filter_comp_spv.inc")
30#include "graphics/shaders/reflection_probe_filter_comp_spv.inc"
31#define EVENGINE_HAS_REFLECTION_PROBE_FILTER_SPV 1
32#endif
33
34
35namespace eve::graphics::vulkan {
36
37template <class TextureImage>
38void recordTextureCopies(vk::CommandBuffer cb, TextureImage &image, vkb::GenericBuffer &staging, uint32_t width,
39 uint32_t height, uint32_t depth, uint32_t mipLevels, uint32_t layers,
40 uint32_t bytesPerPixel = 4) {
41 vk::DeviceSize offset = 0;
42 for (uint32_t mip = 0; mip < mipLevels; ++mip) {
43 const uint32_t mipWidth = std::max(width >> mip, 1u);
44 const uint32_t mipHeight = std::max(height >> mip, 1u);
45 const uint32_t mipDepth = std::max(depth >> mip, 1u);
46 for (uint32_t layer = 0; layer < layers; ++layer) {
47 image.copy(cb, staging.buffer, mip, layer, mipWidth, mipHeight, mipDepth, uint32_t(offset));
48 offset += vk::DeviceSize(mipWidth) * mipHeight * mipDepth * bytesPerPixel;
49 }
50 }
51 image.setLayout(cb, vk::ImageLayout::eShaderReadOnlyOptimal);
52}
53
54template <class TextureImage>
55void uploadTextureForAllShaderStages(vkb::Device &device, vk::CommandPool commandPool, vk::Queue graphicsQueue,
56 TextureImage &image, uint32_t width, uint32_t height, uint32_t mipLevels,
57 uint32_t layers, const std::vector<uint8_t> &bytes, uint32_t bytesPerPixel = 4,
58 uint32_t depth = 1) {
59 vkb::GenericBuffer staging(
60 device, vk::BufferUsageFlagBits::eTransferSrc, vk::DeviceSize(bytes.size()),
61 vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent);
62 staging.updateLocal(vkb::FrameSlot::gpuIdle(), bytes.data(), vk::DeviceSize(bytes.size()));
63
64 vkb::executeImmediately(device.instance, commandPool, graphicsQueue, [&](vk::CommandBuffer cb) {
65 recordTextureCopies(cb, image, staging, width, height, depth, mipLevels, layers, bytesPerPixel);
66 });
67 staging.release();
68}
69
70void Graphics::writeCombinedImageDescriptor(GpuTexture *gpu) {
71 if (!gpu || !gpu->descriptorSet || !gpu->sampler) return;
72 vk::ImageView view = gpu->imageView();
73 if (!view) return;
74 vkb::UnboundSet unbound = vkb::UnboundSet::reopenAfterIdle(gpu->descriptorSet);
75 vkb::DescriptorSetUpdater updater;
76 updater.beginDescriptorSet(unbound)
77 .beginImages(0, 0, vk::DescriptorType::eCombinedImageSampler)
78 .image(vkb::SampledImage::forLaterSample(gpu->sampler, view))
79 .beginImages(1, 0, vk::DescriptorType::eCombinedImageSampler)
80 .image(vkb::SampledImage::forLaterSample(gpu->sampler, view))
81 .update(device.instance);
82 gpu->descriptorSet = std::move(unbound).publish();
83}
84
85Texture *Graphics::newTexture(int w, int h, const uint8_t *rgba, bool repeatU, bool repeatV) {
88 info.sampler.repeatV = repeatV;
89 return newTexture(w, h, rgba, info);
90}
91
92Texture *Graphics::newTexture(int w, int h, const uint8_t *rgba, const TextureCreateInfo &rawInfo) {
93 ASSERT(initialized);
94 ASSERT_GT(w, 0);
95 ASSERT_GT(h, 0);
96 ASSERT(rgba != nullptr);
97 if (!initialized) throw Exception("newTexture: graphics not initialized");
98 if (w <= 0 || h <= 0 || !rgba) throw Exception("newTexture: invalid args");
99
100 TextureCreateInfo info = normalizeTextureInfo(rawInfo);
101 const uint32_t mipLevels =
102 info.generateMipmaps ? uint32_t(mipmapCountForSize(w, h)) : 1u;
103
104 auto gpu = std::make_unique<GpuTexture>();
105 gpu->width = w;
106 gpu->height = h;
107 gpu->isCube = false;
108 gpu->mipLevels = mipLevels;
109 gpu->samplerState = info.sampler;
110 gpu->image = vkb::TextureImage2D(device, uint32_t(w), uint32_t(h), mipLevels);
111
112 std::vector<uint8_t> bytes =
113 (mipLevels > 1) ? buildMipChain2D(rgba, uint32_t(w), uint32_t(h), mipLevels)
114 : std::vector<uint8_t>(rgba, rgba + size_t(w) * size_t(h) * 4);
115 uploadTextureForAllShaderStages(device, uploadPool,
116 device.getQueue(vkb::QueueType::graphics), gpu->image,
117 uint32_t(w), uint32_t(h), mipLevels, 1, bytes);
118
119 gpu->sampler = createVkSampler(info.sampler, mipLevels);
120
121 auto sets = vkb::DescriptorSetBuilder().layout(texSetLayout).build(device.instance, descriptorPool);
122
123 gpu->descriptorSet = vkb::BoundSet{sets[0]};
124 writeCombinedImageDescriptor(gpu.get());
125 registerBindlessTexture2D(gpu.get());
126
127 auto tex = std::make_unique<Texture>();
128 tex->width = w;
129 tex->height = h;
130 tex->pixelWidth = w;
131 tex->pixelHeight = h;
132 tex->mipmapCount = int(mipLevels);
133 tex->sampler = info.sampler;
134 tex->gpuHandle = gpu.get();
135
136 Texture *raw = tex.get();
137 ownedTextures.push_back(std::move(tex));
138 ownedGpuTextures.push_back(std::move(gpu));
139 return raw;
140}
141
142Result<Texture *> Graphics::newTextureMipChain(uint32_t width, uint32_t height, uint32_t levels,
143 std::span<const uint8_t> rgba) {
144 auto fail = [](DiagnosticCode code, std::string message) {
145 return Result<Texture *>::failure(Diagnostic::error(code, message, {}, {}, "graphics.texture.mips"));
146 };
147 if (!initialized) return fail(DiagnosticCode::Failed, "graphics is not initialized");
148 const auto maximum = device.physical_device.properties.limits.maxImageDimension2D;
149 if (!width || !height || width > maximum || height > maximum)
150 return fail(DiagnosticCode::InvalidArgument, "mip dimensions exceed device limits");
151 uint64_t expected = 0;
152 uint32_t count = 0, w = width, h = height;
153 for (;;) {
154 expected += uint64_t(w) * h * 4;
155 ++count;
156 if (w == 1 && h == 1) break;
157 w = std::max(w / 2, 1u);
158 h = std::max(h / 2, 1u);
159 }
160 if (levels != count || expected != rgba.size() || expected > UINT32_MAX)
161 return fail(DiagnosticCode::InvalidArgument, "mip count, packed bytes or staging offset budget invalid");
162 std::unique_ptr<GpuTexture> gpu;
163 try {
164 // Reserve both registries before allocating GPU state; publication then cannot allocate.
165 ownedTextures.reserve(ownedTextures.size() + 1);
166 ownedGpuTextures.reserve(ownedGpuTextures.size() + 1);
167 auto tex = std::make_unique<Texture>();
168 std::vector<uint8_t> bytes(rgba.begin(), rgba.end());
169 gpu = std::make_unique<GpuTexture>();
170 gpu->width = int(width);
171 gpu->height = int(height);
172 gpu->mipLevels = levels;
173 gpu->samplerState = TextureSampler::linearMipmap();
174 gpu->image = vkb::TextureImage2D(device, width, height, levels);
175 uploadTextureForAllShaderStages(device, uploadPool, device.getQueue(vkb::QueueType::graphics), gpu->image,
176 width, height, levels, 1, bytes);
177 gpu->sampler = createVkSampler(gpu->samplerState, levels);
178 auto sets = vkb::DescriptorSetBuilder().layout(texSetLayout).build(device.instance, descriptorPool);
179 gpu->descriptorSet = vkb::BoundSet{sets[0]};
180 writeCombinedImageDescriptor(gpu.get());
181 tex->width = tex->pixelWidth = int(width);
182 tex->height = tex->pixelHeight = int(height);
183 tex->mipmapCount = int(levels);
184 tex->sampler = gpu->samplerState;
185 tex->gpuHandle = gpu.get();
186 registerBindlessTexture2D(gpu.get());
187 auto *result = tex.get();
188 ownedTextures.push_back(std::move(tex));
189 ownedGpuTextures.push_back(std::move(gpu));
190 return Result<Texture *>::success(result);
191 } catch (const std::exception &error) {
192 if (gpu) {
193 unregisterBindlessTexture(gpu.get());
194 if (gpu->descriptorSet) device->freeDescriptorSets(descriptorPool, {gpu->descriptorSet.handle});
195 if (gpu->sampler) device->destroySampler(gpu->sampler);
196 }
197 return fail(DiagnosticCode::Failed, error.what());
198 }
199}
200
202 std::span<const uint16_t> rgbaHalf) {
203 auto fail = [](DiagnosticCode code, std::string message) {
205 Diagnostic::error(code, message, {}, {}, "graphics.texture.array"));
206 };
207 if (!initialized) return fail(DiagnosticCode::Failed, "graphics is not initialized");
208 const auto &limits = device.physical_device.properties.limits;
209 const uint64_t texels = uint64_t(width) * height * layers;
210 if (!width || !height || !layers || width > limits.maxImageDimension2D || height > limits.maxImageDimension2D ||
211 layers > limits.maxImageArrayLayers || texels > SIZE_MAX / 4 || rgbaHalf.size() != texels * 4)
212 return fail(DiagnosticCode::InvalidArgument, "array dimensions or packed RGBA16F size are invalid");
213 std::unique_ptr<GpuTexture> gpu;
214 try {
215 ownedTextures.reserve(ownedTextures.size() + 1);
216 ownedGpuTextures.reserve(ownedGpuTextures.size() + 1);
217 auto tex = std::make_unique<Texture>();
218 gpu = std::make_unique<GpuTexture>();
219 vk::ImageCreateInfo imageInfo{{},
220 vk::ImageType::e2D,
221 vk::Format::eR16G16B16A16Sfloat,
222 {width, height, 1},
223 1,
224 layers,
225 vk::SampleCountFlagBits::e1,
226 vk::ImageTiling::eOptimal,
227 vk::ImageUsageFlagBits::eSampled | vk::ImageUsageFlagBits::eTransferDst,
228 vk::SharingMode::eExclusive};
229 gpu->arrayImage =
230 vkb::GenericImage(device, imageInfo, vk::ImageViewType::e2DArray, vk::ImageAspectFlagBits::eColor, false);
231 gpu->isArray = true;
232 gpu->width = int(width);
233 gpu->height = int(height);
234 gpu->samplerState = TextureSampler::linear();
235 std::vector<uint8_t> bytes(rgbaHalf.size_bytes());
236 std::memcpy(bytes.data(), rgbaHalf.data(), bytes.size());
237 uploadTextureForAllShaderStages(device, uploadPool, device.getQueue(vkb::QueueType::graphics), gpu->arrayImage,
238 width, height, 1, layers, bytes, 8);
239 gpu->sampler = createVkSampler(gpu->samplerState, 1);
240 tex->width = tex->pixelWidth = int(width);
241 tex->height = tex->pixelHeight = int(height);
242 tex->layers = int(layers);
243 tex->sampler = gpu->samplerState;
244 tex->gpuHandle = gpu.get();
245 auto *result = tex.get();
246 ownedTextures.push_back(std::move(tex));
247 ownedGpuTextures.push_back(std::move(gpu));
248 return Result<Texture *>::success(result);
249 } catch (const std::exception &error) {
250 if (gpu && gpu->sampler) device->destroySampler(gpu->sampler);
251 return fail(DiagnosticCode::Failed, error.what());
252 }
253}
254
256 std::span<const uint8_t> rgba) {
257 auto fail = [](DiagnosticCode code, std::string message) {
259 Diagnostic::error(code, message, {}, {}, "graphics.texture.volume"));
260 };
261 if (!initialized) return fail(DiagnosticCode::Failed, "graphics is not initialized");
262 const auto &limits = device.physical_device.properties.limits;
263 const uint64_t texels = uint64_t(width) * height * depth;
264 if (!width || !height || !depth || width > limits.maxImageDimension3D || height > limits.maxImageDimension3D ||
265 depth > limits.maxImageDimension3D || texels > SIZE_MAX / 4 || rgba.size() != texels * 4)
266 return fail(DiagnosticCode::InvalidArgument, "volume dimensions or packed RGBA8 size are invalid");
267 std::unique_ptr<GpuTexture> gpu;
268 try {
269 ownedTextures.reserve(ownedTextures.size() + 1);
270 ownedGpuTextures.reserve(ownedGpuTextures.size() + 1);
271 auto tex = std::make_unique<Texture>();
272 gpu = std::make_unique<GpuTexture>();
273 vk::ImageCreateInfo imageInfo{{},
274 vk::ImageType::e3D,
275 vk::Format::eR8G8B8A8Unorm,
276 {width, height, depth},
277 1,
278 1,
279 vk::SampleCountFlagBits::e1,
280 vk::ImageTiling::eOptimal,
281 vk::ImageUsageFlagBits::eSampled | vk::ImageUsageFlagBits::eTransferDst,
282 vk::SharingMode::eExclusive};
283 gpu->arrayImage =
284 vkb::GenericImage(device, imageInfo, vk::ImageViewType::e3D, vk::ImageAspectFlagBits::eColor, false);
285 gpu->isArray = true;
286 gpu->isVolume = true;
287 gpu->width = int(width);
288 gpu->height = int(height);
289 gpu->samplerState = TextureSampler::linear();
290 gpu->samplerState.repeatU = gpu->samplerState.repeatV = gpu->samplerState.repeatW = true;
291 std::vector<uint8_t> bytes(rgba.begin(), rgba.end());
292 uploadTextureForAllShaderStages(device, uploadPool, device.getQueue(vkb::QueueType::graphics), gpu->arrayImage,
293 width, height, 1, 1, bytes, 4, depth);
294 gpu->sampler = createVkSampler(gpu->samplerState, 1);
295 tex->width = tex->pixelWidth = int(width);
296 tex->height = tex->pixelHeight = int(height);
297 tex->depth = int(depth);
298 tex->sampler = gpu->samplerState;
299 tex->gpuHandle = gpu.get();
300 auto *result = tex.get();
301 ownedTextures.push_back(std::move(tex));
302 ownedGpuTextures.push_back(std::move(gpu));
303 return Result<Texture *>::success(result);
304 } catch (const std::exception &error) {
305 if (gpu && gpu->sampler) device->destroySampler(gpu->sampler);
306 return fail(DiagnosticCode::Failed, error.what());
307 }
308}
309
310Texture *Graphics::newCubemap(int faceSize, const uint8_t *rgbaFaces) {
311 // IBL shaders query the actual GGX-prefiltered mip count; generate it by default.
312 return newCubemap(faceSize, rgbaFaces, TextureCreateInfo::withMipmaps(false));
313}
314
315Texture *Graphics::newCubemap(int faceSize, const uint8_t *rgbaFaces,
316 const TextureCreateInfo &rawInfo) {
317 ASSERT(initialized);
318 ASSERT_GT(faceSize, 0);
319 ASSERT(rgbaFaces != nullptr);
320 if (!initialized) throw Exception("newCubemap: graphics not initialized");
321 if (faceSize <= 0 || !rgbaFaces) throw Exception("newCubemap: invalid args");
322
323 TextureCreateInfo info = normalizeTextureInfo(rawInfo);
324 info.sampler.repeatU = false;
325 info.sampler.repeatV = false;
326 info.sampler.repeatW = false;
327 const uint32_t mipLevels =
328 info.generateMipmaps ? uint32_t(mipmapCountForSize(faceSize, faceSize)) : 1u;
329
330 const size_t faceBytes = size_t(faceSize) * size_t(faceSize) * 4u;
331 auto gpu = std::make_unique<GpuTexture>();
332 gpu->width = faceSize;
333 gpu->height = faceSize;
334 gpu->isCube = true;
335 gpu->mipLevels = mipLevels;
336 gpu->samplerState = info.sampler;
337 gpu->cubeImage = vkb::TextureImageCube(device, device.physical_device.memory_properties,
338 uint32_t(faceSize), uint32_t(faceSize), mipLevels);
339
340 std::vector<uint8_t> bytes =
341 (mipLevels > 1) ? buildMipChainCube(rgbaFaces, uint32_t(faceSize), mipLevels)
342 : std::vector<uint8_t>(rgbaFaces, rgbaFaces + faceBytes * 6u);
343 uploadTextureForAllShaderStages(device, uploadPool,
344 device.getQueue(vkb::QueueType::graphics), gpu->cubeImage,
345 uint32_t(faceSize), uint32_t(faceSize), mipLevels, 6, bytes);
346
347 gpu->sampler = createVkSampler(info.sampler, mipLevels);
348 // Cubemap sampled via mesh3d descriptor sets — no 2D texSetLayout binding required here.
349 registerBindlessTextureCube(gpu.get());
350
351 auto tex = std::make_unique<Texture>();
352 tex->width = faceSize;
353 tex->height = faceSize;
354 tex->pixelWidth = faceSize;
355 tex->pixelHeight = faceSize;
356 tex->layers = 6;
357 tex->mipmapCount = int(mipLevels);
358 tex->sampler = info.sampler;
359 tex->gpuHandle = gpu.get();
360
361 Texture *raw = tex.get();
362 ownedTextures.push_back(std::move(tex));
363 ownedGpuTextures.push_back(std::move(gpu));
364 return raw;
365}
366
368 if (!initialized || faceSize <= 0) return nullptr;
369 const uint32_t size = static_cast<uint32_t>(faceSize);
370 const uint32_t mipLevels = uint32_t(mipmapCountForSize(faceSize, faceSize));
371 vk::ImageCreateInfo imageInfo{};
372 imageInfo.flags = vk::ImageCreateFlagBits::eCubeCompatible;
373 imageInfo.imageType = vk::ImageType::e2D;
374 imageInfo.format = vk::Format::eR16G16B16A16Sfloat;
375 imageInfo.extent = vk::Extent3D{size, size, 1};
376 imageInfo.mipLevels = mipLevels;
377 imageInfo.arrayLayers = 6;
378 imageInfo.samples = vk::SampleCountFlagBits::e1;
379 imageInfo.tiling = vk::ImageTiling::eOptimal;
380 imageInfo.usage = vk::ImageUsageFlagBits::eSampled | vk::ImageUsageFlagBits::eTransferDst |
381 vk::ImageUsageFlagBits::eTransferSrc |
382 vk::ImageUsageFlagBits::eColorAttachment | vk::ImageUsageFlagBits::eStorage;
383 imageInfo.sharingMode = vk::SharingMode::eExclusive;
384
385 auto gpu = std::make_unique<GpuTexture>();
386 gpu->rawCubeImage = device->createImageUnique(imageInfo);
387 const vk::MemoryRequirements requirements =
388 device->getImageMemoryRequirements(*gpu->rawCubeImage);
389 uint32_t memoryType = UINT32_MAX;
390 const auto &memoryProperties = device.physical_device.memory_properties;
391 for (uint32_t index = 0; index < memoryProperties.memoryTypeCount; ++index) {
392 if ((requirements.memoryTypeBits & (1u << index)) != 0u &&
393 (memoryProperties.memoryTypes[index].propertyFlags &
394 vk::MemoryPropertyFlagBits::eDeviceLocal) != vk::MemoryPropertyFlags{}) {
395 memoryType = index;
396 break;
397 }
398 }
399 if (memoryType == UINT32_MAX) return nullptr;
400 gpu->rawCubeMemory = device->allocateMemoryUnique(
401 vk::MemoryAllocateInfo{requirements.size, memoryType});
402 device->bindImageMemory(*gpu->rawCubeImage, *gpu->rawCubeMemory, 0);
403
404 vk::ImageViewCreateInfo viewInfo{};
405 viewInfo.image = *gpu->rawCubeImage;
406 viewInfo.viewType = vk::ImageViewType::eCube;
407 viewInfo.format = imageInfo.format;
408 viewInfo.subresourceRange =
409 vk::ImageSubresourceRange{vk::ImageAspectFlagBits::eColor, 0, mipLevels, 0, 6};
410 gpu->rawCubeView = device->createImageViewUnique(viewInfo);
411 gpu->width = faceSize;
412 gpu->height = faceSize;
413 gpu->isCube = true;
414 gpu->isHDR = true;
415 gpu->mipLevels = mipLevels;
416 gpu->samplerState = TextureSampler::linearMipmap();
417 gpu->sampler = createVkSampler(gpu->samplerState, mipLevels);
418
419 vkb::executeImmediately(device.instance, uploadPool,
420 device.getQueue(vkb::QueueType::graphics),
421 [&](vk::CommandBuffer command) {
422 vk::ImageMemoryBarrier barrier{};
423 barrier.srcAccessMask = {};
424 barrier.dstAccessMask = vk::AccessFlagBits::eShaderRead;
425 barrier.oldLayout = vk::ImageLayout::eUndefined;
426 barrier.newLayout = vk::ImageLayout::eShaderReadOnlyOptimal;
427 barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
428 barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
429 barrier.image = *gpu->rawCubeImage;
430 barrier.subresourceRange = viewInfo.subresourceRange;
431 command.pipelineBarrier(vk::PipelineStageFlagBits::eTopOfPipe,
432 vk::PipelineStageFlagBits::eFragmentShader,
433 {}, 0, nullptr, 0, nullptr, 1, &barrier);
434 });
435 registerBindlessTextureCube(gpu.get());
436
437 auto texture = std::make_unique<Texture>();
438 texture->width = faceSize;
439 texture->height = faceSize;
440 texture->pixelWidth = faceSize;
441 texture->pixelHeight = faceSize;
442 texture->layers = 6;
443 texture->mipmapCount = int(mipLevels);
444 texture->sampler = gpu->samplerState;
445 texture->gpuHandle = gpu.get();
446 Texture *raw = texture.get();
447 ownedTextures.push_back(std::move(texture));
448 ownedGpuTextures.push_back(std::move(gpu));
449 return raw;
450}
451
453 auto *canvas = dynamic_cast<OffscreenCanvas *>(source);
454 if (!canvas || !canvas->isHDR() || !cubemap || !cubemap->gpuHandle || face < 0 || face >= 6)
455 return false;
456 auto *target = static_cast<GpuTexture *>(cubemap->gpuHandle);
457 if (!target->isCube || !target->isHDR || !target->rawCubeImage ||
458 canvas->getWidth() != target->width || canvas->getHeight() != target->height)
459 return false;
460 ensureOffscreen3DResources();
461 lastOffscreen3DGpuDurationMs = 0.f;
462 vkb::executeImmediately(device.instance, uploadPool,
463 device.getQueue(vkb::QueueType::graphics),
464 [&](vk::CommandBuffer command) {
465 if (offscreen3DTimestampQueryPool) {
466 command.resetQueryPool(offscreen3DTimestampQueryPool, 0, 2);
467 command.writeTimestamp(vk::PipelineStageFlagBits::eTopOfPipe,
468 offscreen3DTimestampQueryPool, 0);
469 }
470 canvas->colorImage().setLayout(command,
471 vk::ImageLayout::eTransferSrcOptimal);
472 vk::ImageMemoryBarrier toCopy{};
473 toCopy.srcAccessMask = vk::AccessFlagBits::eShaderRead;
474 toCopy.dstAccessMask = vk::AccessFlagBits::eTransferWrite;
475 toCopy.oldLayout = vk::ImageLayout::eShaderReadOnlyOptimal;
476 toCopy.newLayout = vk::ImageLayout::eTransferDstOptimal;
477 toCopy.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
478 toCopy.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
479 toCopy.image = *target->rawCubeImage;
480 toCopy.subresourceRange = {vk::ImageAspectFlagBits::eColor, 0, 1,
481 uint32_t(face), 1};
482 command.pipelineBarrier(vk::PipelineStageFlagBits::eFragmentShader,
483 vk::PipelineStageFlagBits::eTransfer, {}, 0,
484 nullptr, 0, nullptr, 1, &toCopy);
485 vk::ImageCopy copy{};
486 copy.srcSubresource = {vk::ImageAspectFlagBits::eColor, 0, 0, 1};
487 copy.dstSubresource = {vk::ImageAspectFlagBits::eColor, 0,
488 uint32_t(face), 1};
489 copy.extent = vk::Extent3D{uint32_t(target->width),
490 uint32_t(target->height), 1};
491 command.copyImage(canvas->colorImage().image(),
492 vk::ImageLayout::eTransferSrcOptimal,
493 *target->rawCubeImage,
494 vk::ImageLayout::eTransferDstOptimal, 1, &copy);
495 std::swap(toCopy.srcAccessMask, toCopy.dstAccessMask);
496 std::swap(toCopy.oldLayout, toCopy.newLayout);
497 command.pipelineBarrier(vk::PipelineStageFlagBits::eTransfer,
498 vk::PipelineStageFlagBits::eFragmentShader,
499 {}, 0, nullptr, 0, nullptr, 1, &toCopy);
500 canvas->colorImage().setLayout(
501 command, vk::ImageLayout::eShaderReadOnlyOptimal);
502 if (offscreen3DTimestampQueryPool)
503 command.writeTimestamp(
504 vk::PipelineStageFlagBits::eBottomOfPipe,
505 offscreen3DTimestampQueryPool, 1);
506 });
507 if (offscreen3DTimestampQueryPool && offscreen3DTimestampPeriodNs > 0.f) {
508 std::array<uint64_t, 2> ticks{};
509 const vk::Result result = device->getQueryPoolResults(
510 offscreen3DTimestampQueryPool, 0, uint32_t(ticks.size()), sizeof(ticks), ticks.data(),
511 sizeof(uint64_t), vk::QueryResultFlagBits::e64 | vk::QueryResultFlagBits::eWait);
512 if (result == vk::Result::eSuccess && ticks[1] >= ticks[0])
513 lastOffscreen3DGpuDurationMs =
514 float(double(ticks[1] - ticks[0]) * double(offscreen3DTimestampPeriodNs) * 1.0e-6);
515 }
516 return true;
517}
518
519bool Graphics::copyHDRCanvasesToCubemap(Canvas *const *sources, int faceCount,
520 Texture *cubemap) {
521 if (!sources || faceCount < 1 || faceCount > 6 || !cubemap || !cubemap->gpuHandle)
522 return false;
523 auto *target = static_cast<GpuTexture *>(cubemap->gpuHandle);
524 if (!target->isCube || !target->isHDR || !target->rawCubeImage) return false;
525
526 std::array<OffscreenCanvas *, 6> canvases{};
527 for (int face = 0; face < faceCount; ++face) {
528 auto *canvas = dynamic_cast<OffscreenCanvas *>(sources[face]);
529 if (!canvas || !canvas->isHDR() || canvas->getWidth() != target->width ||
530 canvas->getHeight() != target->height)
531 return false;
532 canvases[static_cast<size_t>(face)] = canvas;
533 }
534
535 ensureOffscreen3DResources();
536 lastOffscreen3DGpuDurationMs = 0.f;
537 vkb::executeImmediately(device.instance, uploadPool,
538 device.getQueue(vkb::QueueType::graphics),
539 [&](vk::CommandBuffer command) {
540 if (offscreen3DTimestampQueryPool) {
541 command.resetQueryPool(offscreen3DTimestampQueryPool, 0, 2);
542 command.writeTimestamp(vk::PipelineStageFlagBits::eTopOfPipe,
543 offscreen3DTimestampQueryPool, 0);
544 }
545 for (int face = 0; face < faceCount; ++face) {
546 auto *canvas = canvases[static_cast<size_t>(face)];
547 canvas->colorImage().setLayout(
548 command, vk::ImageLayout::eTransferSrcOptimal);
549
550 vk::ImageMemoryBarrier toCopy{};
551 toCopy.srcAccessMask = vk::AccessFlagBits::eShaderRead;
552 toCopy.dstAccessMask = vk::AccessFlagBits::eTransferWrite;
553 toCopy.oldLayout = vk::ImageLayout::eShaderReadOnlyOptimal;
554 toCopy.newLayout = vk::ImageLayout::eTransferDstOptimal;
555 toCopy.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
556 toCopy.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
557 toCopy.image = *target->rawCubeImage;
558 toCopy.subresourceRange =
559 vk::ImageSubresourceRange(vk::ImageAspectFlagBits::eColor,
560 0, 1, uint32_t(face), 1);
561 command.pipelineBarrier(
562 vk::PipelineStageFlagBits::eFragmentShader,
563 vk::PipelineStageFlagBits::eTransfer, {}, 0, nullptr, 0,
564 nullptr, 1, &toCopy);
565
566 vk::ImageCopy copy{};
567 copy.srcSubresource =
568 vk::ImageSubresourceLayers(vk::ImageAspectFlagBits::eColor,
569 0, 0, 1);
570 copy.dstSubresource =
571 vk::ImageSubresourceLayers(vk::ImageAspectFlagBits::eColor,
572 0, uint32_t(face), 1);
573 copy.extent = vk::Extent3D(uint32_t(canvas->getWidth()),
574 uint32_t(canvas->getHeight()), 1);
575 command.copyImage(canvas->colorImage().image(),
576 vk::ImageLayout::eTransferSrcOptimal,
577 *target->rawCubeImage,
578 vk::ImageLayout::eTransferDstOptimal, 1,
579 &copy);
580
581 std::swap(toCopy.srcAccessMask, toCopy.dstAccessMask);
582 std::swap(toCopy.oldLayout, toCopy.newLayout);
583 command.pipelineBarrier(
584 vk::PipelineStageFlagBits::eTransfer,
585 vk::PipelineStageFlagBits::eFragmentShader, {}, 0, nullptr,
586 0, nullptr, 1, &toCopy);
587 canvas->colorImage().setLayout(
588 command, vk::ImageLayout::eShaderReadOnlyOptimal);
589 }
590 if (offscreen3DTimestampQueryPool)
591 command.writeTimestamp(
592 vk::PipelineStageFlagBits::eBottomOfPipe,
593 offscreen3DTimestampQueryPool, 1);
594 });
595 if (offscreen3DTimestampQueryPool && offscreen3DTimestampPeriodNs > 0.f) {
596 std::array<uint64_t, 2> ticks{};
597 const vk::Result result = device->getQueryPoolResults(
598 offscreen3DTimestampQueryPool, 0, uint32_t(ticks.size()), sizeof(ticks), ticks.data(),
599 sizeof(uint64_t), vk::QueryResultFlagBits::e64 | vk::QueryResultFlagBits::eWait);
600 if (result == vk::Result::eSuccess && ticks[1] >= ticks[0])
601 lastOffscreen3DGpuDurationMs =
602 float(double(ticks[1] - ticks[0]) * double(offscreen3DTimestampPeriodNs) * 1.0e-6);
603 }
604 return true;
605}
606
607bool Graphics::filterHDRReflectionCubemap(Texture *cubemap, int sampleCount) {
608#ifndef EVENGINE_HAS_REFLECTION_PROBE_FILTER_SPV
609 (void)cubemap;
610 (void)sampleCount;
611 return false;
612#else
613 if (!cubemap || !cubemap->gpuHandle) return false;
614 auto *target = static_cast<GpuTexture *>(cubemap->gpuHandle);
615 if (!target->isCube || !target->isHDR || !target->rawCubeImage || target->mipLevels < 2)
616 return false;
617 sampleCount = std::clamp(sampleCount, 8, 512);
618 ensureOffscreen3DResources();
619 lastOffscreen3DGpuDurationMs = 0.f;
620
621 if (!reflectionProbeFilterPass.pipeline()) {
622 std::array<vk::DescriptorSetLayoutBinding, 2> bindings{
623 vk::DescriptorSetLayoutBinding{0, vk::DescriptorType::eCombinedImageSampler, 1,
624 vk::ShaderStageFlagBits::eCompute},
625 vk::DescriptorSetLayoutBinding{1, vk::DescriptorType::eStorageImage, 1,
626 vk::ShaderStageFlagBits::eCompute},
627 };
628 reflectionProbeFilterSetLayout = device->createDescriptorSetLayoutUnique(
629 vk::DescriptorSetLayoutCreateInfo{{}, uint32_t(bindings.size()), bindings.data()});
630 vk::DescriptorSetLayout layout = *reflectionProbeFilterSetLayout;
631 vk::PushConstantRange push{vk::ShaderStageFlagBits::eCompute, 0, 16};
632 reflectionProbeFilterPipelineLayout = device->createPipelineLayoutUnique(
633 vk::PipelineLayoutCreateInfo{{}, 1, &layout, 1, &push});
634 const std::vector<uint32_t> spv(reflection_probe_filter_comp_spv,
635 reflection_probe_filter_comp_spv +
636 reflection_probe_filter_comp_spv_count);
637 if (!reflectionProbeFilterPass.create(device, *reflectionProbeFilterPipelineLayout, spv))
638 return false;
639 }
640
641 vk::ImageViewCreateInfo sourceInfo{};
642 sourceInfo.image = *target->rawCubeImage;
643 sourceInfo.viewType = vk::ImageViewType::eCube;
644 sourceInfo.format = vk::Format::eR16G16B16A16Sfloat;
645 sourceInfo.subresourceRange = {vk::ImageAspectFlagBits::eColor, 0, 1, 0, 6};
646 vk::UniqueImageView sourceView = device->createImageViewUnique(sourceInfo);
647 std::vector<vk::UniqueImageView> targetViews;
648 targetViews.reserve(target->mipLevels - 1u);
649 for (uint32_t mip = 1; mip < target->mipLevels; ++mip) {
650 vk::ImageViewCreateInfo info{};
651 info.image = *target->rawCubeImage;
652 info.viewType = vk::ImageViewType::e2DArray;
653 info.format = sourceInfo.format;
654 info.subresourceRange = {vk::ImageAspectFlagBits::eColor, mip, 1, 0, 6};
655 targetViews.push_back(device->createImageViewUnique(info));
656 }
657
658 std::array<vk::DescriptorPoolSize, 2> poolSizes{
659 vk::DescriptorPoolSize{vk::DescriptorType::eCombinedImageSampler,
660 target->mipLevels - 1u},
661 vk::DescriptorPoolSize{vk::DescriptorType::eStorageImage, target->mipLevels - 1u},
662 };
663 vk::UniqueDescriptorPool pool = device->createDescriptorPoolUnique(vk::DescriptorPoolCreateInfo{
664 {}, target->mipLevels - 1u, uint32_t(poolSizes.size()), poolSizes.data()});
665 std::vector<vk::DescriptorSetLayout> layouts(target->mipLevels - 1u,
666 *reflectionProbeFilterSetLayout);
667 std::vector<vk::DescriptorSet> sets = device->allocateDescriptorSets(
668 vk::DescriptorSetAllocateInfo{*pool, uint32_t(layouts.size()), layouts.data()});
669 for (size_t index = 0; index < sets.size(); ++index) {
670 vk::DescriptorImageInfo sourceImage{target->sampler, *sourceView,
671 vk::ImageLayout::eShaderReadOnlyOptimal};
672 vk::DescriptorImageInfo destinationImage{{}, *targetViews[index],
673 vk::ImageLayout::eGeneral};
674 std::array<vk::WriteDescriptorSet, 2> writes{
675 vk::WriteDescriptorSet{sets[index], 0, 0, 1,
676 vk::DescriptorType::eCombinedImageSampler, &sourceImage},
677 vk::WriteDescriptorSet{sets[index], 1, 0, 1,
678 vk::DescriptorType::eStorageImage, &destinationImage},
679 };
680 device->updateDescriptorSets(uint32_t(writes.size()), writes.data(), 0, nullptr);
681 }
682
683 struct FilterPush {
684 float roughness;
685 uint32_t sampleCount;
686 uint32_t diffuseMode;
687 uint32_t targetSize;
688 };
689 vkb::executeImmediately(device.instance, uploadPool,
690 device.getQueue(vkb::QueueType::graphics),
691 [&](vk::CommandBuffer command) {
692 if (offscreen3DTimestampQueryPool) {
693 command.resetQueryPool(offscreen3DTimestampQueryPool, 0, 2);
694 command.writeTimestamp(vk::PipelineStageFlagBits::eTopOfPipe,
695 offscreen3DTimestampQueryPool, 0);
696 }
697 for (uint32_t mip = 1; mip < target->mipLevels; ++mip) {
698 vk::ImageMemoryBarrier toWrite{};
699 toWrite.srcAccessMask = vk::AccessFlagBits::eShaderRead;
700 toWrite.dstAccessMask = vk::AccessFlagBits::eShaderWrite;
701 toWrite.oldLayout = vk::ImageLayout::eShaderReadOnlyOptimal;
702 toWrite.newLayout = vk::ImageLayout::eGeneral;
703 toWrite.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
704 toWrite.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
705 toWrite.image = *target->rawCubeImage;
706 toWrite.subresourceRange = {vk::ImageAspectFlagBits::eColor, mip,
707 1, 0, 6};
708 command.pipelineBarrier(vk::PipelineStageFlagBits::eFragmentShader,
709 vk::PipelineStageFlagBits::eComputeShader,
710 {}, 0, nullptr, 0, nullptr, 1, &toWrite);
711 const uint32_t size = std::max(uint32_t(target->width) >> mip, 1u);
712 const FilterPush push{
713 float(mip) / float(target->mipLevels - 1u),
714 uint32_t(sampleCount),
715 mip + 1u == target->mipLevels ? 1u : 0u, size};
716 command.bindDescriptorSets(
717 vk::PipelineBindPoint::eCompute,
718 *reflectionProbeFilterPipelineLayout, 0, 1,
719 &sets[static_cast<size_t>(mip - 1u)], 0, nullptr);
720 command.pushConstants(*reflectionProbeFilterPipelineLayout,
721 vk::ShaderStageFlagBits::eCompute, 0,
722 sizeof(push), &push);
723 reflectionProbeFilterPass.record(command, (size + 7u) / 8u,
724 (size + 7u) / 8u, 6);
725 std::swap(toWrite.srcAccessMask, toWrite.dstAccessMask);
726 std::swap(toWrite.oldLayout, toWrite.newLayout);
727 command.pipelineBarrier(vk::PipelineStageFlagBits::eComputeShader,
728 vk::PipelineStageFlagBits::eFragmentShader,
729 {}, 0, nullptr, 0, nullptr, 1, &toWrite);
730 }
731 if (offscreen3DTimestampQueryPool)
732 command.writeTimestamp(
733 vk::PipelineStageFlagBits::eBottomOfPipe,
734 offscreen3DTimestampQueryPool, 1);
735 });
736 if (offscreen3DTimestampQueryPool && offscreen3DTimestampPeriodNs > 0.f) {
737 std::array<uint64_t, 2> ticks{};
738 const vk::Result result = device->getQueryPoolResults(
739 offscreen3DTimestampQueryPool, 0, uint32_t(ticks.size()), sizeof(ticks), ticks.data(),
740 sizeof(uint64_t), vk::QueryResultFlagBits::e64 | vk::QueryResultFlagBits::eWait);
741 if (result == vk::Result::eSuccess && ticks[1] >= ticks[0])
742 lastOffscreen3DGpuDurationMs =
743 float(double(ticks[1] - ticks[0]) * double(offscreen3DTimestampPeriodNs) * 1.0e-6);
744 }
745 return true;
746#endif
747}
748
750 ASSERT(data != nullptr);
751 if (!data) throw Exception("newTexture: null ImageData");
752 if (data->getFormat() != "RGBA8")
753 throw Exception("newTexture: only RGBA8 ImageData supported for now");
754 return newTexture(data->getWidth(), data->getHeight(),
755 static_cast<const uint8_t *>(data->getData()));
756}
757
759 ASSERT(data != nullptr);
760 if (!data) throw Exception("newTexture: null ImageData");
761 if (data->getFormat() != "RGBA8")
762 throw Exception("newTexture: only RGBA8 ImageData supported for now");
763 return newTexture(data->getWidth(), data->getHeight(),
764 static_cast<const uint8_t *>(data->getData()), info);
765}
766
768 if (!data || contentKey.empty() || data->getFormat() != "RGBA8")
770 "RGBA8 image and nonempty content key are required", {},
771 {}, "graphics.texture.shared"));
772 if (auto found = sharedTexturesByContent.find(contentKey); found != sharedTexturesByContent.end())
773 return ResultRef<Texture>::success(std::ref(*found->second));
774 try {
775 Texture *texture = newTexture(data);
776 if (!texture) throw Exception("shared texture upload produced no texture");
777 sharedTexturesByContent.emplace(contentKey, texture);
778 return ResultRef<Texture>::success(std::ref(*texture));
779 } catch (const std::exception &error) {
781 Diagnostic::error(DiagnosticCode::Failed, error.what(), {}, {}, "graphics.texture.shared"));
782 }
783}
784
785
788 if (!texture || !texture->gpuHandle || !initialized) return;
789 for (auto &owned : ownedGpuTextures) {
790 if (owned.get() != texture->gpuHandle) continue;
791 // In-flight frames may still be sampling the old sampler / descriptor.
792 waitForSharedGpuResources();
793 if (owned->sampler) device->destroySampler(owned->sampler);
794 owned->samplerState = sampler;
795 owned->sampler = createVkSampler(sampler, owned->mipLevels);
796 texture->sampler = sampler;
797 if (!owned->isCube) writeCombinedImageDescriptor(owned.get());
798 invalidateTextureBindings();
799 return;
800 }
801}
802
804 if (!texture) return false;
806 if (!texture->gpuHandle) {
807 auto texIt = std::find_if(ownedTextures.begin(), ownedTextures.end(),
808 [&](const std::unique_ptr<Texture> &t) {
809 return t.get() == texture;
810 });
811 if (texIt == ownedTextures.end()) return false;
812 for (auto it = texturesByPath.begin(); it != texturesByPath.end();) {
813 if (it->second == texture)
814 it = texturesByPath.erase(it);
815 else
816 ++it;
817 }
818 for (auto it = sharedTexturesByContent.begin(); it != sharedTexturesByContent.end();) {
819 if (it->second == texture)
820 it = sharedTexturesByContent.erase(it);
821 else
822 ++it;
823 }
824 (void)texIt->release();
825 ownedTextures.erase(texIt);
826 return true;
827 }
828 // Renderer-owned fallback textures must never be released by callers.
829 if (texture == whiteTexture || texture == flatNormalTexture || texture == flatNormalTexture3D ||
830 texture == defaultEnvCubemap || texture == defaultExtrasArray || texture == defaultColorsArray ||
831 texture == defaultVertexArray || texture == defaultMotionArray || texture == defaultVegetationFadeNoise)
832 return false;
833
834 auto *gpu = static_cast<GpuTexture *>(texture->gpuHandle);
835 auto gpuIt = std::find_if(ownedGpuTextures.begin(), ownedGpuTextures.end(),
836 [&](const std::unique_ptr<GpuTexture> &g) {
837 return g.get() == gpu;
838 });
839 if (gpuIt == ownedGpuTextures.end()) return false;
840
841 auto texIt = std::find_if(ownedTextures.begin(), ownedTextures.end(),
842 [&](const std::unique_ptr<Texture> &t) {
843 return t.get() == texture;
844 });
845 if (texIt == ownedTextures.end()) return false;
846
847 // Path-cached textures must leave the hot-reload cache once released.
848 for (auto it = texturesByPath.begin(); it != texturesByPath.end();) {
849 if (it->second == texture)
850 it = texturesByPath.erase(it);
851 else
852 ++it;
853 }
854 for (auto it = sharedTexturesByContent.begin(); it != sharedTexturesByContent.end();) {
855 if (it->second == texture)
856 it = sharedTexturesByContent.erase(it);
857 else
858 ++it;
859 }
860
861 // In-flight frames may still sample the image / sampler; drain first.
862 waitForSharedGpuResources();
863 unregisterBindlessTexture(gpu);
864 if ((*gpuIt)->sampler) device->destroySampler((*gpuIt)->sampler);
865 texture->gpuHandle = nullptr;
866 ownedGpuTextures.erase(gpuIt);
867 // Transfer the CPU facade to the caller instead of destroying it.
868 (void)texIt->release();
869 ownedTextures.erase(texIt);
870 return true;
871}
872
873bool Graphics::replaceTexturePixels(Texture *tex, image::ImageData *data) {
874 if (!tex || !data) return false;
875 if (data->getFormat() != "RGBA8") return false;
876 return replaceTexturePixelsRGBA(tex, data->getWidth(), data->getHeight(),
877 static_cast<const uint8_t *>(data->getData()));
878}
879
880bool Graphics::updateTexture(Texture *tex, int width, int height, const uint8_t *rgba) {
881 if (!tex || width <= 0 || height <= 0 || !rgba) return false;
882 if (width != tex->width || height != tex->height) return false;
883 // Dynamic single-mip textures keep their image, sampler and descriptor set.
884 // Replacing the complete texture here used to allocate one descriptor set
885 // per frame from the shared pool, eventually raising ErrorOutOfPoolMemory.
886 if (tex->mipmapCount == 1) {
887 const auto bytes = std::span<const uint8_t>(rgba, size_t(width) * size_t(height) * 4u);
888 return updateTextureRegion(tex, 0, 0, width, height, bytes, size_t(width) * 4u).ok();
889 }
890 return replaceTexturePixelsRGBA(tex, width, height, rgba);
891}
892
894 int height) {
895 constexpr const char* context = "graphics.updateTextureFromResidentRgba8";
896 if (!tex || width <= 0 || height <= 0 || width != tex->width || height != tex->height || tex->mipmapCount != 1)
899 "Resident upload requires a matching owned single-mip texture", context));
900 const uint64_t required = uint64_t(width) * uint64_t(height) * 4u;
901 if (source.backend != GpuResidentBackend::Vulkan || source.nativeHandle == 0 ||
902 source.offsetBytes > source.sizeBytes || required > source.sizeBytes - source.offsetBytes ||
903 (source.offsetBytes & 3u) != 0)
907 "Resident RGBA8 buffer is incompatible with the Vulkan texture", context));
908
909 auto* gpu = static_cast<GpuTexture*>(tex->gpuHandle);
910 const auto owned =
911 std::find_if(ownedGpuTextures.begin(), ownedGpuTextures.end(),
912 [gpu](const std::unique_ptr<GpuTexture>& candidate) { return candidate.get() == gpu; });
913 if (!initialized || !gpu || owned == ownedGpuTextures.end() || gpu->isCube)
915 "Texture is not an owned Vulkan 2D texture", context));
916
917 VkBuffer rawBuffer = VK_NULL_HANDLE;
918 static_assert(sizeof(rawBuffer) <= sizeof(source.nativeHandle));
919 std::memcpy(&rawBuffer, &source.nativeHandle, sizeof(rawBuffer));
920 const vk::Buffer buffer(rawBuffer);
921
922 waitForSharedGpuResources();
923 vkb::executeImmediately(
924 device.instance, uploadPool, device.getQueue(vkb::QueueType::graphics), [&](vk::CommandBuffer cb) {
925 gpu->image.setLayout(cb, vk::ImageLayout::eTransferDstOptimal);
926 vk::BufferImageCopy copy{};
927 copy.bufferOffset = source.offsetBytes;
928 copy.imageSubresource = {vk::ImageAspectFlagBits::eColor, 0, 0, 1};
929 copy.imageExtent = vk::Extent3D{uint32_t(width), uint32_t(height), 1};
930 cb.copyBufferToImage(buffer, gpu->image.image(), vk::ImageLayout::eTransferDstOptimal, copy);
931
932 vk::ImageMemoryBarrier barrier{};
933 barrier.srcAccessMask = vk::AccessFlagBits::eTransferWrite;
934 barrier.dstAccessMask = vk::AccessFlagBits::eShaderRead;
935 barrier.oldLayout = vk::ImageLayout::eTransferDstOptimal;
936 barrier.newLayout = vk::ImageLayout::eShaderReadOnlyOptimal;
937 barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
938 barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
939 barrier.image = gpu->image.image();
940 barrier.subresourceRange = {vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1};
941 cb.pipelineBarrier(vk::PipelineStageFlagBits::eTransfer,
942 vk::PipelineStageFlagBits::eVertexShader | vk::PipelineStageFlagBits::eFragmentShader |
943 vk::PipelineStageFlagBits::eComputeShader,
944 {}, 0, nullptr, 0, nullptr, 1, &barrier);
945 gpu->image.setCurrentLayout(vk::ImageLayout::eShaderReadOnlyOptimal);
946 });
948}
949
951 int height,
952 std::span<const std::uint8_t> rgba,
953 std::size_t bytesPerRow) {
954 const TextureRegionUpload upload{x, y, width, height, rgba, bytesPerRow};
955 return updateTextureRegions(tex, std::span<const TextureRegionUpload>(&upload, 1));
956}
957
959 Texture *tex, std::span<const TextureRegionUpload> regions) {
960 if (!tex)
962 eve::DiagnosticCode::InvalidArgument, "texture must be non-null",
963 "graphics.updateTextureRegions.texture"));
964 if (tex->mipmapCount != 1)
966 eve::DiagnosticCode::Unsupported, "partial updates require a single-mip texture",
967 "graphics.updateTextureRegions.mipmaps"));
968
969 auto *gpu = static_cast<GpuTexture *>(tex->gpuHandle);
970 const auto owned = std::find_if(ownedGpuTextures.begin(), ownedGpuTextures.end(),
971 [gpu](const std::unique_ptr<GpuTexture> &candidate) {
972 return candidate.get() == gpu;
973 });
974 if (!gpu || owned == ownedGpuTextures.end() || gpu->isCube)
976 eve::DiagnosticCode::InvalidArgument, "texture is not an owned 2D texture",
977 "graphics.updateTextureRegions.texture"));
978 if (regions.empty()) return eve::Result<void>::success();
979
980 std::vector<std::size_t> packedOffsets;
981 packedOffsets.reserve(regions.size());
982 std::size_t packedSize = 0;
983 for (const TextureRegionUpload &region : regions) {
984 if (region.x < 0 || region.y < 0 || region.width <= 0 || region.height <= 0 ||
985 region.x > tex->width - region.width || region.y > tex->height - region.height)
987 eve::DiagnosticCode::InvalidArgument, "invalid texture region",
988 "graphics.updateTextureRegions.region"));
989 const std::size_t packedRow = std::size_t(region.width) * 4U;
990 const std::size_t stride = region.bytesPerRow == 0 ? packedRow : region.bytesPerRow;
991 const std::size_t requiredBytes = stride * std::size_t(region.height - 1) + packedRow;
992 if (stride < packedRow || region.rgba.size() < requiredBytes)
995 "source bytes do not cover the texture region",
996 "graphics.updateTextureRegions.bytes"));
997 packedOffsets.push_back(packedSize);
998 packedSize += packedRow * std::size_t(region.height);
999 }
1000
1001 std::vector<std::uint8_t> packed(packedSize);
1002 for (std::size_t index = 0; index < regions.size(); ++index) {
1003 const TextureRegionUpload &region = regions[index];
1004 const std::size_t packedRow = std::size_t(region.width) * 4U;
1005 const std::size_t stride = region.bytesPerRow == 0 ? packedRow : region.bytesPerRow;
1006 for (int row = 0; row < region.height; ++row)
1007 std::copy_n(region.rgba.data() + std::size_t(row) * stride, packedRow,
1008 packed.data() + packedOffsets[index] + std::size_t(row) * packedRow);
1009 }
1010 vkb::GenericBuffer staging(
1011 device, vk::BufferUsageFlagBits::eTransferSrc, vk::DeviceSize(packed.size()),
1012 vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent);
1013 staging.updateLocal(vkb::FrameSlot::gpuIdle(), packed.data(), vk::DeviceSize(packed.size()));
1014
1015 waitForSharedGpuResources();
1016 vkb::executeImmediately(device.instance, uploadPool,
1017 device.getQueue(vkb::QueueType::graphics),
1018 [&](vk::CommandBuffer cb) {
1019 gpu->image.setLayout(cb, vk::ImageLayout::eTransferDstOptimal);
1020 std::vector<vk::BufferImageCopy> copies;
1021 copies.reserve(regions.size());
1022 for (std::size_t index = 0; index < regions.size(); ++index) {
1023 const TextureRegionUpload &source = regions[index];
1024 vk::BufferImageCopy copy{};
1025 copy.bufferOffset = packedOffsets[index];
1026 copy.imageSubresource = {vk::ImageAspectFlagBits::eColor, 0, 0, 1};
1027 copy.imageOffset = vk::Offset3D{source.x, source.y, 0};
1028 copy.imageExtent = vk::Extent3D{std::uint32_t(source.width),
1029 std::uint32_t(source.height), 1};
1030 copies.push_back(copy);
1031 }
1032 cb.copyBufferToImage(staging.buffer, gpu->image.image(),
1033 vk::ImageLayout::eTransferDstOptimal, copies);
1034
1035 vk::ImageMemoryBarrier barrier{};
1036 barrier.srcAccessMask = vk::AccessFlagBits::eTransferWrite;
1037 barrier.dstAccessMask = vk::AccessFlagBits::eShaderRead;
1038 barrier.oldLayout = vk::ImageLayout::eTransferDstOptimal;
1039 barrier.newLayout = vk::ImageLayout::eShaderReadOnlyOptimal;
1040 barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
1041 barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
1042 barrier.image = gpu->image.image();
1043 barrier.subresourceRange = {vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1};
1044 cb.pipelineBarrier(vk::PipelineStageFlagBits::eTransfer,
1045 vk::PipelineStageFlagBits::eVertexShader |
1046 vk::PipelineStageFlagBits::eFragmentShader |
1047 vk::PipelineStageFlagBits::eComputeShader,
1048 {}, 0, nullptr, 0, nullptr, 1, &barrier);
1049 gpu->image.setCurrentLayout(vk::ImageLayout::eShaderReadOnlyOptimal);
1050 });
1051 staging.release();
1053}
1054
1055bool Graphics::replaceTexturePixelsRGBA(Texture *tex, int w, int h, const uint8_t *rgba) {
1056 if (!tex || !rgba || w <= 0 || h <= 0 || !initialized) return false;
1057
1059 info.sampler = tex->sampler;
1060 info.generateMipmaps = tex->mipmapCount > 1;
1061 info = normalizeTextureInfo(info);
1062 const uint32_t mipLevels =
1063 info.generateMipmaps ? uint32_t(mipmapCountForSize(w, h)) : 1u;
1064
1065 auto gpu = std::make_unique<GpuTexture>();
1066 gpu->width = w;
1067 gpu->height = h;
1068 gpu->isCube = false;
1069 gpu->mipLevels = mipLevels;
1070 gpu->samplerState = info.sampler;
1071 gpu->image = vkb::TextureImage2D(device, uint32_t(w), uint32_t(h), mipLevels);
1072 std::vector<uint8_t> bytes =
1073 (mipLevels > 1) ? buildMipChain2D(rgba, uint32_t(w), uint32_t(h), mipLevels)
1074 : std::vector<uint8_t>(rgba, rgba + size_t(w) * size_t(h) * 4);
1076 device.getQueue(vkb::QueueType::graphics), gpu->image,
1077 uint32_t(w), uint32_t(h), mipLevels, 1, bytes);
1078
1079 gpu->sampler = createVkSampler(info.sampler, mipLevels);
1080
1081 auto sets = vkb::DescriptorSetBuilder().layout(texSetLayout).build(device.instance, descriptorPool);
1082
1083 gpu->descriptorSet = vkb::BoundSet{sets[0]};
1084 writeCombinedImageDescriptor(gpu.get());
1085 registerBindlessTexture2D(gpu.get());
1086
1087 void *oldHandle = tex->gpuHandle;
1088 for (auto &owned : ownedGpuTextures) {
1089 if (owned.get() != oldHandle) continue;
1090 // Destroying the old image/sampler while an in-flight frame still
1091 // samples it is a typical TDR. Drain first, then drop cached sets.
1092 waitForSharedGpuResources();
1093 unregisterBindlessTexture(static_cast<GpuTexture *>(oldHandle));
1094 if (owned->sampler) device->destroySampler(owned->sampler);
1095 owned = std::move(gpu);
1096 tex->gpuHandle = owned.get();
1097 tex->width = w;
1098 tex->height = h;
1099 tex->pixelWidth = w;
1100 tex->pixelHeight = h;
1101 tex->mipmapCount = int(mipLevels);
1102 tex->sampler = info.sampler;
1103 registerBindlessTexture2D(owned.get());
1104 invalidateTextureBindings();
1105 return true;
1106 }
1107
1108 // Texture not in owned list — attach as new ownership.
1109 tex->gpuHandle = gpu.get();
1110 registerBindlessTexture2D(static_cast<GpuTexture *>(tex->gpuHandle));
1111 tex->width = w;
1112 tex->height = h;
1113 tex->pixelWidth = w;
1114 tex->pixelHeight = h;
1115 tex->mipmapCount = int(mipLevels);
1116 tex->sampler = info.sampler;
1117 ownedGpuTextures.push_back(std::move(gpu));
1118 return true;
1119}
1120
1121Texture *Graphics::newTextureFromFile(const std::string &filename) {
1122 ASSERT(!filename.empty());
1123 if (filename.empty()) throw Exception("newTextureFromFile: empty filename");
1124
1125 const std::string key = normalizeTexPath(filename);
1127 throw Exception("Could not load image file: %s", filename.c_str());
1128
1129 auto it = texturesByPath.find(key);
1130 if (it != texturesByPath.end() && it->second) {
1132 if (it->second->hasDeferredFilePixels()) return it->second;
1134 if (!waited.ok()) throw Exception("%s", waited.status().describe().c_str());
1135 auto *data = dynamic_cast<image::ImageData *>(&waited.value().get());
1136 if (!data || !replaceTexturePixels(it->second, data))
1137 throw Exception("newTextureFromFile: reload failed '%s'", filename.c_str());
1138 return it->second;
1139 }
1140
1142 auto tex = std::make_unique<Texture>();
1143 tex->markDeferredFilePixels(this);
1144 Texture *raw = tex.get();
1145 ownedTextures.push_back(std::move(tex));
1146 texturesByPath[key] = raw;
1147 deferredFileTextures_.push_back({key, raw});
1148 return raw;
1149}
1150
1152 return replaceTexturePixels(texture, data);
1153}
1154
1155bool Graphics::reloadTextureFromFile(const std::string &filename) {
1156 if (filename.empty()) return false;
1157 const std::string key = normalizeTexPath(filename);
1158 auto it = texturesByPath.find(key);
1159 if (it == texturesByPath.end() || !it->second) return false;
1160
1162 // The provider hands back a cache-owned ImageData; the pin keeps it alive until
1163 // the pixels have been copied out of it.
1164 image::ImageData *data = nullptr;
1165 eve::ResourcePin keepAlive;
1166 try {
1167 auto *imgMod = image::Image::create();
1168 data = imgMod->newImageDataFromFile(filename);
1169 if (data != nullptr) {
1171 if (!pinned.ok()) return false;
1172 keepAlive = std::move(pinned).takeValue();
1173 // The pin is the authority from here on; the borrowed pointer may have gone
1174 // stale before the pin was taken.
1175 data = static_cast<image::ImageData *>(keepAlive.get());
1176 }
1177 } catch (...) {
1178 return false;
1179 }
1180 if (!data) return false;
1181 return replaceTexturePixels(it->second, data);
1182}
1183
1184} // namespace eve::graphics::vulkan
LogicalId target
float w
Definition AnimClip.cpp:738
float y
Definition AnimClip.cpp:738
float x
Definition AnimClip.cpp:738
ActiveSource owned
Stable, structured diagnostics shared by engine modules.
std::string layout
std::string message
DiagnosticCode code
float maximum[3]
tensor::Graph g
Definition GpuGraph.cpp:7
vkb::Device & device
std::uint32_t key
vk::UniqueSampler sampler
vk::UniqueImage image
float u
Definition Grass.cpp:233
eve::ResourcePin pinned
int h
std::uint32_t height
std::uint32_t width
size_t offset
bool required
std::uint64_t bytes
float roughness
std::vector< std::shared_ptr< DeviceBytes > > bindings
Definition OnnxGpgpu.cpp:38
std::unique_ptr< gpgpu::GpuBuffer > buffer
Definition OnnxGpgpu.cpp:26
std::string error
Definition Package.cpp:60
TileLayer * layer
float t
glm::mat4 view
Move-only, checked operation results for the common layer.
bool found
bool repeatV
bool repeatU
std::uint32_t count
float size
Definition TreeMesh.cpp:156
uint32_t index
const UnitySourceAsset & source
std::uint32_t depth
const AssetImportLimits & limits
const VegetationPresetContext & context
static Diagnostic error(DiagnosticCode code, std::string message, std::string path={}, DiagnosticDetails details={}, std::string source={})
Construct an error diagnostic with the standard error severity.
Definition Diagnostic.h:125
EVENGINE_API_FOUNDATION public API.
Definition Exception.h:13
eve::Result< ResourcePin > pin(Resource *resource)
Keeps one cached resource alive across unload()/clear()/reload.
Definition Resource.cpp:311
ResultRef< Resource > waitFor(std::string key)
Block until key is cached, failed, or unclaimed.
Definition Resource.cpp:226
static ResourceManager & getInstance()
Returns the instance.
Definition Resource.cpp:15
bool ok() const noexcept
Whether this result represents a non-failure outcome.
Definition Result.h:480
Move-only operation result carrying either a value or Status.
Definition Result.h:155
static Result success(T value)
Construct a successful result owning value.
Definition Result.h:164
static Result failure(Status status)
Construct a failed result from a structured status.
Definition Result.h:175
Canvas public API.
Definition Canvas.h:17
virtual bool updateTexture(Texture *texture, int width, int height, const uint8_t *rgba)=0
Replace an existing texture's pixels in place (pointer stays stable).
virtual bool copyHDRCanvasesToCubemap(Canvas *const *sources, int faceCount, Texture *cubemap)
Copy consecutive RGBA16F canvases into cubemap base-level faces.
Definition Graphics.h:590
std::vector< DeferredFileTexture > deferredFileTextures_
Definition Graphics.h:2239
virtual bool releaseTexture(Texture *texture)
Eagerly releases a texture created by this Graphics.
Definition Graphics.h:733
void push(bool all)
Pushes .
virtual void setTextureSampler(Texture *texture, const std::string &filter, const std::string &mipmap, float maxAnisotropy, float lodBias)
Update sampler state without re-uploading pixels (filter / mip / aniso / LOD bias)....
virtual bool copyHDRCanvasToCubemapFace(Canvas *source, Texture *cubemap, int face)
Copy one RGBA16F Canvas into a staging cubemap base-level face.
Definition Graphics.h:572
virtual eve::Result< void > updateTextureFromResidentRgba8(Texture *texture, const GpuResidentBufferView &source, int width, int height)
Copy a same-device resident RGBA8 buffer into an existing texture.
Definition Graphics.h:643
bool fileTextureSourceExists(const std::string &filename) const
File texture source exists.
void ensureFileTexturesReady()
Finish CPU decode and GPU upload for outstanding newTextureFromFile results. @thread Game/render thre...
virtual bool filterHDRReflectionCubemap(Texture *cubemap, int sampleCount=64)
Generate GGX specular mips and final diffuse irradiance for an HDR cubemap. @compatibility Preserves ...
Definition Graphics.h:601
virtual Texture * newTexture(int width, int height, const uint8_t *rgba, bool repeatU=false, bool repeatV=false)=0
Creates a texture. @ownership Caller deletes unless documented otherwise.
virtual bool reloadTextureFromFile(const std::string &filename)=0
Reload a path-cached texture from disk in place (pointer stable). False if unbound.
virtual Texture * newTextureFromFile(const std::string &filename)=0
Creates a texture from file. @ownership Caller deletes unless documented otherwise.
void requestFileImageDecode(const std::string &key)
Request file image decode.
virtual eve::Result< void > updateTextureRegions(Texture *texture, std::span< const TextureRegionUpload > regions)=0
Validate then upload multiple independent mip-zero RGBA8 regions as one batch.
virtual bool uploadDeferredFileTexture(Texture *texture, image::ImageData *data)
Uploads deferred file texture.
void dropDeferredFileTexture(Texture *texture)
Drop deferred file texture.
virtual eve::Result< void > updateTextureRegion(Texture *texture, int x, int y, int width, int height, std::span< const std::uint8_t > rgba, std::size_t bytesPerRow=0)=0
Upload one tightly packed or row-strided RGBA8 rectangle into mip level zero.
virtual ResultRef< Texture > newSharedTexture(image::ImageData *data, const std::string &contentKey)
Reuse or upload immutable RGBA8 pixels under a caller-provided content key.
GPU texture created via Graphics::newTexture. Owns GPU resources through an opaque backend handle.
Definition Texture.h:18
TextureSampler sampler
Definition Texture.h:63
Result< Texture * > newTextureArrayRgba16f(uint32_t width, uint32_t height, uint32_t layers, std::span< const uint16_t > rgbaHalf) override
Creates a texture array rgba 16 f. @ownership Caller deletes unless documented otherwise.
Texture * newHDRCubemap(int faceSize) override
Creates a hdr cubemap. @ownership Caller deletes unless documented otherwise.
Result< Texture * > newTextureMipChain(uint32_t width, uint32_t height, uint32_t levels, std::span< const uint8_t > rgba) override
Creates a texture mip chain. @ownership Caller deletes unless documented otherwise.
Texture * newCubemap(int faceSize, const uint8_t *rgbaFaces) override
Creates a cubemap. @ownership Caller deletes unless documented otherwise.
Texture * newTexture(int width, int height, const uint8_t *rgba, bool repeatU=false, bool repeatV=false) override
Creates a texture. @ownership Caller deletes unless documented otherwise.
Result< Texture * > newTexture3DRgba8(uint32_t width, uint32_t height, uint32_t depth, std::span< const uint8_t > rgba) override
Creates a texture 3 d rgba 8. @ownership Caller deletes unless documented otherwise.
OffscreenCanvas public API.
Definition Canvas.h:15
Represents raw pixel data.
Definition ImageData.h:38
RAII keep-alive for one RuntimeObjectRegistry entry.
T * get() const noexcept
Borrows the pinned object; valid until the pin is released.
void uploadTextureForAllShaderStages(vkb::Device &device, vk::CommandPool commandPool, vk::Queue graphicsQueue, TextureImage &image, uint32_t width, uint32_t height, uint32_t mipLevels, uint32_t layers, const std::vector< uint8_t > &bytes, uint32_t bytesPerPixel=4, uint32_t depth=1)
void recordTextureCopies(vk::CommandBuffer cb, TextureImage &image, vkb::GenericBuffer &staging, uint32_t width, uint32_t height, uint32_t depth, uint32_t mipLevels, uint32_t layers, uint32_t bytesPerPixel=4)
int mipmapCountForSize(int width, int height)
Full mip chain count for a 2D image (including base level).
DiagnosticCode
Stable machine-readable diagnostic codes.
Definition Diagnostic.h:47
Transient backend buffer slice shared between sibling GPU modules. @ownership Non-owning; nativeHandl...
Options for Graphics::newTexture / newCubemap. When generateMipmaps is true and sampler....
static TextureCreateInfo withMipmaps(bool aniso=true, float maxAniso=16.f)
With mipmaps.
One borrowed RGBA8 source rectangle for a batched texture update. @ownership rgba remains owned by th...
Definition Graphics.h:70
std::span< const std::uint8_t > rgba
Definition Graphics.h:75
Sampler state for a Texture (filter, wrap, mip LOD, anisotropy). Defaults match historical engine beh...
static TextureSampler linear()
Linear.
static TextureSampler linearMipmap()
Trilinear (linear + linear mips). Caller should create the texture with generateMipmaps.
GpuTexture public API.
Definition Graphics.h:257
glm::uvec4 info