-
Notifications
You must be signed in to change notification settings - Fork 43
Simple game engine #119
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Simple game engine #119
Conversation
…y commit to start getting proofreading while I still iterate on getting the engine to work. Engine still needs assets. Engine still needs Android project created. Engine currently crashes on running. Add `SimpleEngine` with Entity-Component-System (ECS) implementation - Introduced the `SimpleEngine` project featuring a modular ECS-based architecture. - Implemented core files: `Entity`, `Component`, and `TransformComponent` for managing entities and transformations. - Added CMake setup with Vulkan, GLFW, GLM, TinyGLTF, and KTX dependencies for rendering support. - Integrated shader compilation workflow using `slangc`. - Included initial scene setup with basic camera and cube entities in `main.cpp`.
…mmendations - Consolidated sections to address vendor-agnostic optimizations for mobile GPUs. - Replaced Huawei-specific details with broader guidance applicable to various mobile GPU architectures (Mali, Adreno, PowerVR, etc.). - Improved Vulkan extension examples and added checks for multiple vendor IDs for better compatibility. - Updated best practices and performance tuning sections to emphasize device diversity and testing requirements.
…n details - Simplified and reorganized GPU optimization lists for readability. - Expanded explanations for `VK_KHR_dynamic_rendering_local_read` and `VK_EXT_shader_tile_image` to highlight memory bandwidth reduction benefits. - Improved consistency in vendor-specific and tile-based optimization recommendations. - Added detailed examples and practical use cases for extension advantages in mobile rendering pipelines.
- Expanded tutorial with a comprehensive explanation of rendergraphs, including resource management, dependency tracking, and synchronization benefits. - Added a sample `Rendergraph` class implementation with methods for resource and pass management, including compile and execute functions. - Included practical examples such as deferred renderer setup and Vulkan synchronization best practices using semaphores, fences, and pipeline barriers. - Enhanced Vulkan tutorial content with a focus on simplifying complex rendering workflows and optimizing synchronization.
- Inserted new diagrams for layered architecture, component-based architecture, data-oriented design, and service locator pattern in the engine tutorial. - Adjusted and reordered content in corresponding sections for improved clarity and structure. - Updated references to foundational concepts, ensuring consistency across sections.
- Included a detailed clarification about windowing libraries and their purpose. - Provided examples of popular windowing libraries like GLFW, SDL, Qt, and SFML. - Enhanced understanding of platform-specific abstraction benefits for developers.
…i needs some help. it also looks like cleanup logic and resizing needs work.
…the semaphore. resizing still doesn't work. Next step is to get the parts of the engine to demonstrate things.
…the semaphore. resizing still doesn't work. Next step is to get the parts of the engine to demonstrate things.
…U processing mode.
…nk that's due to material only looking at the vespa. This includes camera controls.
asuessenbach
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Some more comments...
|
|
||
| // Call the resize callback if set | ||
| if (platform->resizeCallback) { | ||
| platform->resizeCallback(platform->width, platform->height); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The code for APP_CMD_INIT_WINDOW and APP_CMD_WINDOW_RESIZED are identical?
|
|
||
| bool AndroidPlatform::HasWindowResized() { | ||
| bool resized = windowResized; | ||
| windowResized = false; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it correct that a query (HasWindowResized) is not const and potentially modifies the queried state?
|
|
||
| void AndroidPlatform::SetWindowTitle(const std::string& title) { | ||
| // No-op on Android - mobile apps don't have window titles | ||
| (void)title; // Suppress unused parameter warning |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use [[maybe_unused]] instead?
| env->DeleteLocalRef(serviceStr); | ||
|
|
||
| // Check Vulkan support | ||
| // In a real implementation, this would check for Vulkan support and available extensions |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
And in this implementation, you don't do that because of?
| * @brief Get the Vulkan RAII device. | ||
| * @return The Vulkan RAII device. | ||
| */ | ||
| const vk::raii::Device& GetRaiiDevice() const { return device; } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it worth to have both, GetDevice() and GetRAIIDevice()?
Wouldn't getting the vk::raii::Device const & be sufficient, as you get to the corresponding vk::Device that easy?
| * @param commandBuffer The command buffer to submit. | ||
| * @param fence The fence to signal when the operation completes. | ||
| */ | ||
| void SubmitToComputeQueue(vk::CommandBuffer commandBuffer, vk::Fence fence) const { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To keep the API consistent, you could pass in a vk::raii::CommandBuffer const & and a vk::raii::Fence const & here.
| * @param oldLayout The old layout. | ||
| * @param newLayout The new layout. | ||
| */ | ||
| void TransitionImageLayout(vk::Image image, vk::Format format, vk::ImageLayout oldLayout, vk::ImageLayout newLayout) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pass in a vk::raii::Image const & here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In this file, some functions get/return a vk::Handle, some get/return a vk::raii::Handle.
Maybe it would be more consistent to always use a vk::raii::Handle.
| std::vector<vk::DescriptorSet> descriptorSetsToBindRaw; | ||
| descriptorSetsToBindRaw.reserve(1); | ||
| descriptorSetsToBindRaw.push_back(*computeDescriptorSets[0]); | ||
| commandBufferRaii.bindDescriptorSets(vk::PipelineBindPoint::eCompute, *computePipelineLayout, 0, descriptorSetsToBindRaw, {}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No need to copy stuff into a one-element vector.
You could just call
commandBufferRaii.bindDescriptorSets(vk::PipelineBindPoint::eCompute, *computePipelineLayout, 0, *computeDescriptorSets[0], {});
| // Extract raw command buffer for submission and release RAII ownership | ||
| // This prevents premature destruction while preserving the recorded commands | ||
| vk::CommandBuffer rawCommandBuffer = *commandBufferRaii; | ||
| commandBufferRaii.release(); // Release RAII ownership to prevent destruction |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Where will the rawCommandBuffer be destroyed, then?
…lkan queue handling. Bistro.gltf loads all textures, and creates physics geometry in under 44 seconds. Seeing 55 fps as well.
|
A code related question, that Andreas might've already asked (hard to see, github hides older comments): What is the idea behind converting (Vulkan-hpp) exceptions into bools? See e.g. Since this is exclusively using Vulkan-hpp and C++, why not stick to exceptions. So in above case, try, catch, log inside the function and raise the exception up to the calling code. |
asuessenbach
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Some more comments...
| if (LoadKTX2FileToRGBA(filePath, data, w, h, c) && renderer->LoadTextureFromMemory(textureId, data.data(), w, h, c)) { | ||
| material->albedoTexturePath = textureId; | ||
| } | ||
| renderer->LoadTextureAsync(filePath); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No need to LoadKTX2FileToRGBA anymore?
| std::vector<uint8_t> data; int w=0,h=0,c=0; | ||
| std::string filePath = baseTexturePath + image.uri; | ||
| if (LoadKTX2FileToRGBA(filePath, data, w, h, c) && renderer->LoadTextureFromMemory(textureId, data.data(), w, h, c)) { | ||
| if (LoadKTX2FileToRGBA(filePath, data, w, h, c)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here, you sequentially LoadKTX2FileToRGBA and then copy the loaded data in-memory in LoadTextureFromMemoryAsync. Both is done in the main thread, before anything asynchronously is done.
Maybe you should LoadKTX2FileToRGBA asynchronously as well?
... it seems, in the code below, you're already doing that.
| CleanupMarkedBodies(); | ||
| } | ||
|
|
||
| void PhysicsSystem::EnqueueRigidBodyCreation(Entity* entity, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe add some comment, that entity is supposed to be valid. Here, it's kept in the Engine::entityMap.
| // Texture aliasing: map canonical IDs to actual loaded keys (e.g., file paths) to avoid duplicates | ||
| inline void RegisterTextureAlias(const std::string& aliasId, const std::string& targetId) { | ||
| std::unique_lock<std::shared_mutex> lock(textureResourcesMutex); | ||
| if (aliasId.empty() || targetId.empty()) return; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No need to lock when aliasID or targetID is empty
| if (aliasId.empty() || targetId.empty()) return; | ||
| // Resolve targetId without re-locking by walking the alias map directly | ||
| std::string resolved = targetId; | ||
| for (int i = 0; i < 8; ++i) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A magical 8?
| resolved = it->second; | ||
| } | ||
| if (aliasId == resolved) { | ||
| textureAliases.erase(aliasId); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In a function named RegisterTextureAlias, I would not expect something to be erased.
What does that mean?
| inline std::string ResolveTextureId(const std::string& id) const { | ||
| std::shared_lock<std::shared_mutex> lock(textureResourcesMutex); | ||
| std::string cur = id; | ||
| for (int i = 0; i < 8; ++i) { // prevent pathological cycles |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What "pathological cycles" do you want to prevent here?
| // Initialize background thread pool for async tasks (textures, etc.) AFTER all Vulkan resources are ready | ||
| try { | ||
| // Size the thread pool based on hardware concurrency, clamped to a sensible range | ||
| unsigned int hw = std::max(2u, std::min(8u, std::thread::hardware_concurrency() ? std::thread::hardware_concurrency() : 4u)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
std::max( ..., std::min(...)) -> std::clamp?
| return true; | ||
| } | ||
|
|
||
| void Renderer::ensureThreadLocalVulkanInit() const { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Assuming, you're using vk::raii-objects only, no vk-objects, you don't need to initialize the VULKAN_HPP_DEFAULT_DISPATCHER at all. The vk::raii-objects hold their own (device-specific) dispatcher.
| .semaphoreType = vk::SemaphoreType::eTimeline, | ||
| .initialValue = 0 | ||
| }; | ||
| vk::SemaphoreCreateInfo timelineCreateInfo{ .pNext = &typeInfo }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe:
vk::StructureChain<vk::SemaphoreCreateInfo, vk::SemaphoreTypeCreateInfo> timelineChain( {}, { .semaphoreType = vk::SemaphoreType::eTimeline } );
asuessenbach
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Some more comments...
| #include <ranges> | ||
| #include <thread> | ||
|
|
||
| VULKAN_HPP_DEFAULT_DISPATCH_LOADER_DYNAMIC_STORAGE; // In a .cpp file |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
With vk::raii, this should not be needed.
| #include <vulkan/vk_platform.h> | ||
|
|
||
| // Debug callback for vk::raii | ||
| static VKAPI_ATTR VkBool32 VKAPI_CALL debugCallbackVkRaii( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe return a vk::Bool32 here.
|
|
||
| // Create the lighting pipeline | ||
| if (!createLightingPipeline()) { | ||
| std::cerr << "Failed to create lighting pipeline" << std::endl; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do you have a specific error message on some steps, but not on others?
| resources.pbrDescriptorSets.clear(); | ||
| resources.uniformBuffers.clear(); | ||
| resources.uniformBufferAllocations.clear(); | ||
| resources.uniformBuffersMapped.clear(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do you explicitly clear all those resources? Aren't they cleared automatically?
| std::cout << "Adding optional extension: " << optionalExt << std::endl; | ||
| break; | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe replace the inner loop with something like
if ( std::ranges::any_of( availableExtensions,
[&optionalExt]( auto const & availableExt ) { return strcmp( availableExt.extensionName, optionalExt ) == 0; } ) )
{
deviceExtensions.push_back( optionalExt );
std::cout << "Adding optional extension: " << optionalExt << std::endl;
}
| // Initialize member variable for proper lifetime management | ||
| pbrPipelineRenderingCreateInfo = vk::PipelineRenderingCreateInfo{ | ||
| .sType = vk::StructureType::ePipelineRenderingCreateInfo, | ||
| .pNext = nullptr, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
.sType again, and .pNext = nullptr
| depthStencilOpaque.depthWriteEnable = VK_TRUE; | ||
|
|
||
| vk::GraphicsPipelineCreateInfo opaquePipelineInfo{ | ||
| .sType = vk::StructureType::eGraphicsPipelineCreateInfo, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
.sType, again
| .sType = vk::StructureType::eGraphicsPipelineCreateInfo, | ||
| .pNext = &pbrPipelineRenderingCreateInfo, | ||
| .flags = vk::PipelineCreateFlags{}, | ||
| .stageCount = 2, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
.stageCount = static_cast<uint32_t>(shaderStages.size(); ?
| depthStencilBlended.depthWriteEnable = VK_FALSE; | ||
|
|
||
| vk::GraphicsPipelineCreateInfo blendedPipelineInfo{ | ||
| .sType = vk::StructureType::eGraphicsPipelineCreateInfo, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
.sType
| } | ||
|
|
||
| // Create a lighting pipeline | ||
| bool Renderer::createLightingPipeline() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe add some comments, highlighting the differences to createGraphicsPipeline?
asuessenbach
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Some more comments...
|
|
||
| // Find queue families | ||
| QueueFamilyIndices indices = findQueueFamilies(physicalDevice); | ||
| uint32_t queueFamilyIndices[] = {indices.graphicsFamily.value(), indices.presentFamily.value()}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
std::array<uint32_t,2>?
| // Set sharing mode | ||
| if (indices.graphicsFamily != indices.presentFamily) { | ||
| createInfo.imageSharingMode = vk::SharingMode::eConcurrent; | ||
| createInfo.queueFamilyIndexCount = 2; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
-> static_cast<uint32_t>(queueFamilyIndices.size())
| .baseArrayLayer = 0, | ||
| .layerCount = 1 | ||
| } | ||
| }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe move the createInfo out of this loop, and just set .image in the loop?
| vk::SemaphoreCreateInfo semaphoreInfo{}; | ||
| vk::FenceCreateInfo fenceInfo{ | ||
| .flags = vk::FenceCreateFlagBits::eSignaled | ||
| }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would move the fenceInfo closer to the loop where it's used.
| inFlightFences.emplace_back(device, fenceInfo); | ||
| } | ||
|
|
||
| // Ensure uploads timeline semaphore exists (created early in createLogicalDevice) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"Ensure..." means, some check or assertion, or what?
| if (modelLoader && entity->GetName().find("_Material_") != std::string::npos) { | ||
| std::string entityName = entity->GetName(); | ||
| size_t tagPos = entityName.find("_Material_"); | ||
| if (tagPos != std::string::npos) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Checking twice for _Material_ ?
| // Extract material name from entity name for any GLTF model entities | ||
| std::string entityName = entity->GetName(); | ||
| size_t tagPos = entityName.find("_Material_"); | ||
| if (tagPos != std::string::npos) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Again checking twice for _Material_ ?
| if (modelLoader && entity->GetName().find("_Material_") != std::string::npos) { | ||
| std::string entityName = entity->GetName(); | ||
| size_t tagPos = entityName.find("_Material_"); | ||
| if (tagPos != std::string::npos) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
And a third time this checking sequence on _Material_. Maybe put everything in those three blocks into one?
| uint32_t instanceCount = static_cast<uint32_t>(std::max(1u, static_cast<uint32_t>(meshComponent->GetInstanceCount()))); | ||
| commandBuffers[currentFrame].drawIndexed(meshIt->second.indexCount, instanceCount, 0, 0, 0); | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The opaque and the blended pass are at least very similar and lengthy. Maybe it's worth to use some helper function, instead?
| framebufferResized = true; | ||
| } | ||
|
|
||
| if (result.first == vk::Result::eErrorOutOfDateKHR || result.first == vk::Result::eSuboptimalKHR || framebufferResized) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Again: eErrorOutOfDataKHR won't be provided via result.first, but by an exception.
|
|
||
| [source,cpp] | ||
| ---- | ||
| class MobileOptimizedEngine { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can the simple engine be built for Android as it is?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No not yet. That's going to come from the samples version.
asuessenbach
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
And some more comments...
| #include <cstring> | ||
| #include <functional> | ||
|
|
||
| // stb_image dependency removed; all GLTF textures are uploaded via memory path from ModelLoader. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this comment worth to have?
After all, this file is added right here, so nobody knows about its potential past.
| ); | ||
|
|
||
| depthImage = std::move(depthImg); | ||
| depthImageAllocation = std::move(depthImgAllocation); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do you introduce those local variables depthImg and depthImgAllocation?
You could directly use std::tie( depthImage, depthImageAllocation ) = ...
| } | ||
| } | ||
|
|
||
| // Resolve on-disk path (may differ from logical ID) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does this comment belong to line 69?
| } | ||
|
|
||
| // Resolve on-disk path (may differ from logical ID) | ||
| std::string resolvedPath = textureId; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't get the difference between resolvedPath and textureId.
| if (it2 != textureResources.end()) { | ||
| return true; | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
With this check here, is the check at the top of the function needed?
| } | ||
| } | ||
| descriptor_path_resolved: ; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Three identical fallback-pathes... can this be organized differently to maybe have just one?
| std::cerr << "Failed to create descriptor sets: " << e.what() << std::endl; | ||
| return false; | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wow, nearly 400 lines of code for a single function!
Maybe it's worth to split that into smaller chunks that are easier to digest?
| if (!baseColor.empty()) { | ||
| texturePath = baseColor; | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just assign texturePath = meshComponent->GetBaseColorTexturePath()?
If it's also empty, it doesn't change anything.
|
|
||
| // Align allocation size to nonCoherentAtomSize (64 bytes) to prevent validation errors | ||
| // VUID-VkMappedMemoryRange-size-01390 requires memory flush sizes to be multiples of nonCoherentAtomSize | ||
| const vk::DeviceSize nonCoherentAtomSize = 64; // Typical value, should query from device properties |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you should query this, why aren't you querying it?
| // Only update PBR descriptor sets (they have light buffer bindings) | ||
| if (!resources.pbrDescriptorSets.empty()) { | ||
| for (size_t i = 0; i < resources.pbrDescriptorSets.size() && i < lightStorageBuffers.size(); ++i) { | ||
| if (i < lightStorageBuffers.size() && *lightStorageBuffers[i].buffer) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You already checked for i < lightStorageBuffers.size() one line above.
asuessenbach
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Some more comments...
| } else if (tiling == vk::ImageTiling::eOptimal && (props.optimalTilingFeatures & features) == features) { | ||
| return format; | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe:
auto formatIt = std::ranges::find_if( candidates,
[&physicalDevice, &tiling, &features]( vk::Format const & format )
{
vk::FormatProperties props = physicalDevice.getFormatProperties( format );
return ( tiling == vk::ImageTiling::eLinear && ( props.linearTilingFeatures & features ) == features ) ||
( tiling == vk::ImageTiling::eOptimal && ( props.optimalTilingFeatures & features ) == features );
} );
if ( formatIt != candidates.end() )
{
return *formatIt;
}
| } catch (const std::exception& e) { | ||
| std::cerr << "Failed to find supported depth format, falling back to D32_SFLOAT: " << e.what() << std::endl; | ||
| // Fallback to D32_SFLOAT which is widely supported | ||
| return vk::Format::eD32Sfloat; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This would mean, eD32Sfloat without support of eDepthStencilAttachment.
Is that reasonable?
| } | ||
|
|
||
| // If not found, return first available format | ||
| return availableFormats[0]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe add an assert( !availableFormats.empty() );
| } | ||
|
|
||
| // If not found, return FIFO mode (guaranteed to be available) | ||
| return vk::PresentModeKHR::eFifo; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe:
auto modeIt = std::ranges::find( availablePresentModes, vk::PresentModeKHR::eMailbox );
return modeIt != availablePresentModes.end() ? *modeIt : vk::PresentModeKHR::eFiFo;
|
|
||
| void Resource::Unload() { | ||
| loaded = false; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are the Load and Unload functions complete?
| glm::vec3 scale = {1.0f, 1.0f, 1.0f}; | ||
|
|
||
| glm::mat4 modelMatrix = glm::mat4(1.0f); | ||
| bool matrixDirty = true; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wouldn't glm::mat4(1.0f) fit is exactly to the position/rotation/scale above?
Then it's not dirty.
| * @param eulerAngles The rotation to apply in radians. | ||
| */ | ||
| void Rotate(const glm::vec3& eulerAngles) { | ||
| rotation += eulerAngles; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Euler angles can't be concatenated this way.
asuessenbach
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
And some more comments...
|
|
||
| // Enable required features | ||
| auto features = physicalDevice.getFeatures2(); | ||
| features.features.samplerAnisotropy = vk::True; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You should not get the supported features and use that struct to enable all supported features.
Instead you should use a local features struct, set the required (and supported, as should have been tested in pickPhysicalDevice) features.
| // Enable Vulkan 1.3 features | ||
| vk::PhysicalDeviceVulkan13Features vulkan13Features; | ||
| vulkan13Features.dynamicRendering = vk::True; | ||
| vulkan13Features.synchronization2 = vk::True; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As above.
| vk::PhysicalDeviceVulkan13Features vulkan13Features; | ||
| vulkan13Features.dynamicRendering = vk::True; | ||
| vulkan13Features.synchronization2 = vk::True; | ||
| features.pNext = &vulkan13Features; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe:
vk::StructureChain<vk::PhysicalDeviceFeatures2, vk::PhysicalDeviceVulkan13Features> featureChain = {
{.features = {.samplerAnisotropy = true }}, // vk::PhysicalDeviceFeatures2
{.synchronization2 = true, .dynamicRendering = true } // vk::PhysicalDeviceVulkan13Features
};
en/Building_a_Simple_Engine/Camera_Transformations/02_math_foundations.adoc
Outdated
Show resolved
Hide resolved
en/Building_a_Simple_Engine/Camera_Transformations/02_math_foundations.adoc
Outdated
Show resolved
Hide resolved
en/Building_a_Simple_Engine/Camera_Transformations/03_camera_implementation.adoc
Outdated
Show resolved
Hide resolved
en/Building_a_Simple_Engine/Camera_Transformations/03_camera_implementation.adoc
Outdated
Show resolved
Hide resolved
en/Building_a_Simple_Engine/Camera_Transformations/03_camera_implementation.adoc
Outdated
Show resolved
Hide resolved
en/Building_a_Simple_Engine/Camera_Transformations/03_camera_implementation.adoc
Outdated
Show resolved
Hide resolved
en/Building_a_Simple_Engine/Camera_Transformations/03_camera_implementation.adoc
Outdated
Show resolved
Hide resolved
asuessenbach
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
And some more comments...
| // Space and Control provide intuitive up/down movement | ||
| if (glfwGetKey(window, GLFW_KEY_SPACE) == GLFW_PRESS) | ||
| camera.processKeyboard(CameraMovement::UP, deltaTime); // Move up along camera's up vector | ||
| if (glfwGetKey(window, GLFW_KEY_LEFT_CONTROL) == GLFW_PRESS) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm a bit surprised about this mapping.
While the other five keys are actual character keys, CONTROL is a modifier key.
Is that actually an established convention?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It probably isn't. An internal designer requested the same key conventions as another program they've been using for UP/Down translation with space being up and left control being down when using WASD. I'm totally game to modify to the desired controls. I think q/e is popular. But most games don't really have cause for up/down translations and I've seen so many mappings that I honestly don't have an opinion anymore.
en/Building_a_Simple_Engine/Camera_Transformations/04_camera_implementation.adoc
Show resolved
Hide resolved
en/Building_a_Simple_Engine/Camera_Transformations/04_transformation_matrices.adoc
Outdated
Show resolved
Hide resolved
en/Building_a_Simple_Engine/Camera_Transformations/05_vulkan_integration.adoc
Outdated
Show resolved
Hide resolved
en/Building_a_Simple_Engine/Engine_Architecture/04_resource_management.adoc
Show resolved
Hide resolved
en/Building_a_Simple_Engine/Engine_Architecture/04_resource_management.adoc
Outdated
Show resolved
Hide resolved
en/Building_a_Simple_Engine/Engine_Architecture/04_resource_management.adoc
Show resolved
Hide resolved
en/Building_a_Simple_Engine/Engine_Architecture/04_resource_management.adoc
Show resolved
Hide resolved
en/Building_a_Simple_Engine/Engine_Architecture/04_resource_management.adoc
Show resolved
Hide resolved
asuessenbach
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
And some more comments...
en/Building_a_Simple_Engine/Engine_Architecture/05_rendering_pipeline.adoc
Show resolved
Hide resolved
en/Building_a_Simple_Engine/Engine_Architecture/05_rendering_pipeline.adoc
Outdated
Show resolved
Hide resolved
| }; | ||
|
|
||
| // Core data storage for the rendergraph system | ||
| std::unordered_map<std::string, Resource> resources; // All resources referenced in the graph |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is the key in resources supposed to be identical to the corresponding Resource::name? Shouldn't Resource::name be removed then?
| // Process input dependencies - this pass must wait for producers | ||
| for (const auto& input : pass.inputs) { | ||
| auto it = resourceWriters.find(input); | ||
| if (it != resourceWriters.end()) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What does it mean, if there's no resourceWriter for this input? Is that an error in the rendergraph?
| } | ||
|
|
||
| // Register output production - subsequent passes may depend on these | ||
| for (const auto& output : pass.outputs) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Registering the pass.outputs here requires all passes are added in order. That is, pass i is not allowed to have outputs from pass j, i < j, as input.
Maybe you should first loop over the passes and gather the outputs into the resourceWriters, and then loop over the passes again to determine the dependencies.
| } | ||
|
|
||
| // Resource access interface for retrieving compiled resources | ||
| Resource* GetResource(const std::string& name) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shouldn't GetResource be a const function, returning a const pointer?
| vk::DependencyFlagBits::eByRegion, // Region-local dependency | ||
| 0, nullptr, 0, nullptr, 1, &barrier // Image barrier only | ||
| ); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would it also work to fill a std::vector<vk::ImageMemoryBarrier> and call commandBuffer.pipelineBarrier just once? Which one would be more performant?
| [source,cpp] | ||
| ---- | ||
| private: | ||
| uint32_t FindMemoryType(uint32_t typeFilter, vk::MemoryPropertyFlags properties) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How does this source snippet fit to the text above?
|
|
||
| auto it = renderPasses.find(name); | ||
| if (it != renderPasses.end()) { | ||
| return dynamic_cast<T*>(it->second.get()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Again: how does the user know that all of its arguments provided for a new RenderPass are ignored?
|
|
||
| [source,cpp] | ||
| ---- | ||
| // Forward declarations |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How does this code snippet fit to the text above?
asuessenbach
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Some more comments...
|
|
||
| ImGuiVulkanUtil::~ImGuiVulkanUtil() { | ||
| // Wait for device to finish operations before destroying resources | ||
| if (device) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can it ever happen that device == nullptr?
|
|
||
| [source,cpp] | ||
| ---- | ||
| void ImGuiVulkanUtil::init(float width, float height) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do you separate initialization from construction?
|
|
||
| === Resource Initialization: Font Data Extraction and Memory Calculation | ||
|
|
||
| First extract font atlas data from ImGui and calculates the memory requirements for GPU storage. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
typo: calculates -> calculate
| indexBuffer.unmap(); | ||
| } | ||
|
|
||
| ==== Begin a rendering scope |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Code end sign is missing
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Especially in this file, but I've seen it in others as well, you often have code like
vk::SamplerCreateInfo samplerInfo{};
samplerInfo.magFilter = vk::Filter::eLinear; // Smooth scaling when magnified
samplerInfo.minFilter = vk::Filter::eLinear; // Smooth scaling when minified
This is totally valid code, but
- You don't need the empty braced initialization
vk::SamplerCreateInfo samplerInfo{}. Other than the pure C-structs, the vk-structs default-initializes all their members. - Instead of setting the members of a struct one-by-one, I would prefer to use designated initializers almost everywhere.
In some files, you also use this style:
vk::SamplerCreateInfo samplerInfo{};
samplerInfo.setMagFilter(vk::Filter::eLinear); // Smooth scaling when magnified
samplerInfo.setMinFilter(vk::Filter::eLinear); // Smooth scaling when minified
which I would also consider to be inferior to the designated initializers. There are just very few cases, where you get something out of those setter functions. Maybe you should (temporarily) compile with VULKAN_HPP_NO_SETTERS set to catch those cases.
|
|
||
| This is the simplest approach and works well for most applications. With dynamic rendering, the code becomes even cleaner: | ||
|
|
||
| === Command Buffer Initialization |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should this and the following section ('til the "Multiple Command Buffers Approach" section) be on level 5, instead of level 3?
| static std::function<void(ImGuiIO&)> inputCallback; | ||
|
|
||
| // Initialization state | ||
| static bool initialized; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe use static inline initialization:
inline static vk::raii::Instance * instance = nullptr;
etc.
| return commandBuffer; | ||
| } | ||
|
|
||
| void ImGuiUtil::endSingleTimeCommands(vk::raii::CommandBuffer& commandBuffer) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are beginSingleTimeCommands and endSingleTimeCommands really specific to ImGuiUtil? Or could they be just some free functions?
| }; | ||
|
|
||
| queue->submit(submitInfo); | ||
| queue->waitIdle(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This needs at least a big and loud comment that you never ever should call queue->waitIdle() (or even device->waitIdle()) in production code.
Other synchronization methods like the fence should be used instead.
|
We might need a different way of capturing all that feedback. With all those discussions, this PR has become so large, that githubh is often having issues loading this for me :/ |
|
I love the feedback and I'm trying my best to keep up. But I agree. I'm getting pretty far behind and it'll take me a bit to even catch up to where everyone else is. Github is having problems with the size of this PR + comments. I'll try to get everything caught up in the next few days. The game engine itself still has some rendering bugs I'm trying to work through as well. |
And I thought, it's just my WLAN that's too slow! |
asuessenbach
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
And some more comments...
|
|
||
| This chapter serves as the foundation for understanding how light interacts with different materials in a physically accurate way. The concepts you'll learn here will be applied in later chapters, including the Loading_Models chapter where we'll use this knowledge to render glTF models with PBR materials. | ||
|
|
||
| Throughout our engine implementation, we'll be using vk::raii dynamic rendering and C++20 modules. The vk::raii namespace provides Resource Acquisition Is Initialization (RAII) wrappers for Vulkan objects, which helps with resource management and makes the code cleaner. Dynamic rendering simplifies the rendering process by eliminating the need for explicit render passes and framebuffers. C++20 modules improve code organization, compilation times, and encapsulation compared to traditional header files. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think, I've asked this before: does this paragraph fit into this chapter?
Either way, you need to format C++20 somehow, otherwise the ++ doesn't show up.
| ---- | ||
|
|
||
| Where: | ||
| * albedo is the base color of the surface |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The bullet points here are not rendered as a list.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Afair I did already raise that (impossible to find ^^). There needs to be an empty line between the `Where´ and the list for it to be rendererd correctly. Also affects other spots afair.
| ---- | ||
|
|
||
| Where: | ||
| * D is the Normal Distribution Function (NDF) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Again, those bullet points are not rendered as a list.
|
|
||
| In this chapter, we'll introduce Physically Based Rendering (PBR) and other lighting models. The concepts we cover here will be applied in later chapters, such as the Loading_Models chapter where we'll use glTF, which uses PBR with the metallic-roughness workflow for its material system. By understanding the theory behind different lighting models, including PBR, we can better leverage the material properties provided by glTF models and extend our rendering capabilities. | ||
|
|
||
| Throughout our engine implementation, we'll be using vk::raii dynamic rendering and C++20 modules. The vk::raii namespace provides Resource Acquisition Is Initialization (RAII) wrappers for Vulkan objects, which helps with resource management and makes the code cleaner. Dynamic rendering simplifies the rendering process by eliminating the need for explicit render passes and framebuffers. C++20 modules improve code organization, compilation times, and encapsulation compared to traditional header files. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This very same paragraph was at various other locations already.
Note the missing ++ of C++20 in the rendered text.
| * *Diffuse*: Light scattered in all directions (using Lambert's cosine law) | ||
| * *Specular*: Shiny highlights (using a power function of the reflection vector and view vector) | ||
|
|
||
| * *Advantages*: Reasonably realistic for many materials, intuitive parameters |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You need to separate those two lists. They're rendered as one list.
| * *Disadvantages*: Not physically accurate, can look artificial under certain lighting conditions | ||
| * *When to use*: For simple real-time applications where PBR is too expensive | ||
|
|
||
| For more information on the Phong lighting model, see the link:https://en.wikipedia.org/wiki/Phong_reflection_model[Wikipedia article]. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe you should emphasize the difference between Gouraud interpolation (calculate color at the vertices and interpolate them) and Phong interpolation (interpolate vertices and normals and calculate color on each pixel).
And in addition to that, Phong introduced the more complex reflection model, which could also be used with the Gouraud interpolation.
|
|
||
| // === LIGHTING SETUP === | ||
| // Calculate view direction (camera to fragment) | ||
| float3 V = normalize(ubo.camPos.xyz - input.WorldPos); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
V is the vector from fragment to camera, not from camera to fragment.
|
|
||
| // Calculate specular BRDF | ||
| float3 numerator = D * G * F; | ||
| float denominator = 4.0 * NdotV * NdotL + 0.0001; // Prevent division by zero |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To prevent division by zero, wouldn't some max be the better choice?
float denominator = max( 4.0 * NdotV * NdotL, 0.0001 );
| * Reinhard with exposure: Multiply color by an exposure before compressing to shift middle gray. | ||
| - Exposure parameter (ubo.exposure): Conceptually shifts scene brightness so midtones sit well under your chosen tone mapper. Even if the snippet shows a fixed operator, you can pre-scale color by exposure to support dynamic auto-exposure. | ||
| - Gamma correction (ubo.gamma): Displays are non-linear (approx 2.2). Lighting must happen in linear space, then we apply pow(color, 1/gamma) right before writing to the sRGB framebuffer. Skipping this causes washed-out or too-dark images. | ||
| - Pipeline note: Prefer sRGB formats for color attachments when presenting. If writing to an sRGB swapchain image, do gamma in shader OR use sRGB formats so hardware handles it — not both. Do exactly one. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think, I don't get this sentence. What exactly is the difference between the two cases described here? Both are dealing with sRGB?
|
|
||
| // Copy the uniform buffer object to the device memory using vk::raii | ||
| // With vk::raii, we can use the mapped memory directly | ||
| memcpy(uniformBuffers[currentFrame].mapped, &ubo, sizeof(ubo)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it vk::raii-specific, that you can use the mapped memory directly?
Or is it because you keep the memory mapped in our Buffer wrapper class?
asuessenbach
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Some more comments...
| 1) Extend the renderer with PBR pipeline objects and a material push-constant block | ||
| 2) Create the PBR pipeline (layout, shaders, blending, formats) alongside the main pipeline | ||
| 3) Record draws: bind PBR pipeline, bind geometry, and push per-material constants per mesh | ||
| 4) Clean up via RAII (no special teardown required) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This list is rendered in just one line.
|
|
||
| == Updating the Cleanup | ||
|
|
||
| We also need to update the cleanup process to destroy our PBR pipeline: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well, we don't need to update the cleanup process.
| @@ -0,0 +1,375 @@ | |||
| ::pp: {plus}{plus} | |||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A colon too much?
| - *Categorization*: Assets are grouped by type (models, textures, shaders, config) | ||
| - *Hierarchy*: Assets are organized in a nested structure (e.g., models > characters > player) | ||
| - *Discoverability*: Common assets are placed in dedicated folders (e.g., common textures, core shaders) making them easy to find | ||
| - *Scalability*: The structure accommodates different quality levels and platform-specific assets (e.g., high-resolution textures, mobile shaders, quality presets) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This list isn't rendered as a list.
|
|
||
| = Loading Models: Asset Pipeline Concepts | ||
| :doctype: book | ||
| :sectnums: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is the first file with sectnums.
Do you want to add that to the other files as well?
| return node; | ||
| } | ||
| } | ||
| return nullptr; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe:
auto nodeIt = std::ranges::find_if( linearNodes, [&name]( auto const & node ){ return node.name == name; } );
return ( nodeIt != linearNodes.end() ) ? *nodeIt : nullptr;
| } | ||
|
|
||
| void updateAnimation(uint32_t index, float deltaTime) { | ||
| if (animations.empty() || index >= animations.size()) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is index >= animations.size() a valid check? Or would it just hide some programmer's error?
In that case, it probably should be replaced by an assertion.
And, by the way: is a public function that updates a specific animation reasonable? Or should it be private and a public function updates all animations.
| } | ||
|
|
||
| for (auto& channel : animation.channels) { | ||
| AnimationSampler& sampler = animation.samplers[channel.samplerIndex]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe add an
assert( channel.samplerIndex < animation.samplers.size() );
| Animation& animation = animations[index]; | ||
| animation.currentTime += deltaTime; | ||
| if (animation.currentTime > animation.end) { | ||
| animation.currentTime = animation.start; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The animation always gets to the start. No matter how much deltaTime might have advanced beyond animation.end.
|
|
||
| // Find the current key frame | ||
| for (size_t i = 0; i < sampler.inputs.size() - 1; i++) { | ||
| if (animation.currentTime >= sampler.inputs[i] && animation.currentTime <= sampler.inputs[i + 1]) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe:
auto keyFrameIt = std::ranges::lower_bound( sampler.inputs, animation.currentTime );
assert( keyFrameIt != sampler.inputs.end() );
size_t keyFrameIndex = std::distance( sampler.inputs.begin(), keyFrameIt );
asuessenbach
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Some more comments...
| glTF supports three interpolation methods: | ||
| * *LINEAR*: Smooth transitions with constant velocity | ||
| * *STEP*: Sudden changes with no interpolation | ||
| * *CUBICSPLINE*: Smooth curves with control points for acceleration and deceleration |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This list isn't rendered as a list.
|
|
||
| [source,cpp] | ||
| ---- | ||
| void drawModel(vk::raii::CommandBuffer& commandBuffer, Model* model) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is the model supposed to be changed in this function?
If not, it should be a const *.
| ==== Image-Based Lighting (IBL) | ||
|
|
||
| link:https://learnopengl.com/PBR/IBL/Diffuse-irradiance[IBL] uses environment maps to simulate global illumination: | ||
| * *Diffuse IBL*: Uses link:https://learnopengl.com/PBR/IBL/Diffuse-irradiance[irradiance maps] for ambient lighting |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This list here and beneath aren't rendered as a list.
|
|
||
| // Store buffer and memory | ||
| instanceBuffers.push_back(instanceBuffer); | ||
| instanceBufferMemories.push_back(instanceBufferMemory); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You probably want to move the buffer and the memory. You can't copy them.
|
|
||
| The material configuration system bridges the gap between artist-authored materials and GPU shader parameters. Push constants provide the fastest path for updating per-object material data, as they bypass the GPU's memory hierarchy and are directly accessible to shader cores. This makes them ideal for material properties that change frequently between draw calls. | ||
|
|
||
| The texture index mapping system (-1 for unused, positive integers for active bindings) allows shaders to conditionally sample textures based on availability. This approach provides flexibility where some materials might have normal maps while others don't, without requiring different shader variants or complex branching logic. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But 0 is a valid index. That is, non-negative integers are valid values for active bindings.
|
|
||
| [NOTE] | ||
| ==== | ||
| We covered PBR material theory and shader details earlier in Loading_Models/05_pbr_rendering.adoc, so we won’t restate that here. This section focuses on the wiring: how material properties are packed into push constants and consumed by the draw call in this chapter’s context. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should this be a link to the 05_pbr_rendering.adoc?
| pushConstants.baseColorFactor = glm::vec4(1.0f); // White base color | ||
| pushConstants.metallicFactor = 1.0f; // Fully metallic (safe default) | ||
| pushConstants.roughnessFactor = 1.0f; // Fully rough (safe default) | ||
| pushConstants.baseColorTextureSet = 1; // Assume default texture |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it correct to have the baseColorTextureSet = 1 for the default material?
| 2) Begin rendering, bind pipeline, set viewport/scissor | ||
| 3) Update camera UBO (view/projection) | ||
| 4) Traverse scene graph and issue per-mesh draws | ||
| 5) End rendering and present |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This list isn't rendered as a list.
| ==== Occlusion Culling | ||
|
|
||
| Occlusion culling involves skipping the rendering of objects that are hidden behind other objects: | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe you should note, that the presented algorithm works, but in its naïve form is unlikely to give a performance boost.
More efficient, but more complex handling would be needed.
asuessenbach
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Some more comments...
| InterpolationType interpolation; | ||
| std::vector<float> inputs; // Key frame timestamps | ||
| std::vector<glm::vec4> outputsVec4; // Key frame values (for rotations) | ||
| std::vector<glm::vec3> outputsVec3; // Key frame values (for translations and scales) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I assume, either outputsVec4 or outputsVec3 are used, never both?
Shouldn't it be a std::variant<std::vector<glm::vec4>, std::vector<glm::vec3>>, or a std::vector<std::variant<glm::vec4, glm::vec3>> then?
Moreover, inputs and outputs are probably meant to have the same size?
Maybe something like std::vector<KeyFrame> keyFrames;, instead, with
struct KeyFrame
{
float time;
std::variant<glm::vec4, glm::vec3> value;
};
| struct Animation { | ||
| std::string name; | ||
| std::vector<AnimationSampler> samplers; | ||
| std::vector<AnimationChannel> channels; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Again, samplers and channels are probably meant to be in sync?
Maybe something like std::vector<std::pair<AnimationSampler, AnimationChannel>>, instead?
Or some more descriptively named struct holding one sampler and channel?
| Animation& animation = animations[index]; | ||
| animation.currentTime += deltaTime; | ||
| if (animation.currentTime > animation.end) { | ||
| animation.currentTime = animation.start; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe something like
while ( animation.currentTime >= animation.end )
{
animation.currentTime -= animation.end - animation.start;
}
|
|
||
| === Animation Update: Channel Iteration and Sampler Access | ||
|
|
||
| New we iterate through all animation channels, establishing the connection between abstract animation data and the specific nodes in our scene graph that will receive transformation updates. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Typo: New -> Now
| [source,cpp] | ||
| ---- | ||
| // Find the current keyframe pair that brackets the animation time | ||
| for (size_t i = 0; i < sampler.inputs.size() - 1; i++) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe use std::lower_bound to find the current keyframe
| // Store intermediate transformations | ||
| std::vector<glm::vec3> fromTranslations; | ||
| std::vector<glm::quat> fromRotations; | ||
| std::vector<glm::vec3> fromScales; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You should probably reserve model.linearNodes.size() elements for those three vectors.
|
|
||
| allTranslations.push_back(translations); | ||
| allRotations.push_back(rotations); | ||
| allScales.push_back(scales); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You should probably move those vectors here, not copy.
|
|
||
| // Special handling for quaternions (rotations) | ||
| // We use nlerp (normalized lerp) for multiple quaternions | ||
| for (size_t animIdx = 0; animIdx < animationIndices.size(); animIdx++) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't see, why you're using two separate loops here.
| weights.push_back(weight); | ||
|
|
||
| // Limit to 3 closest animations for performance | ||
| if (animIndices.size() > 3) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This does not limit to 3, but just reduces by one if there are more than 3.
| headNode = node; | ||
| break; | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use std::find_if?
… the tutorial section first as only 90 total comments there, 200+ in the attachments and there's still bugs in the game engine itself. Holding back game code changes until it's bug free.
…. IDE ran out of memory trying to read them which means it's time for bed. Will do more tomorrow.
…heckin so I can work on other things in the tutorial and come back to this. Integrate mikktspace tangent generation, enhance robust tangent handling for poles, unify shader refraction logic, improve transmission Fresnel, and implement thread-safe rigid body management.
|
|
||
| This chapter represents the culmination of everything we've built throughout the previous chapters, as mobile development requires deep integration with all engine systems. You'll need solid mastery of Vulkan fundamentals and the engine architecture we've developed, since mobile optimization often requires fine-tuning at every level—from resource management and rendering pipelines to memory allocation and synchronization. | ||
|
|
||
| Modern C++ expertise becomes particularly valuable in mobile development, where performance constraints demand efficient code and careful resource management. C++17 and C++20 features like constexpr, structured bindings, and concepts help create mobile-optimized code that performs well under strict power and thermal limitations. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's funny: C++20 is correctly rendered, but C++ and C++17 are missing the ++.
|
|
||
| // Constructor-based initialization to replace separate Initialize() calls | ||
| AudioSystem(Engine* engine, Renderer* renderer) { | ||
| if (!Initialize(engine, renderer)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When you call Initialize in the constructor, you should probably make Initialize a private function.
| } | ||
| if (!entity) { continue; } | ||
| if (!entity->IsActive()) { continue; } | ||
| entity->Update(deltaTime); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe a bit easier to digest:
if (entity && entity->IsActive())
{
entity->Update(deltaTime);
}
|
|
||
| // Exposure slider | ||
| static float exposure = 3.0f; // Higher default for emissive lighting | ||
| static float exposure = 1.2f; // Default tuned to avoid washout |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe add some
static const float defaultExposure = 1.2f;
|
|
||
| // Constructor-based initialization to replace separate Initialize() calls | ||
| ImGuiSystem(Renderer* renderer, uint32_t width, uint32_t height) { | ||
| if (!Initialize(renderer, width, height)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Initialize should probably be private now.
| ModelLoader() = default; | ||
| // Constructor-based initialization to replace separate Initialize() calls | ||
| explicit ModelLoader(Renderer* _renderer) { | ||
| if (!Initialize(_renderer)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Initialize then should be private.
| explicit PhysicsSystem(Renderer* _renderer, bool enableGPU = true) { | ||
| SetRenderer(_renderer); | ||
| SetGPUAccelerationEnabled(enableGPU); | ||
| if (!Initialize()) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Again: Initialize should be private.
|
|
||
| // First, handle dependency: VK_EXT_attachment_feedback_loop_dynamic_state requires VK_EXT_attachment_feedback_loop_layout | ||
| const char* dynState = VK_EXT_ATTACHMENT_FEEDBACK_LOOP_DYNAMIC_STATE_EXTENSION_NAME; | ||
| const char* layoutReq = "VK_EXT_attachment_feedback_loop_layout"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
VK_EXT_ATTACHMENT_FEEDBACK_LOOP_LAYOUT_EXTENSION_NAME?
| // First, handle dependency: VK_EXT_attachment_feedback_loop_dynamic_state requires VK_EXT_attachment_feedback_loop_layout | ||
| const char* dynState = VK_EXT_ATTACHMENT_FEEDBACK_LOOP_DYNAMIC_STATE_EXTENSION_NAME; | ||
| const char* layoutReq = "VK_EXT_attachment_feedback_loop_layout"; | ||
| bool dynSupported = avail.count(dynState) > 0; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe use std::set::contains, instead of count > 0?
| // Enable required features | ||
| auto features = physicalDevice.getFeatures2(); | ||
| features.features.samplerAnisotropy = vk::True; | ||
| features.features.depthBiasClamp = vk::True; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you check somewhere for support of all those features?
asuessenbach
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Some more comments...
| // Signal to the shader whether swapchain is sRGB (1) or not (0) using padding0 | ||
| int outputIsSRGB = (swapChainImageFormat == vk::Format::eR8G8B8A8Srgb || | ||
| swapChainImageFormat == vk::Format::eB8G8R8A8Srgb) ? 1 : 0; | ||
| ubo.padding0 = outputIsSRGB; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think, it's not a good idea to carry some real information in a member called padding0
Besides that: maybe (from vulkan_format_traits.hpp)
ubo.padding0 = ( vk::componentNumericFormat( swapChainImageFormat, 0 ) == "SRGB" );
| ImGui::EndFrame(); | ||
| } | ||
|
|
||
| if (result.first == vk::Result::eErrorOutOfDateKHR || result.first == vk::Result::eSuboptimalKHR || framebufferResized.load(std::memory_order_relaxed)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unfortunately, eErrorOutOfDataKHR will only be available via an exception, not as a value of result.first.
(This can be changed with the Vulkan version 1.4.328, where a new compile-time flag VULKAN_HPP_HANDLE_ERROR_OUT_OF_DATE_AS_SUCCESS is introduced to make this error code a success code.)
| if (result.first != vk::Result::eSuccess) { | ||
| throw std::runtime_error("Failed to acquire swap chain image"); | ||
| } | ||
| if (result.first != vk::Result::eSuccess) { throw std::runtime_error("Failed to acquire swap chain image"); } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
acquireNextImage above should have thrown an exception in case of an error.
That is, this check will always fail.
| void Renderer::Render(const std::vector<std::unique_ptr<Entity>>& entities, CameraComponent* camera, ImGuiSystem* imguiSystem) { | ||
| if (memoryPool) memoryPool->setRenderingActive(true); | ||
| struct RenderingStateGuard { MemoryPool* pool; explicit RenderingStateGuard(MemoryPool* p) : pool(p) {} ~RenderingStateGuard() { if (pool) pool->setRenderingActive(false); } } guard(memoryPool.get()); | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Any reason, why you have compressed this into one line?
| try { | ||
| opaqueSceneColorImage.clear(); | ||
| opaqueSceneColorImageView.clear(); | ||
| opaqueSceneColorSampler.clear(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's not obvious that a function called createImageViews also clears some opaqueSceneColorImage and opaqueSceneColorSampler. And also that it clears the opaqueSceneColorImageView, but doesn't (re-)create it.
| commandBuffers[currentFrame].bindIndexBuffer(*meshIt->second.indexBuffer, 0, vk::IndexType::eUint32); | ||
| updateUniformBuffer(currentFrame, entity, camera); | ||
| auto& descSets = useBasic ? entityIt->second.basicDescriptorSets : entityIt->second.pbrDescriptorSets; | ||
| if (descSets.empty() || currentFrame >= descSets.size()) continue; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Again can this ever happen?
If so, should it be checked before the commandBuffer is filled?
| if (modelLoader && entity->GetName().find("_Material_") != std::string::npos) { | ||
| std::string entityName = entity->GetName(); | ||
| size_t tagPos = entityName.find("_Material_"); | ||
| if (tagPos != std::string::npos) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The same comments regarding "Material" handling as above.
| vk::ImageMemoryBarrier opaqueShaderBarrier{ .srcAccessMask = vk::AccessFlagBits::eTransferRead, .dstAccessMask = vk::AccessFlagBits::eShaderRead, .oldLayout = vk::ImageLayout::eTransferSrcOptimal, .newLayout = vk::ImageLayout::eShaderReadOnlyOptimal, .image = *opaqueSceneColorImage, .subresourceRange = {vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1} }; | ||
| commandBuffers[currentFrame].pipelineBarrier(vk::PipelineStageFlagBits::eTransfer, vk::PipelineStageFlagBits::eFragmentShader, {}, {}, {}, opaqueShaderBarrier); | ||
| vk::ImageMemoryBarrier swapchainTargetBarrier{ .srcAccessMask = vk::AccessFlagBits::eTransferWrite, .dstAccessMask = vk::AccessFlagBits::eColorAttachmentWrite, .oldLayout = vk::ImageLayout::eTransferDstOptimal, .newLayout = vk::ImageLayout::eColorAttachmentOptimal, .image = swapChainImages[imageIndex], .subresourceRange = {vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1} }; | ||
| commandBuffers[currentFrame].pipelineBarrier(vk::PipelineStageFlagBits::eTransfer, vk::PipelineStageFlagBits::eColorAttachmentOutput, {}, {}, {}, swapchainTargetBarrier); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This section is close to unreadable. Some line breaks would probably help.
And, of course, some comments, what each barrier does.
| imguiSystem->Render(commandBuffers[currentFrame], currentFrame); | ||
| } | ||
| commandBuffers[currentFrame].endRendering(); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The opaque and the transparent path seem to be very similar. Maybe worth to introduce a helper function?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A couple of lines are simply too long.
And the function Render as well.
…ture uploads, and thread-safe texture tracking. Refactor Vulkan queue handling and improve modern C++ usage. BRDF now works as intended so we have true PBR scene. We're going to add environment mapping in the complex samples, so this should complete the tutorial verison of the game engine.
# Conflicts: # antora/modules/ROOT/nav.adoc # en/02_Development_environment.adoc


This is the next big tutorial effort. I added the conclusion to the base line tutorial, and this is the "next thing." The game engine presented here is one example of what the tutorial describes and not a 1-1 mapping of the source code to the implementation in the attachments folder.
Future work will add new features to the tutorial in the Vulkan Samples project. This is meant to give the course to describe basics of the game engine and Samples is meant to describe features as they are implemented. This can be used as a template for a starting off point for any project as it is designed to be self contained.
Core features in this tutorial/engine:
Several features are left out intentionally as a method of what we'll implement and feature in the Samples including parallel loading of the assets. Bistro takes a few moments to load as it stands here.