VisuTwin Canvas
C++ 3D Engine — Metal Backend
Loading...
Searching...
No Matches
vulkanGraphicsDevice.cpp
Go to the documentation of this file.
1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2025-2026 Arnis Lektauers
3//
4// Vulkan implementation of the graphics device.
5//
6
7#ifdef VISUTWIN_HAS_VULKAN
8
9#define VMA_IMPLEMENTATION
11
12#include <cstring>
13#include <VkBootstrap.h>
14#include <SDL3/SDL_vulkan.h>
15
16#include "vulkanIndexBuffer.h"
18#include "vulkanShader.h"
19#include "vulkanTexture.h"
20#include "vulkanUtils.h"
21#include "vulkanVertexBuffer.h"
22
26#include "spdlog/spdlog.h"
27
28// Embedded SPIR-V for the basic forward shader.
29#include "engine/shaders/vulkan/forward_basic_spirv.h"
30
31namespace visutwin::canvas
32{
33 // ─────────────────────────────────────────────────────────────────────
34 // Construction / Destruction
35 // ─────────────────────────────────────────────────────────────────────
36
37 VulkanGraphicsDevice::VulkanGraphicsDevice(const GraphicsDeviceOptions& options)
38 {
39 _window = options.window;
40
41 int w = 0, h = 0;
42 SDL_GetWindowSize(_window, &w, &h);
43 _width = w;
44 _height = h;
45
46 initInstance(_window);
47 initDevice();
48
49 // VMA allocator
50 VmaAllocatorCreateInfo allocatorInfo{};
51 allocatorInfo.physicalDevice = _physicalDevice;
52 allocatorInfo.device = _device;
53 allocatorInfo.instance = _instance;
54 allocatorInfo.vulkanApiVersion = VK_API_VERSION_1_3;
55 vmaCreateAllocator(&allocatorInfo, &_vmaAllocator);
56
57 initSwapchain(_width, _height);
58 createDepthResources();
59 createPerFrameResources();
60
61 // Upload command pool + fence (for staging transfers)
62 VkCommandPoolCreateInfo uploadPoolInfo{VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO};
63 uploadPoolInfo.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT;
64 uploadPoolInfo.queueFamilyIndex = _graphicsQueueFamily;
65 vkCreateCommandPool(_device, &uploadPoolInfo, nullptr, &_uploadCommandPool);
66
67 VkFenceCreateInfo uploadFenceInfo{VK_STRUCTURE_TYPE_FENCE_CREATE_INFO};
68 vkCreateFence(_device, &uploadFenceInfo, nullptr, &_uploadFence);
69
70 // Descriptor pool
71 std::array<VkDescriptorPoolSize, 2> poolSizes{};
72 poolSizes[0] = {VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, 256};
73 poolSizes[1] = {VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 1024};
74
75 VkDescriptorPoolCreateInfo dpInfo{VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO};
76 dpInfo.maxSets = 512;
77 dpInfo.poolSizeCount = static_cast<uint32_t>(poolSizes.size());
78 dpInfo.pPoolSizes = poolSizes.data();
79 dpInfo.flags = VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT;
80 vkCreateDescriptorPool(_device, &dpInfo, nullptr, &_descriptorPool);
81
82 // Render pipeline
83 _renderPipeline = std::make_unique<VulkanRenderPipeline>(this);
84
85 // Default sampler
86 VkSamplerCreateInfo samplerInfo{VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO};
87 samplerInfo.magFilter = VK_FILTER_LINEAR;
88 samplerInfo.minFilter = VK_FILTER_LINEAR;
89 samplerInfo.mipmapMode = VK_SAMPLER_MIPMAP_MODE_LINEAR;
90 samplerInfo.addressModeU = VK_SAMPLER_ADDRESS_MODE_REPEAT;
91 samplerInfo.addressModeV = VK_SAMPLER_ADDRESS_MODE_REPEAT;
92 samplerInfo.addressModeW = VK_SAMPLER_ADDRESS_MODE_REPEAT;
93 samplerInfo.maxLod = VK_LOD_CLAMP_NONE;
94 vkCreateSampler(_device, &samplerInfo, nullptr, &_defaultSampler);
95
96 // 1×1 white texture (fallback for unbound texture slots)
97 {
98 VkImageCreateInfo imgInfo{VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO};
99 imgInfo.imageType = VK_IMAGE_TYPE_2D;
100 imgInfo.format = VK_FORMAT_R8G8B8A8_UNORM;
101 imgInfo.extent = {1, 1, 1};
102 imgInfo.mipLevels = 1;
103 imgInfo.arrayLayers = 1;
104 imgInfo.samples = VK_SAMPLE_COUNT_1_BIT;
105 imgInfo.tiling = VK_IMAGE_TILING_OPTIMAL;
106 imgInfo.usage = VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT;
107
108 VmaAllocationCreateInfo aInfo{};
109 aInfo.usage = VMA_MEMORY_USAGE_GPU_ONLY;
110 vmaCreateImage(_vmaAllocator, &imgInfo, &aInfo, &_whiteImage, &_whiteAllocation, nullptr);
111
112 VkImageViewCreateInfo viewInfo{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO};
113 viewInfo.image = _whiteImage;
114 viewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D;
115 viewInfo.format = VK_FORMAT_R8G8B8A8_UNORM;
116 viewInfo.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
117 vkCreateImageView(_device, &viewInfo, nullptr, &_whiteImageView);
118
119 // Upload single white pixel
120 uint32_t whitePixel = 0xFFFFFFFF;
121 VkBuffer stagingBuf;
122 VmaAllocation stagingAlloc;
123 VkBufferCreateInfo sInfo{VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO};
124 sInfo.size = 4;
125 sInfo.usage = VK_BUFFER_USAGE_TRANSFER_SRC_BIT;
126 VmaAllocationCreateInfo saInfo{};
127 saInfo.usage = VMA_MEMORY_USAGE_CPU_ONLY;
128 vmaCreateBuffer(_vmaAllocator, &sInfo, &saInfo, &stagingBuf, &stagingAlloc, nullptr);
129 void* mapped;
130 vmaMapMemory(_vmaAllocator, stagingAlloc, &mapped);
131 memcpy(mapped, &whitePixel, 4);
132 vmaUnmapMemory(_vmaAllocator, stagingAlloc);
133
134 vulkanImmediateSubmit(this, [&](VkCommandBuffer cmd) {
135 vulkanTransitionImageLayout(cmd, _whiteImage,
136 VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL);
137 VkBufferImageCopy region{};
138 region.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1};
139 region.imageExtent = {1, 1, 1};
140 vkCmdCopyBufferToImage(cmd, stagingBuf, _whiteImage,
141 VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &region);
142 vulkanTransitionImageLayout(cmd, _whiteImage,
143 VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
144 VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL);
145 });
146 vmaDestroyBuffer(_vmaAllocator, stagingBuf, stagingAlloc);
147 }
148
149 spdlog::info("VulkanGraphicsDevice initialized ({}x{})", _width, _height);
150 }
151
152 VulkanGraphicsDevice::~VulkanGraphicsDevice()
153 {
154 if (_device != VK_NULL_HANDLE)
155 vkDeviceWaitIdle(_device);
156
157 _renderPipeline.reset();
158
159 if (_defaultSampler != VK_NULL_HANDLE)
160 vkDestroySampler(_device, _defaultSampler, nullptr);
161 if (_whiteImageView != VK_NULL_HANDLE)
162 vkDestroyImageView(_device, _whiteImageView, nullptr);
163 if (_whiteImage != VK_NULL_HANDLE)
164 vmaDestroyImage(_vmaAllocator, _whiteImage, _whiteAllocation);
165
166 if (_descriptorPool != VK_NULL_HANDLE)
167 vkDestroyDescriptorPool(_device, _descriptorPool, nullptr);
168
169 destroyPerFrameResources();
170
171 if (_uploadFence != VK_NULL_HANDLE)
172 vkDestroyFence(_device, _uploadFence, nullptr);
173 if (_uploadCommandPool != VK_NULL_HANDLE)
174 vkDestroyCommandPool(_device, _uploadCommandPool, nullptr);
175
176 destroyDepthResources();
177 cleanupSwapchain();
178
179 if (_vmaAllocator != VK_NULL_HANDLE)
180 vmaDestroyAllocator(_vmaAllocator);
181 if (_device != VK_NULL_HANDLE)
182 vkDestroyDevice(_device, nullptr);
183 if (_surface != VK_NULL_HANDLE)
184 vkDestroySurfaceKHR(_instance, _surface, nullptr);
185 if (_debugMessenger != VK_NULL_HANDLE) {
186 auto fn = reinterpret_cast<PFN_vkDestroyDebugUtilsMessengerEXT>(
187 vkGetInstanceProcAddr(_instance, "vkDestroyDebugUtilsMessengerEXT"));
188 if (fn) fn(_instance, _debugMessenger, nullptr);
189 }
190 if (_instance != VK_NULL_HANDLE)
191 vkDestroyInstance(_instance, nullptr);
192
193 spdlog::info("VulkanGraphicsDevice destroyed");
194 }
195
196 // ─────────────────────────────────────────────────────────────────────
197 // Initialization helpers
198 // ─────────────────────────────────────────────────────────────────────
199
200 void VulkanGraphicsDevice::initInstance(SDL_Window* window)
201 {
202 vkb::InstanceBuilder builder;
203 builder.set_app_name("VisuTwin Canvas")
204 .set_engine_name("VisuTwin")
205 .require_api_version(1, 3, 0)
206 .request_validation_layers(true)
207 .use_default_debug_messenger();
208
209 auto result = builder.build();
210 if (!result) {
211 spdlog::error("Failed to create Vulkan instance: {}", result.error().message());
212 return;
213 }
214 auto vkbInstance = result.value();
215 _instance = vkbInstance.instance;
216 _debugMessenger = vkbInstance.debug_messenger;
217
218 if (!SDL_Vulkan_CreateSurface(window, _instance, nullptr, &_surface)) {
219 spdlog::error("Failed to create Vulkan surface");
220 }
221 }
222
223 void VulkanGraphicsDevice::initDevice()
224 {
225 VkPhysicalDeviceVulkan13Features features13{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_3_FEATURES};
226 features13.dynamicRendering = VK_TRUE;
227 features13.synchronization2 = VK_TRUE;
228
229 vkb::PhysicalDeviceSelector selector{vkb::Instance{_instance, _debugMessenger}};
230 selector.set_surface(_surface)
231 .set_minimum_version(1, 3)
232 .set_required_features_13(features13);
233
234 auto physResult = selector.select();
235 if (!physResult) {
236 spdlog::error("Failed to select Vulkan physical device: {}", physResult.error().message());
237 return;
238 }
239 auto vkbPhysical = physResult.value();
240 _physicalDevice = vkbPhysical.physical_device;
241
242 VkPhysicalDeviceProperties props;
243 vkGetPhysicalDeviceProperties(_physicalDevice, &props);
244 spdlog::info("Vulkan device: {}", props.deviceName);
245
246 vkb::DeviceBuilder deviceBuilder{vkbPhysical};
247 auto devResult = deviceBuilder.build();
248 if (!devResult) {
249 spdlog::error("Failed to create Vulkan device: {}", devResult.error().message());
250 return;
251 }
252 auto vkbDevice = devResult.value();
253 _device = vkbDevice.device;
254
255 auto qr = vkbDevice.get_queue(vkb::QueueType::graphics);
256 if (qr) _graphicsQueue = qr.value();
257 auto qi = vkbDevice.get_queue_index(vkb::QueueType::graphics);
258 if (qi) _graphicsQueueFamily = qi.value();
259 }
260
261 void VulkanGraphicsDevice::initSwapchain(int width, int height)
262 {
263 vkb::SwapchainBuilder swapBuilder{_physicalDevice, _device, _surface};
264 swapBuilder.set_desired_extent(static_cast<uint32_t>(width), static_cast<uint32_t>(height))
265 .set_desired_format({VK_FORMAT_B8G8R8A8_SRGB, VK_COLOR_SPACE_SRGB_NONLINEAR_KHR})
266 .set_desired_present_mode(VK_PRESENT_MODE_FIFO_KHR)
267 .add_image_usage_flags(VK_IMAGE_USAGE_TRANSFER_DST_BIT);
268
269 auto result = swapBuilder.build();
270 if (!result) {
271 spdlog::error("Failed to create Vulkan swapchain: {}", result.error().message());
272 return;
273 }
274 auto vkbSwap = result.value();
275 _swapchain = vkbSwap.swapchain;
276 _swapchainFormat = vkbSwap.image_format;
277 _swapchainExtent = vkbSwap.extent;
278 _swapchainImages = vkbSwap.get_images().value();
279 _swapchainImageViews = vkbSwap.get_image_views().value();
280 }
281
282 void VulkanGraphicsDevice::cleanupSwapchain()
283 {
284 for (auto view : _swapchainImageViews)
285 vkDestroyImageView(_device, view, nullptr);
286 _swapchainImageViews.clear();
287 _swapchainImages.clear();
288 if (_swapchain != VK_NULL_HANDLE) {
289 vkDestroySwapchainKHR(_device, _swapchain, nullptr);
290 _swapchain = VK_NULL_HANDLE;
291 }
292 }
293
294 void VulkanGraphicsDevice::createDepthResources()
295 {
296 VkImageCreateInfo imageInfo{VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO};
297 imageInfo.imageType = VK_IMAGE_TYPE_2D;
298 imageInfo.format = _depthFormat;
299 imageInfo.extent = {_swapchainExtent.width, _swapchainExtent.height, 1};
300 imageInfo.mipLevels = 1;
301 imageInfo.arrayLayers = 1;
302 imageInfo.samples = VK_SAMPLE_COUNT_1_BIT;
303 imageInfo.tiling = VK_IMAGE_TILING_OPTIMAL;
304 imageInfo.usage = VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT;
305
306 VmaAllocationCreateInfo allocInfo{};
307 allocInfo.usage = VMA_MEMORY_USAGE_GPU_ONLY;
308 vmaCreateImage(_vmaAllocator, &imageInfo, &allocInfo,
309 &_depthImage, &_depthAllocation, nullptr);
310
311 VkImageViewCreateInfo viewInfo{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO};
312 viewInfo.image = _depthImage;
313 viewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D;
314 viewInfo.format = _depthFormat;
315 viewInfo.subresourceRange = {VK_IMAGE_ASPECT_DEPTH_BIT, 0, 1, 0, 1};
316 vkCreateImageView(_device, &viewInfo, nullptr, &_depthImageView);
317 }
318
319 void VulkanGraphicsDevice::destroyDepthResources()
320 {
321 if (_depthImageView != VK_NULL_HANDLE)
322 vkDestroyImageView(_device, _depthImageView, nullptr);
323 if (_depthImage != VK_NULL_HANDLE)
324 vmaDestroyImage(_vmaAllocator, _depthImage, _depthAllocation);
325 _depthImageView = VK_NULL_HANDLE;
326 _depthImage = VK_NULL_HANDLE;
327 _depthAllocation = VK_NULL_HANDLE;
328 }
329
330 void VulkanGraphicsDevice::createPerFrameResources()
331 {
332 for (auto& frame : _frames) {
333 VkCommandPoolCreateInfo poolInfo{VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO};
334 poolInfo.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT;
335 poolInfo.queueFamilyIndex = _graphicsQueueFamily;
336 vkCreateCommandPool(_device, &poolInfo, nullptr, &frame.commandPool);
337
338 VkCommandBufferAllocateInfo allocInfo{VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO};
339 allocInfo.commandPool = frame.commandPool;
340 allocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
341 allocInfo.commandBufferCount = 1;
342 vkAllocateCommandBuffers(_device, &allocInfo, &frame.commandBuffer);
343
344 VkSemaphoreCreateInfo semInfo{VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO};
345 vkCreateSemaphore(_device, &semInfo, nullptr, &frame.imageAvailable);
346 vkCreateSemaphore(_device, &semInfo, nullptr, &frame.renderFinished);
347
348 VkFenceCreateInfo fenceInfo{VK_STRUCTURE_TYPE_FENCE_CREATE_INFO};
349 fenceInfo.flags = VK_FENCE_CREATE_SIGNALED_BIT;
350 vkCreateFence(_device, &fenceInfo, nullptr, &frame.inFlightFence);
351 }
352 }
353
354 void VulkanGraphicsDevice::destroyPerFrameResources()
355 {
356 for (auto& frame : _frames) {
357 if (frame.inFlightFence != VK_NULL_HANDLE)
358 vkDestroyFence(_device, frame.inFlightFence, nullptr);
359 if (frame.renderFinished != VK_NULL_HANDLE)
360 vkDestroySemaphore(_device, frame.renderFinished, nullptr);
361 if (frame.imageAvailable != VK_NULL_HANDLE)
362 vkDestroySemaphore(_device, frame.imageAvailable, nullptr);
363 if (frame.commandPool != VK_NULL_HANDLE)
364 vkDestroyCommandPool(_device, frame.commandPool, nullptr);
365 frame = {};
366 }
367 }
368
369 // ─────────────────────────────────────────────────────────────────────
370 // Frame lifecycle
371 // ─────────────────────────────────────────────────────────────────────
372
373 void VulkanGraphicsDevice::onFrameStart()
374 {
375 auto& frame = _frames[_frameIndex];
376
377 vkWaitForFences(_device, 1, &frame.inFlightFence, VK_TRUE, UINT64_MAX);
378 vkResetFences(_device, 1, &frame.inFlightFence);
379
380 VkResult result = vkAcquireNextImageKHR(_device, _swapchain, UINT64_MAX,
381 frame.imageAvailable, VK_NULL_HANDLE, &_swapchainImageIndex);
382 if (result == VK_ERROR_OUT_OF_DATE_KHR) {
383 setResolution(_width, _height);
384 return;
385 }
386
387 vkResetCommandBuffer(frame.commandBuffer, 0);
388
389 VkCommandBufferBeginInfo beginInfo{VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO};
390 beginInfo.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;
391 vkBeginCommandBuffer(frame.commandBuffer, &beginInfo);
392
393 // Reset descriptor pool for this frame
394 // (simple approach: reset the whole pool each frame)
395 vkResetDescriptorPool(_device, _descriptorPool, 0);
396 }
397
398 void VulkanGraphicsDevice::onFrameEnd()
399 {
400 auto& frame = _frames[_frameIndex];
401 VkCommandBuffer cmd = frame.commandBuffer;
402
403 // Transition swapchain image → presentable
404 vulkanTransitionImageLayout(cmd, _swapchainImages[_swapchainImageIndex],
405 VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL,
406 VK_IMAGE_LAYOUT_PRESENT_SRC_KHR);
407
408 vkEndCommandBuffer(cmd);
409
410 // Submit
411 VkPipelineStageFlags waitStage = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
412 VkSubmitInfo submitInfo{VK_STRUCTURE_TYPE_SUBMIT_INFO};
413 submitInfo.waitSemaphoreCount = 1;
414 submitInfo.pWaitSemaphores = &frame.imageAvailable;
415 submitInfo.pWaitDstStageMask = &waitStage;
416 submitInfo.commandBufferCount = 1;
417 submitInfo.pCommandBuffers = &cmd;
418 submitInfo.signalSemaphoreCount = 1;
419 submitInfo.pSignalSemaphores = &frame.renderFinished;
420 vkQueueSubmit(_graphicsQueue, 1, &submitInfo, frame.inFlightFence);
421
422 // Present
423 VkPresentInfoKHR presentInfo{VK_STRUCTURE_TYPE_PRESENT_INFO_KHR};
424 presentInfo.waitSemaphoreCount = 1;
425 presentInfo.pWaitSemaphores = &frame.renderFinished;
426 presentInfo.swapchainCount = 1;
427 presentInfo.pSwapchains = &_swapchain;
428 presentInfo.pImageIndices = &_swapchainImageIndex;
429 vkQueuePresentKHR(_graphicsQueue, &presentInfo);
430
431 _frameIndex = (_frameIndex + 1) % kMaxFramesInFlight;
432 }
433
434 // ─────────────────────────────────────────────────────────────────────
435 // Render pass (dynamic rendering, Vulkan 1.3)
436 // ─────────────────────────────────────────────────────────────────────
437
438 void VulkanGraphicsDevice::startRenderPass(RenderPass* renderPass)
439 {
440 auto& frame = _frames[_frameIndex];
441 VkCommandBuffer cmd = frame.commandBuffer;
442
443 // Transition swapchain image → color attachment
444 vulkanTransitionImageLayout(cmd, _swapchainImages[_swapchainImageIndex],
445 VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL);
446
447 // Transition depth → depth attachment
448 VkImageMemoryBarrier depthBarrier{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER};
449 depthBarrier.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED;
450 depthBarrier.newLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL;
451 depthBarrier.srcAccessMask = 0;
452 depthBarrier.dstAccessMask = VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT;
453 depthBarrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
454 depthBarrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
455 depthBarrier.image = _depthImage;
456 depthBarrier.subresourceRange = {VK_IMAGE_ASPECT_DEPTH_BIT, 0, 1, 0, 1};
457 vkCmdPipelineBarrier(cmd,
458 VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT,
459 VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT,
460 0, 0, nullptr, 0, nullptr, 1, &depthBarrier);
461
462 // Read clear values from RenderPass
463 auto colorOps = renderPass ? renderPass->colorOps() : nullptr;
464
465 VkRenderingAttachmentInfo colorAttachment{VK_STRUCTURE_TYPE_RENDERING_ATTACHMENT_INFO};
466 colorAttachment.imageView = _swapchainImageViews[_swapchainImageIndex];
467 colorAttachment.imageLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
468 colorAttachment.storeOp = VK_ATTACHMENT_STORE_OP_STORE;
469
470 if (colorOps && colorOps->clear) {
471 colorAttachment.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR;
472 colorAttachment.clearValue.color = {{
473 colorOps->clearValue.r, colorOps->clearValue.g,
474 colorOps->clearValue.b, colorOps->clearValue.a}};
475 } else {
476 colorAttachment.loadOp = VK_ATTACHMENT_LOAD_OP_LOAD;
477 }
478
479 VkRenderingAttachmentInfo depthAttachment{VK_STRUCTURE_TYPE_RENDERING_ATTACHMENT_INFO};
480 depthAttachment.imageView = _depthImageView;
481 depthAttachment.imageLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL;
482 depthAttachment.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR;
483 depthAttachment.storeOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;
484 depthAttachment.clearValue.depthStencil = {1.0f, 0};
485
486 auto dsOps = renderPass ? renderPass->depthStencilOps() : nullptr;
487 if (dsOps) {
488 depthAttachment.loadOp = dsOps->clearDepth ? VK_ATTACHMENT_LOAD_OP_CLEAR : VK_ATTACHMENT_LOAD_OP_LOAD;
489 depthAttachment.storeOp = dsOps->storeDepth ? VK_ATTACHMENT_STORE_OP_STORE : VK_ATTACHMENT_STORE_OP_DONT_CARE;
490 depthAttachment.clearValue.depthStencil = {dsOps->clearDepthValue, 0};
491 }
492
493 VkRenderingInfo renderingInfo{VK_STRUCTURE_TYPE_RENDERING_INFO};
494 renderingInfo.renderArea = {{0, 0}, _swapchainExtent};
495 renderingInfo.layerCount = 1;
496 renderingInfo.colorAttachmentCount = 1;
497 renderingInfo.pColorAttachments = &colorAttachment;
498 renderingInfo.pDepthAttachment = &depthAttachment;
499
500 vkCmdBeginRendering(cmd, &renderingInfo);
501
502 // Negative viewport height for Y-flip (match Metal/OpenGL convention)
503 VkViewport viewport{};
504 viewport.x = _vx;
505 viewport.y = static_cast<float>(_swapchainExtent.height) - _vy;
506 viewport.width = _vw > 0 ? _vw : static_cast<float>(_swapchainExtent.width);
507 viewport.height = -(_vh > 0 ? _vh : static_cast<float>(_swapchainExtent.height));
508 viewport.minDepth = 0.0f;
509 viewport.maxDepth = 1.0f;
510 vkCmdSetViewport(cmd, 0, 1, &viewport);
511
512 VkRect2D scissor{};
513 scissor.offset = {_sx, _sy};
514 scissor.extent = {
515 _sw > 0 ? static_cast<uint32_t>(_sw) : _swapchainExtent.width,
516 _sh > 0 ? static_cast<uint32_t>(_sh) : _swapchainExtent.height
517 };
518 vkCmdSetScissor(cmd, 0, 1, &scissor);
519
520 _dynamicRenderingActive = true;
521 _insideRenderPass = true;
522 _currentPipeline = VK_NULL_HANDLE;
523 _pushConstantsDirty = true;
524 }
525
526 void VulkanGraphicsDevice::endRenderPass(RenderPass* renderPass)
527 {
528 (void)renderPass;
529 if (_dynamicRenderingActive) {
530 auto& frame = _frames[_frameIndex];
531 vkCmdEndRendering(frame.commandBuffer);
532 _dynamicRenderingActive = false;
533 }
534 _insideRenderPass = false;
535 }
536
537 // ─────────────────────────────────────────────────────────────────────
538 // Core rendering
539 // ─────────────────────────────────────────────────────────────────────
540
541 void VulkanGraphicsDevice::draw(const Primitive& primitive,
542 const std::shared_ptr<IndexBuffer>& indexBuffer,
543 int numInstances, int indirectSlot, bool first, bool last)
544 {
545 (void)indirectSlot;
546 if (!_shader || !_dynamicRenderingActive) return;
547
548 auto& frame = _frames[_frameIndex];
549 VkCommandBuffer cmd = frame.commandBuffer;
550
551 auto vulkanShader = std::dynamic_pointer_cast<VulkanShader>(_shader);
552 if (!vulkanShader || vulkanShader->vertexModule() == VK_NULL_HANDLE) return;
553
554 if (first) {
555 auto vf = !_vertexBuffers.empty() ? _vertexBuffers[0] : nullptr;
556
557 VkPipeline pipeline = _renderPipeline->get(primitive,
558 vf ? vf->format() : nullptr,
559 vulkanShader, _blendState, _depthState, _cullMode,
560 _swapchainFormat, _depthFormat);
561
562 if (pipeline != _currentPipeline) {
563 vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline);
564 _currentPipeline = pipeline;
565 _pushConstantsDirty = true;
566 }
567
568 // Bind vertex buffer
569 if (vf) {
570 auto* vb = static_cast<VulkanVertexBuffer*>(vf.get());
571 if (vb->buffer() != VK_NULL_HANDLE) {
572 VkBuffer buf = vb->buffer();
573 VkDeviceSize offset = 0;
574 vkCmdBindVertexBuffers(cmd, 0, 1, &buf, &offset);
575 }
576 }
577 }
578
579 // Push constants (transforms)
580 if (_pushConstantsDirty) {
581 vkCmdPushConstants(cmd, _renderPipeline->pipelineLayout(),
582 VK_SHADER_STAGE_VERTEX_BIT, 0, sizeof(PushConstants), &_pushConstants);
583 _pushConstantsDirty = false;
584 }
585
586 // Bind default texture descriptor set (set 1) if no material textures
587 // For now: bind the white fallback texture at binding 0
588 {
589 VkDescriptorSet texSet;
590 VkDescriptorSetAllocateInfo dsAlloc{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO};
591 dsAlloc.descriptorPool = _descriptorPool;
592 dsAlloc.descriptorSetCount = 1;
593 auto layout = _renderPipeline->textureSetLayout();
594 dsAlloc.pSetLayouts = &layout;
595
596 if (vkAllocateDescriptorSets(_device, &dsAlloc, &texSet) == VK_SUCCESS) {
597 // Determine texture to bind
598 VkImageView texView = _whiteImageView;
599 VkSampler texSampler = _defaultSampler;
600
601 // Check if material has a diffuse map (slot 0 = baseColorMap)
602 if (_material) {
603 std::vector<TextureSlot> texSlots;
604 _material->getTextureSlots(texSlots);
605 for (auto& ts : texSlots) {
606 if (ts.slot == 0 && ts.texture != nullptr) {
607 auto* impl = ts.texture->impl();
608 if (impl) {
609 auto* vkTex = static_cast<gpu::VulkanTexture*>(impl);
610 if (vkTex->imageView() != VK_NULL_HANDLE) {
611 texView = vkTex->imageView();
612 if (vkTex->sampler() != VK_NULL_HANDLE)
613 texSampler = vkTex->sampler();
614 }
615 }
616 break;
617 }
618 }
619 }
620
621 // Write all 6 texture bindings (use white fallback for unbound slots)
622 std::array<VkDescriptorImageInfo, 6> imageInfos{};
623 std::array<VkWriteDescriptorSet, 6> writes{};
624 for (uint32_t i = 0; i < 6; i++) {
625 imageInfos[i].sampler = _defaultSampler;
626 imageInfos[i].imageView = _whiteImageView;
627 imageInfos[i].imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
628
629 writes[i].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
630 writes[i].dstSet = texSet;
631 writes[i].dstBinding = i;
632 writes[i].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
633 writes[i].descriptorCount = 1;
634 writes[i].pImageInfo = &imageInfos[i];
635 }
636 // Override binding 0 with actual texture
637 imageInfos[0].sampler = texSampler;
638 imageInfos[0].imageView = texView;
639
640 vkUpdateDescriptorSets(_device, 6, writes.data(), 0, nullptr);
641 vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS,
642 _renderPipeline->pipelineLayout(), 1, 1, &texSet, 0, nullptr);
643 }
644 }
645
646 // Draw
647 if (indexBuffer) {
648 auto* ib = static_cast<VulkanIndexBuffer*>(indexBuffer.get());
649 if (ib->buffer() != VK_NULL_HANDLE) {
650 VkIndexType idxType = (indexBuffer->format() == INDEXFORMAT_UINT32)
651 ? VK_INDEX_TYPE_UINT32 : VK_INDEX_TYPE_UINT16;
652 vkCmdBindIndexBuffer(cmd, ib->buffer(), 0, idxType);
653 vkCmdDrawIndexed(cmd, primitive.count, numInstances,
654 primitive.base, primitive.baseVertex, 0);
655 }
656 } else {
657 vkCmdDraw(cmd, primitive.count, numInstances, primitive.base, 0);
658 }
659
660 recordDrawCall();
661
662 if (last) {
663 clearVertexBuffer();
664 _currentPipeline = VK_NULL_HANDLE;
665 }
666 }
667
668 // ─────────────────────────────────────────────────────────────────────
669 // Uniform setters
670 // ─────────────────────────────────────────────────────────────────────
671
672 void VulkanGraphicsDevice::setTransformUniforms(
673 const Matrix4& viewProjection, const Matrix4& model)
674 {
675 memcpy(_pushConstants.viewProjection, viewProjection.c, 64);
676 memcpy(_pushConstants.model, model.c, 64);
677 _pushConstantsDirty = true;
678 }
679
680 void VulkanGraphicsDevice::setLightingUniforms(const Color& ambientColor,
681 const std::vector<GpuLightData>& lights, const Vector3& cameraPosition,
682 bool enableNormalMaps, float exposure, const FogParams& fogParams,
683 const ShadowParams& shadowParams, int toneMapping)
684 {
685 (void)ambientColor; (void)lights; (void)cameraPosition;
686 (void)enableNormalMaps; (void)exposure; (void)fogParams;
687 (void)shadowParams; (void)toneMapping;
688 // TODO: pack into LightingUniforms UBO when full lighting is implemented
689 }
690
691 void VulkanGraphicsDevice::setEnvironmentUniforms(
692 Texture* envAtlas, float skyboxIntensity, float skyboxMip,
693 const Vector3& skyDomeCenter, bool isDome, Texture* skyboxCubeMap)
694 {
695 (void)envAtlas; (void)skyboxIntensity; (void)skyboxMip;
696 (void)skyDomeCenter; (void)isDome; (void)skyboxCubeMap;
697 }
698
699 // ─────────────────────────────────────────────────────────────────────
700 // Resource creation
701 // ─────────────────────────────────────────────────────────────────────
702
703 std::shared_ptr<Shader> VulkanGraphicsDevice::createShader(
704 const ShaderDefinition& definition, const std::string& sourceCode)
705 {
706 (void)sourceCode;
707 // Use embedded SPIR-V for the basic forward shader
708 return std::make_shared<VulkanShader>(this, definition,
709 vulkan_spirv::kForwardBasicVert, vulkan_spirv::kForwardBasicVertSize,
710 vulkan_spirv::kForwardBasicFrag, vulkan_spirv::kForwardBasicFragSize);
711 }
712
713 std::unique_ptr<gpu::HardwareTexture> VulkanGraphicsDevice::createGPUTexture(Texture* texture)
714 {
715 return std::make_unique<gpu::VulkanTexture>(texture);
716 }
717
718 std::shared_ptr<VertexBuffer> VulkanGraphicsDevice::createVertexBuffer(
719 const std::shared_ptr<VertexFormat>& format, int numVertices,
720 const VertexBufferOptions& options)
721 {
722 return std::make_shared<VulkanVertexBuffer>(this, format, numVertices, options);
723 }
724
725 std::shared_ptr<IndexBuffer> VulkanGraphicsDevice::createIndexBuffer(
726 IndexFormat format, int numIndices, const std::vector<uint8_t>& data)
727 {
728 auto ib = std::make_shared<VulkanIndexBuffer>(this, format, numIndices);
729 if (!data.empty()) ib->setData(data);
730 return ib;
731 }
732
733 std::shared_ptr<RenderTarget> VulkanGraphicsDevice::createRenderTarget(
734 const RenderTargetOptions& options)
735 {
736 (void)options;
737 // TODO: offscreen render targets
738 return nullptr;
739 }
740
741 // ─────────────────────────────────────────────────────────────────────
742 // Display management
743 // ─────────────────────────────────────────────────────────────────────
744
745 void VulkanGraphicsDevice::setResolution(int width, int height)
746 {
747 if (width == _width && height == _height) return;
748 _width = width;
749 _height = height;
750
751 if (_device != VK_NULL_HANDLE) {
752 vkDeviceWaitIdle(_device);
753 destroyDepthResources();
754 cleanupSwapchain();
755 initSwapchain(_width, _height);
756 createDepthResources();
757 }
758 }
759
760 std::pair<int, int> VulkanGraphicsDevice::size() const
761 {
762 return {_width, _height};
763 }
764}
765
766#endif // VISUTWIN_HAS_VULKAN
GPU texture resource supporting 2D, cubemap, volume, and array formats with mipmap management.
Definition texture.h:57
RGBA color with floating-point components in [0, 1].
Definition color.h:18
4x4 column-major transformation matrix with SIMD acceleration.
Definition matrix4.h:31
Describes how vertex and index data should be interpreted for a draw call.
Definition mesh.h:33
3D vector for positions, directions, and normals with multi-backend SIMD acceleration.
Definition vector3.h:29