feat: bring the Vulkan backend to parity with OpenGL and default to it - #182
Merged
Conversation
The Vulkan backend allocated a single mip level and left generate_mipmaps as a no-op, so every mipmapped texture (materials, UI panes, cached images, the IBL skybox) sampled a degraded chain relative to OpenGL's glGenerateMipmap. Allocate the full mip chain up front for mipmapped colour textures whose format advertises a linear blit, and fill levels 1..n with a vkCmdBlitImage down-sample that walks all array layers (so cube faces are covered) and leaves the whole chain in SHADER_READ_ONLY_OPTIMAL. Depth and 3D textures keep a single level, and a format without linear-blit support falls back to one level rather than an undefined chain.
The Vulkan backend reported no compute-prefilter support, so the IBL environment fell back to the CPU convolution while OpenGL ran the GPU path -- a performance gap on the road to dropping OpenGL. Implement the storage-image plumbing the convolution needs and have vk_device advertise supports_compute_prefilter: - texture_descriptor gains an opt-in storage flag; the Vulkan image adds VK_IMAGE_USAGE_STORAGE_BIT only when set and the format backs a storage image, so ordinary sampled textures keep their usual usage set. The OpenGL backend ignores the flag (image binding is unconditional there). - A storage descriptor must name a single mip level, so bind groups now reference a lazily-built, per-level image view (cube levels bind all six faces through one VK_IMAGE_VIEW_TYPE_CUBE view) instead of the whole-chain sampling view; the views are cached on the texture and released with it. - The compute pass moves bound storage textures to VK_IMAGE_LAYOUT_GENERAL before dispatch and restores the sampled layout at end(), so the later lighting passes read them as combined image samplers. Output already matched via the CPU fallback; this brings the GPU path to parity. The convolution math and shaders are shared with OpenGL and unchanged.
Flip the ALPHAENGINE_GRAPHICS_BACKEND default (and the unknown-value fallback) from opengl to vulkan now that the Vulkan backend is at feature parity -- mipmap generation and the GPU IBL prefilter both land on it. OpenGL stays available via ALPHAENGINE_GRAPHICS_BACKEND=opengl.
CI build artifactsWindows Debug and Release builds are attached to the workflow run summary under Artifacts:
Run #247 — commit |
Three pre-existing Vulkan-backend gaps surfaced once Vulkan became the default and the PBR demo exercised it (the OpenGL backend tolerated all three): - Vertex stride: sphere and plane uploaded position+uv+normal (32B) but standard_material declares a tangent at location 3 (offset 32), so the bound stride fell short of the attribute extent. Generate tangents for both primitives like box already does. The pos/uv/normal offsets are unchanged, so materials that ignore the tangent still read correctly from the wider stride. - Unbound samplers: a material that leaves a texture slot empty (no albedo/normal/... map, or no IBL cube when no environment is attached) left the descriptor unwritten, which Vulkan rejects for a statically-used binding. The device now keeps 1x1 white 2D and cube placeholders and binds the dimension-matching one for any empty texture slot; bind_group_layout_entry carries a dimension hint so the cube samplers (irradiance/prefiltered) get a cube, not a 2D, default. - Scene depth layout: the off-screen depth attachment stayed in DEPTH_STENCIL_ATTACHMENT_OPTIMAL but the velocity pass samples it, so it now resolves to SHADER_READ_ONLY_OPTIMAL (swapchain depth, never sampled, stays an attachment), and the outgoing subpass dependency releases the depth write for the fragment-shader read.
submit() detached the recorded command buffer from the encoder via release_command_buffer() but never freed it, so every frame leaked one VkCommandBuffer into the pool. Over a long run that pile grows into the hundreds of thousands, and tearing it all down in one vkDestroyCommandPool stalled shutdown for tens of seconds (far worse under validation layers). Runtime was unaffected because allocating a fresh buffer each frame is cheap, which is why it only showed at exit. Return each buffer to the pool once the GPU is done with it: the off-screen-only path already waits on the queue, so it frees immediately; the presenting path hands the buffer to the deferred-destroy queue, which begin_frame drains only after vkWaitForFences -- so it is freed exactly when the in-flight fence signals rather than leaked for the run.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Closes the remaining functional gaps between the Vulkan and OpenGL backends and makes Vulkan the default, as a step toward eventually deprecating OpenGL. Both backends still compile and the choice remains a runtime switch.
Changes
1. Vulkan mipmap generation (
vk_device_texture.cpp)Previously the Vulkan backend allocated a single mip level and
generate_mipmapswas a no-op, so every mipmapped texture (materials, UI panes, cached images, the IBL skybox) sampled a degraded chain versus OpenGL'sglGenerateMipmap. Now mipmapped colour textures allocate the full chain up front (when the format advertises a linear blit), andgenerate_mipmapsfills levels 1..n with avkCmdBlitImagedown-sample that walks all array layers (cube faces included), leaving the chain inSHADER_READ_ONLY_OPTIMAL. Depth/3D textures stay single-level.2. GPU IBL prefilter on Vulkan (
texture.hpp,vk_resources.hpp,vk_device_*.cpp,vk_command_encoder.*,ibl/environment.cpp)supports_compute_prefilter()now returnstrueon Vulkan, so the IBL convolution runs on the GPU like OpenGL instead of falling back to the CPU. The supporting plumbing:texture_descriptorgains an opt-instorageflag; the Vulkan image addsVK_IMAGE_USAGE_STORAGE_BITonly when set and the format backs a storage image, so ordinary sampled textures keep their usual usage set. OpenGL ignores the flag.VK_IMAGE_VIEW_TYPE_CUBEview), cached on the texture and released with it.VK_IMAGE_LAYOUT_GENERALbefore dispatch and restores the sampled layout atend().The convolution math and shaders are shared with OpenGL and unchanged.
3. Default backend → Vulkan (
core/settings.*,CLAUDE.md)ALPHAENGINE_GRAPHICS_BACKENDnow defaults tovulkan(and the unknown-value fallback does too). OpenGL stays available viaALPHAENGINE_GRAPHICS_BACKEND=opengl.Testing
This environment has no Vulkan GPU, so the changes are compile/link-verified only — not run. The first on-hardware launch now takes the Vulkan path by default; please run it once with validation layers enabled (
VK_INSTANCE_LAYERS=VK_LAYER_KHRONOS_validation) to confirm the mipmap blits and the cube storage-image / layout-transition handling in the IBL compute path. Fallback isALPHAENGINE_GRAPHICS_BACKEND=opengl.