diff --git a/Makefile b/Makefile index f84fb36..17e19ef 100644 --- a/Makefile +++ b/Makefile @@ -5,6 +5,16 @@ ENGINE_NAME := quark GLSLC ?= glslc SHADERS_OUT_DIR := shaders/bin +SHADER_SRC_DIR := src/backend/shaders +SHADER_INC_DIR := $(SHADER_SRC_DIR)/include + +SHADER_VERT := $(SHADER_SRC_DIR)/shader.vert +SHADER_FRAG := $(SHADER_SRC_DIR)/shader.frag +SHADER_COMMON := $(SHADER_INC_DIR)/common.glsl + +SHADER_VERT_SPV := $(SHADERS_OUT_DIR)/shader.vert.spv +SHADER_FRAG_SPV := $(SHADERS_OUT_DIR)/shader.frag.spv + # PIN TO A HASH IN A BETTER WAY THAN THIS? VCPKG_COMMIT := 11bbc873e00e9e58d4e9dffb30b7a5493a030e0b @@ -86,10 +96,16 @@ format-check: -print0 | xargs -0 clang-format -n -Werror # TODO: make obsolete by vulkan's internal API -shaders: +shaders: $(SHADER_VERT_SPV) $(SHADER_FRAG_SPV) + +$(SHADERS_OUT_DIR): @mkdir -p $(SHADERS_OUT_DIR) - @$(GLSLC) src/backend/shaders/shader.vert -o $(SHADERS_OUT_DIR)/shader.vert.spv - @$(GLSLC) src/backend/shaders/shader.frag -o $(SHADERS_OUT_DIR)/shader.frag.spv + +$(SHADER_VERT_SPV): $(SHADER_VERT) $(SHADER_COMMON) | $(SHADERS_OUT_DIR) + @$(GLSLC) -I$(SHADER_INC_DIR) $(SHADER_VERT) -o $@ + +$(SHADER_FRAG_SPV): $(SHADER_FRAG) $(SHADER_COMMON) | $(SHADERS_OUT_DIR) + @$(GLSLC) -I$(SHADER_INC_DIR) $(SHADER_FRAG) -o $@ clean: rm -rf $(BUILD_DIR) diff --git a/README.md b/README.md index 3355046..83432d3 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,7 @@ The engine shall support Advanced possible supports - Virtualized Geometry +- Global Illumination The game that shall be made with this engine is a game with movement inspired by quake. The game shall feature older-quake-like graphics and will likely have a single player, @@ -28,17 +29,25 @@ The focus of this game engine is to make a game playable on all devices with lit - [x] Albedos (start of PBR) - [x] Diffuse option - [x] material SSBO (PBR) -- [ ] gITF imports +- [x] gITF imports - [x] mesh import - [x] base texture/material - - [ ] normals - - [ ] other PBR -- [ ] PBR API for non imports + - [x] normals + - [x] other PBR +- [x] lighting +- [x] PBR API for non imports - [x] Vulkan Memory Allocator (VMA) refactor +- [ ] Mipmaps - [ ] Depth pre-pass - [ ] Forward+ lighting +- [ ] offscreen intermediate pass + +## MVP 2 +- [ ] Editor - [ ] Job System + - Basic Multithreading for uploader [x] + - Dynamic picking of threads for jobs [ ] - [ ] ECS -- [ ] Mipmaps + - [ ] Basic physics engine - [ ] Player Controller diff --git a/assets/tree2.glb b/assets/tree2.glb new file mode 100644 index 0000000..f6cd9f0 Binary files /dev/null and b/assets/tree2.glb differ diff --git a/src/backend/core/vk_device.cpp b/src/backend/core/vk_device.cpp index e66834e..0058d37 100644 --- a/src/backend/core/vk_device.cpp +++ b/src/backend/core/vk_device.cpp @@ -264,18 +264,91 @@ bool VkDeviceCtx::createLogicalDevice() { queueCreateInfo.queueCount = 1; queueCreateInfo.pQueuePriorities = &queuePriority; - VkPhysicalDeviceFeatures deviceFeatures{}; + VkPhysicalDeviceVulkan12Features supVk12{}; + supVk12.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_2_FEATURES; + + VkPhysicalDeviceDynamicRenderingFeatures supDyn{}; + supDyn.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DYNAMIC_RENDERING_FEATURES; + + VkPhysicalDeviceFeatures2 supFeats2{}; + supFeats2.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2; + supFeats2.pNext = &supVk12; + supVk12.pNext = &supDyn; + + vkGetPhysicalDeviceFeatures2(m_physicalDevice, &supFeats2); + + const bool hasUniformNonUniform = + (supVk12.shaderUniformBufferArrayNonUniformIndexing == VK_TRUE); + + const bool hasStorageNonUniform = + (supVk12.shaderStorageBufferArrayNonUniformIndexing == VK_TRUE); + + const bool hasSampledImageNonUniform = + (supVk12.shaderSampledImageArrayNonUniformIndexing == VK_TRUE); + + const bool hasPartiallyBound = + (supVk12.descriptorBindingPartiallyBound == VK_TRUE); + + const bool hasUniformUAB = + (supVk12.descriptorBindingUniformBufferUpdateAfterBind == VK_TRUE); + + const bool hasStorageUAB = + (supVk12.descriptorBindingStorageBufferUpdateAfterBind == VK_TRUE); + + const bool hasSampledImageUAB = + (supVk12.descriptorBindingSampledImageUpdateAfterBind == VK_TRUE); + + const bool hasRuntimeArray = (supVk12.runtimeDescriptorArray == VK_TRUE); + + if (!hasUniformNonUniform || !hasStorageNonUniform || + !hasSampledImageNonUniform || !hasPartiallyBound || !hasUniformUAB || + !hasStorageUAB || !hasSampledImageUAB) { + LOGE("Bindless textures not supported on this device"); + LOGE(" shaderUniformBufferArrayNonUniformIndexing= {}", + hasUniformNonUniform ? "YES" : "NO"); + LOGE(" shaderStorageBufferArrayNonUniformIndexing= {}", + hasStorageNonUniform ? "YES" : "NO"); + LOGE(" shaderSampledImageArrayNonUniformIndexing = {}", + hasSampledImageNonUniform ? "YES" : "NO"); + LOGE(" descriptorBindingPartiallyBound = {}", + hasPartiallyBound ? "YES" : "NO"); + LOGE(" descriptorBindingUniformBufferUpdateAfterBind = {}", + hasUniformUAB ? "YES" : "NO"); + LOGE(" descriptorBindingStorageBufferUpdateAfterBind= {}", + hasStorageUAB ? "YES" : "NO"); + LOGE(" descriptorBindingSampledImageUpdateAfterBind = {}", + hasSampledImageUAB ? "YES" : "NO"); + LOGE(" runtimeDescriptorArray = {}", hasRuntimeArray ? "YES" : "NO"); + return false; + } - VkPhysicalDeviceDynamicRenderingFeatures dyn{}; - dyn.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DYNAMIC_RENDERING_FEATURES; - dyn.dynamicRendering = VK_TRUE; + VkPhysicalDeviceVulkan12Features enVk12{}; + enVk12.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_2_FEATURES; + enVk12.shaderUniformBufferArrayNonUniformIndexing = VK_TRUE; + enVk12.shaderStorageBufferArrayNonUniformIndexing = VK_TRUE; + enVk12.shaderSampledImageArrayNonUniformIndexing = VK_TRUE; + enVk12.descriptorBindingPartiallyBound = VK_TRUE; + enVk12.descriptorBindingUniformBufferUpdateAfterBind = VK_TRUE; + enVk12.descriptorBindingStorageBufferUpdateAfterBind = VK_TRUE; + enVk12.descriptorBindingSampledImageUpdateAfterBind = VK_TRUE; + enVk12.runtimeDescriptorArray = hasRuntimeArray ? VK_TRUE : VK_FALSE; + + VkPhysicalDeviceDynamicRenderingFeatures enDyn{}; + enDyn.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DYNAMIC_RENDERING_FEATURES; + enDyn.dynamicRendering = VK_TRUE; + + VkPhysicalDeviceFeatures2 enabledFeats2{}; + enabledFeats2.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2; + enabledFeats2.pNext = &enVk12; + enVk12.pNext = &enDyn; + + enabledFeats2.features = VkPhysicalDeviceFeatures{}; VkDeviceCreateInfo createInfo{}; createInfo.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO; - createInfo.pNext = &dyn; + createInfo.pNext = &enabledFeats2; createInfo.queueCreateInfoCount = 1; createInfo.pQueueCreateInfos = &queueCreateInfo; - createInfo.pEnabledFeatures = &deviceFeatures; createInfo.enabledExtensionCount = static_cast(kDeviceExtensions.size()); @@ -293,5 +366,15 @@ bool VkDeviceCtx::createLogicalDevice() { &m_queues.graphics); m_queues.graphicsFamily = indices.graphicsFamily.value(); + LOGI("Enabled bindless features:"); + LOGI(" shaderUniformBufferArrayNonUniformIndexing=YES"); + LOGI(" shaderStorageBufferArrayNonUniformIndexing=YES"); + LOGI(" shaderSampledImageArrayNonUniformIndexing=YES"); + LOGI(" descriptorBindingPartiallyBound=YES"); + LOGI(" descriptorBindingUniformBufferUpdateAfterBind=YES"); + LOGI(" descriptorBindingStorageBufferUpdateAfterBind=YES"); + LOGI(" descriptorBindingSampledImageUpdateAfterBind=YES"); + LOGI(" runtimeDescriptorArray={}", hasRuntimeArray ? "YES" : "NO"); + return true; } diff --git a/src/backend/core/vk_instance.cpp b/src/backend/core/vk_instance.cpp index a6f65d1..94899b6 100644 --- a/src/backend/core/vk_instance.cpp +++ b/src/backend/core/vk_instance.cpp @@ -29,7 +29,6 @@ debugCallback(VkDebugUtilsMessageSeverityFlagBitsEXT messageSeverity, (void)messageTypes; (void)pUserData; - // TODO: fix LOGE("Validation layer: {}", pCallbackData->pMessage); return VK_FALSE; } diff --git a/src/backend/gpu/descriptors/vk_material_sets.cpp b/src/backend/gpu/descriptors/vk_material_sets.cpp index d7f68de..f57da32 100644 --- a/src/backend/gpu/descriptors/vk_material_sets.cpp +++ b/src/backend/gpu/descriptors/vk_material_sets.cpp @@ -1,16 +1,17 @@ #include "backend/gpu/descriptors/vk_material_sets.hpp" #include "backend/gpu/textures/vk_texture.hpp" +#include "backend/profiling/telemetry/telemetry.hpp" +#include "engine/logging/log.hpp" #include -#include +#include #include bool VkMaterialSets::init(VkDevice device, VkDescriptorSetLayout layout, - uint32_t maxMaterials) { - if (device == VK_NULL_HANDLE || layout == VK_NULL_HANDLE || - maxMaterials == 0) { - std::cerr << "[MaterialSets] Invalid init args\n"; + uint32_t maxTextures) { + if (layout == VK_NULL_HANDLE || maxTextures == 0) { + LOGE("Invalid initlization args"); return false; } @@ -18,26 +19,39 @@ bool VkMaterialSets::init(VkDevice device, VkDescriptorSetLayout layout, m_device = device; m_layout = layout; + m_maxTextures = maxTextures; VkDescriptorPoolSize poolSize{}; poolSize.type = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; - poolSize.descriptorCount = maxMaterials; + poolSize.descriptorCount = maxTextures; VkDescriptorPoolCreateInfo poolInfo{}; poolInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO; - poolInfo.maxSets = maxMaterials; + poolInfo.flags = VK_DESCRIPTOR_POOL_CREATE_UPDATE_AFTER_BIND_BIT; + poolInfo.maxSets = 1; poolInfo.poolSizeCount = 1; poolInfo.pPoolSizes = &poolSize; VkResult res = vkCreateDescriptorPool(m_device, &poolInfo, nullptr, &m_pool); if (res != VK_SUCCESS) { - std::cerr << "[MaterialSets] vkCreateDescriptorPool failed: " << res - << "\n"; + LOGE("vkCreateDescriptorPool failed: {}", fmt::underlying(res)); + shutdown(); + return false; + } + + VkDescriptorSetAllocateInfo allocInfo{}; + allocInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO; + allocInfo.descriptorPool = m_pool; + allocInfo.descriptorSetCount = 1; + allocInfo.pSetLayouts = &m_layout; + + res = vkAllocateDescriptorSets(m_device, &allocInfo, &m_set); + if (res != VK_SUCCESS) { + LOGE("vkAllocateDescriptorSets failed: {}", fmt::underlying(res)); shutdown(); return false; } - m_sets.reserve(maxMaterials); return true; } @@ -49,65 +63,42 @@ void VkMaterialSets::shutdown() noexcept { } m_pool = VK_NULL_HANDLE; - m_sets.clear(); m_layout = VK_NULL_HANDLE; + m_set = VK_NULL_HANDLE; m_device = VK_NULL_HANDLE; + m_maxTextures = 0; } -uint32_t VkMaterialSets::allocateForTexture(const VkTexture2D &tex) { - if (m_device == VK_NULL_HANDLE || m_pool == VK_NULL_HANDLE || - m_layout == VK_NULL_HANDLE) { - std::cerr << "[MaterialSets] Not initialized\n"; - return UINT32_MAX; - } - - if (!tex.valid()) { - std::cerr << "[MaterialSets] Invalid texture\n"; - return UINT32_MAX; - } - - VkDescriptorSet set = VK_NULL_HANDLE; - VkDescriptorSetAllocateInfo allocInfo{}; - allocInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO; - allocInfo.descriptorPool = m_pool; - allocInfo.descriptorSetCount = 1; - allocInfo.pSetLayouts = &m_layout; - - VkResult res = vkAllocateDescriptorSets(m_device, &allocInfo, &set); - if (res != VK_SUCCESS) { - std::cerr << "[MaterialSets] vkAllocateDescriptorSets failed: " << res - << "\n"; - return UINT32_MAX; +bool VkMaterialSets::writeTexture(uint32_t slot, const VkTexture2D &texture) { + if (!texture.valid() || slot >= m_maxTextures || m_set == VK_NULL_HANDLE) { + return false; } VkDescriptorImageInfo imgInfo{}; imgInfo.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; - imgInfo.imageView = tex.view; - imgInfo.sampler = tex.sampler; + imgInfo.imageView = texture.view; + imgInfo.sampler = texture.sampler; VkWriteDescriptorSet write{}; write.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; - write.dstSet = set; + write.dstSet = m_set; write.dstBinding = 0; + write.dstArrayElement = slot; write.descriptorCount = 1; write.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; write.pImageInfo = &imgInfo; vkUpdateDescriptorSets(m_device, 1, &write, 0, nullptr); - - const uint32_t idx = static_cast(m_sets.size()); - m_sets.push_back(set); - - return idx; + return true; } void VkMaterialSets::bind(VkCommandBuffer cmd, VkPipelineLayout pipelineLayout, - uint32_t setIndex, uint32_t materialIndex) const { - if (materialIndex >= m_sets.size()) { + uint32_t setIndex) const { + if (m_set == VK_NULL_HANDLE) { return; } - VkDescriptorSet set = m_sets[materialIndex]; vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, pipelineLayout, - setIndex, 1, &set, 0, nullptr); + setIndex, 1, &m_set, 0, nullptr); + PROFILE_CPU_INC_DESCRIPTOR_BINDS(1); } diff --git a/src/backend/gpu/descriptors/vk_material_sets.hpp b/src/backend/gpu/descriptors/vk_material_sets.hpp index 6d210d6..32bb9cd 100644 --- a/src/backend/gpu/descriptors/vk_material_sets.hpp +++ b/src/backend/gpu/descriptors/vk_material_sets.hpp @@ -4,7 +4,6 @@ #include #include -#include #include class VkMaterialSets { @@ -26,22 +25,21 @@ class VkMaterialSets { m_device = std::exchange(other.m_device, VK_NULL_HANDLE); m_pool = std::exchange(other.m_pool, VK_NULL_HANDLE); m_layout = std::exchange(other.m_layout, VK_NULL_HANDLE); - m_sets = std::move(other.m_sets); + m_set = std::move(other.m_set); return *this; } bool init(VkDevice device, VkDescriptorSetLayout layout, - uint32_t maxMaterials); + uint32_t maxTextures); void shutdown() noexcept; - // TODO: change name to allocateBaseColorMaterial and then add - // allocatePbrMaterial - uint32_t allocateForTexture(const VkTexture2D &tex); + bool writeTexture(uint32_t slot, const VkTexture2D &texture); void bind(VkCommandBuffer cmd, VkPipelineLayout pipelineLayout, - uint32_t setIndex, uint32_t materialIndex) const; + uint32_t setIndex) const; + [[nodiscard]] VkDescriptorSet set() const noexcept { return m_set; } [[nodiscard]] VkDescriptorSetLayout layout() const noexcept { return m_layout; } @@ -50,5 +48,7 @@ class VkMaterialSets { VkDevice m_device = VK_NULL_HANDLE; // non-owning VkDescriptorPool m_pool = VK_NULL_HANDLE; // non-owning VkDescriptorSetLayout m_layout = VK_NULL_HANDLE; // non-owning - std::vector m_sets; + VkDescriptorSet m_set = VK_NULL_HANDLE; + + uint32_t m_maxTextures = 0; // non-owning }; diff --git a/src/backend/gpu/descriptors/vk_scene_sets.cpp b/src/backend/gpu/descriptors/vk_scene_sets.cpp index 6035e33..4dd1806 100644 --- a/src/backend/gpu/descriptors/vk_scene_sets.cpp +++ b/src/backend/gpu/descriptors/vk_scene_sets.cpp @@ -1,116 +1,143 @@ #include "backend/gpu/descriptors/vk_scene_sets.hpp" #include "backend/gpu/buffers/vk_per_frame_uniform_buffers.hpp" +#include "backend/profiling/telemetry/telemetry.hpp" +#include "engine/logging/log.hpp" #include #include -#include +#include +#include #include bool VkSceneSets::init(VkDevice device, VkDescriptorSetLayout layout, - const VkPerFrameUniformBuffers &bufs, + const VkPerFrameUniformBuffers &sceneBufs, VkBuffer instanceBuffer, VkDeviceSize instanceFrameStrideBytes, - VkBuffer materialBuffer, - VkDeviceSize materialTableBytes) { - if (device == VK_NULL_HANDLE || layout == VK_NULL_HANDLE || !bufs.valid() || - instanceBuffer == VK_NULL_HANDLE || instanceFrameStrideBytes == 0 || - materialBuffer == VK_NULL_HANDLE || materialTableBytes == 0) { - std::cerr << "[PerFrameSets] init invalid args\n"; + VkBuffer materialBuffer, VkDeviceSize materialTableBytes, + VkBuffer lightBuffer, + VkDeviceSize lightFrameStrideBytes) { + if (device == VK_NULL_HANDLE || layout == VK_NULL_HANDLE || + !sceneBufs.valid() || instanceBuffer == VK_NULL_HANDLE || + instanceFrameStrideBytes == 0 || materialBuffer == VK_NULL_HANDLE || + materialTableBytes == 0) { + LOGE("Initialization arguments invalid"); + return false; + } + + const uint32_t framesInFlight = sceneBufs.frameCount(); + if (framesInFlight == 0) { + LOGE("sceneBufs has 0 frames"); return false; } shutdown(); m_device = device; + m_framesInFlight = framesInFlight; - const uint32_t framesInFlight = bufs.frameCount(); - + // UBO descriptors: scene[frames] = frames + // SSBO descriptors: instance[frames] + material[1] + lights[1] = 2 * + // framesInFlight + 2 std::array poolSizes{}; poolSizes[0].type = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; poolSizes[0].descriptorCount = framesInFlight; poolSizes[1].type = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER; - poolSizes[1].descriptorCount = framesInFlight * 2; // instance + materials + poolSizes[1].descriptorCount = 2 * framesInFlight + 1; VkDescriptorPoolCreateInfo poolInfo{}; poolInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO; - poolInfo.maxSets = framesInFlight; + poolInfo.flags = VK_DESCRIPTOR_POOL_CREATE_UPDATE_AFTER_BIND_BIT; + poolInfo.maxSets = 1; poolInfo.poolSizeCount = (uint32_t)poolSizes.size(); poolInfo.pPoolSizes = poolSizes.data(); VkResult res = vkCreateDescriptorPool(m_device, &poolInfo, nullptr, &m_pool); if (res != VK_SUCCESS) { - std::cerr << "[PerFrameSets] vkCreateDescriptorPool failed: " << res - << "\n"; + LOGE("vkCreateDescriptorPool failed: {}", fmt::underlying(res)); shutdown(); return false; } - std::vector layouts(framesInFlight, layout); - VkDescriptorSetAllocateInfo allocInfo{}; allocInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO; allocInfo.descriptorPool = m_pool; - allocInfo.descriptorSetCount = framesInFlight; - allocInfo.pSetLayouts = layouts.data(); + allocInfo.descriptorSetCount = 1; + allocInfo.pSetLayouts = &layout; - m_sets.resize(framesInFlight); - res = vkAllocateDescriptorSets(m_device, &allocInfo, m_sets.data()); + res = vkAllocateDescriptorSets(m_device, &allocInfo, &m_set); if (res != VK_SUCCESS) { - std::cerr << "[PerFrameSets] Failed to allocate descriptor sets\n"; + LOGE("vkAllocateDescriptorSets failed: {}", fmt::underlying(res)); shutdown(); return false; } + std::vector sceneInfos(framesInFlight); + std::vector instanceInfos(framesInFlight); + std::vector lightInfos(framesInFlight); + + for (uint32_t i = 0; i < framesInFlight; ++i) { + sceneInfos[i].buffer = sceneBufs.buffer(i).handle(); + sceneInfos[i].offset = 0; + sceneInfos[i].range = sceneBufs.stride(); + + instanceInfos[i].buffer = instanceBuffer; + instanceInfos[i].offset = VkDeviceSize(i) * instanceFrameStrideBytes; + instanceInfos[i].range = instanceFrameStrideBytes; + + lightInfos[i].buffer = lightBuffer; + lightInfos[i].offset = VkDeviceSize(i) * lightFrameStrideBytes; + lightInfos[i].range = lightFrameStrideBytes; + } + // Global material table VkDescriptorBufferInfo materialInfo{}; materialInfo.buffer = materialBuffer; materialInfo.offset = 0; materialInfo.range = materialTableBytes; - // Write set 0 bindings for each frame - for (uint32_t i = 0; i < framesInFlight; ++i) { - VkDescriptorBufferInfo uboInfo{}; - uboInfo.buffer = bufs.buffer(i).handle(); - uboInfo.offset = 0; - uboInfo.range = bufs.stride(); - - VkDescriptorBufferInfo instanceInfo{}; - instanceInfo.buffer = instanceBuffer; - instanceInfo.offset = VkDeviceSize(i) * instanceFrameStrideBytes; - instanceInfo.range = instanceFrameStrideBytes; - - std::array writes{}; - - // binding 0: camera UBO - writes[0].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; - writes[0].dstSet = m_sets[i]; - writes[0].dstBinding = 0; - writes[0].descriptorCount = 1; - writes[0].descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; - writes[0].pBufferInfo = &uboInfo; - - // binding 1: instance SSBO - writes[1].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; - writes[1].dstSet = m_sets[i]; - writes[1].dstBinding = 1; - writes[1].descriptorCount = 1; - writes[1].descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER; - writes[1].pBufferInfo = &instanceInfo; - - // binding 2: material table SSBO - writes[2].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; - writes[2].dstSet = m_sets[i]; - writes[2].dstBinding = 2; - writes[2].descriptorCount = 1; - writes[2].descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER; - writes[2].pBufferInfo = &materialInfo; - - vkUpdateDescriptorSets(m_device, (uint32_t)writes.size(), writes.data(), 0, - nullptr); - } + std::array writes{}; + + // binding 0: SceneUBO[] + writes[0].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; + writes[0].dstSet = m_set; + writes[0].dstBinding = 0; + writes[0].dstArrayElement = 0; + writes[0].descriptorCount = framesInFlight; + writes[0].descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; + writes[0].pBufferInfo = sceneInfos.data(); + + // binding 1: InstanceSSBO[] + writes[1].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; + writes[1].dstSet = m_set; + writes[1].dstBinding = 1; + writes[1].dstArrayElement = 0; + writes[1].descriptorCount = framesInFlight; + writes[1].descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER; + writes[1].pBufferInfo = instanceInfos.data(); + + // binding 2: MaterialSSBO + writes[2].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; + writes[2].dstSet = m_set; + writes[2].dstBinding = 2; + writes[2].dstArrayElement = 0; + writes[2].descriptorCount = 1; + writes[2].descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER; + writes[2].pBufferInfo = &materialInfo; + + // binding 3: LightSSBO + writes[3].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; + writes[3].dstSet = m_set; + writes[3].dstBinding = 3; + writes[3].dstArrayElement = 0; + writes[3].descriptorCount = framesInFlight; + writes[3].descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER; + writes[3].pBufferInfo = lightInfos.data(); + + vkUpdateDescriptorSets(m_device, (uint32_t)writes.size(), writes.data(), 0, + nullptr); return true; } @@ -121,19 +148,19 @@ void VkSceneSets::shutdown() noexcept { } m_pool = VK_NULL_HANDLE; - m_sets.clear(); + m_set = VK_NULL_HANDLE; + m_framesInFlight = 0; m_device = VK_NULL_HANDLE; } void VkSceneSets::bind(VkCommandBuffer cmd, VkPipelineLayout pipelineLayout, - uint32_t setIndex, uint32_t frameIndex) const { - if (frameIndex >= m_sets.size()) { + uint32_t setIndex) const { + if (m_set == VK_NULL_HANDLE) { return; } - VkDescriptorSet set = m_sets[frameIndex]; - // TODO: use dynamic offset to have on descriptor per object UBO ring buffer vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, pipelineLayout, - setIndex, 1, &set, 0, nullptr); + setIndex, 1, &m_set, 0, nullptr); + PROFILE_CPU_INC_DESCRIPTOR_BINDS(1); } diff --git a/src/backend/gpu/descriptors/vk_scene_sets.hpp b/src/backend/gpu/descriptors/vk_scene_sets.hpp index 1f96d9e..aec5ec5 100644 --- a/src/backend/gpu/descriptors/vk_scene_sets.hpp +++ b/src/backend/gpu/descriptors/vk_scene_sets.hpp @@ -4,7 +4,6 @@ #include #include -#include #include class VkSceneSets { @@ -25,30 +24,28 @@ class VkSceneSets { m_device = std::exchange(other.m_device, VK_NULL_HANDLE); m_pool = std::exchange(other.m_pool, VK_NULL_HANDLE); - m_sets = std::move(other.m_sets); + m_set = std::move(other.m_set); return *this; } bool init(VkDevice device, VkDescriptorSetLayout layout, - const VkPerFrameUniformBuffers &uboBufs, VkBuffer instanceBuffer, + const VkPerFrameUniformBuffers &sceneBufs, VkBuffer instanceBuffer, VkDeviceSize instanceFrameStrideBytes, VkBuffer materialBuffer, - VkDeviceSize materialTableBytes); + VkDeviceSize materialTableBytes, VkBuffer lightBuffer, + VkDeviceSize lightFrameStrideBytes); void shutdown() noexcept; void bind(VkCommandBuffer cmd, VkPipelineLayout pipelineLayout, - uint32_t setIndex, uint32_t frameIndex) const; + uint32_t setIndex) const; [[nodiscard]] bool valid() const noexcept { return m_pool != VK_NULL_HANDLE; } - [[nodiscard]] VkDescriptorSet set(uint32_t frameIndex) const noexcept { - return m_sets[frameIndex]; - } - [[nodiscard]] uint32_t setCount() const noexcept { - return static_cast(m_sets.size()); - } + [[nodiscard]] VkDescriptorSet set() const noexcept { return m_set; } private: VkDevice m_device = VK_NULL_HANDLE; // non-owning VkDescriptorPool m_pool = VK_NULL_HANDLE; // owning - std::vector m_sets; + VkDescriptorSet m_set = VK_NULL_HANDLE; + + uint32_t m_framesInFlight = 0; }; diff --git a/src/backend/gpu/descriptors/vk_shader_interface.cpp b/src/backend/gpu/descriptors/vk_shader_interface.cpp index ca37677..263aeef 100644 --- a/src/backend/gpu/descriptors/vk_shader_interface.cpp +++ b/src/backend/gpu/descriptors/vk_shader_interface.cpp @@ -1,87 +1,129 @@ #include "backend/gpu/descriptors/vk_shader_interface.hpp" +#include "engine/logging/log.hpp" #include "render/scene/push_constants.hpp" #include #include +#include #include -#include #include -bool VkShaderInterface::init(VkDevice device) { +bool VkShaderInterface::init(VkDevice device, uint32_t framesInFlight, + uint32_t maxTextures) { if (device == VK_NULL_HANDLE) { - std::cerr << "[ShaderInterface] init invalid args\n"; + LOGE("Initialization invalid arguments"); return false; } shutdown(); - m_device = device; - // set=0 binding=0: per frame UBO (camera) - VkDescriptorSetLayoutBinding uboBinding{}; - uboBinding.binding = 0; - uboBinding.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; - uboBinding.descriptorCount = 1; - uboBinding.stageFlags = VK_SHADER_STAGE_VERTEX_BIT; + m_device = device; + m_maxTextures = maxTextures; + + // set=0 binding=1: scene per frame bindless array + VkDescriptorSetLayoutBinding sceneUbo{}; + sceneUbo.binding = 0; + sceneUbo.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; + sceneUbo.descriptorCount = framesInFlight; + sceneUbo.stageFlags = + VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT; // set=0 binding=1: instance SSBO - VkDescriptorSetLayoutBinding instanceBinding{}; - instanceBinding.binding = 1; - instanceBinding.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER; - instanceBinding.descriptorCount = 1; - instanceBinding.stageFlags = VK_SHADER_STAGE_VERTEX_BIT; + VkDescriptorSetLayoutBinding instanceSsbo{}; + instanceSsbo.binding = 1; + instanceSsbo.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER; + instanceSsbo.descriptorCount = framesInFlight; + instanceSsbo.stageFlags = VK_SHADER_STAGE_VERTEX_BIT; // set=0 binding=2: material table SSBO - VkDescriptorSetLayoutBinding materialBinding{}; - materialBinding.binding = 2; - materialBinding.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER; - materialBinding.descriptorCount = 1; - materialBinding.stageFlags = + VkDescriptorSetLayoutBinding materialSsbo{}; + materialSsbo.binding = 2; + materialSsbo.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER; + materialSsbo.descriptorCount = 1; + materialSsbo.stageFlags = VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT; - std::array bindings{ - uboBinding, instanceBinding, materialBinding}; - - VkDescriptorSetLayoutCreateInfo perFrameInfo{}; - perFrameInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO; - perFrameInfo.bindingCount = static_cast(bindings.size()); - perFrameInfo.pBindings = bindings.data(); - - VkResult res = vkCreateDescriptorSetLayout(m_device, &perFrameInfo, nullptr, + // set=0 binding=3: point lights SSBO + VkDescriptorSetLayoutBinding lightsSsbo{}; + lightsSsbo.binding = 3; + lightsSsbo.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER; + lightsSsbo.descriptorCount = framesInFlight; + lightsSsbo.stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT; + + std::array sceneBindings{ + sceneUbo, instanceSsbo, materialSsbo, lightsSsbo}; + + std::array sceneFlags{ + VK_DESCRIPTOR_BINDING_PARTIALLY_BOUND_BIT | + VK_DESCRIPTOR_BINDING_UPDATE_AFTER_BIND_BIT, + VK_DESCRIPTOR_BINDING_PARTIALLY_BOUND_BIT | + VK_DESCRIPTOR_BINDING_UPDATE_AFTER_BIND_BIT, + VK_DESCRIPTOR_BINDING_UPDATE_AFTER_BIND_BIT, + VK_DESCRIPTOR_BINDING_PARTIALLY_BOUND_BIT | + VK_DESCRIPTOR_BINDING_UPDATE_AFTER_BIND_BIT, + }; + + VkDescriptorSetLayoutBindingFlagsCreateInfo sceneFlagsInfo{}; + sceneFlagsInfo.sType = + VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_BINDING_FLAGS_CREATE_INFO; + sceneFlagsInfo.bindingCount = (uint32_t)sceneFlags.size(); + sceneFlagsInfo.pBindingFlags = sceneFlags.data(); + + VkDescriptorSetLayoutCreateInfo sceneInfo{}; + sceneInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO; + sceneInfo.pNext = &sceneFlagsInfo; + sceneInfo.flags = VK_DESCRIPTOR_SET_LAYOUT_CREATE_UPDATE_AFTER_BIND_POOL_BIT; + sceneInfo.bindingCount = (uint32_t)(sceneBindings.size()); + sceneInfo.pBindings = sceneBindings.data(); + + VkResult res = vkCreateDescriptorSetLayout(m_device, &sceneInfo, nullptr, &m_setLayoutScene); - if (res != VK_SUCCESS) { - std::cerr << "[ShaderInterface] create per-frame set layout failed: " << res - << "\n"; + LOGE("Scene set layout creation failed: {}", fmt::underlying(res)); shutdown(); return false; } - // set=1 binding=0: Material sampler2D - VkDescriptorSetLayoutBinding textureBinding{}; - textureBinding.binding = 0; - textureBinding.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; - textureBinding.descriptorCount = 1; - textureBinding.stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT; + // set=1 binding=0: bindless sampler2D + VkDescriptorSetLayoutBinding tex{}; + tex.binding = 0; + tex.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; + tex.descriptorCount = m_maxTextures; + tex.stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT; + + // Binding flags for descriptor indexing + VkDescriptorBindingFlags bindingFlags = + VK_DESCRIPTOR_BINDING_PARTIALLY_BOUND_BIT | + VK_DESCRIPTOR_BINDING_UPDATE_AFTER_BIND_BIT; + + VkDescriptorSetLayoutBindingFlagsCreateInfo flagsInfo{}; + flagsInfo.sType = + VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_BINDING_FLAGS_CREATE_INFO; + flagsInfo.bindingCount = 1; + flagsInfo.pBindingFlags = &bindingFlags; VkDescriptorSetLayoutCreateInfo materialLayoutInfo{}; materialLayoutInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO; + materialLayoutInfo.pNext = &flagsInfo; + materialLayoutInfo.flags = + VK_DESCRIPTOR_SET_LAYOUT_CREATE_UPDATE_AFTER_BIND_POOL_BIT; materialLayoutInfo.bindingCount = 1; - materialLayoutInfo.pBindings = &textureBinding; + materialLayoutInfo.pBindings = &tex; res = vkCreateDescriptorSetLayout(m_device, &materialLayoutInfo, nullptr, &m_setLayoutMaterial); if (res != VK_SUCCESS) { - std::cerr << "[ShaderInterface] create material set layout failed: " << res - << "\n"; + LOGE("Material set layout creation failed"); shutdown(); return false; } // Push constant (model matrix) VkPushConstantRange pushRange{}; - pushRange.stageFlags = VK_SHADER_STAGE_VERTEX_BIT; + pushRange.stageFlags = + VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT; pushRange.offset = 0; pushRange.size = sizeof(DrawPushConstants); @@ -98,8 +140,7 @@ bool VkShaderInterface::init(VkDevice device) { res = vkCreatePipelineLayout(m_device, &pipelineLayoutInfo, nullptr, &m_pipelineLayout); if (res != VK_SUCCESS) { - std::cerr << "[ShaderInterface] vkCreatePipelineLayout failed: " << res - << "\n"; + LOGE("vkCreatePipelineLayout failed: {}", fmt::underlying(res)); m_pipelineLayout = VK_NULL_HANDLE; return false; } @@ -122,6 +163,7 @@ void VkShaderInterface::shutdown() noexcept { } } + m_maxTextures = 0; m_pipelineLayout = VK_NULL_HANDLE; m_setLayoutMaterial = VK_NULL_HANDLE; m_setLayoutScene = VK_NULL_HANDLE; diff --git a/src/backend/gpu/descriptors/vk_shader_interface.hpp b/src/backend/gpu/descriptors/vk_shader_interface.hpp index 83be5af..4b255a0 100644 --- a/src/backend/gpu/descriptors/vk_shader_interface.hpp +++ b/src/backend/gpu/descriptors/vk_shader_interface.hpp @@ -1,5 +1,7 @@ #pragma once +#include +#include #include #include @@ -28,7 +30,7 @@ class VkShaderInterface { return *this; } - bool init(VkDevice device); + bool init(VkDevice device, uint32_t framesInFlight, uint32_t maxTextures); void shutdown() noexcept; [[nodiscard]] VkDescriptorSetLayout setLayoutScene() const noexcept { @@ -45,9 +47,13 @@ class VkShaderInterface { return m_pipelineLayout != VK_NULL_HANDLE; } + [[nodiscard]] uint32_t maxTextures() const noexcept { return m_maxTextures; } + private: VkDevice m_device = VK_NULL_HANDLE; // non-owning VkDescriptorSetLayout m_setLayoutScene = VK_NULL_HANDLE; // owning VkDescriptorSetLayout m_setLayoutMaterial = VK_NULL_HANDLE; // owning VkPipelineLayout m_pipelineLayout = VK_NULL_HANDLE; // owning + + uint32_t m_maxTextures = 0; }; diff --git a/src/backend/gpu/upload/CMakeLists.txt b/src/backend/gpu/upload/CMakeLists.txt index c4c6efa..f512f1e 100644 --- a/src/backend/gpu/upload/CMakeLists.txt +++ b/src/backend/gpu/upload/CMakeLists.txt @@ -3,6 +3,7 @@ add_library(quark_backend_gpu_upload STATIC vk_texture_uploader.cpp vk_instance_uploader.cpp vk_material_uploader.cpp + vk_lights_uploader.cpp vk_upload_context.cpp ) diff --git a/src/backend/gpu/upload/vk_buffer_uploader.cpp b/src/backend/gpu/upload/vk_buffer_uploader.cpp index 0ee1ef5..d9d8774 100644 --- a/src/backend/gpu/upload/vk_buffer_uploader.cpp +++ b/src/backend/gpu/upload/vk_buffer_uploader.cpp @@ -2,10 +2,10 @@ #include "backend/gpu/upload/vk_upload_context.hpp" #include "backend/profiling/telemetry/telemetry.hpp" +#include "engine/logging/log.hpp" #include #include -#include #include #include @@ -21,19 +21,18 @@ bool VkBufferUploader::uploadToDeviceLocalBuffer( VkUploadContext::Recorder recorder, const void *data, VkDeviceSize size, VkBufferUsageFlags finalUsage, VkBufferObj &outBuffer) { if (!recorder) { - std::cerr << "[BufferUploader] Invalid recorder\n"; + LOGE("Recorder is invalid"); return false; } if (data == nullptr || size == 0) { - std::cerr << "[Uploader] Invalid data or size\n"; + LOGE("Data or size is invalid"); return false; } VkStagingAlloc stageAlloc = recorder.allocStaging(size); if (!stageAlloc) { - std::cerr << "[Uploader] Out of staging space (increase per-frame budget " - "or flush earlier)\n"; + LOGE("Out of staging space"); return false; } @@ -47,7 +46,7 @@ bool VkBufferUploader::uploadToDeviceLocalBuffer( if (!outBuffer.init(m_allocator, size, finalUsage | VK_BUFFER_USAGE_TRANSFER_DST_BIT, VkBufferObj::MemUsage::GpuOnly)) { - std::cerr << "[Uploader] Failed to create device-local buffer\n"; + LOGE("Device-local buffer creation failed"); return false; } diff --git a/src/backend/gpu/upload/vk_instance_uploader.cpp b/src/backend/gpu/upload/vk_instance_uploader.cpp index 4391d04..a9faa76 100644 --- a/src/backend/gpu/upload/vk_instance_uploader.cpp +++ b/src/backend/gpu/upload/vk_instance_uploader.cpp @@ -1,20 +1,19 @@ #include "backend/gpu/upload/vk_instance_uploader.hpp" +#include "backend/gpu/upload/vk_upload_context.hpp" #include "backend/profiling/telemetry/telemetry.hpp" +#include "engine/logging/log.hpp" #include -#include #include -bool VkInstanceUploader::init() { return true; } +namespace InstanceUploader { -void VkInstanceUploader::shutdown() noexcept {} - -InstanceUploadResult VkInstanceUploader::uploadMat4Instances( - VkUploadContext::Recorder recorder, VkBuffer instanceBuffer, - VkDeviceSize frameBaseBytes, VkDeviceSize frameStrideBytes, - uint32_t maxInstancesPerFrame, uint32_t &cursorInstances, - std::span models) { +InstanceUploadResult +uploadMat4Instances(VkUploadContext::Recorder recorder, VkBuffer instanceBuffer, + VkDeviceSize frameBaseBytes, VkDeviceSize frameStrideBytes, + uint32_t maxInstancesPerFrame, uint32_t &cursorInstances, + std::span models) { InstanceUploadResult out{}; if (!recorder || instanceBuffer == VK_NULL_HANDLE) { return out; @@ -27,7 +26,7 @@ InstanceUploadResult VkInstanceUploader::uploadMat4Instances( const uint32_t count = static_cast(models.size()); if (cursorInstances + count > maxInstancesPerFrame) { // TODO: split batch - std::cerr << "[InstanceUploader] Instance budget exceeded for frame\n"; + LOGE("Instance budget exceeded for frame"); return out; } @@ -36,13 +35,13 @@ InstanceUploadResult VkInstanceUploader::uploadMat4Instances( const VkDeviceSize endBytes = VkDeviceSize(cursorInstances + count) * sizeof(glm::mat4); if (endBytes > frameStrideBytes) { - std::cerr << "[InstanceUploader] exceeds SSBO descriptor range\n"; + LOGE("SSBO descriptor range exceeded"); return out; } VkStagingAlloc stageAlloc = recorder.allocStaging(bytes, /*alignment*/ 16); if (!stageAlloc) { - std::cerr << "[InstanceUploader] allocStaging failed for instances\n"; + LOGE("allocStaging failed for instances"); return out; } @@ -69,3 +68,5 @@ InstanceUploadResult VkInstanceUploader::uploadMat4Instances( return out; } + +} // namespace InstanceUploader diff --git a/src/backend/gpu/upload/vk_instance_uploader.hpp b/src/backend/gpu/upload/vk_instance_uploader.hpp index 52d6809..88752f3 100644 --- a/src/backend/gpu/upload/vk_instance_uploader.hpp +++ b/src/backend/gpu/upload/vk_instance_uploader.hpp @@ -13,18 +13,14 @@ struct InstanceUploadResult { explicit operator bool() const noexcept { return instanceCount != 0; } }; -class VkInstanceUploader { -public: - bool init(); - void shutdown() noexcept; +namespace InstanceUploader { - // TODO: make cursorInstances multi threaded for parallelized - // batching/instance writes - InstanceUploadResult uploadMat4Instances(VkUploadContext::Recorder recorder, - VkBuffer instanceBuffer, - VkDeviceSize frameBaseBytes, - VkDeviceSize frameStrideBytes, - uint32_t maxInstancesPerFrame, - uint32_t &cursorInstances, - std::span models); -}; +// TODO: make cursorInstances multi threaded for parallelized +// batching/instance writes +InstanceUploadResult +uploadMat4Instances(VkUploadContext::Recorder recorder, VkBuffer instanceBuffer, + VkDeviceSize frameBaseBytes, VkDeviceSize frameStrideBytes, + uint32_t maxInstancesPerFrame, uint32_t &cursorInstances, + std::span models); + +} // namespace InstanceUploader diff --git a/src/backend/gpu/upload/vk_lights_uploader.cpp b/src/backend/gpu/upload/vk_lights_uploader.cpp new file mode 100644 index 0000000..24592be --- /dev/null +++ b/src/backend/gpu/upload/vk_lights_uploader.cpp @@ -0,0 +1,65 @@ +#include "backend/gpu/upload/vk_lights_uploader.hpp" +#include "backend/gpu/upload/vk_upload_context.hpp" +#include "backend/profiling/profilers/upload_profiler.hpp" +#include "backend/profiling/telemetry/telemetry.hpp" +#include "engine/logging/log.hpp" +#include "render/scene/lights_gpu.hpp" +#include +#include +#include + +namespace LightsUploader { + +PointLightUploadResult +uploadPointLights(VkUploadContext::Recorder recorder, VkBuffer pointLightBuffer, + VkDeviceSize frameBaseBytes, VkDeviceSize frameStrideBytes, + uint32_t maxPointLightsPerFrame, + std::span lights) { + PointLightUploadResult out{}; + if (!recorder || pointLightBuffer == VK_NULL_HANDLE) { + return out; + } + + const uint32_t wanted = static_cast(lights.size()); + const uint32_t count = std::min(wanted, maxPointLightsPerFrame); + out.lightCount = count; + + const VkDeviceSize bytes = VkDeviceSize(count) * sizeof(PointLightGPU); + if (bytes == 0) { + return out; + } + + if (bytes > frameStrideBytes) { + LOGE("Point light SSBO descriptor range exceeded (bytes={}, stride={})", + static_cast(bytes), static_cast(frameStrideBytes)); + out.lightCount = 0; + return out; + } + + VkStagingAlloc stageAlloc = recorder.allocStaging(bytes, /*alignment=*/16); + if (!stageAlloc) { + LOGE("allocStaging failed for point lights"); + out.lightCount = 0; + return out; + } + + std::memcpy(stageAlloc.ptr, lights.data(), static_cast(bytes)); + + PROFILE_UPLOAD_INC(UploadProfiler::Stat::UploadMemcpyCount); + PROFILE_UPLOAD_ADD(UploadProfiler::Stat::UploadMemcpyBytes, bytes); + + const VkDeviceSize dstOffset = frameBaseBytes; + + recorder.cmdCopyToBuffer(pointLightBuffer, dstOffset, stageAlloc.offset, + bytes); + recorder.cmdBarrierBufferTransferToShader( + pointLightBuffer, dstOffset, bytes, + VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT); + + // PROFILE_UPLOAD_INC(UploadProfiler::Stat::LightUploadCount); + // PROFILE_UPLOAD_ADD(UploadProfiler::Stat::LightUploadBytes, bytes); + + return out; +} + +} // namespace LightsUploader diff --git a/src/backend/gpu/upload/vk_lights_uploader.hpp b/src/backend/gpu/upload/vk_lights_uploader.hpp new file mode 100644 index 0000000..48f3f60 --- /dev/null +++ b/src/backend/gpu/upload/vk_lights_uploader.hpp @@ -0,0 +1,24 @@ +#pragma once + +#include "backend/gpu/upload/vk_upload_context.hpp" +#include "render/scene/lights_gpu.hpp" +#include +#include +#include + +struct PointLightUploadResult { + uint32_t lightCount = 0; + explicit operator bool() const noexcept { return lightCount != 0; } +}; + +namespace LightsUploader { + +// Uploads the whole per-frame light list into that frame slice +PointLightUploadResult uploadPointLights(VkUploadContext::Recorder recorder, + VkBuffer pointLightBuffer, + VkDeviceSize frameBaseBytes, + VkDeviceSize frameStrideBytes, + uint32_t maxPointLightsPerFrame, + std::span lights); + +} // namespace LightsUploader diff --git a/src/backend/gpu/upload/vk_material_uploader.cpp b/src/backend/gpu/upload/vk_material_uploader.cpp index 09e4a48..6526ffb 100644 --- a/src/backend/gpu/upload/vk_material_uploader.cpp +++ b/src/backend/gpu/upload/vk_material_uploader.cpp @@ -2,22 +2,18 @@ #include "backend/gpu/upload/vk_upload_context.hpp" #include "backend/profiling/telemetry/telemetry.hpp" +#include "engine/logging/log.hpp" #include "render/resources/material_gpu.hpp" #include #include -#include #include -bool VkMaterialUploader::init() { return true; } +namespace MaterialUploader { -void VkMaterialUploader::shutdown() noexcept {} - -bool VkMaterialUploader::uploadOne(VkUploadContext::Recorder recorder, - VkBuffer materialBuffer, - VkDeviceSize dstOffsetBytes, - const MaterialGPU &material, - VkPipelineStageFlags dstStage) { +bool uploadOne(VkUploadContext::Recorder recorder, VkBuffer materialBuffer, + VkDeviceSize dstOffsetBytes, const MaterialGPU &material, + VkPipelineStageFlags dstStage) { if (!recorder || materialBuffer == VK_NULL_HANDLE) { return false; } @@ -26,7 +22,7 @@ bool VkMaterialUploader::uploadOne(VkUploadContext::Recorder recorder, VkStagingAlloc stage = recorder.allocStaging(bytes, /*alignment=*/16); if (!stage) { - std::cerr << "[MaterialUploader] allocStaging failed\n"; + LOGE("allocStaging failed"); return false; } @@ -45,3 +41,5 @@ bool VkMaterialUploader::uploadOne(VkUploadContext::Recorder recorder, return true; } + +} // namespace MaterialUploader diff --git a/src/backend/gpu/upload/vk_material_uploader.hpp b/src/backend/gpu/upload/vk_material_uploader.hpp index a5a7e9c..34e9f0e 100644 --- a/src/backend/gpu/upload/vk_material_uploader.hpp +++ b/src/backend/gpu/upload/vk_material_uploader.hpp @@ -5,15 +5,11 @@ #include -class UploadProfiler; +namespace MaterialUploader { -class VkMaterialUploader { -public: - bool init(); - void shutdown() noexcept; +bool uploadOne( + VkUploadContext::Recorder recorder, VkBuffer materialBuffer, + VkDeviceSize dstOffsetBytes, const MaterialGPU &material, + VkPipelineStageFlags dstStage = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT); - bool uploadOne( - VkUploadContext::Recorder recorder, VkBuffer materialBuffer, - VkDeviceSize dstOffsetBytes, const MaterialGPU &material, - VkPipelineStageFlags dstStage = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT); -}; +} diff --git a/src/backend/gpu/upload/vk_texture_uploader.cpp b/src/backend/gpu/upload/vk_texture_uploader.cpp index f7c9e2f..939ed0a 100644 --- a/src/backend/gpu/upload/vk_texture_uploader.cpp +++ b/src/backend/gpu/upload/vk_texture_uploader.cpp @@ -5,6 +5,7 @@ #include "backend/gpu/textures/vk_texture_utils.hpp" #include "backend/gpu/upload/vk_upload_context.hpp" #include "backend/profiling/telemetry/telemetry.hpp" +#include "engine/logging/log.hpp" #include #include @@ -22,16 +23,17 @@ bool VkTextureUploader::init(VkBackendCtx &ctx) { void VkTextureUploader::shutdown() noexcept { m_ctx = nullptr; } bool VkTextureUploader::uploadRGBA8(VkUploadContext::Recorder recorder, - const void *rgbaPixels, uint32_t width, - uint32_t height, VkTexture2D &out, + const void *rgbaPixels, VkFormat fmt, + uint32_t width, uint32_t height, + VkTexture2D &out, VkPipelineStageFlags finalStage) { if (!recorder) { - std::cerr << "[TextureUpload] Invalid recorder\n"; + LOGE("Recorder is invalid"); return false; } if (rgbaPixels == nullptr || width == 0 || height == 0) { - std::cerr << "[TextureUpload] Invalid pixels/size\n"; + LOGE("Pixels/size are invalid"); return false; } @@ -41,9 +43,7 @@ bool VkTextureUploader::uploadRGBA8(VkUploadContext::Recorder recorder, VkStagingAlloc stageAlloc = recorder.allocStaging(size, /*alignment=*/16); if (!stageAlloc) { - std::cerr - << "[TextureUpload] Out of staging space (increase per-frame budget " - "or flush earlier)\n"; + LOGE("Out of staging space"); return false; } @@ -54,11 +54,10 @@ bool VkTextureUploader::uploadRGBA8(VkUploadContext::Recorder recorder, out.shutdown(); - // TODO: check for VK_FORMAT_R8G8B8A8_UNORM - if (!out.image.init2D( - m_ctx->allocator(), width, height, VK_FORMAT_R8G8B8A8_SRGB, - VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT, - VK_IMAGE_TILING_OPTIMAL)) { + if (!out.image.init2D(m_ctx->allocator(), width, height, fmt, + VK_IMAGE_USAGE_TRANSFER_DST_BIT | + VK_IMAGE_USAGE_SAMPLED_BIT, + VK_IMAGE_TILING_OPTIMAL)) { std::cerr << "[TextureUpload] Failed to create device-local image\n"; return false; } @@ -74,13 +73,14 @@ bool VkTextureUploader::uploadRGBA8(VkUploadContext::Recorder recorder, out.device = device; - if (!vkCreateTextureView(device, out.image.handle(), VK_FORMAT_R8G8B8A8_SRGB, - out.view)) { + if (!vkCreateTextureView(device, out.image.handle(), fmt, out.view)) { + LOGE("vkCreateTextureView failed"); out.shutdown(); return false; } if (!vkCreateTextureSampler(device, out.sampler)) { + LOGE("vkCreateTextureSampler failed"); out.shutdown(); return false; } diff --git a/src/backend/gpu/upload/vk_texture_uploader.hpp b/src/backend/gpu/upload/vk_texture_uploader.hpp index 0c472f3..fe25906 100644 --- a/src/backend/gpu/upload/vk_texture_uploader.hpp +++ b/src/backend/gpu/upload/vk_texture_uploader.hpp @@ -14,7 +14,7 @@ class VkTextureUploader { void shutdown() noexcept; bool uploadRGBA8( - VkUploadContext::Recorder recorder, const void *rgbaPixels, + VkUploadContext::Recorder recorder, const void *rgbaPixels, VkFormat fmt, uint32_t width, uint32_t height, VkTexture2D &out, VkPipelineStageFlags finalStage = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT); diff --git a/src/backend/gpu/upload/vk_upload_context.cpp b/src/backend/gpu/upload/vk_upload_context.cpp index 3961717..7e37aeb 100644 --- a/src/backend/gpu/upload/vk_upload_context.cpp +++ b/src/backend/gpu/upload/vk_upload_context.cpp @@ -3,6 +3,7 @@ #include "backend/core/vk_backend_ctx.hpp" #include "backend/gpu/buffers/vk_buffer.hpp" #include "backend/profiling/telemetry/telemetry.hpp" +#include "engine/logging/log.hpp" #include "util/vk_barrier.hpp" #include @@ -10,7 +11,7 @@ #include #include #include -#include +#include #include #include #include @@ -114,7 +115,7 @@ bool VkUploadContext::initCommon(VkBackendCtx &ctx, Mode mode, if (m_pools == nullptr || m_cmds == nullptr || m_fences == nullptr || m_heads == nullptr || m_begun == nullptr || m_hadWork == nullptr) { - std::cerr << "[UploadCtx] Allocation failed\n"; + LOGE("Allocation failed"); shutdown(); return false; } @@ -127,7 +128,7 @@ bool VkUploadContext::initCommon(VkBackendCtx &ctx, Mode mode, if (!m_staging.init(m_ctx->allocator(), totalBytes, VK_BUFFER_USAGE_TRANSFER_SRC_BIT, VkBufferObj::MemUsage::CpuToGpu, /*mapped*/ true)) { - std::cerr << "[UploadCtx] Failed to create staging buffer\n"; + LOGE("Staging buffer creation failed"); shutdown(); return false; } @@ -140,7 +141,7 @@ bool VkUploadContext::initCommon(VkBackendCtx &ctx, Mode mode, VkResult res = vmaMapMemory(m_ctx->allocator(), m_staging.allocation(), &mapped); if (res != VK_SUCCESS || mapped == nullptr) { - std::cerr << "[UploadCtx] vmaMapMemory staging failed: " << res << "\n"; + LOGE("vmaMapMemory staging failed: {}", fmt::underlying(res)); shutdown(); return false; } @@ -157,7 +158,7 @@ bool VkUploadContext::initCommon(VkBackendCtx &ctx, Mode mode, VkResult res = vkCreateFence(m_ctx->device(), &fenceInfo, nullptr, &m_fences[fi]); if (res != VK_SUCCESS) { - std::cerr << "[UploadCtx] vkCreateFence failed: " << res << "\n"; + LOGE("vkCreateFence failed: {}", fmt::underlying(res)); shutdown(); return false; } @@ -175,7 +176,7 @@ bool VkUploadContext::initCommon(VkBackendCtx &ctx, Mode mode, VkResult res = vkCreateCommandPool(m_ctx->device(), &cmdPoolInfo, nullptr, &pool); if (res != VK_SUCCESS) { - std::cerr << "[UploadCtx] vkCreateComandPool failed: " << res << "\n"; + LOGE("vkCreateCommandPool failed: {}", fmt::underlying(res)); shutdown(); return false; } @@ -190,8 +191,7 @@ bool VkUploadContext::initCommon(VkBackendCtx &ctx, Mode mode, VkCommandBuffer cmd = VK_NULL_HANDLE; res = vkAllocateCommandBuffers(m_ctx->device(), &cmdAllocInfo, &cmd); if (res != VK_SUCCESS) { - std::cerr << "[UploadCtx] vkAllocateCommandBuffers failed: " << res - << "\n"; + LOGE("vkAllocateCommandBuffers failed: {}", fmt::underlying(res)); shutdown(); return false; } @@ -210,7 +210,7 @@ bool VkUploadContext::initFrameRing(VkBackendCtx &ctx, uint32_t framesInFlight, VkDeviceSize bytesPerFrameSlice, uint32_t threadCount) { if (framesInFlight == 0 || bytesPerFrameSlice == 0 || threadCount == 0) { - std::cerr << "[UploadCtx] Invalid initFrameRing args\n"; + LOGE("Invalid arguments"); return false; } @@ -221,7 +221,7 @@ bool VkUploadContext::initFrameRing(VkBackendCtx &ctx, uint32_t framesInFlight, bool VkUploadContext::initOneShot(VkBackendCtx &ctx, VkDeviceSize totalBytes, uint32_t threadCount) { if (totalBytes == 0 || threadCount == 0) { - std::cerr << "[UploadCtx] Invalid initOnShot args\n"; + LOGE("Invalid arguments"); return false; } @@ -305,12 +305,12 @@ void VkUploadContext::shutdown() noexcept { bool VkUploadContext::waitAndReset(uint32_t frameIndex) { if (m_ctx == nullptr || m_pools == VK_NULL_HANDLE || m_fences == nullptr) { - std::cerr << "[UploadCtx] waitAndReset invalid state\n"; + LOGE("waitAndReset invalid state"); return false; } if (frameIndex >= m_framesInFlight) { - std::cerr << "[UploadCtx] waitAndReset frameIndex out of range\n"; + LOGE("waitAndReset frameIndex out of range"); return false; } @@ -320,7 +320,7 @@ bool VkUploadContext::waitAndReset(uint32_t frameIndex) { VkResult res = vkWaitForFences(m_ctx->device(), 1, &fence, VK_TRUE, UINT64_MAX); if (res != VK_SUCCESS) { - std::cerr << "[UploadCtx] vkWaitForFences failed: " << res << "\n"; + LOGE("vkWaitForFences failed: {}", fmt::underlying(res)); return false; } @@ -341,7 +341,7 @@ bool VkUploadContext::waitAndReset(uint32_t frameIndex) { bool VkUploadContext::beginFrame(uint32_t frameIndex) { if (m_mode != Mode::FrameRing) { - std::cerr << "[UploadCtx] beginFrame called on non-FrameRing context\n"; + LOGE("beginFrame called on non-FrameRing context"); return false; } return waitAndReset(frameIndex); @@ -349,7 +349,7 @@ bool VkUploadContext::beginFrame(uint32_t frameIndex) { bool VkUploadContext::beginBatch() { if (m_mode != Mode::OneShot) { - std::cerr << "[UploadCtx] beginBatch called on non-OneShot context\n"; + LOGE("beginBatch called on non-OneShot context"); return false; } return waitAndReset(0); @@ -362,6 +362,7 @@ VkUploadContext::Recorder VkUploadContext::recorder(uint32_t frameIndex, } if (threadIndex >= m_threadCount) { + LOGE("threadIndex is larger than threadCount"); return {}; } @@ -370,6 +371,7 @@ VkUploadContext::Recorder VkUploadContext::recorder(uint32_t frameIndex, } if (frameIndex >= m_framesInFlight) { + LOGE("frameIndex is larger than framesInFlight"); return {}; } @@ -377,8 +379,6 @@ VkUploadContext::Recorder VkUploadContext::recorder(uint32_t frameIndex, } bool VkUploadContext::beginCmd(uint32_t frameIndex, uint32_t threadIndex) { - // TODO: make single method for this, renderer, and command to use - VkCommandBuffer cmd = cmdAt(frameIndex, threadIndex); if (cmd == VK_NULL_HANDLE) { return false; @@ -390,7 +390,7 @@ bool VkUploadContext::beginCmd(uint32_t frameIndex, uint32_t threadIndex) { VkResult res = vkBeginCommandBuffer(cmd, &bufBeginInfo); if (res != VK_SUCCESS) { - std::cerr << "[UploadCtx] vkBeginCommandBuffer failed: " << res << "\n"; + LOGE("vkBeginCommandBuffer failed: {}", fmt::underlying(res)); return false; } @@ -406,7 +406,7 @@ bool VkUploadContext::endCmd(uint32_t frameIndex, uint32_t threadIndex) { VkResult res = vkEndCommandBuffer(cmd); if (res != VK_SUCCESS) { - std::cerr << "[UploadCtx] vkEndCommandBuffer failed: " << res << "\n"; + LOGE("vkEndCommandBuffer failed: {}", fmt::underlying(res)); return false; } @@ -481,7 +481,7 @@ bool VkUploadContext::submit(uint32_t frameIndex, bool wait) { VkFence fence = m_fences[frameIndex]; VkResult res = vkResetFences(m_ctx->device(), 1, &fence); if (res != VK_SUCCESS) { - std::cerr << "[UploadCtx] vkResetFences failed: " << res << "\n"; + LOGE("vkResetFences failed: {}", fmt::underlying(res)); return false; } @@ -492,7 +492,7 @@ bool VkUploadContext::submit(uint32_t frameIndex, bool wait) { res = vkQueueSubmit(m_ctx->graphicsQueue(), 1, &submitInfo, fence); if (res != VK_SUCCESS) { - std::cerr << "[UploadCtx] vkQueueSubmit failed: " << res << "\n"; + LOGE("vkQueueSubmit failed: {}", fmt::underlying(res)); return false; } @@ -501,7 +501,7 @@ bool VkUploadContext::submit(uint32_t frameIndex, bool wait) { if (wait) { res = vkWaitForFences(m_ctx->device(), 1, &fence, VK_TRUE, UINT64_MAX); if (res != VK_SUCCESS) { - std::cerr << "[UploadCtx] vkWaitForFences(wait) failed: " << res << "\n"; + LOGE("vkWaitForFences(wait) failed: {}", fmt::underlying(res)); return false; } } @@ -511,7 +511,7 @@ bool VkUploadContext::submit(uint32_t frameIndex, bool wait) { bool VkUploadContext::flushFrame(uint32_t frameIndex, bool wait) { if (m_mode != Mode::FrameRing) { - std::cerr << "[UploadCtx] flushFrame called on non-FrameRing context\n"; + LOGE("flushFrame called on non-FrameRing context"); return false; } @@ -524,7 +524,7 @@ bool VkUploadContext::flushFrame(uint32_t frameIndex, bool wait) { bool VkUploadContext::flushBatch(bool wait) { if (m_mode != Mode::OneShot) { - std::cerr << "[UploadCtx] flushBatch called on non-OneShot context\n"; + LOGE("flushBatch called on non-oneShot context"); return false; } @@ -553,8 +553,8 @@ void VkUploadContext::transitionImage(VkCommandBuffer cmd, VkImage image, srcAccess = VK_ACCESS_TRANSFER_WRITE_BIT; dstAccess = VK_ACCESS_SHADER_READ_BIT; } else { - std::cerr << "[UploadCtx] Unsupported layout transition " << oldLayout - << " -> " << newLayout << "\n"; + LOGE("Unsupported layout transition {} -> {}", fmt::underlying(oldLayout), + fmt::underlying(newLayout)); return; } @@ -649,8 +649,8 @@ void VkUploadContext::Recorder::cmdUploadRGBA8ToImage( const uint32_t k = m_ctx->idx(m_frameIndex, m_threadIndex); m_ctx->m_hadWork[k] = 1; - m_ctx->transitionImage(c, image, VK_IMAGE_LAYOUT_UNDEFINED, - VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, finalStage); + transitionImage(c, image, VK_IMAGE_LAYOUT_UNDEFINED, + VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, finalStage); VkBufferImageCopy region{}; region.bufferOffset = srcOffset; @@ -666,6 +666,6 @@ void VkUploadContext::Recorder::cmdUploadRGBA8ToImage( vkCmdCopyBufferToImage(c, m_ctx->m_staging.handle(), image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, ®ion); - m_ctx->transitionImage(c, image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, - finalLayout, finalStage); + transitionImage(c, image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, finalLayout, + finalStage); } diff --git a/src/backend/gpu/upload/vk_upload_context.hpp b/src/backend/gpu/upload/vk_upload_context.hpp index 6589907..fe62442 100644 --- a/src/backend/gpu/upload/vk_upload_context.hpp +++ b/src/backend/gpu/upload/vk_upload_context.hpp @@ -125,9 +125,9 @@ class VkUploadContext { VkStagingAlloc allocStaging(uint32_t frameIndex, VkDeviceSize size, VkDeviceSize alignment); - void transitionImage(VkCommandBuffer cmd, VkImage image, - VkImageLayout oldLayout, VkImageLayout newLayout, - VkPipelineStageFlags finalStage); + static void transitionImage(VkCommandBuffer cmd, VkImage image, + VkImageLayout oldLayout, VkImageLayout newLayout, + VkPipelineStageFlags finalStage); [[nodiscard]] uint32_t idx(uint32_t frameIndex, uint32_t threadIndex) const noexcept { diff --git a/src/backend/gpu/vk_vertex_layout.hpp b/src/backend/gpu/vk_vertex_layout.hpp index 7668984..5a90caf 100644 --- a/src/backend/gpu/vk_vertex_layout.hpp +++ b/src/backend/gpu/vk_vertex_layout.hpp @@ -17,9 +17,9 @@ inline VkVertexInputBindingDescription bindingDescription() { return binding; } -inline std::array +inline std::array attributeDescriptions() { - std::array attrs{}; + std::array attrs{}; // Location 0 -> vec3 position attrs[0].location = 0; @@ -27,17 +27,23 @@ attributeDescriptions() { attrs[0].format = VK_FORMAT_R32G32B32_SFLOAT; attrs[0].offset = offsetof(engine::Vertex, pos); - // Location 1 -> vec3 color + // Location 1 -> vec3 normal attrs[1].location = 1; attrs[1].binding = 0; attrs[1].format = VK_FORMAT_R32G32B32_SFLOAT; - attrs[1].offset = offsetof(engine::Vertex, color); + attrs[1].offset = offsetof(engine::Vertex, normal); - // Location 2 -> vec2 uv + // Location 2 -> vec3 color attrs[2].location = 2; attrs[2].binding = 0; - attrs[2].format = VK_FORMAT_R32G32_SFLOAT; - attrs[2].offset = offsetof(engine::Vertex, uv); + attrs[2].format = VK_FORMAT_R32G32B32_SFLOAT; + attrs[2].offset = offsetof(engine::Vertex, color); + + // Location 3 -> vec2 uv + attrs[3].location = 3; + attrs[3].binding = 0; + attrs[3].format = VK_FORMAT_R32G32_SFLOAT; + attrs[3].offset = offsetof(engine::Vertex, uv); return attrs; } diff --git a/src/backend/graphics/vk_pipeline.cpp b/src/backend/graphics/vk_pipeline.cpp index 6f0c333..75d40d2 100644 --- a/src/backend/graphics/vk_pipeline.cpp +++ b/src/backend/graphics/vk_pipeline.cpp @@ -6,8 +6,8 @@ #include #include +#include #include -#include #include #include #include @@ -30,12 +30,12 @@ bool VkGraphicsPipeline::init(VkDevice device, VkFormat colorFormat, VulkanShaderModule fragModule; if (!createShaderModuleFromFile(m_device, vertSpvPath, vertModule)) { - std::cerr << "[Pipeline] Failed to load vertex shader\n"; + LOGE("Vertex shader loading failed"); return false; } if (!createShaderModuleFromFile(m_device, fragSpvPath, fragModule)) { - std::cerr << "[Pipeline] Failed to load fragment shader\n"; + LOGE("Fragment shader loading failed"); return false; } @@ -60,7 +60,6 @@ bool VkGraphicsPipeline::init(VkDevice device, VkFormat colorFormat, return false; } - std::cout << "[Pipeline] Graphics pipeline created\n"; return true; } @@ -175,8 +174,7 @@ bool VkGraphicsPipeline::createGraphicsPipeline( const VkResult res = vkCreateGraphicsPipelines( m_device, VK_NULL_HANDLE, 1, &pipelineInfo, nullptr, &m_graphicsPipeline); if (res != VK_SUCCESS) { - std::cerr << "[Pipeline] vkCreateGraphicsPipelines() failed: " << res - << "\n"; + LOGE("vkCreateGraphicsPipelines() failed: {}", fmt::underlying(res)); m_graphicsPipeline = VK_NULL_HANDLE; return false; } diff --git a/src/backend/presentation/vk_swapchain.cpp b/src/backend/presentation/vk_swapchain.cpp index 968bb39..51d20ca 100644 --- a/src/backend/presentation/vk_swapchain.cpp +++ b/src/backend/presentation/vk_swapchain.cpp @@ -67,7 +67,6 @@ VkSurfaceFormatKHR VkSwapchain::chooseSwapSurfaceFormat( } // Prefer BGRA8 and sRGB_NONLINEAR - // TODO: Make this user defined through a public API for (const auto &availableFormat : availableFormats) { if (availableFormat.colorSpace == VK_COLOR_SPACE_SRGB_NONLINEAR_KHR && availableFormat.format == VK_FORMAT_B8G8R8A8_SRGB) { diff --git a/src/backend/shaders/include/common.glsl b/src/backend/shaders/include/common.glsl new file mode 100644 index 0000000..72343df --- /dev/null +++ b/src/backend/shaders/include/common.glsl @@ -0,0 +1,64 @@ +#ifndef COMMON_GLSL +#define COMMON_GLSL + +const uint kMaxDirLights = 4u; + +struct CameraUBO { + mat4 view; + mat4 proj; + vec4 cameraPosWS_pad; +}; + +struct DebugUBO { + uint view; + uint flags; + float value0; + float value1; +}; + +struct DirectionalLight { + vec4 directionWS_illuminanceLux; // xyz dir, w lux + vec4 colorLinear_pad; // rgb color +}; + +struct PointLight { + vec3 positionWS; + float radius; + + vec3 colorLinear; + float lumens; +}; + +struct SceneUBO { + CameraUBO camera; + DebugUBO dbg; + + // TODO: pack into single vec + uint pointLightCount; + uint dirLightCount; + uint padA; + uint padB; + + DirectionalLight dirLights[kMaxDirLights]; +}; + +// Material table +// 80 bytes per material +struct Material { + vec4 baseColorFactor; // rgba + vec4 emissiveFactor; // rgb + pad + + // x=metallic, y=roughness, z=aoStrength, w=alphaCutoff + vec4 mrAoAlpha; + + // Texture indices + uvec4 tex0; // x=baseColor, y=normal, z=metalRough, w=occlusion + uvec4 tex1; // x=emissive, y=reserved, z=reserved, w=reserved + + // flags: bits for alphaMode, doubleSided, etc. + uvec4 flags; +}; + +const uint kNoTex = 0xFFFFFFFFu; + +#endif // COMMON_GLSL diff --git a/src/backend/shaders/shader.frag b/src/backend/shaders/shader.frag index 18e59b1..11e936d 100644 --- a/src/backend/shaders/shader.frag +++ b/src/backend/shaders/shader.frag @@ -1,36 +1,220 @@ #version 450 +#extension GL_GOOGLE_include_directive : require +#extension GL_EXT_nonuniform_qualifier : require -layout(set=1, binding=0) uniform sampler2D u_baseColor; +#include "common.glsl" -layout(location = 0) in vec3 v_color; -layout(location = 1) in vec2 v_uv; -layout(location = 2) flat in uint v_matId; +const float PI = 3.14159265358979323846; -layout(location = 0) out vec4 outColor; +float saturate(float x) { return clamp(x, 0.0, 1.0); } + +vec3 radianceDirectional(DirectionalLight Ld) { + vec3 color = Ld.colorLinear_pad.rgb; + float lux = Ld.directionWS_illuminanceLux.w; + return color * lux; +} + +vec3 directionToDirectionalLight(DirectionalLight Ld) { + return normalize(-Ld.directionWS_illuminanceLux.xyz); +} + +bool getPointLight(vec3 P, PointLight Lp, out vec3 L, out vec3 radiance) { + vec3 toL = Lp.positionWS - P; + float d2 = dot(toL, toL); + float d = sqrt(d2); + if (d <= 1e-4) return false; + + // Radius cutoff + float att = 1.0 - saturate(d / max(Lp.radius, 1e-4)); + att = att * att; + + L = toL / d; + + float inv4pi = 0.07957747155; // 1/(4*pi) + float E = (Lp.lumens * inv4pi) / max(d2, 1e-4); + + radiance = (Lp.colorLinear * E) * att; + return true; +} + +float D_GGX(float NoH, float a2) { + float denom = NoH * NoH * (a2 - 1.0) + 1.0; + return a2 / max(PI * denom * denom, 1e-8); +} + +float G_SchlickGGX(float NoX, float k) { + return NoX / max(NoX * (1.0 - k) + k, 1e-8); +} + +float G_Smith(float NoV, float NoL, float k) { + return G_SchlickGGX(NoV, k) * G_SchlickGGX(NoL, k); +} + +vec3 F_Schlick(vec3 F0, float VoH) { + float f = pow(1.0 - VoH, 5.0); + return F0 + (1.0 - F0) * f; +} + +vec3 evalDirectBRDF(vec3 N, vec3 V, vec3 L, vec3 radiance, vec3 albedo, float metallic, float roughness) { + float NoV = saturate(dot(N, V)); + float NoL = saturate(dot(N, L)); + if (NoV <= 1e-4 || NoL <= 1e-4) return vec3(0.0); + + vec3 H = normalize(V + L); + + float NoH = saturate(dot(N, H)); + float VoH = saturate(dot(V, H)); + + // Perceptual roughness -> alpha + float r = clamp(roughness, 0.04, 1.0); + float a = r * r; + float a2 = a * a; + + // Fresnel at normal incidence + vec3 F0 = mix(vec3(0.04), albedo, metallic); + + vec3 F = F_Schlick(F0, VoH); + float D = D_GGX(NoH, a2); + + float k = (r + 1.0); + k = (k * k) * 0.125; // /8 + float G = G_Smith(NoV, NoL, k); + + vec3 spec = (D * G) * F / max(4.0 * NoV * NoL, 1e-4); + + // Diffuse with lambert, energy compensated + vec3 kd = (vec3(1.0) - F) * (1.0 - metallic); + vec3 diff = kd * albedo * (1.0 / PI); -// Material table -// 80 bytes per material -struct Material { - vec4 baseColorFactor; // rgba - vec4 emissiveFactor; // rgb + pad + return (diff + spec) * radiance * NoL; +} + +layout(set = 1, binding = 0) uniform sampler2D g_tex[]; - // x=metallic, y=roughness, z=aoStrength, w=alphaCutoff - vec4 mrAoAlpha; +layout(location = 0) in vec3 vColor; +layout(location = 1) in vec2 v_uv; +layout(location = 2) flat in uint v_matId; +layout(location = 3) in vec3 v_worldPos; +layout(location = 4) in vec3 v_worldN; - // Texture indices - uvec4 tex0; // x=baseColor, y=normal, z=metalRough, w=occlusion - uvec4 tex1; // x=emissive, y=reserved, z=reserved, w=reserved +layout(location = 0) out vec4 outColor; - // flags: bits for alphaMode, doubleSided, etc. - uvec4 flags; -}; +layout(set = 0, binding = 0, std140) uniform SceneSet { + SceneUBO scene; +} g_scene[]; layout(set = 0, binding = 2, std430) readonly buffer MaterialSSBO { Material materials[]; } mats; +layout(set = 0, binding = 3, std430) readonly buffer PointLightSSBO { + PointLight lights[]; +} g_pointLights[]; + +layout(push_constant) uniform Push { + uint frameIndex; + uint baseInstance; + uint materialId; +} push; + +vec4 sampleTex(uint idx, vec2 uv, vec4 fallback) { + if (idx == kNoTex) return fallback; + return texture(g_tex[nonuniformEXT(idx)], uv); +} + void main() { - vec4 tex = texture(u_baseColor, v_uv); - vec4 factor = mats.materials[v_matId].baseColorFactor; - outColor = tex * factor; + uint f = push.frameIndex; + DebugUBO dbg = g_scene[nonuniformEXT(f)].scene.dbg; + + Material m = mats.materials[v_matId]; + + // BaseColor + vec4 baseTex = sampleTex(m.tex0.x, v_uv, vec4(1.0)); + vec4 base = baseTex * m.baseColorFactor; + + // MetallicRoughness + vec4 mrTex = sampleTex(m.tex0.z, v_uv, vec4(0.0, 1.0, 0.0, 1.0)); + float roughness = clamp(m.mrAoAlpha.y * mrTex.g, 0.001, 1.0); + float metallic = clamp(m.mrAoAlpha.x * mrTex.b, 0.0, 1.0); + + // Occlusion (linear) + vec4 aoTex = sampleTex(m.tex0.w, v_uv, vec4(1.0)); + float ao = mix(1.0, aoTex.r, clamp(m.mrAoAlpha.z, 0.0, 1.0)); + + // Emissive (sRGB) + vec3 emissiveTex = sampleTex(m.tex1.x, v_uv, vec4(0.0, 0.0, 0.0, 1.0)).rgb; + vec3 emissive = emissiveTex * m.emissiveFactor.rgb; + + // Alpha + float alpha = base.a; + uint alphaMode = (m.flags.x & 0x3u); // 0 opaque, 1 mask, 2 blend + if (alphaMode == 0u) alpha = 1.0; + else if (alphaMode == 1u) { + if (alpha < m.mrAoAlpha.w) discard; + alpha = 1.0; + } + + vec3 N = normalize(v_worldN); + + // Directional: from point -> camera + SceneUBO s = g_scene[nonuniformEXT(f)].scene; + vec3 camPos = s.camera.cameraPosWS_pad.xyz; + vec3 V = normalize(camPos - v_worldPos); + + vec3 albedo = base.rgb; + + vec3 lit = vec3(0.0); + + // Directional lights + uint dlCount = min(s.dirLightCount, kMaxDirLights); + for (uint i = 0u; i < dlCount; ++i) { + DirectionalLight Ld = s.dirLights[i]; + vec3 L = directionToDirectionalLight(Ld); + vec3 radiance = radianceDirectional(Ld); + lit += evalDirectBRDF(N, V, L, radiance, albedo, metallic, roughness); + } + + // Point lights + uint plCount = s.pointLightCount; + for (uint i = 0u; i < plCount; ++i) { + PointLight Lp = g_pointLights[nonuniformEXT(f)].lights[i]; + vec3 L; + vec3 radiance; + if (getPointLight(v_worldPos, Lp, L, radiance)) { + lit += evalDirectBRDF(N, V, L, radiance, albedo, metallic, roughness); + } + } + + // TODO: remove when IBL is made + vec3 up = vec3(0.0, 0.0, 1.0); + + float hemi = 0.5 + 0.5 * dot(N, up); + vec3 sky = vec3(0.04); + vec3 ground = vec3(0.01); + + vec3 F0 = mix(vec3(0.04), albedo, metallic); + vec3 F = F_Schlick(F0, saturate(dot(N, V))); + vec3 kd = (vec3(1.0) - F) * (1.0 - metallic); + + vec3 ambient = kd * albedo * mix(ground, sky, hemi); + lit += ambient; + + lit *= ao; + + if (dbg.view == 1u) { outColor = vec4(base.rgb, 1.0); return; } + if (dbg.view == 2u) { outColor = vec4(vec3(metallic), 1.0); return; } + if (dbg.view == 3u) { outColor = vec4(vec3(roughness), 1.0); return; } + if (dbg.view == 4u) { outColor = vec4(vec3(ao), 1.0); return; } + if (dbg.view == 5u) { outColor = vec4(emissive, 1.0); return; } + if (dbg.view == 6u) { outColor = vec4(vColor, 1.0); return; } // vertex color + if (dbg.view == 7u) { outColor = vec4(fract(v_uv), 0.0, 1.0); return; } + if (dbg.view == 8u) { outColor = vec4(normalize(v_worldN) * 0.5 + 0.5, 1.0); return; } + if (dbg.view == 9u) { outColor = vec4(fract(v_worldPos * 0.1), 1.0); return; } + if (dbg.view == 10u) { outColor = vec4(lit, 1.0); return; } + + // TODO: remove after HDR + tonemapping + float exposure = 1.0 / 1.0; + vec3 colorOut = 1.0 - exp(-(lit + emissive) * exposure); + + outColor = vec4(colorOut, alpha); } diff --git a/src/backend/shaders/shader.vert b/src/backend/shaders/shader.vert index 4bff1dd..655277f 100644 --- a/src/backend/shaders/shader.vert +++ b/src/backend/shaders/shader.vert @@ -1,54 +1,54 @@ #version 450 +#extension GL_GOOGLE_include_directive : require +#extension GL_EXT_nonuniform_qualifier : require + +#include "common.glsl" layout(location = 0) in vec3 inPos; -layout(location = 1) in vec3 inColor; -layout(location = 2) in vec2 inUV; +layout(location = 1) in vec3 inNormal; +layout(location = 2) in vec3 inColor; +layout(location = 3) in vec2 inUV; layout(location = 0) out vec3 vColor; layout(location = 1) out vec2 v_uv; -layout(location = 2) out uint v_matId; +layout(location = 2) flat out uint v_matId; +layout(location = 3) out vec3 v_worldPos; +layout(location = 4) out vec3 v_worldN; -layout(set = 0, binding = 0) uniform CameraUBO { - mat4 view; - mat4 proj; -} camera; +layout(set = 0, binding = 0, std140) uniform SceneSet { + SceneUBO scene; +} g_scene[]; // instance data -layout(set = 0, binding = 1) readonly buffer InstanceSSBO { +layout(set = 0, binding = 1, std430) readonly buffer InstanceSSBO { mat4 model[]; -} inst; - -// Material table -// 80 bytes per material -struct Material { - vec4 baseColorFactor; // rgba - vec4 emissiveFactor; // rgb + pad - - // x=metallic, y=roughness, z=aoStrength, w=alphaCutoff - vec4 mrAoAlpha; - - // Texture indices - uvec4 tex0; // x=baseColor, y=normal, z=metalRough, w=occlusion - uvec4 tex1; // x=emissive, y=reserved, z=reserved, w=reserved - - // flags: bits for alphaMode, doubleSided, etc. - uvec4 flags; -}; +} g_inst[]; layout(set = 0, binding = 2, std430) readonly buffer MaterialSSBO { Material materials[]; } mats; layout(push_constant) uniform Push { + uint frameIndex; uint baseInstance; uint materialId; } push; void main() { + uint f = push.frameIndex; uint idx = push.baseInstance + gl_InstanceIndex; - mat4 M = inst.model[idx]; - gl_Position = camera.proj * camera.view * M * vec4(inPos, 1.0); + mat4 M = g_inst[nonuniformEXT(f)].model[idx]; + SceneUBO s = g_scene[nonuniformEXT(f)].scene; + + vec4 wp = M * vec4(inPos, 1.0); + v_worldPos = wp.xyz; + + mat3 Nmat = transpose(inverse(mat3(M))); + v_worldN = normalize(Nmat * inNormal); + + gl_Position = s.camera.proj * s.camera.view * wp; + v_uv = inUV; vColor = inColor; v_matId = push.materialId; diff --git a/src/engine/assets/gltf/gltf_cpu_loader.cpp b/src/engine/assets/gltf/gltf_cpu_loader.cpp index 07be70f..5b75503 100644 --- a/src/engine/assets/gltf/gltf_cpu_loader.cpp +++ b/src/engine/assets/gltf/gltf_cpu_loader.cpp @@ -1,4 +1,5 @@ #include "gltf_cpu_loader.hpp" +#include "engine/logging/log.hpp" #include #include @@ -8,9 +9,11 @@ #include #include #include +#include #include #include #include +#include #include #include #include @@ -102,23 +105,12 @@ static void readVecN(const cgltf_accessor *acc, int n, } } -static std::string baseColorUri(const cgltf_material *material) { - if (material == nullptr) { +static std::string textureUri(const cgltf_texture_view &view) { + if (view.texture == nullptr || view.texture->image == nullptr) { return {}; } - // TODO: add roughness - const cgltf_pbr_metallic_roughness &pbr = material->pbr_metallic_roughness; - if (pbr.base_color_texture.texture != nullptr) { - return {}; - }; - - const cgltf_texture *tex = pbr.base_color_texture.texture; - if (tex == nullptr || tex->image == nullptr) { - return {}; - } - - const cgltf_image *img = tex->image; + const cgltf_image *img = view.texture->image; if (img->uri == nullptr) { return {}; } @@ -179,8 +171,34 @@ static void loadMaterials(const cgltf_data *data, GltfSceneCpu &out, static_cast(out.materials.size()); GltfMaterialCpu m{}; - m.baseColorTextureUri = baseColorUri(mat); + + m.baseColorTextureUri = + textureUri(mat->pbr_metallic_roughness.base_color_texture); + m.metallicRoughnessTextureUri = + textureUri(mat->pbr_metallic_roughness.metallic_roughness_texture); + m.normalTextureUri = textureUri(mat->occlusion_texture); + m.emissiveTextureUri = textureUri(mat->emissive_texture); + m.baseColorFactor = baseColorFactor(mat); + m.emissiveFactor = + glm::vec3(mat->emissive_factor[0], mat->emissive_factor[1], + mat->emissive_factor[2]); + + m.metallicFactor = mat->pbr_metallic_roughness.metallic_factor; + m.roughnessFactor = mat->pbr_metallic_roughness.roughness_factor; + + m.occlusionStrength = mat->occlusion_texture.scale; + m.doubleSided = (mat->double_sided != 0); + + if (mat->alpha_mode == cgltf_alpha_mode_mask) { + m.alphaMode = GltfAlphaMode::Mask; + } else if (mat->alpha_mode == cgltf_alpha_mode_blend) { + m.alphaMode = GltfAlphaMode::Blend; + } else { + m.alphaMode = GltfAlphaMode::Opaque; + } + + m.alphaCutoff = mat->alpha_cutoff; out.materials.push_back(m); materialMap[mat] = outIdx; @@ -193,6 +211,7 @@ static void loadTrianglePrimitives(const cgltf_primitive *primitive, GltfSceneCpu &out, PrimitiveMap &primitiveMap) { const cgltf_accessor *posAcc = nullptr; + const cgltf_accessor *nrmAcc = nullptr; const cgltf_accessor *uvAcc = nullptr; const cgltf_accessor *colAcc = nullptr; @@ -204,6 +223,10 @@ static void loadTrianglePrimitives(const cgltf_primitive *primitive, posAcc = a.data; } + if (a.type == cgltf_attribute_type_normal) { + nrmAcc = a.data; + } + if (a.type == cgltf_attribute_type_texcoord && a.index == 0) { uvAcc = a.data; } @@ -225,10 +248,19 @@ static void loadTrianglePrimitives(const cgltf_primitive *primitive, // Read arrays std::vector posF; + std::vector nrmF; std::vector uvF; std::vector colF; readVecN(posAcc, 3, posF); + if (nrmAcc != nullptr) { + if (nrmAcc->type != cgltf_type_vec3) { + LOGE("NORMAL not vec3; ignoring"); + } else { + readVecN(nrmAcc, 3, nrmF); + } + } + if (uvAcc != nullptr) { if (uvAcc->type != cgltf_type_vec2) { std::cerr << "[gltf] TEXCOORD_0 not vec2; ignoring\n"; @@ -277,6 +309,13 @@ static void loadTrianglePrimitives(const cgltf_primitive *primitive, } } + // Default normals + vert.normal = {0.0F, 0.0F, 1.0F}; + if (!nrmF.empty()) { + vert.normal = {nrmF[(vertexIdx * 3) + 0], nrmF[(vertexIdx * 3) + 1], + nrmF[(vertexIdx * 3) + 2]}; + } + // Default UV vert.uv = {0.0F, 0.0F}; if (!uvF.empty()) { @@ -302,6 +341,45 @@ static void loadTrianglePrimitives(const cgltf_primitive *primitive, } } + // Generate normals if missing + if (nrmF.empty()) { + std::vector acc(meshData.vertices.size(), glm::vec3(0.0F)); + + auto addTri = [&](uint32_t i0, uint32_t i1, uint32_t i2) { + const glm::vec3 p0 = meshData.vertices[i0].pos; + const glm::vec3 p1 = meshData.vertices[i1].pos; + const glm::vec3 p2 = meshData.vertices[i2].pos; + glm::vec3 n = glm::cross(p1 - p0, p2 - p0); + acc[i0] += n; + acc[i1] += n; + acc[i2] += n; + }; + + if (!meshData.indices.empty()) { + for (size_t i = 0; i + 2 < meshData.indices.size(); i += 3) { + addTri(meshData.indices[i], meshData.indices[i + 1], + meshData.indices[i + 2]); + } + } else { + // non indexed triangles assumed to be in order + for (size_t i = 0; i + 2 < meshData.vertices.size(); i += 3) { + addTri((uint32_t)i, (uint32_t)i + 1, (uint32_t)i + 2); + } + } + + for (size_t i = 0; i < meshData.vertices.size(); ++i) { + glm::vec3 n = acc[i]; + float len2 = glm::dot(n, n); + if (len2 > 1e-20F) { + n = glm::normalize(n); + } else { + n = {0.0F, 0.0F, 1.0F}; + } + + meshData.vertices[i].normal = n; + } + } + std::uint32_t matIdx = UINT32_MAX; if (primitive->material != nullptr) { auto it = materialMap.find(primitive->material); diff --git a/src/engine/assets/gltf/gltf_gpu_builder.cpp b/src/engine/assets/gltf/gltf_gpu_builder.cpp index 895faf3..7a0da1a 100644 --- a/src/engine/assets/gltf/gltf_gpu_builder.cpp +++ b/src/engine/assets/gltf/gltf_gpu_builder.cpp @@ -1,62 +1,112 @@ #include "engine/assets/gltf/gltf_gpu_builder.hpp" #include "engine/assets/gltf/gltf_path.hpp" -#include "render/resources/material_gpu.hpp" #include "render/resources/material_system.hpp" #include +#include #include -#include +#include #include +#include namespace engine::assets { +namespace { + +struct TexCacheKey { + std::string path; + MaterialSystem::TextureUsage usage; + + bool operator==(const TexCacheKey &other) const noexcept { + return usage == other.usage && path == other.path; + } +}; + +struct TexCacheKeyHash { + size_t operator()(const TexCacheKey &key) const noexcept { + std::hash hs; + std::hash hi; + size_t h = hs(key.path); + h ^= (hi(static_cast(key.usage)) + 0x9e3779b97f4a7c15ULL + (h << 6) + + (h >> 2)); + return h; + } +}; + +} // namespace + bool buildGltfSceneGpu(Renderer &renderer, const std::string &gltfPath, const GltfSceneCpu &cpu, GltfSceneGpu &outGpu, const GltfBuildOptions &options) { outGpu = {}; outGpu.materialIds.resize(cpu.materials.size(), UINT32_MAX); - // TODO: cache textures by resolved path - std::unordered_map texCache; + std::unordered_map texCache; + + auto loadTexCached = + [&](const std::string &uri, + MaterialSystem::TextureUsage usage) -> std::optional { + if (uri.empty()) { + return std::nullopt; + } + + const std::string texPath = resolveUriRelativeToFile(gltfPath, uri); + TexCacheKey key{.path = texPath, .usage = usage}; + + if (auto it = texCache.find(key); it != texCache.end()) { + if (it->second.id != UINT32_MAX) { + return it->second; + } + + return std::nullopt; + } + + TextureHandle handle = + renderer.loadTextureFromFile(texPath, options.flipTextureY, usage); + + texCache.emplace(std::move(key), handle); + + if (handle.id == UINT32_MAX) { + return std::nullopt; + } + + return handle; + }; for (size_t materialIdx = 0; materialIdx < cpu.materials.size(); ++materialIdx) { const auto &m = cpu.materials[materialIdx]; - uint32_t matId = UINT32_MAX; + MaterialSystem::MaterialDescription desc{}; + desc.baseColorFactor = m.baseColorFactor; + desc.emissiveFactor = m.emissiveFactor; - if (!m.baseColorTextureUri.empty()) { - const std::string texPath = - resolveUriRelativeToFile(gltfPath, m.baseColorTextureUri); + desc.metallicFactor = m.metallicFactor; + desc.roughnessFactor = m.roughnessFactor; + desc.ambientOcclusionFactor = m.occlusionStrength; + desc.alphaCutoff = m.alphaCutoff; - std::cout << "baseColorTextureUri found\n"; + desc.alphaMode = static_cast(m.alphaMode); + desc.doubleSided = m.doubleSided; - TextureHandle texHandle{UINT32_MAX}; - if (auto item = texCache.find(texPath); item != texCache.end()) { - texHandle = item->second; - } else { - texHandle = - renderer.createTextureFromFile(texPath, options.flipTextureY); - if (texHandle.id != UINT32_MAX) { - texCache.emplace(texPath, texHandle); - } - } + // Textures + desc.baseColorTexture = loadTexCached(m.baseColorTextureUri, + MaterialSystem::TextureUsage::sRGB); - if (texHandle.id != UINT32_MAX) { - matId = renderer.createMaterialFromTexture(texHandle); + desc.emissiveTexture = + loadTexCached(m.emissiveTextureUri, MaterialSystem::TextureUsage::sRGB); - // set factor (texture * factor) - if (matId != UINT32_MAX) { - MaterialGPU gpu{}; - gpu.baseColorFactor = m.baseColorFactor; - (void)renderer.updateMaterialGPU(matId, gpu); - } - } - } else { - matId = renderer.createMaterialFromBaseColorFactor(m.baseColorFactor); - } + desc.metallicRoughnessTexture = loadTexCached( + m.metallicRoughnessTextureUri, MaterialSystem::TextureUsage::UNORM); + + desc.ambientOcclusionTexture = loadTexCached( + m.occlusionTextureUri, MaterialSystem::TextureUsage::UNORM); + + desc.normal = + loadTexCached(m.normalTextureUri, MaterialSystem::TextureUsage::UNORM); + const uint32_t matId = renderer.createMaterial(desc); outGpu.materialIds[materialIdx] = matId; } diff --git a/src/engine/assets/gltf/gltf_types.hpp b/src/engine/assets/gltf/gltf_types.hpp index 88b3399..58fa708 100644 --- a/src/engine/assets/gltf/gltf_types.hpp +++ b/src/engine/assets/gltf/gltf_types.hpp @@ -2,19 +2,33 @@ #include "engine/mesh/mesh_data.hpp" +#include #include +#include #include #include #include namespace engine::assets { +enum class GltfAlphaMode : uint8_t { Opaque = 0, Mask = 1, Blend = 2 }; + struct GltfMaterialCpu { - // TODO: add more than albedo std::string baseColorTextureUri; + std::string metallicRoughnessTextureUri; + std::string occlusionTextureUri; + std::string emissiveTextureUri; + std::string normalTextureUri; + + glm::vec4 baseColorFactor{1, 1, 1, 1}; + glm::vec3 emissiveFactor{0, 0, 0}; + float metallicFactor = 1.0F; + float roughnessFactor = 1.0F; + float occlusionStrength = 1.0F; - // gLTF baseColorFactor in (RGBA) - glm::vec4 baseColorFactor{1.0F, 1.0F, 1.0F, 1.0F}; + GltfAlphaMode alphaMode = GltfAlphaMode::Opaque; + float alphaCutoff = 0.5F; + bool doubleSided = false; }; struct GltfPrimitiveCpu { diff --git a/src/engine/camera/camera.cpp b/src/engine/camera/camera.cpp index b4c55f4..ba9a862 100644 --- a/src/engine/camera/camera.cpp +++ b/src/engine/camera/camera.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -46,6 +47,7 @@ CameraUBO Camera::makeUbo(VkExtent2D extent) const { CameraUBO ubo{}; const glm::vec3 f = forward(); + ubo.cameraPosWS_pad = glm::vec4(position, 0.0F); ubo.view = glm::lookAt(position, position + f, glm::vec3(0.0F, 0.0F, 1.0F)); const float width = static_cast(extent.width); diff --git a/src/engine/camera/camera_ubo.hpp b/src/engine/camera/camera_ubo.hpp index 5e1b879..98e96db 100644 --- a/src/engine/camera/camera_ubo.hpp +++ b/src/engine/camera/camera_ubo.hpp @@ -1,10 +1,12 @@ #pragma once #include +#include -struct CameraUBO { +struct alignas(16) CameraUBO { glm::mat4 view; glm::mat4 proj; + glm::vec4 cameraPosWS_pad; // xyz = cameraPosWS, w = pad }; static_assert(sizeof(CameraUBO) % 16 == 0); diff --git a/src/engine/geometry/mesh_builder.hpp b/src/engine/geometry/mesh_builder.hpp index de81f87..5ced0a0 100644 --- a/src/engine/geometry/mesh_builder.hpp +++ b/src/engine/geometry/mesh_builder.hpp @@ -4,9 +4,12 @@ #include "engine/mesh/vertex.hpp" #include +#include #include #include +#include #include +#include #include #include @@ -48,10 +51,20 @@ class MeshBuilder { const std::array &uvs = quadUvs01()) { const std::uint32_t base = static_cast(vertices.size()); - vertices.push_back(Vertex{.pos = a, .color = color, .uv = uvs[0]}); - vertices.push_back(Vertex{.pos = b, .color = color, .uv = uvs[1]}); - vertices.push_back(Vertex{.pos = c, .color = color, .uv = uvs[2]}); - vertices.push_back(Vertex{.pos = d, .color = color, .uv = uvs[3]}); + // normals + glm::vec3 n = glm::normalize(glm::cross(b - a, c - a)); + if (!std::isfinite(n.x) || !std::isfinite(n.y) || !std::isfinite(n.z)) { + n = {0.0F, 0.0F, 1.0F}; + } + + vertices.push_back( + Vertex{.pos = a, .normal = n, .color = color, .uv = uvs[0]}); + vertices.push_back( + Vertex{.pos = b, .normal = n, .color = color, .uv = uvs[1]}); + vertices.push_back( + Vertex{.pos = c, .normal = n, .color = color, .uv = uvs[2]}); + vertices.push_back( + Vertex{.pos = d, .normal = n, .color = color, .uv = uvs[3]}); addQuadIndices(base); } diff --git a/src/engine/geometry/primitives.cpp b/src/engine/geometry/primitives.cpp index cd88d5f..1ebe3e9 100644 --- a/src/engine/geometry/primitives.cpp +++ b/src/engine/geometry/primitives.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -18,22 +19,23 @@ MeshData triangle(float size) { builder.reserve(3, 0); const float h = size * 0.5F; + const glm::vec3 n{0.0F, 0.0F, 1.0F}; builder.vertices.push_back(engine::Vertex{ .pos = {0.0F, -h, 0.0F}, - .color = {1.0F, 1.0F, 0.0F}, + .normal = n, .uv = {0.5F, 0.0F}, }); builder.vertices.push_back(engine::Vertex{ .pos = {+h, +h, 0.0F}, - .color = {1.0F, 0.0F, 1.0F}, + .normal = n, .uv = {1.0F, 1.0F}, }); builder.vertices.push_back(engine::Vertex{ .pos = {-h, +h, 0.0F}, - .color = {0.0F, 1.0F, 1.0F}, + .normal = n, .uv = {0.0F, 1.0F}, }); @@ -61,22 +63,22 @@ MeshData cube(float size) { // +Z (front) builder.addQuad({-h, -h, +h}, {+h, -h, +h}, {+h, +h, +h}, {-h, +h, +h}, - {1, 0, 0}); + {1, 1, 1}); // -Z (back) builder.addQuad({+h, -h, -h}, {-h, -h, -h}, {-h, +h, -h}, {+h, +h, -h}, - {0, 1, 0}); + {1, 1, 1}); // -X (left) builder.addQuad({-h, -h, -h}, {-h, -h, +h}, {-h, +h, +h}, {-h, +h, -h}, - {0, 0, 1}); + {1, 1, 1}); // +X (right) builder.addQuad({+h, -h, +h}, {+h, -h, -h}, {+h, +h, -h}, {+h, +h, +h}, - {1, 1, 0}); + {1, 1, 1}); // +Y (top) builder.addQuad({-h, +h, +h}, {+h, +h, +h}, {+h, +h, -h}, {-h, +h, -h}, - {1, 0, 1}); + {1, 1, 1}); // -Y (bottom) builder.addQuad({-h, -h, -h}, {+h, -h, -h}, {+h, -h, +h}, {-h, -h, +h}, - {0, 1, 1}); + {1, 1, 1}); return std::move(builder).build(); } @@ -90,6 +92,7 @@ MeshData circle(uint32_t segments, float radius) { engine::MeshBuilder builder; segments = std::max(segments, 3U); + const glm::vec3 n{0.0F, 0.0F, 1.0F}; builder.reserve(static_cast(segments) + 2U, static_cast(segments) * 3U); @@ -97,7 +100,7 @@ MeshData circle(uint32_t segments, float radius) { // center builder.vertices.push_back(engine::Vertex{ .pos = glm::vec3{0.0F, 0.0F, 0.0F}, - .color = glm::vec3{1.0F, 1.0F, 1.0F}, + .normal = n, .uv = {0.5F, 0.5F}, }); @@ -115,7 +118,7 @@ MeshData circle(uint32_t segments, float radius) { builder.vertices.push_back(engine::Vertex{ .pos = glm::vec3{x, y, 0.0F}, - .color = glm::vec3{1.0F, 0.0F, 0.0F}, + .normal = n, .uv = uv, }); } @@ -132,30 +135,48 @@ MeshData circle(uint32_t segments, float radius) { MeshData poincareDisk() { MeshData m; + const glm::vec3 n{0.0F, 0.0F, 1.0F}; + m.vertices = { // Bottom arc (curving inward) - engine::Vertex{.pos = glm::vec3{-0.6F, -0.4F, 0.0F}, - .color = glm::vec3{1.0F, 0.0F, 0.0F}}, - engine::Vertex{.pos = glm::vec3{0.6F, -0.4F, 0.0F}, - .color = glm::vec3{1.0F, 0.0F, 0.0F}}, + engine::Vertex{ + .pos = glm::vec3{-0.6F, -0.4F, 0.0F}, + .normal = n, + }, + engine::Vertex{ + .pos = glm::vec3{0.6F, -0.4F, 0.0F}, + .normal = n, + }, // Right arc - engine::Vertex{.pos = glm::vec3{0.8F, -0.1F, 0.0F}, - .color = glm::vec3{0.0F, 1.0F, 0.0F}}, - engine::Vertex{.pos = glm::vec3{0.8F, 0.1F, 0.0F}, - .color = glm::vec3{0.0F, 1.0F, 0.0F}}, + engine::Vertex{ + .pos = glm::vec3{0.8F, -0.1F, 0.0F}, + .normal = n, + }, + engine::Vertex{ + .pos = glm::vec3{0.8F, 0.1F, 0.0F}, + .normal = n, + }, // Top arc - engine::Vertex{.pos = glm::vec3{0.6F, 0.4F, 0.0F}, - .color = glm::vec3{0.0F, 0.0F, 1.0F}}, - engine::Vertex{.pos = glm::vec3{-0.6F, 0.4F, 0.0F}, - .color = glm::vec3{0.0F, 0.0F, 1.0F}}, + engine::Vertex{ + .pos = glm::vec3{0.6F, 0.4F, 0.0F}, + .normal = n, + }, + engine::Vertex{ + .pos = glm::vec3{-0.6F, 0.4F, 0.0F}, + .normal = n, + }, // Left arc - engine::Vertex{.pos = glm::vec3{-0.8F, 0.1F, 0.0F}, - .color = glm::vec3{1.0F, 1.0F, 0.0F}}, - engine::Vertex{.pos = glm::vec3{-0.8F, -0.1F, 0.0F}, - .color = glm::vec3{1.0F, 1.0F, 0.0F}}, + engine::Vertex{ + .pos = glm::vec3{-0.8F, 0.1F, 0.0F}, + .normal = n, + }, + engine::Vertex{ + .pos = glm::vec3{-0.8F, -0.1F, 0.0F}, + .normal = n, + }, }; m.indices = {0U, 1U, 2U, 2U, 3U, 4U, 4U, 5U, 6U, diff --git a/src/engine/mesh/vertex.hpp b/src/engine/mesh/vertex.hpp index 45a4bab..17b8abe 100644 --- a/src/engine/mesh/vertex.hpp +++ b/src/engine/mesh/vertex.hpp @@ -7,10 +7,11 @@ namespace engine { struct Vertex { glm::vec3 pos{0.0F}; + glm::vec3 normal{0.0F, 0.0F, 1.0F}; glm::vec3 color{1.0F}; glm::vec2 uv{0.0F}; }; -static_assert(sizeof(Vertex) == sizeof(float) * 8); +static_assert(sizeof(Vertex) == sizeof(float) * 11); } // namespace engine diff --git a/src/main.cpp b/src/main.cpp index a8aad18..a2f5af2 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -8,6 +8,8 @@ #include "render/renderer.hpp" #include "render/resources/material_system.hpp" #include "render/resources/mesh_store.hpp" +#include "render/scene/lights_gpu.hpp" +#include "render/scene/scene_ubo.hpp" #include #include @@ -15,6 +17,7 @@ #include #include #include +#include #include #include #include @@ -80,14 +83,13 @@ int main() { CameraController controller(app.window(), &camera); controller.enableCursorCapture(true); - // engine::assets::GltfLoadOptions opt{}; - // opt.flipTexcoordV = true; - // opt.axis.yUpToZUp = true; - // - // engine::assets::GltfAsset tree; - MeshHandle cube{}; + engine::assets::GltfLoadOptions opt{}; + opt.flipTexcoordV = true; + opt.axis.yUpToZUp = true; + + engine::assets::GltfAsset tree; - TextureHandle texture{}; + MeshHandle cube{}; uint32_t material = UINT32_MAX; { @@ -97,16 +99,23 @@ int main() { } cube = app.meshes().cube(); - // engine::assets::loadGltf(app.renderer(), "assets/tree.glb", tree, opt); + engine::assets::loadGltf(app.renderer(), "assets/tree2.glb", tree, opt); + + TextureHandle albedo = app.renderer().loadTextureFromFile( + "assets/terry.jpg", true, MaterialSystem::TextureUsage::sRGB); + + MaterialSystem::MaterialDescription desc{}; + // desc.baseColorTexture = albedo; + desc.metallicFactor = 0.0F; + desc.roughnessFactor = 0.0F; - texture = app.renderer().createTextureFromFile("assets/terry.jpg", true); - material = app.renderer().createMaterialFromTexture(texture); + material = app.renderer().createMaterial(desc); } std::vector draw; - const uint32_t cubeCount = 10'000; - draw.reserve(cubeCount); - // draw.reserve(tree.drawItems.size() + 2); + // const uint32_t cubeCount = 10'000; + // draw.reserve(cubeCount); + draw.reserve(tree.drawItems.size() + 2); // draw.reserve(2); app.run([&](float dt) { @@ -117,21 +126,34 @@ int main() { const float t = (float)glfwGetTime(); draw.clear(); - // DrawItem cubeA{}; - // cubeA.mesh = cube; - // cubeA.material = material; - // cubeA.model = engine::makeModel({-3, 0, 0}, {0, 0, t}); - // draw.push_back(cubeA); - // - // DrawItem cubeB{}; - // cubeB.mesh = cube; - // cubeB.material = material; - // cubeB.model = engine::makeModel({+3, 0, 0}, {0, 0, -t}); - // draw.push_back(cubeB); - // - // draw.insert(draw.end(), tree.drawItems.begin(), tree.drawItems.end()); - - pushCubeGrid(draw, cube, material, cubeCount, 2.5F, t); + DirectionalLight sun{}; + sun.directionWS_illuminanceLux = + glm::normalize(glm::vec4(0.0F, 0.0F, -1.0F, 30'000.0F)); + sun.colorLinear_pad = glm::vec4(1.0F, 1.0F, 1.0F, 0.0F); + app.renderer().addDirectionalLight(sun); + + PointLightGPU p{}; + p.positionWS = glm::vec3(-3.1, 0, 0); + p.radius = 6.0F; + p.colorLinear = glm::vec3(1.0F, 0.8F, 0.6F); + p.lumens = 1800.0F; + app.renderer().addPointLight(p); + + DrawItem cubeA{}; + cubeA.mesh = cube; + cubeA.material = material; + cubeA.model = engine::makeModel({-3, 0, 0}, {0, 0, t}); + draw.push_back(cubeA); + + DrawItem cubeB{}; + cubeB.mesh = cube; + cubeB.material = material; + cubeB.model = engine::makeModel({+3, 0, 0}, {0, 0, -t}); + draw.push_back(cubeB); + + draw.insert(draw.end(), tree.drawItems.begin(), tree.drawItems.end()); + + // pushCubeGrid(draw, cube, material, cubeCount, 2.5F, t); (void)app.renderer().drawFrame(app.presenter(), draw); }); diff --git a/src/render/renderer.cpp b/src/render/renderer.cpp index 1a4c06f..637084f 100644 --- a/src/render/renderer.cpp +++ b/src/render/renderer.cpp @@ -13,8 +13,10 @@ #include "engine/mesh/mesh_data.hpp" #include "render/rendergraph/swapchain_targets.hpp" +#include "render/resources/material_system.hpp" #include "render/resources/mesh_gpu.hpp" #include "render/resources/mesh_store.hpp" +#include "render/scene/debug_ubo.hpp" #include "render/scene/push_constants.hpp" #include "util/scope_exit.hpp" @@ -38,7 +40,7 @@ static constexpr VkDeviceSize kMiB = 1024ULL * 1024ULL; // 8 MiB -static constexpr VkDeviceSize kUploadStaticBudget = 8ULL * kMiB; +static constexpr VkDeviceSize kUploadStaticBudget = 64ULL * kMiB; // 2 MiB static constexpr VkDeviceSize kUploadFrameBudget = 2ULL * kMiB; @@ -46,6 +48,10 @@ static constexpr VkDeviceSize kUploadFrameBudget = 2ULL * kMiB; static constexpr uint32_t kRequestedMaxInstancesPerFrame = 16U * 1024U; static constexpr uint32_t kRequestedMaxMaterials = 1024U; +static constexpr uint32_t kRequestedMaxLightsPerFrame = 1024U; + +static constexpr uint32_t kMaxTextures = 8192; + bool Renderer::init(VkBackendCtx &ctx, VkPresenter &presenter, uint32_t framesInFlight, const std::string &vertSpvPath, const std::string &fragSpvPath, JobSystem &jobs) { @@ -73,10 +79,11 @@ bool Renderer::init(VkBackendCtx &ctx, VkPresenter &presenter, LOGI("Renderer initialized: framesInFlight={} | threadCount: {} | shaders: " "vert='{}' frag='{}' " "| " - "uploadMiB: static={} frame={} | caps: instances={} materials={}", + "uploadMiB: static={} frame={} | caps: instances={} materials={} " + "Textures={}", framesInFlight, m_jobs->threadCount(), vertSpvPath, fragSpvPath, kUploadStaticBudget / kMiB, kUploadFrameBudget / kMiB, - kRequestedMaxInstancesPerFrame, kRequestedMaxMaterials); + kRequestedMaxInstancesPerFrame, kRequestedMaxMaterials, kMaxTextures); VkDevice device = m_ctx->device(); @@ -94,7 +101,7 @@ bool Renderer::init(VkBackendCtx &ctx, VkPresenter &presenter, } // Create shader interface - if (!m_interface.init(device)) { + if (!m_interface.init(device, framesInFlight, kMaxTextures)) { LOGE("Failed to initialize shader interface"); shutdown(); return false; @@ -116,7 +123,6 @@ bool Renderer::init(VkBackendCtx &ctx, VkPresenter &presenter, } LOGI("Main render pass initialized"); - // TODO: use job system workers instead of hard setting to 1 thread if (!m_uploads.init(*m_ctx, m_framesInFlight, kUploadStaticBudget, kUploadFrameBudget, m_jobs->threadCount())) { LOGE("Failed to initialize upload manager"); @@ -124,14 +130,9 @@ bool Renderer::init(VkBackendCtx &ctx, VkPresenter &presenter, return false; } - if (!m_uploads.beginStatic()) { - LOGE("Failed to begin upload frame"); - shutdown(); - return false; - } - if (!m_scene.init(*m_ctx, m_framesInFlight, m_interface, - kRequestedMaxInstancesPerFrame, kRequestedMaxMaterials)) { + kRequestedMaxInstancesPerFrame, kRequestedMaxMaterials, + kRequestedMaxLightsPerFrame)) { LOGE("Failed to initialize scene data"); shutdown(); return false; @@ -146,19 +147,7 @@ bool Renderer::init(VkBackendCtx &ctx, VkPresenter &presenter, m_resources.materials().bindMaterialTable(m_scene.materialBuffer(), m_scene.materialCapacity()); - // Create a 1x1 default white texture and material - // TOOD: use job system worker instead of hardcoding 0 - if (!m_resources.materials().createDefaultMaterial( - m_uploads.staticRecorder(2))) { - LOGE("Failed to create the default material"); - shutdown(); - return false; - } - LOGD("Default material created"); - - // Submit + wait for default material - if (!m_uploads.flushStatic(false)) { - LOGE("Failed to flush static uploads"); + if (!createDefaultMaterial()) { shutdown(); return false; } @@ -213,6 +202,29 @@ void Renderer::shutdown() noexcept { m_fragPath.clear(); } +bool Renderer::createDefaultMaterial() noexcept { + if (!m_uploads.beginStatic()) { + LOGE("Failed to begin upload frame"); + return false; + } + + // Create a 1x1 default white texture and material + // TOOD: use job system worker instead of hardcoding 0 + if (!m_resources.materials().createDefaultMaterial( + m_uploads.staticRecorder(0))) { + return false; + } + LOGD("Default material created"); + + // Submit + wait for default material + if (!m_uploads.flushStatic(false)) { + LOGE("Failed to flush static uploads"); + return false; + } + + return true; +} + void Renderer::recordFrame(VkCommandBuffer cmd, VkPresenter &presenter, const SwapchainTargets &targets, uint32_t imageIndex, const MeshHandle mesh, uint32_t material, @@ -303,8 +315,9 @@ void Renderer::recordFrame(VkCommandBuffer cmd, VkPresenter &presenter, scissor.extent = extent; vkCmdSetScissor(cmd, 0, 1, &scissor); - m_scene.bind(cmd, m_interface, m_frames.currentFrameIndex()); - PROFILE_CPU_INC_DESCRIPTOR_BINDS(1); + m_scene.bind(cmd, m_interface); + m_resources.materials().bindTextureTable(cmd, m_interface.pipelineLayout(), + 1); // TODO: sort by mesh, material and stream directly into the uploader // without building vectors per batch @@ -335,7 +348,9 @@ BatchMap Renderer::buildBatches(std::span items) const { continue; } - uint32_t mat = m_resources.materials().resolveMaterial(item.material); + uint32_t mat = (item.material != UINT32_MAX) + ? item.material + : m_resources.materials().defaultMaterial(); batches[BatchKey{.mesh = item.mesh, .material = mat}].push_back(item.model); } @@ -371,17 +386,14 @@ void Renderer::drawBatches(VkCommandBuffer cmd, uint32_t frameIndex, const uint32_t instanceCount = instanceUpload.instanceCount; PROFILE_CPU_ADD_INSTANCES(instanceCount); - m_resources.materials().bindMaterial(cmd, m_interface.pipelineLayout(), 1, - key.material); - PROFILE_CPU_INC_DESCRIPTOR_BINDS(1); - DrawPushConstants pushConstants{}; pushConstants.baseInstance = instanceUpload.baseInstance; pushConstants.materialId = key.material; vkCmdPushConstants(cmd, m_interface.pipelineLayout(), - VK_SHADER_STAGE_VERTEX_BIT, 0, sizeof(DrawPushConstants), - &pushConstants); + VK_SHADER_STAGE_VERTEX_BIT | + VK_SHADER_STAGE_FRAGMENT_BIT, + 0, sizeof(DrawPushConstants), &pushConstants); VkDeviceSize vertBufOffset = 0; VkBuffer vertBuf = mesh->vertex.handle(); @@ -469,7 +481,19 @@ bool Renderer::drawFrame(VkPresenter &presenter, { PROFILE_CPU_SCOPE(CpuProfiler::Stat::UpdatePerFrameUBO); + + VkUploadContext::Recorder recorder = m_uploads.frameRecorder(0); + if (recorder) { + (void)m_scene.commitLights(recorder, frameIndex); + } + (void)m_scene.update(frameIndex, m_cameraUbo); + +#ifndef NDEBUG + DebugUBO dbg{}; + dbg.view = 0; + m_scene.setDebug(dbg); +#endif } VkCommandBuffer cmd = m_commands.buffers()[frameIndex]; @@ -558,40 +582,19 @@ const MeshGpu *Renderer::get(MeshHandle handle) const { return m_resources.meshes().get(handle); } -TextureHandle Renderer::createTextureFromFile(const std::string &path, - bool flipY) { - - return m_resources.materials().createTextureFromFile( - m_uploads.staticRecorder(2), path, flipY); -} - -uint32_t Renderer::createMaterialFromTexture(TextureHandle handle) { +TextureHandle +Renderer::loadTextureFromFile(const std::string &path, bool flipY, + MaterialSystem::TextureUsage usage) { // TODO: make logic for if static or frame recorder - LOGI("Creating Material from texture"); - return m_resources.materials().createMaterialFromTexture( - m_uploads.staticRecorder(2), handle); -} - -uint32_t Renderer::createMaterialFromBaseColorFactor(const glm::vec4 &factor) { - // TODO: make logic for if static or frame recorder - return m_resources.materials().createMaterialFromBaseColorFactor( - m_uploads.staticRecorder(2), factor); -} - -bool Renderer::createTextureFromImage(const engine::ImageData &img, - VkTexture2D &outTex) { - return m_resources.materials().createTextureFromImage( - m_uploads.staticRecorder(2), img, outTex); -} - -void Renderer::setActiveMaterial(uint32_t materialIndex) { - m_resources.materials().setActiveMaterial(materialIndex); + return m_resources.materials().loadTextureFromFile( + m_uploads.staticRecorder(0), path, flipY, usage); } -bool Renderer::updateMaterialGPU(uint32_t materialId, const MaterialGPU &gpu) { +uint32_t +Renderer::createMaterial(const MaterialSystem::MaterialDescription &desc) { // TODO: make logic for if static or frame recorder - return m_resources.materials().updateMaterialGPU(m_uploads.staticRecorder(3), - materialId, gpu); + return m_resources.materials().createMaterial(m_uploads.staticRecorder(0), + desc); } bool Renderer::beginUpload(uint32_t frameIndex) { diff --git a/src/render/renderer.hpp b/src/render/renderer.hpp index 7a42372..9e982de 100644 --- a/src/render/renderer.hpp +++ b/src/render/renderer.hpp @@ -10,12 +10,13 @@ #include "render/rendergraph/main_pass.hpp" #include "render/rendergraph/swapchain_targets.hpp" -#include "render/resources/material_gpu.hpp" #include "render/resources/material_system.hpp" #include "render/resources/mesh_store.hpp" #include "render/resources/resource_store.hpp" +#include "render/scene/lights_gpu.hpp" #include "render/scene/scene_data.hpp" +#include "render/scene/scene_ubo.hpp" #include "render/upload/upload_manager.hpp" #include "backend/gpu/descriptors/vk_shader_interface.hpp" @@ -121,27 +122,30 @@ class Renderer { void setCameraUBO(const CameraUBO &ubo) { m_cameraUbo = ubo; } + // Uploading + bool beginUpload(uint32_t frameIndex); + bool endUpload(bool wait); + + bool beginStaticUploads(); + bool endStaticUploads(bool wait); + // Meshes MeshHandle createMesh(const engine::Vertex *vertices, uint32_t vertexCount, const uint32_t *indices, uint32_t indexCount); MeshHandle createMesh(const engine::MeshData &mesh); [[nodiscard]] const MeshGpu *get(MeshHandle handle) const; - // Materials - TextureHandle createTextureFromFile(const std::string &path, bool flipY); - bool createTextureFromImage(const engine::ImageData &img, - VkTexture2D &outTex); - - uint32_t createMaterialFromTexture(TextureHandle textureHandle); - uint32_t createMaterialFromBaseColorFactor(const glm::vec4 &factor); - void setActiveMaterial(uint32_t materialIndex); - bool updateMaterialGPU(uint32_t materialId, const MaterialGPU &gpu); + // Materials/Textures + uint32_t createMaterial(const MaterialSystem::MaterialDescription &desc); + TextureHandle loadTextureFromFile(const std::string &path, bool flipY, + MaterialSystem::TextureUsage usage); - bool beginUpload(uint32_t frameIndex); - bool endUpload(bool wait); - - bool beginStaticUploads(); - bool endStaticUploads(bool wait); + // Lights + void clearLights() { m_scene.clearLights(); } + void addDirectionalLight(DirectionalLight &light) { + m_scene.addDirectionalLight(light); + } + void addPointLight(PointLightGPU &light) { m_scene.addPointLight(light); } // TODO: make PImpl private: diff --git a/src/render/resources/material_gpu.hpp b/src/render/resources/material_gpu.hpp index b294e30..170d7c9 100644 --- a/src/render/resources/material_gpu.hpp +++ b/src/render/resources/material_gpu.hpp @@ -4,7 +4,7 @@ #include #include -struct MaterialGPU { +struct alignas(16) MaterialGPU { static constexpr uint32_t kNoTexture = 0xFFFFFFFFU; glm::vec4 baseColorFactor{1.0F}; // rgba @@ -26,3 +26,4 @@ struct MaterialGPU { // multiple of 16 bytes for for std430 static_assert(sizeof(MaterialGPU) % 16 == 0); +static_assert(alignof(MaterialGPU) == 16); diff --git a/src/render/resources/material_system.cpp b/src/render/resources/material_system.cpp index 6a698ff..87301be 100644 --- a/src/render/resources/material_system.cpp +++ b/src/render/resources/material_system.cpp @@ -1,250 +1,373 @@ #include "render/resources/material_system.hpp" +#include "backend/gpu/textures/vk_texture.hpp" +#include "backend/gpu/upload/vk_material_uploader.hpp" #include "backend/gpu/upload/vk_upload_context.hpp" #include "engine/assets/stb_image/stb_image_loader.hpp" #include "engine/logging/log.hpp" #include "render/resources/material_gpu.hpp" #include +#include #include -#include +#include #include namespace { -uint32_t clampMateriaCapacity(VkPhysicalDevice physicalDevice, - uint32_t requested) noexcept { +uint32_t clampMaterialSsboCapactiy(VkPhysicalDevice physicalDevice, + uint32_t requested) noexcept { VkPhysicalDeviceProperties props{}; vkGetPhysicalDeviceProperties(physicalDevice, &props); - const uint32_t maxSampled = props.limits.maxDescriptorSetSampledImages; + const VkDeviceSize maxBytes = props.limits.maxStorageBufferRange; + const uint32_t maxElements = maxBytes >= sizeof(MaterialGPU) + ? uint32_t(maxBytes / sizeof(MaterialGPU)) + : 0U; - if (maxSampled == 0) { + if (maxElements == 0) { return 0; } - return std::min(requested, maxSampled); + return std::min(requested, maxElements); } } // namespace bool MaterialSystem::init(VkBackendCtx &ctx, VkDescriptorSetLayout materialSetLayout, - uint32_t materialCapacity) { + uint32_t materialCapacity, uint32_t maxTextures) { shutdown(); if (!m_textureUploader.init(ctx)) { - std::cerr << "[MaterialSystem] Failed to init texture uploader\n"; - shutdown(); - return false; - } - - if (!m_materialUploader.init()) { - std::cerr << "[MaterialSystem] Failed to init material uploader\n"; + LOGE("Texture uploader initialization failed"); shutdown(); return false; } const uint32_t cappedCapacity = - clampMateriaCapacity(ctx.physicalDevice(), materialCapacity); + clampMaterialSsboCapactiy(ctx.physicalDevice(), materialCapacity); if (cappedCapacity == 0) { - std::cerr << "[MaterialSystem] Material capacity invalid after clamp\n"; + LOGE("Material SSBO capacity invalid after clamp"); shutdown(); return false; } - if (!m_materialSets.init(ctx.device(), materialSetLayout, materialCapacity)) { - std::cerr << "[MaterialSystem] Failed to init material sets\n"; + if (!m_materialSet.init(ctx.device(), materialSetLayout, maxTextures)) { + LOGE("Material sets initialization failed"); shutdown(); return false; } + m_nextMaterialId = 0; + m_maxTextures = maxTextures; + m_defaultMaterial = UINT32_MAX; + return true; } void MaterialSystem::shutdown() noexcept { - m_materialSets.shutdown(); + m_materialSet.shutdown(); // Textures for (auto &texture : m_textures) { texture.shutdown(); } m_textures.clear(); + m_maxTextures = 0; m_textureUploader.shutdown(); - m_materialUploader.shutdown(); m_materialTable = VK_NULL_HANDLE; m_materialTableCapacity = 0; + m_nextMaterialId = 0; + + m_whiteTexture = {}; + m_blackTexture = {}; + m_defaultMetalRough = {}; + m_defaultOcclusion = {}; + m_defaultNormal = {}; m_defaultMaterial = UINT32_MAX; - m_activeMaterial = UINT32_MAX; } -bool MaterialSystem::createDefaultMaterial( - VkUploadContext::Recorder staticRecorder) noexcept { - VkTexture2D tex; +uint32_t MaterialSystem::allocMaterialId() noexcept { + if (m_materialTable == VK_NULL_HANDLE || m_materialTableCapacity == 0) { + LOGE("MaterialId allocation failed because of improper material table " + "setup"); + return UINT32_MAX; + } - static constexpr std::array kWhiteRGBA8{255, 255, 255, 255}; + const uint32_t id = m_nextMaterialId++; + if (id >= m_materialTableCapacity) { + LOGE("MaterialId is larger than material table capacity"); + return UINT32_MAX; + } - if (!m_textureUploader.uploadRGBA8(staticRecorder, kWhiteRGBA8.data(), 1, 1, - tex)) { - std::cerr << "[MaterialSystem] Failed to create default white texture\n"; - return false; + return id; +} + +VkFormat MaterialSystem::formatFor(TextureUsage usage) noexcept { + switch (usage) { + case TextureUsage::sRGB: + return VK_FORMAT_R8G8B8A8_SRGB; + default: + return VK_FORMAT_R8G8B8A8_UNORM; + } +} + +TextureHandle MaterialSystem::storeTextureAndWrite(VkTexture2D &&tex) { + if (!tex.valid()) { + LOGE("Texture is invalid"); + return {}; + } + + const uint32_t slot = static_cast(m_textures.size()); + if (slot >= m_maxTextures) { + LOGE("Texture slot is larger than max textures"); + return {}; } m_textures.push_back(std::move(tex)); - m_whiteTexture = TextureHandle{static_cast(m_textures.size() - 1)}; - m_defaultMaterial = createMaterialFromTexture(staticRecorder, m_whiteTexture); - if (m_defaultMaterial == UINT32_MAX) { - std::cerr << "[MaterialSystem] Failed to create default material\n"; - return false; + if (!m_materialSet.writeTexture(slot, m_textures[slot])) { + LOGE("Write texture failed"); + return {}; } - m_activeMaterial = m_defaultMaterial; - return true; + return TextureHandle{slot}; } TextureHandle -MaterialSystem::createTextureFromFile(VkUploadContext::Recorder staticRecorder, - const std::string &path, bool flipY) { +MaterialSystem::loadTextureFromFile(VkUploadContext::Recorder staticRecorder, + const std::string &path, bool flipY, + TextureUsage usage) { engine::ImageData img; if (!engine::assets::loadImageRGBA8(path, img, flipY)) { - std::cerr << "[MaterialSystem] Failed to load image: " << path << "\n"; + LOGE("Failed to load image: {}", path); return {}; } VkTexture2D tex; - if (!m_textureUploader.uploadRGBA8(staticRecorder, img.pixels.data(), + const VkFormat fmt = formatFor(usage); + if (!m_textureUploader.uploadRGBA8(staticRecorder, img.pixels.data(), fmt, img.width, img.height, tex)) { - std::cerr << "[MaterialSystem] Failed to create texture from file\n"; + LOGE("Texture from file creation failed"); return {}; } - m_textures.push_back(std::move(tex)); - return TextureHandle{static_cast(m_textures.size() - 1)}; + return storeTextureAndWrite(std::move(tex)); } -bool MaterialSystem::createTextureFromImage( +bool MaterialSystem::uploadTextureFromImage( VkUploadContext::Recorder staticRecorder, const engine::ImageData &img, - VkTexture2D &outTex) { + TextureUsage usage, VkTexture2D &outTex) { if (!img.valid()) { - std::cerr << "[MaterialSystem] createTextureFromImage invalid image\n"; + LOGE("createTextureFromImage invalid image"); return false; } const size_t expected = size_t(img.width) * img.height * 4ULL; if (img.pixels.size() != expected) { - std::cerr << "[MaterialSystem] Image byte size mismatch: have=" - << img.pixels.size() << " expected=" << expected << "\n"; + LOGE("Image byte size mismatch: have={} expected={}", img.pixels.size(), + expected); return false; } - return m_textureUploader.uploadRGBA8(staticRecorder, img.pixels.data(), + const VkFormat fmt = formatFor(usage); + return m_textureUploader.uploadRGBA8(staticRecorder, img.pixels.data(), fmt, img.width, img.height, outTex); } -uint32_t -MaterialSystem::createMaterialFromTexture(VkUploadContext::Recorder recorder, - TextureHandle textureHandle) { - if (textureHandle.id >= m_textures.size() || - !m_textures[textureHandle.id].valid()) { - std::cerr << "[MaterialSystem] Invalid texture handle\n"; - return UINT32_MAX; - } - - const uint32_t id = - m_materialSets.allocateForTexture(m_textures[textureHandle.id]); - if (id == UINT32_MAX) { - return UINT32_MAX; +bool MaterialSystem::writeMaterialGPU(VkUploadContext::Recorder recorder, + uint32_t materialId, + const MaterialGPU &gpu) { + if (m_materialTable == VK_NULL_HANDLE) { + LOGE("Material table not bound"); + return false; } - MaterialGPU gpu; - - if (!writeMaterialGPU(recorder, id, gpu)) { - std::cerr << "[MaterialSystem] Failed to write material GPU table\n"; - return UINT32_MAX; + if (materialId == UINT32_MAX || materialId >= m_materialTableCapacity) { + LOGE("Material id out of range"); + return false; } - return id; + const VkDeviceSize dstOffset = VkDeviceSize(materialId) * sizeof(MaterialGPU); + return MaterialUploader::uploadOne(recorder, m_materialTable, dstOffset, gpu); } -uint32_t MaterialSystem::createMaterialFromBaseColorFactor( - VkUploadContext::Recorder recorder, const glm::vec4 &factor) { - if (m_whiteTexture.id == UINT32_MAX || - m_whiteTexture.id >= m_textures.size() || - !m_textures[m_whiteTexture.id].valid()) { - std::cerr << "[MaterialSystem] White texture not available\n"; +uint32_t MaterialSystem::createMaterial(VkUploadContext::Recorder recorder, + const MaterialDescription &desc) { + if (!m_whiteTexture.valid() || !m_blackTexture.valid() || + !m_defaultMetalRough.valid() || !m_defaultOcclusion.valid() || + !m_defaultNormal.valid()) { + LOGE("Defaults not initialized"); return UINT32_MAX; } - const uint32_t id = - m_materialSets.allocateForTexture(m_textures[m_whiteTexture.id]); + const uint32_t id = allocMaterialId(); if (id == UINT32_MAX) { return UINT32_MAX; } + // Resolve textures with defaults + const TextureHandle baseColor = + desc.baseColorTexture.value_or(m_whiteTexture); + const TextureHandle emissive = desc.emissiveTexture.value_or(m_blackTexture); + const TextureHandle metallicRoughness = + desc.metallicRoughnessTexture.value_or(m_defaultMetalRough); + const TextureHandle occlusion = + desc.ambientOcclusionTexture.value_or(m_defaultOcclusion); + const TextureHandle normal = desc.normal.value_or(m_defaultNormal); + MaterialGPU gpu{}; - gpu.baseColorFactor = factor; + gpu.baseColorFactor = desc.baseColorFactor; + gpu.emissiveFactor = {desc.emissiveFactor.x, desc.emissiveFactor.y, + desc.emissiveFactor.z, 0.0F}; + gpu.mrAoAlpha = {desc.metallicFactor, desc.roughnessFactor, + desc.ambientOcclusionFactor, desc.alphaCutoff}; + + gpu.tex0.x = baseColor.id; + gpu.tex1.x = emissive.id; + + gpu.tex0.y = normal.id; + gpu.tex0.z = metallicRoughness.id; + gpu.tex0.w = occlusion.id; if (!writeMaterialGPU(recorder, id, gpu)) { - std::cerr << "[MaterialSystem] Failed to write material GPU table\n"; return UINT32_MAX; } return id; } -void MaterialSystem::setActiveMaterial(uint32_t materialIndex) { - m_activeMaterial = materialIndex; +bool MaterialSystem::updateMaterialGPU(VkUploadContext::Recorder recorder, + uint32_t materialId, + const MaterialGPU &gpu) { + return writeMaterialGPU(recorder, materialId, gpu); } -uint32_t MaterialSystem::resolveMaterial(uint32_t overrideMaterial) const { - // priority: override -> active -> default - if (overrideMaterial != UINT32_MAX) { - return overrideMaterial; - } +bool MaterialSystem::createDefaultMaterial( + VkUploadContext::Recorder staticRecorder) noexcept { - if (m_activeMaterial != UINT32_MAX) { - return m_activeMaterial; + // BaseColor default: white (sRGB) + { + VkTexture2D tex; + static constexpr std::array kWhiteRGBA8{255, 255, 255, + 255}; + if (!m_textureUploader.uploadRGBA8(staticRecorder, kWhiteRGBA8.data(), + VK_FORMAT_R8G8B8A8_SRGB, 1, 1, tex)) { + LOGE("Default white texture creation failed"); + return false; + } + + m_whiteTexture = storeTextureAndWrite(std::move(tex)); + if (m_whiteTexture.id == UINT32_MAX) { + return false; + } } - return m_defaultMaterial; -} + // Emissive default: black (sRGB) + { + VkTexture2D tex; + static constexpr std::array kBlack{0, 0, 0, 255}; + if (!m_textureUploader.uploadRGBA8(staticRecorder, kBlack.data(), + VK_FORMAT_R8G8B8A8_SRGB, 1, 1, tex)) { + LOGE("Default black texture creation failed"); + return false; + } + + m_blackTexture = storeTextureAndWrite(std::move(tex)); + if (m_blackTexture.id == UINT32_MAX) { + return false; + } + } -void MaterialSystem::bindMaterial(VkCommandBuffer cmd, VkPipelineLayout layout, - uint32_t setIndex, uint32_t materialIndex) { - const uint32_t mat = resolveMaterial(materialIndex); - m_materialSets.bind(cmd, layout, setIndex, mat); -} + // MetallicRoughness default (linear UNORM): + // roughness = 1 in G, metallic = 0 in B. R unused, set 0. A = 255. + { + VkTexture2D tex; + static constexpr std::array kMr{0, 255, 255, 255}; + if (!m_textureUploader.uploadRGBA8(staticRecorder, kMr.data(), + VK_FORMAT_R8G8B8A8_UNORM, 1, 1, tex)) { + LOGE("Default MetallicRoughness texture creation failed"); + } + + m_defaultMetalRough = storeTextureAndWrite(std::move(tex)); + if (m_defaultMetalRough.id == UINT32_MAX) { + return false; + } + } -void MaterialSystem::bindMaterialTable(VkBuffer materialTableBuffer, - uint32_t maxMaterialsInTable) noexcept { - m_materialTable = materialTableBuffer; - m_materialTableCapacity = maxMaterialsInTable; -} + // Occlusion default (linear UNORM): AO = 1 in R. (255, *, *, 255) + { + VkTexture2D tex; + static constexpr std::array kAo{255, 255, 255, 255}; + if (!m_textureUploader.uploadRGBA8(staticRecorder, kAo.data(), + VK_FORMAT_R8G8B8A8_UNORM, 1, 1, tex)) { + LOGE("Default ambient occlusion creation failed"); + return false; + } + + m_defaultOcclusion = storeTextureAndWrite(std::move(tex)); + if (m_defaultOcclusion.id == UINT32_MAX) { + return false; + } + } -bool MaterialSystem::writeMaterialGPU(VkUploadContext::Recorder recorder, - uint32_t materialId, - const MaterialGPU &gpu) { - if (m_materialTable == VK_NULL_HANDLE) { - std::cerr << "[MaterialSystem] Material table not bound\n"; - return false; + // Normal default (linear UNORM): (0.5, 0.5, 1) => (128, 128, 255, 255) + { + VkTexture2D tex; + static constexpr std::array kNormal{128, 128, 255, 255}; + if (!m_textureUploader.uploadRGBA8(staticRecorder, kNormal.data(), + VK_FORMAT_R8G8B8A8_UNORM, 1, 1, tex)) { + LOGE("Default normal creation failed"); + return false; + } + + m_defaultNormal = storeTextureAndWrite(std::move(tex)); + if (m_defaultNormal.id == UINT32_MAX) { + return false; + } } - if (materialId == UINT32_MAX || materialId >= m_materialTableCapacity) { - std::cerr << "[MaterialSystem] Material id out of range\n"; - return false; + { + const uint32_t id = allocMaterialId(); + if (id == UINT32_MAX) { + LOGE("Out of material table capacity or table not " + "bound"); + return false; + } + + MaterialSystem::MaterialDescription desc{}; + desc.baseColorFactor = {1, 1, 1, 1}; + desc.emissiveFactor = {0, 0, 0}; + desc.metallicFactor = 0.0F; + desc.roughnessFactor = 1.0F; + desc.ambientOcclusionFactor = 1.0F; + desc.alphaCutoff = 0.5F; + + desc.baseColorTexture = m_whiteTexture; + desc.emissiveTexture = m_blackTexture; + desc.normal = m_defaultNormal; + desc.metallicRoughnessTexture = m_defaultMetalRough; + desc.ambientOcclusionTexture = m_defaultOcclusion; + + m_defaultMaterial = createMaterial(staticRecorder, desc); + return m_defaultMaterial != UINT32_MAX; } - const VkDeviceSize dstOffset = VkDeviceSize(materialId) * sizeof(MaterialGPU); - return m_materialUploader.uploadOne(recorder, m_materialTable, dstOffset, - gpu); + return true; } -bool MaterialSystem::updateMaterialGPU(VkUploadContext::Recorder recorder, - uint32_t materialId, - const MaterialGPU &gpu) { - return writeMaterialGPU(recorder, materialId, gpu); +void MaterialSystem::bindTextureTable(VkCommandBuffer cmd, + VkPipelineLayout layout, + uint32_t setIndex) const { + m_materialSet.bind(cmd, layout, setIndex); +} + +void MaterialSystem::bindMaterialTable(VkBuffer materialTableBuffer, + uint32_t maxMaterialsInTable) noexcept { + m_materialTable = materialTableBuffer; + m_materialTableCapacity = maxMaterialsInTable; } diff --git a/src/render/resources/material_system.hpp b/src/render/resources/material_system.hpp index 9829319..517d741 100644 --- a/src/render/resources/material_system.hpp +++ b/src/render/resources/material_system.hpp @@ -3,68 +3,123 @@ #include "backend/core/vk_backend_ctx.hpp" #include "backend/gpu/descriptors/vk_material_sets.hpp" #include "backend/gpu/textures/vk_texture.hpp" -#include "backend/gpu/upload/vk_material_uploader.hpp" #include "backend/gpu/upload/vk_texture_uploader.hpp" #include "backend/gpu/upload/vk_upload_context.hpp" #include "engine/assets/image_data.hpp" #include "render/resources/material_gpu.hpp" #include +#include #include +#include +#include #include struct TextureHandle { uint32_t id = UINT32_MAX; + + [[nodiscard]] bool valid() const noexcept { return id != UINT32_MAX; } }; class MaterialSystem { public: + enum class TextureUsage : uint8_t { + sRGB, + UNORM, + }; + + static constexpr uint32_t kInvalidId = UINT32_MAX; + + struct MaterialDescription { + glm::vec4 baseColorFactor{1, 1, 1, 1}; + glm::vec3 emissiveFactor{0, 0, 0}; + float metallicFactor = 0.0F; + float roughnessFactor = 0.5F; + float ambientOcclusionFactor = 1.0F; + + std::optional baseColorTexture; + std::optional emissiveTexture; + std::optional metallicRoughnessTexture; + std::optional ambientOcclusionTexture; + std::optional normal; + + uint32_t alphaMode = 0; + float alphaCutoff = 0.5F; + bool doubleSided = false; + }; + bool init(VkBackendCtx &ctx, VkDescriptorSetLayout materialSetLayout, - uint32_t materialCapacity); + uint32_t materialCapacity, uint32_t maxTextures); void shutdown() noexcept; - TextureHandle createTextureFromFile(VkUploadContext::Recorder staticRec, - const std::string &path, bool flipY); - bool createTextureFromImage(VkUploadContext::Recorder staticRec, - const engine::ImageData &img, + TextureHandle loadTextureFromFile(VkUploadContext::Recorder staticRec, + const std::string &path, bool flipY, + TextureUsage usage); + + bool uploadTextureFromImage(VkUploadContext::Recorder staticRec, + const engine::ImageData &img, TextureUsage usage, VkTexture2D &outTex); - uint32_t createMaterialFromTexture(VkUploadContext::Recorder recorder, - TextureHandle textureHandle); - uint32_t createMaterialFromBaseColorFactor(VkUploadContext::Recorder recorder, - const glm::vec4 &factor); + uint32_t createMaterial(VkUploadContext::Recorder recorder, + const MaterialDescription &descriptor); + + bool updateMaterialGPU(VkUploadContext::Recorder recorder, + uint32_t materialId, const MaterialGPU &gpu); - void setActiveMaterial(uint32_t materialIndex); - [[nodiscard]] uint32_t setActiveMaterial() const { return m_activeMaterial; } + bool createDefaultMaterial(VkUploadContext::Recorder staticRec) noexcept; - void bindMaterial(VkCommandBuffer cmd, VkPipelineLayout layout, - uint32_t setIndex, uint32_t materialIndex); + void bindTextureTable(VkCommandBuffer cmd, VkPipelineLayout layout, + uint32_t setIndex) const; + // Material table (SSBO) is provided by SceneData void bindMaterialTable(VkBuffer materialTableBuffer, uint32_t maxMaterialsInTable) noexcept; - bool updateMaterialGPU(VkUploadContext::Recorder recorder, - uint32_t materialId, const MaterialGPU &gpu); + [[nodiscard]] TextureHandle defaultWhiteSrgb() const noexcept { + return m_whiteTexture; + } + [[nodiscard]] TextureHandle defaultBlackSrgb() const noexcept { + return m_blackTexture; + } + [[nodiscard]] TextureHandle defaultMetalRoughLinear() const noexcept { + return m_defaultMetalRough; + } + [[nodiscard]] TextureHandle defaultOcclusionLinear() const noexcept { + return m_defaultOcclusion; + } + [[nodiscard]] TextureHandle defaultNormalLinear() const noexcept { + return m_defaultNormal; + } + + [[nodiscard]] uint32_t defaultMaterial() const noexcept { + return m_defaultMaterial; + } - bool createDefaultMaterial(VkUploadContext::Recorder staticRec) noexcept; +private: + [[nodiscard]] static VkFormat formatFor(TextureUsage usage) noexcept; - [[nodiscard]] uint32_t resolveMaterial(uint32_t overrideMaterial) const; + TextureHandle storeTextureAndWrite(VkTexture2D &&tex); + + [[nodiscard]] uint32_t allocMaterialId() noexcept; -private: bool writeMaterialGPU(VkUploadContext::Recorder recorder, uint32_t materialId, const MaterialGPU &gpu); VkTextureUploader m_textureUploader; - VkMaterialUploader m_materialUploader; std::vector m_textures; - VkMaterialSets m_materialSets; + uint32_t m_maxTextures = 0; + VkMaterialSets m_materialSet; VkBuffer m_materialTable = VK_NULL_HANDLE; // non-owning uint32_t m_materialTableCapacity = 0; + uint32_t m_nextMaterialId = 0; - uint32_t m_defaultMaterial = UINT32_MAX; - TextureHandle m_whiteTexture{UINT32_MAX}; + TextureHandle m_whiteTexture{}; + TextureHandle m_blackTexture{}; + TextureHandle m_defaultMetalRough{}; + TextureHandle m_defaultOcclusion{}; + TextureHandle m_defaultNormal{}; - uint32_t m_activeMaterial = UINT32_MAX; + uint32_t m_defaultMaterial = UINT32_MAX; }; diff --git a/src/render/resources/resource_store.cpp b/src/render/resources/resource_store.cpp index b4f934d..a27d26b 100644 --- a/src/render/resources/resource_store.cpp +++ b/src/render/resources/resource_store.cpp @@ -17,7 +17,7 @@ bool ResourceStore::init(VkBackendCtx &ctx, const VkShaderInterface &interface, } if (!m_materials.init(ctx, interface.setLayoutMaterial(), - data.materialCapacity())) { + data.materialCapacity(), interface.maxTextures())) { std::cerr << "[ResourceStore] MaterialSystem init failed\n"; shutdown(); return false; diff --git a/src/render/scene/debug_ubo.hpp b/src/render/scene/debug_ubo.hpp new file mode 100644 index 0000000..571d72f --- /dev/null +++ b/src/render/scene/debug_ubo.hpp @@ -0,0 +1,14 @@ +#pragma once + +#include + +struct alignas(16) DebugUBO { + uint32_t view = + 0; // 0=final, 1=baseColor, 2=metallic, 3=roughness, 4=ao, + // 5=emissive, 6=vertex color, 7=uv, 8=worldNormals, 9=worldPos + uint32_t flags = 0; // bitfield toggles + float value0 = 0.0F; + float value1 = 0.0F; +}; + +static_assert(sizeof(DebugUBO) == 16); diff --git a/src/render/scene/lights_gpu.hpp b/src/render/scene/lights_gpu.hpp new file mode 100644 index 0000000..3e2e592 --- /dev/null +++ b/src/render/scene/lights_gpu.hpp @@ -0,0 +1,14 @@ +#pragma once + +#include + +struct alignas(16) PointLightGPU { + glm::vec3 positionWS{0.0F}; + float radius{1.0F}; + + glm::vec3 colorLinear{1.0F}; + float lumens{0.0F}; +}; + +static_assert(sizeof(PointLightGPU) == 32); +static_assert(alignof(PointLightGPU) >= 16); diff --git a/src/render/scene/push_constants.hpp b/src/render/scene/push_constants.hpp index ae3c417..e38fe14 100644 --- a/src/render/scene/push_constants.hpp +++ b/src/render/scene/push_constants.hpp @@ -3,8 +3,9 @@ #include struct DrawPushConstants { + uint32_t frameIndex = 0; uint32_t baseInstance = 0; uint32_t materialId = 0; }; -static_assert(sizeof(DrawPushConstants) == 8); +static_assert(sizeof(DrawPushConstants) == 12); diff --git a/src/render/scene/scene_data.cpp b/src/render/scene/scene_data.cpp index a49b915..45d61cb 100644 --- a/src/render/scene/scene_data.cpp +++ b/src/render/scene/scene_data.cpp @@ -3,43 +3,54 @@ #include "backend/core/vk_backend_ctx.hpp" #include "backend/gpu/buffers/vk_buffer.hpp" #include "backend/gpu/descriptors/vk_shader_interface.hpp" +#include "backend/gpu/upload/vk_instance_uploader.hpp" +#include "backend/gpu/upload/vk_lights_uploader.hpp" +#include "backend/gpu/upload/vk_upload_context.hpp" #include "backend/profiling/telemetry/telemetry.hpp" #include "engine/camera/camera_ubo.hpp" +#include "engine/logging/log.hpp" #include "render/resources/material_gpu.hpp" +#include "render/scene/lights_gpu.hpp" +#include "render/scene/scene_ubo.hpp" #include #include -#include +#include #include #include bool SceneData::init(VkBackendCtx &ctx, uint32_t framesInFlight, const VkShaderInterface &interface, uint32_t requestedMaxInstancesPerFrame, - uint32_t requestedMaxMaterials) { + uint32_t requestedMaxMaterials, + uint32_t requestedMaxPointLights) { shutdown(); if (framesInFlight == 0) { - std::cerr << "[SceneData] framesInFlight must be greater than 0\n"; + LOGE("framesInFlight must be greater than 0"); return false; } if (requestedMaxInstancesPerFrame == 0) { - std::cerr << "[SceneData] requestedMaxInstancesPerFrame must be > 0\n"; + LOGE("requestedMaxInstancesPerFrame must be > 0"); return false; } if (requestedMaxMaterials == 0) { - std::cerr << "[SceneData] requestedMaxMaterials must be > 0\n"; + LOGE("requestedMaxMaterials must be > 0"); return false; } + if (requestedMaxPointLights == 0) { + LOGE("requestedMaxPointLights must be > 0"); + } + if (!queryDeviceLimits(ctx.physicalDevice())) { shutdown(); return false; } - if (!initCameraBuffers(ctx.allocator(), framesInFlight)) { + if (!initSceneBuffers(ctx.allocator(), framesInFlight)) { shutdown(); return false; } @@ -55,12 +66,16 @@ bool SceneData::init(VkBackendCtx &ctx, uint32_t framesInFlight, return false; } - if (!initDescriptorSets(ctx.device(), interface)) { + if (!initPointLightBuffer(ctx.allocator(), framesInFlight, + requestedMaxPointLights)) { shutdown(); return false; } - (void)m_instanceUploader.init(); + if (!initDescriptorSets(ctx.device(), interface)) { + shutdown(); + return false; + } m_initiailized = true; return true; @@ -72,17 +87,17 @@ bool SceneData::queryDeviceLimits(VkPhysicalDevice physicalDevice) { m_maxStorageBufferRange = props.limits.maxStorageBufferRange; if (m_maxStorageBufferRange == 0) { - std::cerr << "[SceneData] maxStorageBufferRange is 0\n"; + LOGE("maxStorageBufferRange is 0"); return false; } return true; } -bool SceneData::initCameraBuffers(VmaAllocator allocator, - uint32_t framesInFlight) { - if (!m_cameraBufs.init(allocator, framesInFlight, sizeof(CameraUBO))) { - std::cerr << "[SceneData] Failed to init camera UBO buffers\n"; +bool SceneData::initSceneBuffers(VmaAllocator allocator, + uint32_t framesInFlight) { + if (!m_sceneBufs.init(allocator, framesInFlight, sizeof(SceneUBO))) { + LOGE("Camera UBO buffers initialization failed"); return false; } @@ -105,7 +120,7 @@ bool SceneData::initInstanceBuffer(VmaAllocator allocator, } if (m_maxInstancesPerFrame == 0 || wantedStride == 0) { - std::cerr << "[SceneData] maxStorageBufferRange too small for instances\n"; + LOGE("maxStorageBufferRange too small for instances"); return false; } @@ -119,7 +134,7 @@ bool SceneData::initInstanceBuffer(VmaAllocator allocator, if (!m_instanceBuf.init(allocator, totalBytes, usage, VkBufferObj::MemUsage::GpuOnly, /*mapped*/ false)) { - std::cerr << "[SceneData] Failed to create instance SSBO\n"; + LOGE("Instance SSBO creation failed"); return false; } @@ -143,7 +158,7 @@ bool SceneData::initMaterialBuffer(VmaAllocator allocator, } if (m_materialCapacity == 0 || m_materialTableBytes == 0) { - std::cerr << "[SceneData] maxStorageBufferRange too small for materials\n"; + LOGE("maxStorageBufferRange too small for materials"); return false; } @@ -152,7 +167,7 @@ bool SceneData::initMaterialBuffer(VmaAllocator allocator, if (!m_materialBuf.init(allocator, m_materialTableBytes, matUsage, VkBufferObj::MemUsage::GpuOnly, /*mapped*/ false)) { - std::cerr << "[SceneData] Failed to create instance SSBO\n"; + LOGE("Material table creation failed"); return false; } @@ -162,29 +177,78 @@ bool SceneData::initMaterialBuffer(VmaAllocator allocator, return true; } +bool SceneData::initPointLightBuffer(VmaAllocator allocator, + uint32_t framesInFlight, + uint32_t requestedMaxPointLights) { + m_maxPointLightsPerFrame = requestedMaxPointLights; + + VkDeviceSize wantedStride = + VkDeviceSize(m_maxPointLightsPerFrame) * sizeof(PointLightGPU); + + // Clamp to maxStorageBufferRange + if (wantedStride > m_maxStorageBufferRange) { + m_maxPointLightsPerFrame = + static_cast(m_maxStorageBufferRange / sizeof(PointLightGPU)); + wantedStride = + VkDeviceSize(m_maxPointLightsPerFrame) * sizeof(PointLightGPU); + } + + if (m_maxPointLightsPerFrame == 0 || wantedStride == 0) { + LOGE("maxStorageBufferRange too small for point lights"); + return false; + } + + m_pointLightFrameStride = wantedStride; + + const VkDeviceSize totalBytes = + VkDeviceSize(framesInFlight) * m_pointLightFrameStride; + + const VkBufferUsageFlags usage = + VK_BUFFER_USAGE_TRANSFER_DST_BIT | VK_BUFFER_USAGE_STORAGE_BUFFER_BIT; + + if (!m_pointLightBuf.init(allocator, totalBytes, usage, + VkBufferObj::MemUsage::GpuOnly, /*mapped=*/false)) { + LOGE("Point light SSBO creation failed"); + return false; + } + + // PROFILE_UPLOAD_ADD(UploadProfiler::Stat::LightAllocation, + // static_cast(totalBytes)); + + return true; +} + bool SceneData::initDescriptorSets(VkDevice device, const VkShaderInterface &interface) { - if (!m_sets.init(device, interface.setLayoutScene(), m_cameraBufs, + if (!m_sets.init(device, interface.setLayoutScene(), m_sceneBufs, m_instanceBuf.handle(), m_instanceFrameStride, - m_materialBuf.handle(), m_materialTableBytes)) { - std::cerr << "[SceneData] Failed to init scene descriptor sets\n"; + m_materialBuf.handle(), m_materialTableBytes, + m_pointLightBuf.handle(), m_pointLightFrameStride)) { + LOGE("Scene descriptor initialization failed"); return false; } return true; } void SceneData::shutdown() noexcept { - - m_instanceUploader.shutdown(); m_sets.shutdown(); + + m_pointLightBuf.shutdown(); m_materialBuf.shutdown(); m_instanceBuf.shutdown(); - m_cameraBufs.shutdown(); + m_sceneBufs.shutdown(); + + m_pointLightFrameStride = 0; + m_maxPointLightsPerFrame = 0; + m_pointLightCountThisFrame = 0; m_instanceFrameStride = 0; m_maxInstancesPerFrame = 0; m_materialTableBytes = 0; + m_materialTableBytes = 0; + m_materialCapacity = 0; + m_initiailized = false; } @@ -193,22 +257,34 @@ bool SceneData::update(uint32_t frameIndex, const CameraUBO &camera) { return false; } - if (!m_cameraBufs.update(frameIndex, &camera, sizeof(CameraUBO))) { - std::cerr << "[PerFrameData] Failed to update camera UBO\n"; + SceneUBO scene{}; + scene.camera = camera; + scene.debug = m_debug; + + scene.pointLightCount = m_pointLightCountThisFrame; + scene.dirLightCount = m_dirLightCountThisFrame; + scene.dirLights = m_dirLights; + + if (!m_sceneBufs.update(frameIndex, &scene, sizeof(SceneUBO))) { + LOGE("Scene UBO update failed"); return false; } + m_pointLightCountThisFrame = 0; + m_dirLightCountThisFrame = 0; + m_pointLightsScratch.clear(); + return true; } -void SceneData::bind(VkCommandBuffer cmd, const VkShaderInterface &interface, - uint32_t frameIndex) const { +void SceneData::bind(VkCommandBuffer cmd, + const VkShaderInterface &interface) const { if (!m_initiailized) { return; } // set 0 - m_sets.bind(cmd, interface.pipelineLayout(), 0, frameIndex); + m_sets.bind(cmd, interface.pipelineLayout(), 0); } InstanceUploadResult @@ -218,7 +294,52 @@ SceneData::uploadInstances(VkUploadContext::Recorder recorder, const VkDeviceSize frameBase = VkDeviceSize(frameIndex) * m_instanceFrameStride; - return m_instanceUploader.uploadMat4Instances( + return InstanceUploader::uploadMat4Instances( recorder, m_instanceBuf.handle(), frameBase, m_instanceFrameStride, m_maxInstancesPerFrame, cursorInstances, models); } + +bool SceneData::uploadPointLights(VkUploadContext::Recorder recorder, + uint32_t frameIndex, + std::span lights) { + const VkDeviceSize frameBase = + VkDeviceSize(frameIndex) * m_pointLightFrameStride; + + const auto res = LightsUploader::uploadPointLights( + recorder, m_pointLightBuf.handle(), frameBase, m_pointLightFrameStride, + m_maxPointLightsPerFrame, lights); + + m_pointLightCountThisFrame = res.lightCount; + return true; +} + +// TODO: move into its own file +void SceneData::clearLights() { + m_dirLightCountThisFrame = 0; + m_pointLightsScratch.clear(); + m_pointLightCountThisFrame = 0; +} + +void SceneData::addDirectionalLight(const DirectionalLight &light) { + if (m_dirLightCountThisFrame >= kMaxDirLights) { + LOGE("Direcitonal lights count this frame is higher than kMaxDirLights"); + return; + } + + m_dirLights[m_dirLightCountThisFrame++] = light; +} + +void SceneData::addPointLight(const PointLightGPU &light) { + m_pointLightsScratch.push_back(light); +} + +bool SceneData::commitLights(VkUploadContext::Recorder recorder, + uint32_t frameIndex) { + if (!m_initiailized) { + return false; + } + + (void)uploadPointLights(recorder, frameIndex, m_pointLightsScratch); + + return true; +} diff --git a/src/render/scene/scene_data.hpp b/src/render/scene/scene_data.hpp index ecf67f7..1eb8bf1 100644 --- a/src/render/scene/scene_data.hpp +++ b/src/render/scene/scene_data.hpp @@ -2,15 +2,22 @@ #include "backend/core/vk_backend_ctx.hpp" #include "backend/gpu/buffers/vk_buffer.hpp" +#include "backend/gpu/buffers/vk_per_frame_uniform_buffers.hpp" #include "backend/gpu/descriptors/vk_scene_sets.hpp" #include "backend/gpu/descriptors/vk_shader_interface.hpp" #include "backend/gpu/upload/vk_instance_uploader.hpp" #include "backend/gpu/upload/vk_upload_context.hpp" #include "engine/camera/camera_ubo.hpp" +#include "render/scene/debug_ubo.hpp" +#include "render/scene/lights_gpu.hpp" +#include "render/scene/scene_ubo.hpp" +#include #include #include +#include #include +#include #include class SceneData { @@ -27,18 +34,21 @@ class SceneData { bool init(VkBackendCtx &ctx, uint32_t framesInFlight, const VkShaderInterface &interface, uint32_t requestedMaxInstancesPerFrame, - uint32_t requestedMaxMaterials); + uint32_t requestedMaxMaterials, uint32_t requestedMaxPointLights); void shutdown() noexcept; bool update(uint32_t frameIndex, const CameraUBO &camera); - void bind(VkCommandBuffer cmd, const VkShaderInterface &interface, - uint32_t frameIndex) const; + void bind(VkCommandBuffer cmd, const VkShaderInterface &interface) const; InstanceUploadResult uploadInstances(VkUploadContext::Recorder recorder, uint32_t frameIndex, uint32_t &cursorInstances, std::span models); + bool uploadPointLights(VkUploadContext::Recorder recorder, + uint32_t frameIndex, + std::span lights); + [[nodiscard]] VkBuffer materialBuffer() const noexcept { return m_materialBuf.handle(); } @@ -59,17 +69,41 @@ class SceneData { return m_maxInstancesPerFrame; } + [[nodiscard]] VkBuffer pointLightBuffer() const noexcept { + return m_pointLightBuf.handle(); + } + [[nodiscard]] VkDeviceSize pointLightFrameStride() const noexcept { + return m_pointLightFrameStride; + } + [[nodiscard]] uint32_t maxPointLightsPerFrame() const noexcept { + return m_maxPointLightsPerFrame; + } + + void setDebug(const DebugUBO &debug) { m_debug = debug; } + + void clearLights(); + void addDirectionalLight(const DirectionalLight &light); + void addDirectionalLight(glm::vec3 directionWS, glm::vec3 colorLinear, + float illuminanceLux); + + void addPointLight(const PointLightGPU &light); + void addPointLight(glm::vec3 posWS, glm::vec3 colorLinear, float umens, + float radius); + + bool commitLights(VkUploadContext::Recorder recorder, uint32_t frameIndex); + private: - bool initCameraBuffers(VmaAllocator allocator, uint32_t framesInFlight); + bool initSceneBuffers(VmaAllocator allocator, uint32_t framesInFlight); bool queryDeviceLimits(VkPhysicalDevice physicalDevice); bool initInstanceBuffer(VmaAllocator allocator, uint32_t framesInFlight, uint32_t requestedMaxInstancesPerFrame); bool initMaterialBuffer(VmaAllocator allocator, uint32_t requestedMaxMaterials); + bool initPointLightBuffer(VmaAllocator allocator, uint32_t framesInFlight, + uint32_t requestedMaxPointLights); bool initDescriptorSets(VkDevice device, const VkShaderInterface &interface); - VkPerFrameUniformBuffers m_cameraBufs; // sizeof(CameraUBO) per frame - + VkPerFrameUniformBuffers m_sceneBufs; VkDeviceSize m_maxStorageBufferRange = 0; VkBufferObj m_materialBuf; // device-local storage buffer (global) @@ -79,8 +113,19 @@ class SceneData { VkBufferObj m_instanceBuf; // device-local storage buffer VkDeviceSize m_instanceFrameStride = 0; uint32_t m_maxInstancesPerFrame = 0; - VkInstanceUploader m_instanceUploader; + + VkBufferObj m_pointLightBuf; // device-local storage buffer (per-frame slices) + VkDeviceSize m_pointLightFrameStride = 0; + uint32_t m_maxPointLightsPerFrame = 0; + + DebugUBO m_debug = {}; VkSceneSets m_sets; // set 0 bindings bool m_initiailized = false; + + std::array m_dirLights{}; + uint32_t m_dirLightCountThisFrame = 0; + + std::vector m_pointLightsScratch; + uint32_t m_pointLightCountThisFrame = 0; }; diff --git a/src/render/scene/scene_ubo.hpp b/src/render/scene/scene_ubo.hpp new file mode 100644 index 0000000..b06012e --- /dev/null +++ b/src/render/scene/scene_ubo.hpp @@ -0,0 +1,34 @@ +#pragma once + +#include "engine/camera/camera_ubo.hpp" +#include "render/scene/debug_ubo.hpp" +#include +#include +#include + +static constexpr uint32_t kMaxDirLights = 4; + +struct alignas(16) DirectionalLight { + glm::vec4 directionWS_illuminanceLux; // xyz dir, w lux + + glm::vec4 colorLinear_pad; // rgb color, w pad +}; + +static_assert(sizeof(DirectionalLight) == 32); + +static_assert(sizeof(DirectionalLight) == 32); + +struct alignas(16) SceneUBO { + CameraUBO camera; + DebugUBO debug; + + uint32_t pointLightCount; + uint32_t dirLightCount; + + uint32_t padA; + uint32_t padB; + + std::array dirLights; +}; + +static_assert(sizeof(SceneUBO) % 16 == 0);