From 893113616e947244771d727f0008382cd39a6939 Mon Sep 17 00:00:00 2001 From: lukas <89145039+codethinki@users.noreply.github.com> Date: Tue, 13 Oct 2026 20:13:21 +0200 Subject: [PATCH 001/135] rebuild setup: old code moved to legacy, linux presets, loader smoke test - jvk, jolly, demo and tests moved to legacy/ as reference, not built anymore - presets like cth (msvc, clang, gcc), cth_ROOT points at the sibling cth install - CMakeUserPresets.json is not tracked anymore - vcpkg: current baseline, only gtest and volk for now - cth_cmake updated to main - tests/: loader smoke test (vulkan 1.3 loader and device) --- .gitignore | 3 +- CMakeLists.txt | 12 +- CMakePresets.json | 109 +++++++++++++++--- CMakeUserPresets.json | 33 ------ {demo => legacy/demo}/CMakeLists.txt | 0 .../demo}/assets/bin/shader/shader.frag.spv | Bin .../demo}/assets/bin/shader/shader.vert.spv | Bin .../assets/img/texture/first_texture.png | Bin .../demo}/demo_shaders/CMakeLists.txt | 0 .../demo}/demo_shaders/src/shader.frag | 0 .../demo}/demo_shaders/src/shader.vert | 0 .../demo}/demo_shaders/src/test.bat | 0 {demo => legacy/demo}/src/HlcApp.cpp | 0 {demo => legacy/demo}/src/HlcApp.hpp | 0 {demo => legacy/demo}/src/demo_main.cpp | 0 {demo => legacy/demo}/src/pch/glob/boost.hpp | 0 {demo => legacy/demo}/src/pch/glob/cth.hpp | 0 {demo => legacy/demo}/src/pch/glob/glm.hpp | 0 {demo => legacy/demo}/src/pch/glob/std.hpp | 0 {demo => legacy/demo}/src/pch/glob/volk.hpp | 0 {demo => legacy/demo}/src/pch/pch.cpp | 0 {demo => legacy/demo}/src/pch/pch.hpp | 0 .../demo}/src/render/FrameResources.cpp | 0 .../demo}/src/render/FrameResources.hpp | 0 .../demo}/src/render/HlcFrameInfo.hpp | 0 .../demo}/src/render/HlcRenderSystem.cpp | 0 .../demo}/src/render/HlcRenderSystem.hpp | 0 .../src/render/frame_resources_config.hpp | 0 .../demo}/src/render/push_constant.hpp | 0 {jolly => legacy/jolly}/CMakeLists.txt | 0 {jolly => legacy/jolly}/doc/roadmap.md | 0 {jolly => legacy/jolly}/doc/scene.md | 0 {jolly => legacy/jolly}/doc/tinygltf.md | 0 .../jolly}/incl/jolly/core/core.hpp | 0 .../jolly}/incl/jolly/core/core_config.hpp | 0 .../model/primitive/loader/gltf_utility.hpp | 0 .../data/model/primitive/vertex_buffer.hpp | 0 .../jolly/data/model/primitive/vertex_key.hpp | 0 .../incl/jolly/data/staging_allocator.hpp | 0 .../incl/jolly/object/HlcRenderObject.hpp | 0 .../incl/jolly/object/HlcStandardObject.hpp | 0 .../incl/jolly/render/graph/RenderStage.hpp | 0 .../jolly/render/graph/RenderStageConfig.hpp | 0 .../jolly}/incl/jolly/render/graph/dag.hpp | 0 .../incl/jolly/render/graph/renderer.hpp | 0 .../incl/jolly/render/model/CthVertex.hpp | 0 .../incl/jolly/render/model/HlcModel.hpp | 0 .../jolly/render/model/HlcModelManager.hpp | 0 .../incl/jolly/render/render_pipeline.hpp | 0 .../render/submit/cmd/primary_cmd_buffer.hpp | 0 .../submit/cmd/secondary_cmd_buffer.hpp | 0 .../cmd/secondary_cmd_buffer_config.hpp | 0 .../submit/cmd/utility/task_container.hpp | 0 .../cmd/utility/task_container_mixin.hpp | 0 .../utility/thread_safe_task_container.hpp | 0 .../jolly}/incl/jolly/render/submit/queue.hpp | 0 .../jolly/render/submit/queue_properties.hpp | 0 .../incl/jolly/render/submit/submit_info.hpp | 0 .../incl/jolly/render/sync/RenderPulse.hpp | 0 .../jolly}/incl/jolly/render/sync/fence.hpp | 0 .../render/sync/pipeline_stage_flags.hpp | 0 .../jolly/render/sync/pipeline_wait_stage.hpp | 0 .../jolly}/incl/jolly/user/HlcCamera.hpp | 0 .../incl/jolly/user/HlcInputController.hpp | 0 .../jolly}/incl/jolly/user/HlcUser.hpp | 0 .../jolly}/incl/jolly/utility/CthOSWindow.hpp | 0 .../incl/jolly/utility/GraphicsCore.hpp | 0 .../incl/jolly/utility/GraphicsSyncConfig.hpp | 0 .../jolly/utility/graphics_core_config.hpp | 0 .../incl/jolly/utility/jvk_conversion.hpp | 0 .../jolly}/incl/jolly/utility/os/os_def.hpp | 0 .../jolly}/incl/jolly/utility/types.hpp | 0 {jolly => legacy/jolly}/src/core/core.cpp | 0 {jolly => legacy/jolly}/src/core/queue.cpp | 0 .../jolly}/src/core/submit_info.cpp | 0 .../data/model/primitive/vertex_buffer.cpp | 0 .../jolly}/src/data/staging_allocator.cpp | 0 .../jolly}/src/object/HlcStandardObject.cpp | 0 .../jolly}/src/pch/glob/boost.hpp | 0 {jolly => legacy/jolly}/src/pch/glob/cth.hpp | 0 {jolly => legacy/jolly}/src/pch/glob/glm.hpp | 0 {jolly => legacy/jolly}/src/pch/glob/std.hpp | 0 {jolly => legacy/jolly}/src/pch/pch.cpp | 0 {jolly => legacy/jolly}/src/pch/pch.hpp | 0 .../jolly}/src/render/graph/RenderStage.cpp | 0 .../jolly}/src/render/graph/renderer.cpp | 0 .../jolly}/src/render/model/HlcModel.cpp | 0 .../src/render/model/HlcModelManager.cpp | 0 .../render/submit/cmd/primary_cmd_buffer.cpp | 0 .../submit/cmd/secondary_cmd_buffer.cpp | 0 .../jolly}/src/render/sync/fence.cpp | 0 .../sync/pipeline_stage_flags_helpers.hpp | 0 .../sync/pipeline_wait_stage_helpers.hpp | 0 .../jolly}/src/user/HlcCamera.cpp | 0 .../jolly}/src/user/HlcInputController.cpp | 0 .../jolly}/src/utility/CthGraphicsCore.cpp | 0 .../src/utility/CthGraphicsSyncConfig.cpp | 0 .../jolly}/src/utility/CthOSWindow.cpp | 0 .../jolly}/src/utility/vk_convert.hpp | 0 {jvk => legacy/jvk}/CMakeLists.txt | 0 {jvk => legacy/jvk}/incl/jvk/base/core.hpp | 0 .../jvk}/incl/jvk/base/core_config.hpp | 0 .../jvk}/incl/jvk/base/destruction_queue.hpp | 0 .../jvk/base/destruction_queue_config.hpp | 0 {jvk => legacy/jvk}/incl/jvk/base/device.hpp | 0 .../jvk}/incl/jvk/base/device_config.hpp | 0 .../jvk}/incl/jvk/base/device_table.hpp | 0 .../jvk}/incl/jvk/base/instance.hpp | 0 {jvk => legacy/jvk}/incl/jvk/base/jvk.hpp | 0 .../jvk}/incl/jvk/base/physical_device.hpp | 0 .../jvk}/incl/jvk/base/queue/present_info.hpp | 0 .../jvk}/incl/jvk/base/queue/queue.hpp | 0 .../jvk}/incl/jvk/base/queue/queue_family.hpp | 0 .../jvk}/incl/jvk/base/queue/queue_state.hpp | 0 .../jvk}/incl/jvk/base/queue/submit_info.hpp | 0 .../jvk}/incl/jvk/data/buffer/base_buffer.hpp | 0 .../jvk}/incl/jvk/data/buffer/buffer.hpp | 0 .../jvk}/incl/jvk/data/buffer/inl/buffer.inl | 0 .../incl/jvk/data/descriptor/descriptor.hpp | 0 .../jvk/data/descriptor/descriptor_pool.hpp | 0 .../jvk/data/descriptor/descriptor_set.hpp | 0 .../descriptor/types/buffer_descriptors.hpp | 0 .../descriptor/types/image_descriptors.hpp | 0 .../jvk}/incl/jvk/data/img/image.hpp | 0 .../jvk}/incl/jvk/data/img/image_config.hpp | 0 .../jvk}/incl/jvk/data/img/image_view.hpp | 0 .../incl/jvk/data/img/texture/sampler.hpp | 0 .../incl/jvk/data/img/texture/texture.hpp | 0 .../jvk}/incl/jvk/data/memory/memory.hpp | 0 .../jvk}/incl/jvk/debug/debug_messenger.hpp | 0 .../jvk}/incl/jvk/render/cmd/cmd_buffer.hpp | 0 .../incl/jvk/render/cmd/cmd_buffer_config.hpp | 0 .../jvk}/incl/jvk/render/cmd/cmd_pool.hpp | 0 .../pass/attachment/attachment_collection.hpp | 0 .../attachment_collection_config.hpp | 0 .../attachment/attachment_description.hpp | 0 .../render/pass/framebuffer/fb_collection.hpp | 0 .../pass/framebuffer/fb_collection_config.hpp | 0 .../render/pass/framebuffer/framebuffer.hpp | 0 .../framebuffer/swapchain_fb_collection.hpp | 0 .../jvk}/incl/jvk/render/pass/render_pass.hpp | 0 .../render/pass/render_pass_begin_config.hpp | 0 .../jvk/render/pass/render_pass_config.hpp | 0 .../jvk}/incl/jvk/render/pass/subpass.hpp | 0 .../pipeline/layout/descriptor_set_layout.hpp | 0 .../pipeline/layout/pipeline_layout.hpp | 0 .../incl/jvk/render/pipeline/pipeline.hpp | 0 .../pipeline/pipeline_vertex_description.hpp | 0 .../jvk/render/pipeline/shader/shader.hpp | 0 .../jvk}/incl/jvk/render/sync/fence.hpp | 0 .../incl/jvk/render/sync/os_fence_handle.hpp | 0 .../incl/jvk/render/sync/pipeline_barrier.hpp | 0 .../jvk/render/sync/pipeline_wait_stage.hpp | 0 .../jvk}/incl/jvk/render/sync/semaphore.hpp | 0 .../jvk/render/sync/timline_semaphore.hpp | 0 .../jvk}/incl/jvk/surface/surface.hpp | 0 .../jvk}/incl/jvk/surface/surface_config.hpp | 0 .../incl/jvk/surface/swapchain/swapchain.hpp | 0 .../surface/swapchain/swapchain_config.hpp | 0 .../swapchain/swapchain_subpass_config.hpp | 0 .../jvk/utility/constant/debug_constants.hpp | 0 .../jvk/utility/constant/device_constants.hpp | 0 .../jvk}/incl/jvk/utility/constants.hpp | 0 .../device/physical_device_features.hpp | 0 .../jvk}/incl/jvk/utility/format.hpp | 0 .../incl/jvk/utility/format/format_string.hpp | 0 .../incl/jvk/utility/format/vk_to_string.hpp | 0 .../incl/jvk/utility/format/vk_to_string.inl | 0 .../jvk}/incl/jvk/utility/os/os_constants.hpp | 0 .../jvk}/incl/jvk/utility/os/os_def.hpp | 0 .../jvk}/incl/jvk/utility/os/os_exception.hpp | 0 .../incl/jvk/utility/os/os_helper_include.hpp | 0 .../jvk}/incl/jvk/utility/os/os_window.hpp | 0 .../jvk}/incl/jvk/utility/types.hpp | 0 .../jvk}/incl/jvk/utility/vk_exceptions.hpp | 0 .../jvk}/incl/jvk/utility/vk_hash.hpp | 0 .../jvk}/incl/jvk/utility/vk_overloads.hpp | 0 {jvk => legacy/jvk}/src/base/core.cpp | 0 {jvk => legacy/jvk}/src/base/device.cpp | 0 {jvk => legacy/jvk}/src/base/instance.cpp | 0 {jvk => legacy/jvk}/src/base/jvk.cpp | 0 .../jvk}/src/base/physical_device.cpp | 0 .../jvk}/src/base/queue/present_info.cpp | 0 {jvk => legacy/jvk}/src/base/queue/queue.cpp | 0 .../jvk}/src/base/queue/submit_info.cpp | 0 .../jvk}/src/data/buffer/base_buffer.cpp | 0 .../src/data/descriptor/descriptor_pool.cpp | 0 .../src/data/descriptor/descriptor_set.cpp | 0 .../jvk}/src/data/destruction_queue.cpp | 0 {jvk => legacy/jvk}/src/data/img/image.cpp | 0 .../jvk}/src/data/img/image_config.cpp | 0 .../jvk}/src/data/img/image_view.cpp | 0 .../jvk}/src/data/img/texture/sampler.cpp | 0 .../jvk}/src/data/img/texture/texture.cpp | 0 .../jvk}/src/data/memory/memory.cpp | 0 .../jvk}/src/debug/debug_messenger.cpp | 0 {jvk => legacy/jvk}/src/pch/glob/boost.hpp | 0 {jvk => legacy/jvk}/src/pch/glob/cth.hpp | 0 {jvk => legacy/jvk}/src/pch/glob/glm.hpp | 0 {jvk => legacy/jvk}/src/pch/glob/gsl.hpp | 0 {jvk => legacy/jvk}/src/pch/glob/std.hpp | 0 {jvk => legacy/jvk}/src/pch/glob/volk.hpp | 0 {jvk => legacy/jvk}/src/pch/jvk/pch_def.hpp | 0 {jvk => legacy/jvk}/src/pch/lib/volk/volk.cpp | 0 {jvk => legacy/jvk}/src/pch/pch.cpp | 0 {jvk => legacy/jvk}/src/pch/pch.hpp | 0 .../jvk}/src/render/cmd/cmd_buffer.cpp | 0 .../jvk}/src/render/cmd/cmd_pool.cpp | 0 .../pass/attachment/attachment_collection.cpp | 0 .../attachment/attachment_description.cpp | 0 .../render/pass/framebuffer/fb_collection.cpp | 0 .../pass/framebuffer/fb_collection_config.cpp | 0 .../render/pass/framebuffer/framebuffer.cpp | 0 .../framebuffer/swapchain_fb_collection.cpp | 0 .../jvk}/src/render/pass/render_pass.cpp | 0 .../jvk}/src/render/pass/subpass.cpp | 0 .../pipeline/layout/descriptor_set_layout.cpp | 0 .../pipeline/layout/pipeline_layout.cpp | 0 .../jvk}/src/render/pipeline/pipeline.cpp | 0 .../src/render/pipeline/shader/shader.cpp | 0 {jvk => legacy/jvk}/src/render/sync/fence.cpp | 0 .../jvk}/src/render/sync/os_fence_handle.cpp | 0 .../jvk}/src/render/sync/pipeline_barrier.cpp | 0 .../jvk}/src/render/sync/semaphore.cpp | 0 .../src/render/sync/timline_semaphore.cpp | 0 {jvk => legacy/jvk}/src/surface/surface.cpp | 0 .../jvk}/src/surface/swapchain/swapchain.cpp | 0 .../swapchain/swapchain_subpass_config.cpp | 0 .../device/physical_device_features.cpp | 0 .../jvk}/src/utility/os/incl_os.hpp | 0 .../jvk}/src/utility/os/incl_posix.hpp | 0 .../jvk}/src/utility/os/os_window.cpp | 0 .../jvk}/src/utility/os/win/incl_windows.hpp | 0 .../jvk}/src/utility/os/win/win_window.hpp | 0 legacy/tests/CMakeLists.txt | 16 +++ .../tests}/src/test_utils/pch.cpp | 0 {tests => legacy/tests}/src/test_utils/pch.h | 0 .../utility/format/format_string_test.cpp | 0 lib/cth_cmake | 2 +- tests/CMakeLists.txt | 12 +- tests/src/loader_test.cpp | 52 +++++++++ vcpkg-configuration.json | 11 +- vcpkg.json | 7 -- 243 files changed, 178 insertions(+), 79 deletions(-) delete mode 100644 CMakeUserPresets.json rename {demo => legacy/demo}/CMakeLists.txt (100%) rename {demo => legacy/demo}/assets/bin/shader/shader.frag.spv (100%) rename {demo => legacy/demo}/assets/bin/shader/shader.vert.spv (100%) rename {demo => legacy/demo}/assets/img/texture/first_texture.png (100%) rename {demo => legacy/demo}/demo_shaders/CMakeLists.txt (100%) rename {demo => legacy/demo}/demo_shaders/src/shader.frag (100%) rename {demo => legacy/demo}/demo_shaders/src/shader.vert (100%) rename {demo => legacy/demo}/demo_shaders/src/test.bat (100%) rename {demo => legacy/demo}/src/HlcApp.cpp (100%) rename {demo => legacy/demo}/src/HlcApp.hpp (100%) rename {demo => legacy/demo}/src/demo_main.cpp (100%) rename {demo => legacy/demo}/src/pch/glob/boost.hpp (100%) rename {demo => legacy/demo}/src/pch/glob/cth.hpp (100%) rename {demo => legacy/demo}/src/pch/glob/glm.hpp (100%) rename {demo => legacy/demo}/src/pch/glob/std.hpp (100%) rename {demo => legacy/demo}/src/pch/glob/volk.hpp (100%) rename {demo => legacy/demo}/src/pch/pch.cpp (100%) rename {demo => legacy/demo}/src/pch/pch.hpp (100%) rename {demo => legacy/demo}/src/render/FrameResources.cpp (100%) rename {demo => legacy/demo}/src/render/FrameResources.hpp (100%) rename {demo => legacy/demo}/src/render/HlcFrameInfo.hpp (100%) rename {demo => legacy/demo}/src/render/HlcRenderSystem.cpp (100%) rename {demo => legacy/demo}/src/render/HlcRenderSystem.hpp (100%) rename {demo => legacy/demo}/src/render/frame_resources_config.hpp (100%) rename {demo => legacy/demo}/src/render/push_constant.hpp (100%) rename {jolly => legacy/jolly}/CMakeLists.txt (100%) rename {jolly => legacy/jolly}/doc/roadmap.md (100%) rename {jolly => legacy/jolly}/doc/scene.md (100%) rename {jolly => legacy/jolly}/doc/tinygltf.md (100%) rename {jolly => legacy/jolly}/incl/jolly/core/core.hpp (100%) rename {jolly => legacy/jolly}/incl/jolly/core/core_config.hpp (100%) rename {jolly => legacy/jolly}/incl/jolly/data/model/primitive/loader/gltf_utility.hpp (100%) rename {jolly => legacy/jolly}/incl/jolly/data/model/primitive/vertex_buffer.hpp (100%) rename {jolly => legacy/jolly}/incl/jolly/data/model/primitive/vertex_key.hpp (100%) rename {jolly => legacy/jolly}/incl/jolly/data/staging_allocator.hpp (100%) rename {jolly => legacy/jolly}/incl/jolly/object/HlcRenderObject.hpp (100%) rename {jolly => legacy/jolly}/incl/jolly/object/HlcStandardObject.hpp (100%) rename {jolly => legacy/jolly}/incl/jolly/render/graph/RenderStage.hpp (100%) rename {jolly => legacy/jolly}/incl/jolly/render/graph/RenderStageConfig.hpp (100%) rename {jolly => legacy/jolly}/incl/jolly/render/graph/dag.hpp (100%) rename {jolly => legacy/jolly}/incl/jolly/render/graph/renderer.hpp (100%) rename {jolly => legacy/jolly}/incl/jolly/render/model/CthVertex.hpp (100%) rename {jolly => legacy/jolly}/incl/jolly/render/model/HlcModel.hpp (100%) rename {jolly => legacy/jolly}/incl/jolly/render/model/HlcModelManager.hpp (100%) rename {jolly => legacy/jolly}/incl/jolly/render/render_pipeline.hpp (100%) rename {jolly => legacy/jolly}/incl/jolly/render/submit/cmd/primary_cmd_buffer.hpp (100%) rename {jolly => legacy/jolly}/incl/jolly/render/submit/cmd/secondary_cmd_buffer.hpp (100%) rename {jolly => legacy/jolly}/incl/jolly/render/submit/cmd/secondary_cmd_buffer_config.hpp (100%) rename {jolly => legacy/jolly}/incl/jolly/render/submit/cmd/utility/task_container.hpp (100%) rename {jolly => legacy/jolly}/incl/jolly/render/submit/cmd/utility/task_container_mixin.hpp (100%) rename {jolly => legacy/jolly}/incl/jolly/render/submit/cmd/utility/thread_safe_task_container.hpp (100%) rename {jolly => legacy/jolly}/incl/jolly/render/submit/queue.hpp (100%) rename {jolly => legacy/jolly}/incl/jolly/render/submit/queue_properties.hpp (100%) rename {jolly => legacy/jolly}/incl/jolly/render/submit/submit_info.hpp (100%) rename {jolly => legacy/jolly}/incl/jolly/render/sync/RenderPulse.hpp (100%) rename {jolly => legacy/jolly}/incl/jolly/render/sync/fence.hpp (100%) rename {jolly => legacy/jolly}/incl/jolly/render/sync/pipeline_stage_flags.hpp (100%) rename {jolly => legacy/jolly}/incl/jolly/render/sync/pipeline_wait_stage.hpp (100%) rename {jolly => legacy/jolly}/incl/jolly/user/HlcCamera.hpp (100%) rename {jolly => legacy/jolly}/incl/jolly/user/HlcInputController.hpp (100%) rename {jolly => legacy/jolly}/incl/jolly/user/HlcUser.hpp (100%) rename {jolly => legacy/jolly}/incl/jolly/utility/CthOSWindow.hpp (100%) rename {jolly => legacy/jolly}/incl/jolly/utility/GraphicsCore.hpp (100%) rename {jolly => legacy/jolly}/incl/jolly/utility/GraphicsSyncConfig.hpp (100%) rename {jolly => legacy/jolly}/incl/jolly/utility/graphics_core_config.hpp (100%) rename {jolly => legacy/jolly}/incl/jolly/utility/jvk_conversion.hpp (100%) rename {jolly => legacy/jolly}/incl/jolly/utility/os/os_def.hpp (100%) rename {jolly => legacy/jolly}/incl/jolly/utility/types.hpp (100%) rename {jolly => legacy/jolly}/src/core/core.cpp (100%) rename {jolly => legacy/jolly}/src/core/queue.cpp (100%) rename {jolly => legacy/jolly}/src/core/submit_info.cpp (100%) rename {jolly => legacy/jolly}/src/data/model/primitive/vertex_buffer.cpp (100%) rename {jolly => legacy/jolly}/src/data/staging_allocator.cpp (100%) rename {jolly => legacy/jolly}/src/object/HlcStandardObject.cpp (100%) rename {jolly => legacy/jolly}/src/pch/glob/boost.hpp (100%) rename {jolly => legacy/jolly}/src/pch/glob/cth.hpp (100%) rename {jolly => legacy/jolly}/src/pch/glob/glm.hpp (100%) rename {jolly => legacy/jolly}/src/pch/glob/std.hpp (100%) rename {jolly => legacy/jolly}/src/pch/pch.cpp (100%) rename {jolly => legacy/jolly}/src/pch/pch.hpp (100%) rename {jolly => legacy/jolly}/src/render/graph/RenderStage.cpp (100%) rename {jolly => legacy/jolly}/src/render/graph/renderer.cpp (100%) rename {jolly => legacy/jolly}/src/render/model/HlcModel.cpp (100%) rename {jolly => legacy/jolly}/src/render/model/HlcModelManager.cpp (100%) rename {jolly => legacy/jolly}/src/render/submit/cmd/primary_cmd_buffer.cpp (100%) rename {jolly => legacy/jolly}/src/render/submit/cmd/secondary_cmd_buffer.cpp (100%) rename {jolly => legacy/jolly}/src/render/sync/fence.cpp (100%) rename {jolly => legacy/jolly}/src/render/sync/pipeline_stage_flags_helpers.hpp (100%) rename {jolly => legacy/jolly}/src/render/sync/pipeline_wait_stage_helpers.hpp (100%) rename {jolly => legacy/jolly}/src/user/HlcCamera.cpp (100%) rename {jolly => legacy/jolly}/src/user/HlcInputController.cpp (100%) rename {jolly => legacy/jolly}/src/utility/CthGraphicsCore.cpp (100%) rename {jolly => legacy/jolly}/src/utility/CthGraphicsSyncConfig.cpp (100%) rename {jolly => legacy/jolly}/src/utility/CthOSWindow.cpp (100%) rename {jolly => legacy/jolly}/src/utility/vk_convert.hpp (100%) rename {jvk => legacy/jvk}/CMakeLists.txt (100%) rename {jvk => legacy/jvk}/incl/jvk/base/core.hpp (100%) rename {jvk => legacy/jvk}/incl/jvk/base/core_config.hpp (100%) rename {jvk => legacy/jvk}/incl/jvk/base/destruction_queue.hpp (100%) rename {jvk => legacy/jvk}/incl/jvk/base/destruction_queue_config.hpp (100%) rename {jvk => legacy/jvk}/incl/jvk/base/device.hpp (100%) rename {jvk => legacy/jvk}/incl/jvk/base/device_config.hpp (100%) rename {jvk => legacy/jvk}/incl/jvk/base/device_table.hpp (100%) rename {jvk => legacy/jvk}/incl/jvk/base/instance.hpp (100%) rename {jvk => legacy/jvk}/incl/jvk/base/jvk.hpp (100%) rename {jvk => legacy/jvk}/incl/jvk/base/physical_device.hpp (100%) rename {jvk => legacy/jvk}/incl/jvk/base/queue/present_info.hpp (100%) rename {jvk => legacy/jvk}/incl/jvk/base/queue/queue.hpp (100%) rename {jvk => legacy/jvk}/incl/jvk/base/queue/queue_family.hpp (100%) rename {jvk => legacy/jvk}/incl/jvk/base/queue/queue_state.hpp (100%) rename {jvk => legacy/jvk}/incl/jvk/base/queue/submit_info.hpp (100%) rename {jvk => legacy/jvk}/incl/jvk/data/buffer/base_buffer.hpp (100%) rename {jvk => legacy/jvk}/incl/jvk/data/buffer/buffer.hpp (100%) rename {jvk => legacy/jvk}/incl/jvk/data/buffer/inl/buffer.inl (100%) rename {jvk => legacy/jvk}/incl/jvk/data/descriptor/descriptor.hpp (100%) rename {jvk => legacy/jvk}/incl/jvk/data/descriptor/descriptor_pool.hpp (100%) rename {jvk => legacy/jvk}/incl/jvk/data/descriptor/descriptor_set.hpp (100%) rename {jvk => legacy/jvk}/incl/jvk/data/descriptor/types/buffer_descriptors.hpp (100%) rename {jvk => legacy/jvk}/incl/jvk/data/descriptor/types/image_descriptors.hpp (100%) rename {jvk => legacy/jvk}/incl/jvk/data/img/image.hpp (100%) rename {jvk => legacy/jvk}/incl/jvk/data/img/image_config.hpp (100%) rename {jvk => legacy/jvk}/incl/jvk/data/img/image_view.hpp (100%) rename {jvk => legacy/jvk}/incl/jvk/data/img/texture/sampler.hpp (100%) rename {jvk => legacy/jvk}/incl/jvk/data/img/texture/texture.hpp (100%) rename {jvk => legacy/jvk}/incl/jvk/data/memory/memory.hpp (100%) rename {jvk => legacy/jvk}/incl/jvk/debug/debug_messenger.hpp (100%) rename {jvk => legacy/jvk}/incl/jvk/render/cmd/cmd_buffer.hpp (100%) rename {jvk => legacy/jvk}/incl/jvk/render/cmd/cmd_buffer_config.hpp (100%) rename {jvk => legacy/jvk}/incl/jvk/render/cmd/cmd_pool.hpp (100%) rename {jvk => legacy/jvk}/incl/jvk/render/pass/attachment/attachment_collection.hpp (100%) rename {jvk => legacy/jvk}/incl/jvk/render/pass/attachment/attachment_collection_config.hpp (100%) rename {jvk => legacy/jvk}/incl/jvk/render/pass/attachment/attachment_description.hpp (100%) rename {jvk => legacy/jvk}/incl/jvk/render/pass/framebuffer/fb_collection.hpp (100%) rename {jvk => legacy/jvk}/incl/jvk/render/pass/framebuffer/fb_collection_config.hpp (100%) rename {jvk => legacy/jvk}/incl/jvk/render/pass/framebuffer/framebuffer.hpp (100%) rename {jvk => legacy/jvk}/incl/jvk/render/pass/framebuffer/swapchain_fb_collection.hpp (100%) rename {jvk => legacy/jvk}/incl/jvk/render/pass/render_pass.hpp (100%) rename {jvk => legacy/jvk}/incl/jvk/render/pass/render_pass_begin_config.hpp (100%) rename {jvk => legacy/jvk}/incl/jvk/render/pass/render_pass_config.hpp (100%) rename {jvk => legacy/jvk}/incl/jvk/render/pass/subpass.hpp (100%) rename {jvk => legacy/jvk}/incl/jvk/render/pipeline/layout/descriptor_set_layout.hpp (100%) rename {jvk => legacy/jvk}/incl/jvk/render/pipeline/layout/pipeline_layout.hpp (100%) rename {jvk => legacy/jvk}/incl/jvk/render/pipeline/pipeline.hpp (100%) rename {jvk => legacy/jvk}/incl/jvk/render/pipeline/pipeline_vertex_description.hpp (100%) rename {jvk => legacy/jvk}/incl/jvk/render/pipeline/shader/shader.hpp (100%) rename {jvk => legacy/jvk}/incl/jvk/render/sync/fence.hpp (100%) rename {jvk => legacy/jvk}/incl/jvk/render/sync/os_fence_handle.hpp (100%) rename {jvk => legacy/jvk}/incl/jvk/render/sync/pipeline_barrier.hpp (100%) rename {jvk => legacy/jvk}/incl/jvk/render/sync/pipeline_wait_stage.hpp (100%) rename {jvk => legacy/jvk}/incl/jvk/render/sync/semaphore.hpp (100%) rename {jvk => legacy/jvk}/incl/jvk/render/sync/timline_semaphore.hpp (100%) rename {jvk => legacy/jvk}/incl/jvk/surface/surface.hpp (100%) rename {jvk => legacy/jvk}/incl/jvk/surface/surface_config.hpp (100%) rename {jvk => legacy/jvk}/incl/jvk/surface/swapchain/swapchain.hpp (100%) rename {jvk => legacy/jvk}/incl/jvk/surface/swapchain/swapchain_config.hpp (100%) rename {jvk => legacy/jvk}/incl/jvk/surface/swapchain/swapchain_subpass_config.hpp (100%) rename {jvk => legacy/jvk}/incl/jvk/utility/constant/debug_constants.hpp (100%) rename {jvk => legacy/jvk}/incl/jvk/utility/constant/device_constants.hpp (100%) rename {jvk => legacy/jvk}/incl/jvk/utility/constants.hpp (100%) rename {jvk => legacy/jvk}/incl/jvk/utility/device/physical_device_features.hpp (100%) rename {jvk => legacy/jvk}/incl/jvk/utility/format.hpp (100%) rename {jvk => legacy/jvk}/incl/jvk/utility/format/format_string.hpp (100%) rename {jvk => legacy/jvk}/incl/jvk/utility/format/vk_to_string.hpp (100%) rename {jvk => legacy/jvk}/incl/jvk/utility/format/vk_to_string.inl (100%) rename {jvk => legacy/jvk}/incl/jvk/utility/os/os_constants.hpp (100%) rename {jvk => legacy/jvk}/incl/jvk/utility/os/os_def.hpp (100%) rename {jvk => legacy/jvk}/incl/jvk/utility/os/os_exception.hpp (100%) rename {jvk => legacy/jvk}/incl/jvk/utility/os/os_helper_include.hpp (100%) rename {jvk => legacy/jvk}/incl/jvk/utility/os/os_window.hpp (100%) rename {jvk => legacy/jvk}/incl/jvk/utility/types.hpp (100%) rename {jvk => legacy/jvk}/incl/jvk/utility/vk_exceptions.hpp (100%) rename {jvk => legacy/jvk}/incl/jvk/utility/vk_hash.hpp (100%) rename {jvk => legacy/jvk}/incl/jvk/utility/vk_overloads.hpp (100%) rename {jvk => legacy/jvk}/src/base/core.cpp (100%) rename {jvk => legacy/jvk}/src/base/device.cpp (100%) rename {jvk => legacy/jvk}/src/base/instance.cpp (100%) rename {jvk => legacy/jvk}/src/base/jvk.cpp (100%) rename {jvk => legacy/jvk}/src/base/physical_device.cpp (100%) rename {jvk => legacy/jvk}/src/base/queue/present_info.cpp (100%) rename {jvk => legacy/jvk}/src/base/queue/queue.cpp (100%) rename {jvk => legacy/jvk}/src/base/queue/submit_info.cpp (100%) rename {jvk => legacy/jvk}/src/data/buffer/base_buffer.cpp (100%) rename {jvk => legacy/jvk}/src/data/descriptor/descriptor_pool.cpp (100%) rename {jvk => legacy/jvk}/src/data/descriptor/descriptor_set.cpp (100%) rename {jvk => legacy/jvk}/src/data/destruction_queue.cpp (100%) rename {jvk => legacy/jvk}/src/data/img/image.cpp (100%) rename {jvk => legacy/jvk}/src/data/img/image_config.cpp (100%) rename {jvk => legacy/jvk}/src/data/img/image_view.cpp (100%) rename {jvk => legacy/jvk}/src/data/img/texture/sampler.cpp (100%) rename {jvk => legacy/jvk}/src/data/img/texture/texture.cpp (100%) rename {jvk => legacy/jvk}/src/data/memory/memory.cpp (100%) rename {jvk => legacy/jvk}/src/debug/debug_messenger.cpp (100%) rename {jvk => legacy/jvk}/src/pch/glob/boost.hpp (100%) rename {jvk => legacy/jvk}/src/pch/glob/cth.hpp (100%) rename {jvk => legacy/jvk}/src/pch/glob/glm.hpp (100%) rename {jvk => legacy/jvk}/src/pch/glob/gsl.hpp (100%) rename {jvk => legacy/jvk}/src/pch/glob/std.hpp (100%) rename {jvk => legacy/jvk}/src/pch/glob/volk.hpp (100%) rename {jvk => legacy/jvk}/src/pch/jvk/pch_def.hpp (100%) rename {jvk => legacy/jvk}/src/pch/lib/volk/volk.cpp (100%) rename {jvk => legacy/jvk}/src/pch/pch.cpp (100%) rename {jvk => legacy/jvk}/src/pch/pch.hpp (100%) rename {jvk => legacy/jvk}/src/render/cmd/cmd_buffer.cpp (100%) rename {jvk => legacy/jvk}/src/render/cmd/cmd_pool.cpp (100%) rename {jvk => legacy/jvk}/src/render/pass/attachment/attachment_collection.cpp (100%) rename {jvk => legacy/jvk}/src/render/pass/attachment/attachment_description.cpp (100%) rename {jvk => legacy/jvk}/src/render/pass/framebuffer/fb_collection.cpp (100%) rename {jvk => legacy/jvk}/src/render/pass/framebuffer/fb_collection_config.cpp (100%) rename {jvk => legacy/jvk}/src/render/pass/framebuffer/framebuffer.cpp (100%) rename {jvk => legacy/jvk}/src/render/pass/framebuffer/swapchain_fb_collection.cpp (100%) rename {jvk => legacy/jvk}/src/render/pass/render_pass.cpp (100%) rename {jvk => legacy/jvk}/src/render/pass/subpass.cpp (100%) rename {jvk => legacy/jvk}/src/render/pipeline/layout/descriptor_set_layout.cpp (100%) rename {jvk => legacy/jvk}/src/render/pipeline/layout/pipeline_layout.cpp (100%) rename {jvk => legacy/jvk}/src/render/pipeline/pipeline.cpp (100%) rename {jvk => legacy/jvk}/src/render/pipeline/shader/shader.cpp (100%) rename {jvk => legacy/jvk}/src/render/sync/fence.cpp (100%) rename {jvk => legacy/jvk}/src/render/sync/os_fence_handle.cpp (100%) rename {jvk => legacy/jvk}/src/render/sync/pipeline_barrier.cpp (100%) rename {jvk => legacy/jvk}/src/render/sync/semaphore.cpp (100%) rename {jvk => legacy/jvk}/src/render/sync/timline_semaphore.cpp (100%) rename {jvk => legacy/jvk}/src/surface/surface.cpp (100%) rename {jvk => legacy/jvk}/src/surface/swapchain/swapchain.cpp (100%) rename {jvk => legacy/jvk}/src/surface/swapchain/swapchain_subpass_config.cpp (100%) rename {jvk => legacy/jvk}/src/utility/device/physical_device_features.cpp (100%) rename {jvk => legacy/jvk}/src/utility/os/incl_os.hpp (100%) rename {jvk => legacy/jvk}/src/utility/os/incl_posix.hpp (100%) rename {jvk => legacy/jvk}/src/utility/os/os_window.cpp (100%) rename {jvk => legacy/jvk}/src/utility/os/win/incl_windows.hpp (100%) rename {jvk => legacy/jvk}/src/utility/os/win/win_window.hpp (100%) create mode 100644 legacy/tests/CMakeLists.txt rename {tests => legacy/tests}/src/test_utils/pch.cpp (100%) rename {tests => legacy/tests}/src/test_utils/pch.h (100%) rename {tests => legacy/tests}/src/vulkan/utility/format/format_string_test.cpp (100%) create mode 100644 tests/src/loader_test.cpp diff --git a/.gitignore b/.gitignore index 62bb99dd..f6f178e0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ vcpkg_installed/ out/ -.vs/ \ No newline at end of file +.vs/ +CMakeUserPresets.json diff --git a/CMakeLists.txt b/CMakeLists.txt index deede4de..42c8134e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -9,14 +9,10 @@ cth_set_compiler_specifics() cth_set_latest_c_cpp_standard() set(CMAKE_CXX_SCAN_FOR_MODULES OFF) - -# Add subprojects -add_subdirectory(jvk) -add_subdirectory(jolly) -add_subdirectory(demo) - # Tests -enable_testing() +if(JOLLY_ENABLE_TESTS) + enable_testing() -add_subdirectory(tests) + add_subdirectory(tests) +endif() diff --git a/CMakePresets.json b/CMakePresets.json index faac3861..1d288bcf 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -2,40 +2,117 @@ "version": 3, "configurePresets": [ { - "name": "cth_cmake", + "name": "base", "hidden": true, - "toolchainFile": "${sourceDir}/lib/cth_cmake/toolchain.cmake" + "cacheVariables": { + "CMAKE_TOOLCHAIN_FILE": "${sourceDir}/lib/cth_cmake/toolchain.cmake", + "cth_ROOT": "${sourceDir}/../cth/out/install/${presetName}" + } }, { - "name": "windows-msvc-base", + "name": "add_tests", "hidden": true, - "inherits": "cth_cmake", - "generator": "Ninja", - "binaryDir": "${sourceDir}/out/build/${presetName}", "cacheVariables": { - "CMAKE_CXX_COMPILER": "cl.exe" - }, - "condition": { - "type": "equals", - "lhs": "${hostSystemName}", - "rhs": "Windows" + "JOLLY_ENABLE_TESTS": true } }, { - "name": "win-msvc-debug", - "inherits": "windows-msvc-base", + "name": "debug-base", "hidden": true, + "inherits": [ + "base", + "add_tests" + ], "cacheVariables": { "CMAKE_BUILD_TYPE": "Debug" } }, { - "name": "win-msvc-release", - "inherits": "windows-msvc-base", + "name": "release-base", "hidden": true, + "inherits": [ + "base", + "add_tests" + ], "cacheVariables": { "CMAKE_BUILD_TYPE": "Release" } + }, + { + "name": "ninja-base", + "hidden": true, + "generator": "Ninja", + "binaryDir": "${sourceDir}/out/build/${presetName}", + "installDir": "${sourceDir}/out/install/${presetName}" + }, + { + "name": "msvc-base", + "hidden": true, + "inherits": "ninja-base", + "cacheVariables": { + "CMAKE_C_COMPILER": "cl", + "CMAKE_CXX_COMPILER": "cl" + } + }, + { + "name": "clang-base", + "hidden": true, + "inherits": "ninja-base", + "cacheVariables": { + "CMAKE_C_COMPILER": "clang", + "CMAKE_CXX_COMPILER": "clang++" + } + }, + { + "name": "gcc-base", + "hidden": true, + "inherits": "ninja-base", + "cacheVariables": { + "CMAKE_C_COMPILER": "gcc", + "CMAKE_CXX_COMPILER": "g++" + } + }, + { + "name": "msvc_debug", + "inherits": [ + "msvc-base", + "debug-base" + ] + }, + { + "name": "msvc_release", + "inherits": [ + "msvc-base", + "release-base" + ] + }, + { + "name": "clang_debug", + "inherits": [ + "clang-base", + "debug-base" + ] + }, + { + "name": "clang_release", + "inherits": [ + "clang-base", + "release-base" + ] + }, + { + "name": "gcc_debug", + "inherits": [ + "gcc-base", + "debug-base" + ] + }, + { + "name": "gcc_release", + "inherits": [ + "gcc-base", + "release-base" + ] } ] } diff --git a/CMakeUserPresets.json b/CMakeUserPresets.json deleted file mode 100644 index b36c5a94..00000000 --- a/CMakeUserPresets.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "version": 3, - "configurePresets": [ - { - "name": "win-debug-settings", - "hidden": true, - "cacheVariables": { - "cth_ROOT": "${sourceDir}/../cth/out/install/msvc_debug" - } - }, - { - "name": "win-release-settings", - "hidden": true, - "cacheVariables": { - "cth_ROOT": "${sourceDir}/../cth/out/install/msvc_release" - } - }, - { - "name": "win_msvc_debug", - "inherits": [ - "win-msvc-debug", - "win-debug-settings" - ] - }, - { - "name": "win_msvc_release", - "inherits": [ - "win-msvc-release", - "win-release-settings" - ] - } - ] -} \ No newline at end of file diff --git a/demo/CMakeLists.txt b/legacy/demo/CMakeLists.txt similarity index 100% rename from demo/CMakeLists.txt rename to legacy/demo/CMakeLists.txt diff --git a/demo/assets/bin/shader/shader.frag.spv b/legacy/demo/assets/bin/shader/shader.frag.spv similarity index 100% rename from demo/assets/bin/shader/shader.frag.spv rename to legacy/demo/assets/bin/shader/shader.frag.spv diff --git a/demo/assets/bin/shader/shader.vert.spv b/legacy/demo/assets/bin/shader/shader.vert.spv similarity index 100% rename from demo/assets/bin/shader/shader.vert.spv rename to legacy/demo/assets/bin/shader/shader.vert.spv diff --git a/demo/assets/img/texture/first_texture.png b/legacy/demo/assets/img/texture/first_texture.png similarity index 100% rename from demo/assets/img/texture/first_texture.png rename to legacy/demo/assets/img/texture/first_texture.png diff --git a/demo/demo_shaders/CMakeLists.txt b/legacy/demo/demo_shaders/CMakeLists.txt similarity index 100% rename from demo/demo_shaders/CMakeLists.txt rename to legacy/demo/demo_shaders/CMakeLists.txt diff --git a/demo/demo_shaders/src/shader.frag b/legacy/demo/demo_shaders/src/shader.frag similarity index 100% rename from demo/demo_shaders/src/shader.frag rename to legacy/demo/demo_shaders/src/shader.frag diff --git a/demo/demo_shaders/src/shader.vert b/legacy/demo/demo_shaders/src/shader.vert similarity index 100% rename from demo/demo_shaders/src/shader.vert rename to legacy/demo/demo_shaders/src/shader.vert diff --git a/demo/demo_shaders/src/test.bat b/legacy/demo/demo_shaders/src/test.bat similarity index 100% rename from demo/demo_shaders/src/test.bat rename to legacy/demo/demo_shaders/src/test.bat diff --git a/demo/src/HlcApp.cpp b/legacy/demo/src/HlcApp.cpp similarity index 100% rename from demo/src/HlcApp.cpp rename to legacy/demo/src/HlcApp.cpp diff --git a/demo/src/HlcApp.hpp b/legacy/demo/src/HlcApp.hpp similarity index 100% rename from demo/src/HlcApp.hpp rename to legacy/demo/src/HlcApp.hpp diff --git a/demo/src/demo_main.cpp b/legacy/demo/src/demo_main.cpp similarity index 100% rename from demo/src/demo_main.cpp rename to legacy/demo/src/demo_main.cpp diff --git a/demo/src/pch/glob/boost.hpp b/legacy/demo/src/pch/glob/boost.hpp similarity index 100% rename from demo/src/pch/glob/boost.hpp rename to legacy/demo/src/pch/glob/boost.hpp diff --git a/demo/src/pch/glob/cth.hpp b/legacy/demo/src/pch/glob/cth.hpp similarity index 100% rename from demo/src/pch/glob/cth.hpp rename to legacy/demo/src/pch/glob/cth.hpp diff --git a/demo/src/pch/glob/glm.hpp b/legacy/demo/src/pch/glob/glm.hpp similarity index 100% rename from demo/src/pch/glob/glm.hpp rename to legacy/demo/src/pch/glob/glm.hpp diff --git a/demo/src/pch/glob/std.hpp b/legacy/demo/src/pch/glob/std.hpp similarity index 100% rename from demo/src/pch/glob/std.hpp rename to legacy/demo/src/pch/glob/std.hpp diff --git a/demo/src/pch/glob/volk.hpp b/legacy/demo/src/pch/glob/volk.hpp similarity index 100% rename from demo/src/pch/glob/volk.hpp rename to legacy/demo/src/pch/glob/volk.hpp diff --git a/demo/src/pch/pch.cpp b/legacy/demo/src/pch/pch.cpp similarity index 100% rename from demo/src/pch/pch.cpp rename to legacy/demo/src/pch/pch.cpp diff --git a/demo/src/pch/pch.hpp b/legacy/demo/src/pch/pch.hpp similarity index 100% rename from demo/src/pch/pch.hpp rename to legacy/demo/src/pch/pch.hpp diff --git a/demo/src/render/FrameResources.cpp b/legacy/demo/src/render/FrameResources.cpp similarity index 100% rename from demo/src/render/FrameResources.cpp rename to legacy/demo/src/render/FrameResources.cpp diff --git a/demo/src/render/FrameResources.hpp b/legacy/demo/src/render/FrameResources.hpp similarity index 100% rename from demo/src/render/FrameResources.hpp rename to legacy/demo/src/render/FrameResources.hpp diff --git a/demo/src/render/HlcFrameInfo.hpp b/legacy/demo/src/render/HlcFrameInfo.hpp similarity index 100% rename from demo/src/render/HlcFrameInfo.hpp rename to legacy/demo/src/render/HlcFrameInfo.hpp diff --git a/demo/src/render/HlcRenderSystem.cpp b/legacy/demo/src/render/HlcRenderSystem.cpp similarity index 100% rename from demo/src/render/HlcRenderSystem.cpp rename to legacy/demo/src/render/HlcRenderSystem.cpp diff --git a/demo/src/render/HlcRenderSystem.hpp b/legacy/demo/src/render/HlcRenderSystem.hpp similarity index 100% rename from demo/src/render/HlcRenderSystem.hpp rename to legacy/demo/src/render/HlcRenderSystem.hpp diff --git a/demo/src/render/frame_resources_config.hpp b/legacy/demo/src/render/frame_resources_config.hpp similarity index 100% rename from demo/src/render/frame_resources_config.hpp rename to legacy/demo/src/render/frame_resources_config.hpp diff --git a/demo/src/render/push_constant.hpp b/legacy/demo/src/render/push_constant.hpp similarity index 100% rename from demo/src/render/push_constant.hpp rename to legacy/demo/src/render/push_constant.hpp diff --git a/jolly/CMakeLists.txt b/legacy/jolly/CMakeLists.txt similarity index 100% rename from jolly/CMakeLists.txt rename to legacy/jolly/CMakeLists.txt diff --git a/jolly/doc/roadmap.md b/legacy/jolly/doc/roadmap.md similarity index 100% rename from jolly/doc/roadmap.md rename to legacy/jolly/doc/roadmap.md diff --git a/jolly/doc/scene.md b/legacy/jolly/doc/scene.md similarity index 100% rename from jolly/doc/scene.md rename to legacy/jolly/doc/scene.md diff --git a/jolly/doc/tinygltf.md b/legacy/jolly/doc/tinygltf.md similarity index 100% rename from jolly/doc/tinygltf.md rename to legacy/jolly/doc/tinygltf.md diff --git a/jolly/incl/jolly/core/core.hpp b/legacy/jolly/incl/jolly/core/core.hpp similarity index 100% rename from jolly/incl/jolly/core/core.hpp rename to legacy/jolly/incl/jolly/core/core.hpp diff --git a/jolly/incl/jolly/core/core_config.hpp b/legacy/jolly/incl/jolly/core/core_config.hpp similarity index 100% rename from jolly/incl/jolly/core/core_config.hpp rename to legacy/jolly/incl/jolly/core/core_config.hpp diff --git a/jolly/incl/jolly/data/model/primitive/loader/gltf_utility.hpp b/legacy/jolly/incl/jolly/data/model/primitive/loader/gltf_utility.hpp similarity index 100% rename from jolly/incl/jolly/data/model/primitive/loader/gltf_utility.hpp rename to legacy/jolly/incl/jolly/data/model/primitive/loader/gltf_utility.hpp diff --git a/jolly/incl/jolly/data/model/primitive/vertex_buffer.hpp b/legacy/jolly/incl/jolly/data/model/primitive/vertex_buffer.hpp similarity index 100% rename from jolly/incl/jolly/data/model/primitive/vertex_buffer.hpp rename to legacy/jolly/incl/jolly/data/model/primitive/vertex_buffer.hpp diff --git a/jolly/incl/jolly/data/model/primitive/vertex_key.hpp b/legacy/jolly/incl/jolly/data/model/primitive/vertex_key.hpp similarity index 100% rename from jolly/incl/jolly/data/model/primitive/vertex_key.hpp rename to legacy/jolly/incl/jolly/data/model/primitive/vertex_key.hpp diff --git a/jolly/incl/jolly/data/staging_allocator.hpp b/legacy/jolly/incl/jolly/data/staging_allocator.hpp similarity index 100% rename from jolly/incl/jolly/data/staging_allocator.hpp rename to legacy/jolly/incl/jolly/data/staging_allocator.hpp diff --git a/jolly/incl/jolly/object/HlcRenderObject.hpp b/legacy/jolly/incl/jolly/object/HlcRenderObject.hpp similarity index 100% rename from jolly/incl/jolly/object/HlcRenderObject.hpp rename to legacy/jolly/incl/jolly/object/HlcRenderObject.hpp diff --git a/jolly/incl/jolly/object/HlcStandardObject.hpp b/legacy/jolly/incl/jolly/object/HlcStandardObject.hpp similarity index 100% rename from jolly/incl/jolly/object/HlcStandardObject.hpp rename to legacy/jolly/incl/jolly/object/HlcStandardObject.hpp diff --git a/jolly/incl/jolly/render/graph/RenderStage.hpp b/legacy/jolly/incl/jolly/render/graph/RenderStage.hpp similarity index 100% rename from jolly/incl/jolly/render/graph/RenderStage.hpp rename to legacy/jolly/incl/jolly/render/graph/RenderStage.hpp diff --git a/jolly/incl/jolly/render/graph/RenderStageConfig.hpp b/legacy/jolly/incl/jolly/render/graph/RenderStageConfig.hpp similarity index 100% rename from jolly/incl/jolly/render/graph/RenderStageConfig.hpp rename to legacy/jolly/incl/jolly/render/graph/RenderStageConfig.hpp diff --git a/jolly/incl/jolly/render/graph/dag.hpp b/legacy/jolly/incl/jolly/render/graph/dag.hpp similarity index 100% rename from jolly/incl/jolly/render/graph/dag.hpp rename to legacy/jolly/incl/jolly/render/graph/dag.hpp diff --git a/jolly/incl/jolly/render/graph/renderer.hpp b/legacy/jolly/incl/jolly/render/graph/renderer.hpp similarity index 100% rename from jolly/incl/jolly/render/graph/renderer.hpp rename to legacy/jolly/incl/jolly/render/graph/renderer.hpp diff --git a/jolly/incl/jolly/render/model/CthVertex.hpp b/legacy/jolly/incl/jolly/render/model/CthVertex.hpp similarity index 100% rename from jolly/incl/jolly/render/model/CthVertex.hpp rename to legacy/jolly/incl/jolly/render/model/CthVertex.hpp diff --git a/jolly/incl/jolly/render/model/HlcModel.hpp b/legacy/jolly/incl/jolly/render/model/HlcModel.hpp similarity index 100% rename from jolly/incl/jolly/render/model/HlcModel.hpp rename to legacy/jolly/incl/jolly/render/model/HlcModel.hpp diff --git a/jolly/incl/jolly/render/model/HlcModelManager.hpp b/legacy/jolly/incl/jolly/render/model/HlcModelManager.hpp similarity index 100% rename from jolly/incl/jolly/render/model/HlcModelManager.hpp rename to legacy/jolly/incl/jolly/render/model/HlcModelManager.hpp diff --git a/jolly/incl/jolly/render/render_pipeline.hpp b/legacy/jolly/incl/jolly/render/render_pipeline.hpp similarity index 100% rename from jolly/incl/jolly/render/render_pipeline.hpp rename to legacy/jolly/incl/jolly/render/render_pipeline.hpp diff --git a/jolly/incl/jolly/render/submit/cmd/primary_cmd_buffer.hpp b/legacy/jolly/incl/jolly/render/submit/cmd/primary_cmd_buffer.hpp similarity index 100% rename from jolly/incl/jolly/render/submit/cmd/primary_cmd_buffer.hpp rename to legacy/jolly/incl/jolly/render/submit/cmd/primary_cmd_buffer.hpp diff --git a/jolly/incl/jolly/render/submit/cmd/secondary_cmd_buffer.hpp b/legacy/jolly/incl/jolly/render/submit/cmd/secondary_cmd_buffer.hpp similarity index 100% rename from jolly/incl/jolly/render/submit/cmd/secondary_cmd_buffer.hpp rename to legacy/jolly/incl/jolly/render/submit/cmd/secondary_cmd_buffer.hpp diff --git a/jolly/incl/jolly/render/submit/cmd/secondary_cmd_buffer_config.hpp b/legacy/jolly/incl/jolly/render/submit/cmd/secondary_cmd_buffer_config.hpp similarity index 100% rename from jolly/incl/jolly/render/submit/cmd/secondary_cmd_buffer_config.hpp rename to legacy/jolly/incl/jolly/render/submit/cmd/secondary_cmd_buffer_config.hpp diff --git a/jolly/incl/jolly/render/submit/cmd/utility/task_container.hpp b/legacy/jolly/incl/jolly/render/submit/cmd/utility/task_container.hpp similarity index 100% rename from jolly/incl/jolly/render/submit/cmd/utility/task_container.hpp rename to legacy/jolly/incl/jolly/render/submit/cmd/utility/task_container.hpp diff --git a/jolly/incl/jolly/render/submit/cmd/utility/task_container_mixin.hpp b/legacy/jolly/incl/jolly/render/submit/cmd/utility/task_container_mixin.hpp similarity index 100% rename from jolly/incl/jolly/render/submit/cmd/utility/task_container_mixin.hpp rename to legacy/jolly/incl/jolly/render/submit/cmd/utility/task_container_mixin.hpp diff --git a/jolly/incl/jolly/render/submit/cmd/utility/thread_safe_task_container.hpp b/legacy/jolly/incl/jolly/render/submit/cmd/utility/thread_safe_task_container.hpp similarity index 100% rename from jolly/incl/jolly/render/submit/cmd/utility/thread_safe_task_container.hpp rename to legacy/jolly/incl/jolly/render/submit/cmd/utility/thread_safe_task_container.hpp diff --git a/jolly/incl/jolly/render/submit/queue.hpp b/legacy/jolly/incl/jolly/render/submit/queue.hpp similarity index 100% rename from jolly/incl/jolly/render/submit/queue.hpp rename to legacy/jolly/incl/jolly/render/submit/queue.hpp diff --git a/jolly/incl/jolly/render/submit/queue_properties.hpp b/legacy/jolly/incl/jolly/render/submit/queue_properties.hpp similarity index 100% rename from jolly/incl/jolly/render/submit/queue_properties.hpp rename to legacy/jolly/incl/jolly/render/submit/queue_properties.hpp diff --git a/jolly/incl/jolly/render/submit/submit_info.hpp b/legacy/jolly/incl/jolly/render/submit/submit_info.hpp similarity index 100% rename from jolly/incl/jolly/render/submit/submit_info.hpp rename to legacy/jolly/incl/jolly/render/submit/submit_info.hpp diff --git a/jolly/incl/jolly/render/sync/RenderPulse.hpp b/legacy/jolly/incl/jolly/render/sync/RenderPulse.hpp similarity index 100% rename from jolly/incl/jolly/render/sync/RenderPulse.hpp rename to legacy/jolly/incl/jolly/render/sync/RenderPulse.hpp diff --git a/jolly/incl/jolly/render/sync/fence.hpp b/legacy/jolly/incl/jolly/render/sync/fence.hpp similarity index 100% rename from jolly/incl/jolly/render/sync/fence.hpp rename to legacy/jolly/incl/jolly/render/sync/fence.hpp diff --git a/jolly/incl/jolly/render/sync/pipeline_stage_flags.hpp b/legacy/jolly/incl/jolly/render/sync/pipeline_stage_flags.hpp similarity index 100% rename from jolly/incl/jolly/render/sync/pipeline_stage_flags.hpp rename to legacy/jolly/incl/jolly/render/sync/pipeline_stage_flags.hpp diff --git a/jolly/incl/jolly/render/sync/pipeline_wait_stage.hpp b/legacy/jolly/incl/jolly/render/sync/pipeline_wait_stage.hpp similarity index 100% rename from jolly/incl/jolly/render/sync/pipeline_wait_stage.hpp rename to legacy/jolly/incl/jolly/render/sync/pipeline_wait_stage.hpp diff --git a/jolly/incl/jolly/user/HlcCamera.hpp b/legacy/jolly/incl/jolly/user/HlcCamera.hpp similarity index 100% rename from jolly/incl/jolly/user/HlcCamera.hpp rename to legacy/jolly/incl/jolly/user/HlcCamera.hpp diff --git a/jolly/incl/jolly/user/HlcInputController.hpp b/legacy/jolly/incl/jolly/user/HlcInputController.hpp similarity index 100% rename from jolly/incl/jolly/user/HlcInputController.hpp rename to legacy/jolly/incl/jolly/user/HlcInputController.hpp diff --git a/jolly/incl/jolly/user/HlcUser.hpp b/legacy/jolly/incl/jolly/user/HlcUser.hpp similarity index 100% rename from jolly/incl/jolly/user/HlcUser.hpp rename to legacy/jolly/incl/jolly/user/HlcUser.hpp diff --git a/jolly/incl/jolly/utility/CthOSWindow.hpp b/legacy/jolly/incl/jolly/utility/CthOSWindow.hpp similarity index 100% rename from jolly/incl/jolly/utility/CthOSWindow.hpp rename to legacy/jolly/incl/jolly/utility/CthOSWindow.hpp diff --git a/jolly/incl/jolly/utility/GraphicsCore.hpp b/legacy/jolly/incl/jolly/utility/GraphicsCore.hpp similarity index 100% rename from jolly/incl/jolly/utility/GraphicsCore.hpp rename to legacy/jolly/incl/jolly/utility/GraphicsCore.hpp diff --git a/jolly/incl/jolly/utility/GraphicsSyncConfig.hpp b/legacy/jolly/incl/jolly/utility/GraphicsSyncConfig.hpp similarity index 100% rename from jolly/incl/jolly/utility/GraphicsSyncConfig.hpp rename to legacy/jolly/incl/jolly/utility/GraphicsSyncConfig.hpp diff --git a/jolly/incl/jolly/utility/graphics_core_config.hpp b/legacy/jolly/incl/jolly/utility/graphics_core_config.hpp similarity index 100% rename from jolly/incl/jolly/utility/graphics_core_config.hpp rename to legacy/jolly/incl/jolly/utility/graphics_core_config.hpp diff --git a/jolly/incl/jolly/utility/jvk_conversion.hpp b/legacy/jolly/incl/jolly/utility/jvk_conversion.hpp similarity index 100% rename from jolly/incl/jolly/utility/jvk_conversion.hpp rename to legacy/jolly/incl/jolly/utility/jvk_conversion.hpp diff --git a/jolly/incl/jolly/utility/os/os_def.hpp b/legacy/jolly/incl/jolly/utility/os/os_def.hpp similarity index 100% rename from jolly/incl/jolly/utility/os/os_def.hpp rename to legacy/jolly/incl/jolly/utility/os/os_def.hpp diff --git a/jolly/incl/jolly/utility/types.hpp b/legacy/jolly/incl/jolly/utility/types.hpp similarity index 100% rename from jolly/incl/jolly/utility/types.hpp rename to legacy/jolly/incl/jolly/utility/types.hpp diff --git a/jolly/src/core/core.cpp b/legacy/jolly/src/core/core.cpp similarity index 100% rename from jolly/src/core/core.cpp rename to legacy/jolly/src/core/core.cpp diff --git a/jolly/src/core/queue.cpp b/legacy/jolly/src/core/queue.cpp similarity index 100% rename from jolly/src/core/queue.cpp rename to legacy/jolly/src/core/queue.cpp diff --git a/jolly/src/core/submit_info.cpp b/legacy/jolly/src/core/submit_info.cpp similarity index 100% rename from jolly/src/core/submit_info.cpp rename to legacy/jolly/src/core/submit_info.cpp diff --git a/jolly/src/data/model/primitive/vertex_buffer.cpp b/legacy/jolly/src/data/model/primitive/vertex_buffer.cpp similarity index 100% rename from jolly/src/data/model/primitive/vertex_buffer.cpp rename to legacy/jolly/src/data/model/primitive/vertex_buffer.cpp diff --git a/jolly/src/data/staging_allocator.cpp b/legacy/jolly/src/data/staging_allocator.cpp similarity index 100% rename from jolly/src/data/staging_allocator.cpp rename to legacy/jolly/src/data/staging_allocator.cpp diff --git a/jolly/src/object/HlcStandardObject.cpp b/legacy/jolly/src/object/HlcStandardObject.cpp similarity index 100% rename from jolly/src/object/HlcStandardObject.cpp rename to legacy/jolly/src/object/HlcStandardObject.cpp diff --git a/jolly/src/pch/glob/boost.hpp b/legacy/jolly/src/pch/glob/boost.hpp similarity index 100% rename from jolly/src/pch/glob/boost.hpp rename to legacy/jolly/src/pch/glob/boost.hpp diff --git a/jolly/src/pch/glob/cth.hpp b/legacy/jolly/src/pch/glob/cth.hpp similarity index 100% rename from jolly/src/pch/glob/cth.hpp rename to legacy/jolly/src/pch/glob/cth.hpp diff --git a/jolly/src/pch/glob/glm.hpp b/legacy/jolly/src/pch/glob/glm.hpp similarity index 100% rename from jolly/src/pch/glob/glm.hpp rename to legacy/jolly/src/pch/glob/glm.hpp diff --git a/jolly/src/pch/glob/std.hpp b/legacy/jolly/src/pch/glob/std.hpp similarity index 100% rename from jolly/src/pch/glob/std.hpp rename to legacy/jolly/src/pch/glob/std.hpp diff --git a/jolly/src/pch/pch.cpp b/legacy/jolly/src/pch/pch.cpp similarity index 100% rename from jolly/src/pch/pch.cpp rename to legacy/jolly/src/pch/pch.cpp diff --git a/jolly/src/pch/pch.hpp b/legacy/jolly/src/pch/pch.hpp similarity index 100% rename from jolly/src/pch/pch.hpp rename to legacy/jolly/src/pch/pch.hpp diff --git a/jolly/src/render/graph/RenderStage.cpp b/legacy/jolly/src/render/graph/RenderStage.cpp similarity index 100% rename from jolly/src/render/graph/RenderStage.cpp rename to legacy/jolly/src/render/graph/RenderStage.cpp diff --git a/jolly/src/render/graph/renderer.cpp b/legacy/jolly/src/render/graph/renderer.cpp similarity index 100% rename from jolly/src/render/graph/renderer.cpp rename to legacy/jolly/src/render/graph/renderer.cpp diff --git a/jolly/src/render/model/HlcModel.cpp b/legacy/jolly/src/render/model/HlcModel.cpp similarity index 100% rename from jolly/src/render/model/HlcModel.cpp rename to legacy/jolly/src/render/model/HlcModel.cpp diff --git a/jolly/src/render/model/HlcModelManager.cpp b/legacy/jolly/src/render/model/HlcModelManager.cpp similarity index 100% rename from jolly/src/render/model/HlcModelManager.cpp rename to legacy/jolly/src/render/model/HlcModelManager.cpp diff --git a/jolly/src/render/submit/cmd/primary_cmd_buffer.cpp b/legacy/jolly/src/render/submit/cmd/primary_cmd_buffer.cpp similarity index 100% rename from jolly/src/render/submit/cmd/primary_cmd_buffer.cpp rename to legacy/jolly/src/render/submit/cmd/primary_cmd_buffer.cpp diff --git a/jolly/src/render/submit/cmd/secondary_cmd_buffer.cpp b/legacy/jolly/src/render/submit/cmd/secondary_cmd_buffer.cpp similarity index 100% rename from jolly/src/render/submit/cmd/secondary_cmd_buffer.cpp rename to legacy/jolly/src/render/submit/cmd/secondary_cmd_buffer.cpp diff --git a/jolly/src/render/sync/fence.cpp b/legacy/jolly/src/render/sync/fence.cpp similarity index 100% rename from jolly/src/render/sync/fence.cpp rename to legacy/jolly/src/render/sync/fence.cpp diff --git a/jolly/src/render/sync/pipeline_stage_flags_helpers.hpp b/legacy/jolly/src/render/sync/pipeline_stage_flags_helpers.hpp similarity index 100% rename from jolly/src/render/sync/pipeline_stage_flags_helpers.hpp rename to legacy/jolly/src/render/sync/pipeline_stage_flags_helpers.hpp diff --git a/jolly/src/render/sync/pipeline_wait_stage_helpers.hpp b/legacy/jolly/src/render/sync/pipeline_wait_stage_helpers.hpp similarity index 100% rename from jolly/src/render/sync/pipeline_wait_stage_helpers.hpp rename to legacy/jolly/src/render/sync/pipeline_wait_stage_helpers.hpp diff --git a/jolly/src/user/HlcCamera.cpp b/legacy/jolly/src/user/HlcCamera.cpp similarity index 100% rename from jolly/src/user/HlcCamera.cpp rename to legacy/jolly/src/user/HlcCamera.cpp diff --git a/jolly/src/user/HlcInputController.cpp b/legacy/jolly/src/user/HlcInputController.cpp similarity index 100% rename from jolly/src/user/HlcInputController.cpp rename to legacy/jolly/src/user/HlcInputController.cpp diff --git a/jolly/src/utility/CthGraphicsCore.cpp b/legacy/jolly/src/utility/CthGraphicsCore.cpp similarity index 100% rename from jolly/src/utility/CthGraphicsCore.cpp rename to legacy/jolly/src/utility/CthGraphicsCore.cpp diff --git a/jolly/src/utility/CthGraphicsSyncConfig.cpp b/legacy/jolly/src/utility/CthGraphicsSyncConfig.cpp similarity index 100% rename from jolly/src/utility/CthGraphicsSyncConfig.cpp rename to legacy/jolly/src/utility/CthGraphicsSyncConfig.cpp diff --git a/jolly/src/utility/CthOSWindow.cpp b/legacy/jolly/src/utility/CthOSWindow.cpp similarity index 100% rename from jolly/src/utility/CthOSWindow.cpp rename to legacy/jolly/src/utility/CthOSWindow.cpp diff --git a/jolly/src/utility/vk_convert.hpp b/legacy/jolly/src/utility/vk_convert.hpp similarity index 100% rename from jolly/src/utility/vk_convert.hpp rename to legacy/jolly/src/utility/vk_convert.hpp diff --git a/jvk/CMakeLists.txt b/legacy/jvk/CMakeLists.txt similarity index 100% rename from jvk/CMakeLists.txt rename to legacy/jvk/CMakeLists.txt diff --git a/jvk/incl/jvk/base/core.hpp b/legacy/jvk/incl/jvk/base/core.hpp similarity index 100% rename from jvk/incl/jvk/base/core.hpp rename to legacy/jvk/incl/jvk/base/core.hpp diff --git a/jvk/incl/jvk/base/core_config.hpp b/legacy/jvk/incl/jvk/base/core_config.hpp similarity index 100% rename from jvk/incl/jvk/base/core_config.hpp rename to legacy/jvk/incl/jvk/base/core_config.hpp diff --git a/jvk/incl/jvk/base/destruction_queue.hpp b/legacy/jvk/incl/jvk/base/destruction_queue.hpp similarity index 100% rename from jvk/incl/jvk/base/destruction_queue.hpp rename to legacy/jvk/incl/jvk/base/destruction_queue.hpp diff --git a/jvk/incl/jvk/base/destruction_queue_config.hpp b/legacy/jvk/incl/jvk/base/destruction_queue_config.hpp similarity index 100% rename from jvk/incl/jvk/base/destruction_queue_config.hpp rename to legacy/jvk/incl/jvk/base/destruction_queue_config.hpp diff --git a/jvk/incl/jvk/base/device.hpp b/legacy/jvk/incl/jvk/base/device.hpp similarity index 100% rename from jvk/incl/jvk/base/device.hpp rename to legacy/jvk/incl/jvk/base/device.hpp diff --git a/jvk/incl/jvk/base/device_config.hpp b/legacy/jvk/incl/jvk/base/device_config.hpp similarity index 100% rename from jvk/incl/jvk/base/device_config.hpp rename to legacy/jvk/incl/jvk/base/device_config.hpp diff --git a/jvk/incl/jvk/base/device_table.hpp b/legacy/jvk/incl/jvk/base/device_table.hpp similarity index 100% rename from jvk/incl/jvk/base/device_table.hpp rename to legacy/jvk/incl/jvk/base/device_table.hpp diff --git a/jvk/incl/jvk/base/instance.hpp b/legacy/jvk/incl/jvk/base/instance.hpp similarity index 100% rename from jvk/incl/jvk/base/instance.hpp rename to legacy/jvk/incl/jvk/base/instance.hpp diff --git a/jvk/incl/jvk/base/jvk.hpp b/legacy/jvk/incl/jvk/base/jvk.hpp similarity index 100% rename from jvk/incl/jvk/base/jvk.hpp rename to legacy/jvk/incl/jvk/base/jvk.hpp diff --git a/jvk/incl/jvk/base/physical_device.hpp b/legacy/jvk/incl/jvk/base/physical_device.hpp similarity index 100% rename from jvk/incl/jvk/base/physical_device.hpp rename to legacy/jvk/incl/jvk/base/physical_device.hpp diff --git a/jvk/incl/jvk/base/queue/present_info.hpp b/legacy/jvk/incl/jvk/base/queue/present_info.hpp similarity index 100% rename from jvk/incl/jvk/base/queue/present_info.hpp rename to legacy/jvk/incl/jvk/base/queue/present_info.hpp diff --git a/jvk/incl/jvk/base/queue/queue.hpp b/legacy/jvk/incl/jvk/base/queue/queue.hpp similarity index 100% rename from jvk/incl/jvk/base/queue/queue.hpp rename to legacy/jvk/incl/jvk/base/queue/queue.hpp diff --git a/jvk/incl/jvk/base/queue/queue_family.hpp b/legacy/jvk/incl/jvk/base/queue/queue_family.hpp similarity index 100% rename from jvk/incl/jvk/base/queue/queue_family.hpp rename to legacy/jvk/incl/jvk/base/queue/queue_family.hpp diff --git a/jvk/incl/jvk/base/queue/queue_state.hpp b/legacy/jvk/incl/jvk/base/queue/queue_state.hpp similarity index 100% rename from jvk/incl/jvk/base/queue/queue_state.hpp rename to legacy/jvk/incl/jvk/base/queue/queue_state.hpp diff --git a/jvk/incl/jvk/base/queue/submit_info.hpp b/legacy/jvk/incl/jvk/base/queue/submit_info.hpp similarity index 100% rename from jvk/incl/jvk/base/queue/submit_info.hpp rename to legacy/jvk/incl/jvk/base/queue/submit_info.hpp diff --git a/jvk/incl/jvk/data/buffer/base_buffer.hpp b/legacy/jvk/incl/jvk/data/buffer/base_buffer.hpp similarity index 100% rename from jvk/incl/jvk/data/buffer/base_buffer.hpp rename to legacy/jvk/incl/jvk/data/buffer/base_buffer.hpp diff --git a/jvk/incl/jvk/data/buffer/buffer.hpp b/legacy/jvk/incl/jvk/data/buffer/buffer.hpp similarity index 100% rename from jvk/incl/jvk/data/buffer/buffer.hpp rename to legacy/jvk/incl/jvk/data/buffer/buffer.hpp diff --git a/jvk/incl/jvk/data/buffer/inl/buffer.inl b/legacy/jvk/incl/jvk/data/buffer/inl/buffer.inl similarity index 100% rename from jvk/incl/jvk/data/buffer/inl/buffer.inl rename to legacy/jvk/incl/jvk/data/buffer/inl/buffer.inl diff --git a/jvk/incl/jvk/data/descriptor/descriptor.hpp b/legacy/jvk/incl/jvk/data/descriptor/descriptor.hpp similarity index 100% rename from jvk/incl/jvk/data/descriptor/descriptor.hpp rename to legacy/jvk/incl/jvk/data/descriptor/descriptor.hpp diff --git a/jvk/incl/jvk/data/descriptor/descriptor_pool.hpp b/legacy/jvk/incl/jvk/data/descriptor/descriptor_pool.hpp similarity index 100% rename from jvk/incl/jvk/data/descriptor/descriptor_pool.hpp rename to legacy/jvk/incl/jvk/data/descriptor/descriptor_pool.hpp diff --git a/jvk/incl/jvk/data/descriptor/descriptor_set.hpp b/legacy/jvk/incl/jvk/data/descriptor/descriptor_set.hpp similarity index 100% rename from jvk/incl/jvk/data/descriptor/descriptor_set.hpp rename to legacy/jvk/incl/jvk/data/descriptor/descriptor_set.hpp diff --git a/jvk/incl/jvk/data/descriptor/types/buffer_descriptors.hpp b/legacy/jvk/incl/jvk/data/descriptor/types/buffer_descriptors.hpp similarity index 100% rename from jvk/incl/jvk/data/descriptor/types/buffer_descriptors.hpp rename to legacy/jvk/incl/jvk/data/descriptor/types/buffer_descriptors.hpp diff --git a/jvk/incl/jvk/data/descriptor/types/image_descriptors.hpp b/legacy/jvk/incl/jvk/data/descriptor/types/image_descriptors.hpp similarity index 100% rename from jvk/incl/jvk/data/descriptor/types/image_descriptors.hpp rename to legacy/jvk/incl/jvk/data/descriptor/types/image_descriptors.hpp diff --git a/jvk/incl/jvk/data/img/image.hpp b/legacy/jvk/incl/jvk/data/img/image.hpp similarity index 100% rename from jvk/incl/jvk/data/img/image.hpp rename to legacy/jvk/incl/jvk/data/img/image.hpp diff --git a/jvk/incl/jvk/data/img/image_config.hpp b/legacy/jvk/incl/jvk/data/img/image_config.hpp similarity index 100% rename from jvk/incl/jvk/data/img/image_config.hpp rename to legacy/jvk/incl/jvk/data/img/image_config.hpp diff --git a/jvk/incl/jvk/data/img/image_view.hpp b/legacy/jvk/incl/jvk/data/img/image_view.hpp similarity index 100% rename from jvk/incl/jvk/data/img/image_view.hpp rename to legacy/jvk/incl/jvk/data/img/image_view.hpp diff --git a/jvk/incl/jvk/data/img/texture/sampler.hpp b/legacy/jvk/incl/jvk/data/img/texture/sampler.hpp similarity index 100% rename from jvk/incl/jvk/data/img/texture/sampler.hpp rename to legacy/jvk/incl/jvk/data/img/texture/sampler.hpp diff --git a/jvk/incl/jvk/data/img/texture/texture.hpp b/legacy/jvk/incl/jvk/data/img/texture/texture.hpp similarity index 100% rename from jvk/incl/jvk/data/img/texture/texture.hpp rename to legacy/jvk/incl/jvk/data/img/texture/texture.hpp diff --git a/jvk/incl/jvk/data/memory/memory.hpp b/legacy/jvk/incl/jvk/data/memory/memory.hpp similarity index 100% rename from jvk/incl/jvk/data/memory/memory.hpp rename to legacy/jvk/incl/jvk/data/memory/memory.hpp diff --git a/jvk/incl/jvk/debug/debug_messenger.hpp b/legacy/jvk/incl/jvk/debug/debug_messenger.hpp similarity index 100% rename from jvk/incl/jvk/debug/debug_messenger.hpp rename to legacy/jvk/incl/jvk/debug/debug_messenger.hpp diff --git a/jvk/incl/jvk/render/cmd/cmd_buffer.hpp b/legacy/jvk/incl/jvk/render/cmd/cmd_buffer.hpp similarity index 100% rename from jvk/incl/jvk/render/cmd/cmd_buffer.hpp rename to legacy/jvk/incl/jvk/render/cmd/cmd_buffer.hpp diff --git a/jvk/incl/jvk/render/cmd/cmd_buffer_config.hpp b/legacy/jvk/incl/jvk/render/cmd/cmd_buffer_config.hpp similarity index 100% rename from jvk/incl/jvk/render/cmd/cmd_buffer_config.hpp rename to legacy/jvk/incl/jvk/render/cmd/cmd_buffer_config.hpp diff --git a/jvk/incl/jvk/render/cmd/cmd_pool.hpp b/legacy/jvk/incl/jvk/render/cmd/cmd_pool.hpp similarity index 100% rename from jvk/incl/jvk/render/cmd/cmd_pool.hpp rename to legacy/jvk/incl/jvk/render/cmd/cmd_pool.hpp diff --git a/jvk/incl/jvk/render/pass/attachment/attachment_collection.hpp b/legacy/jvk/incl/jvk/render/pass/attachment/attachment_collection.hpp similarity index 100% rename from jvk/incl/jvk/render/pass/attachment/attachment_collection.hpp rename to legacy/jvk/incl/jvk/render/pass/attachment/attachment_collection.hpp diff --git a/jvk/incl/jvk/render/pass/attachment/attachment_collection_config.hpp b/legacy/jvk/incl/jvk/render/pass/attachment/attachment_collection_config.hpp similarity index 100% rename from jvk/incl/jvk/render/pass/attachment/attachment_collection_config.hpp rename to legacy/jvk/incl/jvk/render/pass/attachment/attachment_collection_config.hpp diff --git a/jvk/incl/jvk/render/pass/attachment/attachment_description.hpp b/legacy/jvk/incl/jvk/render/pass/attachment/attachment_description.hpp similarity index 100% rename from jvk/incl/jvk/render/pass/attachment/attachment_description.hpp rename to legacy/jvk/incl/jvk/render/pass/attachment/attachment_description.hpp diff --git a/jvk/incl/jvk/render/pass/framebuffer/fb_collection.hpp b/legacy/jvk/incl/jvk/render/pass/framebuffer/fb_collection.hpp similarity index 100% rename from jvk/incl/jvk/render/pass/framebuffer/fb_collection.hpp rename to legacy/jvk/incl/jvk/render/pass/framebuffer/fb_collection.hpp diff --git a/jvk/incl/jvk/render/pass/framebuffer/fb_collection_config.hpp b/legacy/jvk/incl/jvk/render/pass/framebuffer/fb_collection_config.hpp similarity index 100% rename from jvk/incl/jvk/render/pass/framebuffer/fb_collection_config.hpp rename to legacy/jvk/incl/jvk/render/pass/framebuffer/fb_collection_config.hpp diff --git a/jvk/incl/jvk/render/pass/framebuffer/framebuffer.hpp b/legacy/jvk/incl/jvk/render/pass/framebuffer/framebuffer.hpp similarity index 100% rename from jvk/incl/jvk/render/pass/framebuffer/framebuffer.hpp rename to legacy/jvk/incl/jvk/render/pass/framebuffer/framebuffer.hpp diff --git a/jvk/incl/jvk/render/pass/framebuffer/swapchain_fb_collection.hpp b/legacy/jvk/incl/jvk/render/pass/framebuffer/swapchain_fb_collection.hpp similarity index 100% rename from jvk/incl/jvk/render/pass/framebuffer/swapchain_fb_collection.hpp rename to legacy/jvk/incl/jvk/render/pass/framebuffer/swapchain_fb_collection.hpp diff --git a/jvk/incl/jvk/render/pass/render_pass.hpp b/legacy/jvk/incl/jvk/render/pass/render_pass.hpp similarity index 100% rename from jvk/incl/jvk/render/pass/render_pass.hpp rename to legacy/jvk/incl/jvk/render/pass/render_pass.hpp diff --git a/jvk/incl/jvk/render/pass/render_pass_begin_config.hpp b/legacy/jvk/incl/jvk/render/pass/render_pass_begin_config.hpp similarity index 100% rename from jvk/incl/jvk/render/pass/render_pass_begin_config.hpp rename to legacy/jvk/incl/jvk/render/pass/render_pass_begin_config.hpp diff --git a/jvk/incl/jvk/render/pass/render_pass_config.hpp b/legacy/jvk/incl/jvk/render/pass/render_pass_config.hpp similarity index 100% rename from jvk/incl/jvk/render/pass/render_pass_config.hpp rename to legacy/jvk/incl/jvk/render/pass/render_pass_config.hpp diff --git a/jvk/incl/jvk/render/pass/subpass.hpp b/legacy/jvk/incl/jvk/render/pass/subpass.hpp similarity index 100% rename from jvk/incl/jvk/render/pass/subpass.hpp rename to legacy/jvk/incl/jvk/render/pass/subpass.hpp diff --git a/jvk/incl/jvk/render/pipeline/layout/descriptor_set_layout.hpp b/legacy/jvk/incl/jvk/render/pipeline/layout/descriptor_set_layout.hpp similarity index 100% rename from jvk/incl/jvk/render/pipeline/layout/descriptor_set_layout.hpp rename to legacy/jvk/incl/jvk/render/pipeline/layout/descriptor_set_layout.hpp diff --git a/jvk/incl/jvk/render/pipeline/layout/pipeline_layout.hpp b/legacy/jvk/incl/jvk/render/pipeline/layout/pipeline_layout.hpp similarity index 100% rename from jvk/incl/jvk/render/pipeline/layout/pipeline_layout.hpp rename to legacy/jvk/incl/jvk/render/pipeline/layout/pipeline_layout.hpp diff --git a/jvk/incl/jvk/render/pipeline/pipeline.hpp b/legacy/jvk/incl/jvk/render/pipeline/pipeline.hpp similarity index 100% rename from jvk/incl/jvk/render/pipeline/pipeline.hpp rename to legacy/jvk/incl/jvk/render/pipeline/pipeline.hpp diff --git a/jvk/incl/jvk/render/pipeline/pipeline_vertex_description.hpp b/legacy/jvk/incl/jvk/render/pipeline/pipeline_vertex_description.hpp similarity index 100% rename from jvk/incl/jvk/render/pipeline/pipeline_vertex_description.hpp rename to legacy/jvk/incl/jvk/render/pipeline/pipeline_vertex_description.hpp diff --git a/jvk/incl/jvk/render/pipeline/shader/shader.hpp b/legacy/jvk/incl/jvk/render/pipeline/shader/shader.hpp similarity index 100% rename from jvk/incl/jvk/render/pipeline/shader/shader.hpp rename to legacy/jvk/incl/jvk/render/pipeline/shader/shader.hpp diff --git a/jvk/incl/jvk/render/sync/fence.hpp b/legacy/jvk/incl/jvk/render/sync/fence.hpp similarity index 100% rename from jvk/incl/jvk/render/sync/fence.hpp rename to legacy/jvk/incl/jvk/render/sync/fence.hpp diff --git a/jvk/incl/jvk/render/sync/os_fence_handle.hpp b/legacy/jvk/incl/jvk/render/sync/os_fence_handle.hpp similarity index 100% rename from jvk/incl/jvk/render/sync/os_fence_handle.hpp rename to legacy/jvk/incl/jvk/render/sync/os_fence_handle.hpp diff --git a/jvk/incl/jvk/render/sync/pipeline_barrier.hpp b/legacy/jvk/incl/jvk/render/sync/pipeline_barrier.hpp similarity index 100% rename from jvk/incl/jvk/render/sync/pipeline_barrier.hpp rename to legacy/jvk/incl/jvk/render/sync/pipeline_barrier.hpp diff --git a/jvk/incl/jvk/render/sync/pipeline_wait_stage.hpp b/legacy/jvk/incl/jvk/render/sync/pipeline_wait_stage.hpp similarity index 100% rename from jvk/incl/jvk/render/sync/pipeline_wait_stage.hpp rename to legacy/jvk/incl/jvk/render/sync/pipeline_wait_stage.hpp diff --git a/jvk/incl/jvk/render/sync/semaphore.hpp b/legacy/jvk/incl/jvk/render/sync/semaphore.hpp similarity index 100% rename from jvk/incl/jvk/render/sync/semaphore.hpp rename to legacy/jvk/incl/jvk/render/sync/semaphore.hpp diff --git a/jvk/incl/jvk/render/sync/timline_semaphore.hpp b/legacy/jvk/incl/jvk/render/sync/timline_semaphore.hpp similarity index 100% rename from jvk/incl/jvk/render/sync/timline_semaphore.hpp rename to legacy/jvk/incl/jvk/render/sync/timline_semaphore.hpp diff --git a/jvk/incl/jvk/surface/surface.hpp b/legacy/jvk/incl/jvk/surface/surface.hpp similarity index 100% rename from jvk/incl/jvk/surface/surface.hpp rename to legacy/jvk/incl/jvk/surface/surface.hpp diff --git a/jvk/incl/jvk/surface/surface_config.hpp b/legacy/jvk/incl/jvk/surface/surface_config.hpp similarity index 100% rename from jvk/incl/jvk/surface/surface_config.hpp rename to legacy/jvk/incl/jvk/surface/surface_config.hpp diff --git a/jvk/incl/jvk/surface/swapchain/swapchain.hpp b/legacy/jvk/incl/jvk/surface/swapchain/swapchain.hpp similarity index 100% rename from jvk/incl/jvk/surface/swapchain/swapchain.hpp rename to legacy/jvk/incl/jvk/surface/swapchain/swapchain.hpp diff --git a/jvk/incl/jvk/surface/swapchain/swapchain_config.hpp b/legacy/jvk/incl/jvk/surface/swapchain/swapchain_config.hpp similarity index 100% rename from jvk/incl/jvk/surface/swapchain/swapchain_config.hpp rename to legacy/jvk/incl/jvk/surface/swapchain/swapchain_config.hpp diff --git a/jvk/incl/jvk/surface/swapchain/swapchain_subpass_config.hpp b/legacy/jvk/incl/jvk/surface/swapchain/swapchain_subpass_config.hpp similarity index 100% rename from jvk/incl/jvk/surface/swapchain/swapchain_subpass_config.hpp rename to legacy/jvk/incl/jvk/surface/swapchain/swapchain_subpass_config.hpp diff --git a/jvk/incl/jvk/utility/constant/debug_constants.hpp b/legacy/jvk/incl/jvk/utility/constant/debug_constants.hpp similarity index 100% rename from jvk/incl/jvk/utility/constant/debug_constants.hpp rename to legacy/jvk/incl/jvk/utility/constant/debug_constants.hpp diff --git a/jvk/incl/jvk/utility/constant/device_constants.hpp b/legacy/jvk/incl/jvk/utility/constant/device_constants.hpp similarity index 100% rename from jvk/incl/jvk/utility/constant/device_constants.hpp rename to legacy/jvk/incl/jvk/utility/constant/device_constants.hpp diff --git a/jvk/incl/jvk/utility/constants.hpp b/legacy/jvk/incl/jvk/utility/constants.hpp similarity index 100% rename from jvk/incl/jvk/utility/constants.hpp rename to legacy/jvk/incl/jvk/utility/constants.hpp diff --git a/jvk/incl/jvk/utility/device/physical_device_features.hpp b/legacy/jvk/incl/jvk/utility/device/physical_device_features.hpp similarity index 100% rename from jvk/incl/jvk/utility/device/physical_device_features.hpp rename to legacy/jvk/incl/jvk/utility/device/physical_device_features.hpp diff --git a/jvk/incl/jvk/utility/format.hpp b/legacy/jvk/incl/jvk/utility/format.hpp similarity index 100% rename from jvk/incl/jvk/utility/format.hpp rename to legacy/jvk/incl/jvk/utility/format.hpp diff --git a/jvk/incl/jvk/utility/format/format_string.hpp b/legacy/jvk/incl/jvk/utility/format/format_string.hpp similarity index 100% rename from jvk/incl/jvk/utility/format/format_string.hpp rename to legacy/jvk/incl/jvk/utility/format/format_string.hpp diff --git a/jvk/incl/jvk/utility/format/vk_to_string.hpp b/legacy/jvk/incl/jvk/utility/format/vk_to_string.hpp similarity index 100% rename from jvk/incl/jvk/utility/format/vk_to_string.hpp rename to legacy/jvk/incl/jvk/utility/format/vk_to_string.hpp diff --git a/jvk/incl/jvk/utility/format/vk_to_string.inl b/legacy/jvk/incl/jvk/utility/format/vk_to_string.inl similarity index 100% rename from jvk/incl/jvk/utility/format/vk_to_string.inl rename to legacy/jvk/incl/jvk/utility/format/vk_to_string.inl diff --git a/jvk/incl/jvk/utility/os/os_constants.hpp b/legacy/jvk/incl/jvk/utility/os/os_constants.hpp similarity index 100% rename from jvk/incl/jvk/utility/os/os_constants.hpp rename to legacy/jvk/incl/jvk/utility/os/os_constants.hpp diff --git a/jvk/incl/jvk/utility/os/os_def.hpp b/legacy/jvk/incl/jvk/utility/os/os_def.hpp similarity index 100% rename from jvk/incl/jvk/utility/os/os_def.hpp rename to legacy/jvk/incl/jvk/utility/os/os_def.hpp diff --git a/jvk/incl/jvk/utility/os/os_exception.hpp b/legacy/jvk/incl/jvk/utility/os/os_exception.hpp similarity index 100% rename from jvk/incl/jvk/utility/os/os_exception.hpp rename to legacy/jvk/incl/jvk/utility/os/os_exception.hpp diff --git a/jvk/incl/jvk/utility/os/os_helper_include.hpp b/legacy/jvk/incl/jvk/utility/os/os_helper_include.hpp similarity index 100% rename from jvk/incl/jvk/utility/os/os_helper_include.hpp rename to legacy/jvk/incl/jvk/utility/os/os_helper_include.hpp diff --git a/jvk/incl/jvk/utility/os/os_window.hpp b/legacy/jvk/incl/jvk/utility/os/os_window.hpp similarity index 100% rename from jvk/incl/jvk/utility/os/os_window.hpp rename to legacy/jvk/incl/jvk/utility/os/os_window.hpp diff --git a/jvk/incl/jvk/utility/types.hpp b/legacy/jvk/incl/jvk/utility/types.hpp similarity index 100% rename from jvk/incl/jvk/utility/types.hpp rename to legacy/jvk/incl/jvk/utility/types.hpp diff --git a/jvk/incl/jvk/utility/vk_exceptions.hpp b/legacy/jvk/incl/jvk/utility/vk_exceptions.hpp similarity index 100% rename from jvk/incl/jvk/utility/vk_exceptions.hpp rename to legacy/jvk/incl/jvk/utility/vk_exceptions.hpp diff --git a/jvk/incl/jvk/utility/vk_hash.hpp b/legacy/jvk/incl/jvk/utility/vk_hash.hpp similarity index 100% rename from jvk/incl/jvk/utility/vk_hash.hpp rename to legacy/jvk/incl/jvk/utility/vk_hash.hpp diff --git a/jvk/incl/jvk/utility/vk_overloads.hpp b/legacy/jvk/incl/jvk/utility/vk_overloads.hpp similarity index 100% rename from jvk/incl/jvk/utility/vk_overloads.hpp rename to legacy/jvk/incl/jvk/utility/vk_overloads.hpp diff --git a/jvk/src/base/core.cpp b/legacy/jvk/src/base/core.cpp similarity index 100% rename from jvk/src/base/core.cpp rename to legacy/jvk/src/base/core.cpp diff --git a/jvk/src/base/device.cpp b/legacy/jvk/src/base/device.cpp similarity index 100% rename from jvk/src/base/device.cpp rename to legacy/jvk/src/base/device.cpp diff --git a/jvk/src/base/instance.cpp b/legacy/jvk/src/base/instance.cpp similarity index 100% rename from jvk/src/base/instance.cpp rename to legacy/jvk/src/base/instance.cpp diff --git a/jvk/src/base/jvk.cpp b/legacy/jvk/src/base/jvk.cpp similarity index 100% rename from jvk/src/base/jvk.cpp rename to legacy/jvk/src/base/jvk.cpp diff --git a/jvk/src/base/physical_device.cpp b/legacy/jvk/src/base/physical_device.cpp similarity index 100% rename from jvk/src/base/physical_device.cpp rename to legacy/jvk/src/base/physical_device.cpp diff --git a/jvk/src/base/queue/present_info.cpp b/legacy/jvk/src/base/queue/present_info.cpp similarity index 100% rename from jvk/src/base/queue/present_info.cpp rename to legacy/jvk/src/base/queue/present_info.cpp diff --git a/jvk/src/base/queue/queue.cpp b/legacy/jvk/src/base/queue/queue.cpp similarity index 100% rename from jvk/src/base/queue/queue.cpp rename to legacy/jvk/src/base/queue/queue.cpp diff --git a/jvk/src/base/queue/submit_info.cpp b/legacy/jvk/src/base/queue/submit_info.cpp similarity index 100% rename from jvk/src/base/queue/submit_info.cpp rename to legacy/jvk/src/base/queue/submit_info.cpp diff --git a/jvk/src/data/buffer/base_buffer.cpp b/legacy/jvk/src/data/buffer/base_buffer.cpp similarity index 100% rename from jvk/src/data/buffer/base_buffer.cpp rename to legacy/jvk/src/data/buffer/base_buffer.cpp diff --git a/jvk/src/data/descriptor/descriptor_pool.cpp b/legacy/jvk/src/data/descriptor/descriptor_pool.cpp similarity index 100% rename from jvk/src/data/descriptor/descriptor_pool.cpp rename to legacy/jvk/src/data/descriptor/descriptor_pool.cpp diff --git a/jvk/src/data/descriptor/descriptor_set.cpp b/legacy/jvk/src/data/descriptor/descriptor_set.cpp similarity index 100% rename from jvk/src/data/descriptor/descriptor_set.cpp rename to legacy/jvk/src/data/descriptor/descriptor_set.cpp diff --git a/jvk/src/data/destruction_queue.cpp b/legacy/jvk/src/data/destruction_queue.cpp similarity index 100% rename from jvk/src/data/destruction_queue.cpp rename to legacy/jvk/src/data/destruction_queue.cpp diff --git a/jvk/src/data/img/image.cpp b/legacy/jvk/src/data/img/image.cpp similarity index 100% rename from jvk/src/data/img/image.cpp rename to legacy/jvk/src/data/img/image.cpp diff --git a/jvk/src/data/img/image_config.cpp b/legacy/jvk/src/data/img/image_config.cpp similarity index 100% rename from jvk/src/data/img/image_config.cpp rename to legacy/jvk/src/data/img/image_config.cpp diff --git a/jvk/src/data/img/image_view.cpp b/legacy/jvk/src/data/img/image_view.cpp similarity index 100% rename from jvk/src/data/img/image_view.cpp rename to legacy/jvk/src/data/img/image_view.cpp diff --git a/jvk/src/data/img/texture/sampler.cpp b/legacy/jvk/src/data/img/texture/sampler.cpp similarity index 100% rename from jvk/src/data/img/texture/sampler.cpp rename to legacy/jvk/src/data/img/texture/sampler.cpp diff --git a/jvk/src/data/img/texture/texture.cpp b/legacy/jvk/src/data/img/texture/texture.cpp similarity index 100% rename from jvk/src/data/img/texture/texture.cpp rename to legacy/jvk/src/data/img/texture/texture.cpp diff --git a/jvk/src/data/memory/memory.cpp b/legacy/jvk/src/data/memory/memory.cpp similarity index 100% rename from jvk/src/data/memory/memory.cpp rename to legacy/jvk/src/data/memory/memory.cpp diff --git a/jvk/src/debug/debug_messenger.cpp b/legacy/jvk/src/debug/debug_messenger.cpp similarity index 100% rename from jvk/src/debug/debug_messenger.cpp rename to legacy/jvk/src/debug/debug_messenger.cpp diff --git a/jvk/src/pch/glob/boost.hpp b/legacy/jvk/src/pch/glob/boost.hpp similarity index 100% rename from jvk/src/pch/glob/boost.hpp rename to legacy/jvk/src/pch/glob/boost.hpp diff --git a/jvk/src/pch/glob/cth.hpp b/legacy/jvk/src/pch/glob/cth.hpp similarity index 100% rename from jvk/src/pch/glob/cth.hpp rename to legacy/jvk/src/pch/glob/cth.hpp diff --git a/jvk/src/pch/glob/glm.hpp b/legacy/jvk/src/pch/glob/glm.hpp similarity index 100% rename from jvk/src/pch/glob/glm.hpp rename to legacy/jvk/src/pch/glob/glm.hpp diff --git a/jvk/src/pch/glob/gsl.hpp b/legacy/jvk/src/pch/glob/gsl.hpp similarity index 100% rename from jvk/src/pch/glob/gsl.hpp rename to legacy/jvk/src/pch/glob/gsl.hpp diff --git a/jvk/src/pch/glob/std.hpp b/legacy/jvk/src/pch/glob/std.hpp similarity index 100% rename from jvk/src/pch/glob/std.hpp rename to legacy/jvk/src/pch/glob/std.hpp diff --git a/jvk/src/pch/glob/volk.hpp b/legacy/jvk/src/pch/glob/volk.hpp similarity index 100% rename from jvk/src/pch/glob/volk.hpp rename to legacy/jvk/src/pch/glob/volk.hpp diff --git a/jvk/src/pch/jvk/pch_def.hpp b/legacy/jvk/src/pch/jvk/pch_def.hpp similarity index 100% rename from jvk/src/pch/jvk/pch_def.hpp rename to legacy/jvk/src/pch/jvk/pch_def.hpp diff --git a/jvk/src/pch/lib/volk/volk.cpp b/legacy/jvk/src/pch/lib/volk/volk.cpp similarity index 100% rename from jvk/src/pch/lib/volk/volk.cpp rename to legacy/jvk/src/pch/lib/volk/volk.cpp diff --git a/jvk/src/pch/pch.cpp b/legacy/jvk/src/pch/pch.cpp similarity index 100% rename from jvk/src/pch/pch.cpp rename to legacy/jvk/src/pch/pch.cpp diff --git a/jvk/src/pch/pch.hpp b/legacy/jvk/src/pch/pch.hpp similarity index 100% rename from jvk/src/pch/pch.hpp rename to legacy/jvk/src/pch/pch.hpp diff --git a/jvk/src/render/cmd/cmd_buffer.cpp b/legacy/jvk/src/render/cmd/cmd_buffer.cpp similarity index 100% rename from jvk/src/render/cmd/cmd_buffer.cpp rename to legacy/jvk/src/render/cmd/cmd_buffer.cpp diff --git a/jvk/src/render/cmd/cmd_pool.cpp b/legacy/jvk/src/render/cmd/cmd_pool.cpp similarity index 100% rename from jvk/src/render/cmd/cmd_pool.cpp rename to legacy/jvk/src/render/cmd/cmd_pool.cpp diff --git a/jvk/src/render/pass/attachment/attachment_collection.cpp b/legacy/jvk/src/render/pass/attachment/attachment_collection.cpp similarity index 100% rename from jvk/src/render/pass/attachment/attachment_collection.cpp rename to legacy/jvk/src/render/pass/attachment/attachment_collection.cpp diff --git a/jvk/src/render/pass/attachment/attachment_description.cpp b/legacy/jvk/src/render/pass/attachment/attachment_description.cpp similarity index 100% rename from jvk/src/render/pass/attachment/attachment_description.cpp rename to legacy/jvk/src/render/pass/attachment/attachment_description.cpp diff --git a/jvk/src/render/pass/framebuffer/fb_collection.cpp b/legacy/jvk/src/render/pass/framebuffer/fb_collection.cpp similarity index 100% rename from jvk/src/render/pass/framebuffer/fb_collection.cpp rename to legacy/jvk/src/render/pass/framebuffer/fb_collection.cpp diff --git a/jvk/src/render/pass/framebuffer/fb_collection_config.cpp b/legacy/jvk/src/render/pass/framebuffer/fb_collection_config.cpp similarity index 100% rename from jvk/src/render/pass/framebuffer/fb_collection_config.cpp rename to legacy/jvk/src/render/pass/framebuffer/fb_collection_config.cpp diff --git a/jvk/src/render/pass/framebuffer/framebuffer.cpp b/legacy/jvk/src/render/pass/framebuffer/framebuffer.cpp similarity index 100% rename from jvk/src/render/pass/framebuffer/framebuffer.cpp rename to legacy/jvk/src/render/pass/framebuffer/framebuffer.cpp diff --git a/jvk/src/render/pass/framebuffer/swapchain_fb_collection.cpp b/legacy/jvk/src/render/pass/framebuffer/swapchain_fb_collection.cpp similarity index 100% rename from jvk/src/render/pass/framebuffer/swapchain_fb_collection.cpp rename to legacy/jvk/src/render/pass/framebuffer/swapchain_fb_collection.cpp diff --git a/jvk/src/render/pass/render_pass.cpp b/legacy/jvk/src/render/pass/render_pass.cpp similarity index 100% rename from jvk/src/render/pass/render_pass.cpp rename to legacy/jvk/src/render/pass/render_pass.cpp diff --git a/jvk/src/render/pass/subpass.cpp b/legacy/jvk/src/render/pass/subpass.cpp similarity index 100% rename from jvk/src/render/pass/subpass.cpp rename to legacy/jvk/src/render/pass/subpass.cpp diff --git a/jvk/src/render/pipeline/layout/descriptor_set_layout.cpp b/legacy/jvk/src/render/pipeline/layout/descriptor_set_layout.cpp similarity index 100% rename from jvk/src/render/pipeline/layout/descriptor_set_layout.cpp rename to legacy/jvk/src/render/pipeline/layout/descriptor_set_layout.cpp diff --git a/jvk/src/render/pipeline/layout/pipeline_layout.cpp b/legacy/jvk/src/render/pipeline/layout/pipeline_layout.cpp similarity index 100% rename from jvk/src/render/pipeline/layout/pipeline_layout.cpp rename to legacy/jvk/src/render/pipeline/layout/pipeline_layout.cpp diff --git a/jvk/src/render/pipeline/pipeline.cpp b/legacy/jvk/src/render/pipeline/pipeline.cpp similarity index 100% rename from jvk/src/render/pipeline/pipeline.cpp rename to legacy/jvk/src/render/pipeline/pipeline.cpp diff --git a/jvk/src/render/pipeline/shader/shader.cpp b/legacy/jvk/src/render/pipeline/shader/shader.cpp similarity index 100% rename from jvk/src/render/pipeline/shader/shader.cpp rename to legacy/jvk/src/render/pipeline/shader/shader.cpp diff --git a/jvk/src/render/sync/fence.cpp b/legacy/jvk/src/render/sync/fence.cpp similarity index 100% rename from jvk/src/render/sync/fence.cpp rename to legacy/jvk/src/render/sync/fence.cpp diff --git a/jvk/src/render/sync/os_fence_handle.cpp b/legacy/jvk/src/render/sync/os_fence_handle.cpp similarity index 100% rename from jvk/src/render/sync/os_fence_handle.cpp rename to legacy/jvk/src/render/sync/os_fence_handle.cpp diff --git a/jvk/src/render/sync/pipeline_barrier.cpp b/legacy/jvk/src/render/sync/pipeline_barrier.cpp similarity index 100% rename from jvk/src/render/sync/pipeline_barrier.cpp rename to legacy/jvk/src/render/sync/pipeline_barrier.cpp diff --git a/jvk/src/render/sync/semaphore.cpp b/legacy/jvk/src/render/sync/semaphore.cpp similarity index 100% rename from jvk/src/render/sync/semaphore.cpp rename to legacy/jvk/src/render/sync/semaphore.cpp diff --git a/jvk/src/render/sync/timline_semaphore.cpp b/legacy/jvk/src/render/sync/timline_semaphore.cpp similarity index 100% rename from jvk/src/render/sync/timline_semaphore.cpp rename to legacy/jvk/src/render/sync/timline_semaphore.cpp diff --git a/jvk/src/surface/surface.cpp b/legacy/jvk/src/surface/surface.cpp similarity index 100% rename from jvk/src/surface/surface.cpp rename to legacy/jvk/src/surface/surface.cpp diff --git a/jvk/src/surface/swapchain/swapchain.cpp b/legacy/jvk/src/surface/swapchain/swapchain.cpp similarity index 100% rename from jvk/src/surface/swapchain/swapchain.cpp rename to legacy/jvk/src/surface/swapchain/swapchain.cpp diff --git a/jvk/src/surface/swapchain/swapchain_subpass_config.cpp b/legacy/jvk/src/surface/swapchain/swapchain_subpass_config.cpp similarity index 100% rename from jvk/src/surface/swapchain/swapchain_subpass_config.cpp rename to legacy/jvk/src/surface/swapchain/swapchain_subpass_config.cpp diff --git a/jvk/src/utility/device/physical_device_features.cpp b/legacy/jvk/src/utility/device/physical_device_features.cpp similarity index 100% rename from jvk/src/utility/device/physical_device_features.cpp rename to legacy/jvk/src/utility/device/physical_device_features.cpp diff --git a/jvk/src/utility/os/incl_os.hpp b/legacy/jvk/src/utility/os/incl_os.hpp similarity index 100% rename from jvk/src/utility/os/incl_os.hpp rename to legacy/jvk/src/utility/os/incl_os.hpp diff --git a/jvk/src/utility/os/incl_posix.hpp b/legacy/jvk/src/utility/os/incl_posix.hpp similarity index 100% rename from jvk/src/utility/os/incl_posix.hpp rename to legacy/jvk/src/utility/os/incl_posix.hpp diff --git a/jvk/src/utility/os/os_window.cpp b/legacy/jvk/src/utility/os/os_window.cpp similarity index 100% rename from jvk/src/utility/os/os_window.cpp rename to legacy/jvk/src/utility/os/os_window.cpp diff --git a/jvk/src/utility/os/win/incl_windows.hpp b/legacy/jvk/src/utility/os/win/incl_windows.hpp similarity index 100% rename from jvk/src/utility/os/win/incl_windows.hpp rename to legacy/jvk/src/utility/os/win/incl_windows.hpp diff --git a/jvk/src/utility/os/win/win_window.hpp b/legacy/jvk/src/utility/os/win/win_window.hpp similarity index 100% rename from jvk/src/utility/os/win/win_window.hpp rename to legacy/jvk/src/utility/os/win/win_window.hpp diff --git a/legacy/tests/CMakeLists.txt b/legacy/tests/CMakeLists.txt new file mode 100644 index 00000000..4a827f09 --- /dev/null +++ b/legacy/tests/CMakeLists.txt @@ -0,0 +1,16 @@ +cth_glob_cpp(TEST_SRC "src") + +add_executable(tests ${TEST_SRC}) + +target_include_directories( + tests PRIVATE + incl + ../../cth/ +) + +find_package(GTest CONFIG REQUIRED) + +target_link_libraries(tests PRIVATE GTest::gtest_main jvk jolly) + +include(GoogleTest) +gtest_discover_tests(tests) \ No newline at end of file diff --git a/tests/src/test_utils/pch.cpp b/legacy/tests/src/test_utils/pch.cpp similarity index 100% rename from tests/src/test_utils/pch.cpp rename to legacy/tests/src/test_utils/pch.cpp diff --git a/tests/src/test_utils/pch.h b/legacy/tests/src/test_utils/pch.h similarity index 100% rename from tests/src/test_utils/pch.h rename to legacy/tests/src/test_utils/pch.h diff --git a/tests/src/vulkan/utility/format/format_string_test.cpp b/legacy/tests/src/vulkan/utility/format/format_string_test.cpp similarity index 100% rename from tests/src/vulkan/utility/format/format_string_test.cpp rename to legacy/tests/src/vulkan/utility/format/format_string_test.cpp diff --git a/lib/cth_cmake b/lib/cth_cmake index 34b81860..5162ddad 160000 --- a/lib/cth_cmake +++ b/lib/cth_cmake @@ -1 +1 @@ -Subproject commit 34b818607e8ae63da2159a53a5d3f13208ea0210 +Subproject commit 5162ddad62653abac2652b50a4fdde4091e861b0 diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 4a827f09..58b3abea 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -2,15 +2,11 @@ cth_glob_cpp(TEST_SRC "src") add_executable(tests ${TEST_SRC}) -target_include_directories( - tests PRIVATE - incl - ../../cth/ -) - find_package(GTest CONFIG REQUIRED) +find_package(volk CONFIG REQUIRED) +find_package(cth CONFIG REQUIRED COMPONENTS cth) -target_link_libraries(tests PRIVATE GTest::gtest_main jvk jolly) +target_link_libraries(tests PRIVATE GTest::gtest_main volk::volk cth::cth) include(GoogleTest) -gtest_discover_tests(tests) \ No newline at end of file +gtest_discover_tests(tests) diff --git a/tests/src/loader_test.cpp b/tests/src/loader_test.cpp new file mode 100644 index 00000000..8cc3634b --- /dev/null +++ b/tests/src/loader_test.cpp @@ -0,0 +1,52 @@ +#include +#include + +#include + + +namespace { + +TEST(loader, supports_vulkan_1_3) { + ASSERT_EQ(volkInitialize(), VK_SUCCESS); + + uint32_t version = 0; + ASSERT_EQ(vkEnumerateInstanceVersion(&version), VK_SUCCESS); + EXPECT_GE(version, VK_API_VERSION_1_3); + + volkFinalize(); +} + +TEST(loader, finds_vulkan_1_3_device) { + ASSERT_EQ(volkInitialize(), VK_SUCCESS); + + VkApplicationInfo const appInfo{ + .sType = VK_STRUCTURE_TYPE_APPLICATION_INFO, + .apiVersion = VK_API_VERSION_1_3, + }; + VkInstanceCreateInfo const createInfo{ + .sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO, + .pApplicationInfo = &appInfo, + }; + + VkInstance instance = VK_NULL_HANDLE; + ASSERT_EQ(vkCreateInstance(&createInfo, nullptr, &instance), VK_SUCCESS); + volkLoadInstanceOnly(instance); + + uint32_t deviceCount = 0; + ASSERT_EQ(vkEnumeratePhysicalDevices(instance, &deviceCount, nullptr), VK_SUCCESS); + std::vector devices(deviceCount); + ASSERT_EQ(vkEnumeratePhysicalDevices(instance, &deviceCount, devices.data()), VK_SUCCESS); + + bool found = false; + for(auto const device : devices) { + VkPhysicalDeviceProperties properties{}; + vkGetPhysicalDeviceProperties(device, &properties); + found |= properties.apiVersion >= VK_API_VERSION_1_3; + } + EXPECT_TRUE(found); + + vkDestroyInstance(instance, nullptr); + volkFinalize(); +} + +} diff --git a/vcpkg-configuration.json b/vcpkg-configuration.json index 5375da75..324e898b 100644 --- a/vcpkg-configuration.json +++ b/vcpkg-configuration.json @@ -1,13 +1,14 @@ { "default-registry": { - "kind": "builtin", - "baseline": "6ecbbbdf31cba47aafa7cf6189b1e73e10ac61f8" + "kind": "git", + "baseline": "80d54ff62d528339c626a6fbc3489a7f25956ade", + "repository": "https://github.com/microsoft/vcpkg" }, "registries": [ { - "kind": "builtin", - "baseline": "6ecbbbdf31cba47aafa7cf6189b1e73e10ac61f8", - "packages": [] + "kind": "artifact", + "location": "https://github.com/microsoft/vcpkg-ce-catalog/archive/refs/heads/main.zip", + "name": "microsoft" } ] } diff --git a/vcpkg.json b/vcpkg.json index 01323ebe..26160c26 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -1,13 +1,6 @@ { "dependencies": [ - "boost-asio", - "boost-pfr", - "glfw3", - "glm", "gtest", - "ms-gsl", - "range-v3", - "stb", "volk" ] } From 5c187771ddd49cb6cc43cfd74394495eb8d42f0f Mon Sep 17 00:00:00 2001 From: lukas <89145039+codethinki@users.noreply.github.com> Date: Wed, 14 Oct 2026 05:38:26 +0200 Subject: [PATCH 002/135] vcpkg: vulkan-memory-allocator --- vcpkg.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/vcpkg.json b/vcpkg.json index 26160c26..dbd3f367 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -1,6 +1,7 @@ { "dependencies": [ "gtest", - "volk" + "volk", + "vulkan-memory-allocator" ] } From 9f4ae14bc1869a564590c1e83f5ca774abb5cd69 Mon Sep 17 00:00:00 2001 From: lukas <89145039+codethinki@users.noreply.github.com> Date: Fri, 16 Oct 2026 12:48:57 +0200 Subject: [PATCH 003/135] cvk: port Instance and DebugMessenger, code and doc style guide - AGENTS.md: code and doc style guide (STE100), CLAUDE.md links it - cvk: Instance, DebugMessenger, Cvk (volk loader), utility types, constants, format, exceptions - cvk tests: instance and debug messenger unit tests - vcpkg: vulkan-utility-libraries for the enum names --- AGENTS.md | 280 ++++++++++++++++++ CLAUDE.md | 1 + CMakeLists.txt | 7 +- cvk/CMakeLists.txt | 29 ++ cvk/incl/cvk/base/cvk.hpp | 30 ++ cvk/incl/cvk/base/instance.hpp | 181 +++++++++++ cvk/incl/cvk/debug/debug_messenger.hpp | 134 +++++++++ .../cvk/utility/constant/debug_constants.hpp | 20 ++ cvk/incl/cvk/utility/constants.hpp | 22 ++ cvk/incl/cvk/utility/format.hpp | 23 ++ cvk/incl/cvk/utility/types.hpp | 20 ++ cvk/incl/cvk/utility/vk_exceptions.hpp | 100 +++++++ cvk/src/base/cvk.cpp | 36 +++ cvk/src/base/instance.cpp | 225 ++++++++++++++ cvk/src/debug/debug_messenger.cpp | 195 ++++++++++++ cvk/src/pch/cvk/pch_def.hpp | 9 + cvk/src/pch/glob/cth.hpp | 9 + cvk/src/pch/glob/std.hpp | 27 ++ cvk/src/pch/glob/volk.hpp | 2 + cvk/src/pch/pch.hpp | 11 + cvk/tests/CMakeLists.txt | 17 ++ cvk/tests/src/base/instance_test.cpp | 95 ++++++ cvk/tests/src/debug/debug_messenger_test.cpp | 119 ++++++++ cvk/tests/src/environment.cpp | 19 ++ cvk/tests/src/test.hpp | 14 + vcpkg.json | 3 +- 26 files changed, 1626 insertions(+), 2 deletions(-) create mode 100644 AGENTS.md create mode 100644 CLAUDE.md create mode 100644 cvk/CMakeLists.txt create mode 100644 cvk/incl/cvk/base/cvk.hpp create mode 100644 cvk/incl/cvk/base/instance.hpp create mode 100644 cvk/incl/cvk/debug/debug_messenger.hpp create mode 100644 cvk/incl/cvk/utility/constant/debug_constants.hpp create mode 100644 cvk/incl/cvk/utility/constants.hpp create mode 100644 cvk/incl/cvk/utility/format.hpp create mode 100644 cvk/incl/cvk/utility/types.hpp create mode 100644 cvk/incl/cvk/utility/vk_exceptions.hpp create mode 100644 cvk/src/base/cvk.cpp create mode 100644 cvk/src/base/instance.cpp create mode 100644 cvk/src/debug/debug_messenger.cpp create mode 100644 cvk/src/pch/cvk/pch_def.hpp create mode 100644 cvk/src/pch/glob/cth.hpp create mode 100644 cvk/src/pch/glob/std.hpp create mode 100644 cvk/src/pch/glob/volk.hpp create mode 100644 cvk/src/pch/pch.hpp create mode 100644 cvk/tests/CMakeLists.txt create mode 100644 cvk/tests/src/base/instance_test.cpp create mode 100644 cvk/tests/src/debug/debug_messenger_test.cpp create mode 100644 cvk/tests/src/environment.cpp create mode 100644 cvk/tests/src/test.hpp diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000..498cda98 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,280 @@ +# Agent guide + +How to write code and documentation in this repository. +Read it before you change a file. Match the existing code. + +## Repository + +| Path | Content | +|---|---| +| `cvk/` | C++ wrapper of the Vulkan C API: one class per Vulkan object, objects own their handle | +| `jvk/` | coroutine layer on top of `cvk` (not started) | +| `legacy/` | the old `jvk`, `jolly` and `demo` code, reference only, not built | +| `tests/` | loader smoke tests | +| `lib/cth_cmake` | cmake utilities (submodule) | + +- Vulkan 1.3 is the baseline. Do not port code that 1.3 makes unnecessary (render passes, framebuffers, the old sync API). +- Use the Vulkan C API through volk. Do not use Vulkan-Hpp. +- Memory comes from VMA. +- `cvk` destroys immediately. It has no destruction queue. Deferred destruction belongs to `jvk`. +- Work happens on a local branch. Do not open pull requests for single steps. +- Commits have no `Co-Authored-By` or "Generated with" lines. + +## Build and test + +```bash +cmake --preset gcc_release +cmake --build out/build/gcc_release +ctest --test-dir out/build/gcc_release --output-on-failure +``` + +- Presets: `gcc_debug`, `gcc_release`, `clang_debug`, `clang_release`, `msvc_debug`, `msvc_release`. +- `cth_ROOT` points at `../cth/out/install/`. That install must exist. +- Debug builds enable the validation layers. Run the tests in a debug build before you report a change as done. +- `VCPKG_ROOT` must point at vcpkg. + +## Code style + +### Formatting + +`.clang-format` is the reference. Short form: + +- 4 spaces, no tabs, 120 columns. +- Attached braces: `class X {`, `void f() {`, `if(x) {`. +- No space between a keyword and `(`: `if(`, `for(`, `while(`, `switch(`, `if constexpr(`. +- Pointer and reference bind left: `T* ptr`, `T const& ref`. +- `const` comes after the type: `T const&`, `auto const`. +- Nested namespaces inside a file are indented one level (`namespace dev { ... }`). + +### Naming + +| Kind | Style | Example | +|---|---|---| +| class, struct of `cvk` / `jvk` | `PascalCase` | `Semaphore`, `DeviceTable`, `DestructionQueue` | +| nested config and state | `Config`, `State` | `Instance::State` | +| generic library type, trait, concept | `snake_case` | `basic_deletable`, `handle_t`, `destroyable` | +| type alias | `snake_case_t` | `callback_t`, `queue_set_t` | +| function, member function | `camelCase` | `optDestroy()`, `createInfo()` | +| static check function | `debug_check`, `debug_check_handle` | | +| function parameter | `camelCase` | `appName`, `requiredExtensions` | +| local variable | `camelCase` | `createResult`, `requiredExtVec` | +| member variable | `_camelCase` | `_handle`, `_debugMessenger` | +| constant | `UPPER_SNAKE_CASE` | `VALIDATION_LAYERS`, `MAX_SECONDARY_CMD_BUFFERS` | +| template parameter | `PascalCase` | `T`, `Rng`, `Context` | +| macro | `UPPER_SNAKE_CASE` with library prefix | `CVK_RESULT_STABLE_THROW` | +| namespace | short lower case | `cvk`, `cvk::constants`, `cvk::dev` | + +- A raw Vulkan handle in a variable or parameter has the prefix `vk`: `vkInstance`, `vkSemaphore`. +- Function parameters were `snake_case` in the legacy code. New code uses `camelCase`. Rename them when you port. +- `cvk::dev` holds implementation details that are not part of the API. + +### Language + +- Use `constexpr`, `consteval` and `decltype(auto)`. The macros `cxpr`, `cval` and `declauto` do not exist anymore. +- Put `[[nodiscard]]` on every function that returns a value the caller must use. +- Put `explicit` on constructors with one parameter and on tag constructors. +- Use `auto const` for locals that do not change. +- Initialize Vulkan structs with designated initializers. Always write `.sType` and `.pNext`. +- Use deducing `this` (`this auto& self`) where a function must work on the derived type. +- Qualify `std::` names. A `.cpp` file can declare `using std::span;` and similar at the top of its namespace. +- No raw `new` and `delete`. Use `std::unique_ptr`, `cth::move_ptr` and `cth::not_null`. +- Prefer compile time checks (concepts, `static_assert`) to run time checks. +- Never erase types into integers or `void*`. + +### Includes + +Order in a file: + +1. the own header (in a `.cpp`) +2. project headers with quotes: `"cvk/base/instance.hpp"` +3. library headers: ``, ``, `` +4. standard headers: ``, `` + +Separate the groups with one empty line. Forward declare a class when a header only needs its name. + +### Files + +- Headers go to `incl///.hpp`, sources to `src//.cpp`. The folders match. +- Config structs that are large get their own `_config.hpp`. +- Every library has a precompiled header in `src/pch/`. +- Every header starts with `#pragma once`. + +### Class layout + +Classes that own a Vulkan handle follow this order: + +```cpp +class Semaphore { +public: + struct State; + + explicit Semaphore(Core const& core); // base constructor + Semaphore(Core const& core, State const& state); // constructs and wraps + explicit Semaphore(Core const& core, create_t); // constructs and creates + + ~Semaphore() { optDestroy(); } + + void wrap(State const& state); + void create(); + void destroy(); + void optDestroy() { if(created()) destroy(); } + State release(); + + // operations + + static void destroy(DeviceTable table, VkSemaphore vkSemaphore); + +protected: + // helpers that derived classes use + +private: + // helpers + // members + +public: + // accessors: created(), get(), ... + // constants + // copy and move members + // static debug_check functions +}; +``` + +After the class, in the same header: + +```cpp +//State + +namespace cvk { +struct Semaphore::State { + cvk::vk_not_null vkSemaphore; +}; +} + +//debug checks + +namespace cvk { +inline void Semaphore::debug_check(Semaphore const& semaphore) { + CTH_CRITICAL(!semaphore.created(), "semaphore must be created") {} +} +} +``` + +### Object lifecycle + +| Function | Meaning | +|---|---| +| base constructor | stores the configuration, does not create a Vulkan object | +| `wrap(State)` | takes ownership of an existing Vulkan object, calls `optDestroy()` first | +| `create(...)` | creates a new Vulkan object, calls `optDestroy()` first | +| `destroy()` | destroys the Vulkan object now and resets, requires `created()` | +| `optDestroy()` | calls `destroy()` if `created()` | +| `release()` | gives the `State` to the caller and resets, does not destroy | +| `static destroy(...)` | destroys the raw handles, used by `destroy()` | +| `reset()` | sets the members to the not created state, does not destroy | + +- The destructor calls `optDestroy()`. +- A failed `create()` calls `reset()` before it throws. +- `release()` exists so that an owner can use an object it must not destroy (for example swapchain images). + +### Errors + +- `CTH_CRITICAL(condition, "message") {}`: precondition check, active in debug builds only. +- `CTH_STABLE_ERR(condition, "message") { reset(); throw details->exception(); }`: error that can happen at run time. +- `CTH_WARN(condition, "message") {}`: suspicious state that is not an error. +- `CVK_RESULT_STABLE_THROW(result != VK_SUCCESS, result, "message") {}`: throws `cvk::vk_result_exception` with the `VkResult`. +- The condition is the error case: `CTH_CRITICAL(!created(), ...)`, not the expected case. + +### Comment markers + +- `//TODO` work that is still open +- `//TEMP` code that must change before it is final +- `//IMPLEMENT` a planned change of the design +- `//State`, `//debug checks`, `//Config` mark the sections after a class + +No space after `//` in markers. + +## Documentation style + +Documentation uses doxygen comments and follows ASD-STE100 (Simplified Technical English), adapted to the short style of this code base. + +### Form + +- Use `/** ... */` blocks with one tag per line. +- Start after the tag with a lower case letter. +- Do not put a period at the end of a line. +- Write short fragments. Start them with a verb in the present tense: `creates the instance`, `destroys and resets`. +- Use `@ref` for every name of code: `@ref create()`, `@ref Core::created()`. +- Do not document what the signature already tells. A getter without conditions needs no comment. + +### Tags + +| Tag | Use | +|---|---| +| `@brief` | what the function does, one line | +| `@details` | more information. `calls:` followed by a list of the functions it calls | +| `@param` | name, then what it is for: `@param config to use` | +| `@tparam` | template parameter, same form as `@param` | +| `@return` | what the result is | +| `@pre` | condition that must be true before the call: `@pre @ref created()` | +| `@post` | condition that is true after the call: `@post @ref created()` | +| `@throws` | exception type and the cause: `@throws cvk::vk_result_exception result of vkCreateInstance()` | +| `@note` | side effect the caller must know: `@note calls @ref optDestroy()` | +| `@attention` | danger that is not a precondition: undefined behavior, not thread safe | + +- Write preconditions as `@pre`, not as `@attention requires ...`. +- For an exception that cvk throws itself, write the cause after `reason:`: `@throws cth::except::default_exception reason: required extension missing`. + +### Math like notation + +Use symbols when they are shorter and clearer than words: + +- implication: `nullptr -> no messenger`, `@ref memory == nullptr -> memory unknown` +- conditions and ranges: `@pre 0 < workers < MAX_INT` +- special values: `(constants::WHOLE_SIZE => whole buffer)` + +### STE100 rules that apply + +1. Use one word for one meaning. Do not use synonyms. The terms in the table below are fixed. +2. Write descriptions with 25 words or fewer and instructions with 20 words or fewer. +3. Write one topic in one fragment. Split two actions into two lines. +4. Use the active voice and the present tense. +5. Write instructions to the caller in the imperative: `call @ref create() first`. +6. Do not use phrasal verbs (`set up`, `carry out`). Use one verb (`create`, `do`). +7. Do not use `-ing` forms as nouns or adjectives. +8. Use `it`, `this` and `they` only when the reference is clear. +9. Write the most important condition first, then the consequence. + +| Term | Meaning | +|---|---| +| construct | the C++ object comes into existence | +| create | the Vulkan object comes into existence | +| destroy | the Vulkan object stops to exist | +| wrap | take ownership of an existing Vulkan object | +| release | give the ownership to the caller without destroying | +| reset | set the members to the not created state | +| handle | the raw Vulkan handle, for example `VkInstance` | +| state | the `State` struct: everything that `wrap` needs | + +### Example + +```cpp +/** + * @brief creates the instance + * @param messengerConfig std::nullopt -> no messenger, debug builds use the default config + * @pre volk is initialized + * @post @ref created() + * @note calls @ref optDestroy() + * @throws cvk::vk_result_exception result of vkCreateInstance() + */ +void create(std::optional messengerConfig = std::nullopt); +``` + +### When you port legacy code + +- Check every comment against the code. The legacy comments are not always up to date. +- Rewrite comments to this style. +- Report bugs that you find. Do not fix a behavior change silently. + +### Repository text + +- Do not use em dashes or en dashes in comments, docs, commit messages or markdown. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..43c994c2 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1 @@ +@AGENTS.md diff --git a/CMakeLists.txt b/CMakeLists.txt index 42c8134e..6dff9030 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -10,9 +10,14 @@ cth_set_latest_c_cpp_standard() set(CMAKE_CXX_SCAN_FOR_MODULES OFF) -# Tests if(JOLLY_ENABLE_TESTS) enable_testing() +endif() + +# Add subprojects +add_subdirectory(cvk) +# Tests +if(JOLLY_ENABLE_TESTS) add_subdirectory(tests) endif() diff --git a/cvk/CMakeLists.txt b/cvk/CMakeLists.txt new file mode 100644 index 00000000..a41d2c32 --- /dev/null +++ b/cvk/CMakeLists.txt @@ -0,0 +1,29 @@ +cth_glob_cpp(CVK_SRC "src") +cth_glob_cpp(CVK_INCL "incl") + +add_library(cvk STATIC ${CVK_SRC} ${CVK_INCL}) + +cth_target_enable_build_cache(cvk OPTIONAL) + +target_include_directories( + cvk + PUBLIC incl + PRIVATE src +) + +target_precompile_headers( + cvk + PRIVATE + src/pch/pch.hpp +) + +find_package(volk CONFIG REQUIRED) +find_package(VulkanUtilityLibraries CONFIG REQUIRED) +find_package(cth CONFIG REQUIRED COMPONENTS cth) + +target_link_libraries(cvk PUBLIC volk::volk Vulkan::UtilityHeaders cth::cth) + +# tests +if(JOLLY_ENABLE_TESTS) + add_subdirectory(tests) +endif() diff --git a/cvk/incl/cvk/base/cvk.hpp b/cvk/incl/cvk/base/cvk.hpp new file mode 100644 index 00000000..62ab060e --- /dev/null +++ b/cvk/incl/cvk/base/cvk.hpp @@ -0,0 +1,30 @@ +#pragma once + +namespace cvk { +/** + * @brief loads and unloads the vulkan loader + */ +class Cvk { +public: + /** + * @brief loads the vulkan loader through volk + * @post @ref initialized() + * @throws cvk::vk_result_exception result of volkInitialize() + */ + static void init(); + + /** + * @brief unloads the vulkan loader + * @post !@ref initialized() + */ + static void terminate(); + +private: + inline static bool _initialized = false; + static void terminateVolk(); + static void initVolk(); + +public: + [[nodiscard]] static bool initialized() { return _initialized; } +}; +} diff --git a/cvk/incl/cvk/base/instance.hpp b/cvk/incl/cvk/base/instance.hpp new file mode 100644 index 00000000..fd43be6e --- /dev/null +++ b/cvk/incl/cvk/base/instance.hpp @@ -0,0 +1,181 @@ +#pragma once + +#include "cvk/debug/debug_messenger.hpp" +#include "cvk/utility/constants.hpp" +#include "cvk/utility/types.hpp" + +#include + +#include + +#include +#include +#include +#include +#include +#include +#include + + + +namespace cvk { + +/** + * @brief VkInstance wrapper, owns the debug messenger + */ +class Instance { +public: + struct State; + + /** + * @brief base constructor + * @details debug builds enable the validation layers + * @throws cth::except::default_exception reason: required instance extension missing + * @throws cth::except::default_exception reason: required validation layer missing + */ + Instance(std::string_view appName, std::span requiredExtensions); + + /** + * @brief constructs and creates + * @details calls: + * - @ref Instance(std::string_view, std::span) + * - @ref create() + */ + Instance( + std::string_view appName, + std::span requiredExtensions, + std::optional const& messengerConfig + ); + + /** + * @brief constructs and wraps + * @details calls: + * - @ref Instance(std::string_view, std::span) + * - @ref wrap() + */ + Instance(std::string_view appName, std::span requiredExtensions, State state); + + /** + * @note calls @ref optDestroy() + */ + ~Instance() { optDestroy(); } + + /** + * @brief wraps the state + * @post @ref created() + * @post volk uses the functions of this instance + * @note calls @ref optDestroy() + */ + void wrap(State state); + + /** + * @brief creates the instance + * @param messengerConfig std::nullopt -> no messenger, debug builds use @ref DebugMessenger::Config::Default() + * @pre @ref Cvk::initialized() + * @post @ref created() + * @post volk uses the functions of this instance + * @note calls @ref optDestroy() + * @note no validation layers -> no messenger + * @throws cvk::vk_result_exception result of vkCreateInstance() + */ + void create(std::optional messengerConfig = std::nullopt); + + /** + * @brief destroys the messenger and the instance and resets + * @pre @ref created() + */ + void destroy(); + + /** + * @brief if @ref created() -> calls @ref destroy() + */ + void optDestroy() { if(created()) destroy(); } + + /** + * @throws cth::except::default_exception reason: required extension missing + */ + void checkInstanceExtensionSupport(); + + /** + * @throws cth::except::default_exception reason: required validation layer missing + */ + void checkValidationLayerSupport(); + + /** + * @brief adds the extensions of the validation layers to the required extensions + * @note calls @ref checkValidationLayerSupport() + */ + void enableValidationLayers(); + + [[nodiscard]] static std::vector getAvailableValidationLayers(); + [[nodiscard]] static std::vector getAvailableInstanceExtensions(); + [[nodiscard]] VkApplicationInfo appInfo() const; + + static void destroy(VkInstance vkInstance); + +private: + void reset(); + + std::string _name; + + std::vector _requiredExt{}; + std::vector _availableExt; + std::vector _availableLayers{}; + + std::unique_ptr _debugMessenger = nullptr; + move_ptr _handle = VK_NULL_HANDLE; + + static void loadInstanceFunctions(vk_not_null vkInstance); + +public: + [[nodiscard]] bool created() const { return _handle != VK_NULL_HANDLE; } + [[nodiscard]] VkInstance get() const { return _handle.get(); } + [[nodiscard]] std::vector availableExtensions() const { return _availableExt; } + [[nodiscard]] std::vector requiredExtensions() const { return _requiredExt; } + [[nodiscard]] std::vector availableValidationLayers() const { return _availableLayers; } + + static constexpr std::array VALIDATION_LAYERS = { + "VK_LAYER_KHRONOS_validation" + }; + static constexpr std::array VALIDATION_LAYER_EXTENSIONS{ + VK_EXT_DEBUG_UTILS_EXTENSION_NAME, + }; + + static constexpr std::array REQUIRED_INSTANCE_EXTENSIONS{}; + + Instance(Instance const& other) = delete; + Instance& operator=(Instance const& other) = delete; + Instance(Instance&& other) noexcept = default; + Instance& operator=(Instance&& other) noexcept = default; + + + static void debug_check(Instance const& instance); + static void debug_check_handle(vk_not_null vkInstance); +}; + + +} + + +//State + +namespace cvk { +struct Instance::State { + vk_not_null vkInstance; + + /** + * @brief messenger of @ref vkInstance + * @note may be nullptr + */ + std::unique_ptr debugMessenger; +}; + +} + +//debug checks + +namespace cvk { + +inline void Instance::debug_check(Instance const& instance) { debug_check_handle(instance.get()); } +inline void Instance::debug_check_handle([[maybe_unused]] vk_not_null vkInstance) {} +} diff --git a/cvk/incl/cvk/debug/debug_messenger.hpp b/cvk/incl/cvk/debug/debug_messenger.hpp new file mode 100644 index 00000000..26251438 --- /dev/null +++ b/cvk/incl/cvk/debug/debug_messenger.hpp @@ -0,0 +1,134 @@ +#pragma once +#include "cvk/utility/constants.hpp" +#include "cvk/utility/types.hpp" + +#include + +#include +#include + + +namespace cvk { +class Instance; + +/** + * @brief VkDebugUtilsMessengerEXT wrapper + */ +class DebugMessenger { +public: + struct Config; + struct State; + using callback_t = PFN_vkDebugUtilsMessengerCallbackEXT; + + + /** + * @brief base constructor + */ + explicit DebugMessenger(Config config); + + /** + * @brief constructs and creates + * @details calls: + * - @ref DebugMessenger(Config) + * - @ref create() + */ + explicit DebugMessenger(Config const& config, Instance const& instance) : DebugMessenger{config} { + create(instance); + } + + + /** + * @note calls @ref optDestroy() + */ + ~DebugMessenger() { optDestroy(); } + + /** + * @brief creates the messenger + * @pre @ref Instance::created() + * @pre the instance enables VK_EXT_debug_utils + * @post @ref created() + * @note calls @ref optDestroy() + * @throws cvk::vk_result_exception result of vkCreateDebugUtilsMessengerEXT() + */ + void create(Instance const& instance); + + + /** + * @brief destroys and resets + * @pre @ref created() + */ + void destroy(); + + + /** + * @brief if @ref created() -> calls @ref destroy() + */ + void optDestroy() { if(created()) destroy(); } + + static void destroy(vk_not_null vkInstance, VkDebugUtilsMessengerEXT vkMessenger); + + /** + * @brief releases ownership, returns state and resets + * @pre @ref created() + */ + State release(); + + + struct Config { + callback_t callback = defaultCallback(); + VkDebugUtilsMessageSeverityFlagsEXT messageSeverities = constants::DEBUG_MESSAGE_SEVERITY; + VkDebugUtilsMessageTypeFlagsEXT messageTypes = constants::DEBUG_MESSAGE_TYPE; + + /** + * @param callback nullptr -> @ref defaultCallback() + */ + static Config Default(callback_t callback = nullptr); + + /** + * @brief writes the message to cth::log + */ + static callback_t defaultCallback(); + + [[nodiscard]] VkDebugUtilsMessengerCreateInfoEXT createInfo() const; + }; + +protected: + Instance const* _instance = nullptr; + Config _config; + +private: + void reset(); + + move_ptr _handle = VK_NULL_HANDLE; + +public: + [[nodiscard]] bool created() const { return _handle != VK_NULL_HANDLE; } + [[nodiscard]] VkDebugUtilsMessengerEXT get() const { return _handle.get(); } + [[nodiscard]] Config config() const { return _config; } + + DebugMessenger(DebugMessenger const& other) = delete; + DebugMessenger(DebugMessenger&& other) noexcept = default; + DebugMessenger& operator=(DebugMessenger const& other) = delete; + DebugMessenger& operator=(DebugMessenger&& other) noexcept = default; + + static void debug_check(DebugMessenger const& debugMessenger); +}; +} // namespace cvk + +//State + +namespace cvk { +struct DebugMessenger::State { + not_null instance; + vk_not_null vkMessenger; +}; + +} + +//debug checks + +namespace cvk { +inline void DebugMessenger::debug_check(DebugMessenger const& debugMessenger) { + CTH_CRITICAL(!debugMessenger.created(), "debug_messenger not created") {} +} +} diff --git a/cvk/incl/cvk/utility/constant/debug_constants.hpp b/cvk/incl/cvk/utility/constant/debug_constants.hpp new file mode 100644 index 00000000..e3c4eccf --- /dev/null +++ b/cvk/incl/cvk/utility/constant/debug_constants.hpp @@ -0,0 +1,20 @@ +#pragma once +#include + +#include + +namespace cvk::constants { +/** + * @brief true in debug builds, follows @ref cth::debug_mode() + */ +constexpr bool DEBUG_MODE = cth::debug_mode(); + +constexpr VkDebugUtilsMessageSeverityFlagsEXT DEBUG_MESSAGE_SEVERITY = + VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT + | VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT; + +constexpr VkDebugUtilsMessageTypeFlagsEXT DEBUG_MESSAGE_TYPE = + VK_DEBUG_UTILS_MESSAGE_TYPE_GENERAL_BIT_EXT + | VK_DEBUG_UTILS_MESSAGE_TYPE_VALIDATION_BIT_EXT + | VK_DEBUG_UTILS_MESSAGE_TYPE_PERFORMANCE_BIT_EXT; +} // namespace cvk::constants diff --git a/cvk/incl/cvk/utility/constants.hpp b/cvk/incl/cvk/utility/constants.hpp new file mode 100644 index 00000000..e42d17ab --- /dev/null +++ b/cvk/incl/cvk/utility/constants.hpp @@ -0,0 +1,22 @@ +#pragma once +// ReSharper disable CppUnusedIncludeDirective +#include "constant/debug_constants.hpp" +// ReSharper restore CppUnusedIncludeDirective + +#include + +#include + +namespace cvk { +struct create_t {}; + +static constexpr create_t create{}; +} + +namespace cvk::constants { + +constexpr bool ENABLE_VALIDATION_LAYERS = DEBUG_MODE; + +constexpr auto COMPILATION_MODE = cth::COMPILATION_MODE; + +} // namespace cvk::constants diff --git a/cvk/incl/cvk/utility/format.hpp b/cvk/incl/cvk/utility/format.hpp new file mode 100644 index 00000000..20c0a781 --- /dev/null +++ b/cvk/incl/cvk/utility/format.hpp @@ -0,0 +1,23 @@ +// ReSharper disable CppClangTidyCertDcl58Cpp +#pragma once +#include +#include + +#include + +#include + + +CTH_FORMAT_ALIAS(VkResult, std::string_view, string_VkResult); + +CTH_FORMAT_ALIAS(VkFormat, std::string_view, string_VkFormat); + +CTH_FORMAT_ALIAS(VkStructureType, std::string_view, string_VkStructureType); + +CTH_FORMAT_ALIAS(VkDescriptorType, std::string_view, string_VkDescriptorType); + +CTH_FORMAT_ALIAS(VkColorSpaceKHR, std::string_view, string_VkColorSpaceKHR); + +CTH_FORMAT_ALIAS(VkPresentModeKHR, std::string_view, string_VkPresentModeKHR); + +CTH_FORMAT_ALIAS(VkObjectType, std::string_view, string_VkObjectType); diff --git a/cvk/incl/cvk/utility/types.hpp b/cvk/incl/cvk/utility/types.hpp new file mode 100644 index 00000000..e1f3aa87 --- /dev/null +++ b/cvk/incl/cvk/utility/types.hpp @@ -0,0 +1,20 @@ +#pragma once +#include + +#include +#include + +namespace cvk { +template +using vk_not_null = cth::not_null; +template +using not_null = cth::not_null; + +template +using move_ptr = cth::move_ptr; + +template +using unique_not_null = cth::unique_not_null; + +using CompilationMode = cth::CompilationMode; +} diff --git a/cvk/incl/cvk/utility/vk_exceptions.hpp b/cvk/incl/cvk/utility/vk_exceptions.hpp new file mode 100644 index 00000000..a636f8df --- /dev/null +++ b/cvk/incl/cvk/utility/vk_exceptions.hpp @@ -0,0 +1,100 @@ +#pragma once +#include "format.hpp" + +#include + +#include +#include + + +namespace cvk { + +/** + * @brief exception of cvk, the message starts with "cvk: " + */ +class cvk_exception : public cth::except::default_exception { + static constexpr auto PREFIX = "cvk: "; + +public: + cvk_exception( + std::string_view msg, + cth::except::Severity const severity, + std::source_location loc, + std::stacktrace trace + ) : default_exception{ + std::format("{}{}", PREFIX, msg), + severity, + std::move(loc), + std::move(trace) + } {} + + cvk_exception(cth::except::default_exception defaultException) + : default_exception{std::move(defaultException)} { prepend(PREFIX); } +}; + +/** + * @brief exception for a vulkan call that did not return VK_SUCCESS + */ +class vk_result_exception : public cvk_exception { + static constexpr auto SEVERITY = cth::except::ERR; + static constexpr std::string_view MESSAGE_TEMPLATE = "vk error: {}"; + +public: + vk_result_exception( + std::string_view msg, + cth::except::Severity severity = SEVERITY, + std::source_location sourceLocation = {}, + std::stacktrace stacktrace = {} + ) : cvk_exception{ + std::format(MESSAGE_TEMPLATE, msg), + severity, + std::move(sourceLocation), + std::move(stacktrace) + } {} + + vk_result_exception( + VkResult result, + std::string_view msg, + std::source_location sourceLocation = {}, + std::stacktrace stacktrace = {} + ) : vk_result_exception{msg, SEVERITY, std::move(sourceLocation), std::move(stacktrace)} { add(result); } + + vk_result_exception(VkResult result, cth::except::default_exception defaultException) + : cvk_exception{std::move(defaultException)} { add(result); } + + using cvk_exception::add; + + /** + * @brief stores the result and adds it to the message + */ + vk_result_exception& add(VkResult result) { + _vkResult = result; + return cvk_exception::add("VkResult: ({})", result); + } + +private: + VkResult _vkResult = VK_RESULT_MAX_ENUM; + +public: + [[nodiscard]] VkResult vkResult() const noexcept { return _vkResult; } +}; + +} + +#define CVK_STABLE_THROW(expression, msg, ...) \ + CTH_STABLE_THROW_T(cvk::cvk_exception, expression, msg, __VA_ARGS__) + +#define CVK_VK_STABLE_THROW(expression, msg, ...) \ + CTH_STABLE_THROW_T(cvk::vk_result_exception, expression, msg, __VA_ARGS__) + +/** + * @brief acts like @ref CVK_VK_STABLE_THROW and adds the result to the exception + * @param expression true -> throws + * @param result to add to the exception + * @param msg format string + * @details braces and else work like the other CTH_ log macros + * a range for loop over a one element init list evaluates the add before the body + */ +#define CVK_RESULT_STABLE_THROW(expression, result, msg, ...)\ + CVK_VK_STABLE_THROW(expression, msg, __VA_ARGS__) \ + for(auto&& _ : {(details->e().add(result), 0)}) diff --git a/cvk/src/base/cvk.cpp b/cvk/src/base/cvk.cpp new file mode 100644 index 00000000..3ad40a95 --- /dev/null +++ b/cvk/src/base/cvk.cpp @@ -0,0 +1,36 @@ +#include "cvk/base/cvk.hpp" + +#include "cvk/utility/vk_exceptions.hpp" + +#include + + +namespace cvk { +void Cvk::terminate() { + log::msg("terminating cvk..."); + + terminateVolk(); + + log::msg("cvk terminated"); + _initialized = false; +} + +void Cvk::terminateVolk() { volkFinalize(); } + +void Cvk::initVolk() { + auto const result = volkInitialize(); + CVK_RESULT_STABLE_THROW(result != VK_SUCCESS, result, "failed to load vulkan") {} + + log::msg("loaded vulkan (volk)"); +} + +void Cvk::init() { + log::msg("cvk initializing..."); + initVolk(); + + log::msg("cvk initialized"); + _initialized = true; +} + + +} diff --git a/cvk/src/base/instance.cpp b/cvk/src/base/instance.cpp new file mode 100644 index 00000000..b2dbecd4 --- /dev/null +++ b/cvk/src/base/instance.cpp @@ -0,0 +1,225 @@ +#include "cvk/base/instance.hpp" + +#include "cvk/debug/debug_messenger.hpp" +#include "cvk/utility/vk_exceptions.hpp" + +#include +#include + + + +namespace cvk { + +using std::span; +using std::string; +using std::string_view; +using std::vector; + + +Instance::Instance(string_view appName, span requiredExtensions) : _name(appName), + _availableExt{getAvailableInstanceExtensions()} { + _requiredExt.reserve(requiredExtensions.size() + REQUIRED_INSTANCE_EXTENSIONS.size()); + _requiredExt.append_range(requiredExtensions); + _requiredExt.append_range( + REQUIRED_INSTANCE_EXTENSIONS | std::views::transform( + [](string_view const& view) { return string{view}; } + ) + ); + + checkInstanceExtensionSupport(); + + + if constexpr(constants::ENABLE_VALIDATION_LAYERS) enableValidationLayers(); +} + +Instance::Instance( + string_view appName, + span requiredExtensions, + std::optional const& messengerConfig +) : Instance{appName, requiredExtensions} { create(messengerConfig); } + +Instance::Instance( + string_view appName, + span requiredExtensions, + State state +) : Instance{ + appName, + requiredExtensions +} { wrap(std::move(state)); } + +void Instance::wrap(State state) { + optDestroy(); + + _handle = state.vkInstance.get(); + _debugMessenger = std::move(state.debugMessenger); + + loadInstanceFunctions(state.vkInstance); +} + +void Instance::create(std::optional messengerConfig) { + optDestroy(); + + if constexpr(constants::COMPILATION_MODE == CompilationMode::DEBUG) + if(messengerConfig == std::nullopt) + messengerConfig = DebugMessenger::Config::Default(); + + vector requiredExtVec(_requiredExt.size()); + std::ranges::transform(_requiredExt, requiredExtVec.begin(), [](auto const& str) { return str.data(); }); + + auto const appInfo = this->appInfo(); + VkDebugUtilsMessengerCreateInfoEXT debugCreateInfo{}; + + VkInstanceCreateInfo createInfo{ + .sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO, + .pNext = nullptr, + .flags = 0, + .pApplicationInfo = &appInfo, + .enabledLayerCount = 0, + .ppEnabledLayerNames = nullptr, + + .enabledExtensionCount = static_cast(requiredExtVec.size()), + .ppEnabledExtensionNames = requiredExtVec.data(), + }; + + if constexpr(constants::ENABLE_VALIDATION_LAYERS) + if(messengerConfig != std::nullopt) { + createInfo.enabledLayerCount = static_cast(VALIDATION_LAYERS.size()); + createInfo.ppEnabledLayerNames = VALIDATION_LAYERS.data(); + + debugCreateInfo = messengerConfig->createInfo(); + createInfo.pNext = &debugCreateInfo; + } + + VkInstance ptr = VK_NULL_HANDLE; + auto const createInstanceResult = vkCreateInstance(&createInfo, nullptr, &ptr); + + CTH_STABLE_ERR(createInstanceResult != VK_SUCCESS, "failed to create instance!") { + reset(); + throw cvk::vk_result_exception{createInstanceResult, details->exception()}; + } + + _handle = ptr; + loadInstanceFunctions(ptr); + + if constexpr(constants::ENABLE_VALIDATION_LAYERS) + if(messengerConfig != std::nullopt) + _debugMessenger = std::make_unique( + *messengerConfig, + *this + ); +} + +void Instance::destroy() { + debug_check(*this); + + _debugMessenger = nullptr; + destroy(_handle.get()); + + reset(); +} + + +void Instance::checkInstanceExtensionSupport() { + vector missingExtensions{}; + + for(auto& extension : _requiredExt) + if(!std::ranges::contains(_availableExt, extension)) missingExtensions.emplace_back(extension); + + CTH_STABLE_ERR(!missingExtensions.empty(), "instance extensions missing") { + std::ranges::for_each(missingExtensions, [&details](string_view extension) { details->add(extension); }); + + throw details->exception(); + } +} + +void Instance::checkValidationLayerSupport() { + if constexpr(constants::ENABLE_VALIDATION_LAYERS) { + vector missingLayers{}; + + std::ranges::for_each( + VALIDATION_LAYERS, + [&](string_view layer) { + if(!std::ranges::contains(_availableLayers, layer)) missingLayers.emplace_back(layer); + } + ); + CTH_STABLE_ERR(!missingLayers.empty(), "validation layers missing") { + std::ranges::for_each(missingLayers, [&details](string_view layer) { details->add(layer); }); + + throw details->exception(); + } + } +} + +void Instance::enableValidationLayers() { + _availableLayers = getAvailableValidationLayers(); + checkValidationLayerSupport(); + + _requiredExt.insert( + _requiredExt.begin(), + VALIDATION_LAYER_EXTENSIONS.begin(), + VALIDATION_LAYER_EXTENSIONS.end() + ); +} + +vector Instance::getAvailableValidationLayers() { + uint32_t layerCount = 0; + vkEnumerateInstanceLayerProperties(&layerCount, nullptr); + + std::vector availableLayers(layerCount); + vkEnumerateInstanceLayerProperties(&layerCount, availableLayers.data()); + + vector layers(availableLayers.size()); + std::ranges::transform( + availableLayers, + layers.begin(), + [](VkLayerProperties const& layer) { return string(layer.layerName); } + ); + + return layers; +} + +vector Instance::getAvailableInstanceExtensions() { + uint32_t extensionCount = 0; + vkEnumerateInstanceExtensionProperties(nullptr, &extensionCount, nullptr); + std::vector availableExtensions(extensionCount); + vkEnumerateInstanceExtensionProperties(nullptr, &extensionCount, availableExtensions.data()); + + vector availableExtensionsStr(availableExtensions.size()); + std::ranges::transform( + availableExtensions, + availableExtensionsStr.begin(), + [](VkExtensionProperties const& ext) { return ext.extensionName; } + ); + + + return availableExtensionsStr; +} + +VkApplicationInfo Instance::appInfo() const { + return VkApplicationInfo{ + .sType = VK_STRUCTURE_TYPE_APPLICATION_INFO, + .pNext = nullptr, + .pApplicationName = _name.c_str(), + .applicationVersion = 0, + .pEngineName = _name.c_str(), + .engineVersion = 0, + .apiVersion = VK_API_VERSION_1_3, + }; +} + +void Instance::destroy(VkInstance vkInstance) { + CTH_WARN(vkInstance == nullptr, "vkInstance invalid") {} + + vkDestroyInstance(vkInstance, nullptr); + + log::msg("destroyed instance"); +} + +void Instance::reset() { + _debugMessenger = nullptr; + _handle = VK_NULL_HANDLE; +} + +void Instance::loadInstanceFunctions(vk_not_null vkInstance) { volkLoadInstanceOnly(vkInstance.get()); } + +} diff --git a/cvk/src/debug/debug_messenger.cpp b/cvk/src/debug/debug_messenger.cpp new file mode 100644 index 00000000..3c802bac --- /dev/null +++ b/cvk/src/debug/debug_messenger.cpp @@ -0,0 +1,195 @@ +#include "cvk/debug/debug_messenger.hpp" + +#include "cvk/base/instance.hpp" +#include "cvk/utility/vk_exceptions.hpp" + +#include + +#include + + +namespace cvk { + +DebugMessenger::DebugMessenger(Config config) : _config{std::move(config)} {} + +void DebugMessenger::create(Instance const& instance) { + Instance::debug_check(instance); + + + optDestroy(); + + _instance = &instance; + + + auto const info = _config.createInfo(); + + + VkDebugUtilsMessengerEXT ptr = VK_NULL_HANDLE; + + auto const createResult = vkCreateDebugUtilsMessengerEXT(_instance->get(), &info, nullptr, &ptr); + + CTH_STABLE_ERR(createResult != VK_SUCCESS, "failed to set up debug messenger") { + reset(); + throw cvk::vk_result_exception{createResult, details->exception()}; + } + + _handle = ptr; +} + +void DebugMessenger::destroy() { + debug_check(*this); + + destroy(_instance->get(), _handle.get()); + + reset(); +} + + +void DebugMessenger::destroy(vk_not_null vkInstance, VkDebugUtilsMessengerEXT vkMessenger) { + Instance::debug_check_handle(vkInstance.get()); + CTH_WARN(vkMessenger == VK_NULL_HANDLE, "messenger invalid") {} + + vkDestroyDebugUtilsMessengerEXT(vkInstance.get(), vkMessenger, nullptr); +} + +DebugMessenger::State DebugMessenger::release() { + debug_check(*this); + + State const state{_instance, _handle.get()}; + reset(); + return state; +} + +void DebugMessenger::reset() { + _instance = nullptr; + _handle = VK_NULL_HANDLE; +} + +} // namespace cvk + +//Config + +namespace cvk::dev { +VKAPI_ATTR VkBool32 VKAPI_CALL default_debug_callback( + VkDebugUtilsMessageSeverityFlagBitsEXT messageSeverity, + VkDebugUtilsMessageTypeFlagsEXT messageType, + VkDebugUtilsMessengerCallbackDataEXT const* callbackData, + void* userData +); +} + +namespace cvk { +DebugMessenger::Config DebugMessenger::Config::Default(callback_t callback) { + return Config{callback == nullptr ? defaultCallback() : callback}; +} + +DebugMessenger::callback_t DebugMessenger::Config::defaultCallback() { return dev::default_debug_callback; } + +VkDebugUtilsMessengerCreateInfoEXT DebugMessenger::Config::createInfo() const { + return VkDebugUtilsMessengerCreateInfoEXT{ + .sType = VK_STRUCTURE_TYPE_DEBUG_UTILS_MESSENGER_CREATE_INFO_EXT, + .pNext = nullptr, + .flags = 0, + .messageSeverity = messageSeverities, + .messageType = messageTypes, + .pfnUserCallback = callback, + .pUserData = nullptr, + }; +} + +} + + +namespace cvk::dev { +struct component_info { + std::string name; + VkObjectType objectType; + uint64_t handle; +}; +} + +CTH_FORMAT_CLASS_CUSTOM( + cvk::dev::component_info, + "name: {0}, type: {1}, handle: {2:#x}", + [](cvk::dev::component_info const& c) { return std::tie(c.name, c.objectType, c.handle); } +); + + +namespace cvk::dev { + +namespace { + auto to_severity(VkDebugUtilsMessageSeverityFlagsEXT vkSeverity) { + constexpr std::array severities{ + std::pair{VK_DEBUG_UTILS_MESSAGE_SEVERITY_VERBOSE_BIT_EXT, cth::except::LOG}, + std::pair{VK_DEBUG_UTILS_MESSAGE_SEVERITY_INFO_BIT_EXT, cth::except::INFO}, + std::pair{VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT, cth::except::WARNING}, + std::pair{VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT, cth::except::ERR} + }; + + + for(auto [vkSeverityFlagBits, severity] : severities) + if(vkSeverity <= vkSeverityFlagBits) + return severity; + + return cth::except::CRITICAL; + } + + std::string to_msg(VkDebugUtilsMessageTypeFlagsEXT vkMsgType) { + constexpr std::array msgTypes{ + std::pair{VK_DEBUG_UTILS_MESSAGE_TYPE_GENERAL_BIT_EXT, "GENERAL"}, + std::pair{VK_DEBUG_UTILS_MESSAGE_TYPE_VALIDATION_BIT_EXT, "VALIDATION"}, + std::pair{VK_DEBUG_UTILS_MESSAGE_TYPE_PERFORMANCE_BIT_EXT, "PERFORMANCE"} + }; + + cth::dt::string_joiner joiner{" | "}; + + for(auto [vkMsgTypeFlagBit, name] : msgTypes) + if(vkMsgType & vkMsgTypeFlagBit) + joiner += name; + + return joiner; + } + +} + +VKAPI_ATTR VkBool32 VKAPI_CALL default_debug_callback( + VkDebugUtilsMessageSeverityFlagBitsEXT messageSeverity, + VkDebugUtilsMessageTypeFlagsEXT messageType, + VkDebugUtilsMessengerCallbackDataEXT const* callbackData, + [[maybe_unused]] void* userData +) { + auto const severity = to_severity(messageSeverity); + + auto type = to_msg(messageType); + if(type.empty()) type = "UNKNOWN"; + + + std::vector objects{}; + objects.reserve(callbackData->objectCount); + + for(auto const& object : std::span{callbackData->pObjects, callbackData->objectCount}) { + auto const namePtr = object.pObjectName; + objects.emplace_back( + namePtr == nullptr ? "UNKNOWN" : namePtr, + object.objectType, + object.objectHandle + ); + } + + auto const idNamePtr = callbackData->pMessageIdName; + + cth::log::msg( + severity, + "VALIDATION LAYER: {0} {1}:\n NAME: {2}\n\t (CODE: {3})\n{4}\n OBJECTS: {5}\n", + type, + to_string(severity), + idNamePtr == nullptr ? "UNKNOWN" : idNamePtr, + callbackData->messageIdNumber, + callbackData->pMessage, + objects + ); + + + return VK_FALSE; +} +} diff --git a/cvk/src/pch/cvk/pch_def.hpp b/cvk/src/pch/cvk/pch_def.hpp new file mode 100644 index 00000000..7f1e76dc --- /dev/null +++ b/cvk/src/pch/cvk/pch_def.hpp @@ -0,0 +1,9 @@ +#pragma once + +#include +#include + +namespace cvk { +namespace except = cth::except; +namespace log = cth::log; +} diff --git a/cvk/src/pch/glob/cth.hpp b/cvk/src/pch/glob/cth.hpp new file mode 100644 index 00000000..710fa5cf --- /dev/null +++ b/cvk/src/pch/glob/cth.hpp @@ -0,0 +1,9 @@ +#pragma once +#include +#include +#include +#include +#include +#include +#include +#include diff --git a/cvk/src/pch/glob/std.hpp b/cvk/src/pch/glob/std.hpp new file mode 100644 index 00000000..ad7924f4 --- /dev/null +++ b/cvk/src/pch/glob/std.hpp @@ -0,0 +1,27 @@ +#pragma once +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include diff --git a/cvk/src/pch/glob/volk.hpp b/cvk/src/pch/glob/volk.hpp new file mode 100644 index 00000000..e78ae406 --- /dev/null +++ b/cvk/src/pch/glob/volk.hpp @@ -0,0 +1,2 @@ +#pragma once +#include diff --git a/cvk/src/pch/pch.hpp b/cvk/src/pch/pch.hpp new file mode 100644 index 00000000..309c960d --- /dev/null +++ b/cvk/src/pch/pch.hpp @@ -0,0 +1,11 @@ +#pragma once + +#include "glob/cth.hpp" +#include "glob/std.hpp" + +#include "glob/volk.hpp" + +#include "cvk/pch_def.hpp" + +#undef far +#undef near diff --git a/cvk/tests/CMakeLists.txt b/cvk/tests/CMakeLists.txt new file mode 100644 index 00000000..fb077905 --- /dev/null +++ b/cvk/tests/CMakeLists.txt @@ -0,0 +1,17 @@ +cth_glob_cpp(CVK_TESTS_SRC "src") + +add_executable(cvk_tests ${CVK_TESTS_SRC}) + +cth_target_enable_build_cache(cvk_tests OPTIONAL) + +target_include_directories( + cvk_tests + PRIVATE src +) + +find_package(GTest CONFIG REQUIRED) + +target_link_libraries(cvk_tests PRIVATE GTest::gtest_main cvk) + +include(GoogleTest) +gtest_discover_tests(cvk_tests) diff --git a/cvk/tests/src/base/instance_test.cpp b/cvk/tests/src/base/instance_test.cpp new file mode 100644 index 00000000..a13ebd28 --- /dev/null +++ b/cvk/tests/src/base/instance_test.cpp @@ -0,0 +1,95 @@ +#include "test.hpp" + +#include "cvk/base/instance.hpp" + +#include +#include +#include + + +namespace cvk { +using test::APP_NAME; +using test::NO_EXTENSIONS; + +CVK_TEST(Instance, base_constructor_does_not_create) { + Instance const instance{APP_NAME, NO_EXTENSIONS}; + + EXPECT_FALSE(instance.created()); +} + +CVK_TEST(Instance, create_and_destroy) { + Instance instance{APP_NAME, NO_EXTENSIONS}; + + instance.create(); + ASSERT_TRUE(instance.created()); + EXPECT_EQ(volkGetLoadedInstance(), instance.get()); + + instance.destroy(); + EXPECT_FALSE(instance.created()); +} + +CVK_TEST(Instance, constructor_creates) { + Instance const instance{APP_NAME, NO_EXTENSIONS, std::nullopt}; + + EXPECT_TRUE(instance.created()); +} + +CVK_TEST(Instance, create_twice_loads_the_new_instance) { + Instance instance{APP_NAME, NO_EXTENSIONS, std::nullopt}; + + instance.create(); + + ASSERT_TRUE(instance.created()); + EXPECT_EQ(volkGetLoadedInstance(), instance.get()); +} + +CVK_TEST(Instance, reports_required_extensions) { + std::vector const required{VK_KHR_SURFACE_EXTENSION_NAME}; + + Instance const instance{APP_NAME, required}; + + EXPECT_TRUE(std::ranges::contains(instance.requiredExtensions(), std::string{VK_KHR_SURFACE_EXTENSION_NAME})); +} + +CVK_TEST(Instance, missing_extension_throws) { + std::vector const required{"VK_CVK_extension_that_does_not_exist"}; + + auto const construct = [&] { Instance const instance{APP_NAME, required}; }; + + EXPECT_THROW(construct(), cth::except::default_exception); +} + +CVK_TEST(Instance, wrap_takes_ownership) { + VkApplicationInfo const appInfo{ + .sType = VK_STRUCTURE_TYPE_APPLICATION_INFO, + .pNext = nullptr, + .pApplicationName = APP_NAME.data(), + .applicationVersion = 0, + .pEngineName = APP_NAME.data(), + .engineVersion = 0, + .apiVersion = VK_API_VERSION_1_3, + }; + VkInstanceCreateInfo const createInfo{ + .sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO, + .pNext = nullptr, + .flags = 0, + .pApplicationInfo = &appInfo, + .enabledLayerCount = 0, + .ppEnabledLayerNames = nullptr, + .enabledExtensionCount = 0, + .ppEnabledExtensionNames = nullptr, + }; + VkInstance vkInstance = VK_NULL_HANDLE; + ASSERT_EQ(vkCreateInstance(&createInfo, nullptr, &vkInstance), VK_SUCCESS); + + Instance instance{APP_NAME, NO_EXTENSIONS, Instance::State{vkInstance, nullptr}}; + + ASSERT_TRUE(instance.created()); + EXPECT_EQ(instance.get(), vkInstance); + EXPECT_EQ(volkGetLoadedInstance(), vkInstance); + + instance.destroy(); + EXPECT_FALSE(instance.created()); +} + +} diff --git a/cvk/tests/src/debug/debug_messenger_test.cpp b/cvk/tests/src/debug/debug_messenger_test.cpp new file mode 100644 index 00000000..1dc85479 --- /dev/null +++ b/cvk/tests/src/debug/debug_messenger_test.cpp @@ -0,0 +1,119 @@ +#include "test.hpp" + +#include "cvk/base/instance.hpp" +#include "cvk/debug/debug_messenger.hpp" + +#include + + +namespace cvk { +using test::APP_NAME; +using test::NO_EXTENSIONS; + +namespace { + constexpr std::string_view TEST_MESSAGE = "cvk test message"; + + size_t received = 0; + + VKAPI_ATTR VkBool32 VKAPI_CALL count_test_messages( + VkDebugUtilsMessageSeverityFlagBitsEXT, + VkDebugUtilsMessageTypeFlagsEXT, + VkDebugUtilsMessengerCallbackDataEXT const* callbackData, + void* + ) { + if(callbackData->pMessage == TEST_MESSAGE) ++received; + return VK_FALSE; + } + + void submit_test_message(Instance const& instance) { + VkDebugUtilsMessengerCallbackDataEXT const data{ + .sType = VK_STRUCTURE_TYPE_DEBUG_UTILS_MESSENGER_CALLBACK_DATA_EXT, + .pNext = nullptr, + .flags = 0, + .pMessageIdName = nullptr, + .messageIdNumber = 0, + .pMessage = TEST_MESSAGE.data(), + .queueLabelCount = 0, + .pQueueLabels = nullptr, + .cmdBufLabelCount = 0, + .pCmdBufLabels = nullptr, + .objectCount = 0, + .pObjects = nullptr, + }; + + vkSubmitDebugUtilsMessageEXT( + instance.get(), + VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT, + VK_DEBUG_UTILS_MESSAGE_TYPE_GENERAL_BIT_EXT, + &data + ); + } +} + +CVK_TEST(DebugMessenger, forwards_messages_to_the_callback) { + if constexpr(!constants::ENABLE_VALIDATION_LAYERS) GTEST_SKIP() << "needs the validation layers"; + + Instance const instance{APP_NAME, NO_EXTENSIONS, std::nullopt}; + DebugMessenger const messenger{DebugMessenger::Config::Default(count_test_messages), instance}; + ASSERT_TRUE(messenger.created()); + + received = 0; + submit_test_message(instance); + + EXPECT_EQ(received, 1); +} + +CVK_TEST(DebugMessenger, accepts_a_lambda) { + if constexpr(!constants::ENABLE_VALIDATION_LAYERS) GTEST_SKIP() << "needs the validation layers"; + + Instance const instance{APP_NAME, NO_EXTENSIONS, std::nullopt}; + + DebugMessenger::callback_t const callback = []( + VkDebugUtilsMessageSeverityFlagBitsEXT, + VkDebugUtilsMessageTypeFlagsEXT, + VkDebugUtilsMessengerCallbackDataEXT const* callbackData, + void* + ) -> VkBool32 { + if(callbackData->pMessage == TEST_MESSAGE) ++received; + return VK_FALSE; + }; + DebugMessenger const messenger{DebugMessenger::Config::Default(callback), instance}; + + received = 0; + submit_test_message(instance); + + EXPECT_EQ(received, 1); +} + +CVK_TEST(DebugMessenger, destroy_resets) { + if constexpr(!constants::ENABLE_VALIDATION_LAYERS) GTEST_SKIP() << "needs the validation layers"; + + Instance const instance{APP_NAME, NO_EXTENSIONS, std::nullopt}; + DebugMessenger messenger{DebugMessenger::Config::Default(count_test_messages), instance}; + + messenger.destroy(); + EXPECT_FALSE(messenger.created()); + + received = 0; + submit_test_message(instance); + + EXPECT_EQ(received, 0); +} + +CVK_TEST(DebugMessenger, release_keeps_the_messenger) { + if constexpr(!constants::ENABLE_VALIDATION_LAYERS) GTEST_SKIP() << "needs the validation layers"; + + Instance const instance{APP_NAME, NO_EXTENSIONS, std::nullopt}; + DebugMessenger messenger{DebugMessenger::Config::Default(count_test_messages), instance}; + + auto const state = messenger.release(); + EXPECT_FALSE(messenger.created()); + + received = 0; + submit_test_message(instance); + EXPECT_EQ(received, 1); + + DebugMessenger::destroy(state.instance->get(), state.vkMessenger.get()); +} + +} diff --git a/cvk/tests/src/environment.cpp b/cvk/tests/src/environment.cpp new file mode 100644 index 00000000..4346f2e5 --- /dev/null +++ b/cvk/tests/src/environment.cpp @@ -0,0 +1,19 @@ +#include "cvk/base/cvk.hpp" + +#include + + +namespace cvk::test { +namespace { + /** + * @brief loads the vulkan loader once for all tests + */ + class Environment : public testing::Environment { + public: + void SetUp() override { Cvk::init(); } + void TearDown() override { Cvk::terminate(); } + }; + + [[maybe_unused]] testing::Environment* const ENVIRONMENT = testing::AddGlobalTestEnvironment(new Environment{}); +} +} diff --git a/cvk/tests/src/test.hpp b/cvk/tests/src/test.hpp new file mode 100644 index 00000000..326cc44b --- /dev/null +++ b/cvk/tests/src/test.hpp @@ -0,0 +1,14 @@ +#pragma once +#include + +#include +#include +#include + +#define CVK_TEST(suite, test_name) S_TEST(cvk, suite, test_name) + +namespace cvk::test { +constexpr std::string_view APP_NAME = "cvk_tests"; + +inline std::vector const NO_EXTENSIONS{}; +} diff --git a/vcpkg.json b/vcpkg.json index dbd3f367..01133334 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -2,6 +2,7 @@ "dependencies": [ "gtest", "volk", - "vulkan-memory-allocator" + "vulkan-memory-allocator", + "vulkan-utility-libraries" ] } From da550db4b44008c4f16e2efa4db1e6fba6984d12 Mon Sep 17 00:00:00 2001 From: lukas <89145039+codethinki@users.noreply.github.com> Date: Fri, 16 Oct 2026 12:57:37 +0200 Subject: [PATCH 004/135] AGENTS.md: fragments win over full sentences --- AGENTS.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/AGENTS.md b/AGENTS.md index 498cda98..59f88bde 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -202,7 +202,8 @@ Documentation uses doxygen comments and follows ASD-STE100 (Simplified Technical - Use `/** ... */` blocks with one tag per line. - Start after the tag with a lower case letter. - Do not put a period at the end of a line. -- Write short fragments. Start them with a verb in the present tense: `creates the instance`, `destroys and resets`. +- Write short fragments, not full sentences. Start them with a verb in the present tense: `creates the instance`, `destroys and resets`. +- Fragments win over STE100: where STE100 asks for a full sentence or an article, write the shorter fragment. - Use `@ref` for every name of code: `@ref create()`, `@ref Core::created()`. - Do not document what the signature already tells. A getter without conditions needs no comment. From f66b2a76e02366194f459a221101ce839607c6ab Mon Sep 17 00:00:00 2001 From: lukas <89145039+codethinki@users.noreply.github.com> Date: Fri, 16 Oct 2026 13:01:59 +0200 Subject: [PATCH 005/135] AGENTS.md: branch structure --- AGENTS.md | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/AGENTS.md b/AGENTS.md index 59f88bde..36329fa9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -17,9 +17,17 @@ Read it before you change a file. Match the existing code. - Use the Vulkan C API through volk. Do not use Vulkan-Hpp. - Memory comes from VMA. - `cvk` destroys immediately. It has no destruction queue. Deferred destruction belongs to `jvk`. -- Work happens on a local branch. Do not open pull requests for single steps. - Commits have no `Co-Authored-By` or "Generated with" lines. +## Branches + +- `master` is the main branch. The rebuild goes into it when it is finished. +- `v2` is the main branch of the rebuild until then. +- `_v2/jvk_rebuild` is the current work branch. It goes into `v2`. +- Topic branches off the work branch are named `_v2/_jvk_rebuild/`. +- `dev` and the `_dev/...` branches are old. Do not use them. +- Work locally. Do not open pull requests for single steps. + ## Build and test ```bash From 333de52053494e927270efa9cc04aa9ff2f389cd Mon Sep 17 00:00:00 2001 From: lukas <89145039+codethinki@users.noreply.github.com> Date: Fri, 16 Oct 2026 13:08:15 +0200 Subject: [PATCH 006/135] style: no section comments above namespaces, no empty lines at namespace edges --- AGENTS.md | 8 ++------ cvk/incl/cvk/base/instance.hpp | 9 --------- cvk/incl/cvk/debug/debug_messenger.hpp | 5 ----- cvk/incl/cvk/utility/constants.hpp | 2 -- cvk/incl/cvk/utility/vk_exceptions.hpp | 2 -- cvk/src/base/cvk.cpp | 2 -- cvk/src/base/instance.cpp | 2 -- cvk/src/debug/debug_messenger.cpp | 7 ------- cvk/tests/src/base/instance_test.cpp | 1 - cvk/tests/src/debug/debug_messenger_test.cpp | 1 - 10 files changed, 2 insertions(+), 37 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 36329fa9..393efda5 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -53,6 +53,7 @@ ctest --test-dir out/build/gcc_release --output-on-failure - Pointer and reference bind left: `T* ptr`, `T const& ref`. - `const` comes after the type: `T const&`, `auto const`. - Nested namespaces inside a file are indented one level (`namespace dev { ... }`). +- No empty line right after `namespace x {` and no empty line right before its closing `}`. ### Naming @@ -150,16 +151,12 @@ public: After the class, in the same header: ```cpp -//State - namespace cvk { struct Semaphore::State { cvk::vk_not_null vkSemaphore; }; } -//debug checks - namespace cvk { inline void Semaphore::debug_check(Semaphore const& semaphore) { CTH_CRITICAL(!semaphore.created(), "semaphore must be created") {} @@ -197,9 +194,8 @@ inline void Semaphore::debug_check(Semaphore const& semaphore) { - `//TODO` work that is still open - `//TEMP` code that must change before it is final - `//IMPLEMENT` a planned change of the design -- `//State`, `//debug checks`, `//Config` mark the sections after a class -No space after `//` in markers. +No space after `//` in markers. Do not put section comments above namespaces. ## Documentation style diff --git a/cvk/incl/cvk/base/instance.hpp b/cvk/incl/cvk/base/instance.hpp index fd43be6e..01a6f1e4 100644 --- a/cvk/incl/cvk/base/instance.hpp +++ b/cvk/incl/cvk/base/instance.hpp @@ -19,7 +19,6 @@ namespace cvk { - /** * @brief VkInstance wrapper, owns the debug messenger */ @@ -152,13 +151,9 @@ class Instance { static void debug_check(Instance const& instance); static void debug_check_handle(vk_not_null vkInstance); }; - - } -//State - namespace cvk { struct Instance::State { vk_not_null vkInstance; @@ -169,13 +164,9 @@ struct Instance::State { */ std::unique_ptr debugMessenger; }; - } -//debug checks - namespace cvk { - inline void Instance::debug_check(Instance const& instance) { debug_check_handle(instance.get()); } inline void Instance::debug_check_handle([[maybe_unused]] vk_not_null vkInstance) {} } diff --git a/cvk/incl/cvk/debug/debug_messenger.hpp b/cvk/incl/cvk/debug/debug_messenger.hpp index 26251438..7ca692a0 100644 --- a/cvk/incl/cvk/debug/debug_messenger.hpp +++ b/cvk/incl/cvk/debug/debug_messenger.hpp @@ -115,18 +115,13 @@ class DebugMessenger { }; } // namespace cvk -//State - namespace cvk { struct DebugMessenger::State { not_null instance; vk_not_null vkMessenger; }; - } -//debug checks - namespace cvk { inline void DebugMessenger::debug_check(DebugMessenger const& debugMessenger) { CTH_CRITICAL(!debugMessenger.created(), "debug_messenger not created") {} diff --git a/cvk/incl/cvk/utility/constants.hpp b/cvk/incl/cvk/utility/constants.hpp index e42d17ab..e2385c24 100644 --- a/cvk/incl/cvk/utility/constants.hpp +++ b/cvk/incl/cvk/utility/constants.hpp @@ -14,9 +14,7 @@ static constexpr create_t create{}; } namespace cvk::constants { - constexpr bool ENABLE_VALIDATION_LAYERS = DEBUG_MODE; constexpr auto COMPILATION_MODE = cth::COMPILATION_MODE; - } // namespace cvk::constants diff --git a/cvk/incl/cvk/utility/vk_exceptions.hpp b/cvk/incl/cvk/utility/vk_exceptions.hpp index a636f8df..9a4e9b33 100644 --- a/cvk/incl/cvk/utility/vk_exceptions.hpp +++ b/cvk/incl/cvk/utility/vk_exceptions.hpp @@ -8,7 +8,6 @@ namespace cvk { - /** * @brief exception of cvk, the message starts with "cvk: " */ @@ -78,7 +77,6 @@ class vk_result_exception : public cvk_exception { public: [[nodiscard]] VkResult vkResult() const noexcept { return _vkResult; } }; - } #define CVK_STABLE_THROW(expression, msg, ...) \ diff --git a/cvk/src/base/cvk.cpp b/cvk/src/base/cvk.cpp index 3ad40a95..8a3fe6e8 100644 --- a/cvk/src/base/cvk.cpp +++ b/cvk/src/base/cvk.cpp @@ -31,6 +31,4 @@ void Cvk::init() { log::msg("cvk initialized"); _initialized = true; } - - } diff --git a/cvk/src/base/instance.cpp b/cvk/src/base/instance.cpp index b2dbecd4..6c6f7e1a 100644 --- a/cvk/src/base/instance.cpp +++ b/cvk/src/base/instance.cpp @@ -9,7 +9,6 @@ namespace cvk { - using std::span; using std::string; using std::string_view; @@ -221,5 +220,4 @@ void Instance::reset() { } void Instance::loadInstanceFunctions(vk_not_null vkInstance) { volkLoadInstanceOnly(vkInstance.get()); } - } diff --git a/cvk/src/debug/debug_messenger.cpp b/cvk/src/debug/debug_messenger.cpp index 3c802bac..efc5a59d 100644 --- a/cvk/src/debug/debug_messenger.cpp +++ b/cvk/src/debug/debug_messenger.cpp @@ -9,7 +9,6 @@ namespace cvk { - DebugMessenger::DebugMessenger(Config config) : _config{std::move(config)} {} void DebugMessenger::create(Instance const& instance) { @@ -64,11 +63,8 @@ void DebugMessenger::reset() { _instance = nullptr; _handle = VK_NULL_HANDLE; } - } // namespace cvk -//Config - namespace cvk::dev { VKAPI_ATTR VkBool32 VKAPI_CALL default_debug_callback( VkDebugUtilsMessageSeverityFlagBitsEXT messageSeverity, @@ -96,7 +92,6 @@ VkDebugUtilsMessengerCreateInfoEXT DebugMessenger::Config::createInfo() const { .pUserData = nullptr, }; } - } @@ -116,7 +111,6 @@ CTH_FORMAT_CLASS_CUSTOM( namespace cvk::dev { - namespace { auto to_severity(VkDebugUtilsMessageSeverityFlagsEXT vkSeverity) { constexpr std::array severities{ @@ -149,7 +143,6 @@ namespace { return joiner; } - } VKAPI_ATTR VkBool32 VKAPI_CALL default_debug_callback( diff --git a/cvk/tests/src/base/instance_test.cpp b/cvk/tests/src/base/instance_test.cpp index a13ebd28..e1cd5d3c 100644 --- a/cvk/tests/src/base/instance_test.cpp +++ b/cvk/tests/src/base/instance_test.cpp @@ -91,5 +91,4 @@ CVK_TEST(Instance, wrap_takes_ownership) { instance.destroy(); EXPECT_FALSE(instance.created()); } - } diff --git a/cvk/tests/src/debug/debug_messenger_test.cpp b/cvk/tests/src/debug/debug_messenger_test.cpp index 1dc85479..ebb55725 100644 --- a/cvk/tests/src/debug/debug_messenger_test.cpp +++ b/cvk/tests/src/debug/debug_messenger_test.cpp @@ -115,5 +115,4 @@ CVK_TEST(DebugMessenger, release_keeps_the_messenger) { DebugMessenger::destroy(state.instance->get(), state.vkMessenger.get()); } - } From f3e50d3bf980cd0b4bb775ab542b3240bf4a5e3e Mon Sep 17 00:00:00 2001 From: lukas <89145039+codethinki@users.noreply.github.com> Date: Fri, 16 Oct 2026 13:12:25 +0200 Subject: [PATCH 007/135] style: accessors and constants in the first public section --- AGENTS.md | 9 ++++++-- cvk/incl/cvk/base/cvk.hpp | 5 ++--- cvk/incl/cvk/base/instance.hpp | 31 +++++++++++++------------- cvk/incl/cvk/debug/debug_messenger.hpp | 8 +++---- cvk/incl/cvk/utility/vk_exceptions.hpp | 5 ++--- 5 files changed, 30 insertions(+), 28 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 393efda5..09ed42cd 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -117,6 +117,8 @@ class Semaphore { public: struct State; + // constants + explicit Semaphore(Core const& core); // base constructor Semaphore(Core const& core, State const& state); // constructs and wraps explicit Semaphore(Core const& core, create_t); // constructs and creates @@ -133,6 +135,8 @@ public: static void destroy(DeviceTable table, VkSemaphore vkSemaphore); + // accessors: created(), get(), ... + protected: // helpers that derived classes use @@ -141,13 +145,14 @@ private: // members public: - // accessors: created(), get(), ... - // constants // copy and move members // static debug_check functions }; ``` +- The first public section holds the API: types, constants, lifecycle, operations, accessors. +- The last public section holds what every class has and readers rarely need: copy and move members, `debug_check` functions. + After the class, in the same header: ```cpp diff --git a/cvk/incl/cvk/base/cvk.hpp b/cvk/incl/cvk/base/cvk.hpp index 62ab060e..8aa76154 100644 --- a/cvk/incl/cvk/base/cvk.hpp +++ b/cvk/incl/cvk/base/cvk.hpp @@ -19,12 +19,11 @@ class Cvk { */ static void terminate(); + [[nodiscard]] static bool initialized() { return _initialized; } + private: inline static bool _initialized = false; static void terminateVolk(); static void initVolk(); - -public: - [[nodiscard]] static bool initialized() { return _initialized; } }; } diff --git a/cvk/incl/cvk/base/instance.hpp b/cvk/incl/cvk/base/instance.hpp index 01a6f1e4..c88159f1 100644 --- a/cvk/incl/cvk/base/instance.hpp +++ b/cvk/incl/cvk/base/instance.hpp @@ -26,6 +26,15 @@ class Instance { public: struct State; + static constexpr std::array VALIDATION_LAYERS = { + "VK_LAYER_KHRONOS_validation" + }; + static constexpr std::array VALIDATION_LAYER_EXTENSIONS{ + VK_EXT_DEBUG_UTILS_EXTENSION_NAME, + }; + + static constexpr std::array REQUIRED_INSTANCE_EXTENSIONS{}; + /** * @brief base constructor * @details debug builds enable the validation layers @@ -112,6 +121,12 @@ class Instance { static void destroy(VkInstance vkInstance); + [[nodiscard]] bool created() const { return _handle != VK_NULL_HANDLE; } + [[nodiscard]] VkInstance get() const { return _handle.get(); } + [[nodiscard]] std::vector availableExtensions() const { return _availableExt; } + [[nodiscard]] std::vector requiredExtensions() const { return _requiredExt; } + [[nodiscard]] std::vector availableValidationLayers() const { return _availableLayers; } + private: void reset(); @@ -127,27 +142,11 @@ class Instance { static void loadInstanceFunctions(vk_not_null vkInstance); public: - [[nodiscard]] bool created() const { return _handle != VK_NULL_HANDLE; } - [[nodiscard]] VkInstance get() const { return _handle.get(); } - [[nodiscard]] std::vector availableExtensions() const { return _availableExt; } - [[nodiscard]] std::vector requiredExtensions() const { return _requiredExt; } - [[nodiscard]] std::vector availableValidationLayers() const { return _availableLayers; } - - static constexpr std::array VALIDATION_LAYERS = { - "VK_LAYER_KHRONOS_validation" - }; - static constexpr std::array VALIDATION_LAYER_EXTENSIONS{ - VK_EXT_DEBUG_UTILS_EXTENSION_NAME, - }; - - static constexpr std::array REQUIRED_INSTANCE_EXTENSIONS{}; - Instance(Instance const& other) = delete; Instance& operator=(Instance const& other) = delete; Instance(Instance&& other) noexcept = default; Instance& operator=(Instance&& other) noexcept = default; - static void debug_check(Instance const& instance); static void debug_check_handle(vk_not_null vkInstance); }; diff --git a/cvk/incl/cvk/debug/debug_messenger.hpp b/cvk/incl/cvk/debug/debug_messenger.hpp index 7ca692a0..5aa3ad73 100644 --- a/cvk/incl/cvk/debug/debug_messenger.hpp +++ b/cvk/incl/cvk/debug/debug_messenger.hpp @@ -92,6 +92,10 @@ class DebugMessenger { [[nodiscard]] VkDebugUtilsMessengerCreateInfoEXT createInfo() const; }; + [[nodiscard]] bool created() const { return _handle != VK_NULL_HANDLE; } + [[nodiscard]] VkDebugUtilsMessengerEXT get() const { return _handle.get(); } + [[nodiscard]] Config config() const { return _config; } + protected: Instance const* _instance = nullptr; Config _config; @@ -102,10 +106,6 @@ class DebugMessenger { move_ptr _handle = VK_NULL_HANDLE; public: - [[nodiscard]] bool created() const { return _handle != VK_NULL_HANDLE; } - [[nodiscard]] VkDebugUtilsMessengerEXT get() const { return _handle.get(); } - [[nodiscard]] Config config() const { return _config; } - DebugMessenger(DebugMessenger const& other) = delete; DebugMessenger(DebugMessenger&& other) noexcept = default; DebugMessenger& operator=(DebugMessenger const& other) = delete; diff --git a/cvk/incl/cvk/utility/vk_exceptions.hpp b/cvk/incl/cvk/utility/vk_exceptions.hpp index 9a4e9b33..39cf20ea 100644 --- a/cvk/incl/cvk/utility/vk_exceptions.hpp +++ b/cvk/incl/cvk/utility/vk_exceptions.hpp @@ -71,11 +71,10 @@ class vk_result_exception : public cvk_exception { return cvk_exception::add("VkResult: ({})", result); } + [[nodiscard]] VkResult vkResult() const noexcept { return _vkResult; } + private: VkResult _vkResult = VK_RESULT_MAX_ENUM; - -public: - [[nodiscard]] VkResult vkResult() const noexcept { return _vkResult; } }; } From d19d843dd38e530dca47c2e7bebb0a95bc25bd40 Mon Sep 17 00:00:00 2001 From: lukas <89145039+codethinki@users.noreply.github.com> Date: Fri, 16 Oct 2026 13:12:49 +0200 Subject: [PATCH 008/135] AGENTS.md: move only rule for handle classes --- AGENTS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/AGENTS.md b/AGENTS.md index 09ed42cd..f2df8f44 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -152,6 +152,7 @@ public: - The first public section holds the API: types, constants, lifecycle, operations, accessors. - The last public section holds what every class has and readers rarely need: copy and move members, `debug_check` functions. +- Classes that own a handle are move only. A class that differs (copyable, not movable, custom move) says so in its `@brief`. After the class, in the same header: From 538755b3e8c095e4b49bf80008de84b5096df0b8 Mon Sep 17 00:00:00 2001 From: lukas <89145039+codethinki@users.noreply.github.com> Date: Fri, 16 Oct 2026 13:20:35 +0200 Subject: [PATCH 009/135] vcpkg: current baseline for vulkan 1.4 headers, AGENTS.md: free functions are snake_case --- AGENTS.md | 3 ++- vcpkg-configuration.json | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index f2df8f44..a7dfae11 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -63,7 +63,8 @@ ctest --test-dir out/build/gcc_release --output-on-failure | nested config and state | `Config`, `State` | `Instance::State` | | generic library type, trait, concept | `snake_case` | `basic_deletable`, `handle_t`, `destroyable` | | type alias | `snake_case_t` | `callback_t`, `queue_set_t` | -| function, member function | `camelCase` | `optDestroy()`, `createInfo()` | +| member function | `camelCase` | `optDestroy()`, `createInfo()` | +| free function | `snake_case` | `to_queue_properties()`, `default_debug_callback()` | | static check function | `debug_check`, `debug_check_handle` | | | function parameter | `camelCase` | `appName`, `requiredExtensions` | | local variable | `camelCase` | `createResult`, `requiredExtVec` | diff --git a/vcpkg-configuration.json b/vcpkg-configuration.json index 324e898b..bbd85de6 100644 --- a/vcpkg-configuration.json +++ b/vcpkg-configuration.json @@ -1,7 +1,7 @@ { "default-registry": { "kind": "git", - "baseline": "80d54ff62d528339c626a6fbc3489a7f25956ade", + "baseline": "3cbc1db4d867ec83c89fba4c461321c11f78b5e3", "repository": "https://github.com/microsoft/vcpkg" }, "registries": [ From e9d14d9ad175653e846d99a917c5370413c646bf Mon Sep 17 00:00:00 2001 From: lukas <89145039+codethinki@users.noreply.github.com> Date: Fri, 16 Oct 2026 13:38:00 +0200 Subject: [PATCH 010/135] cmake: link boost-pfr, the cth package does not export it --- cvk/CMakeLists.txt | 4 +++- vcpkg.json | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/cvk/CMakeLists.txt b/cvk/CMakeLists.txt index a41d2c32..d7ca01bb 100644 --- a/cvk/CMakeLists.txt +++ b/cvk/CMakeLists.txt @@ -20,8 +20,10 @@ target_precompile_headers( find_package(volk CONFIG REQUIRED) find_package(VulkanUtilityLibraries CONFIG REQUIRED) find_package(cth CONFIG REQUIRED COMPONENTS cth) +# cth/hash/hash_aggregates.hpp includes boost/pfr.hpp, the cth package does not export it +find_package(boost_pfr CONFIG REQUIRED) -target_link_libraries(cvk PUBLIC volk::volk Vulkan::UtilityHeaders cth::cth) +target_link_libraries(cvk PUBLIC volk::volk Vulkan::UtilityHeaders cth::cth Boost::pfr) # tests if(JOLLY_ENABLE_TESTS) diff --git a/vcpkg.json b/vcpkg.json index 01133334..6a7a7773 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -1,5 +1,6 @@ { "dependencies": [ + "boost-pfr", "gtest", "volk", "vulkan-memory-allocator", From 1831b3706b904c5c03b80d007ea75b73053b6a3e Mon Sep 17 00:00:00 2001 From: lukas <89145039+codethinki@users.noreply.github.com> Date: Fri, 16 Oct 2026 13:38:00 +0200 Subject: [PATCH 011/135] fix(debug_messenger): store the VkInstance, the instance can move The messenger kept an Instance const*. Instance is movable, so after a move the messenger destroyed itself through the moved-from instance (VK_NULL_HANDLE). --- cvk/incl/cvk/debug/debug_messenger.hpp | 7 +++++-- cvk/src/debug/debug_messenger.cpp | 10 +++++----- cvk/tests/src/debug/debug_messenger_test.cpp | 2 +- 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/cvk/incl/cvk/debug/debug_messenger.hpp b/cvk/incl/cvk/debug/debug_messenger.hpp index 5aa3ad73..a0daef3b 100644 --- a/cvk/incl/cvk/debug/debug_messenger.hpp +++ b/cvk/incl/cvk/debug/debug_messenger.hpp @@ -97,7 +97,10 @@ class DebugMessenger { [[nodiscard]] Config config() const { return _config; } protected: - Instance const* _instance = nullptr; + /** + * @brief handle, not the wrapper: the instance can move + */ + VkInstance _vkInstance = VK_NULL_HANDLE; Config _config; private: @@ -117,7 +120,7 @@ class DebugMessenger { namespace cvk { struct DebugMessenger::State { - not_null instance; + vk_not_null vkInstance; vk_not_null vkMessenger; }; } diff --git a/cvk/src/debug/debug_messenger.cpp b/cvk/src/debug/debug_messenger.cpp index efc5a59d..ac96bf8b 100644 --- a/cvk/src/debug/debug_messenger.cpp +++ b/cvk/src/debug/debug_messenger.cpp @@ -17,7 +17,7 @@ void DebugMessenger::create(Instance const& instance) { optDestroy(); - _instance = &instance; + _vkInstance = instance.get(); auto const info = _config.createInfo(); @@ -25,7 +25,7 @@ void DebugMessenger::create(Instance const& instance) { VkDebugUtilsMessengerEXT ptr = VK_NULL_HANDLE; - auto const createResult = vkCreateDebugUtilsMessengerEXT(_instance->get(), &info, nullptr, &ptr); + auto const createResult = vkCreateDebugUtilsMessengerEXT(_vkInstance, &info, nullptr, &ptr); CTH_STABLE_ERR(createResult != VK_SUCCESS, "failed to set up debug messenger") { reset(); @@ -38,7 +38,7 @@ void DebugMessenger::create(Instance const& instance) { void DebugMessenger::destroy() { debug_check(*this); - destroy(_instance->get(), _handle.get()); + destroy(_vkInstance, _handle.get()); reset(); } @@ -54,13 +54,13 @@ void DebugMessenger::destroy(vk_not_null vkInstance, VkDebugUtilsMes DebugMessenger::State DebugMessenger::release() { debug_check(*this); - State const state{_instance, _handle.get()}; + State const state{_vkInstance, _handle.get()}; reset(); return state; } void DebugMessenger::reset() { - _instance = nullptr; + _vkInstance = VK_NULL_HANDLE; _handle = VK_NULL_HANDLE; } } // namespace cvk diff --git a/cvk/tests/src/debug/debug_messenger_test.cpp b/cvk/tests/src/debug/debug_messenger_test.cpp index ebb55725..529bd55e 100644 --- a/cvk/tests/src/debug/debug_messenger_test.cpp +++ b/cvk/tests/src/debug/debug_messenger_test.cpp @@ -113,6 +113,6 @@ CVK_TEST(DebugMessenger, release_keeps_the_messenger) { submit_test_message(instance); EXPECT_EQ(received, 1); - DebugMessenger::destroy(state.instance->get(), state.vkMessenger.get()); + DebugMessenger::destroy(state.vkInstance, state.vkMessenger.get()); } } From d7e8a005cd955019b1a9fc425a81ee25e240313d Mon Sep 17 00:00:00 2001 From: lukas <89145039+codethinki@users.noreply.github.com> Date: Fri, 16 Oct 2026 13:38:00 +0200 Subject: [PATCH 012/135] cvk: port physical device, surface, device, queue and core - physical device features as one pNext chain for vulkan 1.0 to 1.3 - surface without the destruction queue, formats keep the allowed order - physical device takes surfaces, a family presents only if it presents to every surface - device enables swapchainMaintenance1 with the present extensions - queues hold the function table, not the device, so a device can move - core takes the instance and the surfaces, no hidden windows - core: empty queue sets use the default set - tests for each type, present tests use VK_EXT_headless_surface --- cvk/incl/cvk/base/core.hpp | 209 +++++++++++ cvk/incl/cvk/base/core_config.hpp | 56 +++ cvk/incl/cvk/base/device.hpp | 178 +++++++++ cvk/incl/cvk/base/device_config.hpp | 14 + cvk/incl/cvk/base/device_table.hpp | 14 + cvk/incl/cvk/base/physical_device.hpp | 264 +++++++++++++ cvk/incl/cvk/base/queue/queue.hpp | 116 ++++++ cvk/incl/cvk/base/queue/queue_family.hpp | 59 +++ cvk/incl/cvk/base/queue/queue_state.hpp | 39 ++ cvk/incl/cvk/surface/surface.hpp | 144 ++++++++ cvk/incl/cvk/surface/surface_config.hpp | 28 ++ .../cvk/utility/constant/device_constants.hpp | 48 +++ .../device/physical_device_features.hpp | 151 ++++++++ cvk/incl/cvk/utility/format.hpp | 9 + cvk/incl/cvk/utility/vk_hash.hpp | 7 + cvk/incl/cvk/utility/vk_overloads.hpp | 21 ++ cvk/src/base/core.cpp | 187 ++++++++++ cvk/src/base/device.cpp | 190 ++++++++++ cvk/src/base/physical_device.cpp | 346 ++++++++++++++++++ cvk/src/base/queue/queue.cpp | 85 +++++ cvk/src/surface/surface.cpp | 162 ++++++++ .../device/physical_device_features.cpp | 56 +++ cvk/tests/src/base/core_test.cpp | 183 +++++++++ cvk/tests/src/base/device_test.cpp | 186 ++++++++++ cvk/tests/src/base/physical_device_test.cpp | 150 ++++++++ cvk/tests/src/base/queue/queue_test.cpp | 110 ++++++ cvk/tests/src/headless.cpp | 41 +++ cvk/tests/src/headless.hpp | 32 ++ cvk/tests/src/surface/surface_test.cpp | 103 ++++++ .../device/physical_device_features_test.cpp | 105 ++++++ 30 files changed, 3293 insertions(+) create mode 100644 cvk/incl/cvk/base/core.hpp create mode 100644 cvk/incl/cvk/base/core_config.hpp create mode 100644 cvk/incl/cvk/base/device.hpp create mode 100644 cvk/incl/cvk/base/device_config.hpp create mode 100644 cvk/incl/cvk/base/device_table.hpp create mode 100644 cvk/incl/cvk/base/physical_device.hpp create mode 100644 cvk/incl/cvk/base/queue/queue.hpp create mode 100644 cvk/incl/cvk/base/queue/queue_family.hpp create mode 100644 cvk/incl/cvk/base/queue/queue_state.hpp create mode 100644 cvk/incl/cvk/surface/surface.hpp create mode 100644 cvk/incl/cvk/surface/surface_config.hpp create mode 100644 cvk/incl/cvk/utility/constant/device_constants.hpp create mode 100644 cvk/incl/cvk/utility/device/physical_device_features.hpp create mode 100644 cvk/incl/cvk/utility/vk_hash.hpp create mode 100644 cvk/incl/cvk/utility/vk_overloads.hpp create mode 100644 cvk/src/base/core.cpp create mode 100644 cvk/src/base/device.cpp create mode 100644 cvk/src/base/physical_device.cpp create mode 100644 cvk/src/base/queue/queue.cpp create mode 100644 cvk/src/surface/surface.cpp create mode 100644 cvk/src/utility/device/physical_device_features.cpp create mode 100644 cvk/tests/src/base/core_test.cpp create mode 100644 cvk/tests/src/base/device_test.cpp create mode 100644 cvk/tests/src/base/physical_device_test.cpp create mode 100644 cvk/tests/src/base/queue/queue_test.cpp create mode 100644 cvk/tests/src/headless.cpp create mode 100644 cvk/tests/src/headless.hpp create mode 100644 cvk/tests/src/surface/surface_test.cpp create mode 100644 cvk/tests/src/utility/device/physical_device_features_test.cpp diff --git a/cvk/incl/cvk/base/core.hpp b/cvk/incl/cvk/base/core.hpp new file mode 100644 index 00000000..c8601593 --- /dev/null +++ b/cvk/incl/cvk/base/core.hpp @@ -0,0 +1,209 @@ +#pragma once +#include "core_config.hpp" +#include "device_table.hpp" + +#include "cvk/base/queue/queue.hpp" +#include "cvk/utility/types.hpp" + +#include + +#include + +#include +#include +#include +#include + + +namespace cvk { +class Device; +class Instance; +class PhysicalDevice; +class Surface; + +/** + * @brief picks the physical device and owns it with the device and the queues + * @details the instance outlives the core + */ +class Core { +public: + using Config = CoreConfig; + using queue_set_t = Config::queue_set_t; + struct State; + + /** + * @brief base constructor + * @pre @ref Instance::created() + */ + explicit Core(Instance const& instance); + + /** + * @brief constructs and creates + * @details calls: + * - @ref Core(Instance const&) + * - @ref create() + */ + Core(Instance const& instance, std::span surfaces, Config const& config); + + /** + * @brief constructs and wraps + * @details calls: + * - @ref Core(Instance const&) + * - @ref wrap() + */ + Core(Instance const& instance, State state); + + /** + * @note calls @ref optDestroy() + */ + ~Core(); + + /** + * @brief wraps the state + * @details empty @ref State::queues -> copies the queues of the device + * @post @ref created() + * @note calls @ref optDestroy() + */ + void wrap(State state); + + /** + * @brief picks a physical device for the first possible queue set, creates the device and the queues + * @param surfaces the present queues present to every surface + * @post @ref created() + * @note calls @ref optDestroy() + * @throws cvk::cvk_exception reason: no physical device meets the requirements + * @throws cvk::vk_result_exception result of vkCreateDevice() + */ + void create(std::span surfaces, Config const& config); + + /** + * @brief destroys and resets + * @pre @ref created() + */ + void destroy(); + + /** + * @brief if @ref created() -> calls @ref destroy() + */ + void optDestroy() { if(created()) destroy(); } + + /** + * @brief releases ownership, returns the state and resets + * @pre @ref created() + */ + State release(); + + [[nodiscard]] bool created() const { return _device != nullptr; } + + /** + * @return index of the used queue set in @ref Config::queueSets + * @pre @ref created() + * @pre the config had queue sets + */ + [[nodiscard]] size_t queueSetIndex() const; + + [[nodiscard]] Instance const& instance() const { return *_instance; } + [[nodiscard]] VkInstance vkInstance() const; + + /** + * @pre @ref created() + */ + [[nodiscard]] PhysicalDevice const& physicalDevice() const; + [[nodiscard]] VkPhysicalDevice vkPhysicalDevice() const; + + /** + * @pre @ref created() + */ + [[nodiscard]] Device const& device() const; + [[nodiscard]] DeviceTable deviceTable() const; + [[nodiscard]] VolkDeviceTable const* functions() const; + [[nodiscard]] VkDevice vkDevice() const; + + /** + * @param idx index in @ref Config::queueProperties + * @pre @ref created() + */ + [[nodiscard]] auto& queue(this auto& self, size_t idx) { + debug_check(self); + return self._queues[idx]; + } + + [[nodiscard]] std::span queues() const { return _queues; } + +private: + struct queue_mappings { + std::vector uniqueQueueFamilyIndices; + std::vector queuesToUniqueQueues; + }; + struct queue_set_relations { + std::vector uniqueQueues; + std::vector queueToUniqueQueue; + }; + + void reset(); + + /** + * @post sets @ref _physicalDevice and @ref _queueSetIndex + * @throws cvk::cvk_exception reason: no physical device meets the requirements + */ + [[nodiscard]] queue_mappings createPhysicalDevice(std::span surfaces, Config const& config); + + [[nodiscard]] static queue_set_relations createQueueSetRelations( + queue_set_t const& queueSet, + std::span queueProperties + ); + + /** + * @return std::nullopt -> no physical device supports the queue set + */ + [[nodiscard]] std::optional tryCreatePhysicalDevice( + std::span surfaces, + Config const& config, + queue_set_t const& queueSet + ); + + void createQueues(std::span queuesToUniqueQueues); + + + not_null _instance; + + std::unique_ptr _physicalDevice; + std::unique_ptr _device; + std::optional _queueSetIndex; + std::vector _queues; + +public: + Core(Core const& other) = delete; + Core(Core&& other) noexcept; + Core& operator=(Core const& other) = delete; + Core& operator=(Core&& other) noexcept; + + static void debug_check(Core const& core); +}; +} + +namespace cvk { +struct Core::State { + /** + * @pre @ref PhysicalDevice::created() + */ + unique_not_null physicalDevice; + /** + * @pre @ref Device::created() + * @pre created with @ref physicalDevice + */ + unique_not_null device; + /** + * @brief queue per entry of @ref Config::queueProperties + * @details empty -> the queues of @ref device + */ + std::vector queues{}; + std::optional queueSetIndex = std::nullopt; +}; +} + +namespace cvk { +inline void Core::debug_check(Core const& core) { + CTH_CRITICAL(!core.created(), "core must be created") {} +} +} diff --git a/cvk/incl/cvk/base/core_config.hpp b/cvk/incl/cvk/base/core_config.hpp new file mode 100644 index 00000000..ecfc4089 --- /dev/null +++ b/cvk/incl/cvk/base/core_config.hpp @@ -0,0 +1,56 @@ +#pragma once +#include "queue/queue_family.hpp" + +#include "cvk/utility/device/physical_device_features.hpp" + +#include +#include + +#include +#include +#include + + +namespace cvk { +struct CoreConfig { + using queue_set_t = cth::dt::union_find; + + /** + * @brief properties of each queue to create + */ + std::vector queueProperties; + + /** + * @brief groups of queues, the first possible set is used, a group shares one vulkan queue + * @details each root is a group, the properties of a group are combined, + * groups go to different queue families where possible + * @note empty -> every queue is a group, same as {0, ..., N - 1} + */ + std::vector queueSets{}; + + /** + * @brief device extensions in addition to the defaults of @ref PhysicalDevice::AutoPick() + */ + std::vector requiredExtensions{}; + + /** + * @brief device features in addition to the defaults of @ref PhysicalDevice::AutoPick() + */ + utils::PhysicalDeviceFeatures requiredFeatures{}; + + static void debug_check(CoreConfig const& config); +}; +} + +namespace cvk { +inline void CoreConfig::debug_check(CoreConfig const& config) { + CTH_CRITICAL(config.queueProperties.empty(), "at least one queue is required") {} + CTH_CRITICAL( + std::ranges::any_of( + config.queueSets, + [queues = config.queueProperties.size()](queue_set_t const& set) { return set.size() != queues; } + ), + "sets must span all queues" + ) {} +} +} diff --git a/cvk/incl/cvk/base/device.hpp b/cvk/incl/cvk/base/device.hpp new file mode 100644 index 00000000..84ab45fb --- /dev/null +++ b/cvk/incl/cvk/base/device.hpp @@ -0,0 +1,178 @@ +#pragma once +#include "device_config.hpp" +#include "device_table.hpp" + +#include "cvk/base/queue/queue.hpp" +#include "cvk/utility/types.hpp" + +#include + +#include + +#include +#include +#include +#include + + +namespace cvk { +class Instance; +class PhysicalDevice; + +/** + * @brief VkDevice wrapper, owns the function table and the queues + */ +class Device { + using queue_families_queue_counts_t = std::unordered_map; + +public: + using Config = DeviceConfig; + struct State; + + /** + * @brief base constructor + * @pre @ref Instance::created() + * @pre @ref PhysicalDevice::created() + */ + Device(Instance const& instance, PhysicalDevice const& physicalDevice); + + /** + * @brief constructs and wraps + * @details calls: + * - @ref Device(Instance const&, PhysicalDevice const&) + * - @ref wrap() + */ + Device(Instance const& instance, PhysicalDevice const& physicalDevice, State state); + + /** + * @brief constructs and creates + * @details calls: + * - @ref Device(Instance const&, PhysicalDevice const&) + * - @ref create() + */ + Device(Instance const& instance, PhysicalDevice const& physicalDevice, Config const& config); + + /** + * @note calls @ref optDestroy() + */ + ~Device() { optDestroy(); } + + /** + * @brief wraps the state + * @details nullptr @ref State::functionTable -> loads the functions + * @post @ref created() + * @note calls @ref optDestroy() + * @note does not retrieve queues + */ + void wrap(State state); + + /** + * @brief creates the device and retrieves the queues + * @details enables the required extensions and features of the physical device, + * VK_KHR_swapchain_maintenance1 required -> enables the swapchainMaintenance1 feature + * @post @ref created() + * @note calls @ref optDestroy() + * @throws cvk::vk_result_exception result of vkCreateDevice() + */ + void create(Config const& config); + + /** + * @brief destroys and resets + * @pre @ref created() + */ + void destroy(); + + /** + * @brief if @ref created() -> calls @ref destroy() + */ + void optDestroy() { if(created()) destroy(); } + + /** + * @brief releases ownership, returns the state and resets + * @pre @ref created() + */ + State release(); + + /** + * @brief blocks until every queue of the device finished its work + * @pre @ref created() + * @throws cvk::vk_result_exception result of vkDeviceWaitIdle() + */ + void waitIdle() const; + + /** + * @param destroy vkDestroyDevice of the function table of @ref vkDevice + */ + static void destroy(VkDevice vkDevice, PFN_vkDestroyDevice destroy); + + [[nodiscard]] std::span queues() const { return _queues; } + [[nodiscard]] std::span queues() { return _queues; } + + [[nodiscard]] DeviceTable table() const { return DeviceTable{_handle.get(), _functionTable.get()}; } + [[nodiscard]] VolkDeviceTable const* functions() const { return _functionTable.get(); } + [[nodiscard]] VkDevice get() const { return _handle.get(); } + + [[nodiscard]] Instance const& instance() const { return *_instance; } + [[nodiscard]] PhysicalDevice const& physicalDevice() const { return *_physicalDevice; } + + [[nodiscard]] bool created() const { return _handle != VK_NULL_HANDLE; } + +private: + void reset(); + + [[nodiscard]] static queue_families_queue_counts_t calcQueueFamiliesQueueCounts( + std::span familyIndices + ); + + /** + * @throws cvk::vk_result_exception result of vkCreateDevice() + */ + void createLogicalDevice(queue_families_queue_counts_t const& queueFamiliesQueueCounts); + + void loadFunctionTable() const; + + /** + * @brief retrieves the queues + * @param familyIndices family index of each queue + */ + void createQueues(std::span familyIndices); + + + not_null _instance; + not_null _physicalDevice; + + move_ptr _handle = VK_NULL_HANDLE; + std::unique_ptr _functionTable = std::make_unique(); + + std::vector _queues{}; + +public: + Device(Device const& other) = delete; + Device(Device&& other) noexcept = default; + Device& operator=(Device const& other) = delete; + Device& operator=(Device&& other) noexcept = default; + + static void debug_check(Device const& device); + static void debug_check_handle(vk_not_null vkDevice); +}; +} + +namespace cvk { +struct Device::State { + vk_not_null vkDevice; + /** + * @brief volk function table of @ref vkDevice + * @details nullptr -> @ref wrap() loads it + */ + std::unique_ptr functionTable; +}; +} + +namespace cvk { +inline void Device::debug_check(Device const& device) { + CTH_CRITICAL(!device.created(), "device must be created") {} + debug_check_handle(device.get()); +} + +inline void Device::debug_check_handle([[maybe_unused]] vk_not_null vkDevice) {} +} diff --git a/cvk/incl/cvk/base/device_config.hpp b/cvk/incl/cvk/base/device_config.hpp new file mode 100644 index 00000000..97f4a827 --- /dev/null +++ b/cvk/incl/cvk/base/device_config.hpp @@ -0,0 +1,14 @@ +#pragma once +#include "queue/queue_family.hpp" + +#include + +namespace cvk { +struct DeviceConfig { + /** + * @brief family index of every queue to create + * @details see @ref PhysicalDevice::Create() + */ + std::vector queueFamilyIndices; +}; +} diff --git a/cvk/incl/cvk/base/device_table.hpp b/cvk/incl/cvk/base/device_table.hpp new file mode 100644 index 00000000..b13045fa --- /dev/null +++ b/cvk/incl/cvk/base/device_table.hpp @@ -0,0 +1,14 @@ +#pragma once +#include "cvk/utility/types.hpp" + +#include + +namespace cvk { +struct DeviceTable { + vk_not_null vkDevice; + not_null table; + + VolkDeviceTable const* operator ->() const { return table.get(); } + [[nodiscard]] VkDevice device() const { return vkDevice.get(); } +}; +} diff --git a/cvk/incl/cvk/base/physical_device.hpp b/cvk/incl/cvk/base/physical_device.hpp new file mode 100644 index 00000000..9481e177 --- /dev/null +++ b/cvk/incl/cvk/base/physical_device.hpp @@ -0,0 +1,264 @@ +#pragma once +#include "cvk/base/queue/queue_family.hpp" +#include "cvk/utility/types.hpp" +#include "cvk/utility/device/physical_device_features.hpp" + +#include + +#include + +#include +#include +#include +#include + + +namespace cvk { +class Instance; +class Surface; + +/** + * @brief VkPhysicalDevice wrapper with the queried properties + * @details copyable, the instance owns the physical device + */ +class PhysicalDevice { +public: + struct State; + struct CreateResult; + + /** + * @brief base constructor + * @pre @ref Instance::created() + */ + explicit PhysicalDevice( + Instance const& instance, + utils::PhysicalDeviceFeatures requiredFeatures, + std::span requiredExtensions + ); + + /** + * @brief constructs and creates + * @details calls: + * - @ref PhysicalDevice(Instance const&, utils::PhysicalDeviceFeatures, std::span) + * - @ref create(std::span, vk_not_null) + */ + explicit PhysicalDevice( + Instance const& instance, + utils::PhysicalDeviceFeatures const& requiredFeatures, + std::span requiredExtensions, + std::span surfaces, + vk_not_null vkDevice + ); + + /** + * @brief constructs and wraps + * @details calls: + * - @ref PhysicalDevice(Instance const&, utils::PhysicalDeviceFeatures, std::span) + * - @ref wrap() + */ + explicit PhysicalDevice( + Instance const& instance, + utils::PhysicalDeviceFeatures const& requiredFeatures, + std::span requiredExtensions, + State const& state + ); + + ~PhysicalDevice() = default; + + /** + * @brief constructs and creates if the device meets the requirements + * @param queueProperties passed to @ref suitable() + * @return std::nullopt -> not @ref suitable() + */ + [[nodiscard]] static std::optional Create( + Instance const& instance, + std::span surfaces, + std::span queueProperties, + std::span requiredExtensions, + utils::PhysicalDeviceFeatures const& requiredFeatures, + vk_not_null vkDevice + ); + + /** + * @brief wraps the state + * @details empty or std::nullopt members of @ref state -> queries them + * @post @ref created() + */ + void wrap(State const& state); + + /** + * @brief queries the device + * @param surfaces to query the present support of the queue families, empty -> no present support + * @post @ref created() + */ + void create(std::span surfaces, vk_not_null vkDevice); + + /** + * @brief queries the device + * @param queueFamilies of the device + * @post @ref created() + */ + void create(std::span queueFamilies, vk_not_null vkDevice); + + /** + * @brief enumerates the devices and picks the first that meets the requirements + * @param queueProperties queues the device must support + * @return std::nullopt -> no device meets the requirements + * @details adds to the requirements: + * - @ref constants::REQUIRED_DEVICE_EXTENSIONS and @ref constants::required_device_features() + * - a PRESENT queue -> @ref constants::PRESENT_DEVICE_EXTENSIONS + */ + [[nodiscard]] static std::optional AutoPick( + Instance const& instance, + std::span surfaces, + std::span queueProperties, + std::span requiredExtensions = {}, + utils::PhysicalDeviceFeatures const& requiredFeatures = {} + ); + + /** + * @return the device has the required features, the required extensions and the queues + * @pre @ref created() + */ + [[nodiscard]] bool suitable(std::span queueProperties) const; + + /** + * @return required features the device does not have + * @pre @ref created() + */ + [[nodiscard]] std::vector supports( + utils::PhysicalDeviceFeatures const& requiredFeatures + ) const; + + /** + * @return required extensions the device does not have + */ + [[nodiscard]] std::vector supports(std::span requiredExtensions) const; + + /** + * @return first of @ref candidates that supports @ref features with @ref tiling + * @pre @ref created() + * @throws cvk::cvk_exception reason: no candidate supports the features + */ + [[nodiscard]] VkFormat findSupportedFormat( + std::span candidates, + VkImageTiling tiling, + VkFormatFeatureFlags features + ) const; + + /** + * @brief finds queue families for the requested queues + * @param queueProperties requested queues + * @return family index per requested queue, empty -> no combination + * @pre @ref created() + */ + [[nodiscard]] std::vector queueFamilyIndices( + std::span queueProperties + ) const; + + /** + * @pre @ref created() + */ + [[nodiscard]] bool supportsQueueSet(std::span queueProperties) const; + + /** + * @throws cvk::vk_result_exception result of vkEnumeratePhysicalDevices() + */ + [[nodiscard]] static std::vector enumerateVkDevices(vk_not_null vkInstance); + + /** + * @throws cvk::vk_result_exception result of vkEnumerateDeviceExtensionProperties() + */ + [[nodiscard]] static std::vector queryExtensions(vk_not_null vkDevice); + + [[nodiscard]] static VkPhysicalDeviceProperties queryProperties(vk_not_null vkDevice); + + [[nodiscard]] static VkPhysicalDeviceMemoryProperties queryMemoryProperties( + vk_not_null vkDevice + ); + + /** + * @param surfaces a family presents only if it presents to every surface, empty -> no present support + */ + [[nodiscard]] static std::vector queryQueueFamilies( + std::span surfaces, + vk_not_null vkDevice + ); + + [[nodiscard]] static VkSampleCountFlagBits evalMaxSampleCount(VkPhysicalDeviceProperties const& properties); + + [[nodiscard]] bool created() const { return _handle != nullptr; } + [[nodiscard]] VkPhysicalDevice get() const { return _handle.get(); } + [[nodiscard]] auto const& features() const { return _features; } + [[nodiscard]] auto const& requiredFeatures() const { return _requiredFeatures; } + [[nodiscard]] std::span extensions() const { return _extensions; } + [[nodiscard]] std::span requiredExtensions() const { return _requiredExtensions; } + [[nodiscard]] auto const& properties() const { return _properties; } + [[nodiscard]] auto const& memProperties() const { return _memProperties; } + [[nodiscard]] VkSampleCountFlagBits maxSampleCount() const { return _maxSampleCount; } + [[nodiscard]] VkPhysicalDeviceLimits const& limits() const { return _properties.limits; } + + /** + * @param idx queue family index + * @return nullptr -> no family with @ref idx + */ + [[nodiscard]] QueueFamily const* queueFamily(queue_family_index_t idx) const; + + [[nodiscard]] std::span queueFamilies() const { return _queueFamilies; } + +private: + not_null _instance; + utils::PhysicalDeviceFeatures _requiredFeatures; + std::vector _requiredExtensions{}; + + move_ptr _handle; + + utils::PhysicalDeviceFeatures _features{}; + std::vector _extensions{}; + VkPhysicalDeviceProperties _properties{}; + VkSampleCountFlagBits _maxSampleCount = VK_SAMPLE_COUNT_FLAG_BITS_MAX_ENUM; + + VkPhysicalDeviceMemoryProperties _memProperties{}; + + std::vector _queueFamilies{}; + +public: + PhysicalDevice(PhysicalDevice const& other) = default; + PhysicalDevice(PhysicalDevice&& other) noexcept = default; + PhysicalDevice& operator=(PhysicalDevice const& other) = default; + PhysicalDevice& operator=(PhysicalDevice&& other) noexcept = default; + + static void debug_check(PhysicalDevice const& device); + static void debug_check_handle(vk_not_null vkDevice); +}; +} + +namespace cvk { +struct PhysicalDevice::State { + vk_not_null vkDevice; + /** + * @details empty -> queried without present support + */ + std::vector queueFamilies; + + std::optional features = std::nullopt; + std::vector extensions{}; + std::optional properties = std::nullopt; + VkSampleCountFlagBits maxSampleCount = VK_SAMPLE_COUNT_FLAG_BITS_MAX_ENUM; + std::optional memProperties = std::nullopt; +}; + +struct PhysicalDevice::CreateResult { + PhysicalDevice device; + std::vector queueFamilyIndices; +}; +} + +namespace cvk { +inline void PhysicalDevice::debug_check(PhysicalDevice const& device) { + CTH_CRITICAL(!device.created(), "physical device must be created") {} + debug_check_handle(device.get()); +} + +inline void PhysicalDevice::debug_check_handle([[maybe_unused]] vk_not_null vkDevice) {} +} diff --git a/cvk/incl/cvk/base/queue/queue.hpp b/cvk/incl/cvk/base/queue/queue.hpp new file mode 100644 index 00000000..ae210160 --- /dev/null +++ b/cvk/incl/cvk/base/queue/queue.hpp @@ -0,0 +1,116 @@ +#pragma once +#include "queue_family.hpp" +#include "queue_state.hpp" + +#include + +#include + + +namespace cvk { +/** + * @brief VkQueue wrapper + * @details copyable, thread safe, copies share the mutex of the vulkan queue + */ +class Queue { +public: + using State = QueueState; + + explicit Queue(QueueFamilyProperties familyProperties) : _familyProperties{familyProperties} {} + Queue(QueueFamilyProperties familyProperties, State state) : Queue{familyProperties} { wrap(std::move(state)); } + ~Queue() { optDestroy(); } + + /** + * @brief wraps the vulkan queue + * @details the device calls this, not the user + * @post @ref created() + * @note calls @ref optDestroy() + */ + void wrap(State state); + + /** + * @brief resets, the device owns the vulkan queue + * @pre @ref created() + */ + void destroy(); + + /** + * @brief if @ref created() -> calls @ref destroy() + */ + void optDestroy() { if(created()) destroy(); } + + /** + * @brief releases ownership, returns the state and resets + * @pre @ref created() + */ + State release(); + + /** + * @brief submits to the vulkan queue + * @param fence VK_NULL_HANDLE -> no fence + * @pre @ref created() + * @throws cvk::vk_result_exception result of vkQueueSubmit2() + */ + void raw_submit(VkSubmitInfo2 const& submitInfo, VkFence fence = VK_NULL_HANDLE) const; + + /** + * @brief presents through vkQueuePresentKHR() + * @return VK_SUCCESS, VK_SUBOPTIMAL_KHR or VK_ERROR_OUT_OF_DATE_KHR + * @pre @ref created() + * @pre @ref can_present() + * @throws cvk::vk_result_exception other result of vkQueuePresentKHR() + */ + [[nodiscard]] VkResult raw_present(VkPresentInfoKHR const& presentInfo) const; + + /** + * @brief blocks until the queue finished all its work + * @pre @ref created() + * @throws cvk::vk_result_exception result of vkQueueWaitIdle() + */ + void wait() const; + + [[nodiscard]] bool can_present() const { return contains(_familyProperties, QueueFamilyProperties::PRESENT); } + [[nodiscard]] bool created() const { return _handle != VK_NULL_HANDLE; } + [[nodiscard]] VkQueue get() const { return _handle.get(); } + [[nodiscard]] uint32_t index() const { return _queueIndex; } + [[nodiscard]] uint32_t familyIndex() const { return _familyIndex; } + [[nodiscard]] QueueFamilyProperties familyProperties() const { return _familyProperties; } + +private: + void reset(); + + QueueFamilyProperties _familyProperties; + VolkDeviceTable const* _functions = nullptr; + + move_ptr _handle = VK_NULL_HANDLE; + std::shared_ptr _handleMtx{}; + + uint32_t _familyIndex = 0; + uint32_t _queueIndex = 0; + +public: + Queue(Queue const& other) = default; + Queue(Queue&& other) = default; + Queue& operator=(Queue const& other) = default; + Queue& operator=(Queue&& other) = default; + + static void debug_check(Queue const& queue); + static void debug_check_present(Queue const& queue); + static void debug_check_handle(VkQueue vkQueue); +}; +} + +namespace cvk { +inline void Queue::debug_check(Queue const& queue) { + CTH_CRITICAL(!queue.created(), "queue must be created") {} +} + +inline void Queue::debug_check_present(Queue const& queue) { + debug_check(queue); + CTH_CRITICAL(!queue.can_present(), "queue is not a present queue") {} +} + +inline void Queue::debug_check_handle(VkQueue vkQueue) { + CTH_CRITICAL(vkQueue == VK_NULL_HANDLE, "vkQueue must not be invalid (VK_NULL_HANDLE)") {} +} +} diff --git a/cvk/incl/cvk/base/queue/queue_family.hpp b/cvk/incl/cvk/base/queue/queue_family.hpp new file mode 100644 index 00000000..a7d647ed --- /dev/null +++ b/cvk/incl/cvk/base/queue/queue_family.hpp @@ -0,0 +1,59 @@ +#pragma once +#include "cvk/utility/vk_hash.hpp" +#include "cvk/utility/vk_overloads.hpp" + +#include + +#include + + +namespace cvk { +using cth::en::flag_val; + +enum class QueueFamilyProperties { + NONE = 0, + GRAPHICS = flag_val(0), + COMPUTE = flag_val(1), + TRANSFER = flag_val(2), + PRESENT = flag_val(3) +}; +} + +CTH_GEN_ENUM_FLAG_OVERLOADS(cvk::QueueFamilyProperties) + +namespace cvk { +constexpr QueueFamilyProperties to_queue_properties(VkQueueFlags flags, bool presentSupport) { + auto result = QueueFamilyProperties::NONE; + if(flags & VK_QUEUE_GRAPHICS_BIT) result |= QueueFamilyProperties::GRAPHICS; + if(flags & VK_QUEUE_COMPUTE_BIT) result |= QueueFamilyProperties::COMPUTE; + if(flags & VK_QUEUE_TRANSFER_BIT) result |= QueueFamilyProperties::TRANSFER; + if(presentSupport) result |= QueueFamilyProperties::PRESENT; + return result; +} + +using queue_family_index_t = uint32_t; + +struct QueueFamily { + queue_family_index_t index; + QueueFamilyProperties properties; + VkQueueFamilyProperties vkProperties; + + static constexpr QueueFamily Vk( + queue_family_index_t index, + VkQueueFamilyProperties const& vkProperties, + bool presentSupport + ) { + return { + .index = index, + .properties = to_queue_properties(vkProperties.queueFlags, presentSupport), + .vkProperties = vkProperties + }; + } +}; + +constexpr bool operator==(QueueFamily const& l, QueueFamily const& r) { + return l.index == r.index && l.properties == r.properties && l.vkProperties == r.vkProperties; +} +} + +CTH_HASH_AGGREGATE(cvk::QueueFamily) diff --git a/cvk/incl/cvk/base/queue/queue_state.hpp b/cvk/incl/cvk/base/queue/queue_state.hpp new file mode 100644 index 00000000..738ff33c --- /dev/null +++ b/cvk/incl/cvk/base/queue/queue_state.hpp @@ -0,0 +1,39 @@ +#pragma once +#include "cvk/utility/types.hpp" + +#include + +#include + +#include +#include + +namespace cvk { +struct QueueState { + /** + * @brief function table of the device + * @details stable when the device moves + */ + not_null functions; + vk_not_null vkQueue; + /** + * @brief vulkan family index + */ + uint32_t familyIndex; + /** + * @brief index in the family + */ + uint32_t queueIndex; + + /** + * @brief guards the vulkan queue, shared by every copy of the queue + * @pre not nullptr + */ + std::shared_ptr vkQueueMtx = std::make_shared(); + + + static void debug_check(QueueState const& state) { + CTH_CRITICAL(state.vkQueueMtx == nullptr, "mutex must not be nullptr") {} + } +}; +} diff --git a/cvk/incl/cvk/surface/surface.hpp b/cvk/incl/cvk/surface/surface.hpp new file mode 100644 index 00000000..5423aca4 --- /dev/null +++ b/cvk/incl/cvk/surface/surface.hpp @@ -0,0 +1,144 @@ +#pragma once +#include "surface_config.hpp" + +#include "cvk/utility/types.hpp" + +#include + +#include + +#include + + +namespace cvk { +class Instance; +class PhysicalDevice; + +/** + * @brief VkSurfaceKHR wrapper + * @details the window system creates the surface, @ref wrap() takes it + */ +class Surface { +public: + using Config = SurfaceConfig; + using format_t = Config::format_t; + using present_mode_t = Config::present_mode_t; + struct State; + + /** + * @brief base constructor + * @pre @ref Instance::created() + */ + explicit Surface(Instance const& instance, Config config = {}); + + /** + * @brief constructs and wraps + * @details calls: + * - @ref Surface(Instance const&, Config) + * - @ref wrap() + */ + Surface(Instance const& instance, Config config, State const& state); + + /** + * @note calls @ref optDestroy() + */ + ~Surface() { optDestroy(); } + + /** + * @brief wraps the state + * @post @ref created() + * @note calls @ref optDestroy() + */ + void wrap(State const& state); + + /** + * @brief destroys and resets + * @pre @ref created() + */ + void destroy(); + + /** + * @brief if @ref created() -> calls @ref destroy() + */ + void optDestroy() { if(created()) destroy(); } + + /** + * @brief releases ownership, returns the state and resets + * @pre @ref created() + */ + State release(); + + /** + * @return queue family @ref familyIndex can present to this surface + * @throws cvk::vk_result_exception result of vkGetPhysicalDeviceSurfaceSupportKHR() + */ + [[nodiscard]] bool supportsFamily(PhysicalDevice const& physicalDevice, uint32_t familyIndex) const; + + /** + * @return allowed and available present modes, in the order of @ref Config::allowedPresentModes + * @throws cvk::vk_result_exception result of vkGetPhysicalDeviceSurfacePresentModesKHR() + */ + [[nodiscard]] std::vector presentModes(PhysicalDevice const& physicalDevice) const; + + /** + * @throws cvk::vk_result_exception result of vkGetPhysicalDeviceSurfaceCapabilitiesKHR() + */ + [[nodiscard]] VkSurfaceCapabilitiesKHR capabilities(PhysicalDevice const& physicalDevice) const; + + /** + * @return allowed and available formats, in the order of @ref Config::allowedSurfaceFormats + * @attention no format -> the surface is not usable with @ref physicalDevice + * @throws cvk::vk_result_exception result of vkGetPhysicalDeviceSurfaceFormatsKHR() + */ + [[nodiscard]] std::vector formats(PhysicalDevice const& physicalDevice) const; + + /** + * @return the most preferred allowed and available format + * @throws cth::except::default_exception reason: no allowed format available + */ + [[nodiscard]] format_t format(PhysicalDevice const& physicalDevice) const; + + /** + * @return the most preferred allowed and available present mode + * @throws cth::except::default_exception reason: no allowed present mode available + */ + [[nodiscard]] present_mode_t presentMode(PhysicalDevice const& physicalDevice) const; + + static void destroy(vk_not_null vkInstance, VkSurfaceKHR vkSurface); + + [[nodiscard]] bool created() const { return _handle != VK_NULL_HANDLE; } + [[nodiscard]] VkSurfaceKHR get() const { return _handle.get(); } + [[nodiscard]] Config const& config() const { return _config; } + +private: + void reset(); + + not_null _instance; + Config _config; + + move_ptr _handle = VK_NULL_HANDLE; + +public: + Surface(Surface const& other) = delete; + Surface(Surface&& other) noexcept = default; + Surface& operator=(Surface const& other) = delete; + Surface& operator=(Surface&& other) noexcept = default; + + static void debug_check(Surface const& surface); + static void debug_check_handle(vk_not_null vkSurface); +}; +} + +namespace cvk { +struct Surface::State { + vk_not_null vkSurface; +}; +} + +namespace cvk { +inline void Surface::debug_check(Surface const& surface) { + CTH_CRITICAL(!surface.created(), "surface must be created") {} + debug_check_handle(surface.get()); +} +inline void Surface::debug_check_handle([[maybe_unused]] vk_not_null vkSurface) {} +} diff --git a/cvk/incl/cvk/surface/surface_config.hpp b/cvk/incl/cvk/surface/surface_config.hpp new file mode 100644 index 00000000..6bd030f1 --- /dev/null +++ b/cvk/incl/cvk/surface/surface_config.hpp @@ -0,0 +1,28 @@ +#pragma once +#include + +#include +#include + +namespace cvk { +struct SurfaceConfig { + using format_t = VkSurfaceFormatKHR; + using present_mode_t = VkPresentModeKHR; + static constexpr std::array DEFAULT_ALLOWED_FORMATS{ + {{VK_FORMAT_B8G8R8A8_SRGB, VK_COLOR_SPACE_SRGB_NONLINEAR_KHR}} + }; + static constexpr std::array DEFAULT_ALLOWED_PRESENT_MODES{{VK_PRESENT_MODE_FIFO_KHR}}; + + /** + * @brief formats the surface allows, the first is the most preferred + * @details empty -> every available format + */ + std::vector allowedSurfaceFormats{std::from_range, DEFAULT_ALLOWED_FORMATS}; + + /** + * @brief present modes the surface allows, the first is the most preferred + * @details empty -> every available present mode + */ + std::vector allowedPresentModes{std::from_range, DEFAULT_ALLOWED_PRESENT_MODES}; +}; +} diff --git a/cvk/incl/cvk/utility/constant/device_constants.hpp b/cvk/incl/cvk/utility/constant/device_constants.hpp new file mode 100644 index 00000000..d6cb1026 --- /dev/null +++ b/cvk/incl/cvk/utility/constant/device_constants.hpp @@ -0,0 +1,48 @@ +#pragma once +#include "cvk/utility/device/physical_device_features.hpp" + +#include + +#include +#include + +namespace cvk::constants { +/** + * @brief extensions that every device enables + */ +inline constexpr std::array REQUIRED_DEVICE_EXTENSIONS{}; + +/** + * @brief extensions that a device with a present queue enables + */ +inline constexpr std::array PRESENT_DEVICE_EXTENSIONS{ + VK_KHR_SWAPCHAIN_EXTENSION_NAME, + VK_KHR_SWAPCHAIN_MAINTENANCE_1_EXTENSION_NAME, +}; + +/** + * @brief instance extensions that a device with a present queue requires, besides the platform surface extension + */ +inline constexpr std::array PRESENT_INSTANCE_EXTENSIONS{ + VK_KHR_SURFACE_EXTENSION_NAME, + VK_KHR_GET_SURFACE_CAPABILITIES_2_EXTENSION_NAME, + VK_KHR_SURFACE_MAINTENANCE_1_EXTENSION_NAME, +}; + +/** + * @brief features that every device enables + */ +[[nodiscard]] inline utils::PhysicalDeviceFeatures required_device_features() { + utils::PhysicalDeviceFeatures features{}; + + features.features().features.samplerAnisotropy = VK_TRUE; + + features.features().timelineSemaphore = VK_TRUE; + + auto& features13 = features.features(); + features13.synchronization2 = VK_TRUE; + features13.dynamicRendering = VK_TRUE; + + return features; +} +} diff --git a/cvk/incl/cvk/utility/device/physical_device_features.hpp b/cvk/incl/cvk/utility/device/physical_device_features.hpp new file mode 100644 index 00000000..2699dea8 --- /dev/null +++ b/cvk/incl/cvk/utility/device/physical_device_features.hpp @@ -0,0 +1,151 @@ +#pragma once +#include "cvk/utility/format.hpp" +#include "cvk/utility/types.hpp" + +#include + +#include + +#include +#include +#include +#include + + +namespace cvk::utils { +/** + * @brief feature that a device does not have + * @details @ref index is the position in the VkBool32 flags of the struct with @ref sType + */ +struct MissingFeature { + VkStructureType sType; + size_t index; +}; + +/** + * @brief features of the vulkan 1.0 to 1.3 core as one pNext chain + * @details copyable, copy and move link the chain again + */ +class PhysicalDeviceFeatures { +public: + using chain_t = std::tuple< + VkPhysicalDeviceFeatures2, + VkPhysicalDeviceVulkan11Features, + VkPhysicalDeviceVulkan12Features, + VkPhysicalDeviceVulkan13Features + >; + + /** + * @brief every feature false + */ + PhysicalDeviceFeatures(); + + /** + * @brief queries the features of the device + */ + explicit PhysicalDeviceFeatures(vk_not_null vkDevice); + + ~PhysicalDeviceFeatures() = default; + + /** + * @return features that @ref requiredFeatures enables and this does not + */ + [[nodiscard]] std::vector supports(PhysicalDeviceFeatures const& requiredFeatures) const; + + /** + * @brief enables every feature that @ref other enables + */ + void merge(PhysicalDeviceFeatures const& other); + + /** + * @tparam T one of @ref chain_t + */ + template + [[nodiscard]] auto& features(this auto& self) { return std::get(self._chain); } + + /** + * @brief start of the pNext chain, for VkDeviceCreateInfo::pNext + */ + [[nodiscard]] VkPhysicalDeviceFeatures2 const* get() const { return &std::get(_chain); } + +private: + void link(); + + chain_t _chain; + +public: + PhysicalDeviceFeatures(PhysicalDeviceFeatures const& other) : _chain{other._chain} { link(); } + PhysicalDeviceFeatures(PhysicalDeviceFeatures&& other) noexcept : _chain{other._chain} { link(); } + PhysicalDeviceFeatures& operator=(PhysicalDeviceFeatures const& other) { + _chain = other._chain; + link(); + return *this; + } + PhysicalDeviceFeatures& operator=(PhysicalDeviceFeatures&& other) noexcept { + _chain = other._chain; + link(); + return *this; + } +}; +} + +namespace cvk::utils::dev { +/** + * @brief position of the VkBool32 flags in a feature struct + */ +template +struct feature_flags; + +template<> +struct feature_flags { + static constexpr size_t OFFSET = offsetof(VkPhysicalDeviceFeatures2, features); + static constexpr size_t COUNT = sizeof(VkPhysicalDeviceFeatures) / sizeof(VkBool32); +}; + +template<> +struct feature_flags { + static constexpr size_t OFFSET = offsetof(VkPhysicalDeviceVulkan11Features, storageBuffer16BitAccess); + static constexpr size_t COUNT = + (offsetof(VkPhysicalDeviceVulkan11Features, shaderDrawParameters) - OFFSET) / sizeof(VkBool32) + 1; +}; + +template<> +struct feature_flags { + static constexpr size_t OFFSET = offsetof(VkPhysicalDeviceVulkan12Features, samplerMirrorClampToEdge); + static constexpr size_t COUNT = + (offsetof(VkPhysicalDeviceVulkan12Features, subgroupBroadcastDynamicId) - OFFSET) / sizeof(VkBool32) + 1; +}; + +template<> +struct feature_flags { + static constexpr size_t OFFSET = offsetof(VkPhysicalDeviceVulkan13Features, robustImageAccess); + static constexpr size_t COUNT = + (offsetof(VkPhysicalDeviceVulkan13Features, maintenance4) - OFFSET) / sizeof(VkBool32) + 1; +}; + +static_assert(feature_flags::COUNT == 55); +static_assert(feature_flags::COUNT == 12); +static_assert(feature_flags::COUNT == 47); +static_assert(feature_flags::COUNT == 15); + +/** + * @return the VkBool32 flags of @ref features + */ +template +[[nodiscard]] auto flags(T& features) { + using feature_t = std::remove_const_t; + using bool_t = std::conditional_t, VkBool32 const, VkBool32>; + using byte_t = std::conditional_t, std::byte const, std::byte>; + + constexpr size_t count = feature_flags::COUNT; + + auto* const begin = reinterpret_cast(&features) + feature_flags::OFFSET; + return std::span{reinterpret_cast(begin), count}; +} +} + +CTH_FORMAT_CLASS( + cvk::utils::MissingFeature, + "{}, flag: {}", + [](cvk::utils::MissingFeature const& feature) { return std::tie(feature.sType, feature.index); } +); diff --git a/cvk/incl/cvk/utility/format.hpp b/cvk/incl/cvk/utility/format.hpp index 20c0a781..6f5f06b7 100644 --- a/cvk/incl/cvk/utility/format.hpp +++ b/cvk/incl/cvk/utility/format.hpp @@ -6,6 +6,7 @@ #include #include +#include CTH_FORMAT_ALIAS(VkResult, std::string_view, string_VkResult); @@ -21,3 +22,11 @@ CTH_FORMAT_ALIAS(VkColorSpaceKHR, std::string_view, string_VkColorSpaceKHR); CTH_FORMAT_ALIAS(VkPresentModeKHR, std::string_view, string_VkPresentModeKHR); CTH_FORMAT_ALIAS(VkObjectType, std::string_view, string_VkObjectType); + +CTH_FORMAT_CLASS( + VkSurfaceFormatKHR, + "{}, {}", + [](VkSurfaceFormatKHR const& f) { return std::tie(f.format, f.colorSpace); } +); + +CTH_FORMAT_CLASS(VkExtent2D, "{}, {}", [](VkExtent2D const& e) { return std::tie(e.width, e.height); }); diff --git a/cvk/incl/cvk/utility/vk_hash.hpp b/cvk/incl/cvk/utility/vk_hash.hpp new file mode 100644 index 00000000..50e840e0 --- /dev/null +++ b/cvk/incl/cvk/utility/vk_hash.hpp @@ -0,0 +1,7 @@ +#pragma once +#include + +#include + +CTH_HASH_AGGREGATE(VkExtent3D) +CTH_HASH_AGGREGATE(VkQueueFamilyProperties) diff --git a/cvk/incl/cvk/utility/vk_overloads.hpp b/cvk/incl/cvk/utility/vk_overloads.hpp new file mode 100644 index 00000000..f5486ca6 --- /dev/null +++ b/cvk/incl/cvk/utility/vk_overloads.hpp @@ -0,0 +1,21 @@ +#pragma once +#include + +constexpr bool operator==(VkSurfaceFormatKHR const& a, VkSurfaceFormatKHR const& b) { + return a.format == b.format && a.colorSpace == b.colorSpace; +} + +constexpr bool operator==(VkExtent2D const& a, VkExtent2D const& b) { + return a.width == b.width && a.height == b.height; +} + +constexpr bool operator==(VkExtent3D const& a, VkExtent3D const& b) { + return a.width == b.width && a.height == b.height && a.depth == b.depth; +} + +constexpr bool operator==(VkQueueFamilyProperties const& a, VkQueueFamilyProperties const& b) { + return a.queueFlags == b.queueFlags + && a.queueCount == b.queueCount + && a.timestampValidBits == b.timestampValidBits + && a.minImageTransferGranularity == b.minImageTransferGranularity; +} diff --git a/cvk/src/base/core.cpp b/cvk/src/base/core.cpp new file mode 100644 index 00000000..836055c7 --- /dev/null +++ b/cvk/src/base/core.cpp @@ -0,0 +1,187 @@ +#include "cvk/base/core.hpp" + +#include "cvk/base/device.hpp" +#include "cvk/base/instance.hpp" +#include "cvk/base/physical_device.hpp" +#include "cvk/surface/surface.hpp" +#include "cvk/utility/vk_exceptions.hpp" + +#include +#include + + +namespace cvk { +using std::span; +using std::vector; + +Core::Core(Instance const& instance) : _instance{&instance} {} + +Core::Core(Instance const& instance, span surfaces, Config const& config) : Core{instance} { + create(surfaces, config); +} + +Core::Core(Instance const& instance, State state) : Core{instance} { wrap(std::move(state)); } + +Core::~Core() { optDestroy(); } + +void Core::wrap(State state) { + PhysicalDevice::debug_check(*state.physicalDevice); + Device::debug_check(*state.device); + CTH_CRITICAL( + &state.device->physicalDevice() != state.physicalDevice.get(), + "device must be created with the physical device of the state" + ) {} + + optDestroy(); + + _physicalDevice = state.physicalDevice.release_val(); + _device = state.device.release_val(); + _queueSetIndex = state.queueSetIndex; + + if(state.queues.empty()) _queues.assign_range(_device->queues()); + else _queues = std::move(state.queues); +} + +void Core::create(span surfaces, Config const& config) { + Config::debug_check(config); + + optDestroy(); + + auto [uniqueQueueFamilyIndices, queuesToUniqueQueues] = createPhysicalDevice(surfaces, config); + + _device = std::make_unique( + *_instance, + *_physicalDevice, + Device::Config{std::move(uniqueQueueFamilyIndices)} + ); + + createQueues(queuesToUniqueQueues); +} + +void Core::destroy() { + debug_check(*this); + reset(); +} + +Core::State Core::release() { + debug_check(*this); + + State state{ + .physicalDevice = std::move(_physicalDevice), + .device = std::move(_device), + .queues = std::move(_queues), + .queueSetIndex = _queueSetIndex, + }; + reset(); + return state; +} + +size_t Core::queueSetIndex() const { + debug_check(*this); + CTH_CRITICAL(!_queueSetIndex.has_value(), "queue set index has no value, the config had no queue sets") {} + return *_queueSetIndex; +} + +VkInstance Core::vkInstance() const { return _instance->get(); } + +PhysicalDevice const& Core::physicalDevice() const { + debug_check(*this); + return *_physicalDevice; +} + +VkPhysicalDevice Core::vkPhysicalDevice() const { return physicalDevice().get(); } + +Device const& Core::device() const { + debug_check(*this); + return *_device; +} + +DeviceTable Core::deviceTable() const { return device().table(); } +VolkDeviceTable const* Core::functions() const { return device().functions(); } +VkDevice Core::vkDevice() const { return device().get(); } + +void Core::reset() { + _queues.clear(); + _device = nullptr; + _physicalDevice = nullptr; + _queueSetIndex = std::nullopt; +} + +auto Core::createPhysicalDevice(span surfaces, Config const& config) -> queue_mappings { + if(config.queueSets.empty()) { + auto mappings = tryCreatePhysicalDevice(surfaces, config, queue_set_t{config.queueProperties.size()}); + CVK_STABLE_THROW(!mappings, "no physical device supports the queues") {} + return *std::move(mappings); + } + + for(auto const& [setIndex, queueSet] : std::views::enumerate(config.queueSets)) { + auto mappings = tryCreatePhysicalDevice(surfaces, config, queueSet); + if(!mappings) continue; + + _queueSetIndex = static_cast(setIndex); + return *std::move(mappings); + } + + CVK_STABLE_THROW(true, "no physical device supports any queue set") {} + std::unreachable(); +} + +auto Core::createQueueSetRelations( + queue_set_t const& queueSet, + span queueProperties +) -> queue_set_relations { + using index_type = queue_set_t::index_type; + + auto const roots = queueSet.roots(); + + std::unordered_map const rootToQueue{ + std::from_range, + std::views::zip(roots, std::views::iota(0uz)) + }; + + vector uniqueQueues(roots.size()); + vector queueToUniqueQueue(queueProperties.size()); + + for(index_type i = 0; i < queueSet.size(); i++) { + auto const queueIdx = rootToQueue.at(queueSet.find(i)); + queueToUniqueQueue[i] = queueIdx; + uniqueQueues[queueIdx] |= queueProperties[i]; + } + + return {std::move(uniqueQueues), std::move(queueToUniqueQueue)}; +} + +auto Core::tryCreatePhysicalDevice( + span surfaces, + Config const& config, + queue_set_t const& queueSet +) -> std::optional { + CTH_CRITICAL(queueSet.size() != config.queueProperties.size(), "queue set must span all queues") {} + + auto [uniqueQueues, queueToUniqueQueue] = createQueueSetRelations(queueSet, config.queueProperties); + + auto result = PhysicalDevice::AutoPick( + *_instance, + surfaces, + uniqueQueues, + config.requiredExtensions, + config.requiredFeatures + ); + if(!result) return std::nullopt; + + _physicalDevice = std::make_unique(std::move(result->device)); + + return queue_mappings{std::move(result->queueFamilyIndices), std::move(queueToUniqueQueue)}; +} + +void Core::createQueues(span queuesToUniqueQueues) { + auto const uniqueQueues = _device->queues(); + + _queues.reserve(queuesToUniqueQueues.size()); + for(auto const uniqueQueueIdx : queuesToUniqueQueues) + _queues.push_back(uniqueQueues[uniqueQueueIdx]); +} + +Core::Core(Core&& other) noexcept = default; +Core& Core::operator=(Core&& other) noexcept = default; +} diff --git a/cvk/src/base/device.cpp b/cvk/src/base/device.cpp new file mode 100644 index 00000000..3b959ebf --- /dev/null +++ b/cvk/src/base/device.cpp @@ -0,0 +1,190 @@ +#include "cvk/base/device.hpp" + +#include "cvk/base/instance.hpp" +#include "cvk/base/physical_device.hpp" +#include "cvk/utility/vk_exceptions.hpp" + +#include + +#include +#include + + +namespace cvk { +using std::span; +using std::vector; + +Device::Device(Instance const& instance, PhysicalDevice const& physicalDevice) : _instance{&instance}, + _physicalDevice{&physicalDevice} {} + +Device::Device(Instance const& instance, PhysicalDevice const& physicalDevice, State state) : Device{ + instance, + physicalDevice +} { wrap(std::move(state)); } + +Device::Device( + Instance const& instance, + PhysicalDevice const& physicalDevice, + Config const& config +) : Device{ + instance, + physicalDevice +} { create(config); } + +void Device::wrap(State state) { + optDestroy(); + + _handle = state.vkDevice.get(); + + _functionTable = std::move(state.functionTable); + if(_functionTable == nullptr) { + _functionTable = std::make_unique(); + loadFunctionTable(); + } +} + +void Device::create(Config const& config) { + optDestroy(); + + auto const& familyIndices = config.queueFamilyIndices; + auto const queueCounts = calcQueueFamiliesQueueCounts(familyIndices); + + createLogicalDevice(queueCounts); + loadFunctionTable(); + + createQueues(familyIndices); +} + +void Device::destroy() { + debug_check(*this); + + destroy(_handle.release(), functions()->vkDestroyDevice); + reset(); +} + +Device::State Device::release() { + debug_check(*this); + + State state{ + .vkDevice = _handle.release(), + .functionTable = std::move(_functionTable) + }; + reset(); + return state; +} + +void Device::waitIdle() const { + debug_check(*this); + + auto const result = functions()->vkDeviceWaitIdle(_handle.get()); + CVK_RESULT_STABLE_THROW(result != VK_SUCCESS, result, "failed to wait for device") {} +} + +void Device::destroy(VkDevice vkDevice, PFN_vkDestroyDevice destroy) { + CTH_WARN(vkDevice == VK_NULL_HANDLE, "vkDevice invalid (VK_NULL_HANDLE)") {} + + destroy(vkDevice, nullptr); + + log::msg("destroyed device"); +} + +void Device::reset() { + _handle = nullptr; + _queues.clear(); + + if(_functionTable == nullptr) _functionTable = std::make_unique(); + std::memset(_functionTable.get(), 0, sizeof(VolkDeviceTable)); +} + +auto Device::calcQueueFamiliesQueueCounts(span familyIndices) -> queue_families_queue_counts_t { + queue_families_queue_counts_t queueCounts{}; + + for(auto const& familyIndex : familyIndices) + ++queueCounts[familyIndex]; + + return queueCounts; +} + +void Device::createLogicalDevice(queue_families_queue_counts_t const& queueFamiliesQueueCounts) { + CTH_CRITICAL(queueFamiliesQueueCounts.empty(), "at least one queue is required") {} + CTH_CRITICAL(created(), "device was already created") {} + + vector queueCreateInfos{}; + queueCreateInfos.reserve(queueFamiliesQueueCounts.size()); + + vector> queuePriorities{}; + queuePriorities.reserve(queueFamiliesQueueCounts.size()); + + for(auto const [queueFamily, queueCount] : queueFamiliesQueueCounts) { + queuePriorities.emplace_back(queueCount, 1.0f); + + queueCreateInfos.push_back( + { + .sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO, + .pNext = nullptr, + .flags = 0, + .queueFamilyIndex = queueFamily, + .queueCount = queueCount, + .pQueuePriorities = queuePriorities.back().data() + } + ); + } + auto const requiredExtensions = cth::str::to_c_str_vector(_physicalDevice->requiredExtensions()); + + auto features = _physicalDevice->requiredFeatures(); + + VkPhysicalDeviceSwapchainMaintenance1FeaturesKHR swapchainMaintenance{ + .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SWAPCHAIN_MAINTENANCE_1_FEATURES_KHR, + .pNext = nullptr, + .swapchainMaintenance1 = VK_TRUE, + }; + bool const maintenance1 = std::ranges::contains( + _physicalDevice->requiredExtensions(), + std::string_view{VK_KHR_SWAPCHAIN_MAINTENANCE_1_EXTENSION_NAME} + ); + if(maintenance1) features.features().pNext = &swapchainMaintenance; + + VkDeviceCreateInfo const createInfo{ + .sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO, + .pNext = features.get(), + .flags = 0, + .queueCreateInfoCount = static_cast(queueCreateInfos.size()), + .pQueueCreateInfos = queueCreateInfos.data(), + .enabledLayerCount = 0, + .ppEnabledLayerNames = nullptr, + .enabledExtensionCount = static_cast(requiredExtensions.size()), + .ppEnabledExtensionNames = requiredExtensions.data(), + .pEnabledFeatures = nullptr, + }; + VkDevice ptr = VK_NULL_HANDLE; + + auto const createResult = vkCreateDevice(_physicalDevice->get(), &createInfo, nullptr, &ptr); + CTH_STABLE_ERR(createResult != VK_SUCCESS, "failed to create logical device") { + reset(); + throw cvk::vk_result_exception{createResult, details->exception()}; + } + _handle = ptr; +} + +void Device::loadFunctionTable() const { volkLoadDeviceTable(_functionTable.get(), get()); } + +void Device::createQueues(span familyIndices) { + std::unordered_map queueCounts{}; + + _queues.reserve(familyIndices.size()); + + for(auto const familyIndex : familyIndices) { + auto& queueIndex = queueCounts[familyIndex]; + + VkQueue ptr = VK_NULL_HANDLE; + table()->vkGetDeviceQueue(_handle.get(), familyIndex, queueIndex, &ptr); + + CVK_STABLE_THROW(ptr == VK_NULL_HANDLE, "failed to get device queue") {} + + _queues.emplace_back( + _physicalDevice->queueFamily(familyIndex)->properties, + Queue::State{_functionTable.get(), ptr, familyIndex, queueIndex++, std::make_shared()} + ); + } +} +} diff --git a/cvk/src/base/physical_device.cpp b/cvk/src/base/physical_device.cpp new file mode 100644 index 00000000..e3bd06bb --- /dev/null +++ b/cvk/src/base/physical_device.cpp @@ -0,0 +1,346 @@ +#include "cvk/base/physical_device.hpp" + +#include "cvk/base/instance.hpp" +#include "cvk/surface/surface.hpp" +#include "cvk/utility/constant/device_constants.hpp" +#include "cvk/utility/vk_exceptions.hpp" + +#include + +#include +#include + + +namespace cvk { +using std::span; +using std::string; +using std::vector; + +PhysicalDevice::PhysicalDevice( + Instance const& instance, + utils::PhysicalDeviceFeatures requiredFeatures, + span requiredExtensions +) : _instance{&instance}, + _requiredFeatures{std::move(requiredFeatures)}, + _requiredExtensions{std::from_range, requiredExtensions} {} + +PhysicalDevice::PhysicalDevice( + Instance const& instance, + utils::PhysicalDeviceFeatures const& requiredFeatures, + span requiredExtensions, + span surfaces, + vk_not_null vkDevice +) : PhysicalDevice{instance, requiredFeatures, requiredExtensions} { create(surfaces, vkDevice); } + +PhysicalDevice::PhysicalDevice( + Instance const& instance, + utils::PhysicalDeviceFeatures const& requiredFeatures, + span requiredExtensions, + State const& state +) : PhysicalDevice{instance, requiredFeatures, requiredExtensions} { wrap(state); } + + +auto PhysicalDevice::Create( + Instance const& instance, + span surfaces, + span queueProperties, + span requiredExtensions, + utils::PhysicalDeviceFeatures const& requiredFeatures, + vk_not_null vkDevice +) -> std::optional { + PhysicalDevice physicalDevice{instance, requiredFeatures, requiredExtensions, surfaces, vkDevice}; + + if(!physicalDevice.suitable(queueProperties)) + return std::nullopt; + + auto indices = physicalDevice.queueFamilyIndices(queueProperties); + return CreateResult{ + std::move(physicalDevice), + std::move(indices) + }; +} + +void PhysicalDevice::wrap(State const& state) { + auto const device = state.vkDevice; + debug_check_handle(device); + + _handle = device.get(); + + _features = state.features.has_value() ? *state.features : utils::PhysicalDeviceFeatures{device}; + _extensions = !state.extensions.empty() ? state.extensions : queryExtensions(device); + _properties = state.properties.has_value() ? *state.properties : queryProperties(device); + + _maxSampleCount = state.maxSampleCount != VK_SAMPLE_COUNT_FLAG_BITS_MAX_ENUM + ? state.maxSampleCount + : evalMaxSampleCount(_properties); + + _memProperties = state.memProperties.has_value() ? *state.memProperties : queryMemoryProperties(device); + _queueFamilies = !state.queueFamilies.empty() ? state.queueFamilies : queryQueueFamilies({}, device); +} + +void PhysicalDevice::create(span surfaces, vk_not_null vkDevice) { + create(queryQueueFamilies(surfaces, vkDevice), vkDevice); +} + +void PhysicalDevice::create(span queueFamilies, vk_not_null vkDevice) { + debug_check_handle(vkDevice); + + _handle = vkDevice.get(); + _features = utils::PhysicalDeviceFeatures{vkDevice}; + _extensions = queryExtensions(vkDevice); + _properties = queryProperties(vkDevice); + _maxSampleCount = evalMaxSampleCount(_properties); + _memProperties = queryMemoryProperties(vkDevice); + _queueFamilies = {std::from_range, queueFamilies}; +} + + +bool PhysicalDevice::suitable(span queueProperties) const { + debug_check(*this); + + auto const missingFeatures = supports(_requiredFeatures); + auto const missingExtensions = supports(_requiredExtensions); + auto const queueIndices = queueFamilyIndices(queueProperties); + bool const suitable = missingFeatures.empty() && missingExtensions.empty() && !queueIndices.empty(); + + CTH_WARN(!suitable, "physical device ({}) is not suitable", _properties.deviceName) { + if(queueIndices.empty()) + details->add("missing queue families"); + + for(auto const& missingExtension : missingExtensions) + details->add("missing extension: {}", missingExtension); + + for(auto const& missingFeature : missingFeatures) + details->add("missing feature: {}", missingFeature); + } + + return suitable; +} + +auto PhysicalDevice::AutoPick( + Instance const& instance, + span surfaces, + span queueProperties, + span requiredExtensions, + utils::PhysicalDeviceFeatures const& requiredFeatures +) -> std::optional { + Instance::debug_check(instance); + + auto const devices = enumerateVkDevices(instance.get()); + + auto const toString = [](std::string_view extension) { return string{extension}; }; + + vector extensions{std::from_range, requiredExtensions}; + extensions.append_range(constants::REQUIRED_DEVICE_EXTENSIONS | std::views::transform(toString)); + + bool const present = std::ranges::any_of( + queueProperties, + [](QueueFamilyProperties properties) { return contains(properties, QueueFamilyProperties::PRESENT); } + ); + if(present) extensions.append_range(constants::PRESENT_DEVICE_EXTENSIONS | std::views::transform(toString)); + + auto features = constants::required_device_features(); + features.merge(requiredFeatures); + + + std::optional chosen{}; + + for(auto const& device : devices) { + auto createResult = Create(instance, surfaces, queueProperties, extensions, features, device); + + if(createResult.has_value()) { + chosen = std::move(createResult); + break; + } + } + + if(!chosen) + return std::nullopt; + + log::msg("chosen physical device: {}", chosen->device._properties.deviceName); + + return chosen; +} + +auto PhysicalDevice::supports( + utils::PhysicalDeviceFeatures const& requiredFeatures +) const -> vector { + debug_check(*this); + + return _features.supports(requiredFeatures); +} + +auto PhysicalDevice::supports(span requiredExtensions) const -> vector { + vector missingExtensions{}; + for(auto const& requiredExtension : requiredExtensions) + if(!std::ranges::contains(_extensions, requiredExtension)) missingExtensions.emplace_back(requiredExtension); + + return missingExtensions; +} + +auto PhysicalDevice::findSupportedFormat( + span candidates, + VkImageTiling tiling, + VkFormatFeatureFlags features +) const -> VkFormat { + debug_check(*this); + + for(VkFormat const format : candidates) { + VkFormatProperties props{}; + vkGetPhysicalDeviceFormatProperties(_handle.get(), format, &props); + + if(tiling == VK_IMAGE_TILING_OPTIMAL && (props.optimalTilingFeatures & features) == features) + return format; + if(tiling == VK_IMAGE_TILING_LINEAR && (props.linearTilingFeatures & features) == features) + return format; + } + CVK_STABLE_THROW(true, "format unsupported") {} + return VK_FORMAT_MAX_ENUM; +} + +auto PhysicalDevice::queueFamilyIndices( + span queueProperties +) const -> vector { + debug_check(*this); + + if(queueProperties.empty()) + return {}; + + vector> queueIndices{queueProperties.size()}; + + for(auto [requiredProperties, indices] : std::views::zip(queueProperties, queueIndices)) + for(auto const& family : _queueFamilies) + if((family.properties & requiredProperties) == requiredProperties) + indices.push_back(family.index); + + vector familiesMaxQueues(_queueFamilies.size()); + + for(auto [src, dst] : std::views::zip(_queueFamilies, familiesMaxQueues)) + dst = src.vkProperties.queueCount; + + return cth::alg::assign(queueIndices, familiesMaxQueues); +} + +bool PhysicalDevice::supportsQueueSet(span queueProperties) const { + debug_check(*this); + + return !queueFamilyIndices(queueProperties).empty(); +} + +vector PhysicalDevice::enumerateVkDevices(vk_not_null vkInstance) { + Instance::debug_check_handle(vkInstance); + + uint32_t deviceCount = 0; + auto const countResult = vkEnumeratePhysicalDevices(vkInstance.get(), &deviceCount, nullptr); + CVK_RESULT_STABLE_THROW(countResult != VK_SUCCESS, countResult, "failed to count physical devices") {} + + vector devices(deviceCount); + auto const writeResult = vkEnumeratePhysicalDevices(vkInstance.get(), &deviceCount, devices.data()); + CVK_RESULT_STABLE_THROW(writeResult != VK_SUCCESS, writeResult, "failed to enumerate physical devices") {} + + return devices; +} + +vector PhysicalDevice::queryExtensions(vk_not_null vkDevice) { + uint32_t extensionCount = 0; + auto const countResult = vkEnumerateDeviceExtensionProperties(vkDevice.get(), nullptr, &extensionCount, nullptr); + CVK_RESULT_STABLE_THROW(countResult != VK_SUCCESS, countResult, "failed to count device extensions") {} + + vector availableExtensions(extensionCount); + auto const writeResult = vkEnumerateDeviceExtensionProperties( + vkDevice.get(), + nullptr, + &extensionCount, + availableExtensions.data() + ); + CVK_RESULT_STABLE_THROW( + writeResult != VK_SUCCESS, + writeResult, + "failed to enumerate device extension properties" + ) {} + + vector extensions(extensionCount); + std::ranges::transform( + availableExtensions, + extensions.begin(), + [](VkExtensionProperties const& extension) { return extension.extensionName; } + ); + + return extensions; +} + +VkPhysicalDeviceProperties PhysicalDevice::queryProperties(vk_not_null vkDevice) { + VkPhysicalDeviceProperties properties{}; + vkGetPhysicalDeviceProperties(vkDevice.get(), &properties); + return properties; +} + +VkPhysicalDeviceMemoryProperties PhysicalDevice::queryMemoryProperties(vk_not_null vkDevice) { + VkPhysicalDeviceMemoryProperties memProperties{}; + vkGetPhysicalDeviceMemoryProperties(vkDevice.get(), &memProperties); + return memProperties; +} + +vector PhysicalDevice::queryQueueFamilies( + span surfaces, + vk_not_null vkDevice +) { + uint32_t count = 0; + vkGetPhysicalDeviceQueueFamilyProperties(vkDevice.get(), &count, nullptr); + vector familyProperties(count); + vkGetPhysicalDeviceQueueFamilyProperties(vkDevice.get(), &count, familyProperties.data()); + + vector queueFamilies{}; + queueFamilies.reserve(familyProperties.size()); + + for(uint32_t i = 0; i < familyProperties.size(); i++) { + bool presentSupport = !surfaces.empty(); + + for(auto const& surface : surfaces) { + VkBool32 support = VK_FALSE; + auto const result = vkGetPhysicalDeviceSurfaceSupportKHR(vkDevice.get(), i, surface.get(), &support); + CVK_RESULT_STABLE_THROW(result != VK_SUCCESS, result, "device-surface support query failed") {} + + presentSupport = presentSupport && support == VK_TRUE; + } + + queueFamilies.push_back(QueueFamily::Vk(i, familyProperties[i], presentSupport)); + } + + return queueFamilies; +} + +VkSampleCountFlagBits PhysicalDevice::evalMaxSampleCount(VkPhysicalDeviceProperties const& properties) { + VkSampleCountFlags const supportedSamples = properties.limits.framebufferColorSampleCounts + & properties.limits.framebufferDepthSampleCounts; + + constexpr std::array sampleFlagBits = { + VK_SAMPLE_COUNT_64_BIT, + VK_SAMPLE_COUNT_32_BIT, + VK_SAMPLE_COUNT_16_BIT, + VK_SAMPLE_COUNT_8_BIT, + VK_SAMPLE_COUNT_4_BIT, + VK_SAMPLE_COUNT_2_BIT, + VK_SAMPLE_COUNT_1_BIT + }; + + for(auto const sampleFlagBit : sampleFlagBits) + if(supportedSamples & sampleFlagBit) + return sampleFlagBit; + + CTH_CRITICAL(true, "invalid state, sample count not supported") {} + return VK_SAMPLE_COUNT_1_BIT; +} + +QueueFamily const* PhysicalDevice::queueFamily(queue_family_index_t idx) const { + auto const it = std::ranges::find_if( + _queueFamilies, + [idx](QueueFamily const& family) { return family.index == idx; } + ); + + if(it == _queueFamilies.end()) + return nullptr; + + return &*it; +} +} diff --git a/cvk/src/base/queue/queue.cpp b/cvk/src/base/queue/queue.cpp new file mode 100644 index 00000000..86a0cf35 --- /dev/null +++ b/cvk/src/base/queue/queue.cpp @@ -0,0 +1,85 @@ +#include "cvk/base/queue/queue.hpp" + +#include "cvk/utility/vk_exceptions.hpp" + + +namespace cvk { +void Queue::wrap(State state) { + State::debug_check(state); + optDestroy(); + + _handle = state.vkQueue.get(); + _functions = state.functions.get(); + _handleMtx = std::move(state.vkQueueMtx); + _familyIndex = state.familyIndex; + _queueIndex = state.queueIndex; +} + +void Queue::destroy() { + debug_check(*this); + reset(); +} + +Queue::State Queue::release() { + debug_check(*this); + State state{ + _functions, + _handle.get(), + _familyIndex, + _queueIndex, + std::move(_handleMtx), + }; + reset(); + return state; +} + +void Queue::raw_submit(VkSubmitInfo2 const& submitInfo, VkFence fence) const { + debug_check(*this); + + VkResult result; + { + std::scoped_lock const lock{*_handleMtx}; + result = _functions->vkQueueSubmit2(_handle.get(), 1, &submitInfo, fence); + } + + CVK_RESULT_STABLE_THROW(result != VK_SUCCESS, result, "failed to submit to queue") {} +} + +VkResult Queue::raw_present(VkPresentInfoKHR const& presentInfo) const { + debug_check_present(*this); + + VkResult result; + { + std::scoped_lock const lock{*_handleMtx}; + result = _functions->vkQueuePresentKHR(_handle.get(), &presentInfo); + } + + CVK_RESULT_STABLE_THROW( + result != VK_SUCCESS && result != VK_SUBOPTIMAL_KHR && result != VK_ERROR_OUT_OF_DATE_KHR, + result, + "failed to present" + ) {} + + return result; +} + +void Queue::wait() const { + debug_check(*this); + + VkResult result; + { + std::scoped_lock const lock{*_handleMtx}; + result = _functions->vkQueueWaitIdle(_handle.get()); + } + + CVK_RESULT_STABLE_THROW(result != VK_SUCCESS, result, "failed to wait for queue") {} +} + +void Queue::reset() { + _functions = nullptr; + _handle = VK_NULL_HANDLE; + _handleMtx = nullptr; + _familyIndex = 0; + _queueIndex = 0; +} +} diff --git a/cvk/src/surface/surface.cpp b/cvk/src/surface/surface.cpp new file mode 100644 index 00000000..9bcd4a82 --- /dev/null +++ b/cvk/src/surface/surface.cpp @@ -0,0 +1,162 @@ +#include "cvk/surface/surface.hpp" + +#include "cvk/base/instance.hpp" +#include "cvk/base/physical_device.hpp" +#include "cvk/utility/vk_exceptions.hpp" +#include "cvk/utility/vk_overloads.hpp" + +#include + + +namespace cvk { +using std::vector; + +namespace { + /** + * @return @ref allowed in its order, filtered by @ref available, empty @ref allowed -> @ref available + */ + template + [[nodiscard]] vector filter_allowed(vector available, std::span allowed) { + if(allowed.empty()) return available; + + vector result{}; + for(auto const& value : allowed) + if(std::ranges::contains(available, value)) result.push_back(value); + return result; + } +} + +Surface::Surface(Instance const& instance, Config config) : _instance{&instance}, _config{std::move(config)} {} + +Surface::Surface(Instance const& instance, Config config, State const& state) : Surface{instance, std::move(config)} { + wrap(state); +} + +void Surface::wrap(State const& state) { + debug_check_handle(state.vkSurface); + optDestroy(); + + _handle = state.vkSurface.get(); +} + +void Surface::destroy() { + debug_check(*this); + + destroy(_instance->get(), _handle.get()); + + reset(); +} + +Surface::State Surface::release() { + debug_check(*this); + State const state{_handle.get()}; + + reset(); + return state; +} + +bool Surface::supportsFamily(PhysicalDevice const& physicalDevice, uint32_t familyIndex) const { + debug_check(*this); + + VkBool32 support = false; + auto const result = vkGetPhysicalDeviceSurfaceSupportKHR( + physicalDevice.get(), + familyIndex, + _handle.get(), + &support + ); + CVK_RESULT_STABLE_THROW(result != VK_SUCCESS, result, "device-surface support query failed") {} + + return support; +} + +auto Surface::presentModes(PhysicalDevice const& physicalDevice) const -> vector { + debug_check(*this); + + uint32_t size = 0; + auto const countResult = vkGetPhysicalDeviceSurfacePresentModesKHR( + physicalDevice.get(), + _handle.get(), + &size, + nullptr + ); + CVK_RESULT_STABLE_THROW(countResult != VK_SUCCESS, countResult, "device-surface present modes query failed") {} + + vector modes(size); + auto const writeResult = vkGetPhysicalDeviceSurfacePresentModesKHR( + physicalDevice.get(), + _handle.get(), + &size, + modes.data() + ); + CVK_RESULT_STABLE_THROW(writeResult != VK_SUCCESS, writeResult, "device-surface present modes query failed") {} + + return filter_allowed(std::move(modes), _config.allowedPresentModes); +} + +auto Surface::formats(PhysicalDevice const& physicalDevice) const -> vector { + debug_check(*this); + + uint32_t size = 0; + auto const countResult = vkGetPhysicalDeviceSurfaceFormatsKHR( + physicalDevice.get(), + _handle.get(), + &size, + nullptr + ); + CVK_RESULT_STABLE_THROW(countResult != VK_SUCCESS, countResult, "device-surface formats query failed") {} + + vector formats(size); + auto const writeResult = vkGetPhysicalDeviceSurfaceFormatsKHR( + physicalDevice.get(), + _handle.get(), + &size, + formats.data() + ); + CVK_RESULT_STABLE_THROW(writeResult != VK_SUCCESS, writeResult, "device-surface formats query failed") {} + + return filter_allowed(std::move(formats), _config.allowedSurfaceFormats); +} + +auto Surface::format(PhysicalDevice const& physicalDevice) const -> format_t { + auto const availableFormats = formats(physicalDevice); + CTH_STABLE_ERR(availableFormats.empty(), "no suitable format found") { + details->add("allowed: {}", _config.allowedSurfaceFormats); + throw details->exception(); + } + return availableFormats[0]; +} + +VkSurfaceCapabilitiesKHR Surface::capabilities(PhysicalDevice const& physicalDevice) const { + debug_check(*this); + + VkSurfaceCapabilitiesKHR capabilities{}; + auto const result = vkGetPhysicalDeviceSurfaceCapabilitiesKHR( + physicalDevice.get(), + _handle.get(), + &capabilities + ); + CVK_RESULT_STABLE_THROW(result != VK_SUCCESS, result, "device-surface capabilities query failed") {} + + return capabilities; +} + +auto Surface::presentMode(PhysicalDevice const& physicalDevice) const -> present_mode_t { + auto const availableModes = presentModes(physicalDevice); + CTH_STABLE_ERR(availableModes.empty(), "no suitable present mode found") { + details->add("allowed: {}", _config.allowedPresentModes); + throw details->exception(); + } + return availableModes[0]; +} + +void Surface::destroy(vk_not_null vkInstance, VkSurfaceKHR vkSurface) { + CTH_WARN(vkSurface == VK_NULL_HANDLE, "vkSurface invalid (VK_NULL_HANDLE)") {} + + vkDestroySurfaceKHR(vkInstance.get(), vkSurface, nullptr); + + log::msg("destroyed surface"); +} + +void Surface::reset() { _handle = VK_NULL_HANDLE; } +} diff --git a/cvk/src/utility/device/physical_device_features.cpp b/cvk/src/utility/device/physical_device_features.cpp new file mode 100644 index 00000000..15220f34 --- /dev/null +++ b/cvk/src/utility/device/physical_device_features.cpp @@ -0,0 +1,56 @@ +#include "cvk/utility/device/physical_device_features.hpp" + +#include + + +namespace cvk::utils { +PhysicalDeviceFeatures::PhysicalDeviceFeatures() : _chain{ + VkPhysicalDeviceFeatures2{.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2, .pNext = nullptr, .features{}}, + VkPhysicalDeviceVulkan11Features{.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_1_FEATURES, .pNext = nullptr}, + VkPhysicalDeviceVulkan12Features{.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_2_FEATURES, .pNext = nullptr}, + VkPhysicalDeviceVulkan13Features{.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_3_FEATURES, .pNext = nullptr}, +} { link(); } + +PhysicalDeviceFeatures::PhysicalDeviceFeatures(vk_not_null vkDevice) : PhysicalDeviceFeatures{} { + vkGetPhysicalDeviceFeatures2(vkDevice.get(), &features()); +} + +auto PhysicalDeviceFeatures::supports( + PhysicalDeviceFeatures const& requiredFeatures +) const -> std::vector { + std::vector missingFeatures{}; + + auto const check = [&](T const& available, T const& required) { + auto const flags = std::views::zip(dev::flags(available), dev::flags(required)); + + for(auto const [index, flag] : std::views::enumerate(flags)) + if(std::get<1>(flag) && !std::get<0>(flag)) + missingFeatures.emplace_back(available.sType, static_cast(index)); + }; + + std::apply( + [&](Ts const&... available) { (check(available, std::get(requiredFeatures._chain)), ...); }, + _chain + ); + + return missingFeatures; +} + +void PhysicalDeviceFeatures::merge(PhysicalDeviceFeatures const& other) { + auto const merge = [&](T& to, T const& from) { + for(auto [toFlag, fromFlag] : std::views::zip(dev::flags(to), dev::flags(from))) + toFlag |= fromFlag; + }; + + std::apply([&](Ts&... to) { (merge(to, std::get(other._chain)), ...); }, _chain); +} + +void PhysicalDeviceFeatures::link() { + auto& [features2, features11, features12, features13] = _chain; + + features2.pNext = &features11; + features11.pNext = &features12; + features12.pNext = &features13; + features13.pNext = nullptr; +} +} diff --git a/cvk/tests/src/base/core_test.cpp b/cvk/tests/src/base/core_test.cpp new file mode 100644 index 00000000..936e9e0b --- /dev/null +++ b/cvk/tests/src/base/core_test.cpp @@ -0,0 +1,183 @@ +#include "headless.hpp" +#include "test.hpp" + +#include "cvk/base/core.hpp" +#include "cvk/base/device.hpp" +#include "cvk/base/instance.hpp" +#include "cvk/base/physical_device.hpp" +#include "cvk/utility/vk_exceptions.hpp" + +#include +#include +#include + + +namespace cvk { +using test::APP_NAME; +using test::NO_EXTENSIONS; + +namespace { + /** + * @brief one queue, some devices have one queue only + */ + Core::Config const CONFIG{.queueProperties = {QueueFamilyProperties::GRAPHICS | QueueFamilyProperties::COMPUTE}}; + Core::Config const TWO_QUEUES{.queueProperties = {QueueFamilyProperties::GRAPHICS, QueueFamilyProperties::COMPUTE}}; + + /** + * @brief every queue in one group + */ + Core::queue_set_t one_group(size_t queues) { + Core::queue_set_t::group_t group(queues); + std::iota(group.begin(), group.end(), 0uz); + return Core::queue_set_t{queues, {group}}; + } +} + +CVK_TEST(Core, base_constructor_does_not_create) { + Instance const instance{APP_NAME, NO_EXTENSIONS, std::nullopt}; + + Core const core{instance}; + + EXPECT_FALSE(core.created()); + EXPECT_EQ(&core.instance(), &instance); +} + +CVK_TEST(Core, create_without_queue_sets) { + Instance const instance{APP_NAME, NO_EXTENSIONS, std::nullopt}; + + Core const core{instance, {}, CONFIG}; + + ASSERT_TRUE(core.created()); + EXPECT_EQ(core.vkInstance(), instance.get()); + EXPECT_EQ(core.vkPhysicalDevice(), core.physicalDevice().get()); + EXPECT_EQ(core.vkDevice(), core.device().get()); + EXPECT_EQ(core.functions(), core.device().functions()); + EXPECT_EQ(core.deviceTable().device(), core.vkDevice()); + EXPECT_EQ(&core.device().physicalDevice(), &core.physicalDevice()); + + ASSERT_EQ(core.queues().size(), CONFIG.queueProperties.size()); + EXPECT_EQ(core.device().queues().size(), CONFIG.queueProperties.size()); + + for(size_t i = 0; i < CONFIG.queueProperties.size(); i++) { + auto const& queue = core.queue(i); + ASSERT_TRUE(queue.created()); + EXPECT_TRUE(contains(queue.familyProperties(), CONFIG.queueProperties[i])); + queue.wait(); + } +} + +CVK_TEST(Core, create_one_queue_per_entry) { + Instance const instance{APP_NAME, NO_EXTENSIONS, std::nullopt}; + + if(!PhysicalDevice::AutoPick(instance, {}, TWO_QUEUES.queueProperties)) + GTEST_SKIP() << "needs a device with two queues"; + + Core const core{instance, {}, TWO_QUEUES}; + + ASSERT_EQ(core.queues().size(), 2); + EXPECT_EQ(core.device().queues().size(), 2); + EXPECT_NE(core.queue(0).get(), core.queue(1).get()); +} + +CVK_TEST(Core, queue_set_group_shares_a_queue) { + Instance const instance{APP_NAME, NO_EXTENSIONS, std::nullopt}; + + auto config = TWO_QUEUES; + config.queueSets.push_back(one_group(config.queueProperties.size())); + + Core const core{instance, {}, config}; + + ASSERT_TRUE(core.created()); + EXPECT_EQ(core.queueSetIndex(), 0); + EXPECT_EQ(core.device().queues().size(), 1); + EXPECT_EQ(core.queue(0).get(), core.queue(1).get()); + + EXPECT_TRUE(contains(core.queue(0).familyProperties(), QueueFamilyProperties::GRAPHICS)); + EXPECT_TRUE(contains(core.queue(0).familyProperties(), QueueFamilyProperties::COMPUTE)); +} + +CVK_TEST(Core, first_possible_queue_set_wins) { + Instance const instance{APP_NAME, NO_EXTENSIONS, std::nullopt}; + + // no device has this many compute queues + constexpr size_t queues = 256; + + Core::Config const config{ + .queueProperties = std::vector(queues, QueueFamilyProperties::COMPUTE), + .queueSets = {Core::queue_set_t{queues}, one_group(queues)}, + }; + + Core const core{instance, {}, config}; + + ASSERT_TRUE(core.created()); + EXPECT_EQ(core.queueSetIndex(), 1); + EXPECT_EQ(core.device().queues().size(), 1); + EXPECT_EQ(core.queues().size(), queues); +} + +CVK_TEST(Core, no_suitable_device_throws) { + Instance const instance{APP_NAME, NO_EXTENSIONS, std::nullopt}; + Core::Config const config{.queueProperties = {QueueFamilyProperties::PRESENT}}; + + auto const construct = [&] { Core const core{instance, {}, config}; }; + + EXPECT_THROW(construct(), cvk_exception); +} + +CVK_TEST(Core, release_and_wrap) { + Instance const instance{APP_NAME, NO_EXTENSIONS, std::nullopt}; + + Core core{instance, {}, CONFIG}; + auto const vkDevice = core.vkDevice(); + + auto state = core.release(); + EXPECT_FALSE(core.created()); + EXPECT_TRUE(core.queues().empty()); + + Core const wrapped{instance, std::move(state)}; + + ASSERT_TRUE(wrapped.created()); + EXPECT_EQ(wrapped.vkDevice(), vkDevice); + ASSERT_EQ(wrapped.queues().size(), CONFIG.queueProperties.size()); + wrapped.queue(0).wait(); +} + +CVK_TEST(Core, wrap_without_queues_takes_the_device_queues) { + Instance const instance{APP_NAME, NO_EXTENSIONS, std::nullopt}; + + Core core{instance, {}, CONFIG}; + auto state = core.release(); + state.queues.clear(); + + Core const wrapped{instance, std::move(state)}; + + EXPECT_EQ(wrapped.queues().size(), wrapped.device().queues().size()); +} + +CVK_TEST(Core, queues_work_after_move) { + Instance const instance{APP_NAME, NO_EXTENSIONS, std::nullopt}; + + Core core{instance, {}, CONFIG}; + Core const moved{std::move(core)}; + + EXPECT_FALSE(core.created()); + ASSERT_TRUE(moved.created()); + for(auto const& queue : moved.queues()) + queue.wait(); +} + +CVK_TEST(Core, present_queue_with_headless_surface) { + Instance const instance{APP_NAME, test::headless_extensions(), std::nullopt}; + std::array const surfaces{test::headless_surface(instance)}; + + Core::Config const config{ + .queueProperties = {QueueFamilyProperties::GRAPHICS | QueueFamilyProperties::PRESENT} + }; + + Core const core{instance, surfaces, config}; + + ASSERT_TRUE(core.created()); + EXPECT_TRUE(core.queue(0).can_present()); + EXPECT_TRUE(surfaces[0].supportsFamily(core.physicalDevice(), core.queue(0).familyIndex())); +} +} diff --git a/cvk/tests/src/base/device_test.cpp b/cvk/tests/src/base/device_test.cpp new file mode 100644 index 00000000..ce99d461 --- /dev/null +++ b/cvk/tests/src/base/device_test.cpp @@ -0,0 +1,186 @@ +#include "headless.hpp" +#include "test.hpp" + +#include "cvk/base/device.hpp" +#include "cvk/base/instance.hpp" +#include "cvk/base/physical_device.hpp" + +#include +#include +#include + + +namespace cvk { +using test::APP_NAME; +using test::NO_EXTENSIONS; + +namespace { + /** + * @brief one queue, some devices have one queue only + */ + constexpr std::array QUEUES{ + QueueFamilyProperties::GRAPHICS | QueueFamilyProperties::COMPUTE | QueueFamilyProperties::TRANSFER + }; + constexpr std::array THREE_QUEUES{ + QueueFamilyProperties::GRAPHICS, + QueueFamilyProperties::COMPUTE, + QueueFamilyProperties::TRANSFER + }; +} + +CVK_TEST(Device, base_constructor_does_not_create) { + Instance const instance{APP_NAME, NO_EXTENSIONS, std::nullopt}; + auto const [physicalDevice, _] = test::pick(instance, QUEUES); + + Device const device{instance, physicalDevice}; + + EXPECT_FALSE(device.created()); + EXPECT_TRUE(device.queues().empty()); +} + +CVK_TEST(Device, create_retrieves_the_queue) { + Instance const instance{APP_NAME, NO_EXTENSIONS, std::nullopt}; + auto const [physicalDevice, familyIndices] = test::pick(instance, QUEUES); + + Device const device{instance, physicalDevice, Device::Config{familyIndices}}; + + ASSERT_TRUE(device.created()); + EXPECT_EQ(&device.physicalDevice(), &physicalDevice); + EXPECT_EQ(&device.instance(), &instance); + ASSERT_EQ(device.queues().size(), 1); + + auto const& queue = device.queues()[0]; + ASSERT_TRUE(queue.created()); + EXPECT_EQ(queue.familyIndex(), familyIndices[0]); + EXPECT_EQ(queue.index(), 0); + EXPECT_TRUE(contains(queue.familyProperties(), QUEUES[0])); + queue.wait(); + + device.waitIdle(); +} + +CVK_TEST(Device, create_retrieves_the_queues) { + Instance const instance{APP_NAME, NO_EXTENSIONS, std::nullopt}; + + auto const result = PhysicalDevice::AutoPick(instance, {}, THREE_QUEUES); + if(!result) GTEST_SKIP() << "needs a device with three queues"; + auto const& [physicalDevice, familyIndices] = *result; + + Device const device{instance, physicalDevice, Device::Config{familyIndices}}; + + ASSERT_TRUE(device.created()); + ASSERT_EQ(device.queues().size(), THREE_QUEUES.size()); + + std::map queueIndices{}; + for(auto const& [queue, familyIndex, properties] : std::views::zip(device.queues(), familyIndices, THREE_QUEUES)) { + ASSERT_TRUE(queue.created()); + EXPECT_EQ(queue.familyIndex(), familyIndex); + EXPECT_EQ(queue.index(), queueIndices[familyIndex]++); + EXPECT_TRUE(contains(queue.familyProperties(), properties)); + queue.wait(); + } + + device.waitIdle(); +} + +CVK_TEST(Device, queues_of_one_family_get_increasing_indices) { + Instance const instance{APP_NAME, NO_EXTENSIONS, std::nullopt}; + auto const [physicalDevice, _] = test::pick(instance, QUEUES); + + auto const families = physicalDevice.queueFamilies(); + auto const family = std::ranges::find_if(families, [](auto const& f) { return f.vkProperties.queueCount >= 2; }); + if(family == families.end()) GTEST_SKIP() << "needs a queue family with two queues"; + + Device const device{instance, physicalDevice, Device::Config{{family->index, family->index}}}; + + ASSERT_EQ(device.queues().size(), 2); + EXPECT_EQ(device.queues()[0].index(), 0); + EXPECT_EQ(device.queues()[1].index(), 1); + EXPECT_NE(device.queues()[0].get(), device.queues()[1].get()); +} + +CVK_TEST(Device, queues_work_after_move) { + Instance const instance{APP_NAME, NO_EXTENSIONS, std::nullopt}; + auto const [physicalDevice, familyIndices] = test::pick(instance, QUEUES); + + Device device{instance, physicalDevice, Device::Config{familyIndices}}; + auto const vkDevice = device.get(); + + Device const moved{std::move(device)}; + + EXPECT_FALSE(device.created()); + ASSERT_TRUE(moved.created()); + EXPECT_EQ(moved.get(), vkDevice); + + for(auto const& queue : moved.queues()) + queue.wait(); +} + +CVK_TEST(Device, release_and_wrap) { + Instance const instance{APP_NAME, NO_EXTENSIONS, std::nullopt}; + auto const [physicalDevice, familyIndices] = test::pick(instance, QUEUES); + + Device device{instance, physicalDevice, Device::Config{familyIndices}}; + auto const vkDevice = device.get(); + + auto state = device.release(); + + EXPECT_FALSE(device.created()); + EXPECT_TRUE(device.queues().empty()); + EXPECT_EQ(state.vkDevice.get(), vkDevice); + ASSERT_NE(state.functionTable, nullptr); + + Device const wrapped{instance, physicalDevice, std::move(state)}; + + ASSERT_TRUE(wrapped.created()); + EXPECT_EQ(wrapped.get(), vkDevice); + EXPECT_TRUE(wrapped.queues().empty()); + wrapped.waitIdle(); +} + +CVK_TEST(Device, wrap_without_table_loads_it) { + Instance const instance{APP_NAME, NO_EXTENSIONS, std::nullopt}; + auto const [physicalDevice, familyIndices] = test::pick(instance, QUEUES); + + Device device{instance, physicalDevice, Device::Config{familyIndices}}; + auto state = device.release(); + state.functionTable = nullptr; + + Device const wrapped{instance, physicalDevice, std::move(state)}; + + ASSERT_TRUE(wrapped.created()); + EXPECT_NE(wrapped.functions()->vkDestroyDevice, nullptr); + wrapped.waitIdle(); +} + +CVK_TEST(Device, release_twice_is_safe) { + Instance const instance{APP_NAME, NO_EXTENSIONS, std::nullopt}; + auto const [physicalDevice, familyIndices] = test::pick(instance, QUEUES); + + Device device{instance, physicalDevice, Device::Config{familyIndices}}; + Device first{instance, physicalDevice, device.release()}; + + device.create(Device::Config{familyIndices}); + ASSERT_TRUE(device.created()); + EXPECT_NE(device.functions()->vkDestroyDevice, nullptr); + + Device const second{instance, physicalDevice, device.release()}; + EXPECT_TRUE(second.created()); +} + +CVK_TEST(Device, present_enables_swapchain_maintenance1) { + Instance const instance{APP_NAME, test::headless_extensions(), std::nullopt}; + std::array const surfaces{test::headless_surface(instance)}; + std::array const queues{QueueFamilyProperties::GRAPHICS | QueueFamilyProperties::PRESENT}; + + auto const [physicalDevice, familyIndices] = test::pick(instance, queues, surfaces); + + Device const device{instance, physicalDevice, Device::Config{familyIndices}}; + + ASSERT_TRUE(device.created()); + ASSERT_EQ(device.queues().size(), 1); + EXPECT_TRUE(device.queues()[0].can_present()); + EXPECT_NE(device.functions()->vkCreateSwapchainKHR, nullptr); + EXPECT_NE(device.functions()->vkReleaseSwapchainImagesKHR, nullptr); +} +} diff --git a/cvk/tests/src/base/physical_device_test.cpp b/cvk/tests/src/base/physical_device_test.cpp new file mode 100644 index 00000000..d64fbf0b --- /dev/null +++ b/cvk/tests/src/base/physical_device_test.cpp @@ -0,0 +1,150 @@ +#include "headless.hpp" +#include "test.hpp" + +#include "cvk/base/instance.hpp" +#include "cvk/base/physical_device.hpp" +#include "cvk/utility/constant/device_constants.hpp" + +#include +#include +#include +#include + + +namespace cvk { +using test::APP_NAME; +using test::NO_EXTENSIONS; + +namespace { + /** + * @brief one queue, some devices have one queue only + */ + constexpr std::array QUEUES{ + QueueFamilyProperties::GRAPHICS | QueueFamilyProperties::COMPUTE | QueueFamilyProperties::TRANSFER + }; + constexpr std::array PRESENT_QUEUES{QueueFamilyProperties::GRAPHICS | QueueFamilyProperties::PRESENT}; + + bool requires_extension(PhysicalDevice const& device, std::string_view extension) { + return std::ranges::contains(device.requiredExtensions(), extension); + } +} + +CVK_TEST(PhysicalDevice, auto_pick_finds_the_queues) { + Instance const instance{APP_NAME, NO_EXTENSIONS, std::nullopt}; + + auto const result = PhysicalDevice::AutoPick(instance, {}, QUEUES); + + ASSERT_TRUE(result.has_value()); + auto const& [device, familyIndices] = *result; + + ASSERT_TRUE(device.created()); + ASSERT_EQ(familyIndices.size(), QUEUES.size()); + + for(auto const [familyIndex, properties] : std::views::zip(familyIndices, QUEUES)) { + auto const* family = device.queueFamily(familyIndex); + ASSERT_NE(family, nullptr); + EXPECT_TRUE(contains(family->properties, properties)); + } +} + +CVK_TEST(PhysicalDevice, auto_pick_adds_the_defaults) { + Instance const instance{APP_NAME, NO_EXTENSIONS, std::nullopt}; + + auto const [device, _] = test::pick(instance, QUEUES); + + EXPECT_TRUE(device.supports(constants::required_device_features()).empty()); + EXPECT_TRUE(device.requiredFeatures().supports(constants::required_device_features()).empty()); + + for(auto const extension : constants::PRESENT_DEVICE_EXTENSIONS) + EXPECT_FALSE(requires_extension(device, extension)) << extension; +} + +CVK_TEST(PhysicalDevice, auto_pick_present_without_surfaces_finds_nothing) { + Instance const instance{APP_NAME, NO_EXTENSIONS, std::nullopt}; + + EXPECT_FALSE(PhysicalDevice::AutoPick(instance, {}, PRESENT_QUEUES).has_value()); +} + +CVK_TEST(PhysicalDevice, auto_pick_present_requires_the_present_extensions) { + Instance const instance{APP_NAME, test::headless_extensions(), std::nullopt}; + std::array const surfaces{test::headless_surface(instance)}; + + auto const [device, familyIndices] = test::pick(instance, PRESENT_QUEUES, surfaces); + + for(auto const extension : constants::PRESENT_DEVICE_EXTENSIONS) + EXPECT_TRUE(requires_extension(device, extension)) << extension; + + ASSERT_EQ(familyIndices.size(), 1); + EXPECT_TRUE(surfaces[0].supportsFamily(device, familyIndices[0])); +} + +CVK_TEST(PhysicalDevice, auto_pick_missing_extension_finds_nothing) { + Instance const instance{APP_NAME, NO_EXTENSIONS, std::nullopt}; + std::vector const extensions{"VK_CVK_extension_that_does_not_exist"}; + + EXPECT_FALSE(PhysicalDevice::AutoPick(instance, {}, QUEUES, extensions).has_value()); +} + +CVK_TEST(PhysicalDevice, query_queue_families_without_surfaces_has_no_present) { + Instance const instance{APP_NAME, NO_EXTENSIONS, std::nullopt}; + + for(auto const vkDevice : PhysicalDevice::enumerateVkDevices(instance.get())) + for(auto const& family : PhysicalDevice::queryQueueFamilies({}, vkDevice)) + EXPECT_FALSE(contains(family.properties, QueueFamilyProperties::PRESENT)); +} + +CVK_TEST(PhysicalDevice, wrap_queries_the_missing_members) { + Instance const instance{APP_NAME, NO_EXTENSIONS, std::nullopt}; + auto const [picked, _] = test::pick(instance, QUEUES); + + PhysicalDevice const device{ + instance, + picked.requiredFeatures(), + picked.requiredExtensions(), + PhysicalDevice::State{.vkDevice = picked.get(), .queueFamilies = {}} + }; + + ASSERT_TRUE(device.created()); + EXPECT_EQ(device.get(), picked.get()); + EXPECT_EQ(device.maxSampleCount(), picked.maxSampleCount()); + EXPECT_EQ(device.queueFamilies().size(), picked.queueFamilies().size()); + EXPECT_TRUE(std::ranges::equal(device.extensions(), picked.extensions())); + EXPECT_STREQ(device.properties().deviceName, picked.properties().deviceName); +} + +CVK_TEST(PhysicalDevice, copy_keeps_the_queried_members) { + Instance const instance{APP_NAME, NO_EXTENSIONS, std::nullopt}; + auto const [device, _] = test::pick(instance, QUEUES); + + PhysicalDevice const copy{device}; + + EXPECT_EQ(copy.get(), device.get()); + EXPECT_TRUE(std::ranges::equal(copy.queueFamilies(), device.queueFamilies())); + EXPECT_TRUE(copy.supports(device.requiredFeatures()).empty()); + EXPECT_EQ(copy.features().get()->pNext, ©.features().features()); +} + +CVK_TEST(PhysicalDevice, find_supported_format) { + Instance const instance{APP_NAME, NO_EXTENSIONS, std::nullopt}; + auto const [device, _] = test::pick(instance, QUEUES); + + // R8G8B8A8_UNORM sampled images with optimal tiling are mandatory + std::array const candidates{VK_FORMAT_R8G8B8A8_UNORM}; + + auto const format = device.findSupportedFormat( + candidates, + VK_IMAGE_TILING_OPTIMAL, + VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT + ); + EXPECT_EQ(format, VK_FORMAT_R8G8B8A8_UNORM); + + auto const findNone = [&] { + [[maybe_unused]] auto const none = device.findSupportedFormat( + {}, + VK_IMAGE_TILING_OPTIMAL, + VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT + ); + }; + EXPECT_THROW(findNone(), cth::except::default_exception); +} +} diff --git a/cvk/tests/src/base/queue/queue_test.cpp b/cvk/tests/src/base/queue/queue_test.cpp new file mode 100644 index 00000000..9a458054 --- /dev/null +++ b/cvk/tests/src/base/queue/queue_test.cpp @@ -0,0 +1,110 @@ +#include "headless.hpp" +#include "test.hpp" + +#include "cvk/base/device.hpp" +#include "cvk/base/instance.hpp" +#include "cvk/base/physical_device.hpp" + +#include +#include + + +namespace cvk { +using test::APP_NAME; +using test::NO_EXTENSIONS; + +namespace { + constexpr std::array QUEUES{QueueFamilyProperties::GRAPHICS}; + + VkSubmitInfo2 const EMPTY_SUBMIT{ + .sType = VK_STRUCTURE_TYPE_SUBMIT_INFO_2, + .pNext = nullptr, + .flags = 0, + .waitSemaphoreInfoCount = 0, + .pWaitSemaphoreInfos = nullptr, + .commandBufferInfoCount = 0, + .pCommandBufferInfos = nullptr, + .signalSemaphoreInfoCount = 0, + .pSignalSemaphoreInfos = nullptr, + }; +} + +CVK_TEST(Queue, base_constructor_does_not_create) { + Queue const queue{QueueFamilyProperties::GRAPHICS}; + + EXPECT_FALSE(queue.created()); + EXPECT_FALSE(queue.can_present()); +} + +CVK_TEST(Queue, copies_share_the_queue) { + Instance const instance{APP_NAME, NO_EXTENSIONS, std::nullopt}; + auto const [physicalDevice, familyIndices] = test::pick(instance, QUEUES); + Device const device{instance, physicalDevice, Device::Config{familyIndices}}; + + auto const& queue = device.queues()[0]; + Queue const copy{queue}; + + ASSERT_TRUE(copy.created()); + EXPECT_EQ(copy.get(), queue.get()); + EXPECT_EQ(copy.familyIndex(), queue.familyIndex()); + EXPECT_EQ(copy.index(), queue.index()); + copy.wait(); +} + +CVK_TEST(Queue, raw_submit_signals_the_fence) { + Instance const instance{APP_NAME, NO_EXTENSIONS, std::nullopt}; + auto const [physicalDevice, familyIndices] = test::pick(instance, QUEUES); + Device const device{instance, physicalDevice, Device::Config{familyIndices}}; + auto const table = device.table(); + + VkFenceCreateInfo const fenceInfo{.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO, .pNext = nullptr, .flags = 0}; + VkFence fence = VK_NULL_HANDLE; + ASSERT_EQ(table->vkCreateFence(table.device(), &fenceInfo, nullptr, &fence), VK_SUCCESS); + + auto const& queue = device.queues()[0]; + queue.raw_submit(EMPTY_SUBMIT, fence); + + EXPECT_EQ(table->vkWaitForFences(table.device(), 1, &fence, VK_TRUE, UINT64_MAX), VK_SUCCESS); + + table->vkDestroyFence(table.device(), fence, nullptr); +} + +CVK_TEST(Queue, raw_submit_without_fence) { + Instance const instance{APP_NAME, NO_EXTENSIONS, std::nullopt}; + auto const [physicalDevice, familyIndices] = test::pick(instance, QUEUES); + Device const device{instance, physicalDevice, Device::Config{familyIndices}}; + + auto const& queue = device.queues()[0]; + queue.raw_submit(EMPTY_SUBMIT); + queue.wait(); +} + +CVK_TEST(Queue, release_and_wrap) { + Instance const instance{APP_NAME, NO_EXTENSIONS, std::nullopt}; + auto const [physicalDevice, familyIndices] = test::pick(instance, QUEUES); + Device const device{instance, physicalDevice, Device::Config{familyIndices}}; + + Queue copy{device.queues()[0]}; + auto const vkQueue = copy.get(); + + auto state = copy.release(); + EXPECT_FALSE(copy.created()); + + Queue const wrapped{copy.familyProperties(), std::move(state)}; + + ASSERT_TRUE(wrapped.created()); + EXPECT_EQ(wrapped.get(), vkQueue); + wrapped.wait(); +} + +CVK_TEST(Queue, present_family_can_present) { + Instance const instance{APP_NAME, test::headless_extensions(), std::nullopt}; + std::array const surfaces{test::headless_surface(instance)}; + std::array const queues{QueueFamilyProperties::GRAPHICS | QueueFamilyProperties::PRESENT}; + + auto const [physicalDevice, familyIndices] = test::pick(instance, queues, surfaces); + Device const device{instance, physicalDevice, Device::Config{familyIndices}}; + + EXPECT_TRUE(device.queues()[0].can_present()); +} +} diff --git a/cvk/tests/src/headless.cpp b/cvk/tests/src/headless.cpp new file mode 100644 index 00000000..23024fd0 --- /dev/null +++ b/cvk/tests/src/headless.cpp @@ -0,0 +1,41 @@ +#include "headless.hpp" + +#include "cvk/utility/constant/device_constants.hpp" + +#include + + +namespace cvk::test { +std::vector headless_extensions() { + std::vector extensions{ + std::from_range, + constants::PRESENT_INSTANCE_EXTENSIONS | std::views::transform( + [](std::string_view extension) { return std::string{extension}; } + ) + }; + extensions.emplace_back(VK_EXT_HEADLESS_SURFACE_EXTENSION_NAME); + return extensions; +} + +Surface headless_surface(Instance const& instance, Surface::Config config) { + VkHeadlessSurfaceCreateInfoEXT const createInfo{ + .sType = VK_STRUCTURE_TYPE_HEADLESS_SURFACE_CREATE_INFO_EXT, + .pNext = nullptr, + .flags = 0, + }; + + VkSurfaceKHR vkSurface = VK_NULL_HANDLE; + auto const result = vkCreateHeadlessSurfaceEXT(instance.get(), &createInfo, nullptr, &vkSurface); + CTH_STABLE_THROW(result != VK_SUCCESS, "failed to create headless surface") {} + + return Surface{instance, std::move(config), Surface::State{vkSurface}}; +} + +PhysicalDevice::CreateResult pick( + Instance const& instance, + std::span queueProperties, + std::span surfaces +) { + return PhysicalDevice::AutoPick(instance, surfaces, queueProperties).value(); +} +} diff --git a/cvk/tests/src/headless.hpp b/cvk/tests/src/headless.hpp new file mode 100644 index 00000000..02f27393 --- /dev/null +++ b/cvk/tests/src/headless.hpp @@ -0,0 +1,32 @@ +#pragma once +#include "cvk/base/instance.hpp" +#include "cvk/base/physical_device.hpp" +#include "cvk/surface/surface.hpp" + +#include +#include +#include + + +namespace cvk::test { +/** + * @brief instance extensions for headless surfaces with present support + */ +[[nodiscard]] std::vector headless_extensions(); + +/** + * @brief creates a headless surface + * @pre @ref instance enables @ref headless_extensions() + */ +[[nodiscard]] Surface headless_surface(Instance const& instance, Surface::Config config = {}); + +/** + * @brief @ref PhysicalDevice::AutoPick() for tests + * @throws std::bad_optional_access no device meets the requirements + */ +[[nodiscard]] PhysicalDevice::CreateResult pick( + Instance const& instance, + std::span queueProperties, + std::span surfaces = {} +); +} diff --git a/cvk/tests/src/surface/surface_test.cpp b/cvk/tests/src/surface/surface_test.cpp new file mode 100644 index 00000000..d53eb2c4 --- /dev/null +++ b/cvk/tests/src/surface/surface_test.cpp @@ -0,0 +1,103 @@ +#include "headless.hpp" +#include "test.hpp" + +#include "cvk/base/instance.hpp" +#include "cvk/base/physical_device.hpp" +#include "cvk/surface/surface.hpp" + +#include +#include + + +namespace cvk { +using test::APP_NAME; + +namespace { + constexpr std::array PRESENT_QUEUES{QueueFamilyProperties::GRAPHICS | QueueFamilyProperties::PRESENT}; + + /** + * @brief config that allows everything + */ + Surface::Config const ALLOW_ALL{.allowedSurfaceFormats = {}, .allowedPresentModes = {}}; +} + +CVK_TEST(Surface, wrap_takes_ownership) { + Instance const instance{APP_NAME, test::headless_extensions(), std::nullopt}; + + auto surface = test::headless_surface(instance); + + ASSERT_TRUE(surface.created()); + EXPECT_NE(surface.get(), VK_NULL_HANDLE); + + surface.destroy(); + EXPECT_FALSE(surface.created()); +} + +CVK_TEST(Surface, release_keeps_the_surface) { + Instance const instance{APP_NAME, test::headless_extensions(), std::nullopt}; + auto surface = test::headless_surface(instance); + + auto const state = surface.release(); + EXPECT_FALSE(surface.created()); + + Surface::destroy(instance.get(), state.vkSurface.get()); +} + +CVK_TEST(Surface, empty_config_allows_everything) { + Instance const instance{APP_NAME, test::headless_extensions(), std::nullopt}; + std::array const surfaces{test::headless_surface(instance, ALLOW_ALL)}; + auto const [device, _] = test::pick(instance, PRESENT_QUEUES, surfaces); + + auto const& surface = surfaces[0]; + + EXPECT_FALSE(surface.formats(device).empty()); + EXPECT_FALSE(surface.presentModes(device).empty()); + EXPECT_TRUE(std::ranges::contains(surface.presentModes(device), VK_PRESENT_MODE_FIFO_KHR)); +} + +CVK_TEST(Surface, formats_keep_the_allowed_order) { + Instance const instance{APP_NAME, test::headless_extensions(), std::nullopt}; + std::array const surfaces{test::headless_surface(instance, ALLOW_ALL)}; + auto const [device, _] = test::pick(instance, PRESENT_QUEUES, surfaces); + + auto const available = surfaces[0].formats(device); + if(available.size() < 2) GTEST_SKIP() << "needs two surface formats"; + + Surface::Config const config{ + .allowedSurfaceFormats = {available[1], available[0]}, + .allowedPresentModes = {}, + }; + auto const surface = test::headless_surface(instance, config); + + auto const formats = surface.formats(device); + ASSERT_EQ(formats.size(), 2); + EXPECT_EQ(formats[0], available[1]); + EXPECT_EQ(formats[1], available[0]); + EXPECT_EQ(surface.format(device), available[1]); +} + +CVK_TEST(Surface, no_allowed_format_throws) { + Instance const instance{APP_NAME, test::headless_extensions(), std::nullopt}; + std::array const surfaces{test::headless_surface(instance, ALLOW_ALL)}; + auto const [device, _] = test::pick(instance, PRESENT_QUEUES, surfaces); + + Surface::Config const config{ + .allowedSurfaceFormats = {{VK_FORMAT_R64G64B64A64_SFLOAT, VK_COLOR_SPACE_SRGB_NONLINEAR_KHR}}, + .allowedPresentModes = {}, + }; + auto const surface = test::headless_surface(instance, config); + + EXPECT_TRUE(surface.formats(device).empty()); + EXPECT_THROW([[maybe_unused]] auto const f = surface.format(device), cth::except::default_exception); +} + +CVK_TEST(Surface, capabilities) { + Instance const instance{APP_NAME, test::headless_extensions(), std::nullopt}; + std::array const surfaces{test::headless_surface(instance)}; + auto const [device, _] = test::pick(instance, PRESENT_QUEUES, surfaces); + + auto const capabilities = surfaces[0].capabilities(device); + + EXPECT_GE(capabilities.minImageCount, 1); +} +} diff --git a/cvk/tests/src/utility/device/physical_device_features_test.cpp b/cvk/tests/src/utility/device/physical_device_features_test.cpp new file mode 100644 index 00000000..b95f343b --- /dev/null +++ b/cvk/tests/src/utility/device/physical_device_features_test.cpp @@ -0,0 +1,105 @@ +#include "test.hpp" + +#include "cvk/utility/device/physical_device_features.hpp" + +#include + + +namespace cvk::utils { +namespace { + void expect_linked(PhysicalDeviceFeatures const& features) { + auto const* const features2 = features.get(); + + EXPECT_EQ(features2, &features.features()); + EXPECT_EQ(features2->sType, VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2); + EXPECT_EQ(features2->pNext, &features.features()); + EXPECT_EQ( + features.features().pNext, + &features.features() + ); + EXPECT_EQ( + features.features().pNext, + &features.features() + ); + EXPECT_EQ(features.features().pNext, nullptr); + } + + constexpr size_t SAMPLER_ANISOTROPY_INDEX = offsetof(VkPhysicalDeviceFeatures, samplerAnisotropy) / sizeof(VkBool32); + constexpr size_t SYNCHRONIZATION2_INDEX = + (offsetof(VkPhysicalDeviceVulkan13Features, synchronization2) + - dev::feature_flags::OFFSET) / sizeof(VkBool32); +} + +CVK_TEST(PhysicalDeviceFeatures, default_is_linked_and_false) { + PhysicalDeviceFeatures const features{}; + + expect_linked(features); + + EXPECT_FALSE(features.features().features.samplerAnisotropy); + EXPECT_FALSE(features.features().synchronization2); +} + +CVK_TEST(PhysicalDeviceFeatures, copy_links_its_own_chain) { + PhysicalDeviceFeatures features{}; + features.features().timelineSemaphore = VK_TRUE; + + PhysicalDeviceFeatures const copy{features}; + expect_linked(copy); + EXPECT_TRUE(copy.features().timelineSemaphore); + + PhysicalDeviceFeatures assigned{}; + assigned = copy; + expect_linked(assigned); + EXPECT_TRUE(assigned.features().timelineSemaphore); +} + +CVK_TEST(PhysicalDeviceFeatures, move_links_its_own_chain) { + PhysicalDeviceFeatures features{}; + features.features().multiview = VK_TRUE; + + PhysicalDeviceFeatures const moved{std::move(features)}; + expect_linked(moved); + EXPECT_TRUE(moved.features().multiview); +} + +CVK_TEST(PhysicalDeviceFeatures, supports_reports_missing_features) { + PhysicalDeviceFeatures available{}; + available.features().timelineSemaphore = VK_TRUE; + + PhysicalDeviceFeatures required{}; + required.features().features.samplerAnisotropy = VK_TRUE; + required.features().timelineSemaphore = VK_TRUE; + required.features().synchronization2 = VK_TRUE; + + auto const missing = available.supports(required); + + ASSERT_EQ(missing.size(), 2); + EXPECT_EQ(missing[0].sType, VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2); + EXPECT_EQ(missing[0].index, SAMPLER_ANISOTROPY_INDEX); + EXPECT_EQ(missing[1].sType, VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_3_FEATURES); + EXPECT_EQ(missing[1].index, SYNCHRONIZATION2_INDEX); +} + +CVK_TEST(PhysicalDeviceFeatures, supports_itself) { + PhysicalDeviceFeatures features{}; + features.features().features.samplerAnisotropy = VK_TRUE; + features.features().dynamicRendering = VK_TRUE; + + EXPECT_TRUE(features.supports(features).empty()); +} + +CVK_TEST(PhysicalDeviceFeatures, merge_enables_the_features_of_both) { + PhysicalDeviceFeatures features{}; + features.features().features.samplerAnisotropy = VK_TRUE; + + PhysicalDeviceFeatures other{}; + other.features().synchronization2 = VK_TRUE; + + features.merge(other); + + EXPECT_TRUE(features.features().features.samplerAnisotropy); + EXPECT_TRUE(features.features().synchronization2); + EXPECT_FALSE(features.features().dynamicRendering); + expect_linked(features); +} +} From 0efbf3dc72c8b0c878d4796eb752fdb635a2b107 Mon Sep 17 00:00:00 2001 From: lukas <89145039+codethinki@users.noreply.github.com> Date: Fri, 16 Oct 2026 14:36:05 +0200 Subject: [PATCH 013/135] fix: move assignment destroys the old handle Defaulted move assignment leaked the old handle, cth::move_ptr does not destroy on assignment. Instance, DebugMessenger, Surface and Device now move assign with cth::reconstruct_at (needs cth with cth/utility.hpp). Tests count validation errors: a leaked child object shows as VUID-vkDestroyInstance-instance-00629. --- AGENTS.md | 6 +++++ cvk/incl/cvk/base/device.hpp | 3 ++- cvk/incl/cvk/base/instance.hpp | 3 ++- cvk/incl/cvk/debug/debug_messenger.hpp | 5 ++-- cvk/incl/cvk/surface/surface.hpp | 3 ++- cvk/tests/src/base/core_test.cpp | 22 ++++++++++++++++ cvk/tests/src/base/device_test.cpp | 25 +++++++++++++++++++ cvk/tests/src/base/instance_test.cpp | 11 ++++++++ cvk/tests/src/debug/debug_messenger_test.cpp | 23 +++++++++++++++++ cvk/tests/src/surface/surface_test.cpp | 20 +++++++++++++++ .../device/physical_device_features_test.cpp | 3 ++- cvk/tests/src/validation.cpp | 24 ++++++++++++++++++ cvk/tests/src/validation.hpp | 20 +++++++++++++++ 13 files changed, 162 insertions(+), 6 deletions(-) create mode 100644 cvk/tests/src/validation.cpp create mode 100644 cvk/tests/src/validation.hpp diff --git a/AGENTS.md b/AGENTS.md index a7dfae11..16fb54f7 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -39,6 +39,8 @@ ctest --test-dir out/build/gcc_release --output-on-failure - Presets: `gcc_debug`, `gcc_release`, `clang_debug`, `clang_release`, `msvc_debug`, `msvc_release`. - `cth_ROOT` points at `../cth/out/install/`. That install must exist. - Debug builds enable the validation layers. Run the tests in a debug build before you report a change as done. +- Tests that need more queues than the device has skip. Mesa Intel devices get more queues with `ANV_QUEUE_OVERRIDE=gc=2,c=1`. +- After a new cth install, build with `--clean-first`. The precompiled header does not see the changed cth headers. - `VCPKG_ROOT` must point at vcpkg. ## Code style @@ -154,6 +156,10 @@ public: - The first public section holds the API: types, constants, lifecycle, operations, accessors. - The last public section holds what every class has and readers rarely need: copy and move members, `debug_check` functions. - Classes that own a handle are move only. A class that differs (copyable, not movable, custom move) says so in its `@brief`. +- Their move assignment destroys the old handle: `X& operator=(X&& other) noexcept { return cth::reconstruct_at(*this, std::move(other)); }`. + `= default` leaks the old handle, `cth::move_ptr` does not destroy on assignment. +- A class derived from a handle class defines its move assignment with `cth::reconstruct_at` too. + `= default` calls the base move assignment on the base class subobject, there `reconstruct_at` is undefined behavior. After the class, in the same header: diff --git a/cvk/incl/cvk/base/device.hpp b/cvk/incl/cvk/base/device.hpp index 84ab45fb..b428cadc 100644 --- a/cvk/incl/cvk/base/device.hpp +++ b/cvk/incl/cvk/base/device.hpp @@ -8,6 +8,7 @@ #include #include +#include #include #include @@ -150,7 +151,7 @@ class Device { Device(Device const& other) = delete; Device(Device&& other) noexcept = default; Device& operator=(Device const& other) = delete; - Device& operator=(Device&& other) noexcept = default; + Device& operator=(Device&& other) noexcept { return cth::reconstruct_at(*this, std::move(other)); } static void debug_check(Device const& device); static void debug_check_handle(vk_not_null vkDevice); diff --git a/cvk/incl/cvk/base/instance.hpp b/cvk/incl/cvk/base/instance.hpp index c88159f1..8543437f 100644 --- a/cvk/incl/cvk/base/instance.hpp +++ b/cvk/incl/cvk/base/instance.hpp @@ -7,6 +7,7 @@ #include #include +#include #include #include @@ -145,7 +146,7 @@ class Instance { Instance(Instance const& other) = delete; Instance& operator=(Instance const& other) = delete; Instance(Instance&& other) noexcept = default; - Instance& operator=(Instance&& other) noexcept = default; + Instance& operator=(Instance&& other) noexcept { return cth::reconstruct_at(*this, std::move(other)); } static void debug_check(Instance const& instance); static void debug_check_handle(vk_not_null vkInstance); diff --git a/cvk/incl/cvk/debug/debug_messenger.hpp b/cvk/incl/cvk/debug/debug_messenger.hpp index a0daef3b..dd9f6eea 100644 --- a/cvk/incl/cvk/debug/debug_messenger.hpp +++ b/cvk/incl/cvk/debug/debug_messenger.hpp @@ -4,8 +4,9 @@ #include -#include #include +#include +#include namespace cvk { @@ -112,7 +113,7 @@ class DebugMessenger { DebugMessenger(DebugMessenger const& other) = delete; DebugMessenger(DebugMessenger&& other) noexcept = default; DebugMessenger& operator=(DebugMessenger const& other) = delete; - DebugMessenger& operator=(DebugMessenger&& other) noexcept = default; + DebugMessenger& operator=(DebugMessenger&& other) noexcept { return cth::reconstruct_at(*this, std::move(other)); } static void debug_check(DebugMessenger const& debugMessenger); }; diff --git a/cvk/incl/cvk/surface/surface.hpp b/cvk/incl/cvk/surface/surface.hpp index 5423aca4..14addf75 100644 --- a/cvk/incl/cvk/surface/surface.hpp +++ b/cvk/incl/cvk/surface/surface.hpp @@ -6,6 +6,7 @@ #include #include +#include #include @@ -122,7 +123,7 @@ class Surface { Surface(Surface const& other) = delete; Surface(Surface&& other) noexcept = default; Surface& operator=(Surface const& other) = delete; - Surface& operator=(Surface&& other) noexcept = default; + Surface& operator=(Surface&& other) noexcept { return cth::reconstruct_at(*this, std::move(other)); } static void debug_check(Surface const& surface); static void debug_check_handle(vk_not_null vkSurface); diff --git a/cvk/tests/src/base/core_test.cpp b/cvk/tests/src/base/core_test.cpp index 936e9e0b..2ec760a6 100644 --- a/cvk/tests/src/base/core_test.cpp +++ b/cvk/tests/src/base/core_test.cpp @@ -1,10 +1,12 @@ #include "headless.hpp" #include "test.hpp" +#include "validation.hpp" #include "cvk/base/core.hpp" #include "cvk/base/device.hpp" #include "cvk/base/instance.hpp" #include "cvk/base/physical_device.hpp" +#include "cvk/utility/constants.hpp" #include "cvk/utility/vk_exceptions.hpp" #include @@ -180,4 +182,24 @@ CVK_TEST(Core, present_queue_with_headless_surface) { EXPECT_TRUE(core.queue(0).can_present()); EXPECT_TRUE(surfaces[0].supportsFamily(core.physicalDevice(), core.queue(0).familyIndex())); } + +CVK_TEST(Core, move_assignment_destroys_the_old_core) { + if constexpr(!constants::ENABLE_VALIDATION_LAYERS) GTEST_SKIP() << "needs the validation layers"; + + test::reset_validation_errors(); + { + Instance const instance{APP_NAME, NO_EXTENSIONS, test::counting_messenger()}; + + Core core{instance, {}, CONFIG}; + Core other{instance, {}, CONFIG}; + auto const vkDevice = other.vkDevice(); + + core = std::move(other); + + ASSERT_EQ(core.vkDevice(), vkDevice); + EXPECT_FALSE(other.created()); + core.queue(0).wait(); + } + EXPECT_EQ(test::validation_errors(), 0); +} } diff --git a/cvk/tests/src/base/device_test.cpp b/cvk/tests/src/base/device_test.cpp index ce99d461..d7cc4940 100644 --- a/cvk/tests/src/base/device_test.cpp +++ b/cvk/tests/src/base/device_test.cpp @@ -1,9 +1,11 @@ #include "headless.hpp" #include "test.hpp" +#include "validation.hpp" #include "cvk/base/device.hpp" #include "cvk/base/instance.hpp" #include "cvk/base/physical_device.hpp" +#include "cvk/utility/constants.hpp" #include #include @@ -183,4 +185,27 @@ CVK_TEST(Device, present_enables_swapchain_maintenance1) { EXPECT_NE(device.functions()->vkCreateSwapchainKHR, nullptr); EXPECT_NE(device.functions()->vkReleaseSwapchainImagesKHR, nullptr); } + +CVK_TEST(Device, move_assignment_destroys_the_old_device) { + if constexpr(!constants::ENABLE_VALIDATION_LAYERS) GTEST_SKIP() << "needs the validation layers"; + + test::reset_validation_errors(); + { + Instance const instance{APP_NAME, NO_EXTENSIONS, test::counting_messenger()}; + auto const [physicalDevice, familyIndices] = test::pick(instance, QUEUES); + + Device device{instance, physicalDevice, Device::Config{familyIndices}}; + Device other{instance, physicalDevice, Device::Config{familyIndices}}; + auto const vkDevice = other.get(); + + device = std::move(other); + + ASSERT_EQ(device.get(), vkDevice); + EXPECT_FALSE(other.created()); + ASSERT_EQ(device.queues().size(), 1); + device.queues()[0].wait(); + device.waitIdle(); + } + EXPECT_EQ(test::validation_errors(), 0); +} } diff --git a/cvk/tests/src/base/instance_test.cpp b/cvk/tests/src/base/instance_test.cpp index e1cd5d3c..23c3b523 100644 --- a/cvk/tests/src/base/instance_test.cpp +++ b/cvk/tests/src/base/instance_test.cpp @@ -91,4 +91,15 @@ CVK_TEST(Instance, wrap_takes_ownership) { instance.destroy(); EXPECT_FALSE(instance.created()); } + +CVK_TEST(Instance, move_assignment_takes_the_other_instance) { + Instance instance{APP_NAME, NO_EXTENSIONS, std::nullopt}; + Instance other{APP_NAME, NO_EXTENSIONS, std::nullopt}; + auto const vkInstance = other.get(); + + instance = std::move(other); + + EXPECT_EQ(instance.get(), vkInstance); + EXPECT_FALSE(other.created()); +} } diff --git a/cvk/tests/src/debug/debug_messenger_test.cpp b/cvk/tests/src/debug/debug_messenger_test.cpp index 529bd55e..2de93f33 100644 --- a/cvk/tests/src/debug/debug_messenger_test.cpp +++ b/cvk/tests/src/debug/debug_messenger_test.cpp @@ -1,4 +1,5 @@ #include "test.hpp" +#include "validation.hpp" #include "cvk/base/instance.hpp" #include "cvk/debug/debug_messenger.hpp" @@ -115,4 +116,26 @@ CVK_TEST(DebugMessenger, release_keeps_the_messenger) { DebugMessenger::destroy(state.vkInstance, state.vkMessenger.get()); } + +CVK_TEST(DebugMessenger, move_assignment_destroys_the_old_messenger) { + if constexpr(!constants::ENABLE_VALIDATION_LAYERS) GTEST_SKIP() << "needs the validation layers"; + + test::reset_validation_errors(); + { + Instance const instance{APP_NAME, NO_EXTENSIONS, test::counting_messenger()}; + DebugMessenger messenger{DebugMessenger::Config::Default(count_test_messages), instance}; + DebugMessenger other{DebugMessenger::Config::Default(count_test_messages), instance}; + auto const vkMessenger = other.get(); + + messenger = std::move(other); + + EXPECT_EQ(messenger.get(), vkMessenger); + EXPECT_FALSE(other.created()); + + received = 0; + submit_test_message(instance); + EXPECT_EQ(received, 1); + } + EXPECT_EQ(test::validation_errors(), 0); +} } diff --git a/cvk/tests/src/surface/surface_test.cpp b/cvk/tests/src/surface/surface_test.cpp index d53eb2c4..8375851f 100644 --- a/cvk/tests/src/surface/surface_test.cpp +++ b/cvk/tests/src/surface/surface_test.cpp @@ -1,9 +1,11 @@ #include "headless.hpp" #include "test.hpp" +#include "validation.hpp" #include "cvk/base/instance.hpp" #include "cvk/base/physical_device.hpp" #include "cvk/surface/surface.hpp" +#include "cvk/utility/constants.hpp" #include #include @@ -100,4 +102,22 @@ CVK_TEST(Surface, capabilities) { EXPECT_GE(capabilities.minImageCount, 1); } + +CVK_TEST(Surface, move_assignment_destroys_the_old_surface) { + if constexpr(!constants::ENABLE_VALIDATION_LAYERS) GTEST_SKIP() << "needs the validation layers"; + + test::reset_validation_errors(); + { + Instance const instance{APP_NAME, test::headless_extensions(), test::counting_messenger()}; + auto surface = test::headless_surface(instance); + auto other = test::headless_surface(instance); + auto const vkSurface = other.get(); + + surface = std::move(other); + + EXPECT_EQ(surface.get(), vkSurface); + EXPECT_FALSE(other.created()); + } + EXPECT_EQ(test::validation_errors(), 0); +} } diff --git a/cvk/tests/src/utility/device/physical_device_features_test.cpp b/cvk/tests/src/utility/device/physical_device_features_test.cpp index b95f343b..df79457a 100644 --- a/cvk/tests/src/utility/device/physical_device_features_test.cpp +++ b/cvk/tests/src/utility/device/physical_device_features_test.cpp @@ -24,7 +24,8 @@ namespace { EXPECT_EQ(features.features().pNext, nullptr); } - constexpr size_t SAMPLER_ANISOTROPY_INDEX = offsetof(VkPhysicalDeviceFeatures, samplerAnisotropy) / sizeof(VkBool32); + constexpr size_t SAMPLER_ANISOTROPY_INDEX = + offsetof(VkPhysicalDeviceFeatures, samplerAnisotropy) / sizeof(VkBool32); constexpr size_t SYNCHRONIZATION2_INDEX = (offsetof(VkPhysicalDeviceVulkan13Features, synchronization2) - dev::feature_flags::OFFSET) / sizeof(VkBool32); diff --git a/cvk/tests/src/validation.cpp b/cvk/tests/src/validation.cpp new file mode 100644 index 00000000..c8e9783a --- /dev/null +++ b/cvk/tests/src/validation.cpp @@ -0,0 +1,24 @@ +#include "validation.hpp" + + +namespace cvk::test { +namespace { + size_t errors = 0; + + VKAPI_ATTR VkBool32 VKAPI_CALL count_errors( + VkDebugUtilsMessageSeverityFlagBitsEXT severity, + VkDebugUtilsMessageTypeFlagsEXT types, + VkDebugUtilsMessengerCallbackDataEXT const* callbackData, + void* userData + ) { + if(severity >= VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT) ++errors; + return DebugMessenger::Config::defaultCallback()(severity, types, callbackData, userData); + } +} + +DebugMessenger::Config counting_messenger() { return DebugMessenger::Config::Default(count_errors); } + +size_t validation_errors() { return errors; } + +void reset_validation_errors() { errors = 0; } +} diff --git a/cvk/tests/src/validation.hpp b/cvk/tests/src/validation.hpp new file mode 100644 index 00000000..bd633d03 --- /dev/null +++ b/cvk/tests/src/validation.hpp @@ -0,0 +1,20 @@ +#pragma once +#include "cvk/debug/debug_messenger.hpp" + +#include + + +namespace cvk::test { +/** + * @brief messenger config that logs every message and counts the errors + * @details for the instance create info too -> counts the errors of vkDestroyInstance(), e.g. leaked objects + */ +[[nodiscard]] DebugMessenger::Config counting_messenger(); + +/** + * @return errors since the last @ref reset_validation_errors() + */ +[[nodiscard]] size_t validation_errors(); + +void reset_validation_errors(); +} From d2264537d97ae42410a41bb6ec5c3c214ebdb09f Mon Sep 17 00:00:00 2001 From: lukas <89145039+codethinki@users.noreply.github.com> Date: Fri, 16 Oct 2026 14:58:35 +0200 Subject: [PATCH 014/135] cvk: port fence, semaphore and timeline semaphore - objects store core.deviceTable(), not the core: the core can move - no virtuals: TimelineSemaphore is a private Semaphore, it does not pass as a binary semaphore - timeline semaphore: core vulkan 1.2 functions, wait copies the counter (legacy read a dead temporary), wait and gpuValue check the result - fence: no os handle export (legacy stub was broken), FenceAwaiter goes to jvk - PipelineWaitStage is replaced by VkSemaphoreSubmitInfo - test::Context fails a test on validation errors, leaks included --- AGENTS.md | 3 + cvk/incl/cvk/render/sync/fence.hpp | 171 ++++++++++++++++++ cvk/incl/cvk/render/sync/semaphore.hpp | 123 +++++++++++++ .../cvk/render/sync/timeline_semaphore.hpp | 148 +++++++++++++++ cvk/src/render/sync/fence.cpp | 118 ++++++++++++ cvk/src/render/sync/semaphore.cpp | 66 +++++++ cvk/src/render/sync/timeline_semaphore.cpp | 102 +++++++++++ cvk/tests/src/context.cpp | 17 ++ cvk/tests/src/context.hpp | 34 ++++ cvk/tests/src/render/sync/fence_test.cpp | 106 +++++++++++ cvk/tests/src/render/sync/semaphore_test.cpp | 101 +++++++++++ .../render/sync/timeline_semaphore_test.cpp | 127 +++++++++++++ 12 files changed, 1116 insertions(+) create mode 100644 cvk/incl/cvk/render/sync/fence.hpp create mode 100644 cvk/incl/cvk/render/sync/semaphore.hpp create mode 100644 cvk/incl/cvk/render/sync/timeline_semaphore.hpp create mode 100644 cvk/src/render/sync/fence.cpp create mode 100644 cvk/src/render/sync/semaphore.cpp create mode 100644 cvk/src/render/sync/timeline_semaphore.cpp create mode 100644 cvk/tests/src/context.cpp create mode 100644 cvk/tests/src/context.hpp create mode 100644 cvk/tests/src/render/sync/fence_test.cpp create mode 100644 cvk/tests/src/render/sync/semaphore_test.cpp create mode 100644 cvk/tests/src/render/sync/timeline_semaphore_test.cpp diff --git a/AGENTS.md b/AGENTS.md index 16fb54f7..4a6932fa 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -41,6 +41,7 @@ ctest --test-dir out/build/gcc_release --output-on-failure - Debug builds enable the validation layers. Run the tests in a debug build before you report a change as done. - Tests that need more queues than the device has skip. Mesa Intel devices get more queues with `ANV_QUEUE_OVERRIDE=gc=2,c=1`. - After a new cth install, build with `--clean-first`. The precompiled header does not see the changed cth headers. +- Tests that need a device use `test::Context` (`cvk/tests/src/context.hpp`). It fails the test on validation errors, leaked objects included. - `VCPKG_ROOT` must point at vcpkg. ## Code style @@ -191,6 +192,8 @@ inline void Semaphore::debug_check(Semaphore const& semaphore) { | `reset()` | sets the members to the not created state, does not destroy | - The destructor calls `optDestroy()`. +- Vulkan has its own reset for some objects (`vkResetFences`, `vkResetCommandBuffer`). There the public `reset()` is the Vulkan operation and the private member reset is `resetState()`. +- Device objects take `Core const&` in the base constructor and store `core.deviceTable()`, not the core. The core can move, the device table stays valid. - A failed `create()` calls `reset()` before it throws. - `release()` exists so that an owner can use an object it must not destroy (for example swapchain images). diff --git a/cvk/incl/cvk/render/sync/fence.hpp b/cvk/incl/cvk/render/sync/fence.hpp new file mode 100644 index 00000000..f1187b43 --- /dev/null +++ b/cvk/incl/cvk/render/sync/fence.hpp @@ -0,0 +1,171 @@ +#pragma once +#include "cvk/base/device_table.hpp" +#include "cvk/utility/types.hpp" + +#include + +#include +#include + +#include + + +namespace cvk { +class Core; + +/** + * @brief VkFence wrapper + */ +class Fence { +public: + struct State; + + /** + * @brief base constructor + * @pre @ref Core::created() + */ + explicit Fence(Core const& core); + + /** + * @brief constructs and wraps + * @details calls: + * - @ref Fence(Core const&) + * - @ref wrap() + */ + Fence(Core const& core, State const& state); + + /** + * @brief constructs and creates + * @details calls: + * - @ref Fence(Core const&) + * - @ref create() + */ + Fence(Core const& core, VkFenceCreateFlags flags); + + /** + * @note calls @ref optDestroy() + */ + ~Fence() { optDestroy(); } + + /** + * @brief wraps the state + * @post @ref created() + * @note calls @ref optDestroy() + */ + void wrap(State const& state); + + /** + * @brief creates the fence + * @param flags VK_FENCE_CREATE_SIGNALED_BIT -> created signaled + * @post @ref created() + * @note calls @ref optDestroy() + * @throws cvk::vk_result_exception result of vkCreateFence() + */ + void create(VkFenceCreateFlags flags = 0); + + /** + * @brief destroys and resets + * @pre @ref created() + * @pre the gpu does not use the fence + */ + void destroy(); + + /** + * @brief if @ref created() -> calls @ref destroy() + */ + void optDestroy() { if(created()) destroy(); } + + /** + * @brief releases ownership, returns the state and resets + * @pre @ref created() + */ + State release(); + + /** + * @return VK_SUCCESS -> signaled, VK_NOT_READY -> not signaled + * @pre @ref created() + * @throws cvk::vk_result_exception other result of vkGetFenceStatus() + */ + [[nodiscard]] VkResult status() const; + + /** + * @pre @ref created() + */ + [[nodiscard]] bool signaled() const { return status() == VK_SUCCESS; } + + /** + * @brief sets the fence to not signaled + * @pre @ref created() + * @throws cvk::vk_result_exception result of vkResetFences() + */ + void reset() const; + + /** + * @brief blocks until the fence is signaled or the timeout ends + * @param timeout in nanoseconds + * @return VK_SUCCESS or VK_TIMEOUT + * @pre @ref created() + * @throws cvk::vk_result_exception other result of vkWaitForFences() + */ + [[nodiscard]] VkResult wait(uint64_t timeout) const; + + /** + * @brief blocks until the fence is signaled + * @pre @ref created() + * @throws cvk::vk_result_exception result of vkWaitForFences() + */ + void wait() const; + + /** + * @brief waits, then resets if signaled + * @param timeout in nanoseconds + * @return VK_SUCCESS -> reset, VK_TIMEOUT -> not reset + * @details calls: + * - @ref wait(uint64_t) const + * - @ref reset() + */ + [[nodiscard]] VkResult waitReset(uint64_t timeout) const; + + /** + * @brief waits and resets + * @details calls: + * - @ref wait() const + * - @ref reset() + */ + void waitReset() const; + + static void destroy(DeviceTable table, VkFence vkFence); + + [[nodiscard]] bool created() const { return _handle != VK_NULL_HANDLE; } + [[nodiscard]] VkFence get() const { return _handle.get(); } + [[nodiscard]] DeviceTable deviceTable() const { return _deviceTable; } + +private: + void resetState(); + + [[nodiscard]] static VkFenceCreateInfo createInfo(VkFenceCreateFlags flags); + + DeviceTable _deviceTable; + move_ptr _handle = VK_NULL_HANDLE; + +public: + Fence(Fence const& other) = delete; + Fence(Fence&& other) noexcept = default; + Fence& operator=(Fence const& other) = delete; + Fence& operator=(Fence&& other) noexcept { return cth::reconstruct_at(*this, std::move(other)); } + + static void debug_check(Fence const& fence); +}; +} + +namespace cvk { +struct Fence::State { + vk_not_null vkFence; +}; +} + +namespace cvk { +inline void Fence::debug_check(Fence const& fence) { + CTH_CRITICAL(!fence.created(), "fence must be created") {} +} +} diff --git a/cvk/incl/cvk/render/sync/semaphore.hpp b/cvk/incl/cvk/render/sync/semaphore.hpp new file mode 100644 index 00000000..adf03631 --- /dev/null +++ b/cvk/incl/cvk/render/sync/semaphore.hpp @@ -0,0 +1,123 @@ +#pragma once +#include "cvk/base/device_table.hpp" +#include "cvk/utility/constants.hpp" +#include "cvk/utility/types.hpp" + +#include + +#include +#include + + +namespace cvk { +class Core; + +/** + * @brief binary VkSemaphore wrapper + */ +class Semaphore { +public: + struct State; + + /** + * @brief base constructor + * @pre @ref Core::created() + */ + explicit Semaphore(Core const& core); + + /** + * @brief constructs and wraps + * @details calls: + * - @ref Semaphore(Core const&) + * - @ref wrap() + */ + Semaphore(Core const& core, State const& state); + + /** + * @brief constructs and creates + * @details calls: + * - @ref Semaphore(Core const&) + * - @ref create() + */ + Semaphore(Core const& core, create_t); + + /** + * @note calls @ref optDestroy() + */ + ~Semaphore() { optDestroy(); } + + /** + * @brief wraps the state + * @post @ref created() + * @note calls @ref optDestroy() + */ + void wrap(State const& state); + + /** + * @brief creates the semaphore + * @post @ref created() + * @note calls @ref optDestroy() + * @throws cvk::vk_result_exception result of vkCreateSemaphore() + */ + void create(); + + /** + * @brief destroys and resets + * @pre @ref created() + * @pre the gpu does not use the semaphore + */ + void destroy(); + + /** + * @brief if @ref created() -> calls @ref destroy() + */ + void optDestroy() { if(created()) destroy(); } + + /** + * @brief releases ownership, returns the state and resets + * @pre @ref created() + */ + State release(); + + static void destroy(DeviceTable table, VkSemaphore vkSemaphore); + + [[nodiscard]] bool created() const { return _handle != VK_NULL_HANDLE; } + [[nodiscard]] VkSemaphore get() const { return _handle.get(); } + [[nodiscard]] DeviceTable deviceTable() const { return _deviceTable; } + +protected: + /** + * @brief creates the semaphore with info + * @post @ref created() + * @note calls @ref optDestroy() + * @throws cvk::vk_result_exception result of vkCreateSemaphore() + */ + void create(VkSemaphoreCreateInfo const& info); + +private: + void reset(); + + DeviceTable _deviceTable; + move_ptr _handle = VK_NULL_HANDLE; + +public: + Semaphore(Semaphore const& other) = delete; + Semaphore(Semaphore&& other) noexcept = default; + Semaphore& operator=(Semaphore const& other) = delete; + Semaphore& operator=(Semaphore&& other) noexcept { return cth::reconstruct_at(*this, std::move(other)); } + + static void debug_check(Semaphore const& semaphore); +}; +} + +namespace cvk { +struct Semaphore::State { + vk_not_null vkSemaphore; +}; +} + +namespace cvk { +inline void Semaphore::debug_check(Semaphore const& semaphore) { + CTH_CRITICAL(!semaphore.created(), "semaphore must be created") {} +} +} diff --git a/cvk/incl/cvk/render/sync/timeline_semaphore.hpp b/cvk/incl/cvk/render/sync/timeline_semaphore.hpp new file mode 100644 index 00000000..71ceedb8 --- /dev/null +++ b/cvk/incl/cvk/render/sync/timeline_semaphore.hpp @@ -0,0 +1,148 @@ +#pragma once +#include "semaphore.hpp" + +#include + +#include +#include + + +namespace cvk { +/** + * @brief timeline VkSemaphore with a host counter + * @details the counter is the last value that @ref next() or @ref signal() gave out, + * private base: a timeline semaphore must not pass as a binary @ref Semaphore, + * move members are custom: the counter is atomic + */ +class TimelineSemaphore : Semaphore { +public: + struct State; + + /** + * @brief base constructor + * @pre @ref Core::created() + */ + explicit TimelineSemaphore(Core const& core); + + /** + * @brief constructs and wraps + * @details calls: + * - @ref TimelineSemaphore(Core const&) + * - @ref wrap() + */ + TimelineSemaphore(Core const& core, State const& state); + + /** + * @brief constructs and creates + * @details calls: + * - @ref TimelineSemaphore(Core const&) + * - @ref create() + */ + TimelineSemaphore(Core const& core, create_t); + + /** + * @note calls @ref optDestroy() + */ + ~TimelineSemaphore() = default; + + /** + * @brief wraps the state + * @post @ref created() + * @note calls @ref optDestroy() + */ + void wrap(State const& state); + + /** + * @brief creates the semaphore + * @param initialValue of the semaphore and the counter + * @post @ref created() + * @note calls @ref optDestroy() + * @throws cvk::vk_result_exception result of vkCreateSemaphore() + */ + void create(uint64_t initialValue = 0); + + using Semaphore::destroy; + using Semaphore::optDestroy; + + /** + * @brief releases ownership, returns the state and resets + * @pre @ref created() + */ + State release(); + + /** + * @brief advances the counter and signals the semaphore from the host + * @return the signaled value + * @pre @ref created() + * @throws cvk::vk_result_exception result of vkSignalSemaphore() + */ + uint64_t signal(); + + /** + * @brief blocks until the semaphore reaches @ref value() or the timeout ends + * @param timeout in nanoseconds + * @return VK_SUCCESS or VK_TIMEOUT + * @pre @ref created() + * @throws cvk::vk_result_exception other result of vkWaitSemaphores() + */ + [[nodiscard]] VkResult wait(uint64_t timeout) const; + + /** + * @brief blocks until the semaphore reaches @ref value() + * @pre @ref created() + * @throws cvk::vk_result_exception result of vkWaitSemaphores() + */ + void wait() const; + + /** + * @brief advances the counter + * @return value for the next signal operation + * @details thread safe + */ + [[nodiscard]] uint64_t next() const { return ++_value; } + + /** + * @return the counter, value to wait for + */ + [[nodiscard]] uint64_t value() const { return _value; } + + /** + * @return current value of the semaphore + * @pre @ref created() + * @throws cvk::vk_result_exception result of vkGetSemaphoreCounterValue() + */ + [[nodiscard]] uint64_t gpuValue() const; + + using Semaphore::created; + using Semaphore::deviceTable; + using Semaphore::get; + +private: + mutable std::atomic _value = 0; + +public: + TimelineSemaphore(TimelineSemaphore const& other) = delete; + TimelineSemaphore(TimelineSemaphore&& other) noexcept : Semaphore{std::move(other)}, + _value{other._value.exchange(0)} {} + TimelineSemaphore& operator=(TimelineSemaphore const& other) = delete; + TimelineSemaphore& operator=(TimelineSemaphore&& other) noexcept { + return cth::reconstruct_at(*this, std::move(other)); + } + + static void debug_check(TimelineSemaphore const& semaphore); +}; +} + +namespace cvk { +struct TimelineSemaphore::State { + vk_not_null vkSemaphore; + /** + * @brief counter, see @ref value() + */ + uint64_t value; +}; +} + +namespace cvk { +inline void TimelineSemaphore::debug_check(TimelineSemaphore const& semaphore) { Semaphore::debug_check(semaphore); } +} diff --git a/cvk/src/render/sync/fence.cpp b/cvk/src/render/sync/fence.cpp new file mode 100644 index 00000000..187c282c --- /dev/null +++ b/cvk/src/render/sync/fence.cpp @@ -0,0 +1,118 @@ +#include "cvk/render/sync/fence.hpp" + +#include "cvk/base/core.hpp" +#include "cvk/utility/vk_exceptions.hpp" + +#include + + +namespace cvk { +Fence::Fence(Core const& core) : _deviceTable{core.deviceTable()} {} + +Fence::Fence(Core const& core, State const& state) : Fence{core} { wrap(state); } + +Fence::Fence(Core const& core, VkFenceCreateFlags flags) : Fence{core} { create(flags); } + +void Fence::wrap(State const& state) { + optDestroy(); + + _handle = state.vkFence.get(); +} + +void Fence::create(VkFenceCreateFlags flags) { + optDestroy(); + + auto const info = createInfo(flags); + + VkFence ptr = VK_NULL_HANDLE; + auto const result = _deviceTable->vkCreateFence(_deviceTable.device(), &info, nullptr, &ptr); + + CTH_STABLE_ERR(result != VK_SUCCESS, "failed to create fence") { + resetState(); + throw cvk::vk_result_exception{result, details->exception()}; + } + + _handle = ptr; +} + +void Fence::destroy() { + debug_check(*this); + + destroy(_deviceTable, _handle.get()); + + resetState(); +} + +Fence::State Fence::release() { + debug_check(*this); + + State const state{_handle.get()}; + resetState(); + return state; +} + +VkResult Fence::status() const { + debug_check(*this); + + auto const result = _deviceTable->vkGetFenceStatus(_deviceTable.device(), _handle.get()); + + CVK_RESULT_STABLE_THROW( + result != VK_SUCCESS && result != VK_NOT_READY, + result, + "failed to get fence status" + ) {} + + return result; +} + +void Fence::reset() const { + debug_check(*this); + + auto const vkFence = _handle.get(); + auto const result = _deviceTable->vkResetFences(_deviceTable.device(), 1, &vkFence); + + CVK_RESULT_STABLE_THROW(result != VK_SUCCESS, result, "failed to reset fence") {} +} + +VkResult Fence::wait(uint64_t timeout) const { + debug_check(*this); + + auto const vkFence = _handle.get(); + auto const result = _deviceTable->vkWaitForFences(_deviceTable.device(), 1, &vkFence, VK_TRUE, timeout); + + CVK_RESULT_STABLE_THROW(result != VK_SUCCESS && result != VK_TIMEOUT, result, "failed to wait for fence") {} + + return result; +} + +void Fence::wait() const { [[maybe_unused]] auto const result = wait(std::numeric_limits::max()); } + +VkResult Fence::waitReset(uint64_t timeout) const { + auto const result = wait(timeout); + if(result == VK_TIMEOUT) return result; + + reset(); + return result; +} + +void Fence::waitReset() const { + wait(); + reset(); +} + +void Fence::destroy(DeviceTable table, VkFence vkFence) { + CTH_WARN(vkFence == VK_NULL_HANDLE, "vkFence invalid (VK_NULL_HANDLE)") {} + + table->vkDestroyFence(table.device(), vkFence, nullptr); +} + +void Fence::resetState() { _handle = VK_NULL_HANDLE; } + +VkFenceCreateInfo Fence::createInfo(VkFenceCreateFlags flags) { + return VkFenceCreateInfo{ + .sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO, + .pNext = nullptr, + .flags = flags, + }; +} +} diff --git a/cvk/src/render/sync/semaphore.cpp b/cvk/src/render/sync/semaphore.cpp new file mode 100644 index 00000000..95bb00ab --- /dev/null +++ b/cvk/src/render/sync/semaphore.cpp @@ -0,0 +1,66 @@ +#include "cvk/render/sync/semaphore.hpp" + +#include "cvk/base/core.hpp" +#include "cvk/utility/vk_exceptions.hpp" + + +namespace cvk { +Semaphore::Semaphore(Core const& core) : _deviceTable{core.deviceTable()} {} + +Semaphore::Semaphore(Core const& core, State const& state) : Semaphore{core} { wrap(state); } + +Semaphore::Semaphore(Core const& core, create_t) : Semaphore{core} { create(); } + +void Semaphore::wrap(State const& state) { + optDestroy(); + + _handle = state.vkSemaphore.get(); +} + +void Semaphore::create() { + constexpr VkSemaphoreCreateInfo info{ + .sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO, + .pNext = nullptr, + .flags = 0, + }; + create(info); +} + +void Semaphore::destroy() { + debug_check(*this); + + destroy(_deviceTable, _handle.get()); + + reset(); +} + +Semaphore::State Semaphore::release() { + debug_check(*this); + + State const state{_handle.get()}; + reset(); + return state; +} + +void Semaphore::destroy(DeviceTable table, VkSemaphore vkSemaphore) { + CTH_WARN(vkSemaphore == VK_NULL_HANDLE, "vkSemaphore invalid (VK_NULL_HANDLE)") {} + + table->vkDestroySemaphore(table.device(), vkSemaphore, nullptr); +} + +void Semaphore::create(VkSemaphoreCreateInfo const& info) { + optDestroy(); + + VkSemaphore ptr = VK_NULL_HANDLE; + auto const result = _deviceTable->vkCreateSemaphore(_deviceTable.device(), &info, nullptr, &ptr); + + CTH_STABLE_ERR(result != VK_SUCCESS, "failed to create semaphore") { + reset(); + throw cvk::vk_result_exception{result, details->exception()}; + } + + _handle = ptr; +} + +void Semaphore::reset() { _handle = VK_NULL_HANDLE; } +} diff --git a/cvk/src/render/sync/timeline_semaphore.cpp b/cvk/src/render/sync/timeline_semaphore.cpp new file mode 100644 index 00000000..8c8db92b --- /dev/null +++ b/cvk/src/render/sync/timeline_semaphore.cpp @@ -0,0 +1,102 @@ +#include "cvk/render/sync/timeline_semaphore.hpp" + +#include "cvk/utility/vk_exceptions.hpp" + +#include + + +namespace cvk { +TimelineSemaphore::TimelineSemaphore(Core const& core) : Semaphore{core} {} + +TimelineSemaphore::TimelineSemaphore(Core const& core, State const& state) : TimelineSemaphore{core} { + wrap(state); +} + +TimelineSemaphore::TimelineSemaphore(Core const& core, create_t) : TimelineSemaphore{core} { create(); } + +void TimelineSemaphore::wrap(State const& state) { + Semaphore::wrap(Semaphore::State{state.vkSemaphore}); + _value = state.value; +} + +void TimelineSemaphore::create(uint64_t initialValue) { + VkSemaphoreTypeCreateInfo const typeInfo{ + .sType = VK_STRUCTURE_TYPE_SEMAPHORE_TYPE_CREATE_INFO, + .pNext = nullptr, + .semaphoreType = VK_SEMAPHORE_TYPE_TIMELINE, + .initialValue = initialValue, + }; + VkSemaphoreCreateInfo const info{ + .sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO, + .pNext = &typeInfo, + .flags = 0, + }; + Semaphore::create(info); + + _value = initialValue; +} + +TimelineSemaphore::State TimelineSemaphore::release() { + auto const [vkSemaphore] = Semaphore::release(); + return State{vkSemaphore, _value.exchange(0)}; +} + +uint64_t TimelineSemaphore::signal() { + debug_check(*this); + + auto const table = deviceTable(); + VkSemaphoreSignalInfo const info{ + .sType = VK_STRUCTURE_TYPE_SEMAPHORE_SIGNAL_INFO, + .pNext = nullptr, + .semaphore = get(), + .value = next(), + }; + auto const result = table->vkSignalSemaphore(table.device(), &info); + + CVK_RESULT_STABLE_THROW(result != VK_SUCCESS, result, "failed to signal semaphore") {} + + return info.value; +} + +VkResult TimelineSemaphore::wait(uint64_t timeout) const { + debug_check(*this); + + auto const table = deviceTable(); + auto const vkSemaphore = get(); + uint64_t const value = _value; + + VkSemaphoreWaitInfo const info{ + .sType = VK_STRUCTURE_TYPE_SEMAPHORE_WAIT_INFO, + .pNext = nullptr, + .flags = 0, + .semaphoreCount = 1, + .pSemaphores = &vkSemaphore, + .pValues = &value, + }; + auto const result = table->vkWaitSemaphores(table.device(), &info, timeout); + + CVK_RESULT_STABLE_THROW( + result != VK_SUCCESS && result != VK_TIMEOUT, + result, + "failed to wait for semaphore" + ) {} + + return result; +} + +void TimelineSemaphore::wait() const { + [[maybe_unused]] auto const result = wait(std::numeric_limits::max()); +} + +uint64_t TimelineSemaphore::gpuValue() const { + debug_check(*this); + + auto const table = deviceTable(); + uint64_t value = 0; + auto const result = table->vkGetSemaphoreCounterValue(table.device(), get(), &value); + + CVK_RESULT_STABLE_THROW(result != VK_SUCCESS, result, "failed to get semaphore counter value") {} + + return value; +} +} diff --git a/cvk/tests/src/context.cpp b/cvk/tests/src/context.cpp new file mode 100644 index 00000000..194b7d59 --- /dev/null +++ b/cvk/tests/src/context.cpp @@ -0,0 +1,17 @@ +#include "context.hpp" + +#include "validation.hpp" + + +namespace cvk::test { +Core::Config const Context::CONFIG{ + .queueProperties = {QueueFamilyProperties::GRAPHICS | QueueFamilyProperties::COMPUTE}, +}; + +Context::Context(Core::Config const& config) : instance{APP_NAME, NO_EXTENSIONS, counting_messenger()}, + core{instance, {}, config} {} + +Context::ValidationCheck::ValidationCheck() { reset_validation_errors(); } + +Context::ValidationCheck::~ValidationCheck() { EXPECT_EQ(validation_errors(), 0); } +} diff --git a/cvk/tests/src/context.hpp b/cvk/tests/src/context.hpp new file mode 100644 index 00000000..7c1d8f3c --- /dev/null +++ b/cvk/tests/src/context.hpp @@ -0,0 +1,34 @@ +#pragma once +#include "test.hpp" + +#include "cvk/base/core.hpp" +#include "cvk/base/instance.hpp" + + +namespace cvk::test { +/** + * @brief instance and core for tests, fails the test on validation errors + * @details members destroy in reverse order -> the check sees the errors of vkDestroyInstance() too, e.g. leaks + */ +struct Context { + /** + * @brief one queue, some devices have one queue only + */ + static Core::Config const CONFIG; + + explicit Context(Core::Config const& config = CONFIG); + + struct ValidationCheck { + ValidationCheck(); + ~ValidationCheck(); + } validationCheck; + + Instance instance; + Core core; + + Context(Context const& other) = delete; + Context(Context&& other) = delete; + Context& operator=(Context const& other) = delete; + Context& operator=(Context&& other) = delete; +}; +} diff --git a/cvk/tests/src/render/sync/fence_test.cpp b/cvk/tests/src/render/sync/fence_test.cpp new file mode 100644 index 00000000..19186573 --- /dev/null +++ b/cvk/tests/src/render/sync/fence_test.cpp @@ -0,0 +1,106 @@ +#include "context.hpp" +#include "test.hpp" + +#include "cvk/render/sync/fence.hpp" + + +namespace cvk { +namespace { + VkSubmitInfo2 const EMPTY_SUBMIT{ + .sType = VK_STRUCTURE_TYPE_SUBMIT_INFO_2, + .pNext = nullptr, + .flags = 0, + .waitSemaphoreInfoCount = 0, + .pWaitSemaphoreInfos = nullptr, + .commandBufferInfoCount = 0, + .pCommandBufferInfos = nullptr, + .signalSemaphoreInfoCount = 0, + .pSignalSemaphoreInfos = nullptr, + }; +} + +CVK_TEST(Fence, base_constructor_does_not_create) { + test::Context const context{}; + + Fence const fence{context.core}; + + EXPECT_FALSE(fence.created()); +} + +CVK_TEST(Fence, create_not_signaled) { + test::Context const context{}; + + Fence const fence{context.core, 0}; + + ASSERT_TRUE(fence.created()); + EXPECT_EQ(fence.status(), VK_NOT_READY); + EXPECT_FALSE(fence.signaled()); + EXPECT_EQ(fence.wait(0), VK_TIMEOUT); +} + +CVK_TEST(Fence, create_signaled_and_reset) { + test::Context const context{}; + + Fence const fence{context.core, VK_FENCE_CREATE_SIGNALED_BIT}; + + ASSERT_TRUE(fence.signaled()); + fence.wait(); + + fence.reset(); + EXPECT_FALSE(fence.signaled()); +} + +CVK_TEST(Fence, submit_signals) { + test::Context const context{}; + Fence const fence{context.core, 0}; + + context.core.queue(0).raw_submit(EMPTY_SUBMIT, fence.get()); + + fence.wait(); + EXPECT_TRUE(fence.signaled()); + + fence.waitReset(); + EXPECT_FALSE(fence.signaled()); +} + +CVK_TEST(Fence, wait_reset_timeout_keeps_the_state) { + test::Context const context{}; + Fence const fence{context.core, 0}; + + EXPECT_EQ(fence.waitReset(0), VK_TIMEOUT); + EXPECT_FALSE(fence.signaled()); + + context.core.queue(0).raw_submit(EMPTY_SUBMIT, fence.get()); + + EXPECT_EQ(fence.waitReset(UINT64_MAX), VK_SUCCESS); + EXPECT_FALSE(fence.signaled()); +} + +CVK_TEST(Fence, release_and_wrap) { + test::Context const context{}; + Fence fence{context.core, VK_FENCE_CREATE_SIGNALED_BIT}; + auto const vkFence = fence.get(); + + auto const state = fence.release(); + EXPECT_FALSE(fence.created()); + EXPECT_EQ(state.vkFence.get(), vkFence); + + Fence const wrapped{context.core, state}; + ASSERT_TRUE(wrapped.created()); + EXPECT_EQ(wrapped.get(), vkFence); + EXPECT_TRUE(wrapped.signaled()); +} + +CVK_TEST(Fence, move_assignment_destroys_the_old_fence) { + test::Context const context{}; + Fence fence{context.core, 0}; + Fence other{context.core, VK_FENCE_CREATE_SIGNALED_BIT}; + auto const vkFence = other.get(); + + fence = std::move(other); + + EXPECT_EQ(fence.get(), vkFence); + EXPECT_FALSE(other.created()); + EXPECT_TRUE(fence.signaled()); +} +} diff --git a/cvk/tests/src/render/sync/semaphore_test.cpp b/cvk/tests/src/render/sync/semaphore_test.cpp new file mode 100644 index 00000000..b36ab405 --- /dev/null +++ b/cvk/tests/src/render/sync/semaphore_test.cpp @@ -0,0 +1,101 @@ +#include "context.hpp" +#include "test.hpp" + +#include "cvk/render/sync/fence.hpp" +#include "cvk/render/sync/semaphore.hpp" + + +namespace cvk { +namespace { + VkSemaphoreSubmitInfo semaphore_info(Semaphore const& semaphore) { + return VkSemaphoreSubmitInfo{ + .sType = VK_STRUCTURE_TYPE_SEMAPHORE_SUBMIT_INFO, + .pNext = nullptr, + .semaphore = semaphore.get(), + .value = 0, + .stageMask = VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT, + .deviceIndex = 0, + }; + } +} + +CVK_TEST(Semaphore, base_constructor_does_not_create) { + test::Context const context{}; + + Semaphore const semaphore{context.core}; + + EXPECT_FALSE(semaphore.created()); +} + +CVK_TEST(Semaphore, create_and_destroy) { + test::Context const context{}; + Semaphore semaphore{context.core, create}; + + ASSERT_TRUE(semaphore.created()); + EXPECT_EQ(semaphore.deviceTable().device(), context.core.vkDevice()); + + semaphore.destroy(); + EXPECT_FALSE(semaphore.created()); +} + +CVK_TEST(Semaphore, submit_signals_and_waits) { + test::Context const context{}; + Semaphore const semaphore{context.core, create}; + Fence const fence{context.core, 0}; + auto const& queue = context.core.queue(0); + + auto const info = semaphore_info(semaphore); + + VkSubmitInfo2 const signalSubmit{ + .sType = VK_STRUCTURE_TYPE_SUBMIT_INFO_2, + .pNext = nullptr, + .flags = 0, + .waitSemaphoreInfoCount = 0, + .pWaitSemaphoreInfos = nullptr, + .commandBufferInfoCount = 0, + .pCommandBufferInfos = nullptr, + .signalSemaphoreInfoCount = 1, + .pSignalSemaphoreInfos = &info, + }; + VkSubmitInfo2 const waitSubmit{ + .sType = VK_STRUCTURE_TYPE_SUBMIT_INFO_2, + .pNext = nullptr, + .flags = 0, + .waitSemaphoreInfoCount = 1, + .pWaitSemaphoreInfos = &info, + .commandBufferInfoCount = 0, + .pCommandBufferInfos = nullptr, + .signalSemaphoreInfoCount = 0, + .pSignalSemaphoreInfos = nullptr, + }; + + queue.raw_submit(signalSubmit); + queue.raw_submit(waitSubmit, fence.get()); + + fence.wait(); +} + +CVK_TEST(Semaphore, release_and_wrap) { + test::Context const context{}; + Semaphore semaphore{context.core, create}; + auto const vkSemaphore = semaphore.get(); + + auto const state = semaphore.release(); + EXPECT_FALSE(semaphore.created()); + + Semaphore const wrapped{context.core, state}; + EXPECT_EQ(wrapped.get(), vkSemaphore); +} + +CVK_TEST(Semaphore, move_assignment_destroys_the_old_semaphore) { + test::Context const context{}; + Semaphore semaphore{context.core, create}; + Semaphore other{context.core, create}; + auto const vkSemaphore = other.get(); + + semaphore = std::move(other); + + EXPECT_EQ(semaphore.get(), vkSemaphore); + EXPECT_FALSE(other.created()); +} +} diff --git a/cvk/tests/src/render/sync/timeline_semaphore_test.cpp b/cvk/tests/src/render/sync/timeline_semaphore_test.cpp new file mode 100644 index 00000000..1316e01f --- /dev/null +++ b/cvk/tests/src/render/sync/timeline_semaphore_test.cpp @@ -0,0 +1,127 @@ +#include "context.hpp" +#include "test.hpp" + +#include "cvk/render/sync/timeline_semaphore.hpp" + +#include + + +namespace cvk { +CVK_TEST(TimelineSemaphore, base_constructor_does_not_create) { + test::Context const context{}; + + TimelineSemaphore const semaphore{context.core}; + + EXPECT_FALSE(semaphore.created()); +} + +CVK_TEST(TimelineSemaphore, create_sets_the_initial_value) { + test::Context const context{}; + TimelineSemaphore semaphore{context.core}; + + semaphore.create(5); + + ASSERT_TRUE(semaphore.created()); + EXPECT_EQ(semaphore.value(), 5); + EXPECT_EQ(semaphore.gpuValue(), 5); + EXPECT_EQ(semaphore.wait(0), VK_SUCCESS); +} + +CVK_TEST(TimelineSemaphore, signal_advances_the_counter) { + test::Context const context{}; + TimelineSemaphore semaphore{context.core, create}; + + EXPECT_EQ(semaphore.signal(), 1); + EXPECT_EQ(semaphore.value(), 1); + EXPECT_EQ(semaphore.gpuValue(), 1); + semaphore.wait(); +} + +CVK_TEST(TimelineSemaphore, wait_for_a_value_not_signaled_times_out) { + test::Context const context{}; + TimelineSemaphore const semaphore{context.core, create}; + + EXPECT_EQ(semaphore.next(), 1); + + EXPECT_EQ(semaphore.wait(0), VK_TIMEOUT); + EXPECT_EQ(semaphore.gpuValue(), 0); +} + +CVK_TEST(TimelineSemaphore, submit_signals_the_next_value) { + test::Context const context{}; + TimelineSemaphore const semaphore{context.core, create}; + + VkSemaphoreSubmitInfo const signalInfo{ + .sType = VK_STRUCTURE_TYPE_SEMAPHORE_SUBMIT_INFO, + .pNext = nullptr, + .semaphore = semaphore.get(), + .value = semaphore.next(), + .stageMask = VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT, + .deviceIndex = 0, + }; + VkSubmitInfo2 const submit{ + .sType = VK_STRUCTURE_TYPE_SUBMIT_INFO_2, + .pNext = nullptr, + .flags = 0, + .waitSemaphoreInfoCount = 0, + .pWaitSemaphoreInfos = nullptr, + .commandBufferInfoCount = 0, + .pCommandBufferInfos = nullptr, + .signalSemaphoreInfoCount = 1, + .pSignalSemaphoreInfos = &signalInfo, + }; + + context.core.queue(0).raw_submit(submit); + + semaphore.wait(); + EXPECT_EQ(semaphore.gpuValue(), signalInfo.value); +} + +CVK_TEST(TimelineSemaphore, release_and_wrap_keep_the_counter) { + test::Context const context{}; + TimelineSemaphore semaphore{context.core, create}; + [[maybe_unused]] auto const signaled = semaphore.signal(); + + auto const state = semaphore.release(); + EXPECT_FALSE(semaphore.created()); + EXPECT_EQ(semaphore.value(), 0); + EXPECT_EQ(state.value, 1); + + TimelineSemaphore const wrapped{context.core, state}; + EXPECT_EQ(wrapped.get(), state.vkSemaphore.get()); + EXPECT_EQ(wrapped.value(), 1); + EXPECT_EQ(wrapped.gpuValue(), 1); +} + +CVK_TEST(TimelineSemaphore, move_keeps_the_counter) { + test::Context const context{}; + TimelineSemaphore semaphore{context.core, create}; + semaphore.create(3); + + TimelineSemaphore const moved{std::move(semaphore)}; + + EXPECT_FALSE(semaphore.created()); + EXPECT_EQ(semaphore.value(), 0); + EXPECT_EQ(moved.value(), 3); + EXPECT_EQ(moved.gpuValue(), 3); +} + +CVK_TEST(TimelineSemaphore, move_assignment_destroys_the_old_semaphore) { + test::Context const context{}; + TimelineSemaphore semaphore{context.core, create}; + TimelineSemaphore other{context.core, create}; + other.create(7); + auto const vkSemaphore = other.get(); + + semaphore = std::move(other); + + EXPECT_EQ(semaphore.get(), vkSemaphore); + EXPECT_EQ(semaphore.value(), 7); + EXPECT_FALSE(other.created()); +} + +CVK_TEST(TimelineSemaphore, is_not_a_binary_semaphore) { + static_assert(!std::convertible_to); + static_assert(!std::convertible_to); +} +} From 06ab4a064adb15476214aaa2428531352a227066 Mon Sep 17 00:00:00 2001 From: lukas <89145039+codethinki@users.noreply.github.com> Date: Fri, 16 Oct 2026 15:03:21 +0200 Subject: [PATCH 015/135] style: declarations leave out parameter names that repeat the type VkDevice vkDevice -> VkDevice, Core const& core -> Core const&. Definitions keep the names. Also: the create_t constructors of Semaphore and TimelineSemaphore are explicit. --- .idea/codeStyles/Project.xml | 569 ++++++++++++++++++ AGENTS.md | 9 +- cvk/incl/cvk/base/core.hpp | 16 +- cvk/incl/cvk/base/device.hpp | 18 +- cvk/incl/cvk/base/instance.hpp | 12 +- cvk/incl/cvk/base/physical_device.hpp | 18 +- cvk/incl/cvk/base/queue/queue.hpp | 8 +- cvk/incl/cvk/debug/debug_messenger.hpp | 8 +- cvk/incl/cvk/render/sync/fence.hpp | 12 +- cvk/incl/cvk/render/sync/semaphore.hpp | 12 +- .../cvk/render/sync/timeline_semaphore.hpp | 8 +- cvk/incl/cvk/surface/surface.hpp | 26 +- cvk/tests/src/context.hpp | 2 +- cvk/tests/src/headless.hpp | 6 +- 14 files changed, 647 insertions(+), 77 deletions(-) create mode 100644 .idea/codeStyles/Project.xml diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml new file mode 100644 index 00000000..20849850 --- /dev/null +++ b/.idea/codeStyles/Project.xml @@ -0,0 +1,569 @@ + + + + \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md index 4a6932fa..d5f85123 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -78,6 +78,7 @@ ctest --test-dir out/build/gcc_release --output-on-failure | namespace | short lower case | `cvk`, `cvk::constants`, `cvk::dev` | - A raw Vulkan handle in a variable or parameter has the prefix `vk`: `vkInstance`, `vkSemaphore`. +- A declaration leaves out a parameter name that repeats the type: `void wrap(State const&)`, `destroy(DeviceTable table, VkSemaphore)`, not `VkSemaphore vkSemaphore`. The definition keeps the name. - Function parameters were `snake_case` in the legacy code. New code uses `camelCase`. Rename them when you port. - `cvk::dev` holds implementation details that are not part of the API. @@ -123,9 +124,9 @@ public: // constants - explicit Semaphore(Core const& core); // base constructor - Semaphore(Core const& core, State const& state); // constructs and wraps - explicit Semaphore(Core const& core, create_t); // constructs and creates + explicit Semaphore(Core const&); // base constructor + Semaphore(Core const&, State const&); // constructs and wraps + explicit Semaphore(Core const&, create_t); // constructs and creates ~Semaphore() { optDestroy(); } @@ -137,7 +138,7 @@ public: // operations - static void destroy(DeviceTable table, VkSemaphore vkSemaphore); + static void destroy(DeviceTable table, VkSemaphore); // accessors: created(), get(), ... diff --git a/cvk/incl/cvk/base/core.hpp b/cvk/incl/cvk/base/core.hpp index c8601593..8e5ad256 100644 --- a/cvk/incl/cvk/base/core.hpp +++ b/cvk/incl/cvk/base/core.hpp @@ -35,7 +35,7 @@ class Core { * @brief base constructor * @pre @ref Instance::created() */ - explicit Core(Instance const& instance); + explicit Core(Instance const&); /** * @brief constructs and creates @@ -43,7 +43,7 @@ class Core { * - @ref Core(Instance const&) * - @ref create() */ - Core(Instance const& instance, std::span surfaces, Config const& config); + Core(Instance const&, std::span surfaces, Config const&); /** * @brief constructs and wraps @@ -51,7 +51,7 @@ class Core { * - @ref Core(Instance const&) * - @ref wrap() */ - Core(Instance const& instance, State state); + Core(Instance const&, State); /** * @note calls @ref optDestroy() @@ -64,7 +64,7 @@ class Core { * @post @ref created() * @note calls @ref optDestroy() */ - void wrap(State state); + void wrap(State); /** * @brief picks a physical device for the first possible queue set, creates the device and the queues @@ -74,7 +74,7 @@ class Core { * @throws cvk::cvk_exception reason: no physical device meets the requirements * @throws cvk::vk_result_exception result of vkCreateDevice() */ - void create(std::span surfaces, Config const& config); + void create(std::span surfaces, Config const&); /** * @brief destroys and resets @@ -146,7 +146,7 @@ class Core { * @post sets @ref _physicalDevice and @ref _queueSetIndex * @throws cvk::cvk_exception reason: no physical device meets the requirements */ - [[nodiscard]] queue_mappings createPhysicalDevice(std::span surfaces, Config const& config); + [[nodiscard]] queue_mappings createPhysicalDevice(std::span surfaces, Config const&); [[nodiscard]] static queue_set_relations createQueueSetRelations( queue_set_t const& queueSet, @@ -158,7 +158,7 @@ class Core { */ [[nodiscard]] std::optional tryCreatePhysicalDevice( std::span surfaces, - Config const& config, + Config const&, queue_set_t const& queueSet ); @@ -178,7 +178,7 @@ class Core { Core& operator=(Core const& other) = delete; Core& operator=(Core&& other) noexcept; - static void debug_check(Core const& core); + static void debug_check(Core const&); }; } diff --git a/cvk/incl/cvk/base/device.hpp b/cvk/incl/cvk/base/device.hpp index b428cadc..9170b456 100644 --- a/cvk/incl/cvk/base/device.hpp +++ b/cvk/incl/cvk/base/device.hpp @@ -35,7 +35,7 @@ class Device { * @pre @ref Instance::created() * @pre @ref PhysicalDevice::created() */ - Device(Instance const& instance, PhysicalDevice const& physicalDevice); + Device(Instance const&, PhysicalDevice const&); /** * @brief constructs and wraps @@ -43,7 +43,7 @@ class Device { * - @ref Device(Instance const&, PhysicalDevice const&) * - @ref wrap() */ - Device(Instance const& instance, PhysicalDevice const& physicalDevice, State state); + Device(Instance const&, PhysicalDevice const&, State); /** * @brief constructs and creates @@ -51,7 +51,7 @@ class Device { * - @ref Device(Instance const&, PhysicalDevice const&) * - @ref create() */ - Device(Instance const& instance, PhysicalDevice const& physicalDevice, Config const& config); + Device(Instance const&, PhysicalDevice const&, Config const&); /** * @note calls @ref optDestroy() @@ -65,7 +65,7 @@ class Device { * @note calls @ref optDestroy() * @note does not retrieve queues */ - void wrap(State state); + void wrap(State); /** * @brief creates the device and retrieves the queues @@ -75,7 +75,7 @@ class Device { * @note calls @ref optDestroy() * @throws cvk::vk_result_exception result of vkCreateDevice() */ - void create(Config const& config); + void create(Config const&); /** * @brief destroys and resets @@ -102,9 +102,9 @@ class Device { void waitIdle() const; /** - * @param destroy vkDestroyDevice of the function table of @ref vkDevice + * @param destroy vkDestroyDevice of the function table of the device */ - static void destroy(VkDevice vkDevice, PFN_vkDestroyDevice destroy); + static void destroy(VkDevice, PFN_vkDestroyDevice destroy); [[nodiscard]] std::span queues() const { return _queues; } [[nodiscard]] std::span queues() { return _queues; } @@ -153,8 +153,8 @@ class Device { Device& operator=(Device const& other) = delete; Device& operator=(Device&& other) noexcept { return cth::reconstruct_at(*this, std::move(other)); } - static void debug_check(Device const& device); - static void debug_check_handle(vk_not_null vkDevice); + static void debug_check(Device const&); + static void debug_check_handle(vk_not_null); }; } diff --git a/cvk/incl/cvk/base/instance.hpp b/cvk/incl/cvk/base/instance.hpp index 8543437f..2f7c35db 100644 --- a/cvk/incl/cvk/base/instance.hpp +++ b/cvk/incl/cvk/base/instance.hpp @@ -62,7 +62,7 @@ class Instance { * - @ref Instance(std::string_view, std::span) * - @ref wrap() */ - Instance(std::string_view appName, std::span requiredExtensions, State state); + Instance(std::string_view appName, std::span requiredExtensions, State); /** * @note calls @ref optDestroy() @@ -75,7 +75,7 @@ class Instance { * @post volk uses the functions of this instance * @note calls @ref optDestroy() */ - void wrap(State state); + void wrap(State); /** * @brief creates the instance @@ -120,7 +120,7 @@ class Instance { [[nodiscard]] static std::vector getAvailableInstanceExtensions(); [[nodiscard]] VkApplicationInfo appInfo() const; - static void destroy(VkInstance vkInstance); + static void destroy(VkInstance); [[nodiscard]] bool created() const { return _handle != VK_NULL_HANDLE; } [[nodiscard]] VkInstance get() const { return _handle.get(); } @@ -140,7 +140,7 @@ class Instance { std::unique_ptr _debugMessenger = nullptr; move_ptr _handle = VK_NULL_HANDLE; - static void loadInstanceFunctions(vk_not_null vkInstance); + static void loadInstanceFunctions(vk_not_null); public: Instance(Instance const& other) = delete; @@ -148,8 +148,8 @@ class Instance { Instance(Instance&& other) noexcept = default; Instance& operator=(Instance&& other) noexcept { return cth::reconstruct_at(*this, std::move(other)); } - static void debug_check(Instance const& instance); - static void debug_check_handle(vk_not_null vkInstance); + static void debug_check(Instance const&); + static void debug_check_handle(vk_not_null); }; } diff --git a/cvk/incl/cvk/base/physical_device.hpp b/cvk/incl/cvk/base/physical_device.hpp index 9481e177..0c15eabe 100644 --- a/cvk/incl/cvk/base/physical_device.hpp +++ b/cvk/incl/cvk/base/physical_device.hpp @@ -31,7 +31,7 @@ class PhysicalDevice { * @pre @ref Instance::created() */ explicit PhysicalDevice( - Instance const& instance, + Instance const&, utils::PhysicalDeviceFeatures requiredFeatures, std::span requiredExtensions ); @@ -43,7 +43,7 @@ class PhysicalDevice { * - @ref create(std::span, vk_not_null) */ explicit PhysicalDevice( - Instance const& instance, + Instance const&, utils::PhysicalDeviceFeatures const& requiredFeatures, std::span requiredExtensions, std::span surfaces, @@ -57,10 +57,10 @@ class PhysicalDevice { * - @ref wrap() */ explicit PhysicalDevice( - Instance const& instance, + Instance const&, utils::PhysicalDeviceFeatures const& requiredFeatures, std::span requiredExtensions, - State const& state + State const& ); ~PhysicalDevice() = default; @@ -71,7 +71,7 @@ class PhysicalDevice { * @return std::nullopt -> not @ref suitable() */ [[nodiscard]] static std::optional Create( - Instance const& instance, + Instance const&, std::span surfaces, std::span queueProperties, std::span requiredExtensions, @@ -81,10 +81,10 @@ class PhysicalDevice { /** * @brief wraps the state - * @details empty or std::nullopt members of @ref state -> queries them + * @details empty or std::nullopt members of the state -> queries them * @post @ref created() */ - void wrap(State const& state); + void wrap(State const&); /** * @brief queries the device @@ -109,7 +109,7 @@ class PhysicalDevice { * - a PRESENT queue -> @ref constants::PRESENT_DEVICE_EXTENSIONS */ [[nodiscard]] static std::optional AutoPick( - Instance const& instance, + Instance const&, std::span surfaces, std::span queueProperties, std::span requiredExtensions = {}, @@ -164,7 +164,7 @@ class PhysicalDevice { /** * @throws cvk::vk_result_exception result of vkEnumeratePhysicalDevices() */ - [[nodiscard]] static std::vector enumerateVkDevices(vk_not_null vkInstance); + [[nodiscard]] static std::vector enumerateVkDevices(vk_not_null); /** * @throws cvk::vk_result_exception result of vkEnumerateDeviceExtensionProperties() diff --git a/cvk/incl/cvk/base/queue/queue.hpp b/cvk/incl/cvk/base/queue/queue.hpp index ae210160..97160bdb 100644 --- a/cvk/incl/cvk/base/queue/queue.hpp +++ b/cvk/incl/cvk/base/queue/queue.hpp @@ -26,7 +26,7 @@ class Queue { * @post @ref created() * @note calls @ref optDestroy() */ - void wrap(State state); + void wrap(State); /** * @brief resets, the device owns the vulkan queue @@ -94,9 +94,9 @@ class Queue { Queue& operator=(Queue const& other) = default; Queue& operator=(Queue&& other) = default; - static void debug_check(Queue const& queue); - static void debug_check_present(Queue const& queue); - static void debug_check_handle(VkQueue vkQueue); + static void debug_check(Queue const&); + static void debug_check_present(Queue const&); + static void debug_check_handle(VkQueue); }; } diff --git a/cvk/incl/cvk/debug/debug_messenger.hpp b/cvk/incl/cvk/debug/debug_messenger.hpp index dd9f6eea..3e6ffcf2 100644 --- a/cvk/incl/cvk/debug/debug_messenger.hpp +++ b/cvk/incl/cvk/debug/debug_messenger.hpp @@ -25,7 +25,7 @@ class DebugMessenger { /** * @brief base constructor */ - explicit DebugMessenger(Config config); + explicit DebugMessenger(Config); /** * @brief constructs and creates @@ -51,7 +51,7 @@ class DebugMessenger { * @note calls @ref optDestroy() * @throws cvk::vk_result_exception result of vkCreateDebugUtilsMessengerEXT() */ - void create(Instance const& instance); + void create(Instance const&); /** @@ -66,7 +66,7 @@ class DebugMessenger { */ void optDestroy() { if(created()) destroy(); } - static void destroy(vk_not_null vkInstance, VkDebugUtilsMessengerEXT vkMessenger); + static void destroy(vk_not_null, VkDebugUtilsMessengerEXT vkMessenger); /** * @brief releases ownership, returns state and resets @@ -115,7 +115,7 @@ class DebugMessenger { DebugMessenger& operator=(DebugMessenger const& other) = delete; DebugMessenger& operator=(DebugMessenger&& other) noexcept { return cth::reconstruct_at(*this, std::move(other)); } - static void debug_check(DebugMessenger const& debugMessenger); + static void debug_check(DebugMessenger const&); }; } // namespace cvk diff --git a/cvk/incl/cvk/render/sync/fence.hpp b/cvk/incl/cvk/render/sync/fence.hpp index f1187b43..7d61681d 100644 --- a/cvk/incl/cvk/render/sync/fence.hpp +++ b/cvk/incl/cvk/render/sync/fence.hpp @@ -24,7 +24,7 @@ class Fence { * @brief base constructor * @pre @ref Core::created() */ - explicit Fence(Core const& core); + explicit Fence(Core const&); /** * @brief constructs and wraps @@ -32,7 +32,7 @@ class Fence { * - @ref Fence(Core const&) * - @ref wrap() */ - Fence(Core const& core, State const& state); + Fence(Core const&, State const&); /** * @brief constructs and creates @@ -40,7 +40,7 @@ class Fence { * - @ref Fence(Core const&) * - @ref create() */ - Fence(Core const& core, VkFenceCreateFlags flags); + Fence(Core const&, VkFenceCreateFlags flags); /** * @note calls @ref optDestroy() @@ -52,7 +52,7 @@ class Fence { * @post @ref created() * @note calls @ref optDestroy() */ - void wrap(State const& state); + void wrap(State const&); /** * @brief creates the fence @@ -134,7 +134,7 @@ class Fence { */ void waitReset() const; - static void destroy(DeviceTable table, VkFence vkFence); + static void destroy(DeviceTable table, VkFence); [[nodiscard]] bool created() const { return _handle != VK_NULL_HANDLE; } [[nodiscard]] VkFence get() const { return _handle.get(); } @@ -154,7 +154,7 @@ class Fence { Fence& operator=(Fence const& other) = delete; Fence& operator=(Fence&& other) noexcept { return cth::reconstruct_at(*this, std::move(other)); } - static void debug_check(Fence const& fence); + static void debug_check(Fence const&); }; } diff --git a/cvk/incl/cvk/render/sync/semaphore.hpp b/cvk/incl/cvk/render/sync/semaphore.hpp index adf03631..3b6734ac 100644 --- a/cvk/incl/cvk/render/sync/semaphore.hpp +++ b/cvk/incl/cvk/render/sync/semaphore.hpp @@ -23,7 +23,7 @@ class Semaphore { * @brief base constructor * @pre @ref Core::created() */ - explicit Semaphore(Core const& core); + explicit Semaphore(Core const&); /** * @brief constructs and wraps @@ -31,7 +31,7 @@ class Semaphore { * - @ref Semaphore(Core const&) * - @ref wrap() */ - Semaphore(Core const& core, State const& state); + Semaphore(Core const&, State const&); /** * @brief constructs and creates @@ -39,7 +39,7 @@ class Semaphore { * - @ref Semaphore(Core const&) * - @ref create() */ - Semaphore(Core const& core, create_t); + explicit Semaphore(Core const&, create_t); /** * @note calls @ref optDestroy() @@ -51,7 +51,7 @@ class Semaphore { * @post @ref created() * @note calls @ref optDestroy() */ - void wrap(State const& state); + void wrap(State const&); /** * @brief creates the semaphore @@ -79,7 +79,7 @@ class Semaphore { */ State release(); - static void destroy(DeviceTable table, VkSemaphore vkSemaphore); + static void destroy(DeviceTable table, VkSemaphore); [[nodiscard]] bool created() const { return _handle != VK_NULL_HANDLE; } [[nodiscard]] VkSemaphore get() const { return _handle.get(); } @@ -106,7 +106,7 @@ class Semaphore { Semaphore& operator=(Semaphore const& other) = delete; Semaphore& operator=(Semaphore&& other) noexcept { return cth::reconstruct_at(*this, std::move(other)); } - static void debug_check(Semaphore const& semaphore); + static void debug_check(Semaphore const&); }; } diff --git a/cvk/incl/cvk/render/sync/timeline_semaphore.hpp b/cvk/incl/cvk/render/sync/timeline_semaphore.hpp index 71ceedb8..1247ee2f 100644 --- a/cvk/incl/cvk/render/sync/timeline_semaphore.hpp +++ b/cvk/incl/cvk/render/sync/timeline_semaphore.hpp @@ -22,7 +22,7 @@ class TimelineSemaphore : Semaphore { * @brief base constructor * @pre @ref Core::created() */ - explicit TimelineSemaphore(Core const& core); + explicit TimelineSemaphore(Core const&); /** * @brief constructs and wraps @@ -30,7 +30,7 @@ class TimelineSemaphore : Semaphore { * - @ref TimelineSemaphore(Core const&) * - @ref wrap() */ - TimelineSemaphore(Core const& core, State const& state); + TimelineSemaphore(Core const&, State const&); /** * @brief constructs and creates @@ -38,7 +38,7 @@ class TimelineSemaphore : Semaphore { * - @ref TimelineSemaphore(Core const&) * - @ref create() */ - TimelineSemaphore(Core const& core, create_t); + explicit TimelineSemaphore(Core const&, create_t); /** * @note calls @ref optDestroy() @@ -50,7 +50,7 @@ class TimelineSemaphore : Semaphore { * @post @ref created() * @note calls @ref optDestroy() */ - void wrap(State const& state); + void wrap(State const&); /** * @brief creates the semaphore diff --git a/cvk/incl/cvk/surface/surface.hpp b/cvk/incl/cvk/surface/surface.hpp index 14addf75..cb68442a 100644 --- a/cvk/incl/cvk/surface/surface.hpp +++ b/cvk/incl/cvk/surface/surface.hpp @@ -30,7 +30,7 @@ class Surface { * @brief base constructor * @pre @ref Instance::created() */ - explicit Surface(Instance const& instance, Config config = {}); + explicit Surface(Instance const&, Config = {}); /** * @brief constructs and wraps @@ -38,7 +38,7 @@ class Surface { * - @ref Surface(Instance const&, Config) * - @ref wrap() */ - Surface(Instance const& instance, Config config, State const& state); + Surface(Instance const&, Config, State const&); /** * @note calls @ref optDestroy() @@ -50,7 +50,7 @@ class Surface { * @post @ref created() * @note calls @ref optDestroy() */ - void wrap(State const& state); + void wrap(State const&); /** * @brief destroys and resets @@ -73,39 +73,39 @@ class Surface { * @return queue family @ref familyIndex can present to this surface * @throws cvk::vk_result_exception result of vkGetPhysicalDeviceSurfaceSupportKHR() */ - [[nodiscard]] bool supportsFamily(PhysicalDevice const& physicalDevice, uint32_t familyIndex) const; + [[nodiscard]] bool supportsFamily(PhysicalDevice const&, uint32_t familyIndex) const; /** * @return allowed and available present modes, in the order of @ref Config::allowedPresentModes * @throws cvk::vk_result_exception result of vkGetPhysicalDeviceSurfacePresentModesKHR() */ - [[nodiscard]] std::vector presentModes(PhysicalDevice const& physicalDevice) const; + [[nodiscard]] std::vector presentModes(PhysicalDevice const&) const; /** * @throws cvk::vk_result_exception result of vkGetPhysicalDeviceSurfaceCapabilitiesKHR() */ - [[nodiscard]] VkSurfaceCapabilitiesKHR capabilities(PhysicalDevice const& physicalDevice) const; + [[nodiscard]] VkSurfaceCapabilitiesKHR capabilities(PhysicalDevice const&) const; /** * @return allowed and available formats, in the order of @ref Config::allowedSurfaceFormats - * @attention no format -> the surface is not usable with @ref physicalDevice + * @attention no format -> the surface is not usable with the physical device * @throws cvk::vk_result_exception result of vkGetPhysicalDeviceSurfaceFormatsKHR() */ - [[nodiscard]] std::vector formats(PhysicalDevice const& physicalDevice) const; + [[nodiscard]] std::vector formats(PhysicalDevice const&) const; /** * @return the most preferred allowed and available format * @throws cth::except::default_exception reason: no allowed format available */ - [[nodiscard]] format_t format(PhysicalDevice const& physicalDevice) const; + [[nodiscard]] format_t format(PhysicalDevice const&) const; /** * @return the most preferred allowed and available present mode * @throws cth::except::default_exception reason: no allowed present mode available */ - [[nodiscard]] present_mode_t presentMode(PhysicalDevice const& physicalDevice) const; + [[nodiscard]] present_mode_t presentMode(PhysicalDevice const&) const; - static void destroy(vk_not_null vkInstance, VkSurfaceKHR vkSurface); + static void destroy(vk_not_null, VkSurfaceKHR); [[nodiscard]] bool created() const { return _handle != VK_NULL_HANDLE; } [[nodiscard]] VkSurfaceKHR get() const { return _handle.get(); } @@ -125,8 +125,8 @@ class Surface { Surface& operator=(Surface const& other) = delete; Surface& operator=(Surface&& other) noexcept { return cth::reconstruct_at(*this, std::move(other)); } - static void debug_check(Surface const& surface); - static void debug_check_handle(vk_not_null vkSurface); + static void debug_check(Surface const&); + static void debug_check_handle(vk_not_null); }; } diff --git a/cvk/tests/src/context.hpp b/cvk/tests/src/context.hpp index 7c1d8f3c..4d513d30 100644 --- a/cvk/tests/src/context.hpp +++ b/cvk/tests/src/context.hpp @@ -16,7 +16,7 @@ struct Context { */ static Core::Config const CONFIG; - explicit Context(Core::Config const& config = CONFIG); + explicit Context(Core::Config const& = CONFIG); struct ValidationCheck { ValidationCheck(); diff --git a/cvk/tests/src/headless.hpp b/cvk/tests/src/headless.hpp index 02f27393..f3cfb1fa 100644 --- a/cvk/tests/src/headless.hpp +++ b/cvk/tests/src/headless.hpp @@ -16,16 +16,16 @@ namespace cvk::test { /** * @brief creates a headless surface - * @pre @ref instance enables @ref headless_extensions() + * @pre the instance enables @ref headless_extensions() */ -[[nodiscard]] Surface headless_surface(Instance const& instance, Surface::Config config = {}); +[[nodiscard]] Surface headless_surface(Instance const&, Surface::Config = {}); /** * @brief @ref PhysicalDevice::AutoPick() for tests * @throws std::bad_optional_access no device meets the requirements */ [[nodiscard]] PhysicalDevice::CreateResult pick( - Instance const& instance, + Instance const&, std::span queueProperties, std::span surfaces = {} ); From 7c185a725ecdbe4402531024f2922c5afa89bbea Mon Sep 17 00:00:00 2001 From: lukas <89145039+codethinki@users.noreply.github.com> Date: Fri, 16 Oct 2026 15:10:10 +0200 Subject: [PATCH 016/135] cvk: port shader, descriptor set layout, pipeline layout and pipeline - all four on the cvk lifecycle (legacy was pinned, //TEMP modernize) - shader: SPIR-V as uint32_t words (alignment, size), load() checks the file, no runtime glslc (windows only), stageInfo() replaces ShaderSpecialization - descriptor set layout: bindings by number, sorted, gaps allowed (legacy used the number as index: holes and remove duplicated binding 0) - pipeline layout: set layouts by set number, push constant ranges, copies the handles (legacy kept dangling pointers and wrote out of bounds for gaps) - pipeline: dynamic rendering (VkPipelineRenderingCreateInfo), value config in pipeline_config.hpp, pointers set at create(), no derivative pipelines (legacy never set the layout for them), bind() waits for the cmd layer - tests use SPIR-V generated from cvk/tests/shaders --- .../pipeline/layout/descriptor_set_layout.hpp | 153 +++++++++++++++++ .../pipeline/layout/pipeline_layout.hpp | 132 +++++++++++++++ cvk/incl/cvk/render/pipeline/pipeline.hpp | 126 ++++++++++++++ .../cvk/render/pipeline/pipeline_config.hpp | 143 ++++++++++++++++ .../cvk/render/pipeline/shader/shader.hpp | 157 ++++++++++++++++++ .../pipeline/layout/descriptor_set_layout.cpp | 113 +++++++++++++ .../pipeline/layout/pipeline_layout.cpp | 89 ++++++++++ cvk/src/render/pipeline/pipeline.cpp | 146 ++++++++++++++++ cvk/src/render/pipeline/pipeline_config.cpp | 36 ++++ cvk/src/render/pipeline/shader/shader.cpp | 118 +++++++++++++ cvk/tests/shaders/triangle.frag | 9 + cvk/tests/shaders/triangle.vert | 9 + .../layout/descriptor_set_layout_test.cpp | 89 ++++++++++ .../pipeline/layout/pipeline_layout_test.cpp | 88 ++++++++++ .../src/render/pipeline/pipeline_test.cpp | 150 +++++++++++++++++ .../render/pipeline/shader/shader_test.cpp | 103 ++++++++++++ cvk/tests/src/shaders.hpp | 62 +++++++ 17 files changed, 1723 insertions(+) create mode 100644 cvk/incl/cvk/render/pipeline/layout/descriptor_set_layout.hpp create mode 100644 cvk/incl/cvk/render/pipeline/layout/pipeline_layout.hpp create mode 100644 cvk/incl/cvk/render/pipeline/pipeline.hpp create mode 100644 cvk/incl/cvk/render/pipeline/pipeline_config.hpp create mode 100644 cvk/incl/cvk/render/pipeline/shader/shader.hpp create mode 100644 cvk/src/render/pipeline/layout/descriptor_set_layout.cpp create mode 100644 cvk/src/render/pipeline/layout/pipeline_layout.cpp create mode 100644 cvk/src/render/pipeline/pipeline.cpp create mode 100644 cvk/src/render/pipeline/pipeline_config.cpp create mode 100644 cvk/src/render/pipeline/shader/shader.cpp create mode 100644 cvk/tests/shaders/triangle.frag create mode 100644 cvk/tests/shaders/triangle.vert create mode 100644 cvk/tests/src/render/pipeline/layout/descriptor_set_layout_test.cpp create mode 100644 cvk/tests/src/render/pipeline/layout/pipeline_layout_test.cpp create mode 100644 cvk/tests/src/render/pipeline/pipeline_test.cpp create mode 100644 cvk/tests/src/render/pipeline/shader/shader_test.cpp create mode 100644 cvk/tests/src/shaders.hpp diff --git a/cvk/incl/cvk/render/pipeline/layout/descriptor_set_layout.hpp b/cvk/incl/cvk/render/pipeline/layout/descriptor_set_layout.hpp new file mode 100644 index 00000000..6a65fade --- /dev/null +++ b/cvk/incl/cvk/render/pipeline/layout/descriptor_set_layout.hpp @@ -0,0 +1,153 @@ +#pragma once +#include "cvk/base/device_table.hpp" +#include "cvk/utility/constants.hpp" +#include "cvk/utility/types.hpp" + +#include + +#include +#include + +#include +#include +#include + + +namespace cvk { +class Core; + +/** + * @brief VkDescriptorSetLayout wrapper + */ +class DescriptorSetLayout { +public: + struct Config; + struct State; + + /** + * @brief base constructor + * @pre @ref Core::created() + */ + DescriptorSetLayout(Core const&, Config); + + /** + * @brief constructs and wraps + * @details calls: + * - @ref DescriptorSetLayout(Core const&, Config) + * - @ref wrap() + */ + DescriptorSetLayout(Core const&, Config, State const&); + + /** + * @brief constructs and creates + * @details calls: + * - @ref DescriptorSetLayout(Core const&, Config) + * - @ref create() + */ + explicit DescriptorSetLayout(Core const&, Config, create_t); + + /** + * @note calls @ref optDestroy() + */ + ~DescriptorSetLayout() { optDestroy(); } + + /** + * @brief wraps the state + * @pre the layout was created with the config + * @post @ref created() + * @note calls @ref optDestroy() + */ + void wrap(State const&); + + /** + * @brief creates the layout + * @post @ref created() + * @note calls @ref optDestroy() + * @throws cvk::vk_result_exception result of vkCreateDescriptorSetLayout() + */ + void create(); + + /** + * @brief destroys and resets + * @pre @ref created() + */ + void destroy(); + + /** + * @brief if @ref created() -> calls @ref destroy() + */ + void optDestroy() { if(created()) destroy(); } + + /** + * @brief releases ownership, returns the state and resets + * @pre @ref created() + */ + State release(); + + static void destroy(DeviceTable table, VkDescriptorSetLayout); + + [[nodiscard]] bool created() const { return _handle != VK_NULL_HANDLE; } + [[nodiscard]] VkDescriptorSetLayout get() const { return _handle.get(); } + [[nodiscard]] DeviceTable deviceTable() const { return _deviceTable; } + + /** + * @return bindings, sorted by binding number + */ + [[nodiscard]] std::span bindings() const; + + /** + * @return nullptr -> no binding with that number + */ + [[nodiscard]] VkDescriptorSetLayoutBinding const* binding(uint32_t binding) const; + +private: + void reset(); + + DeviceTable _deviceTable; + std::vector _bindings; + move_ptr _handle = VK_NULL_HANDLE; + +public: + DescriptorSetLayout(DescriptorSetLayout const& other) = delete; + DescriptorSetLayout(DescriptorSetLayout&& other) noexcept = default; + DescriptorSetLayout& operator=(DescriptorSetLayout const& other) = delete; + DescriptorSetLayout& operator=(DescriptorSetLayout&& other) noexcept { + return cth::reconstruct_at(*this, std::move(other)); + } + + static void debug_check(DescriptorSetLayout const&); +}; +} + +namespace cvk { +struct DescriptorSetLayout::Config { + /** + * @brief adds a binding, the numbers may have gaps + * @param binding number of the binding + * @param count of descriptors, 0 -> the binding is reserved without descriptors + * @pre no binding with the same number + */ + Config& add(uint32_t binding, VkDescriptorType type, VkShaderStageFlags stages, uint32_t count = 1); + + /** + * @brief removes a binding + * @pre a binding with the number exists + */ + Config& remove(uint32_t binding); + + /** + * @brief bindings sorted by binding number + */ + std::vector bindings{}; +}; + +struct DescriptorSetLayout::State { + vk_not_null vkLayout; +}; +} + +namespace cvk { +inline void DescriptorSetLayout::debug_check(DescriptorSetLayout const& layout) { + CTH_CRITICAL(!layout.created(), "descriptor set layout must be created") {} +} +} diff --git a/cvk/incl/cvk/render/pipeline/layout/pipeline_layout.hpp b/cvk/incl/cvk/render/pipeline/layout/pipeline_layout.hpp new file mode 100644 index 00000000..803d7075 --- /dev/null +++ b/cvk/incl/cvk/render/pipeline/layout/pipeline_layout.hpp @@ -0,0 +1,132 @@ +#pragma once +#include "cvk/base/device_table.hpp" +#include "cvk/utility/constants.hpp" +#include "cvk/utility/types.hpp" + +#include + +#include +#include + +#include + + +namespace cvk { +class Core; +class DescriptorSetLayout; + +/** + * @brief VkPipelineLayout wrapper + */ +class PipelineLayout { +public: + struct Config; + struct State; + + /** + * @brief base constructor, copies the handles of the set layouts + * @pre @ref Core::created() + * @pre the set layouts are created + * @throws cth::except::default_exception reason: more set layouts than maxBoundDescriptorSets + */ + PipelineLayout(Core const&, Config const&); + + /** + * @brief constructs and wraps + * @details calls: + * - @ref PipelineLayout(Core const&, Config const&) + * - @ref wrap() + */ + PipelineLayout(Core const&, Config const&, State const&); + + /** + * @brief constructs and creates + * @details calls: + * - @ref PipelineLayout(Core const&, Config const&) + * - @ref create() + */ + explicit PipelineLayout(Core const&, Config const&, create_t); + + /** + * @note calls @ref optDestroy() + */ + ~PipelineLayout() { optDestroy(); } + + /** + * @brief wraps the state + * @pre the layout was created with the config + * @post @ref created() + * @note calls @ref optDestroy() + */ + void wrap(State const&); + + /** + * @brief creates the layout + * @pre the set layouts of the config are not destroyed + * @post @ref created() + * @note calls @ref optDestroy() + * @throws cvk::vk_result_exception result of vkCreatePipelineLayout() + */ + void create(); + + /** + * @brief destroys and resets + * @pre @ref created() + */ + void destroy(); + + /** + * @brief if @ref created() -> calls @ref destroy() + */ + void optDestroy() { if(created()) destroy(); } + + /** + * @brief releases ownership, returns the state and resets + * @pre @ref created() + */ + State release(); + + static void destroy(DeviceTable table, VkPipelineLayout); + + [[nodiscard]] bool created() const { return _handle != VK_NULL_HANDLE; } + [[nodiscard]] VkPipelineLayout get() const { return _handle.get(); } + [[nodiscard]] DeviceTable deviceTable() const { return _deviceTable; } + +private: + void reset(); + + DeviceTable _deviceTable; + std::vector _vkSetLayouts; + std::vector _pushConstantRanges; + move_ptr _handle = VK_NULL_HANDLE; + +public: + PipelineLayout(PipelineLayout const& other) = delete; + PipelineLayout(PipelineLayout&& other) noexcept = default; + PipelineLayout& operator=(PipelineLayout const& other) = delete; + PipelineLayout& operator=(PipelineLayout&& other) noexcept { return cth::reconstruct_at(*this, std::move(other)); } + + static void debug_check(PipelineLayout const&); +}; +} + +namespace cvk { +struct PipelineLayout::Config { + /** + * @brief set layout per set number, index == set number + */ + std::vector> setLayouts{}; + + std::vector pushConstantRanges{}; +}; + +struct PipelineLayout::State { + vk_not_null vkLayout; +}; +} + +namespace cvk { +inline void PipelineLayout::debug_check(PipelineLayout const& layout) { + CTH_CRITICAL(!layout.created(), "pipeline layout must be created") {} +} +} diff --git a/cvk/incl/cvk/render/pipeline/pipeline.hpp b/cvk/incl/cvk/render/pipeline/pipeline.hpp new file mode 100644 index 00000000..efcb7348 --- /dev/null +++ b/cvk/incl/cvk/render/pipeline/pipeline.hpp @@ -0,0 +1,126 @@ +#pragma once +#include "pipeline_config.hpp" + +#include "cvk/base/device_table.hpp" +#include "cvk/utility/constants.hpp" +#include "cvk/utility/types.hpp" + +#include + +#include +#include + + + +namespace cvk { +class Core; +class PipelineLayout; + +/** + * @brief graphics VkPipeline wrapper for dynamic rendering + */ +class Pipeline { +public: + using Config = PipelineConfig; + struct State; + + /** + * @brief base constructor, copies the handle of the layout + * @pre @ref Core::created() + * @pre @ref PipelineLayout::created() + */ + Pipeline(Core const&, PipelineLayout const&, Config); + + /** + * @brief constructs and wraps + * @details calls: + * - @ref Pipeline(Core const&, PipelineLayout const&, Config) + * - @ref wrap() + */ + Pipeline(Core const&, PipelineLayout const&, Config, State const&); + + /** + * @brief constructs and creates + * @details calls: + * - @ref Pipeline(Core const&, PipelineLayout const&, Config) + * - @ref create() + */ + explicit Pipeline(Core const&, PipelineLayout const&, Config, create_t); + + /** + * @note calls @ref optDestroy() + */ + ~Pipeline() { optDestroy(); } + + /** + * @brief wraps the state + * @pre the pipeline was created with the config and the layout + * @post @ref created() + * @note calls @ref optDestroy() + */ + void wrap(State const&); + + /** + * @brief creates the pipeline + * @pre the layout and the shader modules of the config are not destroyed + * @post @ref created() + * @note calls @ref optDestroy() + * @throws cvk::vk_result_exception result of vkCreateGraphicsPipelines() + */ + void create(); + + /** + * @brief destroys and resets + * @pre @ref created() + * @pre the gpu does not use the pipeline + */ + void destroy(); + + /** + * @brief if @ref created() -> calls @ref destroy() + */ + void optDestroy() { if(created()) destroy(); } + + /** + * @brief releases ownership, returns the state and resets + * @pre @ref created() + */ + State release(); + + static void destroy(DeviceTable table, VkPipeline); + + [[nodiscard]] bool created() const { return _handle != VK_NULL_HANDLE; } + [[nodiscard]] VkPipeline get() const { return _handle.get(); } + [[nodiscard]] VkPipelineLayout layout() const { return _vkLayout.get(); } + [[nodiscard]] Config const& config() const { return _config; } + [[nodiscard]] DeviceTable deviceTable() const { return _deviceTable; } + +private: + void reset(); + + DeviceTable _deviceTable; + vk_not_null _vkLayout; + Config _config; + move_ptr _handle = VK_NULL_HANDLE; + +public: + Pipeline(Pipeline const& other) = delete; + Pipeline(Pipeline&& other) noexcept = default; + Pipeline& operator=(Pipeline const& other) = delete; + Pipeline& operator=(Pipeline&& other) noexcept { return cth::reconstruct_at(*this, std::move(other)); } + + static void debug_check(Pipeline const&); +}; +} + +namespace cvk { +struct Pipeline::State { + vk_not_null vkPipeline; +}; +} + +namespace cvk { +inline void Pipeline::debug_check(Pipeline const& pipeline) { + CTH_CRITICAL(!pipeline.created(), "pipeline must be created") {} +} +} diff --git a/cvk/incl/cvk/render/pipeline/pipeline_config.hpp b/cvk/incl/cvk/render/pipeline/pipeline_config.hpp new file mode 100644 index 00000000..aa6430ff --- /dev/null +++ b/cvk/incl/cvk/render/pipeline/pipeline_config.hpp @@ -0,0 +1,143 @@ +#pragma once +#include + +#include +#include + + +namespace cvk { +class Shader; + +/** + * @brief graphics pipeline state, the defaults are the legacy defaults + * @details @ref Pipeline::create() sets the pointer and count members of the Vulkan structs from the vectors + */ +struct PipelineConfig { + /** + * @brief alpha blending: src alpha, one minus src alpha + */ + static constexpr VkPipelineColorBlendAttachmentState ALPHA_BLENDING{ + .blendEnable = VK_TRUE, + .srcColorBlendFactor = VK_BLEND_FACTOR_SRC_ALPHA, + .dstColorBlendFactor = VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA, + .colorBlendOp = VK_BLEND_OP_ADD, + .srcAlphaBlendFactor = VK_BLEND_FACTOR_ONE, + .dstAlphaBlendFactor = VK_BLEND_FACTOR_ZERO, + .alphaBlendOp = VK_BLEND_OP_ADD, + .colorWriteMask = VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT | VK_COLOR_COMPONENT_B_BIT + | VK_COLOR_COMPONENT_A_BIT, + }; + + /** + * @brief config with one @ref ALPHA_BLENDING attachment per color format + * @param depthFormat VK_FORMAT_UNDEFINED -> no depth attachment + */ + [[nodiscard]] static PipelineConfig Default( + std::span colorFormats, + VkFormat depthFormat = VK_FORMAT_UNDEFINED + ); + + /** + * @brief adds the stage of the shader + * @param specialization nullptr -> no specialization constants + * @attention @ref specialization must outlive @ref Pipeline::create() + * @pre @ref Shader::created() + * @pre no stage of the same type + */ + PipelineConfig& addStage(Shader const&, VkSpecializationInfo const* specialization = nullptr); + + /** + * @pre a stage of the type exists + */ + PipelineConfig& removeStage(VkShaderStageFlagBits stage); + + std::vector stages{}; + + std::vector vertexBindings{}; + std::vector vertexAttributes{}; + + VkPipelineInputAssemblyStateCreateInfo inputAssembly{ + .sType = VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO, + .pNext = nullptr, + .flags = 0, + .topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST, + .primitiveRestartEnable = VK_FALSE, + }; + + VkPipelineRasterizationStateCreateInfo rasterization{ + .sType = VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO, + .pNext = nullptr, + .flags = 0, + .depthClampEnable = VK_FALSE, + .rasterizerDiscardEnable = VK_FALSE, + .polygonMode = VK_POLYGON_MODE_FILL, + .cullMode = VK_CULL_MODE_BACK_BIT, + .frontFace = VK_FRONT_FACE_CLOCKWISE, + .depthBiasEnable = VK_FALSE, + .depthBiasConstantFactor = 0.f, + .depthBiasClamp = 0.f, + .depthBiasSlopeFactor = 0.f, + .lineWidth = 1.f, + }; + + VkPipelineMultisampleStateCreateInfo multisample{ + .sType = VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO, + .pNext = nullptr, + .flags = 0, + .rasterizationSamples = VK_SAMPLE_COUNT_1_BIT, + .sampleShadingEnable = VK_FALSE, + .minSampleShading = 0.f, + .pSampleMask = nullptr, + .alphaToCoverageEnable = VK_FALSE, + .alphaToOneEnable = VK_FALSE, + }; + + VkPipelineDepthStencilStateCreateInfo depthStencil{ + .sType = VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO, + .pNext = nullptr, + .flags = 0, + .depthTestEnable = VK_TRUE, + .depthWriteEnable = VK_TRUE, + .depthCompareOp = VK_COMPARE_OP_LESS, + .depthBoundsTestEnable = VK_FALSE, + .stencilTestEnable = VK_FALSE, + .front = {}, + .back = {}, + .minDepthBounds = 0.f, + .maxDepthBounds = 1.f, + }; + + /** + * @brief color blend state, attachmentCount and pAttachments come from @ref colorBlendAttachments + */ + VkPipelineColorBlendStateCreateInfo colorBlend{ + .sType = VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO, + .pNext = nullptr, + .flags = 0, + .logicOpEnable = VK_FALSE, + .logicOp = VK_LOGIC_OP_COPY, + .attachmentCount = 0, + .pAttachments = nullptr, + .blendConstants = {0.f, 0.f, 0.f, 0.f}, + }; + + /** + * @brief blend state per color attachment + * @pre size == @ref colorFormats size + */ + std::vector colorBlendAttachments{}; + + uint32_t viewportCount = 1; + uint32_t scissorCount = 1; + + std::vector dynamicStates{VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR}; + + /** + * @brief formats of the color attachments of the rendering + */ + std::vector colorFormats{}; + VkFormat depthFormat = VK_FORMAT_UNDEFINED; + VkFormat stencilFormat = VK_FORMAT_UNDEFINED; + uint32_t viewMask = 0; +}; +} diff --git a/cvk/incl/cvk/render/pipeline/shader/shader.hpp b/cvk/incl/cvk/render/pipeline/shader/shader.hpp new file mode 100644 index 00000000..e9543aac --- /dev/null +++ b/cvk/incl/cvk/render/pipeline/shader/shader.hpp @@ -0,0 +1,157 @@ +#pragma once +#include "cvk/base/device_table.hpp" +#include "cvk/utility/types.hpp" + +#include + +#include +#include + +#include +#include +#include +#include + + +namespace cvk { +class Core; + +/** + * @brief VkShaderModule wrapper + * @details a pipeline does not use the module after its creation + */ +class Shader { +public: + struct State; + using path_t = std::filesystem::path; + + /** + * @brief base constructor + * @param stage of the shader + * @pre @ref Core::created() + */ + Shader(Core const&, VkShaderStageFlagBits stage); + + /** + * @brief constructs and wraps + * @details calls: + * - @ref Shader(Core const&, VkShaderStageFlagBits) + * - @ref wrap() + */ + Shader(Core const&, VkShaderStageFlagBits stage, State const&); + + /** + * @brief constructs and creates + * @details calls: + * - @ref Shader(Core const&, VkShaderStageFlagBits) + * - @ref create(std::span) + */ + Shader(Core const&, VkShaderStageFlagBits stage, std::span spv); + + /** + * @brief constructs, loads and creates + * @details calls: + * - @ref Shader(Core const&, VkShaderStageFlagBits) + * - @ref create(path_t const&) + */ + Shader(Core const&, VkShaderStageFlagBits stage, path_t const& spvPath); + + /** + * @note calls @ref optDestroy() + */ + ~Shader() { optDestroy(); } + + /** + * @brief wraps the state + * @post @ref created() + * @note calls @ref optDestroy() + */ + void wrap(State const&); + + /** + * @brief creates the module + * @param spv SPIR-V code + * @post @ref created() + * @note calls @ref optDestroy() + * @throws cvk::vk_result_exception result of vkCreateShaderModule() + */ + void create(std::span spv); + + /** + * @brief loads the SPIR-V file and creates the module + * @details calls: + * - @ref load() + * - @ref create(std::span) + */ + void create(path_t const& spvPath); + + /** + * @brief destroys and resets + * @pre @ref created() + */ + void destroy(); + + /** + * @brief if @ref created() -> calls @ref destroy() + */ + void optDestroy() { if(created()) destroy(); } + + /** + * @brief releases ownership, returns the state and resets + * @pre @ref created() + */ + State release(); + + /** + * @return stage info for a pipeline + * @param specialization nullptr -> no specialization constants + * @param entryPoint name of the entry function + * @pre @ref created() + * @attention @ref specialization and @ref entryPoint must outlive the pipeline creation + */ + [[nodiscard]] VkPipelineShaderStageCreateInfo stageInfo( + VkSpecializationInfo const* specialization = nullptr, + char const* entryPoint = "main" + ) const; + + /** + * @brief reads a SPIR-V file + * @throws cvk::cvk_exception reason: file missing, size not a multiple of 4 or 0 + */ + [[nodiscard]] static std::vector load(path_t const& spvPath); + + static void destroy(DeviceTable table, VkShaderModule); + + [[nodiscard]] bool created() const { return _handle != VK_NULL_HANDLE; } + [[nodiscard]] VkShaderModule get() const { return _handle.get(); } + [[nodiscard]] VkShaderStageFlagBits stage() const { return _stage; } + [[nodiscard]] DeviceTable deviceTable() const { return _deviceTable; } + +private: + void reset(); + + DeviceTable _deviceTable; + VkShaderStageFlagBits _stage; + move_ptr _handle = VK_NULL_HANDLE; + +public: + Shader(Shader const& other) = delete; + Shader(Shader&& other) noexcept = default; + Shader& operator=(Shader const& other) = delete; + Shader& operator=(Shader&& other) noexcept { return cth::reconstruct_at(*this, std::move(other)); } + + static void debug_check(Shader const&); +}; +} + +namespace cvk { +struct Shader::State { + vk_not_null vkShader; +}; +} + +namespace cvk { +inline void Shader::debug_check(Shader const& shader) { + CTH_CRITICAL(!shader.created(), "shader must be created") {} +} +} diff --git a/cvk/src/render/pipeline/layout/descriptor_set_layout.cpp b/cvk/src/render/pipeline/layout/descriptor_set_layout.cpp new file mode 100644 index 00000000..eaa77a90 --- /dev/null +++ b/cvk/src/render/pipeline/layout/descriptor_set_layout.cpp @@ -0,0 +1,113 @@ +#include "cvk/render/pipeline/layout/descriptor_set_layout.hpp" + +#include "cvk/base/core.hpp" +#include "cvk/utility/vk_exceptions.hpp" + +#include + + +namespace cvk { +DescriptorSetLayout::DescriptorSetLayout(Core const& core, Config config) : _deviceTable{core.deviceTable()}, + _bindings{std::move(config.bindings)} {} + +DescriptorSetLayout::DescriptorSetLayout(Core const& core, Config config, State const& state) : DescriptorSetLayout{ + core, + std::move(config) +} { wrap(state); } + +DescriptorSetLayout::DescriptorSetLayout(Core const& core, Config config, create_t) : DescriptorSetLayout{ + core, + std::move(config) +} { create(); } + +void DescriptorSetLayout::wrap(State const& state) { + optDestroy(); + + _handle = state.vkLayout.get(); +} + +void DescriptorSetLayout::create() { + optDestroy(); + + VkDescriptorSetLayoutCreateInfo const info{ + .sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO, + .pNext = nullptr, + .flags = 0, + .bindingCount = static_cast(_bindings.size()), + .pBindings = _bindings.data(), + }; + + VkDescriptorSetLayout ptr = VK_NULL_HANDLE; + auto const result = _deviceTable->vkCreateDescriptorSetLayout(_deviceTable.device(), &info, nullptr, &ptr); + + CTH_STABLE_ERR(result != VK_SUCCESS, "failed to create descriptor set layout") { + reset(); + throw cvk::vk_result_exception{result, details->exception()}; + } + + _handle = ptr; +} + +void DescriptorSetLayout::destroy() { + debug_check(*this); + + destroy(_deviceTable, _handle.get()); + + reset(); +} + +DescriptorSetLayout::State DescriptorSetLayout::release() { + debug_check(*this); + + State const state{_handle.get()}; + reset(); + return state; +} + +void DescriptorSetLayout::destroy(DeviceTable table, VkDescriptorSetLayout vkLayout) { + CTH_WARN(vkLayout == VK_NULL_HANDLE, "vkLayout invalid (VK_NULL_HANDLE)") {} + + table->vkDestroyDescriptorSetLayout(table.device(), vkLayout, nullptr); +} + +std::span DescriptorSetLayout::bindings() const { return _bindings; } + +VkDescriptorSetLayoutBinding const* DescriptorSetLayout::binding(uint32_t binding) const { + auto const it = std::ranges::lower_bound(_bindings, binding, {}, &VkDescriptorSetLayoutBinding::binding); + return it != _bindings.end() && it->binding == binding ? std::to_address(it) : nullptr; +} + +void DescriptorSetLayout::reset() { _handle = VK_NULL_HANDLE; } +} + +namespace cvk { +DescriptorSetLayout::Config& DescriptorSetLayout::Config::add( + uint32_t binding, + VkDescriptorType type, + VkShaderStageFlags stages, + uint32_t count +) { + auto const it = std::ranges::lower_bound(bindings, binding, {}, &VkDescriptorSetLayoutBinding::binding); + CTH_CRITICAL(it != bindings.end() && it->binding == binding, "binding {} already exists", binding) {} + + bindings.insert( + it, + VkDescriptorSetLayoutBinding{ + .binding = binding, + .descriptorType = type, + .descriptorCount = count, + .stageFlags = stages, + .pImmutableSamplers = nullptr, + } + ); + return *this; +} + +DescriptorSetLayout::Config& DescriptorSetLayout::Config::remove(uint32_t binding) { + auto const it = std::ranges::lower_bound(bindings, binding, {}, &VkDescriptorSetLayoutBinding::binding); + CTH_CRITICAL(it == bindings.end() || it->binding != binding, "binding {} does not exist", binding) {} + + bindings.erase(it); + return *this; +} +} diff --git a/cvk/src/render/pipeline/layout/pipeline_layout.cpp b/cvk/src/render/pipeline/layout/pipeline_layout.cpp new file mode 100644 index 00000000..bb171296 --- /dev/null +++ b/cvk/src/render/pipeline/layout/pipeline_layout.cpp @@ -0,0 +1,89 @@ +#include "cvk/render/pipeline/layout/pipeline_layout.hpp" + +#include "cvk/base/core.hpp" +#include "cvk/base/physical_device.hpp" +#include "cvk/render/pipeline/layout/descriptor_set_layout.hpp" +#include "cvk/utility/vk_exceptions.hpp" + +#include + + +namespace cvk { +PipelineLayout::PipelineLayout(Core const& core, Config const& config) : _deviceTable{core.deviceTable()}, + _pushConstantRanges{config.pushConstantRanges} { + auto const maxSets = core.physicalDevice().limits().maxBoundDescriptorSets; + CTH_STABLE_ERR(config.setLayouts.size() > maxSets, "more set layouts than maxBoundDescriptorSets") { + details->add("set layouts: {}, max: {}", config.setLayouts.size(), maxSets); + throw details->exception(); + } + + _vkSetLayouts.reserve(config.setLayouts.size()); + for(auto const& layout : config.setLayouts) { + DescriptorSetLayout::debug_check(*layout); + _vkSetLayouts.push_back(layout->get()); + } +} + +PipelineLayout::PipelineLayout(Core const& core, Config const& config, State const& state) : PipelineLayout{ + core, + config +} { wrap(state); } + +PipelineLayout::PipelineLayout(Core const& core, Config const& config, create_t) : PipelineLayout{core, config} { + create(); +} + +void PipelineLayout::wrap(State const& state) { + optDestroy(); + + _handle = state.vkLayout.get(); +} + +void PipelineLayout::create() { + optDestroy(); + + VkPipelineLayoutCreateInfo const info{ + .sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO, + .pNext = nullptr, + .flags = 0, + .setLayoutCount = static_cast(_vkSetLayouts.size()), + .pSetLayouts = _vkSetLayouts.data(), + .pushConstantRangeCount = static_cast(_pushConstantRanges.size()), + .pPushConstantRanges = _pushConstantRanges.data(), + }; + + VkPipelineLayout ptr = VK_NULL_HANDLE; + auto const result = _deviceTable->vkCreatePipelineLayout(_deviceTable.device(), &info, nullptr, &ptr); + + CTH_STABLE_ERR(result != VK_SUCCESS, "failed to create pipeline layout") { + reset(); + throw cvk::vk_result_exception{result, details->exception()}; + } + + _handle = ptr; +} + +void PipelineLayout::destroy() { + debug_check(*this); + + destroy(_deviceTable, _handle.get()); + + reset(); +} + +PipelineLayout::State PipelineLayout::release() { + debug_check(*this); + + State const state{_handle.get()}; + reset(); + return state; +} + +void PipelineLayout::destroy(DeviceTable table, VkPipelineLayout vkLayout) { + CTH_WARN(vkLayout == VK_NULL_HANDLE, "vkLayout invalid (VK_NULL_HANDLE)") {} + + table->vkDestroyPipelineLayout(table.device(), vkLayout, nullptr); +} + +void PipelineLayout::reset() { _handle = VK_NULL_HANDLE; } +} diff --git a/cvk/src/render/pipeline/pipeline.cpp b/cvk/src/render/pipeline/pipeline.cpp new file mode 100644 index 00000000..a015e57a --- /dev/null +++ b/cvk/src/render/pipeline/pipeline.cpp @@ -0,0 +1,146 @@ +#include "cvk/render/pipeline/pipeline.hpp" + +#include "cvk/base/core.hpp" +#include "cvk/render/pipeline/layout/pipeline_layout.hpp" +#include "cvk/utility/vk_exceptions.hpp" + + +namespace cvk { +Pipeline::Pipeline(Core const& core, PipelineLayout const& layout, Config config) : _deviceTable{core.deviceTable()}, + _vkLayout{layout.get()}, + _config{std::move(config)} {} + +Pipeline::Pipeline(Core const& core, PipelineLayout const& layout, Config config, State const& state) : Pipeline{ + core, + layout, + std::move(config) +} { wrap(state); } + +Pipeline::Pipeline(Core const& core, PipelineLayout const& layout, Config config, create_t) : Pipeline{ + core, + layout, + std::move(config) +} { create(); } + +void Pipeline::wrap(State const& state) { + optDestroy(); + + _handle = state.vkPipeline.get(); +} + +void Pipeline::create() { + CTH_CRITICAL( + _config.colorBlendAttachments.size() != _config.colorFormats.size(), + "one color blend attachment per color format required" + ) {} + + optDestroy(); + + auto const& config = _config; + + VkPipelineVertexInputStateCreateInfo const vertexInput{ + .sType = VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO, + .pNext = nullptr, + .flags = 0, + .vertexBindingDescriptionCount = static_cast(config.vertexBindings.size()), + .pVertexBindingDescriptions = config.vertexBindings.data(), + .vertexAttributeDescriptionCount = static_cast(config.vertexAttributes.size()), + .pVertexAttributeDescriptions = config.vertexAttributes.data(), + }; + + VkPipelineViewportStateCreateInfo const viewport{ + .sType = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO, + .pNext = nullptr, + .flags = 0, + .viewportCount = config.viewportCount, + .pViewports = nullptr, + .scissorCount = config.scissorCount, + .pScissors = nullptr, + }; + + auto colorBlend = config.colorBlend; + colorBlend.attachmentCount = static_cast(config.colorBlendAttachments.size()); + colorBlend.pAttachments = config.colorBlendAttachments.data(); + + VkPipelineDynamicStateCreateInfo const dynamicState{ + .sType = VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO, + .pNext = nullptr, + .flags = 0, + .dynamicStateCount = static_cast(config.dynamicStates.size()), + .pDynamicStates = config.dynamicStates.data(), + }; + + VkPipelineRenderingCreateInfo const rendering{ + .sType = VK_STRUCTURE_TYPE_PIPELINE_RENDERING_CREATE_INFO, + .pNext = nullptr, + .viewMask = config.viewMask, + .colorAttachmentCount = static_cast(config.colorFormats.size()), + .pColorAttachmentFormats = config.colorFormats.data(), + .depthAttachmentFormat = config.depthFormat, + .stencilAttachmentFormat = config.stencilFormat, + }; + + VkGraphicsPipelineCreateInfo const info{ + .sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO, + .pNext = &rendering, + .flags = 0, + .stageCount = static_cast(config.stages.size()), + .pStages = config.stages.data(), + .pVertexInputState = &vertexInput, + .pInputAssemblyState = &config.inputAssembly, + .pTessellationState = nullptr, + .pViewportState = &viewport, + .pRasterizationState = &config.rasterization, + .pMultisampleState = &config.multisample, + .pDepthStencilState = &config.depthStencil, + .pColorBlendState = &colorBlend, + .pDynamicState = &dynamicState, + .layout = _vkLayout.get(), + .renderPass = VK_NULL_HANDLE, + .subpass = 0, + .basePipelineHandle = VK_NULL_HANDLE, + .basePipelineIndex = -1, + }; + + VkPipeline ptr = VK_NULL_HANDLE; + auto const result = _deviceTable->vkCreateGraphicsPipelines( + _deviceTable.device(), + VK_NULL_HANDLE, + 1, + &info, + nullptr, + &ptr + ); + + CTH_STABLE_ERR(result != VK_SUCCESS, "failed to create graphics pipeline") { + reset(); + throw cvk::vk_result_exception{result, details->exception()}; + } + + _handle = ptr; +} + +void Pipeline::destroy() { + debug_check(*this); + + destroy(_deviceTable, _handle.get()); + + reset(); +} + +Pipeline::State Pipeline::release() { + debug_check(*this); + + State const state{_handle.get()}; + reset(); + return state; +} + +void Pipeline::destroy(DeviceTable table, VkPipeline vkPipeline) { + CTH_WARN(vkPipeline == VK_NULL_HANDLE, "vkPipeline invalid (VK_NULL_HANDLE)") {} + + table->vkDestroyPipeline(table.device(), vkPipeline, nullptr); +} + +void Pipeline::reset() { _handle = VK_NULL_HANDLE; } +} diff --git a/cvk/src/render/pipeline/pipeline_config.cpp b/cvk/src/render/pipeline/pipeline_config.cpp new file mode 100644 index 00000000..bd78c51d --- /dev/null +++ b/cvk/src/render/pipeline/pipeline_config.cpp @@ -0,0 +1,36 @@ +#include "cvk/render/pipeline/pipeline_config.hpp" + +#include "cvk/render/pipeline/shader/shader.hpp" + +#include + + +namespace cvk { +PipelineConfig PipelineConfig::Default(std::span colorFormats, VkFormat depthFormat) { + PipelineConfig config{}; + config.colorFormats.assign_range(colorFormats); + config.colorBlendAttachments.assign(colorFormats.size(), ALPHA_BLENDING); + config.depthFormat = depthFormat; + return config; +} + +PipelineConfig& PipelineConfig::addStage(Shader const& shader, VkSpecializationInfo const* specialization) { + CTH_CRITICAL( + std::ranges::contains(stages, shader.stage(), &VkPipelineShaderStageCreateInfo::stage), + "stage already added" + ) {} + + stages.push_back(shader.stageInfo(specialization)); + return *this; +} + +PipelineConfig& PipelineConfig::removeStage(VkShaderStageFlagBits stage) { + auto const removed = std::erase_if( + stages, + [stage](VkPipelineShaderStageCreateInfo const& info) { return info.stage == stage; } + ); + CTH_CRITICAL(removed == 0, "stage not added") {} + + return *this; +} +} diff --git a/cvk/src/render/pipeline/shader/shader.cpp b/cvk/src/render/pipeline/shader/shader.cpp new file mode 100644 index 00000000..54dc0fd6 --- /dev/null +++ b/cvk/src/render/pipeline/shader/shader.cpp @@ -0,0 +1,118 @@ +#include "cvk/render/pipeline/shader/shader.hpp" + +#include "cvk/base/core.hpp" +#include "cvk/utility/vk_exceptions.hpp" + +#include + + +namespace cvk { +Shader::Shader(Core const& core, VkShaderStageFlagBits stage) : _deviceTable{core.deviceTable()}, _stage{stage} {} + +Shader::Shader(Core const& core, VkShaderStageFlagBits stage, State const& state) : Shader{core, stage} { + wrap(state); +} + +Shader::Shader(Core const& core, VkShaderStageFlagBits stage, std::span spv) : Shader{core, stage} { + create(spv); +} + +Shader::Shader(Core const& core, VkShaderStageFlagBits stage, path_t const& spvPath) : Shader{core, stage} { + create(spvPath); +} + +void Shader::wrap(State const& state) { + optDestroy(); + + _handle = state.vkShader.get(); +} + +void Shader::create(std::span spv) { + CTH_CRITICAL(spv.empty(), "spv must not be empty") {} + + optDestroy(); + + VkShaderModuleCreateInfo const info{ + .sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO, + .pNext = nullptr, + .flags = 0, + .codeSize = spv.size_bytes(), + .pCode = spv.data(), + }; + + VkShaderModule ptr = VK_NULL_HANDLE; + auto const result = _deviceTable->vkCreateShaderModule(_deviceTable.device(), &info, nullptr, &ptr); + + CTH_STABLE_ERR(result != VK_SUCCESS, "failed to create shader module") { + reset(); + throw cvk::vk_result_exception{result, details->exception()}; + } + + _handle = ptr; +} + +void Shader::create(path_t const& spvPath) { + auto const spv = load(spvPath); + create(spv); +} + +void Shader::destroy() { + debug_check(*this); + + destroy(_deviceTable, _handle.get()); + + reset(); +} + +Shader::State Shader::release() { + debug_check(*this); + + State const state{_handle.get()}; + reset(); + return state; +} + +VkPipelineShaderStageCreateInfo Shader::stageInfo( + VkSpecializationInfo const* specialization, + char const* entryPoint +) const { + debug_check(*this); + + return VkPipelineShaderStageCreateInfo{ + .sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO, + .pNext = nullptr, + .flags = 0, + .stage = _stage, + .module = _handle.get(), + .pName = entryPoint, + .pSpecializationInfo = specialization, + }; +} + +std::vector Shader::load(path_t const& spvPath) { + CVK_STABLE_THROW(!std::filesystem::exists(spvPath), "SPIR-V file does not exist") { + details->add("file: {}", spvPath.string()); + } + + auto const size = std::filesystem::file_size(spvPath); + CVK_STABLE_THROW(size == 0 || size % sizeof(uint32_t) != 0, "SPIR-V file size must be a multiple of 4") { + details->add("file: {}, size: {}", spvPath.string(), size); + } + + std::vector spv(size / sizeof(uint32_t)); + + std::ifstream file{spvPath, std::ios::binary}; + file.read(reinterpret_cast(spv.data()), static_cast(size)); + CVK_STABLE_THROW(!file, "failed to read SPIR-V file") { details->add("file: {}", spvPath.string()); } + + return spv; +} + +void Shader::destroy(DeviceTable table, VkShaderModule vkShader) { + CTH_WARN(vkShader == VK_NULL_HANDLE, "vkShader invalid (VK_NULL_HANDLE)") {} + + table->vkDestroyShaderModule(table.device(), vkShader, nullptr); +} + +void Shader::reset() { _handle = VK_NULL_HANDLE; } +} diff --git a/cvk/tests/shaders/triangle.frag b/cvk/tests/shaders/triangle.frag new file mode 100644 index 00000000..459e1b3e --- /dev/null +++ b/cvk/tests/shaders/triangle.frag @@ -0,0 +1,9 @@ +#version 450 + +layout(push_constant) uniform Push { + vec4 color; +} push; + +layout(location = 0) out vec4 outColor; + +void main() { outColor = push.color; } diff --git a/cvk/tests/shaders/triangle.vert b/cvk/tests/shaders/triangle.vert new file mode 100644 index 00000000..c9ae4568 --- /dev/null +++ b/cvk/tests/shaders/triangle.vert @@ -0,0 +1,9 @@ +#version 450 + +layout(location = 0) in vec3 position; + +layout(set = 0, binding = 0) uniform Camera { + mat4 viewProjection; +} camera; + +void main() { gl_Position = camera.viewProjection * vec4(position, 1.0); } diff --git a/cvk/tests/src/render/pipeline/layout/descriptor_set_layout_test.cpp b/cvk/tests/src/render/pipeline/layout/descriptor_set_layout_test.cpp new file mode 100644 index 00000000..39e48d1a --- /dev/null +++ b/cvk/tests/src/render/pipeline/layout/descriptor_set_layout_test.cpp @@ -0,0 +1,89 @@ +#include "context.hpp" +#include "test.hpp" + +#include "cvk/render/pipeline/layout/descriptor_set_layout.hpp" + +#include + + +namespace cvk { +namespace { + /** + * @brief bindings 0 and 2, gap at 1 + */ + DescriptorSetLayout::Config gap_config() { + DescriptorSetLayout::Config config{}; + config.add(2, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, VK_SHADER_STAGE_FRAGMENT_BIT) + .add(0, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, VK_SHADER_STAGE_VERTEX_BIT, 2); + return config; + } +} + +CVK_TEST(DescriptorSetLayout, config_sorts_bindings) { + auto const config = gap_config(); + + ASSERT_EQ(config.bindings.size(), 2); + EXPECT_EQ(config.bindings[0].binding, 0); + EXPECT_EQ(config.bindings[0].descriptorCount, 2); + EXPECT_EQ(config.bindings[1].binding, 2); +} + +CVK_TEST(DescriptorSetLayout, config_remove) { + auto config = gap_config(); + + config.remove(0); + + ASSERT_EQ(config.bindings.size(), 1); + EXPECT_EQ(config.bindings[0].binding, 2); +} + +CVK_TEST(DescriptorSetLayout, create_with_gaps) { + test::Context const context{}; + + DescriptorSetLayout const layout{context.core, gap_config(), create}; + + ASSERT_TRUE(layout.created()); + EXPECT_EQ(layout.bindings().size(), 2); + + auto const* binding = layout.binding(2); + ASSERT_NE(binding, nullptr); + EXPECT_EQ(binding->descriptorType, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER); + EXPECT_EQ(layout.binding(1), nullptr); + EXPECT_EQ(layout.binding(3), nullptr); +} + +CVK_TEST(DescriptorSetLayout, create_empty) { + test::Context const context{}; + + DescriptorSetLayout const layout{context.core, {}, create}; + + EXPECT_TRUE(layout.created()); + EXPECT_TRUE(layout.bindings().empty()); +} + +CVK_TEST(DescriptorSetLayout, release_and_wrap) { + test::Context const context{}; + DescriptorSetLayout layout{context.core, gap_config(), create}; + auto const vkLayout = layout.get(); + + auto const state = layout.release(); + EXPECT_FALSE(layout.created()); + + DescriptorSetLayout const wrapped{context.core, gap_config(), state}; + EXPECT_EQ(wrapped.get(), vkLayout); + EXPECT_EQ(wrapped.bindings().size(), 2); +} + +CVK_TEST(DescriptorSetLayout, move_assignment_destroys_the_old_layout) { + test::Context const context{}; + DescriptorSetLayout layout{context.core, {}, create}; + DescriptorSetLayout other{context.core, gap_config(), create}; + auto const vkLayout = other.get(); + + layout = std::move(other); + + EXPECT_EQ(layout.get(), vkLayout); + EXPECT_EQ(layout.bindings().size(), 2); + EXPECT_FALSE(other.created()); +} +} diff --git a/cvk/tests/src/render/pipeline/layout/pipeline_layout_test.cpp b/cvk/tests/src/render/pipeline/layout/pipeline_layout_test.cpp new file mode 100644 index 00000000..63791f73 --- /dev/null +++ b/cvk/tests/src/render/pipeline/layout/pipeline_layout_test.cpp @@ -0,0 +1,88 @@ +#include "context.hpp" +#include "test.hpp" + +#include "cvk/base/physical_device.hpp" +#include "cvk/render/pipeline/layout/descriptor_set_layout.hpp" +#include "cvk/render/pipeline/layout/pipeline_layout.hpp" + + +namespace cvk { +namespace { + DescriptorSetLayout::Config uniform_config() { + DescriptorSetLayout::Config config{}; + config.add(0, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, VK_SHADER_STAGE_VERTEX_BIT); + return config; + } +} + +CVK_TEST(PipelineLayout, create_empty) { + test::Context const context{}; + + PipelineLayout const layout{context.core, {}, create}; + + EXPECT_TRUE(layout.created()); +} + +CVK_TEST(PipelineLayout, create_with_sets_and_push_constants) { + test::Context const context{}; + DescriptorSetLayout const set0{context.core, uniform_config(), create}; + DescriptorSetLayout const set1{context.core, {}, create}; + + PipelineLayout::Config const config{ + .setLayouts = {&set0, &set1}, + .pushConstantRanges = {{.stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT, .offset = 0, .size = 16}}, + }; + + PipelineLayout const layout{context.core, config, create}; + + EXPECT_TRUE(layout.created()); +} + +CVK_TEST(PipelineLayout, too_many_sets_throws) { + test::Context const context{}; + DescriptorSetLayout const set{context.core, {}, create}; + + auto const maxSets = context.core.physicalDevice().limits().maxBoundDescriptorSets; + PipelineLayout::Config const config{ + .setLayouts = std::vector>(maxSets + 1, &set), + }; + + auto const construct = [&] { PipelineLayout const layout{context.core, config}; }; + + EXPECT_THROW(construct(), cth::except::default_exception); +} + +CVK_TEST(PipelineLayout, set_layouts_can_be_destroyed_after_create) { + test::Context const context{}; + auto set = std::make_unique(context.core, uniform_config(), create); + + PipelineLayout const layout{context.core, PipelineLayout::Config{.setLayouts = {set.get()}}, create}; + set = nullptr; + + EXPECT_TRUE(layout.created()); +} + +CVK_TEST(PipelineLayout, release_and_wrap) { + test::Context const context{}; + PipelineLayout layout{context.core, {}, create}; + auto const vkLayout = layout.get(); + + auto const state = layout.release(); + EXPECT_FALSE(layout.created()); + + PipelineLayout const wrapped{context.core, {}, state}; + EXPECT_EQ(wrapped.get(), vkLayout); +} + +CVK_TEST(PipelineLayout, move_assignment_destroys_the_old_layout) { + test::Context const context{}; + PipelineLayout layout{context.core, {}, create}; + PipelineLayout other{context.core, {}, create}; + auto const vkLayout = other.get(); + + layout = std::move(other); + + EXPECT_EQ(layout.get(), vkLayout); + EXPECT_FALSE(other.created()); +} +} diff --git a/cvk/tests/src/render/pipeline/pipeline_test.cpp b/cvk/tests/src/render/pipeline/pipeline_test.cpp new file mode 100644 index 00000000..aee3ff69 --- /dev/null +++ b/cvk/tests/src/render/pipeline/pipeline_test.cpp @@ -0,0 +1,150 @@ +#include "context.hpp" +#include "shaders.hpp" +#include "test.hpp" + +#include "cvk/render/pipeline/layout/descriptor_set_layout.hpp" +#include "cvk/render/pipeline/layout/pipeline_layout.hpp" +#include "cvk/render/pipeline/pipeline.hpp" +#include "cvk/render/pipeline/shader/shader.hpp" + +#include +#include + + +namespace cvk { +namespace { + constexpr std::array COLOR_FORMATS{VK_FORMAT_B8G8R8A8_UNORM}; + constexpr VkFormat DEPTH_FORMAT = VK_FORMAT_D32_SFLOAT; + + /** + * @brief layout and shaders of the triangle shaders + */ + struct Triangle { + explicit Triangle(Core const& core) : setLayout{core, set_config(), create}, + layout{ + core, + PipelineLayout::Config{ + .setLayouts = {&setLayout}, + .pushConstantRanges = {{.stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT, .offset = 0, .size = 16}}, + }, + create + }, + vertex{core, VK_SHADER_STAGE_VERTEX_BIT, test::shaders::TRIANGLE_VERT}, + fragment{core, VK_SHADER_STAGE_FRAGMENT_BIT, test::shaders::TRIANGLE_FRAG} {} + + [[nodiscard]] Pipeline::Config config() const { + auto config = Pipeline::Config::Default(COLOR_FORMATS, DEPTH_FORMAT); + config.addStage(vertex).addStage(fragment); + config.vertexBindings = { + {.binding = 0, .stride = 3 * sizeof(float), .inputRate = VK_VERTEX_INPUT_RATE_VERTEX}, + }; + config.vertexAttributes = { + {.location = 0, .binding = 0, .format = VK_FORMAT_R32G32B32_SFLOAT, .offset = 0}, + }; + return config; + } + + static DescriptorSetLayout::Config set_config() { + DescriptorSetLayout::Config config{}; + config.add(0, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, VK_SHADER_STAGE_VERTEX_BIT); + return config; + } + + DescriptorSetLayout setLayout; + PipelineLayout layout; + Shader vertex; + Shader fragment; + }; +} + +CVK_TEST(Pipeline, config_default) { + auto const config = Pipeline::Config::Default(COLOR_FORMATS, DEPTH_FORMAT); + + EXPECT_TRUE(std::ranges::equal(config.colorFormats, COLOR_FORMATS)); + ASSERT_EQ(config.colorBlendAttachments.size(), 1); + EXPECT_EQ(config.colorBlendAttachments[0].blendEnable, VK_TRUE); + EXPECT_EQ(config.depthFormat, DEPTH_FORMAT); +} + +CVK_TEST(Pipeline, config_stages) { + test::Context const context{}; + Triangle const triangle{context.core}; + + auto config = triangle.config(); + ASSERT_EQ(config.stages.size(), 2); + EXPECT_EQ(config.stages[0].module, triangle.vertex.get()); + + config.removeStage(VK_SHADER_STAGE_VERTEX_BIT); + ASSERT_EQ(config.stages.size(), 1); + EXPECT_EQ(config.stages[0].stage, VK_SHADER_STAGE_FRAGMENT_BIT); +} + +CVK_TEST(Pipeline, base_constructor_does_not_create) { + test::Context const context{}; + Triangle const triangle{context.core}; + + Pipeline const pipeline{context.core, triangle.layout, triangle.config()}; + + EXPECT_FALSE(pipeline.created()); + EXPECT_EQ(pipeline.layout(), triangle.layout.get()); +} + +CVK_TEST(Pipeline, create) { + test::Context const context{}; + Triangle const triangle{context.core}; + + Pipeline const pipeline{context.core, triangle.layout, triangle.config(), create}; + + EXPECT_TRUE(pipeline.created()); + EXPECT_EQ(pipeline.config().stages.size(), 2); +} + +CVK_TEST(Pipeline, create_without_depth) { + test::Context const context{}; + Triangle const triangle{context.core}; + + auto config = triangle.config(); + config.depthFormat = VK_FORMAT_UNDEFINED; + + Pipeline const pipeline{context.core, triangle.layout, std::move(config), create}; + + EXPECT_TRUE(pipeline.created()); +} + +CVK_TEST(Pipeline, shaders_can_be_destroyed_after_create) { + test::Context const context{}; + auto triangle = std::make_unique(context.core); + + Pipeline pipeline{context.core, triangle->layout, triangle->config(), create}; + triangle->vertex.destroy(); + triangle->fragment.destroy(); + + EXPECT_TRUE(pipeline.created()); +} + +CVK_TEST(Pipeline, release_and_wrap) { + test::Context const context{}; + Triangle const triangle{context.core}; + Pipeline pipeline{context.core, triangle.layout, triangle.config(), create}; + auto const vkPipeline = pipeline.get(); + + auto const state = pipeline.release(); + EXPECT_FALSE(pipeline.created()); + + Pipeline const wrapped{context.core, triangle.layout, triangle.config(), state}; + EXPECT_EQ(wrapped.get(), vkPipeline); +} + +CVK_TEST(Pipeline, move_assignment_destroys_the_old_pipeline) { + test::Context const context{}; + Triangle const triangle{context.core}; + Pipeline pipeline{context.core, triangle.layout, triangle.config(), create}; + Pipeline other{context.core, triangle.layout, triangle.config(), create}; + auto const vkPipeline = other.get(); + + pipeline = std::move(other); + + EXPECT_EQ(pipeline.get(), vkPipeline); + EXPECT_FALSE(other.created()); +} +} diff --git a/cvk/tests/src/render/pipeline/shader/shader_test.cpp b/cvk/tests/src/render/pipeline/shader/shader_test.cpp new file mode 100644 index 00000000..7ffd408b --- /dev/null +++ b/cvk/tests/src/render/pipeline/shader/shader_test.cpp @@ -0,0 +1,103 @@ +#include "context.hpp" +#include "shaders.hpp" +#include "test.hpp" + +#include "cvk/render/pipeline/shader/shader.hpp" +#include "cvk/utility/vk_exceptions.hpp" + +#include +#include + + +namespace cvk { +namespace { + using test::shaders::TRIANGLE_VERT; + + /** + * @brief writes bytes to a file in the temp directory + */ + std::filesystem::path write_temp(std::string_view name, std::span bytes) { + auto path = std::filesystem::temp_directory_path() / name; + std::ofstream file{path, std::ios::binary}; + file.write(bytes.data(), static_cast(bytes.size())); + return path; + } +} + +CVK_TEST(Shader, base_constructor_does_not_create) { + test::Context const context{}; + + Shader const shader{context.core, VK_SHADER_STAGE_VERTEX_BIT}; + + EXPECT_FALSE(shader.created()); + EXPECT_EQ(shader.stage(), VK_SHADER_STAGE_VERTEX_BIT); +} + +CVK_TEST(Shader, create_from_spv) { + test::Context const context{}; + + Shader const shader{context.core, VK_SHADER_STAGE_VERTEX_BIT, TRIANGLE_VERT}; + + ASSERT_TRUE(shader.created()); + + auto const info = shader.stageInfo(); + EXPECT_EQ(info.sType, VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO); + EXPECT_EQ(info.stage, VK_SHADER_STAGE_VERTEX_BIT); + EXPECT_EQ(info.module, shader.get()); + EXPECT_STREQ(info.pName, "main"); + EXPECT_EQ(info.pSpecializationInfo, nullptr); +} + +CVK_TEST(Shader, create_from_file) { + test::Context const context{}; + std::span const bytes{reinterpret_cast(TRIANGLE_VERT.data()), sizeof(TRIANGLE_VERT)}; + auto const path = write_temp("cvk_test_triangle.vert.spv", bytes); + + Shader const shader{context.core, VK_SHADER_STAGE_VERTEX_BIT, path}; + + EXPECT_TRUE(shader.created()); + EXPECT_TRUE(std::ranges::equal(Shader::load(path), TRIANGLE_VERT)); + + std::filesystem::remove(path); +} + +CVK_TEST(Shader, load_rejects_bad_files) { + auto const missing = std::filesystem::temp_directory_path() / "cvk_test_missing.spv"; + std::filesystem::remove(missing); + EXPECT_THROW([[maybe_unused]] auto const spv = Shader::load(missing), cvk_exception); + + constexpr std::array notWords{1, 2, 3, 4, 5}; + auto const odd = write_temp("cvk_test_odd.spv", notWords); + EXPECT_THROW([[maybe_unused]] auto const spv = Shader::load(odd), cvk_exception); + std::filesystem::remove(odd); + + auto const empty = write_temp("cvk_test_empty.spv", {}); + EXPECT_THROW([[maybe_unused]] auto const spv = Shader::load(empty), cvk_exception); + std::filesystem::remove(empty); +} + +CVK_TEST(Shader, release_and_wrap) { + test::Context const context{}; + Shader shader{context.core, VK_SHADER_STAGE_VERTEX_BIT, TRIANGLE_VERT}; + auto const vkShader = shader.get(); + + auto const state = shader.release(); + EXPECT_FALSE(shader.created()); + + Shader const wrapped{context.core, VK_SHADER_STAGE_VERTEX_BIT, state}; + EXPECT_EQ(wrapped.get(), vkShader); +} + +CVK_TEST(Shader, move_assignment_destroys_the_old_shader) { + test::Context const context{}; + Shader shader{context.core, VK_SHADER_STAGE_VERTEX_BIT, TRIANGLE_VERT}; + Shader other{context.core, VK_SHADER_STAGE_FRAGMENT_BIT, test::shaders::TRIANGLE_FRAG}; + auto const vkShader = other.get(); + + shader = std::move(other); + + EXPECT_EQ(shader.get(), vkShader); + EXPECT_EQ(shader.stage(), VK_SHADER_STAGE_FRAGMENT_BIT); + EXPECT_FALSE(other.created()); +} +} diff --git a/cvk/tests/src/shaders.hpp b/cvk/tests/src/shaders.hpp new file mode 100644 index 00000000..d95abd25 --- /dev/null +++ b/cvk/tests/src/shaders.hpp @@ -0,0 +1,62 @@ +#pragma once +#include +#include + +/** + * generated from cvk/tests/shaders, regenerate with: + * glslc --target-env=vulkan1.3 -O -mfmt=num -o .num + */ +namespace cvk::test::shaders { +/** + * @brief vertex shader: location 0 vec3 position, set 0 binding 0 uniform buffer + */ +inline constexpr std::array TRIANGLE_VERT{ + 0x07230203, 0x00010600, 0x000d000b, 0x00000023, 0x00000000, 0x00020011, 0x00000001, 0x0006000b, + 0x00000001, 0x4c534c47, 0x6474732e, 0x3035342e, 0x00000000, 0x0003000e, 0x00000000, 0x00000001, + 0x0008000f, 0x00000000, 0x00000004, 0x6e69616d, 0x00000000, 0x0000000d, 0x00000013, 0x00000019, + 0x00030047, 0x0000000b, 0x00000002, 0x00050048, 0x0000000b, 0x00000000, 0x0000000b, 0x00000000, + 0x00050048, 0x0000000b, 0x00000001, 0x0000000b, 0x00000001, 0x00050048, 0x0000000b, 0x00000002, + 0x0000000b, 0x00000003, 0x00050048, 0x0000000b, 0x00000003, 0x0000000b, 0x00000004, 0x00030047, + 0x00000011, 0x00000002, 0x00040048, 0x00000011, 0x00000000, 0x00000005, 0x00050048, 0x00000011, + 0x00000000, 0x00000007, 0x00000010, 0x00050048, 0x00000011, 0x00000000, 0x00000023, 0x00000000, + 0x00040047, 0x00000013, 0x00000021, 0x00000000, 0x00040047, 0x00000013, 0x00000022, 0x00000000, + 0x00040047, 0x00000019, 0x0000001e, 0x00000000, 0x00020013, 0x00000002, 0x00030021, 0x00000003, + 0x00000002, 0x00030016, 0x00000006, 0x00000020, 0x00040017, 0x00000007, 0x00000006, 0x00000004, + 0x00040015, 0x00000008, 0x00000020, 0x00000000, 0x0004002b, 0x00000008, 0x00000009, 0x00000001, + 0x0004001c, 0x0000000a, 0x00000006, 0x00000009, 0x0006001e, 0x0000000b, 0x00000007, 0x00000006, + 0x0000000a, 0x0000000a, 0x00040020, 0x0000000c, 0x00000003, 0x0000000b, 0x0004003b, 0x0000000c, + 0x0000000d, 0x00000003, 0x00040015, 0x0000000e, 0x00000020, 0x00000001, 0x0004002b, 0x0000000e, + 0x0000000f, 0x00000000, 0x00040018, 0x00000010, 0x00000007, 0x00000004, 0x0003001e, 0x00000011, + 0x00000010, 0x00040020, 0x00000012, 0x00000002, 0x00000011, 0x0004003b, 0x00000012, 0x00000013, + 0x00000002, 0x00040020, 0x00000014, 0x00000002, 0x00000010, 0x00040017, 0x00000017, 0x00000006, + 0x00000003, 0x00040020, 0x00000018, 0x00000001, 0x00000017, 0x0004003b, 0x00000018, 0x00000019, + 0x00000001, 0x0004002b, 0x00000006, 0x0000001b, 0x3f800000, 0x00040020, 0x00000021, 0x00000003, + 0x00000007, 0x00050036, 0x00000002, 0x00000004, 0x00000000, 0x00000003, 0x000200f8, 0x00000005, + 0x00050041, 0x00000014, 0x00000015, 0x00000013, 0x0000000f, 0x0004003d, 0x00000010, 0x00000016, + 0x00000015, 0x0004003d, 0x00000017, 0x0000001a, 0x00000019, 0x00050051, 0x00000006, 0x0000001c, + 0x0000001a, 0x00000000, 0x00050051, 0x00000006, 0x0000001d, 0x0000001a, 0x00000001, 0x00050051, + 0x00000006, 0x0000001e, 0x0000001a, 0x00000002, 0x00070050, 0x00000007, 0x0000001f, 0x0000001c, + 0x0000001d, 0x0000001e, 0x0000001b, 0x00050091, 0x00000007, 0x00000020, 0x00000016, 0x0000001f, + 0x00050041, 0x00000021, 0x00000022, 0x0000000d, 0x0000000f, 0x0003003e, 0x00000022, 0x00000020, + 0x000100fd, 0x00010038, +}; + +/** + * @brief fragment shader: push constant vec4 color, location 0 color output + */ +inline constexpr std::array TRIANGLE_FRAG{ + 0x07230203, 0x00010600, 0x000d000b, 0x00000012, 0x00000000, 0x00020011, 0x00000001, 0x0006000b, + 0x00000001, 0x4c534c47, 0x6474732e, 0x3035342e, 0x00000000, 0x0003000e, 0x00000000, 0x00000001, + 0x0007000f, 0x00000004, 0x00000004, 0x6e69616d, 0x00000000, 0x00000009, 0x0000000c, 0x00030010, + 0x00000004, 0x00000007, 0x00040047, 0x00000009, 0x0000001e, 0x00000000, 0x00030047, 0x0000000a, + 0x00000002, 0x00050048, 0x0000000a, 0x00000000, 0x00000023, 0x00000000, 0x00020013, 0x00000002, + 0x00030021, 0x00000003, 0x00000002, 0x00030016, 0x00000006, 0x00000020, 0x00040017, 0x00000007, + 0x00000006, 0x00000004, 0x00040020, 0x00000008, 0x00000003, 0x00000007, 0x0004003b, 0x00000008, + 0x00000009, 0x00000003, 0x0003001e, 0x0000000a, 0x00000007, 0x00040020, 0x0000000b, 0x00000009, + 0x0000000a, 0x0004003b, 0x0000000b, 0x0000000c, 0x00000009, 0x00040015, 0x0000000d, 0x00000020, + 0x00000001, 0x0004002b, 0x0000000d, 0x0000000e, 0x00000000, 0x00040020, 0x0000000f, 0x00000009, + 0x00000007, 0x00050036, 0x00000002, 0x00000004, 0x00000000, 0x00000003, 0x000200f8, 0x00000005, + 0x00050041, 0x0000000f, 0x00000010, 0x0000000c, 0x0000000e, 0x0004003d, 0x00000007, 0x00000011, + 0x00000010, 0x0003003e, 0x00000009, 0x00000011, 0x000100fd, 0x00010038, +}; +} From e3c372732e3ba5485e62acc78bc75c401fd0fd2e Mon Sep 17 00:00:00 2001 From: lukas <89145039+codethinki@users.noreply.github.com> Date: Fri, 16 Oct 2026 15:11:37 +0200 Subject: [PATCH 017/135] cvk: port sampler - cvk lifecycle (legacy only had a creating constructor and no release) - maxAnisotropy clamped to maxSamplerAnisotropy - compareOp is optional: legacy used NEVER for no comparison, so a NEVER compare could not be expressed - named fields instead of filter and address mode arrays, sampler_config.hpp - moved to data/img, texture goes to jvk --- cvk/incl/cvk/data/img/sampler.hpp | 125 +++++++++++++++++++++++ cvk/incl/cvk/data/img/sampler_config.hpp | 42 ++++++++ cvk/src/data/img/sampler.cpp | 64 ++++++++++++ cvk/src/data/img/sampler_config.cpp | 27 +++++ cvk/tests/src/data/img/sampler_test.cpp | 77 ++++++++++++++ 5 files changed, 335 insertions(+) create mode 100644 cvk/incl/cvk/data/img/sampler.hpp create mode 100644 cvk/incl/cvk/data/img/sampler_config.hpp create mode 100644 cvk/src/data/img/sampler.cpp create mode 100644 cvk/src/data/img/sampler_config.cpp create mode 100644 cvk/tests/src/data/img/sampler_test.cpp diff --git a/cvk/incl/cvk/data/img/sampler.hpp b/cvk/incl/cvk/data/img/sampler.hpp new file mode 100644 index 00000000..afed29d5 --- /dev/null +++ b/cvk/incl/cvk/data/img/sampler.hpp @@ -0,0 +1,125 @@ +#pragma once +#include "sampler_config.hpp" + +#include "cvk/base/device_table.hpp" +#include "cvk/utility/constants.hpp" +#include "cvk/utility/types.hpp" + +#include + +#include +#include + + +namespace cvk { +class Core; + +/** + * @brief VkSampler wrapper + */ +class Sampler { +public: + using Config = SamplerConfig; + struct State; + + /** + * @brief base constructor + * @details clamps @ref Config::maxAnisotropy to maxSamplerAnisotropy of the device + * @pre @ref Core::created() + */ + Sampler(Core const&, Config const&); + + /** + * @brief constructs and wraps + * @details calls: + * - @ref Sampler(Core const&, Config const&) + * - @ref wrap() + */ + Sampler(Core const&, Config const&, State const&); + + /** + * @brief constructs and creates + * @details calls: + * - @ref Sampler(Core const&, Config const&) + * - @ref create() + */ + explicit Sampler(Core const&, Config const&, create_t); + + /** + * @note calls @ref optDestroy() + */ + ~Sampler() { optDestroy(); } + + /** + * @brief wraps the state + * @pre the sampler was created with the config + * @post @ref created() + * @note calls @ref optDestroy() + */ + void wrap(State const&); + + /** + * @brief creates the sampler + * @post @ref created() + * @note calls @ref optDestroy() + * @throws cvk::vk_result_exception result of vkCreateSampler() + */ + void create(); + + /** + * @brief destroys and resets + * @pre @ref created() + * @pre the gpu does not use the sampler + */ + void destroy(); + + /** + * @brief if @ref created() -> calls @ref destroy() + */ + void optDestroy() { if(created()) destroy(); } + + /** + * @brief releases ownership, returns the state and resets + * @pre @ref created() + */ + State release(); + + static void destroy(DeviceTable table, VkSampler); + + [[nodiscard]] bool created() const { return _handle != VK_NULL_HANDLE; } + [[nodiscard]] VkSampler get() const { return _handle.get(); } + [[nodiscard]] DeviceTable deviceTable() const { return _deviceTable; } + + /** + * @return the config, with the clamped anisotropy + */ + [[nodiscard]] Config const& config() const { return _config; } + +private: + void reset(); + + DeviceTable _deviceTable; + Config _config; + move_ptr _handle = VK_NULL_HANDLE; + +public: + Sampler(Sampler const& other) = delete; + Sampler(Sampler&& other) noexcept = default; + Sampler& operator=(Sampler const& other) = delete; + Sampler& operator=(Sampler&& other) noexcept { return cth::reconstruct_at(*this, std::move(other)); } + + static void debug_check(Sampler const&); +}; +} + +namespace cvk { +struct Sampler::State { + vk_not_null vkSampler; +}; +} + +namespace cvk { +inline void Sampler::debug_check(Sampler const& sampler) { + CTH_CRITICAL(!sampler.created(), "sampler must be created") {} +} +} diff --git a/cvk/incl/cvk/data/img/sampler_config.hpp b/cvk/incl/cvk/data/img/sampler_config.hpp new file mode 100644 index 00000000..39fd02a5 --- /dev/null +++ b/cvk/incl/cvk/data/img/sampler_config.hpp @@ -0,0 +1,42 @@ +#pragma once +#include + +#include + + +namespace cvk { +/** + * @brief sampler state, defaults: linear filtering, repeat, 16x anisotropy + */ +struct SamplerConfig { + VkFilter magFilter = VK_FILTER_LINEAR; + VkFilter minFilter = VK_FILTER_LINEAR; + VkSamplerMipmapMode mipmapMode = VK_SAMPLER_MIPMAP_MODE_LINEAR; + + VkSamplerAddressMode addressModeU = VK_SAMPLER_ADDRESS_MODE_REPEAT; + VkSamplerAddressMode addressModeV = VK_SAMPLER_ADDRESS_MODE_REPEAT; + VkSamplerAddressMode addressModeW = VK_SAMPLER_ADDRESS_MODE_REPEAT; + + float mipLodBias = 0.f; + float minLod = 0.f; + /** + * @brief VK_LOD_CLAMP_NONE -> no clamp + */ + float maxLod = VK_LOD_CLAMP_NONE; + + /** + * @brief 0 -> no anisotropic filtering + */ + float maxAnisotropy = 16.f; + + /** + * @brief std::nullopt -> no comparison + */ + std::optional compareOp = std::nullopt; + + VkBorderColor borderColor = VK_BORDER_COLOR_INT_OPAQUE_BLACK; + VkBool32 unnormalizedCoordinates = VK_FALSE; + + [[nodiscard]] VkSamplerCreateInfo createInfo() const; +}; +} diff --git a/cvk/src/data/img/sampler.cpp b/cvk/src/data/img/sampler.cpp new file mode 100644 index 00000000..58934b7e --- /dev/null +++ b/cvk/src/data/img/sampler.cpp @@ -0,0 +1,64 @@ +#include "cvk/data/img/sampler.hpp" + +#include "cvk/base/core.hpp" +#include "cvk/base/physical_device.hpp" +#include "cvk/utility/vk_exceptions.hpp" + +#include + + +namespace cvk { +Sampler::Sampler(Core const& core, Config const& config) : _deviceTable{core.deviceTable()}, _config{config} { + _config.maxAnisotropy = std::min(_config.maxAnisotropy, core.physicalDevice().limits().maxSamplerAnisotropy); +} + +Sampler::Sampler(Core const& core, Config const& config, State const& state) : Sampler{core, config} { wrap(state); } + +Sampler::Sampler(Core const& core, Config const& config, create_t) : Sampler{core, config} { create(); } + +void Sampler::wrap(State const& state) { + optDestroy(); + + _handle = state.vkSampler.get(); +} + +void Sampler::create() { + optDestroy(); + + auto const info = _config.createInfo(); + + VkSampler ptr = VK_NULL_HANDLE; + auto const result = _deviceTable->vkCreateSampler(_deviceTable.device(), &info, nullptr, &ptr); + + CTH_STABLE_ERR(result != VK_SUCCESS, "failed to create sampler") { + reset(); + throw cvk::vk_result_exception{result, details->exception()}; + } + + _handle = ptr; +} + +void Sampler::destroy() { + debug_check(*this); + + destroy(_deviceTable, _handle.get()); + + reset(); +} + +Sampler::State Sampler::release() { + debug_check(*this); + + State const state{_handle.get()}; + reset(); + return state; +} + +void Sampler::destroy(DeviceTable table, VkSampler vkSampler) { + CTH_WARN(vkSampler == VK_NULL_HANDLE, "vkSampler invalid (VK_NULL_HANDLE)") {} + + table->vkDestroySampler(table.device(), vkSampler, nullptr); +} + +void Sampler::reset() { _handle = VK_NULL_HANDLE; } +} diff --git a/cvk/src/data/img/sampler_config.cpp b/cvk/src/data/img/sampler_config.cpp new file mode 100644 index 00000000..c474b58e --- /dev/null +++ b/cvk/src/data/img/sampler_config.cpp @@ -0,0 +1,27 @@ +#include "cvk/data/img/sampler_config.hpp" + + +namespace cvk { +VkSamplerCreateInfo SamplerConfig::createInfo() const { + return VkSamplerCreateInfo{ + .sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO, + .pNext = nullptr, + .flags = 0, + .magFilter = magFilter, + .minFilter = minFilter, + .mipmapMode = mipmapMode, + .addressModeU = addressModeU, + .addressModeV = addressModeV, + .addressModeW = addressModeW, + .mipLodBias = mipLodBias, + .anisotropyEnable = maxAnisotropy != 0.f ? VK_TRUE : VK_FALSE, + .maxAnisotropy = maxAnisotropy, + .compareEnable = compareOp.has_value() ? VK_TRUE : VK_FALSE, + .compareOp = compareOp.value_or(VK_COMPARE_OP_NEVER), + .minLod = minLod, + .maxLod = maxLod, + .borderColor = borderColor, + .unnormalizedCoordinates = unnormalizedCoordinates, + }; +} +} diff --git a/cvk/tests/src/data/img/sampler_test.cpp b/cvk/tests/src/data/img/sampler_test.cpp new file mode 100644 index 00000000..14747935 --- /dev/null +++ b/cvk/tests/src/data/img/sampler_test.cpp @@ -0,0 +1,77 @@ +#include "context.hpp" +#include "test.hpp" + +#include "cvk/base/physical_device.hpp" +#include "cvk/data/img/sampler.hpp" + + +namespace cvk { +CVK_TEST(Sampler, config_create_info) { + Sampler::Config config{}; + + auto info = config.createInfo(); + EXPECT_EQ(info.anisotropyEnable, VK_TRUE); + EXPECT_EQ(info.compareEnable, VK_FALSE); + + config.maxAnisotropy = 0.f; + config.compareOp = VK_COMPARE_OP_NEVER; + info = config.createInfo(); + EXPECT_EQ(info.anisotropyEnable, VK_FALSE); + EXPECT_EQ(info.compareEnable, VK_TRUE); + EXPECT_EQ(info.compareOp, VK_COMPARE_OP_NEVER); +} + +CVK_TEST(Sampler, base_constructor_clamps_anisotropy) { + test::Context const context{}; + auto const limit = context.core.physicalDevice().limits().maxSamplerAnisotropy; + + Sampler const sampler{context.core, Sampler::Config{.maxAnisotropy = limit * 2}}; + + EXPECT_FALSE(sampler.created()); + EXPECT_EQ(sampler.config().maxAnisotropy, limit); +} + +CVK_TEST(Sampler, create_default) { + test::Context const context{}; + + Sampler const sampler{context.core, {}, create}; + + EXPECT_TRUE(sampler.created()); +} + +CVK_TEST(Sampler, create_compare_without_anisotropy) { + test::Context const context{}; + + Sampler const sampler{ + context.core, + Sampler::Config{.maxAnisotropy = 0.f, .compareOp = VK_COMPARE_OP_LESS_OR_EQUAL}, + create + }; + + EXPECT_TRUE(sampler.created()); +} + +CVK_TEST(Sampler, release_and_wrap) { + test::Context const context{}; + Sampler sampler{context.core, {}, create}; + auto const vkSampler = sampler.get(); + + auto const state = sampler.release(); + EXPECT_FALSE(sampler.created()); + + Sampler const wrapped{context.core, {}, state}; + EXPECT_EQ(wrapped.get(), vkSampler); +} + +CVK_TEST(Sampler, move_assignment_destroys_the_old_sampler) { + test::Context const context{}; + Sampler sampler{context.core, {}, create}; + Sampler other{context.core, {}, create}; + auto const vkSampler = other.get(); + + sampler = std::move(other); + + EXPECT_EQ(sampler.get(), vkSampler); + EXPECT_FALSE(other.created()); +} +} From 885d76ee0d48a0d5cc1fdc11a1d06694505c22f6 Mon Sep 17 00:00:00 2001 From: lukas <89145039+codethinki@users.noreply.github.com> Date: Fri, 16 Oct 2026 15:39:08 +0200 Subject: [PATCH 018/135] cvk: fence os handle export, fixed The port had dropped Fence::extractOsHandle(). Restored and fixed: - create() takes export handle types, chains VkExportFenceCreateInfo (legacy never made the fence exportable) - the get info carries the real fence (legacy: VK_NULL_HANDLE from a consteval function) - the handle is written to a local and returned (legacy passed static_cast(nullptr) as output pointer) - linux sType is VK_STRUCTURE_TYPE_FENCE_GET_FD_INFO_KHR (legacy used the win32 one) - returns cth::os::unique_native_handle, the caller owned handle is closed (legacy leaked it) - linux exports a sync fd (pollable, the export resets the fence), windows the opaque win32 handle - windows loads vkGetFenceWin32HandleKHR with vkGetDeviceProcAddr: volk from vcpkg is built without platform defines - constants::OS_FENCE_HANDLE_TYPE and OS_FENCE_DEVICE_EXTENSIONS, cvk links cth::os - tested on linux (Mesa ANV and llvmpipe), the windows path is not compiled here --- cvk/CMakeLists.txt | 4 +- cvk/incl/cvk/render/sync/fence.hpp | 29 ++++++- .../cvk/utility/constant/os_constants.hpp | 31 +++++++ cvk/src/render/sync/fence.cpp | 79 +++++++++++++++--- cvk/tests/src/render/sync/fence_test.cpp | 80 +++++++++++++++++++ 5 files changed, 205 insertions(+), 18 deletions(-) create mode 100644 cvk/incl/cvk/utility/constant/os_constants.hpp diff --git a/cvk/CMakeLists.txt b/cvk/CMakeLists.txt index d7ca01bb..1efe8c4a 100644 --- a/cvk/CMakeLists.txt +++ b/cvk/CMakeLists.txt @@ -19,11 +19,11 @@ target_precompile_headers( find_package(volk CONFIG REQUIRED) find_package(VulkanUtilityLibraries CONFIG REQUIRED) -find_package(cth CONFIG REQUIRED COMPONENTS cth) +find_package(cth CONFIG REQUIRED COMPONENTS cth os) # cth/hash/hash_aggregates.hpp includes boost/pfr.hpp, the cth package does not export it find_package(boost_pfr CONFIG REQUIRED) -target_link_libraries(cvk PUBLIC volk::volk Vulkan::UtilityHeaders cth::cth Boost::pfr) +target_link_libraries(cvk PUBLIC volk::volk Vulkan::UtilityHeaders cth::cth cth::os Boost::pfr) # tests if(JOLLY_ENABLE_TESTS) diff --git a/cvk/incl/cvk/render/sync/fence.hpp b/cvk/incl/cvk/render/sync/fence.hpp index 7d61681d..23d157c3 100644 --- a/cvk/incl/cvk/render/sync/fence.hpp +++ b/cvk/incl/cvk/render/sync/fence.hpp @@ -5,6 +5,7 @@ #include #include +#include #include #include @@ -40,7 +41,7 @@ class Fence { * - @ref Fence(Core const&) * - @ref create() */ - Fence(Core const&, VkFenceCreateFlags flags); + Fence(Core const&, VkFenceCreateFlags flags, VkExternalFenceHandleTypeFlags exportTypes = 0); /** * @note calls @ref optDestroy() @@ -57,11 +58,13 @@ class Fence { /** * @brief creates the fence * @param flags VK_FENCE_CREATE_SIGNALED_BIT -> created signaled + * @param exportTypes handle types the fence can export, 0 -> not exportable, + * @ref constants::OS_FENCE_HANDLE_TYPE for @ref extractOsHandle() * @post @ref created() * @note calls @ref optDestroy() * @throws cvk::vk_result_exception result of vkCreateFence() */ - void create(VkFenceCreateFlags flags = 0); + void create(VkFenceCreateFlags flags = 0, VkExternalFenceHandleTypeFlags exportTypes = 0); /** * @brief destroys and resets @@ -134,19 +137,33 @@ class Fence { */ void waitReset() const; + /** + * @brief exports the fence as an OS handle, see @ref constants::OS_FENCE_HANDLE_TYPE + * @details windows: the fence keeps its state, + * posix: the export resets the fence (sync fd, copy transference), invalid handle -> already signaled + * @return handle, waitable with cth::os::wait() or a scheduler + * @pre @ref created() with @ref constants::OS_FENCE_HANDLE_TYPE in the export types + * @pre the device enables @ref constants::OS_FENCE_DEVICE_EXTENSIONS + * @pre posix: the fence is signaled or a signal operation is pending + * @attention posix: blocks until the pending work reaches the kernel, e.g. a wait before signal on a timeline + * semaphore that the host signals later -> deadlock + * @throws cvk::vk_result_exception result of vkGetFenceWin32HandleKHR() or vkGetFenceFdKHR() + */ + [[nodiscard]] cth::os::unique_native_handle extractOsHandle() const; + static void destroy(DeviceTable table, VkFence); [[nodiscard]] bool created() const { return _handle != VK_NULL_HANDLE; } [[nodiscard]] VkFence get() const { return _handle.get(); } [[nodiscard]] DeviceTable deviceTable() const { return _deviceTable; } + [[nodiscard]] VkExternalFenceHandleTypeFlags exportTypes() const { return _exportTypes; } private: void resetState(); - [[nodiscard]] static VkFenceCreateInfo createInfo(VkFenceCreateFlags flags); - DeviceTable _deviceTable; move_ptr _handle = VK_NULL_HANDLE; + VkExternalFenceHandleTypeFlags _exportTypes = 0; public: Fence(Fence const& other) = delete; @@ -161,6 +178,10 @@ class Fence { namespace cvk { struct Fence::State { vk_not_null vkFence; + /** + * @brief handle types the fence was created to export + */ + VkExternalFenceHandleTypeFlags exportTypes = 0; }; } diff --git a/cvk/incl/cvk/utility/constant/os_constants.hpp b/cvk/incl/cvk/utility/constant/os_constants.hpp new file mode 100644 index 00000000..40eee19b --- /dev/null +++ b/cvk/incl/cvk/utility/constant/os_constants.hpp @@ -0,0 +1,31 @@ +#pragma once +#include + +#include + +#include +#include + +namespace cvk::constants { +/** + * @brief handle type of @ref Fence::extractOsHandle() + * @details windows: opaque win32 handle, posix: sync fd, pollable + */ +inline constexpr VkExternalFenceHandleTypeFlagBits OS_FENCE_HANDLE_TYPE = +#ifdef CTH_PLATFORM_WINDOWS + VK_EXTERNAL_FENCE_HANDLE_TYPE_OPAQUE_WIN32_BIT; +#else + VK_EXTERNAL_FENCE_HANDLE_TYPE_SYNC_FD_BIT; +#endif + +/** + * @brief device extensions that @ref Fence::extractOsHandle() requires + */ +inline constexpr std::array OS_FENCE_DEVICE_EXTENSIONS{ +#ifdef CTH_PLATFORM_WINDOWS + "VK_KHR_external_fence_win32", +#else + VK_KHR_EXTERNAL_FENCE_FD_EXTENSION_NAME, +#endif +}; +} diff --git a/cvk/src/render/sync/fence.cpp b/cvk/src/render/sync/fence.cpp index 187c282c..30970381 100644 --- a/cvk/src/render/sync/fence.cpp +++ b/cvk/src/render/sync/fence.cpp @@ -1,28 +1,49 @@ #include "cvk/render/sync/fence.hpp" #include "cvk/base/core.hpp" +#include "cvk/utility/constant/os_constants.hpp" #include "cvk/utility/vk_exceptions.hpp" #include +#ifdef CTH_PLATFORM_WINDOWS +#define NOMINMAX +#define WIN32_LEAN_AND_MEAN +#include + +#include +#endif + namespace cvk { Fence::Fence(Core const& core) : _deviceTable{core.deviceTable()} {} Fence::Fence(Core const& core, State const& state) : Fence{core} { wrap(state); } -Fence::Fence(Core const& core, VkFenceCreateFlags flags) : Fence{core} { create(flags); } +Fence::Fence(Core const& core, VkFenceCreateFlags flags, VkExternalFenceHandleTypeFlags exportTypes) : Fence{core} { + create(flags, exportTypes); +} void Fence::wrap(State const& state) { optDestroy(); _handle = state.vkFence.get(); + _exportTypes = state.exportTypes; } -void Fence::create(VkFenceCreateFlags flags) { +void Fence::create(VkFenceCreateFlags flags, VkExternalFenceHandleTypeFlags exportTypes) { optDestroy(); - auto const info = createInfo(flags); + VkExportFenceCreateInfo const exportInfo{ + .sType = VK_STRUCTURE_TYPE_EXPORT_FENCE_CREATE_INFO, + .pNext = nullptr, + .handleTypes = exportTypes, + }; + VkFenceCreateInfo const info{ + .sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO, + .pNext = exportTypes != 0 ? &exportInfo : nullptr, + .flags = flags, + }; VkFence ptr = VK_NULL_HANDLE; auto const result = _deviceTable->vkCreateFence(_deviceTable.device(), &info, nullptr, &ptr); @@ -33,6 +54,7 @@ void Fence::create(VkFenceCreateFlags flags) { } _handle = ptr; + _exportTypes = exportTypes; } void Fence::destroy() { @@ -46,7 +68,7 @@ void Fence::destroy() { Fence::State Fence::release() { debug_check(*this); - State const state{_handle.get()}; + State const state{_handle.get(), _exportTypes}; resetState(); return state; } @@ -100,19 +122,52 @@ void Fence::waitReset() const { reset(); } +cth::os::unique_native_handle Fence::extractOsHandle() const { + debug_check(*this); + CTH_CRITICAL( + (_exportTypes & constants::OS_FENCE_HANDLE_TYPE) == 0, + "fence must be created with constants::OS_FENCE_HANDLE_TYPE in the export types" + ) {} + +#ifdef CTH_PLATFORM_WINDOWS + // volk loads platform functions only if it was built with the platform define + auto const getHandle = reinterpret_cast( + vkGetDeviceProcAddr(_deviceTable.device(), "vkGetFenceWin32HandleKHR") + ); + CVK_STABLE_THROW(getHandle == nullptr, "vkGetFenceWin32HandleKHR not available") {} + + VkFenceGetWin32HandleInfoKHR const info{ + .sType = VK_STRUCTURE_TYPE_FENCE_GET_WIN32_HANDLE_INFO_KHR, + .pNext = nullptr, + .fence = _handle.get(), + .handleType = constants::OS_FENCE_HANDLE_TYPE, + }; + HANDLE handle = nullptr; + auto const result = getHandle(_deviceTable.device(), &info, &handle); +#else + VkFenceGetFdInfoKHR const info{ + .sType = VK_STRUCTURE_TYPE_FENCE_GET_FD_INFO_KHR, + .pNext = nullptr, + .fence = _handle.get(), + .handleType = constants::OS_FENCE_HANDLE_TYPE, + }; + int handle = cth::os::invalid_handle(); + auto const result = _deviceTable->vkGetFenceFdKHR(_deviceTable.device(), &info, &handle); +#endif + + CVK_RESULT_STABLE_THROW(result != VK_SUCCESS, result, "failed to export the fence") {} + + return cth::os::unique_native_handle{handle}; +} + void Fence::destroy(DeviceTable table, VkFence vkFence) { CTH_WARN(vkFence == VK_NULL_HANDLE, "vkFence invalid (VK_NULL_HANDLE)") {} table->vkDestroyFence(table.device(), vkFence, nullptr); } -void Fence::resetState() { _handle = VK_NULL_HANDLE; } - -VkFenceCreateInfo Fence::createInfo(VkFenceCreateFlags flags) { - return VkFenceCreateInfo{ - .sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO, - .pNext = nullptr, - .flags = flags, - }; +void Fence::resetState() { + _handle = VK_NULL_HANDLE; + _exportTypes = 0; } } diff --git a/cvk/tests/src/render/sync/fence_test.cpp b/cvk/tests/src/render/sync/fence_test.cpp index 19186573..31ae507b 100644 --- a/cvk/tests/src/render/sync/fence_test.cpp +++ b/cvk/tests/src/render/sync/fence_test.cpp @@ -1,7 +1,16 @@ #include "context.hpp" #include "test.hpp" +#include "cvk/base/physical_device.hpp" #include "cvk/render/sync/fence.hpp" +#include "cvk/utility/constant/os_constants.hpp" +#include "cvk/utility/vk_exceptions.hpp" + +#include + +#include +#include +#include namespace cvk { @@ -17,6 +26,30 @@ namespace { .signalSemaphoreInfoCount = 0, .pSignalSemaphoreInfos = nullptr, }; + + Core::Config os_fence_config() { + auto config = test::Context::CONFIG; + config.requiredExtensions.append_range( + constants::OS_FENCE_DEVICE_EXTENSIONS | std::views::transform( + [](std::string_view extension) { return std::string{extension}; } + ) + ); + return config; + } + + /** + * @return the device can export @ref constants::OS_FENCE_HANDLE_TYPE + */ + bool os_fence_exportable(Core const& core) { + VkPhysicalDeviceExternalFenceInfo const info{ + .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_EXTERNAL_FENCE_INFO, + .pNext = nullptr, + .handleType = constants::OS_FENCE_HANDLE_TYPE, + }; + VkExternalFenceProperties properties{.sType = VK_STRUCTURE_TYPE_EXTERNAL_FENCE_PROPERTIES, .pNext = nullptr}; + vkGetPhysicalDeviceExternalFenceProperties(core.vkPhysicalDevice(), &info, &properties); + return (properties.externalFenceFeatures & VK_EXTERNAL_FENCE_FEATURE_EXPORTABLE_BIT) != 0; + } } CVK_TEST(Fence, base_constructor_does_not_create) { @@ -103,4 +136,51 @@ CVK_TEST(Fence, move_assignment_destroys_the_old_fence) { EXPECT_FALSE(other.created()); EXPECT_TRUE(fence.signaled()); } + +CVK_TEST(Fence, extract_os_handle_waits_for_the_submit) { + std::optional context{}; + try { context.emplace(os_fence_config()); } catch(cvk_exception const&) { GTEST_SKIP() << "no os fence extension"; } + if(!os_fence_exportable(context->core)) GTEST_SKIP() << "os fence handle type not exportable"; + + Fence const fence{context->core, 0, constants::OS_FENCE_HANDLE_TYPE}; + EXPECT_EQ(fence.exportTypes(), constants::OS_FENCE_HANDLE_TYPE); + + auto const& queue = context->core.queue(0); + queue.raw_submit(EMPTY_SUBMIT, fence.get()); + + auto const handle = fence.extractOsHandle(); + if(handle) EXPECT_EQ(cth::os::wait(handle.get(), std::chrono::seconds{5}), cth::os::wait_result::WAITED); + +#ifndef CTH_PLATFORM_WINDOWS + // sync fd export resets the fence + EXPECT_FALSE(fence.signaled()); +#endif + + queue.wait(); +} + +CVK_TEST(Fence, extract_os_handle_of_a_signaled_fence) { + std::optional context{}; + try { context.emplace(os_fence_config()); } catch(cvk_exception const&) { GTEST_SKIP() << "no os fence extension"; } + if(!os_fence_exportable(context->core)) GTEST_SKIP() << "os fence handle type not exportable"; + + Fence const fence{context->core, VK_FENCE_CREATE_SIGNALED_BIT, constants::OS_FENCE_HANDLE_TYPE}; + + auto const handle = fence.extractOsHandle(); + + // invalid handle -> already signaled + EXPECT_TRUE(!handle || cth::os::check_ready(handle.get())); +} + +CVK_TEST(Fence, release_and_wrap_keep_the_export_types) { + test::Context const context{}; + Fence fence{context.core, 0, constants::OS_FENCE_HANDLE_TYPE}; + + auto const state = fence.release(); + EXPECT_EQ(state.exportTypes, constants::OS_FENCE_HANDLE_TYPE); + EXPECT_EQ(fence.exportTypes(), 0); + + Fence const wrapped{context.core, state}; + EXPECT_EQ(wrapped.exportTypes(), constants::OS_FENCE_HANDLE_TYPE); +} } From 18a063ad2f8263def5ce0a5f7d95eca93b013f94 Mon Sep 17 00:00:00 2001 From: lukas <89145039+codethinki@users.noreply.github.com> Date: Fri, 16 Oct 2026 15:41:42 +0200 Subject: [PATCH 019/135] cvk: shader runtime compile and derivative pipelines, fixed The port had dropped both. Restored and fixed: - Shader::compile() and the compiling constructor run any glslc compatible compiler, linux and windows (legacy: windows only through cth::win::cmd::hidden) - a failed compile throws with the compiler output, other compiler output is a warning (legacy aborted the process on any output, warnings included) - the log goes to the temp directory (legacy: shader_compile_log.txt in the working directory), no npos substr parsing - derivative pipelines take the layout of the parent and add VK_PIPELINE_CREATE_DERIVATIVE_BIT (legacy left the layout null and never set the flag), PipelineConfig::flags for ALLOW_DERIVATIVES --- cvk/incl/cvk/render/pipeline/pipeline.hpp | 24 ++++++- .../cvk/render/pipeline/pipeline_config.hpp | 5 ++ .../cvk/render/pipeline/shader/shader.hpp | 33 +++++++++ cvk/src/render/pipeline/pipeline.cpp | 21 +++++- cvk/src/render/pipeline/shader/shader.cpp | 72 +++++++++++++++++++ cvk/tests/CMakeLists.txt | 2 + .../src/render/pipeline/pipeline_test.cpp | 18 +++++ .../render/pipeline/shader/shader_test.cpp | 38 ++++++++++ cvk/tests/src/test.hpp | 6 ++ 9 files changed, 216 insertions(+), 3 deletions(-) diff --git a/cvk/incl/cvk/render/pipeline/pipeline.hpp b/cvk/incl/cvk/render/pipeline/pipeline.hpp index efcb7348..8eac9ab5 100644 --- a/cvk/incl/cvk/render/pipeline/pipeline.hpp +++ b/cvk/incl/cvk/render/pipeline/pipeline.hpp @@ -47,6 +47,21 @@ class Pipeline { */ explicit Pipeline(Core const&, PipelineLayout const&, Config, create_t); + /** + * @brief base constructor of a derivative pipeline, copies the handle and the layout of the parent + * @pre @ref Core::created() + * @pre @ref created() of the parent, with VK_PIPELINE_CREATE_ALLOW_DERIVATIVES_BIT in its flags + */ + Pipeline(Core const&, Pipeline const& parent, Config); + + /** + * @brief constructs and creates a derivative pipeline + * @details calls: + * - @ref Pipeline(Core const&, Pipeline const&, Config) + * - @ref create() + */ + explicit Pipeline(Core const&, Pipeline const& parent, Config, create_t); + /** * @note calls @ref optDestroy() */ @@ -62,7 +77,8 @@ class Pipeline { /** * @brief creates the pipeline - * @pre the layout and the shader modules of the config are not destroyed + * @details derivative pipeline -> adds VK_PIPELINE_CREATE_DERIVATIVE_BIT + * @pre the layout, the parent and the shader modules of the config are not destroyed * @post @ref created() * @note calls @ref optDestroy() * @throws cvk::vk_result_exception result of vkCreateGraphicsPipelines() @@ -92,6 +108,11 @@ class Pipeline { [[nodiscard]] bool created() const { return _handle != VK_NULL_HANDLE; } [[nodiscard]] VkPipeline get() const { return _handle.get(); } [[nodiscard]] VkPipelineLayout layout() const { return _vkLayout.get(); } + + /** + * @return VK_NULL_HANDLE -> not a derivative pipeline + */ + [[nodiscard]] VkPipeline parent() const { return _vkParent; } [[nodiscard]] Config const& config() const { return _config; } [[nodiscard]] DeviceTable deviceTable() const { return _deviceTable; } @@ -100,6 +121,7 @@ class Pipeline { DeviceTable _deviceTable; vk_not_null _vkLayout; + VkPipeline _vkParent = VK_NULL_HANDLE; Config _config; move_ptr _handle = VK_NULL_HANDLE; diff --git a/cvk/incl/cvk/render/pipeline/pipeline_config.hpp b/cvk/incl/cvk/render/pipeline/pipeline_config.hpp index aa6430ff..e5d40f59 100644 --- a/cvk/incl/cvk/render/pipeline/pipeline_config.hpp +++ b/cvk/incl/cvk/render/pipeline/pipeline_config.hpp @@ -51,6 +51,11 @@ struct PipelineConfig { */ PipelineConfig& removeStage(VkShaderStageFlagBits stage); + /** + * @brief VK_PIPELINE_CREATE_ALLOW_DERIVATIVES_BIT -> the pipeline can be a parent + */ + VkPipelineCreateFlags flags = 0; + std::vector stages{}; std::vector vertexBindings{}; diff --git a/cvk/incl/cvk/render/pipeline/shader/shader.hpp b/cvk/incl/cvk/render/pipeline/shader/shader.hpp index e9543aac..cfe32b35 100644 --- a/cvk/incl/cvk/render/pipeline/shader/shader.hpp +++ b/cvk/incl/cvk/render/pipeline/shader/shader.hpp @@ -10,6 +10,7 @@ #include #include #include +#include #include @@ -25,6 +26,8 @@ class Shader { struct State; using path_t = std::filesystem::path; + static constexpr std::string_view DEFAULT_COMPILER = "glslc"; + /** * @brief base constructor * @param stage of the shader @@ -56,6 +59,22 @@ class Shader { */ Shader(Core const&, VkShaderStageFlagBits stage, path_t const& spvPath); + /** + * @brief constructs, compiles the glsl file, loads and creates + * @details for development, release builds compile shaders at build time + * calls: + * - @ref compile() + * - @ref Shader(Core const&, VkShaderStageFlagBits, path_t const&) + * @note warns in release builds + */ + Shader( + Core const&, + VkShaderStageFlagBits stage, + path_t const& spvPath, + path_t const& glslPath, + path_t const& compilerPath = DEFAULT_COMPILER + ); + /** * @note calls @ref optDestroy() */ @@ -120,6 +139,20 @@ class Shader { */ [[nodiscard]] static std::vector load(path_t const& spvPath); + /** + * @brief compiles a glsl file to a SPIR-V file + * @param compilerPath glslc compatible compiler + * @param flags for the compiler + * @details compiler output without error -> logged as warning + * @throws cvk::cvk_exception reason: glsl file missing, compiler failed, details: compiler output + */ + static void compile( + path_t const& glslPath, + path_t const& spvPath, + path_t const& compilerPath = DEFAULT_COMPILER, + std::string_view flags = "-O" + ); + static void destroy(DeviceTable table, VkShaderModule); [[nodiscard]] bool created() const { return _handle != VK_NULL_HANDLE; } diff --git a/cvk/src/render/pipeline/pipeline.cpp b/cvk/src/render/pipeline/pipeline.cpp index a015e57a..2fcdea1a 100644 --- a/cvk/src/render/pipeline/pipeline.cpp +++ b/cvk/src/render/pipeline/pipeline.cpp @@ -22,6 +22,23 @@ Pipeline::Pipeline(Core const& core, PipelineLayout const& layout, Config config std::move(config) } { create(); } +Pipeline::Pipeline(Core const& core, Pipeline const& parent, Config config) : _deviceTable{core.deviceTable()}, + _vkLayout{parent.layout()}, + _vkParent{parent.get()}, + _config{std::move(config)} { + debug_check(parent); + CTH_CRITICAL( + (parent.config().flags & VK_PIPELINE_CREATE_ALLOW_DERIVATIVES_BIT) == 0, + "parent must allow derivatives (VK_PIPELINE_CREATE_ALLOW_DERIVATIVES_BIT)" + ) {} +} + +Pipeline::Pipeline(Core const& core, Pipeline const& parent, Config config, create_t) : Pipeline{ + core, + parent, + std::move(config) +} { create(); } + void Pipeline::wrap(State const& state) { optDestroy(); @@ -83,7 +100,7 @@ void Pipeline::create() { VkGraphicsPipelineCreateInfo const info{ .sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO, .pNext = &rendering, - .flags = 0, + .flags = config.flags | (_vkParent != VK_NULL_HANDLE ? VK_PIPELINE_CREATE_DERIVATIVE_BIT : 0u), .stageCount = static_cast(config.stages.size()), .pStages = config.stages.data(), .pVertexInputState = &vertexInput, @@ -98,7 +115,7 @@ void Pipeline::create() { .layout = _vkLayout.get(), .renderPass = VK_NULL_HANDLE, .subpass = 0, - .basePipelineHandle = VK_NULL_HANDLE, + .basePipelineHandle = _vkParent, .basePipelineIndex = -1, }; diff --git a/cvk/src/render/pipeline/shader/shader.cpp b/cvk/src/render/pipeline/shader/shader.cpp index 54dc0fd6..db4996d4 100644 --- a/cvk/src/render/pipeline/shader/shader.cpp +++ b/cvk/src/render/pipeline/shader/shader.cpp @@ -1,8 +1,14 @@ #include "cvk/render/pipeline/shader/shader.hpp" #include "cvk/base/core.hpp" +#include "cvk/utility/constants.hpp" #include "cvk/utility/vk_exceptions.hpp" +#include +#include + +#include +#include #include @@ -21,6 +27,19 @@ Shader::Shader(Core const& core, VkShaderStageFlagBits stage, path_t const& spvP create(spvPath); } +Shader::Shader( + Core const& core, + VkShaderStageFlagBits stage, + path_t const& spvPath, + path_t const& glslPath, + path_t const& compilerPath +) : Shader{core, stage} { + CTH_STABLE_WARN(!constants::DEBUG_MODE, "compiles shaders at run time, use this in debug builds only") {} + + compile(glslPath, spvPath, compilerPath); + create(spvPath); +} + void Shader::wrap(State const& state) { optDestroy(); @@ -108,6 +127,59 @@ std::vector Shader::load(path_t const& spvPath) { return spv; } +void Shader::compile( + path_t const& glslPath, + path_t const& spvPath, + path_t const& compilerPath, + std::string_view flags +) { + CVK_STABLE_THROW(!std::filesystem::exists(glslPath), "glsl file does not exist") { + details->add("file: {}", glslPath.string()); + } + +#ifdef CTH_PLATFORM_WINDOWS + constexpr std::string_view nullDevice = "NUL"; +#else + constexpr std::string_view nullDevice = "/dev/null"; +#endif + auto const logName = std::format("cvk_{}.log", spvPath.filename().string()); + auto const logPath = std::filesystem::temp_directory_path() / logName; + + auto command = std::format( + R"("{}" {} -c "{}" -o "{}" >{} 2>"{}")", + compilerPath.string(), + flags, + glslPath.string(), + spvPath.string(), + nullDevice, + logPath.string() + ); +#ifdef CTH_PLATFORM_WINDOWS + // cmd.exe /c removes the outer quotes of a command that starts with a quote + command = std::format("\"{}\"", command); +#endif + + int const result = std::system(command.c_str()); + + std::vector output{}; + if(std::filesystem::exists(logPath)) { + output = cth::io::file::chop(logPath); + std::erase_if(output, [](std::string const& line) { return line.empty(); }); + std::filesystem::remove(logPath); + } + + CVK_STABLE_THROW(result != 0, "shader compilation failed") { + details->add("file: {}", glslPath.string()); + details->add("command: {}", command); + for(auto const& line : output) details->add("{}", line); + } + + CTH_STABLE_WARN(!output.empty(), "shader compiler output") { + details->add("file: {}", glslPath.string()); + for(auto const& line : output) details->add("{}", line); + } +} + void Shader::destroy(DeviceTable table, VkShaderModule vkShader) { CTH_WARN(vkShader == VK_NULL_HANDLE, "vkShader invalid (VK_NULL_HANDLE)") {} diff --git a/cvk/tests/CMakeLists.txt b/cvk/tests/CMakeLists.txt index fb077905..7063fcdb 100644 --- a/cvk/tests/CMakeLists.txt +++ b/cvk/tests/CMakeLists.txt @@ -9,6 +9,8 @@ target_include_directories( PRIVATE src ) +target_compile_definitions(cvk_tests PRIVATE CVK_TEST_SHADER_DIR="${CMAKE_CURRENT_SOURCE_DIR}/shaders") + find_package(GTest CONFIG REQUIRED) target_link_libraries(cvk_tests PRIVATE GTest::gtest_main cvk) diff --git a/cvk/tests/src/render/pipeline/pipeline_test.cpp b/cvk/tests/src/render/pipeline/pipeline_test.cpp index aee3ff69..49449bb6 100644 --- a/cvk/tests/src/render/pipeline/pipeline_test.cpp +++ b/cvk/tests/src/render/pipeline/pipeline_test.cpp @@ -147,4 +147,22 @@ CVK_TEST(Pipeline, move_assignment_destroys_the_old_pipeline) { EXPECT_EQ(pipeline.get(), vkPipeline); EXPECT_FALSE(other.created()); } + +CVK_TEST(Pipeline, derivative) { + test::Context const context{}; + Triangle const triangle{context.core}; + + auto parentConfig = triangle.config(); + parentConfig.flags = VK_PIPELINE_CREATE_ALLOW_DERIVATIVES_BIT; + Pipeline const parent{context.core, triangle.layout, std::move(parentConfig), create}; + + auto childConfig = triangle.config(); + childConfig.rasterization.cullMode = VK_CULL_MODE_NONE; + Pipeline const child{context.core, parent, std::move(childConfig), create}; + + ASSERT_TRUE(child.created()); + EXPECT_EQ(child.parent(), parent.get()); + EXPECT_EQ(child.layout(), triangle.layout.get()); + EXPECT_EQ(parent.parent(), VK_NULL_HANDLE); +} } diff --git a/cvk/tests/src/render/pipeline/shader/shader_test.cpp b/cvk/tests/src/render/pipeline/shader/shader_test.cpp index 7ffd408b..205a7ed2 100644 --- a/cvk/tests/src/render/pipeline/shader/shader_test.cpp +++ b/cvk/tests/src/render/pipeline/shader/shader_test.cpp @@ -5,9 +5,16 @@ #include "cvk/render/pipeline/shader/shader.hpp" #include "cvk/utility/vk_exceptions.hpp" +#include #include #include +#ifdef _WIN32 +#define CVK_NULL_DEVICE "NUL" +#else +#define CVK_NULL_DEVICE "/dev/null" +#endif + namespace cvk { namespace { @@ -100,4 +107,35 @@ CVK_TEST(Shader, move_assignment_destroys_the_old_shader) { EXPECT_EQ(shader.stage(), VK_SHADER_STAGE_FRAGMENT_BIT); EXPECT_FALSE(other.created()); } + +CVK_TEST(Shader, compile_and_create) { + if(std::system("glslc --version >" CVK_NULL_DEVICE " 2>&1") != 0) GTEST_SKIP() << "glslc not found"; + test::Context const context{}; + auto const spvPath = std::filesystem::temp_directory_path() / "cvk_test_compiled.vert.spv"; + + Shader const shader{context.core, VK_SHADER_STAGE_VERTEX_BIT, spvPath, test::SHADER_DIR / "triangle.vert"}; + + EXPECT_TRUE(shader.created()); + EXPECT_TRUE(std::filesystem::exists(spvPath)); + + std::filesystem::remove(spvPath); +} + +CVK_TEST(Shader, compile_error_throws) { + if(std::system("glslc --version >" CVK_NULL_DEVICE " 2>&1") != 0) GTEST_SKIP() << "glslc not found"; + constexpr std::string_view broken = "#version 450\nvoid main() { undeclared = 1; }\n"; + auto const glslPath = write_temp("cvk_test_broken.vert", broken); + auto const spvPath = std::filesystem::temp_directory_path() / "cvk_test_broken.vert.spv"; + + EXPECT_THROW(Shader::compile(glslPath, spvPath), cvk_exception); + + std::filesystem::remove(glslPath); +} + +CVK_TEST(Shader, compile_missing_glsl_throws) { + auto const missing = std::filesystem::temp_directory_path() / "cvk_test_missing.vert"; + std::filesystem::remove(missing); + + EXPECT_THROW(Shader::compile(missing, missing.string() + ".spv"), cvk_exception); +} } diff --git a/cvk/tests/src/test.hpp b/cvk/tests/src/test.hpp index 326cc44b..d2c4a67a 100644 --- a/cvk/tests/src/test.hpp +++ b/cvk/tests/src/test.hpp @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include #include @@ -11,4 +12,9 @@ namespace cvk::test { constexpr std::string_view APP_NAME = "cvk_tests"; inline std::vector const NO_EXTENSIONS{}; + +/** + * @brief glsl sources of the test shaders + */ +inline std::filesystem::path const SHADER_DIR{CVK_TEST_SHADER_DIR}; } From 4da1c1d36db6041998ef79a6698b9b244466b2f2 Mon Sep 17 00:00:00 2001 From: lukas <89145039+codethinki@users.noreply.github.com> Date: Fri, 16 Oct 2026 15:49:04 +0200 Subject: [PATCH 020/135] cvk: objects store the core again, parents do not move Rule in AGENTS.md: a parent does not move and is not destroyed while objects depend on it. Fence, Semaphore, TimelineSemaphore, Shader, DescriptorSetLayout, PipelineLayout, Pipeline and Sampler store not_null like legacy (they stored core.deviceTable() for core moves), core() replaces deviceTable(), base constructors check Core::debug_check. --- AGENTS.md | 3 ++- cvk/incl/cvk/data/img/sampler.hpp | 4 ++-- .../pipeline/layout/descriptor_set_layout.hpp | 4 ++-- .../render/pipeline/layout/pipeline_layout.hpp | 4 ++-- cvk/incl/cvk/render/pipeline/pipeline.hpp | 4 ++-- cvk/incl/cvk/render/pipeline/shader/shader.hpp | 4 ++-- cvk/incl/cvk/render/sync/fence.hpp | 4 ++-- cvk/incl/cvk/render/sync/semaphore.hpp | 4 ++-- .../cvk/render/sync/timeline_semaphore.hpp | 2 +- cvk/src/data/img/sampler.cpp | 7 ++++--- .../pipeline/layout/descriptor_set_layout.cpp | 8 ++++---- .../render/pipeline/layout/pipeline_layout.cpp | 7 ++++--- cvk/src/render/pipeline/pipeline.cpp | 13 +++++++------ cvk/src/render/pipeline/shader/shader.cpp | 8 +++++--- cvk/src/render/sync/fence.cpp | 18 +++++++++--------- cvk/src/render/sync/semaphore.cpp | 6 +++--- cvk/src/render/sync/timeline_semaphore.cpp | 7 ++++--- cvk/tests/src/render/sync/semaphore_test.cpp | 2 +- 18 files changed, 58 insertions(+), 51 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index d5f85123..1dc19e10 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -194,7 +194,8 @@ inline void Semaphore::debug_check(Semaphore const& semaphore) { - The destructor calls `optDestroy()`. - Vulkan has its own reset for some objects (`vkResetFences`, `vkResetCommandBuffer`). There the public `reset()` is the Vulkan operation and the private member reset is `resetState()`. -- Device objects take `Core const&` in the base constructor and store `core.deviceTable()`, not the core. The core can move, the device table stays valid. +- Device objects take `Core const&` in the base constructor and store `not_null _core`. +- A parent (instance, core, device, pool, ...) does not move and is not destroyed while objects depend on it. Code does not have to support it. - A failed `create()` calls `reset()` before it throws. - `release()` exists so that an owner can use an object it must not destroy (for example swapchain images). diff --git a/cvk/incl/cvk/data/img/sampler.hpp b/cvk/incl/cvk/data/img/sampler.hpp index afed29d5..33f08efb 100644 --- a/cvk/incl/cvk/data/img/sampler.hpp +++ b/cvk/incl/cvk/data/img/sampler.hpp @@ -88,7 +88,7 @@ class Sampler { [[nodiscard]] bool created() const { return _handle != VK_NULL_HANDLE; } [[nodiscard]] VkSampler get() const { return _handle.get(); } - [[nodiscard]] DeviceTable deviceTable() const { return _deviceTable; } + [[nodiscard]] Core const& core() const { return *_core; } /** * @return the config, with the clamped anisotropy @@ -98,7 +98,7 @@ class Sampler { private: void reset(); - DeviceTable _deviceTable; + not_null _core; Config _config; move_ptr _handle = VK_NULL_HANDLE; diff --git a/cvk/incl/cvk/render/pipeline/layout/descriptor_set_layout.hpp b/cvk/incl/cvk/render/pipeline/layout/descriptor_set_layout.hpp index 6a65fade..a3edbc08 100644 --- a/cvk/incl/cvk/render/pipeline/layout/descriptor_set_layout.hpp +++ b/cvk/incl/cvk/render/pipeline/layout/descriptor_set_layout.hpp @@ -88,7 +88,7 @@ class DescriptorSetLayout { [[nodiscard]] bool created() const { return _handle != VK_NULL_HANDLE; } [[nodiscard]] VkDescriptorSetLayout get() const { return _handle.get(); } - [[nodiscard]] DeviceTable deviceTable() const { return _deviceTable; } + [[nodiscard]] Core const& core() const { return *_core; } /** * @return bindings, sorted by binding number @@ -103,7 +103,7 @@ class DescriptorSetLayout { private: void reset(); - DeviceTable _deviceTable; + not_null _core; std::vector _bindings; move_ptr _handle = VK_NULL_HANDLE; diff --git a/cvk/incl/cvk/render/pipeline/layout/pipeline_layout.hpp b/cvk/incl/cvk/render/pipeline/layout/pipeline_layout.hpp index 803d7075..de0d33e8 100644 --- a/cvk/incl/cvk/render/pipeline/layout/pipeline_layout.hpp +++ b/cvk/incl/cvk/render/pipeline/layout/pipeline_layout.hpp @@ -90,12 +90,12 @@ class PipelineLayout { [[nodiscard]] bool created() const { return _handle != VK_NULL_HANDLE; } [[nodiscard]] VkPipelineLayout get() const { return _handle.get(); } - [[nodiscard]] DeviceTable deviceTable() const { return _deviceTable; } + [[nodiscard]] Core const& core() const { return *_core; } private: void reset(); - DeviceTable _deviceTable; + not_null _core; std::vector _vkSetLayouts; std::vector _pushConstantRanges; move_ptr _handle = VK_NULL_HANDLE; diff --git a/cvk/incl/cvk/render/pipeline/pipeline.hpp b/cvk/incl/cvk/render/pipeline/pipeline.hpp index 8eac9ab5..ba1bc6fe 100644 --- a/cvk/incl/cvk/render/pipeline/pipeline.hpp +++ b/cvk/incl/cvk/render/pipeline/pipeline.hpp @@ -114,12 +114,12 @@ class Pipeline { */ [[nodiscard]] VkPipeline parent() const { return _vkParent; } [[nodiscard]] Config const& config() const { return _config; } - [[nodiscard]] DeviceTable deviceTable() const { return _deviceTable; } + [[nodiscard]] Core const& core() const { return *_core; } private: void reset(); - DeviceTable _deviceTable; + not_null _core; vk_not_null _vkLayout; VkPipeline _vkParent = VK_NULL_HANDLE; Config _config; diff --git a/cvk/incl/cvk/render/pipeline/shader/shader.hpp b/cvk/incl/cvk/render/pipeline/shader/shader.hpp index cfe32b35..271d53c2 100644 --- a/cvk/incl/cvk/render/pipeline/shader/shader.hpp +++ b/cvk/incl/cvk/render/pipeline/shader/shader.hpp @@ -158,12 +158,12 @@ class Shader { [[nodiscard]] bool created() const { return _handle != VK_NULL_HANDLE; } [[nodiscard]] VkShaderModule get() const { return _handle.get(); } [[nodiscard]] VkShaderStageFlagBits stage() const { return _stage; } - [[nodiscard]] DeviceTable deviceTable() const { return _deviceTable; } + [[nodiscard]] Core const& core() const { return *_core; } private: void reset(); - DeviceTable _deviceTable; + not_null _core; VkShaderStageFlagBits _stage; move_ptr _handle = VK_NULL_HANDLE; diff --git a/cvk/incl/cvk/render/sync/fence.hpp b/cvk/incl/cvk/render/sync/fence.hpp index 23d157c3..2a377172 100644 --- a/cvk/incl/cvk/render/sync/fence.hpp +++ b/cvk/incl/cvk/render/sync/fence.hpp @@ -155,13 +155,13 @@ class Fence { [[nodiscard]] bool created() const { return _handle != VK_NULL_HANDLE; } [[nodiscard]] VkFence get() const { return _handle.get(); } - [[nodiscard]] DeviceTable deviceTable() const { return _deviceTable; } + [[nodiscard]] Core const& core() const { return *_core; } [[nodiscard]] VkExternalFenceHandleTypeFlags exportTypes() const { return _exportTypes; } private: void resetState(); - DeviceTable _deviceTable; + not_null _core; move_ptr _handle = VK_NULL_HANDLE; VkExternalFenceHandleTypeFlags _exportTypes = 0; diff --git a/cvk/incl/cvk/render/sync/semaphore.hpp b/cvk/incl/cvk/render/sync/semaphore.hpp index 3b6734ac..431a0789 100644 --- a/cvk/incl/cvk/render/sync/semaphore.hpp +++ b/cvk/incl/cvk/render/sync/semaphore.hpp @@ -83,7 +83,7 @@ class Semaphore { [[nodiscard]] bool created() const { return _handle != VK_NULL_HANDLE; } [[nodiscard]] VkSemaphore get() const { return _handle.get(); } - [[nodiscard]] DeviceTable deviceTable() const { return _deviceTable; } + [[nodiscard]] Core const& core() const { return *_core; } protected: /** @@ -97,7 +97,7 @@ class Semaphore { private: void reset(); - DeviceTable _deviceTable; + not_null _core; move_ptr _handle = VK_NULL_HANDLE; public: diff --git a/cvk/incl/cvk/render/sync/timeline_semaphore.hpp b/cvk/incl/cvk/render/sync/timeline_semaphore.hpp index 1247ee2f..a44cf5f5 100644 --- a/cvk/incl/cvk/render/sync/timeline_semaphore.hpp +++ b/cvk/incl/cvk/render/sync/timeline_semaphore.hpp @@ -113,8 +113,8 @@ class TimelineSemaphore : Semaphore { */ [[nodiscard]] uint64_t gpuValue() const; + using Semaphore::core; using Semaphore::created; - using Semaphore::deviceTable; using Semaphore::get; private: diff --git a/cvk/src/data/img/sampler.cpp b/cvk/src/data/img/sampler.cpp index 58934b7e..24aae488 100644 --- a/cvk/src/data/img/sampler.cpp +++ b/cvk/src/data/img/sampler.cpp @@ -8,7 +8,8 @@ namespace cvk { -Sampler::Sampler(Core const& core, Config const& config) : _deviceTable{core.deviceTable()}, _config{config} { +Sampler::Sampler(Core const& core, Config const& config) : _core{&core}, _config{config} { + Core::debug_check(core); _config.maxAnisotropy = std::min(_config.maxAnisotropy, core.physicalDevice().limits().maxSamplerAnisotropy); } @@ -28,7 +29,7 @@ void Sampler::create() { auto const info = _config.createInfo(); VkSampler ptr = VK_NULL_HANDLE; - auto const result = _deviceTable->vkCreateSampler(_deviceTable.device(), &info, nullptr, &ptr); + auto const result = _core->functions()->vkCreateSampler(_core->vkDevice(), &info, nullptr, &ptr); CTH_STABLE_ERR(result != VK_SUCCESS, "failed to create sampler") { reset(); @@ -41,7 +42,7 @@ void Sampler::create() { void Sampler::destroy() { debug_check(*this); - destroy(_deviceTable, _handle.get()); + destroy(_core->deviceTable(), _handle.get()); reset(); } diff --git a/cvk/src/render/pipeline/layout/descriptor_set_layout.cpp b/cvk/src/render/pipeline/layout/descriptor_set_layout.cpp index eaa77a90..19cc14ed 100644 --- a/cvk/src/render/pipeline/layout/descriptor_set_layout.cpp +++ b/cvk/src/render/pipeline/layout/descriptor_set_layout.cpp @@ -7,8 +7,8 @@ namespace cvk { -DescriptorSetLayout::DescriptorSetLayout(Core const& core, Config config) : _deviceTable{core.deviceTable()}, - _bindings{std::move(config.bindings)} {} +DescriptorSetLayout::DescriptorSetLayout(Core const& core, Config config) : _core{&core}, + _bindings{std::move(config.bindings)} { Core::debug_check(core); } DescriptorSetLayout::DescriptorSetLayout(Core const& core, Config config, State const& state) : DescriptorSetLayout{ core, @@ -38,7 +38,7 @@ void DescriptorSetLayout::create() { }; VkDescriptorSetLayout ptr = VK_NULL_HANDLE; - auto const result = _deviceTable->vkCreateDescriptorSetLayout(_deviceTable.device(), &info, nullptr, &ptr); + auto const result = _core->functions()->vkCreateDescriptorSetLayout(_core->vkDevice(), &info, nullptr, &ptr); CTH_STABLE_ERR(result != VK_SUCCESS, "failed to create descriptor set layout") { reset(); @@ -51,7 +51,7 @@ void DescriptorSetLayout::create() { void DescriptorSetLayout::destroy() { debug_check(*this); - destroy(_deviceTable, _handle.get()); + destroy(_core->deviceTable(), _handle.get()); reset(); } diff --git a/cvk/src/render/pipeline/layout/pipeline_layout.cpp b/cvk/src/render/pipeline/layout/pipeline_layout.cpp index bb171296..16aed6c7 100644 --- a/cvk/src/render/pipeline/layout/pipeline_layout.cpp +++ b/cvk/src/render/pipeline/layout/pipeline_layout.cpp @@ -9,8 +9,9 @@ namespace cvk { -PipelineLayout::PipelineLayout(Core const& core, Config const& config) : _deviceTable{core.deviceTable()}, +PipelineLayout::PipelineLayout(Core const& core, Config const& config) : _core{&core}, _pushConstantRanges{config.pushConstantRanges} { + Core::debug_check(core); auto const maxSets = core.physicalDevice().limits().maxBoundDescriptorSets; CTH_STABLE_ERR(config.setLayouts.size() > maxSets, "more set layouts than maxBoundDescriptorSets") { details->add("set layouts: {}, max: {}", config.setLayouts.size(), maxSets); @@ -53,7 +54,7 @@ void PipelineLayout::create() { }; VkPipelineLayout ptr = VK_NULL_HANDLE; - auto const result = _deviceTable->vkCreatePipelineLayout(_deviceTable.device(), &info, nullptr, &ptr); + auto const result = _core->functions()->vkCreatePipelineLayout(_core->vkDevice(), &info, nullptr, &ptr); CTH_STABLE_ERR(result != VK_SUCCESS, "failed to create pipeline layout") { reset(); @@ -66,7 +67,7 @@ void PipelineLayout::create() { void PipelineLayout::destroy() { debug_check(*this); - destroy(_deviceTable, _handle.get()); + destroy(_core->deviceTable(), _handle.get()); reset(); } diff --git a/cvk/src/render/pipeline/pipeline.cpp b/cvk/src/render/pipeline/pipeline.cpp index 2fcdea1a..31b612e1 100644 --- a/cvk/src/render/pipeline/pipeline.cpp +++ b/cvk/src/render/pipeline/pipeline.cpp @@ -6,9 +6,9 @@ namespace cvk { -Pipeline::Pipeline(Core const& core, PipelineLayout const& layout, Config config) : _deviceTable{core.deviceTable()}, +Pipeline::Pipeline(Core const& core, PipelineLayout const& layout, Config config) : _core{&core}, _vkLayout{layout.get()}, - _config{std::move(config)} {} + _config{std::move(config)} { Core::debug_check(core); } Pipeline::Pipeline(Core const& core, PipelineLayout const& layout, Config config, State const& state) : Pipeline{ core, @@ -22,10 +22,11 @@ Pipeline::Pipeline(Core const& core, PipelineLayout const& layout, Config config std::move(config) } { create(); } -Pipeline::Pipeline(Core const& core, Pipeline const& parent, Config config) : _deviceTable{core.deviceTable()}, +Pipeline::Pipeline(Core const& core, Pipeline const& parent, Config config) : _core{&core}, _vkLayout{parent.layout()}, _vkParent{parent.get()}, _config{std::move(config)} { + Core::debug_check(core); debug_check(parent); CTH_CRITICAL( (parent.config().flags & VK_PIPELINE_CREATE_ALLOW_DERIVATIVES_BIT) == 0, @@ -120,8 +121,8 @@ void Pipeline::create() { }; VkPipeline ptr = VK_NULL_HANDLE; - auto const result = _deviceTable->vkCreateGraphicsPipelines( - _deviceTable.device(), + auto const result = _core->functions()->vkCreateGraphicsPipelines( + _core->vkDevice(), VK_NULL_HANDLE, 1, &info, @@ -140,7 +141,7 @@ void Pipeline::create() { void Pipeline::destroy() { debug_check(*this); - destroy(_deviceTable, _handle.get()); + destroy(_core->deviceTable(), _handle.get()); reset(); } diff --git a/cvk/src/render/pipeline/shader/shader.cpp b/cvk/src/render/pipeline/shader/shader.cpp index db4996d4..e405210e 100644 --- a/cvk/src/render/pipeline/shader/shader.cpp +++ b/cvk/src/render/pipeline/shader/shader.cpp @@ -13,7 +13,9 @@ namespace cvk { -Shader::Shader(Core const& core, VkShaderStageFlagBits stage) : _deviceTable{core.deviceTable()}, _stage{stage} {} +Shader::Shader(Core const& core, VkShaderStageFlagBits stage) : _core{&core}, _stage{stage} { + Core::debug_check(core); +} Shader::Shader(Core const& core, VkShaderStageFlagBits stage, State const& state) : Shader{core, stage} { wrap(state); @@ -60,7 +62,7 @@ void Shader::create(std::span spv) { }; VkShaderModule ptr = VK_NULL_HANDLE; - auto const result = _deviceTable->vkCreateShaderModule(_deviceTable.device(), &info, nullptr, &ptr); + auto const result = _core->functions()->vkCreateShaderModule(_core->vkDevice(), &info, nullptr, &ptr); CTH_STABLE_ERR(result != VK_SUCCESS, "failed to create shader module") { reset(); @@ -78,7 +80,7 @@ void Shader::create(path_t const& spvPath) { void Shader::destroy() { debug_check(*this); - destroy(_deviceTable, _handle.get()); + destroy(_core->deviceTable(), _handle.get()); reset(); } diff --git a/cvk/src/render/sync/fence.cpp b/cvk/src/render/sync/fence.cpp index 30970381..61d678ad 100644 --- a/cvk/src/render/sync/fence.cpp +++ b/cvk/src/render/sync/fence.cpp @@ -16,7 +16,7 @@ namespace cvk { -Fence::Fence(Core const& core) : _deviceTable{core.deviceTable()} {} +Fence::Fence(Core const& core) : _core{&core} { Core::debug_check(core); } Fence::Fence(Core const& core, State const& state) : Fence{core} { wrap(state); } @@ -46,7 +46,7 @@ void Fence::create(VkFenceCreateFlags flags, VkExternalFenceHandleTypeFlags expo }; VkFence ptr = VK_NULL_HANDLE; - auto const result = _deviceTable->vkCreateFence(_deviceTable.device(), &info, nullptr, &ptr); + auto const result = _core->functions()->vkCreateFence(_core->vkDevice(), &info, nullptr, &ptr); CTH_STABLE_ERR(result != VK_SUCCESS, "failed to create fence") { resetState(); @@ -60,7 +60,7 @@ void Fence::create(VkFenceCreateFlags flags, VkExternalFenceHandleTypeFlags expo void Fence::destroy() { debug_check(*this); - destroy(_deviceTable, _handle.get()); + destroy(_core->deviceTable(), _handle.get()); resetState(); } @@ -76,7 +76,7 @@ Fence::State Fence::release() { VkResult Fence::status() const { debug_check(*this); - auto const result = _deviceTable->vkGetFenceStatus(_deviceTable.device(), _handle.get()); + auto const result = _core->functions()->vkGetFenceStatus(_core->vkDevice(), _handle.get()); CVK_RESULT_STABLE_THROW( result != VK_SUCCESS && result != VK_NOT_READY, @@ -91,7 +91,7 @@ void Fence::reset() const { debug_check(*this); auto const vkFence = _handle.get(); - auto const result = _deviceTable->vkResetFences(_deviceTable.device(), 1, &vkFence); + auto const result = _core->functions()->vkResetFences(_core->vkDevice(), 1, &vkFence); CVK_RESULT_STABLE_THROW(result != VK_SUCCESS, result, "failed to reset fence") {} } @@ -100,7 +100,7 @@ VkResult Fence::wait(uint64_t timeout) const { debug_check(*this); auto const vkFence = _handle.get(); - auto const result = _deviceTable->vkWaitForFences(_deviceTable.device(), 1, &vkFence, VK_TRUE, timeout); + auto const result = _core->functions()->vkWaitForFences(_core->vkDevice(), 1, &vkFence, VK_TRUE, timeout); CVK_RESULT_STABLE_THROW(result != VK_SUCCESS && result != VK_TIMEOUT, result, "failed to wait for fence") {} @@ -132,7 +132,7 @@ cth::os::unique_native_handle Fence::extractOsHandle() const { #ifdef CTH_PLATFORM_WINDOWS // volk loads platform functions only if it was built with the platform define auto const getHandle = reinterpret_cast( - vkGetDeviceProcAddr(_deviceTable.device(), "vkGetFenceWin32HandleKHR") + vkGetDeviceProcAddr(_core->vkDevice(), "vkGetFenceWin32HandleKHR") ); CVK_STABLE_THROW(getHandle == nullptr, "vkGetFenceWin32HandleKHR not available") {} @@ -143,7 +143,7 @@ cth::os::unique_native_handle Fence::extractOsHandle() const { .handleType = constants::OS_FENCE_HANDLE_TYPE, }; HANDLE handle = nullptr; - auto const result = getHandle(_deviceTable.device(), &info, &handle); + auto const result = getHandle(_core->vkDevice(), &info, &handle); #else VkFenceGetFdInfoKHR const info{ .sType = VK_STRUCTURE_TYPE_FENCE_GET_FD_INFO_KHR, @@ -152,7 +152,7 @@ cth::os::unique_native_handle Fence::extractOsHandle() const { .handleType = constants::OS_FENCE_HANDLE_TYPE, }; int handle = cth::os::invalid_handle(); - auto const result = _deviceTable->vkGetFenceFdKHR(_deviceTable.device(), &info, &handle); + auto const result = _core->functions()->vkGetFenceFdKHR(_core->vkDevice(), &info, &handle); #endif CVK_RESULT_STABLE_THROW(result != VK_SUCCESS, result, "failed to export the fence") {} diff --git a/cvk/src/render/sync/semaphore.cpp b/cvk/src/render/sync/semaphore.cpp index 95bb00ab..d181dc63 100644 --- a/cvk/src/render/sync/semaphore.cpp +++ b/cvk/src/render/sync/semaphore.cpp @@ -5,7 +5,7 @@ namespace cvk { -Semaphore::Semaphore(Core const& core) : _deviceTable{core.deviceTable()} {} +Semaphore::Semaphore(Core const& core) : _core{&core} { Core::debug_check(core); } Semaphore::Semaphore(Core const& core, State const& state) : Semaphore{core} { wrap(state); } @@ -29,7 +29,7 @@ void Semaphore::create() { void Semaphore::destroy() { debug_check(*this); - destroy(_deviceTable, _handle.get()); + destroy(_core->deviceTable(), _handle.get()); reset(); } @@ -52,7 +52,7 @@ void Semaphore::create(VkSemaphoreCreateInfo const& info) { optDestroy(); VkSemaphore ptr = VK_NULL_HANDLE; - auto const result = _deviceTable->vkCreateSemaphore(_deviceTable.device(), &info, nullptr, &ptr); + auto const result = _core->functions()->vkCreateSemaphore(_core->vkDevice(), &info, nullptr, &ptr); CTH_STABLE_ERR(result != VK_SUCCESS, "failed to create semaphore") { reset(); diff --git a/cvk/src/render/sync/timeline_semaphore.cpp b/cvk/src/render/sync/timeline_semaphore.cpp index 8c8db92b..123f1bc1 100644 --- a/cvk/src/render/sync/timeline_semaphore.cpp +++ b/cvk/src/render/sync/timeline_semaphore.cpp @@ -1,5 +1,6 @@ #include "cvk/render/sync/timeline_semaphore.hpp" +#include "cvk/base/core.hpp" #include "cvk/utility/vk_exceptions.hpp" #include @@ -44,7 +45,7 @@ TimelineSemaphore::State TimelineSemaphore::release() { uint64_t TimelineSemaphore::signal() { debug_check(*this); - auto const table = deviceTable(); + auto const table = core().deviceTable(); VkSemaphoreSignalInfo const info{ .sType = VK_STRUCTURE_TYPE_SEMAPHORE_SIGNAL_INFO, .pNext = nullptr, @@ -61,7 +62,7 @@ uint64_t TimelineSemaphore::signal() { VkResult TimelineSemaphore::wait(uint64_t timeout) const { debug_check(*this); - auto const table = deviceTable(); + auto const table = core().deviceTable(); auto const vkSemaphore = get(); uint64_t const value = _value; @@ -91,7 +92,7 @@ void TimelineSemaphore::wait() const { uint64_t TimelineSemaphore::gpuValue() const { debug_check(*this); - auto const table = deviceTable(); + auto const table = core().deviceTable(); uint64_t value = 0; auto const result = table->vkGetSemaphoreCounterValue(table.device(), get(), &value); diff --git a/cvk/tests/src/render/sync/semaphore_test.cpp b/cvk/tests/src/render/sync/semaphore_test.cpp index b36ab405..9bafe459 100644 --- a/cvk/tests/src/render/sync/semaphore_test.cpp +++ b/cvk/tests/src/render/sync/semaphore_test.cpp @@ -32,7 +32,7 @@ CVK_TEST(Semaphore, create_and_destroy) { Semaphore semaphore{context.core, create}; ASSERT_TRUE(semaphore.created()); - EXPECT_EQ(semaphore.deviceTable().device(), context.core.vkDevice()); + EXPECT_EQ(&semaphore.core(), &context.core); semaphore.destroy(); EXPECT_FALSE(semaphore.created()); From 5322bb4c2f27fec64d6e64ae20789724e2668adc Mon Sep 17 00:00:00 2001 From: lukas <89145039+codethinki@users.noreply.github.com> Date: Fri, 16 Oct 2026 16:12:10 +0200 Subject: [PATCH 021/135] cvk: unique_handle, unique_ptr with destroy context deleters - core_deleter (stores the core), instance_deleter (stores the instance), handle_deleter (no context) - default_deleter_t picks the deleter from the signature of T::destroy - custom deleters derive from them, Deleter::pointer for handles that are not pointers - unique_handle exposes get(), core(), instance(), deleter() --- cvk/incl/cvk/utility/unique_handle.hpp | 185 +++++++++++++ cvk/src/utility/unique_handle.cpp | 11 + cvk/tests/src/utility/unique_handle_test.cpp | 262 +++++++++++++++++++ 3 files changed, 458 insertions(+) create mode 100644 cvk/incl/cvk/utility/unique_handle.hpp create mode 100644 cvk/src/utility/unique_handle.cpp create mode 100644 cvk/tests/src/utility/unique_handle_test.cpp diff --git a/cvk/incl/cvk/utility/unique_handle.hpp b/cvk/incl/cvk/utility/unique_handle.hpp new file mode 100644 index 00000000..278df543 --- /dev/null +++ b/cvk/incl/cvk/utility/unique_handle.hpp @@ -0,0 +1,185 @@ +#pragma once +#include "cvk/base/device_table.hpp" +#include "cvk/utility/types.hpp" + +#include + +#include +#include +#include +#include + + +namespace cvk { +class Core; +class Instance; +} + +namespace cvk::dev { +[[nodiscard]] DeviceTable device_table(Core const& core); +[[nodiscard]] VkInstance vk_instance(Instance const& instance); +} + +namespace cvk { +/** + * @brief T::destroy(DeviceTable, Handle) exists + */ +template +concept core_destroyable = requires(DeviceTable table, Handle handle) { T::destroy(table, handle); }; + +/** + * @brief T::destroy(VkInstance, Handle) exists + */ +template +concept instance_destroyable = requires(VkInstance vkInstance, Handle handle) { T::destroy(vkInstance, handle); }; + +/** + * @brief T::destroy(Handle) exists + */ +template +concept handle_destroyable = requires(Handle handle) { T::destroy(handle); }; + + +/** + * @brief deleter of device objects, stores the core + * @details calls T::destroy(core.deviceTable(), handle), derive from it for destroy functions that need more + * @pre the core outlives the deleter + */ +template +class core_deleter { +public: + explicit core_deleter(Core const& core) : _core{&core} {} + + template + void operator()(Handle handle) const { T::destroy(dev::device_table(*_core), handle); } + + [[nodiscard]] Core const& core() const { return *_core; } + +private: + not_null _core; +}; + +/** + * @brief deleter of instance objects, stores the instance + * @details calls T::destroy(instance.get(), handle), derive from it for destroy functions that need more + * @pre the instance outlives the deleter + */ +template +class instance_deleter { +public: + explicit instance_deleter(Instance const& instance) : _instance{&instance} {} + + template + void operator()(Handle handle) const { T::destroy(dev::vk_instance(*_instance), handle); } + + [[nodiscard]] Instance const& instance() const { return *_instance; } + +private: + not_null _instance; +}; + +/** + * @brief deleter without context, calls T::destroy(handle) + */ +template +struct handle_deleter { + template + void operator()(Handle handle) const { T::destroy(handle); } +}; + +namespace dev { + /** + * @brief T::destroy fits no default deleter + */ + struct no_default_deleter {}; +} + +/** + * @brief deleter that fits the static destroy of T + * @details first match: + * - T::destroy(DeviceTable, Handle) -> @ref core_deleter + * - T::destroy(VkInstance, Handle) -> @ref instance_deleter + * - T::destroy(Handle) -> @ref handle_deleter + * @attention inside T: declare T::destroy before the member that uses this, else no deleter fits + */ +template +using default_deleter_t = std::conditional_t< + core_destroyable, + core_deleter, + std::conditional_t< + instance_destroyable, + instance_deleter, + std::conditional_t, handle_deleter, dev::no_default_deleter> + > +>; + + +/** + * @brief owning Vulkan handle, std::unique_ptr with a deleter that stores the destroy context + * @tparam T owner class, its static destroy picks the default deleter + * @tparam Handle Vulkan handle, or Deleter::pointer for handles that are not pointers + * @tparam Deleter custom deleters derive from @ref core_deleter, @ref instance_deleter or @ref handle_deleter + * @details move assignment destroys the old handle with the old deleter, then takes handle and deleter + */ +template> +class unique_handle { + static_assert( + !std::same_as, + "T::destroy fits no default deleter: declare it before the member or supply a deleter" + ); + + using ptr_t = std::unique_ptr, Deleter>; + static_assert(std::same_as, "Handle must be a pointer or Deleter::pointer"); + +public: + using handle_t = Handle; + using deleter_t = Deleter; + + /** + * @brief constructs the deleter, owns no handle + */ + template requires std::constructible_from + explicit unique_handle(Args&&... args) : _ptr{Handle{}, Deleter{std::forward(args)...}} {} + + ~unique_handle() = default; + + /** + * @brief destroys the owned handle, then owns @ref handle + */ + void reset(Handle handle = Handle{}) noexcept { _ptr.reset(handle); } + + /** + * @brief gives up the handle without destroying, keeps the deleter + */ + [[nodiscard]] Handle release() noexcept { return _ptr.release(); } + + [[nodiscard]] Handle get() const noexcept { return _ptr.get(); } + [[nodiscard]] explicit operator bool() const noexcept { return static_cast(_ptr); } + + [[nodiscard]] Deleter const& deleter() const noexcept { return _ptr.get_deleter(); } + [[nodiscard]] Deleter& deleter() noexcept { return _ptr.get_deleter(); } + + /** + * @pre the deleter has core() + */ + [[nodiscard]] decltype(auto) core() const requires requires(Deleter const& d) { d.core(); } { + return deleter().core(); + } + + /** + * @pre the deleter has instance() + */ + [[nodiscard]] decltype(auto) instance() const requires requires(Deleter const& d) { d.instance(); } { + return deleter().instance(); + } + +private: + ptr_t _ptr; + +public: + unique_handle(unique_handle const& other) = delete; + unique_handle(unique_handle&& other) noexcept = default; + unique_handle& operator=(unique_handle const& other) = delete; + unique_handle& operator=(unique_handle&& other) noexcept = default; +}; +} diff --git a/cvk/src/utility/unique_handle.cpp b/cvk/src/utility/unique_handle.cpp new file mode 100644 index 00000000..307739fb --- /dev/null +++ b/cvk/src/utility/unique_handle.cpp @@ -0,0 +1,11 @@ +#include "cvk/utility/unique_handle.hpp" + +#include "cvk/base/core.hpp" +#include "cvk/base/instance.hpp" + + +namespace cvk::dev { +DeviceTable device_table(Core const& core) { return core.deviceTable(); } + +VkInstance vk_instance(Instance const& instance) { return instance.get(); } +} diff --git a/cvk/tests/src/utility/unique_handle_test.cpp b/cvk/tests/src/utility/unique_handle_test.cpp new file mode 100644 index 00000000..4525caa8 --- /dev/null +++ b/cvk/tests/src/utility/unique_handle_test.cpp @@ -0,0 +1,262 @@ +#include "context.hpp" +#include "headless.hpp" +#include "test.hpp" +#include "validation.hpp" + +#include "cvk/base/device.hpp" +#include "cvk/base/instance.hpp" +#include "cvk/debug/debug_messenger.hpp" +#include "cvk/data/img/sampler.hpp" +#include "cvk/render/pipeline/layout/descriptor_set_layout.hpp" +#include "cvk/render/pipeline/layout/pipeline_layout.hpp" +#include "cvk/render/pipeline/pipeline.hpp" +#include "cvk/render/pipeline/shader/shader.hpp" +#include "cvk/render/sync/fence.hpp" +#include "cvk/render/sync/semaphore.hpp" +#include "cvk/render/sync/timeline_semaphore.hpp" +#include "cvk/surface/surface.hpp" +#include "cvk/utility/constants.hpp" +#include "cvk/utility/unique_handle.hpp" + +#include +#include + + +namespace cvk { +namespace { + VkFence create_vk_fence(Core const& core) { + VkFenceCreateInfo const info{.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO, .pNext = nullptr, .flags = 0}; + VkFence vkFence = VK_NULL_HANDLE; + [[maybe_unused]] auto const result = core.functions()->vkCreateFence(core.vkDevice(), &info, nullptr, &vkFence); + return vkFence; + } + + /** + * @brief owner with a destroy that needs the pool, like a command buffer + */ + struct PoolOwned { + static void destroy(DeviceTable table, VkCommandPool vkPool, VkCommandBuffer vkBuffer) { + table->vkFreeCommandBuffers(table.device(), vkPool, 1, &vkBuffer); + } + }; + + /** + * @brief custom deleter: derives from core_deleter, adds the pool + */ + struct pool_deleter : core_deleter { + pool_deleter(Core const& core, VkCommandPool vkPool) : core_deleter{core}, pool{vkPool} {} + + void operator()(VkCommandBuffer vkBuffer) const { + PoolOwned::destroy(dev::device_table(core()), pool, vkBuffer); + } + + VkCommandPool pool; + }; + + /** + * @brief two handles destroyed together, like a VMA image with its allocation + */ + struct pair_handle { + int* first = nullptr; + int* second = nullptr; + + pair_handle() = default; + pair_handle(std::nullptr_t) {} + pair_handle(int* first, int* second) : first{first}, second{second} {} + + // libstdc++ unique_ptr tests the pointer with if(ptr) + explicit operator bool() const { return first != nullptr; } + + friend bool operator==(pair_handle const&, pair_handle const&) = default; + friend bool operator==(pair_handle const& handle, std::nullptr_t) { return handle.first == nullptr; } + }; + + struct PairOwned { + static inline int destroyed = 0; + + static void destroy(pair_handle handle) { + delete handle.first; + delete handle.second; + ++destroyed; + } + }; + + struct pair_deleter : handle_deleter { + using pointer = pair_handle; + }; +} + +CVK_TEST(unique_handle, default_deleters_of_the_classes) { + static_assert(std::same_as, core_deleter>); + static_assert(std::same_as, core_deleter>); + static_assert( + std::same_as, core_deleter> + ); + static_assert(std::same_as, core_deleter>); + static_assert( + std::same_as, core_deleter> + ); + static_assert(std::same_as, core_deleter>); + static_assert(std::same_as, core_deleter>); + static_assert(std::same_as, core_deleter>); + + static_assert(std::same_as, instance_deleter>); + static_assert( + std::same_as, instance_deleter> + ); + + static_assert(std::same_as, handle_deleter>); + + // destroy(VkDevice, PFN_vkDestroyDevice) -> needs a custom deleter + static_assert(std::same_as, dev::no_default_deleter>); +} + +CVK_TEST(unique_handle, size) { + static_assert(sizeof(unique_handle) == sizeof(VkInstance)); + static_assert(sizeof(unique_handle) == sizeof(Core const*) + sizeof(VkFence)); + static_assert(sizeof(unique_handle) == sizeof(Instance const*) + sizeof(VkSurfaceKHR)); +} + +CVK_TEST(unique_handle, core_deleter) { + test::Context const context{}; + + unique_handle handle{context.core}; + EXPECT_FALSE(handle); + EXPECT_EQ(&handle.core(), &context.core); + + auto const vkFence = create_vk_fence(context.core); + handle.reset(vkFence); + ASSERT_TRUE(handle); + EXPECT_EQ(handle.get(), vkFence); + + handle.reset(); + EXPECT_FALSE(handle); +} + +CVK_TEST(unique_handle, move_assignment_destroys_the_old_handle) { + test::Context const context{}; + unique_handle handle{context.core}; + unique_handle other{context.core}; + handle.reset(create_vk_fence(context.core)); + auto const vkFence = create_vk_fence(context.core); + other.reset(vkFence); + + handle = std::move(other); + + EXPECT_EQ(handle.get(), vkFence); + EXPECT_FALSE(other); + EXPECT_EQ(&other.core(), &context.core); +} + +CVK_TEST(unique_handle, release_keeps_the_handle) { + test::Context const context{}; + unique_handle handle{context.core}; + auto const vkFence = create_vk_fence(context.core); + handle.reset(vkFence); + + EXPECT_EQ(handle.release(), vkFence); + EXPECT_FALSE(handle); + + Fence::destroy(context.core.deviceTable(), vkFence); +} + +CVK_TEST(unique_handle, instance_deleter) { + if constexpr(!constants::ENABLE_VALIDATION_LAYERS) GTEST_SKIP() << "needs the validation layers"; + + test::reset_validation_errors(); + { + Instance const instance{test::APP_NAME, test::headless_extensions(), test::counting_messenger()}; + auto surface = test::headless_surface(instance); + + unique_handle handle{instance}; + handle.reset(surface.release().vkSurface.get()); + + EXPECT_EQ(&handle.instance(), &instance); + } + EXPECT_EQ(test::validation_errors(), 0); +} + +CVK_TEST(unique_handle, handle_deleter) { + VkApplicationInfo const appInfo{ + .sType = VK_STRUCTURE_TYPE_APPLICATION_INFO, + .pNext = nullptr, + .pApplicationName = test::APP_NAME.data(), + .applicationVersion = 0, + .pEngineName = test::APP_NAME.data(), + .engineVersion = 0, + .apiVersion = VK_API_VERSION_1_3, + }; + VkInstanceCreateInfo const info{ + .sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO, + .pNext = nullptr, + .flags = 0, + .pApplicationInfo = &appInfo, + .enabledLayerCount = 0, + .ppEnabledLayerNames = nullptr, + .enabledExtensionCount = 0, + .ppEnabledExtensionNames = nullptr, + }; + VkInstance vkInstance = VK_NULL_HANDLE; + ASSERT_EQ(vkCreateInstance(&info, nullptr, &vkInstance), VK_SUCCESS); + + unique_handle handle{}; + handle.reset(vkInstance); + + EXPECT_EQ(handle.get(), vkInstance); +} + +CVK_TEST(unique_handle, custom_deleter) { + test::Context const context{}; + auto const table = context.core.deviceTable(); + + VkCommandPoolCreateInfo const poolInfo{ + .sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO, + .pNext = nullptr, + .flags = 0, + .queueFamilyIndex = context.core.queue(0).familyIndex(), + }; + VkCommandPool vkPool = VK_NULL_HANDLE; + ASSERT_EQ(table->vkCreateCommandPool(table.device(), &poolInfo, nullptr, &vkPool), VK_SUCCESS); + + { + unique_handle handle{context.core, vkPool}; + EXPECT_EQ(&handle.core(), &context.core); + EXPECT_EQ(handle.deleter().pool, vkPool); + + VkCommandBufferAllocateInfo const allocInfo{ + .sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO, + .pNext = nullptr, + .commandPool = vkPool, + .level = VK_COMMAND_BUFFER_LEVEL_PRIMARY, + .commandBufferCount = 1, + }; + VkCommandBuffer vkBuffer = VK_NULL_HANDLE; + ASSERT_EQ(table->vkAllocateCommandBuffers(table.device(), &allocInfo, &vkBuffer), VK_SUCCESS); + + handle.reset(vkBuffer); + EXPECT_EQ(handle.get(), vkBuffer); + } + + table->vkDestroyCommandPool(table.device(), vkPool, nullptr); +} + +CVK_TEST(unique_handle, non_pointer_handle) { + PairOwned::destroyed = 0; + { + unique_handle handle{}; + EXPECT_FALSE(handle); + + handle.reset(pair_handle{new int{1}, new int{2}}); + ASSERT_TRUE(handle); + EXPECT_EQ(*handle.get().second, 2); + + unique_handle other{}; + other.reset(pair_handle{new int{3}, new int{4}}); + + handle = std::move(other); + EXPECT_EQ(PairOwned::destroyed, 1); + EXPECT_EQ(*handle.get().first, 3); + } + EXPECT_EQ(PairOwned::destroyed, 2); +} +} From 24a7371ffb41670c0f9420aa0926602046734cd9 Mon Sep 17 00:00:00 2001 From: lukas <89145039+codethinki@users.noreply.github.com> Date: Fri, 16 Oct 2026 16:17:14 +0200 Subject: [PATCH 022/135] cvk: trial conversion to unique_handle One class per destroy shape: - Fence: default core_deleter - Surface: default instance_deleter - Instance: default handle_deleter, the messenger is declared before the handle - DebugMessenger: custom deleter, the instance comes with create(), not with the base constructor - Device: custom deleter, vkDestroyDevice comes from the function table All five move assign with = default. The instance move test counts validation errors now. --- cvk/incl/cvk/base/device.hpp | 19 ++++++++++++----- cvk/incl/cvk/base/instance.hpp | 9 ++++---- cvk/incl/cvk/debug/debug_messenger.hpp | 21 ++++++++++++------- cvk/incl/cvk/render/sync/fence.hpp | 11 +++++----- cvk/incl/cvk/surface/surface.hpp | 10 ++++----- cvk/src/base/device.cpp | 19 ++++++++++------- cvk/src/base/instance.cpp | 11 ++++------ cvk/src/debug/debug_messenger.cpp | 19 ++++++++--------- cvk/src/render/sync/fence.cpp | 29 ++++++++++++-------------- cvk/src/surface/surface.cpp | 10 ++++----- cvk/tests/src/base/instance_test.cpp | 21 ++++++++++++------- 11 files changed, 96 insertions(+), 83 deletions(-) diff --git a/cvk/incl/cvk/base/device.hpp b/cvk/incl/cvk/base/device.hpp index 9170b456..931e8791 100644 --- a/cvk/incl/cvk/base/device.hpp +++ b/cvk/incl/cvk/base/device.hpp @@ -4,11 +4,11 @@ #include "cvk/base/queue/queue.hpp" #include "cvk/utility/types.hpp" +#include "cvk/utility/unique_handle.hpp" #include #include -#include #include #include @@ -116,7 +116,7 @@ class Device { [[nodiscard]] Instance const& instance() const { return *_instance; } [[nodiscard]] PhysicalDevice const& physicalDevice() const { return *_physicalDevice; } - [[nodiscard]] bool created() const { return _handle != VK_NULL_HANDLE; } + [[nodiscard]] bool created() const { return static_cast(_handle); } private: void reset(); @@ -130,7 +130,7 @@ class Device { */ void createLogicalDevice(queue_families_queue_counts_t const& queueFamiliesQueueCounts); - void loadFunctionTable() const; + void loadFunctionTable(VkDevice vkDevice) const; /** * @brief retrieves the queues @@ -142,7 +142,16 @@ class Device { not_null _instance; not_null _physicalDevice; - move_ptr _handle = VK_NULL_HANDLE; + /** + * @brief vkDestroyDevice comes from the function table, it is loaded with @ref create() or @ref wrap() + */ + struct Deleter : handle_deleter { + PFN_vkDestroyDevice destroyFunction = nullptr; + + void operator()(VkDevice vkDevice) const { destroy(vkDevice, destroyFunction); } + }; + + unique_handle _handle; std::unique_ptr _functionTable = std::make_unique(); std::vector _queues{}; @@ -151,7 +160,7 @@ class Device { Device(Device const& other) = delete; Device(Device&& other) noexcept = default; Device& operator=(Device const& other) = delete; - Device& operator=(Device&& other) noexcept { return cth::reconstruct_at(*this, std::move(other)); } + Device& operator=(Device&& other) noexcept = default; static void debug_check(Device const&); static void debug_check_handle(vk_not_null); diff --git a/cvk/incl/cvk/base/instance.hpp b/cvk/incl/cvk/base/instance.hpp index 2f7c35db..be16c255 100644 --- a/cvk/incl/cvk/base/instance.hpp +++ b/cvk/incl/cvk/base/instance.hpp @@ -3,11 +3,11 @@ #include "cvk/debug/debug_messenger.hpp" #include "cvk/utility/constants.hpp" #include "cvk/utility/types.hpp" +#include "cvk/utility/unique_handle.hpp" #include #include -#include #include #include @@ -122,7 +122,7 @@ class Instance { static void destroy(VkInstance); - [[nodiscard]] bool created() const { return _handle != VK_NULL_HANDLE; } + [[nodiscard]] bool created() const { return static_cast(_handle); } [[nodiscard]] VkInstance get() const { return _handle.get(); } [[nodiscard]] std::vector availableExtensions() const { return _availableExt; } [[nodiscard]] std::vector requiredExtensions() const { return _requiredExt; } @@ -137,8 +137,9 @@ class Instance { std::vector _availableExt; std::vector _availableLayers{}; + // declared before the handle: move assignment destroys the old messenger before the old instance std::unique_ptr _debugMessenger = nullptr; - move_ptr _handle = VK_NULL_HANDLE; + unique_handle _handle; static void loadInstanceFunctions(vk_not_null); @@ -146,7 +147,7 @@ class Instance { Instance(Instance const& other) = delete; Instance& operator=(Instance const& other) = delete; Instance(Instance&& other) noexcept = default; - Instance& operator=(Instance&& other) noexcept { return cth::reconstruct_at(*this, std::move(other)); } + Instance& operator=(Instance&& other) noexcept = default; static void debug_check(Instance const&); static void debug_check_handle(vk_not_null); diff --git a/cvk/incl/cvk/debug/debug_messenger.hpp b/cvk/incl/cvk/debug/debug_messenger.hpp index 3e6ffcf2..018e18c9 100644 --- a/cvk/incl/cvk/debug/debug_messenger.hpp +++ b/cvk/incl/cvk/debug/debug_messenger.hpp @@ -1,12 +1,12 @@ #pragma once #include "cvk/utility/constants.hpp" #include "cvk/utility/types.hpp" +#include "cvk/utility/unique_handle.hpp" #include #include #include -#include namespace cvk { @@ -93,27 +93,32 @@ class DebugMessenger { [[nodiscard]] VkDebugUtilsMessengerCreateInfoEXT createInfo() const; }; - [[nodiscard]] bool created() const { return _handle != VK_NULL_HANDLE; } + [[nodiscard]] bool created() const { return static_cast(_handle); } [[nodiscard]] VkDebugUtilsMessengerEXT get() const { return _handle.get(); } [[nodiscard]] Config config() const { return _config; } protected: - /** - * @brief handle, not the wrapper: the instance can move - */ - VkInstance _vkInstance = VK_NULL_HANDLE; Config _config; private: + /** + * @brief the instance comes with @ref create(), not with the base constructor + */ + struct Deleter : handle_deleter { + VkInstance vkInstance = VK_NULL_HANDLE; + + void operator()(VkDebugUtilsMessengerEXT vkMessenger) const; + }; + void reset(); - move_ptr _handle = VK_NULL_HANDLE; + unique_handle _handle; public: DebugMessenger(DebugMessenger const& other) = delete; DebugMessenger(DebugMessenger&& other) noexcept = default; DebugMessenger& operator=(DebugMessenger const& other) = delete; - DebugMessenger& operator=(DebugMessenger&& other) noexcept { return cth::reconstruct_at(*this, std::move(other)); } + DebugMessenger& operator=(DebugMessenger&& other) noexcept = default; static void debug_check(DebugMessenger const&); }; diff --git a/cvk/incl/cvk/render/sync/fence.hpp b/cvk/incl/cvk/render/sync/fence.hpp index 2a377172..0b8023f7 100644 --- a/cvk/incl/cvk/render/sync/fence.hpp +++ b/cvk/incl/cvk/render/sync/fence.hpp @@ -1,12 +1,12 @@ #pragma once #include "cvk/base/device_table.hpp" #include "cvk/utility/types.hpp" +#include "cvk/utility/unique_handle.hpp" #include #include #include -#include #include @@ -153,23 +153,22 @@ class Fence { static void destroy(DeviceTable table, VkFence); - [[nodiscard]] bool created() const { return _handle != VK_NULL_HANDLE; } + [[nodiscard]] bool created() const { return static_cast(_handle); } [[nodiscard]] VkFence get() const { return _handle.get(); } - [[nodiscard]] Core const& core() const { return *_core; } + [[nodiscard]] Core const& core() const { return _handle.core(); } [[nodiscard]] VkExternalFenceHandleTypeFlags exportTypes() const { return _exportTypes; } private: void resetState(); - not_null _core; - move_ptr _handle = VK_NULL_HANDLE; + unique_handle _handle; VkExternalFenceHandleTypeFlags _exportTypes = 0; public: Fence(Fence const& other) = delete; Fence(Fence&& other) noexcept = default; Fence& operator=(Fence const& other) = delete; - Fence& operator=(Fence&& other) noexcept { return cth::reconstruct_at(*this, std::move(other)); } + Fence& operator=(Fence&& other) noexcept = default; static void debug_check(Fence const&); }; diff --git a/cvk/incl/cvk/surface/surface.hpp b/cvk/incl/cvk/surface/surface.hpp index cb68442a..4b8e3986 100644 --- a/cvk/incl/cvk/surface/surface.hpp +++ b/cvk/incl/cvk/surface/surface.hpp @@ -2,11 +2,11 @@ #include "surface_config.hpp" #include "cvk/utility/types.hpp" +#include "cvk/utility/unique_handle.hpp" #include #include -#include #include @@ -107,23 +107,21 @@ class Surface { static void destroy(vk_not_null, VkSurfaceKHR); - [[nodiscard]] bool created() const { return _handle != VK_NULL_HANDLE; } + [[nodiscard]] bool created() const { return static_cast(_handle); } [[nodiscard]] VkSurfaceKHR get() const { return _handle.get(); } [[nodiscard]] Config const& config() const { return _config; } private: void reset(); - not_null _instance; Config _config; - - move_ptr _handle = VK_NULL_HANDLE; + unique_handle _handle; public: Surface(Surface const& other) = delete; Surface(Surface&& other) noexcept = default; Surface& operator=(Surface const& other) = delete; - Surface& operator=(Surface&& other) noexcept { return cth::reconstruct_at(*this, std::move(other)); } + Surface& operator=(Surface&& other) noexcept = default; static void debug_check(Surface const&); static void debug_check_handle(vk_not_null); diff --git a/cvk/src/base/device.cpp b/cvk/src/base/device.cpp index 3b959ebf..8a8a1496 100644 --- a/cvk/src/base/device.cpp +++ b/cvk/src/base/device.cpp @@ -34,13 +34,14 @@ Device::Device( void Device::wrap(State state) { optDestroy(); - _handle = state.vkDevice.get(); - _functionTable = std::move(state.functionTable); if(_functionTable == nullptr) { _functionTable = std::make_unique(); - loadFunctionTable(); + loadFunctionTable(state.vkDevice.get()); } + + _handle.deleter().destroyFunction = _functionTable->vkDestroyDevice; + _handle.reset(state.vkDevice.get()); } void Device::create(Config const& config) { @@ -50,7 +51,6 @@ void Device::create(Config const& config) { auto const queueCounts = calcQueueFamiliesQueueCounts(familyIndices); createLogicalDevice(queueCounts); - loadFunctionTable(); createQueues(familyIndices); } @@ -58,7 +58,7 @@ void Device::create(Config const& config) { void Device::destroy() { debug_check(*this); - destroy(_handle.release(), functions()->vkDestroyDevice); + _handle.reset(); reset(); } @@ -89,7 +89,7 @@ void Device::destroy(VkDevice vkDevice, PFN_vkDestroyDevice destroy) { } void Device::reset() { - _handle = nullptr; + _handle.deleter().destroyFunction = nullptr; _queues.clear(); if(_functionTable == nullptr) _functionTable = std::make_unique(); @@ -163,10 +163,13 @@ void Device::createLogicalDevice(queue_families_queue_counts_t const& queueFamil reset(); throw cvk::vk_result_exception{createResult, details->exception()}; } - _handle = ptr; + + loadFunctionTable(ptr); + _handle.deleter().destroyFunction = _functionTable->vkDestroyDevice; + _handle.reset(ptr); } -void Device::loadFunctionTable() const { volkLoadDeviceTable(_functionTable.get(), get()); } +void Device::loadFunctionTable(VkDevice vkDevice) const { volkLoadDeviceTable(_functionTable.get(), vkDevice); } void Device::createQueues(span familyIndices) { std::unordered_map queueCounts{}; diff --git a/cvk/src/base/instance.cpp b/cvk/src/base/instance.cpp index 6c6f7e1a..c4eabca5 100644 --- a/cvk/src/base/instance.cpp +++ b/cvk/src/base/instance.cpp @@ -49,7 +49,7 @@ Instance::Instance( void Instance::wrap(State state) { optDestroy(); - _handle = state.vkInstance.get(); + _handle.reset(state.vkInstance.get()); _debugMessenger = std::move(state.debugMessenger); loadInstanceFunctions(state.vkInstance); @@ -97,7 +97,7 @@ void Instance::create(std::optional messengerConfig) { throw cvk::vk_result_exception{createInstanceResult, details->exception()}; } - _handle = ptr; + _handle.reset(ptr); loadInstanceFunctions(ptr); if constexpr(constants::ENABLE_VALIDATION_LAYERS) @@ -112,7 +112,7 @@ void Instance::destroy() { debug_check(*this); _debugMessenger = nullptr; - destroy(_handle.get()); + _handle.reset(); reset(); } @@ -214,10 +214,7 @@ void Instance::destroy(VkInstance vkInstance) { log::msg("destroyed instance"); } -void Instance::reset() { - _debugMessenger = nullptr; - _handle = VK_NULL_HANDLE; -} +void Instance::reset() { _debugMessenger = nullptr; } void Instance::loadInstanceFunctions(vk_not_null vkInstance) { volkLoadInstanceOnly(vkInstance.get()); } } diff --git a/cvk/src/debug/debug_messenger.cpp b/cvk/src/debug/debug_messenger.cpp index ac96bf8b..1ee152fc 100644 --- a/cvk/src/debug/debug_messenger.cpp +++ b/cvk/src/debug/debug_messenger.cpp @@ -17,28 +17,26 @@ void DebugMessenger::create(Instance const& instance) { optDestroy(); - _vkInstance = instance.get(); - - auto const info = _config.createInfo(); VkDebugUtilsMessengerEXT ptr = VK_NULL_HANDLE; - auto const createResult = vkCreateDebugUtilsMessengerEXT(_vkInstance, &info, nullptr, &ptr); + auto const createResult = vkCreateDebugUtilsMessengerEXT(instance.get(), &info, nullptr, &ptr); CTH_STABLE_ERR(createResult != VK_SUCCESS, "failed to set up debug messenger") { reset(); throw cvk::vk_result_exception{createResult, details->exception()}; } - _handle = ptr; + _handle.deleter().vkInstance = instance.get(); + _handle.reset(ptr); } void DebugMessenger::destroy() { debug_check(*this); - destroy(_vkInstance, _handle.get()); + _handle.reset(); reset(); } @@ -54,14 +52,15 @@ void DebugMessenger::destroy(vk_not_null vkInstance, VkDebugUtilsMes DebugMessenger::State DebugMessenger::release() { debug_check(*this); - State const state{_vkInstance, _handle.get()}; + State const state{_handle.deleter().vkInstance, _handle.release()}; reset(); return state; } -void DebugMessenger::reset() { - _vkInstance = VK_NULL_HANDLE; - _handle = VK_NULL_HANDLE; +void DebugMessenger::reset() { _handle.deleter().vkInstance = VK_NULL_HANDLE; } + +void DebugMessenger::Deleter::operator()(VkDebugUtilsMessengerEXT vkMessenger) const { + DebugMessenger::destroy(vkInstance, vkMessenger); } } // namespace cvk diff --git a/cvk/src/render/sync/fence.cpp b/cvk/src/render/sync/fence.cpp index 61d678ad..4b3782f4 100644 --- a/cvk/src/render/sync/fence.cpp +++ b/cvk/src/render/sync/fence.cpp @@ -16,7 +16,7 @@ namespace cvk { -Fence::Fence(Core const& core) : _core{&core} { Core::debug_check(core); } +Fence::Fence(Core const& core) : _handle{core} { Core::debug_check(core); } Fence::Fence(Core const& core, State const& state) : Fence{core} { wrap(state); } @@ -27,7 +27,7 @@ Fence::Fence(Core const& core, VkFenceCreateFlags flags, VkExternalFenceHandleTy void Fence::wrap(State const& state) { optDestroy(); - _handle = state.vkFence.get(); + _handle.reset(state.vkFence.get()); _exportTypes = state.exportTypes; } @@ -46,21 +46,21 @@ void Fence::create(VkFenceCreateFlags flags, VkExternalFenceHandleTypeFlags expo }; VkFence ptr = VK_NULL_HANDLE; - auto const result = _core->functions()->vkCreateFence(_core->vkDevice(), &info, nullptr, &ptr); + auto const result = core().functions()->vkCreateFence(core().vkDevice(), &info, nullptr, &ptr); CTH_STABLE_ERR(result != VK_SUCCESS, "failed to create fence") { resetState(); throw cvk::vk_result_exception{result, details->exception()}; } - _handle = ptr; + _handle.reset(ptr); _exportTypes = exportTypes; } void Fence::destroy() { debug_check(*this); - destroy(_core->deviceTable(), _handle.get()); + _handle.reset(); resetState(); } @@ -68,7 +68,7 @@ void Fence::destroy() { Fence::State Fence::release() { debug_check(*this); - State const state{_handle.get(), _exportTypes}; + State const state{_handle.release(), _exportTypes}; resetState(); return state; } @@ -76,7 +76,7 @@ Fence::State Fence::release() { VkResult Fence::status() const { debug_check(*this); - auto const result = _core->functions()->vkGetFenceStatus(_core->vkDevice(), _handle.get()); + auto const result = core().functions()->vkGetFenceStatus(core().vkDevice(), _handle.get()); CVK_RESULT_STABLE_THROW( result != VK_SUCCESS && result != VK_NOT_READY, @@ -91,7 +91,7 @@ void Fence::reset() const { debug_check(*this); auto const vkFence = _handle.get(); - auto const result = _core->functions()->vkResetFences(_core->vkDevice(), 1, &vkFence); + auto const result = core().functions()->vkResetFences(core().vkDevice(), 1, &vkFence); CVK_RESULT_STABLE_THROW(result != VK_SUCCESS, result, "failed to reset fence") {} } @@ -100,7 +100,7 @@ VkResult Fence::wait(uint64_t timeout) const { debug_check(*this); auto const vkFence = _handle.get(); - auto const result = _core->functions()->vkWaitForFences(_core->vkDevice(), 1, &vkFence, VK_TRUE, timeout); + auto const result = core().functions()->vkWaitForFences(core().vkDevice(), 1, &vkFence, VK_TRUE, timeout); CVK_RESULT_STABLE_THROW(result != VK_SUCCESS && result != VK_TIMEOUT, result, "failed to wait for fence") {} @@ -132,7 +132,7 @@ cth::os::unique_native_handle Fence::extractOsHandle() const { #ifdef CTH_PLATFORM_WINDOWS // volk loads platform functions only if it was built with the platform define auto const getHandle = reinterpret_cast( - vkGetDeviceProcAddr(_core->vkDevice(), "vkGetFenceWin32HandleKHR") + vkGetDeviceProcAddr(core().vkDevice(), "vkGetFenceWin32HandleKHR") ); CVK_STABLE_THROW(getHandle == nullptr, "vkGetFenceWin32HandleKHR not available") {} @@ -143,7 +143,7 @@ cth::os::unique_native_handle Fence::extractOsHandle() const { .handleType = constants::OS_FENCE_HANDLE_TYPE, }; HANDLE handle = nullptr; - auto const result = getHandle(_core->vkDevice(), &info, &handle); + auto const result = getHandle(core().vkDevice(), &info, &handle); #else VkFenceGetFdInfoKHR const info{ .sType = VK_STRUCTURE_TYPE_FENCE_GET_FD_INFO_KHR, @@ -152,7 +152,7 @@ cth::os::unique_native_handle Fence::extractOsHandle() const { .handleType = constants::OS_FENCE_HANDLE_TYPE, }; int handle = cth::os::invalid_handle(); - auto const result = _core->functions()->vkGetFenceFdKHR(_core->vkDevice(), &info, &handle); + auto const result = core().functions()->vkGetFenceFdKHR(core().vkDevice(), &info, &handle); #endif CVK_RESULT_STABLE_THROW(result != VK_SUCCESS, result, "failed to export the fence") {} @@ -166,8 +166,5 @@ void Fence::destroy(DeviceTable table, VkFence vkFence) { table->vkDestroyFence(table.device(), vkFence, nullptr); } -void Fence::resetState() { - _handle = VK_NULL_HANDLE; - _exportTypes = 0; -} +void Fence::resetState() { _exportTypes = 0; } } diff --git a/cvk/src/surface/surface.cpp b/cvk/src/surface/surface.cpp index 9bcd4a82..24923e6d 100644 --- a/cvk/src/surface/surface.cpp +++ b/cvk/src/surface/surface.cpp @@ -26,7 +26,7 @@ namespace { } } -Surface::Surface(Instance const& instance, Config config) : _instance{&instance}, _config{std::move(config)} {} +Surface::Surface(Instance const& instance, Config config) : _config{std::move(config)}, _handle{instance} {} Surface::Surface(Instance const& instance, Config config, State const& state) : Surface{instance, std::move(config)} { wrap(state); @@ -36,20 +36,20 @@ void Surface::wrap(State const& state) { debug_check_handle(state.vkSurface); optDestroy(); - _handle = state.vkSurface.get(); + _handle.reset(state.vkSurface.get()); } void Surface::destroy() { debug_check(*this); - destroy(_instance->get(), _handle.get()); + _handle.reset(); reset(); } Surface::State Surface::release() { debug_check(*this); - State const state{_handle.get()}; + State const state{_handle.release()}; reset(); return state; @@ -158,5 +158,5 @@ void Surface::destroy(vk_not_null vkInstance, VkSurfaceKHR vkSurface log::msg("destroyed surface"); } -void Surface::reset() { _handle = VK_NULL_HANDLE; } +void Surface::reset() {} } diff --git a/cvk/tests/src/base/instance_test.cpp b/cvk/tests/src/base/instance_test.cpp index 23c3b523..121379e7 100644 --- a/cvk/tests/src/base/instance_test.cpp +++ b/cvk/tests/src/base/instance_test.cpp @@ -1,4 +1,5 @@ #include "test.hpp" +#include "validation.hpp" #include "cvk/base/instance.hpp" @@ -93,13 +94,17 @@ CVK_TEST(Instance, wrap_takes_ownership) { } CVK_TEST(Instance, move_assignment_takes_the_other_instance) { - Instance instance{APP_NAME, NO_EXTENSIONS, std::nullopt}; - Instance other{APP_NAME, NO_EXTENSIONS, std::nullopt}; - auto const vkInstance = other.get(); - - instance = std::move(other); - - EXPECT_EQ(instance.get(), vkInstance); - EXPECT_FALSE(other.created()); + test::reset_validation_errors(); + { + Instance instance{APP_NAME, NO_EXTENSIONS, test::counting_messenger()}; + Instance other{APP_NAME, NO_EXTENSIONS, test::counting_messenger()}; + auto const vkInstance = other.get(); + + instance = std::move(other); + + EXPECT_EQ(instance.get(), vkInstance); + EXPECT_FALSE(other.created()); + } + EXPECT_EQ(test::validation_errors(), 0); } } From 541e4620eafc6fdce0627c2d99768ad371ba6de7 Mon Sep 17 00:00:00 2001 From: lukas <89145039+codethinki@users.noreply.github.com> Date: Fri, 16 Oct 2026 16:56:15 +0200 Subject: [PATCH 023/135] cvk: unique_handle takes the context with the handle - constructors and reset(handle, context...) construct the deleter, a missing context does not compile - reset() only destroys, no mutable deleter() anymore - instance_deleter stores the VkInstance: the instance owns its messenger and can move - DebugMessenger takes the instance in the base constructor, create() has no parameter, State without the instance - Device::destroy(VkDevice) gets vkDestroyDevice through vkGetDeviceProcAddr, no custom deleter --- cvk/incl/cvk/base/device.hpp | 15 +---- cvk/incl/cvk/debug/debug_messenger.hpp | 24 ++------ cvk/incl/cvk/utility/unique_handle.hpp | 62 +++++++++++++------- cvk/src/base/device.cpp | 8 +-- cvk/src/base/instance.cpp | 5 +- cvk/src/debug/debug_messenger.cpp | 26 ++++---- cvk/src/render/sync/fence.cpp | 4 +- cvk/src/surface/surface.cpp | 2 +- cvk/tests/src/debug/debug_messenger_test.cpp | 14 ++--- cvk/tests/src/utility/unique_handle_test.cpp | 55 +++++++++++++---- 10 files changed, 119 insertions(+), 96 deletions(-) diff --git a/cvk/incl/cvk/base/device.hpp b/cvk/incl/cvk/base/device.hpp index 931e8791..629b98d4 100644 --- a/cvk/incl/cvk/base/device.hpp +++ b/cvk/incl/cvk/base/device.hpp @@ -102,9 +102,9 @@ class Device { void waitIdle() const; /** - * @param destroy vkDestroyDevice of the function table of the device + * @brief destroys the device, gets vkDestroyDevice through vkGetDeviceProcAddr() */ - static void destroy(VkDevice, PFN_vkDestroyDevice destroy); + static void destroy(VkDevice); [[nodiscard]] std::span queues() const { return _queues; } [[nodiscard]] std::span queues() { return _queues; } @@ -142,16 +142,7 @@ class Device { not_null _instance; not_null _physicalDevice; - /** - * @brief vkDestroyDevice comes from the function table, it is loaded with @ref create() or @ref wrap() - */ - struct Deleter : handle_deleter { - PFN_vkDestroyDevice destroyFunction = nullptr; - - void operator()(VkDevice vkDevice) const { destroy(vkDevice, destroyFunction); } - }; - - unique_handle _handle; + unique_handle _handle; std::unique_ptr _functionTable = std::make_unique(); std::vector _queues{}; diff --git a/cvk/incl/cvk/debug/debug_messenger.hpp b/cvk/incl/cvk/debug/debug_messenger.hpp index 018e18c9..5cadb351 100644 --- a/cvk/incl/cvk/debug/debug_messenger.hpp +++ b/cvk/incl/cvk/debug/debug_messenger.hpp @@ -24,18 +24,17 @@ class DebugMessenger { /** * @brief base constructor + * @pre @ref Instance::created() */ - explicit DebugMessenger(Config); + DebugMessenger(Instance const&, Config); /** * @brief constructs and creates * @details calls: - * - @ref DebugMessenger(Config) + * - @ref DebugMessenger(Instance const&, Config) * - @ref create() */ - explicit DebugMessenger(Config const& config, Instance const& instance) : DebugMessenger{config} { - create(instance); - } + explicit DebugMessenger(Instance const&, Config, create_t); /** @@ -45,13 +44,12 @@ class DebugMessenger { /** * @brief creates the messenger - * @pre @ref Instance::created() * @pre the instance enables VK_EXT_debug_utils * @post @ref created() * @note calls @ref optDestroy() * @throws cvk::vk_result_exception result of vkCreateDebugUtilsMessengerEXT() */ - void create(Instance const&); + void create(); /** @@ -101,18 +99,9 @@ class DebugMessenger { Config _config; private: - /** - * @brief the instance comes with @ref create(), not with the base constructor - */ - struct Deleter : handle_deleter { - VkInstance vkInstance = VK_NULL_HANDLE; - - void operator()(VkDebugUtilsMessengerEXT vkMessenger) const; - }; - void reset(); - unique_handle _handle; + unique_handle _handle; public: DebugMessenger(DebugMessenger const& other) = delete; @@ -126,7 +115,6 @@ class DebugMessenger { namespace cvk { struct DebugMessenger::State { - vk_not_null vkInstance; vk_not_null vkMessenger; }; } diff --git a/cvk/incl/cvk/utility/unique_handle.hpp b/cvk/incl/cvk/utility/unique_handle.hpp index 278df543..ce340518 100644 --- a/cvk/incl/cvk/utility/unique_handle.hpp +++ b/cvk/incl/cvk/utility/unique_handle.hpp @@ -41,8 +41,8 @@ concept handle_destroyable = requires(Handle handle) { T::destroy(handle); }; /** - * @brief deleter of device objects, stores the core - * @details calls T::destroy(core.deviceTable(), handle), derive from it for destroy functions that need more + * @brief deleter of device objects, context: the core + * @details calls T::destroy(core.deviceTable(), handle) * @pre the core outlives the deleter */ template @@ -60,22 +60,23 @@ class core_deleter { }; /** - * @brief deleter of instance objects, stores the instance - * @details calls T::destroy(instance.get(), handle), derive from it for destroy functions that need more - * @pre the instance outlives the deleter + * @brief deleter of instance objects, context: the VkInstance + * @details calls T::destroy(vkInstance, handle), + * stores the handle, not the Instance: the instance owns its messenger and can move */ template class instance_deleter { public: - explicit instance_deleter(Instance const& instance) : _instance{&instance} {} + explicit instance_deleter(VkInstance vkInstance) : _vkInstance{vkInstance} {} + explicit instance_deleter(Instance const& instance) : instance_deleter{dev::vk_instance(instance)} {} template - void operator()(Handle handle) const { T::destroy(dev::vk_instance(*_instance), handle); } + void operator()(Handle handle) const { T::destroy(_vkInstance.get(), handle); } - [[nodiscard]] Instance const& instance() const { return *_instance; } + [[nodiscard]] VkInstance vkInstance() const { return _vkInstance.get(); } private: - not_null _instance; + vk_not_null _vkInstance; }; /** @@ -115,11 +116,13 @@ using default_deleter_t = std::conditional_t< /** - * @brief owning Vulkan handle, std::unique_ptr with a deleter that stores the destroy context + * @brief owning Vulkan handle, std::unique_ptr with a deleter that holds the destroy context * @tparam T owner class, its static destroy picks the default deleter * @tparam Handle Vulkan handle, or Deleter::pointer for handles that are not pointers * @tparam Deleter custom deleters derive from @ref core_deleter, @ref instance_deleter or @ref handle_deleter - * @details move assignment destroys the old handle with the old deleter, then takes handle and deleter + * @details a handle comes only together with its context: the constructors and reset() construct the deleter, + * a missing context argument does not compile, + * move assignment destroys the old handle with the old context, then takes handle and context */ template> class unique_handle { @@ -136,20 +139,40 @@ class unique_handle { using deleter_t = Deleter; /** - * @brief constructs the deleter, owns no handle + * @brief owns no handle + * @param context to construct the deleter */ template requires std::constructible_from - explicit unique_handle(Args&&... args) : _ptr{Handle{}, Deleter{std::forward(args)...}} {} + explicit unique_handle(Args&&... context) : _ptr{Handle{}, Deleter{std::forward(context)...}} {} + + /** + * @brief owns @ref handle + * @param context to construct the deleter + */ + template requires std::constructible_from + explicit unique_handle(Handle handle, Args&&... context) : + _ptr{handle, Deleter{std::forward(context)...}} {} ~unique_handle() = default; /** - * @brief destroys the owned handle, then owns @ref handle + * @brief destroys the owned handle, keeps the context */ - void reset(Handle handle = Handle{}) noexcept { _ptr.reset(handle); } + void reset() noexcept { _ptr.reset(); } + + /** + * @brief destroys the owned handle with the old context, then owns @ref handle with the new context + * @param context to construct the deleter + */ + template requires std::constructible_from + void reset(Handle handle, Args&&... context) noexcept { + _ptr.reset(); + _ptr.get_deleter() = Deleter{std::forward(context)...}; + _ptr.reset(handle); + } /** - * @brief gives up the handle without destroying, keeps the deleter + * @brief gives up the handle without destroying, keeps the context */ [[nodiscard]] Handle release() noexcept { return _ptr.release(); } @@ -157,7 +180,6 @@ class unique_handle { [[nodiscard]] explicit operator bool() const noexcept { return static_cast(_ptr); } [[nodiscard]] Deleter const& deleter() const noexcept { return _ptr.get_deleter(); } - [[nodiscard]] Deleter& deleter() noexcept { return _ptr.get_deleter(); } /** * @pre the deleter has core() @@ -167,10 +189,10 @@ class unique_handle { } /** - * @pre the deleter has instance() + * @pre the deleter has vkInstance() */ - [[nodiscard]] decltype(auto) instance() const requires requires(Deleter const& d) { d.instance(); } { - return deleter().instance(); + [[nodiscard]] VkInstance vkInstance() const requires requires(Deleter const& d) { d.vkInstance(); } { + return deleter().vkInstance(); } private: diff --git a/cvk/src/base/device.cpp b/cvk/src/base/device.cpp index 8a8a1496..8b73fc05 100644 --- a/cvk/src/base/device.cpp +++ b/cvk/src/base/device.cpp @@ -40,7 +40,6 @@ void Device::wrap(State state) { loadFunctionTable(state.vkDevice.get()); } - _handle.deleter().destroyFunction = _functionTable->vkDestroyDevice; _handle.reset(state.vkDevice.get()); } @@ -80,16 +79,16 @@ void Device::waitIdle() const { CVK_RESULT_STABLE_THROW(result != VK_SUCCESS, result, "failed to wait for device") {} } -void Device::destroy(VkDevice vkDevice, PFN_vkDestroyDevice destroy) { +void Device::destroy(VkDevice vkDevice) { CTH_WARN(vkDevice == VK_NULL_HANDLE, "vkDevice invalid (VK_NULL_HANDLE)") {} - destroy(vkDevice, nullptr); + auto const destroyDevice = reinterpret_cast(vkGetDeviceProcAddr(vkDevice, "vkDestroyDevice")); + destroyDevice(vkDevice, nullptr); log::msg("destroyed device"); } void Device::reset() { - _handle.deleter().destroyFunction = nullptr; _queues.clear(); if(_functionTable == nullptr) _functionTable = std::make_unique(); @@ -165,7 +164,6 @@ void Device::createLogicalDevice(queue_families_queue_counts_t const& queueFamil } loadFunctionTable(ptr); - _handle.deleter().destroyFunction = _functionTable->vkDestroyDevice; _handle.reset(ptr); } diff --git a/cvk/src/base/instance.cpp b/cvk/src/base/instance.cpp index c4eabca5..5a987372 100644 --- a/cvk/src/base/instance.cpp +++ b/cvk/src/base/instance.cpp @@ -102,10 +102,7 @@ void Instance::create(std::optional messengerConfig) { if constexpr(constants::ENABLE_VALIDATION_LAYERS) if(messengerConfig != std::nullopt) - _debugMessenger = std::make_unique( - *messengerConfig, - *this - ); + _debugMessenger = std::make_unique(*this, *messengerConfig, cvk::create); } void Instance::destroy() { diff --git a/cvk/src/debug/debug_messenger.cpp b/cvk/src/debug/debug_messenger.cpp index 1ee152fc..b7b34132 100644 --- a/cvk/src/debug/debug_messenger.cpp +++ b/cvk/src/debug/debug_messenger.cpp @@ -9,28 +9,28 @@ namespace cvk { -DebugMessenger::DebugMessenger(Config config) : _config{std::move(config)} {} - -void DebugMessenger::create(Instance const& instance) { - Instance::debug_check(instance); +DebugMessenger::DebugMessenger(Instance const& instance, Config config) : _config{std::move(config)}, + _handle{instance} { Instance::debug_check(instance); } +DebugMessenger::DebugMessenger(Instance const& instance, Config config, create_t) : DebugMessenger{ + instance, + std::move(config) +} { create(); } +void DebugMessenger::create() { optDestroy(); auto const info = _config.createInfo(); - VkDebugUtilsMessengerEXT ptr = VK_NULL_HANDLE; - - auto const createResult = vkCreateDebugUtilsMessengerEXT(instance.get(), &info, nullptr, &ptr); + auto const createResult = vkCreateDebugUtilsMessengerEXT(_handle.vkInstance(), &info, nullptr, &ptr); CTH_STABLE_ERR(createResult != VK_SUCCESS, "failed to set up debug messenger") { reset(); throw cvk::vk_result_exception{createResult, details->exception()}; } - _handle.deleter().vkInstance = instance.get(); - _handle.reset(ptr); + _handle.reset(ptr, _handle.vkInstance()); } void DebugMessenger::destroy() { @@ -52,16 +52,12 @@ void DebugMessenger::destroy(vk_not_null vkInstance, VkDebugUtilsMes DebugMessenger::State DebugMessenger::release() { debug_check(*this); - State const state{_handle.deleter().vkInstance, _handle.release()}; + State const state{_handle.release()}; reset(); return state; } -void DebugMessenger::reset() { _handle.deleter().vkInstance = VK_NULL_HANDLE; } - -void DebugMessenger::Deleter::operator()(VkDebugUtilsMessengerEXT vkMessenger) const { - DebugMessenger::destroy(vkInstance, vkMessenger); -} +void DebugMessenger::reset() {} } // namespace cvk namespace cvk::dev { diff --git a/cvk/src/render/sync/fence.cpp b/cvk/src/render/sync/fence.cpp index 4b3782f4..41867699 100644 --- a/cvk/src/render/sync/fence.cpp +++ b/cvk/src/render/sync/fence.cpp @@ -27,7 +27,7 @@ Fence::Fence(Core const& core, VkFenceCreateFlags flags, VkExternalFenceHandleTy void Fence::wrap(State const& state) { optDestroy(); - _handle.reset(state.vkFence.get()); + _handle.reset(state.vkFence.get(), core()); _exportTypes = state.exportTypes; } @@ -53,7 +53,7 @@ void Fence::create(VkFenceCreateFlags flags, VkExternalFenceHandleTypeFlags expo throw cvk::vk_result_exception{result, details->exception()}; } - _handle.reset(ptr); + _handle.reset(ptr, core()); _exportTypes = exportTypes; } diff --git a/cvk/src/surface/surface.cpp b/cvk/src/surface/surface.cpp index 24923e6d..afc7d8a1 100644 --- a/cvk/src/surface/surface.cpp +++ b/cvk/src/surface/surface.cpp @@ -36,7 +36,7 @@ void Surface::wrap(State const& state) { debug_check_handle(state.vkSurface); optDestroy(); - _handle.reset(state.vkSurface.get()); + _handle.reset(state.vkSurface.get(), _handle.vkInstance()); } void Surface::destroy() { diff --git a/cvk/tests/src/debug/debug_messenger_test.cpp b/cvk/tests/src/debug/debug_messenger_test.cpp index 2de93f33..128858f2 100644 --- a/cvk/tests/src/debug/debug_messenger_test.cpp +++ b/cvk/tests/src/debug/debug_messenger_test.cpp @@ -55,7 +55,7 @@ CVK_TEST(DebugMessenger, forwards_messages_to_the_callback) { if constexpr(!constants::ENABLE_VALIDATION_LAYERS) GTEST_SKIP() << "needs the validation layers"; Instance const instance{APP_NAME, NO_EXTENSIONS, std::nullopt}; - DebugMessenger const messenger{DebugMessenger::Config::Default(count_test_messages), instance}; + DebugMessenger const messenger{instance, DebugMessenger::Config::Default(count_test_messages), create}; ASSERT_TRUE(messenger.created()); received = 0; @@ -78,7 +78,7 @@ CVK_TEST(DebugMessenger, accepts_a_lambda) { if(callbackData->pMessage == TEST_MESSAGE) ++received; return VK_FALSE; }; - DebugMessenger const messenger{DebugMessenger::Config::Default(callback), instance}; + DebugMessenger const messenger{instance, DebugMessenger::Config::Default(callback), create}; received = 0; submit_test_message(instance); @@ -90,7 +90,7 @@ CVK_TEST(DebugMessenger, destroy_resets) { if constexpr(!constants::ENABLE_VALIDATION_LAYERS) GTEST_SKIP() << "needs the validation layers"; Instance const instance{APP_NAME, NO_EXTENSIONS, std::nullopt}; - DebugMessenger messenger{DebugMessenger::Config::Default(count_test_messages), instance}; + DebugMessenger messenger{instance, DebugMessenger::Config::Default(count_test_messages), create}; messenger.destroy(); EXPECT_FALSE(messenger.created()); @@ -105,7 +105,7 @@ CVK_TEST(DebugMessenger, release_keeps_the_messenger) { if constexpr(!constants::ENABLE_VALIDATION_LAYERS) GTEST_SKIP() << "needs the validation layers"; Instance const instance{APP_NAME, NO_EXTENSIONS, std::nullopt}; - DebugMessenger messenger{DebugMessenger::Config::Default(count_test_messages), instance}; + DebugMessenger messenger{instance, DebugMessenger::Config::Default(count_test_messages), create}; auto const state = messenger.release(); EXPECT_FALSE(messenger.created()); @@ -114,7 +114,7 @@ CVK_TEST(DebugMessenger, release_keeps_the_messenger) { submit_test_message(instance); EXPECT_EQ(received, 1); - DebugMessenger::destroy(state.vkInstance, state.vkMessenger.get()); + DebugMessenger::destroy(instance.get(), state.vkMessenger.get()); } CVK_TEST(DebugMessenger, move_assignment_destroys_the_old_messenger) { @@ -123,8 +123,8 @@ CVK_TEST(DebugMessenger, move_assignment_destroys_the_old_messenger) { test::reset_validation_errors(); { Instance const instance{APP_NAME, NO_EXTENSIONS, test::counting_messenger()}; - DebugMessenger messenger{DebugMessenger::Config::Default(count_test_messages), instance}; - DebugMessenger other{DebugMessenger::Config::Default(count_test_messages), instance}; + DebugMessenger messenger{instance, DebugMessenger::Config::Default(count_test_messages), create}; + DebugMessenger other{instance, DebugMessenger::Config::Default(count_test_messages), create}; auto const vkMessenger = other.get(); messenger = std::move(other); diff --git a/cvk/tests/src/utility/unique_handle_test.cpp b/cvk/tests/src/utility/unique_handle_test.cpp index 4525caa8..b01eae75 100644 --- a/cvk/tests/src/utility/unique_handle_test.cpp +++ b/cvk/tests/src/utility/unique_handle_test.cpp @@ -84,6 +84,17 @@ namespace { struct pair_deleter : handle_deleter { using pointer = pair_handle; }; + + /** + * @brief reset(handle, context...) compiles + */ + template + concept resettable = requires(H& handle, typename H::handle_t vkHandle, Context&&... context) { + handle.reset(vkHandle, std::forward(context)...); + }; + + template + concept deleter_assignable = requires(H& handle, typename H::deleter_t deleter) { handle.deleter() = deleter; }; } CVK_TEST(unique_handle, default_deleters_of_the_classes) { @@ -107,14 +118,36 @@ CVK_TEST(unique_handle, default_deleters_of_the_classes) { static_assert(std::same_as, handle_deleter>); - // destroy(VkDevice, PFN_vkDestroyDevice) -> needs a custom deleter - static_assert(std::same_as, dev::no_default_deleter>); + static_assert(std::same_as, handle_deleter>); +} + +CVK_TEST(unique_handle, a_handle_needs_its_context) { + using fence_handle = unique_handle; + static_assert(!std::default_initializable); + static_assert(!std::constructible_from); + static_assert(!resettable); + static_assert(resettable); + + using surface_handle = unique_handle; + static_assert(!resettable); + static_assert(resettable); + + using instance_handle = unique_handle; + static_assert(std::default_initializable); + static_assert(resettable); + + using pool_handle = unique_handle; + static_assert(!resettable); + static_assert(resettable); + + // the context of a handle cannot change on its own + static_assert(!deleter_assignable); } CVK_TEST(unique_handle, size) { static_assert(sizeof(unique_handle) == sizeof(VkInstance)); static_assert(sizeof(unique_handle) == sizeof(Core const*) + sizeof(VkFence)); - static_assert(sizeof(unique_handle) == sizeof(Instance const*) + sizeof(VkSurfaceKHR)); + static_assert(sizeof(unique_handle) == sizeof(VkInstance) + sizeof(VkSurfaceKHR)); } CVK_TEST(unique_handle, core_deleter) { @@ -125,7 +158,7 @@ CVK_TEST(unique_handle, core_deleter) { EXPECT_EQ(&handle.core(), &context.core); auto const vkFence = create_vk_fence(context.core); - handle.reset(vkFence); + handle.reset(vkFence, context.core); ASSERT_TRUE(handle); EXPECT_EQ(handle.get(), vkFence); @@ -137,9 +170,9 @@ CVK_TEST(unique_handle, move_assignment_destroys_the_old_handle) { test::Context const context{}; unique_handle handle{context.core}; unique_handle other{context.core}; - handle.reset(create_vk_fence(context.core)); + handle.reset(create_vk_fence(context.core), context.core); auto const vkFence = create_vk_fence(context.core); - other.reset(vkFence); + other.reset(vkFence, context.core); handle = std::move(other); @@ -150,9 +183,8 @@ CVK_TEST(unique_handle, move_assignment_destroys_the_old_handle) { CVK_TEST(unique_handle, release_keeps_the_handle) { test::Context const context{}; - unique_handle handle{context.core}; auto const vkFence = create_vk_fence(context.core); - handle.reset(vkFence); + unique_handle handle{vkFence, context.core}; EXPECT_EQ(handle.release(), vkFence); EXPECT_FALSE(handle); @@ -168,10 +200,9 @@ CVK_TEST(unique_handle, instance_deleter) { Instance const instance{test::APP_NAME, test::headless_extensions(), test::counting_messenger()}; auto surface = test::headless_surface(instance); - unique_handle handle{instance}; - handle.reset(surface.release().vkSurface.get()); + unique_handle handle{surface.release().vkSurface.get(), instance}; - EXPECT_EQ(&handle.instance(), &instance); + EXPECT_EQ(handle.vkInstance(), instance.get()); } EXPECT_EQ(test::validation_errors(), 0); } @@ -233,7 +264,7 @@ CVK_TEST(unique_handle, custom_deleter) { VkCommandBuffer vkBuffer = VK_NULL_HANDLE; ASSERT_EQ(table->vkAllocateCommandBuffers(table.device(), &allocInfo, &vkBuffer), VK_SUCCESS); - handle.reset(vkBuffer); + handle.reset(vkBuffer, context.core, vkPool); EXPECT_EQ(handle.get(), vkBuffer); } From 56a213d337f0b146e24ebdf8f208d8d0f5445189 Mon Sep 17 00:00:00 2001 From: lukas <89145039+codethinki@users.noreply.github.com> Date: Fri, 16 Oct 2026 17:38:34 +0200 Subject: [PATCH 024/135] style: lifecycle reset() is invalidate() reset keeps its std meaning (unique_ptr::reset destroys) and names Vulkan operations (vkResetFences). The private lifecycle reset() and Fence::resetState() are invalidate(), docs say invalidates. --- AGENTS.md | 16 ++++++++-------- cvk/incl/cvk/base/core.hpp | 6 +++--- cvk/incl/cvk/base/device.hpp | 6 +++--- cvk/incl/cvk/base/instance.hpp | 4 ++-- cvk/incl/cvk/base/queue/queue.hpp | 6 +++--- cvk/incl/cvk/data/img/sampler.hpp | 6 +++--- cvk/incl/cvk/debug/debug_messenger.hpp | 6 +++--- .../pipeline/layout/descriptor_set_layout.hpp | 6 +++--- .../render/pipeline/layout/pipeline_layout.hpp | 6 +++--- cvk/incl/cvk/render/pipeline/pipeline.hpp | 6 +++--- cvk/incl/cvk/render/pipeline/shader/shader.hpp | 6 +++--- cvk/incl/cvk/render/sync/fence.hpp | 8 ++++---- cvk/incl/cvk/render/sync/semaphore.hpp | 6 +++--- cvk/incl/cvk/render/sync/timeline_semaphore.hpp | 2 +- cvk/incl/cvk/surface/surface.hpp | 6 +++--- cvk/src/base/core.cpp | 6 +++--- cvk/src/base/device.cpp | 8 ++++---- cvk/src/base/instance.cpp | 6 +++--- cvk/src/base/queue/queue.cpp | 6 +++--- cvk/src/data/img/sampler.cpp | 8 ++++---- cvk/src/debug/debug_messenger.cpp | 8 ++++---- .../pipeline/layout/descriptor_set_layout.cpp | 8 ++++---- .../render/pipeline/layout/pipeline_layout.cpp | 8 ++++---- cvk/src/render/pipeline/pipeline.cpp | 8 ++++---- cvk/src/render/pipeline/shader/shader.cpp | 8 ++++---- cvk/src/render/sync/fence.cpp | 8 ++++---- cvk/src/render/sync/semaphore.cpp | 8 ++++---- cvk/src/surface/surface.cpp | 6 +++--- 28 files changed, 96 insertions(+), 96 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 1dc19e10..47781f84 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -186,23 +186,23 @@ inline void Semaphore::debug_check(Semaphore const& semaphore) { | base constructor | stores the configuration, does not create a Vulkan object | | `wrap(State)` | takes ownership of an existing Vulkan object, calls `optDestroy()` first | | `create(...)` | creates a new Vulkan object, calls `optDestroy()` first | -| `destroy()` | destroys the Vulkan object now and resets, requires `created()` | +| `destroy()` | destroys the Vulkan object now and invalidates, requires `created()` | | `optDestroy()` | calls `destroy()` if `created()` | -| `release()` | gives the `State` to the caller and resets, does not destroy | +| `release()` | gives the `State` to the caller and invalidates, does not destroy | | `static destroy(...)` | destroys the raw handles, used by `destroy()` | -| `reset()` | sets the members to the not created state, does not destroy | +| `invalidate()` | sets the members to the not created state, does not destroy | - The destructor calls `optDestroy()`. -- Vulkan has its own reset for some objects (`vkResetFences`, `vkResetCommandBuffer`). There the public `reset()` is the Vulkan operation and the private member reset is `resetState()`. +- `reset` keeps its std meaning (`std::unique_ptr::reset` destroys) and names Vulkan operations (`vkResetFences`). The lifecycle uses `invalidate()`. - Device objects take `Core const&` in the base constructor and store `not_null _core`. - A parent (instance, core, device, pool, ...) does not move and is not destroyed while objects depend on it. Code does not have to support it. -- A failed `create()` calls `reset()` before it throws. +- A failed `create()` calls `invalidate()` before it throws. - `release()` exists so that an owner can use an object it must not destroy (for example swapchain images). ### Errors - `CTH_CRITICAL(condition, "message") {}`: precondition check, active in debug builds only. -- `CTH_STABLE_ERR(condition, "message") { reset(); throw details->exception(); }`: error that can happen at run time. +- `CTH_STABLE_ERR(condition, "message") { invalidate(); throw details->exception(); }`: error that can happen at run time. - `CTH_WARN(condition, "message") {}`: suspicious state that is not an error. - `CVK_RESULT_STABLE_THROW(result != VK_SUCCESS, result, "message") {}`: throws `cvk::vk_result_exception` with the `VkResult`. - The condition is the error case: `CTH_CRITICAL(!created(), ...)`, not the expected case. @@ -224,7 +224,7 @@ Documentation uses doxygen comments and follows ASD-STE100 (Simplified Technical - Use `/** ... */` blocks with one tag per line. - Start after the tag with a lower case letter. - Do not put a period at the end of a line. -- Write short fragments, not full sentences. Start them with a verb in the present tense: `creates the instance`, `destroys and resets`. +- Write short fragments, not full sentences. Start them with a verb in the present tense: `creates the instance`, `destroys and invalidates`. - Fragments win over STE100: where STE100 asks for a full sentence or an article, write the shorter fragment. - Use `@ref` for every name of code: `@ref create()`, `@ref Core::created()`. - Do not document what the signature already tells. A getter without conditions needs no comment. @@ -274,7 +274,7 @@ Use symbols when they are shorter and clearer than words: | destroy | the Vulkan object stops to exist | | wrap | take ownership of an existing Vulkan object | | release | give the ownership to the caller without destroying | -| reset | set the members to the not created state | +| invalidate | set the members to the not created state | | handle | the raw Vulkan handle, for example `VkInstance` | | state | the `State` struct: everything that `wrap` needs | diff --git a/cvk/incl/cvk/base/core.hpp b/cvk/incl/cvk/base/core.hpp index 8e5ad256..a5cefd99 100644 --- a/cvk/incl/cvk/base/core.hpp +++ b/cvk/incl/cvk/base/core.hpp @@ -77,7 +77,7 @@ class Core { void create(std::span surfaces, Config const&); /** - * @brief destroys and resets + * @brief destroys and invalidates * @pre @ref created() */ void destroy(); @@ -88,7 +88,7 @@ class Core { void optDestroy() { if(created()) destroy(); } /** - * @brief releases ownership, returns the state and resets + * @brief releases ownership, returns the state and invalidates * @pre @ref created() */ State release(); @@ -140,7 +140,7 @@ class Core { std::vector queueToUniqueQueue; }; - void reset(); + void invalidate(); /** * @post sets @ref _physicalDevice and @ref _queueSetIndex diff --git a/cvk/incl/cvk/base/device.hpp b/cvk/incl/cvk/base/device.hpp index 629b98d4..3616b2d5 100644 --- a/cvk/incl/cvk/base/device.hpp +++ b/cvk/incl/cvk/base/device.hpp @@ -78,7 +78,7 @@ class Device { void create(Config const&); /** - * @brief destroys and resets + * @brief destroys and invalidates * @pre @ref created() */ void destroy(); @@ -89,7 +89,7 @@ class Device { void optDestroy() { if(created()) destroy(); } /** - * @brief releases ownership, returns the state and resets + * @brief releases ownership, returns the state and invalidates * @pre @ref created() */ State release(); @@ -119,7 +119,7 @@ class Device { [[nodiscard]] bool created() const { return static_cast(_handle); } private: - void reset(); + void invalidate(); [[nodiscard]] static queue_families_queue_counts_t calcQueueFamiliesQueueCounts( std::span familyIndices diff --git a/cvk/incl/cvk/base/instance.hpp b/cvk/incl/cvk/base/instance.hpp index be16c255..27182a87 100644 --- a/cvk/incl/cvk/base/instance.hpp +++ b/cvk/incl/cvk/base/instance.hpp @@ -90,7 +90,7 @@ class Instance { void create(std::optional messengerConfig = std::nullopt); /** - * @brief destroys the messenger and the instance and resets + * @brief destroys the messenger and the instance and invalidates * @pre @ref created() */ void destroy(); @@ -129,7 +129,7 @@ class Instance { [[nodiscard]] std::vector availableValidationLayers() const { return _availableLayers; } private: - void reset(); + void invalidate(); std::string _name; diff --git a/cvk/incl/cvk/base/queue/queue.hpp b/cvk/incl/cvk/base/queue/queue.hpp index 97160bdb..423149ea 100644 --- a/cvk/incl/cvk/base/queue/queue.hpp +++ b/cvk/incl/cvk/base/queue/queue.hpp @@ -29,7 +29,7 @@ class Queue { void wrap(State); /** - * @brief resets, the device owns the vulkan queue + * @brief invalidates, the device owns the vulkan queue * @pre @ref created() */ void destroy(); @@ -40,7 +40,7 @@ class Queue { void optDestroy() { if(created()) destroy(); } /** - * @brief releases ownership, returns the state and resets + * @brief releases ownership, returns the state and invalidates * @pre @ref created() */ State release(); @@ -77,7 +77,7 @@ class Queue { [[nodiscard]] QueueFamilyProperties familyProperties() const { return _familyProperties; } private: - void reset(); + void invalidate(); QueueFamilyProperties _familyProperties; VolkDeviceTable const* _functions = nullptr; diff --git a/cvk/incl/cvk/data/img/sampler.hpp b/cvk/incl/cvk/data/img/sampler.hpp index 33f08efb..3fdd6e4e 100644 --- a/cvk/incl/cvk/data/img/sampler.hpp +++ b/cvk/incl/cvk/data/img/sampler.hpp @@ -67,7 +67,7 @@ class Sampler { void create(); /** - * @brief destroys and resets + * @brief destroys and invalidates * @pre @ref created() * @pre the gpu does not use the sampler */ @@ -79,7 +79,7 @@ class Sampler { void optDestroy() { if(created()) destroy(); } /** - * @brief releases ownership, returns the state and resets + * @brief releases ownership, returns the state and invalidates * @pre @ref created() */ State release(); @@ -96,7 +96,7 @@ class Sampler { [[nodiscard]] Config const& config() const { return _config; } private: - void reset(); + void invalidate(); not_null _core; Config _config; diff --git a/cvk/incl/cvk/debug/debug_messenger.hpp b/cvk/incl/cvk/debug/debug_messenger.hpp index 5cadb351..d987101f 100644 --- a/cvk/incl/cvk/debug/debug_messenger.hpp +++ b/cvk/incl/cvk/debug/debug_messenger.hpp @@ -53,7 +53,7 @@ class DebugMessenger { /** - * @brief destroys and resets + * @brief destroys and invalidates * @pre @ref created() */ void destroy(); @@ -67,7 +67,7 @@ class DebugMessenger { static void destroy(vk_not_null, VkDebugUtilsMessengerEXT vkMessenger); /** - * @brief releases ownership, returns state and resets + * @brief releases ownership, returns state and invalidates * @pre @ref created() */ State release(); @@ -99,7 +99,7 @@ class DebugMessenger { Config _config; private: - void reset(); + void invalidate(); unique_handle _handle; diff --git a/cvk/incl/cvk/render/pipeline/layout/descriptor_set_layout.hpp b/cvk/incl/cvk/render/pipeline/layout/descriptor_set_layout.hpp index a3edbc08..4b8c7a50 100644 --- a/cvk/incl/cvk/render/pipeline/layout/descriptor_set_layout.hpp +++ b/cvk/incl/cvk/render/pipeline/layout/descriptor_set_layout.hpp @@ -68,7 +68,7 @@ class DescriptorSetLayout { void create(); /** - * @brief destroys and resets + * @brief destroys and invalidates * @pre @ref created() */ void destroy(); @@ -79,7 +79,7 @@ class DescriptorSetLayout { void optDestroy() { if(created()) destroy(); } /** - * @brief releases ownership, returns the state and resets + * @brief releases ownership, returns the state and invalidates * @pre @ref created() */ State release(); @@ -101,7 +101,7 @@ class DescriptorSetLayout { [[nodiscard]] VkDescriptorSetLayoutBinding const* binding(uint32_t binding) const; private: - void reset(); + void invalidate(); not_null _core; std::vector _bindings; diff --git a/cvk/incl/cvk/render/pipeline/layout/pipeline_layout.hpp b/cvk/incl/cvk/render/pipeline/layout/pipeline_layout.hpp index de0d33e8..2c557d72 100644 --- a/cvk/incl/cvk/render/pipeline/layout/pipeline_layout.hpp +++ b/cvk/incl/cvk/render/pipeline/layout/pipeline_layout.hpp @@ -70,7 +70,7 @@ class PipelineLayout { void create(); /** - * @brief destroys and resets + * @brief destroys and invalidates * @pre @ref created() */ void destroy(); @@ -81,7 +81,7 @@ class PipelineLayout { void optDestroy() { if(created()) destroy(); } /** - * @brief releases ownership, returns the state and resets + * @brief releases ownership, returns the state and invalidates * @pre @ref created() */ State release(); @@ -93,7 +93,7 @@ class PipelineLayout { [[nodiscard]] Core const& core() const { return *_core; } private: - void reset(); + void invalidate(); not_null _core; std::vector _vkSetLayouts; diff --git a/cvk/incl/cvk/render/pipeline/pipeline.hpp b/cvk/incl/cvk/render/pipeline/pipeline.hpp index ba1bc6fe..cf502e01 100644 --- a/cvk/incl/cvk/render/pipeline/pipeline.hpp +++ b/cvk/incl/cvk/render/pipeline/pipeline.hpp @@ -86,7 +86,7 @@ class Pipeline { void create(); /** - * @brief destroys and resets + * @brief destroys and invalidates * @pre @ref created() * @pre the gpu does not use the pipeline */ @@ -98,7 +98,7 @@ class Pipeline { void optDestroy() { if(created()) destroy(); } /** - * @brief releases ownership, returns the state and resets + * @brief releases ownership, returns the state and invalidates * @pre @ref created() */ State release(); @@ -117,7 +117,7 @@ class Pipeline { [[nodiscard]] Core const& core() const { return *_core; } private: - void reset(); + void invalidate(); not_null _core; vk_not_null _vkLayout; diff --git a/cvk/incl/cvk/render/pipeline/shader/shader.hpp b/cvk/incl/cvk/render/pipeline/shader/shader.hpp index 271d53c2..f50ff02c 100644 --- a/cvk/incl/cvk/render/pipeline/shader/shader.hpp +++ b/cvk/incl/cvk/render/pipeline/shader/shader.hpp @@ -105,7 +105,7 @@ class Shader { void create(path_t const& spvPath); /** - * @brief destroys and resets + * @brief destroys and invalidates * @pre @ref created() */ void destroy(); @@ -116,7 +116,7 @@ class Shader { void optDestroy() { if(created()) destroy(); } /** - * @brief releases ownership, returns the state and resets + * @brief releases ownership, returns the state and invalidates * @pre @ref created() */ State release(); @@ -161,7 +161,7 @@ class Shader { [[nodiscard]] Core const& core() const { return *_core; } private: - void reset(); + void invalidate(); not_null _core; VkShaderStageFlagBits _stage; diff --git a/cvk/incl/cvk/render/sync/fence.hpp b/cvk/incl/cvk/render/sync/fence.hpp index 0b8023f7..d777e4d5 100644 --- a/cvk/incl/cvk/render/sync/fence.hpp +++ b/cvk/incl/cvk/render/sync/fence.hpp @@ -67,7 +67,7 @@ class Fence { void create(VkFenceCreateFlags flags = 0, VkExternalFenceHandleTypeFlags exportTypes = 0); /** - * @brief destroys and resets + * @brief destroys and invalidates * @pre @ref created() * @pre the gpu does not use the fence */ @@ -79,7 +79,7 @@ class Fence { void optDestroy() { if(created()) destroy(); } /** - * @brief releases ownership, returns the state and resets + * @brief releases ownership, returns the state and invalidates * @pre @ref created() */ State release(); @@ -130,7 +130,7 @@ class Fence { [[nodiscard]] VkResult waitReset(uint64_t timeout) const; /** - * @brief waits and resets + * @brief waits and invalidates * @details calls: * - @ref wait() const * - @ref reset() @@ -159,7 +159,7 @@ class Fence { [[nodiscard]] VkExternalFenceHandleTypeFlags exportTypes() const { return _exportTypes; } private: - void resetState(); + void invalidate(); unique_handle _handle; VkExternalFenceHandleTypeFlags _exportTypes = 0; diff --git a/cvk/incl/cvk/render/sync/semaphore.hpp b/cvk/incl/cvk/render/sync/semaphore.hpp index 431a0789..3e850971 100644 --- a/cvk/incl/cvk/render/sync/semaphore.hpp +++ b/cvk/incl/cvk/render/sync/semaphore.hpp @@ -62,7 +62,7 @@ class Semaphore { void create(); /** - * @brief destroys and resets + * @brief destroys and invalidates * @pre @ref created() * @pre the gpu does not use the semaphore */ @@ -74,7 +74,7 @@ class Semaphore { void optDestroy() { if(created()) destroy(); } /** - * @brief releases ownership, returns the state and resets + * @brief releases ownership, returns the state and invalidates * @pre @ref created() */ State release(); @@ -95,7 +95,7 @@ class Semaphore { void create(VkSemaphoreCreateInfo const& info); private: - void reset(); + void invalidate(); not_null _core; move_ptr _handle = VK_NULL_HANDLE; diff --git a/cvk/incl/cvk/render/sync/timeline_semaphore.hpp b/cvk/incl/cvk/render/sync/timeline_semaphore.hpp index a44cf5f5..189597ed 100644 --- a/cvk/incl/cvk/render/sync/timeline_semaphore.hpp +++ b/cvk/incl/cvk/render/sync/timeline_semaphore.hpp @@ -65,7 +65,7 @@ class TimelineSemaphore : Semaphore { using Semaphore::optDestroy; /** - * @brief releases ownership, returns the state and resets + * @brief releases ownership, returns the state and invalidates * @pre @ref created() */ State release(); diff --git a/cvk/incl/cvk/surface/surface.hpp b/cvk/incl/cvk/surface/surface.hpp index 4b8e3986..d5e60c61 100644 --- a/cvk/incl/cvk/surface/surface.hpp +++ b/cvk/incl/cvk/surface/surface.hpp @@ -53,7 +53,7 @@ class Surface { void wrap(State const&); /** - * @brief destroys and resets + * @brief destroys and invalidates * @pre @ref created() */ void destroy(); @@ -64,7 +64,7 @@ class Surface { void optDestroy() { if(created()) destroy(); } /** - * @brief releases ownership, returns the state and resets + * @brief releases ownership, returns the state and invalidates * @pre @ref created() */ State release(); @@ -112,7 +112,7 @@ class Surface { [[nodiscard]] Config const& config() const { return _config; } private: - void reset(); + void invalidate(); Config _config; unique_handle _handle; diff --git a/cvk/src/base/core.cpp b/cvk/src/base/core.cpp index 836055c7..bec20e65 100644 --- a/cvk/src/base/core.cpp +++ b/cvk/src/base/core.cpp @@ -60,7 +60,7 @@ void Core::create(span surfaces, Config const& config) { void Core::destroy() { debug_check(*this); - reset(); + invalidate(); } Core::State Core::release() { @@ -72,7 +72,7 @@ Core::State Core::release() { .queues = std::move(_queues), .queueSetIndex = _queueSetIndex, }; - reset(); + invalidate(); return state; } @@ -100,7 +100,7 @@ DeviceTable Core::deviceTable() const { return device().table(); } VolkDeviceTable const* Core::functions() const { return device().functions(); } VkDevice Core::vkDevice() const { return device().get(); } -void Core::reset() { +void Core::invalidate() { _queues.clear(); _device = nullptr; _physicalDevice = nullptr; diff --git a/cvk/src/base/device.cpp b/cvk/src/base/device.cpp index 8b73fc05..0aa76af6 100644 --- a/cvk/src/base/device.cpp +++ b/cvk/src/base/device.cpp @@ -58,7 +58,7 @@ void Device::destroy() { debug_check(*this); _handle.reset(); - reset(); + invalidate(); } Device::State Device::release() { @@ -68,7 +68,7 @@ Device::State Device::release() { .vkDevice = _handle.release(), .functionTable = std::move(_functionTable) }; - reset(); + invalidate(); return state; } @@ -88,7 +88,7 @@ void Device::destroy(VkDevice vkDevice) { log::msg("destroyed device"); } -void Device::reset() { +void Device::invalidate() { _queues.clear(); if(_functionTable == nullptr) _functionTable = std::make_unique(); @@ -159,7 +159,7 @@ void Device::createLogicalDevice(queue_families_queue_counts_t const& queueFamil auto const createResult = vkCreateDevice(_physicalDevice->get(), &createInfo, nullptr, &ptr); CTH_STABLE_ERR(createResult != VK_SUCCESS, "failed to create logical device") { - reset(); + invalidate(); throw cvk::vk_result_exception{createResult, details->exception()}; } diff --git a/cvk/src/base/instance.cpp b/cvk/src/base/instance.cpp index 5a987372..0e4a8057 100644 --- a/cvk/src/base/instance.cpp +++ b/cvk/src/base/instance.cpp @@ -93,7 +93,7 @@ void Instance::create(std::optional messengerConfig) { auto const createInstanceResult = vkCreateInstance(&createInfo, nullptr, &ptr); CTH_STABLE_ERR(createInstanceResult != VK_SUCCESS, "failed to create instance!") { - reset(); + invalidate(); throw cvk::vk_result_exception{createInstanceResult, details->exception()}; } @@ -111,7 +111,7 @@ void Instance::destroy() { _debugMessenger = nullptr; _handle.reset(); - reset(); + invalidate(); } @@ -211,7 +211,7 @@ void Instance::destroy(VkInstance vkInstance) { log::msg("destroyed instance"); } -void Instance::reset() { _debugMessenger = nullptr; } +void Instance::invalidate() { _debugMessenger = nullptr; } void Instance::loadInstanceFunctions(vk_not_null vkInstance) { volkLoadInstanceOnly(vkInstance.get()); } } diff --git a/cvk/src/base/queue/queue.cpp b/cvk/src/base/queue/queue.cpp index 86a0cf35..17607eb1 100644 --- a/cvk/src/base/queue/queue.cpp +++ b/cvk/src/base/queue/queue.cpp @@ -17,7 +17,7 @@ void Queue::wrap(State state) { void Queue::destroy() { debug_check(*this); - reset(); + invalidate(); } Queue::State Queue::release() { @@ -29,7 +29,7 @@ Queue::State Queue::release() { _queueIndex, std::move(_handleMtx), }; - reset(); + invalidate(); return state; } @@ -75,7 +75,7 @@ void Queue::wait() const { CVK_RESULT_STABLE_THROW(result != VK_SUCCESS, result, "failed to wait for queue") {} } -void Queue::reset() { +void Queue::invalidate() { _functions = nullptr; _handle = VK_NULL_HANDLE; _handleMtx = nullptr; diff --git a/cvk/src/data/img/sampler.cpp b/cvk/src/data/img/sampler.cpp index 24aae488..63787165 100644 --- a/cvk/src/data/img/sampler.cpp +++ b/cvk/src/data/img/sampler.cpp @@ -32,7 +32,7 @@ void Sampler::create() { auto const result = _core->functions()->vkCreateSampler(_core->vkDevice(), &info, nullptr, &ptr); CTH_STABLE_ERR(result != VK_SUCCESS, "failed to create sampler") { - reset(); + invalidate(); throw cvk::vk_result_exception{result, details->exception()}; } @@ -44,14 +44,14 @@ void Sampler::destroy() { destroy(_core->deviceTable(), _handle.get()); - reset(); + invalidate(); } Sampler::State Sampler::release() { debug_check(*this); State const state{_handle.get()}; - reset(); + invalidate(); return state; } @@ -61,5 +61,5 @@ void Sampler::destroy(DeviceTable table, VkSampler vkSampler) { table->vkDestroySampler(table.device(), vkSampler, nullptr); } -void Sampler::reset() { _handle = VK_NULL_HANDLE; } +void Sampler::invalidate() { _handle = VK_NULL_HANDLE; } } diff --git a/cvk/src/debug/debug_messenger.cpp b/cvk/src/debug/debug_messenger.cpp index b7b34132..65875c1c 100644 --- a/cvk/src/debug/debug_messenger.cpp +++ b/cvk/src/debug/debug_messenger.cpp @@ -26,7 +26,7 @@ void DebugMessenger::create() { auto const createResult = vkCreateDebugUtilsMessengerEXT(_handle.vkInstance(), &info, nullptr, &ptr); CTH_STABLE_ERR(createResult != VK_SUCCESS, "failed to set up debug messenger") { - reset(); + invalidate(); throw cvk::vk_result_exception{createResult, details->exception()}; } @@ -38,7 +38,7 @@ void DebugMessenger::destroy() { _handle.reset(); - reset(); + invalidate(); } @@ -53,11 +53,11 @@ DebugMessenger::State DebugMessenger::release() { debug_check(*this); State const state{_handle.release()}; - reset(); + invalidate(); return state; } -void DebugMessenger::reset() {} +void DebugMessenger::invalidate() {} } // namespace cvk namespace cvk::dev { diff --git a/cvk/src/render/pipeline/layout/descriptor_set_layout.cpp b/cvk/src/render/pipeline/layout/descriptor_set_layout.cpp index 19cc14ed..8afedeec 100644 --- a/cvk/src/render/pipeline/layout/descriptor_set_layout.cpp +++ b/cvk/src/render/pipeline/layout/descriptor_set_layout.cpp @@ -41,7 +41,7 @@ void DescriptorSetLayout::create() { auto const result = _core->functions()->vkCreateDescriptorSetLayout(_core->vkDevice(), &info, nullptr, &ptr); CTH_STABLE_ERR(result != VK_SUCCESS, "failed to create descriptor set layout") { - reset(); + invalidate(); throw cvk::vk_result_exception{result, details->exception()}; } @@ -53,14 +53,14 @@ void DescriptorSetLayout::destroy() { destroy(_core->deviceTable(), _handle.get()); - reset(); + invalidate(); } DescriptorSetLayout::State DescriptorSetLayout::release() { debug_check(*this); State const state{_handle.get()}; - reset(); + invalidate(); return state; } @@ -77,7 +77,7 @@ VkDescriptorSetLayoutBinding const* DescriptorSetLayout::binding(uint32_t bindin return it != _bindings.end() && it->binding == binding ? std::to_address(it) : nullptr; } -void DescriptorSetLayout::reset() { _handle = VK_NULL_HANDLE; } +void DescriptorSetLayout::invalidate() { _handle = VK_NULL_HANDLE; } } namespace cvk { diff --git a/cvk/src/render/pipeline/layout/pipeline_layout.cpp b/cvk/src/render/pipeline/layout/pipeline_layout.cpp index 16aed6c7..d7fb0dd0 100644 --- a/cvk/src/render/pipeline/layout/pipeline_layout.cpp +++ b/cvk/src/render/pipeline/layout/pipeline_layout.cpp @@ -57,7 +57,7 @@ void PipelineLayout::create() { auto const result = _core->functions()->vkCreatePipelineLayout(_core->vkDevice(), &info, nullptr, &ptr); CTH_STABLE_ERR(result != VK_SUCCESS, "failed to create pipeline layout") { - reset(); + invalidate(); throw cvk::vk_result_exception{result, details->exception()}; } @@ -69,14 +69,14 @@ void PipelineLayout::destroy() { destroy(_core->deviceTable(), _handle.get()); - reset(); + invalidate(); } PipelineLayout::State PipelineLayout::release() { debug_check(*this); State const state{_handle.get()}; - reset(); + invalidate(); return state; } @@ -86,5 +86,5 @@ void PipelineLayout::destroy(DeviceTable table, VkPipelineLayout vkLayout) { table->vkDestroyPipelineLayout(table.device(), vkLayout, nullptr); } -void PipelineLayout::reset() { _handle = VK_NULL_HANDLE; } +void PipelineLayout::invalidate() { _handle = VK_NULL_HANDLE; } } diff --git a/cvk/src/render/pipeline/pipeline.cpp b/cvk/src/render/pipeline/pipeline.cpp index 31b612e1..757be627 100644 --- a/cvk/src/render/pipeline/pipeline.cpp +++ b/cvk/src/render/pipeline/pipeline.cpp @@ -131,7 +131,7 @@ void Pipeline::create() { ); CTH_STABLE_ERR(result != VK_SUCCESS, "failed to create graphics pipeline") { - reset(); + invalidate(); throw cvk::vk_result_exception{result, details->exception()}; } @@ -143,14 +143,14 @@ void Pipeline::destroy() { destroy(_core->deviceTable(), _handle.get()); - reset(); + invalidate(); } Pipeline::State Pipeline::release() { debug_check(*this); State const state{_handle.get()}; - reset(); + invalidate(); return state; } @@ -160,5 +160,5 @@ void Pipeline::destroy(DeviceTable table, VkPipeline vkPipeline) { table->vkDestroyPipeline(table.device(), vkPipeline, nullptr); } -void Pipeline::reset() { _handle = VK_NULL_HANDLE; } +void Pipeline::invalidate() { _handle = VK_NULL_HANDLE; } } diff --git a/cvk/src/render/pipeline/shader/shader.cpp b/cvk/src/render/pipeline/shader/shader.cpp index e405210e..8aaff8ea 100644 --- a/cvk/src/render/pipeline/shader/shader.cpp +++ b/cvk/src/render/pipeline/shader/shader.cpp @@ -65,7 +65,7 @@ void Shader::create(std::span spv) { auto const result = _core->functions()->vkCreateShaderModule(_core->vkDevice(), &info, nullptr, &ptr); CTH_STABLE_ERR(result != VK_SUCCESS, "failed to create shader module") { - reset(); + invalidate(); throw cvk::vk_result_exception{result, details->exception()}; } @@ -82,14 +82,14 @@ void Shader::destroy() { destroy(_core->deviceTable(), _handle.get()); - reset(); + invalidate(); } Shader::State Shader::release() { debug_check(*this); State const state{_handle.get()}; - reset(); + invalidate(); return state; } @@ -188,5 +188,5 @@ void Shader::destroy(DeviceTable table, VkShaderModule vkShader) { table->vkDestroyShaderModule(table.device(), vkShader, nullptr); } -void Shader::reset() { _handle = VK_NULL_HANDLE; } +void Shader::invalidate() { _handle = VK_NULL_HANDLE; } } diff --git a/cvk/src/render/sync/fence.cpp b/cvk/src/render/sync/fence.cpp index 41867699..058f8efe 100644 --- a/cvk/src/render/sync/fence.cpp +++ b/cvk/src/render/sync/fence.cpp @@ -49,7 +49,7 @@ void Fence::create(VkFenceCreateFlags flags, VkExternalFenceHandleTypeFlags expo auto const result = core().functions()->vkCreateFence(core().vkDevice(), &info, nullptr, &ptr); CTH_STABLE_ERR(result != VK_SUCCESS, "failed to create fence") { - resetState(); + invalidate(); throw cvk::vk_result_exception{result, details->exception()}; } @@ -62,14 +62,14 @@ void Fence::destroy() { _handle.reset(); - resetState(); + invalidate(); } Fence::State Fence::release() { debug_check(*this); State const state{_handle.release(), _exportTypes}; - resetState(); + invalidate(); return state; } @@ -166,5 +166,5 @@ void Fence::destroy(DeviceTable table, VkFence vkFence) { table->vkDestroyFence(table.device(), vkFence, nullptr); } -void Fence::resetState() { _exportTypes = 0; } +void Fence::invalidate() { _exportTypes = 0; } } diff --git a/cvk/src/render/sync/semaphore.cpp b/cvk/src/render/sync/semaphore.cpp index d181dc63..551d1582 100644 --- a/cvk/src/render/sync/semaphore.cpp +++ b/cvk/src/render/sync/semaphore.cpp @@ -31,14 +31,14 @@ void Semaphore::destroy() { destroy(_core->deviceTable(), _handle.get()); - reset(); + invalidate(); } Semaphore::State Semaphore::release() { debug_check(*this); State const state{_handle.get()}; - reset(); + invalidate(); return state; } @@ -55,12 +55,12 @@ void Semaphore::create(VkSemaphoreCreateInfo const& info) { auto const result = _core->functions()->vkCreateSemaphore(_core->vkDevice(), &info, nullptr, &ptr); CTH_STABLE_ERR(result != VK_SUCCESS, "failed to create semaphore") { - reset(); + invalidate(); throw cvk::vk_result_exception{result, details->exception()}; } _handle = ptr; } -void Semaphore::reset() { _handle = VK_NULL_HANDLE; } +void Semaphore::invalidate() { _handle = VK_NULL_HANDLE; } } diff --git a/cvk/src/surface/surface.cpp b/cvk/src/surface/surface.cpp index afc7d8a1..93e18d59 100644 --- a/cvk/src/surface/surface.cpp +++ b/cvk/src/surface/surface.cpp @@ -44,14 +44,14 @@ void Surface::destroy() { _handle.reset(); - reset(); + invalidate(); } Surface::State Surface::release() { debug_check(*this); State const state{_handle.release()}; - reset(); + invalidate(); return state; } @@ -158,5 +158,5 @@ void Surface::destroy(vk_not_null vkInstance, VkSurfaceKHR vkSurface log::msg("destroyed surface"); } -void Surface::reset() {} +void Surface::invalidate() {} } From 6f56e44c76b58261cd2a4e008d3ec856301b6620 Mon Sep 17 00:00:00 2001 From: lukas <89145039+codethinki@users.noreply.github.com> Date: Fri, 16 Oct 2026 18:16:04 +0200 Subject: [PATCH 025/135] cvk: every handle class on unique_handle, explicit deleters - unique_handle, no deleter detection - Semaphore, Shader, DescriptorSetLayout, PipelineLayout, Pipeline, Sampler converted, move assignment = default - TimelineSemaphore: move assignment = base + atomic counter - reconstruct_at is gone from cvk, AGENTS.md describes unique_handle instead - Instance::release() (the lifecycle function was missing) --- AGENTS.md | 14 ++-- cvk/incl/cvk/base/device.hpp | 2 +- cvk/incl/cvk/base/instance.hpp | 8 ++- cvk/incl/cvk/data/img/sampler.hpp | 11 ++- cvk/incl/cvk/debug/debug_messenger.hpp | 2 +- .../pipeline/layout/descriptor_set_layout.hpp | 13 ++-- .../pipeline/layout/pipeline_layout.hpp | 11 ++- cvk/incl/cvk/render/pipeline/pipeline.hpp | 11 ++- .../cvk/render/pipeline/shader/shader.hpp | 11 ++- cvk/incl/cvk/render/sync/fence.hpp | 2 +- cvk/incl/cvk/render/sync/semaphore.hpp | 11 ++- .../cvk/render/sync/timeline_semaphore.hpp | 6 +- cvk/incl/cvk/surface/surface.hpp | 2 +- cvk/incl/cvk/utility/unique_handle.hpp | 56 +--------------- cvk/src/base/instance.cpp | 8 +++ cvk/src/data/img/sampler.cpp | 14 ++-- .../pipeline/layout/descriptor_set_layout.cpp | 14 ++-- .../pipeline/layout/pipeline_layout.cpp | 14 ++-- cvk/src/render/pipeline/pipeline.cpp | 18 ++--- cvk/src/render/pipeline/shader/shader.cpp | 14 ++-- cvk/src/render/sync/semaphore.cpp | 14 ++-- cvk/tests/src/base/instance_test.cpp | 13 ++++ cvk/tests/src/utility/unique_handle_test.cpp | 67 +++++-------------- 23 files changed, 137 insertions(+), 199 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 47781f84..a518c35c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -158,10 +158,12 @@ public: - The first public section holds the API: types, constants, lifecycle, operations, accessors. - The last public section holds what every class has and readers rarely need: copy and move members, `debug_check` functions. - Classes that own a handle are move only. A class that differs (copyable, not movable, custom move) says so in its `@brief`. -- Their move assignment destroys the old handle: `X& operator=(X&& other) noexcept { return cth::reconstruct_at(*this, std::move(other)); }`. - `= default` leaks the old handle, `cth::move_ptr` does not destroy on assignment. -- A class derived from a handle class defines its move assignment with `cth::reconstruct_at` too. - `= default` calls the base move assignment on the base class subobject, there `reconstruct_at` is undefined behavior. +- They store the handle in `cvk::unique_handle` (`cvk/utility/unique_handle.hpp`): a `std::unique_ptr` with a deleter that holds the destroy context and calls the static `destroy`. + Deleters: `core_deleter` (the core), `instance_deleter` (the `VkInstance`), `handle_deleter` (no context) or a deleter derived from them. +- Handle and context go in together: `_handle.reset(ptr, core())`. A missing context argument does not compile. +- Move constructor and move assignment are `= default`. The move assignment destroys the old handle with the old context. +- A class that owns more than one object declares the objects that depend on its handle first. `= default` assigns in declaration order: `Instance` declares `_debugMessenger` before `_handle`. +- Classes that do not own their handle (`Queue`, `PhysicalDevice`) keep `cth::move_ptr`. After the class, in the same header: @@ -189,12 +191,12 @@ inline void Semaphore::debug_check(Semaphore const& semaphore) { | `destroy()` | destroys the Vulkan object now and invalidates, requires `created()` | | `optDestroy()` | calls `destroy()` if `created()` | | `release()` | gives the `State` to the caller and invalidates, does not destroy | -| `static destroy(...)` | destroys the raw handles, used by `destroy()` | +| `static destroy(...)` | destroys the raw handles, used by the deleter | | `invalidate()` | sets the members to the not created state, does not destroy | - The destructor calls `optDestroy()`. - `reset` keeps its std meaning (`std::unique_ptr::reset` destroys) and names Vulkan operations (`vkResetFences`). The lifecycle uses `invalidate()`. -- Device objects take `Core const&` in the base constructor and store `not_null _core`. +- Device objects take `Core const&` in the base constructor. The `core_deleter` of the handle stores it, `core()` returns it. - A parent (instance, core, device, pool, ...) does not move and is not destroyed while objects depend on it. Code does not have to support it. - A failed `create()` calls `invalidate()` before it throws. - `release()` exists so that an owner can use an object it must not destroy (for example swapchain images). diff --git a/cvk/incl/cvk/base/device.hpp b/cvk/incl/cvk/base/device.hpp index 3616b2d5..6822ec55 100644 --- a/cvk/incl/cvk/base/device.hpp +++ b/cvk/incl/cvk/base/device.hpp @@ -142,7 +142,7 @@ class Device { not_null _instance; not_null _physicalDevice; - unique_handle _handle; + unique_handle> _handle; std::unique_ptr _functionTable = std::make_unique(); std::vector _queues{}; diff --git a/cvk/incl/cvk/base/instance.hpp b/cvk/incl/cvk/base/instance.hpp index 27182a87..f364c5f8 100644 --- a/cvk/incl/cvk/base/instance.hpp +++ b/cvk/incl/cvk/base/instance.hpp @@ -100,6 +100,12 @@ class Instance { */ void optDestroy() { if(created()) destroy(); } + /** + * @brief releases ownership of the instance and the messenger, returns the state and invalidates + * @pre @ref created() + */ + State release(); + /** * @throws cth::except::default_exception reason: required extension missing */ @@ -139,7 +145,7 @@ class Instance { // declared before the handle: move assignment destroys the old messenger before the old instance std::unique_ptr _debugMessenger = nullptr; - unique_handle _handle; + unique_handle> _handle; static void loadInstanceFunctions(vk_not_null); diff --git a/cvk/incl/cvk/data/img/sampler.hpp b/cvk/incl/cvk/data/img/sampler.hpp index 3fdd6e4e..46d31300 100644 --- a/cvk/incl/cvk/data/img/sampler.hpp +++ b/cvk/incl/cvk/data/img/sampler.hpp @@ -4,11 +4,11 @@ #include "cvk/base/device_table.hpp" #include "cvk/utility/constants.hpp" #include "cvk/utility/types.hpp" +#include "cvk/utility/unique_handle.hpp" #include #include -#include namespace cvk { @@ -86,9 +86,9 @@ class Sampler { static void destroy(DeviceTable table, VkSampler); - [[nodiscard]] bool created() const { return _handle != VK_NULL_HANDLE; } + [[nodiscard]] bool created() const { return static_cast(_handle); } [[nodiscard]] VkSampler get() const { return _handle.get(); } - [[nodiscard]] Core const& core() const { return *_core; } + [[nodiscard]] Core const& core() const { return _handle.core(); } /** * @return the config, with the clamped anisotropy @@ -98,15 +98,14 @@ class Sampler { private: void invalidate(); - not_null _core; + unique_handle> _handle; Config _config; - move_ptr _handle = VK_NULL_HANDLE; public: Sampler(Sampler const& other) = delete; Sampler(Sampler&& other) noexcept = default; Sampler& operator=(Sampler const& other) = delete; - Sampler& operator=(Sampler&& other) noexcept { return cth::reconstruct_at(*this, std::move(other)); } + Sampler& operator=(Sampler&& other) noexcept = default; static void debug_check(Sampler const&); }; diff --git a/cvk/incl/cvk/debug/debug_messenger.hpp b/cvk/incl/cvk/debug/debug_messenger.hpp index d987101f..b3b6e974 100644 --- a/cvk/incl/cvk/debug/debug_messenger.hpp +++ b/cvk/incl/cvk/debug/debug_messenger.hpp @@ -101,7 +101,7 @@ class DebugMessenger { private: void invalidate(); - unique_handle _handle; + unique_handle> _handle; public: DebugMessenger(DebugMessenger const& other) = delete; diff --git a/cvk/incl/cvk/render/pipeline/layout/descriptor_set_layout.hpp b/cvk/incl/cvk/render/pipeline/layout/descriptor_set_layout.hpp index 4b8c7a50..07cb613d 100644 --- a/cvk/incl/cvk/render/pipeline/layout/descriptor_set_layout.hpp +++ b/cvk/incl/cvk/render/pipeline/layout/descriptor_set_layout.hpp @@ -2,11 +2,11 @@ #include "cvk/base/device_table.hpp" #include "cvk/utility/constants.hpp" #include "cvk/utility/types.hpp" +#include "cvk/utility/unique_handle.hpp" #include #include -#include #include #include @@ -86,9 +86,9 @@ class DescriptorSetLayout { static void destroy(DeviceTable table, VkDescriptorSetLayout); - [[nodiscard]] bool created() const { return _handle != VK_NULL_HANDLE; } + [[nodiscard]] bool created() const { return static_cast(_handle); } [[nodiscard]] VkDescriptorSetLayout get() const { return _handle.get(); } - [[nodiscard]] Core const& core() const { return *_core; } + [[nodiscard]] Core const& core() const { return _handle.core(); } /** * @return bindings, sorted by binding number @@ -103,17 +103,14 @@ class DescriptorSetLayout { private: void invalidate(); - not_null _core; + unique_handle> _handle; std::vector _bindings; - move_ptr _handle = VK_NULL_HANDLE; public: DescriptorSetLayout(DescriptorSetLayout const& other) = delete; DescriptorSetLayout(DescriptorSetLayout&& other) noexcept = default; DescriptorSetLayout& operator=(DescriptorSetLayout const& other) = delete; - DescriptorSetLayout& operator=(DescriptorSetLayout&& other) noexcept { - return cth::reconstruct_at(*this, std::move(other)); - } + DescriptorSetLayout& operator=(DescriptorSetLayout&& other) noexcept = default; static void debug_check(DescriptorSetLayout const&); }; diff --git a/cvk/incl/cvk/render/pipeline/layout/pipeline_layout.hpp b/cvk/incl/cvk/render/pipeline/layout/pipeline_layout.hpp index 2c557d72..739c51a2 100644 --- a/cvk/incl/cvk/render/pipeline/layout/pipeline_layout.hpp +++ b/cvk/incl/cvk/render/pipeline/layout/pipeline_layout.hpp @@ -2,11 +2,11 @@ #include "cvk/base/device_table.hpp" #include "cvk/utility/constants.hpp" #include "cvk/utility/types.hpp" +#include "cvk/utility/unique_handle.hpp" #include #include -#include #include @@ -88,23 +88,22 @@ class PipelineLayout { static void destroy(DeviceTable table, VkPipelineLayout); - [[nodiscard]] bool created() const { return _handle != VK_NULL_HANDLE; } + [[nodiscard]] bool created() const { return static_cast(_handle); } [[nodiscard]] VkPipelineLayout get() const { return _handle.get(); } - [[nodiscard]] Core const& core() const { return *_core; } + [[nodiscard]] Core const& core() const { return _handle.core(); } private: void invalidate(); - not_null _core; + unique_handle> _handle; std::vector _vkSetLayouts; std::vector _pushConstantRanges; - move_ptr _handle = VK_NULL_HANDLE; public: PipelineLayout(PipelineLayout const& other) = delete; PipelineLayout(PipelineLayout&& other) noexcept = default; PipelineLayout& operator=(PipelineLayout const& other) = delete; - PipelineLayout& operator=(PipelineLayout&& other) noexcept { return cth::reconstruct_at(*this, std::move(other)); } + PipelineLayout& operator=(PipelineLayout&& other) noexcept = default; static void debug_check(PipelineLayout const&); }; diff --git a/cvk/incl/cvk/render/pipeline/pipeline.hpp b/cvk/incl/cvk/render/pipeline/pipeline.hpp index cf502e01..ecc8f169 100644 --- a/cvk/incl/cvk/render/pipeline/pipeline.hpp +++ b/cvk/incl/cvk/render/pipeline/pipeline.hpp @@ -4,11 +4,11 @@ #include "cvk/base/device_table.hpp" #include "cvk/utility/constants.hpp" #include "cvk/utility/types.hpp" +#include "cvk/utility/unique_handle.hpp" #include #include -#include @@ -105,7 +105,7 @@ class Pipeline { static void destroy(DeviceTable table, VkPipeline); - [[nodiscard]] bool created() const { return _handle != VK_NULL_HANDLE; } + [[nodiscard]] bool created() const { return static_cast(_handle); } [[nodiscard]] VkPipeline get() const { return _handle.get(); } [[nodiscard]] VkPipelineLayout layout() const { return _vkLayout.get(); } @@ -114,22 +114,21 @@ class Pipeline { */ [[nodiscard]] VkPipeline parent() const { return _vkParent; } [[nodiscard]] Config const& config() const { return _config; } - [[nodiscard]] Core const& core() const { return *_core; } + [[nodiscard]] Core const& core() const { return _handle.core(); } private: void invalidate(); - not_null _core; + unique_handle> _handle; vk_not_null _vkLayout; VkPipeline _vkParent = VK_NULL_HANDLE; Config _config; - move_ptr _handle = VK_NULL_HANDLE; public: Pipeline(Pipeline const& other) = delete; Pipeline(Pipeline&& other) noexcept = default; Pipeline& operator=(Pipeline const& other) = delete; - Pipeline& operator=(Pipeline&& other) noexcept { return cth::reconstruct_at(*this, std::move(other)); } + Pipeline& operator=(Pipeline&& other) noexcept = default; static void debug_check(Pipeline const&); }; diff --git a/cvk/incl/cvk/render/pipeline/shader/shader.hpp b/cvk/incl/cvk/render/pipeline/shader/shader.hpp index f50ff02c..e7db73f6 100644 --- a/cvk/incl/cvk/render/pipeline/shader/shader.hpp +++ b/cvk/incl/cvk/render/pipeline/shader/shader.hpp @@ -1,11 +1,11 @@ #pragma once #include "cvk/base/device_table.hpp" #include "cvk/utility/types.hpp" +#include "cvk/utility/unique_handle.hpp" #include #include -#include #include #include @@ -155,23 +155,22 @@ class Shader { static void destroy(DeviceTable table, VkShaderModule); - [[nodiscard]] bool created() const { return _handle != VK_NULL_HANDLE; } + [[nodiscard]] bool created() const { return static_cast(_handle); } [[nodiscard]] VkShaderModule get() const { return _handle.get(); } [[nodiscard]] VkShaderStageFlagBits stage() const { return _stage; } - [[nodiscard]] Core const& core() const { return *_core; } + [[nodiscard]] Core const& core() const { return _handle.core(); } private: void invalidate(); - not_null _core; + unique_handle> _handle; VkShaderStageFlagBits _stage; - move_ptr _handle = VK_NULL_HANDLE; public: Shader(Shader const& other) = delete; Shader(Shader&& other) noexcept = default; Shader& operator=(Shader const& other) = delete; - Shader& operator=(Shader&& other) noexcept { return cth::reconstruct_at(*this, std::move(other)); } + Shader& operator=(Shader&& other) noexcept = default; static void debug_check(Shader const&); }; diff --git a/cvk/incl/cvk/render/sync/fence.hpp b/cvk/incl/cvk/render/sync/fence.hpp index d777e4d5..f24d454d 100644 --- a/cvk/incl/cvk/render/sync/fence.hpp +++ b/cvk/incl/cvk/render/sync/fence.hpp @@ -161,7 +161,7 @@ class Fence { private: void invalidate(); - unique_handle _handle; + unique_handle> _handle; VkExternalFenceHandleTypeFlags _exportTypes = 0; public: diff --git a/cvk/incl/cvk/render/sync/semaphore.hpp b/cvk/incl/cvk/render/sync/semaphore.hpp index 3e850971..b4beff61 100644 --- a/cvk/incl/cvk/render/sync/semaphore.hpp +++ b/cvk/incl/cvk/render/sync/semaphore.hpp @@ -2,11 +2,11 @@ #include "cvk/base/device_table.hpp" #include "cvk/utility/constants.hpp" #include "cvk/utility/types.hpp" +#include "cvk/utility/unique_handle.hpp" #include #include -#include namespace cvk { @@ -81,9 +81,9 @@ class Semaphore { static void destroy(DeviceTable table, VkSemaphore); - [[nodiscard]] bool created() const { return _handle != VK_NULL_HANDLE; } + [[nodiscard]] bool created() const { return static_cast(_handle); } [[nodiscard]] VkSemaphore get() const { return _handle.get(); } - [[nodiscard]] Core const& core() const { return *_core; } + [[nodiscard]] Core const& core() const { return _handle.core(); } protected: /** @@ -97,14 +97,13 @@ class Semaphore { private: void invalidate(); - not_null _core; - move_ptr _handle = VK_NULL_HANDLE; + unique_handle> _handle; public: Semaphore(Semaphore const& other) = delete; Semaphore(Semaphore&& other) noexcept = default; Semaphore& operator=(Semaphore const& other) = delete; - Semaphore& operator=(Semaphore&& other) noexcept { return cth::reconstruct_at(*this, std::move(other)); } + Semaphore& operator=(Semaphore&& other) noexcept = default; static void debug_check(Semaphore const&); }; diff --git a/cvk/incl/cvk/render/sync/timeline_semaphore.hpp b/cvk/incl/cvk/render/sync/timeline_semaphore.hpp index 189597ed..4c23079b 100644 --- a/cvk/incl/cvk/render/sync/timeline_semaphore.hpp +++ b/cvk/incl/cvk/render/sync/timeline_semaphore.hpp @@ -1,8 +1,6 @@ #pragma once #include "semaphore.hpp" -#include - #include #include @@ -126,7 +124,9 @@ class TimelineSemaphore : Semaphore { _value{other._value.exchange(0)} {} TimelineSemaphore& operator=(TimelineSemaphore const& other) = delete; TimelineSemaphore& operator=(TimelineSemaphore&& other) noexcept { - return cth::reconstruct_at(*this, std::move(other)); + Semaphore::operator=(std::move(other)); + _value = other._value.exchange(0); + return *this; } static void debug_check(TimelineSemaphore const& semaphore); diff --git a/cvk/incl/cvk/surface/surface.hpp b/cvk/incl/cvk/surface/surface.hpp index d5e60c61..be2f3366 100644 --- a/cvk/incl/cvk/surface/surface.hpp +++ b/cvk/incl/cvk/surface/surface.hpp @@ -115,7 +115,7 @@ class Surface { void invalidate(); Config _config; - unique_handle _handle; + unique_handle> _handle; public: Surface(Surface const& other) = delete; diff --git a/cvk/incl/cvk/utility/unique_handle.hpp b/cvk/incl/cvk/utility/unique_handle.hpp index ce340518..d6cfa9a5 100644 --- a/cvk/incl/cvk/utility/unique_handle.hpp +++ b/cvk/incl/cvk/utility/unique_handle.hpp @@ -21,25 +21,6 @@ namespace cvk::dev { } namespace cvk { -/** - * @brief T::destroy(DeviceTable, Handle) exists - */ -template -concept core_destroyable = requires(DeviceTable table, Handle handle) { T::destroy(table, handle); }; - -/** - * @brief T::destroy(VkInstance, Handle) exists - */ -template -concept instance_destroyable = requires(VkInstance vkInstance, Handle handle) { T::destroy(vkInstance, handle); }; - -/** - * @brief T::destroy(Handle) exists - */ -template -concept handle_destroyable = requires(Handle handle) { T::destroy(handle); }; - - /** * @brief deleter of device objects, context: the core * @details calls T::destroy(core.deviceTable(), handle) @@ -88,49 +69,16 @@ struct handle_deleter { void operator()(Handle handle) const { T::destroy(handle); } }; -namespace dev { - /** - * @brief T::destroy fits no default deleter - */ - struct no_default_deleter {}; -} - -/** - * @brief deleter that fits the static destroy of T - * @details first match: - * - T::destroy(DeviceTable, Handle) -> @ref core_deleter - * - T::destroy(VkInstance, Handle) -> @ref instance_deleter - * - T::destroy(Handle) -> @ref handle_deleter - * @attention inside T: declare T::destroy before the member that uses this, else no deleter fits - */ -template -using default_deleter_t = std::conditional_t< - core_destroyable, - core_deleter, - std::conditional_t< - instance_destroyable, - instance_deleter, - std::conditional_t, handle_deleter, dev::no_default_deleter> - > ->; - - /** * @brief owning Vulkan handle, std::unique_ptr with a deleter that holds the destroy context - * @tparam T owner class, its static destroy picks the default deleter * @tparam Handle Vulkan handle, or Deleter::pointer for handles that are not pointers - * @tparam Deleter custom deleters derive from @ref core_deleter, @ref instance_deleter or @ref handle_deleter + * @tparam Deleter @ref core_deleter, @ref instance_deleter, @ref handle_deleter or a deleter derived from them * @details a handle comes only together with its context: the constructors and reset() construct the deleter, * a missing context argument does not compile, * move assignment destroys the old handle with the old context, then takes handle and context */ -template> +template class unique_handle { - static_assert( - !std::same_as, - "T::destroy fits no default deleter: declare it before the member or supply a deleter" - ); - using ptr_t = std::unique_ptr, Deleter>; static_assert(std::same_as, "Handle must be a pointer or Deleter::pointer"); diff --git a/cvk/src/base/instance.cpp b/cvk/src/base/instance.cpp index 0e4a8057..81199984 100644 --- a/cvk/src/base/instance.cpp +++ b/cvk/src/base/instance.cpp @@ -114,6 +114,14 @@ void Instance::destroy() { invalidate(); } +Instance::State Instance::release() { + debug_check(*this); + + State state{_handle.release(), std::move(_debugMessenger)}; + invalidate(); + return state; +} + void Instance::checkInstanceExtensionSupport() { vector missingExtensions{}; diff --git a/cvk/src/data/img/sampler.cpp b/cvk/src/data/img/sampler.cpp index 63787165..f4b103a3 100644 --- a/cvk/src/data/img/sampler.cpp +++ b/cvk/src/data/img/sampler.cpp @@ -8,7 +8,7 @@ namespace cvk { -Sampler::Sampler(Core const& core, Config const& config) : _core{&core}, _config{config} { +Sampler::Sampler(Core const& core, Config const& config) : _handle{core}, _config{config} { Core::debug_check(core); _config.maxAnisotropy = std::min(_config.maxAnisotropy, core.physicalDevice().limits().maxSamplerAnisotropy); } @@ -20,7 +20,7 @@ Sampler::Sampler(Core const& core, Config const& config, create_t) : Sampler{cor void Sampler::wrap(State const& state) { optDestroy(); - _handle = state.vkSampler.get(); + _handle.reset(state.vkSampler.get(), core()); } void Sampler::create() { @@ -29,20 +29,20 @@ void Sampler::create() { auto const info = _config.createInfo(); VkSampler ptr = VK_NULL_HANDLE; - auto const result = _core->functions()->vkCreateSampler(_core->vkDevice(), &info, nullptr, &ptr); + auto const result = core().functions()->vkCreateSampler(core().vkDevice(), &info, nullptr, &ptr); CTH_STABLE_ERR(result != VK_SUCCESS, "failed to create sampler") { invalidate(); throw cvk::vk_result_exception{result, details->exception()}; } - _handle = ptr; + _handle.reset(ptr, core()); } void Sampler::destroy() { debug_check(*this); - destroy(_core->deviceTable(), _handle.get()); + _handle.reset(); invalidate(); } @@ -50,7 +50,7 @@ void Sampler::destroy() { Sampler::State Sampler::release() { debug_check(*this); - State const state{_handle.get()}; + State const state{_handle.release()}; invalidate(); return state; } @@ -61,5 +61,5 @@ void Sampler::destroy(DeviceTable table, VkSampler vkSampler) { table->vkDestroySampler(table.device(), vkSampler, nullptr); } -void Sampler::invalidate() { _handle = VK_NULL_HANDLE; } +void Sampler::invalidate() {} } diff --git a/cvk/src/render/pipeline/layout/descriptor_set_layout.cpp b/cvk/src/render/pipeline/layout/descriptor_set_layout.cpp index 8afedeec..b828ec0d 100644 --- a/cvk/src/render/pipeline/layout/descriptor_set_layout.cpp +++ b/cvk/src/render/pipeline/layout/descriptor_set_layout.cpp @@ -7,7 +7,7 @@ namespace cvk { -DescriptorSetLayout::DescriptorSetLayout(Core const& core, Config config) : _core{&core}, +DescriptorSetLayout::DescriptorSetLayout(Core const& core, Config config) : _handle{core}, _bindings{std::move(config.bindings)} { Core::debug_check(core); } DescriptorSetLayout::DescriptorSetLayout(Core const& core, Config config, State const& state) : DescriptorSetLayout{ @@ -23,7 +23,7 @@ DescriptorSetLayout::DescriptorSetLayout(Core const& core, Config config, create void DescriptorSetLayout::wrap(State const& state) { optDestroy(); - _handle = state.vkLayout.get(); + _handle.reset(state.vkLayout.get(), core()); } void DescriptorSetLayout::create() { @@ -38,20 +38,20 @@ void DescriptorSetLayout::create() { }; VkDescriptorSetLayout ptr = VK_NULL_HANDLE; - auto const result = _core->functions()->vkCreateDescriptorSetLayout(_core->vkDevice(), &info, nullptr, &ptr); + auto const result = core().functions()->vkCreateDescriptorSetLayout(core().vkDevice(), &info, nullptr, &ptr); CTH_STABLE_ERR(result != VK_SUCCESS, "failed to create descriptor set layout") { invalidate(); throw cvk::vk_result_exception{result, details->exception()}; } - _handle = ptr; + _handle.reset(ptr, core()); } void DescriptorSetLayout::destroy() { debug_check(*this); - destroy(_core->deviceTable(), _handle.get()); + _handle.reset(); invalidate(); } @@ -59,7 +59,7 @@ void DescriptorSetLayout::destroy() { DescriptorSetLayout::State DescriptorSetLayout::release() { debug_check(*this); - State const state{_handle.get()}; + State const state{_handle.release()}; invalidate(); return state; } @@ -77,7 +77,7 @@ VkDescriptorSetLayoutBinding const* DescriptorSetLayout::binding(uint32_t bindin return it != _bindings.end() && it->binding == binding ? std::to_address(it) : nullptr; } -void DescriptorSetLayout::invalidate() { _handle = VK_NULL_HANDLE; } +void DescriptorSetLayout::invalidate() {} } namespace cvk { diff --git a/cvk/src/render/pipeline/layout/pipeline_layout.cpp b/cvk/src/render/pipeline/layout/pipeline_layout.cpp index d7fb0dd0..a04d4046 100644 --- a/cvk/src/render/pipeline/layout/pipeline_layout.cpp +++ b/cvk/src/render/pipeline/layout/pipeline_layout.cpp @@ -9,7 +9,7 @@ namespace cvk { -PipelineLayout::PipelineLayout(Core const& core, Config const& config) : _core{&core}, +PipelineLayout::PipelineLayout(Core const& core, Config const& config) : _handle{core}, _pushConstantRanges{config.pushConstantRanges} { Core::debug_check(core); auto const maxSets = core.physicalDevice().limits().maxBoundDescriptorSets; @@ -37,7 +37,7 @@ PipelineLayout::PipelineLayout(Core const& core, Config const& config, create_t) void PipelineLayout::wrap(State const& state) { optDestroy(); - _handle = state.vkLayout.get(); + _handle.reset(state.vkLayout.get(), core()); } void PipelineLayout::create() { @@ -54,20 +54,20 @@ void PipelineLayout::create() { }; VkPipelineLayout ptr = VK_NULL_HANDLE; - auto const result = _core->functions()->vkCreatePipelineLayout(_core->vkDevice(), &info, nullptr, &ptr); + auto const result = core().functions()->vkCreatePipelineLayout(core().vkDevice(), &info, nullptr, &ptr); CTH_STABLE_ERR(result != VK_SUCCESS, "failed to create pipeline layout") { invalidate(); throw cvk::vk_result_exception{result, details->exception()}; } - _handle = ptr; + _handle.reset(ptr, core()); } void PipelineLayout::destroy() { debug_check(*this); - destroy(_core->deviceTable(), _handle.get()); + _handle.reset(); invalidate(); } @@ -75,7 +75,7 @@ void PipelineLayout::destroy() { PipelineLayout::State PipelineLayout::release() { debug_check(*this); - State const state{_handle.get()}; + State const state{_handle.release()}; invalidate(); return state; } @@ -86,5 +86,5 @@ void PipelineLayout::destroy(DeviceTable table, VkPipelineLayout vkLayout) { table->vkDestroyPipelineLayout(table.device(), vkLayout, nullptr); } -void PipelineLayout::invalidate() { _handle = VK_NULL_HANDLE; } +void PipelineLayout::invalidate() {} } diff --git a/cvk/src/render/pipeline/pipeline.cpp b/cvk/src/render/pipeline/pipeline.cpp index 757be627..ecb7a1ab 100644 --- a/cvk/src/render/pipeline/pipeline.cpp +++ b/cvk/src/render/pipeline/pipeline.cpp @@ -6,7 +6,7 @@ namespace cvk { -Pipeline::Pipeline(Core const& core, PipelineLayout const& layout, Config config) : _core{&core}, +Pipeline::Pipeline(Core const& core, PipelineLayout const& layout, Config config) : _handle{core}, _vkLayout{layout.get()}, _config{std::move(config)} { Core::debug_check(core); } @@ -22,7 +22,7 @@ Pipeline::Pipeline(Core const& core, PipelineLayout const& layout, Config config std::move(config) } { create(); } -Pipeline::Pipeline(Core const& core, Pipeline const& parent, Config config) : _core{&core}, +Pipeline::Pipeline(Core const& core, Pipeline const& parent, Config config) : _handle{core}, _vkLayout{parent.layout()}, _vkParent{parent.get()}, _config{std::move(config)} { @@ -43,7 +43,7 @@ Pipeline::Pipeline(Core const& core, Pipeline const& parent, Config config, crea void Pipeline::wrap(State const& state) { optDestroy(); - _handle = state.vkPipeline.get(); + _handle.reset(state.vkPipeline.get(), core()); } void Pipeline::create() { @@ -121,8 +121,8 @@ void Pipeline::create() { }; VkPipeline ptr = VK_NULL_HANDLE; - auto const result = _core->functions()->vkCreateGraphicsPipelines( - _core->vkDevice(), + auto const result = core().functions()->vkCreateGraphicsPipelines( + core().vkDevice(), VK_NULL_HANDLE, 1, &info, @@ -135,13 +135,13 @@ void Pipeline::create() { throw cvk::vk_result_exception{result, details->exception()}; } - _handle = ptr; + _handle.reset(ptr, core()); } void Pipeline::destroy() { debug_check(*this); - destroy(_core->deviceTable(), _handle.get()); + _handle.reset(); invalidate(); } @@ -149,7 +149,7 @@ void Pipeline::destroy() { Pipeline::State Pipeline::release() { debug_check(*this); - State const state{_handle.get()}; + State const state{_handle.release()}; invalidate(); return state; } @@ -160,5 +160,5 @@ void Pipeline::destroy(DeviceTable table, VkPipeline vkPipeline) { table->vkDestroyPipeline(table.device(), vkPipeline, nullptr); } -void Pipeline::invalidate() { _handle = VK_NULL_HANDLE; } +void Pipeline::invalidate() {} } diff --git a/cvk/src/render/pipeline/shader/shader.cpp b/cvk/src/render/pipeline/shader/shader.cpp index 8aaff8ea..7ef9ae5b 100644 --- a/cvk/src/render/pipeline/shader/shader.cpp +++ b/cvk/src/render/pipeline/shader/shader.cpp @@ -13,7 +13,7 @@ namespace cvk { -Shader::Shader(Core const& core, VkShaderStageFlagBits stage) : _core{&core}, _stage{stage} { +Shader::Shader(Core const& core, VkShaderStageFlagBits stage) : _handle{core}, _stage{stage} { Core::debug_check(core); } @@ -45,7 +45,7 @@ Shader::Shader( void Shader::wrap(State const& state) { optDestroy(); - _handle = state.vkShader.get(); + _handle.reset(state.vkShader.get(), core()); } void Shader::create(std::span spv) { @@ -62,14 +62,14 @@ void Shader::create(std::span spv) { }; VkShaderModule ptr = VK_NULL_HANDLE; - auto const result = _core->functions()->vkCreateShaderModule(_core->vkDevice(), &info, nullptr, &ptr); + auto const result = core().functions()->vkCreateShaderModule(core().vkDevice(), &info, nullptr, &ptr); CTH_STABLE_ERR(result != VK_SUCCESS, "failed to create shader module") { invalidate(); throw cvk::vk_result_exception{result, details->exception()}; } - _handle = ptr; + _handle.reset(ptr, core()); } void Shader::create(path_t const& spvPath) { @@ -80,7 +80,7 @@ void Shader::create(path_t const& spvPath) { void Shader::destroy() { debug_check(*this); - destroy(_core->deviceTable(), _handle.get()); + _handle.reset(); invalidate(); } @@ -88,7 +88,7 @@ void Shader::destroy() { Shader::State Shader::release() { debug_check(*this); - State const state{_handle.get()}; + State const state{_handle.release()}; invalidate(); return state; } @@ -188,5 +188,5 @@ void Shader::destroy(DeviceTable table, VkShaderModule vkShader) { table->vkDestroyShaderModule(table.device(), vkShader, nullptr); } -void Shader::invalidate() { _handle = VK_NULL_HANDLE; } +void Shader::invalidate() {} } diff --git a/cvk/src/render/sync/semaphore.cpp b/cvk/src/render/sync/semaphore.cpp index 551d1582..43dab7e7 100644 --- a/cvk/src/render/sync/semaphore.cpp +++ b/cvk/src/render/sync/semaphore.cpp @@ -5,7 +5,7 @@ namespace cvk { -Semaphore::Semaphore(Core const& core) : _core{&core} { Core::debug_check(core); } +Semaphore::Semaphore(Core const& core) : _handle{core} { Core::debug_check(core); } Semaphore::Semaphore(Core const& core, State const& state) : Semaphore{core} { wrap(state); } @@ -14,7 +14,7 @@ Semaphore::Semaphore(Core const& core, create_t) : Semaphore{core} { create(); } void Semaphore::wrap(State const& state) { optDestroy(); - _handle = state.vkSemaphore.get(); + _handle.reset(state.vkSemaphore.get(), core()); } void Semaphore::create() { @@ -29,7 +29,7 @@ void Semaphore::create() { void Semaphore::destroy() { debug_check(*this); - destroy(_core->deviceTable(), _handle.get()); + _handle.reset(); invalidate(); } @@ -37,7 +37,7 @@ void Semaphore::destroy() { Semaphore::State Semaphore::release() { debug_check(*this); - State const state{_handle.get()}; + State const state{_handle.release()}; invalidate(); return state; } @@ -52,15 +52,15 @@ void Semaphore::create(VkSemaphoreCreateInfo const& info) { optDestroy(); VkSemaphore ptr = VK_NULL_HANDLE; - auto const result = _core->functions()->vkCreateSemaphore(_core->vkDevice(), &info, nullptr, &ptr); + auto const result = core().functions()->vkCreateSemaphore(core().vkDevice(), &info, nullptr, &ptr); CTH_STABLE_ERR(result != VK_SUCCESS, "failed to create semaphore") { invalidate(); throw cvk::vk_result_exception{result, details->exception()}; } - _handle = ptr; + _handle.reset(ptr, core()); } -void Semaphore::invalidate() { _handle = VK_NULL_HANDLE; } +void Semaphore::invalidate() {} } diff --git a/cvk/tests/src/base/instance_test.cpp b/cvk/tests/src/base/instance_test.cpp index 121379e7..a6c122eb 100644 --- a/cvk/tests/src/base/instance_test.cpp +++ b/cvk/tests/src/base/instance_test.cpp @@ -107,4 +107,17 @@ CVK_TEST(Instance, move_assignment_takes_the_other_instance) { } EXPECT_EQ(test::validation_errors(), 0); } + +CVK_TEST(Instance, release_and_wrap) { + Instance instance{APP_NAME, NO_EXTENSIONS, std::nullopt}; + auto const vkInstance = instance.get(); + + auto state = instance.release(); + EXPECT_FALSE(instance.created()); + EXPECT_EQ(state.vkInstance.get(), vkInstance); + + Instance const wrapped{APP_NAME, NO_EXTENSIONS, std::move(state)}; + ASSERT_TRUE(wrapped.created()); + EXPECT_EQ(wrapped.get(), vkInstance); +} } diff --git a/cvk/tests/src/utility/unique_handle_test.cpp b/cvk/tests/src/utility/unique_handle_test.cpp index b01eae75..cea5fb25 100644 --- a/cvk/tests/src/utility/unique_handle_test.cpp +++ b/cvk/tests/src/utility/unique_handle_test.cpp @@ -3,17 +3,8 @@ #include "test.hpp" #include "validation.hpp" -#include "cvk/base/device.hpp" #include "cvk/base/instance.hpp" -#include "cvk/debug/debug_messenger.hpp" -#include "cvk/data/img/sampler.hpp" -#include "cvk/render/pipeline/layout/descriptor_set_layout.hpp" -#include "cvk/render/pipeline/layout/pipeline_layout.hpp" -#include "cvk/render/pipeline/pipeline.hpp" -#include "cvk/render/pipeline/shader/shader.hpp" #include "cvk/render/sync/fence.hpp" -#include "cvk/render/sync/semaphore.hpp" -#include "cvk/render/sync/timeline_semaphore.hpp" #include "cvk/surface/surface.hpp" #include "cvk/utility/constants.hpp" #include "cvk/utility/unique_handle.hpp" @@ -97,46 +88,22 @@ namespace { concept deleter_assignable = requires(H& handle, typename H::deleter_t deleter) { handle.deleter() = deleter; }; } -CVK_TEST(unique_handle, default_deleters_of_the_classes) { - static_assert(std::same_as, core_deleter>); - static_assert(std::same_as, core_deleter>); - static_assert( - std::same_as, core_deleter> - ); - static_assert(std::same_as, core_deleter>); - static_assert( - std::same_as, core_deleter> - ); - static_assert(std::same_as, core_deleter>); - static_assert(std::same_as, core_deleter>); - static_assert(std::same_as, core_deleter>); - - static_assert(std::same_as, instance_deleter>); - static_assert( - std::same_as, instance_deleter> - ); - - static_assert(std::same_as, handle_deleter>); - - static_assert(std::same_as, handle_deleter>); -} - CVK_TEST(unique_handle, a_handle_needs_its_context) { - using fence_handle = unique_handle; + using fence_handle = unique_handle>; static_assert(!std::default_initializable); static_assert(!std::constructible_from); static_assert(!resettable); static_assert(resettable); - using surface_handle = unique_handle; + using surface_handle = unique_handle>; static_assert(!resettable); static_assert(resettable); - using instance_handle = unique_handle; + using instance_handle = unique_handle>; static_assert(std::default_initializable); static_assert(resettable); - using pool_handle = unique_handle; + using pool_handle = unique_handle; static_assert(!resettable); static_assert(resettable); @@ -145,15 +112,17 @@ CVK_TEST(unique_handle, a_handle_needs_its_context) { } CVK_TEST(unique_handle, size) { - static_assert(sizeof(unique_handle) == sizeof(VkInstance)); - static_assert(sizeof(unique_handle) == sizeof(Core const*) + sizeof(VkFence)); - static_assert(sizeof(unique_handle) == sizeof(VkInstance) + sizeof(VkSurfaceKHR)); + static_assert(sizeof(unique_handle>) == sizeof(VkInstance)); + static_assert(sizeof(unique_handle>) == sizeof(Core const*) + sizeof(VkFence)); + static_assert( + sizeof(unique_handle>) == sizeof(VkInstance) + sizeof(VkSurfaceKHR) + ); } CVK_TEST(unique_handle, core_deleter) { test::Context const context{}; - unique_handle handle{context.core}; + unique_handle> handle{context.core}; EXPECT_FALSE(handle); EXPECT_EQ(&handle.core(), &context.core); @@ -168,8 +137,8 @@ CVK_TEST(unique_handle, core_deleter) { CVK_TEST(unique_handle, move_assignment_destroys_the_old_handle) { test::Context const context{}; - unique_handle handle{context.core}; - unique_handle other{context.core}; + unique_handle> handle{context.core}; + unique_handle> other{context.core}; handle.reset(create_vk_fence(context.core), context.core); auto const vkFence = create_vk_fence(context.core); other.reset(vkFence, context.core); @@ -184,7 +153,7 @@ CVK_TEST(unique_handle, move_assignment_destroys_the_old_handle) { CVK_TEST(unique_handle, release_keeps_the_handle) { test::Context const context{}; auto const vkFence = create_vk_fence(context.core); - unique_handle handle{vkFence, context.core}; + unique_handle> handle{vkFence, context.core}; EXPECT_EQ(handle.release(), vkFence); EXPECT_FALSE(handle); @@ -200,7 +169,7 @@ CVK_TEST(unique_handle, instance_deleter) { Instance const instance{test::APP_NAME, test::headless_extensions(), test::counting_messenger()}; auto surface = test::headless_surface(instance); - unique_handle handle{surface.release().vkSurface.get(), instance}; + unique_handle> handle{surface.release().vkSurface.get(), instance}; EXPECT_EQ(handle.vkInstance(), instance.get()); } @@ -230,7 +199,7 @@ CVK_TEST(unique_handle, handle_deleter) { VkInstance vkInstance = VK_NULL_HANDLE; ASSERT_EQ(vkCreateInstance(&info, nullptr, &vkInstance), VK_SUCCESS); - unique_handle handle{}; + unique_handle> handle{}; handle.reset(vkInstance); EXPECT_EQ(handle.get(), vkInstance); @@ -250,7 +219,7 @@ CVK_TEST(unique_handle, custom_deleter) { ASSERT_EQ(table->vkCreateCommandPool(table.device(), &poolInfo, nullptr, &vkPool), VK_SUCCESS); { - unique_handle handle{context.core, vkPool}; + unique_handle handle{context.core, vkPool}; EXPECT_EQ(&handle.core(), &context.core); EXPECT_EQ(handle.deleter().pool, vkPool); @@ -274,14 +243,14 @@ CVK_TEST(unique_handle, custom_deleter) { CVK_TEST(unique_handle, non_pointer_handle) { PairOwned::destroyed = 0; { - unique_handle handle{}; + unique_handle handle{}; EXPECT_FALSE(handle); handle.reset(pair_handle{new int{1}, new int{2}}); ASSERT_TRUE(handle); EXPECT_EQ(*handle.get().second, 2); - unique_handle other{}; + unique_handle other{}; other.reset(pair_handle{new int{3}, new int{4}}); handle = std::move(other); From 10712a1397721939abb0948e5ffd71c8af4a91f6 Mon Sep 17 00:00:00 2001 From: lukas <89145039+codethinki@users.noreply.github.com> Date: Fri, 16 Oct 2026 18:20:47 +0200 Subject: [PATCH 026/135] cvk: core_handle alias core_handle = unique_handle>, used by every device object. AGENTS.md: an alias that names a type of its own is snake_case without _t. --- AGENTS.md | 2 ++ cvk/incl/cvk/data/img/sampler.hpp | 2 +- .../render/pipeline/layout/descriptor_set_layout.hpp | 2 +- .../cvk/render/pipeline/layout/pipeline_layout.hpp | 2 +- cvk/incl/cvk/render/pipeline/pipeline.hpp | 2 +- cvk/incl/cvk/render/pipeline/shader/shader.hpp | 2 +- cvk/incl/cvk/render/sync/fence.hpp | 2 +- cvk/incl/cvk/render/sync/semaphore.hpp | 2 +- cvk/incl/cvk/utility/unique_handle.hpp | 7 +++++++ cvk/tests/src/utility/unique_handle_test.cpp | 12 ++++++------ 10 files changed, 22 insertions(+), 13 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index a518c35c..16923cb2 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -66,6 +66,7 @@ ctest --test-dir out/build/gcc_release --output-on-failure | nested config and state | `Config`, `State` | `Instance::State` | | generic library type, trait, concept | `snake_case` | `basic_deletable`, `handle_t`, `destroyable` | | type alias | `snake_case_t` | `callback_t`, `queue_set_t` | +| alias that names a type of its own | `snake_case` | `core_handle` | | member function | `camelCase` | `optDestroy()`, `createInfo()` | | free function | `snake_case` | `to_queue_properties()`, `default_debug_callback()` | | static check function | `debug_check`, `debug_check_handle` | | @@ -160,6 +161,7 @@ public: - Classes that own a handle are move only. A class that differs (copyable, not movable, custom move) says so in its `@brief`. - They store the handle in `cvk::unique_handle` (`cvk/utility/unique_handle.hpp`): a `std::unique_ptr` with a deleter that holds the destroy context and calls the static `destroy`. Deleters: `core_deleter` (the core), `instance_deleter` (the `VkInstance`), `handle_deleter` (no context) or a deleter derived from them. + Device objects use `core_handle`, short for `unique_handle>`. - Handle and context go in together: `_handle.reset(ptr, core())`. A missing context argument does not compile. - Move constructor and move assignment are `= default`. The move assignment destroys the old handle with the old context. - A class that owns more than one object declares the objects that depend on its handle first. `= default` assigns in declaration order: `Instance` declares `_debugMessenger` before `_handle`. diff --git a/cvk/incl/cvk/data/img/sampler.hpp b/cvk/incl/cvk/data/img/sampler.hpp index 46d31300..d0964186 100644 --- a/cvk/incl/cvk/data/img/sampler.hpp +++ b/cvk/incl/cvk/data/img/sampler.hpp @@ -98,7 +98,7 @@ class Sampler { private: void invalidate(); - unique_handle> _handle; + core_handle _handle; Config _config; public: diff --git a/cvk/incl/cvk/render/pipeline/layout/descriptor_set_layout.hpp b/cvk/incl/cvk/render/pipeline/layout/descriptor_set_layout.hpp index 07cb613d..50d1820e 100644 --- a/cvk/incl/cvk/render/pipeline/layout/descriptor_set_layout.hpp +++ b/cvk/incl/cvk/render/pipeline/layout/descriptor_set_layout.hpp @@ -103,7 +103,7 @@ class DescriptorSetLayout { private: void invalidate(); - unique_handle> _handle; + core_handle _handle; std::vector _bindings; public: diff --git a/cvk/incl/cvk/render/pipeline/layout/pipeline_layout.hpp b/cvk/incl/cvk/render/pipeline/layout/pipeline_layout.hpp index 739c51a2..a97fb022 100644 --- a/cvk/incl/cvk/render/pipeline/layout/pipeline_layout.hpp +++ b/cvk/incl/cvk/render/pipeline/layout/pipeline_layout.hpp @@ -95,7 +95,7 @@ class PipelineLayout { private: void invalidate(); - unique_handle> _handle; + core_handle _handle; std::vector _vkSetLayouts; std::vector _pushConstantRanges; diff --git a/cvk/incl/cvk/render/pipeline/pipeline.hpp b/cvk/incl/cvk/render/pipeline/pipeline.hpp index ecc8f169..20ab238c 100644 --- a/cvk/incl/cvk/render/pipeline/pipeline.hpp +++ b/cvk/incl/cvk/render/pipeline/pipeline.hpp @@ -119,7 +119,7 @@ class Pipeline { private: void invalidate(); - unique_handle> _handle; + core_handle _handle; vk_not_null _vkLayout; VkPipeline _vkParent = VK_NULL_HANDLE; Config _config; diff --git a/cvk/incl/cvk/render/pipeline/shader/shader.hpp b/cvk/incl/cvk/render/pipeline/shader/shader.hpp index e7db73f6..98439b31 100644 --- a/cvk/incl/cvk/render/pipeline/shader/shader.hpp +++ b/cvk/incl/cvk/render/pipeline/shader/shader.hpp @@ -163,7 +163,7 @@ class Shader { private: void invalidate(); - unique_handle> _handle; + core_handle _handle; VkShaderStageFlagBits _stage; public: diff --git a/cvk/incl/cvk/render/sync/fence.hpp b/cvk/incl/cvk/render/sync/fence.hpp index f24d454d..2c5ae054 100644 --- a/cvk/incl/cvk/render/sync/fence.hpp +++ b/cvk/incl/cvk/render/sync/fence.hpp @@ -161,7 +161,7 @@ class Fence { private: void invalidate(); - unique_handle> _handle; + core_handle _handle; VkExternalFenceHandleTypeFlags _exportTypes = 0; public: diff --git a/cvk/incl/cvk/render/sync/semaphore.hpp b/cvk/incl/cvk/render/sync/semaphore.hpp index b4beff61..ce67efee 100644 --- a/cvk/incl/cvk/render/sync/semaphore.hpp +++ b/cvk/incl/cvk/render/sync/semaphore.hpp @@ -97,7 +97,7 @@ class Semaphore { private: void invalidate(); - unique_handle> _handle; + core_handle _handle; public: Semaphore(Semaphore const& other) = delete; diff --git a/cvk/incl/cvk/utility/unique_handle.hpp b/cvk/incl/cvk/utility/unique_handle.hpp index d6cfa9a5..ddec2f61 100644 --- a/cvk/incl/cvk/utility/unique_handle.hpp +++ b/cvk/incl/cvk/utility/unique_handle.hpp @@ -152,4 +152,11 @@ class unique_handle { unique_handle& operator=(unique_handle const& other) = delete; unique_handle& operator=(unique_handle&& other) noexcept = default; }; + +/** + * @brief @ref unique_handle of a device object, the deleter stores the core + * @tparam T owner class with a static destroy(DeviceTable, Handle) + */ +template +using core_handle = unique_handle>; } diff --git a/cvk/tests/src/utility/unique_handle_test.cpp b/cvk/tests/src/utility/unique_handle_test.cpp index cea5fb25..678bdcfd 100644 --- a/cvk/tests/src/utility/unique_handle_test.cpp +++ b/cvk/tests/src/utility/unique_handle_test.cpp @@ -89,7 +89,7 @@ namespace { } CVK_TEST(unique_handle, a_handle_needs_its_context) { - using fence_handle = unique_handle>; + using fence_handle = core_handle; static_assert(!std::default_initializable); static_assert(!std::constructible_from); static_assert(!resettable); @@ -113,7 +113,7 @@ CVK_TEST(unique_handle, a_handle_needs_its_context) { CVK_TEST(unique_handle, size) { static_assert(sizeof(unique_handle>) == sizeof(VkInstance)); - static_assert(sizeof(unique_handle>) == sizeof(Core const*) + sizeof(VkFence)); + static_assert(sizeof(core_handle) == sizeof(Core const*) + sizeof(VkFence)); static_assert( sizeof(unique_handle>) == sizeof(VkInstance) + sizeof(VkSurfaceKHR) ); @@ -122,7 +122,7 @@ CVK_TEST(unique_handle, size) { CVK_TEST(unique_handle, core_deleter) { test::Context const context{}; - unique_handle> handle{context.core}; + core_handle handle{context.core}; EXPECT_FALSE(handle); EXPECT_EQ(&handle.core(), &context.core); @@ -137,8 +137,8 @@ CVK_TEST(unique_handle, core_deleter) { CVK_TEST(unique_handle, move_assignment_destroys_the_old_handle) { test::Context const context{}; - unique_handle> handle{context.core}; - unique_handle> other{context.core}; + core_handle handle{context.core}; + core_handle other{context.core}; handle.reset(create_vk_fence(context.core), context.core); auto const vkFence = create_vk_fence(context.core); other.reset(vkFence, context.core); @@ -153,7 +153,7 @@ CVK_TEST(unique_handle, move_assignment_destroys_the_old_handle) { CVK_TEST(unique_handle, release_keeps_the_handle) { test::Context const context{}; auto const vkFence = create_vk_fence(context.core); - unique_handle> handle{vkFence, context.core}; + core_handle handle{vkFence, context.core}; EXPECT_EQ(handle.release(), vkFence); EXPECT_FALSE(handle); From 18db7f50385737ac9b12dd0f49c9730eca442382 Mon Sep 17 00:00:00 2001 From: lukas <89145039+codethinki@users.noreply.github.com> Date: Fri, 16 Oct 2026 18:24:28 +0200 Subject: [PATCH 027/135] AGENTS.md: no naming row for core_handle --- AGENTS.md | 1 - 1 file changed, 1 deletion(-) diff --git a/AGENTS.md b/AGENTS.md index 16923cb2..0b207080 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -66,7 +66,6 @@ ctest --test-dir out/build/gcc_release --output-on-failure | nested config and state | `Config`, `State` | `Instance::State` | | generic library type, trait, concept | `snake_case` | `basic_deletable`, `handle_t`, `destroyable` | | type alias | `snake_case_t` | `callback_t`, `queue_set_t` | -| alias that names a type of its own | `snake_case` | `core_handle` | | member function | `camelCase` | `optDestroy()`, `createInfo()` | | free function | `snake_case` | `to_queue_properties()`, `default_debug_callback()` | | static check function | `debug_check`, `debug_check_handle` | | From f893f03968172c2e1479aba99a83b936d3b9b0d2 Mon Sep 17 00:00:00 2001 From: lukas <89145039+codethinki@users.noreply.github.com> Date: Fri, 16 Oct 2026 18:26:51 +0200 Subject: [PATCH 028/135] AGENTS.md: _t only for aliases that rename a type --- AGENTS.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/AGENTS.md b/AGENTS.md index 0b207080..2d4e903d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -65,7 +65,7 @@ ctest --test-dir out/build/gcc_release --output-on-failure | class, struct of `cvk` / `jvk` | `PascalCase` | `Semaphore`, `DeviceTable`, `DestructionQueue` | | nested config and state | `Config`, `State` | `Instance::State` | | generic library type, trait, concept | `snake_case` | `basic_deletable`, `handle_t`, `destroyable` | -| type alias | `snake_case_t` | `callback_t`, `queue_set_t` | +| type alias that renames a type | `snake_case_t` | `callback_t`, `queue_set_t` | | member function | `camelCase` | `optDestroy()`, `createInfo()` | | free function | `snake_case` | `to_queue_properties()`, `default_debug_callback()` | | static check function | `debug_check`, `debug_check_handle` | | @@ -78,6 +78,7 @@ ctest --test-dir out/build/gcc_release --output-on-failure | namespace | short lower case | `cvk`, `cvk::constants`, `cvk::dev` | - A raw Vulkan handle in a variable or parameter has the prefix `vk`: `vkInstance`, `vkSemaphore`. +- `_t` marks an alias that only renames a type: `callback_t` is a function pointer, `queue_set_t` a `union_find`. An alias that is a type of its own gets a plain std style name, like `std::string` for `std::basic_string`. - A declaration leaves out a parameter name that repeats the type: `void wrap(State const&)`, `destroy(DeviceTable table, VkSemaphore)`, not `VkSemaphore vkSemaphore`. The definition keeps the name. - Function parameters were `snake_case` in the legacy code. New code uses `camelCase`. Rename them when you port. - `cvk::dev` holds implementation details that are not part of the API. From 2158043fee2a51e42d842db78b62a37eb2c3cc5e Mon Sep 17 00:00:00 2001 From: lukas <89145039+codethinki@users.noreply.github.com> Date: Fri, 16 Oct 2026 22:55:49 +0200 Subject: [PATCH 029/135] style: lifecycle invalidate() is forget() invalidate names the Vulkan operation on mapped memory (vkInvalidateMappedMemoryRanges). Fence::waitReset() brief back to 'waits and resets', the last rename changed it by mistake. --- AGENTS.md | 16 ++++++++-------- cvk/incl/cvk/base/core.hpp | 6 +++--- cvk/incl/cvk/base/device.hpp | 6 +++--- cvk/incl/cvk/base/instance.hpp | 6 +++--- cvk/incl/cvk/base/queue/queue.hpp | 6 +++--- cvk/incl/cvk/data/img/sampler.hpp | 6 +++--- cvk/incl/cvk/debug/debug_messenger.hpp | 6 +++--- .../pipeline/layout/descriptor_set_layout.hpp | 6 +++--- .../render/pipeline/layout/pipeline_layout.hpp | 6 +++--- cvk/incl/cvk/render/pipeline/pipeline.hpp | 6 +++--- cvk/incl/cvk/render/pipeline/shader/shader.hpp | 6 +++--- cvk/incl/cvk/render/sync/fence.hpp | 8 ++++---- cvk/incl/cvk/render/sync/semaphore.hpp | 6 +++--- cvk/incl/cvk/render/sync/timeline_semaphore.hpp | 2 +- cvk/incl/cvk/surface/surface.hpp | 6 +++--- cvk/src/base/core.cpp | 6 +++--- cvk/src/base/device.cpp | 8 ++++---- cvk/src/base/instance.cpp | 8 ++++---- cvk/src/base/queue/queue.cpp | 6 +++--- cvk/src/data/img/sampler.cpp | 8 ++++---- cvk/src/debug/debug_messenger.cpp | 8 ++++---- .../pipeline/layout/descriptor_set_layout.cpp | 8 ++++---- .../render/pipeline/layout/pipeline_layout.cpp | 8 ++++---- cvk/src/render/pipeline/pipeline.cpp | 8 ++++---- cvk/src/render/pipeline/shader/shader.cpp | 8 ++++---- cvk/src/render/sync/fence.cpp | 8 ++++---- cvk/src/render/sync/semaphore.cpp | 8 ++++---- cvk/src/surface/surface.cpp | 6 +++--- 28 files changed, 98 insertions(+), 98 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 2d4e903d..5344f1f1 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -190,23 +190,23 @@ inline void Semaphore::debug_check(Semaphore const& semaphore) { | base constructor | stores the configuration, does not create a Vulkan object | | `wrap(State)` | takes ownership of an existing Vulkan object, calls `optDestroy()` first | | `create(...)` | creates a new Vulkan object, calls `optDestroy()` first | -| `destroy()` | destroys the Vulkan object now and invalidates, requires `created()` | +| `destroy()` | destroys the Vulkan object now and forgets, requires `created()` | | `optDestroy()` | calls `destroy()` if `created()` | -| `release()` | gives the `State` to the caller and invalidates, does not destroy | +| `release()` | gives the `State` to the caller and forgets, does not destroy | | `static destroy(...)` | destroys the raw handles, used by the deleter | -| `invalidate()` | sets the members to the not created state, does not destroy | +| `forget()` | sets the members to the not created state, does not destroy | - The destructor calls `optDestroy()`. -- `reset` keeps its std meaning (`std::unique_ptr::reset` destroys) and names Vulkan operations (`vkResetFences`). The lifecycle uses `invalidate()`. +- `reset` keeps its std meaning (`std::unique_ptr::reset` destroys). `reset` and `invalidate` name Vulkan operations (`vkResetFences`, `vkInvalidateMappedMemoryRanges`). The lifecycle uses `forget()`. - Device objects take `Core const&` in the base constructor. The `core_deleter` of the handle stores it, `core()` returns it. - A parent (instance, core, device, pool, ...) does not move and is not destroyed while objects depend on it. Code does not have to support it. -- A failed `create()` calls `invalidate()` before it throws. +- A failed `create()` calls `forget()` before it throws. - `release()` exists so that an owner can use an object it must not destroy (for example swapchain images). ### Errors - `CTH_CRITICAL(condition, "message") {}`: precondition check, active in debug builds only. -- `CTH_STABLE_ERR(condition, "message") { invalidate(); throw details->exception(); }`: error that can happen at run time. +- `CTH_STABLE_ERR(condition, "message") { forget(); throw details->exception(); }`: error that can happen at run time. - `CTH_WARN(condition, "message") {}`: suspicious state that is not an error. - `CVK_RESULT_STABLE_THROW(result != VK_SUCCESS, result, "message") {}`: throws `cvk::vk_result_exception` with the `VkResult`. - The condition is the error case: `CTH_CRITICAL(!created(), ...)`, not the expected case. @@ -228,7 +228,7 @@ Documentation uses doxygen comments and follows ASD-STE100 (Simplified Technical - Use `/** ... */` blocks with one tag per line. - Start after the tag with a lower case letter. - Do not put a period at the end of a line. -- Write short fragments, not full sentences. Start them with a verb in the present tense: `creates the instance`, `destroys and invalidates`. +- Write short fragments, not full sentences. Start them with a verb in the present tense: `creates the instance`, `destroys and forgets`. - Fragments win over STE100: where STE100 asks for a full sentence or an article, write the shorter fragment. - Use `@ref` for every name of code: `@ref create()`, `@ref Core::created()`. - Do not document what the signature already tells. A getter without conditions needs no comment. @@ -278,7 +278,7 @@ Use symbols when they are shorter and clearer than words: | destroy | the Vulkan object stops to exist | | wrap | take ownership of an existing Vulkan object | | release | give the ownership to the caller without destroying | -| invalidate | set the members to the not created state | +| forget | set the members to the not created state | | handle | the raw Vulkan handle, for example `VkInstance` | | state | the `State` struct: everything that `wrap` needs | diff --git a/cvk/incl/cvk/base/core.hpp b/cvk/incl/cvk/base/core.hpp index a5cefd99..d5388df1 100644 --- a/cvk/incl/cvk/base/core.hpp +++ b/cvk/incl/cvk/base/core.hpp @@ -77,7 +77,7 @@ class Core { void create(std::span surfaces, Config const&); /** - * @brief destroys and invalidates + * @brief destroys and forgets * @pre @ref created() */ void destroy(); @@ -88,7 +88,7 @@ class Core { void optDestroy() { if(created()) destroy(); } /** - * @brief releases ownership, returns the state and invalidates + * @brief releases ownership, returns the state and forgets * @pre @ref created() */ State release(); @@ -140,7 +140,7 @@ class Core { std::vector queueToUniqueQueue; }; - void invalidate(); + void forget(); /** * @post sets @ref _physicalDevice and @ref _queueSetIndex diff --git a/cvk/incl/cvk/base/device.hpp b/cvk/incl/cvk/base/device.hpp index 6822ec55..093b84ce 100644 --- a/cvk/incl/cvk/base/device.hpp +++ b/cvk/incl/cvk/base/device.hpp @@ -78,7 +78,7 @@ class Device { void create(Config const&); /** - * @brief destroys and invalidates + * @brief destroys and forgets * @pre @ref created() */ void destroy(); @@ -89,7 +89,7 @@ class Device { void optDestroy() { if(created()) destroy(); } /** - * @brief releases ownership, returns the state and invalidates + * @brief releases ownership, returns the state and forgets * @pre @ref created() */ State release(); @@ -119,7 +119,7 @@ class Device { [[nodiscard]] bool created() const { return static_cast(_handle); } private: - void invalidate(); + void forget(); [[nodiscard]] static queue_families_queue_counts_t calcQueueFamiliesQueueCounts( std::span familyIndices diff --git a/cvk/incl/cvk/base/instance.hpp b/cvk/incl/cvk/base/instance.hpp index f364c5f8..c955d086 100644 --- a/cvk/incl/cvk/base/instance.hpp +++ b/cvk/incl/cvk/base/instance.hpp @@ -90,7 +90,7 @@ class Instance { void create(std::optional messengerConfig = std::nullopt); /** - * @brief destroys the messenger and the instance and invalidates + * @brief destroys the messenger and the instance and forgets * @pre @ref created() */ void destroy(); @@ -101,7 +101,7 @@ class Instance { void optDestroy() { if(created()) destroy(); } /** - * @brief releases ownership of the instance and the messenger, returns the state and invalidates + * @brief releases ownership of the instance and the messenger, returns the state and forgets * @pre @ref created() */ State release(); @@ -135,7 +135,7 @@ class Instance { [[nodiscard]] std::vector availableValidationLayers() const { return _availableLayers; } private: - void invalidate(); + void forget(); std::string _name; diff --git a/cvk/incl/cvk/base/queue/queue.hpp b/cvk/incl/cvk/base/queue/queue.hpp index 423149ea..40c7480e 100644 --- a/cvk/incl/cvk/base/queue/queue.hpp +++ b/cvk/incl/cvk/base/queue/queue.hpp @@ -29,7 +29,7 @@ class Queue { void wrap(State); /** - * @brief invalidates, the device owns the vulkan queue + * @brief forgets, the device owns the vulkan queue * @pre @ref created() */ void destroy(); @@ -40,7 +40,7 @@ class Queue { void optDestroy() { if(created()) destroy(); } /** - * @brief releases ownership, returns the state and invalidates + * @brief releases ownership, returns the state and forgets * @pre @ref created() */ State release(); @@ -77,7 +77,7 @@ class Queue { [[nodiscard]] QueueFamilyProperties familyProperties() const { return _familyProperties; } private: - void invalidate(); + void forget(); QueueFamilyProperties _familyProperties; VolkDeviceTable const* _functions = nullptr; diff --git a/cvk/incl/cvk/data/img/sampler.hpp b/cvk/incl/cvk/data/img/sampler.hpp index d0964186..1b0cfbd4 100644 --- a/cvk/incl/cvk/data/img/sampler.hpp +++ b/cvk/incl/cvk/data/img/sampler.hpp @@ -67,7 +67,7 @@ class Sampler { void create(); /** - * @brief destroys and invalidates + * @brief destroys and forgets * @pre @ref created() * @pre the gpu does not use the sampler */ @@ -79,7 +79,7 @@ class Sampler { void optDestroy() { if(created()) destroy(); } /** - * @brief releases ownership, returns the state and invalidates + * @brief releases ownership, returns the state and forgets * @pre @ref created() */ State release(); @@ -96,7 +96,7 @@ class Sampler { [[nodiscard]] Config const& config() const { return _config; } private: - void invalidate(); + void forget(); core_handle _handle; Config _config; diff --git a/cvk/incl/cvk/debug/debug_messenger.hpp b/cvk/incl/cvk/debug/debug_messenger.hpp index b3b6e974..0285d20e 100644 --- a/cvk/incl/cvk/debug/debug_messenger.hpp +++ b/cvk/incl/cvk/debug/debug_messenger.hpp @@ -53,7 +53,7 @@ class DebugMessenger { /** - * @brief destroys and invalidates + * @brief destroys and forgets * @pre @ref created() */ void destroy(); @@ -67,7 +67,7 @@ class DebugMessenger { static void destroy(vk_not_null, VkDebugUtilsMessengerEXT vkMessenger); /** - * @brief releases ownership, returns state and invalidates + * @brief releases ownership, returns state and forgets * @pre @ref created() */ State release(); @@ -99,7 +99,7 @@ class DebugMessenger { Config _config; private: - void invalidate(); + void forget(); unique_handle> _handle; diff --git a/cvk/incl/cvk/render/pipeline/layout/descriptor_set_layout.hpp b/cvk/incl/cvk/render/pipeline/layout/descriptor_set_layout.hpp index 50d1820e..489c4caf 100644 --- a/cvk/incl/cvk/render/pipeline/layout/descriptor_set_layout.hpp +++ b/cvk/incl/cvk/render/pipeline/layout/descriptor_set_layout.hpp @@ -68,7 +68,7 @@ class DescriptorSetLayout { void create(); /** - * @brief destroys and invalidates + * @brief destroys and forgets * @pre @ref created() */ void destroy(); @@ -79,7 +79,7 @@ class DescriptorSetLayout { void optDestroy() { if(created()) destroy(); } /** - * @brief releases ownership, returns the state and invalidates + * @brief releases ownership, returns the state and forgets * @pre @ref created() */ State release(); @@ -101,7 +101,7 @@ class DescriptorSetLayout { [[nodiscard]] VkDescriptorSetLayoutBinding const* binding(uint32_t binding) const; private: - void invalidate(); + void forget(); core_handle _handle; std::vector _bindings; diff --git a/cvk/incl/cvk/render/pipeline/layout/pipeline_layout.hpp b/cvk/incl/cvk/render/pipeline/layout/pipeline_layout.hpp index a97fb022..66ff17a8 100644 --- a/cvk/incl/cvk/render/pipeline/layout/pipeline_layout.hpp +++ b/cvk/incl/cvk/render/pipeline/layout/pipeline_layout.hpp @@ -70,7 +70,7 @@ class PipelineLayout { void create(); /** - * @brief destroys and invalidates + * @brief destroys and forgets * @pre @ref created() */ void destroy(); @@ -81,7 +81,7 @@ class PipelineLayout { void optDestroy() { if(created()) destroy(); } /** - * @brief releases ownership, returns the state and invalidates + * @brief releases ownership, returns the state and forgets * @pre @ref created() */ State release(); @@ -93,7 +93,7 @@ class PipelineLayout { [[nodiscard]] Core const& core() const { return _handle.core(); } private: - void invalidate(); + void forget(); core_handle _handle; std::vector _vkSetLayouts; diff --git a/cvk/incl/cvk/render/pipeline/pipeline.hpp b/cvk/incl/cvk/render/pipeline/pipeline.hpp index 20ab238c..90b9d6be 100644 --- a/cvk/incl/cvk/render/pipeline/pipeline.hpp +++ b/cvk/incl/cvk/render/pipeline/pipeline.hpp @@ -86,7 +86,7 @@ class Pipeline { void create(); /** - * @brief destroys and invalidates + * @brief destroys and forgets * @pre @ref created() * @pre the gpu does not use the pipeline */ @@ -98,7 +98,7 @@ class Pipeline { void optDestroy() { if(created()) destroy(); } /** - * @brief releases ownership, returns the state and invalidates + * @brief releases ownership, returns the state and forgets * @pre @ref created() */ State release(); @@ -117,7 +117,7 @@ class Pipeline { [[nodiscard]] Core const& core() const { return _handle.core(); } private: - void invalidate(); + void forget(); core_handle _handle; vk_not_null _vkLayout; diff --git a/cvk/incl/cvk/render/pipeline/shader/shader.hpp b/cvk/incl/cvk/render/pipeline/shader/shader.hpp index 98439b31..f5ff699b 100644 --- a/cvk/incl/cvk/render/pipeline/shader/shader.hpp +++ b/cvk/incl/cvk/render/pipeline/shader/shader.hpp @@ -105,7 +105,7 @@ class Shader { void create(path_t const& spvPath); /** - * @brief destroys and invalidates + * @brief destroys and forgets * @pre @ref created() */ void destroy(); @@ -116,7 +116,7 @@ class Shader { void optDestroy() { if(created()) destroy(); } /** - * @brief releases ownership, returns the state and invalidates + * @brief releases ownership, returns the state and forgets * @pre @ref created() */ State release(); @@ -161,7 +161,7 @@ class Shader { [[nodiscard]] Core const& core() const { return _handle.core(); } private: - void invalidate(); + void forget(); core_handle _handle; VkShaderStageFlagBits _stage; diff --git a/cvk/incl/cvk/render/sync/fence.hpp b/cvk/incl/cvk/render/sync/fence.hpp index 2c5ae054..8f807e85 100644 --- a/cvk/incl/cvk/render/sync/fence.hpp +++ b/cvk/incl/cvk/render/sync/fence.hpp @@ -67,7 +67,7 @@ class Fence { void create(VkFenceCreateFlags flags = 0, VkExternalFenceHandleTypeFlags exportTypes = 0); /** - * @brief destroys and invalidates + * @brief destroys and forgets * @pre @ref created() * @pre the gpu does not use the fence */ @@ -79,7 +79,7 @@ class Fence { void optDestroy() { if(created()) destroy(); } /** - * @brief releases ownership, returns the state and invalidates + * @brief releases ownership, returns the state and forgets * @pre @ref created() */ State release(); @@ -130,7 +130,7 @@ class Fence { [[nodiscard]] VkResult waitReset(uint64_t timeout) const; /** - * @brief waits and invalidates + * @brief waits and resets * @details calls: * - @ref wait() const * - @ref reset() @@ -159,7 +159,7 @@ class Fence { [[nodiscard]] VkExternalFenceHandleTypeFlags exportTypes() const { return _exportTypes; } private: - void invalidate(); + void forget(); core_handle _handle; VkExternalFenceHandleTypeFlags _exportTypes = 0; diff --git a/cvk/incl/cvk/render/sync/semaphore.hpp b/cvk/incl/cvk/render/sync/semaphore.hpp index ce67efee..227380d4 100644 --- a/cvk/incl/cvk/render/sync/semaphore.hpp +++ b/cvk/incl/cvk/render/sync/semaphore.hpp @@ -62,7 +62,7 @@ class Semaphore { void create(); /** - * @brief destroys and invalidates + * @brief destroys and forgets * @pre @ref created() * @pre the gpu does not use the semaphore */ @@ -74,7 +74,7 @@ class Semaphore { void optDestroy() { if(created()) destroy(); } /** - * @brief releases ownership, returns the state and invalidates + * @brief releases ownership, returns the state and forgets * @pre @ref created() */ State release(); @@ -95,7 +95,7 @@ class Semaphore { void create(VkSemaphoreCreateInfo const& info); private: - void invalidate(); + void forget(); core_handle _handle; diff --git a/cvk/incl/cvk/render/sync/timeline_semaphore.hpp b/cvk/incl/cvk/render/sync/timeline_semaphore.hpp index 4c23079b..c88d0b9c 100644 --- a/cvk/incl/cvk/render/sync/timeline_semaphore.hpp +++ b/cvk/incl/cvk/render/sync/timeline_semaphore.hpp @@ -63,7 +63,7 @@ class TimelineSemaphore : Semaphore { using Semaphore::optDestroy; /** - * @brief releases ownership, returns the state and invalidates + * @brief releases ownership, returns the state and forgets * @pre @ref created() */ State release(); diff --git a/cvk/incl/cvk/surface/surface.hpp b/cvk/incl/cvk/surface/surface.hpp index be2f3366..4961d391 100644 --- a/cvk/incl/cvk/surface/surface.hpp +++ b/cvk/incl/cvk/surface/surface.hpp @@ -53,7 +53,7 @@ class Surface { void wrap(State const&); /** - * @brief destroys and invalidates + * @brief destroys and forgets * @pre @ref created() */ void destroy(); @@ -64,7 +64,7 @@ class Surface { void optDestroy() { if(created()) destroy(); } /** - * @brief releases ownership, returns the state and invalidates + * @brief releases ownership, returns the state and forgets * @pre @ref created() */ State release(); @@ -112,7 +112,7 @@ class Surface { [[nodiscard]] Config const& config() const { return _config; } private: - void invalidate(); + void forget(); Config _config; unique_handle> _handle; diff --git a/cvk/src/base/core.cpp b/cvk/src/base/core.cpp index bec20e65..9a16640b 100644 --- a/cvk/src/base/core.cpp +++ b/cvk/src/base/core.cpp @@ -60,7 +60,7 @@ void Core::create(span surfaces, Config const& config) { void Core::destroy() { debug_check(*this); - invalidate(); + forget(); } Core::State Core::release() { @@ -72,7 +72,7 @@ Core::State Core::release() { .queues = std::move(_queues), .queueSetIndex = _queueSetIndex, }; - invalidate(); + forget(); return state; } @@ -100,7 +100,7 @@ DeviceTable Core::deviceTable() const { return device().table(); } VolkDeviceTable const* Core::functions() const { return device().functions(); } VkDevice Core::vkDevice() const { return device().get(); } -void Core::invalidate() { +void Core::forget() { _queues.clear(); _device = nullptr; _physicalDevice = nullptr; diff --git a/cvk/src/base/device.cpp b/cvk/src/base/device.cpp index 0aa76af6..16f39acc 100644 --- a/cvk/src/base/device.cpp +++ b/cvk/src/base/device.cpp @@ -58,7 +58,7 @@ void Device::destroy() { debug_check(*this); _handle.reset(); - invalidate(); + forget(); } Device::State Device::release() { @@ -68,7 +68,7 @@ Device::State Device::release() { .vkDevice = _handle.release(), .functionTable = std::move(_functionTable) }; - invalidate(); + forget(); return state; } @@ -88,7 +88,7 @@ void Device::destroy(VkDevice vkDevice) { log::msg("destroyed device"); } -void Device::invalidate() { +void Device::forget() { _queues.clear(); if(_functionTable == nullptr) _functionTable = std::make_unique(); @@ -159,7 +159,7 @@ void Device::createLogicalDevice(queue_families_queue_counts_t const& queueFamil auto const createResult = vkCreateDevice(_physicalDevice->get(), &createInfo, nullptr, &ptr); CTH_STABLE_ERR(createResult != VK_SUCCESS, "failed to create logical device") { - invalidate(); + forget(); throw cvk::vk_result_exception{createResult, details->exception()}; } diff --git a/cvk/src/base/instance.cpp b/cvk/src/base/instance.cpp index 81199984..34af741a 100644 --- a/cvk/src/base/instance.cpp +++ b/cvk/src/base/instance.cpp @@ -93,7 +93,7 @@ void Instance::create(std::optional messengerConfig) { auto const createInstanceResult = vkCreateInstance(&createInfo, nullptr, &ptr); CTH_STABLE_ERR(createInstanceResult != VK_SUCCESS, "failed to create instance!") { - invalidate(); + forget(); throw cvk::vk_result_exception{createInstanceResult, details->exception()}; } @@ -111,14 +111,14 @@ void Instance::destroy() { _debugMessenger = nullptr; _handle.reset(); - invalidate(); + forget(); } Instance::State Instance::release() { debug_check(*this); State state{_handle.release(), std::move(_debugMessenger)}; - invalidate(); + forget(); return state; } @@ -219,7 +219,7 @@ void Instance::destroy(VkInstance vkInstance) { log::msg("destroyed instance"); } -void Instance::invalidate() { _debugMessenger = nullptr; } +void Instance::forget() { _debugMessenger = nullptr; } void Instance::loadInstanceFunctions(vk_not_null vkInstance) { volkLoadInstanceOnly(vkInstance.get()); } } diff --git a/cvk/src/base/queue/queue.cpp b/cvk/src/base/queue/queue.cpp index 17607eb1..39acf1f8 100644 --- a/cvk/src/base/queue/queue.cpp +++ b/cvk/src/base/queue/queue.cpp @@ -17,7 +17,7 @@ void Queue::wrap(State state) { void Queue::destroy() { debug_check(*this); - invalidate(); + forget(); } Queue::State Queue::release() { @@ -29,7 +29,7 @@ Queue::State Queue::release() { _queueIndex, std::move(_handleMtx), }; - invalidate(); + forget(); return state; } @@ -75,7 +75,7 @@ void Queue::wait() const { CVK_RESULT_STABLE_THROW(result != VK_SUCCESS, result, "failed to wait for queue") {} } -void Queue::invalidate() { +void Queue::forget() { _functions = nullptr; _handle = VK_NULL_HANDLE; _handleMtx = nullptr; diff --git a/cvk/src/data/img/sampler.cpp b/cvk/src/data/img/sampler.cpp index f4b103a3..53eb4f3d 100644 --- a/cvk/src/data/img/sampler.cpp +++ b/cvk/src/data/img/sampler.cpp @@ -32,7 +32,7 @@ void Sampler::create() { auto const result = core().functions()->vkCreateSampler(core().vkDevice(), &info, nullptr, &ptr); CTH_STABLE_ERR(result != VK_SUCCESS, "failed to create sampler") { - invalidate(); + forget(); throw cvk::vk_result_exception{result, details->exception()}; } @@ -44,14 +44,14 @@ void Sampler::destroy() { _handle.reset(); - invalidate(); + forget(); } Sampler::State Sampler::release() { debug_check(*this); State const state{_handle.release()}; - invalidate(); + forget(); return state; } @@ -61,5 +61,5 @@ void Sampler::destroy(DeviceTable table, VkSampler vkSampler) { table->vkDestroySampler(table.device(), vkSampler, nullptr); } -void Sampler::invalidate() {} +void Sampler::forget() {} } diff --git a/cvk/src/debug/debug_messenger.cpp b/cvk/src/debug/debug_messenger.cpp index 65875c1c..2f5d5ccc 100644 --- a/cvk/src/debug/debug_messenger.cpp +++ b/cvk/src/debug/debug_messenger.cpp @@ -26,7 +26,7 @@ void DebugMessenger::create() { auto const createResult = vkCreateDebugUtilsMessengerEXT(_handle.vkInstance(), &info, nullptr, &ptr); CTH_STABLE_ERR(createResult != VK_SUCCESS, "failed to set up debug messenger") { - invalidate(); + forget(); throw cvk::vk_result_exception{createResult, details->exception()}; } @@ -38,7 +38,7 @@ void DebugMessenger::destroy() { _handle.reset(); - invalidate(); + forget(); } @@ -53,11 +53,11 @@ DebugMessenger::State DebugMessenger::release() { debug_check(*this); State const state{_handle.release()}; - invalidate(); + forget(); return state; } -void DebugMessenger::invalidate() {} +void DebugMessenger::forget() {} } // namespace cvk namespace cvk::dev { diff --git a/cvk/src/render/pipeline/layout/descriptor_set_layout.cpp b/cvk/src/render/pipeline/layout/descriptor_set_layout.cpp index b828ec0d..55da2b83 100644 --- a/cvk/src/render/pipeline/layout/descriptor_set_layout.cpp +++ b/cvk/src/render/pipeline/layout/descriptor_set_layout.cpp @@ -41,7 +41,7 @@ void DescriptorSetLayout::create() { auto const result = core().functions()->vkCreateDescriptorSetLayout(core().vkDevice(), &info, nullptr, &ptr); CTH_STABLE_ERR(result != VK_SUCCESS, "failed to create descriptor set layout") { - invalidate(); + forget(); throw cvk::vk_result_exception{result, details->exception()}; } @@ -53,14 +53,14 @@ void DescriptorSetLayout::destroy() { _handle.reset(); - invalidate(); + forget(); } DescriptorSetLayout::State DescriptorSetLayout::release() { debug_check(*this); State const state{_handle.release()}; - invalidate(); + forget(); return state; } @@ -77,7 +77,7 @@ VkDescriptorSetLayoutBinding const* DescriptorSetLayout::binding(uint32_t bindin return it != _bindings.end() && it->binding == binding ? std::to_address(it) : nullptr; } -void DescriptorSetLayout::invalidate() {} +void DescriptorSetLayout::forget() {} } namespace cvk { diff --git a/cvk/src/render/pipeline/layout/pipeline_layout.cpp b/cvk/src/render/pipeline/layout/pipeline_layout.cpp index a04d4046..ca95d976 100644 --- a/cvk/src/render/pipeline/layout/pipeline_layout.cpp +++ b/cvk/src/render/pipeline/layout/pipeline_layout.cpp @@ -57,7 +57,7 @@ void PipelineLayout::create() { auto const result = core().functions()->vkCreatePipelineLayout(core().vkDevice(), &info, nullptr, &ptr); CTH_STABLE_ERR(result != VK_SUCCESS, "failed to create pipeline layout") { - invalidate(); + forget(); throw cvk::vk_result_exception{result, details->exception()}; } @@ -69,14 +69,14 @@ void PipelineLayout::destroy() { _handle.reset(); - invalidate(); + forget(); } PipelineLayout::State PipelineLayout::release() { debug_check(*this); State const state{_handle.release()}; - invalidate(); + forget(); return state; } @@ -86,5 +86,5 @@ void PipelineLayout::destroy(DeviceTable table, VkPipelineLayout vkLayout) { table->vkDestroyPipelineLayout(table.device(), vkLayout, nullptr); } -void PipelineLayout::invalidate() {} +void PipelineLayout::forget() {} } diff --git a/cvk/src/render/pipeline/pipeline.cpp b/cvk/src/render/pipeline/pipeline.cpp index ecb7a1ab..d5fa6092 100644 --- a/cvk/src/render/pipeline/pipeline.cpp +++ b/cvk/src/render/pipeline/pipeline.cpp @@ -131,7 +131,7 @@ void Pipeline::create() { ); CTH_STABLE_ERR(result != VK_SUCCESS, "failed to create graphics pipeline") { - invalidate(); + forget(); throw cvk::vk_result_exception{result, details->exception()}; } @@ -143,14 +143,14 @@ void Pipeline::destroy() { _handle.reset(); - invalidate(); + forget(); } Pipeline::State Pipeline::release() { debug_check(*this); State const state{_handle.release()}; - invalidate(); + forget(); return state; } @@ -160,5 +160,5 @@ void Pipeline::destroy(DeviceTable table, VkPipeline vkPipeline) { table->vkDestroyPipeline(table.device(), vkPipeline, nullptr); } -void Pipeline::invalidate() {} +void Pipeline::forget() {} } diff --git a/cvk/src/render/pipeline/shader/shader.cpp b/cvk/src/render/pipeline/shader/shader.cpp index 7ef9ae5b..ec0f6781 100644 --- a/cvk/src/render/pipeline/shader/shader.cpp +++ b/cvk/src/render/pipeline/shader/shader.cpp @@ -65,7 +65,7 @@ void Shader::create(std::span spv) { auto const result = core().functions()->vkCreateShaderModule(core().vkDevice(), &info, nullptr, &ptr); CTH_STABLE_ERR(result != VK_SUCCESS, "failed to create shader module") { - invalidate(); + forget(); throw cvk::vk_result_exception{result, details->exception()}; } @@ -82,14 +82,14 @@ void Shader::destroy() { _handle.reset(); - invalidate(); + forget(); } Shader::State Shader::release() { debug_check(*this); State const state{_handle.release()}; - invalidate(); + forget(); return state; } @@ -188,5 +188,5 @@ void Shader::destroy(DeviceTable table, VkShaderModule vkShader) { table->vkDestroyShaderModule(table.device(), vkShader, nullptr); } -void Shader::invalidate() {} +void Shader::forget() {} } diff --git a/cvk/src/render/sync/fence.cpp b/cvk/src/render/sync/fence.cpp index 058f8efe..6be9390d 100644 --- a/cvk/src/render/sync/fence.cpp +++ b/cvk/src/render/sync/fence.cpp @@ -49,7 +49,7 @@ void Fence::create(VkFenceCreateFlags flags, VkExternalFenceHandleTypeFlags expo auto const result = core().functions()->vkCreateFence(core().vkDevice(), &info, nullptr, &ptr); CTH_STABLE_ERR(result != VK_SUCCESS, "failed to create fence") { - invalidate(); + forget(); throw cvk::vk_result_exception{result, details->exception()}; } @@ -62,14 +62,14 @@ void Fence::destroy() { _handle.reset(); - invalidate(); + forget(); } Fence::State Fence::release() { debug_check(*this); State const state{_handle.release(), _exportTypes}; - invalidate(); + forget(); return state; } @@ -166,5 +166,5 @@ void Fence::destroy(DeviceTable table, VkFence vkFence) { table->vkDestroyFence(table.device(), vkFence, nullptr); } -void Fence::invalidate() { _exportTypes = 0; } +void Fence::forget() { _exportTypes = 0; } } diff --git a/cvk/src/render/sync/semaphore.cpp b/cvk/src/render/sync/semaphore.cpp index 43dab7e7..79fa5fab 100644 --- a/cvk/src/render/sync/semaphore.cpp +++ b/cvk/src/render/sync/semaphore.cpp @@ -31,14 +31,14 @@ void Semaphore::destroy() { _handle.reset(); - invalidate(); + forget(); } Semaphore::State Semaphore::release() { debug_check(*this); State const state{_handle.release()}; - invalidate(); + forget(); return state; } @@ -55,12 +55,12 @@ void Semaphore::create(VkSemaphoreCreateInfo const& info) { auto const result = core().functions()->vkCreateSemaphore(core().vkDevice(), &info, nullptr, &ptr); CTH_STABLE_ERR(result != VK_SUCCESS, "failed to create semaphore") { - invalidate(); + forget(); throw cvk::vk_result_exception{result, details->exception()}; } _handle.reset(ptr, core()); } -void Semaphore::invalidate() {} +void Semaphore::forget() {} } diff --git a/cvk/src/surface/surface.cpp b/cvk/src/surface/surface.cpp index 93e18d59..ab2542cc 100644 --- a/cvk/src/surface/surface.cpp +++ b/cvk/src/surface/surface.cpp @@ -44,14 +44,14 @@ void Surface::destroy() { _handle.reset(); - invalidate(); + forget(); } Surface::State Surface::release() { debug_check(*this); State const state{_handle.release()}; - invalidate(); + forget(); return state; } @@ -158,5 +158,5 @@ void Surface::destroy(vk_not_null vkInstance, VkSurfaceKHR vkSurface log::msg("destroyed surface"); } -void Surface::invalidate() {} +void Surface::forget() {} } From 7ee658319dad5be8023f244774df1e6906f2aef2 Mon Sep 17 00:00:00 2001 From: lukas <89145039+codethinki@users.noreply.github.com> Date: Fri, 16 Oct 2026 22:55:49 +0200 Subject: [PATCH 030/135] cvk tests: handle_deleter test loads the instance functions The test segfaulted when it ran alone (ctest runs one test per process): volk had no vkDestroyInstance. Instance::destroy(VkInstance) documents the precondition. --- cvk/incl/cvk/base/instance.hpp | 3 +++ cvk/tests/src/utility/unique_handle_test.cpp | 2 ++ 2 files changed, 5 insertions(+) diff --git a/cvk/incl/cvk/base/instance.hpp b/cvk/incl/cvk/base/instance.hpp index c955d086..ea50fc1e 100644 --- a/cvk/incl/cvk/base/instance.hpp +++ b/cvk/incl/cvk/base/instance.hpp @@ -126,6 +126,9 @@ class Instance { [[nodiscard]] static std::vector getAvailableInstanceExtensions(); [[nodiscard]] VkApplicationInfo appInfo() const; + /** + * @pre volk loaded the instance functions, @ref create() and @ref wrap() load them + */ static void destroy(VkInstance); [[nodiscard]] bool created() const { return static_cast(_handle); } diff --git a/cvk/tests/src/utility/unique_handle_test.cpp b/cvk/tests/src/utility/unique_handle_test.cpp index 678bdcfd..0684d765 100644 --- a/cvk/tests/src/utility/unique_handle_test.cpp +++ b/cvk/tests/src/utility/unique_handle_test.cpp @@ -198,6 +198,8 @@ CVK_TEST(unique_handle, handle_deleter) { }; VkInstance vkInstance = VK_NULL_HANDLE; ASSERT_EQ(vkCreateInstance(&info, nullptr, &vkInstance), VK_SUCCESS); + // Instance::destroy() calls vkDestroyInstance through volk + volkLoadInstanceOnly(vkInstance); unique_handle> handle{}; handle.reset(vkInstance); From 349a185617e05a258fee4934c966feb0bbe4f777 Mon Sep 17 00:00:00 2001 From: lukas <89145039+codethinki@users.noreply.github.com> Date: Fri, 16 Oct 2026 23:18:52 +0200 Subject: [PATCH 031/135] cvk: VMA allocator owned by the core, Memory Allocator wraps VmaAllocator, its flags follow the features and extensions the device enabled. Core creates it after the device and destroys it before the device. Memory wraps VmaAllocation: the legacy Memory, map, flush, invalidate through VMA. vma_deleter frees through the allocator of the core. --- AGENTS.md | 4 +- cvk/CMakeLists.txt | 6 +- cvk/incl/cvk/base/allocator.hpp | 128 +++++++++++++++ cvk/incl/cvk/base/core.hpp | 22 ++- cvk/incl/cvk/data/memory/memory.hpp | 181 +++++++++++++++++++++ cvk/incl/cvk/data/memory/memory_config.hpp | 53 ++++++ cvk/incl/cvk/utility/unique_handle.hpp | 23 ++- cvk/src/base/allocator.cpp | 107 ++++++++++++ cvk/src/base/core.cpp | 24 ++- cvk/src/data/memory/memory.cpp | 120 ++++++++++++++ cvk/src/utility/unique_handle.cpp | 3 + cvk/src/utility/vma.cpp | 5 + cvk/tests/src/base/allocator_test.cpp | 78 +++++++++ cvk/tests/src/base/core_test.cpp | 16 ++ cvk/tests/src/data/memory/memory_test.cpp | 135 +++++++++++++++ 15 files changed, 895 insertions(+), 10 deletions(-) create mode 100644 cvk/incl/cvk/base/allocator.hpp create mode 100644 cvk/incl/cvk/data/memory/memory.hpp create mode 100644 cvk/incl/cvk/data/memory/memory_config.hpp create mode 100644 cvk/src/base/allocator.cpp create mode 100644 cvk/src/data/memory/memory.cpp create mode 100644 cvk/src/utility/vma.cpp create mode 100644 cvk/tests/src/base/allocator_test.cpp create mode 100644 cvk/tests/src/data/memory/memory_test.cpp diff --git a/AGENTS.md b/AGENTS.md index 5344f1f1..2110770a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -77,7 +77,7 @@ ctest --test-dir out/build/gcc_release --output-on-failure | macro | `UPPER_SNAKE_CASE` with library prefix | `CVK_RESULT_STABLE_THROW` | | namespace | short lower case | `cvk`, `cvk::constants`, `cvk::dev` | -- A raw Vulkan handle in a variable or parameter has the prefix `vk`: `vkInstance`, `vkSemaphore`. +- A raw Vulkan handle in a variable or parameter has the prefix `vk`: `vkInstance`, `vkSemaphore`. A raw VMA handle has the prefix `vma`: `vmaAllocator`, `vmaAllocation`. - `_t` marks an alias that only renames a type: `callback_t` is a function pointer, `queue_set_t` a `union_find`. An alias that is a type of its own gets a plain std style name, like `std::string` for `std::basic_string`. - A declaration leaves out a parameter name that repeats the type: `void wrap(State const&)`, `destroy(DeviceTable table, VkSemaphore)`, not `VkSemaphore vkSemaphore`. The definition keeps the name. - Function parameters were `snake_case` in the legacy code. New code uses `camelCase`. Rename them when you port. @@ -160,7 +160,7 @@ public: - The last public section holds what every class has and readers rarely need: copy and move members, `debug_check` functions. - Classes that own a handle are move only. A class that differs (copyable, not movable, custom move) says so in its `@brief`. - They store the handle in `cvk::unique_handle` (`cvk/utility/unique_handle.hpp`): a `std::unique_ptr` with a deleter that holds the destroy context and calls the static `destroy`. - Deleters: `core_deleter` (the core), `instance_deleter` (the `VkInstance`), `handle_deleter` (no context) or a deleter derived from them. + Deleters: `core_deleter` (the core), `vma_deleter` (the core, destroys through its allocator), `instance_deleter` (the `VkInstance`), `handle_deleter` (no context) or a deleter derived from them. Device objects use `core_handle`, short for `unique_handle>`. - Handle and context go in together: `_handle.reset(ptr, core())`. A missing context argument does not compile. - Move constructor and move assignment are `= default`. The move assignment destroys the old handle with the old context. diff --git a/cvk/CMakeLists.txt b/cvk/CMakeLists.txt index 1efe8c4a..17baa54b 100644 --- a/cvk/CMakeLists.txt +++ b/cvk/CMakeLists.txt @@ -18,12 +18,16 @@ target_precompile_headers( ) find_package(volk CONFIG REQUIRED) +find_package(VulkanMemoryAllocator CONFIG REQUIRED) find_package(VulkanUtilityLibraries CONFIG REQUIRED) find_package(cth CONFIG REQUIRED COMPONENTS cth os) # cth/hash/hash_aggregates.hpp includes boost/pfr.hpp, the cth package does not export it find_package(boost_pfr CONFIG REQUIRED) -target_link_libraries(cvk PUBLIC volk::volk Vulkan::UtilityHeaders cth::cth cth::os Boost::pfr) +target_link_libraries( + cvk + PUBLIC volk::volk Vulkan::UtilityHeaders GPUOpen::VulkanMemoryAllocator cth::cth cth::os Boost::pfr +) # tests if(JOLLY_ENABLE_TESTS) diff --git a/cvk/incl/cvk/base/allocator.hpp b/cvk/incl/cvk/base/allocator.hpp new file mode 100644 index 00000000..0a69a66a --- /dev/null +++ b/cvk/incl/cvk/base/allocator.hpp @@ -0,0 +1,128 @@ +#pragma once +#include "cvk/utility/constants.hpp" +#include "cvk/utility/types.hpp" +#include "cvk/utility/unique_handle.hpp" + +#include +#include + +#include + + +namespace cvk { +class Device; +class PhysicalDevice; + +/** + * @brief VmaAllocator wrapper, allocates the memory of one device + * @details VMA places many allocations in one VkDeviceMemory, + * one allocator per device is enough, + * calls with the allocator are thread safe, VMA synchronizes them + */ +class Allocator { +public: + struct State; + + /** + * @brief base constructor + * @pre @ref Device::created() + */ + explicit Allocator(Device const&); + + /** + * @brief constructs and wraps + * @details calls: + * - @ref Allocator(Device const&) + * - @ref wrap() + */ + Allocator(Device const&, State const&); + + /** + * @brief constructs and creates + * @details calls: + * - @ref Allocator(Device const&) + * - @ref create() + */ + explicit Allocator(Device const&, create_t); + + /** + * @note calls @ref optDestroy() + */ + ~Allocator() { optDestroy(); } + + /** + * @brief wraps the state + * @pre the allocator was created with the device + * @post @ref created() + * @note calls @ref optDestroy() + */ + void wrap(State const&); + + /** + * @brief creates the allocator + * @details the flags follow the device: @ref flags() + * @pre the device enabled the required extensions and features of its physical device + * @post @ref created() + * @note calls @ref optDestroy() + * @throws cvk::vk_result_exception result of vmaImportVulkanFunctionsFromVolk() + * @throws cvk::vk_result_exception result of vmaCreateAllocator() + */ + void create(); + + /** + * @brief destroys and forgets + * @pre @ref created() + * @pre every memory of the allocator is destroyed + */ + void destroy(); + + /** + * @brief if @ref created() -> calls @ref destroy() + */ + void optDestroy() { if(created()) destroy(); } + + /** + * @brief releases ownership, returns the state and forgets + * @pre @ref created() + */ + State release(); + + static void destroy(VmaAllocator); + + /** + * @return VMA flags for the required extensions and features of the physical device + * @details bufferDeviceAddress, maintenance4, VK_EXT_memory_budget + */ + [[nodiscard]] static VmaAllocatorCreateFlags flags(PhysicalDevice const&); + + [[nodiscard]] bool created() const { return static_cast(_handle); } + [[nodiscard]] VmaAllocator get() const { return _handle.get(); } + [[nodiscard]] Device const& device() const { return *_device; } + +private: + void forget(); + + not_null _device; + unique_handle> _handle; + +public: + Allocator(Allocator const& other) = delete; + Allocator(Allocator&& other) noexcept = default; + Allocator& operator=(Allocator const& other) = delete; + Allocator& operator=(Allocator&& other) noexcept = default; + + static void debug_check(Allocator const&); +}; +} + +namespace cvk { +struct Allocator::State { + vk_not_null vmaAllocator; +}; +} + +namespace cvk { +inline void Allocator::debug_check(Allocator const& allocator) { + CTH_CRITICAL(!allocator.created(), "allocator must be created") {} +} +} diff --git a/cvk/incl/cvk/base/core.hpp b/cvk/incl/cvk/base/core.hpp index d5388df1..4c33c31b 100644 --- a/cvk/incl/cvk/base/core.hpp +++ b/cvk/incl/cvk/base/core.hpp @@ -16,13 +16,14 @@ namespace cvk { +class Allocator; class Device; class Instance; class PhysicalDevice; class Surface; /** - * @brief picks the physical device and owns it with the device and the queues + * @brief picks the physical device and owns it with the device, the allocator and the queues * @details the instance outlives the core */ class Core { @@ -60,19 +61,21 @@ class Core { /** * @brief wraps the state - * @details empty @ref State::queues -> copies the queues of the device + * @details empty @ref State::queues -> copies the queues of the device, + * nullptr @ref State::allocator -> creates the allocator * @post @ref created() * @note calls @ref optDestroy() */ void wrap(State); /** - * @brief picks a physical device for the first possible queue set, creates the device and the queues + * @brief picks a physical device for the first possible queue set, creates the device, the allocator and the queues * @param surfaces the present queues present to every surface * @post @ref created() * @note calls @ref optDestroy() * @throws cvk::cvk_exception reason: no physical device meets the requirements * @throws cvk::vk_result_exception result of vkCreateDevice() + * @throws cvk::vk_result_exception result of vmaCreateAllocator() */ void create(std::span surfaces, Config const&); @@ -119,6 +122,11 @@ class Core { [[nodiscard]] VolkDeviceTable const* functions() const; [[nodiscard]] VkDevice vkDevice() const; + /** + * @pre @ref created() + */ + [[nodiscard]] Allocator const& allocator() const; + /** * @param idx index in @ref Config::queueProperties * @pre @ref created() @@ -168,6 +176,8 @@ class Core { not_null _instance; std::unique_ptr _physicalDevice; + // declared before the device: move assignment destroys the old allocator before the old device + std::unique_ptr _allocator; std::unique_ptr _device; std::optional _queueSetIndex; std::vector _queues; @@ -193,6 +203,12 @@ struct Core::State { * @pre created with @ref physicalDevice */ unique_not_null device; + /** + * @details nullptr -> @ref wrap() creates it + * @pre @ref Allocator::created() + * @pre created with @ref device + */ + std::unique_ptr allocator; /** * @brief queue per entry of @ref Config::queueProperties * @details empty -> the queues of @ref device diff --git a/cvk/incl/cvk/data/memory/memory.hpp b/cvk/incl/cvk/data/memory/memory.hpp new file mode 100644 index 00000000..b0d1d94f --- /dev/null +++ b/cvk/incl/cvk/data/memory/memory.hpp @@ -0,0 +1,181 @@ +#pragma once +#include "memory_config.hpp" + +#include "cvk/utility/types.hpp" +#include "cvk/utility/unique_handle.hpp" + +#include +#include + +#include + +#include + + +namespace cvk { +class Core; + +/** + * @brief VmaAllocation wrapper, a range of device memory + * @details VMA places many allocations in one VkDeviceMemory, @ref info() returns the range + * @attention calls with one memory are not thread safe + */ +class Memory { +public: + using Config = MemoryConfig; + struct State; + + /** + * @brief base constructor + * @pre @ref Core::created() + */ + Memory(Core const&, Config const&); + + /** + * @brief constructs and wraps + * @details calls: + * - @ref Memory(Core const&, Config const&) + * - @ref wrap() + */ + Memory(Core const&, Config const&, State const&); + + /** + * @brief constructs and creates + * @details calls: + * - @ref Memory(Core const&, Config const&) + * - @ref create() + */ + Memory(Core const&, Config const&, VkMemoryRequirements const&); + + /** + * @note calls @ref optDestroy() + */ + ~Memory() { optDestroy(); } + + /** + * @brief wraps the state + * @pre the allocator of the core created the allocation + * @post @ref created() + * @note calls @ref optDestroy() + */ + void wrap(State const&); + + /** + * @brief allocates memory for the requirements + * @pre !@ref Config::autoUsage(), VMA_MEMORY_USAGE_AUTO* needs a buffer or an image + * @post @ref created() + * @note calls @ref optDestroy() + * @throws cvk::vk_result_exception result of vmaAllocateMemory() + */ + void create(VkMemoryRequirements const&); + + /** + * @brief frees the memory and forgets + * @pre @ref created() + * @pre the gpu does not use the memory + */ + void destroy(); + + /** + * @brief if @ref created() -> calls @ref destroy() + */ + void optDestroy() { if(created()) destroy(); } + + /** + * @brief releases ownership, returns the state and forgets + * @pre @ref created() + */ + State release(); + + /** + * @brief maps the memory + * @param size (VK_WHOLE_SIZE => to the end) + * @param offset from the start of the memory + * @return the mapped range + * @pre @ref created() + * @pre @ref properties() has VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT + * @pre offset + size <= @ref size() + * @note VMA counts the maps, each call needs one @ref unmap() + * @throws cvk::vk_result_exception result of vmaMapMemory() + */ + [[nodiscard]] std::span map(size_t size = VK_WHOLE_SIZE, size_t offset = 0) const; + + /** + * @brief unmaps the memory once + * @pre @ref map() was called more often than @ref unmap() + */ + void unmap() const; + + /** + * @brief makes host writes visible to the device + * @param size (VK_WHOLE_SIZE => to the end) + * @param offset from the start of the memory + * @details host coherent memory -> does nothing + * @pre @ref created() + * @throws cvk::vk_result_exception result of vmaFlushAllocation() + */ + void flush(size_t size = VK_WHOLE_SIZE, size_t offset = 0) const; + + /** + * @brief makes device writes visible to the host + * @param size (VK_WHOLE_SIZE => to the end) + * @param offset from the start of the memory + * @details host coherent memory -> does nothing + * @pre @ref created() + * @throws cvk::vk_result_exception result of vmaInvalidateAllocation() + */ + void invalidate(size_t size = VK_WHOLE_SIZE, size_t offset = 0) const; + + static void destroy(VmaAllocator, VmaAllocation); + + [[nodiscard]] bool created() const { return static_cast(_handle); } + [[nodiscard]] VmaAllocation get() const { return _handle.get(); } + [[nodiscard]] Core const& core() const { return _handle.core(); } + [[nodiscard]] Config const& config() const { return _config; } + + /** + * @return the VkDeviceMemory, the offset and the size of the range + * @pre @ref created() + */ + [[nodiscard]] VmaAllocationInfo info() const; + + /** + * @return the property flags of the memory type that VMA picked + * @pre @ref created() + */ + [[nodiscard]] VkMemoryPropertyFlags properties() const; + + /** + * @pre @ref created() + */ + [[nodiscard]] size_t size() const { return info().size; } + +private: + void forget(); + + [[nodiscard]] VmaAllocator allocator() const; + + unique_handle> _handle; + Config _config; + +public: + Memory(Memory const& other) = delete; + Memory(Memory&& other) noexcept = default; + Memory& operator=(Memory const& other) = delete; + Memory& operator=(Memory&& other) noexcept = default; + + static void debug_check(Memory const&); +}; +} + +namespace cvk { +struct Memory::State { + vk_not_null vmaAllocation; +}; +} + +namespace cvk { +inline void Memory::debug_check(Memory const& memory) { + CTH_CRITICAL(!memory.created(), "memory must be created") {} +} +} diff --git a/cvk/incl/cvk/data/memory/memory_config.hpp b/cvk/incl/cvk/data/memory/memory_config.hpp new file mode 100644 index 00000000..bebb0703 --- /dev/null +++ b/cvk/incl/cvk/data/memory/memory_config.hpp @@ -0,0 +1,53 @@ +#pragma once +#include +#include + + +namespace cvk { +/** + * @brief what the memory is for, VMA picks the memory type from it + */ +struct MemoryConfig { + /** + * @brief VMA_ALLOCATION_CREATE_* flags, e.g. host access, mapped, dedicated memory + */ + VmaAllocationCreateFlags flags = 0; + + /** + * @brief VMA_MEMORY_USAGE_AUTO* -> only for buffers and images that VMA creates with the memory + */ + VmaMemoryUsage usage = VMA_MEMORY_USAGE_AUTO; + + /** + * @brief the memory type must have these flags + */ + VkMemoryPropertyFlags requiredFlags = 0; + + /** + * @brief VMA prefers memory types with these flags + */ + VkMemoryPropertyFlags preferredFlags = 0; + + [[nodiscard]] constexpr VmaAllocationCreateInfo createInfo() const { + return VmaAllocationCreateInfo{ + .flags = flags, + .usage = usage, + .requiredFlags = requiredFlags, + .preferredFlags = preferredFlags, + .memoryTypeBits = 0, + .pool = VK_NULL_HANDLE, + .pUserData = nullptr, + .priority = 0.0f, + .minAlignment = 0, + }; + } + + /** + * @return @ref usage is VMA_MEMORY_USAGE_AUTO* + */ + [[nodiscard]] constexpr bool autoUsage() const { + return usage == VMA_MEMORY_USAGE_AUTO || usage == VMA_MEMORY_USAGE_AUTO_PREFER_DEVICE || + usage == VMA_MEMORY_USAGE_AUTO_PREFER_HOST; + } +}; +} diff --git a/cvk/incl/cvk/utility/unique_handle.hpp b/cvk/incl/cvk/utility/unique_handle.hpp index ddec2f61..725477b0 100644 --- a/cvk/incl/cvk/utility/unique_handle.hpp +++ b/cvk/incl/cvk/utility/unique_handle.hpp @@ -10,14 +10,17 @@ #include +struct VmaAllocator_T; + namespace cvk { class Core; class Instance; } namespace cvk::dev { -[[nodiscard]] DeviceTable device_table(Core const& core); -[[nodiscard]] VkInstance vk_instance(Instance const& instance); +[[nodiscard]] DeviceTable device_table(Core const&); +[[nodiscard]] VkInstance vk_instance(Instance const&); +[[nodiscard]] VmaAllocator_T* vma_allocator(Core const&); } namespace cvk { @@ -40,6 +43,20 @@ class core_deleter { not_null _core; }; +/** + * @brief deleter of memory objects, context: the core + * @details calls T::destroy(core.allocator().get(), handle) + * @pre the core outlives the deleter + */ +template +class vma_deleter : public core_deleter { +public: + using core_deleter::core_deleter; + + template + void operator()(Handle handle) const { T::destroy(dev::vma_allocator(this->core()), handle); } +}; + /** * @brief deleter of instance objects, context: the VkInstance * @details calls T::destroy(vkInstance, handle), @@ -72,7 +89,7 @@ struct handle_deleter { /** * @brief owning Vulkan handle, std::unique_ptr with a deleter that holds the destroy context * @tparam Handle Vulkan handle, or Deleter::pointer for handles that are not pointers - * @tparam Deleter @ref core_deleter, @ref instance_deleter, @ref handle_deleter or a deleter derived from them + * @tparam Deleter @ref core_deleter, @ref vma_deleter, @ref instance_deleter, @ref handle_deleter or a deleter derived from them * @details a handle comes only together with its context: the constructors and reset() construct the deleter, * a missing context argument does not compile, * move assignment destroys the old handle with the old context, then takes handle and context diff --git a/cvk/src/base/allocator.cpp b/cvk/src/base/allocator.cpp new file mode 100644 index 00000000..7a3fda69 --- /dev/null +++ b/cvk/src/base/allocator.cpp @@ -0,0 +1,107 @@ +#include "cvk/base/allocator.hpp" + +#include "cvk/base/device.hpp" +#include "cvk/base/instance.hpp" +#include "cvk/base/physical_device.hpp" +#include "cvk/utility/vk_exceptions.hpp" + +#include +#include + + +namespace cvk { +Allocator::Allocator(Device const& device) : _device{&device} { Device::debug_check(device); } + +Allocator::Allocator(Device const& device, State const& state) : Allocator{device} { wrap(state); } + +Allocator::Allocator(Device const& device, create_t) : Allocator{device} { create(); } + +void Allocator::wrap(State const& state) { + optDestroy(); + + _handle.reset(state.vmaAllocator.get()); +} + +void Allocator::create() { + optDestroy(); + + auto const& physicalDevice = _device->physicalDevice(); + + VmaAllocatorCreateInfo info{ + .flags = flags(physicalDevice), + .physicalDevice = physicalDevice.get(), + .device = _device->get(), + .preferredLargeHeapBlockSize = 0, + .pAllocationCallbacks = nullptr, + .pDeviceMemoryCallbacks = nullptr, + .pHeapSizeLimit = nullptr, + .pVulkanFunctions = nullptr, + .instance = _device->instance().get(), + .vulkanApiVersion = VK_API_VERSION_1_3, + .pTypeExternalMemoryHandleTypes = nullptr, + }; + + VmaVulkanFunctions functions{}; + auto const importResult = vmaImportVulkanFunctionsFromVolk(&info, &functions); + CTH_STABLE_ERR(importResult != VK_SUCCESS, "failed to import the vulkan functions for VMA") { + forget(); + throw cvk::vk_result_exception{importResult, details->exception()}; + } + info.pVulkanFunctions = &functions; + + VmaAllocator ptr = VK_NULL_HANDLE; + auto const result = vmaCreateAllocator(&info, &ptr); + + CTH_STABLE_ERR(result != VK_SUCCESS, "failed to create allocator") { + forget(); + throw cvk::vk_result_exception{result, details->exception()}; + } + + _handle.reset(ptr); +} + +void Allocator::destroy() { + debug_check(*this); + + _handle.reset(); + + forget(); +} + +Allocator::State Allocator::release() { + debug_check(*this); + + State const state{_handle.release()}; + forget(); + return state; +} + +void Allocator::destroy(VmaAllocator vmaAllocator) { + CTH_WARN(vmaAllocator == VK_NULL_HANDLE, "vmaAllocator invalid (VK_NULL_HANDLE)") {} + + vmaDestroyAllocator(vmaAllocator); +} + +VmaAllocatorCreateFlags Allocator::flags(PhysicalDevice const& physicalDevice) { + auto const& requiredFeatures = physicalDevice.requiredFeatures(); + auto const requiredExtensions = physicalDevice.requiredExtensions(); + + auto const enabled = [requiredExtensions](std::string_view extension) { + return std::ranges::contains(requiredExtensions, extension); + }; + + VmaAllocatorCreateFlags flags = 0; + + if(requiredFeatures.features().bufferDeviceAddress == VK_TRUE) + flags |= VMA_ALLOCATOR_CREATE_BUFFER_DEVICE_ADDRESS_BIT; + + if(requiredFeatures.features().maintenance4 == VK_TRUE) + flags |= VMA_ALLOCATOR_CREATE_KHR_MAINTENANCE4_BIT; + + if(enabled(VK_EXT_MEMORY_BUDGET_EXTENSION_NAME)) flags |= VMA_ALLOCATOR_CREATE_EXT_MEMORY_BUDGET_BIT; + + return flags; +} + +void Allocator::forget() {} +} diff --git a/cvk/src/base/core.cpp b/cvk/src/base/core.cpp index 9a16640b..2d5bdc8a 100644 --- a/cvk/src/base/core.cpp +++ b/cvk/src/base/core.cpp @@ -1,5 +1,6 @@ #include "cvk/base/core.hpp" +#include "cvk/base/allocator.hpp" #include "cvk/base/device.hpp" #include "cvk/base/instance.hpp" #include "cvk/base/physical_device.hpp" @@ -31,11 +32,20 @@ void Core::wrap(State state) { &state.device->physicalDevice() != state.physicalDevice.get(), "device must be created with the physical device of the state" ) {} + if(state.allocator != nullptr) { + Allocator::debug_check(*state.allocator); + CTH_CRITICAL( + &state.allocator->device() != state.device.get(), + "allocator must be created with the device of the state" + ) {} + } optDestroy(); _physicalDevice = state.physicalDevice.release_val(); _device = state.device.release_val(); + _allocator = std::move(state.allocator); + if(_allocator == nullptr) _allocator = std::make_unique(*_device, cvk::create); _queueSetIndex = state.queueSetIndex; if(state.queues.empty()) _queues.assign_range(_device->queues()); @@ -49,11 +59,16 @@ void Core::create(span surfaces, Config const& config) { auto [uniqueQueueFamilyIndices, queuesToUniqueQueues] = createPhysicalDevice(surfaces, config); - _device = std::make_unique( + auto device = std::make_unique( *_instance, *_physicalDevice, Device::Config{std::move(uniqueQueueFamilyIndices)} ); + // a throw destroys the device, the core stays not created + auto allocator = std::make_unique(*device, cvk::create); + + _device = std::move(device); + _allocator = std::move(allocator); createQueues(queuesToUniqueQueues); } @@ -69,6 +84,7 @@ Core::State Core::release() { State state{ .physicalDevice = std::move(_physicalDevice), .device = std::move(_device), + .allocator = std::move(_allocator), .queues = std::move(_queues), .queueSetIndex = _queueSetIndex, }; @@ -100,8 +116,14 @@ DeviceTable Core::deviceTable() const { return device().table(); } VolkDeviceTable const* Core::functions() const { return device().functions(); } VkDevice Core::vkDevice() const { return device().get(); } +Allocator const& Core::allocator() const { + debug_check(*this); + return *_allocator; +} + void Core::forget() { _queues.clear(); + _allocator = nullptr; _device = nullptr; _physicalDevice = nullptr; _queueSetIndex = std::nullopt; diff --git a/cvk/src/data/memory/memory.cpp b/cvk/src/data/memory/memory.cpp new file mode 100644 index 00000000..56ca8a1d --- /dev/null +++ b/cvk/src/data/memory/memory.cpp @@ -0,0 +1,120 @@ +#include "cvk/data/memory/memory.hpp" + +#include "cvk/base/allocator.hpp" +#include "cvk/base/core.hpp" +#include "cvk/utility/vk_exceptions.hpp" + + +namespace cvk { +Memory::Memory(Core const& core, Config const& config) : _handle{core}, _config{config} { Core::debug_check(core); } + +Memory::Memory(Core const& core, Config const& config, State const& state) : Memory{core, config} { wrap(state); } + +Memory::Memory(Core const& core, Config const& config, VkMemoryRequirements const& requirements) : Memory{ + core, + config +} { create(requirements); } + +void Memory::wrap(State const& state) { + optDestroy(); + + _handle.reset(state.vmaAllocation.get(), core()); +} + +void Memory::create(VkMemoryRequirements const& requirements) { + CTH_CRITICAL(_config.autoUsage(), "VMA_MEMORY_USAGE_AUTO* needs a buffer or an image, use the flags") {} + + optDestroy(); + + auto const info = _config.createInfo(); + + VmaAllocation ptr = VK_NULL_HANDLE; + auto const result = vmaAllocateMemory(allocator(), &requirements, &info, &ptr, nullptr); + + CTH_STABLE_ERR(result != VK_SUCCESS, "failed to allocate memory") { + forget(); + throw cvk::vk_result_exception{result, details->exception()}; + } + + _handle.reset(ptr, core()); +} + +void Memory::destroy() { + debug_check(*this); + + _handle.reset(); + + forget(); +} + +Memory::State Memory::release() { + debug_check(*this); + + State const state{_handle.release()}; + forget(); + return state; +} + +std::span Memory::map(size_t size, size_t offset) const { + debug_check(*this); + CTH_CRITICAL((properties() & VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT) == 0, "memory must be host visible") {} + + size_t const memorySize = this->size(); + CTH_CRITICAL(offset > memorySize, "offset must lie in the memory") {} + + size_t const mapSize = size == VK_WHOLE_SIZE ? memorySize - offset : size; + CTH_CRITICAL(mapSize > memorySize - offset, "range must lie in the memory") {} + + void* data = nullptr; + auto const result = vmaMapMemory(allocator(), get(), &data); + CVK_RESULT_STABLE_THROW(result != VK_SUCCESS, result, "failed to map memory") {} + + return {static_cast(data) + offset, mapSize}; +} + +void Memory::unmap() const { + debug_check(*this); + + vmaUnmapMemory(allocator(), get()); +} + +void Memory::flush(size_t size, size_t offset) const { + debug_check(*this); + + auto const result = vmaFlushAllocation(allocator(), get(), offset, size); + CVK_RESULT_STABLE_THROW(result != VK_SUCCESS, result, "failed to flush memory") {} +} + +void Memory::invalidate(size_t size, size_t offset) const { + debug_check(*this); + + auto const result = vmaInvalidateAllocation(allocator(), get(), offset, size); + CVK_RESULT_STABLE_THROW(result != VK_SUCCESS, result, "failed to invalidate memory") {} +} + +void Memory::destroy(VmaAllocator vmaAllocator, VmaAllocation vmaAllocation) { + CTH_WARN(vmaAllocation == VK_NULL_HANDLE, "vmaAllocation invalid (VK_NULL_HANDLE)") {} + + vmaFreeMemory(vmaAllocator, vmaAllocation); +} + +VmaAllocationInfo Memory::info() const { + debug_check(*this); + + VmaAllocationInfo info{}; + vmaGetAllocationInfo(allocator(), get(), &info); + return info; +} + +VkMemoryPropertyFlags Memory::properties() const { + debug_check(*this); + + VkMemoryPropertyFlags flags = 0; + vmaGetAllocationMemoryProperties(allocator(), get(), &flags); + return flags; +} + +void Memory::forget() {} + +VmaAllocator Memory::allocator() const { return core().allocator().get(); } +} diff --git a/cvk/src/utility/unique_handle.cpp b/cvk/src/utility/unique_handle.cpp index 307739fb..eb15907a 100644 --- a/cvk/src/utility/unique_handle.cpp +++ b/cvk/src/utility/unique_handle.cpp @@ -1,5 +1,6 @@ #include "cvk/utility/unique_handle.hpp" +#include "cvk/base/allocator.hpp" #include "cvk/base/core.hpp" #include "cvk/base/instance.hpp" @@ -8,4 +9,6 @@ namespace cvk::dev { DeviceTable device_table(Core const& core) { return core.deviceTable(); } VkInstance vk_instance(Instance const& instance) { return instance.get(); } + +VmaAllocator vma_allocator(Core const& core) { return core.allocator().get(); } } diff --git a/cvk/src/utility/vma.cpp b/cvk/src/utility/vma.cpp new file mode 100644 index 00000000..1c54d24b --- /dev/null +++ b/cvk/src/utility/vma.cpp @@ -0,0 +1,5 @@ +// the implementation of VMA, the headers only declare it +#define VMA_IMPLEMENTATION + +#include +#include diff --git a/cvk/tests/src/base/allocator_test.cpp b/cvk/tests/src/base/allocator_test.cpp new file mode 100644 index 00000000..3f478b0f --- /dev/null +++ b/cvk/tests/src/base/allocator_test.cpp @@ -0,0 +1,78 @@ +#include "context.hpp" +#include "test.hpp" + +#include "cvk/base/allocator.hpp" +#include "cvk/base/device.hpp" +#include "cvk/base/physical_device.hpp" + + +namespace cvk { +CVK_TEST(Allocator, core_creates_the_allocator) { + test::Context const context{}; + + auto const& allocator = context.core.allocator(); + + EXPECT_TRUE(allocator.created()); + EXPECT_EQ(&allocator.device(), &context.core.device()); +} + +CVK_TEST(Allocator, base_constructor_does_not_create) { + test::Context const context{}; + + Allocator const allocator{context.core.device()}; + + EXPECT_FALSE(allocator.created()); +} + +CVK_TEST(Allocator, create_and_destroy) { + test::Context const context{}; + Allocator allocator{context.core.device(), create}; + + ASSERT_TRUE(allocator.created()); + + allocator.destroy(); + EXPECT_FALSE(allocator.created()); +} + +CVK_TEST(Allocator, release_and_wrap) { + test::Context const context{}; + Allocator allocator{context.core.device(), create}; + auto const vmaAllocator = allocator.get(); + + auto const state = allocator.release(); + EXPECT_FALSE(allocator.created()); + + Allocator const wrapped{context.core.device(), state}; + EXPECT_EQ(wrapped.get(), vmaAllocator); +} + +CVK_TEST(Allocator, move_assignment_destroys_the_old_allocator) { + test::Context const context{}; + Allocator allocator{context.core.device(), create}; + Allocator other{context.core.device(), create}; + auto const vmaAllocator = other.get(); + + allocator = std::move(other); + + EXPECT_EQ(allocator.get(), vmaAllocator); + EXPECT_FALSE(other.created()); +} + +CVK_TEST(Allocator, flags_follow_the_required_features) { + auto config = test::Context::CONFIG; + config.requiredFeatures.features().bufferDeviceAddress = VK_TRUE; + test::Context const context{config}; + + auto const flags = Allocator::flags(context.core.physicalDevice()); + + EXPECT_NE(flags & VMA_ALLOCATOR_CREATE_BUFFER_DEVICE_ADDRESS_BIT, 0u); +} + +CVK_TEST(Allocator, no_flags_without_the_features) { + test::Context const context{}; + + auto const flags = Allocator::flags(context.core.physicalDevice()); + + EXPECT_EQ(flags & VMA_ALLOCATOR_CREATE_BUFFER_DEVICE_ADDRESS_BIT, 0u); +} +} diff --git a/cvk/tests/src/base/core_test.cpp b/cvk/tests/src/base/core_test.cpp index 2ec760a6..40db6b62 100644 --- a/cvk/tests/src/base/core_test.cpp +++ b/cvk/tests/src/base/core_test.cpp @@ -2,6 +2,7 @@ #include "test.hpp" #include "validation.hpp" +#include "cvk/base/allocator.hpp" #include "cvk/base/core.hpp" #include "cvk/base/device.hpp" #include "cvk/base/instance.hpp" @@ -131,6 +132,7 @@ CVK_TEST(Core, release_and_wrap) { Core core{instance, {}, CONFIG}; auto const vkDevice = core.vkDevice(); + auto const vmaAllocator = core.allocator().get(); auto state = core.release(); EXPECT_FALSE(core.created()); @@ -140,6 +142,7 @@ CVK_TEST(Core, release_and_wrap) { ASSERT_TRUE(wrapped.created()); EXPECT_EQ(wrapped.vkDevice(), vkDevice); + EXPECT_EQ(wrapped.allocator().get(), vmaAllocator); ASSERT_EQ(wrapped.queues().size(), CONFIG.queueProperties.size()); wrapped.queue(0).wait(); } @@ -156,6 +159,19 @@ CVK_TEST(Core, wrap_without_queues_takes_the_device_queues) { EXPECT_EQ(wrapped.queues().size(), wrapped.device().queues().size()); } +CVK_TEST(Core, wrap_without_allocator_creates_one) { + Instance const instance{APP_NAME, NO_EXTENSIONS, std::nullopt}; + + Core core{instance, {}, CONFIG}; + auto state = core.release(); + state.allocator = nullptr; + + Core const wrapped{instance, std::move(state)}; + + EXPECT_TRUE(wrapped.allocator().created()); + EXPECT_EQ(&wrapped.allocator().device(), &wrapped.device()); +} + CVK_TEST(Core, queues_work_after_move) { Instance const instance{APP_NAME, NO_EXTENSIONS, std::nullopt}; diff --git a/cvk/tests/src/data/memory/memory_test.cpp b/cvk/tests/src/data/memory/memory_test.cpp new file mode 100644 index 00000000..d03b0584 --- /dev/null +++ b/cvk/tests/src/data/memory/memory_test.cpp @@ -0,0 +1,135 @@ +#include "context.hpp" +#include "test.hpp" + +#include "cvk/data/memory/memory.hpp" + +#include +#include + + +namespace cvk { +namespace { + constexpr size_t SIZE = 1024; + + constexpr VkMemoryRequirements REQUIREMENTS{.size = SIZE, .alignment = 16, .memoryTypeBits = ~0u}; + + /** + * @brief host visible memory, memory without a buffer or an image cannot use VMA_MEMORY_USAGE_AUTO + */ + constexpr Memory::Config HOST_CONFIG{ + .usage = VMA_MEMORY_USAGE_UNKNOWN, + .requiredFlags = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, + }; +} + +CVK_TEST(Memory, config_create_info) { + constexpr Memory::Config config{ + .flags = VMA_ALLOCATION_CREATE_DEDICATED_MEMORY_BIT, + .usage = VMA_MEMORY_USAGE_UNKNOWN, + .requiredFlags = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT, + .preferredFlags = VK_MEMORY_PROPERTY_HOST_CACHED_BIT, + }; + + constexpr auto info = config.createInfo(); + + EXPECT_EQ(info.flags, VMA_ALLOCATION_CREATE_DEDICATED_MEMORY_BIT); + EXPECT_EQ(info.usage, VMA_MEMORY_USAGE_UNKNOWN); + EXPECT_EQ(info.requiredFlags, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT); + EXPECT_EQ(info.preferredFlags, VK_MEMORY_PROPERTY_HOST_CACHED_BIT); + EXPECT_EQ(info.pool, VK_NULL_HANDLE); +} + +CVK_TEST(Memory, config_auto_usage) { + static_assert(Memory::Config{}.autoUsage()); + static_assert(Memory::Config{.usage = VMA_MEMORY_USAGE_AUTO_PREFER_HOST}.autoUsage()); + static_assert(!HOST_CONFIG.autoUsage()); +} + +CVK_TEST(Memory, base_constructor_does_not_create) { + test::Context const context{}; + + Memory const memory{context.core, HOST_CONFIG}; + + EXPECT_FALSE(memory.created()); +} + +CVK_TEST(Memory, create_and_destroy) { + test::Context const context{}; + Memory memory{context.core, HOST_CONFIG, REQUIREMENTS}; + + ASSERT_TRUE(memory.created()); + EXPECT_EQ(&memory.core(), &context.core); + EXPECT_NE(memory.info().deviceMemory, VK_NULL_HANDLE); + EXPECT_GE(memory.size(), SIZE); + + memory.destroy(); + EXPECT_FALSE(memory.created()); +} + +CVK_TEST(Memory, properties_have_the_required_flags) { + test::Context const context{}; + Memory const memory{context.core, HOST_CONFIG, REQUIREMENTS}; + + auto const properties = memory.properties(); + + EXPECT_EQ(properties & HOST_CONFIG.requiredFlags, HOST_CONFIG.requiredFlags); +} + +CVK_TEST(Memory, map_write_and_read) { + test::Context const context{}; + Memory const memory{context.core, HOST_CONFIG, REQUIREMENTS}; + + auto const written = memory.map(); + ASSERT_EQ(written.size(), memory.size()); + std::ranges::fill(written, 'x'); + memory.flush(); + memory.unmap(); + + auto const read = memory.map(); + memory.invalidate(); + EXPECT_TRUE(std::ranges::all_of(read, [](char const c) { return c == 'x'; })); + memory.unmap(); +} + +CVK_TEST(Memory, map_a_range) { + test::Context const context{}; + Memory const memory{context.core, HOST_CONFIG, REQUIREMENTS}; + + auto const whole = memory.map(); + auto const part = memory.map(16, 32); + + EXPECT_EQ(part.data(), whole.data() + 32); + EXPECT_EQ(part.size(), 16); + + auto const end = memory.map(VK_WHOLE_SIZE, 32); + EXPECT_EQ(end.size(), memory.size() - 32); + + memory.unmap(); + memory.unmap(); + memory.unmap(); +} + +CVK_TEST(Memory, release_and_wrap) { + test::Context const context{}; + Memory memory{context.core, HOST_CONFIG, REQUIREMENTS}; + auto const vmaAllocation = memory.get(); + + auto const state = memory.release(); + EXPECT_FALSE(memory.created()); + + Memory const wrapped{context.core, HOST_CONFIG, state}; + EXPECT_EQ(wrapped.get(), vmaAllocation); +} + +CVK_TEST(Memory, move_assignment_destroys_the_old_memory) { + test::Context const context{}; + Memory memory{context.core, HOST_CONFIG, REQUIREMENTS}; + Memory other{context.core, HOST_CONFIG, REQUIREMENTS}; + auto const vmaAllocation = other.get(); + + memory = std::move(other); + + EXPECT_EQ(memory.get(), vmaAllocation); + EXPECT_FALSE(other.created()); +} +} From a982ae976ce8e80ddf6d083cc5e65683d869e11d Mon Sep 17 00:00:00 2001 From: lukas <89145039+codethinki@users.noreply.github.com> Date: Fri, 16 Oct 2026 23:34:44 +0200 Subject: [PATCH 032/135] cvk: vma_handle alias --- AGENTS.md | 1 + cvk/incl/cvk/data/memory/memory.hpp | 2 +- cvk/incl/cvk/utility/unique_handle.hpp | 7 +++++++ cvk/tests/src/utility/unique_handle_test.cpp | 6 ++++++ 4 files changed, 15 insertions(+), 1 deletion(-) diff --git a/AGENTS.md b/AGENTS.md index 2110770a..f013bc08 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -162,6 +162,7 @@ public: - They store the handle in `cvk::unique_handle` (`cvk/utility/unique_handle.hpp`): a `std::unique_ptr` with a deleter that holds the destroy context and calls the static `destroy`. Deleters: `core_deleter` (the core), `vma_deleter` (the core, destroys through its allocator), `instance_deleter` (the `VkInstance`), `handle_deleter` (no context) or a deleter derived from them. Device objects use `core_handle`, short for `unique_handle>`. + Objects with VMA memory use `vma_handle`, short for `unique_handle>`. - Handle and context go in together: `_handle.reset(ptr, core())`. A missing context argument does not compile. - Move constructor and move assignment are `= default`. The move assignment destroys the old handle with the old context. - A class that owns more than one object declares the objects that depend on its handle first. `= default` assigns in declaration order: `Instance` declares `_debugMessenger` before `_handle`. diff --git a/cvk/incl/cvk/data/memory/memory.hpp b/cvk/incl/cvk/data/memory/memory.hpp index b0d1d94f..49573c34 100644 --- a/cvk/incl/cvk/data/memory/memory.hpp +++ b/cvk/incl/cvk/data/memory/memory.hpp @@ -155,7 +155,7 @@ class Memory { [[nodiscard]] VmaAllocator allocator() const; - unique_handle> _handle; + vma_handle _handle; Config _config; public: diff --git a/cvk/incl/cvk/utility/unique_handle.hpp b/cvk/incl/cvk/utility/unique_handle.hpp index 725477b0..e8e5f3a2 100644 --- a/cvk/incl/cvk/utility/unique_handle.hpp +++ b/cvk/incl/cvk/utility/unique_handle.hpp @@ -176,4 +176,11 @@ class unique_handle { */ template using core_handle = unique_handle>; + +/** + * @brief @ref unique_handle of a memory object, the deleter stores the core and frees through its allocator + * @tparam T owner class with a static destroy(VmaAllocator, Handle) + */ +template +using vma_handle = unique_handle>; } diff --git a/cvk/tests/src/utility/unique_handle_test.cpp b/cvk/tests/src/utility/unique_handle_test.cpp index 0684d765..0354f77c 100644 --- a/cvk/tests/src/utility/unique_handle_test.cpp +++ b/cvk/tests/src/utility/unique_handle_test.cpp @@ -4,6 +4,7 @@ #include "validation.hpp" #include "cvk/base/instance.hpp" +#include "cvk/data/memory/memory.hpp" #include "cvk/render/sync/fence.hpp" #include "cvk/surface/surface.hpp" #include "cvk/utility/constants.hpp" @@ -103,6 +104,10 @@ CVK_TEST(unique_handle, a_handle_needs_its_context) { static_assert(std::default_initializable); static_assert(resettable); + using memory_handle = vma_handle; + static_assert(!resettable); + static_assert(resettable); + using pool_handle = unique_handle; static_assert(!resettable); static_assert(resettable); @@ -114,6 +119,7 @@ CVK_TEST(unique_handle, a_handle_needs_its_context) { CVK_TEST(unique_handle, size) { static_assert(sizeof(unique_handle>) == sizeof(VkInstance)); static_assert(sizeof(core_handle) == sizeof(Core const*) + sizeof(VkFence)); + static_assert(sizeof(vma_handle) == sizeof(Core const*) + sizeof(VmaAllocation)); static_assert( sizeof(unique_handle>) == sizeof(VkInstance) + sizeof(VkSurfaceKHR) ); From 1025cdb30df0f87fef9c2fe16588ae5158b74c6b Mon Sep 17 00:00:00 2001 From: lukas <89145039+codethinki@users.noreply.github.com> Date: Fri, 16 Oct 2026 23:39:57 +0200 Subject: [PATCH 033/135] cvk: core_handle and vma_handle in their own headers unique_handle.hpp keeps the generic part: unique_handle, instance_deleter, handle_deleter. Only the core handles include the device table, only the vma handles include VMA. --- AGENTS.md | 4 +- cvk/incl/cvk/data/img/sampler.hpp | 2 +- cvk/incl/cvk/data/memory/memory.hpp | 2 +- .../pipeline/layout/descriptor_set_layout.hpp | 2 +- .../pipeline/layout/pipeline_layout.hpp | 2 +- cvk/incl/cvk/render/pipeline/pipeline.hpp | 2 +- .../cvk/render/pipeline/shader/shader.hpp | 2 +- cvk/incl/cvk/render/sync/fence.hpp | 2 +- cvk/incl/cvk/render/sync/semaphore.hpp | 2 +- cvk/incl/cvk/utility/core_handle.hpp | 41 ++++++++++++++ cvk/incl/cvk/utility/unique_handle.hpp | 53 ------------------- cvk/incl/cvk/utility/vma_handle.hpp | 34 ++++++++++++ cvk/src/utility/core_handle.cpp | 8 +++ cvk/src/utility/unique_handle.cpp | 6 --- cvk/src/utility/vma_handle.cpp | 9 ++++ cvk/tests/src/utility/unique_handle_test.cpp | 2 + 16 files changed, 104 insertions(+), 69 deletions(-) create mode 100644 cvk/incl/cvk/utility/core_handle.hpp create mode 100644 cvk/incl/cvk/utility/vma_handle.hpp create mode 100644 cvk/src/utility/core_handle.cpp create mode 100644 cvk/src/utility/vma_handle.cpp diff --git a/AGENTS.md b/AGENTS.md index f013bc08..97096fb5 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -161,8 +161,8 @@ public: - Classes that own a handle are move only. A class that differs (copyable, not movable, custom move) says so in its `@brief`. - They store the handle in `cvk::unique_handle` (`cvk/utility/unique_handle.hpp`): a `std::unique_ptr` with a deleter that holds the destroy context and calls the static `destroy`. Deleters: `core_deleter` (the core), `vma_deleter` (the core, destroys through its allocator), `instance_deleter` (the `VkInstance`), `handle_deleter` (no context) or a deleter derived from them. - Device objects use `core_handle`, short for `unique_handle>`. - Objects with VMA memory use `vma_handle`, short for `unique_handle>`. + Device objects use `core_handle` (`cvk/utility/core_handle.hpp`), short for `unique_handle>`. + Objects with VMA memory use `vma_handle` (`cvk/utility/vma_handle.hpp`), short for `unique_handle>`. - Handle and context go in together: `_handle.reset(ptr, core())`. A missing context argument does not compile. - Move constructor and move assignment are `= default`. The move assignment destroys the old handle with the old context. - A class that owns more than one object declares the objects that depend on its handle first. `= default` assigns in declaration order: `Instance` declares `_debugMessenger` before `_handle`. diff --git a/cvk/incl/cvk/data/img/sampler.hpp b/cvk/incl/cvk/data/img/sampler.hpp index 1b0cfbd4..eddfcaf4 100644 --- a/cvk/incl/cvk/data/img/sampler.hpp +++ b/cvk/incl/cvk/data/img/sampler.hpp @@ -3,8 +3,8 @@ #include "cvk/base/device_table.hpp" #include "cvk/utility/constants.hpp" +#include "cvk/utility/core_handle.hpp" #include "cvk/utility/types.hpp" -#include "cvk/utility/unique_handle.hpp" #include diff --git a/cvk/incl/cvk/data/memory/memory.hpp b/cvk/incl/cvk/data/memory/memory.hpp index 49573c34..e06f7328 100644 --- a/cvk/incl/cvk/data/memory/memory.hpp +++ b/cvk/incl/cvk/data/memory/memory.hpp @@ -2,7 +2,7 @@ #include "memory_config.hpp" #include "cvk/utility/types.hpp" -#include "cvk/utility/unique_handle.hpp" +#include "cvk/utility/vma_handle.hpp" #include #include diff --git a/cvk/incl/cvk/render/pipeline/layout/descriptor_set_layout.hpp b/cvk/incl/cvk/render/pipeline/layout/descriptor_set_layout.hpp index 489c4caf..3f14a2d5 100644 --- a/cvk/incl/cvk/render/pipeline/layout/descriptor_set_layout.hpp +++ b/cvk/incl/cvk/render/pipeline/layout/descriptor_set_layout.hpp @@ -1,8 +1,8 @@ #pragma once #include "cvk/base/device_table.hpp" #include "cvk/utility/constants.hpp" +#include "cvk/utility/core_handle.hpp" #include "cvk/utility/types.hpp" -#include "cvk/utility/unique_handle.hpp" #include diff --git a/cvk/incl/cvk/render/pipeline/layout/pipeline_layout.hpp b/cvk/incl/cvk/render/pipeline/layout/pipeline_layout.hpp index 66ff17a8..8e7741b3 100644 --- a/cvk/incl/cvk/render/pipeline/layout/pipeline_layout.hpp +++ b/cvk/incl/cvk/render/pipeline/layout/pipeline_layout.hpp @@ -1,8 +1,8 @@ #pragma once #include "cvk/base/device_table.hpp" #include "cvk/utility/constants.hpp" +#include "cvk/utility/core_handle.hpp" #include "cvk/utility/types.hpp" -#include "cvk/utility/unique_handle.hpp" #include diff --git a/cvk/incl/cvk/render/pipeline/pipeline.hpp b/cvk/incl/cvk/render/pipeline/pipeline.hpp index 90b9d6be..c2028b33 100644 --- a/cvk/incl/cvk/render/pipeline/pipeline.hpp +++ b/cvk/incl/cvk/render/pipeline/pipeline.hpp @@ -3,8 +3,8 @@ #include "cvk/base/device_table.hpp" #include "cvk/utility/constants.hpp" +#include "cvk/utility/core_handle.hpp" #include "cvk/utility/types.hpp" -#include "cvk/utility/unique_handle.hpp" #include diff --git a/cvk/incl/cvk/render/pipeline/shader/shader.hpp b/cvk/incl/cvk/render/pipeline/shader/shader.hpp index f5ff699b..ed4e342c 100644 --- a/cvk/incl/cvk/render/pipeline/shader/shader.hpp +++ b/cvk/incl/cvk/render/pipeline/shader/shader.hpp @@ -1,7 +1,7 @@ #pragma once #include "cvk/base/device_table.hpp" +#include "cvk/utility/core_handle.hpp" #include "cvk/utility/types.hpp" -#include "cvk/utility/unique_handle.hpp" #include diff --git a/cvk/incl/cvk/render/sync/fence.hpp b/cvk/incl/cvk/render/sync/fence.hpp index 8f807e85..625f41bd 100644 --- a/cvk/incl/cvk/render/sync/fence.hpp +++ b/cvk/incl/cvk/render/sync/fence.hpp @@ -1,7 +1,7 @@ #pragma once #include "cvk/base/device_table.hpp" +#include "cvk/utility/core_handle.hpp" #include "cvk/utility/types.hpp" -#include "cvk/utility/unique_handle.hpp" #include diff --git a/cvk/incl/cvk/render/sync/semaphore.hpp b/cvk/incl/cvk/render/sync/semaphore.hpp index 227380d4..a83d74f3 100644 --- a/cvk/incl/cvk/render/sync/semaphore.hpp +++ b/cvk/incl/cvk/render/sync/semaphore.hpp @@ -1,8 +1,8 @@ #pragma once #include "cvk/base/device_table.hpp" #include "cvk/utility/constants.hpp" +#include "cvk/utility/core_handle.hpp" #include "cvk/utility/types.hpp" -#include "cvk/utility/unique_handle.hpp" #include diff --git a/cvk/incl/cvk/utility/core_handle.hpp b/cvk/incl/cvk/utility/core_handle.hpp new file mode 100644 index 00000000..9a6b1fce --- /dev/null +++ b/cvk/incl/cvk/utility/core_handle.hpp @@ -0,0 +1,41 @@ +#pragma once +#include "cvk/base/device_table.hpp" +#include "cvk/utility/types.hpp" +#include "cvk/utility/unique_handle.hpp" + + +namespace cvk { +class Core; +} + +namespace cvk::dev { +[[nodiscard]] DeviceTable device_table(Core const&); +} + +namespace cvk { +/** + * @brief deleter of device objects, context: the core + * @details calls T::destroy(core.deviceTable(), handle) + * @pre the core outlives the deleter + */ +template +class core_deleter { +public: + explicit core_deleter(Core const& core) : _core{&core} {} + + template + void operator()(Handle handle) const { T::destroy(dev::device_table(*_core), handle); } + + [[nodiscard]] Core const& core() const { return *_core; } + +private: + not_null _core; +}; + +/** + * @brief @ref unique_handle of a device object, the deleter stores the core + * @tparam T owner class with a static destroy(DeviceTable, Handle) + */ +template +using core_handle = unique_handle>; +} diff --git a/cvk/incl/cvk/utility/unique_handle.hpp b/cvk/incl/cvk/utility/unique_handle.hpp index e8e5f3a2..ad58ba2e 100644 --- a/cvk/incl/cvk/utility/unique_handle.hpp +++ b/cvk/incl/cvk/utility/unique_handle.hpp @@ -1,5 +1,4 @@ #pragma once -#include "cvk/base/device_table.hpp" #include "cvk/utility/types.hpp" #include @@ -10,53 +9,15 @@ #include -struct VmaAllocator_T; - namespace cvk { -class Core; class Instance; } namespace cvk::dev { -[[nodiscard]] DeviceTable device_table(Core const&); [[nodiscard]] VkInstance vk_instance(Instance const&); -[[nodiscard]] VmaAllocator_T* vma_allocator(Core const&); } namespace cvk { -/** - * @brief deleter of device objects, context: the core - * @details calls T::destroy(core.deviceTable(), handle) - * @pre the core outlives the deleter - */ -template -class core_deleter { -public: - explicit core_deleter(Core const& core) : _core{&core} {} - - template - void operator()(Handle handle) const { T::destroy(dev::device_table(*_core), handle); } - - [[nodiscard]] Core const& core() const { return *_core; } - -private: - not_null _core; -}; - -/** - * @brief deleter of memory objects, context: the core - * @details calls T::destroy(core.allocator().get(), handle) - * @pre the core outlives the deleter - */ -template -class vma_deleter : public core_deleter { -public: - using core_deleter::core_deleter; - - template - void operator()(Handle handle) const { T::destroy(dev::vma_allocator(this->core()), handle); } -}; - /** * @brief deleter of instance objects, context: the VkInstance * @details calls T::destroy(vkInstance, handle), @@ -169,18 +130,4 @@ class unique_handle { unique_handle& operator=(unique_handle const& other) = delete; unique_handle& operator=(unique_handle&& other) noexcept = default; }; - -/** - * @brief @ref unique_handle of a device object, the deleter stores the core - * @tparam T owner class with a static destroy(DeviceTable, Handle) - */ -template -using core_handle = unique_handle>; - -/** - * @brief @ref unique_handle of a memory object, the deleter stores the core and frees through its allocator - * @tparam T owner class with a static destroy(VmaAllocator, Handle) - */ -template -using vma_handle = unique_handle>; } diff --git a/cvk/incl/cvk/utility/vma_handle.hpp b/cvk/incl/cvk/utility/vma_handle.hpp new file mode 100644 index 00000000..e3e6a492 --- /dev/null +++ b/cvk/incl/cvk/utility/vma_handle.hpp @@ -0,0 +1,34 @@ +#pragma once +#include "cvk/utility/core_handle.hpp" +#include "cvk/utility/unique_handle.hpp" + +#include +#include + + +namespace cvk::dev { +[[nodiscard]] VmaAllocator vma_allocator(Core const&); +} + +namespace cvk { +/** + * @brief deleter of memory objects, context: the core + * @details calls T::destroy(core.allocator().get(), handle) + * @pre the core outlives the deleter + */ +template +class vma_deleter : public core_deleter { +public: + using core_deleter::core_deleter; + + template + void operator()(Handle handle) const { T::destroy(dev::vma_allocator(this->core()), handle); } +}; + +/** + * @brief @ref unique_handle of a memory object, the deleter stores the core and frees through its allocator + * @tparam T owner class with a static destroy(VmaAllocator, Handle) + */ +template +using vma_handle = unique_handle>; +} diff --git a/cvk/src/utility/core_handle.cpp b/cvk/src/utility/core_handle.cpp new file mode 100644 index 00000000..7d576c87 --- /dev/null +++ b/cvk/src/utility/core_handle.cpp @@ -0,0 +1,8 @@ +#include "cvk/utility/core_handle.hpp" + +#include "cvk/base/core.hpp" + + +namespace cvk::dev { +DeviceTable device_table(Core const& core) { return core.deviceTable(); } +} diff --git a/cvk/src/utility/unique_handle.cpp b/cvk/src/utility/unique_handle.cpp index eb15907a..44321ad2 100644 --- a/cvk/src/utility/unique_handle.cpp +++ b/cvk/src/utility/unique_handle.cpp @@ -1,14 +1,8 @@ #include "cvk/utility/unique_handle.hpp" -#include "cvk/base/allocator.hpp" -#include "cvk/base/core.hpp" #include "cvk/base/instance.hpp" namespace cvk::dev { -DeviceTable device_table(Core const& core) { return core.deviceTable(); } - VkInstance vk_instance(Instance const& instance) { return instance.get(); } - -VmaAllocator vma_allocator(Core const& core) { return core.allocator().get(); } } diff --git a/cvk/src/utility/vma_handle.cpp b/cvk/src/utility/vma_handle.cpp new file mode 100644 index 00000000..8f9c17d8 --- /dev/null +++ b/cvk/src/utility/vma_handle.cpp @@ -0,0 +1,9 @@ +#include "cvk/utility/vma_handle.hpp" + +#include "cvk/base/allocator.hpp" +#include "cvk/base/core.hpp" + + +namespace cvk::dev { +VmaAllocator vma_allocator(Core const& core) { return core.allocator().get(); } +} diff --git a/cvk/tests/src/utility/unique_handle_test.cpp b/cvk/tests/src/utility/unique_handle_test.cpp index 0354f77c..955d78ab 100644 --- a/cvk/tests/src/utility/unique_handle_test.cpp +++ b/cvk/tests/src/utility/unique_handle_test.cpp @@ -8,7 +8,9 @@ #include "cvk/render/sync/fence.hpp" #include "cvk/surface/surface.hpp" #include "cvk/utility/constants.hpp" +#include "cvk/utility/core_handle.hpp" #include "cvk/utility/unique_handle.hpp" +#include "cvk/utility/vma_handle.hpp" #include #include From c88d50c4e3f0adfe4f93bf162f5fa7c3a822ea64 Mon Sep 17 00:00:00 2001 From: lukas <89145039+codethinki@users.noreply.github.com> Date: Fri, 16 Oct 2026 23:50:51 +0200 Subject: [PATCH 034/135] cvk: instance_handle in its own header, unique_handle header only instance_deleter and the instance_handle alias go to instance_handle.hpp. unique_handle.hpp keeps unique_handle and handle_deleter and has no source file. --- AGENTS.md | 1 + cvk/incl/cvk/debug/debug_messenger.hpp | 4 +- cvk/incl/cvk/surface/surface.hpp | 4 +- cvk/incl/cvk/utility/instance_handle.hpp | 43 +++++++++++++++++++ cvk/incl/cvk/utility/unique_handle.hpp | 28 ------------ ...{unique_handle.cpp => instance_handle.cpp} | 2 +- cvk/tests/src/utility/unique_handle_test.cpp | 7 +-- 7 files changed, 53 insertions(+), 36 deletions(-) create mode 100644 cvk/incl/cvk/utility/instance_handle.hpp rename cvk/src/utility/{unique_handle.cpp => instance_handle.cpp} (75%) diff --git a/AGENTS.md b/AGENTS.md index 97096fb5..d267a914 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -163,6 +163,7 @@ public: Deleters: `core_deleter` (the core), `vma_deleter` (the core, destroys through its allocator), `instance_deleter` (the `VkInstance`), `handle_deleter` (no context) or a deleter derived from them. Device objects use `core_handle` (`cvk/utility/core_handle.hpp`), short for `unique_handle>`. Objects with VMA memory use `vma_handle` (`cvk/utility/vma_handle.hpp`), short for `unique_handle>`. + Instance objects use `instance_handle` (`cvk/utility/instance_handle.hpp`), short for `unique_handle>`. - Handle and context go in together: `_handle.reset(ptr, core())`. A missing context argument does not compile. - Move constructor and move assignment are `= default`. The move assignment destroys the old handle with the old context. - A class that owns more than one object declares the objects that depend on its handle first. `= default` assigns in declaration order: `Instance` declares `_debugMessenger` before `_handle`. diff --git a/cvk/incl/cvk/debug/debug_messenger.hpp b/cvk/incl/cvk/debug/debug_messenger.hpp index 0285d20e..df9c8b7b 100644 --- a/cvk/incl/cvk/debug/debug_messenger.hpp +++ b/cvk/incl/cvk/debug/debug_messenger.hpp @@ -1,7 +1,7 @@ #pragma once #include "cvk/utility/constants.hpp" +#include "cvk/utility/instance_handle.hpp" #include "cvk/utility/types.hpp" -#include "cvk/utility/unique_handle.hpp" #include @@ -101,7 +101,7 @@ class DebugMessenger { private: void forget(); - unique_handle> _handle; + instance_handle _handle; public: DebugMessenger(DebugMessenger const& other) = delete; diff --git a/cvk/incl/cvk/surface/surface.hpp b/cvk/incl/cvk/surface/surface.hpp index 4961d391..0babc4fe 100644 --- a/cvk/incl/cvk/surface/surface.hpp +++ b/cvk/incl/cvk/surface/surface.hpp @@ -1,8 +1,8 @@ #pragma once #include "surface_config.hpp" +#include "cvk/utility/instance_handle.hpp" #include "cvk/utility/types.hpp" -#include "cvk/utility/unique_handle.hpp" #include @@ -115,7 +115,7 @@ class Surface { void forget(); Config _config; - unique_handle> _handle; + instance_handle _handle; public: Surface(Surface const& other) = delete; diff --git a/cvk/incl/cvk/utility/instance_handle.hpp b/cvk/incl/cvk/utility/instance_handle.hpp new file mode 100644 index 00000000..3abbb7ad --- /dev/null +++ b/cvk/incl/cvk/utility/instance_handle.hpp @@ -0,0 +1,43 @@ +#pragma once +#include "cvk/utility/types.hpp" +#include "cvk/utility/unique_handle.hpp" + +#include + + +namespace cvk { +class Instance; +} + +namespace cvk::dev { +[[nodiscard]] VkInstance vk_instance(Instance const&); +} + +namespace cvk { +/** + * @brief deleter of instance objects, context: the VkInstance + * @details calls T::destroy(vkInstance, handle), + * stores the handle, not the Instance: the instance owns its messenger and can move + */ +template +class instance_deleter { +public: + explicit instance_deleter(VkInstance vkInstance) : _vkInstance{vkInstance} {} + explicit instance_deleter(Instance const& instance) : instance_deleter{dev::vk_instance(instance)} {} + + template + void operator()(Handle handle) const { T::destroy(_vkInstance.get(), handle); } + + [[nodiscard]] VkInstance vkInstance() const { return _vkInstance.get(); } + +private: + vk_not_null _vkInstance; +}; + +/** + * @brief @ref unique_handle of an instance object, the deleter stores the VkInstance + * @tparam T owner class with a static destroy(VkInstance, Handle) + */ +template +using instance_handle = unique_handle>; +} diff --git a/cvk/incl/cvk/utility/unique_handle.hpp b/cvk/incl/cvk/utility/unique_handle.hpp index ad58ba2e..3eaf3108 100644 --- a/cvk/incl/cvk/utility/unique_handle.hpp +++ b/cvk/incl/cvk/utility/unique_handle.hpp @@ -10,34 +10,6 @@ namespace cvk { -class Instance; -} - -namespace cvk::dev { -[[nodiscard]] VkInstance vk_instance(Instance const&); -} - -namespace cvk { -/** - * @brief deleter of instance objects, context: the VkInstance - * @details calls T::destroy(vkInstance, handle), - * stores the handle, not the Instance: the instance owns its messenger and can move - */ -template -class instance_deleter { -public: - explicit instance_deleter(VkInstance vkInstance) : _vkInstance{vkInstance} {} - explicit instance_deleter(Instance const& instance) : instance_deleter{dev::vk_instance(instance)} {} - - template - void operator()(Handle handle) const { T::destroy(_vkInstance.get(), handle); } - - [[nodiscard]] VkInstance vkInstance() const { return _vkInstance.get(); } - -private: - vk_not_null _vkInstance; -}; - /** * @brief deleter without context, calls T::destroy(handle) */ diff --git a/cvk/src/utility/unique_handle.cpp b/cvk/src/utility/instance_handle.cpp similarity index 75% rename from cvk/src/utility/unique_handle.cpp rename to cvk/src/utility/instance_handle.cpp index 44321ad2..5ea8e15c 100644 --- a/cvk/src/utility/unique_handle.cpp +++ b/cvk/src/utility/instance_handle.cpp @@ -1,4 +1,4 @@ -#include "cvk/utility/unique_handle.hpp" +#include "cvk/utility/instance_handle.hpp" #include "cvk/base/instance.hpp" diff --git a/cvk/tests/src/utility/unique_handle_test.cpp b/cvk/tests/src/utility/unique_handle_test.cpp index 955d78ab..27a19101 100644 --- a/cvk/tests/src/utility/unique_handle_test.cpp +++ b/cvk/tests/src/utility/unique_handle_test.cpp @@ -9,6 +9,7 @@ #include "cvk/surface/surface.hpp" #include "cvk/utility/constants.hpp" #include "cvk/utility/core_handle.hpp" +#include "cvk/utility/instance_handle.hpp" #include "cvk/utility/unique_handle.hpp" #include "cvk/utility/vma_handle.hpp" @@ -98,7 +99,7 @@ CVK_TEST(unique_handle, a_handle_needs_its_context) { static_assert(!resettable); static_assert(resettable); - using surface_handle = unique_handle>; + using surface_handle = instance_handle; static_assert(!resettable); static_assert(resettable); @@ -123,7 +124,7 @@ CVK_TEST(unique_handle, size) { static_assert(sizeof(core_handle) == sizeof(Core const*) + sizeof(VkFence)); static_assert(sizeof(vma_handle) == sizeof(Core const*) + sizeof(VmaAllocation)); static_assert( - sizeof(unique_handle>) == sizeof(VkInstance) + sizeof(VkSurfaceKHR) + sizeof(instance_handle) == sizeof(VkInstance) + sizeof(VkSurfaceKHR) ); } @@ -177,7 +178,7 @@ CVK_TEST(unique_handle, instance_deleter) { Instance const instance{test::APP_NAME, test::headless_extensions(), test::counting_messenger()}; auto surface = test::headless_surface(instance); - unique_handle> handle{surface.release().vkSurface.get(), instance}; + instance_handle handle{surface.release().vkSurface.get(), instance}; EXPECT_EQ(handle.vkInstance(), instance.get()); } From 0d14a857d7f0d97159e60d3fc9d27d869eb4d830 Mon Sep 17 00:00:00 2001 From: lukas <89145039+codethinki@users.noreply.github.com> Date: Fri, 16 Oct 2026 23:52:59 +0200 Subject: [PATCH 035/135] cvk: deleters header only core_handle.hpp includes core.hpp, vma_handle.hpp includes core.hpp and allocator.hpp, the dev:: lookup functions and the handle sources are gone. instance_deleter takes the VkInstance only: instance.hpp includes debug_messenger.hpp, which includes instance_handle.hpp. --- cvk/incl/cvk/utility/core_handle.hpp | 12 ++---------- cvk/incl/cvk/utility/instance_handle.hpp | 9 --------- cvk/incl/cvk/utility/vma_handle.hpp | 8 +++----- cvk/src/debug/debug_messenger.cpp | 2 +- cvk/src/surface/surface.cpp | 2 +- cvk/src/utility/core_handle.cpp | 8 -------- cvk/src/utility/instance_handle.cpp | 8 -------- cvk/src/utility/vma_handle.cpp | 9 --------- cvk/tests/src/utility/unique_handle_test.cpp | 4 ++-- 9 files changed, 9 insertions(+), 53 deletions(-) delete mode 100644 cvk/src/utility/core_handle.cpp delete mode 100644 cvk/src/utility/instance_handle.cpp delete mode 100644 cvk/src/utility/vma_handle.cpp diff --git a/cvk/incl/cvk/utility/core_handle.hpp b/cvk/incl/cvk/utility/core_handle.hpp index 9a6b1fce..66b6b94d 100644 --- a/cvk/incl/cvk/utility/core_handle.hpp +++ b/cvk/incl/cvk/utility/core_handle.hpp @@ -1,17 +1,9 @@ #pragma once -#include "cvk/base/device_table.hpp" +#include "cvk/base/core.hpp" #include "cvk/utility/types.hpp" #include "cvk/utility/unique_handle.hpp" -namespace cvk { -class Core; -} - -namespace cvk::dev { -[[nodiscard]] DeviceTable device_table(Core const&); -} - namespace cvk { /** * @brief deleter of device objects, context: the core @@ -24,7 +16,7 @@ class core_deleter { explicit core_deleter(Core const& core) : _core{&core} {} template - void operator()(Handle handle) const { T::destroy(dev::device_table(*_core), handle); } + void operator()(Handle handle) const { T::destroy(_core->deviceTable(), handle); } [[nodiscard]] Core const& core() const { return *_core; } diff --git a/cvk/incl/cvk/utility/instance_handle.hpp b/cvk/incl/cvk/utility/instance_handle.hpp index 3abbb7ad..8a9624e7 100644 --- a/cvk/incl/cvk/utility/instance_handle.hpp +++ b/cvk/incl/cvk/utility/instance_handle.hpp @@ -5,14 +5,6 @@ #include -namespace cvk { -class Instance; -} - -namespace cvk::dev { -[[nodiscard]] VkInstance vk_instance(Instance const&); -} - namespace cvk { /** * @brief deleter of instance objects, context: the VkInstance @@ -23,7 +15,6 @@ template class instance_deleter { public: explicit instance_deleter(VkInstance vkInstance) : _vkInstance{vkInstance} {} - explicit instance_deleter(Instance const& instance) : instance_deleter{dev::vk_instance(instance)} {} template void operator()(Handle handle) const { T::destroy(_vkInstance.get(), handle); } diff --git a/cvk/incl/cvk/utility/vma_handle.hpp b/cvk/incl/cvk/utility/vma_handle.hpp index e3e6a492..b5549e36 100644 --- a/cvk/incl/cvk/utility/vma_handle.hpp +++ b/cvk/incl/cvk/utility/vma_handle.hpp @@ -1,4 +1,6 @@ #pragma once +#include "cvk/base/allocator.hpp" +#include "cvk/base/core.hpp" #include "cvk/utility/core_handle.hpp" #include "cvk/utility/unique_handle.hpp" @@ -6,10 +8,6 @@ #include -namespace cvk::dev { -[[nodiscard]] VmaAllocator vma_allocator(Core const&); -} - namespace cvk { /** * @brief deleter of memory objects, context: the core @@ -22,7 +20,7 @@ class vma_deleter : public core_deleter { using core_deleter::core_deleter; template - void operator()(Handle handle) const { T::destroy(dev::vma_allocator(this->core()), handle); } + void operator()(Handle handle) const { T::destroy(this->core().allocator().get(), handle); } }; /** diff --git a/cvk/src/debug/debug_messenger.cpp b/cvk/src/debug/debug_messenger.cpp index 2f5d5ccc..9b009125 100644 --- a/cvk/src/debug/debug_messenger.cpp +++ b/cvk/src/debug/debug_messenger.cpp @@ -10,7 +10,7 @@ namespace cvk { DebugMessenger::DebugMessenger(Instance const& instance, Config config) : _config{std::move(config)}, - _handle{instance} { Instance::debug_check(instance); } + _handle{instance.get()} { Instance::debug_check(instance); } DebugMessenger::DebugMessenger(Instance const& instance, Config config, create_t) : DebugMessenger{ instance, diff --git a/cvk/src/surface/surface.cpp b/cvk/src/surface/surface.cpp index ab2542cc..bcb34737 100644 --- a/cvk/src/surface/surface.cpp +++ b/cvk/src/surface/surface.cpp @@ -26,7 +26,7 @@ namespace { } } -Surface::Surface(Instance const& instance, Config config) : _config{std::move(config)}, _handle{instance} {} +Surface::Surface(Instance const& instance, Config config) : _config{std::move(config)}, _handle{instance.get()} {} Surface::Surface(Instance const& instance, Config config, State const& state) : Surface{instance, std::move(config)} { wrap(state); diff --git a/cvk/src/utility/core_handle.cpp b/cvk/src/utility/core_handle.cpp deleted file mode 100644 index 7d576c87..00000000 --- a/cvk/src/utility/core_handle.cpp +++ /dev/null @@ -1,8 +0,0 @@ -#include "cvk/utility/core_handle.hpp" - -#include "cvk/base/core.hpp" - - -namespace cvk::dev { -DeviceTable device_table(Core const& core) { return core.deviceTable(); } -} diff --git a/cvk/src/utility/instance_handle.cpp b/cvk/src/utility/instance_handle.cpp deleted file mode 100644 index 5ea8e15c..00000000 --- a/cvk/src/utility/instance_handle.cpp +++ /dev/null @@ -1,8 +0,0 @@ -#include "cvk/utility/instance_handle.hpp" - -#include "cvk/base/instance.hpp" - - -namespace cvk::dev { -VkInstance vk_instance(Instance const& instance) { return instance.get(); } -} diff --git a/cvk/src/utility/vma_handle.cpp b/cvk/src/utility/vma_handle.cpp deleted file mode 100644 index 8f9c17d8..00000000 --- a/cvk/src/utility/vma_handle.cpp +++ /dev/null @@ -1,9 +0,0 @@ -#include "cvk/utility/vma_handle.hpp" - -#include "cvk/base/allocator.hpp" -#include "cvk/base/core.hpp" - - -namespace cvk::dev { -VmaAllocator vma_allocator(Core const& core) { return core.allocator().get(); } -} diff --git a/cvk/tests/src/utility/unique_handle_test.cpp b/cvk/tests/src/utility/unique_handle_test.cpp index 27a19101..4302ae15 100644 --- a/cvk/tests/src/utility/unique_handle_test.cpp +++ b/cvk/tests/src/utility/unique_handle_test.cpp @@ -42,7 +42,7 @@ namespace { pool_deleter(Core const& core, VkCommandPool vkPool) : core_deleter{core}, pool{vkPool} {} void operator()(VkCommandBuffer vkBuffer) const { - PoolOwned::destroy(dev::device_table(core()), pool, vkBuffer); + PoolOwned::destroy(core().deviceTable(), pool, vkBuffer); } VkCommandPool pool; @@ -178,7 +178,7 @@ CVK_TEST(unique_handle, instance_deleter) { Instance const instance{test::APP_NAME, test::headless_extensions(), test::counting_messenger()}; auto surface = test::headless_surface(instance); - instance_handle handle{surface.release().vkSurface.get(), instance}; + instance_handle handle{surface.release().vkSurface.get(), instance.get()}; EXPECT_EQ(handle.vkInstance(), instance.get()); } From f9039ab1332d063222e715b015eb25865ca5e024 Mon Sep 17 00:00:00 2001 From: lukas <89145039+codethinki@users.noreply.github.com> Date: Sat, 17 Oct 2026 00:02:33 +0200 Subject: [PATCH 036/135] cvk: constexpr handles, deleters and config functions unique_handle and the deleters are constexpr, std::unique_ptr is constexpr since C++23. SamplerConfig::createInfo, DescriptorSetLayout::Config::add/remove, PipelineConfig::Default/removeStage are constexpr in their headers, CoreConfig::debug_check is constexpr again like in legacy. Tests run them in constant evaluation, which also rejects leaks and double frees. --- cvk/incl/cvk/base/core_config.hpp | 4 +- cvk/incl/cvk/data/img/sampler_config.hpp | 23 +++++++++++- .../pipeline/layout/descriptor_set_layout.hpp | 37 ++++++++++++++++++- .../cvk/render/pipeline/pipeline_config.hpp | 26 ++++++++++++- cvk/incl/cvk/utility/core_handle.hpp | 6 +-- cvk/incl/cvk/utility/instance_handle.hpp | 6 +-- cvk/incl/cvk/utility/unique_handle.hpp | 28 +++++++------- cvk/incl/cvk/utility/vma_handle.hpp | 2 +- cvk/src/data/img/sampler_config.cpp | 27 -------------- .../pipeline/layout/descriptor_set_layout.cpp | 32 ---------------- cvk/src/render/pipeline/pipeline_config.cpp | 18 --------- cvk/tests/src/data/img/sampler_test.cpp | 5 +++ .../layout/descriptor_set_layout_test.cpp | 10 ++++- .../src/render/pipeline/pipeline_test.cpp | 19 ++++++++++ cvk/tests/src/utility/unique_handle_test.cpp | 32 ++++++++++++++++ 15 files changed, 169 insertions(+), 106 deletions(-) delete mode 100644 cvk/src/data/img/sampler_config.cpp diff --git a/cvk/incl/cvk/base/core_config.hpp b/cvk/incl/cvk/base/core_config.hpp index ecfc4089..6b19c927 100644 --- a/cvk/incl/cvk/base/core_config.hpp +++ b/cvk/incl/cvk/base/core_config.hpp @@ -38,12 +38,12 @@ struct CoreConfig { */ utils::PhysicalDeviceFeatures requiredFeatures{}; - static void debug_check(CoreConfig const& config); + static constexpr void debug_check(CoreConfig const& config); }; } namespace cvk { -inline void CoreConfig::debug_check(CoreConfig const& config) { +constexpr void CoreConfig::debug_check(CoreConfig const& config) { CTH_CRITICAL(config.queueProperties.empty(), "at least one queue is required") {} CTH_CRITICAL( std::ranges::any_of( diff --git a/cvk/incl/cvk/data/img/sampler_config.hpp b/cvk/incl/cvk/data/img/sampler_config.hpp index 39fd02a5..1a6f7f26 100644 --- a/cvk/incl/cvk/data/img/sampler_config.hpp +++ b/cvk/incl/cvk/data/img/sampler_config.hpp @@ -37,6 +37,27 @@ struct SamplerConfig { VkBorderColor borderColor = VK_BORDER_COLOR_INT_OPAQUE_BLACK; VkBool32 unnormalizedCoordinates = VK_FALSE; - [[nodiscard]] VkSamplerCreateInfo createInfo() const; + [[nodiscard]] constexpr VkSamplerCreateInfo createInfo() const { + return VkSamplerCreateInfo{ + .sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO, + .pNext = nullptr, + .flags = 0, + .magFilter = magFilter, + .minFilter = minFilter, + .mipmapMode = mipmapMode, + .addressModeU = addressModeU, + .addressModeV = addressModeV, + .addressModeW = addressModeW, + .mipLodBias = mipLodBias, + .anisotropyEnable = maxAnisotropy != 0.f ? VK_TRUE : VK_FALSE, + .maxAnisotropy = maxAnisotropy, + .compareEnable = compareOp.has_value() ? VK_TRUE : VK_FALSE, + .compareOp = compareOp.value_or(VK_COMPARE_OP_NEVER), + .minLod = minLod, + .maxLod = maxLod, + .borderColor = borderColor, + .unnormalizedCoordinates = unnormalizedCoordinates, + }; + } }; } diff --git a/cvk/incl/cvk/render/pipeline/layout/descriptor_set_layout.hpp b/cvk/incl/cvk/render/pipeline/layout/descriptor_set_layout.hpp index 3f14a2d5..88d47e73 100644 --- a/cvk/incl/cvk/render/pipeline/layout/descriptor_set_layout.hpp +++ b/cvk/incl/cvk/render/pipeline/layout/descriptor_set_layout.hpp @@ -8,6 +8,7 @@ #include +#include #include #include #include @@ -124,13 +125,13 @@ struct DescriptorSetLayout::Config { * @param count of descriptors, 0 -> the binding is reserved without descriptors * @pre no binding with the same number */ - Config& add(uint32_t binding, VkDescriptorType type, VkShaderStageFlags stages, uint32_t count = 1); + constexpr Config& add(uint32_t binding, VkDescriptorType type, VkShaderStageFlags stages, uint32_t count = 1); /** * @brief removes a binding * @pre a binding with the number exists */ - Config& remove(uint32_t binding); + constexpr Config& remove(uint32_t binding); /** * @brief bindings sorted by binding number @@ -143,6 +144,38 @@ struct DescriptorSetLayout::State { }; } +namespace cvk { +constexpr DescriptorSetLayout::Config& DescriptorSetLayout::Config::add( + uint32_t binding, + VkDescriptorType type, + VkShaderStageFlags stages, + uint32_t count +) { + auto const it = std::ranges::lower_bound(bindings, binding, {}, &VkDescriptorSetLayoutBinding::binding); + CTH_CRITICAL(it != bindings.end() && it->binding == binding, "binding {} already exists", binding) {} + + bindings.insert( + it, + VkDescriptorSetLayoutBinding{ + .binding = binding, + .descriptorType = type, + .descriptorCount = count, + .stageFlags = stages, + .pImmutableSamplers = nullptr, + } + ); + return *this; +} + +constexpr DescriptorSetLayout::Config& DescriptorSetLayout::Config::remove(uint32_t binding) { + auto const it = std::ranges::lower_bound(bindings, binding, {}, &VkDescriptorSetLayoutBinding::binding); + CTH_CRITICAL(it == bindings.end() || it->binding != binding, "binding {} does not exist", binding) {} + + bindings.erase(it); + return *this; +} +} + namespace cvk { inline void DescriptorSetLayout::debug_check(DescriptorSetLayout const& layout) { CTH_CRITICAL(!layout.created(), "descriptor set layout must be created") {} diff --git a/cvk/incl/cvk/render/pipeline/pipeline_config.hpp b/cvk/incl/cvk/render/pipeline/pipeline_config.hpp index e5d40f59..063f1faf 100644 --- a/cvk/incl/cvk/render/pipeline/pipeline_config.hpp +++ b/cvk/incl/cvk/render/pipeline/pipeline_config.hpp @@ -1,6 +1,8 @@ #pragma once #include +#include + #include #include @@ -32,7 +34,7 @@ struct PipelineConfig { * @brief config with one @ref ALPHA_BLENDING attachment per color format * @param depthFormat VK_FORMAT_UNDEFINED -> no depth attachment */ - [[nodiscard]] static PipelineConfig Default( + [[nodiscard]] static constexpr PipelineConfig Default( std::span colorFormats, VkFormat depthFormat = VK_FORMAT_UNDEFINED ); @@ -49,7 +51,7 @@ struct PipelineConfig { /** * @pre a stage of the type exists */ - PipelineConfig& removeStage(VkShaderStageFlagBits stage); + constexpr PipelineConfig& removeStage(VkShaderStageFlagBits stage); /** * @brief VK_PIPELINE_CREATE_ALLOW_DERIVATIVES_BIT -> the pipeline can be a parent @@ -146,3 +148,23 @@ struct PipelineConfig { uint32_t viewMask = 0; }; } + +namespace cvk { +constexpr PipelineConfig PipelineConfig::Default(std::span colorFormats, VkFormat depthFormat) { + PipelineConfig config{}; + config.colorFormats.assign_range(colorFormats); + config.colorBlendAttachments.assign(colorFormats.size(), ALPHA_BLENDING); + config.depthFormat = depthFormat; + return config; +} + +constexpr PipelineConfig& PipelineConfig::removeStage(VkShaderStageFlagBits stage) { + auto const removed = std::erase_if( + stages, + [stage](VkPipelineShaderStageCreateInfo const& info) { return info.stage == stage; } + ); + CTH_CRITICAL(removed == 0, "stage not added") {} + + return *this; +} +} diff --git a/cvk/incl/cvk/utility/core_handle.hpp b/cvk/incl/cvk/utility/core_handle.hpp index 66b6b94d..95561b9b 100644 --- a/cvk/incl/cvk/utility/core_handle.hpp +++ b/cvk/incl/cvk/utility/core_handle.hpp @@ -13,12 +13,12 @@ namespace cvk { template class core_deleter { public: - explicit core_deleter(Core const& core) : _core{&core} {} + constexpr explicit core_deleter(Core const& core) : _core{&core} {} template - void operator()(Handle handle) const { T::destroy(_core->deviceTable(), handle); } + constexpr void operator()(Handle handle) const { T::destroy(_core->deviceTable(), handle); } - [[nodiscard]] Core const& core() const { return *_core; } + [[nodiscard]] constexpr Core const& core() const { return *_core; } private: not_null _core; diff --git a/cvk/incl/cvk/utility/instance_handle.hpp b/cvk/incl/cvk/utility/instance_handle.hpp index 8a9624e7..03a81c18 100644 --- a/cvk/incl/cvk/utility/instance_handle.hpp +++ b/cvk/incl/cvk/utility/instance_handle.hpp @@ -14,12 +14,12 @@ namespace cvk { template class instance_deleter { public: - explicit instance_deleter(VkInstance vkInstance) : _vkInstance{vkInstance} {} + constexpr explicit instance_deleter(VkInstance vkInstance) : _vkInstance{vkInstance} {} template - void operator()(Handle handle) const { T::destroy(_vkInstance.get(), handle); } + constexpr void operator()(Handle handle) const { T::destroy(_vkInstance.get(), handle); } - [[nodiscard]] VkInstance vkInstance() const { return _vkInstance.get(); } + [[nodiscard]] constexpr VkInstance vkInstance() const { return _vkInstance.get(); } private: vk_not_null _vkInstance; diff --git a/cvk/incl/cvk/utility/unique_handle.hpp b/cvk/incl/cvk/utility/unique_handle.hpp index 3eaf3108..a1b2f772 100644 --- a/cvk/incl/cvk/utility/unique_handle.hpp +++ b/cvk/incl/cvk/utility/unique_handle.hpp @@ -16,7 +16,7 @@ namespace cvk { template struct handle_deleter { template - void operator()(Handle handle) const { T::destroy(handle); } + constexpr void operator()(Handle handle) const { T::destroy(handle); } }; /** @@ -41,29 +41,29 @@ class unique_handle { * @param context to construct the deleter */ template requires std::constructible_from - explicit unique_handle(Args&&... context) : _ptr{Handle{}, Deleter{std::forward(context)...}} {} + constexpr explicit unique_handle(Args&&... context) : _ptr{Handle{}, Deleter{std::forward(context)...}} {} /** * @brief owns @ref handle * @param context to construct the deleter */ template requires std::constructible_from - explicit unique_handle(Handle handle, Args&&... context) : + constexpr explicit unique_handle(Handle handle, Args&&... context) : _ptr{handle, Deleter{std::forward(context)...}} {} - ~unique_handle() = default; + constexpr ~unique_handle() = default; /** * @brief destroys the owned handle, keeps the context */ - void reset() noexcept { _ptr.reset(); } + constexpr void reset() noexcept { _ptr.reset(); } /** * @brief destroys the owned handle with the old context, then owns @ref handle with the new context * @param context to construct the deleter */ template requires std::constructible_from - void reset(Handle handle, Args&&... context) noexcept { + constexpr void reset(Handle handle, Args&&... context) noexcept { _ptr.reset(); _ptr.get_deleter() = Deleter{std::forward(context)...}; _ptr.reset(handle); @@ -72,24 +72,24 @@ class unique_handle { /** * @brief gives up the handle without destroying, keeps the context */ - [[nodiscard]] Handle release() noexcept { return _ptr.release(); } + [[nodiscard]] constexpr Handle release() noexcept { return _ptr.release(); } - [[nodiscard]] Handle get() const noexcept { return _ptr.get(); } - [[nodiscard]] explicit operator bool() const noexcept { return static_cast(_ptr); } + [[nodiscard]] constexpr Handle get() const noexcept { return _ptr.get(); } + [[nodiscard]] constexpr explicit operator bool() const noexcept { return static_cast(_ptr); } - [[nodiscard]] Deleter const& deleter() const noexcept { return _ptr.get_deleter(); } + [[nodiscard]] constexpr Deleter const& deleter() const noexcept { return _ptr.get_deleter(); } /** * @pre the deleter has core() */ - [[nodiscard]] decltype(auto) core() const requires requires(Deleter const& d) { d.core(); } { + [[nodiscard]] constexpr decltype(auto) core() const requires requires(Deleter const& d) { d.core(); } { return deleter().core(); } /** * @pre the deleter has vkInstance() */ - [[nodiscard]] VkInstance vkInstance() const requires requires(Deleter const& d) { d.vkInstance(); } { + [[nodiscard]] constexpr VkInstance vkInstance() const requires requires(Deleter const& d) { d.vkInstance(); } { return deleter().vkInstance(); } @@ -98,8 +98,8 @@ class unique_handle { public: unique_handle(unique_handle const& other) = delete; - unique_handle(unique_handle&& other) noexcept = default; + constexpr unique_handle(unique_handle&& other) noexcept = default; unique_handle& operator=(unique_handle const& other) = delete; - unique_handle& operator=(unique_handle&& other) noexcept = default; + constexpr unique_handle& operator=(unique_handle&& other) noexcept = default; }; } diff --git a/cvk/incl/cvk/utility/vma_handle.hpp b/cvk/incl/cvk/utility/vma_handle.hpp index b5549e36..83d96350 100644 --- a/cvk/incl/cvk/utility/vma_handle.hpp +++ b/cvk/incl/cvk/utility/vma_handle.hpp @@ -20,7 +20,7 @@ class vma_deleter : public core_deleter { using core_deleter::core_deleter; template - void operator()(Handle handle) const { T::destroy(this->core().allocator().get(), handle); } + constexpr void operator()(Handle handle) const { T::destroy(this->core().allocator().get(), handle); } }; /** diff --git a/cvk/src/data/img/sampler_config.cpp b/cvk/src/data/img/sampler_config.cpp deleted file mode 100644 index c474b58e..00000000 --- a/cvk/src/data/img/sampler_config.cpp +++ /dev/null @@ -1,27 +0,0 @@ -#include "cvk/data/img/sampler_config.hpp" - - -namespace cvk { -VkSamplerCreateInfo SamplerConfig::createInfo() const { - return VkSamplerCreateInfo{ - .sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO, - .pNext = nullptr, - .flags = 0, - .magFilter = magFilter, - .minFilter = minFilter, - .mipmapMode = mipmapMode, - .addressModeU = addressModeU, - .addressModeV = addressModeV, - .addressModeW = addressModeW, - .mipLodBias = mipLodBias, - .anisotropyEnable = maxAnisotropy != 0.f ? VK_TRUE : VK_FALSE, - .maxAnisotropy = maxAnisotropy, - .compareEnable = compareOp.has_value() ? VK_TRUE : VK_FALSE, - .compareOp = compareOp.value_or(VK_COMPARE_OP_NEVER), - .minLod = minLod, - .maxLod = maxLod, - .borderColor = borderColor, - .unnormalizedCoordinates = unnormalizedCoordinates, - }; -} -} diff --git a/cvk/src/render/pipeline/layout/descriptor_set_layout.cpp b/cvk/src/render/pipeline/layout/descriptor_set_layout.cpp index 55da2b83..f21b139f 100644 --- a/cvk/src/render/pipeline/layout/descriptor_set_layout.cpp +++ b/cvk/src/render/pipeline/layout/descriptor_set_layout.cpp @@ -79,35 +79,3 @@ VkDescriptorSetLayoutBinding const* DescriptorSetLayout::binding(uint32_t bindin void DescriptorSetLayout::forget() {} } - -namespace cvk { -DescriptorSetLayout::Config& DescriptorSetLayout::Config::add( - uint32_t binding, - VkDescriptorType type, - VkShaderStageFlags stages, - uint32_t count -) { - auto const it = std::ranges::lower_bound(bindings, binding, {}, &VkDescriptorSetLayoutBinding::binding); - CTH_CRITICAL(it != bindings.end() && it->binding == binding, "binding {} already exists", binding) {} - - bindings.insert( - it, - VkDescriptorSetLayoutBinding{ - .binding = binding, - .descriptorType = type, - .descriptorCount = count, - .stageFlags = stages, - .pImmutableSamplers = nullptr, - } - ); - return *this; -} - -DescriptorSetLayout::Config& DescriptorSetLayout::Config::remove(uint32_t binding) { - auto const it = std::ranges::lower_bound(bindings, binding, {}, &VkDescriptorSetLayoutBinding::binding); - CTH_CRITICAL(it == bindings.end() || it->binding != binding, "binding {} does not exist", binding) {} - - bindings.erase(it); - return *this; -} -} diff --git a/cvk/src/render/pipeline/pipeline_config.cpp b/cvk/src/render/pipeline/pipeline_config.cpp index bd78c51d..cde0b705 100644 --- a/cvk/src/render/pipeline/pipeline_config.cpp +++ b/cvk/src/render/pipeline/pipeline_config.cpp @@ -6,14 +6,6 @@ namespace cvk { -PipelineConfig PipelineConfig::Default(std::span colorFormats, VkFormat depthFormat) { - PipelineConfig config{}; - config.colorFormats.assign_range(colorFormats); - config.colorBlendAttachments.assign(colorFormats.size(), ALPHA_BLENDING); - config.depthFormat = depthFormat; - return config; -} - PipelineConfig& PipelineConfig::addStage(Shader const& shader, VkSpecializationInfo const* specialization) { CTH_CRITICAL( std::ranges::contains(stages, shader.stage(), &VkPipelineShaderStageCreateInfo::stage), @@ -23,14 +15,4 @@ PipelineConfig& PipelineConfig::addStage(Shader const& shader, VkSpecializationI stages.push_back(shader.stageInfo(specialization)); return *this; } - -PipelineConfig& PipelineConfig::removeStage(VkShaderStageFlagBits stage) { - auto const removed = std::erase_if( - stages, - [stage](VkPipelineShaderStageCreateInfo const& info) { return info.stage == stage; } - ); - CTH_CRITICAL(removed == 0, "stage not added") {} - - return *this; -} } diff --git a/cvk/tests/src/data/img/sampler_test.cpp b/cvk/tests/src/data/img/sampler_test.cpp index 14747935..654d78bd 100644 --- a/cvk/tests/src/data/img/sampler_test.cpp +++ b/cvk/tests/src/data/img/sampler_test.cpp @@ -21,6 +21,11 @@ CVK_TEST(Sampler, config_create_info) { EXPECT_EQ(info.compareOp, VK_COMPARE_OP_NEVER); } +CVK_TEST(Sampler, config_create_info_is_constexpr) { + static_assert(Sampler::Config{}.createInfo().anisotropyEnable == VK_TRUE); + static_assert(Sampler::Config{.compareOp = VK_COMPARE_OP_LESS}.createInfo().compareOp == VK_COMPARE_OP_LESS); +} + CVK_TEST(Sampler, base_constructor_clamps_anisotropy) { test::Context const context{}; auto const limit = context.core.physicalDevice().limits().maxSamplerAnisotropy; diff --git a/cvk/tests/src/render/pipeline/layout/descriptor_set_layout_test.cpp b/cvk/tests/src/render/pipeline/layout/descriptor_set_layout_test.cpp index 39e48d1a..f5f4b129 100644 --- a/cvk/tests/src/render/pipeline/layout/descriptor_set_layout_test.cpp +++ b/cvk/tests/src/render/pipeline/layout/descriptor_set_layout_test.cpp @@ -11,7 +11,7 @@ namespace { /** * @brief bindings 0 and 2, gap at 1 */ - DescriptorSetLayout::Config gap_config() { + constexpr DescriptorSetLayout::Config gap_config() { DescriptorSetLayout::Config config{}; config.add(2, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, VK_SHADER_STAGE_FRAGMENT_BIT) .add(0, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, VK_SHADER_STAGE_VERTEX_BIT, 2); @@ -37,6 +37,14 @@ CVK_TEST(DescriptorSetLayout, config_remove) { EXPECT_EQ(config.bindings[0].binding, 2); } +CVK_TEST(DescriptorSetLayout, config_is_constexpr) { + static_assert([] { + auto config = gap_config(); + config.remove(0); + return config.bindings.size() == 1 && config.bindings[0].binding == 2; + }()); +} + CVK_TEST(DescriptorSetLayout, create_with_gaps) { test::Context const context{}; diff --git a/cvk/tests/src/render/pipeline/pipeline_test.cpp b/cvk/tests/src/render/pipeline/pipeline_test.cpp index 49449bb6..1f1718e9 100644 --- a/cvk/tests/src/render/pipeline/pipeline_test.cpp +++ b/cvk/tests/src/render/pipeline/pipeline_test.cpp @@ -66,6 +66,25 @@ CVK_TEST(Pipeline, config_default) { EXPECT_EQ(config.depthFormat, DEPTH_FORMAT); } +CVK_TEST(Pipeline, config_is_constexpr) { + static_assert([] { + auto config = Pipeline::Config::Default(COLOR_FORMATS, DEPTH_FORMAT); + config.stages.push_back( + VkPipelineShaderStageCreateInfo{ + .sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO, + .pNext = nullptr, + .flags = 0, + .stage = VK_SHADER_STAGE_VERTEX_BIT, + .module = VK_NULL_HANDLE, + .pName = "main", + .pSpecializationInfo = nullptr, + } + ); + config.removeStage(VK_SHADER_STAGE_VERTEX_BIT); + return config.colorBlendAttachments.size() == 1 && config.stages.empty(); + }()); +} + CVK_TEST(Pipeline, config_stages) { test::Context const context{}; Triangle const triangle{context.core}; diff --git a/cvk/tests/src/utility/unique_handle_test.cpp b/cvk/tests/src/utility/unique_handle_test.cpp index 4302ae15..b168e2c9 100644 --- a/cvk/tests/src/utility/unique_handle_test.cpp +++ b/cvk/tests/src/utility/unique_handle_test.cpp @@ -80,6 +80,33 @@ namespace { using pointer = pair_handle; }; + /** + * @brief owner of heap memory, constant evaluation rejects leaks and double frees + */ + struct HeapOwned { + static constexpr void destroy(int* ptr) { delete ptr; } + }; + + using heap_handle = unique_handle>; + + consteval int move_assigned_value() { + heap_handle handle{}; + handle.reset(new int{1}); + heap_handle other{new int{2}}; + + handle = std::move(other); + return *handle.get(); + } + + consteval int released_value() { + heap_handle handle{new int{3}}; + int* const ptr = handle.release(); + + int const value = *ptr; + delete ptr; + return value; + } + /** * @brief reset(handle, context...) compiles */ @@ -119,6 +146,11 @@ CVK_TEST(unique_handle, a_handle_needs_its_context) { static_assert(!deleter_assignable); } +CVK_TEST(unique_handle, constexpr) { + static_assert(move_assigned_value() == 2); + static_assert(released_value() == 3); +} + CVK_TEST(unique_handle, size) { static_assert(sizeof(unique_handle>) == sizeof(VkInstance)); static_assert(sizeof(core_handle) == sizeof(Core const*) + sizeof(VkFence)); From 782cbc6779649e280f123b74debbb8a67e87fe5a Mon Sep 17 00:00:00 2001 From: lukas <89145039+codethinki@users.noreply.github.com> Date: Sat, 17 Oct 2026 00:10:15 +0200 Subject: [PATCH 037/135] cvk: constexpr accessors and debug checks Every accessor and debug_check that can run at compile time is constexpr. AGENTS.md: mark everything constexpr that can run at compile time, not the functions that call Vulkan, VMA or the OS and not the lifecycle of handle classes. --- AGENTS.md | 2 ++ cvk/incl/cvk/base/allocator.hpp | 10 +++---- cvk/incl/cvk/base/core.hpp | 12 ++++---- cvk/incl/cvk/base/device.hpp | 24 +++++++-------- cvk/incl/cvk/base/device_table.hpp | 2 +- cvk/incl/cvk/base/instance.hpp | 18 +++++------ cvk/incl/cvk/base/physical_device.hpp | 30 +++++++++---------- cvk/incl/cvk/base/queue/queue.hpp | 24 +++++++-------- cvk/incl/cvk/base/queue/queue_state.hpp | 2 +- cvk/incl/cvk/data/img/sampler.hpp | 12 ++++---- cvk/incl/cvk/data/memory/memory.hpp | 12 ++++---- cvk/incl/cvk/debug/debug_messenger.hpp | 10 +++---- .../pipeline/layout/descriptor_set_layout.hpp | 10 +++---- .../pipeline/layout/pipeline_layout.hpp | 10 +++---- cvk/incl/cvk/render/pipeline/pipeline.hpp | 16 +++++----- .../cvk/render/pipeline/shader/shader.hpp | 12 ++++---- cvk/incl/cvk/render/sync/fence.hpp | 12 ++++---- cvk/incl/cvk/render/sync/semaphore.hpp | 10 +++---- .../cvk/render/sync/timeline_semaphore.hpp | 8 ++--- cvk/incl/cvk/surface/surface.hpp | 14 ++++----- .../device/physical_device_features.hpp | 4 +-- 21 files changed, 128 insertions(+), 126 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index d267a914..8c1fd8b8 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -86,6 +86,8 @@ ctest --test-dir out/build/gcc_release --output-on-failure ### Language - Use `constexpr`, `consteval` and `decltype(auto)`. The macros `cxpr`, `cval` and `declauto` do not exist anymore. +- Mark every function `constexpr` that can run at compile time, also when no code uses it there yet: accessors, `debug_check` functions, configs, value types. +- Functions that call Vulkan, VMA or the OS are not `constexpr`. Neither are the constructors, the destructor and the lifecycle functions of a class that owns a handle: the object cannot exist at compile time. - Put `[[nodiscard]]` on every function that returns a value the caller must use. - Put `explicit` on constructors with one parameter and on tag constructors. - Use `auto const` for locals that do not change. diff --git a/cvk/incl/cvk/base/allocator.hpp b/cvk/incl/cvk/base/allocator.hpp index 0a69a66a..1bfbc3dc 100644 --- a/cvk/incl/cvk/base/allocator.hpp +++ b/cvk/incl/cvk/base/allocator.hpp @@ -95,9 +95,9 @@ class Allocator { */ [[nodiscard]] static VmaAllocatorCreateFlags flags(PhysicalDevice const&); - [[nodiscard]] bool created() const { return static_cast(_handle); } - [[nodiscard]] VmaAllocator get() const { return _handle.get(); } - [[nodiscard]] Device const& device() const { return *_device; } + [[nodiscard]] constexpr bool created() const { return static_cast(_handle); } + [[nodiscard]] constexpr VmaAllocator get() const { return _handle.get(); } + [[nodiscard]] constexpr Device const& device() const { return *_device; } private: void forget(); @@ -111,7 +111,7 @@ class Allocator { Allocator& operator=(Allocator const& other) = delete; Allocator& operator=(Allocator&& other) noexcept = default; - static void debug_check(Allocator const&); + static constexpr void debug_check(Allocator const&); }; } @@ -122,7 +122,7 @@ struct Allocator::State { } namespace cvk { -inline void Allocator::debug_check(Allocator const& allocator) { +constexpr void Allocator::debug_check(Allocator const& allocator) { CTH_CRITICAL(!allocator.created(), "allocator must be created") {} } } diff --git a/cvk/incl/cvk/base/core.hpp b/cvk/incl/cvk/base/core.hpp index 4c33c31b..46db3d6d 100644 --- a/cvk/incl/cvk/base/core.hpp +++ b/cvk/incl/cvk/base/core.hpp @@ -96,7 +96,7 @@ class Core { */ State release(); - [[nodiscard]] bool created() const { return _device != nullptr; } + [[nodiscard]] constexpr bool created() const { return _device != nullptr; } /** * @return index of the used queue set in @ref Config::queueSets @@ -105,7 +105,7 @@ class Core { */ [[nodiscard]] size_t queueSetIndex() const; - [[nodiscard]] Instance const& instance() const { return *_instance; } + [[nodiscard]] constexpr Instance const& instance() const { return *_instance; } [[nodiscard]] VkInstance vkInstance() const; /** @@ -131,12 +131,12 @@ class Core { * @param idx index in @ref Config::queueProperties * @pre @ref created() */ - [[nodiscard]] auto& queue(this auto& self, size_t idx) { + [[nodiscard]] constexpr auto& queue(this auto& self, size_t idx) { debug_check(self); return self._queues[idx]; } - [[nodiscard]] std::span queues() const { return _queues; } + [[nodiscard]] constexpr std::span queues() const { return _queues; } private: struct queue_mappings { @@ -188,7 +188,7 @@ class Core { Core& operator=(Core const& other) = delete; Core& operator=(Core&& other) noexcept; - static void debug_check(Core const&); + static constexpr void debug_check(Core const&); }; } @@ -219,7 +219,7 @@ struct Core::State { } namespace cvk { -inline void Core::debug_check(Core const& core) { +constexpr void Core::debug_check(Core const& core) { CTH_CRITICAL(!core.created(), "core must be created") {} } } diff --git a/cvk/incl/cvk/base/device.hpp b/cvk/incl/cvk/base/device.hpp index 093b84ce..e810d9eb 100644 --- a/cvk/incl/cvk/base/device.hpp +++ b/cvk/incl/cvk/base/device.hpp @@ -106,17 +106,17 @@ class Device { */ static void destroy(VkDevice); - [[nodiscard]] std::span queues() const { return _queues; } - [[nodiscard]] std::span queues() { return _queues; } + [[nodiscard]] constexpr std::span queues() const { return _queues; } + [[nodiscard]] constexpr std::span queues() { return _queues; } - [[nodiscard]] DeviceTable table() const { return DeviceTable{_handle.get(), _functionTable.get()}; } - [[nodiscard]] VolkDeviceTable const* functions() const { return _functionTable.get(); } - [[nodiscard]] VkDevice get() const { return _handle.get(); } + [[nodiscard]] constexpr DeviceTable table() const { return DeviceTable{_handle.get(), _functionTable.get()}; } + [[nodiscard]] constexpr VolkDeviceTable const* functions() const { return _functionTable.get(); } + [[nodiscard]] constexpr VkDevice get() const { return _handle.get(); } - [[nodiscard]] Instance const& instance() const { return *_instance; } - [[nodiscard]] PhysicalDevice const& physicalDevice() const { return *_physicalDevice; } + [[nodiscard]] constexpr Instance const& instance() const { return *_instance; } + [[nodiscard]] constexpr PhysicalDevice const& physicalDevice() const { return *_physicalDevice; } - [[nodiscard]] bool created() const { return static_cast(_handle); } + [[nodiscard]] constexpr bool created() const { return static_cast(_handle); } private: void forget(); @@ -153,8 +153,8 @@ class Device { Device& operator=(Device const& other) = delete; Device& operator=(Device&& other) noexcept = default; - static void debug_check(Device const&); - static void debug_check_handle(vk_not_null); + static constexpr void debug_check(Device const&); + static constexpr void debug_check_handle(vk_not_null); }; } @@ -170,10 +170,10 @@ struct Device::State { } namespace cvk { -inline void Device::debug_check(Device const& device) { +constexpr void Device::debug_check(Device const& device) { CTH_CRITICAL(!device.created(), "device must be created") {} debug_check_handle(device.get()); } -inline void Device::debug_check_handle([[maybe_unused]] vk_not_null vkDevice) {} +constexpr void Device::debug_check_handle([[maybe_unused]] vk_not_null vkDevice) {} } diff --git a/cvk/incl/cvk/base/device_table.hpp b/cvk/incl/cvk/base/device_table.hpp index b13045fa..92275817 100644 --- a/cvk/incl/cvk/base/device_table.hpp +++ b/cvk/incl/cvk/base/device_table.hpp @@ -9,6 +9,6 @@ struct DeviceTable { not_null table; VolkDeviceTable const* operator ->() const { return table.get(); } - [[nodiscard]] VkDevice device() const { return vkDevice.get(); } + [[nodiscard]] constexpr VkDevice device() const { return vkDevice.get(); } }; } diff --git a/cvk/incl/cvk/base/instance.hpp b/cvk/incl/cvk/base/instance.hpp index ea50fc1e..b24abf1a 100644 --- a/cvk/incl/cvk/base/instance.hpp +++ b/cvk/incl/cvk/base/instance.hpp @@ -131,11 +131,11 @@ class Instance { */ static void destroy(VkInstance); - [[nodiscard]] bool created() const { return static_cast(_handle); } - [[nodiscard]] VkInstance get() const { return _handle.get(); } - [[nodiscard]] std::vector availableExtensions() const { return _availableExt; } - [[nodiscard]] std::vector requiredExtensions() const { return _requiredExt; } - [[nodiscard]] std::vector availableValidationLayers() const { return _availableLayers; } + [[nodiscard]] constexpr bool created() const { return static_cast(_handle); } + [[nodiscard]] constexpr VkInstance get() const { return _handle.get(); } + [[nodiscard]] constexpr std::vector availableExtensions() const { return _availableExt; } + [[nodiscard]] constexpr std::vector requiredExtensions() const { return _requiredExt; } + [[nodiscard]] constexpr std::vector availableValidationLayers() const { return _availableLayers; } private: void forget(); @@ -158,8 +158,8 @@ class Instance { Instance(Instance&& other) noexcept = default; Instance& operator=(Instance&& other) noexcept = default; - static void debug_check(Instance const&); - static void debug_check_handle(vk_not_null); + static constexpr void debug_check(Instance const&); + static constexpr void debug_check_handle(vk_not_null); }; } @@ -177,6 +177,6 @@ struct Instance::State { } namespace cvk { -inline void Instance::debug_check(Instance const& instance) { debug_check_handle(instance.get()); } -inline void Instance::debug_check_handle([[maybe_unused]] vk_not_null vkInstance) {} +constexpr void Instance::debug_check(Instance const& instance) { debug_check_handle(instance.get()); } +constexpr void Instance::debug_check_handle([[maybe_unused]] vk_not_null vkInstance) {} } diff --git a/cvk/incl/cvk/base/physical_device.hpp b/cvk/incl/cvk/base/physical_device.hpp index 0c15eabe..712230ec 100644 --- a/cvk/incl/cvk/base/physical_device.hpp +++ b/cvk/incl/cvk/base/physical_device.hpp @@ -187,16 +187,16 @@ class PhysicalDevice { [[nodiscard]] static VkSampleCountFlagBits evalMaxSampleCount(VkPhysicalDeviceProperties const& properties); - [[nodiscard]] bool created() const { return _handle != nullptr; } - [[nodiscard]] VkPhysicalDevice get() const { return _handle.get(); } - [[nodiscard]] auto const& features() const { return _features; } - [[nodiscard]] auto const& requiredFeatures() const { return _requiredFeatures; } - [[nodiscard]] std::span extensions() const { return _extensions; } - [[nodiscard]] std::span requiredExtensions() const { return _requiredExtensions; } - [[nodiscard]] auto const& properties() const { return _properties; } - [[nodiscard]] auto const& memProperties() const { return _memProperties; } - [[nodiscard]] VkSampleCountFlagBits maxSampleCount() const { return _maxSampleCount; } - [[nodiscard]] VkPhysicalDeviceLimits const& limits() const { return _properties.limits; } + [[nodiscard]] constexpr bool created() const { return _handle != nullptr; } + [[nodiscard]] constexpr VkPhysicalDevice get() const { return _handle.get(); } + [[nodiscard]] constexpr auto const& features() const { return _features; } + [[nodiscard]] constexpr auto const& requiredFeatures() const { return _requiredFeatures; } + [[nodiscard]] constexpr std::span extensions() const { return _extensions; } + [[nodiscard]] constexpr std::span requiredExtensions() const { return _requiredExtensions; } + [[nodiscard]] constexpr auto const& properties() const { return _properties; } + [[nodiscard]] constexpr auto const& memProperties() const { return _memProperties; } + [[nodiscard]] constexpr VkSampleCountFlagBits maxSampleCount() const { return _maxSampleCount; } + [[nodiscard]] constexpr VkPhysicalDeviceLimits const& limits() const { return _properties.limits; } /** * @param idx queue family index @@ -204,7 +204,7 @@ class PhysicalDevice { */ [[nodiscard]] QueueFamily const* queueFamily(queue_family_index_t idx) const; - [[nodiscard]] std::span queueFamilies() const { return _queueFamilies; } + [[nodiscard]] constexpr std::span queueFamilies() const { return _queueFamilies; } private: not_null _instance; @@ -228,8 +228,8 @@ class PhysicalDevice { PhysicalDevice& operator=(PhysicalDevice const& other) = default; PhysicalDevice& operator=(PhysicalDevice&& other) noexcept = default; - static void debug_check(PhysicalDevice const& device); - static void debug_check_handle(vk_not_null vkDevice); + static constexpr void debug_check(PhysicalDevice const& device); + static constexpr void debug_check_handle(vk_not_null vkDevice); }; } @@ -255,10 +255,10 @@ struct PhysicalDevice::CreateResult { } namespace cvk { -inline void PhysicalDevice::debug_check(PhysicalDevice const& device) { +constexpr void PhysicalDevice::debug_check(PhysicalDevice const& device) { CTH_CRITICAL(!device.created(), "physical device must be created") {} debug_check_handle(device.get()); } -inline void PhysicalDevice::debug_check_handle([[maybe_unused]] vk_not_null vkDevice) {} +constexpr void PhysicalDevice::debug_check_handle([[maybe_unused]] vk_not_null vkDevice) {} } diff --git a/cvk/incl/cvk/base/queue/queue.hpp b/cvk/incl/cvk/base/queue/queue.hpp index 40c7480e..a14c573c 100644 --- a/cvk/incl/cvk/base/queue/queue.hpp +++ b/cvk/incl/cvk/base/queue/queue.hpp @@ -69,12 +69,12 @@ class Queue { */ void wait() const; - [[nodiscard]] bool can_present() const { return contains(_familyProperties, QueueFamilyProperties::PRESENT); } - [[nodiscard]] bool created() const { return _handle != VK_NULL_HANDLE; } - [[nodiscard]] VkQueue get() const { return _handle.get(); } - [[nodiscard]] uint32_t index() const { return _queueIndex; } - [[nodiscard]] uint32_t familyIndex() const { return _familyIndex; } - [[nodiscard]] QueueFamilyProperties familyProperties() const { return _familyProperties; } + [[nodiscard]] constexpr bool can_present() const { return contains(_familyProperties, QueueFamilyProperties::PRESENT); } + [[nodiscard]] constexpr bool created() const { return _handle != VK_NULL_HANDLE; } + [[nodiscard]] constexpr VkQueue get() const { return _handle.get(); } + [[nodiscard]] constexpr uint32_t index() const { return _queueIndex; } + [[nodiscard]] constexpr uint32_t familyIndex() const { return _familyIndex; } + [[nodiscard]] constexpr QueueFamilyProperties familyProperties() const { return _familyProperties; } private: void forget(); @@ -94,23 +94,23 @@ class Queue { Queue& operator=(Queue const& other) = default; Queue& operator=(Queue&& other) = default; - static void debug_check(Queue const&); - static void debug_check_present(Queue const&); - static void debug_check_handle(VkQueue); + static constexpr void debug_check(Queue const&); + static constexpr void debug_check_present(Queue const&); + static constexpr void debug_check_handle(VkQueue); }; } namespace cvk { -inline void Queue::debug_check(Queue const& queue) { +constexpr void Queue::debug_check(Queue const& queue) { CTH_CRITICAL(!queue.created(), "queue must be created") {} } -inline void Queue::debug_check_present(Queue const& queue) { +constexpr void Queue::debug_check_present(Queue const& queue) { debug_check(queue); CTH_CRITICAL(!queue.can_present(), "queue is not a present queue") {} } -inline void Queue::debug_check_handle(VkQueue vkQueue) { +constexpr void Queue::debug_check_handle(VkQueue vkQueue) { CTH_CRITICAL(vkQueue == VK_NULL_HANDLE, "vkQueue must not be invalid (VK_NULL_HANDLE)") {} } } diff --git a/cvk/incl/cvk/base/queue/queue_state.hpp b/cvk/incl/cvk/base/queue/queue_state.hpp index 738ff33c..1da51380 100644 --- a/cvk/incl/cvk/base/queue/queue_state.hpp +++ b/cvk/incl/cvk/base/queue/queue_state.hpp @@ -32,7 +32,7 @@ struct QueueState { std::shared_ptr vkQueueMtx = std::make_shared(); - static void debug_check(QueueState const& state) { + static constexpr void debug_check(QueueState const& state) { CTH_CRITICAL(state.vkQueueMtx == nullptr, "mutex must not be nullptr") {} } }; diff --git a/cvk/incl/cvk/data/img/sampler.hpp b/cvk/incl/cvk/data/img/sampler.hpp index eddfcaf4..697cf828 100644 --- a/cvk/incl/cvk/data/img/sampler.hpp +++ b/cvk/incl/cvk/data/img/sampler.hpp @@ -86,14 +86,14 @@ class Sampler { static void destroy(DeviceTable table, VkSampler); - [[nodiscard]] bool created() const { return static_cast(_handle); } - [[nodiscard]] VkSampler get() const { return _handle.get(); } - [[nodiscard]] Core const& core() const { return _handle.core(); } + [[nodiscard]] constexpr bool created() const { return static_cast(_handle); } + [[nodiscard]] constexpr VkSampler get() const { return _handle.get(); } + [[nodiscard]] constexpr Core const& core() const { return _handle.core(); } /** * @return the config, with the clamped anisotropy */ - [[nodiscard]] Config const& config() const { return _config; } + [[nodiscard]] constexpr Config const& config() const { return _config; } private: void forget(); @@ -107,7 +107,7 @@ class Sampler { Sampler& operator=(Sampler const& other) = delete; Sampler& operator=(Sampler&& other) noexcept = default; - static void debug_check(Sampler const&); + static constexpr void debug_check(Sampler const&); }; } @@ -118,7 +118,7 @@ struct Sampler::State { } namespace cvk { -inline void Sampler::debug_check(Sampler const& sampler) { +constexpr void Sampler::debug_check(Sampler const& sampler) { CTH_CRITICAL(!sampler.created(), "sampler must be created") {} } } diff --git a/cvk/incl/cvk/data/memory/memory.hpp b/cvk/incl/cvk/data/memory/memory.hpp index e06f7328..01fabca0 100644 --- a/cvk/incl/cvk/data/memory/memory.hpp +++ b/cvk/incl/cvk/data/memory/memory.hpp @@ -128,10 +128,10 @@ class Memory { static void destroy(VmaAllocator, VmaAllocation); - [[nodiscard]] bool created() const { return static_cast(_handle); } - [[nodiscard]] VmaAllocation get() const { return _handle.get(); } - [[nodiscard]] Core const& core() const { return _handle.core(); } - [[nodiscard]] Config const& config() const { return _config; } + [[nodiscard]] constexpr bool created() const { return static_cast(_handle); } + [[nodiscard]] constexpr VmaAllocation get() const { return _handle.get(); } + [[nodiscard]] constexpr Core const& core() const { return _handle.core(); } + [[nodiscard]] constexpr Config const& config() const { return _config; } /** * @return the VkDeviceMemory, the offset and the size of the range @@ -164,7 +164,7 @@ class Memory { Memory& operator=(Memory const& other) = delete; Memory& operator=(Memory&& other) noexcept = default; - static void debug_check(Memory const&); + static constexpr void debug_check(Memory const&); }; } @@ -175,7 +175,7 @@ struct Memory::State { } namespace cvk { -inline void Memory::debug_check(Memory const& memory) { +constexpr void Memory::debug_check(Memory const& memory) { CTH_CRITICAL(!memory.created(), "memory must be created") {} } } diff --git a/cvk/incl/cvk/debug/debug_messenger.hpp b/cvk/incl/cvk/debug/debug_messenger.hpp index df9c8b7b..376036cf 100644 --- a/cvk/incl/cvk/debug/debug_messenger.hpp +++ b/cvk/incl/cvk/debug/debug_messenger.hpp @@ -91,9 +91,9 @@ class DebugMessenger { [[nodiscard]] VkDebugUtilsMessengerCreateInfoEXT createInfo() const; }; - [[nodiscard]] bool created() const { return static_cast(_handle); } - [[nodiscard]] VkDebugUtilsMessengerEXT get() const { return _handle.get(); } - [[nodiscard]] Config config() const { return _config; } + [[nodiscard]] constexpr bool created() const { return static_cast(_handle); } + [[nodiscard]] constexpr VkDebugUtilsMessengerEXT get() const { return _handle.get(); } + [[nodiscard]] constexpr Config config() const { return _config; } protected: Config _config; @@ -109,7 +109,7 @@ class DebugMessenger { DebugMessenger& operator=(DebugMessenger const& other) = delete; DebugMessenger& operator=(DebugMessenger&& other) noexcept = default; - static void debug_check(DebugMessenger const&); + static constexpr void debug_check(DebugMessenger const&); }; } // namespace cvk @@ -120,7 +120,7 @@ struct DebugMessenger::State { } namespace cvk { -inline void DebugMessenger::debug_check(DebugMessenger const& debugMessenger) { +constexpr void DebugMessenger::debug_check(DebugMessenger const& debugMessenger) { CTH_CRITICAL(!debugMessenger.created(), "debug_messenger not created") {} } } diff --git a/cvk/incl/cvk/render/pipeline/layout/descriptor_set_layout.hpp b/cvk/incl/cvk/render/pipeline/layout/descriptor_set_layout.hpp index 88d47e73..48f6e54a 100644 --- a/cvk/incl/cvk/render/pipeline/layout/descriptor_set_layout.hpp +++ b/cvk/incl/cvk/render/pipeline/layout/descriptor_set_layout.hpp @@ -87,9 +87,9 @@ class DescriptorSetLayout { static void destroy(DeviceTable table, VkDescriptorSetLayout); - [[nodiscard]] bool created() const { return static_cast(_handle); } - [[nodiscard]] VkDescriptorSetLayout get() const { return _handle.get(); } - [[nodiscard]] Core const& core() const { return _handle.core(); } + [[nodiscard]] constexpr bool created() const { return static_cast(_handle); } + [[nodiscard]] constexpr VkDescriptorSetLayout get() const { return _handle.get(); } + [[nodiscard]] constexpr Core const& core() const { return _handle.core(); } /** * @return bindings, sorted by binding number @@ -113,7 +113,7 @@ class DescriptorSetLayout { DescriptorSetLayout& operator=(DescriptorSetLayout const& other) = delete; DescriptorSetLayout& operator=(DescriptorSetLayout&& other) noexcept = default; - static void debug_check(DescriptorSetLayout const&); + static constexpr void debug_check(DescriptorSetLayout const&); }; } @@ -177,7 +177,7 @@ constexpr DescriptorSetLayout::Config& DescriptorSetLayout::Config::remove(uint3 } namespace cvk { -inline void DescriptorSetLayout::debug_check(DescriptorSetLayout const& layout) { +constexpr void DescriptorSetLayout::debug_check(DescriptorSetLayout const& layout) { CTH_CRITICAL(!layout.created(), "descriptor set layout must be created") {} } } diff --git a/cvk/incl/cvk/render/pipeline/layout/pipeline_layout.hpp b/cvk/incl/cvk/render/pipeline/layout/pipeline_layout.hpp index 8e7741b3..e083e628 100644 --- a/cvk/incl/cvk/render/pipeline/layout/pipeline_layout.hpp +++ b/cvk/incl/cvk/render/pipeline/layout/pipeline_layout.hpp @@ -88,9 +88,9 @@ class PipelineLayout { static void destroy(DeviceTable table, VkPipelineLayout); - [[nodiscard]] bool created() const { return static_cast(_handle); } - [[nodiscard]] VkPipelineLayout get() const { return _handle.get(); } - [[nodiscard]] Core const& core() const { return _handle.core(); } + [[nodiscard]] constexpr bool created() const { return static_cast(_handle); } + [[nodiscard]] constexpr VkPipelineLayout get() const { return _handle.get(); } + [[nodiscard]] constexpr Core const& core() const { return _handle.core(); } private: void forget(); @@ -105,7 +105,7 @@ class PipelineLayout { PipelineLayout& operator=(PipelineLayout const& other) = delete; PipelineLayout& operator=(PipelineLayout&& other) noexcept = default; - static void debug_check(PipelineLayout const&); + static constexpr void debug_check(PipelineLayout const&); }; } @@ -125,7 +125,7 @@ struct PipelineLayout::State { } namespace cvk { -inline void PipelineLayout::debug_check(PipelineLayout const& layout) { +constexpr void PipelineLayout::debug_check(PipelineLayout const& layout) { CTH_CRITICAL(!layout.created(), "pipeline layout must be created") {} } } diff --git a/cvk/incl/cvk/render/pipeline/pipeline.hpp b/cvk/incl/cvk/render/pipeline/pipeline.hpp index c2028b33..b73dde95 100644 --- a/cvk/incl/cvk/render/pipeline/pipeline.hpp +++ b/cvk/incl/cvk/render/pipeline/pipeline.hpp @@ -105,16 +105,16 @@ class Pipeline { static void destroy(DeviceTable table, VkPipeline); - [[nodiscard]] bool created() const { return static_cast(_handle); } - [[nodiscard]] VkPipeline get() const { return _handle.get(); } - [[nodiscard]] VkPipelineLayout layout() const { return _vkLayout.get(); } + [[nodiscard]] constexpr bool created() const { return static_cast(_handle); } + [[nodiscard]] constexpr VkPipeline get() const { return _handle.get(); } + [[nodiscard]] constexpr VkPipelineLayout layout() const { return _vkLayout.get(); } /** * @return VK_NULL_HANDLE -> not a derivative pipeline */ - [[nodiscard]] VkPipeline parent() const { return _vkParent; } - [[nodiscard]] Config const& config() const { return _config; } - [[nodiscard]] Core const& core() const { return _handle.core(); } + [[nodiscard]] constexpr VkPipeline parent() const { return _vkParent; } + [[nodiscard]] constexpr Config const& config() const { return _config; } + [[nodiscard]] constexpr Core const& core() const { return _handle.core(); } private: void forget(); @@ -130,7 +130,7 @@ class Pipeline { Pipeline& operator=(Pipeline const& other) = delete; Pipeline& operator=(Pipeline&& other) noexcept = default; - static void debug_check(Pipeline const&); + static constexpr void debug_check(Pipeline const&); }; } @@ -141,7 +141,7 @@ struct Pipeline::State { } namespace cvk { -inline void Pipeline::debug_check(Pipeline const& pipeline) { +constexpr void Pipeline::debug_check(Pipeline const& pipeline) { CTH_CRITICAL(!pipeline.created(), "pipeline must be created") {} } } diff --git a/cvk/incl/cvk/render/pipeline/shader/shader.hpp b/cvk/incl/cvk/render/pipeline/shader/shader.hpp index ed4e342c..d4ec4420 100644 --- a/cvk/incl/cvk/render/pipeline/shader/shader.hpp +++ b/cvk/incl/cvk/render/pipeline/shader/shader.hpp @@ -155,10 +155,10 @@ class Shader { static void destroy(DeviceTable table, VkShaderModule); - [[nodiscard]] bool created() const { return static_cast(_handle); } - [[nodiscard]] VkShaderModule get() const { return _handle.get(); } - [[nodiscard]] VkShaderStageFlagBits stage() const { return _stage; } - [[nodiscard]] Core const& core() const { return _handle.core(); } + [[nodiscard]] constexpr bool created() const { return static_cast(_handle); } + [[nodiscard]] constexpr VkShaderModule get() const { return _handle.get(); } + [[nodiscard]] constexpr VkShaderStageFlagBits stage() const { return _stage; } + [[nodiscard]] constexpr Core const& core() const { return _handle.core(); } private: void forget(); @@ -172,7 +172,7 @@ class Shader { Shader& operator=(Shader const& other) = delete; Shader& operator=(Shader&& other) noexcept = default; - static void debug_check(Shader const&); + static constexpr void debug_check(Shader const&); }; } @@ -183,7 +183,7 @@ struct Shader::State { } namespace cvk { -inline void Shader::debug_check(Shader const& shader) { +constexpr void Shader::debug_check(Shader const& shader) { CTH_CRITICAL(!shader.created(), "shader must be created") {} } } diff --git a/cvk/incl/cvk/render/sync/fence.hpp b/cvk/incl/cvk/render/sync/fence.hpp index 625f41bd..c5df95d1 100644 --- a/cvk/incl/cvk/render/sync/fence.hpp +++ b/cvk/incl/cvk/render/sync/fence.hpp @@ -153,10 +153,10 @@ class Fence { static void destroy(DeviceTable table, VkFence); - [[nodiscard]] bool created() const { return static_cast(_handle); } - [[nodiscard]] VkFence get() const { return _handle.get(); } - [[nodiscard]] Core const& core() const { return _handle.core(); } - [[nodiscard]] VkExternalFenceHandleTypeFlags exportTypes() const { return _exportTypes; } + [[nodiscard]] constexpr bool created() const { return static_cast(_handle); } + [[nodiscard]] constexpr VkFence get() const { return _handle.get(); } + [[nodiscard]] constexpr Core const& core() const { return _handle.core(); } + [[nodiscard]] constexpr VkExternalFenceHandleTypeFlags exportTypes() const { return _exportTypes; } private: void forget(); @@ -170,7 +170,7 @@ class Fence { Fence& operator=(Fence const& other) = delete; Fence& operator=(Fence&& other) noexcept = default; - static void debug_check(Fence const&); + static constexpr void debug_check(Fence const&); }; } @@ -185,7 +185,7 @@ struct Fence::State { } namespace cvk { -inline void Fence::debug_check(Fence const& fence) { +constexpr void Fence::debug_check(Fence const& fence) { CTH_CRITICAL(!fence.created(), "fence must be created") {} } } diff --git a/cvk/incl/cvk/render/sync/semaphore.hpp b/cvk/incl/cvk/render/sync/semaphore.hpp index a83d74f3..b1c9ee35 100644 --- a/cvk/incl/cvk/render/sync/semaphore.hpp +++ b/cvk/incl/cvk/render/sync/semaphore.hpp @@ -81,9 +81,9 @@ class Semaphore { static void destroy(DeviceTable table, VkSemaphore); - [[nodiscard]] bool created() const { return static_cast(_handle); } - [[nodiscard]] VkSemaphore get() const { return _handle.get(); } - [[nodiscard]] Core const& core() const { return _handle.core(); } + [[nodiscard]] constexpr bool created() const { return static_cast(_handle); } + [[nodiscard]] constexpr VkSemaphore get() const { return _handle.get(); } + [[nodiscard]] constexpr Core const& core() const { return _handle.core(); } protected: /** @@ -105,7 +105,7 @@ class Semaphore { Semaphore& operator=(Semaphore const& other) = delete; Semaphore& operator=(Semaphore&& other) noexcept = default; - static void debug_check(Semaphore const&); + static constexpr void debug_check(Semaphore const&); }; } @@ -116,7 +116,7 @@ struct Semaphore::State { } namespace cvk { -inline void Semaphore::debug_check(Semaphore const& semaphore) { +constexpr void Semaphore::debug_check(Semaphore const& semaphore) { CTH_CRITICAL(!semaphore.created(), "semaphore must be created") {} } } diff --git a/cvk/incl/cvk/render/sync/timeline_semaphore.hpp b/cvk/incl/cvk/render/sync/timeline_semaphore.hpp index c88d0b9c..9d4a6f65 100644 --- a/cvk/incl/cvk/render/sync/timeline_semaphore.hpp +++ b/cvk/incl/cvk/render/sync/timeline_semaphore.hpp @@ -97,12 +97,12 @@ class TimelineSemaphore : Semaphore { * @return value for the next signal operation * @details thread safe */ - [[nodiscard]] uint64_t next() const { return ++_value; } + [[nodiscard]] constexpr uint64_t next() const { return ++_value; } /** * @return the counter, value to wait for */ - [[nodiscard]] uint64_t value() const { return _value; } + [[nodiscard]] constexpr uint64_t value() const { return _value; } /** * @return current value of the semaphore @@ -129,7 +129,7 @@ class TimelineSemaphore : Semaphore { return *this; } - static void debug_check(TimelineSemaphore const& semaphore); + static constexpr void debug_check(TimelineSemaphore const& semaphore); }; } @@ -144,5 +144,5 @@ struct TimelineSemaphore::State { } namespace cvk { -inline void TimelineSemaphore::debug_check(TimelineSemaphore const& semaphore) { Semaphore::debug_check(semaphore); } +constexpr void TimelineSemaphore::debug_check(TimelineSemaphore const& semaphore) { Semaphore::debug_check(semaphore); } } diff --git a/cvk/incl/cvk/surface/surface.hpp b/cvk/incl/cvk/surface/surface.hpp index 0babc4fe..96672360 100644 --- a/cvk/incl/cvk/surface/surface.hpp +++ b/cvk/incl/cvk/surface/surface.hpp @@ -107,9 +107,9 @@ class Surface { static void destroy(vk_not_null, VkSurfaceKHR); - [[nodiscard]] bool created() const { return static_cast(_handle); } - [[nodiscard]] VkSurfaceKHR get() const { return _handle.get(); } - [[nodiscard]] Config const& config() const { return _config; } + [[nodiscard]] constexpr bool created() const { return static_cast(_handle); } + [[nodiscard]] constexpr VkSurfaceKHR get() const { return _handle.get(); } + [[nodiscard]] constexpr Config const& config() const { return _config; } private: void forget(); @@ -123,8 +123,8 @@ class Surface { Surface& operator=(Surface const& other) = delete; Surface& operator=(Surface&& other) noexcept = default; - static void debug_check(Surface const&); - static void debug_check_handle(vk_not_null); + static constexpr void debug_check(Surface const&); + static constexpr void debug_check_handle(vk_not_null); }; } @@ -135,9 +135,9 @@ struct Surface::State { } namespace cvk { -inline void Surface::debug_check(Surface const& surface) { +constexpr void Surface::debug_check(Surface const& surface) { CTH_CRITICAL(!surface.created(), "surface must be created") {} debug_check_handle(surface.get()); } -inline void Surface::debug_check_handle([[maybe_unused]] vk_not_null vkSurface) {} +constexpr void Surface::debug_check_handle([[maybe_unused]] vk_not_null vkSurface) {} } diff --git a/cvk/incl/cvk/utility/device/physical_device_features.hpp b/cvk/incl/cvk/utility/device/physical_device_features.hpp index 2699dea8..f0b24550 100644 --- a/cvk/incl/cvk/utility/device/physical_device_features.hpp +++ b/cvk/incl/cvk/utility/device/physical_device_features.hpp @@ -61,12 +61,12 @@ class PhysicalDeviceFeatures { * @tparam T one of @ref chain_t */ template - [[nodiscard]] auto& features(this auto& self) { return std::get(self._chain); } + [[nodiscard]] constexpr auto& features(this auto& self) { return std::get(self._chain); } /** * @brief start of the pNext chain, for VkDeviceCreateInfo::pNext */ - [[nodiscard]] VkPhysicalDeviceFeatures2 const* get() const { return &std::get(_chain); } + [[nodiscard]] constexpr VkPhysicalDeviceFeatures2 const* get() const { return &std::get(_chain); } private: void link(); From aa9d794e40a89feb393b7572360d37f82c95be6c Mon Sep 17 00:00:00 2001 From: lukas <89145039+codethinki@users.noreply.github.com> Date: Sat, 17 Oct 2026 00:25:31 +0200 Subject: [PATCH 038/135] cvk: port buffer Buffer owns the VkBuffer and a Memory member, vmaCreateBuffer creates both. The memory part of the legacy BaseBuffer (map cache, write) lives in Memory: Memory::write maps, copies, flushes and unmaps (vmaCopyMemoryToAllocation flushes after the unmap, invalid for non coherent memory), Memory::mapped returns the current mapping. Fixed while porting: inverted bounds check in the static write, wrap without a size, descriptorInfo with an offset and VK_WHOLE_SIZE. copy() and stage() come with the command buffer. --- cvk/incl/cvk/data/buffer/buffer.hpp | 169 +++++++++++++++++++++ cvk/incl/cvk/data/buffer/buffer_config.hpp | 35 +++++ cvk/incl/cvk/data/memory/memory.hpp | 33 ++++ cvk/src/data/buffer/buffer.cpp | 80 ++++++++++ cvk/src/data/memory/memory.cpp | 22 +++ cvk/tests/src/data/buffer/buffer_test.cpp | 157 +++++++++++++++++++ cvk/tests/src/data/memory/memory_test.cpp | 47 ++++++ 7 files changed, 543 insertions(+) create mode 100644 cvk/incl/cvk/data/buffer/buffer.hpp create mode 100644 cvk/incl/cvk/data/buffer/buffer_config.hpp create mode 100644 cvk/src/data/buffer/buffer.cpp create mode 100644 cvk/tests/src/data/buffer/buffer_test.cpp diff --git a/cvk/incl/cvk/data/buffer/buffer.hpp b/cvk/incl/cvk/data/buffer/buffer.hpp new file mode 100644 index 00000000..e5982a15 --- /dev/null +++ b/cvk/incl/cvk/data/buffer/buffer.hpp @@ -0,0 +1,169 @@ +#pragma once +#include "buffer_config.hpp" + +#include "cvk/base/device_table.hpp" +#include "cvk/data/memory/memory.hpp" +#include "cvk/utility/core_handle.hpp" +#include "cvk/utility/types.hpp" + +#include + +#include + +#include + + +namespace cvk { +class Core; + +/** + * @brief VkBuffer wrapper, owns the buffer and its memory + * @details @ref create() creates both with vmaCreateBuffer(), + * map, flush and write through @ref memory() + */ +class Buffer { +public: + using Config = BufferConfig; + struct State; + + /** + * @brief base constructor + * @pre @ref Core::created() + */ + Buffer(Core const&, Config const&); + + /** + * @brief constructs and wraps + * @details calls: + * - @ref Buffer(Core const&, Config const&) + * - @ref wrap() + */ + Buffer(Core const&, Config const&, State const&); + + /** + * @brief constructs and creates + * @details calls: + * - @ref Buffer(Core const&, Config const&) + * - @ref create() + */ + Buffer(Core const&, Config const&, size_t size); + + /** + * @note calls @ref optDestroy() + */ + ~Buffer() { optDestroy(); } + + /** + * @brief wraps the state + * @details std::nullopt @ref State::memory -> @ref destroy() only destroys the buffer + * @pre the buffer is bound to its memory + * @pre the buffer was created with the usage of the config + * @post @ref created() + * @note calls @ref optDestroy() + */ + void wrap(State const&); + + /** + * @brief creates the buffer and its memory + * @param size in bytes + * @post @ref created() + * @note calls @ref optDestroy() + * @throws cvk::vk_result_exception result of vmaCreateBuffer() + */ + void create(size_t size); + + /** + * @brief destroys the buffer, then its memory, and forgets + * @pre @ref created() + * @pre the gpu does not use the buffer + */ + void destroy(); + + /** + * @brief if @ref created() -> calls @ref destroy() + */ + void optDestroy() { if(created()) destroy(); } + + /** + * @brief releases ownership of the buffer and its memory, returns the state and forgets + * @pre @ref created() + */ + State release(); + + /** + * @param size in bytes (VK_WHOLE_SIZE => to the end) + * @param offset in bytes + * @pre @ref created() + * @pre offset + size <= @ref size() + */ + [[nodiscard]] constexpr VkDescriptorBufferInfo descriptorInfo(size_t size = VK_WHOLE_SIZE, size_t offset = 0) const; + + static void destroy(DeviceTable table, VkBuffer); + + [[nodiscard]] constexpr bool created() const { return static_cast(_handle); } + [[nodiscard]] constexpr VkBuffer get() const { return _handle.get(); } + [[nodiscard]] constexpr Core const& core() const { return _handle.core(); } + [[nodiscard]] constexpr Config const& config() const { return _config; } + + /** + * @return size in bytes + */ + [[nodiscard]] constexpr size_t size() const { return _size; } + + /** + * @return the memory of the buffer, not created -> the buffer does not own its memory + */ + [[nodiscard]] constexpr Memory const& memory() const { return _memory; } + +private: + void forget(); + + // declared before the memory: move assignment destroys the old buffer before its memory + core_handle _handle; + Memory _memory; + Config _config; + size_t _size = 0; + +public: + Buffer(Buffer const& other) = delete; + Buffer(Buffer&& other) noexcept = default; + Buffer& operator=(Buffer const& other) = delete; + Buffer& operator=(Buffer&& other) noexcept = default; + + static constexpr void debug_check(Buffer const&); +}; +} + +namespace cvk { +struct Buffer::State { + vk_not_null vkBuffer; + /** + * @brief in bytes + */ + size_t size; + /** + * @brief std::nullopt -> the buffer does not own its memory + */ + std::optional memory = std::nullopt; +}; +} + +namespace cvk { +constexpr VkDescriptorBufferInfo Buffer::descriptorInfo(size_t size, size_t offset) const { + debug_check(*this); + CTH_CRITICAL(offset > _size, "offset must lie in the buffer") {} + CTH_CRITICAL(size != VK_WHOLE_SIZE && size > _size - offset, "range must lie in the buffer") {} + + return VkDescriptorBufferInfo{ + .buffer = get(), + .offset = offset, + .range = size == VK_WHOLE_SIZE ? _size - offset : size, + }; +} +} + +namespace cvk { +constexpr void Buffer::debug_check(Buffer const& buffer) { + CTH_CRITICAL(!buffer.created(), "buffer must be created") {} +} +} diff --git a/cvk/incl/cvk/data/buffer/buffer_config.hpp b/cvk/incl/cvk/data/buffer/buffer_config.hpp new file mode 100644 index 00000000..59a1836a --- /dev/null +++ b/cvk/incl/cvk/data/buffer/buffer_config.hpp @@ -0,0 +1,35 @@ +#pragma once +#include "cvk/data/memory/memory_config.hpp" + +#include + + +namespace cvk { +/** + * @brief what the buffer is for and where its memory lives + */ +struct BufferConfig { + /** + * @pre != 0 + */ + VkBufferUsageFlags usage = 0; + + /** + * @brief memory of the buffer, VMA_MEMORY_USAGE_AUTO picks the memory type from @ref usage + */ + MemoryConfig memory{}; + + [[nodiscard]] constexpr VkBufferCreateInfo createInfo(size_t size) const { + return VkBufferCreateInfo{ + .sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO, + .pNext = nullptr, + .flags = 0, + .size = size, + .usage = usage, + .sharingMode = VK_SHARING_MODE_EXCLUSIVE, + .queueFamilyIndexCount = 0, + .pQueueFamilyIndices = nullptr, + }; + } +}; +} diff --git a/cvk/incl/cvk/data/memory/memory.hpp b/cvk/incl/cvk/data/memory/memory.hpp index 01fabca0..29e3567e 100644 --- a/cvk/incl/cvk/data/memory/memory.hpp +++ b/cvk/incl/cvk/data/memory/memory.hpp @@ -9,6 +9,7 @@ #include +#include #include @@ -126,6 +127,24 @@ class Memory { */ void invalidate(size_t size = VK_WHOLE_SIZE, size_t offset = 0) const; + /** + * @brief copies the data to the memory + * @details maps, copies, flushes and unmaps + * @param offset from the start of the memory + * @pre @ref created() + * @pre @ref properties() has VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT + * @pre offset + data.size() <= @ref size() + * @throws cvk::vk_result_exception result of vmaMapMemory() + * @throws cvk::vk_result_exception result of vmaFlushAllocation() + */ + void write(std::span data, size_t offset = 0) const; + + /** + * @brief copies the data to the mapped range + * @pre data.size() <= mapped.size() + */ + static constexpr void write(std::span data, std::span mapped); + static void destroy(VmaAllocator, VmaAllocation); [[nodiscard]] constexpr bool created() const { return static_cast(_handle); } @@ -150,6 +169,13 @@ class Memory { */ [[nodiscard]] size_t size() const { return info().size; } + /** + * @return the whole memory if mapped, not mapped -> empty + * @details @ref map() or VMA_ALLOCATION_CREATE_MAPPED_BIT maps it + * @pre @ref created() + */ + [[nodiscard]] std::span mapped() const; + private: void forget(); @@ -174,6 +200,13 @@ struct Memory::State { }; } +namespace cvk { +constexpr void Memory::write(std::span data, std::span mapped) { + CTH_CRITICAL(data.size() > mapped.size(), "data must fit into the mapped range") {} + std::ranges::copy(data, mapped.begin()); +} +} + namespace cvk { constexpr void Memory::debug_check(Memory const& memory) { CTH_CRITICAL(!memory.created(), "memory must be created") {} diff --git a/cvk/src/data/buffer/buffer.cpp b/cvk/src/data/buffer/buffer.cpp new file mode 100644 index 00000000..8d18922d --- /dev/null +++ b/cvk/src/data/buffer/buffer.cpp @@ -0,0 +1,80 @@ +#include "cvk/data/buffer/buffer.hpp" + +#include "cvk/base/allocator.hpp" +#include "cvk/base/core.hpp" +#include "cvk/utility/vk_exceptions.hpp" + + +namespace cvk { +Buffer::Buffer(Core const& core, Config const& config) : _handle{core}, _memory{core, config.memory}, + _config{config} { + Core::debug_check(core); + CTH_CRITICAL(config.usage == 0, "usage must not be 0") {} +} + +Buffer::Buffer(Core const& core, Config const& config, State const& state) : Buffer{core, config} { wrap(state); } + +Buffer::Buffer(Core const& core, Config const& config, size_t size) : Buffer{core, config} { create(size); } + +void Buffer::wrap(State const& state) { + optDestroy(); + + _handle.reset(state.vkBuffer.get(), core()); + if(state.memory.has_value()) _memory.wrap(*state.memory); + _size = state.size; +} + +void Buffer::create(size_t size) { + optDestroy(); + + auto const bufferInfo = _config.createInfo(size); + auto const allocationInfo = _memory.config().createInfo(); + + VkBuffer vkBuffer = VK_NULL_HANDLE; + VmaAllocation vmaAllocation = VK_NULL_HANDLE; + auto const result = vmaCreateBuffer( + core().allocator().get(), + &bufferInfo, + &allocationInfo, + &vkBuffer, + &vmaAllocation, + nullptr + ); + + CTH_STABLE_ERR(result != VK_SUCCESS, "failed to create buffer") { + forget(); + throw cvk::vk_result_exception{result, details->exception()}; + } + + _handle.reset(vkBuffer, core()); + _memory.wrap(Memory::State{vmaAllocation}); + _size = size; +} + +void Buffer::destroy() { + debug_check(*this); + + _handle.reset(); + _memory.optDestroy(); + + forget(); +} + +Buffer::State Buffer::release() { + debug_check(*this); + + State state{.vkBuffer = _handle.release(), .size = _size, .memory = std::nullopt}; + if(_memory.created()) state.memory = _memory.release(); + + forget(); + return state; +} + +void Buffer::destroy(DeviceTable table, VkBuffer vkBuffer) { + CTH_WARN(vkBuffer == VK_NULL_HANDLE, "vkBuffer invalid (VK_NULL_HANDLE)") {} + + table->vkDestroyBuffer(table.device(), vkBuffer, nullptr); +} + +void Buffer::forget() { _size = 0; } +} diff --git a/cvk/src/data/memory/memory.cpp b/cvk/src/data/memory/memory.cpp index 56ca8a1d..7958c52b 100644 --- a/cvk/src/data/memory/memory.cpp +++ b/cvk/src/data/memory/memory.cpp @@ -92,6 +92,21 @@ void Memory::invalidate(size_t size, size_t offset) const { CVK_RESULT_STABLE_THROW(result != VK_SUCCESS, result, "failed to invalidate memory") {} } +void Memory::write(std::span data, size_t offset) const { + debug_check(*this); + CTH_CRITICAL((properties() & VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT) == 0, "memory must be host visible") {} + CTH_CRITICAL(offset > size() || data.size() > size() - offset, "range must lie in the memory") {} + + if(data.empty()) return; + + // not vmaCopyMemoryToAllocation(): it flushes after the unmap, non coherent memory must be mapped to flush + write(data, map(data.size(), offset)); + auto const result = vmaFlushAllocation(allocator(), get(), offset, data.size()); + unmap(); + + CVK_RESULT_STABLE_THROW(result != VK_SUCCESS, result, "failed to flush memory") {} +} + void Memory::destroy(VmaAllocator vmaAllocator, VmaAllocation vmaAllocation) { CTH_WARN(vmaAllocation == VK_NULL_HANDLE, "vmaAllocation invalid (VK_NULL_HANDLE)") {} @@ -106,6 +121,13 @@ VmaAllocationInfo Memory::info() const { return info; } +std::span Memory::mapped() const { + auto const allocationInfo = info(); + if(allocationInfo.pMappedData == nullptr) return {}; + + return {static_cast(allocationInfo.pMappedData), allocationInfo.size}; +} + VkMemoryPropertyFlags Memory::properties() const { debug_check(*this); diff --git a/cvk/tests/src/data/buffer/buffer_test.cpp b/cvk/tests/src/data/buffer/buffer_test.cpp new file mode 100644 index 00000000..6a755f51 --- /dev/null +++ b/cvk/tests/src/data/buffer/buffer_test.cpp @@ -0,0 +1,157 @@ +#include "context.hpp" +#include "test.hpp" + +#include "cvk/data/buffer/buffer.hpp" + +#include +#include + + +namespace cvk { +namespace { + constexpr size_t SIZE = 256; + + /** + * @brief staging buffer, the cpu writes it once + */ + constexpr Buffer::Config STAGING_CONFIG{ + .usage = VK_BUFFER_USAGE_TRANSFER_SRC_BIT, + .memory = {.flags = VMA_ALLOCATION_CREATE_HOST_ACCESS_SEQUENTIAL_WRITE_BIT}, + }; + + /** + * @brief buffer the cpu reads back + */ + constexpr Buffer::Config READBACK_CONFIG{ + .usage = VK_BUFFER_USAGE_TRANSFER_DST_BIT, + .memory = {.flags = VMA_ALLOCATION_CREATE_HOST_ACCESS_RANDOM_BIT}, + }; + + /** + * @brief gpu only buffer + */ + constexpr Buffer::Config DEVICE_CONFIG{.usage = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT}; +} + +CVK_TEST(Buffer, config_create_info) { + constexpr auto info = STAGING_CONFIG.createInfo(SIZE); + + static_assert(info.size == SIZE); + static_assert(info.usage == VK_BUFFER_USAGE_TRANSFER_SRC_BIT); + static_assert(info.sharingMode == VK_SHARING_MODE_EXCLUSIVE); + static_assert(STAGING_CONFIG.memory.autoUsage()); +} + +CVK_TEST(Buffer, base_constructor_does_not_create) { + test::Context const context{}; + + Buffer const buffer{context.core, STAGING_CONFIG}; + + EXPECT_FALSE(buffer.created()); + EXPECT_FALSE(buffer.memory().created()); +} + +CVK_TEST(Buffer, create_and_destroy) { + test::Context const context{}; + Buffer buffer{context.core, DEVICE_CONFIG, SIZE}; + + ASSERT_TRUE(buffer.created()); + EXPECT_EQ(&buffer.core(), &context.core); + EXPECT_EQ(buffer.size(), SIZE); + ASSERT_TRUE(buffer.memory().created()); + EXPECT_GE(buffer.memory().size(), SIZE); + + buffer.destroy(); + EXPECT_FALSE(buffer.created()); + EXPECT_FALSE(buffer.memory().created()); + EXPECT_EQ(buffer.size(), 0); +} + +CVK_TEST(Buffer, auto_usage_picks_host_visible_memory_for_host_access) { + test::Context const context{}; + + Buffer const buffer{context.core, STAGING_CONFIG, SIZE}; + + EXPECT_NE(buffer.memory().properties() & VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT, 0u); +} + +CVK_TEST(Buffer, write_and_read_through_the_memory) { + test::Context const context{}; + Buffer const buffer{context.core, READBACK_CONFIG, SIZE}; + std::array data{}; + std::ranges::fill(data, 'x'); + + buffer.memory().write(data); + + auto const mapped = buffer.memory().map(SIZE); + buffer.memory().invalidate(); + EXPECT_TRUE(std::ranges::equal(mapped, data)); + buffer.memory().unmap(); +} + +CVK_TEST(Buffer, descriptor_info) { + test::Context const context{}; + Buffer const buffer{context.core, DEVICE_CONFIG, SIZE}; + + auto const whole = buffer.descriptorInfo(); + EXPECT_EQ(whole.buffer, buffer.get()); + EXPECT_EQ(whole.offset, 0); + EXPECT_EQ(whole.range, SIZE); + + auto const end = buffer.descriptorInfo(VK_WHOLE_SIZE, 64); + EXPECT_EQ(end.offset, 64); + EXPECT_EQ(end.range, SIZE - 64); + + auto const part = buffer.descriptorInfo(16, 32); + EXPECT_EQ(part.range, 16); +} + +CVK_TEST(Buffer, release_and_wrap) { + test::Context const context{}; + Buffer buffer{context.core, DEVICE_CONFIG, SIZE}; + auto const vkBuffer = buffer.get(); + auto const vmaAllocation = buffer.memory().get(); + + auto const state = buffer.release(); + EXPECT_FALSE(buffer.created()); + EXPECT_FALSE(buffer.memory().created()); + ASSERT_TRUE(state.memory.has_value()); + + Buffer const wrapped{context.core, DEVICE_CONFIG, state}; + EXPECT_EQ(wrapped.get(), vkBuffer); + EXPECT_EQ(wrapped.memory().get(), vmaAllocation); + EXPECT_EQ(wrapped.size(), SIZE); +} + +CVK_TEST(Buffer, wrap_without_memory_destroys_only_the_buffer) { + test::Context const context{}; + Buffer buffer{context.core, DEVICE_CONFIG, SIZE}; + auto state = buffer.release(); + + // another owner frees the memory, after the buffer + Memory const memory{context.core, DEVICE_CONFIG.memory, *state.memory}; + state.memory = std::nullopt; + + { + Buffer const wrapped{context.core, DEVICE_CONFIG, state}; + EXPECT_FALSE(wrapped.memory().created()); + EXPECT_EQ(wrapped.size(), SIZE); + } + + EXPECT_TRUE(memory.created()); +} + +CVK_TEST(Buffer, move_assignment_destroys_the_old_buffer) { + test::Context const context{}; + Buffer buffer{context.core, DEVICE_CONFIG, SIZE}; + Buffer other{context.core, DEVICE_CONFIG, SIZE}; + auto const vkBuffer = other.get(); + + buffer = std::move(other); + + EXPECT_EQ(buffer.get(), vkBuffer); + EXPECT_TRUE(buffer.memory().created()); + EXPECT_FALSE(other.created()); + EXPECT_FALSE(other.memory().created()); +} +} diff --git a/cvk/tests/src/data/memory/memory_test.cpp b/cvk/tests/src/data/memory/memory_test.cpp index d03b0584..2185c527 100644 --- a/cvk/tests/src/data/memory/memory_test.cpp +++ b/cvk/tests/src/data/memory/memory_test.cpp @@ -4,6 +4,7 @@ #include "cvk/data/memory/memory.hpp" #include +#include #include @@ -109,6 +110,52 @@ CVK_TEST(Memory, map_a_range) { memory.unmap(); } +CVK_TEST(Memory, write_copies_the_data) { + test::Context const context{}; + Memory const memory{context.core, HOST_CONFIG, REQUIREMENTS}; + std::array data{}; + std::ranges::fill(data, 'y'); + + memory.write(data, 32); + + auto const mapped = memory.map(data.size(), 32); + EXPECT_TRUE(std::ranges::equal(mapped, data)); + memory.unmap(); +} + +CVK_TEST(Memory, mapped_follows_map_and_unmap) { + test::Context const context{}; + Memory const memory{context.core, HOST_CONFIG, REQUIREMENTS}; + + EXPECT_TRUE(memory.mapped().empty()); + + auto const data = memory.map(); + EXPECT_EQ(memory.mapped().data(), data.data()); + EXPECT_EQ(memory.mapped().size(), memory.size()); + + memory.unmap(); + EXPECT_TRUE(memory.mapped().empty()); +} + +CVK_TEST(Memory, mapped_bit_maps_persistently) { + test::Context const context{}; + auto config = HOST_CONFIG; + config.flags |= VMA_ALLOCATION_CREATE_MAPPED_BIT; + + Memory const memory{context.core, config, REQUIREMENTS}; + + EXPECT_FALSE(memory.mapped().empty()); +} + +CVK_TEST(Memory, write_to_a_mapped_range_is_constexpr) { + static_assert([] { + std::array mapped{}; + std::array const data{'a', 'b'}; + Memory::write(data, mapped); + return mapped[0] == 'a' && mapped[1] == 'b' && mapped[2] == 0; + }()); +} + CVK_TEST(Memory, release_and_wrap) { test::Context const context{}; Memory memory{context.core, HOST_CONFIG, REQUIREMENTS}; From 02b1047dd20cff204bf59acc19a5b119e200458a Mon Sep 17 00:00:00 2001 From: lukas <89145039+codethinki@users.noreply.github.com> Date: Sat, 17 Oct 2026 00:37:03 +0200 Subject: [PATCH 039/135] cvk: buffer config with create flags and queue families The create info no longer hardcodes flags and the exclusive sharing mode. Two or more queue families -> VK_SHARING_MODE_CONCURRENT. BufferConfig::debug_check checks the usage and unique families, the Buffer base constructor checks the families exist. --- cvk/incl/cvk/data/buffer/buffer.hpp | 1 + cvk/incl/cvk/data/buffer/buffer_config.hpp | 46 +++++++++++++++++++--- cvk/src/data/buffer/buffer.cpp | 14 ++++++- cvk/tests/src/data/buffer/buffer_test.cpp | 44 +++++++++++++++++++++ 4 files changed, 99 insertions(+), 6 deletions(-) diff --git a/cvk/incl/cvk/data/buffer/buffer.hpp b/cvk/incl/cvk/data/buffer/buffer.hpp index e5982a15..9e7bd9e5 100644 --- a/cvk/incl/cvk/data/buffer/buffer.hpp +++ b/cvk/incl/cvk/data/buffer/buffer.hpp @@ -29,6 +29,7 @@ class Buffer { /** * @brief base constructor * @pre @ref Core::created() + * @pre the queue families of the config exist on the physical device */ Buffer(Core const&, Config const&); diff --git a/cvk/incl/cvk/data/buffer/buffer_config.hpp b/cvk/incl/cvk/data/buffer/buffer_config.hpp index 59a1836a..5b67e3fd 100644 --- a/cvk/incl/cvk/data/buffer/buffer_config.hpp +++ b/cvk/incl/cvk/data/buffer/buffer_config.hpp @@ -1,12 +1,18 @@ #pragma once +#include "cvk/base/queue/queue_family.hpp" #include "cvk/data/memory/memory_config.hpp" #include +#include + +#include +#include + namespace cvk { /** - * @brief what the buffer is for and where its memory lives + * @brief what the buffer is for, who uses it and where its memory lives */ struct BufferConfig { /** @@ -14,22 +20,52 @@ struct BufferConfig { */ VkBufferUsageFlags usage = 0; + /** + * @brief sparse binding, protected memory, ... + */ + VkBufferCreateFlags flags = 0; + + /** + * @brief queue families that use the buffer at the same time + * @details size < 2 -> VK_SHARING_MODE_EXCLUSIVE, else VK_SHARING_MODE_CONCURRENT + * @pre unique + */ + std::vector queueFamilies{}; + /** * @brief memory of the buffer, VMA_MEMORY_USAGE_AUTO picks the memory type from @ref usage */ MemoryConfig memory{}; + /** + * @param size in bytes + * @attention pQueueFamilyIndices points into @ref queueFamilies + */ [[nodiscard]] constexpr VkBufferCreateInfo createInfo(size_t size) const { + bool const concurrent = queueFamilies.size() >= 2; + return VkBufferCreateInfo{ .sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO, .pNext = nullptr, - .flags = 0, + .flags = flags, .size = size, .usage = usage, - .sharingMode = VK_SHARING_MODE_EXCLUSIVE, - .queueFamilyIndexCount = 0, - .pQueueFamilyIndices = nullptr, + .sharingMode = concurrent ? VK_SHARING_MODE_CONCURRENT : VK_SHARING_MODE_EXCLUSIVE, + .queueFamilyIndexCount = concurrent ? static_cast(queueFamilies.size()) : 0, + .pQueueFamilyIndices = concurrent ? queueFamilies.data() : nullptr, }; } + + static constexpr void debug_check(BufferConfig const&); }; } + +namespace cvk { +constexpr void BufferConfig::debug_check(BufferConfig const& config) { + CTH_CRITICAL(config.usage == 0, "usage must not be 0") {} + + auto families = config.queueFamilies; + std::ranges::sort(families); + CTH_CRITICAL(std::ranges::adjacent_find(families) != families.end(), "queue families must be unique") {} +} +} diff --git a/cvk/src/data/buffer/buffer.cpp b/cvk/src/data/buffer/buffer.cpp index 8d18922d..e6eded07 100644 --- a/cvk/src/data/buffer/buffer.cpp +++ b/cvk/src/data/buffer/buffer.cpp @@ -2,14 +2,26 @@ #include "cvk/base/allocator.hpp" #include "cvk/base/core.hpp" +#include "cvk/base/physical_device.hpp" #include "cvk/utility/vk_exceptions.hpp" +#include + namespace cvk { Buffer::Buffer(Core const& core, Config const& config) : _handle{core}, _memory{core, config.memory}, _config{config} { Core::debug_check(core); - CTH_CRITICAL(config.usage == 0, "usage must not be 0") {} + Config::debug_check(config); + CTH_CRITICAL( + std::ranges::any_of( + config.queueFamilies, + [familyCount = core.physicalDevice().queueFamilies().size()](queue_family_index_t family) { + return family >= familyCount; + } + ), + "queue families must exist on the physical device" + ) {} } Buffer::Buffer(Core const& core, Config const& config, State const& state) : Buffer{core, config} { wrap(state); } diff --git a/cvk/tests/src/data/buffer/buffer_test.cpp b/cvk/tests/src/data/buffer/buffer_test.cpp index 6a755f51..58a58e44 100644 --- a/cvk/tests/src/data/buffer/buffer_test.cpp +++ b/cvk/tests/src/data/buffer/buffer_test.cpp @@ -1,6 +1,7 @@ #include "context.hpp" #include "test.hpp" +#include "cvk/base/physical_device.hpp" #include "cvk/data/buffer/buffer.hpp" #include @@ -38,10 +39,40 @@ CVK_TEST(Buffer, config_create_info) { static_assert(info.size == SIZE); static_assert(info.usage == VK_BUFFER_USAGE_TRANSFER_SRC_BIT); + static_assert(info.flags == 0); static_assert(info.sharingMode == VK_SHARING_MODE_EXCLUSIVE); + static_assert(info.queueFamilyIndexCount == 0 && info.pQueueFamilyIndices == nullptr); static_assert(STAGING_CONFIG.memory.autoUsage()); } +CVK_TEST(Buffer, config_one_queue_family_is_exclusive) { + static_assert([] { + Buffer::Config config{.usage = VK_BUFFER_USAGE_TRANSFER_SRC_BIT, .queueFamilies = {2}}; + auto const info = config.createInfo(SIZE); + return info.sharingMode == VK_SHARING_MODE_EXCLUSIVE && info.queueFamilyIndexCount == 0; + }()); +} + +CVK_TEST(Buffer, config_queue_families_are_concurrent) { + static_assert([] { + Buffer::Config config{ + .usage = VK_BUFFER_USAGE_TRANSFER_SRC_BIT, + .flags = VK_BUFFER_CREATE_PROTECTED_BIT, + .queueFamilies = {0, 2}, + }; + auto const info = config.createInfo(SIZE); + return info.sharingMode == VK_SHARING_MODE_CONCURRENT && info.queueFamilyIndexCount == 2 + && info.pQueueFamilyIndices == config.queueFamilies.data() && info.flags == VK_BUFFER_CREATE_PROTECTED_BIT; + }()); +} + +CVK_TEST(Buffer, config_debug_check_is_constexpr) { + static_assert([] { + Buffer::Config::debug_check({.usage = VK_BUFFER_USAGE_TRANSFER_SRC_BIT, .queueFamilies = {1, 0}}); + return true; + }()); +} + CVK_TEST(Buffer, base_constructor_does_not_create) { test::Context const context{}; @@ -67,6 +98,19 @@ CVK_TEST(Buffer, create_and_destroy) { EXPECT_EQ(buffer.size(), 0); } +CVK_TEST(Buffer, create_concurrent) { + test::Context const context{}; + auto const familyCount = context.core.physicalDevice().queueFamilies().size(); + if(familyCount < 2) GTEST_SKIP() << "needs 2 queue families"; + + auto config = DEVICE_CONFIG; + config.queueFamilies = {0, 1}; + + Buffer const buffer{context.core, config, SIZE}; + + EXPECT_TRUE(buffer.created()); +} + CVK_TEST(Buffer, auto_usage_picks_host_visible_memory_for_host_access) { test::Context const context{}; From 29b6870c2bd5c5b69a0f346dff9d0660212ea636 Mon Sep 17 00:00:00 2001 From: lukas <89145039+codethinki@users.noreply.github.com> Date: Sat, 17 Oct 2026 00:40:59 +0200 Subject: [PATCH 040/135] cvk: buffer passes the queue families as configured The create info always passes the family list, Vulkan ignores it for the exclusive sharing mode. The Buffer base constructor no longer checks that the families exist: the validation layers report it (VUID-VkBufferCreateInfo-sharingMode-01419). --- cvk/incl/cvk/data/buffer/buffer.hpp | 1 - cvk/incl/cvk/data/buffer/buffer_config.hpp | 8 +++----- cvk/src/data/buffer/buffer.cpp | 12 ------------ cvk/tests/src/data/buffer/buffer_test.cpp | 2 +- 4 files changed, 4 insertions(+), 19 deletions(-) diff --git a/cvk/incl/cvk/data/buffer/buffer.hpp b/cvk/incl/cvk/data/buffer/buffer.hpp index 9e7bd9e5..e5982a15 100644 --- a/cvk/incl/cvk/data/buffer/buffer.hpp +++ b/cvk/incl/cvk/data/buffer/buffer.hpp @@ -29,7 +29,6 @@ class Buffer { /** * @brief base constructor * @pre @ref Core::created() - * @pre the queue families of the config exist on the physical device */ Buffer(Core const&, Config const&); diff --git a/cvk/incl/cvk/data/buffer/buffer_config.hpp b/cvk/incl/cvk/data/buffer/buffer_config.hpp index 5b67e3fd..27b2413e 100644 --- a/cvk/incl/cvk/data/buffer/buffer_config.hpp +++ b/cvk/incl/cvk/data/buffer/buffer_config.hpp @@ -42,17 +42,15 @@ struct BufferConfig { * @attention pQueueFamilyIndices points into @ref queueFamilies */ [[nodiscard]] constexpr VkBufferCreateInfo createInfo(size_t size) const { - bool const concurrent = queueFamilies.size() >= 2; - return VkBufferCreateInfo{ .sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO, .pNext = nullptr, .flags = flags, .size = size, .usage = usage, - .sharingMode = concurrent ? VK_SHARING_MODE_CONCURRENT : VK_SHARING_MODE_EXCLUSIVE, - .queueFamilyIndexCount = concurrent ? static_cast(queueFamilies.size()) : 0, - .pQueueFamilyIndices = concurrent ? queueFamilies.data() : nullptr, + .sharingMode = queueFamilies.size() >= 2 ? VK_SHARING_MODE_CONCURRENT : VK_SHARING_MODE_EXCLUSIVE, + .queueFamilyIndexCount = static_cast(queueFamilies.size()), + .pQueueFamilyIndices = queueFamilies.data(), }; } diff --git a/cvk/src/data/buffer/buffer.cpp b/cvk/src/data/buffer/buffer.cpp index e6eded07..688c3b4b 100644 --- a/cvk/src/data/buffer/buffer.cpp +++ b/cvk/src/data/buffer/buffer.cpp @@ -2,26 +2,14 @@ #include "cvk/base/allocator.hpp" #include "cvk/base/core.hpp" -#include "cvk/base/physical_device.hpp" #include "cvk/utility/vk_exceptions.hpp" -#include - namespace cvk { Buffer::Buffer(Core const& core, Config const& config) : _handle{core}, _memory{core, config.memory}, _config{config} { Core::debug_check(core); Config::debug_check(config); - CTH_CRITICAL( - std::ranges::any_of( - config.queueFamilies, - [familyCount = core.physicalDevice().queueFamilies().size()](queue_family_index_t family) { - return family >= familyCount; - } - ), - "queue families must exist on the physical device" - ) {} } Buffer::Buffer(Core const& core, Config const& config, State const& state) : Buffer{core, config} { wrap(state); } diff --git a/cvk/tests/src/data/buffer/buffer_test.cpp b/cvk/tests/src/data/buffer/buffer_test.cpp index 58a58e44..301db4f7 100644 --- a/cvk/tests/src/data/buffer/buffer_test.cpp +++ b/cvk/tests/src/data/buffer/buffer_test.cpp @@ -49,7 +49,7 @@ CVK_TEST(Buffer, config_one_queue_family_is_exclusive) { static_assert([] { Buffer::Config config{.usage = VK_BUFFER_USAGE_TRANSFER_SRC_BIT, .queueFamilies = {2}}; auto const info = config.createInfo(SIZE); - return info.sharingMode == VK_SHARING_MODE_EXCLUSIVE && info.queueFamilyIndexCount == 0; + return info.sharingMode == VK_SHARING_MODE_EXCLUSIVE && info.queueFamilyIndexCount == 1; }()); } From 76b806853c81dd7649cb690ddb8aa729111ea4f2 Mon Sep 17 00:00:00 2001 From: lukas <89145039+codethinki@users.noreply.github.com> Date: Sat, 17 Oct 2026 00:51:29 +0200 Subject: [PATCH 041/135] cvk: BaseBuffer and typed Buffer The byte buffer is BaseBuffer (renamed from Buffer), Buffer is a thin header only layer on top: element counts and offsets, typed map and write, no members of its own. T must be trivially copyable, the typed map starts the lifetime of the elements. Fixed against the legacy Buffer: descriptorInfo dropped the offset for WHOLE_SIZE, flush and invalidate multiplied WHOLE_SIZE by sizeof(T). --- cvk/incl/cvk/data/buffer/base_buffer.hpp | 170 +++++++++++++++ cvk/incl/cvk/data/buffer/buffer.hpp | 200 +++++++++-------- .../buffer/{buffer.cpp => base_buffer.cpp} | 20 +- .../src/data/buffer/base_buffer_test.cpp | 201 ++++++++++++++++++ cvk/tests/src/data/buffer/buffer_test.cpp | 197 +++++++---------- 5 files changed, 551 insertions(+), 237 deletions(-) create mode 100644 cvk/incl/cvk/data/buffer/base_buffer.hpp rename cvk/src/data/buffer/{buffer.cpp => base_buffer.cpp} (69%) create mode 100644 cvk/tests/src/data/buffer/base_buffer_test.cpp diff --git a/cvk/incl/cvk/data/buffer/base_buffer.hpp b/cvk/incl/cvk/data/buffer/base_buffer.hpp new file mode 100644 index 00000000..4881d49f --- /dev/null +++ b/cvk/incl/cvk/data/buffer/base_buffer.hpp @@ -0,0 +1,170 @@ +#pragma once +#include "buffer_config.hpp" + +#include "cvk/base/device_table.hpp" +#include "cvk/data/memory/memory.hpp" +#include "cvk/utility/core_handle.hpp" +#include "cvk/utility/types.hpp" + +#include + +#include + +#include + + +namespace cvk { +class Core; + +/** + * @brief VkBuffer wrapper, owns the buffer and its memory, sizes in bytes + * @details @ref create() creates both with vmaCreateBuffer(), + * map, flush and write through @ref memory(), + * @ref Buffer adds an element type + */ +class BaseBuffer { +public: + using Config = BufferConfig; + struct State; + + /** + * @brief base constructor + * @pre @ref Core::created() + */ + BaseBuffer(Core const&, Config const&); + + /** + * @brief constructs and wraps + * @details calls: + * - @ref BaseBuffer(Core const&, Config const&) + * - @ref wrap() + */ + BaseBuffer(Core const&, Config const&, State const&); + + /** + * @brief constructs and creates + * @details calls: + * - @ref BaseBuffer(Core const&, Config const&) + * - @ref create() + */ + BaseBuffer(Core const&, Config const&, size_t size); + + /** + * @note calls @ref optDestroy() + */ + ~BaseBuffer() { optDestroy(); } + + /** + * @brief wraps the state + * @details std::nullopt @ref State::memory -> @ref destroy() only destroys the buffer + * @pre the buffer is bound to its memory + * @pre the buffer was created with the usage of the config + * @post @ref created() + * @note calls @ref optDestroy() + */ + void wrap(State const&); + + /** + * @brief creates the buffer and its memory + * @param size in bytes + * @post @ref created() + * @note calls @ref optDestroy() + * @throws cvk::vk_result_exception result of vmaCreateBuffer() + */ + void create(size_t size); + + /** + * @brief destroys the buffer, then its memory, and forgets + * @pre @ref created() + * @pre the gpu does not use the buffer + */ + void destroy(); + + /** + * @brief if @ref created() -> calls @ref destroy() + */ + void optDestroy() { if(created()) destroy(); } + + /** + * @brief releases ownership of the buffer and its memory, returns the state and forgets + * @pre @ref created() + */ + State release(); + + /** + * @param size in bytes (VK_WHOLE_SIZE => to the end) + * @param offset in bytes + * @pre @ref created() + * @pre offset + size <= @ref size() + */ + [[nodiscard]] constexpr VkDescriptorBufferInfo descriptorInfo(size_t size = VK_WHOLE_SIZE, size_t offset = 0) const; + + static void destroy(DeviceTable table, VkBuffer); + + [[nodiscard]] constexpr bool created() const { return static_cast(_handle); } + [[nodiscard]] constexpr VkBuffer get() const { return _handle.get(); } + [[nodiscard]] constexpr Core const& core() const { return _handle.core(); } + [[nodiscard]] constexpr Config const& config() const { return _config; } + + /** + * @return size in bytes + */ + [[nodiscard]] constexpr size_t size() const { return _size; } + + /** + * @return the memory of the buffer, not created -> the buffer does not own its memory + */ + [[nodiscard]] constexpr Memory const& memory() const { return _memory; } + +private: + void forget(); + + // declared before the memory: move assignment destroys the old buffer before its memory + core_handle _handle; + Memory _memory; + Config _config; + size_t _size = 0; + +public: + BaseBuffer(BaseBuffer const& other) = delete; + BaseBuffer(BaseBuffer&& other) noexcept = default; + BaseBuffer& operator=(BaseBuffer const& other) = delete; + BaseBuffer& operator=(BaseBuffer&& other) noexcept = default; + + static constexpr void debug_check(BaseBuffer const&); +}; +} + +namespace cvk { +struct BaseBuffer::State { + vk_not_null vkBuffer; + /** + * @brief in bytes + */ + size_t size; + /** + * @brief std::nullopt -> the buffer does not own its memory + */ + std::optional memory = std::nullopt; +}; +} + +namespace cvk { +constexpr VkDescriptorBufferInfo BaseBuffer::descriptorInfo(size_t size, size_t offset) const { + debug_check(*this); + CTH_CRITICAL(offset > _size, "offset must lie in the buffer") {} + CTH_CRITICAL(size != VK_WHOLE_SIZE && size > _size - offset, "range must lie in the buffer") {} + + return VkDescriptorBufferInfo{ + .buffer = get(), + .offset = offset, + .range = size == VK_WHOLE_SIZE ? _size - offset : size, + }; +} +} + +namespace cvk { +constexpr void BaseBuffer::debug_check(BaseBuffer const& buffer) { + CTH_CRITICAL(!buffer.created(), "buffer must be created") {} +} +} diff --git a/cvk/incl/cvk/data/buffer/buffer.hpp b/cvk/incl/cvk/data/buffer/buffer.hpp index e5982a15..10c83f92 100644 --- a/cvk/incl/cvk/data/buffer/buffer.hpp +++ b/cvk/incl/cvk/data/buffer/buffer.hpp @@ -1,36 +1,30 @@ #pragma once -#include "buffer_config.hpp" - -#include "cvk/base/device_table.hpp" -#include "cvk/data/memory/memory.hpp" -#include "cvk/utility/core_handle.hpp" -#include "cvk/utility/types.hpp" +#include "base_buffer.hpp" #include #include -#include +#include +#include +#include +#include namespace cvk { -class Core; - /** - * @brief VkBuffer wrapper, owns the buffer and its memory - * @details @ref create() creates both with vmaCreateBuffer(), - * map, flush and write through @ref memory() + * @brief @ref BaseBuffer with an element type, sizes and offsets in elements + * @tparam T element, the gpu copies it byte by byte + * @details adds no members: a @ref BaseBuffer reference to it sees the same buffer in bytes */ -class Buffer { +template requires std::is_trivially_copyable_v +class Buffer : public BaseBuffer { public: - using Config = BufferConfig; - struct State; - /** * @brief base constructor * @pre @ref Core::created() */ - Buffer(Core const&, Config const&); + Buffer(Core const& core, Config const& config) : BaseBuffer{core, config} {} /** * @brief constructs and wraps @@ -38,7 +32,7 @@ class Buffer { * - @ref Buffer(Core const&, Config const&) * - @ref wrap() */ - Buffer(Core const&, Config const&, State const&); + Buffer(Core const& core, Config const& config, State const& state) : Buffer{core, config} { wrap(state); } /** * @brief constructs and creates @@ -46,124 +40,128 @@ class Buffer { * - @ref Buffer(Core const&, Config const&) * - @ref create() */ - Buffer(Core const&, Config const&, size_t size); - - /** - * @note calls @ref optDestroy() - */ - ~Buffer() { optDestroy(); } + Buffer(Core const& core, Config const& config, size_t count) : Buffer{core, config} { create(count); } /** * @brief wraps the state - * @details std::nullopt @ref State::memory -> @ref destroy() only destroys the buffer - * @pre the buffer is bound to its memory - * @pre the buffer was created with the usage of the config - * @post @ref created() - * @note calls @ref optDestroy() + * @pre @ref State::size is a multiple of sizeof(T) + * @details calls @ref BaseBuffer::wrap() */ - void wrap(State const&); + void wrap(State const& state) { + CTH_CRITICAL(state.size % sizeof(T) != 0, "size must be a multiple of the element size") {} + BaseBuffer::wrap(state); + } /** * @brief creates the buffer and its memory - * @param size in bytes - * @post @ref created() - * @note calls @ref optDestroy() - * @throws cvk::vk_result_exception result of vmaCreateBuffer() + * @param count of elements + * @details calls @ref BaseBuffer::create() */ - void create(size_t size); + void create(size_t count) { BaseBuffer::create(count * sizeof(T)); } /** - * @brief destroys the buffer, then its memory, and forgets - * @pre @ref created() - * @pre the gpu does not use the buffer + * @brief maps the memory as elements + * @param count (VK_WHOLE_SIZE => to the end) + * @param offset in elements + * @return the mapped elements + * @pre offset + count <= @ref elements() + * @note each call needs one @ref unmap() + * @details calls @ref Memory::map() */ - void destroy(); + [[nodiscard]] std::span map(size_t count = VK_WHOLE_SIZE, size_t offset = 0) const { + CTH_CRITICAL(offset > elements(), "offset must lie in the buffer") {} + size_t const mapCount = count == VK_WHOLE_SIZE ? elements() - offset : count; + CTH_CRITICAL(mapCount > elements() - offset, "range must lie in the buffer") {} + + return as_elements(memory().map(mapCount * sizeof(T), offset * sizeof(T))); + } /** - * @brief if @ref created() -> calls @ref destroy() + * @brief unmaps the memory once + * @details calls @ref Memory::unmap() */ - void optDestroy() { if(created()) destroy(); } + void unmap() const { memory().unmap(); } /** - * @brief releases ownership of the buffer and its memory, returns the state and forgets - * @pre @ref created() + * @brief copies the data to the buffer + * @param offset in elements + * @pre offset + data.size() <= @ref elements() + * @details calls @ref Memory::write() */ - State release(); + void write(std::span data, size_t offset = 0) const { + CTH_CRITICAL(offset > elements() || data.size() > elements() - offset, "range must lie in the buffer") {} + + memory().write(std::span{reinterpret_cast(data.data()), data.size_bytes()}, offset * sizeof(T)); + } /** - * @param size in bytes (VK_WHOLE_SIZE => to the end) - * @param offset in bytes - * @pre @ref created() - * @pre offset + size <= @ref size() + * @brief makes host writes visible to the device + * @param count (VK_WHOLE_SIZE => to the end) + * @param offset in elements + * @details calls @ref Memory::flush() */ - [[nodiscard]] constexpr VkDescriptorBufferInfo descriptorInfo(size_t size = VK_WHOLE_SIZE, size_t offset = 0) const; - - static void destroy(DeviceTable table, VkBuffer); + void flush(size_t count = VK_WHOLE_SIZE, size_t offset = 0) const { + memory().flush(bytes(count), offset * sizeof(T)); + } - [[nodiscard]] constexpr bool created() const { return static_cast(_handle); } - [[nodiscard]] constexpr VkBuffer get() const { return _handle.get(); } - [[nodiscard]] constexpr Core const& core() const { return _handle.core(); } - [[nodiscard]] constexpr Config const& config() const { return _config; } + /** + * @brief makes device writes visible to the host + * @param count (VK_WHOLE_SIZE => to the end) + * @param offset in elements + * @details calls @ref Memory::invalidate() + */ + void invalidate(size_t count = VK_WHOLE_SIZE, size_t offset = 0) const { + memory().invalidate(bytes(count), offset * sizeof(T)); + } /** - * @return size in bytes + * @param count (VK_WHOLE_SIZE => to the end) + * @param offset in elements + * @pre offset + count <= @ref elements() + * @details calls @ref BaseBuffer::descriptorInfo() */ - [[nodiscard]] constexpr size_t size() const { return _size; } + [[nodiscard]] constexpr VkDescriptorBufferInfo descriptorInfo( + size_t count = VK_WHOLE_SIZE, + size_t offset = 0 + ) const { + return BaseBuffer::descriptorInfo(bytes(count), offset * sizeof(T)); + } /** - * @return the memory of the buffer, not created -> the buffer does not own its memory + * @brief copies the data to the mapped elements + * @pre data.size() <= mapped.size() */ - [[nodiscard]] constexpr Memory const& memory() const { return _memory; } + static constexpr void write(std::span data, std::span mapped) { + CTH_CRITICAL(data.size() > mapped.size(), "data must fit into the mapped range") {} + std::ranges::copy(data, mapped.begin()); + } + + [[nodiscard]] constexpr size_t elements() const { return size() / sizeof(T); } private: - void forget(); + /** + * @param count (VK_WHOLE_SIZE => VK_WHOLE_SIZE) + */ + [[nodiscard]] static constexpr size_t bytes(size_t count) { + return count == VK_WHOLE_SIZE ? VK_WHOLE_SIZE : count * sizeof(T); + } - // declared before the memory: move assignment destroys the old buffer before its memory - core_handle _handle; - Memory _memory; - Config _config; - size_t _size = 0; + /** + * @brief starts the lifetime of the elements in the mapped bytes, keeps their values + */ + [[nodiscard]] static std::span as_elements(std::span mapped) { + size_t const count = mapped.size() / sizeof(T); +#ifdef __cpp_lib_start_lifetime_as + return {std::start_lifetime_as_array(mapped.data(), count), count}; +#else + return {reinterpret_cast(mapped.data()), count}; +#endif + } public: Buffer(Buffer const& other) = delete; Buffer(Buffer&& other) noexcept = default; Buffer& operator=(Buffer const& other) = delete; Buffer& operator=(Buffer&& other) noexcept = default; - - static constexpr void debug_check(Buffer const&); }; } - -namespace cvk { -struct Buffer::State { - vk_not_null vkBuffer; - /** - * @brief in bytes - */ - size_t size; - /** - * @brief std::nullopt -> the buffer does not own its memory - */ - std::optional memory = std::nullopt; -}; -} - -namespace cvk { -constexpr VkDescriptorBufferInfo Buffer::descriptorInfo(size_t size, size_t offset) const { - debug_check(*this); - CTH_CRITICAL(offset > _size, "offset must lie in the buffer") {} - CTH_CRITICAL(size != VK_WHOLE_SIZE && size > _size - offset, "range must lie in the buffer") {} - - return VkDescriptorBufferInfo{ - .buffer = get(), - .offset = offset, - .range = size == VK_WHOLE_SIZE ? _size - offset : size, - }; -} -} - -namespace cvk { -constexpr void Buffer::debug_check(Buffer const& buffer) { - CTH_CRITICAL(!buffer.created(), "buffer must be created") {} -} -} diff --git a/cvk/src/data/buffer/buffer.cpp b/cvk/src/data/buffer/base_buffer.cpp similarity index 69% rename from cvk/src/data/buffer/buffer.cpp rename to cvk/src/data/buffer/base_buffer.cpp index 688c3b4b..01e2aaa6 100644 --- a/cvk/src/data/buffer/buffer.cpp +++ b/cvk/src/data/buffer/base_buffer.cpp @@ -1,4 +1,4 @@ -#include "cvk/data/buffer/buffer.hpp" +#include "cvk/data/buffer/base_buffer.hpp" #include "cvk/base/allocator.hpp" #include "cvk/base/core.hpp" @@ -6,17 +6,17 @@ namespace cvk { -Buffer::Buffer(Core const& core, Config const& config) : _handle{core}, _memory{core, config.memory}, +BaseBuffer::BaseBuffer(Core const& core, Config const& config) : _handle{core}, _memory{core, config.memory}, _config{config} { Core::debug_check(core); Config::debug_check(config); } -Buffer::Buffer(Core const& core, Config const& config, State const& state) : Buffer{core, config} { wrap(state); } +BaseBuffer::BaseBuffer(Core const& core, Config const& config, State const& state) : BaseBuffer{core, config} { wrap(state); } -Buffer::Buffer(Core const& core, Config const& config, size_t size) : Buffer{core, config} { create(size); } +BaseBuffer::BaseBuffer(Core const& core, Config const& config, size_t size) : BaseBuffer{core, config} { create(size); } -void Buffer::wrap(State const& state) { +void BaseBuffer::wrap(State const& state) { optDestroy(); _handle.reset(state.vkBuffer.get(), core()); @@ -24,7 +24,7 @@ void Buffer::wrap(State const& state) { _size = state.size; } -void Buffer::create(size_t size) { +void BaseBuffer::create(size_t size) { optDestroy(); auto const bufferInfo = _config.createInfo(size); @@ -51,7 +51,7 @@ void Buffer::create(size_t size) { _size = size; } -void Buffer::destroy() { +void BaseBuffer::destroy() { debug_check(*this); _handle.reset(); @@ -60,7 +60,7 @@ void Buffer::destroy() { forget(); } -Buffer::State Buffer::release() { +BaseBuffer::State BaseBuffer::release() { debug_check(*this); State state{.vkBuffer = _handle.release(), .size = _size, .memory = std::nullopt}; @@ -70,11 +70,11 @@ Buffer::State Buffer::release() { return state; } -void Buffer::destroy(DeviceTable table, VkBuffer vkBuffer) { +void BaseBuffer::destroy(DeviceTable table, VkBuffer vkBuffer) { CTH_WARN(vkBuffer == VK_NULL_HANDLE, "vkBuffer invalid (VK_NULL_HANDLE)") {} table->vkDestroyBuffer(table.device(), vkBuffer, nullptr); } -void Buffer::forget() { _size = 0; } +void BaseBuffer::forget() { _size = 0; } } diff --git a/cvk/tests/src/data/buffer/base_buffer_test.cpp b/cvk/tests/src/data/buffer/base_buffer_test.cpp new file mode 100644 index 00000000..e52c1025 --- /dev/null +++ b/cvk/tests/src/data/buffer/base_buffer_test.cpp @@ -0,0 +1,201 @@ +#include "context.hpp" +#include "test.hpp" + +#include "cvk/base/physical_device.hpp" +#include "cvk/data/buffer/base_buffer.hpp" + +#include +#include + + +namespace cvk { +namespace { + constexpr size_t SIZE = 256; + + /** + * @brief staging buffer, the cpu writes it once + */ + constexpr BaseBuffer::Config STAGING_CONFIG{ + .usage = VK_BUFFER_USAGE_TRANSFER_SRC_BIT, + .memory = {.flags = VMA_ALLOCATION_CREATE_HOST_ACCESS_SEQUENTIAL_WRITE_BIT}, + }; + + /** + * @brief buffer the cpu reads back + */ + constexpr BaseBuffer::Config READBACK_CONFIG{ + .usage = VK_BUFFER_USAGE_TRANSFER_DST_BIT, + .memory = {.flags = VMA_ALLOCATION_CREATE_HOST_ACCESS_RANDOM_BIT}, + }; + + /** + * @brief gpu only buffer + */ + constexpr BaseBuffer::Config DEVICE_CONFIG{.usage = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT}; +} + +CVK_TEST(BaseBuffer, config_create_info) { + constexpr auto info = STAGING_CONFIG.createInfo(SIZE); + + static_assert(info.size == SIZE); + static_assert(info.usage == VK_BUFFER_USAGE_TRANSFER_SRC_BIT); + static_assert(info.flags == 0); + static_assert(info.sharingMode == VK_SHARING_MODE_EXCLUSIVE); + static_assert(info.queueFamilyIndexCount == 0 && info.pQueueFamilyIndices == nullptr); + static_assert(STAGING_CONFIG.memory.autoUsage()); +} + +CVK_TEST(BaseBuffer, config_one_queue_family_is_exclusive) { + static_assert([] { + BaseBuffer::Config config{.usage = VK_BUFFER_USAGE_TRANSFER_SRC_BIT, .queueFamilies = {2}}; + auto const info = config.createInfo(SIZE); + return info.sharingMode == VK_SHARING_MODE_EXCLUSIVE && info.queueFamilyIndexCount == 1; + }()); +} + +CVK_TEST(BaseBuffer, config_queue_families_are_concurrent) { + static_assert([] { + BaseBuffer::Config config{ + .usage = VK_BUFFER_USAGE_TRANSFER_SRC_BIT, + .flags = VK_BUFFER_CREATE_PROTECTED_BIT, + .queueFamilies = {0, 2}, + }; + auto const info = config.createInfo(SIZE); + return info.sharingMode == VK_SHARING_MODE_CONCURRENT && info.queueFamilyIndexCount == 2 + && info.pQueueFamilyIndices == config.queueFamilies.data() && info.flags == VK_BUFFER_CREATE_PROTECTED_BIT; + }()); +} + +CVK_TEST(BaseBuffer, config_debug_check_is_constexpr) { + static_assert([] { + BaseBuffer::Config::debug_check({.usage = VK_BUFFER_USAGE_TRANSFER_SRC_BIT, .queueFamilies = {1, 0}}); + return true; + }()); +} + +CVK_TEST(BaseBuffer, base_constructor_does_not_create) { + test::Context const context{}; + + BaseBuffer const buffer{context.core, STAGING_CONFIG}; + + EXPECT_FALSE(buffer.created()); + EXPECT_FALSE(buffer.memory().created()); +} + +CVK_TEST(BaseBuffer, create_and_destroy) { + test::Context const context{}; + BaseBuffer buffer{context.core, DEVICE_CONFIG, SIZE}; + + ASSERT_TRUE(buffer.created()); + EXPECT_EQ(&buffer.core(), &context.core); + EXPECT_EQ(buffer.size(), SIZE); + ASSERT_TRUE(buffer.memory().created()); + EXPECT_GE(buffer.memory().size(), SIZE); + + buffer.destroy(); + EXPECT_FALSE(buffer.created()); + EXPECT_FALSE(buffer.memory().created()); + EXPECT_EQ(buffer.size(), 0); +} + +CVK_TEST(BaseBuffer, create_concurrent) { + test::Context const context{}; + auto const familyCount = context.core.physicalDevice().queueFamilies().size(); + if(familyCount < 2) GTEST_SKIP() << "needs 2 queue families"; + + auto config = DEVICE_CONFIG; + config.queueFamilies = {0, 1}; + + BaseBuffer const buffer{context.core, config, SIZE}; + + EXPECT_TRUE(buffer.created()); +} + +CVK_TEST(BaseBuffer, auto_usage_picks_host_visible_memory_for_host_access) { + test::Context const context{}; + + BaseBuffer const buffer{context.core, STAGING_CONFIG, SIZE}; + + EXPECT_NE(buffer.memory().properties() & VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT, 0u); +} + +CVK_TEST(BaseBuffer, write_and_read_through_the_memory) { + test::Context const context{}; + BaseBuffer const buffer{context.core, READBACK_CONFIG, SIZE}; + std::array data{}; + std::ranges::fill(data, 'x'); + + buffer.memory().write(data); + + auto const mapped = buffer.memory().map(SIZE); + buffer.memory().invalidate(); + EXPECT_TRUE(std::ranges::equal(mapped, data)); + buffer.memory().unmap(); +} + +CVK_TEST(BaseBuffer, descriptor_info) { + test::Context const context{}; + BaseBuffer const buffer{context.core, DEVICE_CONFIG, SIZE}; + + auto const whole = buffer.descriptorInfo(); + EXPECT_EQ(whole.buffer, buffer.get()); + EXPECT_EQ(whole.offset, 0); + EXPECT_EQ(whole.range, SIZE); + + auto const end = buffer.descriptorInfo(VK_WHOLE_SIZE, 64); + EXPECT_EQ(end.offset, 64); + EXPECT_EQ(end.range, SIZE - 64); + + auto const part = buffer.descriptorInfo(16, 32); + EXPECT_EQ(part.range, 16); +} + +CVK_TEST(BaseBuffer, release_and_wrap) { + test::Context const context{}; + BaseBuffer buffer{context.core, DEVICE_CONFIG, SIZE}; + auto const vkBuffer = buffer.get(); + auto const vmaAllocation = buffer.memory().get(); + + auto const state = buffer.release(); + EXPECT_FALSE(buffer.created()); + EXPECT_FALSE(buffer.memory().created()); + ASSERT_TRUE(state.memory.has_value()); + + BaseBuffer const wrapped{context.core, DEVICE_CONFIG, state}; + EXPECT_EQ(wrapped.get(), vkBuffer); + EXPECT_EQ(wrapped.memory().get(), vmaAllocation); + EXPECT_EQ(wrapped.size(), SIZE); +} + +CVK_TEST(BaseBuffer, wrap_without_memory_destroys_only_the_buffer) { + test::Context const context{}; + BaseBuffer buffer{context.core, DEVICE_CONFIG, SIZE}; + auto state = buffer.release(); + + // another owner frees the memory, after the buffer + Memory const memory{context.core, DEVICE_CONFIG.memory, *state.memory}; + state.memory = std::nullopt; + + { + BaseBuffer const wrapped{context.core, DEVICE_CONFIG, state}; + EXPECT_FALSE(wrapped.memory().created()); + EXPECT_EQ(wrapped.size(), SIZE); + } + + EXPECT_TRUE(memory.created()); +} + +CVK_TEST(BaseBuffer, move_assignment_destroys_the_old_buffer) { + test::Context const context{}; + BaseBuffer buffer{context.core, DEVICE_CONFIG, SIZE}; + BaseBuffer other{context.core, DEVICE_CONFIG, SIZE}; + auto const vkBuffer = other.get(); + + buffer = std::move(other); + + EXPECT_EQ(buffer.get(), vkBuffer); + EXPECT_TRUE(buffer.memory().created()); + EXPECT_FALSE(other.created()); + EXPECT_FALSE(other.memory().created()); +} +} diff --git a/cvk/tests/src/data/buffer/buffer_test.cpp b/cvk/tests/src/data/buffer/buffer_test.cpp index 301db4f7..9e1181a9 100644 --- a/cvk/tests/src/data/buffer/buffer_test.cpp +++ b/cvk/tests/src/data/buffer/buffer_test.cpp @@ -1,201 +1,146 @@ #include "context.hpp" #include "test.hpp" -#include "cvk/base/physical_device.hpp" #include "cvk/data/buffer/buffer.hpp" #include #include +#include namespace cvk { namespace { - constexpr size_t SIZE = 256; + struct Vertex { + float x, y, z; + uint32_t color; - /** - * @brief staging buffer, the cpu writes it once - */ - constexpr Buffer::Config STAGING_CONFIG{ - .usage = VK_BUFFER_USAGE_TRANSFER_SRC_BIT, - .memory = {.flags = VMA_ALLOCATION_CREATE_HOST_ACCESS_SEQUENTIAL_WRITE_BIT}, + friend constexpr bool operator==(Vertex const&, Vertex const&) = default; + }; + + constexpr size_t COUNT = 8; + + constexpr std::array TRIANGLE{ + Vertex{0.f, 1.f, 0.f, 0xff0000ff}, + Vertex{1.f, 0.f, 0.f, 0x00ff00ff}, + Vertex{0.f, 0.f, 1.f, 0x0000ffff}, }; /** - * @brief buffer the cpu reads back + * @brief the cpu writes and reads */ - constexpr Buffer::Config READBACK_CONFIG{ - .usage = VK_BUFFER_USAGE_TRANSFER_DST_BIT, + constexpr Buffer::Config HOST_CONFIG{ + .usage = VK_BUFFER_USAGE_VERTEX_BUFFER_BIT, .memory = {.flags = VMA_ALLOCATION_CREATE_HOST_ACCESS_RANDOM_BIT}, }; /** - * @brief gpu only buffer + * @brief Buffer is a valid type */ - constexpr Buffer::Config DEVICE_CONFIG{.usage = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT}; -} + template + concept valid_buffer = requires { sizeof(Buffer); }; -CVK_TEST(Buffer, config_create_info) { - constexpr auto info = STAGING_CONFIG.createInfo(SIZE); - - static_assert(info.size == SIZE); - static_assert(info.usage == VK_BUFFER_USAGE_TRANSFER_SRC_BIT); - static_assert(info.flags == 0); - static_assert(info.sharingMode == VK_SHARING_MODE_EXCLUSIVE); - static_assert(info.queueFamilyIndexCount == 0 && info.pQueueFamilyIndices == nullptr); - static_assert(STAGING_CONFIG.memory.autoUsage()); + void take_base(BaseBuffer const& buffer, size_t size) { EXPECT_EQ(buffer.size(), size); } } -CVK_TEST(Buffer, config_one_queue_family_is_exclusive) { - static_assert([] { - Buffer::Config config{.usage = VK_BUFFER_USAGE_TRANSFER_SRC_BIT, .queueFamilies = {2}}; - auto const info = config.createInfo(SIZE); - return info.sharingMode == VK_SHARING_MODE_EXCLUSIVE && info.queueFamilyIndexCount == 1; - }()); +CVK_TEST(Buffer, elements_must_be_trivially_copyable) { + static_assert(valid_buffer); + static_assert(valid_buffer); + static_assert(!valid_buffer); } -CVK_TEST(Buffer, config_queue_families_are_concurrent) { - static_assert([] { - Buffer::Config config{ - .usage = VK_BUFFER_USAGE_TRANSFER_SRC_BIT, - .flags = VK_BUFFER_CREATE_PROTECTED_BIT, - .queueFamilies = {0, 2}, - }; - auto const info = config.createInfo(SIZE); - return info.sharingMode == VK_SHARING_MODE_CONCURRENT && info.queueFamilyIndexCount == 2 - && info.pQueueFamilyIndices == config.queueFamilies.data() && info.flags == VK_BUFFER_CREATE_PROTECTED_BIT; - }()); -} +CVK_TEST(Buffer, adds_no_members) { static_assert(sizeof(Buffer) == sizeof(BaseBuffer)); } -CVK_TEST(Buffer, config_debug_check_is_constexpr) { +CVK_TEST(Buffer, write_to_mapped_elements_is_constexpr) { static_assert([] { - Buffer::Config::debug_check({.usage = VK_BUFFER_USAGE_TRANSFER_SRC_BIT, .queueFamilies = {1, 0}}); - return true; + std::array mapped{}; + Buffer::write(TRIANGLE, mapped); + return mapped[2] == TRIANGLE[2] && mapped[3] == Vertex{}; }()); } -CVK_TEST(Buffer, base_constructor_does_not_create) { +CVK_TEST(Buffer, create_counts_elements) { test::Context const context{}; - Buffer const buffer{context.core, STAGING_CONFIG}; - - EXPECT_FALSE(buffer.created()); - EXPECT_FALSE(buffer.memory().created()); -} - -CVK_TEST(Buffer, create_and_destroy) { - test::Context const context{}; - Buffer buffer{context.core, DEVICE_CONFIG, SIZE}; + Buffer const buffer{context.core, HOST_CONFIG, COUNT}; ASSERT_TRUE(buffer.created()); - EXPECT_EQ(&buffer.core(), &context.core); - EXPECT_EQ(buffer.size(), SIZE); - ASSERT_TRUE(buffer.memory().created()); - EXPECT_GE(buffer.memory().size(), SIZE); - - buffer.destroy(); - EXPECT_FALSE(buffer.created()); - EXPECT_FALSE(buffer.memory().created()); - EXPECT_EQ(buffer.size(), 0); + EXPECT_EQ(buffer.elements(), COUNT); + EXPECT_EQ(buffer.size(), COUNT * sizeof(Vertex)); + take_base(buffer, COUNT * sizeof(Vertex)); } -CVK_TEST(Buffer, create_concurrent) { +CVK_TEST(Buffer, write_and_map) { test::Context const context{}; - auto const familyCount = context.core.physicalDevice().queueFamilies().size(); - if(familyCount < 2) GTEST_SKIP() << "needs 2 queue families"; + Buffer const buffer{context.core, HOST_CONFIG, COUNT}; - auto config = DEVICE_CONFIG; - config.queueFamilies = {0, 1}; + buffer.write(TRIANGLE, 2); - Buffer const buffer{context.core, config, SIZE}; - - EXPECT_TRUE(buffer.created()); + auto const mapped = buffer.map(TRIANGLE.size(), 2); + buffer.invalidate(); + EXPECT_TRUE(std::ranges::equal(mapped, TRIANGLE)); + buffer.unmap(); } -CVK_TEST(Buffer, auto_usage_picks_host_visible_memory_for_host_access) { +CVK_TEST(Buffer, map_whole_buffer) { test::Context const context{}; + Buffer const buffer{context.core, HOST_CONFIG, COUNT}; - Buffer const buffer{context.core, STAGING_CONFIG, SIZE}; - - EXPECT_NE(buffer.memory().properties() & VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT, 0u); -} + auto const whole = buffer.map(); + EXPECT_EQ(whole.size(), COUNT); -CVK_TEST(Buffer, write_and_read_through_the_memory) { - test::Context const context{}; - Buffer const buffer{context.core, READBACK_CONFIG, SIZE}; - std::array data{}; - std::ranges::fill(data, 'x'); + Buffer::write(TRIANGLE, whole); + buffer.flush(); + buffer.flush(1, 2); - buffer.memory().write(data); + auto const end = buffer.map(VK_WHOLE_SIZE, 5); + EXPECT_EQ(end.size(), COUNT - 5); + EXPECT_EQ(end.data(), whole.data() + 5); - auto const mapped = buffer.memory().map(SIZE); - buffer.memory().invalidate(); - EXPECT_TRUE(std::ranges::equal(mapped, data)); - buffer.memory().unmap(); + buffer.unmap(); + buffer.unmap(); } -CVK_TEST(Buffer, descriptor_info) { +CVK_TEST(Buffer, descriptor_info_in_elements) { test::Context const context{}; - Buffer const buffer{context.core, DEVICE_CONFIG, SIZE}; + Buffer const buffer{context.core, HOST_CONFIG, COUNT}; auto const whole = buffer.descriptorInfo(); - EXPECT_EQ(whole.buffer, buffer.get()); EXPECT_EQ(whole.offset, 0); - EXPECT_EQ(whole.range, SIZE); + EXPECT_EQ(whole.range, COUNT * sizeof(Vertex)); - auto const end = buffer.descriptorInfo(VK_WHOLE_SIZE, 64); - EXPECT_EQ(end.offset, 64); - EXPECT_EQ(end.range, SIZE - 64); + auto const end = buffer.descriptorInfo(VK_WHOLE_SIZE, 3); + EXPECT_EQ(end.offset, 3 * sizeof(Vertex)); + EXPECT_EQ(end.range, (COUNT - 3) * sizeof(Vertex)); - auto const part = buffer.descriptorInfo(16, 32); - EXPECT_EQ(part.range, 16); + auto const part = buffer.descriptorInfo(2, 1); + EXPECT_EQ(part.offset, sizeof(Vertex)); + EXPECT_EQ(part.range, 2 * sizeof(Vertex)); } -CVK_TEST(Buffer, release_and_wrap) { +CVK_TEST(Buffer, release_and_wrap_as_bytes) { test::Context const context{}; - Buffer buffer{context.core, DEVICE_CONFIG, SIZE}; + Buffer buffer{context.core, HOST_CONFIG, COUNT}; + buffer.write(TRIANGLE); auto const vkBuffer = buffer.get(); - auto const vmaAllocation = buffer.memory().get(); auto const state = buffer.release(); EXPECT_FALSE(buffer.created()); - EXPECT_FALSE(buffer.memory().created()); - ASSERT_TRUE(state.memory.has_value()); - - Buffer const wrapped{context.core, DEVICE_CONFIG, state}; - EXPECT_EQ(wrapped.get(), vkBuffer); - EXPECT_EQ(wrapped.memory().get(), vmaAllocation); - EXPECT_EQ(wrapped.size(), SIZE); -} - -CVK_TEST(Buffer, wrap_without_memory_destroys_only_the_buffer) { - test::Context const context{}; - Buffer buffer{context.core, DEVICE_CONFIG, SIZE}; - auto state = buffer.release(); - - // another owner frees the memory, after the buffer - Memory const memory{context.core, DEVICE_CONFIG.memory, *state.memory}; - state.memory = std::nullopt; - - { - Buffer const wrapped{context.core, DEVICE_CONFIG, state}; - EXPECT_FALSE(wrapped.memory().created()); - EXPECT_EQ(wrapped.size(), SIZE); - } - EXPECT_TRUE(memory.created()); + Buffer const bytes{context.core, HOST_CONFIG, state}; + EXPECT_EQ(bytes.get(), vkBuffer); + EXPECT_EQ(bytes.elements(), COUNT * sizeof(Vertex)); } CVK_TEST(Buffer, move_assignment_destroys_the_old_buffer) { test::Context const context{}; - Buffer buffer{context.core, DEVICE_CONFIG, SIZE}; - Buffer other{context.core, DEVICE_CONFIG, SIZE}; + Buffer buffer{context.core, HOST_CONFIG, COUNT}; + Buffer other{context.core, HOST_CONFIG, 2 * COUNT}; auto const vkBuffer = other.get(); buffer = std::move(other); EXPECT_EQ(buffer.get(), vkBuffer); - EXPECT_TRUE(buffer.memory().created()); + EXPECT_EQ(buffer.elements(), 2 * COUNT); EXPECT_FALSE(other.created()); - EXPECT_FALSE(other.memory().created()); } } From 00e50e88f49671c011cd8f0c4f1f24ae56ce0858 Mon Sep 17 00:00:00 2001 From: lukas <89145039+codethinki@users.noreply.github.com> Date: Sat, 17 Oct 2026 00:54:51 +0200 Subject: [PATCH 042/135] cvk: Buffer uses std::start_lifetime_as_array without fallback MSVC supports it now, codethinki/cth#15 checks that in the Windows CI. --- cvk/incl/cvk/data/buffer/buffer.hpp | 4 ---- 1 file changed, 4 deletions(-) diff --git a/cvk/incl/cvk/data/buffer/buffer.hpp b/cvk/incl/cvk/data/buffer/buffer.hpp index 10c83f92..21308773 100644 --- a/cvk/incl/cvk/data/buffer/buffer.hpp +++ b/cvk/incl/cvk/data/buffer/buffer.hpp @@ -151,11 +151,7 @@ class Buffer : public BaseBuffer { */ [[nodiscard]] static std::span as_elements(std::span mapped) { size_t const count = mapped.size() / sizeof(T); -#ifdef __cpp_lib_start_lifetime_as return {std::start_lifetime_as_array(mapped.data(), count), count}; -#else - return {reinterpret_cast(mapped.data()), count}; -#endif } public: From fd38c17042de15782f12f88e7da4bc73e0c76b82 Mon Sep 17 00:00:00 2001 From: lukas <89145039+codethinki@users.noreply.github.com> Date: Sat, 17 Oct 2026 01:02:44 +0200 Subject: [PATCH 043/135] cvk: Buffer falls back to reinterpret_cast without start_lifetime_as The MSVC STL declares start_lifetime_as only for the MSVC compiler, clang on Windows lacks the builtin (microsoft/STL GH-6169). --- cvk/incl/cvk/data/buffer/buffer.hpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/cvk/incl/cvk/data/buffer/buffer.hpp b/cvk/incl/cvk/data/buffer/buffer.hpp index 21308773..ecf8655f 100644 --- a/cvk/incl/cvk/data/buffer/buffer.hpp +++ b/cvk/incl/cvk/data/buffer/buffer.hpp @@ -151,7 +151,12 @@ class Buffer : public BaseBuffer { */ [[nodiscard]] static std::span as_elements(std::span mapped) { size_t const count = mapped.size() / sizeof(T); +#ifdef __cpp_lib_start_lifetime_as return {std::start_lifetime_as_array(mapped.data(), count), count}; +#else + //TODO remove when the MSVC STL has start_lifetime_as on clang (microsoft/STL GH-6169) + return {reinterpret_cast(mapped.data()), count}; +#endif } public: From 17259cf3ac323c9bc05aedd973a74937f4ea9bae Mon Sep 17 00:00:00 2001 From: lukas <89145039+codethinki@users.noreply.github.com> Date: Sat, 17 Oct 2026 01:08:25 +0200 Subject: [PATCH 044/135] cvk: port image Image owns the VkImage and a Memory member, vmaCreateImage creates both, images without memory (swapchain) are wrapped with std::nullopt memory. Tracks the layout of each mip level like legacy. Config like the buffer config: flags and queue families. evalMipLevelCount is constexpr (std::bit_width). Fixed against legacy: destroy() without memory dereferenced nullptr, wrap() looped forever with more level layouts than mip levels, the aspect mask could not hold depth and stencil. copy() and transitionLayout() come with the command buffer and the barriers. --- cvk/incl/cvk/data/img/image.hpp | 169 +++++++++++++++++++++++++ cvk/incl/cvk/data/img/image_config.hpp | 118 +++++++++++++++++ cvk/src/data/img/image.cpp | 95 ++++++++++++++ cvk/tests/src/data/img/image_test.cpp | 163 ++++++++++++++++++++++++ 4 files changed, 545 insertions(+) create mode 100644 cvk/incl/cvk/data/img/image.hpp create mode 100644 cvk/incl/cvk/data/img/image_config.hpp create mode 100644 cvk/src/data/img/image.cpp create mode 100644 cvk/tests/src/data/img/image_test.cpp diff --git a/cvk/incl/cvk/data/img/image.hpp b/cvk/incl/cvk/data/img/image.hpp new file mode 100644 index 00000000..5bb377fe --- /dev/null +++ b/cvk/incl/cvk/data/img/image.hpp @@ -0,0 +1,169 @@ +#pragma once +#include "image_config.hpp" + +#include "cvk/base/device_table.hpp" +#include "cvk/data/memory/memory.hpp" +#include "cvk/utility/core_handle.hpp" +#include "cvk/utility/types.hpp" + +#include + +#include + +#include +#include +#include +#include +#include + + +namespace cvk { +class Core; + +/** + * @brief VkImage wrapper, owns the image and its memory, tracks the layout of each mip level + * @details @ref create() creates both with vmaCreateImage(), + * swapchain images are wrapped without memory + */ +class Image { +public: + using Config = ImageConfig; + struct State; + + /** + * @brief base constructor + * @pre @ref Core::created() + */ + Image(Core const&, Config const&); + + /** + * @brief constructs and wraps + * @details calls: + * - @ref Image(Core const&, Config const&) + * - @ref wrap() + */ + Image(Core const&, Config const&, State const&); + + /** + * @brief constructs and creates + * @details calls: + * - @ref Image(Core const&, Config const&) + * - @ref create() + */ + Image(Core const&, Config const&, VkExtent2D extent); + + /** + * @note calls @ref optDestroy() + */ + ~Image() { optDestroy(); } + + /** + * @brief wraps the state + * @details std::nullopt @ref State::memory -> @ref destroy() only destroys the image + * @pre the image is bound to its memory + * @pre the image was created with the config + * @pre @ref State::levelLayouts size <= @ref Config::mipLevels + * @post @ref created() + * @note calls @ref optDestroy() + */ + void wrap(State const&); + + /** + * @brief creates the image and its memory + * @post @ref created() + * @post every mip level has @ref Config::initialLayout + * @note calls @ref optDestroy() + * @throws cvk::vk_result_exception result of vmaCreateImage() + */ + void create(VkExtent2D extent); + + /** + * @brief destroys the image, then its memory, and forgets + * @pre @ref created() + * @pre the gpu does not use the image + */ + void destroy(); + + /** + * @brief if @ref created() -> calls @ref destroy() + */ + void optDestroy() { if(created()) destroy(); } + + /** + * @brief releases ownership of the image and its memory, returns the state and forgets + * @pre @ref created() + */ + State release(); + + /** + * @return mip levels down to 1x1 + * @pre width > 0, height > 0 + */ + [[nodiscard]] static constexpr uint32_t evalMipLevelCount(VkExtent2D extent) { + return static_cast(std::bit_width(std::max(extent.width, extent.height))); + } + + static void destroy(DeviceTable table, VkImage); + + [[nodiscard]] constexpr bool created() const { return static_cast(_handle); } + [[nodiscard]] constexpr VkImage get() const { return _handle.get(); } + [[nodiscard]] constexpr Core const& core() const { return _handle.core(); } + [[nodiscard]] constexpr Config const& config() const { return _config; } + + [[nodiscard]] constexpr VkExtent2D extent() const { return _extent; } + [[nodiscard]] constexpr VkFormat format() const { return _config.format; } + [[nodiscard]] constexpr VkImageAspectFlags aspectMask() const { return _config.aspectMask; } + [[nodiscard]] constexpr uint32_t mipLevels() const { return _config.mipLevels; } + + /** + * @pre mipLevel < @ref mipLevels() + */ + [[nodiscard]] constexpr VkImageLayout layout(uint32_t mipLevel) const { return _levelLayouts[mipLevel]; } + [[nodiscard]] constexpr std::span layouts() const { return _levelLayouts; } + + /** + * @return the memory of the image, not created -> the image does not own its memory + */ + [[nodiscard]] constexpr Memory const& memory() const { return _memory; } + +private: + void forget(); + + // declared before the memory: move assignment destroys the old image before its memory + core_handle _handle; + Memory _memory; + Config _config; + VkExtent2D _extent{}; + std::vector _levelLayouts; + +public: + Image(Image const& other) = delete; + Image(Image&& other) noexcept = default; + Image& operator=(Image const& other) = delete; + Image& operator=(Image&& other) noexcept = default; + + static constexpr void debug_check(Image const&); +}; +} + +namespace cvk { +struct Image::State { + vk_not_null vkImage; + VkExtent2D extent; + /** + * @brief std::nullopt -> the image does not own its memory, e.g. swapchain images + */ + std::optional memory = std::nullopt; + /** + * @brief current layout of each mip level + * @details size < @ref Config::mipLevels -> the other levels have @ref Config::initialLayout + */ + std::vector levelLayouts{}; +}; +} + +namespace cvk { +constexpr void Image::debug_check(Image const& image) { + CTH_CRITICAL(!image.created(), "image must be created") {} +} +} diff --git a/cvk/incl/cvk/data/img/image_config.hpp b/cvk/incl/cvk/data/img/image_config.hpp new file mode 100644 index 00000000..042e39f2 --- /dev/null +++ b/cvk/incl/cvk/data/img/image_config.hpp @@ -0,0 +1,118 @@ +#pragma once +#include "cvk/base/queue/queue_family.hpp" +#include "cvk/data/memory/memory_config.hpp" + +#include + +#include + +#include +#include + + +namespace cvk { +/** + * @brief 2D image: format, usage, mip levels, who uses it and where its memory lives + */ +struct ImageConfig { + /** + * @pre != 0 + */ + VkImageAspectFlags aspectMask = 0; + + /** + * @pre != VK_FORMAT_UNDEFINED + */ + VkFormat format = VK_FORMAT_UNDEFINED; + + /** + * @pre != 0 + */ + VkImageUsageFlags usage = 0; + + /** + * @brief cube compatible, mutable format, sparse, ... + */ + VkImageCreateFlags flags = 0; + + VkImageTiling tiling = VK_IMAGE_TILING_OPTIMAL; + + /** + * @pre >= 1 + */ + uint32_t mipLevels = 1; + + VkSampleCountFlagBits samples = VK_SAMPLE_COUNT_1_BIT; + + /** + * @pre VK_IMAGE_LAYOUT_UNDEFINED or VK_IMAGE_LAYOUT_PREINITIALIZED + */ + VkImageLayout initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; + + /** + * @brief queue families that use the image at the same time + * @details size < 2 -> VK_SHARING_MODE_EXCLUSIVE, else VK_SHARING_MODE_CONCURRENT + * @pre unique + */ + std::vector queueFamilies{}; + + /** + * @brief memory of the image + */ + MemoryConfig memory{}; + + /** + * @brief depth attachment, depth aspect only + */ + [[nodiscard]] static constexpr ImageConfig DepthBuffer(VkFormat depthFormat, VkSampleCountFlagBits samples) { + return ImageConfig{ + .aspectMask = VK_IMAGE_ASPECT_DEPTH_BIT, + .format = depthFormat, + .usage = VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT, + .samples = samples, + }; + } + + /** + * @attention pQueueFamilyIndices points into @ref queueFamilies + */ + [[nodiscard]] constexpr VkImageCreateInfo createInfo(VkExtent2D extent) const { + return VkImageCreateInfo{ + .sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO, + .pNext = nullptr, + .flags = flags, + .imageType = VK_IMAGE_TYPE_2D, + .format = format, + .extent = {extent.width, extent.height, 1}, + .mipLevels = mipLevels, + .arrayLayers = 1, + .samples = samples, + .tiling = tiling, + .usage = usage, + .sharingMode = queueFamilies.size() >= 2 ? VK_SHARING_MODE_CONCURRENT : VK_SHARING_MODE_EXCLUSIVE, + .queueFamilyIndexCount = static_cast(queueFamilies.size()), + .pQueueFamilyIndices = queueFamilies.data(), + .initialLayout = initialLayout, + }; + } + + static constexpr void debug_check(ImageConfig const&); +}; +} + +namespace cvk { +constexpr void ImageConfig::debug_check(ImageConfig const& config) { + CTH_CRITICAL(config.aspectMask == 0, "aspect mask must not be 0") {} + CTH_CRITICAL(config.format == VK_FORMAT_UNDEFINED, "format must not be undefined") {} + CTH_CRITICAL(config.usage == 0, "usage must not be 0") {} + CTH_CRITICAL(config.mipLevels == 0, "at least one mip level is required") {} + CTH_CRITICAL( + config.initialLayout != VK_IMAGE_LAYOUT_UNDEFINED && config.initialLayout != VK_IMAGE_LAYOUT_PREINITIALIZED, + "initial layout must be undefined or preinitialized" + ) {} + + auto families = config.queueFamilies; + std::ranges::sort(families); + CTH_CRITICAL(std::ranges::adjacent_find(families) != families.end(), "queue families must be unique") {} +} +} diff --git a/cvk/src/data/img/image.cpp b/cvk/src/data/img/image.cpp new file mode 100644 index 00000000..ff906935 --- /dev/null +++ b/cvk/src/data/img/image.cpp @@ -0,0 +1,95 @@ +#include "cvk/data/img/image.hpp" + +#include "cvk/base/allocator.hpp" +#include "cvk/base/core.hpp" +#include "cvk/utility/vk_exceptions.hpp" + +#include + + +namespace cvk { +Image::Image(Core const& core, Config const& config) : _handle{core}, _memory{core, config.memory}, + _config{config}, _levelLayouts(config.mipLevels, config.initialLayout) { + Core::debug_check(core); + Config::debug_check(config); +} + +Image::Image(Core const& core, Config const& config, State const& state) : Image{core, config} { wrap(state); } + +Image::Image(Core const& core, Config const& config, VkExtent2D extent) : Image{core, config} { create(extent); } + +void Image::wrap(State const& state) { + CTH_CRITICAL(state.levelLayouts.size() > _config.mipLevels, "more level layouts than mip levels") {} + + optDestroy(); + + _handle.reset(state.vkImage.get(), core()); + if(state.memory.has_value()) _memory.wrap(*state.memory); + _extent = state.extent; + _levelLayouts.assign(_config.mipLevels, _config.initialLayout); + std::ranges::copy(state.levelLayouts, _levelLayouts.begin()); +} + +void Image::create(VkExtent2D extent) { + optDestroy(); + + auto const imageInfo = _config.createInfo(extent); + auto const allocationInfo = _memory.config().createInfo(); + + VkImage vkImage = VK_NULL_HANDLE; + VmaAllocation vmaAllocation = VK_NULL_HANDLE; + auto const result = vmaCreateImage( + core().allocator().get(), + &imageInfo, + &allocationInfo, + &vkImage, + &vmaAllocation, + nullptr + ); + + CTH_STABLE_ERR(result != VK_SUCCESS, "failed to create image") { + forget(); + throw cvk::vk_result_exception{result, details->exception()}; + } + + _handle.reset(vkImage, core()); + _memory.wrap(Memory::State{vmaAllocation}); + _extent = extent; + _levelLayouts.assign(_config.mipLevels, _config.initialLayout); +} + +void Image::destroy() { + debug_check(*this); + + _handle.reset(); + _memory.optDestroy(); + + forget(); +} + +Image::State Image::release() { + debug_check(*this); + + State state{ + .vkImage = _handle.release(), + .extent = _extent, + .memory = std::nullopt, + .levelLayouts = std::move(_levelLayouts), + }; + if(_memory.created()) state.memory = _memory.release(); + + forget(); + return state; +} + +void Image::destroy(DeviceTable table, VkImage vkImage) { + CTH_WARN(vkImage == VK_NULL_HANDLE, "vkImage invalid (VK_NULL_HANDLE)") {} + + table->vkDestroyImage(table.device(), vkImage, nullptr); +} + +void Image::forget() { + _extent = {}; + _levelLayouts.assign(_config.mipLevels, _config.initialLayout); +} +} diff --git a/cvk/tests/src/data/img/image_test.cpp b/cvk/tests/src/data/img/image_test.cpp new file mode 100644 index 00000000..2e9c48ba --- /dev/null +++ b/cvk/tests/src/data/img/image_test.cpp @@ -0,0 +1,163 @@ +#include "context.hpp" +#include "test.hpp" + +#include "cvk/data/img/image.hpp" + +#include + + +namespace cvk { +namespace { + constexpr VkExtent2D EXTENT{64, 32}; + + /** + * @brief sampled texture with a full mip chain + */ + constexpr Image::Config TEXTURE_CONFIG{ + .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, + .format = VK_FORMAT_R8G8B8A8_UNORM, + .usage = VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT, + .mipLevels = Image::evalMipLevelCount(EXTENT), + }; + + bool all_layouts(Image const& image, VkImageLayout layout) { + return std::ranges::all_of(image.layouts(), [layout](VkImageLayout l) { return l == layout; }); + } +} + +CVK_TEST(Image, eval_mip_level_count) { + static_assert(Image::evalMipLevelCount({1, 1}) == 1); + static_assert(Image::evalMipLevelCount({2, 1}) == 2); + static_assert(Image::evalMipLevelCount({256, 256}) == 9); + static_assert(Image::evalMipLevelCount({300, 200}) == 9); + static_assert(Image::evalMipLevelCount({1, 1024}) == 11); +} + +CVK_TEST(Image, config_create_info) { + constexpr auto info = TEXTURE_CONFIG.createInfo(EXTENT); + + static_assert(info.imageType == VK_IMAGE_TYPE_2D); + static_assert(info.extent.width == EXTENT.width && info.extent.height == EXTENT.height && info.extent.depth == 1); + static_assert(info.mipLevels == 7); + static_assert(info.arrayLayers == 1); + static_assert(info.sharingMode == VK_SHARING_MODE_EXCLUSIVE); + static_assert(info.initialLayout == VK_IMAGE_LAYOUT_UNDEFINED); +} + +CVK_TEST(Image, config_queue_families_are_concurrent) { + static_assert([] { + auto config = TEXTURE_CONFIG; + config.queueFamilies = {0, 1}; + auto const info = config.createInfo(EXTENT); + return info.sharingMode == VK_SHARING_MODE_CONCURRENT && info.queueFamilyIndexCount == 2; + }()); +} + +CVK_TEST(Image, config_depth_buffer) { + constexpr auto config = Image::Config::DepthBuffer(VK_FORMAT_D16_UNORM, VK_SAMPLE_COUNT_1_BIT); + + static_assert(config.aspectMask == VK_IMAGE_ASPECT_DEPTH_BIT); + static_assert(config.usage == VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT); + static_assert(config.memory.autoUsage()); + static_assert((Image::Config::debug_check(config), true)); +} + +CVK_TEST(Image, base_constructor_does_not_create) { + test::Context const context{}; + + Image const image{context.core, TEXTURE_CONFIG}; + + EXPECT_FALSE(image.created()); + EXPECT_FALSE(image.memory().created()); + EXPECT_EQ(image.layouts().size(), TEXTURE_CONFIG.mipLevels); + EXPECT_TRUE(all_layouts(image, VK_IMAGE_LAYOUT_UNDEFINED)); +} + +CVK_TEST(Image, create_and_destroy) { + test::Context const context{}; + Image image{context.core, TEXTURE_CONFIG, EXTENT}; + + ASSERT_TRUE(image.created()); + EXPECT_EQ(&image.core(), &context.core); + EXPECT_EQ(image.extent().width, EXTENT.width); + EXPECT_EQ(image.extent().height, EXTENT.height); + EXPECT_EQ(image.mipLevels(), 7); + EXPECT_TRUE(image.memory().created()); + EXPECT_TRUE(all_layouts(image, VK_IMAGE_LAYOUT_UNDEFINED)); + + image.destroy(); + EXPECT_FALSE(image.created()); + EXPECT_FALSE(image.memory().created()); + EXPECT_EQ(image.extent().width, 0); +} + +CVK_TEST(Image, create_depth_buffer) { + test::Context const context{}; + + Image const image{context.core, Image::Config::DepthBuffer(VK_FORMAT_D16_UNORM, VK_SAMPLE_COUNT_1_BIT), EXTENT}; + + EXPECT_TRUE(image.created()); + EXPECT_EQ(image.aspectMask(), VK_IMAGE_ASPECT_DEPTH_BIT); +} + +CVK_TEST(Image, release_and_wrap) { + test::Context const context{}; + Image image{context.core, TEXTURE_CONFIG, EXTENT}; + auto const vkImage = image.get(); + auto const vmaAllocation = image.memory().get(); + + auto const state = image.release(); + EXPECT_FALSE(image.created()); + EXPECT_FALSE(image.memory().created()); + ASSERT_TRUE(state.memory.has_value()); + EXPECT_EQ(state.levelLayouts.size(), TEXTURE_CONFIG.mipLevels); + + Image const wrapped{context.core, TEXTURE_CONFIG, state}; + EXPECT_EQ(wrapped.get(), vkImage); + EXPECT_EQ(wrapped.memory().get(), vmaAllocation); + EXPECT_EQ(wrapped.extent().width, EXTENT.width); +} + +CVK_TEST(Image, wrap_fills_missing_layouts) { + test::Context const context{}; + Image image{context.core, TEXTURE_CONFIG, EXTENT}; + auto state = image.release(); + state.levelLayouts = {VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL}; + + Image const wrapped{context.core, TEXTURE_CONFIG, state}; + + ASSERT_EQ(wrapped.layouts().size(), TEXTURE_CONFIG.mipLevels); + EXPECT_EQ(wrapped.layout(1), VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL); + EXPECT_EQ(wrapped.layout(2), VK_IMAGE_LAYOUT_UNDEFINED); +} + +CVK_TEST(Image, wrap_without_memory_destroys_only_the_image) { + test::Context const context{}; + Image image{context.core, TEXTURE_CONFIG, EXTENT}; + auto state = image.release(); + + // another owner frees the memory, after the image, like the swapchain + Memory const memory{context.core, TEXTURE_CONFIG.memory, *state.memory}; + state.memory = std::nullopt; + + { + Image const wrapped{context.core, TEXTURE_CONFIG, state}; + EXPECT_FALSE(wrapped.memory().created()); + } + + EXPECT_TRUE(memory.created()); +} + +CVK_TEST(Image, move_assignment_destroys_the_old_image) { + test::Context const context{}; + Image image{context.core, TEXTURE_CONFIG, EXTENT}; + Image other{context.core, TEXTURE_CONFIG, EXTENT}; + auto const vkImage = other.get(); + + image = std::move(other); + + EXPECT_EQ(image.get(), vkImage); + EXPECT_TRUE(image.memory().created()); + EXPECT_FALSE(other.created()); +} +} From a0f373706229e9a1d481a681f355bc7e2cebc451 Mon Sep 17 00:00:00 2001 From: lukas <89145039+codethinki@users.noreply.github.com> Date: Sat, 17 Oct 2026 01:25:15 +0200 Subject: [PATCH 045/135] cvk: command pool and command buffers Command buffers own their handle: create allocates from the pool, the deleter frees through it. CmdPool is a plain wrapper with reset() and counts its command buffers: destroying a pool with live command buffers is a CTH_CRITICAL (legacy unused()). BaseCmdBuffer with PrimaryCmdBuffer (begin, execute) and SecondaryCmdBuffer (begin outside a rendering or with the inherited attachments of a dynamic rendering). The usage flags are a begin() parameter. Frames and reuse belong to jvk. Fixed against legacy: the secondary inheritance info had no sType. --- AGENTS.md | 2 +- cvk/incl/cvk/render/cmd/cmd_buffer.hpp | 293 +++++++++++++++++++ cvk/incl/cvk/render/cmd/cmd_pool.hpp | 143 +++++++++ cvk/incl/cvk/render/cmd/cmd_pool_config.hpp | 40 +++ cvk/src/render/cmd/cmd_buffer.cpp | 203 +++++++++++++ cvk/src/render/cmd/cmd_pool.cpp | 74 +++++ cvk/tests/src/render/cmd/cmd_buffer_test.cpp | 183 ++++++++++++ cvk/tests/src/render/cmd/cmd_pool_test.cpp | 116 ++++++++ 8 files changed, 1053 insertions(+), 1 deletion(-) create mode 100644 cvk/incl/cvk/render/cmd/cmd_buffer.hpp create mode 100644 cvk/incl/cvk/render/cmd/cmd_pool.hpp create mode 100644 cvk/incl/cvk/render/cmd/cmd_pool_config.hpp create mode 100644 cvk/src/render/cmd/cmd_buffer.cpp create mode 100644 cvk/src/render/cmd/cmd_pool.cpp create mode 100644 cvk/tests/src/render/cmd/cmd_buffer_test.cpp create mode 100644 cvk/tests/src/render/cmd/cmd_pool_test.cpp diff --git a/AGENTS.md b/AGENTS.md index 8c1fd8b8..0137bd05 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -162,7 +162,7 @@ public: - The last public section holds what every class has and readers rarely need: copy and move members, `debug_check` functions. - Classes that own a handle are move only. A class that differs (copyable, not movable, custom move) says so in its `@brief`. - They store the handle in `cvk::unique_handle` (`cvk/utility/unique_handle.hpp`): a `std::unique_ptr` with a deleter that holds the destroy context and calls the static `destroy`. - Deleters: `core_deleter` (the core), `vma_deleter` (the core, destroys through its allocator), `instance_deleter` (the `VkInstance`), `handle_deleter` (no context) or a deleter derived from them. + Deleters: `core_deleter` (the core), `vma_deleter` (the core, destroys through its allocator), `instance_deleter` (the `VkInstance`), `handle_deleter` (no context), a deleter derived from them or one of its own (`BaseCmdBuffer` frees through its pool). Device objects use `core_handle` (`cvk/utility/core_handle.hpp`), short for `unique_handle>`. Objects with VMA memory use `vma_handle` (`cvk/utility/vma_handle.hpp`), short for `unique_handle>`. Instance objects use `instance_handle` (`cvk/utility/instance_handle.hpp`), short for `unique_handle>`. diff --git a/cvk/incl/cvk/render/cmd/cmd_buffer.hpp b/cvk/incl/cvk/render/cmd/cmd_buffer.hpp new file mode 100644 index 00000000..dec5df59 --- /dev/null +++ b/cvk/incl/cvk/render/cmd/cmd_buffer.hpp @@ -0,0 +1,293 @@ +#pragma once +#include "cmd_pool.hpp" + +#include "cvk/base/device_table.hpp" +#include "cvk/utility/constants.hpp" +#include "cvk/utility/types.hpp" +#include "cvk/utility/unique_handle.hpp" + +#include + +#include + +#include +#include + + +namespace cvk { +class Core; + +/** + * @brief VkCommandBuffer wrapper, allocated from its pool, base of @ref PrimaryCmdBuffer and @ref SecondaryCmdBuffer + * @details the derived classes add the begin of the recording and their commands, no members + */ +class BaseCmdBuffer { +public: + struct State; + + /** + * @note calls @ref optDestroy() + */ + ~BaseCmdBuffer() { optDestroy(); } + + /** + * @brief wraps the state + * @pre the pool of the command buffer allocated it with the level + * @pre the command buffer is not recording + * @post @ref created() + * @note calls @ref optDestroy() + */ + void wrap(State const&); + + /** + * @brief allocates the command buffer from the pool + * @pre @ref CmdPool::created() + * @post @ref created() + * @note calls @ref optDestroy() + * @throws cvk::vk_result_exception result of vkAllocateCommandBuffers() + */ + void create(); + + /** + * @brief frees and forgets + * @pre @ref created() + * @pre the gpu does not use the command buffer + */ + void destroy(); + + /** + * @brief if @ref created() -> calls @ref destroy() + */ + void optDestroy() { if(created()) destroy(); } + + /** + * @brief releases ownership, returns the state and forgets + * @pre @ref created() + */ + State release(); + + /** + * @brief ends the recording + * @pre @ref recording() + * @throws cvk::vk_result_exception result of vkEndCommandBuffer() + */ + void end(); + + /** + * @brief resets the command buffer to the initial state + * @param flags VK_COMMAND_BUFFER_RESET_RELEASE_RESOURCES_BIT -> returns the memory to the pool + * @pre @ref created() + * @pre the pool has VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT + * @throws cvk::vk_result_exception result of vkResetCommandBuffer() + */ + void reset(VkCommandBufferResetFlags flags = 0); + + static void destroy(DeviceTable table, VkCommandPool, VkCommandBuffer); + + /** + * @brief frees the command buffers at once + */ + static void destroy(DeviceTable table, VkCommandPool, std::span vkBuffers); + + [[nodiscard]] constexpr bool created() const { return static_cast(_handle); } + [[nodiscard]] constexpr VkCommandBuffer get() const { return _handle.get(); } + [[nodiscard]] constexpr Core const& core() const { return _handle.core(); } + [[nodiscard]] constexpr CmdPool const& pool() const { return _handle.deleter().pool(); } + [[nodiscard]] constexpr VkCommandBufferLevel level() const { return _level; } + [[nodiscard]] constexpr bool recording() const { return _recording; } + +protected: + /** + * @brief base constructor + * @pre @ref CmdPool::created() + */ + BaseCmdBuffer(CmdPool const&, VkCommandBufferLevel); + + /** + * @brief begins the recording + * @pre @ref created() + * @pre !@ref recording() + * @post @ref recording() + * @throws cvk::vk_result_exception result of vkBeginCommandBuffer() + */ + void begin(VkCommandBufferBeginInfo const&); + +private: + /** + * @brief frees the command buffer through its pool and counts the command buffers of the pool + */ + class pool_deleter { + public: + constexpr explicit pool_deleter(CmdPool const& pool) : _pool{&pool} {} + + void operator()(VkCommandBuffer) const; + + [[nodiscard]] constexpr CmdPool const& pool() const { return *_pool; } + [[nodiscard]] constexpr Core const& core() const { return _pool->core(); } + + private: + not_null _pool; + }; + + void forget(); + + unique_handle _handle; + VkCommandBufferLevel _level; + bool _recording = false; + +public: + BaseCmdBuffer(BaseCmdBuffer const& other) = delete; + BaseCmdBuffer(BaseCmdBuffer&& other) noexcept = default; + BaseCmdBuffer& operator=(BaseCmdBuffer const& other) = delete; + BaseCmdBuffer& operator=(BaseCmdBuffer&& other) noexcept = default; + + static constexpr void debug_check(BaseCmdBuffer const&); +}; +} + +namespace cvk { +struct BaseCmdBuffer::State { + vk_not_null vkBuffer; +}; +} + +namespace cvk { +constexpr void BaseCmdBuffer::debug_check(BaseCmdBuffer const& buffer) { + CTH_CRITICAL(!buffer.created(), "command buffer must be created") {} +} +} + + +namespace cvk { +class SecondaryCmdBuffer; + +/** + * @brief primary command buffer, queues submit it + */ +class PrimaryCmdBuffer : public BaseCmdBuffer { +public: + /** + * @brief base constructor + * @pre @ref CmdPool::created() + */ + explicit PrimaryCmdBuffer(CmdPool const&); + + /** + * @brief constructs and wraps + * @details calls: + * - @ref PrimaryCmdBuffer(CmdPool const&) + * - @ref wrap() + */ + PrimaryCmdBuffer(CmdPool const&, State const&); + + /** + * @brief constructs and creates + * @details calls: + * - @ref PrimaryCmdBuffer(CmdPool const&) + * - @ref create() + */ + explicit PrimaryCmdBuffer(CmdPool const&, create_t); + + /** + * @brief begins the recording + * @param usage VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT, ... + * @pre @ref created() + * @pre !@ref recording() + * @post @ref recording() + * @throws cvk::vk_result_exception result of vkBeginCommandBuffer() + */ + void begin(VkCommandBufferUsageFlags usage = 0); + + /** + * @brief records the secondary command buffers + * @pre @ref recording() + * @pre the secondaries are created and not recording + */ + void execute(std::span secondaries) const; +}; +} + + +namespace cvk { +/** + * @brief secondary command buffer, primary command buffers execute it + */ +class SecondaryCmdBuffer : public BaseCmdBuffer { +public: + struct Inheritance; + + /** + * @brief base constructor + * @pre @ref CmdPool::created() + */ + explicit SecondaryCmdBuffer(CmdPool const&); + + /** + * @brief constructs and wraps + * @details calls: + * - @ref SecondaryCmdBuffer(CmdPool const&) + * - @ref wrap() + */ + SecondaryCmdBuffer(CmdPool const&, State const&); + + /** + * @brief constructs and creates + * @details calls: + * - @ref SecondaryCmdBuffer(CmdPool const&) + * - @ref create() + */ + explicit SecondaryCmdBuffer(CmdPool const&, create_t); + + /** + * @brief begins the recording for use outside of a rendering + * @param usage VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT, ... + * @pre @ref created() + * @pre !@ref recording() + * @post @ref recording() + * @throws cvk::vk_result_exception result of vkBeginCommandBuffer() + */ + void begin(VkCommandBufferUsageFlags usage = 0); + + /** + * @brief begins the recording for use inside a rendering with the attachments of the inheritance + * @param usage VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT, ... + * @pre @ref created() + * @pre !@ref recording() + * @post @ref recording() + * @note adds VK_COMMAND_BUFFER_USAGE_RENDER_PASS_CONTINUE_BIT + * @throws cvk::vk_result_exception result of vkBeginCommandBuffer() + */ + void begin(Inheritance const&, VkCommandBufferUsageFlags usage = 0); +}; +} + +namespace cvk { +/** + * @brief attachments of the rendering that executes the secondary command buffer + */ +struct SecondaryCmdBuffer::Inheritance { + std::vector colorFormats{}; + VkFormat depthFormat = VK_FORMAT_UNDEFINED; + VkFormat stencilFormat = VK_FORMAT_UNDEFINED; + VkSampleCountFlagBits samples = VK_SAMPLE_COUNT_1_BIT; + uint32_t viewMask = 0; + VkRenderingFlags flags = 0; + + /** + * @attention pColorAttachmentFormats points into @ref colorFormats + */ + [[nodiscard]] constexpr VkCommandBufferInheritanceRenderingInfo renderingInfo() const { + return VkCommandBufferInheritanceRenderingInfo{ + .sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_INHERITANCE_RENDERING_INFO, + .pNext = nullptr, + .flags = flags, + .viewMask = viewMask, + .colorAttachmentCount = static_cast(colorFormats.size()), + .pColorAttachmentFormats = colorFormats.data(), + .depthAttachmentFormat = depthFormat, + .stencilAttachmentFormat = stencilFormat, + .rasterizationSamples = samples, + }; + } +}; +} diff --git a/cvk/incl/cvk/render/cmd/cmd_pool.hpp b/cvk/incl/cvk/render/cmd/cmd_pool.hpp new file mode 100644 index 00000000..387a7262 --- /dev/null +++ b/cvk/incl/cvk/render/cmd/cmd_pool.hpp @@ -0,0 +1,143 @@ +#pragma once +#include "cmd_pool_config.hpp" + +#include "cvk/base/device_table.hpp" +#include "cvk/utility/constants.hpp" +#include "cvk/utility/core_handle.hpp" +#include "cvk/utility/types.hpp" + +#include + +#include + + +namespace cvk { +class BaseCmdBuffer; +class Core; + +/** + * @brief VkCommandPool wrapper, counts its command buffers + * @details the pool and its command buffers are used by one thread at a time + */ +class CmdPool { +public: + using Config = CmdPoolConfig; + struct State; + + /** + * @brief base constructor + * @pre @ref Core::created() + */ + CmdPool(Core const&, Config const&); + + /** + * @brief constructs and wraps + * @details calls: + * - @ref CmdPool(Core const&, Config const&) + * - @ref wrap() + */ + CmdPool(Core const&, Config const&, State const&); + + /** + * @brief constructs and creates + * @details calls: + * - @ref CmdPool(Core const&, Config const&) + * - @ref create() + */ + explicit CmdPool(Core const&, Config const&, create_t); + + /** + * @note calls @ref optDestroy() + */ + ~CmdPool() { optDestroy(); } + + /** + * @brief wraps the state + * @pre the pool was created with the config + * @post @ref created() + * @note calls @ref optDestroy() + */ + void wrap(State const&); + + /** + * @brief creates the pool + * @post @ref created() + * @note calls @ref optDestroy() + * @throws cvk::vk_result_exception result of vkCreateCommandPool() + */ + void create(); + + /** + * @brief destroys and forgets + * @pre @ref created() + * @pre @ref unused() + */ + void destroy(); + + /** + * @brief if @ref created() -> calls @ref destroy() + */ + void optDestroy() { if(created()) destroy(); } + + /** + * @brief releases ownership, returns the state and forgets + * @pre @ref created() + * @pre @ref unused() + */ + State release(); + + /** + * @brief resets every command buffer of the pool to the initial state + * @param flags VK_COMMAND_POOL_RESET_RELEASE_RESOURCES_BIT -> returns the memory of the pool to the system + * @pre @ref created() + * @pre no command buffer of the pool is recording or pending + * @throws cvk::vk_result_exception result of vkResetCommandPool() + */ + void reset(VkCommandPoolResetFlags flags = 0) const; + + static void destroy(DeviceTable table, VkCommandPool); + + [[nodiscard]] constexpr bool created() const { return static_cast(_handle); } + [[nodiscard]] constexpr VkCommandPool get() const { return _handle.get(); } + [[nodiscard]] constexpr Core const& core() const { return _handle.core(); } + [[nodiscard]] constexpr Config const& config() const { return _config; } + + /** + * @return no command buffer of the pool exists + */ + [[nodiscard]] constexpr bool unused() const { return _buffers == 0; } + +private: + void forget(); + + core_handle _handle; + Config _config; + + /** + * @brief command buffers of the pool, @ref BaseCmdBuffer counts them + */ + mutable size_t _buffers = 0; + + friend BaseCmdBuffer; + +public: + CmdPool(CmdPool const& other) = delete; + CmdPool(CmdPool&& other) noexcept = default; + CmdPool& operator=(CmdPool const& other) = delete; + CmdPool& operator=(CmdPool&& other) noexcept = default; + + static constexpr void debug_check(CmdPool const&); +}; +} + +namespace cvk { +struct CmdPool::State { + vk_not_null vkPool; +}; +} + +namespace cvk { +constexpr void CmdPool::debug_check(CmdPool const& pool) { + CTH_CRITICAL(!pool.created(), "command pool must be created") {} +} +} diff --git a/cvk/incl/cvk/render/cmd/cmd_pool_config.hpp b/cvk/incl/cvk/render/cmd/cmd_pool_config.hpp new file mode 100644 index 00000000..82425493 --- /dev/null +++ b/cvk/incl/cvk/render/cmd/cmd_pool_config.hpp @@ -0,0 +1,40 @@ +#pragma once +#include "cvk/base/queue/queue.hpp" +#include "cvk/base/queue/queue_family.hpp" + +#include + + +namespace cvk { +/** + * @brief queue family and flags of a command pool + */ +struct CmdPoolConfig { + /** + * @brief the command buffers of the pool are submitted to queues of this family + */ + queue_family_index_t queueFamilyIndex = 0; + + /** + * @brief VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT -> @ref BaseCmdBuffer::reset() is allowed + */ + VkCommandPoolCreateFlags flags = VK_COMMAND_POOL_CREATE_TRANSIENT_BIT + | VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT; + + /** + * @brief config for the family of the queue, default flags + */ + [[nodiscard]] static constexpr CmdPoolConfig Default(Queue const& queue) { + return CmdPoolConfig{.queueFamilyIndex = queue.familyIndex()}; + } + + [[nodiscard]] constexpr VkCommandPoolCreateInfo createInfo() const { + return VkCommandPoolCreateInfo{ + .sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO, + .pNext = nullptr, + .flags = flags, + .queueFamilyIndex = queueFamilyIndex, + }; + } +}; +} diff --git a/cvk/src/render/cmd/cmd_buffer.cpp b/cvk/src/render/cmd/cmd_buffer.cpp new file mode 100644 index 00000000..610d5824 --- /dev/null +++ b/cvk/src/render/cmd/cmd_buffer.cpp @@ -0,0 +1,203 @@ +#include "cvk/render/cmd/cmd_buffer.hpp" + +#include "cvk/base/core.hpp" +#include "cvk/utility/vk_exceptions.hpp" + +#include +#include + + +namespace cvk { +using std::span; + +BaseCmdBuffer::BaseCmdBuffer(CmdPool const& pool, VkCommandBufferLevel level) : _handle{pool}, _level{level} { + CmdPool::debug_check(pool); +} + +void BaseCmdBuffer::wrap(State const& state) { + optDestroy(); + + _handle.reset(state.vkBuffer.get(), pool()); + ++pool()._buffers; +} + +void BaseCmdBuffer::create() { + CmdPool::debug_check(pool()); + + optDestroy(); + + VkCommandBufferAllocateInfo const info{ + .sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO, + .pNext = nullptr, + .commandPool = pool().get(), + .level = _level, + .commandBufferCount = 1, + }; + + VkCommandBuffer ptr = VK_NULL_HANDLE; + auto const result = core().functions()->vkAllocateCommandBuffers(core().vkDevice(), &info, &ptr); + + CTH_STABLE_ERR(result != VK_SUCCESS, "failed to allocate command buffer") { + forget(); + throw cvk::vk_result_exception{result, details->exception()}; + } + + _handle.reset(ptr, pool()); + ++pool()._buffers; +} + +void BaseCmdBuffer::destroy() { + debug_check(*this); + + _handle.reset(); + + forget(); +} + +BaseCmdBuffer::State BaseCmdBuffer::release() { + debug_check(*this); + + State const state{_handle.release()}; + --pool()._buffers; + + forget(); + return state; +} + +void BaseCmdBuffer::end() { + debug_check(*this); + CTH_CRITICAL(!recording(), "command buffer must be recording") {} + + auto const result = core().functions()->vkEndCommandBuffer(get()); + _recording = false; + + CVK_RESULT_STABLE_THROW(result != VK_SUCCESS, result, "failed to end command buffer") {} +} + +void BaseCmdBuffer::reset(VkCommandBufferResetFlags flags) { + debug_check(*this); + CTH_CRITICAL( + (pool().config().flags & VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT) == 0, + "pool must allow command buffer resets" + ) {} + + auto const result = core().functions()->vkResetCommandBuffer(get(), flags); + _recording = false; + + CVK_RESULT_STABLE_THROW(result != VK_SUCCESS, result, "failed to reset command buffer") {} +} + +void BaseCmdBuffer::destroy(DeviceTable table, VkCommandPool vkPool, VkCommandBuffer vkBuffer) { + CTH_WARN(vkBuffer == VK_NULL_HANDLE, "vkBuffer invalid (VK_NULL_HANDLE)") {} + + table->vkFreeCommandBuffers(table.device(), vkPool, 1, &vkBuffer); +} + +void BaseCmdBuffer::destroy(DeviceTable table, VkCommandPool vkPool, span vkBuffers) { + CTH_WARN(std::ranges::contains(vkBuffers, VK_NULL_HANDLE), "> 0 vkBuffers invalid (VK_NULL_HANDLE)") {} + + table->vkFreeCommandBuffers(table.device(), vkPool, static_cast(vkBuffers.size()), vkBuffers.data()); +} + +void BaseCmdBuffer::begin(VkCommandBufferBeginInfo const& info) { + debug_check(*this); + CTH_CRITICAL(recording(), "command buffer must not be recording") {} + + auto const result = core().functions()->vkBeginCommandBuffer(get(), &info); + CVK_RESULT_STABLE_THROW(result != VK_SUCCESS, result, "failed to begin command buffer") {} + + _recording = true; +} + +void BaseCmdBuffer::forget() { _recording = false; } + +void BaseCmdBuffer::pool_deleter::operator()(VkCommandBuffer vkBuffer) const { + --_pool->_buffers; + destroy(_pool->core().deviceTable(), _pool->get(), vkBuffer); +} +} + +namespace cvk { +PrimaryCmdBuffer::PrimaryCmdBuffer(CmdPool const& pool) : BaseCmdBuffer{pool, VK_COMMAND_BUFFER_LEVEL_PRIMARY} {} + +PrimaryCmdBuffer::PrimaryCmdBuffer(CmdPool const& pool, State const& state) : PrimaryCmdBuffer{pool} { wrap(state); } + +PrimaryCmdBuffer::PrimaryCmdBuffer(CmdPool const& pool, create_t) : PrimaryCmdBuffer{pool} { create(); } + +void PrimaryCmdBuffer::begin(VkCommandBufferUsageFlags usage) { + VkCommandBufferBeginInfo const info{ + .sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO, + .pNext = nullptr, + .flags = usage, + .pInheritanceInfo = nullptr, + }; + BaseCmdBuffer::begin(info); +} + +void PrimaryCmdBuffer::execute(span secondaries) const { + debug_check(*this); + CTH_CRITICAL(!recording(), "command buffer must be recording") {} + CTH_CRITICAL( + std::ranges::any_of( + secondaries, + [](SecondaryCmdBuffer const& secondary) { return !secondary.created() || secondary.recording(); } + ), + "secondaries must be created and not recording" + ) {} + + std::vector const vkBuffers{std::from_range, secondaries | std::views::transform(&SecondaryCmdBuffer::get)}; + + core().functions()->vkCmdExecuteCommands(get(), static_cast(vkBuffers.size()), vkBuffers.data()); +} +} + +namespace cvk { +SecondaryCmdBuffer::SecondaryCmdBuffer(CmdPool const& pool) : BaseCmdBuffer{pool, VK_COMMAND_BUFFER_LEVEL_SECONDARY} {} + +SecondaryCmdBuffer::SecondaryCmdBuffer(CmdPool const& pool, State const& state) : SecondaryCmdBuffer{pool} { + wrap(state); +} + +SecondaryCmdBuffer::SecondaryCmdBuffer(CmdPool const& pool, create_t) : SecondaryCmdBuffer{pool} { create(); } + +void SecondaryCmdBuffer::begin(VkCommandBufferUsageFlags usage) { + VkCommandBufferInheritanceInfo const inheritanceInfo{ + .sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_INHERITANCE_INFO, + .pNext = nullptr, + .renderPass = VK_NULL_HANDLE, + .subpass = 0, + .framebuffer = VK_NULL_HANDLE, + .occlusionQueryEnable = VK_FALSE, + .queryFlags = 0, + .pipelineStatistics = 0, + }; + VkCommandBufferBeginInfo const info{ + .sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO, + .pNext = nullptr, + .flags = usage, + .pInheritanceInfo = &inheritanceInfo, + }; + BaseCmdBuffer::begin(info); +} + +void SecondaryCmdBuffer::begin(Inheritance const& inheritance, VkCommandBufferUsageFlags usage) { + auto const renderingInfo = inheritance.renderingInfo(); + VkCommandBufferInheritanceInfo const inheritanceInfo{ + .sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_INHERITANCE_INFO, + .pNext = &renderingInfo, + .renderPass = VK_NULL_HANDLE, + .subpass = 0, + .framebuffer = VK_NULL_HANDLE, + .occlusionQueryEnable = VK_FALSE, + .queryFlags = 0, + .pipelineStatistics = 0, + }; + VkCommandBufferBeginInfo const info{ + .sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO, + .pNext = nullptr, + .flags = usage | VK_COMMAND_BUFFER_USAGE_RENDER_PASS_CONTINUE_BIT, + .pInheritanceInfo = &inheritanceInfo, + }; + BaseCmdBuffer::begin(info); +} +} diff --git a/cvk/src/render/cmd/cmd_pool.cpp b/cvk/src/render/cmd/cmd_pool.cpp new file mode 100644 index 00000000..89c25951 --- /dev/null +++ b/cvk/src/render/cmd/cmd_pool.cpp @@ -0,0 +1,74 @@ +#include "cvk/render/cmd/cmd_pool.hpp" + +#include "cvk/base/core.hpp" +#include "cvk/utility/vk_exceptions.hpp" + + +namespace cvk { +CmdPool::CmdPool(Core const& core, Config const& config) : _handle{core}, _config{config} { + Core::debug_check(core); +} + +CmdPool::CmdPool(Core const& core, Config const& config, State const& state) : CmdPool{core, config} { wrap(state); } + +CmdPool::CmdPool(Core const& core, Config const& config, create_t) : CmdPool{core, config} { create(); } + +void CmdPool::wrap(State const& state) { + optDestroy(); + + _handle.reset(state.vkPool.get(), core()); +} + +void CmdPool::create() { + optDestroy(); + + auto const info = _config.createInfo(); + + VkCommandPool ptr = VK_NULL_HANDLE; + auto const result = core().functions()->vkCreateCommandPool(core().vkDevice(), &info, nullptr, &ptr); + + CTH_STABLE_ERR(result != VK_SUCCESS, "failed to create command pool") { + forget(); + throw cvk::vk_result_exception{result, details->exception()}; + } + + _handle.reset(ptr, core()); +} + +void CmdPool::destroy() { + debug_check(*this); + CTH_CRITICAL(!unused(), "command buffers of the pool must be destroyed first") { + details->add("command buffers: {}", _buffers); + } + + _handle.reset(); + + forget(); +} + +CmdPool::State CmdPool::release() { + debug_check(*this); + CTH_CRITICAL(!unused(), "command buffers of the pool must be destroyed first") { + details->add("command buffers: {}", _buffers); + } + + State const state{_handle.release()}; + forget(); + return state; +} + +void CmdPool::reset(VkCommandPoolResetFlags flags) const { + debug_check(*this); + + auto const result = core().functions()->vkResetCommandPool(core().vkDevice(), get(), flags); + CVK_RESULT_STABLE_THROW(result != VK_SUCCESS, result, "failed to reset command pool") {} +} + +void CmdPool::destroy(DeviceTable table, VkCommandPool vkPool) { + CTH_WARN(vkPool == VK_NULL_HANDLE, "vkPool invalid (VK_NULL_HANDLE)") {} + + table->vkDestroyCommandPool(table.device(), vkPool, nullptr); +} + +void CmdPool::forget() {} +} diff --git a/cvk/tests/src/render/cmd/cmd_buffer_test.cpp b/cvk/tests/src/render/cmd/cmd_buffer_test.cpp new file mode 100644 index 00000000..da092f4e --- /dev/null +++ b/cvk/tests/src/render/cmd/cmd_buffer_test.cpp @@ -0,0 +1,183 @@ +#include "context.hpp" +#include "test.hpp" + +#include "cvk/render/cmd/cmd_buffer.hpp" +#include "cvk/render/sync/fence.hpp" + +#include + + +namespace cvk { +namespace { + CmdPool::Config pool_config(test::Context const& context) { + return CmdPool::Config::Default(context.core.queue(0)); + } + + void submit_and_wait(test::Context const& context, PrimaryCmdBuffer const& primary) { + Fence const fence{context.core, 0}; + + VkCommandBufferSubmitInfo const bufferInfo{ + .sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_SUBMIT_INFO, + .pNext = nullptr, + .commandBuffer = primary.get(), + .deviceMask = 0, + }; + VkSubmitInfo2 const submitInfo{ + .sType = VK_STRUCTURE_TYPE_SUBMIT_INFO_2, + .pNext = nullptr, + .flags = 0, + .waitSemaphoreInfoCount = 0, + .pWaitSemaphoreInfos = nullptr, + .commandBufferInfoCount = 1, + .pCommandBufferInfos = &bufferInfo, + .signalSemaphoreInfoCount = 0, + .pSignalSemaphoreInfos = nullptr, + }; + + context.core.queue(0).raw_submit(submitInfo, fence.get()); + fence.wait(); + } +} + +CVK_TEST(CmdBuffer, inheritance_rendering_info) { + static_assert([] { + SecondaryCmdBuffer::Inheritance inheritance{ + .colorFormats = {VK_FORMAT_B8G8R8A8_UNORM}, + .depthFormat = VK_FORMAT_D16_UNORM, + .samples = VK_SAMPLE_COUNT_4_BIT, + }; + auto const info = inheritance.renderingInfo(); + return info.colorAttachmentCount == 1 && info.pColorAttachmentFormats == inheritance.colorFormats.data() + && info.depthAttachmentFormat == VK_FORMAT_D16_UNORM && info.rasterizationSamples == VK_SAMPLE_COUNT_4_BIT; + }()); +} + +CVK_TEST(CmdBuffer, base_constructor_does_not_create) { + test::Context const context{}; + CmdPool const pool{context.core, pool_config(context), create}; + + PrimaryCmdBuffer const primary{pool}; + SecondaryCmdBuffer const secondary{pool}; + + EXPECT_FALSE(primary.created()); + EXPECT_FALSE(secondary.created()); + EXPECT_EQ(primary.level(), VK_COMMAND_BUFFER_LEVEL_PRIMARY); + EXPECT_EQ(secondary.level(), VK_COMMAND_BUFFER_LEVEL_SECONDARY); + EXPECT_EQ(&primary.pool(), &pool); +} + +CVK_TEST(CmdBuffer, create_and_destroy) { + test::Context const context{}; + CmdPool const pool{context.core, pool_config(context), create}; + PrimaryCmdBuffer primary{pool, create}; + + ASSERT_TRUE(primary.created()); + EXPECT_EQ(&primary.core(), &context.core); + EXPECT_FALSE(primary.recording()); + + primary.destroy(); + EXPECT_FALSE(primary.created()); +} + +CVK_TEST(CmdBuffer, begin_and_end) { + test::Context const context{}; + CmdPool const pool{context.core, pool_config(context), create}; + PrimaryCmdBuffer primary{pool, create}; + + primary.begin(VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT); + EXPECT_TRUE(primary.recording()); + + primary.end(); + EXPECT_FALSE(primary.recording()); +} + +CVK_TEST(CmdBuffer, submit) { + test::Context const context{}; + CmdPool const pool{context.core, pool_config(context), create}; + PrimaryCmdBuffer primary{pool, create}; + primary.begin(VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT); + primary.end(); + + submit_and_wait(context, primary); +} + +CVK_TEST(CmdBuffer, reset_and_record_again) { + test::Context const context{}; + CmdPool const pool{context.core, pool_config(context), create}; + PrimaryCmdBuffer primary{pool, create}; + primary.begin(); + + primary.reset(); + EXPECT_FALSE(primary.recording()); + + primary.begin(); + primary.end(); + submit_and_wait(context, primary); +} + +CVK_TEST(CmdBuffer, execute_secondaries) { + test::Context const context{}; + CmdPool const pool{context.core, pool_config(context), create}; + std::array secondaries{ + SecondaryCmdBuffer{pool, create}, + SecondaryCmdBuffer{pool, create}, + }; + for(auto& secondary : secondaries) { + secondary.begin(); + secondary.end(); + } + + PrimaryCmdBuffer primary{pool, create}; + primary.begin(VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT); + primary.execute(secondaries); + primary.end(); + + submit_and_wait(context, primary); +} + +CVK_TEST(CmdBuffer, secondary_begin_with_rendering_inheritance) { + test::Context const context{}; + CmdPool const pool{context.core, pool_config(context), create}; + SecondaryCmdBuffer secondary{pool, create}; + + secondary.begin( + SecondaryCmdBuffer::Inheritance{ + .colorFormats = {VK_FORMAT_R8G8B8A8_UNORM}, + .depthFormat = VK_FORMAT_D16_UNORM, + } + ); + EXPECT_TRUE(secondary.recording()); + + secondary.end(); +} + +CVK_TEST(CmdBuffer, release_and_wrap) { + test::Context const context{}; + CmdPool const pool{context.core, pool_config(context), create}; + PrimaryCmdBuffer primary{pool, create}; + auto const vkBuffer = primary.get(); + + auto const state = primary.release(); + EXPECT_FALSE(primary.created()); + + PrimaryCmdBuffer const wrapped{pool, state}; + EXPECT_EQ(wrapped.get(), vkBuffer); + EXPECT_FALSE(pool.unused()); +} + +CVK_TEST(CmdBuffer, move_assignment_frees_the_old_buffer) { + test::Context const context{}; + CmdPool const pool{context.core, pool_config(context), create}; + PrimaryCmdBuffer primary{pool, create}; + PrimaryCmdBuffer other{pool, create}; + auto const vkBuffer = other.get(); + + primary = std::move(other); + + EXPECT_EQ(primary.get(), vkBuffer); + EXPECT_FALSE(other.created()); + + primary.destroy(); + EXPECT_TRUE(pool.unused()); +} +} diff --git a/cvk/tests/src/render/cmd/cmd_pool_test.cpp b/cvk/tests/src/render/cmd/cmd_pool_test.cpp new file mode 100644 index 00000000..b43fae49 --- /dev/null +++ b/cvk/tests/src/render/cmd/cmd_pool_test.cpp @@ -0,0 +1,116 @@ +#include "context.hpp" +#include "test.hpp" + +#include "cvk/render/cmd/cmd_buffer.hpp" +#include "cvk/render/cmd/cmd_pool.hpp" + + +namespace cvk { +namespace { + CmdPool::Config pool_config(test::Context const& context) { + return CmdPool::Config::Default(context.core.queue(0)); + } +} + +CVK_TEST(CmdPool, config_create_info) { + constexpr CmdPool::Config config{.queueFamilyIndex = 2, .flags = VK_COMMAND_POOL_CREATE_TRANSIENT_BIT}; + constexpr auto info = config.createInfo(); + + static_assert(info.queueFamilyIndex == 2); + static_assert(info.flags == VK_COMMAND_POOL_CREATE_TRANSIENT_BIT); +} + +CVK_TEST(CmdPool, config_default_uses_the_queue_family) { + test::Context const context{}; + auto const& queue = context.core.queue(0); + + auto const config = CmdPool::Config::Default(queue); + + EXPECT_EQ(config.queueFamilyIndex, queue.familyIndex()); + EXPECT_NE(config.flags & VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT, 0u); +} + +CVK_TEST(CmdPool, base_constructor_does_not_create) { + test::Context const context{}; + + CmdPool const pool{context.core, pool_config(context)}; + + EXPECT_FALSE(pool.created()); + EXPECT_TRUE(pool.unused()); +} + +CVK_TEST(CmdPool, create_and_destroy) { + test::Context const context{}; + CmdPool pool{context.core, pool_config(context), create}; + + ASSERT_TRUE(pool.created()); + EXPECT_EQ(&pool.core(), &context.core); + + pool.destroy(); + EXPECT_FALSE(pool.created()); +} + +CVK_TEST(CmdPool, counts_its_command_buffers) { + test::Context const context{}; + CmdPool const pool{context.core, pool_config(context), create}; + + { + PrimaryCmdBuffer primary{pool, create}; + SecondaryCmdBuffer const secondary{pool, create}; + EXPECT_FALSE(pool.unused()); + + primary.destroy(); + EXPECT_FALSE(pool.unused()); + } + + EXPECT_TRUE(pool.unused()); +} + +CVK_TEST(CmdPool, release_of_a_buffer_stops_the_count) { + test::Context const context{}; + CmdPool const pool{context.core, pool_config(context), create}; + PrimaryCmdBuffer primary{pool, create}; + + auto const state = primary.release(); + EXPECT_TRUE(pool.unused()); + + BaseCmdBuffer::destroy(context.core.deviceTable(), pool.get(), state.vkBuffer.get()); +} + +CVK_TEST(CmdPool, reset_returns_the_buffers_to_the_initial_state) { + test::Context const context{}; + CmdPool const pool{context.core, pool_config(context), create}; + PrimaryCmdBuffer primary{pool, create}; + primary.begin(); + primary.end(); + + pool.reset(); + + primary.begin(VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT); + primary.end(); +} + +CVK_TEST(CmdPool, release_and_wrap) { + test::Context const context{}; + CmdPool pool{context.core, pool_config(context), create}; + auto const vkPool = pool.get(); + + auto const state = pool.release(); + EXPECT_FALSE(pool.created()); + + CmdPool const wrapped{context.core, pool_config(context), state}; + EXPECT_EQ(wrapped.get(), vkPool); +} + +CVK_TEST(CmdPool, move_assignment_destroys_the_old_pool) { + test::Context const context{}; + CmdPool pool{context.core, pool_config(context), create}; + CmdPool other{context.core, pool_config(context), create}; + auto const vkPool = other.get(); + + pool = std::move(other); + + EXPECT_EQ(pool.get(), vkPool); + EXPECT_FALSE(other.created()); +} +} From 42fee9dd42758cd35f2c95f5c2d9bc355a056267 Mon Sep 17 00:00:00 2001 From: lukas <89145039+codethinki@users.noreply.github.com> Date: Sat, 17 Oct 2026 01:57:09 +0200 Subject: [PATCH 046/135] cvk: command pool counts through public functions, resets end recordings CmdPool::add() and remove() replace the friend access to the counter, code that keeps a released command buffer can count it too. The pool counts its resets, BaseCmdBuffer::recording() compares it with the count at begin(): a pool reset ends every recording of the pool, a moved-from buffer does not record. CmdPool::reset() allows recording buffers like Vulkan (only pending is forbidden). The destructor of BaseCmdBuffer is protected, delete through the derived classes. --- cvk/incl/cvk/render/cmd/cmd_buffer.hpp | 24 +++++++++----- cvk/incl/cvk/render/cmd/cmd_pool.hpp | 32 ++++++++++++++----- cvk/src/render/cmd/cmd_buffer.cpp | 11 ++++--- cvk/src/render/cmd/cmd_pool.cpp | 9 ++++++ cvk/tests/src/render/cmd/cmd_buffer_test.cpp | 24 ++++++++++++++ cvk/tests/src/render/cmd/cmd_pool_test.cpp | 33 ++++++++++++++++++++ 6 files changed, 113 insertions(+), 20 deletions(-) diff --git a/cvk/incl/cvk/render/cmd/cmd_buffer.hpp b/cvk/incl/cvk/render/cmd/cmd_buffer.hpp index dec5df59..04185196 100644 --- a/cvk/incl/cvk/render/cmd/cmd_buffer.hpp +++ b/cvk/incl/cvk/render/cmd/cmd_buffer.hpp @@ -19,17 +19,13 @@ class Core; /** * @brief VkCommandBuffer wrapper, allocated from its pool, base of @ref PrimaryCmdBuffer and @ref SecondaryCmdBuffer - * @details the derived classes add the begin of the recording and their commands, no members + * @details the derived classes add the begin of the recording and their commands, no members, + * the destructor is protected: delete through the derived class */ class BaseCmdBuffer { public: struct State; - /** - * @note calls @ref optDestroy() - */ - ~BaseCmdBuffer() { optDestroy(); } - /** * @brief wraps the state * @pre the pool of the command buffer allocated it with the level @@ -94,7 +90,13 @@ class BaseCmdBuffer { [[nodiscard]] constexpr Core const& core() const { return _handle.core(); } [[nodiscard]] constexpr CmdPool const& pool() const { return _handle.deleter().pool(); } [[nodiscard]] constexpr VkCommandBufferLevel level() const { return _level; } - [[nodiscard]] constexpr bool recording() const { return _recording; } + + /** + * @return recording, a reset of the pool ends it + */ + [[nodiscard]] constexpr bool recording() const { + return created() && _recording && _beginResets == pool().resets(); + } protected: /** @@ -103,6 +105,11 @@ class BaseCmdBuffer { */ BaseCmdBuffer(CmdPool const&, VkCommandBufferLevel); + /** + * @note calls @ref optDestroy() + */ + ~BaseCmdBuffer() { optDestroy(); } + /** * @brief begins the recording * @pre @ref created() @@ -114,7 +121,7 @@ class BaseCmdBuffer { private: /** - * @brief frees the command buffer through its pool and counts the command buffers of the pool + * @brief frees the command buffer through its pool, the pool stops to count it */ class pool_deleter { public: @@ -134,6 +141,7 @@ class BaseCmdBuffer { unique_handle _handle; VkCommandBufferLevel _level; bool _recording = false; + uint64_t _beginResets = 0; public: BaseCmdBuffer(BaseCmdBuffer const& other) = delete; diff --git a/cvk/incl/cvk/render/cmd/cmd_pool.hpp b/cvk/incl/cvk/render/cmd/cmd_pool.hpp index 387a7262..b1e578f0 100644 --- a/cvk/incl/cvk/render/cmd/cmd_pool.hpp +++ b/cvk/incl/cvk/render/cmd/cmd_pool.hpp @@ -12,11 +12,10 @@ namespace cvk { -class BaseCmdBuffer; class Core; /** - * @brief VkCommandPool wrapper, counts its command buffers + * @brief VkCommandPool wrapper, counts its command buffers and its resets * @details the pool and its command buffers are used by one thread at a time */ class CmdPool { @@ -90,11 +89,27 @@ class CmdPool { * @brief resets every command buffer of the pool to the initial state * @param flags VK_COMMAND_POOL_RESET_RELEASE_RESOURCES_BIT -> returns the memory of the pool to the system * @pre @ref created() - * @pre no command buffer of the pool is recording or pending + * @pre no command buffer of the pool is pending + * @post @ref BaseCmdBuffer::recording() of every command buffer of the pool is false * @throws cvk::vk_result_exception result of vkResetCommandPool() */ void reset(VkCommandPoolResetFlags flags = 0) const; + /** + * @brief counts a command buffer of the pool + * @details @ref BaseCmdBuffer::create() and @ref BaseCmdBuffer::wrap() call it, + * code that keeps a released command buffer of the pool may call it too + */ + void add() const; + + /** + * @brief stops to count a command buffer of the pool + * @details @ref BaseCmdBuffer::destroy() and @ref BaseCmdBuffer::release() call it, + * code that frees a counted command buffer itself calls it too + * @pre !@ref unused() + */ + void remove() const; + static void destroy(DeviceTable table, VkCommandPool); [[nodiscard]] constexpr bool created() const { return static_cast(_handle); } @@ -107,18 +122,19 @@ class CmdPool { */ [[nodiscard]] constexpr bool unused() const { return _buffers == 0; } + /** + * @return number of @ref reset() calls + */ + [[nodiscard]] constexpr uint64_t resets() const { return _resets; } + private: void forget(); core_handle _handle; Config _config; - /** - * @brief command buffers of the pool, @ref BaseCmdBuffer counts them - */ mutable size_t _buffers = 0; - - friend BaseCmdBuffer; + mutable uint64_t _resets = 0; public: CmdPool(CmdPool const& other) = delete; diff --git a/cvk/src/render/cmd/cmd_buffer.cpp b/cvk/src/render/cmd/cmd_buffer.cpp index 610d5824..89c29dba 100644 --- a/cvk/src/render/cmd/cmd_buffer.cpp +++ b/cvk/src/render/cmd/cmd_buffer.cpp @@ -18,7 +18,8 @@ void BaseCmdBuffer::wrap(State const& state) { optDestroy(); _handle.reset(state.vkBuffer.get(), pool()); - ++pool()._buffers; + pool().add(); + _recording = false; } void BaseCmdBuffer::create() { @@ -43,7 +44,8 @@ void BaseCmdBuffer::create() { } _handle.reset(ptr, pool()); - ++pool()._buffers; + pool().add(); + _recording = false; } void BaseCmdBuffer::destroy() { @@ -58,7 +60,7 @@ BaseCmdBuffer::State BaseCmdBuffer::release() { debug_check(*this); State const state{_handle.release()}; - --pool()._buffers; + pool().remove(); forget(); return state; @@ -107,12 +109,13 @@ void BaseCmdBuffer::begin(VkCommandBufferBeginInfo const& info) { CVK_RESULT_STABLE_THROW(result != VK_SUCCESS, result, "failed to begin command buffer") {} _recording = true; + _beginResets = pool().resets(); } void BaseCmdBuffer::forget() { _recording = false; } void BaseCmdBuffer::pool_deleter::operator()(VkCommandBuffer vkBuffer) const { - --_pool->_buffers; + _pool->remove(); destroy(_pool->core().deviceTable(), _pool->get(), vkBuffer); } } diff --git a/cvk/src/render/cmd/cmd_pool.cpp b/cvk/src/render/cmd/cmd_pool.cpp index 89c25951..10315534 100644 --- a/cvk/src/render/cmd/cmd_pool.cpp +++ b/cvk/src/render/cmd/cmd_pool.cpp @@ -62,6 +62,15 @@ void CmdPool::reset(VkCommandPoolResetFlags flags) const { auto const result = core().functions()->vkResetCommandPool(core().vkDevice(), get(), flags); CVK_RESULT_STABLE_THROW(result != VK_SUCCESS, result, "failed to reset command pool") {} + + ++_resets; +} + +void CmdPool::add() const { ++_buffers; } + +void CmdPool::remove() const { + CTH_CRITICAL(unused(), "the pool counts no command buffer") {} + --_buffers; } void CmdPool::destroy(DeviceTable table, VkCommandPool vkPool) { diff --git a/cvk/tests/src/render/cmd/cmd_buffer_test.cpp b/cvk/tests/src/render/cmd/cmd_buffer_test.cpp index da092f4e..4cdf3baf 100644 --- a/cvk/tests/src/render/cmd/cmd_buffer_test.cpp +++ b/cvk/tests/src/render/cmd/cmd_buffer_test.cpp @@ -5,6 +5,7 @@ #include "cvk/render/sync/fence.hpp" #include +#include namespace cvk { @@ -52,6 +53,12 @@ CVK_TEST(CmdBuffer, inheritance_rendering_info) { }()); } +CVK_TEST(CmdBuffer, delete_only_through_the_derived_class) { + static_assert(!std::is_destructible_v); + static_assert(std::is_destructible_v); + static_assert(std::is_destructible_v); +} + CVK_TEST(CmdBuffer, base_constructor_does_not_create) { test::Context const context{}; CmdPool const pool{context.core, pool_config(context), create}; @@ -165,6 +172,23 @@ CVK_TEST(CmdBuffer, release_and_wrap) { EXPECT_FALSE(pool.unused()); } +CVK_TEST(CmdBuffer, moved_from_buffer_does_not_record) { + test::Context const context{}; + CmdPool const pool{context.core, pool_config(context), create}; + PrimaryCmdBuffer primary{pool, create}; + primary.begin(); + + PrimaryCmdBuffer moved{std::move(primary)}; + + EXPECT_FALSE(primary.recording()); + EXPECT_TRUE(moved.recording()); + + primary.create(); + EXPECT_FALSE(primary.recording()); + + moved.end(); +} + CVK_TEST(CmdBuffer, move_assignment_frees_the_old_buffer) { test::Context const context{}; CmdPool const pool{context.core, pool_config(context), create}; diff --git a/cvk/tests/src/render/cmd/cmd_pool_test.cpp b/cvk/tests/src/render/cmd/cmd_pool_test.cpp index b43fae49..4f894972 100644 --- a/cvk/tests/src/render/cmd/cmd_pool_test.cpp +++ b/cvk/tests/src/render/cmd/cmd_pool_test.cpp @@ -77,6 +77,20 @@ CVK_TEST(CmdPool, release_of_a_buffer_stops_the_count) { BaseCmdBuffer::destroy(context.core.deviceTable(), pool.get(), state.vkBuffer.get()); } +CVK_TEST(CmdPool, owner_of_a_released_buffer_counts_it) { + test::Context const context{}; + CmdPool const pool{context.core, pool_config(context), create}; + PrimaryCmdBuffer primary{pool, create}; + + auto const state = primary.release(); + pool.add(); + EXPECT_FALSE(pool.unused()); + + BaseCmdBuffer::destroy(context.core.deviceTable(), pool.get(), state.vkBuffer.get()); + pool.remove(); + EXPECT_TRUE(pool.unused()); +} + CVK_TEST(CmdPool, reset_returns_the_buffers_to_the_initial_state) { test::Context const context{}; CmdPool const pool{context.core, pool_config(context), create}; @@ -85,11 +99,30 @@ CVK_TEST(CmdPool, reset_returns_the_buffers_to_the_initial_state) { primary.end(); pool.reset(); + EXPECT_EQ(pool.resets(), 1); primary.begin(VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT); primary.end(); } +CVK_TEST(CmdPool, reset_ends_the_recording) { + test::Context const context{}; + CmdPool const pool{context.core, pool_config(context), create}; + PrimaryCmdBuffer primary{pool, create}; + SecondaryCmdBuffer secondary{pool, create}; + primary.begin(); + secondary.begin(); + + pool.reset(); + + EXPECT_FALSE(primary.recording()); + EXPECT_FALSE(secondary.recording()); + + primary.begin(); + EXPECT_TRUE(primary.recording()); + primary.end(); +} + CVK_TEST(CmdPool, release_and_wrap) { test::Context const context{}; CmdPool pool{context.core, pool_config(context), create}; From cad59977713fb227ee3d0ee2b9c85337d79356a2 Mon Sep 17 00:00:00 2001 From: lukas <89145039+codethinki@users.noreply.github.com> Date: Sat, 17 Oct 2026 02:08:45 +0200 Subject: [PATCH 047/135] cvk: vk_handle_t in every class, handle aliases take the class only Every class of a Vulkan or VMA object declares using vk_handle_t = VkX as its first type, get() returns it. core_handle, vma_handle and instance_handle read T::vk_handle_t, the deleters require the concept vk_handle_owner. unique_handle keeps both parameters: own deleters and handles that are not pointers. --- AGENTS.md | 12 +++-- cvk/incl/cvk/base/allocator.hpp | 5 +- cvk/incl/cvk/base/device.hpp | 5 +- cvk/incl/cvk/base/instance.hpp | 5 +- cvk/incl/cvk/base/physical_device.hpp | 3 +- cvk/incl/cvk/base/queue/queue.hpp | 3 +- cvk/incl/cvk/data/buffer/base_buffer.hpp | 5 +- cvk/incl/cvk/data/img/image.hpp | 5 +- cvk/incl/cvk/data/img/sampler.hpp | 5 +- cvk/incl/cvk/data/memory/memory.hpp | 5 +- cvk/incl/cvk/debug/debug_messenger.hpp | 5 +- cvk/incl/cvk/render/cmd/cmd_buffer.hpp | 5 +- cvk/incl/cvk/render/cmd/cmd_pool.hpp | 5 +- .../pipeline/layout/descriptor_set_layout.hpp | 5 +- .../pipeline/layout/pipeline_layout.hpp | 5 +- cvk/incl/cvk/render/pipeline/pipeline.hpp | 5 +- .../cvk/render/pipeline/shader/shader.hpp | 5 +- cvk/incl/cvk/render/sync/fence.hpp | 5 +- cvk/incl/cvk/render/sync/semaphore.hpp | 5 +- .../cvk/render/sync/timeline_semaphore.hpp | 1 + cvk/incl/cvk/surface/surface.hpp | 5 +- cvk/incl/cvk/utility/core_handle.hpp | 8 +-- cvk/incl/cvk/utility/instance_handle.hpp | 8 +-- cvk/incl/cvk/utility/unique_handle.hpp | 8 ++- cvk/incl/cvk/utility/vma_handle.hpp | 8 +-- cvk/tests/src/utility/unique_handle_test.cpp | 51 +++++++++++++++---- 26 files changed, 123 insertions(+), 64 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 0137bd05..baf649b0 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -123,6 +123,7 @@ Classes that own a Vulkan handle follow this order: ```cpp class Semaphore { public: + using vk_handle_t = VkSemaphore; struct State; // constants @@ -143,7 +144,7 @@ public: static void destroy(DeviceTable table, VkSemaphore); - // accessors: created(), get(), ... + // accessors: created(), get() returns vk_handle_t, ... protected: // helpers that derived classes use @@ -161,11 +162,14 @@ public: - The first public section holds the API: types, constants, lifecycle, operations, accessors. - The last public section holds what every class has and readers rarely need: copy and move members, `debug_check` functions. - Classes that own a handle are move only. A class that differs (copyable, not movable, custom move) says so in its `@brief`. +- Every class of a Vulkan or VMA object declares `using vk_handle_t = VkX;` as its first type, also classes that do not own their handle. `get()` returns `vk_handle_t`. - They store the handle in `cvk::unique_handle` (`cvk/utility/unique_handle.hpp`): a `std::unique_ptr` with a deleter that holds the destroy context and calls the static `destroy`. Deleters: `core_deleter` (the core), `vma_deleter` (the core, destroys through its allocator), `instance_deleter` (the `VkInstance`), `handle_deleter` (no context), a deleter derived from them or one of its own (`BaseCmdBuffer` frees through its pool). - Device objects use `core_handle` (`cvk/utility/core_handle.hpp`), short for `unique_handle>`. - Objects with VMA memory use `vma_handle` (`cvk/utility/vma_handle.hpp`), short for `unique_handle>`. - Instance objects use `instance_handle` (`cvk/utility/instance_handle.hpp`), short for `unique_handle>`. + The deleters require `vk_handle_owner`: `T` declares `vk_handle_t`. + Device objects use `core_handle` (`cvk/utility/core_handle.hpp`), short for `unique_handle>`. + Objects with VMA memory use `vma_handle` (`cvk/utility/vma_handle.hpp`), short for `unique_handle>`. + Instance objects use `instance_handle` (`cvk/utility/instance_handle.hpp`), short for `unique_handle>`. + Other deleters: `unique_handle>`. - Handle and context go in together: `_handle.reset(ptr, core())`. A missing context argument does not compile. - Move constructor and move assignment are `= default`. The move assignment destroys the old handle with the old context. - A class that owns more than one object declares the objects that depend on its handle first. `= default` assigns in declaration order: `Instance` declares `_debugMessenger` before `_handle`. diff --git a/cvk/incl/cvk/base/allocator.hpp b/cvk/incl/cvk/base/allocator.hpp index 1bfbc3dc..43268156 100644 --- a/cvk/incl/cvk/base/allocator.hpp +++ b/cvk/incl/cvk/base/allocator.hpp @@ -21,6 +21,7 @@ class PhysicalDevice; */ class Allocator { public: + using vk_handle_t = VmaAllocator; struct State; /** @@ -96,14 +97,14 @@ class Allocator { [[nodiscard]] static VmaAllocatorCreateFlags flags(PhysicalDevice const&); [[nodiscard]] constexpr bool created() const { return static_cast(_handle); } - [[nodiscard]] constexpr VmaAllocator get() const { return _handle.get(); } + [[nodiscard]] constexpr vk_handle_t get() const { return _handle.get(); } [[nodiscard]] constexpr Device const& device() const { return *_device; } private: void forget(); not_null _device; - unique_handle> _handle; + unique_handle> _handle; public: Allocator(Allocator const& other) = delete; diff --git a/cvk/incl/cvk/base/device.hpp b/cvk/incl/cvk/base/device.hpp index e810d9eb..8361f7a7 100644 --- a/cvk/incl/cvk/base/device.hpp +++ b/cvk/incl/cvk/base/device.hpp @@ -27,6 +27,7 @@ class Device { using queue_families_queue_counts_t = std::unordered_map; public: + using vk_handle_t = VkDevice; using Config = DeviceConfig; struct State; @@ -111,7 +112,7 @@ class Device { [[nodiscard]] constexpr DeviceTable table() const { return DeviceTable{_handle.get(), _functionTable.get()}; } [[nodiscard]] constexpr VolkDeviceTable const* functions() const { return _functionTable.get(); } - [[nodiscard]] constexpr VkDevice get() const { return _handle.get(); } + [[nodiscard]] constexpr vk_handle_t get() const { return _handle.get(); } [[nodiscard]] constexpr Instance const& instance() const { return *_instance; } [[nodiscard]] constexpr PhysicalDevice const& physicalDevice() const { return *_physicalDevice; } @@ -142,7 +143,7 @@ class Device { not_null _instance; not_null _physicalDevice; - unique_handle> _handle; + unique_handle> _handle; std::unique_ptr _functionTable = std::make_unique(); std::vector _queues{}; diff --git a/cvk/incl/cvk/base/instance.hpp b/cvk/incl/cvk/base/instance.hpp index b24abf1a..6d6045f4 100644 --- a/cvk/incl/cvk/base/instance.hpp +++ b/cvk/incl/cvk/base/instance.hpp @@ -25,6 +25,7 @@ namespace cvk { */ class Instance { public: + using vk_handle_t = VkInstance; struct State; static constexpr std::array VALIDATION_LAYERS = { @@ -132,7 +133,7 @@ class Instance { static void destroy(VkInstance); [[nodiscard]] constexpr bool created() const { return static_cast(_handle); } - [[nodiscard]] constexpr VkInstance get() const { return _handle.get(); } + [[nodiscard]] constexpr vk_handle_t get() const { return _handle.get(); } [[nodiscard]] constexpr std::vector availableExtensions() const { return _availableExt; } [[nodiscard]] constexpr std::vector requiredExtensions() const { return _requiredExt; } [[nodiscard]] constexpr std::vector availableValidationLayers() const { return _availableLayers; } @@ -148,7 +149,7 @@ class Instance { // declared before the handle: move assignment destroys the old messenger before the old instance std::unique_ptr _debugMessenger = nullptr; - unique_handle> _handle; + unique_handle> _handle; static void loadInstanceFunctions(vk_not_null); diff --git a/cvk/incl/cvk/base/physical_device.hpp b/cvk/incl/cvk/base/physical_device.hpp index 712230ec..e50a13be 100644 --- a/cvk/incl/cvk/base/physical_device.hpp +++ b/cvk/incl/cvk/base/physical_device.hpp @@ -23,6 +23,7 @@ class Surface; */ class PhysicalDevice { public: + using vk_handle_t = VkPhysicalDevice; struct State; struct CreateResult; @@ -188,7 +189,7 @@ class PhysicalDevice { [[nodiscard]] static VkSampleCountFlagBits evalMaxSampleCount(VkPhysicalDeviceProperties const& properties); [[nodiscard]] constexpr bool created() const { return _handle != nullptr; } - [[nodiscard]] constexpr VkPhysicalDevice get() const { return _handle.get(); } + [[nodiscard]] constexpr vk_handle_t get() const { return _handle.get(); } [[nodiscard]] constexpr auto const& features() const { return _features; } [[nodiscard]] constexpr auto const& requiredFeatures() const { return _requiredFeatures; } [[nodiscard]] constexpr std::span extensions() const { return _extensions; } diff --git a/cvk/incl/cvk/base/queue/queue.hpp b/cvk/incl/cvk/base/queue/queue.hpp index a14c573c..2a319f05 100644 --- a/cvk/incl/cvk/base/queue/queue.hpp +++ b/cvk/incl/cvk/base/queue/queue.hpp @@ -14,6 +14,7 @@ namespace cvk { */ class Queue { public: + using vk_handle_t = VkQueue; using State = QueueState; explicit Queue(QueueFamilyProperties familyProperties) : _familyProperties{familyProperties} {} @@ -71,7 +72,7 @@ class Queue { [[nodiscard]] constexpr bool can_present() const { return contains(_familyProperties, QueueFamilyProperties::PRESENT); } [[nodiscard]] constexpr bool created() const { return _handle != VK_NULL_HANDLE; } - [[nodiscard]] constexpr VkQueue get() const { return _handle.get(); } + [[nodiscard]] constexpr vk_handle_t get() const { return _handle.get(); } [[nodiscard]] constexpr uint32_t index() const { return _queueIndex; } [[nodiscard]] constexpr uint32_t familyIndex() const { return _familyIndex; } [[nodiscard]] constexpr QueueFamilyProperties familyProperties() const { return _familyProperties; } diff --git a/cvk/incl/cvk/data/buffer/base_buffer.hpp b/cvk/incl/cvk/data/buffer/base_buffer.hpp index 4881d49f..e5eeb4fc 100644 --- a/cvk/incl/cvk/data/buffer/base_buffer.hpp +++ b/cvk/incl/cvk/data/buffer/base_buffer.hpp @@ -24,6 +24,7 @@ class Core; */ class BaseBuffer { public: + using vk_handle_t = VkBuffer; using Config = BufferConfig; struct State; @@ -102,7 +103,7 @@ class BaseBuffer { static void destroy(DeviceTable table, VkBuffer); [[nodiscard]] constexpr bool created() const { return static_cast(_handle); } - [[nodiscard]] constexpr VkBuffer get() const { return _handle.get(); } + [[nodiscard]] constexpr vk_handle_t get() const { return _handle.get(); } [[nodiscard]] constexpr Core const& core() const { return _handle.core(); } [[nodiscard]] constexpr Config const& config() const { return _config; } @@ -120,7 +121,7 @@ class BaseBuffer { void forget(); // declared before the memory: move assignment destroys the old buffer before its memory - core_handle _handle; + core_handle _handle; Memory _memory; Config _config; size_t _size = 0; diff --git a/cvk/incl/cvk/data/img/image.hpp b/cvk/incl/cvk/data/img/image.hpp index 5bb377fe..1b8e4db0 100644 --- a/cvk/incl/cvk/data/img/image.hpp +++ b/cvk/incl/cvk/data/img/image.hpp @@ -27,6 +27,7 @@ class Core; */ class Image { public: + using vk_handle_t = VkImage; using Config = ImageConfig; struct State; @@ -106,7 +107,7 @@ class Image { static void destroy(DeviceTable table, VkImage); [[nodiscard]] constexpr bool created() const { return static_cast(_handle); } - [[nodiscard]] constexpr VkImage get() const { return _handle.get(); } + [[nodiscard]] constexpr vk_handle_t get() const { return _handle.get(); } [[nodiscard]] constexpr Core const& core() const { return _handle.core(); } [[nodiscard]] constexpr Config const& config() const { return _config; } @@ -130,7 +131,7 @@ class Image { void forget(); // declared before the memory: move assignment destroys the old image before its memory - core_handle _handle; + core_handle _handle; Memory _memory; Config _config; VkExtent2D _extent{}; diff --git a/cvk/incl/cvk/data/img/sampler.hpp b/cvk/incl/cvk/data/img/sampler.hpp index 697cf828..f2dd0874 100644 --- a/cvk/incl/cvk/data/img/sampler.hpp +++ b/cvk/incl/cvk/data/img/sampler.hpp @@ -19,6 +19,7 @@ class Core; */ class Sampler { public: + using vk_handle_t = VkSampler; using Config = SamplerConfig; struct State; @@ -87,7 +88,7 @@ class Sampler { static void destroy(DeviceTable table, VkSampler); [[nodiscard]] constexpr bool created() const { return static_cast(_handle); } - [[nodiscard]] constexpr VkSampler get() const { return _handle.get(); } + [[nodiscard]] constexpr vk_handle_t get() const { return _handle.get(); } [[nodiscard]] constexpr Core const& core() const { return _handle.core(); } /** @@ -98,7 +99,7 @@ class Sampler { private: void forget(); - core_handle _handle; + core_handle _handle; Config _config; public: diff --git a/cvk/incl/cvk/data/memory/memory.hpp b/cvk/incl/cvk/data/memory/memory.hpp index 29e3567e..20a9eef0 100644 --- a/cvk/incl/cvk/data/memory/memory.hpp +++ b/cvk/incl/cvk/data/memory/memory.hpp @@ -23,6 +23,7 @@ class Core; */ class Memory { public: + using vk_handle_t = VmaAllocation; using Config = MemoryConfig; struct State; @@ -148,7 +149,7 @@ class Memory { static void destroy(VmaAllocator, VmaAllocation); [[nodiscard]] constexpr bool created() const { return static_cast(_handle); } - [[nodiscard]] constexpr VmaAllocation get() const { return _handle.get(); } + [[nodiscard]] constexpr vk_handle_t get() const { return _handle.get(); } [[nodiscard]] constexpr Core const& core() const { return _handle.core(); } [[nodiscard]] constexpr Config const& config() const { return _config; } @@ -181,7 +182,7 @@ class Memory { [[nodiscard]] VmaAllocator allocator() const; - vma_handle _handle; + vma_handle _handle; Config _config; public: diff --git a/cvk/incl/cvk/debug/debug_messenger.hpp b/cvk/incl/cvk/debug/debug_messenger.hpp index 376036cf..b51d9002 100644 --- a/cvk/incl/cvk/debug/debug_messenger.hpp +++ b/cvk/incl/cvk/debug/debug_messenger.hpp @@ -17,6 +17,7 @@ class Instance; */ class DebugMessenger { public: + using vk_handle_t = VkDebugUtilsMessengerEXT; struct Config; struct State; using callback_t = PFN_vkDebugUtilsMessengerCallbackEXT; @@ -92,7 +93,7 @@ class DebugMessenger { }; [[nodiscard]] constexpr bool created() const { return static_cast(_handle); } - [[nodiscard]] constexpr VkDebugUtilsMessengerEXT get() const { return _handle.get(); } + [[nodiscard]] constexpr vk_handle_t get() const { return _handle.get(); } [[nodiscard]] constexpr Config config() const { return _config; } protected: @@ -101,7 +102,7 @@ class DebugMessenger { private: void forget(); - instance_handle _handle; + instance_handle _handle; public: DebugMessenger(DebugMessenger const& other) = delete; diff --git a/cvk/incl/cvk/render/cmd/cmd_buffer.hpp b/cvk/incl/cvk/render/cmd/cmd_buffer.hpp index 04185196..bc9bb611 100644 --- a/cvk/incl/cvk/render/cmd/cmd_buffer.hpp +++ b/cvk/incl/cvk/render/cmd/cmd_buffer.hpp @@ -24,6 +24,7 @@ class Core; */ class BaseCmdBuffer { public: + using vk_handle_t = VkCommandBuffer; struct State; /** @@ -86,7 +87,7 @@ class BaseCmdBuffer { static void destroy(DeviceTable table, VkCommandPool, std::span vkBuffers); [[nodiscard]] constexpr bool created() const { return static_cast(_handle); } - [[nodiscard]] constexpr VkCommandBuffer get() const { return _handle.get(); } + [[nodiscard]] constexpr vk_handle_t get() const { return _handle.get(); } [[nodiscard]] constexpr Core const& core() const { return _handle.core(); } [[nodiscard]] constexpr CmdPool const& pool() const { return _handle.deleter().pool(); } [[nodiscard]] constexpr VkCommandBufferLevel level() const { return _level; } @@ -138,7 +139,7 @@ class BaseCmdBuffer { void forget(); - unique_handle _handle; + unique_handle _handle; VkCommandBufferLevel _level; bool _recording = false; uint64_t _beginResets = 0; diff --git a/cvk/incl/cvk/render/cmd/cmd_pool.hpp b/cvk/incl/cvk/render/cmd/cmd_pool.hpp index b1e578f0..c96465bc 100644 --- a/cvk/incl/cvk/render/cmd/cmd_pool.hpp +++ b/cvk/incl/cvk/render/cmd/cmd_pool.hpp @@ -20,6 +20,7 @@ class Core; */ class CmdPool { public: + using vk_handle_t = VkCommandPool; using Config = CmdPoolConfig; struct State; @@ -113,7 +114,7 @@ class CmdPool { static void destroy(DeviceTable table, VkCommandPool); [[nodiscard]] constexpr bool created() const { return static_cast(_handle); } - [[nodiscard]] constexpr VkCommandPool get() const { return _handle.get(); } + [[nodiscard]] constexpr vk_handle_t get() const { return _handle.get(); } [[nodiscard]] constexpr Core const& core() const { return _handle.core(); } [[nodiscard]] constexpr Config const& config() const { return _config; } @@ -130,7 +131,7 @@ class CmdPool { private: void forget(); - core_handle _handle; + core_handle _handle; Config _config; mutable size_t _buffers = 0; diff --git a/cvk/incl/cvk/render/pipeline/layout/descriptor_set_layout.hpp b/cvk/incl/cvk/render/pipeline/layout/descriptor_set_layout.hpp index 48f6e54a..843b4a37 100644 --- a/cvk/incl/cvk/render/pipeline/layout/descriptor_set_layout.hpp +++ b/cvk/incl/cvk/render/pipeline/layout/descriptor_set_layout.hpp @@ -22,6 +22,7 @@ class Core; */ class DescriptorSetLayout { public: + using vk_handle_t = VkDescriptorSetLayout; struct Config; struct State; @@ -88,7 +89,7 @@ class DescriptorSetLayout { static void destroy(DeviceTable table, VkDescriptorSetLayout); [[nodiscard]] constexpr bool created() const { return static_cast(_handle); } - [[nodiscard]] constexpr VkDescriptorSetLayout get() const { return _handle.get(); } + [[nodiscard]] constexpr vk_handle_t get() const { return _handle.get(); } [[nodiscard]] constexpr Core const& core() const { return _handle.core(); } /** @@ -104,7 +105,7 @@ class DescriptorSetLayout { private: void forget(); - core_handle _handle; + core_handle _handle; std::vector _bindings; public: diff --git a/cvk/incl/cvk/render/pipeline/layout/pipeline_layout.hpp b/cvk/incl/cvk/render/pipeline/layout/pipeline_layout.hpp index e083e628..9b35dfcd 100644 --- a/cvk/incl/cvk/render/pipeline/layout/pipeline_layout.hpp +++ b/cvk/incl/cvk/render/pipeline/layout/pipeline_layout.hpp @@ -20,6 +20,7 @@ class DescriptorSetLayout; */ class PipelineLayout { public: + using vk_handle_t = VkPipelineLayout; struct Config; struct State; @@ -89,13 +90,13 @@ class PipelineLayout { static void destroy(DeviceTable table, VkPipelineLayout); [[nodiscard]] constexpr bool created() const { return static_cast(_handle); } - [[nodiscard]] constexpr VkPipelineLayout get() const { return _handle.get(); } + [[nodiscard]] constexpr vk_handle_t get() const { return _handle.get(); } [[nodiscard]] constexpr Core const& core() const { return _handle.core(); } private: void forget(); - core_handle _handle; + core_handle _handle; std::vector _vkSetLayouts; std::vector _pushConstantRanges; diff --git a/cvk/incl/cvk/render/pipeline/pipeline.hpp b/cvk/incl/cvk/render/pipeline/pipeline.hpp index b73dde95..f869ccde 100644 --- a/cvk/incl/cvk/render/pipeline/pipeline.hpp +++ b/cvk/incl/cvk/render/pipeline/pipeline.hpp @@ -21,6 +21,7 @@ class PipelineLayout; */ class Pipeline { public: + using vk_handle_t = VkPipeline; using Config = PipelineConfig; struct State; @@ -106,7 +107,7 @@ class Pipeline { static void destroy(DeviceTable table, VkPipeline); [[nodiscard]] constexpr bool created() const { return static_cast(_handle); } - [[nodiscard]] constexpr VkPipeline get() const { return _handle.get(); } + [[nodiscard]] constexpr vk_handle_t get() const { return _handle.get(); } [[nodiscard]] constexpr VkPipelineLayout layout() const { return _vkLayout.get(); } /** @@ -119,7 +120,7 @@ class Pipeline { private: void forget(); - core_handle _handle; + core_handle _handle; vk_not_null _vkLayout; VkPipeline _vkParent = VK_NULL_HANDLE; Config _config; diff --git a/cvk/incl/cvk/render/pipeline/shader/shader.hpp b/cvk/incl/cvk/render/pipeline/shader/shader.hpp index d4ec4420..863e1341 100644 --- a/cvk/incl/cvk/render/pipeline/shader/shader.hpp +++ b/cvk/incl/cvk/render/pipeline/shader/shader.hpp @@ -23,6 +23,7 @@ class Core; */ class Shader { public: + using vk_handle_t = VkShaderModule; struct State; using path_t = std::filesystem::path; @@ -156,14 +157,14 @@ class Shader { static void destroy(DeviceTable table, VkShaderModule); [[nodiscard]] constexpr bool created() const { return static_cast(_handle); } - [[nodiscard]] constexpr VkShaderModule get() const { return _handle.get(); } + [[nodiscard]] constexpr vk_handle_t get() const { return _handle.get(); } [[nodiscard]] constexpr VkShaderStageFlagBits stage() const { return _stage; } [[nodiscard]] constexpr Core const& core() const { return _handle.core(); } private: void forget(); - core_handle _handle; + core_handle _handle; VkShaderStageFlagBits _stage; public: diff --git a/cvk/incl/cvk/render/sync/fence.hpp b/cvk/incl/cvk/render/sync/fence.hpp index c5df95d1..6a21c914 100644 --- a/cvk/incl/cvk/render/sync/fence.hpp +++ b/cvk/incl/cvk/render/sync/fence.hpp @@ -19,6 +19,7 @@ class Core; */ class Fence { public: + using vk_handle_t = VkFence; struct State; /** @@ -154,14 +155,14 @@ class Fence { static void destroy(DeviceTable table, VkFence); [[nodiscard]] constexpr bool created() const { return static_cast(_handle); } - [[nodiscard]] constexpr VkFence get() const { return _handle.get(); } + [[nodiscard]] constexpr vk_handle_t get() const { return _handle.get(); } [[nodiscard]] constexpr Core const& core() const { return _handle.core(); } [[nodiscard]] constexpr VkExternalFenceHandleTypeFlags exportTypes() const { return _exportTypes; } private: void forget(); - core_handle _handle; + core_handle _handle; VkExternalFenceHandleTypeFlags _exportTypes = 0; public: diff --git a/cvk/incl/cvk/render/sync/semaphore.hpp b/cvk/incl/cvk/render/sync/semaphore.hpp index b1c9ee35..540940a6 100644 --- a/cvk/incl/cvk/render/sync/semaphore.hpp +++ b/cvk/incl/cvk/render/sync/semaphore.hpp @@ -17,6 +17,7 @@ class Core; */ class Semaphore { public: + using vk_handle_t = VkSemaphore; struct State; /** @@ -82,7 +83,7 @@ class Semaphore { static void destroy(DeviceTable table, VkSemaphore); [[nodiscard]] constexpr bool created() const { return static_cast(_handle); } - [[nodiscard]] constexpr VkSemaphore get() const { return _handle.get(); } + [[nodiscard]] constexpr vk_handle_t get() const { return _handle.get(); } [[nodiscard]] constexpr Core const& core() const { return _handle.core(); } protected: @@ -97,7 +98,7 @@ class Semaphore { private: void forget(); - core_handle _handle; + core_handle _handle; public: Semaphore(Semaphore const& other) = delete; diff --git a/cvk/incl/cvk/render/sync/timeline_semaphore.hpp b/cvk/incl/cvk/render/sync/timeline_semaphore.hpp index 9d4a6f65..de1774c7 100644 --- a/cvk/incl/cvk/render/sync/timeline_semaphore.hpp +++ b/cvk/incl/cvk/render/sync/timeline_semaphore.hpp @@ -14,6 +14,7 @@ namespace cvk { */ class TimelineSemaphore : Semaphore { public: + using Semaphore::vk_handle_t; struct State; /** diff --git a/cvk/incl/cvk/surface/surface.hpp b/cvk/incl/cvk/surface/surface.hpp index 96672360..02cda310 100644 --- a/cvk/incl/cvk/surface/surface.hpp +++ b/cvk/incl/cvk/surface/surface.hpp @@ -21,6 +21,7 @@ class PhysicalDevice; */ class Surface { public: + using vk_handle_t = VkSurfaceKHR; using Config = SurfaceConfig; using format_t = Config::format_t; using present_mode_t = Config::present_mode_t; @@ -108,14 +109,14 @@ class Surface { static void destroy(vk_not_null, VkSurfaceKHR); [[nodiscard]] constexpr bool created() const { return static_cast(_handle); } - [[nodiscard]] constexpr VkSurfaceKHR get() const { return _handle.get(); } + [[nodiscard]] constexpr vk_handle_t get() const { return _handle.get(); } [[nodiscard]] constexpr Config const& config() const { return _config; } private: void forget(); Config _config; - instance_handle _handle; + instance_handle _handle; public: Surface(Surface const& other) = delete; diff --git a/cvk/incl/cvk/utility/core_handle.hpp b/cvk/incl/cvk/utility/core_handle.hpp index 95561b9b..0e95ca86 100644 --- a/cvk/incl/cvk/utility/core_handle.hpp +++ b/cvk/incl/cvk/utility/core_handle.hpp @@ -10,7 +10,7 @@ namespace cvk { * @details calls T::destroy(core.deviceTable(), handle) * @pre the core outlives the deleter */ -template +template class core_deleter { public: constexpr explicit core_deleter(Core const& core) : _core{&core} {} @@ -26,8 +26,8 @@ class core_deleter { /** * @brief @ref unique_handle of a device object, the deleter stores the core - * @tparam T owner class with a static destroy(DeviceTable, Handle) + * @tparam T owner class with vk_handle_t and a static destroy(DeviceTable, vk_handle_t) */ -template -using core_handle = unique_handle>; +template +using core_handle = unique_handle>; } diff --git a/cvk/incl/cvk/utility/instance_handle.hpp b/cvk/incl/cvk/utility/instance_handle.hpp index 03a81c18..0b3826bf 100644 --- a/cvk/incl/cvk/utility/instance_handle.hpp +++ b/cvk/incl/cvk/utility/instance_handle.hpp @@ -11,7 +11,7 @@ namespace cvk { * @details calls T::destroy(vkInstance, handle), * stores the handle, not the Instance: the instance owns its messenger and can move */ -template +template class instance_deleter { public: constexpr explicit instance_deleter(VkInstance vkInstance) : _vkInstance{vkInstance} {} @@ -27,8 +27,8 @@ class instance_deleter { /** * @brief @ref unique_handle of an instance object, the deleter stores the VkInstance - * @tparam T owner class with a static destroy(VkInstance, Handle) + * @tparam T owner class with vk_handle_t and a static destroy(VkInstance, vk_handle_t) */ -template -using instance_handle = unique_handle>; +template +using instance_handle = unique_handle>; } diff --git a/cvk/incl/cvk/utility/unique_handle.hpp b/cvk/incl/cvk/utility/unique_handle.hpp index a1b2f772..4c7a2429 100644 --- a/cvk/incl/cvk/utility/unique_handle.hpp +++ b/cvk/incl/cvk/utility/unique_handle.hpp @@ -11,9 +11,15 @@ namespace cvk { /** - * @brief deleter without context, calls T::destroy(handle) + * @brief T names the Vulkan handle it holds: using vk_handle_t = VkX */ template +concept vk_handle_owner = requires { typename T::vk_handle_t; }; + +/** + * @brief deleter without context, calls T::destroy(handle) + */ +template struct handle_deleter { template constexpr void operator()(Handle handle) const { T::destroy(handle); } diff --git a/cvk/incl/cvk/utility/vma_handle.hpp b/cvk/incl/cvk/utility/vma_handle.hpp index 83d96350..85df7c27 100644 --- a/cvk/incl/cvk/utility/vma_handle.hpp +++ b/cvk/incl/cvk/utility/vma_handle.hpp @@ -14,7 +14,7 @@ namespace cvk { * @details calls T::destroy(core.allocator().get(), handle) * @pre the core outlives the deleter */ -template +template class vma_deleter : public core_deleter { public: using core_deleter::core_deleter; @@ -25,8 +25,8 @@ class vma_deleter : public core_deleter { /** * @brief @ref unique_handle of a memory object, the deleter stores the core and frees through its allocator - * @tparam T owner class with a static destroy(VmaAllocator, Handle) + * @tparam T owner class with vk_handle_t and a static destroy(VmaAllocator, vk_handle_t) */ -template -using vma_handle = unique_handle>; +template +using vma_handle = unique_handle>; } diff --git a/cvk/tests/src/utility/unique_handle_test.cpp b/cvk/tests/src/utility/unique_handle_test.cpp index b168e2c9..7e93ae73 100644 --- a/cvk/tests/src/utility/unique_handle_test.cpp +++ b/cvk/tests/src/utility/unique_handle_test.cpp @@ -6,6 +6,7 @@ #include "cvk/base/instance.hpp" #include "cvk/data/memory/memory.hpp" #include "cvk/render/sync/fence.hpp" +#include "cvk/render/sync/timeline_semaphore.hpp" #include "cvk/surface/surface.hpp" #include "cvk/utility/constants.hpp" #include "cvk/utility/core_handle.hpp" @@ -30,6 +31,8 @@ namespace { * @brief owner with a destroy that needs the pool, like a command buffer */ struct PoolOwned { + using vk_handle_t = VkCommandBuffer; + static void destroy(DeviceTable table, VkCommandPool vkPool, VkCommandBuffer vkBuffer) { table->vkFreeCommandBuffers(table.device(), vkPool, 1, &vkBuffer); } @@ -67,6 +70,8 @@ namespace { }; struct PairOwned { + using vk_handle_t = pair_handle; + static inline int destroyed = 0; static void destroy(pair_handle handle) { @@ -84,6 +89,8 @@ namespace { * @brief owner of heap memory, constant evaluation rejects leaks and double frees */ struct HeapOwned { + using vk_handle_t = int*; + static constexpr void destroy(int* ptr) { delete ptr; } }; @@ -119,14 +126,36 @@ namespace { concept deleter_assignable = requires(H& handle, typename H::deleter_t deleter) { handle.deleter() = deleter; }; } +namespace { + /** + * @brief a class without vk_handle_t + */ + struct Unnamed { + static void destroy(DeviceTable, VkFence) {} + }; + + template + concept core_deleter_of = requires { typename core_deleter; }; +} + +CVK_TEST(unique_handle, owner_names_its_handle) { + static_assert(vk_handle_owner); + static_assert(vk_handle_owner); + static_assert(std::same_as); + static_assert(std::same_as::handle_t, VkFence>); + + static_assert(!vk_handle_owner); + static_assert(!core_deleter_of); +} + CVK_TEST(unique_handle, a_handle_needs_its_context) { - using fence_handle = core_handle; + using fence_handle = core_handle; static_assert(!std::default_initializable); static_assert(!std::constructible_from); static_assert(!resettable); static_assert(resettable); - using surface_handle = instance_handle; + using surface_handle = instance_handle; static_assert(!resettable); static_assert(resettable); @@ -134,7 +163,7 @@ CVK_TEST(unique_handle, a_handle_needs_its_context) { static_assert(std::default_initializable); static_assert(resettable); - using memory_handle = vma_handle; + using memory_handle = vma_handle; static_assert(!resettable); static_assert(resettable); @@ -153,17 +182,17 @@ CVK_TEST(unique_handle, constexpr) { CVK_TEST(unique_handle, size) { static_assert(sizeof(unique_handle>) == sizeof(VkInstance)); - static_assert(sizeof(core_handle) == sizeof(Core const*) + sizeof(VkFence)); - static_assert(sizeof(vma_handle) == sizeof(Core const*) + sizeof(VmaAllocation)); + static_assert(sizeof(core_handle) == sizeof(Core const*) + sizeof(VkFence)); + static_assert(sizeof(vma_handle) == sizeof(Core const*) + sizeof(VmaAllocation)); static_assert( - sizeof(instance_handle) == sizeof(VkInstance) + sizeof(VkSurfaceKHR) + sizeof(instance_handle) == sizeof(VkInstance) + sizeof(VkSurfaceKHR) ); } CVK_TEST(unique_handle, core_deleter) { test::Context const context{}; - core_handle handle{context.core}; + core_handle handle{context.core}; EXPECT_FALSE(handle); EXPECT_EQ(&handle.core(), &context.core); @@ -178,8 +207,8 @@ CVK_TEST(unique_handle, core_deleter) { CVK_TEST(unique_handle, move_assignment_destroys_the_old_handle) { test::Context const context{}; - core_handle handle{context.core}; - core_handle other{context.core}; + core_handle handle{context.core}; + core_handle other{context.core}; handle.reset(create_vk_fence(context.core), context.core); auto const vkFence = create_vk_fence(context.core); other.reset(vkFence, context.core); @@ -194,7 +223,7 @@ CVK_TEST(unique_handle, move_assignment_destroys_the_old_handle) { CVK_TEST(unique_handle, release_keeps_the_handle) { test::Context const context{}; auto const vkFence = create_vk_fence(context.core); - core_handle handle{vkFence, context.core}; + core_handle handle{vkFence, context.core}; EXPECT_EQ(handle.release(), vkFence); EXPECT_FALSE(handle); @@ -210,7 +239,7 @@ CVK_TEST(unique_handle, instance_deleter) { Instance const instance{test::APP_NAME, test::headless_extensions(), test::counting_messenger()}; auto surface = test::headless_surface(instance); - instance_handle handle{surface.release().vkSurface.get(), instance.get()}; + instance_handle handle{surface.release().vkSurface.get(), instance.get()}; EXPECT_EQ(handle.vkInstance(), instance.get()); } From ce1a553803aa1e3e8b808c9ef890ac0916a03bfe Mon Sep 17 00:00:00 2001 From: lukas <89145039+codethinki@users.noreply.github.com> Date: Sat, 17 Oct 2026 02:18:47 +0200 Subject: [PATCH 048/135] cvk: buffer copy and stage, pipeline bind BaseBuffer::copy() records vkCmdCopyBuffer2, VK_WHOLE_SIZE copies as much as fits into both buffers, stage() copies the whole staging buffer. Buffer has typed copy() and stage() in elements. Pipeline::bind() records the bind to the graphics bind point. The commands take BaseCmdBuffer const&: primary and secondary command buffers record them. Fixed against legacy: copy() always copied the size of the destination buffer. Tests: test::submit_and_wait() and test::host_read_barrier() in the test context, copies end to end. Three lines over 120 columns wrapped. --- cvk/incl/cvk/base/queue/queue.hpp | 4 +- cvk/incl/cvk/data/buffer/base_buffer.hpp | 26 +++++++ cvk/incl/cvk/data/buffer/buffer.hpp | 26 +++++++ cvk/incl/cvk/render/pipeline/pipeline.hpp | 8 +++ .../device/physical_device_features.hpp | 4 +- cvk/incl/cvk/utility/unique_handle.hpp | 3 +- cvk/src/data/buffer/base_buffer.cpp | 60 ++++++++++++++++- cvk/src/render/pipeline/pipeline.cpp | 8 +++ cvk/tests/src/context.cpp | 51 ++++++++++++++ cvk/tests/src/context.hpp | 17 +++++ .../src/data/buffer/base_buffer_test.cpp | 67 +++++++++++++++++++ cvk/tests/src/data/buffer/buffer_test.cpp | 57 ++++++++++++++++ cvk/tests/src/render/cmd/cmd_buffer_test.cpp | 32 +-------- .../src/render/pipeline/pipeline_test.cpp | 15 +++++ 14 files changed, 345 insertions(+), 33 deletions(-) diff --git a/cvk/incl/cvk/base/queue/queue.hpp b/cvk/incl/cvk/base/queue/queue.hpp index 2a319f05..e1bf0041 100644 --- a/cvk/incl/cvk/base/queue/queue.hpp +++ b/cvk/incl/cvk/base/queue/queue.hpp @@ -70,7 +70,9 @@ class Queue { */ void wait() const; - [[nodiscard]] constexpr bool can_present() const { return contains(_familyProperties, QueueFamilyProperties::PRESENT); } + [[nodiscard]] constexpr bool can_present() const { + return contains(_familyProperties, QueueFamilyProperties::PRESENT); + } [[nodiscard]] constexpr bool created() const { return _handle != VK_NULL_HANDLE; } [[nodiscard]] constexpr vk_handle_t get() const { return _handle.get(); } [[nodiscard]] constexpr uint32_t index() const { return _queueIndex; } diff --git a/cvk/incl/cvk/data/buffer/base_buffer.hpp b/cvk/incl/cvk/data/buffer/base_buffer.hpp index e5eeb4fc..a21ec95e 100644 --- a/cvk/incl/cvk/data/buffer/base_buffer.hpp +++ b/cvk/incl/cvk/data/buffer/base_buffer.hpp @@ -14,6 +14,7 @@ namespace cvk { +class BaseCmdBuffer; class Core; /** @@ -92,6 +93,31 @@ class BaseBuffer { */ State release(); + /** + * @brief records a copy from the source buffer + * @param size in bytes (VK_WHOLE_SIZE => as much as fits into both buffers) + * @param srcOffset in bytes + * @param dstOffset in bytes + * @pre @ref created() and src created + * @pre @ref BaseCmdBuffer::recording() + * @pre src usage has VK_BUFFER_USAGE_TRANSFER_SRC_BIT, the usage has VK_BUFFER_USAGE_TRANSFER_DST_BIT + * @pre 0 < copied size, the ranges lie in the buffers + */ + void copy( + BaseCmdBuffer const&, + BaseBuffer const& src, + size_t size = VK_WHOLE_SIZE, + size_t srcOffset = 0, + size_t dstOffset = 0 + ) const; + + /** + * @brief records a copy of the whole staging buffer + * @param dstOffset in bytes + * @details calls @ref copy() + */ + void stage(BaseCmdBuffer const&, BaseBuffer const& stagingBuffer, size_t dstOffset = 0) const; + /** * @param size in bytes (VK_WHOLE_SIZE => to the end) * @param offset in bytes diff --git a/cvk/incl/cvk/data/buffer/buffer.hpp b/cvk/incl/cvk/data/buffer/buffer.hpp index ecf8655f..ce7165bf 100644 --- a/cvk/incl/cvk/data/buffer/buffer.hpp +++ b/cvk/incl/cvk/data/buffer/buffer.hpp @@ -114,6 +114,32 @@ class Buffer : public BaseBuffer { memory().invalidate(bytes(count), offset * sizeof(T)); } + /** + * @brief records a copy from the source buffer + * @param count (VK_WHOLE_SIZE => as much as fits into both buffers) + * @param srcOffset in elements + * @param dstOffset in elements + * @details calls @ref BaseBuffer::copy() + */ + void copy( + BaseCmdBuffer const& cmdBuffer, + Buffer const& src, + size_t count = VK_WHOLE_SIZE, + size_t srcOffset = 0, + size_t dstOffset = 0 + ) const { + BaseBuffer::copy(cmdBuffer, src, bytes(count), srcOffset * sizeof(T), dstOffset * sizeof(T)); + } + + /** + * @brief records a copy of the whole staging buffer + * @param dstOffset in elements + * @details calls @ref BaseBuffer::stage() + */ + void stage(BaseCmdBuffer const& cmdBuffer, Buffer const& stagingBuffer, size_t dstOffset = 0) const { + BaseBuffer::stage(cmdBuffer, stagingBuffer, dstOffset * sizeof(T)); + } + /** * @param count (VK_WHOLE_SIZE => to the end) * @param offset in elements diff --git a/cvk/incl/cvk/render/pipeline/pipeline.hpp b/cvk/incl/cvk/render/pipeline/pipeline.hpp index f869ccde..22f9c4a2 100644 --- a/cvk/incl/cvk/render/pipeline/pipeline.hpp +++ b/cvk/incl/cvk/render/pipeline/pipeline.hpp @@ -14,6 +14,7 @@ namespace cvk { class Core; +class BaseCmdBuffer; class PipelineLayout; /** @@ -104,6 +105,13 @@ class Pipeline { */ State release(); + /** + * @brief records the bind of the pipeline to the graphics bind point + * @pre @ref created() + * @pre @ref BaseCmdBuffer::recording() + */ + void bind(BaseCmdBuffer const&) const; + static void destroy(DeviceTable table, VkPipeline); [[nodiscard]] constexpr bool created() const { return static_cast(_handle); } diff --git a/cvk/incl/cvk/utility/device/physical_device_features.hpp b/cvk/incl/cvk/utility/device/physical_device_features.hpp index f0b24550..d4f26925 100644 --- a/cvk/incl/cvk/utility/device/physical_device_features.hpp +++ b/cvk/incl/cvk/utility/device/physical_device_features.hpp @@ -66,7 +66,9 @@ class PhysicalDeviceFeatures { /** * @brief start of the pNext chain, for VkDeviceCreateInfo::pNext */ - [[nodiscard]] constexpr VkPhysicalDeviceFeatures2 const* get() const { return &std::get(_chain); } + [[nodiscard]] constexpr VkPhysicalDeviceFeatures2 const* get() const { + return &std::get(_chain); + } private: void link(); diff --git a/cvk/incl/cvk/utility/unique_handle.hpp b/cvk/incl/cvk/utility/unique_handle.hpp index 4c7a2429..943b1967 100644 --- a/cvk/incl/cvk/utility/unique_handle.hpp +++ b/cvk/incl/cvk/utility/unique_handle.hpp @@ -28,7 +28,8 @@ struct handle_deleter { /** * @brief owning Vulkan handle, std::unique_ptr with a deleter that holds the destroy context * @tparam Handle Vulkan handle, or Deleter::pointer for handles that are not pointers - * @tparam Deleter @ref core_deleter, @ref vma_deleter, @ref instance_deleter, @ref handle_deleter or a deleter derived from them + * @tparam Deleter @ref core_deleter, @ref vma_deleter, @ref instance_deleter, @ref handle_deleter, + * a deleter derived from them or one of its own * @details a handle comes only together with its context: the constructors and reset() construct the deleter, * a missing context argument does not compile, * move assignment destroys the old handle with the old context, then takes handle and context diff --git a/cvk/src/data/buffer/base_buffer.cpp b/cvk/src/data/buffer/base_buffer.cpp index 01e2aaa6..c0127b22 100644 --- a/cvk/src/data/buffer/base_buffer.cpp +++ b/cvk/src/data/buffer/base_buffer.cpp @@ -2,8 +2,11 @@ #include "cvk/base/allocator.hpp" #include "cvk/base/core.hpp" +#include "cvk/render/cmd/cmd_buffer.hpp" #include "cvk/utility/vk_exceptions.hpp" +#include + namespace cvk { BaseBuffer::BaseBuffer(Core const& core, Config const& config) : _handle{core}, _memory{core, config.memory}, @@ -12,7 +15,9 @@ BaseBuffer::BaseBuffer(Core const& core, Config const& config) : _handle{core}, Config::debug_check(config); } -BaseBuffer::BaseBuffer(Core const& core, Config const& config, State const& state) : BaseBuffer{core, config} { wrap(state); } +BaseBuffer::BaseBuffer(Core const& core, Config const& config, State const& state) : BaseBuffer{core, config} { + wrap(state); +} BaseBuffer::BaseBuffer(Core const& core, Config const& config, size_t size) : BaseBuffer{core, config} { create(size); } @@ -70,6 +75,59 @@ BaseBuffer::State BaseBuffer::release() { return state; } +void BaseBuffer::copy( + BaseCmdBuffer const& cmdBuffer, + BaseBuffer const& src, + size_t size, + size_t srcOffset, + size_t dstOffset +) const { + debug_check(*this); + debug_check(src); + CTH_CRITICAL(!cmdBuffer.recording(), "command buffer must be recording") {} + CTH_CRITICAL( + (src._config.usage & VK_BUFFER_USAGE_TRANSFER_SRC_BIT) == 0, + "src usage must have VK_BUFFER_USAGE_TRANSFER_SRC_BIT" + ) {} + CTH_CRITICAL( + (_config.usage & VK_BUFFER_USAGE_TRANSFER_DST_BIT) == 0, + "dst usage must have VK_BUFFER_USAGE_TRANSFER_DST_BIT" + ) {} + CTH_CRITICAL(srcOffset > src._size || dstOffset > _size, "offsets must lie in the buffers") {} + + size_t const copySize = size == VK_WHOLE_SIZE ? std::min(src._size - srcOffset, _size - dstOffset) : size; + + CTH_CRITICAL(copySize == 0, "copy size must not be 0") {} + CTH_CRITICAL( + copySize > src._size - srcOffset || copySize > _size - dstOffset, + "copy range must lie in the buffers" + ) { + details->add("src: {} + {} > {} (offset + size > src size)", srcOffset, copySize, src._size); + details->add("dst: {} + {} > {} (offset + size > dst size)", dstOffset, copySize, _size); + } + + VkBufferCopy2 const region{ + .sType = VK_STRUCTURE_TYPE_BUFFER_COPY_2, + .pNext = nullptr, + .srcOffset = srcOffset, + .dstOffset = dstOffset, + .size = copySize, + }; + VkCopyBufferInfo2 const info{ + .sType = VK_STRUCTURE_TYPE_COPY_BUFFER_INFO_2, + .pNext = nullptr, + .srcBuffer = src.get(), + .dstBuffer = get(), + .regionCount = 1, + .pRegions = ®ion, + }; + core().functions()->vkCmdCopyBuffer2(cmdBuffer.get(), &info); +} + +void BaseBuffer::stage(BaseCmdBuffer const& cmdBuffer, BaseBuffer const& stagingBuffer, size_t dstOffset) const { + copy(cmdBuffer, stagingBuffer, stagingBuffer.size(), 0, dstOffset); +} + void BaseBuffer::destroy(DeviceTable table, VkBuffer vkBuffer) { CTH_WARN(vkBuffer == VK_NULL_HANDLE, "vkBuffer invalid (VK_NULL_HANDLE)") {} diff --git a/cvk/src/render/pipeline/pipeline.cpp b/cvk/src/render/pipeline/pipeline.cpp index d5fa6092..43cf7cec 100644 --- a/cvk/src/render/pipeline/pipeline.cpp +++ b/cvk/src/render/pipeline/pipeline.cpp @@ -1,6 +1,7 @@ #include "cvk/render/pipeline/pipeline.hpp" #include "cvk/base/core.hpp" +#include "cvk/render/cmd/cmd_buffer.hpp" #include "cvk/render/pipeline/layout/pipeline_layout.hpp" #include "cvk/utility/vk_exceptions.hpp" @@ -154,6 +155,13 @@ Pipeline::State Pipeline::release() { return state; } +void Pipeline::bind(BaseCmdBuffer const& cmdBuffer) const { + debug_check(*this); + CTH_CRITICAL(!cmdBuffer.recording(), "command buffer must be recording") {} + + core().functions()->vkCmdBindPipeline(cmdBuffer.get(), VK_PIPELINE_BIND_POINT_GRAPHICS, get()); +} + void Pipeline::destroy(DeviceTable table, VkPipeline vkPipeline) { CTH_WARN(vkPipeline == VK_NULL_HANDLE, "vkPipeline invalid (VK_NULL_HANDLE)") {} diff --git a/cvk/tests/src/context.cpp b/cvk/tests/src/context.cpp index 194b7d59..7c187131 100644 --- a/cvk/tests/src/context.cpp +++ b/cvk/tests/src/context.cpp @@ -2,6 +2,9 @@ #include "validation.hpp" +#include "cvk/render/cmd/cmd_buffer.hpp" +#include "cvk/render/sync/fence.hpp" + namespace cvk::test { Core::Config const Context::CONFIG{ @@ -14,4 +17,52 @@ Context::Context(Core::Config const& config) : instance{APP_NAME, NO_EXTENSIONS, Context::ValidationCheck::ValidationCheck() { reset_validation_errors(); } Context::ValidationCheck::~ValidationCheck() { EXPECT_EQ(validation_errors(), 0); } + +void submit_and_wait(Context const& context, PrimaryCmdBuffer const& primary) { + Fence const fence{context.core, 0}; + + VkCommandBufferSubmitInfo const bufferInfo{ + .sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_SUBMIT_INFO, + .pNext = nullptr, + .commandBuffer = primary.get(), + .deviceMask = 0, + }; + VkSubmitInfo2 const submitInfo{ + .sType = VK_STRUCTURE_TYPE_SUBMIT_INFO_2, + .pNext = nullptr, + .flags = 0, + .waitSemaphoreInfoCount = 0, + .pWaitSemaphoreInfos = nullptr, + .commandBufferInfoCount = 1, + .pCommandBufferInfos = &bufferInfo, + .signalSemaphoreInfoCount = 0, + .pSignalSemaphoreInfos = nullptr, + }; + + context.core.queue(0).raw_submit(submitInfo, fence.get()); + fence.wait(); +} + +void host_read_barrier(Context const& context, BaseCmdBuffer const& cmdBuffer) { + VkMemoryBarrier2 const barrier{ + .sType = VK_STRUCTURE_TYPE_MEMORY_BARRIER_2, + .pNext = nullptr, + .srcStageMask = VK_PIPELINE_STAGE_2_TRANSFER_BIT, + .srcAccessMask = VK_ACCESS_2_TRANSFER_WRITE_BIT, + .dstStageMask = VK_PIPELINE_STAGE_2_HOST_BIT, + .dstAccessMask = VK_ACCESS_2_HOST_READ_BIT, + }; + VkDependencyInfo const dependency{ + .sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO, + .pNext = nullptr, + .dependencyFlags = 0, + .memoryBarrierCount = 1, + .pMemoryBarriers = &barrier, + .bufferMemoryBarrierCount = 0, + .pBufferMemoryBarriers = nullptr, + .imageMemoryBarrierCount = 0, + .pImageMemoryBarriers = nullptr, + }; + context.core.functions()->vkCmdPipelineBarrier2(cmdBuffer.get(), &dependency); +} } diff --git a/cvk/tests/src/context.hpp b/cvk/tests/src/context.hpp index 4d513d30..3f1d23f5 100644 --- a/cvk/tests/src/context.hpp +++ b/cvk/tests/src/context.hpp @@ -5,6 +5,11 @@ #include "cvk/base/instance.hpp" +namespace cvk { +class BaseCmdBuffer; +class PrimaryCmdBuffer; +} + namespace cvk::test { /** * @brief instance and core for tests, fails the test on validation errors @@ -31,4 +36,16 @@ struct Context { Context& operator=(Context const& other) = delete; Context& operator=(Context&& other) = delete; }; + +/** + * @brief submits the command buffer to the first queue and waits for the gpu + * @pre the command buffer is executable + */ +void submit_and_wait(Context const&, PrimaryCmdBuffer const&); + +/** + * @brief records a memory barrier: transfer writes -> host reads + * @pre the command buffer is recording + */ +void host_read_barrier(Context const&, BaseCmdBuffer const&); } diff --git a/cvk/tests/src/data/buffer/base_buffer_test.cpp b/cvk/tests/src/data/buffer/base_buffer_test.cpp index e52c1025..da008127 100644 --- a/cvk/tests/src/data/buffer/base_buffer_test.cpp +++ b/cvk/tests/src/data/buffer/base_buffer_test.cpp @@ -3,6 +3,7 @@ #include "cvk/base/physical_device.hpp" #include "cvk/data/buffer/base_buffer.hpp" +#include "cvk/render/cmd/cmd_buffer.hpp" #include #include @@ -20,6 +21,22 @@ namespace { .memory = {.flags = VMA_ALLOCATION_CREATE_HOST_ACCESS_SEQUENTIAL_WRITE_BIT}, }; + /** + * @brief records into a primary command buffer, submits and waits + */ + template + void record_and_submit(test::Context const& context, Record record) { + CmdPool const pool{context.core, CmdPool::Config::Default(context.core.queue(0)), create}; + PrimaryCmdBuffer primary{pool, create}; + + primary.begin(VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT); + record(primary); + test::host_read_barrier(context, primary); + primary.end(); + + test::submit_and_wait(context, primary); + } + /** * @brief buffer the cpu reads back */ @@ -133,6 +150,56 @@ CVK_TEST(BaseBuffer, write_and_read_through_the_memory) { buffer.memory().unmap(); } +CVK_TEST(BaseBuffer, copy_a_range) { + test::Context const context{}; + BaseBuffer const staging{context.core, STAGING_CONFIG, SIZE}; + BaseBuffer const readback{context.core, READBACK_CONFIG, SIZE}; + std::array data{}; + std::ranges::fill(data, 'c'); + staging.memory().write(data, 16); + + record_and_submit(context, [&](PrimaryCmdBuffer const& primary) { + readback.copy(primary, staging, data.size(), 16, 32); + }); + + auto const mapped = readback.memory().map(data.size(), 32); + readback.memory().invalidate(); + EXPECT_TRUE(std::ranges::equal(mapped, data)); + readback.memory().unmap(); +} + +CVK_TEST(BaseBuffer, copy_whole_size_fits_both_buffers) { + test::Context const context{}; + BaseBuffer const staging{context.core, STAGING_CONFIG, SIZE}; + BaseBuffer const readback{context.core, READBACK_CONFIG, SIZE / 2}; + std::array data{}; + std::ranges::fill(data, 'w'); + staging.memory().write(data); + + record_and_submit(context, [&](PrimaryCmdBuffer const& primary) { readback.copy(primary, staging); }); + + auto const mapped = readback.memory().map(SIZE / 2); + readback.memory().invalidate(); + EXPECT_TRUE(std::ranges::all_of(mapped, [](char const c) { return c == 'w'; })); + readback.memory().unmap(); +} + +CVK_TEST(BaseBuffer, stage_the_whole_staging_buffer) { + test::Context const context{}; + BaseBuffer const staging{context.core, STAGING_CONFIG, 64}; + BaseBuffer const readback{context.core, READBACK_CONFIG, SIZE}; + std::array data{}; + std::ranges::fill(data, 's'); + staging.memory().write(data); + + record_and_submit(context, [&](PrimaryCmdBuffer const& primary) { readback.stage(primary, staging, 8); }); + + auto const mapped = readback.memory().map(data.size(), 8); + readback.memory().invalidate(); + EXPECT_TRUE(std::ranges::equal(mapped, data)); + readback.memory().unmap(); +} + CVK_TEST(BaseBuffer, descriptor_info) { test::Context const context{}; BaseBuffer const buffer{context.core, DEVICE_CONFIG, SIZE}; diff --git a/cvk/tests/src/data/buffer/buffer_test.cpp b/cvk/tests/src/data/buffer/buffer_test.cpp index 9e1181a9..7969f82b 100644 --- a/cvk/tests/src/data/buffer/buffer_test.cpp +++ b/cvk/tests/src/data/buffer/buffer_test.cpp @@ -2,6 +2,7 @@ #include "test.hpp" #include "cvk/data/buffer/buffer.hpp" +#include "cvk/render/cmd/cmd_buffer.hpp" #include #include @@ -33,6 +34,32 @@ namespace { .memory = {.flags = VMA_ALLOCATION_CREATE_HOST_ACCESS_RANDOM_BIT}, }; + constexpr Buffer::Config STAGING_CONFIG{ + .usage = VK_BUFFER_USAGE_TRANSFER_SRC_BIT, + .memory = {.flags = VMA_ALLOCATION_CREATE_HOST_ACCESS_SEQUENTIAL_WRITE_BIT}, + }; + + constexpr Buffer::Config READBACK_CONFIG{ + .usage = VK_BUFFER_USAGE_TRANSFER_DST_BIT, + .memory = {.flags = VMA_ALLOCATION_CREATE_HOST_ACCESS_RANDOM_BIT}, + }; + + /** + * @brief records into a primary command buffer, submits and waits + */ + template + void record_and_submit(test::Context const& context, Record record) { + CmdPool const pool{context.core, CmdPool::Config::Default(context.core.queue(0)), create}; + PrimaryCmdBuffer primary{pool, create}; + + primary.begin(VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT); + record(primary); + test::host_read_barrier(context, primary); + primary.end(); + + test::submit_and_wait(context, primary); + } + /** * @brief Buffer is a valid type */ @@ -100,6 +127,36 @@ CVK_TEST(Buffer, map_whole_buffer) { buffer.unmap(); } +CVK_TEST(Buffer, copy_elements) { + test::Context const context{}; + Buffer const staging{context.core, STAGING_CONFIG, COUNT}; + Buffer const readback{context.core, READBACK_CONFIG, COUNT}; + staging.write(TRIANGLE, 1); + + record_and_submit(context, [&](PrimaryCmdBuffer const& primary) { + readback.copy(primary, staging, TRIANGLE.size(), 1, 4); + }); + + auto const mapped = readback.map(TRIANGLE.size(), 4); + readback.invalidate(); + EXPECT_TRUE(std::ranges::equal(mapped, TRIANGLE)); + readback.unmap(); +} + +CVK_TEST(Buffer, stage_elements) { + test::Context const context{}; + Buffer const staging{context.core, STAGING_CONFIG, TRIANGLE.size()}; + Buffer const readback{context.core, READBACK_CONFIG, COUNT}; + staging.write(TRIANGLE); + + record_and_submit(context, [&](PrimaryCmdBuffer const& primary) { readback.stage(primary, staging, 5); }); + + auto const mapped = readback.map(TRIANGLE.size(), 5); + readback.invalidate(); + EXPECT_TRUE(std::ranges::equal(mapped, TRIANGLE)); + readback.unmap(); +} + CVK_TEST(Buffer, descriptor_info_in_elements) { test::Context const context{}; Buffer const buffer{context.core, HOST_CONFIG, COUNT}; diff --git a/cvk/tests/src/render/cmd/cmd_buffer_test.cpp b/cvk/tests/src/render/cmd/cmd_buffer_test.cpp index 4cdf3baf..19b38bec 100644 --- a/cvk/tests/src/render/cmd/cmd_buffer_test.cpp +++ b/cvk/tests/src/render/cmd/cmd_buffer_test.cpp @@ -2,7 +2,6 @@ #include "test.hpp" #include "cvk/render/cmd/cmd_buffer.hpp" -#include "cvk/render/sync/fence.hpp" #include #include @@ -13,31 +12,6 @@ namespace { CmdPool::Config pool_config(test::Context const& context) { return CmdPool::Config::Default(context.core.queue(0)); } - - void submit_and_wait(test::Context const& context, PrimaryCmdBuffer const& primary) { - Fence const fence{context.core, 0}; - - VkCommandBufferSubmitInfo const bufferInfo{ - .sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_SUBMIT_INFO, - .pNext = nullptr, - .commandBuffer = primary.get(), - .deviceMask = 0, - }; - VkSubmitInfo2 const submitInfo{ - .sType = VK_STRUCTURE_TYPE_SUBMIT_INFO_2, - .pNext = nullptr, - .flags = 0, - .waitSemaphoreInfoCount = 0, - .pWaitSemaphoreInfos = nullptr, - .commandBufferInfoCount = 1, - .pCommandBufferInfos = &bufferInfo, - .signalSemaphoreInfoCount = 0, - .pSignalSemaphoreInfos = nullptr, - }; - - context.core.queue(0).raw_submit(submitInfo, fence.get()); - fence.wait(); - } } CVK_TEST(CmdBuffer, inheritance_rendering_info) { @@ -105,7 +79,7 @@ CVK_TEST(CmdBuffer, submit) { primary.begin(VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT); primary.end(); - submit_and_wait(context, primary); + test::submit_and_wait(context, primary); } CVK_TEST(CmdBuffer, reset_and_record_again) { @@ -119,7 +93,7 @@ CVK_TEST(CmdBuffer, reset_and_record_again) { primary.begin(); primary.end(); - submit_and_wait(context, primary); + test::submit_and_wait(context, primary); } CVK_TEST(CmdBuffer, execute_secondaries) { @@ -139,7 +113,7 @@ CVK_TEST(CmdBuffer, execute_secondaries) { primary.execute(secondaries); primary.end(); - submit_and_wait(context, primary); + test::submit_and_wait(context, primary); } CVK_TEST(CmdBuffer, secondary_begin_with_rendering_inheritance) { diff --git a/cvk/tests/src/render/pipeline/pipeline_test.cpp b/cvk/tests/src/render/pipeline/pipeline_test.cpp index 1f1718e9..f05f0208 100644 --- a/cvk/tests/src/render/pipeline/pipeline_test.cpp +++ b/cvk/tests/src/render/pipeline/pipeline_test.cpp @@ -2,6 +2,7 @@ #include "shaders.hpp" #include "test.hpp" +#include "cvk/render/cmd/cmd_buffer.hpp" #include "cvk/render/pipeline/layout/descriptor_set_layout.hpp" #include "cvk/render/pipeline/layout/pipeline_layout.hpp" #include "cvk/render/pipeline/pipeline.hpp" @@ -98,6 +99,20 @@ CVK_TEST(Pipeline, config_stages) { EXPECT_EQ(config.stages[0].stage, VK_SHADER_STAGE_FRAGMENT_BIT); } +CVK_TEST(Pipeline, bind) { + test::Context const context{}; + Triangle const triangle{context.core}; + Pipeline const pipeline{context.core, triangle.layout, triangle.config(), create}; + CmdPool const pool{context.core, CmdPool::Config::Default(context.core.queue(0)), create}; + PrimaryCmdBuffer primary{pool, create}; + + primary.begin(VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT); + pipeline.bind(primary); + primary.end(); + + test::submit_and_wait(context, primary); +} + CVK_TEST(Pipeline, base_constructor_does_not_create) { test::Context const context{}; Triangle const triangle{context.core}; From 75d378beaa9e9f7dcd8b1ba2756115deb631c28e Mon Sep 17 00:00:00 2001 From: lukas <89145039+codethinki@users.noreply.github.com> Date: Sat, 17 Oct 2026 02:36:01 +0200 Subject: [PATCH 049/135] cvk: extension features in the feature chain, require dynamic rendering local read PhysicalDeviceFeatures holds extension feature structs (swapchain maintenance1, dynamic rendering local read), link() puts them in the pNext chain only with their extension: queries link the supported ones, the device links the enabled ones. Device no longer builds the maintenance1 struct by hand. Every device requires VK_KHR_dynamic_rendering_local_read and its feature, for tile-based GPUs. Vulkan stays at 1.3, functionality beyond comes from required extensions. --- AGENTS.md | 1 + .../cvk/utility/constant/device_constants.hpp | 7 +- .../device/physical_device_features.hpp | 102 ++++++++++++++++-- cvk/src/base/device.cpp | 10 +- cvk/src/base/physical_device.cpp | 4 +- .../device/physical_device_features.cpp | 49 +++++++-- cvk/tests/src/base/physical_device_test.cpp | 12 +++ .../device/physical_device_features_test.cpp | 60 +++++++++++ 8 files changed, 219 insertions(+), 26 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index baf649b0..62f256ef 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -14,6 +14,7 @@ Read it before you change a file. Match the existing code. | `lib/cth_cmake` | cmake utilities (submodule) | - Vulkan 1.3 is the baseline. Do not port code that 1.3 makes unnecessary (render passes, framebuffers, the old sync API). +- Functionality beyond 1.3 comes from required extensions, not from a higher version: `VK_KHR_dynamic_rendering_local_read` gives tile-based GPUs subpass-like local reads in a dynamic rendering. Extension feature structs go into `utils::PhysicalDeviceFeatures`, `link()` puts them in the chain only with their extension. - Use the Vulkan C API through volk. Do not use Vulkan-Hpp. - Memory comes from VMA. - `cvk` destroys immediately. It has no destruction queue. Deferred destruction belongs to `jvk`. diff --git a/cvk/incl/cvk/utility/constant/device_constants.hpp b/cvk/incl/cvk/utility/constant/device_constants.hpp index d6cb1026..539c3878 100644 --- a/cvk/incl/cvk/utility/constant/device_constants.hpp +++ b/cvk/incl/cvk/utility/constant/device_constants.hpp @@ -9,8 +9,11 @@ namespace cvk::constants { /** * @brief extensions that every device enables + * @details VK_KHR_dynamic_rendering_local_read: tile local reads in a dynamic rendering, like subpasses */ -inline constexpr std::array REQUIRED_DEVICE_EXTENSIONS{}; +inline constexpr std::array REQUIRED_DEVICE_EXTENSIONS{ + VK_KHR_DYNAMIC_RENDERING_LOCAL_READ_EXTENSION_NAME, +}; /** * @brief extensions that a device with a present queue enables @@ -43,6 +46,8 @@ inline constexpr std::array PRESENT_INSTANCE_EXTENSIONS{ features13.synchronization2 = VK_TRUE; features13.dynamicRendering = VK_TRUE; + features.features().dynamicRenderingLocalRead = VK_TRUE; + return features; } } diff --git a/cvk/incl/cvk/utility/device/physical_device_features.hpp b/cvk/incl/cvk/utility/device/physical_device_features.hpp index d4f26925..5ec57181 100644 --- a/cvk/incl/cvk/utility/device/physical_device_features.hpp +++ b/cvk/incl/cvk/utility/device/physical_device_features.hpp @@ -6,8 +6,13 @@ #include +#include +#include +#include #include #include +#include +#include #include #include @@ -23,8 +28,9 @@ struct MissingFeature { }; /** - * @brief features of the vulkan 1.0 to 1.3 core as one pNext chain - * @details copyable, copy and move link the chain again + * @brief features of the vulkan 1.0 to 1.3 core and of extensions as one pNext chain + * @details copyable, copy and move link the chain again, + * an extension struct is in the chain only after @ref link() with its extension */ class PhysicalDeviceFeatures { public: @@ -32,18 +38,25 @@ class PhysicalDeviceFeatures { VkPhysicalDeviceFeatures2, VkPhysicalDeviceVulkan11Features, VkPhysicalDeviceVulkan12Features, - VkPhysicalDeviceVulkan13Features + VkPhysicalDeviceVulkan13Features, + VkPhysicalDeviceSwapchainMaintenance1FeaturesKHR, + VkPhysicalDeviceDynamicRenderingLocalReadFeaturesKHR >; + static constexpr size_t STRUCTS = std::tuple_size_v; + /** * @brief every feature false + * @post the core structs are in the chain */ PhysicalDeviceFeatures(); /** * @brief queries the features of the device + * @param extensions of the device, the query includes their structs + * @post the core structs and the structs of the extensions are in the chain */ - explicit PhysicalDeviceFeatures(vk_not_null vkDevice); + PhysicalDeviceFeatures(vk_not_null, std::span extensions); ~PhysicalDeviceFeatures() = default; @@ -57,6 +70,19 @@ class PhysicalDeviceFeatures { */ void merge(PhysicalDeviceFeatures const& other); + /** + * @brief puts the core structs and the structs of the extensions in the chain + * @details the structs of other extensions stay out, Vulkan forbids them + */ + void link(std::span extensions); + + /** + * @return the struct is in the chain + * @tparam T one of @ref chain_t + */ + template + [[nodiscard]] constexpr bool linked() const; + /** * @tparam T one of @ref chain_t */ @@ -71,21 +97,31 @@ class PhysicalDeviceFeatures { } private: - void link(); + /** + * @brief links the pNext members of the structs in @ref _linked + */ + void relink(); chain_t _chain; + std::array _linked{}; public: - PhysicalDeviceFeatures(PhysicalDeviceFeatures const& other) : _chain{other._chain} { link(); } - PhysicalDeviceFeatures(PhysicalDeviceFeatures&& other) noexcept : _chain{other._chain} { link(); } + PhysicalDeviceFeatures(PhysicalDeviceFeatures const& other) : _chain{other._chain}, _linked{other._linked} { + relink(); + } + PhysicalDeviceFeatures(PhysicalDeviceFeatures&& other) noexcept : _chain{other._chain}, _linked{other._linked} { + relink(); + } PhysicalDeviceFeatures& operator=(PhysicalDeviceFeatures const& other) { _chain = other._chain; - link(); + _linked = other._linked; + relink(); return *this; } PhysicalDeviceFeatures& operator=(PhysicalDeviceFeatures&& other) noexcept { _chain = other._chain; - link(); + _linked = other._linked; + relink(); return *this; } }; @@ -125,11 +161,51 @@ struct feature_flags { (offsetof(VkPhysicalDeviceVulkan13Features, maintenance4) - OFFSET) / sizeof(VkBool32) + 1; }; +template<> +struct feature_flags { + static constexpr size_t OFFSET = offsetof(VkPhysicalDeviceSwapchainMaintenance1FeaturesKHR, swapchainMaintenance1); + static constexpr size_t COUNT = 1; +}; + +template<> +struct feature_flags { + static constexpr size_t OFFSET = + offsetof(VkPhysicalDeviceDynamicRenderingLocalReadFeaturesKHR, dynamicRenderingLocalRead); + static constexpr size_t COUNT = 1; +}; + static_assert(feature_flags::COUNT == 55); static_assert(feature_flags::COUNT == 12); static_assert(feature_flags::COUNT == 47); static_assert(feature_flags::COUNT == 15); +/** + * @brief extension of a feature struct, core struct -> empty + */ +template +struct feature_extension { + static constexpr std::string_view NAME{}; +}; + +template<> +struct feature_extension { + static constexpr std::string_view NAME = VK_KHR_SWAPCHAIN_MAINTENANCE_1_EXTENSION_NAME; +}; + +template<> +struct feature_extension { + static constexpr std::string_view NAME = VK_KHR_DYNAMIC_RENDERING_LOCAL_READ_EXTENSION_NAME; +}; + +/** + * @brief position of T in the chain + */ +template +constexpr size_t chain_index = [](std::type_identity>) { + constexpr std::array same{std::same_as...}; + return static_cast(std::ranges::find(same, true) - same.begin()); +}(std::type_identity{}); + /** * @return the VkBool32 flags of @ref features */ @@ -146,6 +222,14 @@ template } } +namespace cvk::utils { +template +constexpr bool PhysicalDeviceFeatures::linked() const { + static_assert(dev::chain_index < STRUCTS, "T must be in the chain"); + return _linked[dev::chain_index]; +} +} + CTH_FORMAT_CLASS( cvk::utils::MissingFeature, "{}, flag: {}", diff --git a/cvk/src/base/device.cpp b/cvk/src/base/device.cpp index 16f39acc..ec61f250 100644 --- a/cvk/src/base/device.cpp +++ b/cvk/src/base/device.cpp @@ -132,16 +132,14 @@ void Device::createLogicalDevice(queue_families_queue_counts_t const& queueFamil auto features = _physicalDevice->requiredFeatures(); - VkPhysicalDeviceSwapchainMaintenance1FeaturesKHR swapchainMaintenance{ - .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SWAPCHAIN_MAINTENANCE_1_FEATURES_KHR, - .pNext = nullptr, - .swapchainMaintenance1 = VK_TRUE, - }; bool const maintenance1 = std::ranges::contains( _physicalDevice->requiredExtensions(), std::string_view{VK_KHR_SWAPCHAIN_MAINTENANCE_1_EXTENSION_NAME} ); - if(maintenance1) features.features().pNext = &swapchainMaintenance; + if(maintenance1) + features.features().swapchainMaintenance1 = VK_TRUE; + + features.link(_physicalDevice->requiredExtensions()); VkDeviceCreateInfo const createInfo{ .sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO, diff --git a/cvk/src/base/physical_device.cpp b/cvk/src/base/physical_device.cpp index e3bd06bb..f8362776 100644 --- a/cvk/src/base/physical_device.cpp +++ b/cvk/src/base/physical_device.cpp @@ -66,8 +66,8 @@ void PhysicalDevice::wrap(State const& state) { _handle = device.get(); - _features = state.features.has_value() ? *state.features : utils::PhysicalDeviceFeatures{device}; _extensions = !state.extensions.empty() ? state.extensions : queryExtensions(device); + _features = state.features.has_value() ? *state.features : utils::PhysicalDeviceFeatures{device, _extensions}; _properties = state.properties.has_value() ? *state.properties : queryProperties(device); _maxSampleCount = state.maxSampleCount != VK_SAMPLE_COUNT_FLAG_BITS_MAX_ENUM @@ -86,8 +86,8 @@ void PhysicalDevice::create(span queueFamilies, vk_not_null #include +#include namespace cvk::utils { @@ -9,9 +11,23 @@ PhysicalDeviceFeatures::PhysicalDeviceFeatures() : _chain{ VkPhysicalDeviceVulkan11Features{.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_1_FEATURES, .pNext = nullptr}, VkPhysicalDeviceVulkan12Features{.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_2_FEATURES, .pNext = nullptr}, VkPhysicalDeviceVulkan13Features{.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_3_FEATURES, .pNext = nullptr}, -} { link(); } + VkPhysicalDeviceSwapchainMaintenance1FeaturesKHR{ + .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SWAPCHAIN_MAINTENANCE_1_FEATURES_KHR, + .pNext = nullptr, + .swapchainMaintenance1 = VK_FALSE, + }, + VkPhysicalDeviceDynamicRenderingLocalReadFeaturesKHR{ + .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DYNAMIC_RENDERING_LOCAL_READ_FEATURES_KHR, + .pNext = nullptr, + .dynamicRenderingLocalRead = VK_FALSE, + }, +} { link({}); } -PhysicalDeviceFeatures::PhysicalDeviceFeatures(vk_not_null vkDevice) : PhysicalDeviceFeatures{} { +PhysicalDeviceFeatures::PhysicalDeviceFeatures( + vk_not_null vkDevice, + std::span extensions +) : PhysicalDeviceFeatures{} { + link(extensions); vkGetPhysicalDeviceFeatures2(vkDevice.get(), &features()); } @@ -45,12 +61,29 @@ void PhysicalDeviceFeatures::merge(PhysicalDeviceFeatures const& other) { std::apply([&](Ts&... to) { (merge(to, std::get(other._chain)), ...); }, _chain); } -void PhysicalDeviceFeatures::link() { - auto& [features2, features11, features12, features13] = _chain; +void PhysicalDeviceFeatures::link(std::span extensions) { + [&](std::index_sequence) { + ((_linked[Is] = [&] { + constexpr auto name = dev::feature_extension>::NAME; + return name.empty() || std::ranges::contains(extensions, name); + }()), ...); + }(std::make_index_sequence{}); - features2.pNext = &features11; - features11.pNext = &features12; - features12.pNext = &features13; - features13.pNext = nullptr; + relink(); +} + +void PhysicalDeviceFeatures::relink() { + void* next = nullptr; + + // back to front: each linked struct points to the next linked one + [&](std::index_sequence) { + ([&] { + constexpr size_t index = STRUCTS - 1 - Is; + auto& features = std::get(_chain); + + features.pNext = _linked[index] ? next : nullptr; + if(_linked[index]) next = &features; + }(), ...); + }(std::make_index_sequence{}); } } diff --git a/cvk/tests/src/base/physical_device_test.cpp b/cvk/tests/src/base/physical_device_test.cpp index d64fbf0b..63322dd6 100644 --- a/cvk/tests/src/base/physical_device_test.cpp +++ b/cvk/tests/src/base/physical_device_test.cpp @@ -57,6 +57,18 @@ CVK_TEST(PhysicalDevice, auto_pick_adds_the_defaults) { for(auto const extension : constants::PRESENT_DEVICE_EXTENSIONS) EXPECT_FALSE(requires_extension(device, extension)) << extension; + for(auto const extension : constants::REQUIRED_DEVICE_EXTENSIONS) + EXPECT_TRUE(requires_extension(device, extension)) << extension; +} + +CVK_TEST(PhysicalDevice, queries_the_features_of_its_extensions) { + Instance const instance{APP_NAME, NO_EXTENSIONS, std::nullopt}; + + auto const [device, _] = test::pick(instance, QUEUES); + + using local_read_t = VkPhysicalDeviceDynamicRenderingLocalReadFeaturesKHR; + EXPECT_TRUE(device.features().linked()); + EXPECT_TRUE(device.features().features().dynamicRenderingLocalRead); } CVK_TEST(PhysicalDevice, auto_pick_present_without_surfaces_finds_nothing) { diff --git a/cvk/tests/src/utility/device/physical_device_features_test.cpp b/cvk/tests/src/utility/device/physical_device_features_test.cpp index df79457a..05bbfabc 100644 --- a/cvk/tests/src/utility/device/physical_device_features_test.cpp +++ b/cvk/tests/src/utility/device/physical_device_features_test.cpp @@ -3,6 +3,8 @@ #include "cvk/utility/device/physical_device_features.hpp" #include +#include +#include namespace cvk::utils { @@ -24,6 +26,15 @@ namespace { EXPECT_EQ(features.features().pNext, nullptr); } + using local_read_t = VkPhysicalDeviceDynamicRenderingLocalReadFeaturesKHR; + using maintenance1_t = VkPhysicalDeviceSwapchainMaintenance1FeaturesKHR; + + std::vector const LOCAL_READ{VK_KHR_DYNAMIC_RENDERING_LOCAL_READ_EXTENSION_NAME}; + std::vector const BOTH{ + VK_KHR_SWAPCHAIN_MAINTENANCE_1_EXTENSION_NAME, + VK_KHR_DYNAMIC_RENDERING_LOCAL_READ_EXTENSION_NAME, + }; + constexpr size_t SAMPLER_ANISOTROPY_INDEX = offsetof(VkPhysicalDeviceFeatures, samplerAnisotropy) / sizeof(VkBool32); constexpr size_t SYNCHRONIZATION2_INDEX = @@ -63,6 +74,55 @@ CVK_TEST(PhysicalDeviceFeatures, move_links_its_own_chain) { EXPECT_TRUE(moved.features().multiview); } +CVK_TEST(PhysicalDeviceFeatures, extension_structs_are_not_linked_by_default) { + PhysicalDeviceFeatures const features{}; + + EXPECT_TRUE(features.linked()); + EXPECT_FALSE(features.linked()); + EXPECT_FALSE(features.linked()); +} + +CVK_TEST(PhysicalDeviceFeatures, link_adds_the_structs_of_the_extensions) { + PhysicalDeviceFeatures features{}; + + features.link(LOCAL_READ); + EXPECT_TRUE(features.linked()); + EXPECT_FALSE(features.linked()); + EXPECT_EQ(features.features().pNext, &features.features()); + EXPECT_EQ(features.features().pNext, nullptr); + + features.link(BOTH); + EXPECT_EQ(features.features().pNext, &features.features()); + EXPECT_EQ(features.features().pNext, &features.features()); + EXPECT_EQ(features.features().pNext, nullptr); + + features.link({}); + expect_linked(features); + EXPECT_FALSE(features.linked()); +} + +CVK_TEST(PhysicalDeviceFeatures, copy_keeps_the_linked_extensions) { + PhysicalDeviceFeatures features{}; + features.link(LOCAL_READ); + + PhysicalDeviceFeatures const copy{features}; + + EXPECT_TRUE(copy.linked()); + EXPECT_EQ(copy.features().pNext, ©.features()); +} + +CVK_TEST(PhysicalDeviceFeatures, supports_reports_missing_extension_features) { + PhysicalDeviceFeatures const available{}; + PhysicalDeviceFeatures required{}; + required.features().dynamicRenderingLocalRead = VK_TRUE; + + auto const missing = available.supports(required); + + ASSERT_EQ(missing.size(), 1); + EXPECT_EQ(missing[0].sType, VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DYNAMIC_RENDERING_LOCAL_READ_FEATURES_KHR); + EXPECT_EQ(missing[0].index, 0); +} + CVK_TEST(PhysicalDeviceFeatures, supports_reports_missing_features) { PhysicalDeviceFeatures available{}; available.features().timelineSemaphore = VK_TRUE; From f127949cc348e9fffb612fd8d66e411fb5fa7971 Mon Sep 17 00:00:00 2001 From: lukas <89145039+codethinki@users.noreply.github.com> Date: Sat, 17 Oct 2026 02:45:46 +0200 Subject: [PATCH 050/135] cvk: barriers (sync2), image layout transitions and image copy Barrier collects memory, buffer and image barriers and records them in one vkCmdPipelineBarrier2, dependency flags for VK_DEPENDENCY_BY_REGION_BIT. Buffer and image barriers are built at execute(), the old layout comes from the layout tracking, execute() updates it through Image::setLayout(). layout_transition() is the constexpr table of common transitions (legacy, sync2, attachments, present, rendering local read), Image::transitionLayout() uses it. Image::copy() copies a buffer into a mip level. Fixed against legacy: LayoutTransition covered 0 mip levels by default, buffer barriers had no range, Image::copy() copied the full extent into every mip level. Tests: test::record_and_submit() in the test context, image copy into two mip levels end to end. --- cvk/incl/cvk/data/img/image.hpp | 35 ++ cvk/incl/cvk/render/sync/barrier.hpp | 320 ++++++++++++++++++ cvk/incl/cvk/utility/format.hpp | 2 + cvk/src/data/img/image.cpp | 85 +++++ cvk/src/render/sync/barrier.cpp | 132 ++++++++ cvk/tests/src/context.hpp | 22 +- .../src/data/buffer/base_buffer_test.cpp | 29 +- cvk/tests/src/data/buffer/buffer_test.cpp | 24 +- cvk/tests/src/data/img/image_test.cpp | 88 +++++ cvk/tests/src/render/sync/barrier_test.cpp | 130 +++++++ 10 files changed, 826 insertions(+), 41 deletions(-) create mode 100644 cvk/incl/cvk/render/sync/barrier.hpp create mode 100644 cvk/src/render/sync/barrier.cpp create mode 100644 cvk/tests/src/render/sync/barrier_test.cpp diff --git a/cvk/incl/cvk/data/img/image.hpp b/cvk/incl/cvk/data/img/image.hpp index 1b8e4db0..275a6e91 100644 --- a/cvk/incl/cvk/data/img/image.hpp +++ b/cvk/incl/cvk/data/img/image.hpp @@ -18,6 +18,8 @@ namespace cvk { +class BaseBuffer; +class BaseCmdBuffer; class Core; /** @@ -96,6 +98,39 @@ class Image { */ State release(); + /** + * @brief records a copy from the buffer into the mip level + * @param srcOffset in bytes + * @pre @ref created() and src created + * @pre @ref BaseCmdBuffer::recording() + * @pre mipLevel < @ref mipLevels() + * @pre the layout of the mip level is VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL or VK_IMAGE_LAYOUT_GENERAL + * @pre @ref aspectMask() has one bit + * @pre src usage has VK_BUFFER_USAGE_TRANSFER_SRC_BIT, the usage has VK_IMAGE_USAGE_TRANSFER_DST_BIT + */ + void copy(BaseCmdBuffer const&, BaseBuffer const& src, size_t srcOffset = 0, uint32_t mipLevel = 0) const; + + /** + * @brief records a layout transition of the mip levels with the scopes of @ref layout_transition() + * @param mipLevels (VK_REMAINING_MIP_LEVELS => to the last mip level) + * @pre @ref layout_transition() knows the transition from the layout of firstMipLevel + * @details calls @ref Barrier::execute() + */ + void transitionLayout( + BaseCmdBuffer const&, + VkImageLayout newLayout, + uint32_t firstMipLevel = 0, + uint32_t mipLevels = VK_REMAINING_MIP_LEVELS + ); + + /** + * @brief records the layout of the mip levels in the layout tracking + * @param mipLevels (VK_REMAINING_MIP_LEVELS => to the last mip level) + * @details @ref Barrier::execute() calls it after a layout transition + * @pre the mip range lies in the image + */ + void setLayout(uint32_t firstMipLevel, uint32_t mipLevels, VkImageLayout); + /** * @return mip levels down to 1x1 * @pre width > 0, height > 0 diff --git a/cvk/incl/cvk/render/sync/barrier.hpp b/cvk/incl/cvk/render/sync/barrier.hpp new file mode 100644 index 00000000..ae5059d3 --- /dev/null +++ b/cvk/incl/cvk/render/sync/barrier.hpp @@ -0,0 +1,320 @@ +#pragma once +#include "cvk/base/queue/queue_family.hpp" +#include "cvk/utility/types.hpp" + +#include + +#include + +#include +#include +#include +#include + + +namespace cvk { +class BaseBuffer; +class BaseCmdBuffer; +class Image; + +/** + * @brief one side of a barrier: stages, access and queue family + */ +struct BarrierScope { + VkPipelineStageFlags2 stages = VK_PIPELINE_STAGE_2_NONE; + VkAccessFlags2 access = VK_ACCESS_2_NONE; + + /** + * @brief VK_QUEUE_FAMILY_IGNORED -> no queue family ownership transfer + */ + queue_family_index_t queueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; +}; + +/** + * @brief image part of a barrier + */ +struct ImageBarrierInfo { + BarrierScope src{}; + BarrierScope dst{}; + + /** + * @brief std::nullopt -> no layout transition + */ + std::optional newLayout = std::nullopt; + + uint32_t firstMipLevel = 0; + + /** + * @brief VK_REMAINING_MIP_LEVELS -> to the last mip level + */ + uint32_t mipLevels = VK_REMAINING_MIP_LEVELS; + + /** + * @brief 0 -> the aspect mask of the image + */ + VkImageAspectFlags aspectMask = 0; + + [[nodiscard]] static constexpr ImageBarrierInfo LayoutTransition( + VkImageLayout newLayout, + BarrierScope const& src, + BarrierScope const& dst, + uint32_t firstMipLevel = 0, + uint32_t mipLevels = VK_REMAINING_MIP_LEVELS + ) { + return ImageBarrierInfo{ + .src = src, + .dst = dst, + .newLayout = newLayout, + .firstMipLevel = firstMipLevel, + .mipLevels = mipLevels, + }; + } + + /** + * @brief queue family ownership transfer, src and dst name the families + */ + [[nodiscard]] static constexpr ImageBarrierInfo QueueTransition(BarrierScope const& src, BarrierScope const& dst) { + return ImageBarrierInfo{.src = src, .dst = dst}; + } +}; + +/** + * @brief buffer part of a barrier + */ +struct BufferBarrierInfo { + BarrierScope src{}; + BarrierScope dst{}; + + /** + * @brief in bytes + */ + size_t offset = 0; + + /** + * @brief in bytes (VK_WHOLE_SIZE => to the end) + */ + size_t size = VK_WHOLE_SIZE; + + /** + * @brief queue family ownership transfer, src and dst name the families + */ + [[nodiscard]] static constexpr BufferBarrierInfo QueueTransition(BarrierScope const& src, BarrierScope const& dst) { + return BufferBarrierInfo{.src = src, .dst = dst}; + } +}; + +/** + * @brief scopes of a common layout transition + */ +struct LayoutTransition { + BarrierScope src; + BarrierScope dst; +}; + +/** + * @return the scopes of the transition, no common transition -> std::nullopt + */ +[[nodiscard]] constexpr std::optional layout_transition( + VkImageLayout oldLayout, + VkImageLayout newLayout +); + + +/** + * @brief pipeline barrier: memory, buffer and image barriers in one vkCmdPipelineBarrier2() + * @details @ref execute() updates the layout tracking of the images + * @attention the images and buffers do not move until @ref execute() + */ +class Barrier { +public: + using ImageInfo = ImageBarrierInfo; + using BufferInfo = BufferBarrierInfo; + + /** + * @param flags VK_DEPENDENCY_BY_REGION_BIT -> the dependency is per framebuffer region, e.g. per tile + */ + constexpr explicit Barrier(VkDependencyFlags flags = 0) : _flags{flags} {} + + /** + * @brief adds a global memory barrier + * @pre no queue family ownership transfer + */ + constexpr void add(BarrierScope const& src, BarrierScope const& dst); + + /** + * @brief adds an image barrier + * @details the old layout comes from the layout tracking of the image at @ref execute() + * @pre @ref Image::created() + * @pre the mip range lies in the image + */ + void add(Image&, ImageInfo const&); + + /** + * @brief adds a buffer barrier + * @details the buffer handle comes from the buffer at @ref execute() + * @pre @ref BaseBuffer::created() + * @pre the range lies in the buffer + */ + void add(BaseBuffer const&, BufferInfo const&); + + /** + * @brief records the barriers, then updates the layout tracking of the images + * @pre @ref BaseCmdBuffer::recording() + * @pre every image range has one layout + * @pre a new layout is not VK_IMAGE_LAYOUT_UNDEFINED or VK_IMAGE_LAYOUT_PREINITIALIZED + */ + void execute(BaseCmdBuffer const&); + + /** + * @brief removes every barrier + */ + constexpr void clear() { + _memoryBarriers.clear(); + _buffers.clear(); + _images.clear(); + } + + [[nodiscard]] constexpr VkDependencyFlags flags() const { return _flags; } + [[nodiscard]] constexpr bool empty() const { + return _memoryBarriers.empty() && _buffers.empty() && _images.empty(); + } + +private: + struct BufferEntry { + not_null buffer; + BufferInfo info; + }; + + struct ImageEntry { + not_null image; + ImageInfo info; + }; + + VkDependencyFlags _flags; + std::vector _memoryBarriers{}; + std::vector _buffers{}; + std::vector _images{}; +}; +} + +namespace cvk { +constexpr void Barrier::add(BarrierScope const& src, BarrierScope const& dst) { + CTH_CRITICAL( + src.queueFamilyIndex != VK_QUEUE_FAMILY_IGNORED || dst.queueFamilyIndex != VK_QUEUE_FAMILY_IGNORED, + "memory barriers have no queue family ownership transfer" + ) {} + + _memoryBarriers.push_back( + VkMemoryBarrier2{ + .sType = VK_STRUCTURE_TYPE_MEMORY_BARRIER_2, + .pNext = nullptr, + .srcStageMask = src.stages, + .srcAccessMask = src.access, + .dstStageMask = dst.stages, + .dstAccessMask = dst.access, + } + ); +} +} + +namespace cvk { +constexpr std::optional layout_transition(VkImageLayout oldLayout, VkImageLayout newLayout) { + struct Entry { + VkImageLayout oldLayout; + VkImageLayout newLayout; + LayoutTransition transition; + }; + + constexpr BarrierScope none{}; + constexpr BarrierScope transferWrite{VK_PIPELINE_STAGE_2_TRANSFER_BIT, VK_ACCESS_2_TRANSFER_WRITE_BIT}; + constexpr BarrierScope transferRead{VK_PIPELINE_STAGE_2_TRANSFER_BIT, VK_ACCESS_2_TRANSFER_READ_BIT}; + constexpr BarrierScope fragmentRead{VK_PIPELINE_STAGE_2_FRAGMENT_SHADER_BIT, VK_ACCESS_2_SHADER_READ_BIT}; + constexpr BarrierScope colorWrite{ + VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT, + VK_ACCESS_2_COLOR_ATTACHMENT_WRITE_BIT + }; + constexpr BarrierScope color{ + VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT, + VK_ACCESS_2_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_2_COLOR_ATTACHMENT_WRITE_BIT + }; + constexpr BarrierScope depth{ + VK_PIPELINE_STAGE_2_EARLY_FRAGMENT_TESTS_BIT | VK_PIPELINE_STAGE_2_LATE_FRAGMENT_TESTS_BIT, + VK_ACCESS_2_DEPTH_STENCIL_ATTACHMENT_READ_BIT | VK_ACCESS_2_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT + }; + // attachments that the rendering also reads as input attachments + constexpr BarrierScope localRead{ + color.stages | depth.stages | VK_PIPELINE_STAGE_2_FRAGMENT_SHADER_BIT, + color.access | depth.access | VK_ACCESS_2_INPUT_ATTACHMENT_READ_BIT + }; + constexpr BarrierScope localReadWrite{ + color.stages | VK_PIPELINE_STAGE_2_LATE_FRAGMENT_TESTS_BIT, + VK_ACCESS_2_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_2_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT + }; + + constexpr std::array entries{ + Entry{ + VK_IMAGE_LAYOUT_UNDEFINED, + VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, + {none, transferWrite}, + }, + Entry{ + VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, + VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, + {transferWrite, fragmentRead}, + }, + Entry{ + VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, + VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, + {transferWrite, transferRead}, + }, + Entry{ + VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, + VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, + {transferRead, fragmentRead}, + }, + + Entry{ + VK_IMAGE_LAYOUT_UNDEFINED, + VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, + {none, color}, + }, + Entry{ + VK_IMAGE_LAYOUT_UNDEFINED, + VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_OPTIMAL, + {none, depth}, + }, + Entry{ + VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, + VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, + {colorWrite, fragmentRead}, + }, + Entry{ + VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, + VK_IMAGE_LAYOUT_PRESENT_SRC_KHR, + {colorWrite, none}, + }, + + Entry{ + VK_IMAGE_LAYOUT_UNDEFINED, + VK_IMAGE_LAYOUT_RENDERING_LOCAL_READ_KHR, + {none, localRead}, + }, + Entry{ + VK_IMAGE_LAYOUT_RENDERING_LOCAL_READ_KHR, + VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, + {localReadWrite, fragmentRead}, + }, + Entry{ + VK_IMAGE_LAYOUT_RENDERING_LOCAL_READ_KHR, + VK_IMAGE_LAYOUT_PRESENT_SRC_KHR, + {colorWrite, none}, + }, + }; + + auto const it = std::ranges::find_if(entries, [oldLayout, newLayout](Entry const& entry) { + return entry.oldLayout == oldLayout && entry.newLayout == newLayout; + }); + if(it == entries.end()) return std::nullopt; + return it->transition; +} +} diff --git a/cvk/incl/cvk/utility/format.hpp b/cvk/incl/cvk/utility/format.hpp index 6f5f06b7..55eb6234 100644 --- a/cvk/incl/cvk/utility/format.hpp +++ b/cvk/incl/cvk/utility/format.hpp @@ -23,6 +23,8 @@ CTH_FORMAT_ALIAS(VkPresentModeKHR, std::string_view, string_VkPresentModeKHR); CTH_FORMAT_ALIAS(VkObjectType, std::string_view, string_VkObjectType); +CTH_FORMAT_ALIAS(VkImageLayout, std::string_view, string_VkImageLayout); + CTH_FORMAT_CLASS( VkSurfaceFormatKHR, "{}, {}", diff --git a/cvk/src/data/img/image.cpp b/cvk/src/data/img/image.cpp index ff906935..73ea2662 100644 --- a/cvk/src/data/img/image.cpp +++ b/cvk/src/data/img/image.cpp @@ -2,9 +2,13 @@ #include "cvk/base/allocator.hpp" #include "cvk/base/core.hpp" +#include "cvk/data/buffer/base_buffer.hpp" +#include "cvk/render/cmd/cmd_buffer.hpp" +#include "cvk/render/sync/barrier.hpp" #include "cvk/utility/vk_exceptions.hpp" #include +#include namespace cvk { @@ -82,6 +86,87 @@ Image::State Image::release() { return state; } +void Image::copy(BaseCmdBuffer const& cmdBuffer, BaseBuffer const& src, size_t srcOffset, uint32_t mipLevel) const { + debug_check(*this); + BaseBuffer::debug_check(src); + CTH_CRITICAL(!cmdBuffer.recording(), "command buffer must be recording") {} + CTH_CRITICAL(mipLevel >= mipLevels(), "mip level must lie in the image") {} + CTH_CRITICAL(!std::has_single_bit(aspectMask()), "a copy needs exactly one aspect") {} + CTH_CRITICAL( + (src.config().usage & VK_BUFFER_USAGE_TRANSFER_SRC_BIT) == 0, + "src usage must have VK_BUFFER_USAGE_TRANSFER_SRC_BIT" + ) {} + CTH_CRITICAL( + (_config.usage & VK_IMAGE_USAGE_TRANSFER_DST_BIT) == 0, + "image usage must have VK_IMAGE_USAGE_TRANSFER_DST_BIT" + ) {} + + auto const layout = _levelLayouts[mipLevel]; + CTH_CRITICAL( + layout != VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL && layout != VK_IMAGE_LAYOUT_GENERAL, + "mip level must be in VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL or VK_IMAGE_LAYOUT_GENERAL" + ) { + details->add("layout: {}", layout); + } + + VkBufferImageCopy2 const region{ + .sType = VK_STRUCTURE_TYPE_BUFFER_IMAGE_COPY_2, + .pNext = nullptr, + .bufferOffset = srcOffset, + .bufferRowLength = 0, + .bufferImageHeight = 0, + .imageSubresource = { + .aspectMask = aspectMask(), + .mipLevel = mipLevel, + .baseArrayLayer = 0, + .layerCount = 1, + }, + .imageOffset = {0, 0, 0}, + .imageExtent = {std::max(_extent.width >> mipLevel, 1u), std::max(_extent.height >> mipLevel, 1u), 1}, + }; + VkCopyBufferToImageInfo2 const info{ + .sType = VK_STRUCTURE_TYPE_COPY_BUFFER_TO_IMAGE_INFO_2, + .pNext = nullptr, + .srcBuffer = src.get(), + .dstImage = get(), + .dstImageLayout = layout, + .regionCount = 1, + .pRegions = ®ion, + }; + core().functions()->vkCmdCopyBufferToImage2(cmdBuffer.get(), &info); +} + +void Image::transitionLayout( + BaseCmdBuffer const& cmdBuffer, + VkImageLayout newLayout, + uint32_t firstMipLevel, + uint32_t mipLevels +) { + debug_check(*this); + + auto const oldLayout = layout(firstMipLevel); + auto const transition = layout_transition(oldLayout, newLayout); + CTH_CRITICAL(!transition.has_value(), "layout transition has no table entry, use a Barrier") { + details->add("transition: {} -> {}", oldLayout, newLayout); + } + + Barrier barrier{}; + barrier.add( + *this, + ImageBarrierInfo::LayoutTransition(newLayout, transition->src, transition->dst, firstMipLevel, mipLevels) + ); + barrier.execute(cmdBuffer); +} + +void Image::setLayout(uint32_t firstMipLevel, uint32_t mipLevels, VkImageLayout layout) { + CTH_CRITICAL(firstMipLevel >= _levelLayouts.size(), "first mip level must lie in the image") {} + + size_t const count = mipLevels == VK_REMAINING_MIP_LEVELS ? _levelLayouts.size() - firstMipLevel : mipLevels; + CTH_CRITICAL(count > _levelLayouts.size() - firstMipLevel, "mip levels must lie in the image") {} + + std::ranges::fill_n(_levelLayouts.begin() + firstMipLevel, static_cast(count), layout); +} + void Image::destroy(DeviceTable table, VkImage vkImage) { CTH_WARN(vkImage == VK_NULL_HANDLE, "vkImage invalid (VK_NULL_HANDLE)") {} diff --git a/cvk/src/render/sync/barrier.cpp b/cvk/src/render/sync/barrier.cpp new file mode 100644 index 00000000..a2320726 --- /dev/null +++ b/cvk/src/render/sync/barrier.cpp @@ -0,0 +1,132 @@ +#include "cvk/render/sync/barrier.hpp" + +#include "cvk/base/core.hpp" +#include "cvk/data/buffer/base_buffer.hpp" +#include "cvk/data/img/image.hpp" +#include "cvk/render/cmd/cmd_buffer.hpp" + +#include + +#include +#include + + +namespace cvk { +namespace { + VkBufferMemoryBarrier2 buffer_barrier(BaseBuffer const& buffer, BufferBarrierInfo const& info) { + BaseBuffer::debug_check(buffer); + + return VkBufferMemoryBarrier2{ + .sType = VK_STRUCTURE_TYPE_BUFFER_MEMORY_BARRIER_2, + .pNext = nullptr, + .srcStageMask = info.src.stages, + .srcAccessMask = info.src.access, + .dstStageMask = info.dst.stages, + .dstAccessMask = info.dst.access, + .srcQueueFamilyIndex = info.src.queueFamilyIndex, + .dstQueueFamilyIndex = info.dst.queueFamilyIndex, + .buffer = buffer.get(), + .offset = info.offset, + .size = info.size, + }; + } + + /** + * @brief image barrier from the current layout of the mip range + */ + VkImageMemoryBarrier2 image_barrier(Image const& image, ImageBarrierInfo const& info) { + Image::debug_check(image); + + size_t const count = info.mipLevels == VK_REMAINING_MIP_LEVELS ? std::dynamic_extent : info.mipLevels; + auto const layouts = image.layouts().subspan(info.firstMipLevel, count); + auto const oldLayout = layouts.front(); + + CTH_CRITICAL( + std::ranges::any_of(layouts, [oldLayout](VkImageLayout layout) { return layout != oldLayout; }), + "every mip level of the range must have one layout" + ) {} + + auto const newLayout = info.newLayout.value_or(oldLayout); + CTH_CRITICAL( + newLayout == VK_IMAGE_LAYOUT_UNDEFINED || newLayout == VK_IMAGE_LAYOUT_PREINITIALIZED, + "new layout must not be undefined or preinitialized" + ) { + details->add("image layout: {}", oldLayout); + } + + return VkImageMemoryBarrier2{ + .sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER_2, + .pNext = nullptr, + .srcStageMask = info.src.stages, + .srcAccessMask = info.src.access, + .dstStageMask = info.dst.stages, + .dstAccessMask = info.dst.access, + .oldLayout = oldLayout, + .newLayout = newLayout, + .srcQueueFamilyIndex = info.src.queueFamilyIndex, + .dstQueueFamilyIndex = info.dst.queueFamilyIndex, + .image = image.get(), + .subresourceRange = { + .aspectMask = info.aspectMask != 0 ? info.aspectMask : image.aspectMask(), + .baseMipLevel = info.firstMipLevel, + .levelCount = info.mipLevels, + .baseArrayLayer = 0, + .layerCount = 1, + }, + }; + } +} + +void Barrier::add(Image& image, ImageInfo const& info) { + Image::debug_check(image); + CTH_CRITICAL(info.mipLevels == 0, "mip levels must not be 0") {} + CTH_CRITICAL(info.firstMipLevel >= image.mipLevels(), "first mip level must lie in the image") {} + CTH_CRITICAL( + info.mipLevels != VK_REMAINING_MIP_LEVELS && info.mipLevels > image.mipLevels() - info.firstMipLevel, + "mip levels must lie in the image" + ) {} + + _images.emplace_back(&image, info); +} + +void Barrier::add(BaseBuffer const& buffer, BufferInfo const& info) { + BaseBuffer::debug_check(buffer); + CTH_CRITICAL( + info.offset > buffer.size() || (info.size != VK_WHOLE_SIZE && info.size > buffer.size() - info.offset), + "range must lie in the buffer" + ) {} + + _buffers.emplace_back(&buffer, info); +} + +void Barrier::execute(BaseCmdBuffer const& cmdBuffer) { + BaseCmdBuffer::debug_check(cmdBuffer); + CTH_CRITICAL(!cmdBuffer.recording(), "command buffer must be recording") {} + + std::vector bufferBarriers{}; + bufferBarriers.reserve(_buffers.size()); + for(auto const& [buffer, info] : _buffers) + bufferBarriers.push_back(buffer_barrier(*buffer, info)); + + std::vector imageBarriers{}; + imageBarriers.reserve(_images.size()); + for(auto const& [image, info] : _images) + imageBarriers.push_back(image_barrier(*image, info)); + + VkDependencyInfo const dependency{ + .sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO, + .pNext = nullptr, + .dependencyFlags = _flags, + .memoryBarrierCount = static_cast(_memoryBarriers.size()), + .pMemoryBarriers = _memoryBarriers.data(), + .bufferMemoryBarrierCount = static_cast(bufferBarriers.size()), + .pBufferMemoryBarriers = bufferBarriers.data(), + .imageMemoryBarrierCount = static_cast(imageBarriers.size()), + .pImageMemoryBarriers = imageBarriers.data(), + }; + cmdBuffer.core().functions()->vkCmdPipelineBarrier2(cmdBuffer.get(), &dependency); + + for(auto const& [image, info] : _images) + if(info.newLayout.has_value()) image->setLayout(info.firstMipLevel, info.mipLevels, *info.newLayout); +} +} diff --git a/cvk/tests/src/context.hpp b/cvk/tests/src/context.hpp index 3f1d23f5..f79d42d0 100644 --- a/cvk/tests/src/context.hpp +++ b/cvk/tests/src/context.hpp @@ -3,12 +3,10 @@ #include "cvk/base/core.hpp" #include "cvk/base/instance.hpp" +#include "cvk/render/cmd/cmd_buffer.hpp" +#include -namespace cvk { -class BaseCmdBuffer; -class PrimaryCmdBuffer; -} namespace cvk::test { /** @@ -48,4 +46,20 @@ void submit_and_wait(Context const&, PrimaryCmdBuffer const&); * @pre the command buffer is recording */ void host_read_barrier(Context const&, BaseCmdBuffer const&); + +/** + * @brief records into a primary command buffer of a new pool, submits it and waits for the gpu + * @param record called with the recording PrimaryCmdBuffer const& + */ +template +void record_and_submit(Context const& context, Record&& record) { + CmdPool const pool{context.core, CmdPool::Config::Default(context.core.queue(0)), create}; + PrimaryCmdBuffer primary{pool, create}; + + primary.begin(VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT); + std::forward(record)(std::as_const(primary)); + primary.end(); + + submit_and_wait(context, primary); +} } diff --git a/cvk/tests/src/data/buffer/base_buffer_test.cpp b/cvk/tests/src/data/buffer/base_buffer_test.cpp index da008127..f68b6ae6 100644 --- a/cvk/tests/src/data/buffer/base_buffer_test.cpp +++ b/cvk/tests/src/data/buffer/base_buffer_test.cpp @@ -21,22 +21,6 @@ namespace { .memory = {.flags = VMA_ALLOCATION_CREATE_HOST_ACCESS_SEQUENTIAL_WRITE_BIT}, }; - /** - * @brief records into a primary command buffer, submits and waits - */ - template - void record_and_submit(test::Context const& context, Record record) { - CmdPool const pool{context.core, CmdPool::Config::Default(context.core.queue(0)), create}; - PrimaryCmdBuffer primary{pool, create}; - - primary.begin(VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT); - record(primary); - test::host_read_barrier(context, primary); - primary.end(); - - test::submit_and_wait(context, primary); - } - /** * @brief buffer the cpu reads back */ @@ -158,8 +142,9 @@ CVK_TEST(BaseBuffer, copy_a_range) { std::ranges::fill(data, 'c'); staging.memory().write(data, 16); - record_and_submit(context, [&](PrimaryCmdBuffer const& primary) { + test::record_and_submit(context, [&](PrimaryCmdBuffer const& primary) { readback.copy(primary, staging, data.size(), 16, 32); + test::host_read_barrier(context, primary); }); auto const mapped = readback.memory().map(data.size(), 32); @@ -176,7 +161,10 @@ CVK_TEST(BaseBuffer, copy_whole_size_fits_both_buffers) { std::ranges::fill(data, 'w'); staging.memory().write(data); - record_and_submit(context, [&](PrimaryCmdBuffer const& primary) { readback.copy(primary, staging); }); + test::record_and_submit(context, [&](PrimaryCmdBuffer const& primary) { + readback.copy(primary, staging); + test::host_read_barrier(context, primary); + }); auto const mapped = readback.memory().map(SIZE / 2); readback.memory().invalidate(); @@ -192,7 +180,10 @@ CVK_TEST(BaseBuffer, stage_the_whole_staging_buffer) { std::ranges::fill(data, 's'); staging.memory().write(data); - record_and_submit(context, [&](PrimaryCmdBuffer const& primary) { readback.stage(primary, staging, 8); }); + test::record_and_submit(context, [&](PrimaryCmdBuffer const& primary) { + readback.stage(primary, staging, 8); + test::host_read_barrier(context, primary); + }); auto const mapped = readback.memory().map(data.size(), 8); readback.memory().invalidate(); diff --git a/cvk/tests/src/data/buffer/buffer_test.cpp b/cvk/tests/src/data/buffer/buffer_test.cpp index 7969f82b..87b260f5 100644 --- a/cvk/tests/src/data/buffer/buffer_test.cpp +++ b/cvk/tests/src/data/buffer/buffer_test.cpp @@ -44,22 +44,6 @@ namespace { .memory = {.flags = VMA_ALLOCATION_CREATE_HOST_ACCESS_RANDOM_BIT}, }; - /** - * @brief records into a primary command buffer, submits and waits - */ - template - void record_and_submit(test::Context const& context, Record record) { - CmdPool const pool{context.core, CmdPool::Config::Default(context.core.queue(0)), create}; - PrimaryCmdBuffer primary{pool, create}; - - primary.begin(VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT); - record(primary); - test::host_read_barrier(context, primary); - primary.end(); - - test::submit_and_wait(context, primary); - } - /** * @brief Buffer is a valid type */ @@ -133,8 +117,9 @@ CVK_TEST(Buffer, copy_elements) { Buffer const readback{context.core, READBACK_CONFIG, COUNT}; staging.write(TRIANGLE, 1); - record_and_submit(context, [&](PrimaryCmdBuffer const& primary) { + test::record_and_submit(context, [&](PrimaryCmdBuffer const& primary) { readback.copy(primary, staging, TRIANGLE.size(), 1, 4); + test::host_read_barrier(context, primary); }); auto const mapped = readback.map(TRIANGLE.size(), 4); @@ -149,7 +134,10 @@ CVK_TEST(Buffer, stage_elements) { Buffer const readback{context.core, READBACK_CONFIG, COUNT}; staging.write(TRIANGLE); - record_and_submit(context, [&](PrimaryCmdBuffer const& primary) { readback.stage(primary, staging, 5); }); + test::record_and_submit(context, [&](PrimaryCmdBuffer const& primary) { + readback.stage(primary, staging, 5); + test::host_read_barrier(context, primary); + }); auto const mapped = readback.map(TRIANGLE.size(), 5); readback.invalidate(); diff --git a/cvk/tests/src/data/img/image_test.cpp b/cvk/tests/src/data/img/image_test.cpp index 2e9c48ba..0c70fe48 100644 --- a/cvk/tests/src/data/img/image_test.cpp +++ b/cvk/tests/src/data/img/image_test.cpp @@ -1,9 +1,11 @@ #include "context.hpp" #include "test.hpp" +#include "cvk/data/buffer/base_buffer.hpp" #include "cvk/data/img/image.hpp" #include +#include namespace cvk { @@ -20,6 +22,59 @@ namespace { .mipLevels = Image::evalMipLevelCount(EXTENT), }; + /** + * @brief image that the gpu copies into and out of, two mip levels + */ + constexpr Image::Config COPY_CONFIG{ + .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, + .format = VK_FORMAT_R8G8B8A8_UNORM, + .usage = VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT, + .mipLevels = 2, + }; + + constexpr BaseBuffer::Config STAGING_CONFIG{ + .usage = VK_BUFFER_USAGE_TRANSFER_SRC_BIT, + .memory = {.flags = VMA_ALLOCATION_CREATE_HOST_ACCESS_SEQUENTIAL_WRITE_BIT}, + }; + + constexpr BaseBuffer::Config READBACK_CONFIG{ + .usage = VK_BUFFER_USAGE_TRANSFER_DST_BIT, + .memory = {.flags = VMA_ALLOCATION_CREATE_HOST_ACCESS_RANDOM_BIT}, + }; + + /** + * @brief records a copy of the mip level into the buffer, without cvk: the image test needs a second path + */ + void copy_to_buffer( + test::Context const& context, + BaseCmdBuffer const& cmdBuffer, + Image const& image, + BaseBuffer const& dst, + size_t dstOffset, + uint32_t mipLevel + ) { + VkBufferImageCopy2 const region{ + .sType = VK_STRUCTURE_TYPE_BUFFER_IMAGE_COPY_2, + .pNext = nullptr, + .bufferOffset = dstOffset, + .bufferRowLength = 0, + .bufferImageHeight = 0, + .imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, mipLevel, 0, 1}, + .imageOffset = {0, 0, 0}, + .imageExtent = {image.extent().width >> mipLevel, image.extent().height >> mipLevel, 1}, + }; + VkCopyImageToBufferInfo2 const info{ + .sType = VK_STRUCTURE_TYPE_COPY_IMAGE_TO_BUFFER_INFO_2, + .pNext = nullptr, + .srcImage = image.get(), + .srcImageLayout = image.layout(mipLevel), + .dstBuffer = dst.get(), + .regionCount = 1, + .pRegions = ®ion, + }; + context.core.functions()->vkCmdCopyImageToBuffer2(cmdBuffer.get(), &info); + } + bool all_layouts(Image const& image, VkImageLayout layout) { return std::ranges::all_of(image.layouts(), [layout](VkImageLayout l) { return l == layout; }); } @@ -100,6 +155,39 @@ CVK_TEST(Image, create_depth_buffer) { EXPECT_EQ(image.aspectMask(), VK_IMAGE_ASPECT_DEPTH_BIT); } +CVK_TEST(Image, copy_from_a_buffer_into_two_mip_levels) { + test::Context const context{}; + Image image{context.core, COPY_CONFIG, EXTENT}; + + size_t const mip0 = size_t{EXTENT.width} * EXTENT.height * 4; + size_t const mip1 = mip0 / 4; + BaseBuffer const staging{context.core, STAGING_CONFIG, mip0 + mip1}; + BaseBuffer const readback{context.core, READBACK_CONFIG, mip0 + mip1}; + + std::vector pixels(mip0 + mip1); + for(size_t i = 0; i < pixels.size(); ++i) + pixels[i] = static_cast(i % 251); + staging.memory().write(pixels); + + test::record_and_submit(context, [&](PrimaryCmdBuffer const& primary) { + image.transitionLayout(primary, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL); + image.copy(primary, staging, 0, 0); + image.copy(primary, staging, mip0, 1); + + image.transitionLayout(primary, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL); + copy_to_buffer(context, primary, image, readback, 0, 0); + copy_to_buffer(context, primary, image, readback, mip0, 1); + test::host_read_barrier(context, primary); + }); + + auto const mapped = readback.memory().map(pixels.size()); + readback.memory().invalidate(); + EXPECT_TRUE(std::ranges::equal(mapped, pixels)); + readback.memory().unmap(); + + EXPECT_EQ(image.layout(1), VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL); +} + CVK_TEST(Image, release_and_wrap) { test::Context const context{}; Image image{context.core, TEXTURE_CONFIG, EXTENT}; diff --git a/cvk/tests/src/render/sync/barrier_test.cpp b/cvk/tests/src/render/sync/barrier_test.cpp new file mode 100644 index 00000000..21ac1887 --- /dev/null +++ b/cvk/tests/src/render/sync/barrier_test.cpp @@ -0,0 +1,130 @@ +#include "context.hpp" +#include "test.hpp" + +#include "cvk/data/buffer/base_buffer.hpp" +#include "cvk/data/img/image.hpp" +#include "cvk/render/cmd/cmd_buffer.hpp" +#include "cvk/render/sync/barrier.hpp" + +#include + + +namespace cvk { +namespace { + constexpr VkExtent2D EXTENT{16, 16}; + + constexpr Image::Config IMAGE_CONFIG{ + .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, + .format = VK_FORMAT_R8G8B8A8_UNORM, + .usage = VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT, + .mipLevels = Image::evalMipLevelCount(EXTENT), + }; + + constexpr BarrierScope TRANSFER_WRITE{VK_PIPELINE_STAGE_2_TRANSFER_BIT, VK_ACCESS_2_TRANSFER_WRITE_BIT}; + constexpr BarrierScope FRAGMENT_READ{VK_PIPELINE_STAGE_2_FRAGMENT_SHADER_BIT, VK_ACCESS_2_SHADER_READ_BIT}; + + bool all_layouts(Image const& image, VkImageLayout layout) { + return std::ranges::all_of(image.layouts(), [layout](VkImageLayout l) { return l == layout; }); + } +} + +CVK_TEST(Barrier, layout_transition_table) { + constexpr auto toTransfer = layout_transition(VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL); + static_assert(toTransfer.has_value()); + static_assert(toTransfer->src.access == VK_ACCESS_2_NONE); + static_assert(toTransfer->dst.access == VK_ACCESS_2_TRANSFER_WRITE_BIT); + + constexpr auto toLocalRead = layout_transition(VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_RENDERING_LOCAL_READ_KHR); + static_assert(toLocalRead.has_value()); + static_assert((toLocalRead->dst.access & VK_ACCESS_2_INPUT_ATTACHMENT_READ_BIT) != 0); + + static_assert(!layout_transition(VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_IMAGE_LAYOUT_UNDEFINED).has_value()); +} + +CVK_TEST(Barrier, layout_transition_info_covers_the_remaining_mip_levels) { + constexpr auto info = ImageBarrierInfo::LayoutTransition( + VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, + BarrierScope{}, + TRANSFER_WRITE + ); + + static_assert(info.mipLevels == VK_REMAINING_MIP_LEVELS); + static_assert(info.newLayout == VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL); +} + +CVK_TEST(Barrier, memory_barriers_are_constexpr) { + static_assert([] { + Barrier barrier{VK_DEPENDENCY_BY_REGION_BIT}; + barrier.add(TRANSFER_WRITE, FRAGMENT_READ); + bool const added = !barrier.empty(); + barrier.clear(); + return added && barrier.empty(); + }()); +} + +CVK_TEST(Barrier, add_and_clear) { + test::Context const context{}; + BaseBuffer const buffer{context.core, {.usage = VK_BUFFER_USAGE_TRANSFER_DST_BIT}, 64}; + Barrier barrier{}; + EXPECT_TRUE(barrier.empty()); + + barrier.add(TRANSFER_WRITE, FRAGMENT_READ); + barrier.add(buffer, {.src = TRANSFER_WRITE, .dst = FRAGMENT_READ}); + EXPECT_FALSE(barrier.empty()); + + barrier.clear(); + EXPECT_TRUE(barrier.empty()); +} + +CVK_TEST(Barrier, execute_memory_and_buffer_barriers) { + test::Context const context{}; + BaseBuffer const buffer{context.core, {.usage = VK_BUFFER_USAGE_TRANSFER_DST_BIT}, 64}; + Barrier barrier{VK_DEPENDENCY_BY_REGION_BIT}; + barrier.add(TRANSFER_WRITE, FRAGMENT_READ); + barrier.add(buffer, {.src = TRANSFER_WRITE, .dst = FRAGMENT_READ, .offset = 16, .size = 32}); + + test::record_and_submit(context, [&](PrimaryCmdBuffer const& primary) { barrier.execute(primary); }); + + EXPECT_EQ(barrier.flags(), VK_DEPENDENCY_BY_REGION_BIT); +} + +CVK_TEST(Barrier, image_barrier_updates_the_layout_tracking) { + test::Context const context{}; + Image image{context.core, IMAGE_CONFIG, EXTENT}; + + Barrier barrier{}; + barrier.add(image, ImageBarrierInfo::LayoutTransition(VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, {}, TRANSFER_WRITE)); + test::record_and_submit(context, [&](PrimaryCmdBuffer const& primary) { barrier.execute(primary); }); + + EXPECT_TRUE(all_layouts(image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL)); +} + +CVK_TEST(Barrier, image_barrier_on_a_mip_range) { + test::Context const context{}; + Image image{context.core, IMAGE_CONFIG, EXTENT}; + + test::record_and_submit(context, [&](PrimaryCmdBuffer const& primary) { + image.transitionLayout(primary, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL); + image.transitionLayout(primary, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, 1, 2); + }); + + EXPECT_EQ(image.layout(0), VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL); + EXPECT_EQ(image.layout(1), VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL); + EXPECT_EQ(image.layout(2), VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL); + EXPECT_EQ(image.layout(3), VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL); +} + +CVK_TEST(Barrier, execute_twice_starts_from_the_new_layout) { + test::Context const context{}; + Image image{context.core, IMAGE_CONFIG, EXTENT}; + Barrier barrier{}; + barrier.add(image, ImageBarrierInfo::LayoutTransition(VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, {}, TRANSFER_WRITE)); + + test::record_and_submit(context, [&](PrimaryCmdBuffer const& primary) { + barrier.execute(primary); + barrier.execute(primary); + }); + + EXPECT_TRUE(all_layouts(image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL)); +} +} From 6eaf2ca3b78013a0cbcd18dfd1a6477401ad4cf4 Mon Sep 17 00:00:00 2001 From: lukas <89145039+codethinki@users.noreply.github.com> Date: Sat, 17 Oct 2026 02:54:17 +0200 Subject: [PATCH 051/135] cvk: port image view ImageView takes its image in the base constructor (parent rule), the core comes from the image. Config: base mip level, mip levels (VK_REMAINING_MIP_LEVELS instead of the legacy 0) and the aspect mask (legacy TODO, 0 -> the aspect of the image). 2D only, like Image. --- cvk/incl/cvk/data/img/image_view.hpp | 128 ++++++++++++++++++++ cvk/incl/cvk/data/img/image_view_config.hpp | 35 ++++++ cvk/src/data/img/image_view.cpp | 83 +++++++++++++ cvk/tests/src/data/img/image_view_test.cpp | 102 ++++++++++++++++ 4 files changed, 348 insertions(+) create mode 100644 cvk/incl/cvk/data/img/image_view.hpp create mode 100644 cvk/incl/cvk/data/img/image_view_config.hpp create mode 100644 cvk/src/data/img/image_view.cpp create mode 100644 cvk/tests/src/data/img/image_view_test.cpp diff --git a/cvk/incl/cvk/data/img/image_view.hpp b/cvk/incl/cvk/data/img/image_view.hpp new file mode 100644 index 00000000..b7476c4f --- /dev/null +++ b/cvk/incl/cvk/data/img/image_view.hpp @@ -0,0 +1,128 @@ +#pragma once +#include "image_view_config.hpp" + +#include "cvk/base/device_table.hpp" +#include "cvk/data/img/image.hpp" +#include "cvk/utility/constants.hpp" +#include "cvk/utility/core_handle.hpp" +#include "cvk/utility/types.hpp" + +#include + +#include + + +namespace cvk { +/** + * @brief VkImageView wrapper, 2D view of a mip range of its image + */ +class ImageView { +public: + using vk_handle_t = VkImageView; + using Config = ImageViewConfig; + struct State; + + /** + * @brief base constructor + * @details the core comes from the image + */ + ImageView(Image const&, Config const&); + + /** + * @brief constructs and wraps + * @details calls: + * - @ref ImageView(Image const&, Config const&) + * - @ref wrap() + */ + ImageView(Image const&, Config const&, State const&); + + /** + * @brief constructs and creates + * @details calls: + * - @ref ImageView(Image const&, Config const&) + * - @ref create() + */ + explicit ImageView(Image const&, Config const&, create_t); + + /** + * @note calls @ref optDestroy() + */ + ~ImageView() { optDestroy(); } + + /** + * @brief wraps the state + * @pre the view was created for the image with the config + * @post @ref created() + * @note calls @ref optDestroy() + */ + void wrap(State const&); + + /** + * @brief creates the view + * @pre @ref Image::created() + * @pre the mip range lies in the image + * @post @ref created() + * @note calls @ref optDestroy() + * @throws cvk::vk_result_exception result of vkCreateImageView() + */ + void create(); + + /** + * @brief destroys and forgets + * @pre @ref created() + * @pre the gpu does not use the view + */ + void destroy(); + + /** + * @brief if @ref created() -> calls @ref destroy() + */ + void optDestroy() { if(created()) destroy(); } + + /** + * @brief releases ownership, returns the state and forgets + * @pre @ref created() + */ + State release(); + + static void destroy(DeviceTable table, VkImageView); + + [[nodiscard]] constexpr bool created() const { return static_cast(_handle); } + [[nodiscard]] constexpr vk_handle_t get() const { return _handle.get(); } + [[nodiscard]] constexpr Core const& core() const { return _handle.core(); } + [[nodiscard]] constexpr Image const& image() const { return *_image; } + [[nodiscard]] constexpr Config const& config() const { return _config; } + + /** + * @return the subresource range of the view in the image + */ + [[nodiscard]] constexpr VkImageSubresourceRange range() const { return _config.range(_image->aspectMask()); } + +private: + void forget(); + + core_handle _handle; + not_null _image; + Config _config; + +public: + ImageView(ImageView const& other) = delete; + ImageView(ImageView&& other) noexcept = default; + ImageView& operator=(ImageView const& other) = delete; + ImageView& operator=(ImageView&& other) noexcept = default; + + static constexpr void debug_check(ImageView const&); +}; +} + +namespace cvk { +struct ImageView::State { + vk_not_null vkImageView; +}; +} + +namespace cvk { +constexpr void ImageView::debug_check(ImageView const& view) { + CTH_CRITICAL(!view.created(), "image view must be created") {} +} +} diff --git a/cvk/incl/cvk/data/img/image_view_config.hpp b/cvk/incl/cvk/data/img/image_view_config.hpp new file mode 100644 index 00000000..04003be3 --- /dev/null +++ b/cvk/incl/cvk/data/img/image_view_config.hpp @@ -0,0 +1,35 @@ +#pragma once +#include + + +namespace cvk { +/** + * @brief mip range and aspect of an image view + */ +struct ImageViewConfig { + uint32_t baseMipLevel = 0; + + /** + * @brief VK_REMAINING_MIP_LEVELS -> to the last mip level of the image + */ + uint32_t mipLevels = VK_REMAINING_MIP_LEVELS; + + /** + * @brief 0 -> the aspect mask of the image + */ + VkImageAspectFlags aspectMask = 0; + + /** + * @param imageAspectMask aspect mask of the image + */ + [[nodiscard]] constexpr VkImageSubresourceRange range(VkImageAspectFlags imageAspectMask) const { + return VkImageSubresourceRange{ + .aspectMask = aspectMask != 0 ? aspectMask : imageAspectMask, + .baseMipLevel = baseMipLevel, + .levelCount = mipLevels, + .baseArrayLayer = 0, + .layerCount = 1, + }; + } +}; +} diff --git a/cvk/src/data/img/image_view.cpp b/cvk/src/data/img/image_view.cpp new file mode 100644 index 00000000..ec50ee84 --- /dev/null +++ b/cvk/src/data/img/image_view.cpp @@ -0,0 +1,83 @@ +#include "cvk/data/img/image_view.hpp" + +#include "cvk/base/core.hpp" +#include "cvk/utility/vk_exceptions.hpp" + + +namespace cvk { +ImageView::ImageView(Image const& image, Config const& config) : _handle{image.core()}, _image{&image}, + _config{config} {} + +ImageView::ImageView(Image const& image, Config const& config, State const& state) : ImageView{image, config} { + wrap(state); +} + +ImageView::ImageView(Image const& image, Config const& config, create_t) : ImageView{image, config} { create(); } + +void ImageView::wrap(State const& state) { + optDestroy(); + + _handle.reset(state.vkImageView.get(), core()); +} + +void ImageView::create() { + Image::debug_check(*_image); + CTH_CRITICAL(_config.baseMipLevel >= _image->mipLevels(), "base mip level must lie in the image") {} + CTH_CRITICAL( + _config.mipLevels != VK_REMAINING_MIP_LEVELS && _config.mipLevels > _image->mipLevels() - _config.baseMipLevel, + "mip levels must lie in the image" + ) {} + + optDestroy(); + + VkImageViewCreateInfo const info{ + .sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO, + .pNext = nullptr, + .flags = 0, + .image = _image->get(), + .viewType = VK_IMAGE_VIEW_TYPE_2D, + .format = _image->format(), + .components = { + .r = VK_COMPONENT_SWIZZLE_IDENTITY, + .g = VK_COMPONENT_SWIZZLE_IDENTITY, + .b = VK_COMPONENT_SWIZZLE_IDENTITY, + .a = VK_COMPONENT_SWIZZLE_IDENTITY, + }, + .subresourceRange = range(), + }; + + VkImageView ptr = VK_NULL_HANDLE; + auto const result = core().functions()->vkCreateImageView(core().vkDevice(), &info, nullptr, &ptr); + + CTH_STABLE_ERR(result != VK_SUCCESS, "failed to create image view") { + forget(); + throw cvk::vk_result_exception{result, details->exception()}; + } + + _handle.reset(ptr, core()); +} + +void ImageView::destroy() { + debug_check(*this); + + _handle.reset(); + + forget(); +} + +ImageView::State ImageView::release() { + debug_check(*this); + + State const state{_handle.release()}; + forget(); + return state; +} + +void ImageView::destroy(DeviceTable table, VkImageView vkImageView) { + CTH_WARN(vkImageView == VK_NULL_HANDLE, "vkImageView invalid (VK_NULL_HANDLE)") {} + + table->vkDestroyImageView(table.device(), vkImageView, nullptr); +} + +void ImageView::forget() {} +} diff --git a/cvk/tests/src/data/img/image_view_test.cpp b/cvk/tests/src/data/img/image_view_test.cpp new file mode 100644 index 00000000..bf252a32 --- /dev/null +++ b/cvk/tests/src/data/img/image_view_test.cpp @@ -0,0 +1,102 @@ +#include "context.hpp" +#include "test.hpp" + +#include "cvk/data/img/image.hpp" +#include "cvk/data/img/image_view.hpp" + + +namespace cvk { +namespace { + constexpr VkExtent2D EXTENT{32, 32}; + + constexpr Image::Config TEXTURE_CONFIG{ + .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, + .format = VK_FORMAT_R8G8B8A8_UNORM, + .usage = VK_IMAGE_USAGE_SAMPLED_BIT, + .mipLevels = Image::evalMipLevelCount(EXTENT), + }; +} + +CVK_TEST(ImageView, config_range) { + constexpr ImageView::Config config{.baseMipLevel = 2}; + constexpr auto range = config.range(VK_IMAGE_ASPECT_COLOR_BIT); + + static_assert(range.aspectMask == VK_IMAGE_ASPECT_COLOR_BIT); + static_assert(range.baseMipLevel == 2); + static_assert(range.levelCount == VK_REMAINING_MIP_LEVELS); + static_assert(range.layerCount == 1); + + constexpr ImageView::Config depth{.aspectMask = VK_IMAGE_ASPECT_DEPTH_BIT}; + static_assert(depth.range(VK_IMAGE_ASPECT_COLOR_BIT).aspectMask == VK_IMAGE_ASPECT_DEPTH_BIT); +} + +CVK_TEST(ImageView, base_constructor_does_not_create) { + test::Context const context{}; + Image const image{context.core, TEXTURE_CONFIG, EXTENT}; + + ImageView const view{image, {}}; + + EXPECT_FALSE(view.created()); + EXPECT_EQ(&view.image(), &image); + EXPECT_EQ(&view.core(), &context.core); +} + +CVK_TEST(ImageView, create_and_destroy) { + test::Context const context{}; + Image const image{context.core, TEXTURE_CONFIG, EXTENT}; + ImageView view{image, {}, create}; + + ASSERT_TRUE(view.created()); + EXPECT_EQ(view.range().levelCount, VK_REMAINING_MIP_LEVELS); + + view.destroy(); + EXPECT_FALSE(view.created()); +} + +CVK_TEST(ImageView, create_on_a_mip_range) { + test::Context const context{}; + Image const image{context.core, TEXTURE_CONFIG, EXTENT}; + + ImageView const view{image, {.baseMipLevel = 1, .mipLevels = 2}, create}; + + EXPECT_TRUE(view.created()); + EXPECT_EQ(view.range().baseMipLevel, 1); + EXPECT_EQ(view.range().levelCount, 2); +} + +CVK_TEST(ImageView, depth_view_takes_the_aspect_of_the_image) { + test::Context const context{}; + Image const image{context.core, Image::Config::DepthBuffer(VK_FORMAT_D16_UNORM, VK_SAMPLE_COUNT_1_BIT), EXTENT}; + + ImageView const view{image, {}, create}; + + EXPECT_TRUE(view.created()); + EXPECT_EQ(view.range().aspectMask, VK_IMAGE_ASPECT_DEPTH_BIT); +} + +CVK_TEST(ImageView, release_and_wrap) { + test::Context const context{}; + Image const image{context.core, TEXTURE_CONFIG, EXTENT}; + ImageView view{image, {}, create}; + auto const vkView = view.get(); + + auto const state = view.release(); + EXPECT_FALSE(view.created()); + + ImageView const wrapped{image, {}, state}; + EXPECT_EQ(wrapped.get(), vkView); +} + +CVK_TEST(ImageView, move_assignment_destroys_the_old_view) { + test::Context const context{}; + Image const image{context.core, TEXTURE_CONFIG, EXTENT}; + ImageView view{image, {}, create}; + ImageView other{image, {.baseMipLevel = 1}, create}; + auto const vkView = other.get(); + + view = std::move(other); + + EXPECT_EQ(view.get(), vkView); + EXPECT_FALSE(other.created()); +} +} From 8c81a273b62953eb5a7e3df83433edbb366ef0de Mon Sep 17 00:00:00 2001 From: lukas <89145039+codethinki@users.noreply.github.com> Date: Sat, 17 Oct 2026 03:25:24 +0200 Subject: [PATCH 052/135] cvk: dynamic rendering on the command buffer, local read, barriers inside a rendering BaseCmdBuffer::beginRendering() takes the attachment layouts from the layout tracking of the images, the default area fits every attachment, rendering() is the current rendering (formats, attachments, inherited for secondaries). setAttachmentLocations() and setInputAttachmentIndices() for local read. RenderingInheritance (was SecondaryCmdBuffer::Inheritance) and RenderingConfig in rendering_config.hpp. Barrier::execute() checks the rules of barriers inside a dynamic rendering: framebuffer space stages, VK_DEPENDENCY_BY_REGION_BIT, no layout transition, no queue family transfer, attachments in local read or general. PrimaryCmdBuffer::execute() checks the rendering contents flag. Tests: clear and read back, depth, secondary inheritance, local read with a barrier inside the rendering. --- cvk/incl/cvk/render/cmd/cmd_buffer.hpp | 109 +++++++---- cvk/incl/cvk/render/cmd/rendering_config.hpp | 90 +++++++++ cvk/incl/cvk/render/sync/barrier.hpp | 15 +- cvk/src/render/cmd/cmd_buffer.cpp | 193 ++++++++++++++++++- cvk/src/render/sync/barrier.cpp | 69 +++++++ cvk/tests/src/context.cpp | 30 +++ cvk/tests/src/context.hpp | 19 +- cvk/tests/src/data/img/image_test.cpp | 37 +--- cvk/tests/src/render/cmd/rendering_test.cpp | 184 ++++++++++++++++++ 9 files changed, 672 insertions(+), 74 deletions(-) create mode 100644 cvk/incl/cvk/render/cmd/rendering_config.hpp create mode 100644 cvk/tests/src/render/cmd/rendering_test.cpp diff --git a/cvk/incl/cvk/render/cmd/cmd_buffer.hpp b/cvk/incl/cvk/render/cmd/cmd_buffer.hpp index bc9bb611..e2b97135 100644 --- a/cvk/incl/cvk/render/cmd/cmd_buffer.hpp +++ b/cvk/incl/cvk/render/cmd/cmd_buffer.hpp @@ -1,5 +1,6 @@ #pragma once #include "cmd_pool.hpp" +#include "rendering_config.hpp" #include "cvk/base/device_table.hpp" #include "cvk/utility/constants.hpp" @@ -10,12 +11,14 @@ #include +#include #include #include namespace cvk { class Core; +class Image; /** * @brief VkCommandBuffer wrapper, allocated from its pool, base of @ref PrimaryCmdBuffer and @ref SecondaryCmdBuffer @@ -27,6 +30,31 @@ class BaseCmdBuffer { using vk_handle_t = VkCommandBuffer; struct State; + /** + * @brief the rendering the command buffer is inside + */ + struct Rendering { + /** + * @brief what a secondary inherits, the flags without VK_RENDERING_CONTENTS_SECONDARY_COMMAND_BUFFERS_BIT + */ + RenderingInheritance inheritance; + + /** + * @brief flags of the rendering + */ + VkRenderingFlags flags = 0; + + /** + * @brief images of the attachments, empty if inherited + */ + std::vector> attachments{}; + + /** + * @brief true -> a secondary that inherits the rendering of its primary + */ + bool inherited = false; + }; + /** * @brief wraps the state * @pre the pool of the command buffer allocated it with the level @@ -79,6 +107,43 @@ class BaseCmdBuffer { */ void reset(VkCommandBufferResetFlags flags = 0); + /** + * @brief begins a dynamic rendering + * @details the layouts of the attachments come from the layout tracking of their images, + * std::nullopt @ref RenderingConfig::area -> the extent that fits every attachment + * @pre @ref recording(), !@ref rendering() + * @pre the attachment views are created and have one mip level + * @pre the attachment layouts are attachment layouts (not undefined, transfer, shader read or present) + * @post @ref rendering() + */ + void beginRendering(RenderingConfig const&); + + /** + * @brief ends the dynamic rendering + * @pre @ref rendering() began with @ref beginRendering() + */ + void endRendering(); + + /** + * @brief maps the color attachments to the fragment shader outputs + * @param locations output location of each color attachment (VK_ATTACHMENT_UNUSED => no output) + * @pre @ref rendering() with one location per color attachment + */ + void setAttachmentLocations(std::span locations) const; + + /** + * @brief maps the attachments to the input attachments of the fragment shader + * @param colorIndices input attachment index of each color attachment (VK_ATTACHMENT_UNUSED => no input) + * @param depthIndex std::nullopt -> the depth attachment is an input attachment without index + * @param stencilIndex std::nullopt -> the stencil attachment is an input attachment without index + * @pre @ref rendering() with one index per color attachment + */ + void setInputAttachmentIndices( + std::span colorIndices, + std::optional depthIndex = std::nullopt, + std::optional stencilIndex = std::nullopt + ) const; + static void destroy(DeviceTable table, VkCommandPool, VkCommandBuffer); /** @@ -99,6 +164,13 @@ class BaseCmdBuffer { return created() && _recording && _beginResets == pool().resets(); } + /** + * @return the rendering the command buffer records into, nullptr -> outside of a rendering + */ + [[nodiscard]] constexpr Rendering const* rendering() const { + return recording() && _rendering.has_value() ? &*_rendering : nullptr; + } + protected: /** * @brief base constructor @@ -113,12 +185,13 @@ class BaseCmdBuffer { /** * @brief begins the recording + * @param inherited nullptr -> outside of a rendering, else the rendering of the primary * @pre @ref created() * @pre !@ref recording() * @post @ref recording() * @throws cvk::vk_result_exception result of vkBeginCommandBuffer() */ - void begin(VkCommandBufferBeginInfo const&); + void begin(VkCommandBufferBeginInfo const&, RenderingInheritance const* inherited = nullptr); private: /** @@ -143,6 +216,7 @@ class BaseCmdBuffer { VkCommandBufferLevel _level; bool _recording = false; uint64_t _beginResets = 0; + std::optional _rendering = std::nullopt; public: BaseCmdBuffer(BaseCmdBuffer const& other) = delete; @@ -223,7 +297,7 @@ namespace cvk { */ class SecondaryCmdBuffer : public BaseCmdBuffer { public: - struct Inheritance; + using Inheritance = RenderingInheritance; /** * @brief base constructor @@ -269,34 +343,3 @@ class SecondaryCmdBuffer : public BaseCmdBuffer { void begin(Inheritance const&, VkCommandBufferUsageFlags usage = 0); }; } - -namespace cvk { -/** - * @brief attachments of the rendering that executes the secondary command buffer - */ -struct SecondaryCmdBuffer::Inheritance { - std::vector colorFormats{}; - VkFormat depthFormat = VK_FORMAT_UNDEFINED; - VkFormat stencilFormat = VK_FORMAT_UNDEFINED; - VkSampleCountFlagBits samples = VK_SAMPLE_COUNT_1_BIT; - uint32_t viewMask = 0; - VkRenderingFlags flags = 0; - - /** - * @attention pColorAttachmentFormats points into @ref colorFormats - */ - [[nodiscard]] constexpr VkCommandBufferInheritanceRenderingInfo renderingInfo() const { - return VkCommandBufferInheritanceRenderingInfo{ - .sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_INHERITANCE_RENDERING_INFO, - .pNext = nullptr, - .flags = flags, - .viewMask = viewMask, - .colorAttachmentCount = static_cast(colorFormats.size()), - .pColorAttachmentFormats = colorFormats.data(), - .depthAttachmentFormat = depthFormat, - .stencilAttachmentFormat = stencilFormat, - .rasterizationSamples = samples, - }; - } -}; -} diff --git a/cvk/incl/cvk/render/cmd/rendering_config.hpp b/cvk/incl/cvk/render/cmd/rendering_config.hpp new file mode 100644 index 00000000..be1005dc --- /dev/null +++ b/cvk/incl/cvk/render/cmd/rendering_config.hpp @@ -0,0 +1,90 @@ +#pragma once +#include "cvk/utility/types.hpp" + +#include + +#include +#include + + +namespace cvk { +class ImageView; + +/** + * @brief formats, samples and view mask of a rendering, what a secondary command buffer inherits + */ +struct RenderingInheritance { + std::vector colorFormats{}; + VkFormat depthFormat = VK_FORMAT_UNDEFINED; + VkFormat stencilFormat = VK_FORMAT_UNDEFINED; + VkSampleCountFlagBits samples = VK_SAMPLE_COUNT_1_BIT; + uint32_t viewMask = 0; + VkRenderingFlags flags = 0; + + /** + * @attention pColorAttachmentFormats points into @ref colorFormats + */ + [[nodiscard]] constexpr VkCommandBufferInheritanceRenderingInfo renderingInfo() const { + return VkCommandBufferInheritanceRenderingInfo{ + .sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_INHERITANCE_RENDERING_INFO, + .pNext = nullptr, + .flags = flags, + .viewMask = viewMask, + .colorAttachmentCount = static_cast(colorFormats.size()), + .pColorAttachmentFormats = colorFormats.data(), + .depthAttachmentFormat = depthFormat, + .stencilAttachmentFormat = stencilFormat, + .rasterizationSamples = samples, + }; + } +}; + +/** + * @brief attachment of a rendering + */ +struct RenderingAttachment { + /** + * @pre one mip level + */ + not_null view; + + VkAttachmentLoadOp loadOp = VK_ATTACHMENT_LOAD_OP_LOAD; + VkAttachmentStoreOp storeOp = VK_ATTACHMENT_STORE_OP_STORE; + + /** + * @brief VK_ATTACHMENT_LOAD_OP_CLEAR -> the clear value + */ + VkClearValue clearValue{}; + + /** + * @brief nullptr -> no resolve + */ + ImageView const* resolveView = nullptr; + + /** + * @pre != VK_RESOLVE_MODE_NONE if @ref resolveView + */ + VkResolveModeFlagBits resolveMode = VK_RESOLVE_MODE_NONE; +}; + +/** + * @brief attachments, area and flags of a dynamic rendering + */ +struct RenderingConfig { + std::vector colorAttachments{}; + std::optional depthAttachment = std::nullopt; + std::optional stencilAttachment = std::nullopt; + + /** + * @brief std::nullopt -> the extent that fits every attachment + */ + std::optional area = std::nullopt; + + uint32_t viewMask = 0; + + /** + * @brief VK_RENDERING_CONTENTS_SECONDARY_COMMAND_BUFFERS_BIT -> secondaries record the rendering + */ + VkRenderingFlags flags = 0; +}; +} diff --git a/cvk/incl/cvk/render/sync/barrier.hpp b/cvk/incl/cvk/render/sync/barrier.hpp index ae5059d3..878353cb 100644 --- a/cvk/incl/cvk/render/sync/barrier.hpp +++ b/cvk/incl/cvk/render/sync/barrier.hpp @@ -1,5 +1,6 @@ #pragma once #include "cvk/base/queue/queue_family.hpp" +#include "cvk/render/cmd/cmd_buffer.hpp" #include "cvk/utility/types.hpp" #include @@ -9,12 +10,12 @@ #include #include #include +#include #include namespace cvk { class BaseBuffer; -class BaseCmdBuffer; class Image; /** @@ -159,9 +160,12 @@ class Barrier { /** * @brief records the barriers, then updates the layout tracking of the images + * @details inside a rendering: checks the rules of barriers inside a dynamic rendering * @pre @ref BaseCmdBuffer::recording() * @pre every image range has one layout * @pre a new layout is not VK_IMAGE_LAYOUT_UNDEFINED or VK_IMAGE_LAYOUT_PREINITIALIZED + * @pre inside a rendering: framebuffer space stages, VK_DEPENDENCY_BY_REGION_BIT, + * no layout transition, no queue family ownership transfer, attachments in local read or general layout */ void execute(BaseCmdBuffer const&); @@ -180,6 +184,15 @@ class Barrier { } private: + /** + * @brief checks the rules of barriers inside a dynamic rendering + */ + void debug_check_rendering( + BaseCmdBuffer::Rendering const&, + std::span, + std::span + ) const; + struct BufferEntry { not_null buffer; BufferInfo info; diff --git a/cvk/src/render/cmd/cmd_buffer.cpp b/cvk/src/render/cmd/cmd_buffer.cpp index 89c29dba..131e944a 100644 --- a/cvk/src/render/cmd/cmd_buffer.cpp +++ b/cvk/src/render/cmd/cmd_buffer.cpp @@ -1,6 +1,8 @@ #include "cvk/render/cmd/cmd_buffer.hpp" #include "cvk/base/core.hpp" +#include "cvk/data/img/image.hpp" +#include "cvk/data/img/image_view.hpp" #include "cvk/utility/vk_exceptions.hpp" #include @@ -10,6 +12,38 @@ namespace cvk { using std::span; +namespace { + /** + * @brief layout of the view from the layout tracking of its image + */ + VkImageLayout attachment_layout(ImageView const& view) { + auto const layout = view.image().layout(view.range().baseMipLevel); + + CTH_CRITICAL( + layout == VK_IMAGE_LAYOUT_UNDEFINED || layout == VK_IMAGE_LAYOUT_PREINITIALIZED + || layout == VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL || layout == VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL + || layout == VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL || layout == VK_IMAGE_LAYOUT_PRESENT_SRC_KHR, + "attachment must be in an attachment layout" + ) { + details->add("layout: {}", layout); + } + + return layout; + } + + uint32_t view_mip_levels(ImageView const& view) { + auto const range = view.range(); + return range.levelCount == VK_REMAINING_MIP_LEVELS ? view.image().mipLevels() - range.baseMipLevel + : range.levelCount; + } + + VkExtent2D view_extent(ImageView const& view) { + auto const mip = view.range().baseMipLevel; + auto const extent = view.image().extent(); + return {std::max(extent.width >> mip, 1u), std::max(extent.height >> mip, 1u)}; + } +} + BaseCmdBuffer::BaseCmdBuffer(CmdPool const& pool, VkCommandBufferLevel level) : _handle{pool}, _level{level} { CmdPool::debug_check(pool); } @@ -20,6 +54,7 @@ void BaseCmdBuffer::wrap(State const& state) { _handle.reset(state.vkBuffer.get(), pool()); pool().add(); _recording = false; + _rendering.reset(); } void BaseCmdBuffer::create() { @@ -46,6 +81,7 @@ void BaseCmdBuffer::create() { _handle.reset(ptr, pool()); pool().add(); _recording = false; + _rendering.reset(); } void BaseCmdBuffer::destroy() { @@ -69,9 +105,11 @@ BaseCmdBuffer::State BaseCmdBuffer::release() { void BaseCmdBuffer::end() { debug_check(*this); CTH_CRITICAL(!recording(), "command buffer must be recording") {} + CTH_CRITICAL(rendering() != nullptr && !rendering()->inherited, "rendering must be ended first") {} auto const result = core().functions()->vkEndCommandBuffer(get()); _recording = false; + _rendering.reset(); CVK_RESULT_STABLE_THROW(result != VK_SUCCESS, result, "failed to end command buffer") {} } @@ -85,10 +123,149 @@ void BaseCmdBuffer::reset(VkCommandBufferResetFlags flags) { auto const result = core().functions()->vkResetCommandBuffer(get(), flags); _recording = false; + _rendering.reset(); CVK_RESULT_STABLE_THROW(result != VK_SUCCESS, result, "failed to reset command buffer") {} } +void BaseCmdBuffer::beginRendering(RenderingConfig const& config) { + debug_check(*this); + CTH_CRITICAL(!recording(), "command buffer must be recording") {} + CTH_CRITICAL(rendering() != nullptr, "command buffer must be outside of a rendering") {} + + Rendering rendering{ + .inheritance = { + .viewMask = config.viewMask, + .flags = config.flags & ~VK_RENDERING_CONTENTS_SECONDARY_COMMAND_BUFFERS_BIT, + }, + .flags = config.flags, + }; + std::optional extent{}; + + auto const attachmentInfo = [&](RenderingAttachment const& attachment) { + ImageView const& view = *attachment.view; + ImageView::debug_check(view); + CTH_CRITICAL(view_mip_levels(view) != 1, "attachment views must have one mip level") {} + + auto const viewExtent = view_extent(view); + extent = extent.has_value() + ? VkExtent2D{std::min(extent->width, viewExtent.width), std::min(extent->height, viewExtent.height)} + : viewExtent; + rendering.inheritance.samples = view.image().config().samples; + rendering.attachments.emplace_back(&view.image()); + + VkImageView resolveView = VK_NULL_HANDLE; + VkImageLayout resolveLayout = VK_IMAGE_LAYOUT_UNDEFINED; + if(attachment.resolveView != nullptr) { + ImageView::debug_check(*attachment.resolveView); + CTH_CRITICAL(attachment.resolveMode == VK_RESOLVE_MODE_NONE, "a resolve view needs a resolve mode") {} + + resolveView = attachment.resolveView->get(); + resolveLayout = attachment_layout(*attachment.resolveView); + rendering.attachments.emplace_back(&attachment.resolveView->image()); + } + + return VkRenderingAttachmentInfo{ + .sType = VK_STRUCTURE_TYPE_RENDERING_ATTACHMENT_INFO, + .pNext = nullptr, + .imageView = view.get(), + .imageLayout = attachment_layout(view), + .resolveMode = attachment.resolveMode, + .resolveImageView = resolveView, + .resolveImageLayout = resolveLayout, + .loadOp = attachment.loadOp, + .storeOp = attachment.storeOp, + .clearValue = attachment.clearValue, + }; + }; + + std::vector colors{}; + colors.reserve(config.colorAttachments.size()); + for(auto const& attachment : config.colorAttachments) { + colors.push_back(attachmentInfo(attachment)); + rendering.inheritance.colorFormats.push_back(attachment.view->image().format()); + } + + std::optional depth{}; + if(config.depthAttachment.has_value()) { + depth = attachmentInfo(*config.depthAttachment); + rendering.inheritance.depthFormat = config.depthAttachment->view->image().format(); + } + + std::optional stencil{}; + if(config.stencilAttachment.has_value()) { + stencil = attachmentInfo(*config.stencilAttachment); + rendering.inheritance.stencilFormat = config.stencilAttachment->view->image().format(); + } + + CTH_CRITICAL(!extent.has_value() && !config.area.has_value(), "a rendering without attachments needs an area") {} + + VkRenderingInfo const info{ + .sType = VK_STRUCTURE_TYPE_RENDERING_INFO, + .pNext = nullptr, + .flags = config.flags, + .renderArea = config.area.value_or(VkRect2D{.offset = {0, 0}, .extent = extent.value_or(VkExtent2D{})}), + .layerCount = 1, + .viewMask = config.viewMask, + .colorAttachmentCount = static_cast(colors.size()), + .pColorAttachments = colors.data(), + .pDepthAttachment = depth.has_value() ? &*depth : nullptr, + .pStencilAttachment = stencil.has_value() ? &*stencil : nullptr, + }; + core().functions()->vkCmdBeginRendering(get(), &info); + + _rendering = std::move(rendering); +} + +void BaseCmdBuffer::endRendering() { + debug_check(*this); + CTH_CRITICAL(rendering() == nullptr || rendering()->inherited, "command buffer must be inside its own rendering") {} + + core().functions()->vkCmdEndRendering(get()); + + _rendering.reset(); +} + +void BaseCmdBuffer::setAttachmentLocations(span locations) const { + debug_check(*this); + CTH_CRITICAL(rendering() == nullptr, "command buffer must be inside a rendering") {} + CTH_CRITICAL( + locations.size() != rendering()->inheritance.colorFormats.size(), + "one location per color attachment" + ) {} + + VkRenderingAttachmentLocationInfoKHR const info{ + .sType = VK_STRUCTURE_TYPE_RENDERING_ATTACHMENT_LOCATION_INFO_KHR, + .pNext = nullptr, + .colorAttachmentCount = static_cast(locations.size()), + .pColorAttachmentLocations = locations.data(), + }; + core().functions()->vkCmdSetRenderingAttachmentLocationsKHR(get(), &info); +} + +void BaseCmdBuffer::setInputAttachmentIndices( + span colorIndices, + std::optional depthIndex, + std::optional stencilIndex +) const { + debug_check(*this); + CTH_CRITICAL(rendering() == nullptr, "command buffer must be inside a rendering") {} + CTH_CRITICAL( + colorIndices.size() != rendering()->inheritance.colorFormats.size(), + "one input attachment index per color attachment" + ) {} + + VkRenderingInputAttachmentIndexInfoKHR const info{ + .sType = VK_STRUCTURE_TYPE_RENDERING_INPUT_ATTACHMENT_INDEX_INFO_KHR, + .pNext = nullptr, + .colorAttachmentCount = static_cast(colorIndices.size()), + .pColorAttachmentInputIndices = colorIndices.data(), + .pDepthInputAttachmentIndex = depthIndex.has_value() ? &*depthIndex : nullptr, + .pStencilInputAttachmentIndex = stencilIndex.has_value() ? &*stencilIndex : nullptr, + }; + core().functions()->vkCmdSetRenderingInputAttachmentIndicesKHR(get(), &info); +} + void BaseCmdBuffer::destroy(DeviceTable table, VkCommandPool vkPool, VkCommandBuffer vkBuffer) { CTH_WARN(vkBuffer == VK_NULL_HANDLE, "vkBuffer invalid (VK_NULL_HANDLE)") {} @@ -101,7 +278,7 @@ void BaseCmdBuffer::destroy(DeviceTable table, VkCommandPool vkPool, spanvkFreeCommandBuffers(table.device(), vkPool, static_cast(vkBuffers.size()), vkBuffers.data()); } -void BaseCmdBuffer::begin(VkCommandBufferBeginInfo const& info) { +void BaseCmdBuffer::begin(VkCommandBufferBeginInfo const& info, RenderingInheritance const* inherited) { debug_check(*this); CTH_CRITICAL(recording(), "command buffer must not be recording") {} @@ -110,9 +287,15 @@ void BaseCmdBuffer::begin(VkCommandBufferBeginInfo const& info) { _recording = true; _beginResets = pool().resets(); + _rendering.reset(); + if(inherited != nullptr) + _rendering = Rendering{.inheritance = *inherited, .flags = inherited->flags, .inherited = true}; } -void BaseCmdBuffer::forget() { _recording = false; } +void BaseCmdBuffer::forget() { + _recording = false; + _rendering.reset(); +} void BaseCmdBuffer::pool_deleter::operator()(VkCommandBuffer vkBuffer) const { _pool->remove(); @@ -147,6 +330,10 @@ void PrimaryCmdBuffer::execute(span secondaries) const ), "secondaries must be created and not recording" ) {} + CTH_CRITICAL( + rendering() != nullptr && (rendering()->flags & VK_RENDERING_CONTENTS_SECONDARY_COMMAND_BUFFERS_BIT) == 0, + "the rendering must have VK_RENDERING_CONTENTS_SECONDARY_COMMAND_BUFFERS_BIT" + ) {} std::vector const vkBuffers{std::from_range, secondaries | std::views::transform(&SecondaryCmdBuffer::get)}; @@ -201,6 +388,6 @@ void SecondaryCmdBuffer::begin(Inheritance const& inheritance, VkCommandBufferUs .flags = usage | VK_COMMAND_BUFFER_USAGE_RENDER_PASS_CONTINUE_BIT, .pInheritanceInfo = &inheritanceInfo, }; - BaseCmdBuffer::begin(info); + BaseCmdBuffer::begin(info, &inheritance); } } diff --git a/cvk/src/render/sync/barrier.cpp b/cvk/src/render/sync/barrier.cpp index a2320726..89177ed9 100644 --- a/cvk/src/render/sync/barrier.cpp +++ b/cvk/src/render/sync/barrier.cpp @@ -8,11 +8,21 @@ #include #include +#include #include namespace cvk { namespace { + constexpr VkPipelineStageFlags2 FRAMEBUFFER_STAGES = VK_PIPELINE_STAGE_2_FRAGMENT_SHADER_BIT + | VK_PIPELINE_STAGE_2_EARLY_FRAGMENT_TESTS_BIT + | VK_PIPELINE_STAGE_2_LATE_FRAGMENT_TESTS_BIT + | VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT; + + [[nodiscard]] constexpr bool framebuffer_space(VkPipelineStageFlags2 srcStages, VkPipelineStageFlags2 dstStages) { + return ((srcStages | dstStages) & ~FRAMEBUFFER_STAGES) == 0; + } + VkBufferMemoryBarrier2 buffer_barrier(BaseBuffer const& buffer, BufferBarrierInfo const& info) { BaseBuffer::debug_check(buffer); @@ -99,6 +109,63 @@ void Barrier::add(BaseBuffer const& buffer, BufferInfo const& info) { _buffers.emplace_back(&buffer, info); } +void Barrier::debug_check_rendering( + BaseCmdBuffer::Rendering const& rendering, + std::span bufferBarriers, + std::span imageBarriers +) const { + bool srcStages = false; + bool framebufferSpace = true; + auto const check = [&](VkPipelineStageFlags2 src, VkPipelineStageFlags2 dst) { + srcStages |= src != VK_PIPELINE_STAGE_2_NONE; + framebufferSpace &= framebuffer_space(src, dst); + }; + + for(auto const& barrier : _memoryBarriers) + check(barrier.srcStageMask, barrier.dstStageMask); + for(auto const& barrier : bufferBarriers) + check(barrier.srcStageMask, barrier.dstStageMask); + for(auto const& barrier : imageBarriers) + check(barrier.srcStageMask, barrier.dstStageMask); + + CTH_CRITICAL(!framebufferSpace, "barriers inside a rendering must only use framebuffer space stages") {} + CTH_CRITICAL( + srcStages && (_flags & VK_DEPENDENCY_BY_REGION_BIT) == 0, + "barriers inside a rendering need VK_DEPENDENCY_BY_REGION_BIT" + ) {} + CTH_CRITICAL( + std::popcount(rendering.inheritance.viewMask) > 1 && (_flags & VK_DEPENDENCY_VIEW_LOCAL_BIT) == 0, + "barriers inside a rendering with more than one view need VK_DEPENDENCY_VIEW_LOCAL_BIT" + ) {} + auto const transfer = [](VkBufferMemoryBarrier2 const& barrier) { + return barrier.srcQueueFamilyIndex != barrier.dstQueueFamilyIndex; + }; + CTH_CRITICAL( + std::ranges::any_of(bufferBarriers, transfer), + "barriers inside a rendering have no queue family ownership transfer" + ) {} + + for(auto const [barrier, entry] : std::views::zip(imageBarriers, _images)) { + CTH_CRITICAL(barrier.oldLayout != barrier.newLayout, "barriers inside a rendering have no layout transition") {} + CTH_CRITICAL( + barrier.srcQueueFamilyIndex != barrier.dstQueueFamilyIndex, + "barriers inside a rendering have no queue family ownership transfer" + ) {} + bool const attachment = std::ranges::contains( + rendering.attachments, + static_cast(entry.image.get()), + ¬_null::get + ); + CTH_CRITICAL( + attachment && barrier.newLayout != VK_IMAGE_LAYOUT_RENDERING_LOCAL_READ_KHR + && barrier.newLayout != VK_IMAGE_LAYOUT_GENERAL, + "attachments inside a rendering must be in the rendering local read or general layout" + ) { + details->add("layout: {}", barrier.newLayout); + } + } +} + void Barrier::execute(BaseCmdBuffer const& cmdBuffer) { BaseCmdBuffer::debug_check(cmdBuffer); CTH_CRITICAL(!cmdBuffer.recording(), "command buffer must be recording") {} @@ -113,6 +180,8 @@ void Barrier::execute(BaseCmdBuffer const& cmdBuffer) { for(auto const& [image, info] : _images) imageBarriers.push_back(image_barrier(*image, info)); + if(auto const* rendering = cmdBuffer.rendering()) debug_check_rendering(*rendering, bufferBarriers, imageBarriers); + VkDependencyInfo const dependency{ .sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO, .pNext = nullptr, diff --git a/cvk/tests/src/context.cpp b/cvk/tests/src/context.cpp index 7c187131..4e369c50 100644 --- a/cvk/tests/src/context.cpp +++ b/cvk/tests/src/context.cpp @@ -65,4 +65,34 @@ void host_read_barrier(Context const& context, BaseCmdBuffer const& cmdBuffer) { }; context.core.functions()->vkCmdPipelineBarrier2(cmdBuffer.get(), &dependency); } + +void copy_to_buffer( + Context const& context, + BaseCmdBuffer const& cmdBuffer, + Image const& image, + BaseBuffer const& dst, + size_t dstOffset, + uint32_t mipLevel +) { + VkBufferImageCopy2 const region{ + .sType = VK_STRUCTURE_TYPE_BUFFER_IMAGE_COPY_2, + .pNext = nullptr, + .bufferOffset = dstOffset, + .bufferRowLength = 0, + .bufferImageHeight = 0, + .imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, mipLevel, 0, 1}, + .imageOffset = {0, 0, 0}, + .imageExtent = {image.extent().width >> mipLevel, image.extent().height >> mipLevel, 1}, + }; + VkCopyImageToBufferInfo2 const info{ + .sType = VK_STRUCTURE_TYPE_COPY_IMAGE_TO_BUFFER_INFO_2, + .pNext = nullptr, + .srcImage = image.get(), + .srcImageLayout = image.layout(mipLevel), + .dstBuffer = dst.get(), + .regionCount = 1, + .pRegions = ®ion, + }; + context.core.functions()->vkCmdCopyImageToBuffer2(cmdBuffer.get(), &info); +} } diff --git a/cvk/tests/src/context.hpp b/cvk/tests/src/context.hpp index f79d42d0..903d08eb 100644 --- a/cvk/tests/src/context.hpp +++ b/cvk/tests/src/context.hpp @@ -3,6 +3,8 @@ #include "cvk/base/core.hpp" #include "cvk/base/instance.hpp" +#include "cvk/data/buffer/base_buffer.hpp" +#include "cvk/data/img/image.hpp" #include "cvk/render/cmd/cmd_buffer.hpp" #include @@ -47,9 +49,22 @@ void submit_and_wait(Context const&, PrimaryCmdBuffer const&); */ void host_read_barrier(Context const&, BaseCmdBuffer const&); +/** + * @brief records a copy of the mip level into the buffer, without cvk: tests need a second path + * @pre the mip level is in a transfer src layout, the image has a color aspect + */ +void copy_to_buffer( + Context const&, + BaseCmdBuffer const&, + Image const&, + BaseBuffer const& dst, + size_t dstOffset, + uint32_t mipLevel +); + /** * @brief records into a primary command buffer of a new pool, submits it and waits for the gpu - * @param record called with the recording PrimaryCmdBuffer const& + * @param record called with the recording PrimaryCmdBuffer& */ template void record_and_submit(Context const& context, Record&& record) { @@ -57,7 +72,7 @@ void record_and_submit(Context const& context, Record&& record) { PrimaryCmdBuffer primary{pool, create}; primary.begin(VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT); - std::forward(record)(std::as_const(primary)); + std::forward(record)(primary); primary.end(); submit_and_wait(context, primary); diff --git a/cvk/tests/src/data/img/image_test.cpp b/cvk/tests/src/data/img/image_test.cpp index 0c70fe48..c3101db8 100644 --- a/cvk/tests/src/data/img/image_test.cpp +++ b/cvk/tests/src/data/img/image_test.cpp @@ -42,39 +42,6 @@ namespace { .memory = {.flags = VMA_ALLOCATION_CREATE_HOST_ACCESS_RANDOM_BIT}, }; - /** - * @brief records a copy of the mip level into the buffer, without cvk: the image test needs a second path - */ - void copy_to_buffer( - test::Context const& context, - BaseCmdBuffer const& cmdBuffer, - Image const& image, - BaseBuffer const& dst, - size_t dstOffset, - uint32_t mipLevel - ) { - VkBufferImageCopy2 const region{ - .sType = VK_STRUCTURE_TYPE_BUFFER_IMAGE_COPY_2, - .pNext = nullptr, - .bufferOffset = dstOffset, - .bufferRowLength = 0, - .bufferImageHeight = 0, - .imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, mipLevel, 0, 1}, - .imageOffset = {0, 0, 0}, - .imageExtent = {image.extent().width >> mipLevel, image.extent().height >> mipLevel, 1}, - }; - VkCopyImageToBufferInfo2 const info{ - .sType = VK_STRUCTURE_TYPE_COPY_IMAGE_TO_BUFFER_INFO_2, - .pNext = nullptr, - .srcImage = image.get(), - .srcImageLayout = image.layout(mipLevel), - .dstBuffer = dst.get(), - .regionCount = 1, - .pRegions = ®ion, - }; - context.core.functions()->vkCmdCopyImageToBuffer2(cmdBuffer.get(), &info); - } - bool all_layouts(Image const& image, VkImageLayout layout) { return std::ranges::all_of(image.layouts(), [layout](VkImageLayout l) { return l == layout; }); } @@ -175,8 +142,8 @@ CVK_TEST(Image, copy_from_a_buffer_into_two_mip_levels) { image.copy(primary, staging, mip0, 1); image.transitionLayout(primary, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL); - copy_to_buffer(context, primary, image, readback, 0, 0); - copy_to_buffer(context, primary, image, readback, mip0, 1); + test::copy_to_buffer(context, primary, image, readback, 0, 0); + test::copy_to_buffer(context, primary, image, readback, mip0, 1); test::host_read_barrier(context, primary); }); diff --git a/cvk/tests/src/render/cmd/rendering_test.cpp b/cvk/tests/src/render/cmd/rendering_test.cpp new file mode 100644 index 00000000..b37d4a0b --- /dev/null +++ b/cvk/tests/src/render/cmd/rendering_test.cpp @@ -0,0 +1,184 @@ +#include "context.hpp" +#include "test.hpp" + +#include "cvk/data/buffer/base_buffer.hpp" +#include "cvk/data/img/image.hpp" +#include "cvk/data/img/image_view.hpp" +#include "cvk/render/cmd/cmd_buffer.hpp" +#include "cvk/render/sync/barrier.hpp" + +#include +#include +#include +#include + + +namespace cvk { +namespace { + constexpr VkExtent2D EXTENT{16, 16}; + + constexpr Image::Config COLOR_CONFIG{ + .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, + .format = VK_FORMAT_R8G8B8A8_UNORM, + .usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT, + }; + + constexpr Image::Config LOCAL_READ_CONFIG{ + .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, + .format = VK_FORMAT_R8G8B8A8_UNORM, + .usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_INPUT_ATTACHMENT_BIT, + }; + + constexpr BarrierScope COLOR_WRITE{ + VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT, + VK_ACCESS_2_COLOR_ATTACHMENT_WRITE_BIT + }; + + RenderingAttachment clear_red(ImageView const& view) { + return RenderingAttachment{ + .view = &view, + .loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR, + .storeOp = VK_ATTACHMENT_STORE_OP_STORE, + .clearValue = {.color = {.float32 = {1.f, 0.f, 0.f, 1.f}}}, + }; + } +} + +CVK_TEST(Rendering, clear_a_color_attachment) { + test::Context const context{}; + Image image{context.core, COLOR_CONFIG, EXTENT}; + ImageView const view{image, {}, create}; + BaseBuffer const readback{ + context.core, + {.usage = VK_BUFFER_USAGE_TRANSFER_DST_BIT, .memory = {.flags = VMA_ALLOCATION_CREATE_HOST_ACCESS_RANDOM_BIT}}, + size_t{EXTENT.width} * EXTENT.height * 4 + }; + + test::record_and_submit(context, [&](PrimaryCmdBuffer& cmd) { + image.transitionLayout(cmd, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL); + + cmd.beginRendering({.colorAttachments = {clear_red(view)}}); + cmd.endRendering(); + + Barrier barrier{}; + barrier.add( + image, + ImageBarrierInfo::LayoutTransition( + VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, + COLOR_WRITE, + {VK_PIPELINE_STAGE_2_TRANSFER_BIT, VK_ACCESS_2_TRANSFER_READ_BIT} + ) + ); + barrier.execute(cmd); + + test::copy_to_buffer(context, cmd, image, readback, 0, 0); + test::host_read_barrier(context, cmd); + }); + + auto const pixels = readback.memory().map(readback.size()); + readback.memory().invalidate(); + auto const red = std::array{255, 0, 0, 255}; + for(auto const pixel : pixels | std::views::chunk(4)) + EXPECT_TRUE(std::ranges::equal(pixel, red, {}, [](char c) { return static_cast(c); })); + readback.memory().unmap(); +} + +CVK_TEST(Rendering, tracks_the_current_rendering) { + test::Context const context{}; + Image image{context.core, COLOR_CONFIG, EXTENT}; + ImageView const view{image, {}, create}; + + test::record_and_submit(context, [&](PrimaryCmdBuffer& cmd) { + image.transitionLayout(cmd, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL); + EXPECT_EQ(cmd.rendering(), nullptr); + + cmd.beginRendering({.colorAttachments = {clear_red(view)}}); + auto const* rendering = cmd.rendering(); + ASSERT_NE(rendering, nullptr); + EXPECT_FALSE(rendering->inherited); + ASSERT_EQ(rendering->inheritance.colorFormats.size(), 1); + EXPECT_EQ(rendering->inheritance.colorFormats[0], COLOR_CONFIG.format); + ASSERT_EQ(rendering->attachments.size(), 1); + EXPECT_EQ(rendering->attachments[0].get(), &image); + + cmd.endRendering(); + EXPECT_EQ(cmd.rendering(), nullptr); + }); +} + +CVK_TEST(Rendering, clear_a_depth_attachment) { + test::Context const context{}; + Image image{context.core, Image::Config::DepthBuffer(VK_FORMAT_D16_UNORM, VK_SAMPLE_COUNT_1_BIT), EXTENT}; + ImageView const view{image, {}, create}; + + test::record_and_submit(context, [&](PrimaryCmdBuffer& cmd) { + image.transitionLayout(cmd, VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_OPTIMAL); + + cmd.beginRendering( + { + .depthAttachment = RenderingAttachment{ + .view = &view, + .loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR, + .storeOp = VK_ATTACHMENT_STORE_OP_DONT_CARE, + .clearValue = {.depthStencil = {1.f, 0}}, + }, + } + ); + EXPECT_EQ(cmd.rendering()->inheritance.depthFormat, VK_FORMAT_D16_UNORM); + cmd.endRendering(); + }); +} + +CVK_TEST(Rendering, secondary_inherits_the_rendering) { + test::Context const context{}; + Image image{context.core, COLOR_CONFIG, EXTENT}; + ImageView const view{image, {}, create}; + CmdPool const pool{context.core, CmdPool::Config::Default(context.core.queue(0)), create}; + std::array secondaries{SecondaryCmdBuffer{pool, create}}; + + test::record_and_submit(context, [&](PrimaryCmdBuffer& cmd) { + image.transitionLayout(cmd, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL); + cmd.beginRendering( + {.colorAttachments = {clear_red(view)}, .flags = VK_RENDERING_CONTENTS_SECONDARY_COMMAND_BUFFERS_BIT} + ); + + auto& secondary = secondaries[0]; + secondary.begin(cmd.rendering()->inheritance, VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT); + ASSERT_NE(secondary.rendering(), nullptr); + EXPECT_TRUE(secondary.rendering()->inherited); + secondary.end(); + + cmd.execute(secondaries); + cmd.endRendering(); + }); +} + +CVK_TEST(Rendering, local_read_inside_the_rendering) { + test::Context const context{}; + Image image{context.core, LOCAL_READ_CONFIG, EXTENT}; + ImageView const view{image, {}, create}; + constexpr std::array FIRST{0}; + + test::record_and_submit(context, [&](PrimaryCmdBuffer& cmd) { + image.transitionLayout(cmd, VK_IMAGE_LAYOUT_RENDERING_LOCAL_READ_KHR); + + cmd.beginRendering({.colorAttachments = {clear_red(view)}}); + cmd.setAttachmentLocations(FIRST); + cmd.setInputAttachmentIndices(FIRST); + + Barrier barrier{VK_DEPENDENCY_BY_REGION_BIT}; + barrier.add( + image, + { + .src = COLOR_WRITE, + .dst = {VK_PIPELINE_STAGE_2_FRAGMENT_SHADER_BIT, VK_ACCESS_2_INPUT_ATTACHMENT_READ_BIT}, + } + ); + barrier.execute(cmd); + + cmd.endRendering(); + }); + + EXPECT_EQ(image.layout(0), VK_IMAGE_LAYOUT_RENDERING_LOCAL_READ_KHR); +} +} From 0d42ae4ce579d2a10c47f7f8f823b0edb1a435f2 Mon Sep 17 00:00:00 2001 From: lukas <89145039+codethinki@users.noreply.github.com> Date: Sat, 17 Oct 2026 03:34:31 +0200 Subject: [PATCH 053/135] cvk: draws, viewport and scissor, vertex and index buffer binds, push constants On the command buffer: setViewport() and setScissor() (default: the area of the rendering), bindVertexBuffer() and bindIndexBuffer() (Buffer overloads in elements, the index type follows from T), draw(), drawIndexed(), drawIndirect(), drawIndexedIndirect(). PipelineLayout::pushConstants(), the layout is part of the call. Tests: a fullscreen triangle in a push constant color with each draw, every pixel checked. Test shader fill.vert: vertex input without descriptors. --- cvk/incl/cvk/data/buffer/buffer.hpp | 14 ++ cvk/incl/cvk/render/cmd/cmd_buffer.hpp | 113 ++++++++++ .../pipeline/layout/pipeline_layout.hpp | 31 +++ cvk/src/render/cmd/cmd_buffer.cpp | 137 +++++++++++ .../pipeline/layout/pipeline_layout.cpp | 22 ++ cvk/tests/shaders/fill.vert | 5 + cvk/tests/src/render/cmd/draw_test.cpp | 212 ++++++++++++++++++ cvk/tests/src/shaders.hpp | 26 +++ 8 files changed, 560 insertions(+) create mode 100644 cvk/tests/shaders/fill.vert create mode 100644 cvk/tests/src/render/cmd/draw_test.cpp diff --git a/cvk/incl/cvk/data/buffer/buffer.hpp b/cvk/incl/cvk/data/buffer/buffer.hpp index ce7165bf..f767d315 100644 --- a/cvk/incl/cvk/data/buffer/buffer.hpp +++ b/cvk/incl/cvk/data/buffer/buffer.hpp @@ -6,12 +6,26 @@ #include #include +#include +#include #include #include #include namespace cvk { +/** + * @brief T is an index type of Vulkan 1.3 (VK_INDEX_TYPE_UINT8 needs an extension) + */ +template +concept index_element = std::same_as || std::same_as; + +/** + * @brief VkIndexType of T + */ +template +constexpr VkIndexType index_type = std::same_as ? VK_INDEX_TYPE_UINT16 : VK_INDEX_TYPE_UINT32; + /** * @brief @ref BaseBuffer with an element type, sizes and offsets in elements * @tparam T element, the gpu copies it byte by byte diff --git a/cvk/incl/cvk/render/cmd/cmd_buffer.hpp b/cvk/incl/cvk/render/cmd/cmd_buffer.hpp index e2b97135..09b5c0bc 100644 --- a/cvk/incl/cvk/render/cmd/cmd_buffer.hpp +++ b/cvk/incl/cvk/render/cmd/cmd_buffer.hpp @@ -2,6 +2,8 @@ #include "cmd_pool.hpp" #include "rendering_config.hpp" +#include "cvk/data/buffer/buffer.hpp" + #include "cvk/base/device_table.hpp" #include "cvk/utility/constants.hpp" #include "cvk/utility/types.hpp" @@ -53,6 +55,11 @@ class BaseCmdBuffer { * @brief true -> a secondary that inherits the rendering of its primary */ bool inherited = false; + + /** + * @brief render area, inherited -> unknown + */ + VkRect2D area{}; }; /** @@ -144,6 +151,112 @@ class BaseCmdBuffer { std::optional stencilIndex = std::nullopt ) const; + /** + * @brief sets viewport 0 to the area of the rendering, depth 0 to 1 + * @pre @ref rendering() began with @ref beginRendering() + */ + void setViewport() const; + + /** + * @brief sets viewport 0 + * @pre @ref recording() + */ + void setViewport(VkViewport const&) const; + + /** + * @brief sets scissor 0 to the area of the rendering + * @pre @ref rendering() began with @ref beginRendering() + */ + void setScissor() const; + + /** + * @brief sets scissor 0 + * @pre @ref recording() + */ + void setScissor(VkRect2D const&) const; + + /** + * @brief binds the buffer to the vertex input binding + * @param offset in bytes + * @pre @ref recording(), buffer created with VK_BUFFER_USAGE_VERTEX_BUFFER_BIT + */ + void bindVertexBuffer(uint32_t binding, BaseBuffer const&, size_t offset = 0) const; + + /** + * @brief binds the buffer to the vertex input binding + * @param firstVertex offset in elements + * @details calls @ref bindVertexBuffer(uint32_t, BaseBuffer const&, size_t) + */ + template + void bindVertexBuffer(uint32_t binding, Buffer const& buffer, size_t firstVertex = 0) const { + bindVertexBuffer(binding, static_cast(buffer), firstVertex * sizeof(T)); + } + + /** + * @brief binds the index buffer + * @param offset in bytes + * @pre @ref recording(), buffer created with VK_BUFFER_USAGE_INDEX_BUFFER_BIT + * @pre offset is a multiple of the index size + */ + void bindIndexBuffer(BaseBuffer const&, VkIndexType, size_t offset = 0) const; + + /** + * @brief binds the index buffer, the index type follows from T + * @param firstIndex offset in elements + * @details calls @ref bindIndexBuffer(BaseBuffer const&, VkIndexType, size_t) + */ + template + void bindIndexBuffer(Buffer const& buffer, size_t firstIndex = 0) const { + bindIndexBuffer(static_cast(buffer), index_type, firstIndex * sizeof(T)); + } + + /** + * @pre @ref rendering() + */ + void draw( + uint32_t vertexCount, + uint32_t instanceCount = 1, + uint32_t firstVertex = 0, + uint32_t firstInstance = 0 + ) const; + + /** + * @pre @ref rendering(), an index buffer is bound + */ + void drawIndexed( + uint32_t indexCount, + uint32_t instanceCount = 1, + uint32_t firstIndex = 0, + int32_t vertexOffset = 0, + uint32_t firstInstance = 0 + ) const; + + /** + * @brief draws with the VkDrawIndirectCommands in the buffer + * @param offset in bytes + * @pre @ref rendering(), buffer created with VK_BUFFER_USAGE_INDIRECT_BUFFER_BIT + * @pre the commands lie in the buffer + */ + void drawIndirect( + BaseBuffer const&, + size_t offset = 0, + uint32_t drawCount = 1, + uint32_t stride = sizeof(VkDrawIndirectCommand) + ) const; + + /** + * @brief draws with the VkDrawIndexedIndirectCommands in the buffer + * @param offset in bytes + * @pre @ref rendering(), buffer created with VK_BUFFER_USAGE_INDIRECT_BUFFER_BIT, an index buffer is bound + * @pre the commands lie in the buffer + */ + void drawIndexedIndirect( + BaseBuffer const&, + size_t offset = 0, + uint32_t drawCount = 1, + uint32_t stride = sizeof(VkDrawIndexedIndirectCommand) + ) const; + static void destroy(DeviceTable table, VkCommandPool, VkCommandBuffer); /** diff --git a/cvk/incl/cvk/render/pipeline/layout/pipeline_layout.hpp b/cvk/incl/cvk/render/pipeline/layout/pipeline_layout.hpp index 9b35dfcd..7a1df4f7 100644 --- a/cvk/incl/cvk/render/pipeline/layout/pipeline_layout.hpp +++ b/cvk/incl/cvk/render/pipeline/layout/pipeline_layout.hpp @@ -8,10 +8,13 @@ #include +#include +#include #include namespace cvk { +class BaseCmdBuffer; class Core; class DescriptorSetLayout; @@ -87,6 +90,34 @@ class PipelineLayout { */ State release(); + /** + * @brief records push constants + * @param offset in bytes + * @pre @ref created(), @ref BaseCmdBuffer::recording() + * @pre 0 < data.size(), offset and data.size() are multiples of 4 + * @pre the push constant ranges of the config cover the bytes for the stages + */ + void pushConstants( + BaseCmdBuffer const&, + VkShaderStageFlags stages, + std::span data, + uint32_t offset = 0 + ) const; + + /** + * @brief records the value as push constants + * @details calls @ref pushConstants(BaseCmdBuffer const&, VkShaderStageFlags, std::span, uint32_t) + */ + template requires std::is_trivially_copyable_v + void pushConstants( + BaseCmdBuffer const& cmdBuffer, + VkShaderStageFlags stages, + T const& value, + uint32_t offset = 0 + ) const { + pushConstants(cmdBuffer, stages, std::span{reinterpret_cast(&value), sizeof(T)}, offset); + } + static void destroy(DeviceTable table, VkPipelineLayout); [[nodiscard]] constexpr bool created() const { return static_cast(_handle); } diff --git a/cvk/src/render/cmd/cmd_buffer.cpp b/cvk/src/render/cmd/cmd_buffer.cpp index 131e944a..bb420301 100644 --- a/cvk/src/render/cmd/cmd_buffer.cpp +++ b/cvk/src/render/cmd/cmd_buffer.cpp @@ -37,6 +37,30 @@ namespace { : range.levelCount; } + void debug_check_draw(BaseCmdBuffer const& cmdBuffer) { + BaseCmdBuffer::debug_check(cmdBuffer); + CTH_CRITICAL(cmdBuffer.rendering() == nullptr, "draws need a rendering") {} + } + + void debug_check_indirect( + BaseBuffer const& buffer, + size_t offset, + uint32_t drawCount, + uint32_t stride, + size_t size + ) { + BaseBuffer::debug_check(buffer); + CTH_CRITICAL( + (buffer.config().usage & VK_BUFFER_USAGE_INDIRECT_BUFFER_BIT) == 0, + "buffer usage must have VK_BUFFER_USAGE_INDIRECT_BUFFER_BIT" + ) {} + CTH_CRITICAL(offset % 4 != 0, "offset must be a multiple of 4") {} + CTH_CRITICAL( + drawCount > 0 && offset + size_t{drawCount - 1} * stride + size > buffer.size(), + "the commands must lie in the buffer" + ) {} + } + VkExtent2D view_extent(ImageView const& view) { auto const mip = view.range().baseMipLevel; auto const extent = view.image().extent(); @@ -214,6 +238,7 @@ void BaseCmdBuffer::beginRendering(RenderingConfig const& config) { }; core().functions()->vkCmdBeginRendering(get(), &info); + rendering.area = info.renderArea; _rendering = std::move(rendering); } @@ -266,6 +291,118 @@ void BaseCmdBuffer::setInputAttachmentIndices( core().functions()->vkCmdSetRenderingInputAttachmentIndicesKHR(get(), &info); } +void BaseCmdBuffer::setViewport() const { + debug_check(*this); + CTH_CRITICAL(rendering() == nullptr || rendering()->inherited, "command buffer must be inside its own rendering") {} + + auto const& area = rendering()->area; + setViewport( + VkViewport{ + .x = static_cast(area.offset.x), + .y = static_cast(area.offset.y), + .width = static_cast(area.extent.width), + .height = static_cast(area.extent.height), + .minDepth = 0.f, + .maxDepth = 1.f, + } + ); +} + +void BaseCmdBuffer::setViewport(VkViewport const& viewport) const { + debug_check(*this); + CTH_CRITICAL(!recording(), "command buffer must be recording") {} + + core().functions()->vkCmdSetViewport(get(), 0, 1, &viewport); +} + +void BaseCmdBuffer::setScissor() const { + debug_check(*this); + CTH_CRITICAL(rendering() == nullptr || rendering()->inherited, "command buffer must be inside its own rendering") {} + + setScissor(rendering()->area); +} + +void BaseCmdBuffer::setScissor(VkRect2D const& scissor) const { + debug_check(*this); + CTH_CRITICAL(!recording(), "command buffer must be recording") {} + + core().functions()->vkCmdSetScissor(get(), 0, 1, &scissor); +} + +void BaseCmdBuffer::bindVertexBuffer(uint32_t binding, BaseBuffer const& buffer, size_t offset) const { + debug_check(*this); + BaseBuffer::debug_check(buffer); + CTH_CRITICAL(!recording(), "command buffer must be recording") {} + CTH_CRITICAL( + (buffer.config().usage & VK_BUFFER_USAGE_VERTEX_BUFFER_BIT) == 0, + "buffer usage must have VK_BUFFER_USAGE_VERTEX_BUFFER_BIT" + ) {} + CTH_CRITICAL(offset >= buffer.size(), "offset must lie in the buffer") {} + + VkBuffer const vkBuffer = buffer.get(); + VkDeviceSize const vkOffset = offset; + core().functions()->vkCmdBindVertexBuffers(get(), binding, 1, &vkBuffer, &vkOffset); +} + +void BaseCmdBuffer::bindIndexBuffer(BaseBuffer const& buffer, VkIndexType type, size_t offset) const { + debug_check(*this); + BaseBuffer::debug_check(buffer); + CTH_CRITICAL(!recording(), "command buffer must be recording") {} + CTH_CRITICAL( + (buffer.config().usage & VK_BUFFER_USAGE_INDEX_BUFFER_BIT) == 0, + "buffer usage must have VK_BUFFER_USAGE_INDEX_BUFFER_BIT" + ) {} + CTH_CRITICAL(offset >= buffer.size(), "offset must lie in the buffer") {} + CTH_CRITICAL( + offset % (type == VK_INDEX_TYPE_UINT16 ? sizeof(uint16_t) : sizeof(uint32_t)) != 0, + "offset must be a multiple of the index size" + ) {} + + core().functions()->vkCmdBindIndexBuffer(get(), buffer.get(), offset, type); +} + +void BaseCmdBuffer::draw( + uint32_t vertexCount, + uint32_t instanceCount, + uint32_t firstVertex, + uint32_t firstInstance +) const { + debug_check_draw(*this); + + core().functions()->vkCmdDraw(get(), vertexCount, instanceCount, firstVertex, firstInstance); +} + +void BaseCmdBuffer::drawIndexed( + uint32_t indexCount, + uint32_t instanceCount, + uint32_t firstIndex, + int32_t vertexOffset, + uint32_t firstInstance +) const { + debug_check_draw(*this); + + core().functions()->vkCmdDrawIndexed(get(), indexCount, instanceCount, firstIndex, vertexOffset, firstInstance); +} + +void BaseCmdBuffer::drawIndirect(BaseBuffer const& buffer, size_t offset, uint32_t drawCount, uint32_t stride) const { + debug_check_draw(*this); + debug_check_indirect(buffer, offset, drawCount, stride, sizeof(VkDrawIndirectCommand)); + + core().functions()->vkCmdDrawIndirect(get(), buffer.get(), offset, drawCount, stride); +} + +void BaseCmdBuffer::drawIndexedIndirect( + BaseBuffer const& buffer, + size_t offset, + uint32_t drawCount, + uint32_t stride +) const { + debug_check_draw(*this); + debug_check_indirect(buffer, offset, drawCount, stride, sizeof(VkDrawIndexedIndirectCommand)); + + core().functions()->vkCmdDrawIndexedIndirect(get(), buffer.get(), offset, drawCount, stride); +} + void BaseCmdBuffer::destroy(DeviceTable table, VkCommandPool vkPool, VkCommandBuffer vkBuffer) { CTH_WARN(vkBuffer == VK_NULL_HANDLE, "vkBuffer invalid (VK_NULL_HANDLE)") {} diff --git a/cvk/src/render/pipeline/layout/pipeline_layout.cpp b/cvk/src/render/pipeline/layout/pipeline_layout.cpp index ca95d976..9c29ed8f 100644 --- a/cvk/src/render/pipeline/layout/pipeline_layout.cpp +++ b/cvk/src/render/pipeline/layout/pipeline_layout.cpp @@ -2,6 +2,7 @@ #include "cvk/base/core.hpp" #include "cvk/base/physical_device.hpp" +#include "cvk/render/cmd/cmd_buffer.hpp" #include "cvk/render/pipeline/layout/descriptor_set_layout.hpp" #include "cvk/utility/vk_exceptions.hpp" @@ -80,6 +81,27 @@ PipelineLayout::State PipelineLayout::release() { return state; } +void PipelineLayout::pushConstants( + BaseCmdBuffer const& cmdBuffer, + VkShaderStageFlags stages, + std::span data, + uint32_t offset +) const { + debug_check(*this); + CTH_CRITICAL(!cmdBuffer.recording(), "command buffer must be recording") {} + CTH_CRITICAL(data.empty(), "push constants must not be empty") {} + CTH_CRITICAL(offset % 4 != 0 || data.size() % 4 != 0, "offset and size must be multiples of 4") {} + + core().functions()->vkCmdPushConstants( + cmdBuffer.get(), + get(), + stages, + offset, + static_cast(data.size()), + data.data() + ); +} + void PipelineLayout::destroy(DeviceTable table, VkPipelineLayout vkLayout) { CTH_WARN(vkLayout == VK_NULL_HANDLE, "vkLayout invalid (VK_NULL_HANDLE)") {} diff --git a/cvk/tests/shaders/fill.vert b/cvk/tests/shaders/fill.vert new file mode 100644 index 00000000..3df30248 --- /dev/null +++ b/cvk/tests/shaders/fill.vert @@ -0,0 +1,5 @@ +#version 450 + +layout(location = 0) in vec3 position; + +void main() { gl_Position = vec4(position, 1.0); } diff --git a/cvk/tests/src/render/cmd/draw_test.cpp b/cvk/tests/src/render/cmd/draw_test.cpp new file mode 100644 index 00000000..173cc2b2 --- /dev/null +++ b/cvk/tests/src/render/cmd/draw_test.cpp @@ -0,0 +1,212 @@ +#include "context.hpp" +#include "shaders.hpp" +#include "test.hpp" + +#include "cvk/data/buffer/buffer.hpp" +#include "cvk/data/img/image.hpp" +#include "cvk/data/img/image_view.hpp" +#include "cvk/render/cmd/cmd_buffer.hpp" +#include "cvk/render/pipeline/layout/pipeline_layout.hpp" +#include "cvk/render/pipeline/pipeline.hpp" +#include "cvk/render/pipeline/shader/shader.hpp" +#include "cvk/render/sync/barrier.hpp" + +#include +#include +#include +#include + + +namespace cvk { +namespace { + constexpr VkExtent2D EXTENT{16, 16}; + constexpr std::array COLOR_FORMATS{VK_FORMAT_R8G8B8A8_UNORM}; + constexpr std::array GREEN{0.f, 1.f, 0.f, 1.f}; + + constexpr BaseBuffer::Config READBACK_CONFIG{ + .usage = VK_BUFFER_USAGE_TRANSFER_DST_BIT, + .memory = {.flags = VMA_ALLOCATION_CREATE_HOST_ACCESS_RANDOM_BIT}, + }; + + struct Vec3 { + float x, y, z; + }; + + /** + * @brief one triangle that covers the whole viewport + */ + constexpr std::array FULLSCREEN{Vec3{-1.f, -1.f, 0.f}, Vec3{3.f, -1.f, 0.f}, Vec3{-1.f, 3.f, 0.f}}; + + template + constexpr Buffer::Config host_written(VkBufferUsageFlags usage) { + return {.usage = usage, .memory = {.flags = VMA_ALLOCATION_CREATE_HOST_ACCESS_SEQUENTIAL_WRITE_BIT}}; + } + + /** + * @brief pipeline that fills the triangle with the push constant color + */ + struct Fill { + explicit Fill(Core const& core) : layout{ + core, + PipelineLayout::Config{.pushConstantRanges = {{VK_SHADER_STAGE_FRAGMENT_BIT, 0, sizeof(GREEN)}}}, + create + }, + vertex{core, VK_SHADER_STAGE_VERTEX_BIT, test::shaders::FILL_VERT}, + fragment{core, VK_SHADER_STAGE_FRAGMENT_BIT, test::shaders::TRIANGLE_FRAG}, + pipeline{core, layout, config(), create}, + vertices{core, host_written(VK_BUFFER_USAGE_VERTEX_BUFFER_BIT), FULLSCREEN.size()} { + vertices.write(FULLSCREEN); + } + + [[nodiscard]] Pipeline::Config config() const { + auto config = Pipeline::Config::Default(COLOR_FORMATS); + config.addStage(vertex).addStage(fragment); + config.vertexBindings = {{.binding = 0, .stride = sizeof(Vec3), .inputRate = VK_VERTEX_INPUT_RATE_VERTEX}}; + config.vertexAttributes = { + {.location = 0, .binding = 0, .format = VK_FORMAT_R32G32B32_SFLOAT, .offset = 0}, + }; + config.rasterization.cullMode = VK_CULL_MODE_NONE; + return config; + } + + PipelineLayout layout; + Shader vertex; + Shader fragment; + Pipeline pipeline; + Buffer vertices; + }; + + /** + * @brief clears an attachment to black, draws with draw() and expects every pixel green + */ + template + void expect_filled(test::Context const& context, Fill const& fill, Draw draw) { + Image image{ + context.core, + { + .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, + .format = COLOR_FORMATS[0], + .usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT, + }, + EXTENT + }; + ImageView const view{image, {}, create}; + BaseBuffer const readback{context.core, READBACK_CONFIG, size_t{EXTENT.width} * EXTENT.height * 4}; + + test::record_and_submit(context, [&](PrimaryCmdBuffer& cmd) { + image.transitionLayout(cmd, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL); + cmd.beginRendering( + { + .colorAttachments = { + RenderingAttachment{ + .view = &view, + .loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR, + .storeOp = VK_ATTACHMENT_STORE_OP_STORE, + .clearValue = {.color = {.float32 = {0.f, 0.f, 0.f, 1.f}}}, + }, + }, + } + ); + + fill.pipeline.bind(cmd); + cmd.setViewport(); + cmd.setScissor(); + cmd.bindVertexBuffer(0, fill.vertices); + fill.layout.pushConstants(cmd, VK_SHADER_STAGE_FRAGMENT_BIT, GREEN); + draw(cmd); + + cmd.endRendering(); + + Barrier barrier{}; + barrier.add( + image, + ImageBarrierInfo::LayoutTransition( + VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, + {VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT, VK_ACCESS_2_COLOR_ATTACHMENT_WRITE_BIT}, + {VK_PIPELINE_STAGE_2_TRANSFER_BIT, VK_ACCESS_2_TRANSFER_READ_BIT} + ) + ); + barrier.execute(cmd); + + test::copy_to_buffer(context, cmd, image, readback, 0, 0); + test::host_read_barrier(context, cmd); + }); + + auto const pixels = readback.memory().map(readback.size()); + readback.memory().invalidate(); + constexpr std::array green{0, 255, 0, 255}; + for(auto const pixel : pixels | std::views::chunk(4)) + EXPECT_TRUE(std::ranges::equal(pixel, green, {}, [](char c) { return static_cast(c); })); + readback.memory().unmap(); + } +} + +CVK_TEST(Draw, index_type_follows_the_element) { + static_assert(index_type == VK_INDEX_TYPE_UINT16); + static_assert(index_type == VK_INDEX_TYPE_UINT32); + static_assert(!index_element); + static_assert(!index_element); +} + +CVK_TEST(Draw, draw) { + test::Context const context{}; + Fill const fill{context.core}; + + expect_filled(context, fill, [](PrimaryCmdBuffer const& cmd) { cmd.draw(3); }); +} + +CVK_TEST(Draw, draw_indexed) { + test::Context const context{}; + Fill const fill{context.core}; + Buffer indices{context.core, host_written(VK_BUFFER_USAGE_INDEX_BUFFER_BIT), 3}; + indices.write(std::array{0, 1, 2}); + + expect_filled(context, fill, [&](PrimaryCmdBuffer const& cmd) { + cmd.bindIndexBuffer(indices); + cmd.drawIndexed(3); + }); +} + +CVK_TEST(Draw, draw_indirect) { + test::Context const context{}; + Fill const fill{context.core}; + Buffer commands{ + context.core, + host_written(VK_BUFFER_USAGE_INDIRECT_BUFFER_BIT), + 1 + }; + commands.write( + std::array{VkDrawIndirectCommand{.vertexCount = 3, .instanceCount = 1, .firstVertex = 0, .firstInstance = 0}} + ); + + expect_filled(context, fill, [&](PrimaryCmdBuffer const& cmd) { cmd.drawIndirect(commands); }); +} + +CVK_TEST(Draw, draw_indexed_indirect) { + test::Context const context{}; + Fill const fill{context.core}; + Buffer indices{context.core, host_written(VK_BUFFER_USAGE_INDEX_BUFFER_BIT), 3}; + indices.write(std::array{0, 1, 2}); + Buffer commands{ + context.core, + host_written(VK_BUFFER_USAGE_INDIRECT_BUFFER_BIT), + 1 + }; + commands.write( + std::array{ + VkDrawIndexedIndirectCommand{ + .indexCount = 3, + .instanceCount = 1, + .firstIndex = 0, + .vertexOffset = 0, + .firstInstance = 0, + }, + } + ); + + expect_filled(context, fill, [&](PrimaryCmdBuffer const& cmd) { + cmd.bindIndexBuffer(indices); + cmd.drawIndexedIndirect(commands); + }); +} +} diff --git a/cvk/tests/src/shaders.hpp b/cvk/tests/src/shaders.hpp index d95abd25..c14d3ed1 100644 --- a/cvk/tests/src/shaders.hpp +++ b/cvk/tests/src/shaders.hpp @@ -59,4 +59,30 @@ inline constexpr std::array TRIANGLE_FRAG{ 0x00050041, 0x0000000f, 0x00000010, 0x0000000c, 0x0000000e, 0x0004003d, 0x00000007, 0x00000011, 0x00000010, 0x0003003e, 0x00000009, 0x00000011, 0x000100fd, 0x00010038, }; + +/** + * @brief vertex shader: location 0 vec3 position, no descriptors + */ +inline constexpr std::array FILL_VERT{ + 0x07230203, 0x00010600, 0x000d000b, 0x0000001b, 0x00000000, 0x00020011, 0x00000001, 0x0006000b, + 0x00000001, 0x4c534c47, 0x6474732e, 0x3035342e, 0x00000000, 0x0003000e, 0x00000000, 0x00000001, + 0x0007000f, 0x00000000, 0x00000004, 0x6e69616d, 0x00000000, 0x0000000d, 0x00000012, 0x00030047, + 0x0000000b, 0x00000002, 0x00050048, 0x0000000b, 0x00000000, 0x0000000b, 0x00000000, 0x00050048, + 0x0000000b, 0x00000001, 0x0000000b, 0x00000001, 0x00050048, 0x0000000b, 0x00000002, 0x0000000b, + 0x00000003, 0x00050048, 0x0000000b, 0x00000003, 0x0000000b, 0x00000004, 0x00040047, 0x00000012, + 0x0000001e, 0x00000000, 0x00020013, 0x00000002, 0x00030021, 0x00000003, 0x00000002, 0x00030016, + 0x00000006, 0x00000020, 0x00040017, 0x00000007, 0x00000006, 0x00000004, 0x00040015, 0x00000008, + 0x00000020, 0x00000000, 0x0004002b, 0x00000008, 0x00000009, 0x00000001, 0x0004001c, 0x0000000a, + 0x00000006, 0x00000009, 0x0006001e, 0x0000000b, 0x00000007, 0x00000006, 0x0000000a, 0x0000000a, + 0x00040020, 0x0000000c, 0x00000003, 0x0000000b, 0x0004003b, 0x0000000c, 0x0000000d, 0x00000003, + 0x00040015, 0x0000000e, 0x00000020, 0x00000001, 0x0004002b, 0x0000000e, 0x0000000f, 0x00000000, + 0x00040017, 0x00000010, 0x00000006, 0x00000003, 0x00040020, 0x00000011, 0x00000001, 0x00000010, + 0x0004003b, 0x00000011, 0x00000012, 0x00000001, 0x0004002b, 0x00000006, 0x00000014, 0x3f800000, + 0x00040020, 0x00000019, 0x00000003, 0x00000007, 0x00050036, 0x00000002, 0x00000004, 0x00000000, + 0x00000003, 0x000200f8, 0x00000005, 0x0004003d, 0x00000010, 0x00000013, 0x00000012, 0x00050051, + 0x00000006, 0x00000015, 0x00000013, 0x00000000, 0x00050051, 0x00000006, 0x00000016, 0x00000013, + 0x00000001, 0x00050051, 0x00000006, 0x00000017, 0x00000013, 0x00000002, 0x00070050, 0x00000007, + 0x00000018, 0x00000015, 0x00000016, 0x00000017, 0x00000014, 0x00050041, 0x00000019, 0x0000001a, + 0x0000000d, 0x0000000f, 0x0003003e, 0x0000001a, 0x00000018, 0x000100fd, 0x00010038, +}; } From 021e8235755498c485ed341a6b97e1d39bfb1ad8 Mon Sep 17 00:00:00 2001 From: lukas <89145039+codethinki@users.noreply.github.com> Date: Sat, 17 Oct 2026 22:59:47 +0200 Subject: [PATCH 054/135] cvk: explicit image layouts, layout tracking moves to jvk prework - Image holds no layout, Barrier::addTransition replaces Image::transitionLayout - ImageBarrierInfo takes newLayout and oldLayout as values - copy and rendering attachments take the layout with a default for the use - ImageLayouts and the gtl hash map alias wait in jvk/prework, lib/gtl.cmake creates gtl::gtl - RenderingConfig is RenderingInfo --- AGENTS.md | 3 + CMakeLists.txt | 2 + cvk/incl/cvk/data/img/image.hpp | 50 +++-------- cvk/incl/cvk/render/cmd/cmd_buffer.hpp | 8 +- ...endering_config.hpp => rendering_info.hpp} | 12 ++- cvk/incl/cvk/render/sync/barrier.hpp | 51 ++++++++--- cvk/src/data/img/image.cpp | 54 ++--------- cvk/src/render/cmd/cmd_buffer.cpp | 54 ++++++----- cvk/src/render/sync/barrier.cpp | 60 +++++++------ cvk/tests/src/context.cpp | 5 +- cvk/tests/src/context.hpp | 6 +- cvk/tests/src/data/img/image_test.cpp | 35 ++------ cvk/tests/src/render/cmd/draw_test.cpp | 7 +- cvk/tests/src/render/cmd/rendering_test.cpp | 40 ++++++--- cvk/tests/src/render/sync/barrier_test.cpp | 77 +++++++++------- jvk/prework/flat_hash_map.hpp | 11 +++ jvk/prework/image_layouts.cpp | 48 ++++++++++ jvk/prework/image_layouts.hpp | 62 +++++++++++++ jvk/prework/image_layouts_test.cpp | 89 +++++++++++++++++++ lib/gtl.cmake | 7 ++ vcpkg.json | 1 + 21 files changed, 446 insertions(+), 236 deletions(-) rename cvk/incl/cvk/render/cmd/{rendering_config.hpp => rendering_info.hpp} (87%) create mode 100644 jvk/prework/flat_hash_map.hpp create mode 100644 jvk/prework/image_layouts.cpp create mode 100644 jvk/prework/image_layouts.hpp create mode 100644 jvk/prework/image_layouts_test.cpp create mode 100644 lib/gtl.cmake diff --git a/AGENTS.md b/AGENTS.md index 62f256ef..cf1b39d6 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -9,6 +9,7 @@ Read it before you change a file. Match the existing code. |---|---| | `cvk/` | C++ wrapper of the Vulkan C API: one class per Vulkan object, objects own their handle | | `jvk/` | coroutine layer on top of `cvk` (not started) | +| `jvk/prework/` | parts for `jvk` that left `cvk`, not built | | `legacy/` | the old `jvk`, `jolly` and `demo` code, reference only, not built | | `tests/` | loader smoke tests | | `lib/cth_cmake` | cmake utilities (submodule) | @@ -18,6 +19,8 @@ Read it before you change a file. Match the existing code. - Use the Vulkan C API through volk. Do not use Vulkan-Hpp. - Memory comes from VMA. - `cvk` destroys immediately. It has no destruction queue. Deferred destruction belongs to `jvk`. +- `cvk` tracks no image layouts. Every function that uses a layout takes it as a value, with a default where the use fixes it. Layout tracking belongs to `jvk` (`jvk/prework/image_layouts.hpp`). +- Hash maps come from gtl (`jvk/prework/flat_hash_map.hpp`). gtl installs no cmake package, `lib/gtl.cmake` creates the target `gtl::gtl`. - Commits have no `Co-Authored-By` or "Generated with" lines. ## Branches diff --git a/CMakeLists.txt b/CMakeLists.txt index 6dff9030..0215f9cd 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -10,6 +10,8 @@ cth_set_latest_c_cpp_standard() set(CMAKE_CXX_SCAN_FOR_MODULES OFF) +include(lib/gtl.cmake) + if(JOLLY_ENABLE_TESTS) enable_testing() endif() diff --git a/cvk/incl/cvk/data/img/image.hpp b/cvk/incl/cvk/data/img/image.hpp index 275a6e91..202730f0 100644 --- a/cvk/incl/cvk/data/img/image.hpp +++ b/cvk/incl/cvk/data/img/image.hpp @@ -13,8 +13,6 @@ #include #include #include -#include -#include namespace cvk { @@ -23,9 +21,10 @@ class BaseCmdBuffer; class Core; /** - * @brief VkImage wrapper, owns the image and its memory, tracks the layout of each mip level + * @brief VkImage wrapper, owns the image and its memory * @details @ref create() creates both with vmaCreateImage(), - * swapchain images are wrapped without memory + * swapchain images are wrapped without memory, + * the image holds no layout, the commands take it as a value */ class Image { public: @@ -65,7 +64,6 @@ class Image { * @details std::nullopt @ref State::memory -> @ref destroy() only destroys the image * @pre the image is bound to its memory * @pre the image was created with the config - * @pre @ref State::levelLayouts size <= @ref Config::mipLevels * @post @ref created() * @note calls @ref optDestroy() */ @@ -74,7 +72,6 @@ class Image { /** * @brief creates the image and its memory * @post @ref created() - * @post every mip level has @ref Config::initialLayout * @note calls @ref optDestroy() * @throws cvk::vk_result_exception result of vmaCreateImage() */ @@ -101,35 +98,20 @@ class Image { /** * @brief records a copy from the buffer into the mip level * @param srcOffset in bytes + * @param layout of the mip level, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL or VK_IMAGE_LAYOUT_GENERAL * @pre @ref created() and src created * @pre @ref BaseCmdBuffer::recording() * @pre mipLevel < @ref mipLevels() - * @pre the layout of the mip level is VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL or VK_IMAGE_LAYOUT_GENERAL * @pre @ref aspectMask() has one bit * @pre src usage has VK_BUFFER_USAGE_TRANSFER_SRC_BIT, the usage has VK_IMAGE_USAGE_TRANSFER_DST_BIT */ - void copy(BaseCmdBuffer const&, BaseBuffer const& src, size_t srcOffset = 0, uint32_t mipLevel = 0) const; - - /** - * @brief records a layout transition of the mip levels with the scopes of @ref layout_transition() - * @param mipLevels (VK_REMAINING_MIP_LEVELS => to the last mip level) - * @pre @ref layout_transition() knows the transition from the layout of firstMipLevel - * @details calls @ref Barrier::execute() - */ - void transitionLayout( + void copy( BaseCmdBuffer const&, - VkImageLayout newLayout, - uint32_t firstMipLevel = 0, - uint32_t mipLevels = VK_REMAINING_MIP_LEVELS - ); - - /** - * @brief records the layout of the mip levels in the layout tracking - * @param mipLevels (VK_REMAINING_MIP_LEVELS => to the last mip level) - * @details @ref Barrier::execute() calls it after a layout transition - * @pre the mip range lies in the image - */ - void setLayout(uint32_t firstMipLevel, uint32_t mipLevels, VkImageLayout); + BaseBuffer const& src, + size_t srcOffset = 0, + uint32_t mipLevel = 0, + VkImageLayout layout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL + ) const; /** * @return mip levels down to 1x1 @@ -151,12 +133,6 @@ class Image { [[nodiscard]] constexpr VkImageAspectFlags aspectMask() const { return _config.aspectMask; } [[nodiscard]] constexpr uint32_t mipLevels() const { return _config.mipLevels; } - /** - * @pre mipLevel < @ref mipLevels() - */ - [[nodiscard]] constexpr VkImageLayout layout(uint32_t mipLevel) const { return _levelLayouts[mipLevel]; } - [[nodiscard]] constexpr std::span layouts() const { return _levelLayouts; } - /** * @return the memory of the image, not created -> the image does not own its memory */ @@ -170,7 +146,6 @@ class Image { Memory _memory; Config _config; VkExtent2D _extent{}; - std::vector _levelLayouts; public: Image(Image const& other) = delete; @@ -190,11 +165,6 @@ struct Image::State { * @brief std::nullopt -> the image does not own its memory, e.g. swapchain images */ std::optional memory = std::nullopt; - /** - * @brief current layout of each mip level - * @details size < @ref Config::mipLevels -> the other levels have @ref Config::initialLayout - */ - std::vector levelLayouts{}; }; } diff --git a/cvk/incl/cvk/render/cmd/cmd_buffer.hpp b/cvk/incl/cvk/render/cmd/cmd_buffer.hpp index 09b5c0bc..0c0a2479 100644 --- a/cvk/incl/cvk/render/cmd/cmd_buffer.hpp +++ b/cvk/incl/cvk/render/cmd/cmd_buffer.hpp @@ -1,6 +1,6 @@ #pragma once #include "cmd_pool.hpp" -#include "rendering_config.hpp" +#include "rendering_info.hpp" #include "cvk/data/buffer/buffer.hpp" @@ -116,14 +116,14 @@ class BaseCmdBuffer { /** * @brief begins a dynamic rendering - * @details the layouts of the attachments come from the layout tracking of their images, - * std::nullopt @ref RenderingConfig::area -> the extent that fits every attachment + * @details std::nullopt @ref RenderingInfo::area -> the extent that fits every attachment * @pre @ref recording(), !@ref rendering() * @pre the attachment views are created and have one mip level + * @pre the attachment layouts are the layouts of the views * @pre the attachment layouts are attachment layouts (not undefined, transfer, shader read or present) * @post @ref rendering() */ - void beginRendering(RenderingConfig const&); + void beginRendering(RenderingInfo const&); /** * @brief ends the dynamic rendering diff --git a/cvk/incl/cvk/render/cmd/rendering_config.hpp b/cvk/incl/cvk/render/cmd/rendering_info.hpp similarity index 87% rename from cvk/incl/cvk/render/cmd/rendering_config.hpp rename to cvk/incl/cvk/render/cmd/rendering_info.hpp index be1005dc..71862b52 100644 --- a/cvk/incl/cvk/render/cmd/rendering_config.hpp +++ b/cvk/incl/cvk/render/cmd/rendering_info.hpp @@ -48,6 +48,11 @@ struct RenderingAttachment { */ not_null view; + /** + * @brief layout of the view during the rendering, VK_IMAGE_LAYOUT_ATTACHMENT_OPTIMAL fits every aspect + */ + VkImageLayout layout = VK_IMAGE_LAYOUT_ATTACHMENT_OPTIMAL; + VkAttachmentLoadOp loadOp = VK_ATTACHMENT_LOAD_OP_LOAD; VkAttachmentStoreOp storeOp = VK_ATTACHMENT_STORE_OP_STORE; @@ -61,6 +66,11 @@ struct RenderingAttachment { */ ImageView const* resolveView = nullptr; + /** + * @brief layout of @ref resolveView during the rendering + */ + VkImageLayout resolveLayout = VK_IMAGE_LAYOUT_ATTACHMENT_OPTIMAL; + /** * @pre != VK_RESOLVE_MODE_NONE if @ref resolveView */ @@ -70,7 +80,7 @@ struct RenderingAttachment { /** * @brief attachments, area and flags of a dynamic rendering */ -struct RenderingConfig { +struct RenderingInfo { std::vector colorAttachments{}; std::optional depthAttachment = std::nullopt; std::optional stencilAttachment = std::nullopt; diff --git a/cvk/incl/cvk/render/sync/barrier.hpp b/cvk/incl/cvk/render/sync/barrier.hpp index 878353cb..c7c2f1ba 100644 --- a/cvk/incl/cvk/render/sync/barrier.hpp +++ b/cvk/incl/cvk/render/sync/barrier.hpp @@ -39,9 +39,15 @@ struct ImageBarrierInfo { BarrierScope dst{}; /** - * @brief std::nullopt -> no layout transition + * @brief == @ref oldLayout -> no layout transition */ - std::optional newLayout = std::nullopt; + VkImageLayout newLayout = VK_IMAGE_LAYOUT_UNDEFINED; + + /** + * @brief layout of the mip range before the barrier + * @attention VK_IMAGE_LAYOUT_UNDEFINED discards the contents, the validation layers do not report it + */ + VkImageLayout oldLayout = VK_IMAGE_LAYOUT_UNDEFINED; uint32_t firstMipLevel = 0; @@ -57,6 +63,7 @@ struct ImageBarrierInfo { [[nodiscard]] static constexpr ImageBarrierInfo LayoutTransition( VkImageLayout newLayout, + VkImageLayout oldLayout, BarrierScope const& src, BarrierScope const& dst, uint32_t firstMipLevel = 0, @@ -66,16 +73,22 @@ struct ImageBarrierInfo { .src = src, .dst = dst, .newLayout = newLayout, + .oldLayout = oldLayout, .firstMipLevel = firstMipLevel, .mipLevels = mipLevels, }; } /** - * @brief queue family ownership transfer, src and dst name the families + * @brief queue family ownership transfer without layout transition, src and dst name the families + * @param layout of the image */ - [[nodiscard]] static constexpr ImageBarrierInfo QueueTransition(BarrierScope const& src, BarrierScope const& dst) { - return ImageBarrierInfo{.src = src, .dst = dst}; + [[nodiscard]] static constexpr ImageBarrierInfo QueueTransition( + VkImageLayout layout, + BarrierScope const& src, + BarrierScope const& dst + ) { + return ImageBarrierInfo{.src = src, .dst = dst, .newLayout = layout, .oldLayout = layout}; } }; @@ -123,7 +136,6 @@ struct LayoutTransition { /** * @brief pipeline barrier: memory, buffer and image barriers in one vkCmdPipelineBarrier2() - * @details @ref execute() updates the layout tracking of the images * @attention the images and buffers do not move until @ref execute() */ class Barrier { @@ -144,11 +156,24 @@ class Barrier { /** * @brief adds an image barrier - * @details the old layout comes from the layout tracking of the image at @ref execute() * @pre @ref Image::created() * @pre the mip range lies in the image */ - void add(Image&, ImageInfo const&); + void add(Image const&, ImageInfo const&); + + /** + * @brief adds a layout transition with the scopes of @ref layout_transition() + * @param mipLevels (VK_REMAINING_MIP_LEVELS => to the last mip level) + * @pre @ref layout_transition() knows the transition + * @pre the preconditions of @ref add(Image const&, ImageInfo const&) + */ + void addTransition( + Image const&, + VkImageLayout newLayout, + VkImageLayout oldLayout, + uint32_t firstMipLevel = 0, + uint32_t mipLevels = VK_REMAINING_MIP_LEVELS + ); /** * @brief adds a buffer barrier @@ -159,15 +184,15 @@ class Barrier { void add(BaseBuffer const&, BufferInfo const&); /** - * @brief records the barriers, then updates the layout tracking of the images + * @brief records the barriers * @details inside a rendering: checks the rules of barriers inside a dynamic rendering * @pre @ref BaseCmdBuffer::recording() - * @pre every image range has one layout - * @pre a new layout is not VK_IMAGE_LAYOUT_UNDEFINED or VK_IMAGE_LAYOUT_PREINITIALIZED + * @pre the new layout of a layout transition or queue family ownership transfer + * is not VK_IMAGE_LAYOUT_UNDEFINED or VK_IMAGE_LAYOUT_PREINITIALIZED * @pre inside a rendering: framebuffer space stages, VK_DEPENDENCY_BY_REGION_BIT, * no layout transition, no queue family ownership transfer, attachments in local read or general layout */ - void execute(BaseCmdBuffer const&); + void execute(BaseCmdBuffer const&) const; /** * @brief removes every barrier @@ -199,7 +224,7 @@ class Barrier { }; struct ImageEntry { - not_null image; + not_null image; ImageInfo info; }; diff --git a/cvk/src/data/img/image.cpp b/cvk/src/data/img/image.cpp index 73ea2662..65b08962 100644 --- a/cvk/src/data/img/image.cpp +++ b/cvk/src/data/img/image.cpp @@ -4,7 +4,6 @@ #include "cvk/base/core.hpp" #include "cvk/data/buffer/base_buffer.hpp" #include "cvk/render/cmd/cmd_buffer.hpp" -#include "cvk/render/sync/barrier.hpp" #include "cvk/utility/vk_exceptions.hpp" #include @@ -13,7 +12,7 @@ namespace cvk { Image::Image(Core const& core, Config const& config) : _handle{core}, _memory{core, config.memory}, - _config{config}, _levelLayouts(config.mipLevels, config.initialLayout) { + _config{config} { Core::debug_check(core); Config::debug_check(config); } @@ -23,15 +22,11 @@ Image::Image(Core const& core, Config const& config, State const& state) : Image Image::Image(Core const& core, Config const& config, VkExtent2D extent) : Image{core, config} { create(extent); } void Image::wrap(State const& state) { - CTH_CRITICAL(state.levelLayouts.size() > _config.mipLevels, "more level layouts than mip levels") {} - optDestroy(); _handle.reset(state.vkImage.get(), core()); if(state.memory.has_value()) _memory.wrap(*state.memory); _extent = state.extent; - _levelLayouts.assign(_config.mipLevels, _config.initialLayout); - std::ranges::copy(state.levelLayouts, _levelLayouts.begin()); } void Image::create(VkExtent2D extent) { @@ -59,7 +54,6 @@ void Image::create(VkExtent2D extent) { _handle.reset(vkImage, core()); _memory.wrap(Memory::State{vmaAllocation}); _extent = extent; - _levelLayouts.assign(_config.mipLevels, _config.initialLayout); } void Image::destroy() { @@ -78,7 +72,6 @@ Image::State Image::release() { .vkImage = _handle.release(), .extent = _extent, .memory = std::nullopt, - .levelLayouts = std::move(_levelLayouts), }; if(_memory.created()) state.memory = _memory.release(); @@ -86,7 +79,13 @@ Image::State Image::release() { return state; } -void Image::copy(BaseCmdBuffer const& cmdBuffer, BaseBuffer const& src, size_t srcOffset, uint32_t mipLevel) const { +void Image::copy( + BaseCmdBuffer const& cmdBuffer, + BaseBuffer const& src, + size_t srcOffset, + uint32_t mipLevel, + VkImageLayout layout +) const { debug_check(*this); BaseBuffer::debug_check(src); CTH_CRITICAL(!cmdBuffer.recording(), "command buffer must be recording") {} @@ -101,7 +100,6 @@ void Image::copy(BaseCmdBuffer const& cmdBuffer, BaseBuffer const& src, size_t s "image usage must have VK_IMAGE_USAGE_TRANSFER_DST_BIT" ) {} - auto const layout = _levelLayouts[mipLevel]; CTH_CRITICAL( layout != VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL && layout != VK_IMAGE_LAYOUT_GENERAL, "mip level must be in VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL or VK_IMAGE_LAYOUT_GENERAL" @@ -136,45 +134,11 @@ void Image::copy(BaseCmdBuffer const& cmdBuffer, BaseBuffer const& src, size_t s core().functions()->vkCmdCopyBufferToImage2(cmdBuffer.get(), &info); } -void Image::transitionLayout( - BaseCmdBuffer const& cmdBuffer, - VkImageLayout newLayout, - uint32_t firstMipLevel, - uint32_t mipLevels -) { - debug_check(*this); - - auto const oldLayout = layout(firstMipLevel); - auto const transition = layout_transition(oldLayout, newLayout); - CTH_CRITICAL(!transition.has_value(), "layout transition has no table entry, use a Barrier") { - details->add("transition: {} -> {}", oldLayout, newLayout); - } - - Barrier barrier{}; - barrier.add( - *this, - ImageBarrierInfo::LayoutTransition(newLayout, transition->src, transition->dst, firstMipLevel, mipLevels) - ); - barrier.execute(cmdBuffer); -} - -void Image::setLayout(uint32_t firstMipLevel, uint32_t mipLevels, VkImageLayout layout) { - CTH_CRITICAL(firstMipLevel >= _levelLayouts.size(), "first mip level must lie in the image") {} - - size_t const count = mipLevels == VK_REMAINING_MIP_LEVELS ? _levelLayouts.size() - firstMipLevel : mipLevels; - CTH_CRITICAL(count > _levelLayouts.size() - firstMipLevel, "mip levels must lie in the image") {} - - std::ranges::fill_n(_levelLayouts.begin() + firstMipLevel, static_cast(count), layout); -} - void Image::destroy(DeviceTable table, VkImage vkImage) { CTH_WARN(vkImage == VK_NULL_HANDLE, "vkImage invalid (VK_NULL_HANDLE)") {} table->vkDestroyImage(table.device(), vkImage, nullptr); } -void Image::forget() { - _extent = {}; - _levelLayouts.assign(_config.mipLevels, _config.initialLayout); -} +void Image::forget() { _extent = {}; } } diff --git a/cvk/src/render/cmd/cmd_buffer.cpp b/cvk/src/render/cmd/cmd_buffer.cpp index bb420301..f56acb7d 100644 --- a/cvk/src/render/cmd/cmd_buffer.cpp +++ b/cvk/src/render/cmd/cmd_buffer.cpp @@ -13,12 +13,7 @@ namespace cvk { using std::span; namespace { - /** - * @brief layout of the view from the layout tracking of its image - */ - VkImageLayout attachment_layout(ImageView const& view) { - auto const layout = view.image().layout(view.range().baseMipLevel); - + void debug_check_attachment_layout(VkImageLayout layout) { CTH_CRITICAL( layout == VK_IMAGE_LAYOUT_UNDEFINED || layout == VK_IMAGE_LAYOUT_PREINITIALIZED || layout == VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL || layout == VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL @@ -27,8 +22,6 @@ namespace { ) { details->add("layout: {}", layout); } - - return layout; } uint32_t view_mip_levels(ImageView const& view) { @@ -152,17 +145,17 @@ void BaseCmdBuffer::reset(VkCommandBufferResetFlags flags) { CVK_RESULT_STABLE_THROW(result != VK_SUCCESS, result, "failed to reset command buffer") {} } -void BaseCmdBuffer::beginRendering(RenderingConfig const& config) { +void BaseCmdBuffer::beginRendering(RenderingInfo const& info) { debug_check(*this); CTH_CRITICAL(!recording(), "command buffer must be recording") {} CTH_CRITICAL(rendering() != nullptr, "command buffer must be outside of a rendering") {} Rendering rendering{ .inheritance = { - .viewMask = config.viewMask, - .flags = config.flags & ~VK_RENDERING_CONTENTS_SECONDARY_COMMAND_BUFFERS_BIT, + .viewMask = info.viewMask, + .flags = info.flags & ~VK_RENDERING_CONTENTS_SECONDARY_COMMAND_BUFFERS_BIT, }, - .flags = config.flags, + .flags = info.flags, }; std::optional extent{}; @@ -177,6 +170,7 @@ void BaseCmdBuffer::beginRendering(RenderingConfig const& config) { : viewExtent; rendering.inheritance.samples = view.image().config().samples; rendering.attachments.emplace_back(&view.image()); + debug_check_attachment_layout(attachment.layout); VkImageView resolveView = VK_NULL_HANDLE; VkImageLayout resolveLayout = VK_IMAGE_LAYOUT_UNDEFINED; @@ -184,8 +178,10 @@ void BaseCmdBuffer::beginRendering(RenderingConfig const& config) { ImageView::debug_check(*attachment.resolveView); CTH_CRITICAL(attachment.resolveMode == VK_RESOLVE_MODE_NONE, "a resolve view needs a resolve mode") {} + debug_check_attachment_layout(attachment.resolveLayout); + resolveView = attachment.resolveView->get(); - resolveLayout = attachment_layout(*attachment.resolveView); + resolveLayout = attachment.resolveLayout; rendering.attachments.emplace_back(&attachment.resolveView->image()); } @@ -193,7 +189,7 @@ void BaseCmdBuffer::beginRendering(RenderingConfig const& config) { .sType = VK_STRUCTURE_TYPE_RENDERING_ATTACHMENT_INFO, .pNext = nullptr, .imageView = view.get(), - .imageLayout = attachment_layout(view), + .imageLayout = attachment.layout, .resolveMode = attachment.resolveMode, .resolveImageView = resolveView, .resolveImageLayout = resolveLayout, @@ -204,41 +200,41 @@ void BaseCmdBuffer::beginRendering(RenderingConfig const& config) { }; std::vector colors{}; - colors.reserve(config.colorAttachments.size()); - for(auto const& attachment : config.colorAttachments) { + colors.reserve(info.colorAttachments.size()); + for(auto const& attachment : info.colorAttachments) { colors.push_back(attachmentInfo(attachment)); rendering.inheritance.colorFormats.push_back(attachment.view->image().format()); } std::optional depth{}; - if(config.depthAttachment.has_value()) { - depth = attachmentInfo(*config.depthAttachment); - rendering.inheritance.depthFormat = config.depthAttachment->view->image().format(); + if(info.depthAttachment.has_value()) { + depth = attachmentInfo(*info.depthAttachment); + rendering.inheritance.depthFormat = info.depthAttachment->view->image().format(); } std::optional stencil{}; - if(config.stencilAttachment.has_value()) { - stencil = attachmentInfo(*config.stencilAttachment); - rendering.inheritance.stencilFormat = config.stencilAttachment->view->image().format(); + if(info.stencilAttachment.has_value()) { + stencil = attachmentInfo(*info.stencilAttachment); + rendering.inheritance.stencilFormat = info.stencilAttachment->view->image().format(); } - CTH_CRITICAL(!extent.has_value() && !config.area.has_value(), "a rendering without attachments needs an area") {} + CTH_CRITICAL(!extent.has_value() && !info.area.has_value(), "a rendering without attachments needs an area") {} - VkRenderingInfo const info{ + VkRenderingInfo const renderingInfo{ .sType = VK_STRUCTURE_TYPE_RENDERING_INFO, .pNext = nullptr, - .flags = config.flags, - .renderArea = config.area.value_or(VkRect2D{.offset = {0, 0}, .extent = extent.value_or(VkExtent2D{})}), + .flags = info.flags, + .renderArea = info.area.value_or(VkRect2D{.offset = {0, 0}, .extent = extent.value_or(VkExtent2D{})}), .layerCount = 1, - .viewMask = config.viewMask, + .viewMask = info.viewMask, .colorAttachmentCount = static_cast(colors.size()), .pColorAttachments = colors.data(), .pDepthAttachment = depth.has_value() ? &*depth : nullptr, .pStencilAttachment = stencil.has_value() ? &*stencil : nullptr, }; - core().functions()->vkCmdBeginRendering(get(), &info); + core().functions()->vkCmdBeginRendering(get(), &renderingInfo); - rendering.area = info.renderArea; + rendering.area = renderingInfo.renderArea; _rendering = std::move(rendering); } diff --git a/cvk/src/render/sync/barrier.cpp b/cvk/src/render/sync/barrier.cpp index 89177ed9..c6fb89d2 100644 --- a/cvk/src/render/sync/barrier.cpp +++ b/cvk/src/render/sync/barrier.cpp @@ -41,29 +41,9 @@ namespace { }; } - /** - * @brief image barrier from the current layout of the mip range - */ VkImageMemoryBarrier2 image_barrier(Image const& image, ImageBarrierInfo const& info) { Image::debug_check(image); - size_t const count = info.mipLevels == VK_REMAINING_MIP_LEVELS ? std::dynamic_extent : info.mipLevels; - auto const layouts = image.layouts().subspan(info.firstMipLevel, count); - auto const oldLayout = layouts.front(); - - CTH_CRITICAL( - std::ranges::any_of(layouts, [oldLayout](VkImageLayout layout) { return layout != oldLayout; }), - "every mip level of the range must have one layout" - ) {} - - auto const newLayout = info.newLayout.value_or(oldLayout); - CTH_CRITICAL( - newLayout == VK_IMAGE_LAYOUT_UNDEFINED || newLayout == VK_IMAGE_LAYOUT_PREINITIALIZED, - "new layout must not be undefined or preinitialized" - ) { - details->add("image layout: {}", oldLayout); - } - return VkImageMemoryBarrier2{ .sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER_2, .pNext = nullptr, @@ -71,8 +51,8 @@ namespace { .srcAccessMask = info.src.access, .dstStageMask = info.dst.stages, .dstAccessMask = info.dst.access, - .oldLayout = oldLayout, - .newLayout = newLayout, + .oldLayout = info.oldLayout, + .newLayout = info.newLayout, .srcQueueFamilyIndex = info.src.queueFamilyIndex, .dstQueueFamilyIndex = info.dst.queueFamilyIndex, .image = image.get(), @@ -87,7 +67,7 @@ namespace { } } -void Barrier::add(Image& image, ImageInfo const& info) { +void Barrier::add(Image const& image, ImageInfo const& info) { Image::debug_check(image); CTH_CRITICAL(info.mipLevels == 0, "mip levels must not be 0") {} CTH_CRITICAL(info.firstMipLevel >= image.mipLevels(), "first mip level must lie in the image") {} @@ -96,9 +76,36 @@ void Barrier::add(Image& image, ImageInfo const& info) { "mip levels must lie in the image" ) {} + bool const transfer = info.src.queueFamilyIndex != info.dst.queueFamilyIndex; + CTH_CRITICAL( + (info.oldLayout != info.newLayout || transfer) + && (info.newLayout == VK_IMAGE_LAYOUT_UNDEFINED || info.newLayout == VK_IMAGE_LAYOUT_PREINITIALIZED), + "a transition or queue family transfer must not have an undefined or preinitialized new layout" + ) { + details->add("transition: {} -> {}", info.oldLayout, info.newLayout); + } + _images.emplace_back(&image, info); } +void Barrier::addTransition( + Image const& image, + VkImageLayout newLayout, + VkImageLayout oldLayout, + uint32_t firstMipLevel, + uint32_t mipLevels +) { + auto const transition = layout_transition(oldLayout, newLayout); + CTH_CRITICAL(!transition.has_value(), "layout transition has no table entry, use add() with scopes") { + details->add("transition: {} -> {}", oldLayout, newLayout); + } + + add( + image, + ImageInfo::LayoutTransition(newLayout, oldLayout, transition->src, transition->dst, firstMipLevel, mipLevels) + ); +} + void Barrier::add(BaseBuffer const& buffer, BufferInfo const& info) { BaseBuffer::debug_check(buffer); CTH_CRITICAL( @@ -153,7 +160,7 @@ void Barrier::debug_check_rendering( ) {} bool const attachment = std::ranges::contains( rendering.attachments, - static_cast(entry.image.get()), + entry.image.get(), ¬_null::get ); CTH_CRITICAL( @@ -166,7 +173,7 @@ void Barrier::debug_check_rendering( } } -void Barrier::execute(BaseCmdBuffer const& cmdBuffer) { +void Barrier::execute(BaseCmdBuffer const& cmdBuffer) const { BaseCmdBuffer::debug_check(cmdBuffer); CTH_CRITICAL(!cmdBuffer.recording(), "command buffer must be recording") {} @@ -194,8 +201,5 @@ void Barrier::execute(BaseCmdBuffer const& cmdBuffer) { .pImageMemoryBarriers = imageBarriers.data(), }; cmdBuffer.core().functions()->vkCmdPipelineBarrier2(cmdBuffer.get(), &dependency); - - for(auto const& [image, info] : _images) - if(info.newLayout.has_value()) image->setLayout(info.firstMipLevel, info.mipLevels, *info.newLayout); } } diff --git a/cvk/tests/src/context.cpp b/cvk/tests/src/context.cpp index 4e369c50..b3e98fad 100644 --- a/cvk/tests/src/context.cpp +++ b/cvk/tests/src/context.cpp @@ -72,7 +72,8 @@ void copy_to_buffer( Image const& image, BaseBuffer const& dst, size_t dstOffset, - uint32_t mipLevel + uint32_t mipLevel, + VkImageLayout layout ) { VkBufferImageCopy2 const region{ .sType = VK_STRUCTURE_TYPE_BUFFER_IMAGE_COPY_2, @@ -88,7 +89,7 @@ void copy_to_buffer( .sType = VK_STRUCTURE_TYPE_COPY_IMAGE_TO_BUFFER_INFO_2, .pNext = nullptr, .srcImage = image.get(), - .srcImageLayout = image.layout(mipLevel), + .srcImageLayout = layout, .dstBuffer = dst.get(), .regionCount = 1, .pRegions = ®ion, diff --git a/cvk/tests/src/context.hpp b/cvk/tests/src/context.hpp index 903d08eb..e364ebfe 100644 --- a/cvk/tests/src/context.hpp +++ b/cvk/tests/src/context.hpp @@ -51,7 +51,8 @@ void host_read_barrier(Context const&, BaseCmdBuffer const&); /** * @brief records a copy of the mip level into the buffer, without cvk: tests need a second path - * @pre the mip level is in a transfer src layout, the image has a color aspect + * @param layout of the mip level, a transfer src layout + * @pre the image has a color aspect */ void copy_to_buffer( Context const&, @@ -59,7 +60,8 @@ void copy_to_buffer( Image const&, BaseBuffer const& dst, size_t dstOffset, - uint32_t mipLevel + uint32_t mipLevel, + VkImageLayout layout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL ); /** diff --git a/cvk/tests/src/data/img/image_test.cpp b/cvk/tests/src/data/img/image_test.cpp index c3101db8..cdc169ac 100644 --- a/cvk/tests/src/data/img/image_test.cpp +++ b/cvk/tests/src/data/img/image_test.cpp @@ -3,6 +3,7 @@ #include "cvk/data/buffer/base_buffer.hpp" #include "cvk/data/img/image.hpp" +#include "cvk/render/sync/barrier.hpp" #include #include @@ -41,10 +42,6 @@ namespace { .usage = VK_BUFFER_USAGE_TRANSFER_DST_BIT, .memory = {.flags = VMA_ALLOCATION_CREATE_HOST_ACCESS_RANDOM_BIT}, }; - - bool all_layouts(Image const& image, VkImageLayout layout) { - return std::ranges::all_of(image.layouts(), [layout](VkImageLayout l) { return l == layout; }); - } } CVK_TEST(Image, eval_mip_level_count) { @@ -91,8 +88,6 @@ CVK_TEST(Image, base_constructor_does_not_create) { EXPECT_FALSE(image.created()); EXPECT_FALSE(image.memory().created()); - EXPECT_EQ(image.layouts().size(), TEXTURE_CONFIG.mipLevels); - EXPECT_TRUE(all_layouts(image, VK_IMAGE_LAYOUT_UNDEFINED)); } CVK_TEST(Image, create_and_destroy) { @@ -105,7 +100,6 @@ CVK_TEST(Image, create_and_destroy) { EXPECT_EQ(image.extent().height, EXTENT.height); EXPECT_EQ(image.mipLevels(), 7); EXPECT_TRUE(image.memory().created()); - EXPECT_TRUE(all_layouts(image, VK_IMAGE_LAYOUT_UNDEFINED)); image.destroy(); EXPECT_FALSE(image.created()); @@ -124,7 +118,7 @@ CVK_TEST(Image, create_depth_buffer) { CVK_TEST(Image, copy_from_a_buffer_into_two_mip_levels) { test::Context const context{}; - Image image{context.core, COPY_CONFIG, EXTENT}; + Image const image{context.core, COPY_CONFIG, EXTENT}; size_t const mip0 = size_t{EXTENT.width} * EXTENT.height * 4; size_t const mip1 = mip0 / 4; @@ -137,11 +131,16 @@ CVK_TEST(Image, copy_from_a_buffer_into_two_mip_levels) { staging.memory().write(pixels); test::record_and_submit(context, [&](PrimaryCmdBuffer const& primary) { - image.transitionLayout(primary, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL); + Barrier toDst{}; + toDst.addTransition(image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_UNDEFINED); + toDst.execute(primary); + image.copy(primary, staging, 0, 0); image.copy(primary, staging, mip0, 1); - image.transitionLayout(primary, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL); + Barrier toSrc{}; + toSrc.addTransition(image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL); + toSrc.execute(primary); test::copy_to_buffer(context, primary, image, readback, 0, 0); test::copy_to_buffer(context, primary, image, readback, mip0, 1); test::host_read_barrier(context, primary); @@ -151,8 +150,6 @@ CVK_TEST(Image, copy_from_a_buffer_into_two_mip_levels) { readback.memory().invalidate(); EXPECT_TRUE(std::ranges::equal(mapped, pixels)); readback.memory().unmap(); - - EXPECT_EQ(image.layout(1), VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL); } CVK_TEST(Image, release_and_wrap) { @@ -165,7 +162,6 @@ CVK_TEST(Image, release_and_wrap) { EXPECT_FALSE(image.created()); EXPECT_FALSE(image.memory().created()); ASSERT_TRUE(state.memory.has_value()); - EXPECT_EQ(state.levelLayouts.size(), TEXTURE_CONFIG.mipLevels); Image const wrapped{context.core, TEXTURE_CONFIG, state}; EXPECT_EQ(wrapped.get(), vkImage); @@ -173,19 +169,6 @@ CVK_TEST(Image, release_and_wrap) { EXPECT_EQ(wrapped.extent().width, EXTENT.width); } -CVK_TEST(Image, wrap_fills_missing_layouts) { - test::Context const context{}; - Image image{context.core, TEXTURE_CONFIG, EXTENT}; - auto state = image.release(); - state.levelLayouts = {VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL}; - - Image const wrapped{context.core, TEXTURE_CONFIG, state}; - - ASSERT_EQ(wrapped.layouts().size(), TEXTURE_CONFIG.mipLevels); - EXPECT_EQ(wrapped.layout(1), VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL); - EXPECT_EQ(wrapped.layout(2), VK_IMAGE_LAYOUT_UNDEFINED); -} - CVK_TEST(Image, wrap_without_memory_destroys_only_the_image) { test::Context const context{}; Image image{context.core, TEXTURE_CONFIG, EXTENT}; diff --git a/cvk/tests/src/render/cmd/draw_test.cpp b/cvk/tests/src/render/cmd/draw_test.cpp index 173cc2b2..57e84c96 100644 --- a/cvk/tests/src/render/cmd/draw_test.cpp +++ b/cvk/tests/src/render/cmd/draw_test.cpp @@ -81,7 +81,7 @@ namespace { */ template void expect_filled(test::Context const& context, Fill const& fill, Draw draw) { - Image image{ + Image const image{ context.core, { .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, @@ -94,7 +94,9 @@ namespace { BaseBuffer const readback{context.core, READBACK_CONFIG, size_t{EXTENT.width} * EXTENT.height * 4}; test::record_and_submit(context, [&](PrimaryCmdBuffer& cmd) { - image.transitionLayout(cmd, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL); + Barrier transition{}; + transition.addTransition(image, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, VK_IMAGE_LAYOUT_UNDEFINED); + transition.execute(cmd); cmd.beginRendering( { .colorAttachments = { @@ -122,6 +124,7 @@ namespace { image, ImageBarrierInfo::LayoutTransition( VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, + VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, {VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT, VK_ACCESS_2_COLOR_ATTACHMENT_WRITE_BIT}, {VK_PIPELINE_STAGE_2_TRANSFER_BIT, VK_ACCESS_2_TRANSFER_READ_BIT} ) diff --git a/cvk/tests/src/render/cmd/rendering_test.cpp b/cvk/tests/src/render/cmd/rendering_test.cpp index b37d4a0b..f1c88602 100644 --- a/cvk/tests/src/render/cmd/rendering_test.cpp +++ b/cvk/tests/src/render/cmd/rendering_test.cpp @@ -34,9 +34,10 @@ namespace { VK_ACCESS_2_COLOR_ATTACHMENT_WRITE_BIT }; - RenderingAttachment clear_red(ImageView const& view) { + RenderingAttachment clear_red(ImageView const& view, VkImageLayout layout = VK_IMAGE_LAYOUT_ATTACHMENT_OPTIMAL) { return RenderingAttachment{ .view = &view, + .layout = layout, .loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR, .storeOp = VK_ATTACHMENT_STORE_OP_STORE, .clearValue = {.color = {.float32 = {1.f, 0.f, 0.f, 1.f}}}, @@ -46,7 +47,7 @@ namespace { CVK_TEST(Rendering, clear_a_color_attachment) { test::Context const context{}; - Image image{context.core, COLOR_CONFIG, EXTENT}; + Image const image{context.core, COLOR_CONFIG, EXTENT}; ImageView const view{image, {}, create}; BaseBuffer const readback{ context.core, @@ -55,7 +56,9 @@ CVK_TEST(Rendering, clear_a_color_attachment) { }; test::record_and_submit(context, [&](PrimaryCmdBuffer& cmd) { - image.transitionLayout(cmd, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL); + Barrier transition{}; + transition.addTransition(image, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, VK_IMAGE_LAYOUT_UNDEFINED); + transition.execute(cmd); cmd.beginRendering({.colorAttachments = {clear_red(view)}}); cmd.endRendering(); @@ -65,6 +68,7 @@ CVK_TEST(Rendering, clear_a_color_attachment) { image, ImageBarrierInfo::LayoutTransition( VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, + VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, COLOR_WRITE, {VK_PIPELINE_STAGE_2_TRANSFER_BIT, VK_ACCESS_2_TRANSFER_READ_BIT} ) @@ -85,11 +89,13 @@ CVK_TEST(Rendering, clear_a_color_attachment) { CVK_TEST(Rendering, tracks_the_current_rendering) { test::Context const context{}; - Image image{context.core, COLOR_CONFIG, EXTENT}; + Image const image{context.core, COLOR_CONFIG, EXTENT}; ImageView const view{image, {}, create}; test::record_and_submit(context, [&](PrimaryCmdBuffer& cmd) { - image.transitionLayout(cmd, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL); + Barrier transition{}; + transition.addTransition(image, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, VK_IMAGE_LAYOUT_UNDEFINED); + transition.execute(cmd); EXPECT_EQ(cmd.rendering(), nullptr); cmd.beginRendering({.colorAttachments = {clear_red(view)}}); @@ -108,11 +114,13 @@ CVK_TEST(Rendering, tracks_the_current_rendering) { CVK_TEST(Rendering, clear_a_depth_attachment) { test::Context const context{}; - Image image{context.core, Image::Config::DepthBuffer(VK_FORMAT_D16_UNORM, VK_SAMPLE_COUNT_1_BIT), EXTENT}; + Image const image{context.core, Image::Config::DepthBuffer(VK_FORMAT_D16_UNORM, VK_SAMPLE_COUNT_1_BIT), EXTENT}; ImageView const view{image, {}, create}; test::record_and_submit(context, [&](PrimaryCmdBuffer& cmd) { - image.transitionLayout(cmd, VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_OPTIMAL); + Barrier transition{}; + transition.addTransition(image, VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_OPTIMAL, VK_IMAGE_LAYOUT_UNDEFINED); + transition.execute(cmd); cmd.beginRendering( { @@ -131,13 +139,15 @@ CVK_TEST(Rendering, clear_a_depth_attachment) { CVK_TEST(Rendering, secondary_inherits_the_rendering) { test::Context const context{}; - Image image{context.core, COLOR_CONFIG, EXTENT}; + Image const image{context.core, COLOR_CONFIG, EXTENT}; ImageView const view{image, {}, create}; CmdPool const pool{context.core, CmdPool::Config::Default(context.core.queue(0)), create}; std::array secondaries{SecondaryCmdBuffer{pool, create}}; test::record_and_submit(context, [&](PrimaryCmdBuffer& cmd) { - image.transitionLayout(cmd, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL); + Barrier transition{}; + transition.addTransition(image, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, VK_IMAGE_LAYOUT_UNDEFINED); + transition.execute(cmd); cmd.beginRendering( {.colorAttachments = {clear_red(view)}, .flags = VK_RENDERING_CONTENTS_SECONDARY_COMMAND_BUFFERS_BIT} ); @@ -155,14 +165,16 @@ CVK_TEST(Rendering, secondary_inherits_the_rendering) { CVK_TEST(Rendering, local_read_inside_the_rendering) { test::Context const context{}; - Image image{context.core, LOCAL_READ_CONFIG, EXTENT}; + Image const image{context.core, LOCAL_READ_CONFIG, EXTENT}; ImageView const view{image, {}, create}; constexpr std::array FIRST{0}; test::record_and_submit(context, [&](PrimaryCmdBuffer& cmd) { - image.transitionLayout(cmd, VK_IMAGE_LAYOUT_RENDERING_LOCAL_READ_KHR); + Barrier transition{}; + transition.addTransition(image, VK_IMAGE_LAYOUT_RENDERING_LOCAL_READ_KHR, VK_IMAGE_LAYOUT_UNDEFINED); + transition.execute(cmd); - cmd.beginRendering({.colorAttachments = {clear_red(view)}}); + cmd.beginRendering({.colorAttachments = {clear_red(view, VK_IMAGE_LAYOUT_RENDERING_LOCAL_READ_KHR)}}); cmd.setAttachmentLocations(FIRST); cmd.setInputAttachmentIndices(FIRST); @@ -172,13 +184,13 @@ CVK_TEST(Rendering, local_read_inside_the_rendering) { { .src = COLOR_WRITE, .dst = {VK_PIPELINE_STAGE_2_FRAGMENT_SHADER_BIT, VK_ACCESS_2_INPUT_ATTACHMENT_READ_BIT}, + .newLayout = VK_IMAGE_LAYOUT_RENDERING_LOCAL_READ_KHR, + .oldLayout = VK_IMAGE_LAYOUT_RENDERING_LOCAL_READ_KHR, } ); barrier.execute(cmd); cmd.endRendering(); }); - - EXPECT_EQ(image.layout(0), VK_IMAGE_LAYOUT_RENDERING_LOCAL_READ_KHR); } } diff --git a/cvk/tests/src/render/sync/barrier_test.cpp b/cvk/tests/src/render/sync/barrier_test.cpp index 21ac1887..dd4661f3 100644 --- a/cvk/tests/src/render/sync/barrier_test.cpp +++ b/cvk/tests/src/render/sync/barrier_test.cpp @@ -6,8 +6,6 @@ #include "cvk/render/cmd/cmd_buffer.hpp" #include "cvk/render/sync/barrier.hpp" -#include - namespace cvk { namespace { @@ -22,10 +20,6 @@ namespace { constexpr BarrierScope TRANSFER_WRITE{VK_PIPELINE_STAGE_2_TRANSFER_BIT, VK_ACCESS_2_TRANSFER_WRITE_BIT}; constexpr BarrierScope FRAGMENT_READ{VK_PIPELINE_STAGE_2_FRAGMENT_SHADER_BIT, VK_ACCESS_2_SHADER_READ_BIT}; - - bool all_layouts(Image const& image, VkImageLayout layout) { - return std::ranges::all_of(image.layouts(), [layout](VkImageLayout l) { return l == layout; }); - } } CVK_TEST(Barrier, layout_transition_table) { @@ -44,12 +38,22 @@ CVK_TEST(Barrier, layout_transition_table) { CVK_TEST(Barrier, layout_transition_info_covers_the_remaining_mip_levels) { constexpr auto info = ImageBarrierInfo::LayoutTransition( VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, + VK_IMAGE_LAYOUT_UNDEFINED, BarrierScope{}, TRANSFER_WRITE ); + static_assert(info.firstMipLevel == 0); static_assert(info.mipLevels == VK_REMAINING_MIP_LEVELS); static_assert(info.newLayout == VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL); + static_assert(info.oldLayout == VK_IMAGE_LAYOUT_UNDEFINED); +} + +CVK_TEST(Barrier, queue_transition_info_keeps_the_layout) { + constexpr auto info = ImageBarrierInfo::QueueTransition(VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, {}, {}); + + static_assert(info.newLayout == VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL); + static_assert(info.oldLayout == info.newLayout); } CVK_TEST(Barrier, memory_barriers_are_constexpr) { @@ -88,43 +92,56 @@ CVK_TEST(Barrier, execute_memory_and_buffer_barriers) { EXPECT_EQ(barrier.flags(), VK_DEPENDENCY_BY_REGION_BIT); } -CVK_TEST(Barrier, image_barrier_updates_the_layout_tracking) { +CVK_TEST(Barrier, image_layout_transition) { test::Context const context{}; - Image image{context.core, IMAGE_CONFIG, EXTENT}; + Image const image{context.core, IMAGE_CONFIG, EXTENT}; Barrier barrier{}; - barrier.add(image, ImageBarrierInfo::LayoutTransition(VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, {}, TRANSFER_WRITE)); + barrier.add( + image, + ImageBarrierInfo::LayoutTransition( + VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, + VK_IMAGE_LAYOUT_UNDEFINED, + {}, + TRANSFER_WRITE + ) + ); test::record_and_submit(context, [&](PrimaryCmdBuffer const& primary) { barrier.execute(primary); }); - - EXPECT_TRUE(all_layouts(image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL)); } -CVK_TEST(Barrier, image_barrier_on_a_mip_range) { +CVK_TEST(Barrier, add_transition_on_a_mip_range) { test::Context const context{}; - Image image{context.core, IMAGE_CONFIG, EXTENT}; + Image const image{context.core, IMAGE_CONFIG, EXTENT}; + // the validation layers check every old layout test::record_and_submit(context, [&](PrimaryCmdBuffer const& primary) { - image.transitionLayout(primary, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL); - image.transitionLayout(primary, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, 1, 2); + Barrier toTransfer{}; + toTransfer.addTransition(image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_UNDEFINED); + toTransfer.execute(primary); + + Barrier toRead{}; + toRead.addTransition( + image, + VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, + VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, + 1, + 2 + ); + toRead.addTransition(image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 3); + toRead.execute(primary); + + Barrier back{}; + back.addTransition(image, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, 3); + back.execute(primary); }); - - EXPECT_EQ(image.layout(0), VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL); - EXPECT_EQ(image.layout(1), VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL); - EXPECT_EQ(image.layout(2), VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL); - EXPECT_EQ(image.layout(3), VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL); } -CVK_TEST(Barrier, execute_twice_starts_from_the_new_layout) { +CVK_TEST(Barrier, memory_only_image_barrier) { test::Context const context{}; - Image image{context.core, IMAGE_CONFIG, EXTENT}; - Barrier barrier{}; - barrier.add(image, ImageBarrierInfo::LayoutTransition(VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, {}, TRANSFER_WRITE)); + Image const image{context.core, IMAGE_CONFIG, EXTENT}; - test::record_and_submit(context, [&](PrimaryCmdBuffer const& primary) { - barrier.execute(primary); - barrier.execute(primary); - }); - - EXPECT_TRUE(all_layouts(image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL)); + Barrier barrier{}; + barrier.add(image, {.src = TRANSFER_WRITE, .dst = TRANSFER_WRITE}); + test::record_and_submit(context, [&](PrimaryCmdBuffer const& primary) { barrier.execute(primary); }); } } diff --git a/jvk/prework/flat_hash_map.hpp b/jvk/prework/flat_hash_map.hpp new file mode 100644 index 00000000..666f6a35 --- /dev/null +++ b/jvk/prework/flat_hash_map.hpp @@ -0,0 +1,11 @@ +#pragma once +#include + + +namespace cvk { +/** + * @brief hash map without pointer stability, a rehash moves the elements + */ +template +using flat_hash_map_t = gtl::flat_hash_map; +} diff --git a/jvk/prework/image_layouts.cpp b/jvk/prework/image_layouts.cpp new file mode 100644 index 00000000..b30a0bac --- /dev/null +++ b/jvk/prework/image_layouts.cpp @@ -0,0 +1,48 @@ +#include "image_layouts.hpp" + +#include "cvk/data/img/image.hpp" + +#include + +#include + + +namespace cvk { +VkImageLayout ImageLayouts::layout(Image const& image, uint32_t mipLevel) const { + CTH_CRITICAL(mipLevel >= image.mipLevels(), "mip level must lie in the image") {} + + auto const levels = layouts(image); + return levels.empty() ? VK_IMAGE_LAYOUT_UNDEFINED : levels[mipLevel]; +} + +std::span ImageLayouts::layouts(Image const& image) const { + auto const it = _layouts.find(image.get()); + if(it == _layouts.end()) return {}; + + CTH_CRITICAL( + it->second.size() != image.mipLevels(), + "the entry has other mip levels than the image, a destroyed image left it" + ) {} + return it->second; +} + +void ImageLayouts::set(Image const& image, VkImageLayout layout, uint32_t firstMipLevel, uint32_t mipLevels) { + Image::debug_check(image); + CTH_CRITICAL(firstMipLevel >= image.mipLevels(), "first mip level must lie in the image") {} + + uint32_t const count = mipLevels == VK_REMAINING_MIP_LEVELS ? image.mipLevels() - firstMipLevel : mipLevels; + CTH_CRITICAL(count > image.mipLevels() - firstMipLevel, "mip levels must lie in the image") {} + + auto& levels = _layouts.try_emplace(image.get(), image.mipLevels(), VK_IMAGE_LAYOUT_UNDEFINED).first->second; + CTH_CRITICAL( + levels.size() != image.mipLevels(), + "the entry has other mip levels than the image, a destroyed image left it" + ) {} + + std::ranges::fill_n(levels.begin() + firstMipLevel, static_cast(count), layout); +} + +void ImageLayouts::erase(Image const& image) { _layouts.erase(image.get()); } + +bool ImageLayouts::contains(Image const& image) const { return _layouts.contains(image.get()); } +} diff --git a/jvk/prework/image_layouts.hpp b/jvk/prework/image_layouts.hpp new file mode 100644 index 00000000..259da692 --- /dev/null +++ b/jvk/prework/image_layouts.hpp @@ -0,0 +1,62 @@ +#pragma once +#include "flat_hash_map.hpp" + +#include + +#include +#include +#include + + +namespace cvk { +class Image; + +/** + * @brief layout of each mip level of the images, for the functions that transition them + * @details the caller owns it and decides the scope: one command buffer, one thread or one serial frame, + * unknown image -> every mip level is VK_IMAGE_LAYOUT_UNDEFINED + * @attention follows the order of the commands, use it on one thread + * @attention keeps the entry of a destroyed image and a new image can get the same handle, + * call @ref erase() before the image is destroyed + */ +class ImageLayouts { +public: + /** + * @return the layout of the mip level, unknown image -> VK_IMAGE_LAYOUT_UNDEFINED + * @pre mipLevel < @ref Image::mipLevels() + */ + [[nodiscard]] VkImageLayout layout(Image const&, uint32_t mipLevel = 0) const; + + /** + * @return the layout of each mip level, unknown image -> empty + */ + [[nodiscard]] std::span layouts(Image const&) const; + + /** + * @brief sets the layout of the mip levels + * @param mipLevels (VK_REMAINING_MIP_LEVELS => to the last mip level) + * @details unknown image -> the other mip levels are VK_IMAGE_LAYOUT_UNDEFINED + * @pre @ref Image::created() + * @pre the mip range lies in the image + */ + void set(Image const&, VkImageLayout, uint32_t firstMipLevel = 0, uint32_t mipLevels = VK_REMAINING_MIP_LEVELS); + + /** + * @brief removes the image, its layouts are unknown after + */ + void erase(Image const&); + + /** + * @brief removes every image + */ + void clear() { _layouts.clear(); } + + [[nodiscard]] bool contains(Image const&) const; + [[nodiscard]] size_t size() const { return _layouts.size(); } + [[nodiscard]] bool empty() const { return _layouts.empty(); } + +private: + //IMPLEMENT key (image, mip level) -> layout: no vector per image, no mip count check + flat_hash_map_t> _layouts{}; +}; +} diff --git a/jvk/prework/image_layouts_test.cpp b/jvk/prework/image_layouts_test.cpp new file mode 100644 index 00000000..c1fda747 --- /dev/null +++ b/jvk/prework/image_layouts_test.cpp @@ -0,0 +1,89 @@ +#include "context.hpp" +#include "test.hpp" + +#include "cvk/data/img/image.hpp" +#include "image_layouts.hpp" + +#include + + +namespace cvk { +namespace { + constexpr VkExtent2D EXTENT{16, 16}; + + constexpr Image::Config IMAGE_CONFIG{ + .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, + .format = VK_FORMAT_R8G8B8A8_UNORM, + .usage = VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT, + .mipLevels = Image::evalMipLevelCount(EXTENT), + }; +} + +CVK_TEST(ImageLayouts, unknown_image_is_undefined) { + test::Context const context{}; + Image const image{context.core, IMAGE_CONFIG, EXTENT}; + ImageLayouts const layouts{}; + + EXPECT_TRUE(layouts.empty()); + EXPECT_FALSE(layouts.contains(image)); + EXPECT_TRUE(layouts.layouts(image).empty()); + EXPECT_EQ(layouts.layout(image, 2), VK_IMAGE_LAYOUT_UNDEFINED); +} + +CVK_TEST(ImageLayouts, set_a_mip_range) { + test::Context const context{}; + Image const image{context.core, IMAGE_CONFIG, EXTENT}; + ImageLayouts layouts{}; + + layouts.set(image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, 2); + + EXPECT_TRUE(layouts.contains(image)); + EXPECT_EQ(layouts.size(), 1); + ASSERT_EQ(layouts.layouts(image).size(), image.mipLevels()); + EXPECT_EQ(layouts.layout(image, 0), VK_IMAGE_LAYOUT_UNDEFINED); + EXPECT_EQ(layouts.layout(image, 1), VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL); + EXPECT_EQ(layouts.layout(image, 2), VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL); + EXPECT_EQ(layouts.layout(image, 3), VK_IMAGE_LAYOUT_UNDEFINED); +} + +CVK_TEST(ImageLayouts, set_the_remaining_mip_levels) { + test::Context const context{}; + Image const image{context.core, IMAGE_CONFIG, EXTENT}; + ImageLayouts layouts{}; + + layouts.set(image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL); + layouts.set(image, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, 2); + + EXPECT_EQ(layouts.layout(image, 1), VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL); + EXPECT_EQ(layouts.layout(image, 2), VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL); + EXPECT_EQ(layouts.layout(image, image.mipLevels() - 1), VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL); +} + +CVK_TEST(ImageLayouts, follows_the_handle_when_the_image_moves) { + test::Context const context{}; + Image image{context.core, IMAGE_CONFIG, EXTENT}; + ImageLayouts layouts{}; + layouts.set(image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL); + + Image const moved{std::move(image)}; + + EXPECT_EQ(layouts.layout(moved), VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL); +} + +CVK_TEST(ImageLayouts, erase_and_clear) { + test::Context const context{}; + Image const first{context.core, IMAGE_CONFIG, EXTENT}; + Image const second{context.core, IMAGE_CONFIG, EXTENT}; + ImageLayouts layouts{}; + layouts.set(first, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL); + layouts.set(second, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL); + + layouts.erase(first); + EXPECT_FALSE(layouts.contains(first)); + EXPECT_EQ(layouts.layout(first), VK_IMAGE_LAYOUT_UNDEFINED); + EXPECT_TRUE(layouts.contains(second)); + + layouts.clear(); + EXPECT_TRUE(layouts.empty()); +} +} diff --git a/lib/gtl.cmake b/lib/gtl.cmake new file mode 100644 index 00000000..35fa86d5 --- /dev/null +++ b/lib/gtl.cmake @@ -0,0 +1,7 @@ +# gtl installs only its headers and no cmake package, this creates the target gtl::gtl +if(NOT TARGET gtl::gtl) + find_path(GTL_INCLUDE_DIR gtl/phmap.hpp REQUIRED) + + add_library(gtl::gtl INTERFACE IMPORTED) + target_include_directories(gtl::gtl SYSTEM INTERFACE ${GTL_INCLUDE_DIR}) +endif() diff --git a/vcpkg.json b/vcpkg.json index 6a7a7773..74ddfe1d 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -1,6 +1,7 @@ { "dependencies": [ "boost-pfr", + "gtl", "gtest", "volk", "vulkan-memory-allocator", From b5eea5fee5ebb99c5f56964f5c313a6761b64d63 Mon Sep 17 00:00:00 2001 From: lukas <89145039+codethinki@users.noreply.github.com> Date: Sat, 17 Oct 2026 23:48:55 +0200 Subject: [PATCH 055/135] cvk: queue submit, aggregate barrier, sync validation, pipeline keeps its layout - SubmitInfo and SemaphoreSubmitInfo, Queue::submit with an optional fence - timeline values without value resolve at the submit: waits value(), signals next() - Barrier is an aggregate, execute() checks the barriers - debug builds enable synchronization validation through VK_EXT_layer_settings - Pipeline keeps its PipelineLayout, layout() returns it --- AGENTS.md | 3 +- cvk/incl/cvk/base/instance.hpp | 18 ++- cvk/incl/cvk/base/queue/queue.hpp | 22 ++++ cvk/incl/cvk/base/queue/submit_info.hpp | 63 ++++++++++ cvk/incl/cvk/render/pipeline/pipeline.hpp | 10 +- cvk/incl/cvk/render/sync/barrier.hpp | 109 ++++++++-------- cvk/src/base/instance.cpp | 7 ++ cvk/src/base/queue/queue.cpp | 119 ++++++++++++++++++ cvk/src/render/pipeline/pipeline.cpp | 7 +- cvk/src/render/sync/barrier.cpp | 114 ++++++++--------- cvk/tests/src/base/queue/submit_test.cpp | 97 ++++++++++++++ cvk/tests/src/context.cpp | 20 +-- .../src/render/pipeline/pipeline_test.cpp | 4 +- cvk/tests/src/render/sync/barrier_test.cpp | 36 ++++-- 14 files changed, 473 insertions(+), 156 deletions(-) create mode 100644 cvk/incl/cvk/base/queue/submit_info.hpp create mode 100644 cvk/tests/src/base/queue/submit_test.cpp diff --git a/AGENTS.md b/AGENTS.md index cf1b39d6..3999364b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -42,7 +42,7 @@ ctest --test-dir out/build/gcc_release --output-on-failure - Presets: `gcc_debug`, `gcc_release`, `clang_debug`, `clang_release`, `msvc_debug`, `msvc_release`. - `cth_ROOT` points at `../cth/out/install/`. That install must exist. -- Debug builds enable the validation layers. Run the tests in a debug build before you report a change as done. +- Debug builds enable the validation layers with synchronization validation (`Instance::VALIDATION_LAYER_SETTINGS`). Run the tests in a debug build before you report a change as done. - Tests that need more queues than the device has skip. Mesa Intel devices get more queues with `ANV_QUEUE_OVERRIDE=gc=2,c=1`. - After a new cth install, build with `--clean-first`. The precompiled header does not see the changed cth headers. - Tests that need a device use `test::Context` (`cvk/tests/src/context.hpp`). It fails the test on validation errors, leaked objects included. @@ -96,6 +96,7 @@ ctest --test-dir out/build/gcc_release --output-on-failure - Put `explicit` on constructors with one parameter and on tag constructors. - Use `auto const` for locals that do not change. - Initialize Vulkan structs with designated initializers. Always write `.sType` and `.pNext`. +- Data structs (`SubmitInfo`, `Barrier`, the info structs) are aggregates: the caller can set everything with designated initializers. Member functions like `add()` are optional shortcuts, checks run where the data is used. - Use deducing `this` (`this auto& self`) where a function must work on the derived type. - Qualify `std::` names. A `.cpp` file can declare `using std::span;` and similar at the top of its namespace. - No raw `new` and `delete`. Use `std::unique_ptr`, `cth::move_ptr` and `cth::not_null`. diff --git a/cvk/incl/cvk/base/instance.hpp b/cvk/incl/cvk/base/instance.hpp index 6d6045f4..9cd0d377 100644 --- a/cvk/incl/cvk/base/instance.hpp +++ b/cvk/incl/cvk/base/instance.hpp @@ -35,11 +35,27 @@ class Instance { VK_EXT_DEBUG_UTILS_EXTENSION_NAME, }; + static constexpr VkBool32 VALIDATE_SYNC = VK_TRUE; + + /** + * @brief settings of the validation layers + * @details validate_sync: reports missing and too narrow barriers, semaphores and other dependencies + */ + static constexpr std::array VALIDATION_LAYER_SETTINGS{ + VkLayerSettingEXT{ + .pLayerName = VALIDATION_LAYERS[0], + .pSettingName = "validate_sync", + .type = VK_LAYER_SETTING_TYPE_BOOL32_EXT, + .valueCount = 1, + .pValues = &VALIDATE_SYNC, + }, + }; + static constexpr std::array REQUIRED_INSTANCE_EXTENSIONS{}; /** * @brief base constructor - * @details debug builds enable the validation layers + * @details debug builds enable the validation layers with @ref VALIDATION_LAYER_SETTINGS * @throws cth::except::default_exception reason: required instance extension missing * @throws cth::except::default_exception reason: required validation layer missing */ diff --git a/cvk/incl/cvk/base/queue/queue.hpp b/cvk/incl/cvk/base/queue/queue.hpp index e1bf0041..47bbef98 100644 --- a/cvk/incl/cvk/base/queue/queue.hpp +++ b/cvk/incl/cvk/base/queue/queue.hpp @@ -1,13 +1,18 @@ #pragma once #include "queue_family.hpp" #include "queue_state.hpp" +#include "submit_info.hpp" #include #include +#include + namespace cvk { +class Fence; + /** * @brief VkQueue wrapper * @details copyable, thread safe, copies share the mutex of the vulkan queue @@ -46,6 +51,23 @@ class Queue { */ State release(); + /** + * @brief submits the infos in one vkQueueSubmit2() + * @param fence nullptr -> no fence + * @details resolves the std::nullopt timeline values in order: the waits, then the signals of each info + * @pre @ref created() + * @pre the command buffers ended their recording + * @pre the semaphores and the fence are created, the fence is unsignaled + * @throws cvk::vk_result_exception result of vkQueueSubmit2() + */ + void submit(std::span infos, Fence const* fence = nullptr) const; + + /** + * @brief submits the info + * @details calls @ref submit(std::span, Fence const*) const + */ + void submit(SubmitInfo const& info, Fence const* fence = nullptr) const { submit({&info, 1}, fence); } + /** * @brief submits to the vulkan queue * @param fence VK_NULL_HANDLE -> no fence diff --git a/cvk/incl/cvk/base/queue/submit_info.hpp b/cvk/incl/cvk/base/queue/submit_info.hpp new file mode 100644 index 00000000..e73d4e1a --- /dev/null +++ b/cvk/incl/cvk/base/queue/submit_info.hpp @@ -0,0 +1,63 @@ +#pragma once +#include "cvk/utility/types.hpp" + +#include + +#include +#include +#include +#include + + +namespace cvk { +class PrimaryCmdBuffer; +class Semaphore; +class TimelineSemaphore; + +/** + * @brief semaphore of a submit, binary or timeline + * @details the constructor decides the kind + */ +struct SemaphoreSubmitInfo { + /** + * @param stages wait -> the stages that wait, signal -> the stages that finish before the signal + */ + constexpr SemaphoreSubmitInfo( + Semaphore const& semaphore, + VkPipelineStageFlags2 stages = VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT + ) : semaphore{&semaphore}, stages{stages} {} + + /** + * @param stages wait -> the stages that wait, signal -> the stages that finish before the signal + * @param value std::nullopt -> resolved at the submit, + * wait: @ref TimelineSemaphore::value(), signal: @ref TimelineSemaphore::next() + */ + constexpr SemaphoreSubmitInfo( + TimelineSemaphore const& semaphore, + VkPipelineStageFlags2 stages = VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT, + std::optional value = std::nullopt + ) : semaphore{&semaphore}, stages{stages}, value{value} {} + + std::variant, not_null> semaphore; + VkPipelineStageFlags2 stages; + + /** + * @brief timeline semaphores only + * @attention an explicit value does not change the counter of the semaphore + */ + std::optional value = std::nullopt; +}; + +/** + * @brief one batch of a queue submit + */ +struct SubmitInfo { + /** + * @brief the gpu executes them in order + */ + std::vector> cmdBuffers{}; + + std::vector waits{}; + std::vector signals{}; +}; +} diff --git a/cvk/incl/cvk/render/pipeline/pipeline.hpp b/cvk/incl/cvk/render/pipeline/pipeline.hpp index 22f9c4a2..1811c56f 100644 --- a/cvk/incl/cvk/render/pipeline/pipeline.hpp +++ b/cvk/incl/cvk/render/pipeline/pipeline.hpp @@ -19,6 +19,7 @@ class PipelineLayout; /** * @brief graphics VkPipeline wrapper for dynamic rendering + * @details the layout is a parent of the pipeline: it outlives the pipeline */ class Pipeline { public: @@ -27,9 +28,8 @@ class Pipeline { struct State; /** - * @brief base constructor, copies the handle of the layout + * @brief base constructor, keeps the layout * @pre @ref Core::created() - * @pre @ref PipelineLayout::created() */ Pipeline(Core const&, PipelineLayout const&, Config); @@ -50,7 +50,7 @@ class Pipeline { explicit Pipeline(Core const&, PipelineLayout const&, Config, create_t); /** - * @brief base constructor of a derivative pipeline, copies the handle and the layout of the parent + * @brief base constructor of a derivative pipeline, keeps the layout and copies the handle of the parent * @pre @ref Core::created() * @pre @ref created() of the parent, with VK_PIPELINE_CREATE_ALLOW_DERIVATIVES_BIT in its flags */ @@ -116,7 +116,7 @@ class Pipeline { [[nodiscard]] constexpr bool created() const { return static_cast(_handle); } [[nodiscard]] constexpr vk_handle_t get() const { return _handle.get(); } - [[nodiscard]] constexpr VkPipelineLayout layout() const { return _vkLayout.get(); } + [[nodiscard]] constexpr PipelineLayout const& layout() const { return *_layout; } /** * @return VK_NULL_HANDLE -> not a derivative pipeline @@ -129,7 +129,7 @@ class Pipeline { void forget(); core_handle _handle; - vk_not_null _vkLayout; + not_null _layout; VkPipeline _vkParent = VK_NULL_HANDLE; Config _config; diff --git a/cvk/incl/cvk/render/sync/barrier.hpp b/cvk/incl/cvk/render/sync/barrier.hpp index c7c2f1ba..12b99cbe 100644 --- a/cvk/incl/cvk/render/sync/barrier.hpp +++ b/cvk/incl/cvk/render/sync/barrier.hpp @@ -1,6 +1,7 @@ #pragma once #include "cvk/base/queue/queue_family.hpp" #include "cvk/render/cmd/cmd_buffer.hpp" +#include "cvk/utility/format.hpp" #include "cvk/utility/types.hpp" #include @@ -134,40 +135,60 @@ struct LayoutTransition { ); +/** + * @brief global memory part of a barrier + */ +struct MemoryBarrierInfo { + BarrierScope src{}; + BarrierScope dst{}; +}; + /** * @brief pipeline barrier: memory, buffer and image barriers in one vkCmdPipelineBarrier2() + * @details aggregate, the add functions are shortcuts, + * @ref execute() checks the barriers * @attention the images and buffers do not move until @ref execute() */ -class Barrier { -public: +struct Barrier { + using MemoryInfo = MemoryBarrierInfo; using ImageInfo = ImageBarrierInfo; using BufferInfo = BufferBarrierInfo; + struct BufferEntry { + not_null buffer; + BufferInfo info; + }; + + struct ImageEntry { + not_null image; + ImageInfo info; + }; + /** - * @param flags VK_DEPENDENCY_BY_REGION_BIT -> the dependency is per framebuffer region, e.g. per tile + * @brief VK_DEPENDENCY_BY_REGION_BIT -> the dependency is per framebuffer region, e.g. per tile */ - constexpr explicit Barrier(VkDependencyFlags flags = 0) : _flags{flags} {} + VkDependencyFlags flags = 0; + + std::vector memory{}; + std::vector buffers{}; + std::vector images{}; /** * @brief adds a global memory barrier - * @pre no queue family ownership transfer */ - constexpr void add(BarrierScope const& src, BarrierScope const& dst); + constexpr void add(BarrierScope const& src, BarrierScope const& dst) { memory.push_back({src, dst}); } /** * @brief adds an image barrier - * @pre @ref Image::created() - * @pre the mip range lies in the image */ - void add(Image const&, ImageInfo const&); + constexpr void add(Image const& image, ImageInfo const& info) { images.push_back({&image, info}); } /** * @brief adds a layout transition with the scopes of @ref layout_transition() * @param mipLevels (VK_REMAINING_MIP_LEVELS => to the last mip level) * @pre @ref layout_transition() knows the transition - * @pre the preconditions of @ref add(Image const&, ImageInfo const&) */ - void addTransition( + constexpr void addTransition( Image const&, VkImageLayout newLayout, VkImageLayout oldLayout, @@ -177,16 +198,15 @@ class Barrier { /** * @brief adds a buffer barrier - * @details the buffer handle comes from the buffer at @ref execute() - * @pre @ref BaseBuffer::created() - * @pre the range lies in the buffer */ - void add(BaseBuffer const&, BufferInfo const&); + constexpr void add(BaseBuffer const& buffer, BufferInfo const& info) { buffers.push_back({&buffer, info}); } /** * @brief records the barriers * @details inside a rendering: checks the rules of barriers inside a dynamic rendering * @pre @ref BaseCmdBuffer::recording() + * @pre the images and buffers are created and the ranges lie in them + * @pre memory barriers have no queue family ownership transfer * @pre the new layout of a layout transition or queue family ownership transfer * is not VK_IMAGE_LAYOUT_UNDEFINED or VK_IMAGE_LAYOUT_PREINITIALIZED * @pre inside a rendering: framebuffer space stages, VK_DEPENDENCY_BY_REGION_BIT, @@ -195,18 +215,15 @@ class Barrier { void execute(BaseCmdBuffer const&) const; /** - * @brief removes every barrier + * @brief removes every barrier, keeps the flags */ constexpr void clear() { - _memoryBarriers.clear(); - _buffers.clear(); - _images.clear(); + memory.clear(); + buffers.clear(); + images.clear(); } - [[nodiscard]] constexpr VkDependencyFlags flags() const { return _flags; } - [[nodiscard]] constexpr bool empty() const { - return _memoryBarriers.empty() && _buffers.empty() && _images.empty(); - } + [[nodiscard]] constexpr bool empty() const { return memory.empty() && buffers.empty() && images.empty(); } private: /** @@ -214,43 +231,29 @@ class Barrier { */ void debug_check_rendering( BaseCmdBuffer::Rendering const&, + std::span, std::span, std::span ) const; - - struct BufferEntry { - not_null buffer; - BufferInfo info; - }; - - struct ImageEntry { - not_null image; - ImageInfo info; - }; - - VkDependencyFlags _flags; - std::vector _memoryBarriers{}; - std::vector _buffers{}; - std::vector _images{}; }; } namespace cvk { -constexpr void Barrier::add(BarrierScope const& src, BarrierScope const& dst) { - CTH_CRITICAL( - src.queueFamilyIndex != VK_QUEUE_FAMILY_IGNORED || dst.queueFamilyIndex != VK_QUEUE_FAMILY_IGNORED, - "memory barriers have no queue family ownership transfer" - ) {} - - _memoryBarriers.push_back( - VkMemoryBarrier2{ - .sType = VK_STRUCTURE_TYPE_MEMORY_BARRIER_2, - .pNext = nullptr, - .srcStageMask = src.stages, - .srcAccessMask = src.access, - .dstStageMask = dst.stages, - .dstAccessMask = dst.access, - } +constexpr void Barrier::addTransition( + Image const& image, + VkImageLayout newLayout, + VkImageLayout oldLayout, + uint32_t firstMipLevel, + uint32_t mipLevels +) { + auto const transition = layout_transition(oldLayout, newLayout); + CTH_CRITICAL(!transition.has_value(), "layout transition has no table entry, use add() with scopes") { + details->add("transition: {} -> {}", oldLayout, newLayout); + } + + add( + image, + ImageInfo::LayoutTransition(newLayout, oldLayout, transition->src, transition->dst, firstMipLevel, mipLevels) ); } } diff --git a/cvk/src/base/instance.cpp b/cvk/src/base/instance.cpp index 34af741a..a6c37db6 100644 --- a/cvk/src/base/instance.cpp +++ b/cvk/src/base/instance.cpp @@ -67,6 +67,12 @@ void Instance::create(std::optional messengerConfig) { auto const appInfo = this->appInfo(); VkDebugUtilsMessengerCreateInfoEXT debugCreateInfo{}; + VkLayerSettingsCreateInfoEXT const layerSettings{ + .sType = VK_STRUCTURE_TYPE_LAYER_SETTINGS_CREATE_INFO_EXT, + .pNext = nullptr, + .settingCount = static_cast(VALIDATION_LAYER_SETTINGS.size()), + .pSettings = VALIDATION_LAYER_SETTINGS.data(), + }; VkInstanceCreateInfo createInfo{ .sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO, @@ -86,6 +92,7 @@ void Instance::create(std::optional messengerConfig) { createInfo.ppEnabledLayerNames = VALIDATION_LAYERS.data(); debugCreateInfo = messengerConfig->createInfo(); + debugCreateInfo.pNext = &layerSettings; createInfo.pNext = &debugCreateInfo; } diff --git a/cvk/src/base/queue/queue.cpp b/cvk/src/base/queue/queue.cpp index 39acf1f8..f2b0d389 100644 --- a/cvk/src/base/queue/queue.cpp +++ b/cvk/src/base/queue/queue.cpp @@ -1,9 +1,53 @@ #include "cvk/base/queue/queue.hpp" +#include "cvk/render/cmd/cmd_buffer.hpp" +#include "cvk/render/sync/fence.hpp" +#include "cvk/render/sync/semaphore.hpp" +#include "cvk/render/sync/timeline_semaphore.hpp" #include "cvk/utility/vk_exceptions.hpp" +#include +#include +#include + namespace cvk { +using std::span; + +namespace { + VkSemaphoreSubmitInfo vk_semaphore_info(VkSemaphore vkSemaphore, uint64_t value, VkPipelineStageFlags2 stages) { + return VkSemaphoreSubmitInfo{ + .sType = VK_STRUCTURE_TYPE_SEMAPHORE_SUBMIT_INFO, + .pNext = nullptr, + .semaphore = vkSemaphore, + .value = value, + .stageMask = stages, + .deviceIndex = 0, + }; + } + + /** + * @param resolve the value of a timeline semaphore without value + */ + template + VkSemaphoreSubmitInfo semaphore_info(SemaphoreSubmitInfo const& info, Resolve resolve) { + return std::visit( + [&](not_null semaphore) { + T::debug_check(*semaphore); + + if constexpr(std::same_as) { + auto const value = info.value.has_value() ? *info.value : resolve(*semaphore); + return vk_semaphore_info(semaphore->get(), value, info.stages); + } else { + CTH_CRITICAL(info.value.has_value(), "binary semaphores have no value") {} + return vk_semaphore_info(semaphore->get(), 0, info.stages); + } + }, + info.semaphore + ); + } +} + void Queue::wrap(State state) { State::debug_check(state); optDestroy(); @@ -33,6 +77,81 @@ Queue::State Queue::release() { return state; } +void Queue::submit(span infos, Fence const* fence) const { + debug_check(*this); + if(fence != nullptr) Fence::debug_check(*fence); + + size_t cmdBufferCount = 0; + size_t semaphoreCount = 0; + for(auto const& info : infos) { + cmdBufferCount += info.cmdBuffers.size(); + semaphoreCount += info.waits.size() + info.signals.size(); + } + + // reserved: the submit infos point into the vectors + std::vector cmdBuffers{}; + cmdBuffers.reserve(cmdBufferCount); + std::vector semaphores{}; + semaphores.reserve(semaphoreCount); + std::vector submits{}; + submits.reserve(infos.size()); + + auto const waitValue = [](TimelineSemaphore const& timeline) { return timeline.value(); }; + auto const signalValue = [](TimelineSemaphore const& timeline) { return timeline.next(); }; + + for(auto const& info : infos) { + auto const* const waits = semaphores.data() + semaphores.size(); + for(auto const& wait : info.waits) + semaphores.push_back(semaphore_info(wait, waitValue)); + + auto const* const signals = semaphores.data() + semaphores.size(); + for(auto const& signal : info.signals) + semaphores.push_back(semaphore_info(signal, signalValue)); + + auto const* const buffers = cmdBuffers.data() + cmdBuffers.size(); + for(auto const& cmdBuffer : info.cmdBuffers) { + BaseCmdBuffer::debug_check(*cmdBuffer); + CTH_CRITICAL(cmdBuffer->recording(), "command buffers must end their recording first") {} + + cmdBuffers.push_back( + VkCommandBufferSubmitInfo{ + .sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_SUBMIT_INFO, + .pNext = nullptr, + .commandBuffer = cmdBuffer->get(), + .deviceMask = 0, + } + ); + } + + submits.push_back( + VkSubmitInfo2{ + .sType = VK_STRUCTURE_TYPE_SUBMIT_INFO_2, + .pNext = nullptr, + .flags = 0, + .waitSemaphoreInfoCount = static_cast(info.waits.size()), + .pWaitSemaphoreInfos = waits, + .commandBufferInfoCount = static_cast(info.cmdBuffers.size()), + .pCommandBufferInfos = buffers, + .signalSemaphoreInfoCount = static_cast(info.signals.size()), + .pSignalSemaphoreInfos = signals, + } + ); + } + + VkResult result; + { + std::scoped_lock const lock{*_handleMtx}; + result = _functions->vkQueueSubmit2( + _handle.get(), + static_cast(submits.size()), + submits.data(), + fence != nullptr ? fence->get() : VK_NULL_HANDLE + ); + } + + CVK_RESULT_STABLE_THROW(result != VK_SUCCESS, result, "failed to submit to queue") {} +} + void Queue::raw_submit(VkSubmitInfo2 const& submitInfo, VkFence fence) const { debug_check(*this); diff --git a/cvk/src/render/pipeline/pipeline.cpp b/cvk/src/render/pipeline/pipeline.cpp index 43cf7cec..8e5817b3 100644 --- a/cvk/src/render/pipeline/pipeline.cpp +++ b/cvk/src/render/pipeline/pipeline.cpp @@ -8,7 +8,7 @@ namespace cvk { Pipeline::Pipeline(Core const& core, PipelineLayout const& layout, Config config) : _handle{core}, - _vkLayout{layout.get()}, + _layout{&layout}, _config{std::move(config)} { Core::debug_check(core); } Pipeline::Pipeline(Core const& core, PipelineLayout const& layout, Config config, State const& state) : Pipeline{ @@ -24,7 +24,7 @@ Pipeline::Pipeline(Core const& core, PipelineLayout const& layout, Config config } { create(); } Pipeline::Pipeline(Core const& core, Pipeline const& parent, Config config) : _handle{core}, - _vkLayout{parent.layout()}, + _layout{&parent.layout()}, _vkParent{parent.get()}, _config{std::move(config)} { Core::debug_check(core); @@ -48,6 +48,7 @@ void Pipeline::wrap(State const& state) { } void Pipeline::create() { + PipelineLayout::debug_check(*_layout); CTH_CRITICAL( _config.colorBlendAttachments.size() != _config.colorFormats.size(), "one color blend attachment per color format required" @@ -114,7 +115,7 @@ void Pipeline::create() { .pDepthStencilState = &config.depthStencil, .pColorBlendState = &colorBlend, .pDynamicState = &dynamicState, - .layout = _vkLayout.get(), + .layout = _layout->get(), .renderPass = VK_NULL_HANDLE, .subpass = 0, .basePipelineHandle = _vkParent, diff --git a/cvk/src/render/sync/barrier.cpp b/cvk/src/render/sync/barrier.cpp index c6fb89d2..1fa4f6fc 100644 --- a/cvk/src/render/sync/barrier.cpp +++ b/cvk/src/render/sync/barrier.cpp @@ -23,8 +23,27 @@ namespace { return ((srcStages | dstStages) & ~FRAMEBUFFER_STAGES) == 0; } + VkMemoryBarrier2 memory_barrier(MemoryBarrierInfo const& info) { + bool const transfer = info.src.queueFamilyIndex != VK_QUEUE_FAMILY_IGNORED + || info.dst.queueFamilyIndex != VK_QUEUE_FAMILY_IGNORED; + CTH_CRITICAL(transfer, "memory barriers have no queue family ownership transfer") {} + + return VkMemoryBarrier2{ + .sType = VK_STRUCTURE_TYPE_MEMORY_BARRIER_2, + .pNext = nullptr, + .srcStageMask = info.src.stages, + .srcAccessMask = info.src.access, + .dstStageMask = info.dst.stages, + .dstAccessMask = info.dst.access, + }; + } + VkBufferMemoryBarrier2 buffer_barrier(BaseBuffer const& buffer, BufferBarrierInfo const& info) { BaseBuffer::debug_check(buffer); + CTH_CRITICAL( + info.offset > buffer.size() || (info.size != VK_WHOLE_SIZE && info.size > buffer.size() - info.offset), + "range must lie in the buffer" + ) {} return VkBufferMemoryBarrier2{ .sType = VK_STRUCTURE_TYPE_BUFFER_MEMORY_BARRIER_2, @@ -43,6 +62,21 @@ namespace { VkImageMemoryBarrier2 image_barrier(Image const& image, ImageBarrierInfo const& info) { Image::debug_check(image); + CTH_CRITICAL(info.mipLevels == 0, "mip levels must not be 0") {} + CTH_CRITICAL(info.firstMipLevel >= image.mipLevels(), "first mip level must lie in the image") {} + CTH_CRITICAL( + info.mipLevels != VK_REMAINING_MIP_LEVELS && info.mipLevels > image.mipLevels() - info.firstMipLevel, + "mip levels must lie in the image" + ) {} + + bool const transfer = info.src.queueFamilyIndex != info.dst.queueFamilyIndex; + CTH_CRITICAL( + (info.oldLayout != info.newLayout || transfer) + && (info.newLayout == VK_IMAGE_LAYOUT_UNDEFINED || info.newLayout == VK_IMAGE_LAYOUT_PREINITIALIZED), + "a transition or queue family transfer must not have an undefined or preinitialized new layout" + ) { + details->add("transition: {} -> {}", info.oldLayout, info.newLayout); + } return VkImageMemoryBarrier2{ .sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER_2, @@ -67,57 +101,9 @@ namespace { } } -void Barrier::add(Image const& image, ImageInfo const& info) { - Image::debug_check(image); - CTH_CRITICAL(info.mipLevels == 0, "mip levels must not be 0") {} - CTH_CRITICAL(info.firstMipLevel >= image.mipLevels(), "first mip level must lie in the image") {} - CTH_CRITICAL( - info.mipLevels != VK_REMAINING_MIP_LEVELS && info.mipLevels > image.mipLevels() - info.firstMipLevel, - "mip levels must lie in the image" - ) {} - - bool const transfer = info.src.queueFamilyIndex != info.dst.queueFamilyIndex; - CTH_CRITICAL( - (info.oldLayout != info.newLayout || transfer) - && (info.newLayout == VK_IMAGE_LAYOUT_UNDEFINED || info.newLayout == VK_IMAGE_LAYOUT_PREINITIALIZED), - "a transition or queue family transfer must not have an undefined or preinitialized new layout" - ) { - details->add("transition: {} -> {}", info.oldLayout, info.newLayout); - } - - _images.emplace_back(&image, info); -} - -void Barrier::addTransition( - Image const& image, - VkImageLayout newLayout, - VkImageLayout oldLayout, - uint32_t firstMipLevel, - uint32_t mipLevels -) { - auto const transition = layout_transition(oldLayout, newLayout); - CTH_CRITICAL(!transition.has_value(), "layout transition has no table entry, use add() with scopes") { - details->add("transition: {} -> {}", oldLayout, newLayout); - } - - add( - image, - ImageInfo::LayoutTransition(newLayout, oldLayout, transition->src, transition->dst, firstMipLevel, mipLevels) - ); -} - -void Barrier::add(BaseBuffer const& buffer, BufferInfo const& info) { - BaseBuffer::debug_check(buffer); - CTH_CRITICAL( - info.offset > buffer.size() || (info.size != VK_WHOLE_SIZE && info.size > buffer.size() - info.offset), - "range must lie in the buffer" - ) {} - - _buffers.emplace_back(&buffer, info); -} - void Barrier::debug_check_rendering( BaseCmdBuffer::Rendering const& rendering, + std::span memoryBarriers, std::span bufferBarriers, std::span imageBarriers ) const { @@ -128,7 +114,7 @@ void Barrier::debug_check_rendering( framebufferSpace &= framebuffer_space(src, dst); }; - for(auto const& barrier : _memoryBarriers) + for(auto const& barrier : memoryBarriers) check(barrier.srcStageMask, barrier.dstStageMask); for(auto const& barrier : bufferBarriers) check(barrier.srcStageMask, barrier.dstStageMask); @@ -137,11 +123,11 @@ void Barrier::debug_check_rendering( CTH_CRITICAL(!framebufferSpace, "barriers inside a rendering must only use framebuffer space stages") {} CTH_CRITICAL( - srcStages && (_flags & VK_DEPENDENCY_BY_REGION_BIT) == 0, + srcStages && (flags & VK_DEPENDENCY_BY_REGION_BIT) == 0, "barriers inside a rendering need VK_DEPENDENCY_BY_REGION_BIT" ) {} CTH_CRITICAL( - std::popcount(rendering.inheritance.viewMask) > 1 && (_flags & VK_DEPENDENCY_VIEW_LOCAL_BIT) == 0, + std::popcount(rendering.inheritance.viewMask) > 1 && (flags & VK_DEPENDENCY_VIEW_LOCAL_BIT) == 0, "barriers inside a rendering with more than one view need VK_DEPENDENCY_VIEW_LOCAL_BIT" ) {} auto const transfer = [](VkBufferMemoryBarrier2 const& barrier) { @@ -152,7 +138,7 @@ void Barrier::debug_check_rendering( "barriers inside a rendering have no queue family ownership transfer" ) {} - for(auto const [barrier, entry] : std::views::zip(imageBarriers, _images)) { + for(auto const [barrier, entry] : std::views::zip(imageBarriers, images)) { CTH_CRITICAL(barrier.oldLayout != barrier.newLayout, "barriers inside a rendering have no layout transition") {} CTH_CRITICAL( barrier.srcQueueFamilyIndex != barrier.dstQueueFamilyIndex, @@ -177,24 +163,30 @@ void Barrier::execute(BaseCmdBuffer const& cmdBuffer) const { BaseCmdBuffer::debug_check(cmdBuffer); CTH_CRITICAL(!cmdBuffer.recording(), "command buffer must be recording") {} + std::vector memoryBarriers{}; + memoryBarriers.reserve(memory.size()); + for(auto const& info : memory) + memoryBarriers.push_back(memory_barrier(info)); + std::vector bufferBarriers{}; - bufferBarriers.reserve(_buffers.size()); - for(auto const& [buffer, info] : _buffers) + bufferBarriers.reserve(buffers.size()); + for(auto const& [buffer, info] : buffers) bufferBarriers.push_back(buffer_barrier(*buffer, info)); std::vector imageBarriers{}; - imageBarriers.reserve(_images.size()); - for(auto const& [image, info] : _images) + imageBarriers.reserve(images.size()); + for(auto const& [image, info] : images) imageBarriers.push_back(image_barrier(*image, info)); - if(auto const* rendering = cmdBuffer.rendering()) debug_check_rendering(*rendering, bufferBarriers, imageBarriers); + if(auto const* rendering = cmdBuffer.rendering()) + debug_check_rendering(*rendering, memoryBarriers, bufferBarriers, imageBarriers); VkDependencyInfo const dependency{ .sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO, .pNext = nullptr, - .dependencyFlags = _flags, - .memoryBarrierCount = static_cast(_memoryBarriers.size()), - .pMemoryBarriers = _memoryBarriers.data(), + .dependencyFlags = flags, + .memoryBarrierCount = static_cast(memoryBarriers.size()), + .pMemoryBarriers = memoryBarriers.data(), .bufferMemoryBarrierCount = static_cast(bufferBarriers.size()), .pBufferMemoryBarriers = bufferBarriers.data(), .imageMemoryBarrierCount = static_cast(imageBarriers.size()), diff --git a/cvk/tests/src/base/queue/submit_test.cpp b/cvk/tests/src/base/queue/submit_test.cpp new file mode 100644 index 00000000..89bac9d5 --- /dev/null +++ b/cvk/tests/src/base/queue/submit_test.cpp @@ -0,0 +1,97 @@ +#include "context.hpp" +#include "test.hpp" + +#include "cvk/base/queue/queue.hpp" +#include "cvk/render/cmd/cmd_buffer.hpp" +#include "cvk/render/cmd/cmd_pool.hpp" +#include "cvk/render/sync/fence.hpp" +#include "cvk/render/sync/semaphore.hpp" +#include "cvk/render/sync/timeline_semaphore.hpp" + +#include +#include + + +namespace cvk { +CVK_TEST(Submit, semaphore_info_takes_the_kind_from_the_semaphore) { + test::Context const context{}; + Semaphore const binary{context.core, create}; + TimelineSemaphore const timeline{context.core, create}; + + SemaphoreSubmitInfo const binaryInfo{binary}; + SemaphoreSubmitInfo const timelineInfo{timeline, VK_PIPELINE_STAGE_2_TRANSFER_BIT, 3}; + + EXPECT_TRUE(std::holds_alternative>(binaryInfo.semaphore)); + EXPECT_EQ(binaryInfo.stages, VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT); + EXPECT_FALSE(binaryInfo.value.has_value()); + + EXPECT_TRUE(std::holds_alternative>(timelineInfo.semaphore)); + EXPECT_EQ(timelineInfo.stages, VK_PIPELINE_STAGE_2_TRANSFER_BIT); + EXPECT_EQ(timelineInfo.value, 3); +} + +CVK_TEST(Submit, command_buffer_without_fence) { + test::Context const context{}; + CmdPool const pool{context.core, CmdPool::Config::Default(context.core.queue(0)), create}; + PrimaryCmdBuffer primary{pool, create}; + primary.begin(VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT); + primary.end(); + + context.core.queue(0).submit({.cmdBuffers = {&primary}}); + + context.core.queue(0).wait(); +} + +CVK_TEST(Submit, binary_semaphore_orders_two_infos) { + test::Context const context{}; + Semaphore const semaphore{context.core, create}; + Fence const fence{context.core, 0}; + + std::array const infos{ + SubmitInfo{.signals = {semaphore}}, + SubmitInfo{.waits = {semaphore}}, + }; + context.core.queue(0).submit(infos, &fence); + + fence.wait(); +} + +CVK_TEST(Submit, timeline_signal_takes_the_next_value) { + test::Context const context{}; + TimelineSemaphore const timeline{context.core, create}; + + context.core.queue(0).submit({.signals = {timeline}}); + + EXPECT_EQ(timeline.value(), 1); + timeline.wait(); + EXPECT_EQ(timeline.gpuValue(), 1); +} + +CVK_TEST(Submit, timeline_values_resolve_in_order) { + test::Context const context{}; + TimelineSemaphore const first{context.core, create}; + TimelineSemaphore const second{context.core, create}; + + // the wait of the second info resolves after the signal of the first: it waits for 1 + std::array const infos{ + SubmitInfo{.signals = {first}}, + SubmitInfo{.waits = {first}, .signals = {second}}, + }; + context.core.queue(0).submit(infos); + + second.wait(); + EXPECT_EQ(first.gpuValue(), 1); + EXPECT_EQ(second.gpuValue(), 1); +} + +CVK_TEST(Submit, explicit_timeline_value_keeps_the_counter) { + test::Context const context{}; + TimelineSemaphore const timeline{context.core, create}; + + context.core.queue(0).submit({.signals = {{timeline, VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT, 5}}}); + + context.core.queue(0).wait(); + EXPECT_EQ(timeline.gpuValue(), 5); + EXPECT_EQ(timeline.value(), 0); +} +} diff --git a/cvk/tests/src/context.cpp b/cvk/tests/src/context.cpp index b3e98fad..81b3ed2b 100644 --- a/cvk/tests/src/context.cpp +++ b/cvk/tests/src/context.cpp @@ -21,25 +21,7 @@ Context::ValidationCheck::~ValidationCheck() { EXPECT_EQ(validation_errors(), 0) void submit_and_wait(Context const& context, PrimaryCmdBuffer const& primary) { Fence const fence{context.core, 0}; - VkCommandBufferSubmitInfo const bufferInfo{ - .sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_SUBMIT_INFO, - .pNext = nullptr, - .commandBuffer = primary.get(), - .deviceMask = 0, - }; - VkSubmitInfo2 const submitInfo{ - .sType = VK_STRUCTURE_TYPE_SUBMIT_INFO_2, - .pNext = nullptr, - .flags = 0, - .waitSemaphoreInfoCount = 0, - .pWaitSemaphoreInfos = nullptr, - .commandBufferInfoCount = 1, - .pCommandBufferInfos = &bufferInfo, - .signalSemaphoreInfoCount = 0, - .pSignalSemaphoreInfos = nullptr, - }; - - context.core.queue(0).raw_submit(submitInfo, fence.get()); + context.core.queue(0).submit({.cmdBuffers = {&primary}}, &fence); fence.wait(); } diff --git a/cvk/tests/src/render/pipeline/pipeline_test.cpp b/cvk/tests/src/render/pipeline/pipeline_test.cpp index f05f0208..7035dad1 100644 --- a/cvk/tests/src/render/pipeline/pipeline_test.cpp +++ b/cvk/tests/src/render/pipeline/pipeline_test.cpp @@ -120,7 +120,7 @@ CVK_TEST(Pipeline, base_constructor_does_not_create) { Pipeline const pipeline{context.core, triangle.layout, triangle.config()}; EXPECT_FALSE(pipeline.created()); - EXPECT_EQ(pipeline.layout(), triangle.layout.get()); + EXPECT_EQ(&pipeline.layout(), &triangle.layout); } CVK_TEST(Pipeline, create) { @@ -196,7 +196,7 @@ CVK_TEST(Pipeline, derivative) { ASSERT_TRUE(child.created()); EXPECT_EQ(child.parent(), parent.get()); - EXPECT_EQ(child.layout(), triangle.layout.get()); + EXPECT_EQ(&child.layout(), &triangle.layout); EXPECT_EQ(parent.parent(), VK_NULL_HANDLE); } } diff --git a/cvk/tests/src/render/sync/barrier_test.cpp b/cvk/tests/src/render/sync/barrier_test.cpp index dd4661f3..e4908e40 100644 --- a/cvk/tests/src/render/sync/barrier_test.cpp +++ b/cvk/tests/src/render/sync/barrier_test.cpp @@ -66,6 +66,17 @@ CVK_TEST(Barrier, memory_barriers_are_constexpr) { }()); } +CVK_TEST(Barrier, aggregate_is_constexpr) { + static_assert([] { + // not const: gcc rejects the destructor of a const vector in a constant expression + Barrier barrier{ + .flags = VK_DEPENDENCY_BY_REGION_BIT, + .memory = {{TRANSFER_WRITE, FRAGMENT_READ}}, + }; + return !barrier.empty() && barrier.memory.size() == 1 && barrier.buffers.empty(); + }()); +} + CVK_TEST(Barrier, add_and_clear) { test::Context const context{}; BaseBuffer const buffer{context.core, {.usage = VK_BUFFER_USAGE_TRANSFER_DST_BIT}, 64}; @@ -89,23 +100,26 @@ CVK_TEST(Barrier, execute_memory_and_buffer_barriers) { test::record_and_submit(context, [&](PrimaryCmdBuffer const& primary) { barrier.execute(primary); }); - EXPECT_EQ(barrier.flags(), VK_DEPENDENCY_BY_REGION_BIT); + EXPECT_EQ(barrier.flags, VK_DEPENDENCY_BY_REGION_BIT); } CVK_TEST(Barrier, image_layout_transition) { test::Context const context{}; Image const image{context.core, IMAGE_CONFIG, EXTENT}; - Barrier barrier{}; - barrier.add( - image, - ImageBarrierInfo::LayoutTransition( - VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, - VK_IMAGE_LAYOUT_UNDEFINED, - {}, - TRANSFER_WRITE - ) - ); + Barrier const barrier{ + .images = { + { + &image, + ImageBarrierInfo::LayoutTransition( + VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, + VK_IMAGE_LAYOUT_UNDEFINED, + {}, + TRANSFER_WRITE + ), + }, + }, + }; test::record_and_submit(context, [&](PrimaryCmdBuffer const& primary) { barrier.execute(primary); }); } From c8b83ec9cf582031fb6bdad3d1052368edbbc0b6 Mon Sep 17 00:00:00 2001 From: lukas <89145039+codethinki@users.noreply.github.com> Date: Sun, 18 Oct 2026 00:00:37 +0200 Subject: [PATCH 056/135] cvk: require descriptor indexing, mark the gcc const aggregate workaround --- cvk/incl/cvk/utility/constant/device_constants.hpp | 5 ++++- cvk/tests/src/render/sync/barrier_test.cpp | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/cvk/incl/cvk/utility/constant/device_constants.hpp b/cvk/incl/cvk/utility/constant/device_constants.hpp index 539c3878..78d2634c 100644 --- a/cvk/incl/cvk/utility/constant/device_constants.hpp +++ b/cvk/incl/cvk/utility/constant/device_constants.hpp @@ -34,13 +34,16 @@ inline constexpr std::array PRESENT_INSTANCE_EXTENSIONS{ /** * @brief features that every device enables + * @details descriptorIndexing: the device supports the minimum set of descriptor indexing features, for bindless */ [[nodiscard]] inline utils::PhysicalDeviceFeatures required_device_features() { utils::PhysicalDeviceFeatures features{}; features.features().features.samplerAnisotropy = VK_TRUE; - features.features().timelineSemaphore = VK_TRUE; + auto& features12 = features.features(); + features12.timelineSemaphore = VK_TRUE; + features12.descriptorIndexing = VK_TRUE; auto& features13 = features.features(); features13.synchronization2 = VK_TRUE; diff --git a/cvk/tests/src/render/sync/barrier_test.cpp b/cvk/tests/src/render/sync/barrier_test.cpp index e4908e40..21851727 100644 --- a/cvk/tests/src/render/sync/barrier_test.cpp +++ b/cvk/tests/src/render/sync/barrier_test.cpp @@ -68,7 +68,7 @@ CVK_TEST(Barrier, memory_barriers_are_constexpr) { CVK_TEST(Barrier, aggregate_is_constexpr) { static_assert([] { - // not const: gcc rejects the destructor of a const vector in a constant expression + //TEMP not const: gcc 16 rejects a const aggregate with a vector and another member in a constant expression Barrier barrier{ .flags = VK_DEPENDENCY_BY_REGION_BIT, .memory = {{TRANSFER_WRITE, FRAGMENT_READ}}, From 1df765b431ffde34bb6d93f6e2bff1f6fbfc66b9 Mon Sep 17 00:00:00 2001 From: lukas <89145039+codethinki@users.noreply.github.com> Date: Sun, 18 Oct 2026 00:09:58 +0200 Subject: [PATCH 057/135] cvk: aspect mask from the format, attachment format checks - format_aspects(): depth, stencil, both or color, constexpr - ImageConfig::aspectMask 0 -> the aspects of the format - beginRendering checks the formats of the color, depth and stencil attachments --- cvk/incl/cvk/data/img/image.hpp | 8 ++++++- cvk/incl/cvk/data/img/image_config.hpp | 10 ++++++-- cvk/incl/cvk/data/img/image_format.hpp | 22 ++++++++++++++++++ cvk/incl/cvk/render/cmd/cmd_buffer.hpp | 1 + cvk/src/render/cmd/cmd_buffer.cpp | 28 +++++++++++++++++++--- cvk/tests/src/data/img/image_test.cpp | 32 ++++++++++++++++++++++++++ 6 files changed, 95 insertions(+), 6 deletions(-) create mode 100644 cvk/incl/cvk/data/img/image_format.hpp diff --git a/cvk/incl/cvk/data/img/image.hpp b/cvk/incl/cvk/data/img/image.hpp index 202730f0..6753d940 100644 --- a/cvk/incl/cvk/data/img/image.hpp +++ b/cvk/incl/cvk/data/img/image.hpp @@ -1,5 +1,6 @@ #pragma once #include "image_config.hpp" +#include "image_format.hpp" #include "cvk/base/device_table.hpp" #include "cvk/data/memory/memory.hpp" @@ -130,7 +131,12 @@ class Image { [[nodiscard]] constexpr VkExtent2D extent() const { return _extent; } [[nodiscard]] constexpr VkFormat format() const { return _config.format; } - [[nodiscard]] constexpr VkImageAspectFlags aspectMask() const { return _config.aspectMask; } + /** + * @return @ref Config::aspectMask, 0 -> the aspects of the format + */ + [[nodiscard]] constexpr VkImageAspectFlags aspectMask() const { + return _config.aspectMask != 0 ? _config.aspectMask : format_aspects(_config.format); + } [[nodiscard]] constexpr uint32_t mipLevels() const { return _config.mipLevels; } /** diff --git a/cvk/incl/cvk/data/img/image_config.hpp b/cvk/incl/cvk/data/img/image_config.hpp index 042e39f2..889ca11f 100644 --- a/cvk/incl/cvk/data/img/image_config.hpp +++ b/cvk/incl/cvk/data/img/image_config.hpp @@ -1,4 +1,6 @@ #pragma once +#include "image_format.hpp" + #include "cvk/base/queue/queue_family.hpp" #include "cvk/data/memory/memory_config.hpp" @@ -16,7 +18,8 @@ namespace cvk { */ struct ImageConfig { /** - * @pre != 0 + * @brief 0 -> the aspects of @ref format, see @ref format_aspects() + * @pre the aspects of @ref format contain it */ VkImageAspectFlags aspectMask = 0; @@ -102,8 +105,11 @@ struct ImageConfig { namespace cvk { constexpr void ImageConfig::debug_check(ImageConfig const& config) { - CTH_CRITICAL(config.aspectMask == 0, "aspect mask must not be 0") {} CTH_CRITICAL(config.format == VK_FORMAT_UNDEFINED, "format must not be undefined") {} + CTH_CRITICAL( + (config.aspectMask & ~format_aspects(config.format)) != 0, + "the aspects of the format must contain the aspect mask" + ) {} CTH_CRITICAL(config.usage == 0, "usage must not be 0") {} CTH_CRITICAL(config.mipLevels == 0, "at least one mip level is required") {} CTH_CRITICAL( diff --git a/cvk/incl/cvk/data/img/image_format.hpp b/cvk/incl/cvk/data/img/image_format.hpp new file mode 100644 index 00000000..58e669ac --- /dev/null +++ b/cvk/incl/cvk/data/img/image_format.hpp @@ -0,0 +1,22 @@ +#pragma once +#include + + +namespace cvk { +/** + * @return the aspects of the format: depth, stencil, depth and stencil, else color + * @details multi planar formats -> color, cvk does not support their plane aspects + */ +[[nodiscard]] constexpr VkImageAspectFlags format_aspects(VkFormat format) { + switch(format) { + case VK_FORMAT_D16_UNORM: + case VK_FORMAT_X8_D24_UNORM_PACK32: + case VK_FORMAT_D32_SFLOAT: return VK_IMAGE_ASPECT_DEPTH_BIT; + case VK_FORMAT_S8_UINT: return VK_IMAGE_ASPECT_STENCIL_BIT; + case VK_FORMAT_D16_UNORM_S8_UINT: + case VK_FORMAT_D24_UNORM_S8_UINT: + case VK_FORMAT_D32_SFLOAT_S8_UINT: return VK_IMAGE_ASPECT_DEPTH_BIT | VK_IMAGE_ASPECT_STENCIL_BIT; + default: return VK_IMAGE_ASPECT_COLOR_BIT; + } +} +} diff --git a/cvk/incl/cvk/render/cmd/cmd_buffer.hpp b/cvk/incl/cvk/render/cmd/cmd_buffer.hpp index 0c0a2479..92561590 100644 --- a/cvk/incl/cvk/render/cmd/cmd_buffer.hpp +++ b/cvk/incl/cvk/render/cmd/cmd_buffer.hpp @@ -121,6 +121,7 @@ class BaseCmdBuffer { * @pre the attachment views are created and have one mip level * @pre the attachment layouts are the layouts of the views * @pre the attachment layouts are attachment layouts (not undefined, transfer, shader read or present) + * @pre color attachments have color formats, the depth and stencil attachments depth and stencil formats * @post @ref rendering() */ void beginRendering(RenderingInfo const&); diff --git a/cvk/src/render/cmd/cmd_buffer.cpp b/cvk/src/render/cmd/cmd_buffer.cpp index f56acb7d..33127e7c 100644 --- a/cvk/src/render/cmd/cmd_buffer.cpp +++ b/cvk/src/render/cmd/cmd_buffer.cpp @@ -2,6 +2,7 @@ #include "cvk/base/core.hpp" #include "cvk/data/img/image.hpp" +#include "cvk/data/img/image_format.hpp" #include "cvk/data/img/image_view.hpp" #include "cvk/utility/vk_exceptions.hpp" @@ -203,19 +204,40 @@ void BaseCmdBuffer::beginRendering(RenderingInfo const& info) { colors.reserve(info.colorAttachments.size()); for(auto const& attachment : info.colorAttachments) { colors.push_back(attachmentInfo(attachment)); - rendering.inheritance.colorFormats.push_back(attachment.view->image().format()); + + auto const format = attachment.view->image().format(); + CTH_CRITICAL(format_aspects(format) != VK_IMAGE_ASPECT_COLOR_BIT, "color attachments need a color format") { + details->add("format: {}", format); + } + rendering.inheritance.colorFormats.push_back(format); } std::optional depth{}; if(info.depthAttachment.has_value()) { depth = attachmentInfo(*info.depthAttachment); - rendering.inheritance.depthFormat = info.depthAttachment->view->image().format(); + + auto const format = info.depthAttachment->view->image().format(); + CTH_CRITICAL( + (format_aspects(format) & VK_IMAGE_ASPECT_DEPTH_BIT) == 0, + "the depth attachment needs a depth format" + ) { + details->add("format: {}", format); + } + rendering.inheritance.depthFormat = format; } std::optional stencil{}; if(info.stencilAttachment.has_value()) { stencil = attachmentInfo(*info.stencilAttachment); - rendering.inheritance.stencilFormat = info.stencilAttachment->view->image().format(); + + auto const format = info.stencilAttachment->view->image().format(); + CTH_CRITICAL( + (format_aspects(format) & VK_IMAGE_ASPECT_STENCIL_BIT) == 0, + "the stencil attachment needs a stencil format" + ) { + details->add("format: {}", format); + } + rendering.inheritance.stencilFormat = format; } CTH_CRITICAL(!extent.has_value() && !info.area.has_value(), "a rendering without attachments needs an area") {} diff --git a/cvk/tests/src/data/img/image_test.cpp b/cvk/tests/src/data/img/image_test.cpp index cdc169ac..ac08c9e2 100644 --- a/cvk/tests/src/data/img/image_test.cpp +++ b/cvk/tests/src/data/img/image_test.cpp @@ -52,6 +52,38 @@ CVK_TEST(Image, eval_mip_level_count) { static_assert(Image::evalMipLevelCount({1, 1024}) == 11); } +CVK_TEST(Image, format_aspects) { + static_assert(format_aspects(VK_FORMAT_R8G8B8A8_UNORM) == VK_IMAGE_ASPECT_COLOR_BIT); + static_assert(format_aspects(VK_FORMAT_D32_SFLOAT) == VK_IMAGE_ASPECT_DEPTH_BIT); + static_assert(format_aspects(VK_FORMAT_S8_UINT) == VK_IMAGE_ASPECT_STENCIL_BIT); + static_assert( + format_aspects(VK_FORMAT_D24_UNORM_S8_UINT) == (VK_IMAGE_ASPECT_DEPTH_BIT | VK_IMAGE_ASPECT_STENCIL_BIT) + ); +} + +CVK_TEST(Image, config_aspect_mask_fits_the_format) { + static_assert([] { + Image::Config const config{.format = VK_FORMAT_D24_UNORM_S8_UINT, .usage = VK_IMAGE_USAGE_SAMPLED_BIT}; + Image::Config::debug_check(config); + return true; + }()); + constexpr auto depthBuffer = Image::Config::DepthBuffer(VK_FORMAT_D24_UNORM_S8_UINT, VK_SAMPLE_COUNT_1_BIT); + static_assert((Image::Config::debug_check(depthBuffer), true)); +} + +CVK_TEST(Image, aspect_mask_comes_from_the_format) { + test::Context const context{}; + + Image const color{context.core, {.format = VK_FORMAT_R8G8B8A8_UNORM, .usage = VK_IMAGE_USAGE_SAMPLED_BIT}}; + Image const depth{ + context.core, + {.format = VK_FORMAT_D16_UNORM, .usage = VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT} + }; + + EXPECT_EQ(color.aspectMask(), VK_IMAGE_ASPECT_COLOR_BIT); + EXPECT_EQ(depth.aspectMask(), VK_IMAGE_ASPECT_DEPTH_BIT); +} + CVK_TEST(Image, config_create_info) { constexpr auto info = TEXTURE_CONFIG.createInfo(EXTENT); From d620a94689bde786ce521ba5f37b90c981f79474 Mon Sep 17 00:00:00 2001 From: lukas <89145039+codethinki@users.noreply.github.com> Date: Sun, 18 Oct 2026 12:01:15 +0200 Subject: [PATCH 058/135] cvk: swapchain with maintenance1 present fences, fix undefined layout transition scopes - Swapchain owns its images, views, a present semaphore and a present fence per image - acquire() returns the result and an optional image index, present() uses the present fence - resize() retires the old swapchain, destroyed once its present fences signal - unacquire() gives acquired images back (vkReleaseSwapchainImagesKHR) - custom move assignment: the default would destroy the swapchain images - fix: transitions from VK_IMAGE_LAYOUT_UNDEFINED wait for the dst stages, the empty src scope did not chain with an acquire semaphore wait (legacy had the same with TOP_OF_PIPE) - tests: PresentContext with a headless surface --- cvk/incl/cvk/render/sync/barrier.hpp | 12 +- cvk/incl/cvk/surface/swapchain.hpp | 312 +++++++++++++++++++ cvk/incl/cvk/surface/swapchain_config.hpp | 82 +++++ cvk/src/surface/swapchain.cpp | 341 +++++++++++++++++++++ cvk/tests/src/context.cpp | 7 + cvk/tests/src/context.hpp | 28 ++ cvk/tests/src/render/sync/barrier_test.cpp | 1 + cvk/tests/src/surface/swapchain_test.cpp | 227 ++++++++++++++ 8 files changed, 1006 insertions(+), 4 deletions(-) create mode 100644 cvk/incl/cvk/surface/swapchain.hpp create mode 100644 cvk/incl/cvk/surface/swapchain_config.hpp create mode 100644 cvk/src/surface/swapchain.cpp create mode 100644 cvk/tests/src/surface/swapchain_test.cpp diff --git a/cvk/incl/cvk/render/sync/barrier.hpp b/cvk/incl/cvk/render/sync/barrier.hpp index 12b99cbe..1a537607 100644 --- a/cvk/incl/cvk/render/sync/barrier.hpp +++ b/cvk/incl/cvk/render/sync/barrier.hpp @@ -292,11 +292,15 @@ constexpr std::optional layout_transition(VkImageLayout oldLay VK_ACCESS_2_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_2_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT }; + // from VK_IMAGE_LAYOUT_UNDEFINED: src waits for the dst stages without access, + // orders the transition after their last use and after an acquire semaphore wait on these stages + constexpr auto stagesOf = [](BarrierScope const& scope) { return BarrierScope{scope.stages}; }; + constexpr std::array entries{ Entry{ VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, - {none, transferWrite}, + {stagesOf(transferWrite), transferWrite}, }, Entry{ VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, @@ -317,12 +321,12 @@ constexpr std::optional layout_transition(VkImageLayout oldLay Entry{ VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, - {none, color}, + {stagesOf(color), color}, }, Entry{ VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_OPTIMAL, - {none, depth}, + {stagesOf(depth), depth}, }, Entry{ VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, @@ -338,7 +342,7 @@ constexpr std::optional layout_transition(VkImageLayout oldLay Entry{ VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_RENDERING_LOCAL_READ_KHR, - {none, localRead}, + {stagesOf(localRead), localRead}, }, Entry{ VK_IMAGE_LAYOUT_RENDERING_LOCAL_READ_KHR, diff --git a/cvk/incl/cvk/surface/swapchain.hpp b/cvk/incl/cvk/surface/swapchain.hpp new file mode 100644 index 00000000..59d32fb8 --- /dev/null +++ b/cvk/incl/cvk/surface/swapchain.hpp @@ -0,0 +1,312 @@ +#pragma once +#include "swapchain_config.hpp" + +#include "cvk/base/device_table.hpp" +#include "cvk/data/img/image.hpp" +#include "cvk/data/img/image_view.hpp" +#include "cvk/render/sync/fence.hpp" +#include "cvk/render/sync/semaphore.hpp" +#include "cvk/utility/core_handle.hpp" +#include "cvk/utility/types.hpp" + +#include + +#include + +#include +#include +#include +#include +#include + + +namespace cvk { +class Core; +class Queue; +class Surface; + +/** + * @brief VkSwapchainKHR wrapper, owns its images, their views, a present semaphore and a present fence per image + * @details the images are wrapped without memory and released before the swapchain is destroyed, + * @ref resize() retires the old swapchain, the swapchain destroys it once its present fences signal, + * custom move assignment: destroys first, a default one would destroy the swapchain images + * @attention not thread safe + */ +class Swapchain { +public: + using vk_handle_t = VkSwapchainKHR; + using Config = SwapchainConfig; + struct State; + + /** + * @brief result of @ref acquire() + */ + struct Acquired { + /** + * @brief VK_SUCCESS, VK_SUBOPTIMAL_KHR, VK_ERROR_OUT_OF_DATE_KHR, VK_TIMEOUT or VK_NOT_READY + */ + VkResult result; + + /** + * @brief std::nullopt -> no image acquired, the semaphore stays unsignaled + */ + std::optional imageIndex; + }; + + /** + * @brief base constructor + * @pre @ref Core::created(), @ref Surface::created() + * @pre the queue can present to the surface + */ + Swapchain(Core const&, Queue const& presentQueue, Surface const&, Config); + + /** + * @brief constructs and wraps + * @details calls: + * - @ref Swapchain(Core const&, Queue const&, Surface const&, Config) + * - @ref wrap() + */ + Swapchain(Core const&, Queue const& presentQueue, Surface const&, Config, State const&); + + /** + * @brief constructs and creates + * @details calls: + * - @ref Swapchain(Core const&, Queue const&, Surface const&, Config) + * - @ref create() + */ + Swapchain(Core const&, Queue const& presentQueue, Surface const&, Config, VkExtent2D extent); + + /** + * @note calls @ref optDestroy() + * @note waits for the retired swapchains and destroys them + */ + ~Swapchain() { + optDestroy(); + destroyRetired(); + } + + /** + * @brief wraps the state, gets the images, creates their views, present semaphores and present fences + * @pre the swapchain was created for the surface with the config and the state + * @post @ref created() + * @note calls @ref optDestroy() + * @throws cvk::vk_result_exception result of vkGetSwapchainImagesKHR() + */ + void wrap(State const&); + + /** + * @brief creates the swapchain with the format and present mode of the surface + * @param extent of the window, a fixed currentExtent of the surface wins + * @pre extent width and height > 0 + * @post @ref created() + * @note calls @ref optDestroy() + * @throws cvk::vk_result_exception result of vkCreateSwapchainKHR() + */ + void create(VkExtent2D extent); + + /** + * @brief creates a new swapchain with the old one as oldSwapchain, the old one retires + * @param extent of the window, a fixed currentExtent of the surface wins + * @details unacquires the images that are still acquired, + * the swapchain destroys the retired one once its present fences signal + * @pre @ref created() + * @pre extent width and height > 0 + * @pre no present semaphore is signaled without its present + * @throws cvk::vk_result_exception result of vkCreateSwapchainKHR() + */ + void resize(VkExtent2D extent); + + /** + * @brief waits for the present fences, destroys the swapchain and forgets + * @details releases the images, destroys the views, present semaphores, present fences and retired swapchains + * @pre @ref created() + * @pre no present semaphore is signaled without its present + */ + void destroy(); + + /** + * @brief if @ref created() -> calls @ref destroy() + */ + void optDestroy() { if(created()) destroy(); } + + /** + * @brief waits for the present fences, releases ownership, returns the state and forgets + * @details releases the images, destroys the views, present semaphores, present fences and retired swapchains + * @pre @ref created() + * @pre no present semaphore is signaled without its present + */ + State release(); + + /** + * @brief acquires the next image + * @param signal signaled when the image is ready + * @param timeout in nanoseconds + * @details destroys the retired swapchains with signaled present fences + * @pre @ref created() + * @pre signal is unsignaled and has no pending operation + * @throws cvk::vk_result_exception other result of vkAcquireNextImageKHR() + */ + [[nodiscard]] Acquired acquire(Semaphore const& signal, uint64_t timeout = std::numeric_limits::max()); + + /** + * @brief presents the image once its present semaphore signals + * @return VK_SUCCESS, VK_SUBOPTIMAL_KHR or VK_ERROR_OUT_OF_DATE_KHR + * @details VK_ERROR_OUT_OF_DATE_KHR -> the present still waits for the semaphore, + * destroys the retired swapchains with signaled present fences + * @pre the image is acquired + * @pre a submit signals @ref presentSemaphore() of the image + * @throws cvk::vk_result_exception other result of vkQueuePresentKHR() + */ + [[nodiscard]] VkResult present(uint32_t imageIndex); + + /** + * @brief gives acquired images back without a present + * @pre the images are acquired and all uses of them completed + * @attention the acquire semaphores of the images still signal, wait for them before reuse + * @throws cvk::vk_result_exception result of vkReleaseSwapchainImagesKHR() + */ + void unacquire(std::span imageIndices); + + static void destroy(DeviceTable table, VkSwapchainKHR); + + /** + * @return the extent clamped to the limits of the surface, a fixed currentExtent wins + */ + [[nodiscard]] static constexpr VkExtent2D evalExtent(VkExtent2D extent, VkSurfaceCapabilitiesKHR const&); + + /** + * @param minImageCount 0 -> minImageCount of the surface + 1 + * @return the count clamped to the limits of the surface (maxImageCount 0 => no limit) + */ + [[nodiscard]] static constexpr uint32_t evalImageCount(uint32_t minImageCount, VkSurfaceCapabilitiesKHR const&); + + [[nodiscard]] constexpr bool created() const { return static_cast(_handle); } + [[nodiscard]] constexpr vk_handle_t get() const { return _handle.get(); } + [[nodiscard]] constexpr Core const& core() const { return _handle.core(); } + [[nodiscard]] constexpr Config const& config() const { return _config; } + + [[nodiscard]] constexpr VkExtent2D extent() const { return _extent; } + [[nodiscard]] constexpr VkFormat format() const { return _surfaceFormat.format; } + [[nodiscard]] constexpr VkColorSpaceKHR colorSpace() const { return _surfaceFormat.colorSpace; } + [[nodiscard]] constexpr VkPresentModeKHR presentMode() const { return _presentMode; } + [[nodiscard]] constexpr uint32_t imageCount() const { return static_cast(_images.size()); } + + /** + * @return width / height + * @pre @ref created() + */ + [[nodiscard]] constexpr float aspectRatio() const { + return static_cast(_extent.width) / static_cast(_extent.height); + } + + [[nodiscard]] constexpr std::span images() const { return _images; } + [[nodiscard]] constexpr std::span views() const { return _views; } + + /** + * @return the semaphore that @ref present() of the image waits for, a submit signals it + * @pre imageIndex < @ref imageCount() + */ + [[nodiscard]] constexpr Semaphore const& presentSemaphore(uint32_t imageIndex) const { + return _presentSemaphores[imageIndex]; + } + + /** + * @pre imageIndex < @ref imageCount() + */ + [[nodiscard]] constexpr bool acquired(uint32_t imageIndex) const { return _acquired[imageIndex]; } + + /** + * @return count of the retired swapchains that wait for their present fences + */ + [[nodiscard]] constexpr size_t retired() const { return _retired.size(); } + +private: + /** + * @brief old swapchain after @ref resize(), the images are released + * @details members destroy in reverse order: the swapchain last + */ + struct Retired { + core_handle handle; + std::vector images; + std::vector views; + std::vector presentSemaphores; + std::vector presentFences; + }; + + void createSwapchain(VkExtent2D extent, VkSwapchainKHR oldSwapchain); + void createImages(); + + /** + * @brief waits for the present fences, releases the images, destroys the views, semaphores and fences + */ + void destroyImages(); + + /** + * @brief destroys the retired swapchains with signaled present fences + */ + void collectRetired(); + + /** + * @brief waits for the present fences of the retired swapchains and destroys them + */ + void destroyRetired(); + + void forget(); + + not_null _presentQueue; + not_null _surface; + Config _config; + + core_handle _handle; + std::vector _images; + std::vector _views; + std::vector _presentSemaphores; + std::vector _presentFences; + std::vector _acquired; + std::vector _retired; + + VkExtent2D _extent{}; + VkSurfaceFormatKHR _surfaceFormat{}; + VkPresentModeKHR _presentMode = VK_PRESENT_MODE_FIFO_KHR; + +public: + Swapchain(Swapchain const& other) = delete; + Swapchain(Swapchain&& other) noexcept = default; + Swapchain& operator=(Swapchain const& other) = delete; + Swapchain& operator=(Swapchain&& other) noexcept; + + static constexpr void debug_check(Swapchain const&); +}; +} + +namespace cvk { +struct Swapchain::State { + vk_not_null vkSwapchain; + VkExtent2D extent; + VkSurfaceFormatKHR surfaceFormat; + VkPresentModeKHR presentMode; +}; +} + +namespace cvk { +constexpr VkExtent2D Swapchain::evalExtent(VkExtent2D extent, VkSurfaceCapabilitiesKHR const& capabilities) { + if(capabilities.currentExtent.width != std::numeric_limits::max()) return capabilities.currentExtent; + + return VkExtent2D{ + std::clamp(extent.width, capabilities.minImageExtent.width, capabilities.maxImageExtent.width), + std::clamp(extent.height, capabilities.minImageExtent.height, capabilities.maxImageExtent.height), + }; +} + +constexpr uint32_t Swapchain::evalImageCount(uint32_t minImageCount, VkSurfaceCapabilitiesKHR const& capabilities) { + uint32_t const count = std::max( + minImageCount != 0 ? minImageCount : capabilities.minImageCount + 1, + capabilities.minImageCount + ); + return capabilities.maxImageCount != 0 ? std::min(count, capabilities.maxImageCount) : count; +} + +constexpr void Swapchain::debug_check(Swapchain const& swapchain) { + CTH_CRITICAL(!swapchain.created(), "swapchain must be created") {} +} +} diff --git a/cvk/incl/cvk/surface/swapchain_config.hpp b/cvk/incl/cvk/surface/swapchain_config.hpp new file mode 100644 index 00000000..231c7e26 --- /dev/null +++ b/cvk/incl/cvk/surface/swapchain_config.hpp @@ -0,0 +1,82 @@ +#pragma once +#include "cvk/base/queue/queue_family.hpp" + +#include + +#include + +#include +#include + + +namespace cvk { +/** + * @brief image count, usage and sharing of a swapchain, the surface decides format and present mode + */ +struct SwapchainConfig { + /** + * @brief 0 -> minImageCount of the surface + 1, clamped to the limits of the surface + */ + uint32_t minImageCount = 0; + + /** + * @pre != 0 + */ + VkImageUsageFlags usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT; + + /** + * @brief queue families that use the images at the same time + * @details size < 2 -> VK_SHARING_MODE_EXCLUSIVE, else VK_SHARING_MODE_CONCURRENT + * @pre unique + */ + std::vector queueFamilies{}; + + VkCompositeAlphaFlagBitsKHR compositeAlpha = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR; + + /** + * @attention pQueueFamilyIndices points into @ref queueFamilies + */ + [[nodiscard]] constexpr VkSwapchainCreateInfoKHR createInfo( + VkSurfaceKHR surface, + VkSurfaceFormatKHR format, + VkPresentModeKHR presentMode, + VkSurfaceTransformFlagBitsKHR transform, + VkExtent2D extent, + uint32_t imageCount, + VkSwapchainKHR oldSwapchain + ) const { + return VkSwapchainCreateInfoKHR{ + .sType = VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR, + .pNext = nullptr, + .flags = 0, + .surface = surface, + .minImageCount = imageCount, + .imageFormat = format.format, + .imageColorSpace = format.colorSpace, + .imageExtent = extent, + .imageArrayLayers = 1, + .imageUsage = usage, + .imageSharingMode = queueFamilies.size() >= 2 ? VK_SHARING_MODE_CONCURRENT : VK_SHARING_MODE_EXCLUSIVE, + .queueFamilyIndexCount = static_cast(queueFamilies.size()), + .pQueueFamilyIndices = queueFamilies.data(), + .preTransform = transform, + .compositeAlpha = compositeAlpha, + .presentMode = presentMode, + .clipped = VK_TRUE, + .oldSwapchain = oldSwapchain, + }; + } + + static constexpr void debug_check(SwapchainConfig const&); +}; +} + +namespace cvk { +constexpr void SwapchainConfig::debug_check(SwapchainConfig const& config) { + CTH_CRITICAL(config.usage == 0, "usage must not be 0") {} + + auto families = config.queueFamilies; + std::ranges::sort(families); + CTH_CRITICAL(std::ranges::adjacent_find(families) != families.end(), "queue families must be unique") {} +} +} diff --git a/cvk/src/surface/swapchain.cpp b/cvk/src/surface/swapchain.cpp new file mode 100644 index 00000000..a2e7efdc --- /dev/null +++ b/cvk/src/surface/swapchain.cpp @@ -0,0 +1,341 @@ +#include "cvk/surface/swapchain.hpp" + +#include "cvk/base/core.hpp" +#include "cvk/base/physical_device.hpp" +#include "cvk/base/queue/queue.hpp" +#include "cvk/surface/surface.hpp" +#include "cvk/utility/vk_exceptions.hpp" + +#include + + +namespace cvk { +using std::span; +using std::vector; + +Swapchain::Swapchain(Core const& core, Queue const& presentQueue, Surface const& surface, Config config) : + _presentQueue{&presentQueue}, _surface{&surface}, _config{std::move(config)}, _handle{core} { + Core::debug_check(core); + Surface::debug_check(surface); + Queue::debug_check_present(presentQueue); + Config::debug_check(_config); +} + +Swapchain::Swapchain( + Core const& core, + Queue const& presentQueue, + Surface const& surface, + Config config, + State const& state +) : Swapchain{core, presentQueue, surface, std::move(config)} { wrap(state); } + +Swapchain::Swapchain( + Core const& core, + Queue const& presentQueue, + Surface const& surface, + Config config, + VkExtent2D extent +) : Swapchain{core, presentQueue, surface, std::move(config)} { create(extent); } + +void Swapchain::wrap(State const& state) { + optDestroy(); + + _handle.reset(state.vkSwapchain.get(), core()); + _extent = state.extent; + _surfaceFormat = state.surfaceFormat; + _presentMode = state.presentMode; + + createImages(); +} + +void Swapchain::create(VkExtent2D extent) { + optDestroy(); + + createSwapchain(extent, VK_NULL_HANDLE); +} + +void Swapchain::resize(VkExtent2D extent) { + debug_check(*this); + + vector acquiredImages{}; + for(uint32_t index = 0; index < imageCount(); ++index) + if(_acquired[index]) acquiredImages.push_back(index); + unacquire(acquiredImages); + + // the swapchain destroys its images + for(auto& image : _images) + image.release(); + + auto const oldSwapchain = get(); + _retired.push_back( + Retired{ + .handle = std::move(_handle), + .images = std::move(_images), + .views = std::move(_views), + .presentSemaphores = std::move(_presentSemaphores), + .presentFences = std::move(_presentFences), + } + ); + _images.clear(); + _views.clear(); + _presentSemaphores.clear(); + _presentFences.clear(); + _acquired.clear(); + + createSwapchain(extent, oldSwapchain); +} + +void Swapchain::destroy() { + debug_check(*this); + + destroyImages(); + destroyRetired(); + _handle.reset(); + + forget(); +} + +Swapchain::State Swapchain::release() { + debug_check(*this); + + destroyImages(); + destroyRetired(); + + State const state{ + .vkSwapchain = _handle.release(), + .extent = _extent, + .surfaceFormat = _surfaceFormat, + .presentMode = _presentMode, + }; + forget(); + return state; +} + +Swapchain::Acquired Swapchain::acquire(Semaphore const& signal, uint64_t timeout) { + debug_check(*this); + Semaphore::debug_check(signal); + + collectRetired(); + + uint32_t imageIndex = 0; + auto const result = core().functions()->vkAcquireNextImageKHR( + core().vkDevice(), + get(), + timeout, + signal.get(), + VK_NULL_HANDLE, + &imageIndex + ); + + if(result == VK_SUCCESS || result == VK_SUBOPTIMAL_KHR) { + _acquired[imageIndex] = true; + return {result, imageIndex}; + } + + CVK_RESULT_STABLE_THROW( + result != VK_ERROR_OUT_OF_DATE_KHR && result != VK_TIMEOUT && result != VK_NOT_READY, + result, + "failed to acquire a swapchain image" + ) {} + + return {result, std::nullopt}; +} + +VkResult Swapchain::present(uint32_t imageIndex) { + debug_check(*this); + CTH_CRITICAL(imageIndex >= imageCount() || !_acquired[imageIndex], "the image must be acquired") { + details->add("image index: {}", imageIndex); + } + + collectRetired(); + + // signaled by the last present of the image or created signaled + auto const& fence = _presentFences[imageIndex]; + fence.waitReset(); + + VkFence const vkFence = fence.get(); + VkSwapchainPresentFenceInfoKHR const fenceInfo{ + .sType = VK_STRUCTURE_TYPE_SWAPCHAIN_PRESENT_FENCE_INFO_KHR, + .pNext = nullptr, + .swapchainCount = 1, + .pFences = &vkFence, + }; + + VkSemaphore const vkSemaphore = _presentSemaphores[imageIndex].get(); + VkSwapchainKHR const vkSwapchain = get(); + VkPresentInfoKHR const info{ + .sType = VK_STRUCTURE_TYPE_PRESENT_INFO_KHR, + .pNext = &fenceInfo, + .waitSemaphoreCount = 1, + .pWaitSemaphores = &vkSemaphore, + .swapchainCount = 1, + .pSwapchains = &vkSwapchain, + .pImageIndices = &imageIndex, + .pResults = nullptr, + }; + + auto const result = _presentQueue->raw_present(info); + _acquired[imageIndex] = false; + return result; +} + +void Swapchain::unacquire(span imageIndices) { + debug_check(*this); + for(auto const index : imageIndices) + CTH_CRITICAL(index >= imageCount() || !_acquired[index], "the images must be acquired") { + details->add("image index: {}", index); + } + + if(imageIndices.empty()) return; + + VkReleaseSwapchainImagesInfoKHR const info{ + .sType = VK_STRUCTURE_TYPE_RELEASE_SWAPCHAIN_IMAGES_INFO_KHR, + .pNext = nullptr, + .swapchain = get(), + .imageIndexCount = static_cast(imageIndices.size()), + .pImageIndices = imageIndices.data(), + }; + auto const result = core().functions()->vkReleaseSwapchainImagesKHR(core().vkDevice(), &info); + + CVK_RESULT_STABLE_THROW(result != VK_SUCCESS, result, "failed to unacquire swapchain images") {} + + for(auto const index : imageIndices) + _acquired[index] = false; +} + +void Swapchain::destroy(DeviceTable table, VkSwapchainKHR vkSwapchain) { + CTH_WARN(vkSwapchain == VK_NULL_HANDLE, "vkSwapchain invalid (VK_NULL_HANDLE)") {} + + table->vkDestroySwapchainKHR(table.device(), vkSwapchain, nullptr); +} + +void Swapchain::createSwapchain(VkExtent2D extent, VkSwapchainKHR oldSwapchain) { + Surface::debug_check(*_surface); + CTH_CRITICAL(extent.width == 0 || extent.height == 0, "extent width and height must be > 0") {} + + auto const& physicalDevice = core().physicalDevice(); + auto const capabilities = _surface->capabilities(physicalDevice); + auto const surfaceFormat = _surface->format(physicalDevice); + auto const presentMode = _surface->presentMode(physicalDevice); + auto const imageExtent = evalExtent(extent, capabilities); + + auto const info = _config.createInfo( + _surface->get(), + surfaceFormat, + presentMode, + capabilities.currentTransform, + imageExtent, + evalImageCount(_config.minImageCount, capabilities), + oldSwapchain + ); + + VkSwapchainKHR ptr = VK_NULL_HANDLE; + auto const result = core().functions()->vkCreateSwapchainKHR(core().vkDevice(), &info, nullptr, &ptr); + + CTH_STABLE_ERR(result != VK_SUCCESS, "failed to create swapchain") { + forget(); + throw cvk::vk_result_exception{result, details->exception()}; + } + + _handle.reset(ptr, core()); + _extent = imageExtent; + _surfaceFormat = surfaceFormat; + _presentMode = presentMode; + + createImages(); +} + +void Swapchain::createImages() { + auto const* functions = core().functions(); + + uint32_t count = 0; + auto const countResult = functions->vkGetSwapchainImagesKHR(core().vkDevice(), get(), &count, nullptr); + CVK_RESULT_STABLE_THROW(countResult != VK_SUCCESS, countResult, "failed to get the swapchain image count") {} + + vector vkImages(count); + auto const imagesResult = functions->vkGetSwapchainImagesKHR(core().vkDevice(), get(), &count, vkImages.data()); + CVK_RESULT_STABLE_THROW(imagesResult != VK_SUCCESS, imagesResult, "failed to get the swapchain images") {} + + Image::Config const imageConfig{ + .format = _surfaceFormat.format, + .usage = _config.usage, + .queueFamilies = _config.queueFamilies, + }; + + _images.reserve(count); + for(auto const vkImage : vkImages) + _images.emplace_back(core(), imageConfig, Image::State{.vkImage = vkImage, .extent = _extent}); + + _views.reserve(count); + for(auto const& image : _images) + _views.emplace_back(image, ImageView::Config{}, cvk::create); + + _presentSemaphores.reserve(count); + _presentFences.reserve(count); + for(uint32_t index = 0; index < count; ++index) { + _presentSemaphores.emplace_back(core(), cvk::create); + _presentFences.emplace_back(core(), VK_FENCE_CREATE_SIGNALED_BIT); + } + + _acquired.assign(count, false); +} + +void Swapchain::destroyImages() { + for(auto const& fence : _presentFences) + fence.wait(); + + _views.clear(); + // the swapchain destroys its images + for(auto& image : _images) + image.release(); + _images.clear(); + + _presentSemaphores.clear(); + _presentFences.clear(); + _acquired.clear(); +} + +void Swapchain::collectRetired() { + std::erase_if(_retired, [](Retired const& retired) { + return std::ranges::all_of(retired.presentFences, &Fence::signaled); + }); +} + +void Swapchain::destroyRetired() { + for(auto const& retired : _retired) + for(auto const& fence : retired.presentFences) + fence.wait(); + + _retired.clear(); +} + +void Swapchain::forget() { + _extent = {}; + _surfaceFormat = {}; + _presentMode = VK_PRESENT_MODE_FIFO_KHR; +} + +Swapchain& Swapchain::operator=(Swapchain&& other) noexcept { + if(this == &other) return *this; + + optDestroy(); + destroyRetired(); + + _presentQueue = other._presentQueue; + _surface = other._surface; + _config = std::move(other._config); + _handle = std::move(other._handle); + _images = std::move(other._images); + _views = std::move(other._views); + _presentSemaphores = std::move(other._presentSemaphores); + _presentFences = std::move(other._presentFences); + _acquired = std::move(other._acquired); + _retired = std::move(other._retired); + _extent = other._extent; + _surfaceFormat = other._surfaceFormat; + _presentMode = other._presentMode; + + other.forget(); + return *this; +} +} diff --git a/cvk/tests/src/context.cpp b/cvk/tests/src/context.cpp index 81b3ed2b..57149aea 100644 --- a/cvk/tests/src/context.cpp +++ b/cvk/tests/src/context.cpp @@ -14,6 +14,13 @@ Core::Config const Context::CONFIG{ Context::Context(Core::Config const& config) : instance{APP_NAME, NO_EXTENSIONS, counting_messenger()}, core{instance, {}, config} {} +Core::Config const PresentContext::CONFIG{ + .queueProperties = {QueueFamilyProperties::GRAPHICS | QueueFamilyProperties::PRESENT}, +}; + +PresentContext::PresentContext() : instance{APP_NAME, headless_extensions(), counting_messenger()}, + surfaces{headless_surface(instance)}, core{instance, surfaces, CONFIG} {} + Context::ValidationCheck::ValidationCheck() { reset_validation_errors(); } Context::ValidationCheck::~ValidationCheck() { EXPECT_EQ(validation_errors(), 0); } diff --git a/cvk/tests/src/context.hpp b/cvk/tests/src/context.hpp index e364ebfe..2ab6576e 100644 --- a/cvk/tests/src/context.hpp +++ b/cvk/tests/src/context.hpp @@ -1,4 +1,5 @@ #pragma once +#include "headless.hpp" #include "test.hpp" #include "cvk/base/core.hpp" @@ -7,6 +8,7 @@ #include "cvk/data/img/image.hpp" #include "cvk/render/cmd/cmd_buffer.hpp" +#include #include @@ -37,6 +39,32 @@ struct Context { Context& operator=(Context&& other) = delete; }; +/** + * @brief instance, headless surface and core with a present queue, fails the test on validation errors + */ +struct PresentContext { + /** + * @brief one graphics queue that presents + */ + static Core::Config const CONFIG; + + PresentContext(); + + Context::ValidationCheck validationCheck; + + Instance instance; + std::array surfaces; + Core core; + + [[nodiscard]] Surface const& surface() const { return surfaces[0]; } + [[nodiscard]] Queue const& queue() const { return core.queue(0); } + + PresentContext(PresentContext const& other) = delete; + PresentContext(PresentContext&& other) = delete; + PresentContext& operator=(PresentContext const& other) = delete; + PresentContext& operator=(PresentContext&& other) = delete; +}; + /** * @brief submits the command buffer to the first queue and waits for the gpu * @pre the command buffer is executable diff --git a/cvk/tests/src/render/sync/barrier_test.cpp b/cvk/tests/src/render/sync/barrier_test.cpp index 21851727..4e002471 100644 --- a/cvk/tests/src/render/sync/barrier_test.cpp +++ b/cvk/tests/src/render/sync/barrier_test.cpp @@ -25,6 +25,7 @@ namespace { CVK_TEST(Barrier, layout_transition_table) { constexpr auto toTransfer = layout_transition(VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL); static_assert(toTransfer.has_value()); + static_assert(toTransfer->src.stages == VK_PIPELINE_STAGE_2_TRANSFER_BIT); static_assert(toTransfer->src.access == VK_ACCESS_2_NONE); static_assert(toTransfer->dst.access == VK_ACCESS_2_TRANSFER_WRITE_BIT); diff --git a/cvk/tests/src/surface/swapchain_test.cpp b/cvk/tests/src/surface/swapchain_test.cpp new file mode 100644 index 00000000..2d015642 --- /dev/null +++ b/cvk/tests/src/surface/swapchain_test.cpp @@ -0,0 +1,227 @@ +#include "context.hpp" +#include "headless.hpp" +#include "test.hpp" + +#include "cvk/base/queue/queue.hpp" +#include "cvk/render/cmd/cmd_buffer.hpp" +#include "cvk/render/cmd/cmd_pool.hpp" +#include "cvk/render/sync/barrier.hpp" +#include "cvk/render/sync/fence.hpp" +#include "cvk/render/sync/semaphore.hpp" +#include "cvk/surface/surface.hpp" +#include "cvk/surface/swapchain.hpp" + +#include +#include +#include + + +namespace cvk { +namespace { + constexpr VkExtent2D EXTENT{64, 64}; + constexpr uint32_t ANY = std::numeric_limits::max(); + + constexpr VkSurfaceCapabilitiesKHR capabilities( + VkExtent2D currentExtent, + uint32_t minImageCount = 2, + uint32_t maxImageCount = 0 + ) { + return VkSurfaceCapabilitiesKHR{ + .minImageCount = minImageCount, + .maxImageCount = maxImageCount, + .currentExtent = currentExtent, + .minImageExtent = {16, 16}, + .maxImageExtent = {1024, 1024}, + .maxImageArrayLayers = 1, + .supportedTransforms = VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR, + .currentTransform = VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR, + .supportedCompositeAlpha = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR, + .supportedUsageFlags = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT, + }; + } + + /** + * @brief acquires an image, clears it and presents it + * @return result of @ref Swapchain::present() + */ + VkResult present_cleared_image(test::PresentContext const& context, Swapchain& swapchain) { + Semaphore const acquired{context.core, create}; + auto const [result, index] = swapchain.acquire(acquired); + EXPECT_TRUE(index.has_value()) << "acquire result: " << result; + if(!index.has_value()) return result; + + auto const& image = swapchain.images()[*index]; + CmdPool const pool{context.core, CmdPool::Config::Default(context.queue()), create}; + PrimaryCmdBuffer cmd{pool, create}; + + cmd.begin(VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT); + + Barrier toAttachment{}; + toAttachment.addTransition(image, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, VK_IMAGE_LAYOUT_UNDEFINED); + toAttachment.execute(cmd); + + cmd.beginRendering( + { + .colorAttachments = { + RenderingAttachment{ + .view = &swapchain.views()[*index], + .loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR, + .clearValue = {.color = {.float32 = {0.f, 0.f, 1.f, 1.f}}}, + }, + }, + } + ); + cmd.endRendering(); + + Barrier toPresent{}; + toPresent.addTransition(image, VK_IMAGE_LAYOUT_PRESENT_SRC_KHR, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL); + toPresent.execute(cmd); + + cmd.end(); + + Fence const fence{context.core, 0}; + context.queue().submit( + { + .cmdBuffers = {&cmd}, + .waits = {{acquired, VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT}}, + .signals = {swapchain.presentSemaphore(*index)}, + }, + &fence + ); + auto const presented = swapchain.present(*index); + fence.wait(); + + return presented; + } + + /** + * @brief waits for the acquire semaphore of an image that was not presented + */ + void consume(test::PresentContext const& context, Semaphore const& acquired) { + Fence const fence{context.core, 0}; + context.queue().submit({.waits = {acquired}}, &fence); + fence.wait(); + } +} + +CVK_TEST(Swapchain, eval_extent) { + static_assert(Swapchain::evalExtent(EXTENT, capabilities({800, 600})).width == 800); + static_assert(Swapchain::evalExtent(EXTENT, capabilities({ANY, ANY})).width == EXTENT.width); + static_assert(Swapchain::evalExtent({4096, 8}, capabilities({ANY, ANY})).width == 1024); + static_assert(Swapchain::evalExtent({4096, 8}, capabilities({ANY, ANY})).height == 16); +} + +CVK_TEST(Swapchain, eval_image_count) { + static_assert(Swapchain::evalImageCount(0, capabilities({ANY, ANY}, 2, 0)) == 3); + static_assert(Swapchain::evalImageCount(0, capabilities({ANY, ANY}, 2, 2)) == 2); + static_assert(Swapchain::evalImageCount(1, capabilities({ANY, ANY}, 2, 8)) == 2); + static_assert(Swapchain::evalImageCount(5, capabilities({ANY, ANY}, 2, 0)) == 5); +} + +CVK_TEST(Swapchain, base_constructor_does_not_create) { + test::PresentContext const context{}; + + Swapchain const swapchain{context.core, context.queue(), context.surface(), {}}; + + EXPECT_FALSE(swapchain.created()); + EXPECT_EQ(swapchain.imageCount(), 0); +} + +CVK_TEST(Swapchain, create_and_destroy) { + test::PresentContext const context{}; + auto const capabilities = context.surface().capabilities(context.core.physicalDevice()); + + Swapchain swapchain{context.core, context.queue(), context.surface(), {}, EXTENT}; + + ASSERT_TRUE(swapchain.created()); + EXPECT_GE(swapchain.imageCount(), capabilities.minImageCount); + EXPECT_EQ(swapchain.views().size(), swapchain.imageCount()); + EXPECT_EQ(swapchain.extent().width, Swapchain::evalExtent(EXTENT, capabilities).width); + EXPECT_EQ(swapchain.format(), context.surface().format(context.core.physicalDevice()).format); + EXPECT_EQ(swapchain.images()[0].aspectMask(), VK_IMAGE_ASPECT_COLOR_BIT); + + swapchain.destroy(); + EXPECT_FALSE(swapchain.created()); + EXPECT_EQ(swapchain.imageCount(), 0); +} + +CVK_TEST(Swapchain, acquire_clear_present) { + test::PresentContext const context{}; + Swapchain swapchain{context.core, context.queue(), context.surface(), {}, EXTENT}; + + for(uint32_t frame = 0; frame < swapchain.imageCount() + 1; ++frame) { + auto const result = present_cleared_image(context, swapchain); + EXPECT_TRUE(result == VK_SUCCESS || result == VK_SUBOPTIMAL_KHR) << "present result: " << result; + } +} + +CVK_TEST(Swapchain, resize_retires_the_old_swapchain) { + test::PresentContext const context{}; + Swapchain swapchain{context.core, context.queue(), context.surface(), {}, EXTENT}; + EXPECT_EQ(present_cleared_image(context, swapchain), VK_SUCCESS); + + swapchain.resize({32, 32}); + + EXPECT_EQ(swapchain.retired(), 1); + EXPECT_EQ(swapchain.extent().width, 32); + EXPECT_EQ(present_cleared_image(context, swapchain), VK_SUCCESS); +} + +CVK_TEST(Swapchain, unacquire_gives_the_image_back) { + test::PresentContext const context{}; + Swapchain swapchain{context.core, context.queue(), context.surface(), {}, EXTENT}; + Semaphore const acquired{context.core, create}; + + auto const [result, index] = swapchain.acquire(acquired); + ASSERT_TRUE(index.has_value()); + EXPECT_TRUE(swapchain.acquired(*index)); + + swapchain.unacquire(std::array{*index}); + + EXPECT_FALSE(swapchain.acquired(*index)); + consume(context, acquired); +} + +CVK_TEST(Swapchain, resize_unacquires_the_acquired_images) { + test::PresentContext const context{}; + Swapchain swapchain{context.core, context.queue(), context.surface(), {}, EXTENT}; + Semaphore const acquired{context.core, create}; + + auto const [result, index] = swapchain.acquire(acquired); + ASSERT_TRUE(index.has_value()); + + swapchain.resize({32, 32}); + + consume(context, acquired); + EXPECT_EQ(present_cleared_image(context, swapchain), VK_SUCCESS); +} + +CVK_TEST(Swapchain, release_and_wrap) { + test::PresentContext const context{}; + Swapchain swapchain{context.core, context.queue(), context.surface(), {}, EXTENT}; + auto const vkSwapchain = swapchain.get(); + auto const imageCount = swapchain.imageCount(); + + auto const state = swapchain.release(); + EXPECT_FALSE(swapchain.created()); + + Swapchain wrapped{context.core, context.queue(), context.surface(), {}, state}; + EXPECT_EQ(wrapped.get(), vkSwapchain); + EXPECT_EQ(wrapped.imageCount(), imageCount); + EXPECT_EQ(present_cleared_image(context, wrapped), VK_SUCCESS); +} + +CVK_TEST(Swapchain, move_assignment_destroys_the_old_swapchain) { + test::PresentContext const context{}; + auto const other = test::headless_surface(context.instance); + Swapchain swapchain{context.core, context.queue(), context.surface(), {}, EXTENT}; + Swapchain moved{context.core, context.queue(), other, {}, EXTENT}; + auto const vkSwapchain = moved.get(); + + swapchain = std::move(moved); + + EXPECT_EQ(swapchain.get(), vkSwapchain); + EXPECT_FALSE(moved.created()); + EXPECT_EQ(present_cleared_image(context, swapchain), VK_SUCCESS); +} +} From db23dfb2398a441dc676ac2f275ea0c732e7cd91 Mon Sep 17 00:00:00 2001 From: lukas <89145039+codethinki@users.noreply.github.com> Date: Sun, 18 Oct 2026 12:27:58 +0200 Subject: [PATCH 059/135] cvk: todo for the custom swapchain move assignment --- cvk/incl/cvk/surface/swapchain.hpp | 1 + 1 file changed, 1 insertion(+) diff --git a/cvk/incl/cvk/surface/swapchain.hpp b/cvk/incl/cvk/surface/swapchain.hpp index 59d32fb8..71d25315 100644 --- a/cvk/incl/cvk/surface/swapchain.hpp +++ b/cvk/incl/cvk/surface/swapchain.hpp @@ -273,6 +273,7 @@ class Swapchain { Swapchain(Swapchain const& other) = delete; Swapchain(Swapchain&& other) noexcept = default; Swapchain& operator=(Swapchain const& other) = delete; + //TODO the custom move assignment is ugly, find a design where the default one works Swapchain& operator=(Swapchain&& other) noexcept; static constexpr void debug_check(Swapchain const&); From 18c4dec5e5aaa7ac7d6e0774e2b431ba82bf5947 Mon Sep 17 00:00:00 2001 From: lukas <89145039+codethinki@users.noreply.github.com> Date: Sun, 18 Oct 2026 13:13:53 +0200 Subject: [PATCH 060/135] build: cth from the lib/cth submodule - lib/cth: cth as a submodule, lib/cmake/cth.cmake adds it with coro, net and win off - lib/cmake/.cmake: one cmake file per library, gtl.cmake moved there - no cth_ROOT and no separate cth install anymore, cth links boost-pfr itself (cth#16) - lib/cth_cmake at 09a6c5d for cth_add_subdirectory (cth_cmake#7) --- .gitmodules | 4 ++++ AGENTS.md | 8 +++++--- CMakeLists.txt | 3 ++- CMakePresets.json | 3 +-- cvk/CMakeLists.txt | 5 +---- lib/cmake/cth.cmake | 13 +++++++++++++ lib/{ => cmake}/gtl.cmake | 0 lib/cth | 1 + lib/cth_cmake | 2 +- tests/CMakeLists.txt | 1 - 10 files changed, 28 insertions(+), 12 deletions(-) create mode 100644 lib/cmake/cth.cmake rename lib/{ => cmake}/gtl.cmake (100%) create mode 160000 lib/cth diff --git a/.gitmodules b/.gitmodules index 166339fa..897a6dea 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,7 @@ [submodule "cth_cmake"] path = lib/cth_cmake url = https://github.com/codethinki/cth_cmake +[submodule "cth"] + path = lib/cth + url = https://github.com/codethinki/cth + branch = master diff --git a/AGENTS.md b/AGENTS.md index 3999364b..9ca3d6e8 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -12,7 +12,9 @@ Read it before you change a file. Match the existing code. | `jvk/prework/` | parts for `jvk` that left `cvk`, not built | | `legacy/` | the old `jvk`, `jolly` and `demo` code, reference only, not built | | `tests/` | loader smoke tests | +| `lib/cth` | cth (submodule), `lib/cmake/cth.cmake` adds it with only the components the engine needs | | `lib/cth_cmake` | cmake utilities (submodule) | +| `lib/cmake` | one cmake file per library that adds it: `.cmake` | - Vulkan 1.3 is the baseline. Do not port code that 1.3 makes unnecessary (render passes, framebuffers, the old sync API). - Functionality beyond 1.3 comes from required extensions, not from a higher version: `VK_KHR_dynamic_rendering_local_read` gives tile-based GPUs subpass-like local reads in a dynamic rendering. Extension feature structs go into `utils::PhysicalDeviceFeatures`, `link()` puts them in the chain only with their extension. @@ -20,7 +22,7 @@ Read it before you change a file. Match the existing code. - Memory comes from VMA. - `cvk` destroys immediately. It has no destruction queue. Deferred destruction belongs to `jvk`. - `cvk` tracks no image layouts. Every function that uses a layout takes it as a value, with a default where the use fixes it. Layout tracking belongs to `jvk` (`jvk/prework/image_layouts.hpp`). -- Hash maps come from gtl (`jvk/prework/flat_hash_map.hpp`). gtl installs no cmake package, `lib/gtl.cmake` creates the target `gtl::gtl`. +- Hash maps come from gtl (`jvk/prework/flat_hash_map.hpp`). gtl installs no cmake package, `lib/cmake/gtl.cmake` creates the target `gtl::gtl`. - Commits have no `Co-Authored-By` or "Generated with" lines. ## Branches @@ -41,10 +43,10 @@ ctest --test-dir out/build/gcc_release --output-on-failure ``` - Presets: `gcc_debug`, `gcc_release`, `clang_debug`, `clang_release`, `msvc_debug`, `msvc_release`. -- `cth_ROOT` points at `../cth/out/install/`. That install must exist. +- cth comes from the submodule `lib/cth`: clone with `--recursive` or run `git submodule update --init`. - Debug builds enable the validation layers with synchronization validation (`Instance::VALIDATION_LAYER_SETTINGS`). Run the tests in a debug build before you report a change as done. - Tests that need more queues than the device has skip. Mesa Intel devices get more queues with `ANV_QUEUE_OVERRIDE=gc=2,c=1`. -- After a new cth install, build with `--clean-first`. The precompiled header does not see the changed cth headers. +- After a new cth version in `lib/cth`, build with `--clean-first`. The precompiled header does not see the changed cth headers. - Tests that need a device use `test::Context` (`cvk/tests/src/context.hpp`). It fails the test on validation errors, leaked objects included. - `VCPKG_ROOT` must point at vcpkg. diff --git a/CMakeLists.txt b/CMakeLists.txt index 0215f9cd..e0bcc98a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -10,7 +10,8 @@ cth_set_latest_c_cpp_standard() set(CMAKE_CXX_SCAN_FOR_MODULES OFF) -include(lib/gtl.cmake) +include(lib/cmake/cth.cmake) +include(lib/cmake/gtl.cmake) if(JOLLY_ENABLE_TESTS) enable_testing() diff --git a/CMakePresets.json b/CMakePresets.json index 1d288bcf..cdd29c56 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -5,8 +5,7 @@ "name": "base", "hidden": true, "cacheVariables": { - "CMAKE_TOOLCHAIN_FILE": "${sourceDir}/lib/cth_cmake/toolchain.cmake", - "cth_ROOT": "${sourceDir}/../cth/out/install/${presetName}" + "CMAKE_TOOLCHAIN_FILE": "${sourceDir}/lib/cth_cmake/toolchain.cmake" } }, { diff --git a/cvk/CMakeLists.txt b/cvk/CMakeLists.txt index 17baa54b..8470f9c8 100644 --- a/cvk/CMakeLists.txt +++ b/cvk/CMakeLists.txt @@ -20,13 +20,10 @@ target_precompile_headers( find_package(volk CONFIG REQUIRED) find_package(VulkanMemoryAllocator CONFIG REQUIRED) find_package(VulkanUtilityLibraries CONFIG REQUIRED) -find_package(cth CONFIG REQUIRED COMPONENTS cth os) -# cth/hash/hash_aggregates.hpp includes boost/pfr.hpp, the cth package does not export it -find_package(boost_pfr CONFIG REQUIRED) target_link_libraries( cvk - PUBLIC volk::volk Vulkan::UtilityHeaders GPUOpen::VulkanMemoryAllocator cth::cth cth::os Boost::pfr + PUBLIC volk::volk Vulkan::UtilityHeaders GPUOpen::VulkanMemoryAllocator cth::cth cth::os ) # tests diff --git a/lib/cmake/cth.cmake b/lib/cmake/cth.cmake new file mode 100644 index 00000000..05376501 --- /dev/null +++ b/lib/cmake/cth.cmake @@ -0,0 +1,13 @@ +# cth from the lib/cth submodule +if(NOT TARGET cth::cth) + if(NOT EXISTS "${CMAKE_CURRENT_LIST_DIR}/../cth/CMakeLists.txt") + message(FATAL_ERROR "lib/cth is empty, run: git submodule update --init lib/cth") + endif() + + # the engine needs cth and cth_os + set(CTH_BUILD_CORO OFF) + set(CTH_BUILD_NET OFF) + set(CTH_BUILD_WIN OFF) + + add_subdirectory("${CMAKE_CURRENT_LIST_DIR}/../cth" "${CMAKE_BINARY_DIR}/lib/cth") +endif() diff --git a/lib/gtl.cmake b/lib/cmake/gtl.cmake similarity index 100% rename from lib/gtl.cmake rename to lib/cmake/gtl.cmake diff --git a/lib/cth b/lib/cth new file mode 160000 index 00000000..9fb6cb68 --- /dev/null +++ b/lib/cth @@ -0,0 +1 @@ +Subproject commit 9fb6cb68a660be25645316114a2a5e99313faf97 diff --git a/lib/cth_cmake b/lib/cth_cmake index 5162ddad..09a6c5d0 160000 --- a/lib/cth_cmake +++ b/lib/cth_cmake @@ -1 +1 @@ -Subproject commit 5162ddad62653abac2652b50a4fdde4091e861b0 +Subproject commit 09a6c5d02421cba58fcf33bc3e4fd3d4fb646002 diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 58b3abea..593a6c9f 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -4,7 +4,6 @@ add_executable(tests ${TEST_SRC}) find_package(GTest CONFIG REQUIRED) find_package(volk CONFIG REQUIRED) -find_package(cth CONFIG REQUIRED COMPONENTS cth) target_link_libraries(tests PRIVATE GTest::gtest_main volk::volk cth::cth) From 1a7e1ae3038884427d4124d29a10f31108fab1dd Mon Sep 17 00:00:00 2001 From: lukas <89145039+codethinki@users.noreply.github.com> Date: Sun, 18 Oct 2026 17:32:22 +0200 Subject: [PATCH 061/135] cvk: descriptor pool and sets, bindless - DescriptorSetLayout: flags per binding, update after bind layouts get the pool flag - DescriptorPool: Config::add(layout, count, variableCount) counts the descriptors, the pool counts its sets and resets like CmdPool - DescriptorSet: allocates itself, write(DescriptorWrite...) with value type descriptors, bind() on the set, a pool reset frees it, destroy() frees only with FREE_DESCRIPTOR_SET_BIT - device: the minimum set of descriptor indexing features plus descriptorBindingVariableDescriptorCount - legacy bugs not ported: the pool reset kept freed handles and counters, the set writes pointed at the end of the info array, the builder range check compared the binding count - tests: draws with a uniform buffer and with a bindless texture written after the bind --- .../cvk/data/descriptor/descriptor_pool.hpp | 166 ++++++++++ .../descriptor/descriptor_pool_config.hpp | 98 ++++++ .../cvk/data/descriptor/descriptor_set.hpp | 210 +++++++++++++ .../cvk/data/descriptor/descriptor_write.hpp | 73 +++++ .../pipeline/layout/descriptor_set_layout.hpp | 108 ++++++- .../pipeline/layout/pipeline_layout.hpp | 5 + .../cvk/utility/constant/device_constants.hpp | 22 +- cvk/src/data/descriptor/descriptor_pool.cpp | 90 ++++++ cvk/src/data/descriptor/descriptor_set.cpp | 283 ++++++++++++++++++ .../pipeline/layout/descriptor_set_layout.cpp | 26 +- cvk/tests/shaders/bindless.frag | 12 + cvk/tests/shaders/uniform_color.frag | 9 + .../data/descriptor/descriptor_draw_test.cpp | 259 ++++++++++++++++ .../data/descriptor/descriptor_pool_test.cpp | 105 +++++++ .../data/descriptor/descriptor_set_test.cpp | 166 ++++++++++ .../layout/descriptor_set_layout_test.cpp | 47 +++ cvk/tests/src/shaders.hpp | 50 ++++ 17 files changed, 1711 insertions(+), 18 deletions(-) create mode 100644 cvk/incl/cvk/data/descriptor/descriptor_pool.hpp create mode 100644 cvk/incl/cvk/data/descriptor/descriptor_pool_config.hpp create mode 100644 cvk/incl/cvk/data/descriptor/descriptor_set.hpp create mode 100644 cvk/incl/cvk/data/descriptor/descriptor_write.hpp create mode 100644 cvk/src/data/descriptor/descriptor_pool.cpp create mode 100644 cvk/src/data/descriptor/descriptor_set.cpp create mode 100644 cvk/tests/shaders/bindless.frag create mode 100644 cvk/tests/shaders/uniform_color.frag create mode 100644 cvk/tests/src/data/descriptor/descriptor_draw_test.cpp create mode 100644 cvk/tests/src/data/descriptor/descriptor_pool_test.cpp create mode 100644 cvk/tests/src/data/descriptor/descriptor_set_test.cpp diff --git a/cvk/incl/cvk/data/descriptor/descriptor_pool.hpp b/cvk/incl/cvk/data/descriptor/descriptor_pool.hpp new file mode 100644 index 00000000..0a1bac70 --- /dev/null +++ b/cvk/incl/cvk/data/descriptor/descriptor_pool.hpp @@ -0,0 +1,166 @@ +#pragma once +#include "descriptor_pool_config.hpp" + +#include "cvk/base/device_table.hpp" +#include "cvk/utility/constants.hpp" +#include "cvk/utility/core_handle.hpp" +#include "cvk/utility/types.hpp" + +#include + +#include + +#include + + +namespace cvk { +class Core; + +/** + * @brief VkDescriptorPool wrapper, counts its descriptor sets and its resets + * @details the sets allocate themselves, see @ref DescriptorSet::create() + */ +class DescriptorPool { +public: + using vk_handle_t = VkDescriptorPool; + using Config = DescriptorPoolConfig; + struct State; + + /** + * @brief base constructor + * @pre @ref Core::created() + */ + DescriptorPool(Core const&, Config); + + /** + * @brief constructs and wraps + * @details calls: + * - @ref DescriptorPool(Core const&, Config) + * - @ref wrap() + */ + DescriptorPool(Core const&, Config, State const&); + + /** + * @brief constructs and creates + * @details calls: + * - @ref DescriptorPool(Core const&, Config) + * - @ref create() + */ + explicit DescriptorPool(Core const&, Config, create_t); + + /** + * @note calls @ref optDestroy() + */ + ~DescriptorPool() { optDestroy(); } + + /** + * @brief wraps the state + * @pre the pool was created with the config + * @post @ref created() + * @note calls @ref optDestroy() + */ + void wrap(State const&); + + /** + * @brief creates the pool + * @post @ref created() + * @note calls @ref optDestroy() + * @throws cvk::vk_result_exception result of vkCreateDescriptorPool() + */ + void create(); + + /** + * @brief destroys and forgets + * @pre @ref created() + * @pre @ref unused() + */ + void destroy(); + + /** + * @brief if @ref created() -> calls @ref destroy() + */ + void optDestroy() { if(created()) destroy(); } + + /** + * @brief releases ownership, returns the state and forgets + * @pre @ref created() + * @pre @ref unused() + */ + State release(); + + /** + * @brief frees every set of the pool + * @pre @ref created() + * @pre the gpu does not use a set of the pool + * @post @ref unused(), @ref DescriptorSet::created() of every set of the pool is false + * @throws cvk::vk_result_exception result of vkResetDescriptorPool() + */ + void reset() const; + + /** + * @brief counts a set of the pool + * @details @ref DescriptorSet::create() and @ref DescriptorSet::wrap() call it + */ + void add() const; + + /** + * @brief stops to count a set of the pool + * @details @ref DescriptorSet::destroy() and @ref DescriptorSet::release() call it + * @pre !@ref unused() + */ + void remove() const; + + static void destroy(DeviceTable table, VkDescriptorPool); + + [[nodiscard]] constexpr bool created() const { return static_cast(_handle); } + [[nodiscard]] constexpr vk_handle_t get() const { return _handle.get(); } + [[nodiscard]] constexpr Core const& core() const { return _handle.core(); } + [[nodiscard]] constexpr Config const& config() const { return _config; } + + /** + * @return no set of the pool exists + */ + [[nodiscard]] constexpr bool unused() const { return _sets == 0; } + + /** + * @return number of @ref reset() calls + */ + [[nodiscard]] constexpr uint64_t resets() const { return _resets; } + + /** + * @return VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT is set + */ + [[nodiscard]] constexpr bool freesSets() const { + return (_config.flags & VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT) != 0; + } + +private: + void forget(); + + core_handle _handle; + Config _config; + + mutable size_t _sets = 0; + mutable uint64_t _resets = 0; + +public: + DescriptorPool(DescriptorPool const& other) = delete; + DescriptorPool(DescriptorPool&& other) noexcept = default; + DescriptorPool& operator=(DescriptorPool const& other) = delete; + DescriptorPool& operator=(DescriptorPool&& other) noexcept = default; + + static constexpr void debug_check(DescriptorPool const&); +}; +} + +namespace cvk { +struct DescriptorPool::State { + vk_not_null vkPool; +}; +} + +namespace cvk { +constexpr void DescriptorPool::debug_check(DescriptorPool const& pool) { + CTH_CRITICAL(!pool.created(), "descriptor pool must be created") {} +} +} diff --git a/cvk/incl/cvk/data/descriptor/descriptor_pool_config.hpp b/cvk/incl/cvk/data/descriptor/descriptor_pool_config.hpp new file mode 100644 index 00000000..2744d238 --- /dev/null +++ b/cvk/incl/cvk/data/descriptor/descriptor_pool_config.hpp @@ -0,0 +1,98 @@ +#pragma once +#include "cvk/render/pipeline/layout/descriptor_set_layout.hpp" + +#include + +#include + +#include +#include +#include + + +namespace cvk { +/** + * @brief set count, descriptor counts and flags of a descriptor pool + */ +struct DescriptorPoolConfig { + /** + * @pre > 0 + */ + uint32_t maxSets = 0; + + /** + * @brief descriptors per type, for all sets of the pool together + * @pre every count > 0 + */ + std::vector sizes{}; + + /** + * @brief VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT -> @ref DescriptorSet::destroy() frees the set, + * VK_DESCRIPTOR_POOL_CREATE_UPDATE_AFTER_BIND_BIT -> sets of update after bind layouts + */ + VkDescriptorPoolCreateFlags flags = 0; + + /** + * @brief adds count sets of the layout to the set count and the descriptor counts + * @param count of sets + * @param variableCount descriptors of the variable count binding per set (0 => its maximum count) + * @details update after bind layout -> adds VK_DESCRIPTOR_POOL_CREATE_UPDATE_AFTER_BIND_BIT + * @pre variableCount != 0 -> the layout has a variable count binding with a maximum >= variableCount + */ + constexpr DescriptorPoolConfig& add(DescriptorSetLayout const&, uint32_t count = 1, uint32_t variableCount = 0); + + /** + * @attention pPoolSizes points into @ref sizes + */ + [[nodiscard]] constexpr VkDescriptorPoolCreateInfo createInfo() const { + return VkDescriptorPoolCreateInfo{ + .sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO, + .pNext = nullptr, + .flags = flags, + .maxSets = maxSets, + .poolSizeCount = static_cast(sizes.size()), + .pPoolSizes = sizes.data(), + }; + } + + static constexpr void debug_check(DescriptorPoolConfig const&); +}; +} + +namespace cvk { +constexpr DescriptorPoolConfig& DescriptorPoolConfig::add( + DescriptorSetLayout const& layout, + uint32_t count, + uint32_t variableCount +) { + auto const* variable = layout.variableBinding(); + CTH_CRITICAL(variableCount != 0 && variable == nullptr, "a variable count needs a variable count binding") {} + CTH_CRITICAL( + variable != nullptr && variableCount > variable->descriptorCount, + "the variable count must not exceed the maximum count of the binding" + ) {} + + maxSets += count; + if(layout.updateAfterBind()) flags |= VK_DESCRIPTOR_POOL_CREATE_UPDATE_AFTER_BIND_BIT; + + for(auto const& binding : layout.bindings()) { + uint32_t const descriptors = &binding == variable && variableCount != 0 ? variableCount + : binding.descriptorCount; + if(descriptors == 0) continue; + + auto const it = std::ranges::find(sizes, binding.descriptorType, &VkDescriptorPoolSize::type); + if(it != sizes.end()) it->descriptorCount += descriptors * count; + else sizes.push_back({binding.descriptorType, descriptors * count}); + } + + return *this; +} + +constexpr void DescriptorPoolConfig::debug_check(DescriptorPoolConfig const& config) { + CTH_CRITICAL(config.maxSets == 0, "a descriptor pool needs at least one set") {} + CTH_CRITICAL( + std::ranges::any_of(config.sizes, [](VkDescriptorPoolSize const& size) { return size.descriptorCount == 0; }), + "descriptor counts must be > 0" + ) {} +} +} diff --git a/cvk/incl/cvk/data/descriptor/descriptor_set.hpp b/cvk/incl/cvk/data/descriptor/descriptor_set.hpp new file mode 100644 index 00000000..ad61eb35 --- /dev/null +++ b/cvk/incl/cvk/data/descriptor/descriptor_set.hpp @@ -0,0 +1,210 @@ +#pragma once +#include "descriptor_pool.hpp" +#include "descriptor_write.hpp" + +#include "cvk/base/device_table.hpp" +#include "cvk/render/pipeline/layout/descriptor_set_layout.hpp" +#include "cvk/utility/constants.hpp" +#include "cvk/utility/types.hpp" +#include "cvk/utility/unique_handle.hpp" + +#include + +#include + +#include +#include + + +namespace cvk { +class BaseCmdBuffer; +class Core; +class PipelineLayout; + +/** + * @brief VkDescriptorSet wrapper, allocated from its pool with its layout + * @details @ref DescriptorPool::reset() frees the set, @ref created() is false after it + */ +class DescriptorSet { +public: + using vk_handle_t = VkDescriptorSet; + struct State; + + /** + * @brief base constructor + * @param variableCount descriptors of the variable count binding (0 => its maximum count) + * @pre @ref DescriptorPool::created(), @ref DescriptorSetLayout::created() + * @pre variableCount != 0 -> the layout has a variable count binding with a maximum >= variableCount + */ + DescriptorSet(DescriptorPool const&, DescriptorSetLayout const&, uint32_t variableCount = 0); + + /** + * @brief constructs and wraps + * @details calls: + * - @ref DescriptorSet(DescriptorPool const&, DescriptorSetLayout const&, uint32_t) + * - @ref wrap() + */ + DescriptorSet(DescriptorPool const&, DescriptorSetLayout const&, uint32_t variableCount, State const&); + + /** + * @brief constructs and creates + * @details calls: + * - @ref DescriptorSet(DescriptorPool const&, DescriptorSetLayout const&, uint32_t) + * - @ref create() + */ + DescriptorSet(DescriptorPool const&, DescriptorSetLayout const&, uint32_t variableCount, create_t); + + /** + * @brief constructs and creates with the maximum variable count + * @details calls @ref DescriptorSet(DescriptorPool const&, DescriptorSetLayout const&, uint32_t, create_t) + */ + DescriptorSet(DescriptorPool const& pool, DescriptorSetLayout const& layout, create_t) : DescriptorSet{ + pool, + layout, + 0, + cvk::create + } {} + + /** + * @note calls @ref optDestroy() + */ + ~DescriptorSet() { optDestroy(); } + + /** + * @brief wraps the state + * @pre the pool allocated the set with the layout and the variable count + * @post @ref created() + * @note calls @ref optDestroy() + */ + void wrap(State const&); + + /** + * @brief allocates the set from the pool + * @pre @ref DescriptorPool::created() + * @pre update after bind layout -> the pool has VK_DESCRIPTOR_POOL_CREATE_UPDATE_AFTER_BIND_BIT + * @post @ref created() + * @note calls @ref optDestroy() + * @throws cvk::vk_result_exception result of vkAllocateDescriptorSets() + */ + void create(); + + /** + * @brief destroys and forgets + * @details the pool frees sets -> vkFreeDescriptorSets(), else the memory returns with @ref DescriptorPool::reset() + * @pre @ref created() + * @pre the gpu does not use the set + */ + void destroy(); + + /** + * @brief if @ref created() -> calls @ref destroy() + */ + void optDestroy() { if(created()) destroy(); } + + /** + * @brief releases ownership, returns the state and forgets + * @pre @ref created() + */ + State release(); + + /** + * @brief writes the descriptors into the set + * @pre @ref created() + * @pre the bindings exist, the descriptors match their types and lie in them + * @pre the gpu does not use the written descriptors, except with VK_DESCRIPTOR_BINDING_UPDATE_AFTER_BIND_BIT + * @attention texel buffer, inline uniform block and acceleration structure descriptors are not supported + */ + void write(std::span) const; + + /** + * @brief records the bind of the set + * @param setIndex set number in the pipeline layout + * @param dynamicOffsets one per dynamic buffer descriptor of the set + * @pre @ref created() + * @pre @ref BaseCmdBuffer::recording() + * @pre the set layout of setIndex in the pipeline layout is compatible with the layout of the set + */ + void bind( + BaseCmdBuffer const&, + PipelineLayout const&, + uint32_t setIndex, + VkPipelineBindPoint bindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS, + std::span dynamicOffsets = {} + ) const; + + static void destroy(DeviceTable table, VkDescriptorPool, VkDescriptorSet); + + /** + * @return the set is allocated and its pool was not reset since + */ + [[nodiscard]] constexpr bool created() const { + return static_cast(_handle) && _handle.deleter().resets() == pool().resets(); + } + [[nodiscard]] constexpr vk_handle_t get() const { return _handle.get(); } + [[nodiscard]] constexpr DescriptorPool const& pool() const { return _handle.deleter().pool(); } + [[nodiscard]] constexpr Core const& core() const { return pool().core(); } + [[nodiscard]] constexpr DescriptorSetLayout const& layout() const { return *_layout; } + + /** + * @return descriptors of the variable count binding, 0 -> its maximum count or no such binding + */ + [[nodiscard]] constexpr uint32_t variableCount() const { return _variableCount; } + +private: + /** + * @brief frees through the pool, nothing if the pool was reset since the allocation + */ + class pool_deleter { + public: + constexpr explicit pool_deleter(DescriptorPool const& pool) : _pool{&pool}, _resets{pool.resets()} {} + + void operator()(VkDescriptorSet) const; + + [[nodiscard]] constexpr DescriptorPool const& pool() const { return *_pool; } + + /** + * @return resets of the pool at the allocation + */ + [[nodiscard]] constexpr uint64_t resets() const { return _resets; } + + private: + not_null _pool; + uint64_t _resets; + }; + + /** + * @return descriptor count of the binding, the variable count for the variable count binding + */ + [[nodiscard]] constexpr uint32_t capacity(VkDescriptorSetLayoutBinding const&) const; + + void forget(); + + unique_handle _handle; + not_null _layout; + uint32_t _variableCount; + +public: + DescriptorSet(DescriptorSet const& other) = delete; + DescriptorSet(DescriptorSet&& other) noexcept = default; + DescriptorSet& operator=(DescriptorSet const& other) = delete; + DescriptorSet& operator=(DescriptorSet&& other) noexcept = default; + + static constexpr void debug_check(DescriptorSet const&); +}; +} + +namespace cvk { +struct DescriptorSet::State { + vk_not_null vkSet; +}; +} + +namespace cvk { +constexpr uint32_t DescriptorSet::capacity(VkDescriptorSetLayoutBinding const& binding) const { + return &binding == _layout->variableBinding() && _variableCount != 0 ? _variableCount : binding.descriptorCount; +} + +constexpr void DescriptorSet::debug_check(DescriptorSet const& set) { + CTH_CRITICAL(!set.created(), "descriptor set must be created, a pool reset frees it") {} +} +} diff --git a/cvk/incl/cvk/data/descriptor/descriptor_write.hpp b/cvk/incl/cvk/data/descriptor/descriptor_write.hpp new file mode 100644 index 00000000..acdfef1f --- /dev/null +++ b/cvk/incl/cvk/data/descriptor/descriptor_write.hpp @@ -0,0 +1,73 @@ +#pragma once +#include "cvk/data/buffer/base_buffer.hpp" +#include "cvk/data/img/image_view.hpp" +#include "cvk/data/img/sampler.hpp" +#include "cvk/utility/types.hpp" + +#include + +#include +#include + + +namespace cvk { +/** + * @brief image, sampler or both for an image descriptor, the binding decides the descriptor type + */ +struct ImageDescriptor { + /** + * @brief nullptr -> no view, VK_DESCRIPTOR_TYPE_SAMPLER + */ + ImageView const* view = nullptr; + + /** + * @brief layout of the view when the gpu reads it + * @details VK_DESCRIPTOR_TYPE_STORAGE_IMAGE -> VK_IMAGE_LAYOUT_GENERAL + */ + VkImageLayout layout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; + + /** + * @brief nullptr -> no sampler, the descriptor type needs none + */ + Sampler const* sampler = nullptr; + + [[nodiscard]] constexpr VkDescriptorImageInfo info() const { + return VkDescriptorImageInfo{ + .sampler = sampler != nullptr ? sampler->get() : VK_NULL_HANDLE, + .imageView = view != nullptr ? view->get() : VK_NULL_HANDLE, + .imageLayout = view != nullptr ? layout : VK_IMAGE_LAYOUT_UNDEFINED, + }; + } +}; + +/** + * @brief buffer range for a buffer descriptor, the binding decides the descriptor type + */ +struct BufferDescriptor { + not_null buffer; + + /** + * @brief in bytes + */ + size_t offset = 0; + + /** + * @brief in bytes (VK_WHOLE_SIZE => to the end) + */ + size_t size = VK_WHOLE_SIZE; + + [[nodiscard]] constexpr VkDescriptorBufferInfo info() const { return buffer->descriptorInfo(size, offset); } +}; + +/** + * @brief descriptors for consecutive array elements of one binding + * @details image bindings take @ref images, buffer bindings take @ref buffers + */ +struct DescriptorWrite { + uint32_t binding = 0; + uint32_t arrayElement = 0; + + std::vector images{}; + std::vector buffers{}; +}; +} diff --git a/cvk/incl/cvk/render/pipeline/layout/descriptor_set_layout.hpp b/cvk/incl/cvk/render/pipeline/layout/descriptor_set_layout.hpp index 843b4a37..b5ec5e4e 100644 --- a/cvk/incl/cvk/render/pipeline/layout/descriptor_set_layout.hpp +++ b/cvk/incl/cvk/render/pipeline/layout/descriptor_set_layout.hpp @@ -10,6 +10,8 @@ #include #include +#include +#include #include #include @@ -19,6 +21,8 @@ class Core; /** * @brief VkDescriptorSetLayout wrapper + * @details a binding with VK_DESCRIPTOR_BINDING_UPDATE_AFTER_BIND_BIT -> the layout has + * VK_DESCRIPTOR_SET_LAYOUT_CREATE_UPDATE_AFTER_BIND_POOL_BIT */ class DescriptorSetLayout { public: @@ -63,6 +67,7 @@ class DescriptorSetLayout { /** * @brief creates the layout + * @pre @ref Config::debug_check() of the config * @post @ref created() * @note calls @ref optDestroy() * @throws cvk::vk_result_exception result of vkCreateDescriptorSetLayout() @@ -95,18 +100,34 @@ class DescriptorSetLayout { /** * @return bindings, sorted by binding number */ - [[nodiscard]] std::span bindings() const; + [[nodiscard]] constexpr std::span bindings() const { return _bindings; } /** * @return nullptr -> no binding with that number */ - [[nodiscard]] VkDescriptorSetLayoutBinding const* binding(uint32_t binding) const; + [[nodiscard]] constexpr VkDescriptorSetLayoutBinding const* binding(uint32_t binding) const; + + /** + * @return flags of the binding, no binding with that number -> 0 + */ + [[nodiscard]] constexpr VkDescriptorBindingFlags bindingFlags(uint32_t binding) const; + + /** + * @return a binding has VK_DESCRIPTOR_BINDING_UPDATE_AFTER_BIND_BIT + */ + [[nodiscard]] constexpr bool updateAfterBind() const; + + /** + * @return binding with VK_DESCRIPTOR_BINDING_VARIABLE_DESCRIPTOR_COUNT_BIT, nullptr -> none + */ + [[nodiscard]] constexpr VkDescriptorSetLayoutBinding const* variableBinding() const; private: void forget(); core_handle _handle; std::vector _bindings; + std::vector _bindingFlags; public: DescriptorSetLayout(DescriptorSetLayout const& other) = delete; @@ -123,10 +144,18 @@ struct DescriptorSetLayout::Config { /** * @brief adds a binding, the numbers may have gaps * @param binding number of the binding - * @param count of descriptors, 0 -> the binding is reserved without descriptors + * @param count of descriptors, 0 -> the binding is reserved without descriptors, + * VK_DESCRIPTOR_BINDING_VARIABLE_DESCRIPTOR_COUNT_BIT -> the maximum count + * @param flags VK_DESCRIPTOR_BINDING_PARTIALLY_BOUND_BIT, VK_DESCRIPTOR_BINDING_UPDATE_AFTER_BIND_BIT, ... * @pre no binding with the same number */ - constexpr Config& add(uint32_t binding, VkDescriptorType type, VkShaderStageFlags stages, uint32_t count = 1); + constexpr Config& add( + uint32_t binding, + VkDescriptorType type, + VkShaderStageFlags stages, + uint32_t count = 1, + VkDescriptorBindingFlags flags = 0 + ); /** * @brief removes a binding @@ -138,6 +167,13 @@ struct DescriptorSetLayout::Config { * @brief bindings sorted by binding number */ std::vector bindings{}; + + /** + * @brief flags per binding in the order of @ref bindings, empty -> no flags + */ + std::vector bindingFlags{}; + + static constexpr void debug_check(Config const&); }; struct DescriptorSetLayout::State { @@ -150,11 +186,19 @@ constexpr DescriptorSetLayout::Config& DescriptorSetLayout::Config::add( uint32_t binding, VkDescriptorType type, VkShaderStageFlags stages, - uint32_t count + uint32_t count, + VkDescriptorBindingFlags flags ) { auto const it = std::ranges::lower_bound(bindings, binding, {}, &VkDescriptorSetLayoutBinding::binding); CTH_CRITICAL(it != bindings.end() && it->binding == binding, "binding {} already exists", binding) {} + // flags are stored for every binding once one binding has flags + if(flags != 0 || !bindingFlags.empty()) { + auto const index = it - bindings.begin(); + bindingFlags.resize(bindings.size(), 0); + bindingFlags.insert(bindingFlags.begin() + index, flags); + } + bindings.insert( it, VkDescriptorSetLayoutBinding{ @@ -172,12 +216,66 @@ constexpr DescriptorSetLayout::Config& DescriptorSetLayout::Config::remove(uint3 auto const it = std::ranges::lower_bound(bindings, binding, {}, &VkDescriptorSetLayoutBinding::binding); CTH_CRITICAL(it == bindings.end() || it->binding != binding, "binding {} does not exist", binding) {} + if(!bindingFlags.empty()) bindingFlags.erase(bindingFlags.begin() + (it - bindings.begin())); bindings.erase(it); return *this; } } namespace cvk { +constexpr void DescriptorSetLayout::Config::debug_check(Config const& config) { + CTH_CRITICAL( + !config.bindingFlags.empty() && config.bindingFlags.size() != config.bindings.size(), + "binding flags must be empty or one per binding" + ) {} + if(config.bindingFlags.empty()) return; + + for(auto const [index, flags] : config.bindingFlags | std::views::enumerate) { + auto const& binding = config.bindings[index]; + bool const dynamic = binding.descriptorType == VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC + || binding.descriptorType == VK_DESCRIPTOR_TYPE_STORAGE_BUFFER_DYNAMIC; + + CTH_CRITICAL( + (flags & VK_DESCRIPTOR_BINDING_VARIABLE_DESCRIPTOR_COUNT_BIT) != 0 + && static_cast(index) + 1 != config.bindings.size(), + "only the binding with the highest number can have a variable descriptor count" + ) { + details->add("binding: {}", binding.binding); + } + CTH_CRITICAL( + dynamic && (flags & (VK_DESCRIPTOR_BINDING_UPDATE_AFTER_BIND_BIT + | VK_DESCRIPTOR_BINDING_VARIABLE_DESCRIPTOR_COUNT_BIT)) != 0, + "dynamic buffer bindings cannot update after bind or have a variable descriptor count" + ) { + details->add("binding: {}", binding.binding); + } + } +} + +constexpr VkDescriptorSetLayoutBinding const* DescriptorSetLayout::binding(uint32_t binding) const { + auto const it = std::ranges::lower_bound(_bindings, binding, {}, &VkDescriptorSetLayoutBinding::binding); + return it != _bindings.end() && it->binding == binding ? std::to_address(it) : nullptr; +} + +constexpr VkDescriptorBindingFlags DescriptorSetLayout::bindingFlags(uint32_t binding) const { + if(_bindingFlags.empty()) return 0; + + auto const it = std::ranges::lower_bound(_bindings, binding, {}, &VkDescriptorSetLayoutBinding::binding); + return it != _bindings.end() && it->binding == binding ? _bindingFlags[it - _bindings.begin()] : 0; +} + +constexpr bool DescriptorSetLayout::updateAfterBind() const { + return std::ranges::any_of(_bindingFlags, [](VkDescriptorBindingFlags flags) { + return (flags & VK_DESCRIPTOR_BINDING_UPDATE_AFTER_BIND_BIT) != 0; + }); +} + +constexpr VkDescriptorSetLayoutBinding const* DescriptorSetLayout::variableBinding() const { + if(_bindingFlags.empty() || (_bindingFlags.back() & VK_DESCRIPTOR_BINDING_VARIABLE_DESCRIPTOR_COUNT_BIT) == 0) + return nullptr; + return &_bindings.back(); +} + constexpr void DescriptorSetLayout::debug_check(DescriptorSetLayout const& layout) { CTH_CRITICAL(!layout.created(), "descriptor set layout must be created") {} } diff --git a/cvk/incl/cvk/render/pipeline/layout/pipeline_layout.hpp b/cvk/incl/cvk/render/pipeline/layout/pipeline_layout.hpp index 7a1df4f7..9515a8f9 100644 --- a/cvk/incl/cvk/render/pipeline/layout/pipeline_layout.hpp +++ b/cvk/incl/cvk/render/pipeline/layout/pipeline_layout.hpp @@ -124,6 +124,11 @@ class PipelineLayout { [[nodiscard]] constexpr vk_handle_t get() const { return _handle.get(); } [[nodiscard]] constexpr Core const& core() const { return _handle.core(); } + /** + * @return handle of the set layout per set number + */ + [[nodiscard]] constexpr std::span setLayouts() const { return _vkSetLayouts; } + private: void forget(); diff --git a/cvk/incl/cvk/utility/constant/device_constants.hpp b/cvk/incl/cvk/utility/constant/device_constants.hpp index 78d2634c..f8308595 100644 --- a/cvk/incl/cvk/utility/constant/device_constants.hpp +++ b/cvk/incl/cvk/utility/constant/device_constants.hpp @@ -34,16 +34,34 @@ inline constexpr std::array PRESENT_INSTANCE_EXTENSIONS{ /** * @brief features that every device enables - * @details descriptorIndexing: the device supports the minimum set of descriptor indexing features, for bindless + * @details bindless: descriptorIndexing guarantees the minimum set of descriptor indexing features, they are enabled, + * descriptorBindingVariableDescriptorCount is not part of the set, the one extra requirement */ [[nodiscard]] inline utils::PhysicalDeviceFeatures required_device_features() { utils::PhysicalDeviceFeatures features{}; - features.features().features.samplerAnisotropy = VK_TRUE; + auto& features10 = features.features().features; + features10.samplerAnisotropy = VK_TRUE; + features10.shaderSampledImageArrayDynamicIndexing = VK_TRUE; + features10.shaderStorageBufferArrayDynamicIndexing = VK_TRUE; auto& features12 = features.features(); features12.timelineSemaphore = VK_TRUE; features12.descriptorIndexing = VK_TRUE; + features12.shaderUniformTexelBufferArrayDynamicIndexing = VK_TRUE; + features12.shaderStorageTexelBufferArrayDynamicIndexing = VK_TRUE; + features12.shaderSampledImageArrayNonUniformIndexing = VK_TRUE; + features12.shaderStorageBufferArrayNonUniformIndexing = VK_TRUE; + features12.shaderUniformTexelBufferArrayNonUniformIndexing = VK_TRUE; + features12.descriptorBindingSampledImageUpdateAfterBind = VK_TRUE; + features12.descriptorBindingStorageImageUpdateAfterBind = VK_TRUE; + features12.descriptorBindingStorageBufferUpdateAfterBind = VK_TRUE; + features12.descriptorBindingUniformTexelBufferUpdateAfterBind = VK_TRUE; + features12.descriptorBindingStorageTexelBufferUpdateAfterBind = VK_TRUE; + features12.descriptorBindingUpdateUnusedWhilePending = VK_TRUE; + features12.descriptorBindingPartiallyBound = VK_TRUE; + features12.runtimeDescriptorArray = VK_TRUE; + features12.descriptorBindingVariableDescriptorCount = VK_TRUE; auto& features13 = features.features(); features13.synchronization2 = VK_TRUE; diff --git a/cvk/src/data/descriptor/descriptor_pool.cpp b/cvk/src/data/descriptor/descriptor_pool.cpp new file mode 100644 index 00000000..0ac5fec4 --- /dev/null +++ b/cvk/src/data/descriptor/descriptor_pool.cpp @@ -0,0 +1,90 @@ +#include "cvk/data/descriptor/descriptor_pool.hpp" + +#include "cvk/base/core.hpp" +#include "cvk/utility/vk_exceptions.hpp" + + +namespace cvk { +DescriptorPool::DescriptorPool(Core const& core, Config config) : _handle{core}, _config{std::move(config)} { + Core::debug_check(core); + Config::debug_check(_config); +} + +DescriptorPool::DescriptorPool(Core const& core, Config config, State const& state) : DescriptorPool{ + core, + std::move(config) +} { wrap(state); } + +DescriptorPool::DescriptorPool(Core const& core, Config config, create_t) : DescriptorPool{core, std::move(config)} { + create(); +} + +void DescriptorPool::wrap(State const& state) { + optDestroy(); + + _handle.reset(state.vkPool.get(), core()); +} + +void DescriptorPool::create() { + optDestroy(); + + auto const info = _config.createInfo(); + + VkDescriptorPool ptr = VK_NULL_HANDLE; + auto const result = core().functions()->vkCreateDescriptorPool(core().vkDevice(), &info, nullptr, &ptr); + + CTH_STABLE_ERR(result != VK_SUCCESS, "failed to create descriptor pool") { + forget(); + throw cvk::vk_result_exception{result, details->exception()}; + } + + _handle.reset(ptr, core()); +} + +void DescriptorPool::destroy() { + debug_check(*this); + CTH_CRITICAL(!unused(), "descriptor sets of the pool must be destroyed first") { + details->add("descriptor sets: {}", _sets); + } + + _handle.reset(); + + forget(); +} + +DescriptorPool::State DescriptorPool::release() { + debug_check(*this); + CTH_CRITICAL(!unused(), "descriptor sets of the pool must be destroyed first") { + details->add("descriptor sets: {}", _sets); + } + + State const state{_handle.release()}; + forget(); + return state; +} + +void DescriptorPool::reset() const { + debug_check(*this); + + auto const result = core().functions()->vkResetDescriptorPool(core().vkDevice(), get(), 0); + CVK_RESULT_STABLE_THROW(result != VK_SUCCESS, result, "failed to reset descriptor pool") {} + + _sets = 0; + ++_resets; +} + +void DescriptorPool::add() const { ++_sets; } + +void DescriptorPool::remove() const { + CTH_CRITICAL(unused(), "the pool counts no descriptor set") {} + --_sets; +} + +void DescriptorPool::destroy(DeviceTable table, VkDescriptorPool vkPool) { + CTH_WARN(vkPool == VK_NULL_HANDLE, "vkPool invalid (VK_NULL_HANDLE)") {} + + table->vkDestroyDescriptorPool(table.device(), vkPool, nullptr); +} + +void DescriptorPool::forget() {} +} diff --git a/cvk/src/data/descriptor/descriptor_set.cpp b/cvk/src/data/descriptor/descriptor_set.cpp new file mode 100644 index 00000000..c3c043d1 --- /dev/null +++ b/cvk/src/data/descriptor/descriptor_set.cpp @@ -0,0 +1,283 @@ +#include "cvk/data/descriptor/descriptor_set.hpp" + +#include "cvk/base/core.hpp" +#include "cvk/render/cmd/cmd_buffer.hpp" +#include "cvk/render/pipeline/layout/pipeline_layout.hpp" +#include "cvk/utility/vk_exceptions.hpp" + +#include +#include + + +namespace cvk { +using std::span; + +namespace { + enum class DescriptorKind { + IMAGE, + BUFFER, + UNSUPPORTED, + }; + + constexpr DescriptorKind descriptor_kind(VkDescriptorType type) { + switch(type) { + case VK_DESCRIPTOR_TYPE_SAMPLER: + case VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER: + case VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE: + case VK_DESCRIPTOR_TYPE_STORAGE_IMAGE: + case VK_DESCRIPTOR_TYPE_INPUT_ATTACHMENT: return DescriptorKind::IMAGE; + case VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER: + case VK_DESCRIPTOR_TYPE_STORAGE_BUFFER: + case VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC: + case VK_DESCRIPTOR_TYPE_STORAGE_BUFFER_DYNAMIC: return DescriptorKind::BUFFER; + default: return DescriptorKind::UNSUPPORTED; + } + } + + constexpr bool dynamic_buffer(VkDescriptorType type) { + return type == VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC || type == VK_DESCRIPTOR_TYPE_STORAGE_BUFFER_DYNAMIC; + } + + void debug_check_image(ImageDescriptor const& descriptor, VkDescriptorType type) { + CTH_CRITICAL( + (type == VK_DESCRIPTOR_TYPE_SAMPLER || type == VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER) + && descriptor.sampler == nullptr, + "sampler descriptors need a sampler" + ) {} + CTH_CRITICAL( + type != VK_DESCRIPTOR_TYPE_SAMPLER && descriptor.view == nullptr, + "image descriptors need a view" + ) {} + CTH_CRITICAL( + type == VK_DESCRIPTOR_TYPE_STORAGE_IMAGE && descriptor.layout != VK_IMAGE_LAYOUT_GENERAL, + "storage image descriptors need VK_IMAGE_LAYOUT_GENERAL" + ) {} + if(descriptor.view != nullptr) ImageView::debug_check(*descriptor.view); + if(descriptor.sampler != nullptr) Sampler::debug_check(*descriptor.sampler); + } +} + +DescriptorSet::DescriptorSet(DescriptorPool const& pool, DescriptorSetLayout const& layout, uint32_t variableCount) : + _handle{pool}, _layout{&layout}, _variableCount{variableCount} { + DescriptorPool::debug_check(pool); + DescriptorSetLayout::debug_check(layout); + + auto const* variable = layout.variableBinding(); + CTH_CRITICAL(variableCount != 0 && variable == nullptr, "a variable count needs a variable count binding") {} + CTH_CRITICAL( + variable != nullptr && variableCount > variable->descriptorCount, + "the variable count must not exceed the maximum count of the binding" + ) {} +} + +DescriptorSet::DescriptorSet( + DescriptorPool const& pool, + DescriptorSetLayout const& layout, + uint32_t variableCount, + State const& state +) : DescriptorSet{pool, layout, variableCount} { wrap(state); } + +DescriptorSet::DescriptorSet( + DescriptorPool const& pool, + DescriptorSetLayout const& layout, + uint32_t variableCount, + create_t +) : DescriptorSet{pool, layout, variableCount} { create(); } + +void DescriptorSet::wrap(State const& state) { + optDestroy(); + + _handle.reset(state.vkSet.get(), pool()); + pool().add(); +} + +void DescriptorSet::create() { + DescriptorPool::debug_check(pool()); + CTH_CRITICAL( + _layout->updateAfterBind() && (pool().config().flags & VK_DESCRIPTOR_POOL_CREATE_UPDATE_AFTER_BIND_BIT) == 0, + "update after bind layouts need a pool with VK_DESCRIPTOR_POOL_CREATE_UPDATE_AFTER_BIND_BIT" + ) {} + + optDestroy(); + + auto const* variable = _layout->variableBinding(); + uint32_t const variableCount = variable != nullptr ? capacity(*variable) : 0; + VkDescriptorSetVariableDescriptorCountAllocateInfo const variableInfo{ + .sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_VARIABLE_DESCRIPTOR_COUNT_ALLOCATE_INFO, + .pNext = nullptr, + .descriptorSetCount = 1, + .pDescriptorCounts = &variableCount, + }; + + VkDescriptorSetLayout const vkLayout = _layout->get(); + VkDescriptorSetAllocateInfo const info{ + .sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO, + .pNext = variable != nullptr ? &variableInfo : nullptr, + .descriptorPool = pool().get(), + .descriptorSetCount = 1, + .pSetLayouts = &vkLayout, + }; + + VkDescriptorSet ptr = VK_NULL_HANDLE; + auto const result = core().functions()->vkAllocateDescriptorSets(core().vkDevice(), &info, &ptr); + + CTH_STABLE_ERR(result != VK_SUCCESS, "failed to allocate descriptor set") { + forget(); + throw cvk::vk_result_exception{result, details->exception()}; + } + + _handle.reset(ptr, pool()); + pool().add(); +} + +void DescriptorSet::destroy() { + debug_check(*this); + + _handle.reset(); + + forget(); +} + +DescriptorSet::State DescriptorSet::release() { + debug_check(*this); + + State const state{_handle.release()}; + pool().remove(); + + forget(); + return state; +} + +void DescriptorSet::write(span writes) const { + debug_check(*this); + + size_t imageCount = 0; + size_t bufferCount = 0; + for(auto const& write : writes) { + imageCount += write.images.size(); + bufferCount += write.buffers.size(); + } + + // reserved: the writes point into the infos + std::vector imageInfos{}; + imageInfos.reserve(imageCount); + std::vector bufferInfos{}; + bufferInfos.reserve(bufferCount); + std::vector vkWrites{}; + vkWrites.reserve(writes.size()); + + for(auto const& write : writes) { + auto const* binding = _layout->binding(write.binding); + CTH_CRITICAL(binding == nullptr, "the layout has no binding {}", write.binding) {} + + auto const type = binding->descriptorType; + auto const kind = descriptor_kind(type); + auto const count = static_cast(kind == DescriptorKind::IMAGE ? write.images.size() + : write.buffers.size()); + + CTH_CRITICAL(kind == DescriptorKind::UNSUPPORTED, "descriptor type not supported") { + details->add("binding: {}, type: {}", write.binding, type); + } + CTH_CRITICAL( + (kind == DescriptorKind::IMAGE && !write.buffers.empty()) + || (kind == DescriptorKind::BUFFER && !write.images.empty()), + "the descriptors must match the type of the binding" + ) { + details->add("binding: {}, type: {}", write.binding, type); + } + CTH_CRITICAL(count == 0, "a write needs descriptors") {} + CTH_CRITICAL(write.arrayElement + count > capacity(*binding), "the descriptors must lie in the binding") { + details->add("binding: {}, first: {}, count: {}", write.binding, write.arrayElement, count); + } + + VkWriteDescriptorSet vkWrite{ + .sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, + .pNext = nullptr, + .dstSet = get(), + .dstBinding = write.binding, + .dstArrayElement = write.arrayElement, + .descriptorCount = count, + .descriptorType = type, + .pImageInfo = nullptr, + .pBufferInfo = nullptr, + .pTexelBufferView = nullptr, + }; + + if(kind == DescriptorKind::IMAGE) { + vkWrite.pImageInfo = imageInfos.data() + imageInfos.size(); + for(auto const& descriptor : write.images) { + debug_check_image(descriptor, type); + imageInfos.push_back(descriptor.info()); + } + } else { + vkWrite.pBufferInfo = bufferInfos.data() + bufferInfos.size(); + for(auto const& descriptor : write.buffers) + bufferInfos.push_back(descriptor.info()); + } + + vkWrites.push_back(vkWrite); + } + + core().functions()->vkUpdateDescriptorSets( + core().vkDevice(), + static_cast(vkWrites.size()), + vkWrites.data(), + 0, + nullptr + ); +} + +void DescriptorSet::bind( + BaseCmdBuffer const& cmdBuffer, + PipelineLayout const& layout, + uint32_t setIndex, + VkPipelineBindPoint bindPoint, + span dynamicOffsets +) const { + debug_check(*this); + BaseCmdBuffer::debug_check(cmdBuffer); + CTH_CRITICAL(!cmdBuffer.recording(), "command buffer must be recording") {} + PipelineLayout::debug_check(layout); + CTH_CRITICAL(setIndex >= layout.setLayouts().size(), "the pipeline layout has no set {}", setIndex) {} + + auto const dynamicCount = std::ranges::fold_left( + _layout->bindings(), + uint32_t{0}, + [](uint32_t sum, VkDescriptorSetLayoutBinding const& binding) { + return dynamic_buffer(binding.descriptorType) ? sum + binding.descriptorCount : sum; + } + ); + CTH_CRITICAL(dynamicOffsets.size() != dynamicCount, "one dynamic offset per dynamic buffer descriptor") { + details->add("dynamic descriptors: {}, offsets: {}", dynamicCount, dynamicOffsets.size()); + } + + VkDescriptorSet const vkSet = get(); + core().functions()->vkCmdBindDescriptorSets( + cmdBuffer.get(), + bindPoint, + layout.get(), + setIndex, + 1, + &vkSet, + static_cast(dynamicOffsets.size()), + dynamicOffsets.data() + ); +} + +void DescriptorSet::destroy(DeviceTable table, VkDescriptorPool vkPool, VkDescriptorSet vkSet) { + CTH_WARN(vkSet == VK_NULL_HANDLE, "vkSet invalid (VK_NULL_HANDLE)") {} + + auto const result = table->vkFreeDescriptorSets(table.device(), vkPool, 1, &vkSet); + CTH_WARN(result != VK_SUCCESS, "failed to free descriptor set") {} +} + +void DescriptorSet::forget() {} + +void DescriptorSet::pool_deleter::operator()(VkDescriptorSet vkSet) const { + // a reset of the pool freed the set already + if(_resets != _pool->resets()) return; + + _pool->remove(); + if(_pool->freesSets()) destroy(_pool->core().deviceTable(), _pool->get(), vkSet); +} +} diff --git a/cvk/src/render/pipeline/layout/descriptor_set_layout.cpp b/cvk/src/render/pipeline/layout/descriptor_set_layout.cpp index f21b139f..4f8a7f6c 100644 --- a/cvk/src/render/pipeline/layout/descriptor_set_layout.cpp +++ b/cvk/src/render/pipeline/layout/descriptor_set_layout.cpp @@ -7,8 +7,13 @@ namespace cvk { -DescriptorSetLayout::DescriptorSetLayout(Core const& core, Config config) : _handle{core}, - _bindings{std::move(config.bindings)} { Core::debug_check(core); } +DescriptorSetLayout::DescriptorSetLayout(Core const& core, Config config) : _handle{core} { + Core::debug_check(core); + Config::debug_check(config); + + _bindings = std::move(config.bindings); + _bindingFlags = std::move(config.bindingFlags); +} DescriptorSetLayout::DescriptorSetLayout(Core const& core, Config config, State const& state) : DescriptorSetLayout{ core, @@ -29,10 +34,16 @@ void DescriptorSetLayout::wrap(State const& state) { void DescriptorSetLayout::create() { optDestroy(); + VkDescriptorSetLayoutBindingFlagsCreateInfo const flagsInfo{ + .sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_BINDING_FLAGS_CREATE_INFO, + .pNext = nullptr, + .bindingCount = static_cast(_bindingFlags.size()), + .pBindingFlags = _bindingFlags.data(), + }; VkDescriptorSetLayoutCreateInfo const info{ .sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO, - .pNext = nullptr, - .flags = 0, + .pNext = _bindingFlags.empty() ? nullptr : &flagsInfo, + .flags = updateAfterBind() ? VK_DESCRIPTOR_SET_LAYOUT_CREATE_UPDATE_AFTER_BIND_POOL_BIT : 0u, .bindingCount = static_cast(_bindings.size()), .pBindings = _bindings.data(), }; @@ -70,12 +81,5 @@ void DescriptorSetLayout::destroy(DeviceTable table, VkDescriptorSetLayout vkLay table->vkDestroyDescriptorSetLayout(table.device(), vkLayout, nullptr); } -std::span DescriptorSetLayout::bindings() const { return _bindings; } - -VkDescriptorSetLayoutBinding const* DescriptorSetLayout::binding(uint32_t binding) const { - auto const it = std::ranges::lower_bound(_bindings, binding, {}, &VkDescriptorSetLayoutBinding::binding); - return it != _bindings.end() && it->binding == binding ? std::to_address(it) : nullptr; -} - void DescriptorSetLayout::forget() {} } diff --git a/cvk/tests/shaders/bindless.frag b/cvk/tests/shaders/bindless.frag new file mode 100644 index 00000000..d94f5f06 --- /dev/null +++ b/cvk/tests/shaders/bindless.frag @@ -0,0 +1,12 @@ +#version 450 +#extension GL_EXT_nonuniform_qualifier : require + +layout(set = 0, binding = 0) uniform sampler2D textures[]; + +layout(push_constant) uniform Push { + uint index; +} push; + +layout(location = 0) out vec4 outColor; + +void main() { outColor = texture(textures[nonuniformEXT(push.index)], vec2(0.5)); } diff --git a/cvk/tests/shaders/uniform_color.frag b/cvk/tests/shaders/uniform_color.frag new file mode 100644 index 00000000..05f4820a --- /dev/null +++ b/cvk/tests/shaders/uniform_color.frag @@ -0,0 +1,9 @@ +#version 450 + +layout(set = 0, binding = 0) uniform Color { + vec4 color; +} ubo; + +layout(location = 0) out vec4 outColor; + +void main() { outColor = ubo.color; } diff --git a/cvk/tests/src/data/descriptor/descriptor_draw_test.cpp b/cvk/tests/src/data/descriptor/descriptor_draw_test.cpp new file mode 100644 index 00000000..5a3163e9 --- /dev/null +++ b/cvk/tests/src/data/descriptor/descriptor_draw_test.cpp @@ -0,0 +1,259 @@ +#include "context.hpp" +#include "shaders.hpp" +#include "test.hpp" + +#include "cvk/data/buffer/buffer.hpp" +#include "cvk/data/descriptor/descriptor_pool.hpp" +#include "cvk/data/descriptor/descriptor_set.hpp" +#include "cvk/data/img/image.hpp" +#include "cvk/data/img/image_view.hpp" +#include "cvk/data/img/sampler.hpp" +#include "cvk/render/cmd/cmd_buffer.hpp" +#include "cvk/render/cmd/cmd_pool.hpp" +#include "cvk/render/pipeline/layout/descriptor_set_layout.hpp" +#include "cvk/render/pipeline/layout/pipeline_layout.hpp" +#include "cvk/render/pipeline/pipeline.hpp" +#include "cvk/render/pipeline/shader/shader.hpp" +#include "cvk/render/sync/barrier.hpp" + +#include +#include +#include +#include +#include +#include + + +namespace cvk { +namespace { + constexpr VkExtent2D EXTENT{16, 16}; + constexpr std::array COLOR_FORMATS{VK_FORMAT_R8G8B8A8_UNORM}; + constexpr std::array GREEN{0.f, 1.f, 0.f, 1.f}; + constexpr std::array GREEN_PIXEL{0, 255, 0, 255}; + + struct Vec3 { + float x, y, z; + }; + + /** + * @brief one triangle that covers the whole viewport + */ + constexpr std::array FULLSCREEN{Vec3{-1.f, -1.f, 0.f}, Vec3{3.f, -1.f, 0.f}, Vec3{-1.f, 3.f, 0.f}}; + + template + typename Buffer::Config host_written(VkBufferUsageFlags usage) { + return {.usage = usage, .memory = {.flags = VMA_ALLOCATION_CREATE_HOST_ACCESS_SEQUENTIAL_WRITE_BIT}}; + } + + /** + * @brief fullscreen pipeline with one descriptor set and the fragment shader + */ + struct Fill { + Fill(Core const& core, DescriptorSetLayout const& setLayout, std::span fragmentCode, + std::vector pushConstants = {}) : layout{ + core, + PipelineLayout::Config{.setLayouts = {&setLayout}, .pushConstantRanges = std::move(pushConstants)}, + create + }, + vertex{core, VK_SHADER_STAGE_VERTEX_BIT, test::shaders::FILL_VERT}, + fragment{core, VK_SHADER_STAGE_FRAGMENT_BIT, fragmentCode}, + pipeline{core, layout, config(), create}, + vertices{core, host_written(VK_BUFFER_USAGE_VERTEX_BUFFER_BIT), FULLSCREEN.size()} { + vertices.write(FULLSCREEN); + } + + [[nodiscard]] Pipeline::Config config() const { + auto config = Pipeline::Config::Default(COLOR_FORMATS); + config.addStage(vertex).addStage(fragment); + config.vertexBindings = {{.binding = 0, .stride = sizeof(Vec3), .inputRate = VK_VERTEX_INPUT_RATE_VERTEX}}; + config.vertexAttributes = { + {.location = 0, .binding = 0, .format = VK_FORMAT_R32G32B32_SFLOAT, .offset = 0}, + }; + config.rasterization.cullMode = VK_CULL_MODE_NONE; + return config; + } + + PipelineLayout layout; + Shader vertex; + Shader fragment; + Pipeline pipeline; + Buffer vertices; + }; + + /** + * @brief target image, its view and a readback buffer + */ + struct Target { + explicit Target(Core const& core) : image{ + core, + { + .format = COLOR_FORMATS[0], + .usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT, + }, + EXTENT + }, + view{image, {}, create}, + readback{ + core, + { + .usage = VK_BUFFER_USAGE_TRANSFER_DST_BIT, + .memory = {.flags = VMA_ALLOCATION_CREATE_HOST_ACCESS_RANDOM_BIT}, + }, + size_t{EXTENT.width} * EXTENT.height * 4 + } {} + + Image image; + ImageView view; + BaseBuffer readback; + }; + + /** + * @brief records: clear the target, draw with draw(), copy the target into the readback buffer + */ + void record_fill( + test::Context const& context, + BaseCmdBuffer& cmd, + Fill const& fill, + Target const& target, + std::function const& draw + ) { + Barrier toAttachment{}; + toAttachment.addTransition(target.image, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, VK_IMAGE_LAYOUT_UNDEFINED); + toAttachment.execute(cmd); + + cmd.beginRendering( + { + .colorAttachments = { + RenderingAttachment{ + .view = &target.view, + .loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR, + .clearValue = {.color = {.float32 = {0.f, 0.f, 0.f, 1.f}}}, + }, + }, + } + ); + fill.pipeline.bind(cmd); + cmd.setViewport(); + cmd.setScissor(); + cmd.bindVertexBuffer(0, fill.vertices); + draw(cmd); + cmd.draw(3); + cmd.endRendering(); + + Barrier toTransfer{}; + toTransfer.add( + target.image, + ImageBarrierInfo::LayoutTransition( + VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, + VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, + {VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT, VK_ACCESS_2_COLOR_ATTACHMENT_WRITE_BIT}, + {VK_PIPELINE_STAGE_2_TRANSFER_BIT, VK_ACCESS_2_TRANSFER_READ_BIT} + ) + ); + toTransfer.execute(cmd); + + test::copy_to_buffer(context, cmd, target.image, target.readback, 0, 0); + test::host_read_barrier(context, cmd); + } + + void expect_green(Target const& target) { + auto const pixels = target.readback.memory().map(target.readback.size()); + target.readback.memory().invalidate(); + auto const asByte = [](char c) { return static_cast(c); }; + for(auto const pixel : pixels | std::views::chunk(4)) + EXPECT_TRUE(std::ranges::equal(pixel, GREEN_PIXEL, {}, asByte)); + target.readback.memory().unmap(); + } +} + +CVK_TEST(DescriptorDraw, uniform_buffer_color) { + test::Context const context{}; + DescriptorSetLayout::Config setConfig{}; + setConfig.add(0, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, VK_SHADER_STAGE_FRAGMENT_BIT); + DescriptorSetLayout const setLayout{context.core, setConfig, create}; + Fill const fill{context.core, setLayout, test::shaders::UNIFORM_COLOR_FRAG}; + Target const target{context.core}; + + Buffer> color{ + context.core, + host_written>(VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT), + 1 + }; + color.write(std::array, 1>{GREEN}); + + DescriptorPool const pool{context.core, DescriptorPool::Config{}.add(setLayout), create}; + DescriptorSet const set{pool, setLayout, create}; + set.write(std::array{DescriptorWrite{.binding = 0, .buffers = {{&color}}}}); + + test::record_and_submit(context, [&](PrimaryCmdBuffer& cmd) { + record_fill(context, cmd, fill, target, [&](BaseCmdBuffer& c) { set.bind(c, fill.layout, 0); }); + }); + + expect_green(target); +} + +CVK_TEST(DescriptorDraw, bindless_texture_written_after_bind) { + test::Context const context{}; + constexpr uint32_t INDEX = 17; + + DescriptorSetLayout::Config setConfig{}; + setConfig.add( + 0, + VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, + VK_SHADER_STAGE_FRAGMENT_BIT, + 64, + VK_DESCRIPTOR_BINDING_PARTIALLY_BOUND_BIT | VK_DESCRIPTOR_BINDING_UPDATE_AFTER_BIND_BIT + | VK_DESCRIPTOR_BINDING_VARIABLE_DESCRIPTOR_COUNT_BIT + ); + DescriptorSetLayout const setLayout{context.core, setConfig, create}; + Fill const fill{ + context.core, + setLayout, + test::shaders::BINDLESS_FRAG, + {{VK_SHADER_STAGE_FRAGMENT_BIT, 0, sizeof(uint32_t)}} + }; + Target const target{context.core}; + + // 1x1 green texture + Image const texture{ + context.core, + {.format = VK_FORMAT_R8G8B8A8_UNORM, .usage = VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT}, + VkExtent2D{1, 1} + }; + ImageView const view{texture, {}, create}; + Sampler const sampler{context.core, {}, create}; + Buffer staging{context.core, host_written(VK_BUFFER_USAGE_TRANSFER_SRC_BIT), 4}; + staging.write(GREEN_PIXEL); + + DescriptorPool const pool{context.core, DescriptorPool::Config{}.add(setLayout, 1, 32), create}; + DescriptorSet const set{pool, setLayout, 32, create}; + + CmdPool const cmdPool{context.core, CmdPool::Config::Default(context.core.queue(0)), create}; + PrimaryCmdBuffer cmd{cmdPool, create}; + cmd.begin(VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT); + + Barrier toTransfer{}; + toTransfer.addTransition(texture, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_UNDEFINED); + toTransfer.execute(cmd); + texture.copy(cmd, staging); + Barrier toShader{}; + toShader.addTransition(texture, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL); + toShader.execute(cmd); + + record_fill(context, cmd, fill, target, [&](BaseCmdBuffer& c) { + set.bind(c, fill.layout, 0); + fill.layout.pushConstants(c, VK_SHADER_STAGE_FRAGMENT_BIT, INDEX); + }); + cmd.end(); + + // update after bind: the bind is recorded, the descriptor comes now + set.write( + std::array{ + DescriptorWrite{.binding = 0, .arrayElement = INDEX, .images = {{.view = &view, .sampler = &sampler}}}, + } + ); + test::submit_and_wait(context, cmd); + + expect_green(target); +} +} diff --git a/cvk/tests/src/data/descriptor/descriptor_pool_test.cpp b/cvk/tests/src/data/descriptor/descriptor_pool_test.cpp new file mode 100644 index 00000000..e5504112 --- /dev/null +++ b/cvk/tests/src/data/descriptor/descriptor_pool_test.cpp @@ -0,0 +1,105 @@ +#include "context.hpp" +#include "test.hpp" + +#include "cvk/data/descriptor/descriptor_pool.hpp" +#include "cvk/render/pipeline/layout/descriptor_set_layout.hpp" + +#include + + +namespace cvk { +namespace { + constexpr VkDescriptorBindingFlags BINDLESS = VK_DESCRIPTOR_BINDING_PARTIALLY_BOUND_BIT + | VK_DESCRIPTOR_BINDING_UPDATE_AFTER_BIND_BIT + | VK_DESCRIPTOR_BINDING_VARIABLE_DESCRIPTOR_COUNT_BIT; + + /** + * @brief binding 0: 2 uniform buffers, binding 1: 1 combined image sampler + */ + DescriptorSetLayout::Config buffers_and_texture() { + DescriptorSetLayout::Config config{}; + config.add(0, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, VK_SHADER_STAGE_VERTEX_BIT, 2) + .add(1, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, VK_SHADER_STAGE_FRAGMENT_BIT); + return config; + } + + /** + * @brief binding 0: up to 1024 combined image samplers, bindless flags + */ + DescriptorSetLayout::Config bindless() { + DescriptorSetLayout::Config config{}; + config.add(0, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, VK_SHADER_STAGE_FRAGMENT_BIT, 1024, BINDLESS); + return config; + } + + uint32_t count_of(DescriptorPool::Config const& config, VkDescriptorType type) { + auto const it = std::ranges::find(config.sizes, type, &VkDescriptorPoolSize::type); + return it != config.sizes.end() ? it->descriptorCount : 0; + } +} + +CVK_TEST(DescriptorPool, config_add_counts_the_descriptors_of_the_layout) { + test::Context const context{}; + DescriptorSetLayout const layout{context.core, buffers_and_texture(), create}; + + DescriptorPool::Config config{}; + config.add(layout, 3); + + EXPECT_EQ(config.maxSets, 3); + EXPECT_EQ(config.sizes.size(), 2); + EXPECT_EQ(count_of(config, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER), 6); + EXPECT_EQ(count_of(config, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER), 3); + EXPECT_EQ(config.flags, 0); +} + +CVK_TEST(DescriptorPool, config_add_takes_the_variable_count_and_update_after_bind) { + test::Context const context{}; + DescriptorSetLayout const layout{context.core, bindless(), create}; + + DescriptorPool::Config config{}; + config.add(layout, 2, 100); + + EXPECT_EQ(config.maxSets, 2); + EXPECT_EQ(count_of(config, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER), 200); + EXPECT_NE(config.flags & VK_DESCRIPTOR_POOL_CREATE_UPDATE_AFTER_BIND_BIT, 0); +} + +CVK_TEST(DescriptorPool, create_and_destroy) { + test::Context const context{}; + DescriptorSetLayout const layout{context.core, buffers_and_texture(), create}; + + DescriptorPool pool{context.core, DescriptorPool::Config{}.add(layout, 4), create}; + + ASSERT_TRUE(pool.created()); + EXPECT_TRUE(pool.unused()); + EXPECT_FALSE(pool.freesSets()); + + pool.destroy(); + EXPECT_FALSE(pool.created()); +} + +CVK_TEST(DescriptorPool, reset_counts) { + test::Context const context{}; + DescriptorSetLayout const layout{context.core, buffers_and_texture(), create}; + DescriptorPool const pool{context.core, DescriptorPool::Config{}.add(layout), create}; + + pool.reset(); + pool.reset(); + + EXPECT_EQ(pool.resets(), 2); +} + +CVK_TEST(DescriptorPool, release_and_wrap) { + test::Context const context{}; + DescriptorSetLayout const layout{context.core, buffers_and_texture(), create}; + auto const config = DescriptorPool::Config{}.add(layout); + DescriptorPool pool{context.core, config, create}; + auto const vkPool = pool.get(); + + auto const state = pool.release(); + EXPECT_FALSE(pool.created()); + + DescriptorPool const wrapped{context.core, config, state}; + EXPECT_EQ(wrapped.get(), vkPool); +} +} diff --git a/cvk/tests/src/data/descriptor/descriptor_set_test.cpp b/cvk/tests/src/data/descriptor/descriptor_set_test.cpp new file mode 100644 index 00000000..ddd93669 --- /dev/null +++ b/cvk/tests/src/data/descriptor/descriptor_set_test.cpp @@ -0,0 +1,166 @@ +#include "context.hpp" +#include "test.hpp" + +#include "cvk/data/buffer/base_buffer.hpp" +#include "cvk/data/descriptor/descriptor_pool.hpp" +#include "cvk/data/descriptor/descriptor_set.hpp" +#include "cvk/data/img/image.hpp" +#include "cvk/data/img/image_view.hpp" +#include "cvk/data/img/sampler.hpp" +#include "cvk/render/pipeline/layout/descriptor_set_layout.hpp" + +#include + + +namespace cvk { +namespace { + constexpr VkDescriptorBindingFlags BINDLESS = VK_DESCRIPTOR_BINDING_PARTIALLY_BOUND_BIT + | VK_DESCRIPTOR_BINDING_UPDATE_AFTER_BIND_BIT + | VK_DESCRIPTOR_BINDING_VARIABLE_DESCRIPTOR_COUNT_BIT; + + /** + * @brief binding 0: uniform buffer, binding 1: combined image sampler + */ + DescriptorSetLayout::Config buffer_and_texture() { + DescriptorSetLayout::Config config{}; + config.add(0, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, VK_SHADER_STAGE_VERTEX_BIT) + .add(1, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, VK_SHADER_STAGE_FRAGMENT_BIT); + return config; + } + + /** + * @brief binding 0: up to 64 combined image samplers, bindless flags + */ + DescriptorSetLayout::Config bindless() { + DescriptorSetLayout::Config config{}; + config.add(0, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, VK_SHADER_STAGE_FRAGMENT_BIT, 64, BINDLESS); + return config; + } + + constexpr Image::Config TEXTURE_CONFIG{.format = VK_FORMAT_R8G8B8A8_UNORM, .usage = VK_IMAGE_USAGE_SAMPLED_BIT}; +} + +CVK_TEST(DescriptorSet, base_constructor_does_not_create) { + test::Context const context{}; + DescriptorSetLayout const layout{context.core, buffer_and_texture(), create}; + DescriptorPool const pool{context.core, DescriptorPool::Config{}.add(layout), create}; + + DescriptorSet const set{pool, layout}; + + EXPECT_FALSE(set.created()); + EXPECT_EQ(&set.pool(), &pool); + EXPECT_EQ(&set.layout(), &layout); + EXPECT_TRUE(pool.unused()); +} + +CVK_TEST(DescriptorSet, destroy_frees_with_a_freeing_pool) { + test::Context const context{}; + DescriptorSetLayout const layout{context.core, buffer_and_texture(), create}; + DescriptorPool const pool{ + context.core, + DescriptorPool::Config{.flags = VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT}.add(layout), + create + }; + + DescriptorSet set{pool, layout, create}; + ASSERT_TRUE(set.created()); + EXPECT_FALSE(pool.unused()); + + set.destroy(); + EXPECT_FALSE(set.created()); + EXPECT_TRUE(pool.unused()); + + // the freed set is available again + set.create(); + EXPECT_TRUE(set.created()); +} + +CVK_TEST(DescriptorSet, destroy_without_free_only_forgets) { + test::Context const context{}; + DescriptorSetLayout const layout{context.core, buffer_and_texture(), create}; + DescriptorPool const pool{context.core, DescriptorPool::Config{}.add(layout), create}; + + DescriptorSet set{pool, layout, create}; + set.destroy(); + + EXPECT_FALSE(set.created()); + EXPECT_TRUE(pool.unused()); + pool.reset(); +} + +CVK_TEST(DescriptorSet, pool_reset_frees_the_set) { + test::Context const context{}; + DescriptorSetLayout const layout{context.core, buffer_and_texture(), create}; + DescriptorPool const pool{context.core, DescriptorPool::Config{}.add(layout), create}; + DescriptorSet set{pool, layout, create}; + + pool.reset(); + + EXPECT_FALSE(set.created()); + EXPECT_TRUE(pool.unused()); + + set.create(); + EXPECT_TRUE(set.created()); +} + +CVK_TEST(DescriptorSet, write_buffer_and_image) { + test::Context const context{}; + DescriptorSetLayout const layout{context.core, buffer_and_texture(), create}; + DescriptorPool const pool{context.core, DescriptorPool::Config{}.add(layout), create}; + DescriptorSet const set{pool, layout, create}; + + BaseBuffer const buffer{context.core, {.usage = VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT}, 64}; + Image const image{context.core, TEXTURE_CONFIG, VkExtent2D{4, 4}}; + ImageView const view{image, {}, create}; + Sampler const sampler{context.core, {}, create}; + + set.write( + std::array{ + DescriptorWrite{.binding = 0, .buffers = {{&buffer, 0, 16}}}, + DescriptorWrite{.binding = 1, .images = {{.view = &view, .sampler = &sampler}}}, + } + ); +} + +CVK_TEST(DescriptorSet, variable_count) { + test::Context const context{}; + DescriptorSetLayout const layout{context.core, bindless(), create}; + DescriptorPool const pool{context.core, DescriptorPool::Config{}.add(layout, 1, 8), create}; + DescriptorSet const set{pool, layout, 8, create}; + + Image const image{context.core, TEXTURE_CONFIG, VkExtent2D{4, 4}}; + ImageView const view{image, {}, create}; + Sampler const sampler{context.core, {}, create}; + + EXPECT_EQ(set.variableCount(), 8); + set.write( + std::array{ + DescriptorWrite{ + .binding = 0, + .arrayElement = 6, + .images = {{.view = &view, .sampler = &sampler}, {.view = &view, .sampler = &sampler}}, + }, + } + ); +} + +CVK_TEST(DescriptorSet, release_and_wrap) { + test::Context const context{}; + DescriptorSetLayout const layout{context.core, buffer_and_texture(), create}; + DescriptorPool const pool{ + context.core, + DescriptorPool::Config{.flags = VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT}.add(layout), + create + }; + DescriptorSet set{pool, layout, create}; + auto const vkSet = set.get(); + + auto const state = set.release(); + EXPECT_FALSE(set.created()); + EXPECT_TRUE(pool.unused()); + + DescriptorSet const wrapped{pool, layout, 0, state}; + EXPECT_EQ(wrapped.get(), vkSet); + EXPECT_FALSE(pool.unused()); +} +} diff --git a/cvk/tests/src/render/pipeline/layout/descriptor_set_layout_test.cpp b/cvk/tests/src/render/pipeline/layout/descriptor_set_layout_test.cpp index f5f4b129..2d42eb99 100644 --- a/cvk/tests/src/render/pipeline/layout/descriptor_set_layout_test.cpp +++ b/cvk/tests/src/render/pipeline/layout/descriptor_set_layout_test.cpp @@ -94,4 +94,51 @@ CVK_TEST(DescriptorSetLayout, move_assignment_destroys_the_old_layout) { EXPECT_EQ(layout.bindings().size(), 2); EXPECT_FALSE(other.created()); } + +CVK_TEST(DescriptorSetLayout, config_keeps_the_flags_in_binding_order) { + static_assert([] { + DescriptorSetLayout::Config config{}; + config.add(0, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, VK_SHADER_STAGE_VERTEX_BIT) + .add(2, VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE, VK_SHADER_STAGE_FRAGMENT_BIT, 8, + VK_DESCRIPTOR_BINDING_PARTIALLY_BOUND_BIT) + .add(1, VK_DESCRIPTOR_TYPE_SAMPLER, VK_SHADER_STAGE_FRAGMENT_BIT); + bool const added = config.bindingFlags.size() == 3 && config.bindingFlags[0] == 0 + && config.bindingFlags[1] == 0 && config.bindingFlags[2] == VK_DESCRIPTOR_BINDING_PARTIALLY_BOUND_BIT; + + config.remove(1); + return added && config.bindingFlags.size() == 2 + && config.bindingFlags[1] == VK_DESCRIPTOR_BINDING_PARTIALLY_BOUND_BIT; + }()); +} + +CVK_TEST(DescriptorSetLayout, config_keeps_the_flags_of_the_first_binding) { + static_assert([] { + DescriptorSetLayout::Config config{}; + config.add(0, VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE, VK_SHADER_STAGE_FRAGMENT_BIT, 8, + VK_DESCRIPTOR_BINDING_PARTIALLY_BOUND_BIT); + return config.bindingFlags.size() == 1 && config.bindingFlags[0] == VK_DESCRIPTOR_BINDING_PARTIALLY_BOUND_BIT; + }()); +} + +CVK_TEST(DescriptorSetLayout, config_without_flags_keeps_no_flags) { + static_assert(gap_config().bindingFlags.empty()); +} + +CVK_TEST(DescriptorSetLayout, create_bindless) { + test::Context const context{}; + constexpr VkDescriptorBindingFlags BINDLESS = VK_DESCRIPTOR_BINDING_PARTIALLY_BOUND_BIT + | VK_DESCRIPTOR_BINDING_UPDATE_AFTER_BIND_BIT + | VK_DESCRIPTOR_BINDING_VARIABLE_DESCRIPTOR_COUNT_BIT; + + DescriptorSetLayout::Config config{}; + config.add(0, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, VK_SHADER_STAGE_VERTEX_BIT) + .add(1, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, VK_SHADER_STAGE_FRAGMENT_BIT, 1024, BINDLESS); + DescriptorSetLayout const layout{context.core, config, create}; + + ASSERT_TRUE(layout.created()); + EXPECT_TRUE(layout.updateAfterBind()); + EXPECT_EQ(layout.bindingFlags(1), BINDLESS); + EXPECT_EQ(layout.bindingFlags(0), 0); + EXPECT_EQ(layout.variableBinding(), layout.binding(1)); +} } diff --git a/cvk/tests/src/shaders.hpp b/cvk/tests/src/shaders.hpp index c14d3ed1..2da73f96 100644 --- a/cvk/tests/src/shaders.hpp +++ b/cvk/tests/src/shaders.hpp @@ -85,4 +85,54 @@ inline constexpr std::array FILL_VERT{ 0x00000018, 0x00000015, 0x00000016, 0x00000017, 0x00000014, 0x00050041, 0x00000019, 0x0000001a, 0x0000000d, 0x0000000f, 0x0003003e, 0x0000001a, 0x00000018, 0x000100fd, 0x00010038, }; + +/** + * @brief fragment shader: set 0 binding 0 uniform buffer vec4 color, location 0 color output + */ +inline constexpr std::array UNIFORM_COLOR_FRAG{ + 0x07230203, 0x00010600, 0x000d000b, 0x00000012, 0x00000000, 0x00020011, 0x00000001, 0x0006000b, + 0x00000001, 0x4c534c47, 0x6474732e, 0x3035342e, 0x00000000, 0x0003000e, 0x00000000, 0x00000001, + 0x0007000f, 0x00000004, 0x00000004, 0x6e69616d, 0x00000000, 0x00000009, 0x0000000c, 0x00030010, + 0x00000004, 0x00000007, 0x00040047, 0x00000009, 0x0000001e, 0x00000000, 0x00030047, 0x0000000a, + 0x00000002, 0x00050048, 0x0000000a, 0x00000000, 0x00000023, 0x00000000, 0x00040047, 0x0000000c, + 0x00000021, 0x00000000, 0x00040047, 0x0000000c, 0x00000022, 0x00000000, 0x00020013, 0x00000002, + 0x00030021, 0x00000003, 0x00000002, 0x00030016, 0x00000006, 0x00000020, 0x00040017, 0x00000007, + 0x00000006, 0x00000004, 0x00040020, 0x00000008, 0x00000003, 0x00000007, 0x0004003b, 0x00000008, + 0x00000009, 0x00000003, 0x0003001e, 0x0000000a, 0x00000007, 0x00040020, 0x0000000b, 0x00000002, + 0x0000000a, 0x0004003b, 0x0000000b, 0x0000000c, 0x00000002, 0x00040015, 0x0000000d, 0x00000020, + 0x00000001, 0x0004002b, 0x0000000d, 0x0000000e, 0x00000000, 0x00040020, 0x0000000f, 0x00000002, + 0x00000007, 0x00050036, 0x00000002, 0x00000004, 0x00000000, 0x00000003, 0x000200f8, 0x00000005, + 0x00050041, 0x0000000f, 0x00000010, 0x0000000c, 0x0000000e, 0x0004003d, 0x00000007, 0x00000011, + 0x00000010, 0x0003003e, 0x00000009, 0x00000011, 0x000100fd, 0x00010038, +}; + +/** + * @brief fragment shader: set 0 binding 0 runtime array of combined image samplers, push constant uint index + */ +inline constexpr std::array BINDLESS_FRAG{ + 0x07230203, 0x00010600, 0x000d000b, 0x00000020, 0x00000000, 0x00020011, 0x00000001, 0x00020011, + 0x000014b5, 0x00020011, 0x000014b6, 0x00020011, 0x000014bb, 0x0006000b, 0x00000001, 0x4c534c47, + 0x6474732e, 0x3035342e, 0x00000000, 0x0003000e, 0x00000000, 0x00000001, 0x0008000f, 0x00000004, + 0x00000004, 0x6e69616d, 0x00000000, 0x00000009, 0x0000000e, 0x00000012, 0x00030010, 0x00000004, + 0x00000007, 0x00040047, 0x00000009, 0x0000001e, 0x00000000, 0x00040047, 0x0000000e, 0x00000021, + 0x00000000, 0x00040047, 0x0000000e, 0x00000022, 0x00000000, 0x00030047, 0x00000010, 0x00000002, + 0x00050048, 0x00000010, 0x00000000, 0x00000023, 0x00000000, 0x00030047, 0x00000018, 0x000014b4, + 0x00030047, 0x0000001a, 0x000014b4, 0x00030047, 0x0000001b, 0x000014b4, 0x00020013, 0x00000002, + 0x00030021, 0x00000003, 0x00000002, 0x00030016, 0x00000006, 0x00000020, 0x00040017, 0x00000007, + 0x00000006, 0x00000004, 0x00040020, 0x00000008, 0x00000003, 0x00000007, 0x0004003b, 0x00000008, + 0x00000009, 0x00000003, 0x00090019, 0x0000000a, 0x00000006, 0x00000001, 0x00000000, 0x00000000, + 0x00000000, 0x00000001, 0x00000000, 0x0003001b, 0x0000000b, 0x0000000a, 0x0003001d, 0x0000000c, + 0x0000000b, 0x00040020, 0x0000000d, 0x00000000, 0x0000000c, 0x0004003b, 0x0000000d, 0x0000000e, + 0x00000000, 0x00040015, 0x0000000f, 0x00000020, 0x00000000, 0x0003001e, 0x00000010, 0x0000000f, + 0x00040020, 0x00000011, 0x00000009, 0x00000010, 0x0004003b, 0x00000011, 0x00000012, 0x00000009, + 0x00040015, 0x00000013, 0x00000020, 0x00000001, 0x0004002b, 0x00000013, 0x00000014, 0x00000000, + 0x00040020, 0x00000015, 0x00000009, 0x0000000f, 0x00040020, 0x00000019, 0x00000000, 0x0000000b, + 0x00040017, 0x0000001c, 0x00000006, 0x00000002, 0x0004002b, 0x00000006, 0x0000001d, 0x3f000000, + 0x0005002c, 0x0000001c, 0x0000001e, 0x0000001d, 0x0000001d, 0x00050036, 0x00000002, 0x00000004, + 0x00000000, 0x00000003, 0x000200f8, 0x00000005, 0x00050041, 0x00000015, 0x00000016, 0x00000012, + 0x00000014, 0x0004003d, 0x0000000f, 0x00000017, 0x00000016, 0x00040053, 0x0000000f, 0x00000018, + 0x00000017, 0x00050041, 0x00000019, 0x0000001a, 0x0000000e, 0x00000018, 0x0004003d, 0x0000000b, + 0x0000001b, 0x0000001a, 0x00050057, 0x00000007, 0x0000001f, 0x0000001b, 0x0000001e, 0x0003003e, + 0x00000009, 0x0000001f, 0x000100fd, 0x00010038, +}; } From edb735103072f3b315cc9d887bd5fd4a54b32809 Mon Sep 17 00:00:00 2001 From: lukas <89145039+codethinki@users.noreply.github.com> Date: Sun, 18 Oct 2026 23:43:43 +0200 Subject: [PATCH 062/135] cvk: DescriptorPoolConfig constructor from the layouts DescriptorPoolConfig({{&layout, count, variableCount}, ...}, flags) counts the sets and descriptors, add() stays for adding later. The config is no aggregate anymore. --- .../descriptor/descriptor_pool_config.hpp | 27 +++++++++++++++++++ .../data/descriptor/descriptor_draw_test.cpp | 4 +-- .../data/descriptor/descriptor_pool_test.cpp | 23 +++++++++++++--- .../data/descriptor/descriptor_set_test.cpp | 14 +++++----- 4 files changed, 56 insertions(+), 12 deletions(-) diff --git a/cvk/incl/cvk/data/descriptor/descriptor_pool_config.hpp b/cvk/incl/cvk/data/descriptor/descriptor_pool_config.hpp index 2744d238..f4f220d1 100644 --- a/cvk/incl/cvk/data/descriptor/descriptor_pool_config.hpp +++ b/cvk/incl/cvk/data/descriptor/descriptor_pool_config.hpp @@ -7,6 +7,7 @@ #include #include +#include #include @@ -15,6 +16,32 @@ namespace cvk { * @brief set count, descriptor counts and flags of a descriptor pool */ struct DescriptorPoolConfig { + /** + * @brief count sets of one layout + */ + struct Sets { + not_null layout; + uint32_t count = 1; + + /** + * @brief descriptors of the variable count binding per set (0 => its maximum count) + */ + uint32_t variableCount = 0; + }; + + constexpr DescriptorPoolConfig() = default; + + /** + * @brief counts the sets and descriptors of the layouts + * @param flags update after bind layouts add VK_DESCRIPTOR_POOL_CREATE_UPDATE_AFTER_BIND_BIT + * @details calls @ref add() per entry + */ + constexpr DescriptorPoolConfig(std::initializer_list sets, VkDescriptorPoolCreateFlags flags = 0) : + flags{flags} { + for(auto const& [layout, count, variableCount] : sets) + add(*layout, count, variableCount); + } + /** * @pre > 0 */ diff --git a/cvk/tests/src/data/descriptor/descriptor_draw_test.cpp b/cvk/tests/src/data/descriptor/descriptor_draw_test.cpp index 5a3163e9..1552a45e 100644 --- a/cvk/tests/src/data/descriptor/descriptor_draw_test.cpp +++ b/cvk/tests/src/data/descriptor/descriptor_draw_test.cpp @@ -181,7 +181,7 @@ CVK_TEST(DescriptorDraw, uniform_buffer_color) { }; color.write(std::array, 1>{GREEN}); - DescriptorPool const pool{context.core, DescriptorPool::Config{}.add(setLayout), create}; + DescriptorPool const pool{context.core, DescriptorPool::Config{{&setLayout}}, create}; DescriptorSet const set{pool, setLayout, create}; set.write(std::array{DescriptorWrite{.binding = 0, .buffers = {{&color}}}}); @@ -225,7 +225,7 @@ CVK_TEST(DescriptorDraw, bindless_texture_written_after_bind) { Buffer staging{context.core, host_written(VK_BUFFER_USAGE_TRANSFER_SRC_BIT), 4}; staging.write(GREEN_PIXEL); - DescriptorPool const pool{context.core, DescriptorPool::Config{}.add(setLayout, 1, 32), create}; + DescriptorPool const pool{context.core, {{&setLayout, 1, 32}}, create}; DescriptorSet const set{pool, setLayout, 32, create}; CmdPool const cmdPool{context.core, CmdPool::Config::Default(context.core.queue(0)), create}; diff --git a/cvk/tests/src/data/descriptor/descriptor_pool_test.cpp b/cvk/tests/src/data/descriptor/descriptor_pool_test.cpp index e5504112..f8d3de71 100644 --- a/cvk/tests/src/data/descriptor/descriptor_pool_test.cpp +++ b/cvk/tests/src/data/descriptor/descriptor_pool_test.cpp @@ -64,11 +64,28 @@ CVK_TEST(DescriptorPool, config_add_takes_the_variable_count_and_update_after_bi EXPECT_NE(config.flags & VK_DESCRIPTOR_POOL_CREATE_UPDATE_AFTER_BIND_BIT, 0); } +CVK_TEST(DescriptorPool, constructor_counts_every_layout) { + test::Context const context{}; + DescriptorSetLayout const layout{context.core, buffers_and_texture(), create}; + DescriptorSetLayout const textures{context.core, bindless(), create}; + + DescriptorPool::Config const config{ + {{&layout, 2}, {&textures, 1, 100}}, + VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT + }; + + EXPECT_EQ(config.maxSets, 3); + EXPECT_EQ(count_of(config, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER), 4); + EXPECT_EQ(count_of(config, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER), 102); + EXPECT_NE(config.flags & VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT, 0); + EXPECT_NE(config.flags & VK_DESCRIPTOR_POOL_CREATE_UPDATE_AFTER_BIND_BIT, 0); +} + CVK_TEST(DescriptorPool, create_and_destroy) { test::Context const context{}; DescriptorSetLayout const layout{context.core, buffers_and_texture(), create}; - DescriptorPool pool{context.core, DescriptorPool::Config{}.add(layout, 4), create}; + DescriptorPool pool{context.core, DescriptorPool::Config{{&layout, 4}}, create}; ASSERT_TRUE(pool.created()); EXPECT_TRUE(pool.unused()); @@ -81,7 +98,7 @@ CVK_TEST(DescriptorPool, create_and_destroy) { CVK_TEST(DescriptorPool, reset_counts) { test::Context const context{}; DescriptorSetLayout const layout{context.core, buffers_and_texture(), create}; - DescriptorPool const pool{context.core, DescriptorPool::Config{}.add(layout), create}; + DescriptorPool const pool{context.core, DescriptorPool::Config{{&layout}}, create}; pool.reset(); pool.reset(); @@ -92,7 +109,7 @@ CVK_TEST(DescriptorPool, reset_counts) { CVK_TEST(DescriptorPool, release_and_wrap) { test::Context const context{}; DescriptorSetLayout const layout{context.core, buffers_and_texture(), create}; - auto const config = DescriptorPool::Config{}.add(layout); + auto const config = DescriptorPool::Config{{&layout}}; DescriptorPool pool{context.core, config, create}; auto const vkPool = pool.get(); diff --git a/cvk/tests/src/data/descriptor/descriptor_set_test.cpp b/cvk/tests/src/data/descriptor/descriptor_set_test.cpp index ddd93669..d82393d4 100644 --- a/cvk/tests/src/data/descriptor/descriptor_set_test.cpp +++ b/cvk/tests/src/data/descriptor/descriptor_set_test.cpp @@ -43,7 +43,7 @@ namespace { CVK_TEST(DescriptorSet, base_constructor_does_not_create) { test::Context const context{}; DescriptorSetLayout const layout{context.core, buffer_and_texture(), create}; - DescriptorPool const pool{context.core, DescriptorPool::Config{}.add(layout), create}; + DescriptorPool const pool{context.core, DescriptorPool::Config{{&layout}}, create}; DescriptorSet const set{pool, layout}; @@ -58,7 +58,7 @@ CVK_TEST(DescriptorSet, destroy_frees_with_a_freeing_pool) { DescriptorSetLayout const layout{context.core, buffer_and_texture(), create}; DescriptorPool const pool{ context.core, - DescriptorPool::Config{.flags = VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT}.add(layout), + DescriptorPool::Config{{{&layout}}, VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT}, create }; @@ -78,7 +78,7 @@ CVK_TEST(DescriptorSet, destroy_frees_with_a_freeing_pool) { CVK_TEST(DescriptorSet, destroy_without_free_only_forgets) { test::Context const context{}; DescriptorSetLayout const layout{context.core, buffer_and_texture(), create}; - DescriptorPool const pool{context.core, DescriptorPool::Config{}.add(layout), create}; + DescriptorPool const pool{context.core, DescriptorPool::Config{{&layout}}, create}; DescriptorSet set{pool, layout, create}; set.destroy(); @@ -91,7 +91,7 @@ CVK_TEST(DescriptorSet, destroy_without_free_only_forgets) { CVK_TEST(DescriptorSet, pool_reset_frees_the_set) { test::Context const context{}; DescriptorSetLayout const layout{context.core, buffer_and_texture(), create}; - DescriptorPool const pool{context.core, DescriptorPool::Config{}.add(layout), create}; + DescriptorPool const pool{context.core, DescriptorPool::Config{{&layout}}, create}; DescriptorSet set{pool, layout, create}; pool.reset(); @@ -106,7 +106,7 @@ CVK_TEST(DescriptorSet, pool_reset_frees_the_set) { CVK_TEST(DescriptorSet, write_buffer_and_image) { test::Context const context{}; DescriptorSetLayout const layout{context.core, buffer_and_texture(), create}; - DescriptorPool const pool{context.core, DescriptorPool::Config{}.add(layout), create}; + DescriptorPool const pool{context.core, DescriptorPool::Config{{&layout}}, create}; DescriptorSet const set{pool, layout, create}; BaseBuffer const buffer{context.core, {.usage = VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT}, 64}; @@ -125,7 +125,7 @@ CVK_TEST(DescriptorSet, write_buffer_and_image) { CVK_TEST(DescriptorSet, variable_count) { test::Context const context{}; DescriptorSetLayout const layout{context.core, bindless(), create}; - DescriptorPool const pool{context.core, DescriptorPool::Config{}.add(layout, 1, 8), create}; + DescriptorPool const pool{context.core, DescriptorPool::Config{{&layout, 1, 8}}, create}; DescriptorSet const set{pool, layout, 8, create}; Image const image{context.core, TEXTURE_CONFIG, VkExtent2D{4, 4}}; @@ -149,7 +149,7 @@ CVK_TEST(DescriptorSet, release_and_wrap) { DescriptorSetLayout const layout{context.core, buffer_and_texture(), create}; DescriptorPool const pool{ context.core, - DescriptorPool::Config{.flags = VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT}.add(layout), + DescriptorPool::Config{{{&layout}}, VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT}, create }; DescriptorSet set{pool, layout, create}; From acde8132f8f7d1ac3cbff059782033b9c750b567 Mon Sep 17 00:00:00 2001 From: lukas <89145039+codethinki@users.noreply.github.com> Date: Mon, 19 Oct 2026 00:19:31 +0200 Subject: [PATCH 063/135] cvk: not_null_ptr, config constructors instead of the Default factories - cvk::not_null_ptr aliases cth::not_null_ptr (cth#17) and replaces cvk::not_null: not_null -> not_null_ptr - CmdPoolConfig(Queue const&, flags) and CmdPoolConfig(queueFamilyIndex, flags) replace CmdPoolConfig::Default(queue) - PipelineConfig(colorFormats, depthFormat) replaces PipelineConfig::Default() - lib/cth to e6a384d --- cvk/incl/cvk/base/allocator.hpp | 2 +- cvk/incl/cvk/base/core.hpp | 2 +- cvk/incl/cvk/base/device.hpp | 4 +-- cvk/incl/cvk/base/device_table.hpp | 2 +- cvk/incl/cvk/base/physical_device.hpp | 2 +- cvk/incl/cvk/base/queue/queue_state.hpp | 2 +- cvk/incl/cvk/base/queue/submit_info.hpp | 4 +-- .../descriptor/descriptor_pool_config.hpp | 2 +- .../cvk/data/descriptor/descriptor_set.hpp | 4 +-- .../cvk/data/descriptor/descriptor_write.hpp | 2 +- cvk/incl/cvk/data/img/image_view.hpp | 2 +- cvk/incl/cvk/render/cmd/cmd_buffer.hpp | 4 +-- cvk/incl/cvk/render/cmd/cmd_pool_config.hpp | 26 ++++++++++++------- cvk/incl/cvk/render/cmd/rendering_info.hpp | 2 +- .../pipeline/layout/pipeline_layout.hpp | 2 +- cvk/incl/cvk/render/pipeline/pipeline.hpp | 2 +- .../cvk/render/pipeline/pipeline_config.hpp | 18 ++++++------- cvk/incl/cvk/render/sync/barrier.hpp | 4 +-- cvk/incl/cvk/surface/swapchain.hpp | 4 +-- cvk/incl/cvk/utility/core_handle.hpp | 2 +- cvk/incl/cvk/utility/types.hpp | 2 +- cvk/src/base/queue/queue.cpp | 2 +- cvk/src/render/sync/barrier.cpp | 2 +- cvk/tests/src/base/queue/submit_test.cpp | 6 ++--- cvk/tests/src/context.hpp | 2 +- .../data/descriptor/descriptor_draw_test.cpp | 4 +-- cvk/tests/src/render/cmd/cmd_buffer_test.cpp | 2 +- cvk/tests/src/render/cmd/cmd_pool_test.cpp | 8 +++--- cvk/tests/src/render/cmd/draw_test.cpp | 2 +- cvk/tests/src/render/cmd/rendering_test.cpp | 2 +- .../pipeline/layout/pipeline_layout_test.cpp | 2 +- .../src/render/pipeline/pipeline_test.cpp | 10 +++---- cvk/tests/src/surface/swapchain_test.cpp | 2 +- lib/cth | 2 +- 34 files changed, 74 insertions(+), 66 deletions(-) diff --git a/cvk/incl/cvk/base/allocator.hpp b/cvk/incl/cvk/base/allocator.hpp index 43268156..a438261e 100644 --- a/cvk/incl/cvk/base/allocator.hpp +++ b/cvk/incl/cvk/base/allocator.hpp @@ -103,7 +103,7 @@ class Allocator { private: void forget(); - not_null _device; + not_null_ptr _device; unique_handle> _handle; public: diff --git a/cvk/incl/cvk/base/core.hpp b/cvk/incl/cvk/base/core.hpp index 46db3d6d..a1782528 100644 --- a/cvk/incl/cvk/base/core.hpp +++ b/cvk/incl/cvk/base/core.hpp @@ -173,7 +173,7 @@ class Core { void createQueues(std::span queuesToUniqueQueues); - not_null _instance; + not_null_ptr _instance; std::unique_ptr _physicalDevice; // declared before the device: move assignment destroys the old allocator before the old device diff --git a/cvk/incl/cvk/base/device.hpp b/cvk/incl/cvk/base/device.hpp index 8361f7a7..f761d6ac 100644 --- a/cvk/incl/cvk/base/device.hpp +++ b/cvk/incl/cvk/base/device.hpp @@ -140,8 +140,8 @@ class Device { void createQueues(std::span familyIndices); - not_null _instance; - not_null _physicalDevice; + not_null_ptr _instance; + not_null_ptr _physicalDevice; unique_handle> _handle; std::unique_ptr _functionTable = std::make_unique(); diff --git a/cvk/incl/cvk/base/device_table.hpp b/cvk/incl/cvk/base/device_table.hpp index 92275817..1a54f665 100644 --- a/cvk/incl/cvk/base/device_table.hpp +++ b/cvk/incl/cvk/base/device_table.hpp @@ -6,7 +6,7 @@ namespace cvk { struct DeviceTable { vk_not_null vkDevice; - not_null table; + not_null_ptr table; VolkDeviceTable const* operator ->() const { return table.get(); } [[nodiscard]] constexpr VkDevice device() const { return vkDevice.get(); } diff --git a/cvk/incl/cvk/base/physical_device.hpp b/cvk/incl/cvk/base/physical_device.hpp index e50a13be..54b732f3 100644 --- a/cvk/incl/cvk/base/physical_device.hpp +++ b/cvk/incl/cvk/base/physical_device.hpp @@ -208,7 +208,7 @@ class PhysicalDevice { [[nodiscard]] constexpr std::span queueFamilies() const { return _queueFamilies; } private: - not_null _instance; + not_null_ptr _instance; utils::PhysicalDeviceFeatures _requiredFeatures; std::vector _requiredExtensions{}; diff --git a/cvk/incl/cvk/base/queue/queue_state.hpp b/cvk/incl/cvk/base/queue/queue_state.hpp index 1da51380..33a216e5 100644 --- a/cvk/incl/cvk/base/queue/queue_state.hpp +++ b/cvk/incl/cvk/base/queue/queue_state.hpp @@ -14,7 +14,7 @@ struct QueueState { * @brief function table of the device * @details stable when the device moves */ - not_null functions; + not_null_ptr functions; vk_not_null vkQueue; /** * @brief vulkan family index diff --git a/cvk/incl/cvk/base/queue/submit_info.hpp b/cvk/incl/cvk/base/queue/submit_info.hpp index e73d4e1a..03e3752a 100644 --- a/cvk/incl/cvk/base/queue/submit_info.hpp +++ b/cvk/incl/cvk/base/queue/submit_info.hpp @@ -38,7 +38,7 @@ struct SemaphoreSubmitInfo { std::optional value = std::nullopt ) : semaphore{&semaphore}, stages{stages}, value{value} {} - std::variant, not_null> semaphore; + std::variant, not_null_ptr> semaphore; VkPipelineStageFlags2 stages; /** @@ -55,7 +55,7 @@ struct SubmitInfo { /** * @brief the gpu executes them in order */ - std::vector> cmdBuffers{}; + std::vector> cmdBuffers{}; std::vector waits{}; std::vector signals{}; diff --git a/cvk/incl/cvk/data/descriptor/descriptor_pool_config.hpp b/cvk/incl/cvk/data/descriptor/descriptor_pool_config.hpp index f4f220d1..40907289 100644 --- a/cvk/incl/cvk/data/descriptor/descriptor_pool_config.hpp +++ b/cvk/incl/cvk/data/descriptor/descriptor_pool_config.hpp @@ -20,7 +20,7 @@ struct DescriptorPoolConfig { * @brief count sets of one layout */ struct Sets { - not_null layout; + not_null_ptr layout; uint32_t count = 1; /** diff --git a/cvk/incl/cvk/data/descriptor/descriptor_set.hpp b/cvk/incl/cvk/data/descriptor/descriptor_set.hpp index ad61eb35..af9f43c1 100644 --- a/cvk/incl/cvk/data/descriptor/descriptor_set.hpp +++ b/cvk/incl/cvk/data/descriptor/descriptor_set.hpp @@ -168,7 +168,7 @@ class DescriptorSet { [[nodiscard]] constexpr uint64_t resets() const { return _resets; } private: - not_null _pool; + not_null_ptr _pool; uint64_t _resets; }; @@ -180,7 +180,7 @@ class DescriptorSet { void forget(); unique_handle _handle; - not_null _layout; + not_null_ptr _layout; uint32_t _variableCount; public: diff --git a/cvk/incl/cvk/data/descriptor/descriptor_write.hpp b/cvk/incl/cvk/data/descriptor/descriptor_write.hpp index acdfef1f..3dad3227 100644 --- a/cvk/incl/cvk/data/descriptor/descriptor_write.hpp +++ b/cvk/incl/cvk/data/descriptor/descriptor_write.hpp @@ -44,7 +44,7 @@ struct ImageDescriptor { * @brief buffer range for a buffer descriptor, the binding decides the descriptor type */ struct BufferDescriptor { - not_null buffer; + not_null_ptr buffer; /** * @brief in bytes diff --git a/cvk/incl/cvk/data/img/image_view.hpp b/cvk/incl/cvk/data/img/image_view.hpp index b7476c4f..65fabdba 100644 --- a/cvk/incl/cvk/data/img/image_view.hpp +++ b/cvk/incl/cvk/data/img/image_view.hpp @@ -102,7 +102,7 @@ class ImageView { void forget(); core_handle _handle; - not_null _image; + not_null_ptr _image; Config _config; public: diff --git a/cvk/incl/cvk/render/cmd/cmd_buffer.hpp b/cvk/incl/cvk/render/cmd/cmd_buffer.hpp index 92561590..45021ff0 100644 --- a/cvk/incl/cvk/render/cmd/cmd_buffer.hpp +++ b/cvk/incl/cvk/render/cmd/cmd_buffer.hpp @@ -49,7 +49,7 @@ class BaseCmdBuffer { /** * @brief images of the attachments, empty if inherited */ - std::vector> attachments{}; + std::vector> attachments{}; /** * @brief true -> a secondary that inherits the rendering of its primary @@ -321,7 +321,7 @@ class BaseCmdBuffer { [[nodiscard]] constexpr Core const& core() const { return _pool->core(); } private: - not_null _pool; + not_null_ptr _pool; }; void forget(); diff --git a/cvk/incl/cvk/render/cmd/cmd_pool_config.hpp b/cvk/incl/cvk/render/cmd/cmd_pool_config.hpp index 82425493..ede0848b 100644 --- a/cvk/incl/cvk/render/cmd/cmd_pool_config.hpp +++ b/cvk/incl/cvk/render/cmd/cmd_pool_config.hpp @@ -10,23 +10,31 @@ namespace cvk { * @brief queue family and flags of a command pool */ struct CmdPoolConfig { + static constexpr VkCommandPoolCreateFlags DEFAULT_FLAGS = VK_COMMAND_POOL_CREATE_TRANSIENT_BIT + | VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT; + + constexpr CmdPoolConfig() = default; + + explicit constexpr CmdPoolConfig( + queue_family_index_t queueFamilyIndex, + VkCommandPoolCreateFlags flags = DEFAULT_FLAGS + ) : queueFamilyIndex{queueFamilyIndex}, flags{flags} {} + /** - * @brief the command buffers of the pool are submitted to queues of this family + * @brief config for the family of the queue */ - queue_family_index_t queueFamilyIndex = 0; + explicit constexpr CmdPoolConfig(Queue const& queue, VkCommandPoolCreateFlags flags = DEFAULT_FLAGS) : + CmdPoolConfig{queue.familyIndex(), flags} {} /** - * @brief VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT -> @ref BaseCmdBuffer::reset() is allowed + * @brief the command buffers of the pool are submitted to queues of this family */ - VkCommandPoolCreateFlags flags = VK_COMMAND_POOL_CREATE_TRANSIENT_BIT - | VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT; + queue_family_index_t queueFamilyIndex = 0; /** - * @brief config for the family of the queue, default flags + * @brief VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT -> @ref BaseCmdBuffer::reset() is allowed */ - [[nodiscard]] static constexpr CmdPoolConfig Default(Queue const& queue) { - return CmdPoolConfig{.queueFamilyIndex = queue.familyIndex()}; - } + VkCommandPoolCreateFlags flags = DEFAULT_FLAGS; [[nodiscard]] constexpr VkCommandPoolCreateInfo createInfo() const { return VkCommandPoolCreateInfo{ diff --git a/cvk/incl/cvk/render/cmd/rendering_info.hpp b/cvk/incl/cvk/render/cmd/rendering_info.hpp index 71862b52..bff500e9 100644 --- a/cvk/incl/cvk/render/cmd/rendering_info.hpp +++ b/cvk/incl/cvk/render/cmd/rendering_info.hpp @@ -46,7 +46,7 @@ struct RenderingAttachment { /** * @pre one mip level */ - not_null view; + not_null_ptr view; /** * @brief layout of the view during the rendering, VK_IMAGE_LAYOUT_ATTACHMENT_OPTIMAL fits every aspect diff --git a/cvk/incl/cvk/render/pipeline/layout/pipeline_layout.hpp b/cvk/incl/cvk/render/pipeline/layout/pipeline_layout.hpp index 9515a8f9..d9a34aba 100644 --- a/cvk/incl/cvk/render/pipeline/layout/pipeline_layout.hpp +++ b/cvk/incl/cvk/render/pipeline/layout/pipeline_layout.hpp @@ -151,7 +151,7 @@ struct PipelineLayout::Config { /** * @brief set layout per set number, index == set number */ - std::vector> setLayouts{}; + std::vector> setLayouts{}; std::vector pushConstantRanges{}; }; diff --git a/cvk/incl/cvk/render/pipeline/pipeline.hpp b/cvk/incl/cvk/render/pipeline/pipeline.hpp index 1811c56f..36da3b5c 100644 --- a/cvk/incl/cvk/render/pipeline/pipeline.hpp +++ b/cvk/incl/cvk/render/pipeline/pipeline.hpp @@ -129,7 +129,7 @@ class Pipeline { void forget(); core_handle _handle; - not_null _layout; + not_null_ptr _layout; VkPipeline _vkParent = VK_NULL_HANDLE; Config _config; diff --git a/cvk/incl/cvk/render/pipeline/pipeline_config.hpp b/cvk/incl/cvk/render/pipeline/pipeline_config.hpp index 063f1faf..52dcbeab 100644 --- a/cvk/incl/cvk/render/pipeline/pipeline_config.hpp +++ b/cvk/incl/cvk/render/pipeline/pipeline_config.hpp @@ -3,6 +3,7 @@ #include +#include #include #include @@ -30,11 +31,13 @@ struct PipelineConfig { | VK_COLOR_COMPONENT_A_BIT, }; + constexpr PipelineConfig() = default; + /** - * @brief config with one @ref ALPHA_BLENDING attachment per color format + * @brief one @ref ALPHA_BLENDING attachment per color format * @param depthFormat VK_FORMAT_UNDEFINED -> no depth attachment */ - [[nodiscard]] static constexpr PipelineConfig Default( + explicit constexpr PipelineConfig( std::span colorFormats, VkFormat depthFormat = VK_FORMAT_UNDEFINED ); @@ -150,13 +153,10 @@ struct PipelineConfig { } namespace cvk { -constexpr PipelineConfig PipelineConfig::Default(std::span colorFormats, VkFormat depthFormat) { - PipelineConfig config{}; - config.colorFormats.assign_range(colorFormats); - config.colorBlendAttachments.assign(colorFormats.size(), ALPHA_BLENDING); - config.depthFormat = depthFormat; - return config; -} +constexpr PipelineConfig::PipelineConfig(std::span colorFormats, VkFormat depthFormat) : + colorBlendAttachments(colorFormats.size(), ALPHA_BLENDING), + colorFormats{std::from_range, colorFormats}, + depthFormat{depthFormat} {} constexpr PipelineConfig& PipelineConfig::removeStage(VkShaderStageFlagBits stage) { auto const removed = std::erase_if( diff --git a/cvk/incl/cvk/render/sync/barrier.hpp b/cvk/incl/cvk/render/sync/barrier.hpp index 1a537607..bd8fdab8 100644 --- a/cvk/incl/cvk/render/sync/barrier.hpp +++ b/cvk/incl/cvk/render/sync/barrier.hpp @@ -155,12 +155,12 @@ struct Barrier { using BufferInfo = BufferBarrierInfo; struct BufferEntry { - not_null buffer; + not_null_ptr buffer; BufferInfo info; }; struct ImageEntry { - not_null image; + not_null_ptr image; ImageInfo info; }; diff --git a/cvk/incl/cvk/surface/swapchain.hpp b/cvk/incl/cvk/surface/swapchain.hpp index 71d25315..4c8486d4 100644 --- a/cvk/incl/cvk/surface/swapchain.hpp +++ b/cvk/incl/cvk/surface/swapchain.hpp @@ -253,8 +253,8 @@ class Swapchain { void forget(); - not_null _presentQueue; - not_null _surface; + not_null_ptr _presentQueue; + not_null_ptr _surface; Config _config; core_handle _handle; diff --git a/cvk/incl/cvk/utility/core_handle.hpp b/cvk/incl/cvk/utility/core_handle.hpp index 0e95ca86..ee9176ad 100644 --- a/cvk/incl/cvk/utility/core_handle.hpp +++ b/cvk/incl/cvk/utility/core_handle.hpp @@ -21,7 +21,7 @@ class core_deleter { [[nodiscard]] constexpr Core const& core() const { return *_core; } private: - not_null _core; + not_null_ptr _core; }; /** diff --git a/cvk/incl/cvk/utility/types.hpp b/cvk/incl/cvk/utility/types.hpp index e1f3aa87..b94d0c0f 100644 --- a/cvk/incl/cvk/utility/types.hpp +++ b/cvk/incl/cvk/utility/types.hpp @@ -8,7 +8,7 @@ namespace cvk { template using vk_not_null = cth::not_null; template -using not_null = cth::not_null; +using not_null_ptr = cth::not_null_ptr; template using move_ptr = cth::move_ptr; diff --git a/cvk/src/base/queue/queue.cpp b/cvk/src/base/queue/queue.cpp index f2b0d389..ed50dc60 100644 --- a/cvk/src/base/queue/queue.cpp +++ b/cvk/src/base/queue/queue.cpp @@ -32,7 +32,7 @@ namespace { template VkSemaphoreSubmitInfo semaphore_info(SemaphoreSubmitInfo const& info, Resolve resolve) { return std::visit( - [&](not_null semaphore) { + [&](not_null_ptr semaphore) { T::debug_check(*semaphore); if constexpr(std::same_as) { diff --git a/cvk/src/render/sync/barrier.cpp b/cvk/src/render/sync/barrier.cpp index 1fa4f6fc..dcb48ff6 100644 --- a/cvk/src/render/sync/barrier.cpp +++ b/cvk/src/render/sync/barrier.cpp @@ -147,7 +147,7 @@ void Barrier::debug_check_rendering( bool const attachment = std::ranges::contains( rendering.attachments, entry.image.get(), - ¬_null::get + ¬_null_ptr::get ); CTH_CRITICAL( attachment && barrier.newLayout != VK_IMAGE_LAYOUT_RENDERING_LOCAL_READ_KHR diff --git a/cvk/tests/src/base/queue/submit_test.cpp b/cvk/tests/src/base/queue/submit_test.cpp index 89bac9d5..23bb54d0 100644 --- a/cvk/tests/src/base/queue/submit_test.cpp +++ b/cvk/tests/src/base/queue/submit_test.cpp @@ -21,18 +21,18 @@ CVK_TEST(Submit, semaphore_info_takes_the_kind_from_the_semaphore) { SemaphoreSubmitInfo const binaryInfo{binary}; SemaphoreSubmitInfo const timelineInfo{timeline, VK_PIPELINE_STAGE_2_TRANSFER_BIT, 3}; - EXPECT_TRUE(std::holds_alternative>(binaryInfo.semaphore)); + EXPECT_TRUE(std::holds_alternative>(binaryInfo.semaphore)); EXPECT_EQ(binaryInfo.stages, VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT); EXPECT_FALSE(binaryInfo.value.has_value()); - EXPECT_TRUE(std::holds_alternative>(timelineInfo.semaphore)); + EXPECT_TRUE(std::holds_alternative>(timelineInfo.semaphore)); EXPECT_EQ(timelineInfo.stages, VK_PIPELINE_STAGE_2_TRANSFER_BIT); EXPECT_EQ(timelineInfo.value, 3); } CVK_TEST(Submit, command_buffer_without_fence) { test::Context const context{}; - CmdPool const pool{context.core, CmdPool::Config::Default(context.core.queue(0)), create}; + CmdPool const pool{context.core, CmdPool::Config{context.core.queue(0)}, create}; PrimaryCmdBuffer primary{pool, create}; primary.begin(VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT); primary.end(); diff --git a/cvk/tests/src/context.hpp b/cvk/tests/src/context.hpp index 2ab6576e..00509d8f 100644 --- a/cvk/tests/src/context.hpp +++ b/cvk/tests/src/context.hpp @@ -98,7 +98,7 @@ void copy_to_buffer( */ template void record_and_submit(Context const& context, Record&& record) { - CmdPool const pool{context.core, CmdPool::Config::Default(context.core.queue(0)), create}; + CmdPool const pool{context.core, CmdPool::Config{context.core.queue(0)}, create}; PrimaryCmdBuffer primary{pool, create}; primary.begin(VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT); diff --git a/cvk/tests/src/data/descriptor/descriptor_draw_test.cpp b/cvk/tests/src/data/descriptor/descriptor_draw_test.cpp index 1552a45e..15c43056 100644 --- a/cvk/tests/src/data/descriptor/descriptor_draw_test.cpp +++ b/cvk/tests/src/data/descriptor/descriptor_draw_test.cpp @@ -63,7 +63,7 @@ namespace { } [[nodiscard]] Pipeline::Config config() const { - auto config = Pipeline::Config::Default(COLOR_FORMATS); + Pipeline::Config config{COLOR_FORMATS}; config.addStage(vertex).addStage(fragment); config.vertexBindings = {{.binding = 0, .stride = sizeof(Vec3), .inputRate = VK_VERTEX_INPUT_RATE_VERTEX}}; config.vertexAttributes = { @@ -228,7 +228,7 @@ CVK_TEST(DescriptorDraw, bindless_texture_written_after_bind) { DescriptorPool const pool{context.core, {{&setLayout, 1, 32}}, create}; DescriptorSet const set{pool, setLayout, 32, create}; - CmdPool const cmdPool{context.core, CmdPool::Config::Default(context.core.queue(0)), create}; + CmdPool const cmdPool{context.core, CmdPool::Config{context.core.queue(0)}, create}; PrimaryCmdBuffer cmd{cmdPool, create}; cmd.begin(VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT); diff --git a/cvk/tests/src/render/cmd/cmd_buffer_test.cpp b/cvk/tests/src/render/cmd/cmd_buffer_test.cpp index 19b38bec..4ec40b6c 100644 --- a/cvk/tests/src/render/cmd/cmd_buffer_test.cpp +++ b/cvk/tests/src/render/cmd/cmd_buffer_test.cpp @@ -10,7 +10,7 @@ namespace cvk { namespace { CmdPool::Config pool_config(test::Context const& context) { - return CmdPool::Config::Default(context.core.queue(0)); + return CmdPool::Config{context.core.queue(0)}; } } diff --git a/cvk/tests/src/render/cmd/cmd_pool_test.cpp b/cvk/tests/src/render/cmd/cmd_pool_test.cpp index 4f894972..6da85720 100644 --- a/cvk/tests/src/render/cmd/cmd_pool_test.cpp +++ b/cvk/tests/src/render/cmd/cmd_pool_test.cpp @@ -8,23 +8,23 @@ namespace cvk { namespace { CmdPool::Config pool_config(test::Context const& context) { - return CmdPool::Config::Default(context.core.queue(0)); + return CmdPool::Config{context.core.queue(0)}; } } CVK_TEST(CmdPool, config_create_info) { - constexpr CmdPool::Config config{.queueFamilyIndex = 2, .flags = VK_COMMAND_POOL_CREATE_TRANSIENT_BIT}; + constexpr CmdPool::Config config{2, VK_COMMAND_POOL_CREATE_TRANSIENT_BIT}; constexpr auto info = config.createInfo(); static_assert(info.queueFamilyIndex == 2); static_assert(info.flags == VK_COMMAND_POOL_CREATE_TRANSIENT_BIT); } -CVK_TEST(CmdPool, config_default_uses_the_queue_family) { +CVK_TEST(CmdPool, config_from_the_queue_uses_its_family) { test::Context const context{}; auto const& queue = context.core.queue(0); - auto const config = CmdPool::Config::Default(queue); + CmdPool::Config const config{queue}; EXPECT_EQ(config.queueFamilyIndex, queue.familyIndex()); EXPECT_NE(config.flags & VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT, 0u); diff --git a/cvk/tests/src/render/cmd/draw_test.cpp b/cvk/tests/src/render/cmd/draw_test.cpp index 57e84c96..f96187fd 100644 --- a/cvk/tests/src/render/cmd/draw_test.cpp +++ b/cvk/tests/src/render/cmd/draw_test.cpp @@ -59,7 +59,7 @@ namespace { } [[nodiscard]] Pipeline::Config config() const { - auto config = Pipeline::Config::Default(COLOR_FORMATS); + Pipeline::Config config{COLOR_FORMATS}; config.addStage(vertex).addStage(fragment); config.vertexBindings = {{.binding = 0, .stride = sizeof(Vec3), .inputRate = VK_VERTEX_INPUT_RATE_VERTEX}}; config.vertexAttributes = { diff --git a/cvk/tests/src/render/cmd/rendering_test.cpp b/cvk/tests/src/render/cmd/rendering_test.cpp index f1c88602..732fbcfa 100644 --- a/cvk/tests/src/render/cmd/rendering_test.cpp +++ b/cvk/tests/src/render/cmd/rendering_test.cpp @@ -141,7 +141,7 @@ CVK_TEST(Rendering, secondary_inherits_the_rendering) { test::Context const context{}; Image const image{context.core, COLOR_CONFIG, EXTENT}; ImageView const view{image, {}, create}; - CmdPool const pool{context.core, CmdPool::Config::Default(context.core.queue(0)), create}; + CmdPool const pool{context.core, CmdPool::Config{context.core.queue(0)}, create}; std::array secondaries{SecondaryCmdBuffer{pool, create}}; test::record_and_submit(context, [&](PrimaryCmdBuffer& cmd) { diff --git a/cvk/tests/src/render/pipeline/layout/pipeline_layout_test.cpp b/cvk/tests/src/render/pipeline/layout/pipeline_layout_test.cpp index 63791f73..8327622a 100644 --- a/cvk/tests/src/render/pipeline/layout/pipeline_layout_test.cpp +++ b/cvk/tests/src/render/pipeline/layout/pipeline_layout_test.cpp @@ -44,7 +44,7 @@ CVK_TEST(PipelineLayout, too_many_sets_throws) { auto const maxSets = context.core.physicalDevice().limits().maxBoundDescriptorSets; PipelineLayout::Config const config{ - .setLayouts = std::vector>(maxSets + 1, &set), + .setLayouts = std::vector>(maxSets + 1, &set), }; auto const construct = [&] { PipelineLayout const layout{context.core, config}; }; diff --git a/cvk/tests/src/render/pipeline/pipeline_test.cpp b/cvk/tests/src/render/pipeline/pipeline_test.cpp index 7035dad1..5f800da2 100644 --- a/cvk/tests/src/render/pipeline/pipeline_test.cpp +++ b/cvk/tests/src/render/pipeline/pipeline_test.cpp @@ -34,7 +34,7 @@ namespace { fragment{core, VK_SHADER_STAGE_FRAGMENT_BIT, test::shaders::TRIANGLE_FRAG} {} [[nodiscard]] Pipeline::Config config() const { - auto config = Pipeline::Config::Default(COLOR_FORMATS, DEPTH_FORMAT); + Pipeline::Config config{COLOR_FORMATS, DEPTH_FORMAT}; config.addStage(vertex).addStage(fragment); config.vertexBindings = { {.binding = 0, .stride = 3 * sizeof(float), .inputRate = VK_VERTEX_INPUT_RATE_VERTEX}, @@ -58,8 +58,8 @@ namespace { }; } -CVK_TEST(Pipeline, config_default) { - auto const config = Pipeline::Config::Default(COLOR_FORMATS, DEPTH_FORMAT); +CVK_TEST(Pipeline, config_from_the_formats) { + Pipeline::Config const config{COLOR_FORMATS, DEPTH_FORMAT}; EXPECT_TRUE(std::ranges::equal(config.colorFormats, COLOR_FORMATS)); ASSERT_EQ(config.colorBlendAttachments.size(), 1); @@ -69,7 +69,7 @@ CVK_TEST(Pipeline, config_default) { CVK_TEST(Pipeline, config_is_constexpr) { static_assert([] { - auto config = Pipeline::Config::Default(COLOR_FORMATS, DEPTH_FORMAT); + Pipeline::Config config{COLOR_FORMATS, DEPTH_FORMAT}; config.stages.push_back( VkPipelineShaderStageCreateInfo{ .sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO, @@ -103,7 +103,7 @@ CVK_TEST(Pipeline, bind) { test::Context const context{}; Triangle const triangle{context.core}; Pipeline const pipeline{context.core, triangle.layout, triangle.config(), create}; - CmdPool const pool{context.core, CmdPool::Config::Default(context.core.queue(0)), create}; + CmdPool const pool{context.core, CmdPool::Config{context.core.queue(0)}, create}; PrimaryCmdBuffer primary{pool, create}; primary.begin(VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT); diff --git a/cvk/tests/src/surface/swapchain_test.cpp b/cvk/tests/src/surface/swapchain_test.cpp index 2d015642..2da1c5d9 100644 --- a/cvk/tests/src/surface/swapchain_test.cpp +++ b/cvk/tests/src/surface/swapchain_test.cpp @@ -51,7 +51,7 @@ namespace { if(!index.has_value()) return result; auto const& image = swapchain.images()[*index]; - CmdPool const pool{context.core, CmdPool::Config::Default(context.queue()), create}; + CmdPool const pool{context.core, CmdPool::Config{context.queue()}, create}; PrimaryCmdBuffer cmd{pool, create}; cmd.begin(VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT); diff --git a/lib/cth b/lib/cth index 9fb6cb68..e6a384d7 160000 --- a/lib/cth +++ b/lib/cth @@ -1 +1 @@ -Subproject commit 9fb6cb68a660be25645316114a2a5e99313faf97 +Subproject commit e6a384d73dd08f758b8a6446827bf1ea099dcbea From acae8fe51b4e8fc332f9862ee9dce98ce6bc8076 Mon Sep 17 00:00:00 2001 From: lukas <89145039+codethinki@users.noreply.github.com> Date: Mon, 19 Oct 2026 00:28:13 +0200 Subject: [PATCH 064/135] cvk: precompiled header for the tests - cvk_tests precompiles gtest, context.hpp and test.hpp - AGENTS: the tests have a pch too, no --clean-first after a cth update, the submodule headers are in the dependencies of the pch --- AGENTS.md | 3 +-- cvk/tests/CMakeLists.txt | 6 ++++++ cvk/tests/src/pch/glob/gtest.hpp | 2 ++ cvk/tests/src/pch/pch.hpp | 5 +++++ 4 files changed, 14 insertions(+), 2 deletions(-) create mode 100644 cvk/tests/src/pch/glob/gtest.hpp create mode 100644 cvk/tests/src/pch/pch.hpp diff --git a/AGENTS.md b/AGENTS.md index 9ca3d6e8..a3efb34b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -46,7 +46,6 @@ ctest --test-dir out/build/gcc_release --output-on-failure - cth comes from the submodule `lib/cth`: clone with `--recursive` or run `git submodule update --init`. - Debug builds enable the validation layers with synchronization validation (`Instance::VALIDATION_LAYER_SETTINGS`). Run the tests in a debug build before you report a change as done. - Tests that need more queues than the device has skip. Mesa Intel devices get more queues with `ANV_QUEUE_OVERRIDE=gc=2,c=1`. -- After a new cth version in `lib/cth`, build with `--clean-first`. The precompiled header does not see the changed cth headers. - Tests that need a device use `test::Context` (`cvk/tests/src/context.hpp`). It fails the test on validation errors, leaked objects included. - `VCPKG_ROOT` must point at vcpkg. @@ -120,7 +119,7 @@ Separate the groups with one empty line. Forward declare a class when a header o - Headers go to `incl///.hpp`, sources to `src//.cpp`. The folders match. - Config structs that are large get their own `_config.hpp`. -- Every library has a precompiled header in `src/pch/`. +- Every library and its tests have a precompiled header in `src/pch/`: `pch.hpp` includes one `glob/.hpp` per external library. - Every header starts with `#pragma once`. ### Class layout diff --git a/cvk/tests/CMakeLists.txt b/cvk/tests/CMakeLists.txt index 7063fcdb..6ccc7793 100644 --- a/cvk/tests/CMakeLists.txt +++ b/cvk/tests/CMakeLists.txt @@ -9,6 +9,12 @@ target_include_directories( PRIVATE src ) +target_precompile_headers( + cvk_tests + PRIVATE + src/pch/pch.hpp +) + target_compile_definitions(cvk_tests PRIVATE CVK_TEST_SHADER_DIR="${CMAKE_CURRENT_SOURCE_DIR}/shaders") find_package(GTest CONFIG REQUIRED) diff --git a/cvk/tests/src/pch/glob/gtest.hpp b/cvk/tests/src/pch/glob/gtest.hpp new file mode 100644 index 00000000..e469bb31 --- /dev/null +++ b/cvk/tests/src/pch/glob/gtest.hpp @@ -0,0 +1,2 @@ +#pragma once +#include diff --git a/cvk/tests/src/pch/pch.hpp b/cvk/tests/src/pch/pch.hpp new file mode 100644 index 00000000..d97ada7c --- /dev/null +++ b/cvk/tests/src/pch/pch.hpp @@ -0,0 +1,5 @@ +#pragma once +#include "glob/gtest.hpp" + +#include "context.hpp" +#include "test.hpp" From 8b06bca1f7c0dab54b58a28b4215612ed93b6cb8 Mon Sep 17 00:00:00 2001 From: lukas <89145039+codethinki@users.noreply.github.com> Date: Mon, 19 Oct 2026 01:04:48 +0200 Subject: [PATCH 065/135] cvk: attachment mapping for local read, binds and push constants on the command buffer - AttachmentMapping: color output locations and input attachment indices of a rendering (VK_KHR_dynamic_rendering_local_read), held by the pipeline config, the inheritance of a secondary and the rendering state of the command buffer - fix: pipelines had no mapping, so a mapping other than the default made every draw invalid - bindPipeline() sets the mapping of the pipeline inside the own rendering and checks it against the inherited mapping in a secondary - setAttachmentMapping() replaces setAttachmentLocations() and setInputAttachmentIndices(), the primary still needs it for secondaries - bindPipeline(), bindDescriptorSet() and pushConstants() move from Pipeline, DescriptorSet and PipelineLayout into the command buffer - AGENTS: commands that change the state of the command buffer are its members - tests: a draw reads an attachment of the same rendering, a secondary inherits the mapping --- AGENTS.md | 1 + .../cvk/data/descriptor/descriptor_set.hpp | 18 - cvk/incl/cvk/render/cmd/cmd_buffer.hpp | 79 ++++- cvk/incl/cvk/render/cmd/rendering_info.hpp | 112 +++++- .../pipeline/layout/pipeline_layout.hpp | 30 -- cvk/incl/cvk/render/pipeline/pipeline.hpp | 8 - .../cvk/render/pipeline/pipeline_config.hpp | 8 + cvk/src/data/descriptor/descriptor_set.cpp | 44 --- cvk/src/render/cmd/cmd_buffer.cpp | 137 +++++-- .../pipeline/layout/pipeline_layout.cpp | 22 -- cvk/src/render/pipeline/pipeline.cpp | 18 +- cvk/tests/shaders/local_read.frag | 7 + .../data/descriptor/descriptor_draw_test.cpp | 8 +- cvk/tests/src/render/cmd/draw_test.cpp | 4 +- cvk/tests/src/render/cmd/local_read_test.cpp | 333 ++++++++++++++++++ cvk/tests/src/render/cmd/rendering_test.cpp | 4 +- .../src/render/pipeline/pipeline_test.cpp | 2 +- cvk/tests/src/shaders.hpp | 21 ++ 18 files changed, 661 insertions(+), 195 deletions(-) create mode 100644 cvk/tests/shaders/local_read.frag create mode 100644 cvk/tests/src/render/cmd/local_read_test.cpp diff --git a/AGENTS.md b/AGENTS.md index a3efb34b..7a66685c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -98,6 +98,7 @@ ctest --test-dir out/build/gcc_release --output-on-failure - Use `auto const` for locals that do not change. - Initialize Vulkan structs with designated initializers. Always write `.sType` and `.pNext`. - Data structs (`SubmitInfo`, `Barrier`, the info structs) are aggregates: the caller can set everything with designated initializers. Member functions like `add()` are optional shortcuts, checks run where the data is used. +- Commands that change the state of a command buffer (binds, dynamic state, push constants, renderings, draws) are members of the command buffer: `cmd.bindPipeline(pipeline)`. Commands on the data of an object (barriers, copies, uploads) are members of the object and take the command buffer: `barrier.execute(cmd)`, `image.copy(cmd, staging)`. - Use deducing `this` (`this auto& self`) where a function must work on the derived type. - Qualify `std::` names. A `.cpp` file can declare `using std::span;` and similar at the top of its namespace. - No raw `new` and `delete`. Use `std::unique_ptr`, `cth::move_ptr` and `cth::not_null`. diff --git a/cvk/incl/cvk/data/descriptor/descriptor_set.hpp b/cvk/incl/cvk/data/descriptor/descriptor_set.hpp index af9f43c1..0b45676a 100644 --- a/cvk/incl/cvk/data/descriptor/descriptor_set.hpp +++ b/cvk/incl/cvk/data/descriptor/descriptor_set.hpp @@ -17,9 +17,7 @@ namespace cvk { -class BaseCmdBuffer; class Core; -class PipelineLayout; /** * @brief VkDescriptorSet wrapper, allocated from its pool with its layout @@ -116,22 +114,6 @@ class DescriptorSet { */ void write(std::span) const; - /** - * @brief records the bind of the set - * @param setIndex set number in the pipeline layout - * @param dynamicOffsets one per dynamic buffer descriptor of the set - * @pre @ref created() - * @pre @ref BaseCmdBuffer::recording() - * @pre the set layout of setIndex in the pipeline layout is compatible with the layout of the set - */ - void bind( - BaseCmdBuffer const&, - PipelineLayout const&, - uint32_t setIndex, - VkPipelineBindPoint bindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS, - std::span dynamicOffsets = {} - ) const; - static void destroy(DeviceTable table, VkDescriptorPool, VkDescriptorSet); /** diff --git a/cvk/incl/cvk/render/cmd/cmd_buffer.hpp b/cvk/incl/cvk/render/cmd/cmd_buffer.hpp index 45021ff0..9da5cbbc 100644 --- a/cvk/incl/cvk/render/cmd/cmd_buffer.hpp +++ b/cvk/incl/cvk/render/cmd/cmd_buffer.hpp @@ -15,12 +15,16 @@ #include #include +#include #include namespace cvk { class Core; +class DescriptorSet; class Image; +class Pipeline; +class PipelineLayout; /** * @brief VkCommandBuffer wrapper, allocated from its pool, base of @ref PrimaryCmdBuffer and @ref SecondaryCmdBuffer @@ -133,24 +137,13 @@ class BaseCmdBuffer { void endRendering(); /** - * @brief maps the color attachments to the fragment shader outputs - * @param locations output location of each color attachment (VK_ATTACHMENT_UNUSED => no output) - * @pre @ref rendering() with one location per color attachment - */ - void setAttachmentLocations(std::span locations) const; - - /** - * @brief maps the attachments to the input attachments of the fragment shader - * @param colorIndices input attachment index of each color attachment (VK_ATTACHMENT_UNUSED => no input) - * @param depthIndex std::nullopt -> the depth attachment is an input attachment without index - * @param stencilIndex std::nullopt -> the stencil attachment is an input attachment without index - * @pre @ref rendering() with one index per color attachment + * @brief maps the attachments of the rendering to the fragment shader outputs and input attachments + * @details @ref beginRendering() sets the default mapping, the mapping holds until the next call + * @pre @ref rendering() began with @ref beginRendering() + * @pre @ref AttachmentMapping::debug_check() passes for the color attachments of the rendering + * @post the inheritance of @ref rendering() has the mapping */ - void setInputAttachmentIndices( - std::span colorIndices, - std::optional depthIndex = std::nullopt, - std::optional stencilIndex = std::nullopt - ) const; + void setAttachmentMapping(AttachmentMapping const&); /** * @brief sets viewport 0 to the area of the rendering, depth 0 to 1 @@ -176,6 +169,58 @@ class BaseCmdBuffer { */ void setScissor(VkRect2D const&) const; + /** + * @brief binds the pipeline to the graphics bind point + * @details inside the own rendering: sets the attachment mapping of the pipeline if the current one differs + * @pre @ref recording(), @ref Pipeline::created() + * @pre inside an inherited rendering: the attachment mapping of the pipeline == the inherited one + * @note outside of a rendering: sets no attachment mapping, the draws need the default of @ref beginRendering() + */ + void bindPipeline(Pipeline const&); + + /** + * @brief binds the set to the set number of the pipeline layout + * @param setIndex set number in the pipeline layout + * @param dynamicOffsets one per dynamic buffer descriptor of the set + * @pre @ref recording(), @ref DescriptorSet::created() + * @pre the set layout of setIndex in the pipeline layout is compatible with the layout of the set + */ + void bindDescriptorSet( + DescriptorSet const&, + PipelineLayout const&, + uint32_t setIndex, + VkPipelineBindPoint bindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS, + std::span dynamicOffsets = {} + ) const; + + /** + * @brief records push constants + * @param offset in bytes + * @pre @ref recording(), @ref PipelineLayout::created() + * @pre 0 < data.size(), offset and data.size() are multiples of 4 + * @pre the push constant ranges of the layout cover the bytes for the stages + */ + void pushConstants( + PipelineLayout const&, + VkShaderStageFlags stages, + std::span data, + uint32_t offset = 0 + ) const; + + /** + * @brief records the value as push constants + * @details calls @ref pushConstants(PipelineLayout const&, VkShaderStageFlags, std::span, uint32_t) + */ + template requires std::is_trivially_copyable_v + void pushConstants( + PipelineLayout const& layout, + VkShaderStageFlags stages, + T const& value, + uint32_t offset = 0 + ) const { + pushConstants(layout, stages, std::span{reinterpret_cast(&value), sizeof(T)}, offset); + } + /** * @brief binds the buffer to the vertex input binding * @param offset in bytes diff --git a/cvk/incl/cvk/render/cmd/rendering_info.hpp b/cvk/incl/cvk/render/cmd/rendering_info.hpp index bff500e9..87b363d6 100644 --- a/cvk/incl/cvk/render/cmd/rendering_info.hpp +++ b/cvk/incl/cvk/render/cmd/rendering_info.hpp @@ -3,7 +3,12 @@ #include +#include + +#include +#include #include +#include #include @@ -11,7 +16,75 @@ namespace cvk { class ImageView; /** - * @brief formats, samples and view mask of a rendering, what a secondary command buffer inherits + * @brief maps the attachments of a rendering to the fragment shader outputs and input attachments + * @details a draw requires the same mapping in the bound pipeline and in the command buffer + */ +struct AttachmentMapping { + /** + * @brief fragment shader output location per color attachment (VK_ATTACHMENT_UNUSED => no output) + * @details empty -> color attachment i writes location i + */ + std::vector locations{}; + + /** + * @brief input attachment index per color attachment (VK_ATTACHMENT_UNUSED => no input attachment) + * @details empty -> color attachment i is input attachment i + */ + std::vector colorInputIndices{}; + + /** + * @brief std::nullopt -> the depth attachment is the input attachment without index + */ + std::optional depthInputIndex = std::nullopt; + + /** + * @brief std::nullopt -> the stencil attachment is the input attachment without index + */ + std::optional stencilInputIndex = std::nullopt; + + /** + * @param colorAttachments of the rendering + * @pre @ref debug_check() passes for colorAttachments + * @attention pColorAttachmentLocations points into @ref locations + */ + [[nodiscard]] constexpr VkRenderingAttachmentLocationInfoKHR locationInfo(uint32_t colorAttachments) const { + return VkRenderingAttachmentLocationInfoKHR{ + .sType = VK_STRUCTURE_TYPE_RENDERING_ATTACHMENT_LOCATION_INFO_KHR, + .pNext = nullptr, + .colorAttachmentCount = colorAttachments, + .pColorAttachmentLocations = locations.empty() ? nullptr : locations.data(), + }; + } + + /** + * @param colorAttachments of the rendering + * @pre @ref debug_check() passes for colorAttachments + * @attention the pointers point into this mapping + */ + [[nodiscard]] constexpr VkRenderingInputAttachmentIndexInfoKHR inputIndexInfo(uint32_t colorAttachments) const { + return VkRenderingInputAttachmentIndexInfoKHR{ + .sType = VK_STRUCTURE_TYPE_RENDERING_INPUT_ATTACHMENT_INDEX_INFO_KHR, + .pNext = nullptr, + .colorAttachmentCount = colorAttachments, + .pColorAttachmentInputIndices = colorInputIndices.empty() ? nullptr : colorInputIndices.data(), + .pDepthInputAttachmentIndex = depthInputIndex.has_value() ? &*depthInputIndex : nullptr, + .pStencilInputAttachmentIndex = stencilInputIndex.has_value() ? &*stencilInputIndex : nullptr, + }; + } + + /** + * @brief same mapping, empty == the default + */ + [[nodiscard]] constexpr bool operator==(AttachmentMapping const&) const; + + /** + * @param colorAttachments of the rendering + */ + static constexpr void debug_check(AttachmentMapping const&, size_t colorAttachments); +}; + +/** + * @brief formats, samples, view mask and attachment mapping of a rendering, what a secondary command buffer inherits */ struct RenderingInheritance { std::vector colorFormats{}; @@ -21,6 +94,12 @@ struct RenderingInheritance { uint32_t viewMask = 0; VkRenderingFlags flags = 0; + /** + * @brief mapping at the begin of the secondary + * @pre == the mapping of the primary at the execute + */ + AttachmentMapping attachmentMapping{}; + /** * @attention pColorAttachmentFormats points into @ref colorFormats */ @@ -98,3 +177,34 @@ struct RenderingInfo { VkRenderingFlags flags = 0; }; } + +namespace cvk { +constexpr bool AttachmentMapping::operator==(AttachmentMapping const& other) const { + auto const same = [](std::vector const& left, std::vector const& right) { + // the default maps attachment i to i + auto const identity = [](std::vector const& indices) { + return std::ranges::equal(indices, std::views::iota(uint32_t{0}, static_cast(indices.size()))); + }; + if(left.empty()) return identity(right); + if(right.empty()) return identity(left); + return left == right; + }; + return same(locations, other.locations) && same(colorInputIndices, other.colorInputIndices) + && depthInputIndex == other.depthInputIndex && stencilInputIndex == other.stencilInputIndex; +} + +constexpr void AttachmentMapping::debug_check(AttachmentMapping const& mapping, size_t colorAttachments) { + CTH_CRITICAL( + !mapping.locations.empty() && mapping.locations.size() != colorAttachments, + "one location per color attachment" + ) { + details->add("locations: {}, color attachments: {}", mapping.locations.size(), colorAttachments); + } + CTH_CRITICAL( + !mapping.colorInputIndices.empty() && mapping.colorInputIndices.size() != colorAttachments, + "one input attachment index per color attachment" + ) { + details->add("indices: {}, color attachments: {}", mapping.colorInputIndices.size(), colorAttachments); + } +} +} diff --git a/cvk/incl/cvk/render/pipeline/layout/pipeline_layout.hpp b/cvk/incl/cvk/render/pipeline/layout/pipeline_layout.hpp index d9a34aba..631602c2 100644 --- a/cvk/incl/cvk/render/pipeline/layout/pipeline_layout.hpp +++ b/cvk/incl/cvk/render/pipeline/layout/pipeline_layout.hpp @@ -9,12 +9,10 @@ #include #include -#include #include namespace cvk { -class BaseCmdBuffer; class Core; class DescriptorSetLayout; @@ -90,34 +88,6 @@ class PipelineLayout { */ State release(); - /** - * @brief records push constants - * @param offset in bytes - * @pre @ref created(), @ref BaseCmdBuffer::recording() - * @pre 0 < data.size(), offset and data.size() are multiples of 4 - * @pre the push constant ranges of the config cover the bytes for the stages - */ - void pushConstants( - BaseCmdBuffer const&, - VkShaderStageFlags stages, - std::span data, - uint32_t offset = 0 - ) const; - - /** - * @brief records the value as push constants - * @details calls @ref pushConstants(BaseCmdBuffer const&, VkShaderStageFlags, std::span, uint32_t) - */ - template requires std::is_trivially_copyable_v - void pushConstants( - BaseCmdBuffer const& cmdBuffer, - VkShaderStageFlags stages, - T const& value, - uint32_t offset = 0 - ) const { - pushConstants(cmdBuffer, stages, std::span{reinterpret_cast(&value), sizeof(T)}, offset); - } - static void destroy(DeviceTable table, VkPipelineLayout); [[nodiscard]] constexpr bool created() const { return static_cast(_handle); } diff --git a/cvk/incl/cvk/render/pipeline/pipeline.hpp b/cvk/incl/cvk/render/pipeline/pipeline.hpp index 36da3b5c..5369325a 100644 --- a/cvk/incl/cvk/render/pipeline/pipeline.hpp +++ b/cvk/incl/cvk/render/pipeline/pipeline.hpp @@ -14,7 +14,6 @@ namespace cvk { class Core; -class BaseCmdBuffer; class PipelineLayout; /** @@ -105,13 +104,6 @@ class Pipeline { */ State release(); - /** - * @brief records the bind of the pipeline to the graphics bind point - * @pre @ref created() - * @pre @ref BaseCmdBuffer::recording() - */ - void bind(BaseCmdBuffer const&) const; - static void destroy(DeviceTable table, VkPipeline); [[nodiscard]] constexpr bool created() const { return static_cast(_handle); } diff --git a/cvk/incl/cvk/render/pipeline/pipeline_config.hpp b/cvk/incl/cvk/render/pipeline/pipeline_config.hpp index 52dcbeab..e2d8f0c7 100644 --- a/cvk/incl/cvk/render/pipeline/pipeline_config.hpp +++ b/cvk/incl/cvk/render/pipeline/pipeline_config.hpp @@ -1,4 +1,6 @@ #pragma once +#include "cvk/render/cmd/rendering_info.hpp" + #include #include @@ -149,6 +151,12 @@ struct PipelineConfig { VkFormat depthFormat = VK_FORMAT_UNDEFINED; VkFormat stencilFormat = VK_FORMAT_UNDEFINED; uint32_t viewMask = 0; + + /** + * @brief == the mapping of the command buffer at a draw, see @ref BaseCmdBuffer::setAttachmentMapping() + * @pre @ref AttachmentMapping::debug_check() passes for @ref colorFormats + */ + AttachmentMapping attachmentMapping{}; }; } diff --git a/cvk/src/data/descriptor/descriptor_set.cpp b/cvk/src/data/descriptor/descriptor_set.cpp index c3c043d1..45783551 100644 --- a/cvk/src/data/descriptor/descriptor_set.cpp +++ b/cvk/src/data/descriptor/descriptor_set.cpp @@ -1,11 +1,8 @@ #include "cvk/data/descriptor/descriptor_set.hpp" #include "cvk/base/core.hpp" -#include "cvk/render/cmd/cmd_buffer.hpp" -#include "cvk/render/pipeline/layout/pipeline_layout.hpp" #include "cvk/utility/vk_exceptions.hpp" -#include #include @@ -34,10 +31,6 @@ namespace { } } - constexpr bool dynamic_buffer(VkDescriptorType type) { - return type == VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC || type == VK_DESCRIPTOR_TYPE_STORAGE_BUFFER_DYNAMIC; - } - void debug_check_image(ImageDescriptor const& descriptor, VkDescriptorType type) { CTH_CRITICAL( (type == VK_DESCRIPTOR_TYPE_SAMPLER || type == VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER) @@ -227,43 +220,6 @@ void DescriptorSet::write(span writes) const { ); } -void DescriptorSet::bind( - BaseCmdBuffer const& cmdBuffer, - PipelineLayout const& layout, - uint32_t setIndex, - VkPipelineBindPoint bindPoint, - span dynamicOffsets -) const { - debug_check(*this); - BaseCmdBuffer::debug_check(cmdBuffer); - CTH_CRITICAL(!cmdBuffer.recording(), "command buffer must be recording") {} - PipelineLayout::debug_check(layout); - CTH_CRITICAL(setIndex >= layout.setLayouts().size(), "the pipeline layout has no set {}", setIndex) {} - - auto const dynamicCount = std::ranges::fold_left( - _layout->bindings(), - uint32_t{0}, - [](uint32_t sum, VkDescriptorSetLayoutBinding const& binding) { - return dynamic_buffer(binding.descriptorType) ? sum + binding.descriptorCount : sum; - } - ); - CTH_CRITICAL(dynamicOffsets.size() != dynamicCount, "one dynamic offset per dynamic buffer descriptor") { - details->add("dynamic descriptors: {}, offsets: {}", dynamicCount, dynamicOffsets.size()); - } - - VkDescriptorSet const vkSet = get(); - core().functions()->vkCmdBindDescriptorSets( - cmdBuffer.get(), - bindPoint, - layout.get(), - setIndex, - 1, - &vkSet, - static_cast(dynamicOffsets.size()), - dynamicOffsets.data() - ); -} - void DescriptorSet::destroy(DeviceTable table, VkDescriptorPool vkPool, VkDescriptorSet vkSet) { CTH_WARN(vkSet == VK_NULL_HANDLE, "vkSet invalid (VK_NULL_HANDLE)") {} diff --git a/cvk/src/render/cmd/cmd_buffer.cpp b/cvk/src/render/cmd/cmd_buffer.cpp index 33127e7c..bea6a7e1 100644 --- a/cvk/src/render/cmd/cmd_buffer.cpp +++ b/cvk/src/render/cmd/cmd_buffer.cpp @@ -1,9 +1,12 @@ #include "cvk/render/cmd/cmd_buffer.hpp" #include "cvk/base/core.hpp" +#include "cvk/data/descriptor/descriptor_set.hpp" #include "cvk/data/img/image.hpp" #include "cvk/data/img/image_format.hpp" #include "cvk/data/img/image_view.hpp" +#include "cvk/render/pipeline/layout/pipeline_layout.hpp" +#include "cvk/render/pipeline/pipeline.hpp" #include "cvk/utility/vk_exceptions.hpp" #include @@ -25,6 +28,10 @@ namespace { } } + constexpr bool dynamic_buffer(VkDescriptorType type) { + return type == VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC || type == VK_DESCRIPTOR_TYPE_STORAGE_BUFFER_DYNAMIC; + } + uint32_t view_mip_levels(ImageView const& view) { auto const range = view.range(); return range.levelCount == VK_REMAINING_MIP_LEVELS ? view.image().mipLevels() - range.baseMipLevel @@ -269,44 +276,20 @@ void BaseCmdBuffer::endRendering() { _rendering.reset(); } -void BaseCmdBuffer::setAttachmentLocations(span locations) const { +void BaseCmdBuffer::setAttachmentMapping(AttachmentMapping const& mapping) { debug_check(*this); - CTH_CRITICAL(rendering() == nullptr, "command buffer must be inside a rendering") {} - CTH_CRITICAL( - locations.size() != rendering()->inheritance.colorFormats.size(), - "one location per color attachment" - ) {} + CTH_CRITICAL(rendering() == nullptr || rendering()->inherited, "command buffer must be inside its own rendering") {} - VkRenderingAttachmentLocationInfoKHR const info{ - .sType = VK_STRUCTURE_TYPE_RENDERING_ATTACHMENT_LOCATION_INFO_KHR, - .pNext = nullptr, - .colorAttachmentCount = static_cast(locations.size()), - .pColorAttachmentLocations = locations.data(), - }; - core().functions()->vkCmdSetRenderingAttachmentLocationsKHR(get(), &info); -} + auto& inheritance = _rendering->inheritance; + AttachmentMapping::debug_check(mapping, inheritance.colorFormats.size()); -void BaseCmdBuffer::setInputAttachmentIndices( - span colorIndices, - std::optional depthIndex, - std::optional stencilIndex -) const { - debug_check(*this); - CTH_CRITICAL(rendering() == nullptr, "command buffer must be inside a rendering") {} - CTH_CRITICAL( - colorIndices.size() != rendering()->inheritance.colorFormats.size(), - "one input attachment index per color attachment" - ) {} + inheritance.attachmentMapping = mapping; - VkRenderingInputAttachmentIndexInfoKHR const info{ - .sType = VK_STRUCTURE_TYPE_RENDERING_INPUT_ATTACHMENT_INDEX_INFO_KHR, - .pNext = nullptr, - .colorAttachmentCount = static_cast(colorIndices.size()), - .pColorAttachmentInputIndices = colorIndices.data(), - .pDepthInputAttachmentIndex = depthIndex.has_value() ? &*depthIndex : nullptr, - .pStencilInputAttachmentIndex = stencilIndex.has_value() ? &*stencilIndex : nullptr, - }; - core().functions()->vkCmdSetRenderingInputAttachmentIndicesKHR(get(), &info); + auto const colorAttachments = static_cast(inheritance.colorFormats.size()); + auto const locations = inheritance.attachmentMapping.locationInfo(colorAttachments); + auto const inputIndices = inheritance.attachmentMapping.inputIndexInfo(colorAttachments); + core().functions()->vkCmdSetRenderingAttachmentLocationsKHR(get(), &locations); + core().functions()->vkCmdSetRenderingInputAttachmentIndicesKHR(get(), &inputIndices); } void BaseCmdBuffer::setViewport() const { @@ -347,6 +330,82 @@ void BaseCmdBuffer::setScissor(VkRect2D const& scissor) const { core().functions()->vkCmdSetScissor(get(), 0, 1, &scissor); } +void BaseCmdBuffer::bindPipeline(Pipeline const& pipeline) { + debug_check(*this); + CTH_CRITICAL(!recording(), "command buffer must be recording") {} + Pipeline::debug_check(pipeline); + + auto const* current = rendering(); + auto const& mapping = pipeline.config().attachmentMapping; + CTH_CRITICAL( + current != nullptr && current->inherited && mapping != current->inheritance.attachmentMapping, + "the attachment mapping of the pipeline must match the inherited mapping" + ) {} + if(current != nullptr && !current->inherited && mapping != current->inheritance.attachmentMapping) + setAttachmentMapping(mapping); + + core().functions()->vkCmdBindPipeline(get(), VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline.get()); +} + +void BaseCmdBuffer::bindDescriptorSet( + DescriptorSet const& set, + PipelineLayout const& layout, + uint32_t setIndex, + VkPipelineBindPoint bindPoint, + span dynamicOffsets +) const { + debug_check(*this); + CTH_CRITICAL(!recording(), "command buffer must be recording") {} + DescriptorSet::debug_check(set); + PipelineLayout::debug_check(layout); + CTH_CRITICAL(setIndex >= layout.setLayouts().size(), "the pipeline layout has no set {}", setIndex) {} + + auto const dynamicCount = std::ranges::fold_left( + set.layout().bindings(), + uint32_t{0}, + [](uint32_t sum, VkDescriptorSetLayoutBinding const& binding) { + return dynamic_buffer(binding.descriptorType) ? sum + binding.descriptorCount : sum; + } + ); + CTH_CRITICAL(dynamicOffsets.size() != dynamicCount, "one dynamic offset per dynamic buffer descriptor") { + details->add("dynamic descriptors: {}, offsets: {}", dynamicCount, dynamicOffsets.size()); + } + + VkDescriptorSet const vkSet = set.get(); + core().functions()->vkCmdBindDescriptorSets( + get(), + bindPoint, + layout.get(), + setIndex, + 1, + &vkSet, + static_cast(dynamicOffsets.size()), + dynamicOffsets.data() + ); +} + +void BaseCmdBuffer::pushConstants( + PipelineLayout const& layout, + VkShaderStageFlags stages, + span data, + uint32_t offset +) const { + debug_check(*this); + CTH_CRITICAL(!recording(), "command buffer must be recording") {} + PipelineLayout::debug_check(layout); + CTH_CRITICAL(data.empty(), "push constants must not be empty") {} + CTH_CRITICAL(offset % 4 != 0 || data.size() % 4 != 0, "offset and size must be multiples of 4") {} + + core().functions()->vkCmdPushConstants( + get(), + layout.get(), + stages, + offset, + static_cast(data.size()), + data.data() + ); +} + void BaseCmdBuffer::bindVertexBuffer(uint32_t binding, BaseBuffer const& buffer, size_t offset) const { debug_check(*this); BaseBuffer::debug_check(buffer); @@ -526,7 +585,15 @@ void SecondaryCmdBuffer::begin(VkCommandBufferUsageFlags usage) { } void SecondaryCmdBuffer::begin(Inheritance const& inheritance, VkCommandBufferUsageFlags usage) { - auto const renderingInfo = inheritance.renderingInfo(); + auto const colorAttachments = static_cast(inheritance.colorFormats.size()); + AttachmentMapping::debug_check(inheritance.attachmentMapping, colorAttachments); + + auto const inputIndices = inheritance.attachmentMapping.inputIndexInfo(colorAttachments); + auto locations = inheritance.attachmentMapping.locationInfo(colorAttachments); + locations.pNext = &inputIndices; + auto renderingInfo = inheritance.renderingInfo(); + renderingInfo.pNext = &locations; + VkCommandBufferInheritanceInfo const inheritanceInfo{ .sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_INHERITANCE_INFO, .pNext = &renderingInfo, diff --git a/cvk/src/render/pipeline/layout/pipeline_layout.cpp b/cvk/src/render/pipeline/layout/pipeline_layout.cpp index 9c29ed8f..ca95d976 100644 --- a/cvk/src/render/pipeline/layout/pipeline_layout.cpp +++ b/cvk/src/render/pipeline/layout/pipeline_layout.cpp @@ -2,7 +2,6 @@ #include "cvk/base/core.hpp" #include "cvk/base/physical_device.hpp" -#include "cvk/render/cmd/cmd_buffer.hpp" #include "cvk/render/pipeline/layout/descriptor_set_layout.hpp" #include "cvk/utility/vk_exceptions.hpp" @@ -81,27 +80,6 @@ PipelineLayout::State PipelineLayout::release() { return state; } -void PipelineLayout::pushConstants( - BaseCmdBuffer const& cmdBuffer, - VkShaderStageFlags stages, - std::span data, - uint32_t offset -) const { - debug_check(*this); - CTH_CRITICAL(!cmdBuffer.recording(), "command buffer must be recording") {} - CTH_CRITICAL(data.empty(), "push constants must not be empty") {} - CTH_CRITICAL(offset % 4 != 0 || data.size() % 4 != 0, "offset and size must be multiples of 4") {} - - core().functions()->vkCmdPushConstants( - cmdBuffer.get(), - get(), - stages, - offset, - static_cast(data.size()), - data.data() - ); -} - void PipelineLayout::destroy(DeviceTable table, VkPipelineLayout vkLayout) { CTH_WARN(vkLayout == VK_NULL_HANDLE, "vkLayout invalid (VK_NULL_HANDLE)") {} diff --git a/cvk/src/render/pipeline/pipeline.cpp b/cvk/src/render/pipeline/pipeline.cpp index 8e5817b3..eae49c92 100644 --- a/cvk/src/render/pipeline/pipeline.cpp +++ b/cvk/src/render/pipeline/pipeline.cpp @@ -1,7 +1,6 @@ #include "cvk/render/pipeline/pipeline.hpp" #include "cvk/base/core.hpp" -#include "cvk/render/cmd/cmd_buffer.hpp" #include "cvk/render/pipeline/layout/pipeline_layout.hpp" #include "cvk/utility/vk_exceptions.hpp" @@ -53,6 +52,7 @@ void Pipeline::create() { _config.colorBlendAttachments.size() != _config.colorFormats.size(), "one color blend attachment per color format required" ) {} + AttachmentMapping::debug_check(_config.attachmentMapping, _config.colorFormats.size()); optDestroy(); @@ -90,11 +90,16 @@ void Pipeline::create() { .pDynamicStates = config.dynamicStates.data(), }; + auto const colorAttachments = static_cast(config.colorFormats.size()); + auto const inputIndices = config.attachmentMapping.inputIndexInfo(colorAttachments); + auto locations = config.attachmentMapping.locationInfo(colorAttachments); + locations.pNext = &inputIndices; + VkPipelineRenderingCreateInfo const rendering{ .sType = VK_STRUCTURE_TYPE_PIPELINE_RENDERING_CREATE_INFO, - .pNext = nullptr, + .pNext = &locations, .viewMask = config.viewMask, - .colorAttachmentCount = static_cast(config.colorFormats.size()), + .colorAttachmentCount = colorAttachments, .pColorAttachmentFormats = config.colorFormats.data(), .depthAttachmentFormat = config.depthFormat, .stencilAttachmentFormat = config.stencilFormat, @@ -156,13 +161,6 @@ Pipeline::State Pipeline::release() { return state; } -void Pipeline::bind(BaseCmdBuffer const& cmdBuffer) const { - debug_check(*this); - CTH_CRITICAL(!cmdBuffer.recording(), "command buffer must be recording") {} - - core().functions()->vkCmdBindPipeline(cmdBuffer.get(), VK_PIPELINE_BIND_POINT_GRAPHICS, get()); -} - void Pipeline::destroy(DeviceTable table, VkPipeline vkPipeline) { CTH_WARN(vkPipeline == VK_NULL_HANDLE, "vkPipeline invalid (VK_NULL_HANDLE)") {} diff --git a/cvk/tests/shaders/local_read.frag b/cvk/tests/shaders/local_read.frag new file mode 100644 index 00000000..a64508b1 --- /dev/null +++ b/cvk/tests/shaders/local_read.frag @@ -0,0 +1,7 @@ +#version 450 + +layout(input_attachment_index = 0, set = 0, binding = 0) uniform subpassInput color; + +layout(location = 0) out vec4 outColor; + +void main() { outColor = subpassLoad(color); } diff --git a/cvk/tests/src/data/descriptor/descriptor_draw_test.cpp b/cvk/tests/src/data/descriptor/descriptor_draw_test.cpp index 15c43056..29141bea 100644 --- a/cvk/tests/src/data/descriptor/descriptor_draw_test.cpp +++ b/cvk/tests/src/data/descriptor/descriptor_draw_test.cpp @@ -132,7 +132,7 @@ namespace { }, } ); - fill.pipeline.bind(cmd); + cmd.bindPipeline(fill.pipeline); cmd.setViewport(); cmd.setScissor(); cmd.bindVertexBuffer(0, fill.vertices); @@ -186,7 +186,7 @@ CVK_TEST(DescriptorDraw, uniform_buffer_color) { set.write(std::array{DescriptorWrite{.binding = 0, .buffers = {{&color}}}}); test::record_and_submit(context, [&](PrimaryCmdBuffer& cmd) { - record_fill(context, cmd, fill, target, [&](BaseCmdBuffer& c) { set.bind(c, fill.layout, 0); }); + record_fill(context, cmd, fill, target, [&](BaseCmdBuffer& c) { c.bindDescriptorSet(set, fill.layout, 0); }); }); expect_green(target); @@ -241,8 +241,8 @@ CVK_TEST(DescriptorDraw, bindless_texture_written_after_bind) { toShader.execute(cmd); record_fill(context, cmd, fill, target, [&](BaseCmdBuffer& c) { - set.bind(c, fill.layout, 0); - fill.layout.pushConstants(c, VK_SHADER_STAGE_FRAGMENT_BIT, INDEX); + c.bindDescriptorSet(set, fill.layout, 0); + c.pushConstants(fill.layout, VK_SHADER_STAGE_FRAGMENT_BIT, INDEX); }); cmd.end(); diff --git a/cvk/tests/src/render/cmd/draw_test.cpp b/cvk/tests/src/render/cmd/draw_test.cpp index f96187fd..7d07517b 100644 --- a/cvk/tests/src/render/cmd/draw_test.cpp +++ b/cvk/tests/src/render/cmd/draw_test.cpp @@ -110,11 +110,11 @@ namespace { } ); - fill.pipeline.bind(cmd); + cmd.bindPipeline(fill.pipeline); cmd.setViewport(); cmd.setScissor(); cmd.bindVertexBuffer(0, fill.vertices); - fill.layout.pushConstants(cmd, VK_SHADER_STAGE_FRAGMENT_BIT, GREEN); + cmd.pushConstants(fill.layout, VK_SHADER_STAGE_FRAGMENT_BIT, GREEN); draw(cmd); cmd.endRendering(); diff --git a/cvk/tests/src/render/cmd/local_read_test.cpp b/cvk/tests/src/render/cmd/local_read_test.cpp new file mode 100644 index 00000000..ffc03f2d --- /dev/null +++ b/cvk/tests/src/render/cmd/local_read_test.cpp @@ -0,0 +1,333 @@ +#include "context.hpp" +#include "shaders.hpp" +#include "test.hpp" + +#include "cvk/data/buffer/buffer.hpp" +#include "cvk/data/descriptor/descriptor_pool.hpp" +#include "cvk/data/descriptor/descriptor_set.hpp" +#include "cvk/data/img/image.hpp" +#include "cvk/data/img/image_view.hpp" +#include "cvk/render/cmd/cmd_buffer.hpp" +#include "cvk/render/cmd/cmd_pool.hpp" +#include "cvk/render/pipeline/layout/descriptor_set_layout.hpp" +#include "cvk/render/pipeline/layout/pipeline_layout.hpp" +#include "cvk/render/pipeline/pipeline.hpp" +#include "cvk/render/pipeline/shader/shader.hpp" +#include "cvk/render/sync/barrier.hpp" + +#include +#include +#include +#include + + +namespace cvk { +namespace { + constexpr VkExtent2D EXTENT{16, 16}; + constexpr std::array COLOR_FORMATS{VK_FORMAT_R8G8B8A8_UNORM, VK_FORMAT_R8G8B8A8_UNORM}; + constexpr std::array GREEN{0.f, 1.f, 0.f, 1.f}; + constexpr std::array GREEN_PIXEL{0, 255, 0, 255}; + + constexpr BarrierScope COLOR_WRITE{ + VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT, + VK_ACCESS_2_COLOR_ATTACHMENT_WRITE_BIT + }; + constexpr BarrierScope INPUT_READ{VK_PIPELINE_STAGE_2_FRAGMENT_SHADER_BIT, VK_ACCESS_2_INPUT_ATTACHMENT_READ_BIT}; + + struct Vec3 { + float x, y, z; + }; + + /** + * @brief one triangle that covers the whole viewport + */ + constexpr std::array FULLSCREEN{Vec3{-1.f, -1.f, 0.f}, Vec3{3.f, -1.f, 0.f}, Vec3{-1.f, 3.f, 0.f}}; + + /** + * @brief location 0 -> attachment 0, attachment 1 without output + */ + AttachmentMapping write_first() { return {.locations = {0, VK_ATTACHMENT_UNUSED}}; } + + /** + * @brief attachment 0 -> input attachment 0, location 0 -> attachment 1 + */ + AttachmentMapping copy_first_to_second() { + return {.locations = {VK_ATTACHMENT_UNUSED, 0}, .colorInputIndices = {0, VK_ATTACHMENT_UNUSED}}; + } + + /** + * @brief attachment 0: input attachment, attachment 1: transfer src, both with a view + */ + struct Attachments { + explicit Attachments(Core const& core) : first{ + core, + { + .format = COLOR_FORMATS[0], + .usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_INPUT_ATTACHMENT_BIT, + }, + EXTENT + }, + second{ + core, + { + .format = COLOR_FORMATS[1], + .usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT, + }, + EXTENT + }, + firstView{first, {}, create}, + secondView{second, {}, create} {} + + /** + * @brief records the transitions: first -> rendering local read, second -> color attachment + */ + void toRendering(BaseCmdBuffer const& cmd) const { + Barrier barrier{}; + barrier.addTransition(first, VK_IMAGE_LAYOUT_RENDERING_LOCAL_READ_KHR, VK_IMAGE_LAYOUT_UNDEFINED); + barrier.addTransition(second, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, VK_IMAGE_LAYOUT_UNDEFINED); + barrier.execute(cmd); + } + + /** + * @brief both attachments cleared to black + */ + [[nodiscard]] RenderingInfo renderingInfo(VkRenderingFlags flags = 0) const { + auto const clear = [](ImageView const& view, VkImageLayout layout) { + return RenderingAttachment{ + .view = &view, + .layout = layout, + .loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR, + .storeOp = VK_ATTACHMENT_STORE_OP_STORE, + .clearValue = {.color = {.float32 = {0.f, 0.f, 0.f, 1.f}}}, + }; + }; + return RenderingInfo{ + .colorAttachments = { + clear(firstView, VK_IMAGE_LAYOUT_RENDERING_LOCAL_READ_KHR), + clear(secondView, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL), + }, + .flags = flags, + }; + } + + Image first; + Image second; + ImageView firstView; + ImageView secondView; + }; + + /** + * @brief fullscreen pipeline for the attachments + */ + Pipeline::Config fullscreen(Shader const& vertex, Shader const& fragment, AttachmentMapping mapping) { + Pipeline::Config config{COLOR_FORMATS}; + config.addStage(vertex).addStage(fragment); + config.vertexBindings = {{.binding = 0, .stride = sizeof(Vec3), .inputRate = VK_VERTEX_INPUT_RATE_VERTEX}}; + config.vertexAttributes = {{.location = 0, .binding = 0, .format = VK_FORMAT_R32G32B32_SFLOAT, .offset = 0}}; + config.rasterization.cullMode = VK_CULL_MODE_NONE; + config.attachmentMapping = std::move(mapping); + return config; + } + + /** + * @brief binding 0: input attachment + */ + DescriptorSetLayout::Config input_attachment() { + DescriptorSetLayout::Config config{}; + config.add(0, VK_DESCRIPTOR_TYPE_INPUT_ATTACHMENT, VK_SHADER_STAGE_FRAGMENT_BIT); + return config; + } + + /** + * @brief write: push constant color -> attachment 0, read: attachment 0 -> attachment 1 + */ + struct Draws { + Draws(Core const& core, ImageView const& first) : + vertex{core, VK_SHADER_STAGE_VERTEX_BIT, test::shaders::FILL_VERT}, + color{core, VK_SHADER_STAGE_FRAGMENT_BIT, test::shaders::TRIANGLE_FRAG}, + localRead{core, VK_SHADER_STAGE_FRAGMENT_BIT, test::shaders::LOCAL_READ_FRAG}, + writeLayout{core, {.pushConstantRanges = {{VK_SHADER_STAGE_FRAGMENT_BIT, 0, sizeof(GREEN)}}}, create}, + write{core, writeLayout, fullscreen(vertex, color, write_first()), create}, + inputLayout{core, input_attachment(), create}, + readLayout{core, {.setLayouts = {&inputLayout}}, create}, + read{core, readLayout, fullscreen(vertex, localRead, copy_first_to_second()), create}, + pool{core, {{&inputLayout}}, create}, + input{pool, inputLayout, create}, + vertices{ + core, + { + .usage = VK_BUFFER_USAGE_VERTEX_BUFFER_BIT, + .memory = {.flags = VMA_ALLOCATION_CREATE_HOST_ACCESS_SEQUENTIAL_WRITE_BIT}, + }, + FULLSCREEN.size() + } { + input.write( + std::array{ + DescriptorWrite{ + .binding = 0, + .images = {{.view = &first, .layout = VK_IMAGE_LAYOUT_RENDERING_LOCAL_READ_KHR}}, + }, + } + ); + vertices.write(FULLSCREEN); + } + + /** + * @brief records the draw of @ref write + * @pre the command buffer has a viewport and a scissor + */ + void drawWrite(BaseCmdBuffer& cmd) const { + cmd.bindVertexBuffer(0, vertices); + cmd.bindPipeline(write); + cmd.pushConstants(writeLayout, VK_SHADER_STAGE_FRAGMENT_BIT, GREEN); + cmd.draw(3); + } + + Shader vertex; + Shader color; + Shader localRead; + PipelineLayout writeLayout; + Pipeline write; + DescriptorSetLayout inputLayout; + PipelineLayout readLayout; + Pipeline read; + DescriptorPool pool; + DescriptorSet input; + Buffer vertices; + }; +} + +CVK_TEST(AttachmentMapping, empty_maps_to_the_default) { + static_assert([] { + AttachmentMapping const mapping{}; + auto const locations = mapping.locationInfo(2); + auto const indices = mapping.inputIndexInfo(2); + + return locations.colorAttachmentCount == 2 && locations.pColorAttachmentLocations == nullptr + && indices.colorAttachmentCount == 2 && indices.pColorAttachmentInputIndices == nullptr + && indices.pDepthInputAttachmentIndex == nullptr && indices.pStencilInputAttachmentIndex == nullptr; + }()); +} + +CVK_TEST(AttachmentMapping, infos_point_into_the_mapping) { + static_assert([] { + //TEMP not const: gcc 16 rejects a const aggregate with a vector and another member in a constant expression + AttachmentMapping mapping{ + .locations = {1, 0}, + .colorInputIndices = {VK_ATTACHMENT_UNUSED, 0}, + .depthInputIndex = 1, + }; + auto const locations = mapping.locationInfo(2); + auto const indices = mapping.inputIndexInfo(2); + + return locations.pColorAttachmentLocations[0] == 1 && locations.pColorAttachmentLocations[1] == 0 + && indices.pColorAttachmentInputIndices[1] == 0 && *indices.pDepthInputAttachmentIndex == 1 + && indices.pStencilInputAttachmentIndex == nullptr; + }()); +} + +CVK_TEST(AttachmentMapping, empty_equals_the_default) { + static_assert([] { + //TEMP not const: gcc 16 rejects a const aggregate with a vector and another member in a constant expression + AttachmentMapping identity{.locations = {0, 1}, .colorInputIndices = {0, 1}}; + AttachmentMapping swapped{.locations = {1, 0}}; + AttachmentMapping depth{.depthInputIndex = 0}; + + return AttachmentMapping{} == identity && AttachmentMapping{} != swapped && AttachmentMapping{} != depth; + }()); +} + +CVK_TEST(LocalRead, draw_reads_an_attachment_of_the_same_rendering) { + test::Context const context{}; + Attachments const attachments{context.core}; + Draws const draws{context.core, attachments.firstView}; + BaseBuffer const readback{ + context.core, + {.usage = VK_BUFFER_USAGE_TRANSFER_DST_BIT, .memory = {.flags = VMA_ALLOCATION_CREATE_HOST_ACCESS_RANDOM_BIT}}, + size_t{EXTENT.width} * EXTENT.height * 4 + }; + + test::record_and_submit(context, [&](PrimaryCmdBuffer& cmd) { + attachments.toRendering(cmd); + + cmd.beginRendering(attachments.renderingInfo()); + cmd.setViewport(); + cmd.setScissor(); + + // the binds set the attachment mapping of the pipelines + draws.drawWrite(cmd); + EXPECT_EQ(cmd.rendering()->inheritance.attachmentMapping, write_first()); + + Barrier toInput{VK_DEPENDENCY_BY_REGION_BIT}; + toInput.add( + attachments.first, + { + .src = COLOR_WRITE, + .dst = INPUT_READ, + .newLayout = VK_IMAGE_LAYOUT_RENDERING_LOCAL_READ_KHR, + .oldLayout = VK_IMAGE_LAYOUT_RENDERING_LOCAL_READ_KHR, + } + ); + toInput.execute(cmd); + + cmd.bindPipeline(draws.read); + EXPECT_EQ(cmd.rendering()->inheritance.attachmentMapping, copy_first_to_second()); + cmd.bindDescriptorSet(draws.input, draws.readLayout, 0); + cmd.draw(3); + cmd.endRendering(); + + Barrier toTransfer{}; + toTransfer.add( + attachments.second, + ImageBarrierInfo::LayoutTransition( + VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, + VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, + COLOR_WRITE, + {VK_PIPELINE_STAGE_2_TRANSFER_BIT, VK_ACCESS_2_TRANSFER_READ_BIT} + ) + ); + toTransfer.execute(cmd); + + test::copy_to_buffer(context, cmd, attachments.second, readback, 0, 0); + test::host_read_barrier(context, cmd); + }); + + auto const pixels = readback.memory().map(readback.size()); + readback.memory().invalidate(); + auto const asByte = [](char c) { return static_cast(c); }; + for(auto const pixel : pixels | std::views::chunk(4)) + EXPECT_TRUE(std::ranges::equal(pixel, GREEN_PIXEL, {}, asByte)); + readback.memory().unmap(); +} + +CVK_TEST(LocalRead, secondary_inherits_the_mapping) { + test::Context const context{}; + Attachments const attachments{context.core}; + Draws const draws{context.core, attachments.firstView}; + CmdPool const pool{context.core, CmdPool::Config{context.core.queue(0)}, create}; + std::array secondaries{SecondaryCmdBuffer{pool, create}}; + + test::record_and_submit(context, [&](PrimaryCmdBuffer& cmd) { + attachments.toRendering(cmd); + cmd.beginRendering(attachments.renderingInfo(VK_RENDERING_CONTENTS_SECONDARY_COMMAND_BUFFERS_BIT)); + EXPECT_EQ(cmd.rendering()->inheritance.attachmentMapping, AttachmentMapping{}); + + cmd.setAttachmentMapping(write_first()); + EXPECT_EQ(cmd.rendering()->inheritance.attachmentMapping, write_first()); + + // the draw needs the mapping of the write pipeline in the secondary + auto& secondary = secondaries[0]; + secondary.begin(cmd.rendering()->inheritance, VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT); + EXPECT_EQ(secondary.rendering()->inheritance.attachmentMapping, write_first()); + secondary.setViewport( + {0.f, 0.f, static_cast(EXTENT.width), static_cast(EXTENT.height), 0.f, 1.f} + ); + secondary.setScissor({{0, 0}, EXTENT}); + draws.drawWrite(secondary); + secondary.end(); + + cmd.execute(secondaries); + cmd.endRendering(); + }); +} +} diff --git a/cvk/tests/src/render/cmd/rendering_test.cpp b/cvk/tests/src/render/cmd/rendering_test.cpp index 732fbcfa..43618b81 100644 --- a/cvk/tests/src/render/cmd/rendering_test.cpp +++ b/cvk/tests/src/render/cmd/rendering_test.cpp @@ -167,7 +167,6 @@ CVK_TEST(Rendering, local_read_inside_the_rendering) { test::Context const context{}; Image const image{context.core, LOCAL_READ_CONFIG, EXTENT}; ImageView const view{image, {}, create}; - constexpr std::array FIRST{0}; test::record_and_submit(context, [&](PrimaryCmdBuffer& cmd) { Barrier transition{}; @@ -175,8 +174,7 @@ CVK_TEST(Rendering, local_read_inside_the_rendering) { transition.execute(cmd); cmd.beginRendering({.colorAttachments = {clear_red(view, VK_IMAGE_LAYOUT_RENDERING_LOCAL_READ_KHR)}}); - cmd.setAttachmentLocations(FIRST); - cmd.setInputAttachmentIndices(FIRST); + cmd.setAttachmentMapping({.locations = {0}, .colorInputIndices = {0}}); Barrier barrier{VK_DEPENDENCY_BY_REGION_BIT}; barrier.add( diff --git a/cvk/tests/src/render/pipeline/pipeline_test.cpp b/cvk/tests/src/render/pipeline/pipeline_test.cpp index 5f800da2..17f921b9 100644 --- a/cvk/tests/src/render/pipeline/pipeline_test.cpp +++ b/cvk/tests/src/render/pipeline/pipeline_test.cpp @@ -107,7 +107,7 @@ CVK_TEST(Pipeline, bind) { PrimaryCmdBuffer primary{pool, create}; primary.begin(VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT); - pipeline.bind(primary); + primary.bindPipeline(pipeline); primary.end(); test::submit_and_wait(context, primary); diff --git a/cvk/tests/src/shaders.hpp b/cvk/tests/src/shaders.hpp index 2da73f96..83f000dc 100644 --- a/cvk/tests/src/shaders.hpp +++ b/cvk/tests/src/shaders.hpp @@ -135,4 +135,25 @@ inline constexpr std::array BINDLESS_FRAG{ 0x0000001b, 0x0000001a, 0x00050057, 0x00000007, 0x0000001f, 0x0000001b, 0x0000001e, 0x0003003e, 0x00000009, 0x0000001f, 0x000100fd, 0x00010038, }; + +/** + * @brief fragment shader: set 0 binding 0 input attachment index 0, location 0 color output of its value + */ +inline constexpr std::array LOCAL_READ_FRAG{ + 0x07230203, 0x00010600, 0x000d000b, 0x00000013, 0x00000000, 0x00020011, 0x00000001, 0x00020011, + 0x00000028, 0x0006000b, 0x00000001, 0x4c534c47, 0x6474732e, 0x3035342e, 0x00000000, 0x0003000e, + 0x00000000, 0x00000001, 0x0007000f, 0x00000004, 0x00000004, 0x6e69616d, 0x00000000, 0x00000009, + 0x0000000c, 0x00030010, 0x00000004, 0x00000007, 0x00040047, 0x00000009, 0x0000001e, 0x00000000, + 0x00040047, 0x0000000c, 0x00000021, 0x00000000, 0x00040047, 0x0000000c, 0x00000022, 0x00000000, + 0x00040047, 0x0000000c, 0x0000002b, 0x00000000, 0x00020013, 0x00000002, 0x00030021, 0x00000003, + 0x00000002, 0x00030016, 0x00000006, 0x00000020, 0x00040017, 0x00000007, 0x00000006, 0x00000004, + 0x00040020, 0x00000008, 0x00000003, 0x00000007, 0x0004003b, 0x00000008, 0x00000009, 0x00000003, + 0x00090019, 0x0000000a, 0x00000006, 0x00000006, 0x00000000, 0x00000000, 0x00000000, 0x00000002, + 0x00000000, 0x00040020, 0x0000000b, 0x00000000, 0x0000000a, 0x0004003b, 0x0000000b, 0x0000000c, + 0x00000000, 0x00040015, 0x0000000e, 0x00000020, 0x00000001, 0x0004002b, 0x0000000e, 0x0000000f, + 0x00000000, 0x00040017, 0x00000010, 0x0000000e, 0x00000002, 0x0005002c, 0x00000010, 0x00000011, + 0x0000000f, 0x0000000f, 0x00050036, 0x00000002, 0x00000004, 0x00000000, 0x00000003, 0x000200f8, + 0x00000005, 0x0004003d, 0x0000000a, 0x0000000d, 0x0000000c, 0x00050062, 0x00000007, 0x00000012, + 0x0000000d, 0x00000011, 0x0003003e, 0x00000009, 0x00000012, 0x000100fd, 0x00010038, +}; } From cfccb24c1730d538a1f4a9a50665942fbac1cc30 Mon Sep 17 00:00:00 2001 From: lukas <89145039+codethinki@users.noreply.github.com> Date: Mon, 19 Oct 2026 19:38:17 +0200 Subject: [PATCH 066/135] cvk: command buffer stores only its own rendering, shorter command api - BaseCmdBuffer stores the RenderingInfo it began (area, current attachment mapping), no Rendering struct with an inherited flag and empty fields - PrimaryCmdBuffer::inheritance() builds what a secondary inherits - RenderingInfo::attachmentMapping: the mapping at the begin - checks in secondaries inside the rendering of a primary are left to the validation layers, draws outside of a rendering are checked for primaries - new check: no inline draws with VK_RENDERING_CONTENTS_SECONDARY_COMMAND_BUFFERS_BIT - beginRendering() sets viewport 0 and scissor 0 to the area, setViewport() and setScissor() without arguments are removed - fix: pushConstants() with a span pushed the span object, a contiguous range pushes its elements now (one template, push_constant_data) - buffer offsets are bytes, bindVertexBuffer() takes only BaseBuffer - AGENTS: cvk stores state only where it needs the state to work - lib/cth to 85b058b (mta::trivially_copyable) --- AGENTS.md | 1 + cvk/incl/cvk/render/cmd/cmd_buffer.hpp | 166 ++++++++---------- cvk/incl/cvk/render/cmd/rendering_info.hpp | 9 +- cvk/incl/cvk/render/sync/barrier.hpp | 3 +- cvk/src/render/cmd/cmd_buffer.cpp | 149 +++++++++------- cvk/src/render/sync/barrier.cpp | 22 ++- .../data/descriptor/descriptor_draw_test.cpp | 2 - cvk/tests/src/render/cmd/cmd_buffer_test.cpp | 15 ++ cvk/tests/src/render/cmd/draw_test.cpp | 6 +- cvk/tests/src/render/cmd/local_read_test.cpp | 22 +-- cvk/tests/src/render/cmd/rendering_test.cpp | 22 ++- lib/cth | 2 +- 12 files changed, 226 insertions(+), 193 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 7a66685c..e402d992 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -223,6 +223,7 @@ inline void Semaphore::debug_check(Semaphore const& semaphore) { - `CTH_CRITICAL(condition, "message") {}`: precondition check, active in debug builds only. - `CTH_STABLE_ERR(condition, "message") { forget(); throw details->exception(); }`: error that can happen at run time. - `CTH_WARN(condition, "message") {}`: suspicious state that is not an error. +- `cvk` stores state only where it needs the state to work. A check that needs more state is left to the validation layers. - `CVK_RESULT_STABLE_THROW(result != VK_SUCCESS, result, "message") {}`: throws `cvk::vk_result_exception` with the `VkResult`. - The condition is the error case: `CTH_CRITICAL(!created(), ...)`, not the expected case. diff --git a/cvk/incl/cvk/render/cmd/cmd_buffer.hpp b/cvk/incl/cvk/render/cmd/cmd_buffer.hpp index 9da5cbbc..56d08e50 100644 --- a/cvk/incl/cvk/render/cmd/cmd_buffer.hpp +++ b/cvk/incl/cvk/render/cmd/cmd_buffer.hpp @@ -12,8 +12,11 @@ #include #include +#include +#include #include +#include #include #include #include @@ -22,13 +25,25 @@ namespace cvk { class Core; class DescriptorSet; -class Image; class Pipeline; class PipelineLayout; +/** + * @brief contiguous range of trivially copyable elements + */ +template +concept push_constant_range = std::ranges::contiguous_range + && cth::mta::range_over_cpt; + +/** + * @brief @ref push_constant_range or trivially copyable value that is no pointer + */ +template +concept push_constant_data = push_constant_range || (cth::mta::trivially_copyable && !std::is_pointer_v); + /** * @brief VkCommandBuffer wrapper, allocated from its pool, base of @ref PrimaryCmdBuffer and @ref SecondaryCmdBuffer - * @details the derived classes add the begin of the recording and their commands, no members, + * @details the derived classes add the begin of the recording and their commands, * the destructor is protected: delete through the derived class */ class BaseCmdBuffer { @@ -36,36 +51,6 @@ class BaseCmdBuffer { using vk_handle_t = VkCommandBuffer; struct State; - /** - * @brief the rendering the command buffer is inside - */ - struct Rendering { - /** - * @brief what a secondary inherits, the flags without VK_RENDERING_CONTENTS_SECONDARY_COMMAND_BUFFERS_BIT - */ - RenderingInheritance inheritance; - - /** - * @brief flags of the rendering - */ - VkRenderingFlags flags = 0; - - /** - * @brief images of the attachments, empty if inherited - */ - std::vector> attachments{}; - - /** - * @brief true -> a secondary that inherits the rendering of its primary - */ - bool inherited = false; - - /** - * @brief render area, inherited -> unknown - */ - VkRect2D area{}; - }; - /** * @brief wraps the state * @pre the pool of the command buffer allocated it with the level @@ -121,48 +106,39 @@ class BaseCmdBuffer { /** * @brief begins a dynamic rendering * @details std::nullopt @ref RenderingInfo::area -> the extent that fits every attachment - * @pre @ref recording(), !@ref rendering() + * @pre @ref recording(), outside of a rendering * @pre the attachment views are created and have one mip level * @pre the attachment layouts are the layouts of the views * @pre the attachment layouts are attachment layouts (not undefined, transfer, shader read or present) * @pre color attachments have color formats, the depth and stencil attachments depth and stencil formats - * @post @ref rendering() + * @pre @ref AttachmentMapping::debug_check() passes for the color attachments + * @post @ref rendering() is the info with the area + * @note inline contents -> sets viewport 0 and scissor 0 to the area, depth 0 to 1 + * @note a mapping other than the default -> sets it after the begin */ void beginRendering(RenderingInfo const&); /** * @brief ends the dynamic rendering - * @pre @ref rendering() began with @ref beginRendering() + * @pre @ref rendering() */ void endRendering(); /** * @brief maps the attachments of the rendering to the fragment shader outputs and input attachments - * @details @ref beginRendering() sets the default mapping, the mapping holds until the next call - * @pre @ref rendering() began with @ref beginRendering() + * @details the mapping holds until the next call or the end of the rendering + * @pre @ref rendering() * @pre @ref AttachmentMapping::debug_check() passes for the color attachments of the rendering - * @post the inheritance of @ref rendering() has the mapping + * @post @ref rendering() has the mapping */ void setAttachmentMapping(AttachmentMapping const&); - /** - * @brief sets viewport 0 to the area of the rendering, depth 0 to 1 - * @pre @ref rendering() began with @ref beginRendering() - */ - void setViewport() const; - /** * @brief sets viewport 0 * @pre @ref recording() */ void setViewport(VkViewport const&) const; - /** - * @brief sets scissor 0 to the area of the rendering - * @pre @ref rendering() began with @ref beginRendering() - */ - void setScissor() const; - /** * @brief sets scissor 0 * @pre @ref recording() @@ -171,9 +147,9 @@ class BaseCmdBuffer { /** * @brief binds the pipeline to the graphics bind point - * @details inside the own rendering: sets the attachment mapping of the pipeline if the current one differs + * @details @ref rendering() -> sets the attachment mapping of the pipeline if the current one differs * @pre @ref recording(), @ref Pipeline::created() - * @pre inside an inherited rendering: the attachment mapping of the pipeline == the inherited one + * @pre a secondary inside the rendering of a primary -> the attachment mapping of the pipeline == the inherited one * @note outside of a rendering: sets no attachment mapping, the draws need the default of @ref beginRendering() */ void bindPipeline(Pipeline const&); @@ -195,30 +171,21 @@ class BaseCmdBuffer { /** * @brief records push constants + * @param data @ref push_constant_range -> the bytes of the elements, else the bytes of the value * @param offset in bytes * @pre @ref recording(), @ref PipelineLayout::created() - * @pre 0 < data.size(), offset and data.size() are multiples of 4 + * @pre data has bytes, offset and the byte count are multiples of 4 * @pre the push constant ranges of the layout cover the bytes for the stages */ - void pushConstants( - PipelineLayout const&, - VkShaderStageFlags stages, - std::span data, - uint32_t offset = 0 - ) const; - - /** - * @brief records the value as push constants - * @details calls @ref pushConstants(PipelineLayout const&, VkShaderStageFlags, std::span, uint32_t) - */ - template requires std::is_trivially_copyable_v + template void pushConstants( PipelineLayout const& layout, VkShaderStageFlags stages, - T const& value, + T const& data, uint32_t offset = 0 ) const { - pushConstants(layout, stages, std::span{reinterpret_cast(&value), sizeof(T)}, offset); + if constexpr(push_constant_range) pushConstantBytes(layout, stages, std::as_bytes(std::span{data}), offset); + else pushConstantBytes(layout, stages, std::as_bytes(std::span{&data, 1}), offset); } /** @@ -228,16 +195,6 @@ class BaseCmdBuffer { */ void bindVertexBuffer(uint32_t binding, BaseBuffer const&, size_t offset = 0) const; - /** - * @brief binds the buffer to the vertex input binding - * @param firstVertex offset in elements - * @details calls @ref bindVertexBuffer(uint32_t, BaseBuffer const&, size_t) - */ - template - void bindVertexBuffer(uint32_t binding, Buffer const& buffer, size_t firstVertex = 0) const { - bindVertexBuffer(binding, static_cast(buffer), firstVertex * sizeof(T)); - } - /** * @brief binds the index buffer * @param offset in bytes @@ -248,16 +205,17 @@ class BaseCmdBuffer { /** * @brief binds the index buffer, the index type follows from T - * @param firstIndex offset in elements + * @param offset in bytes * @details calls @ref bindIndexBuffer(BaseBuffer const&, VkIndexType, size_t) */ template - void bindIndexBuffer(Buffer const& buffer, size_t firstIndex = 0) const { - bindIndexBuffer(static_cast(buffer), index_type, firstIndex * sizeof(T)); + void bindIndexBuffer(Buffer const& buffer, size_t offset = 0) const { + bindIndexBuffer(static_cast(buffer), index_type, offset); } /** - * @pre @ref rendering() + * @pre @ref rendering() or a secondary inside the rendering of a primary + * @pre @ref rendering() -> without VK_RENDERING_CONTENTS_SECONDARY_COMMAND_BUFFERS_BIT */ void draw( uint32_t vertexCount, @@ -267,7 +225,7 @@ class BaseCmdBuffer { ) const; /** - * @pre @ref rendering(), an index buffer is bound + * @pre the preconditions of @ref draw(), an index buffer is bound */ void drawIndexed( uint32_t indexCount, @@ -280,7 +238,7 @@ class BaseCmdBuffer { /** * @brief draws with the VkDrawIndirectCommands in the buffer * @param offset in bytes - * @pre @ref rendering(), buffer created with VK_BUFFER_USAGE_INDIRECT_BUFFER_BIT + * @pre the preconditions of @ref draw(), buffer created with VK_BUFFER_USAGE_INDIRECT_BUFFER_BIT * @pre the commands lie in the buffer */ void drawIndirect( @@ -293,7 +251,8 @@ class BaseCmdBuffer { /** * @brief draws with the VkDrawIndexedIndirectCommands in the buffer * @param offset in bytes - * @pre @ref rendering(), buffer created with VK_BUFFER_USAGE_INDIRECT_BUFFER_BIT, an index buffer is bound + * @pre the preconditions of @ref draw(), buffer created with VK_BUFFER_USAGE_INDIRECT_BUFFER_BIT + * @pre an index buffer is bound * @pre the commands lie in the buffer */ void drawIndexedIndirect( @@ -324,9 +283,10 @@ class BaseCmdBuffer { } /** - * @return the rendering the command buffer records into, nullptr -> outside of a rendering + * @return info of the rendering the command buffer began, with the area and the current attachment mapping + * @return nullptr -> no rendering of the command buffer */ - [[nodiscard]] constexpr Rendering const* rendering() const { + [[nodiscard]] constexpr RenderingInfo const* rendering() const { return recording() && _rendering.has_value() ? &*_rendering : nullptr; } @@ -344,13 +304,12 @@ class BaseCmdBuffer { /** * @brief begins the recording - * @param inherited nullptr -> outside of a rendering, else the rendering of the primary * @pre @ref created() * @pre !@ref recording() * @post @ref recording() * @throws cvk::vk_result_exception result of vkBeginCommandBuffer() */ - void begin(VkCommandBufferBeginInfo const&, RenderingInheritance const* inherited = nullptr); + void begin(VkCommandBufferBeginInfo const&); private: /** @@ -369,13 +328,28 @@ class BaseCmdBuffer { not_null_ptr _pool; }; + /** + * @brief records the attachment mapping of @ref rendering() + */ + void recordAttachmentMapping() const; + + /** + * @brief records the bytes as push constants + */ + void pushConstantBytes( + PipelineLayout const&, + VkShaderStageFlags stages, + std::span data, + uint32_t offset + ) const; + void forget(); unique_handle _handle; VkCommandBufferLevel _level; bool _recording = false; uint64_t _beginResets = 0; - std::optional _rendering = std::nullopt; + std::optional _rendering = std::nullopt; public: BaseCmdBuffer(BaseCmdBuffer const& other) = delete; @@ -444,8 +418,19 @@ class PrimaryCmdBuffer : public BaseCmdBuffer { * @brief records the secondary command buffers * @pre @ref recording() * @pre the secondaries are created and not recording + * @pre @ref rendering() -> VK_RENDERING_CONTENTS_SECONDARY_COMMAND_BUFFERS_BIT + * @pre @ref rendering() -> the secondaries continue it with the current attachment mapping + * @pre outside of a rendering -> the secondaries continue no rendering */ void execute(std::span secondaries) const; + + /** + * @brief what a secondary inherits from the rendering + * @details the formats and samples of the attachments, the flags without + * VK_RENDERING_CONTENTS_SECONDARY_COMMAND_BUFFERS_BIT, the current attachment mapping + * @pre @ref rendering() + */ + [[nodiscard]] RenderingInheritance inheritance() const; }; } @@ -491,10 +476,11 @@ class SecondaryCmdBuffer : public BaseCmdBuffer { void begin(VkCommandBufferUsageFlags usage = 0); /** - * @brief begins the recording for use inside a rendering with the attachments of the inheritance + * @brief begins the recording for use inside the rendering of a primary * @param usage VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT, ... * @pre @ref created() * @pre !@ref recording() + * @pre @ref AttachmentMapping::debug_check() passes for the color formats * @post @ref recording() * @note adds VK_COMMAND_BUFFER_USAGE_RENDER_PASS_CONTINUE_BIT * @throws cvk::vk_result_exception result of vkBeginCommandBuffer() diff --git a/cvk/incl/cvk/render/cmd/rendering_info.hpp b/cvk/incl/cvk/render/cmd/rendering_info.hpp index 87b363d6..7e35edc8 100644 --- a/cvk/incl/cvk/render/cmd/rendering_info.hpp +++ b/cvk/incl/cvk/render/cmd/rendering_info.hpp @@ -84,7 +84,7 @@ struct AttachmentMapping { }; /** - * @brief formats, samples, view mask and attachment mapping of a rendering, what a secondary command buffer inherits + * @brief formats, samples, view mask, flags and attachment mapping of a rendering, what a secondary inherits from it */ struct RenderingInheritance { std::vector colorFormats{}; @@ -157,7 +157,7 @@ struct RenderingAttachment { }; /** - * @brief attachments, area and flags of a dynamic rendering + * @brief attachments, area, flags and attachment mapping of a dynamic rendering */ struct RenderingInfo { std::vector colorAttachments{}; @@ -175,6 +175,11 @@ struct RenderingInfo { * @brief VK_RENDERING_CONTENTS_SECONDARY_COMMAND_BUFFERS_BIT -> secondaries record the rendering */ VkRenderingFlags flags = 0; + + /** + * @brief mapping at the begin, @ref BaseCmdBuffer::setAttachmentMapping() changes it inside the rendering + */ + AttachmentMapping attachmentMapping{}; }; } diff --git a/cvk/incl/cvk/render/sync/barrier.hpp b/cvk/incl/cvk/render/sync/barrier.hpp index bd8fdab8..f700e64e 100644 --- a/cvk/incl/cvk/render/sync/barrier.hpp +++ b/cvk/incl/cvk/render/sync/barrier.hpp @@ -228,9 +228,10 @@ struct Barrier { private: /** * @brief checks the rules of barriers inside a dynamic rendering + * @param rendering the command buffer began */ void debug_check_rendering( - BaseCmdBuffer::Rendering const&, + RenderingInfo const& rendering, std::span, std::span, std::span diff --git a/cvk/src/render/cmd/cmd_buffer.cpp b/cvk/src/render/cmd/cmd_buffer.cpp index bea6a7e1..0fc52f00 100644 --- a/cvk/src/render/cmd/cmd_buffer.cpp +++ b/cvk/src/render/cmd/cmd_buffer.cpp @@ -40,7 +40,18 @@ namespace { void debug_check_draw(BaseCmdBuffer const& cmdBuffer) { BaseCmdBuffer::debug_check(cmdBuffer); - CTH_CRITICAL(cmdBuffer.rendering() == nullptr, "draws need a rendering") {} + + // a secondary can be inside the rendering of its primary + auto const* rendering = cmdBuffer.rendering(); + CTH_CRITICAL( + rendering == nullptr && cmdBuffer.level() == VK_COMMAND_BUFFER_LEVEL_PRIMARY, + "draws need a rendering" + ) {} + CTH_CRITICAL( + rendering != nullptr && (rendering->flags & VK_RENDERING_CONTENTS_SECONDARY_COMMAND_BUFFERS_BIT) != 0 + && (rendering->flags & VK_RENDERING_CONTENTS_INLINE_BIT_KHR) == 0, + "the secondaries record the draws of a rendering with VK_RENDERING_CONTENTS_SECONDARY_COMMAND_BUFFERS_BIT" + ) {} } void debug_check_indirect( @@ -130,7 +141,7 @@ BaseCmdBuffer::State BaseCmdBuffer::release() { void BaseCmdBuffer::end() { debug_check(*this); CTH_CRITICAL(!recording(), "command buffer must be recording") {} - CTH_CRITICAL(rendering() != nullptr && !rendering()->inherited, "rendering must be ended first") {} + CTH_CRITICAL(rendering() != nullptr, "rendering must be ended first") {} auto const result = core().functions()->vkEndCommandBuffer(get()); _recording = false; @@ -157,14 +168,8 @@ void BaseCmdBuffer::beginRendering(RenderingInfo const& info) { debug_check(*this); CTH_CRITICAL(!recording(), "command buffer must be recording") {} CTH_CRITICAL(rendering() != nullptr, "command buffer must be outside of a rendering") {} + AttachmentMapping::debug_check(info.attachmentMapping, info.colorAttachments.size()); - Rendering rendering{ - .inheritance = { - .viewMask = info.viewMask, - .flags = info.flags & ~VK_RENDERING_CONTENTS_SECONDARY_COMMAND_BUFFERS_BIT, - }, - .flags = info.flags, - }; std::optional extent{}; auto const attachmentInfo = [&](RenderingAttachment const& attachment) { @@ -176,8 +181,6 @@ void BaseCmdBuffer::beginRendering(RenderingInfo const& info) { extent = extent.has_value() ? VkExtent2D{std::min(extent->width, viewExtent.width), std::min(extent->height, viewExtent.height)} : viewExtent; - rendering.inheritance.samples = view.image().config().samples; - rendering.attachments.emplace_back(&view.image()); debug_check_attachment_layout(attachment.layout); VkImageView resolveView = VK_NULL_HANDLE; @@ -190,7 +193,6 @@ void BaseCmdBuffer::beginRendering(RenderingInfo const& info) { resolveView = attachment.resolveView->get(); resolveLayout = attachment.resolveLayout; - rendering.attachments.emplace_back(&attachment.resolveView->image()); } return VkRenderingAttachmentInfo{ @@ -216,7 +218,6 @@ void BaseCmdBuffer::beginRendering(RenderingInfo const& info) { CTH_CRITICAL(format_aspects(format) != VK_IMAGE_ASPECT_COLOR_BIT, "color attachments need a color format") { details->add("format: {}", format); } - rendering.inheritance.colorFormats.push_back(format); } std::optional depth{}; @@ -230,7 +231,6 @@ void BaseCmdBuffer::beginRendering(RenderingInfo const& info) { ) { details->add("format: {}", format); } - rendering.inheritance.depthFormat = format; } std::optional stencil{}; @@ -244,7 +244,6 @@ void BaseCmdBuffer::beginRendering(RenderingInfo const& info) { ) { details->add("format: {}", format); } - rendering.inheritance.stencilFormat = format; } CTH_CRITICAL(!extent.has_value() && !info.area.has_value(), "a rendering without attachments needs an area") {} @@ -263,40 +262,16 @@ void BaseCmdBuffer::beginRendering(RenderingInfo const& info) { }; core().functions()->vkCmdBeginRendering(get(), &renderingInfo); - rendering.area = renderingInfo.renderArea; - _rendering = std::move(rendering); -} - -void BaseCmdBuffer::endRendering() { - debug_check(*this); - CTH_CRITICAL(rendering() == nullptr || rendering()->inherited, "command buffer must be inside its own rendering") {} - - core().functions()->vkCmdEndRendering(get()); - - _rendering.reset(); -} - -void BaseCmdBuffer::setAttachmentMapping(AttachmentMapping const& mapping) { - debug_check(*this); - CTH_CRITICAL(rendering() == nullptr || rendering()->inherited, "command buffer must be inside its own rendering") {} - - auto& inheritance = _rendering->inheritance; - AttachmentMapping::debug_check(mapping, inheritance.colorFormats.size()); + _rendering = info; + _rendering->area = renderingInfo.renderArea; + if(_rendering->attachmentMapping != AttachmentMapping{}) recordAttachmentMapping(); - inheritance.attachmentMapping = mapping; + // secondaries do not inherit dynamic state + bool const inlineContents = (info.flags & VK_RENDERING_CONTENTS_SECONDARY_COMMAND_BUFFERS_BIT) == 0 + || (info.flags & VK_RENDERING_CONTENTS_INLINE_BIT_KHR) != 0; + if(!inlineContents) return; - auto const colorAttachments = static_cast(inheritance.colorFormats.size()); - auto const locations = inheritance.attachmentMapping.locationInfo(colorAttachments); - auto const inputIndices = inheritance.attachmentMapping.inputIndexInfo(colorAttachments); - core().functions()->vkCmdSetRenderingAttachmentLocationsKHR(get(), &locations); - core().functions()->vkCmdSetRenderingInputAttachmentIndicesKHR(get(), &inputIndices); -} - -void BaseCmdBuffer::setViewport() const { - debug_check(*this); - CTH_CRITICAL(rendering() == nullptr || rendering()->inherited, "command buffer must be inside its own rendering") {} - - auto const& area = rendering()->area; + auto const& area = renderingInfo.renderArea; setViewport( VkViewport{ .x = static_cast(area.offset.x), @@ -307,20 +282,32 @@ void BaseCmdBuffer::setViewport() const { .maxDepth = 1.f, } ); + setScissor(area); } -void BaseCmdBuffer::setViewport(VkViewport const& viewport) const { +void BaseCmdBuffer::endRendering() { debug_check(*this); - CTH_CRITICAL(!recording(), "command buffer must be recording") {} + CTH_CRITICAL(rendering() == nullptr, "command buffer must be inside its own rendering") {} - core().functions()->vkCmdSetViewport(get(), 0, 1, &viewport); + core().functions()->vkCmdEndRendering(get()); + + _rendering.reset(); } -void BaseCmdBuffer::setScissor() const { +void BaseCmdBuffer::setAttachmentMapping(AttachmentMapping const& mapping) { debug_check(*this); - CTH_CRITICAL(rendering() == nullptr || rendering()->inherited, "command buffer must be inside its own rendering") {} + CTH_CRITICAL(rendering() == nullptr, "command buffer must be inside its own rendering") {} + AttachmentMapping::debug_check(mapping, _rendering->colorAttachments.size()); - setScissor(rendering()->area); + _rendering->attachmentMapping = mapping; + recordAttachmentMapping(); +} + +void BaseCmdBuffer::setViewport(VkViewport const& viewport) const { + debug_check(*this); + CTH_CRITICAL(!recording(), "command buffer must be recording") {} + + core().functions()->vkCmdSetViewport(get(), 0, 1, &viewport); } void BaseCmdBuffer::setScissor(VkRect2D const& scissor) const { @@ -335,14 +322,8 @@ void BaseCmdBuffer::bindPipeline(Pipeline const& pipeline) { CTH_CRITICAL(!recording(), "command buffer must be recording") {} Pipeline::debug_check(pipeline); - auto const* current = rendering(); auto const& mapping = pipeline.config().attachmentMapping; - CTH_CRITICAL( - current != nullptr && current->inherited && mapping != current->inheritance.attachmentMapping, - "the attachment mapping of the pipeline must match the inherited mapping" - ) {} - if(current != nullptr && !current->inherited && mapping != current->inheritance.attachmentMapping) - setAttachmentMapping(mapping); + if(rendering() != nullptr && mapping != rendering()->attachmentMapping) setAttachmentMapping(mapping); core().functions()->vkCmdBindPipeline(get(), VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline.get()); } @@ -384,10 +365,10 @@ void BaseCmdBuffer::bindDescriptorSet( ); } -void BaseCmdBuffer::pushConstants( +void BaseCmdBuffer::pushConstantBytes( PipelineLayout const& layout, VkShaderStageFlags stages, - span data, + span data, uint32_t offset ) const { debug_check(*this); @@ -492,7 +473,7 @@ void BaseCmdBuffer::destroy(DeviceTable table, VkCommandPool vkPool, spanvkFreeCommandBuffers(table.device(), vkPool, static_cast(vkBuffers.size()), vkBuffers.data()); } -void BaseCmdBuffer::begin(VkCommandBufferBeginInfo const& info, RenderingInheritance const* inherited) { +void BaseCmdBuffer::begin(VkCommandBufferBeginInfo const& info) { debug_check(*this); CTH_CRITICAL(recording(), "command buffer must not be recording") {} @@ -502,8 +483,16 @@ void BaseCmdBuffer::begin(VkCommandBufferBeginInfo const& info, RenderingInherit _recording = true; _beginResets = pool().resets(); _rendering.reset(); - if(inherited != nullptr) - _rendering = Rendering{.inheritance = *inherited, .flags = inherited->flags, .inherited = true}; +} + +void BaseCmdBuffer::recordAttachmentMapping() const { + auto const& mapping = _rendering->attachmentMapping; + auto const colorAttachments = static_cast(_rendering->colorAttachments.size()); + + auto const locations = mapping.locationInfo(colorAttachments); + auto const inputIndices = mapping.inputIndexInfo(colorAttachments); + core().functions()->vkCmdSetRenderingAttachmentLocationsKHR(get(), &locations); + core().functions()->vkCmdSetRenderingInputAttachmentIndicesKHR(get(), &inputIndices); } void BaseCmdBuffer::forget() { @@ -544,6 +533,7 @@ void PrimaryCmdBuffer::execute(span secondaries) const ), "secondaries must be created and not recording" ) {} + CTH_CRITICAL( rendering() != nullptr && (rendering()->flags & VK_RENDERING_CONTENTS_SECONDARY_COMMAND_BUFFERS_BIT) == 0, "the rendering must have VK_RENDERING_CONTENTS_SECONDARY_COMMAND_BUFFERS_BIT" @@ -553,6 +543,33 @@ void PrimaryCmdBuffer::execute(span secondaries) const core().functions()->vkCmdExecuteCommands(get(), static_cast(vkBuffers.size()), vkBuffers.data()); } + +RenderingInheritance PrimaryCmdBuffer::inheritance() const { + debug_check(*this); + CTH_CRITICAL(rendering() == nullptr, "command buffer must be inside its own rendering") {} + + auto const& info = *rendering(); + auto const format = [](RenderingAttachment const& attachment) { return attachment.view->image().format(); }; + + // every attachment has the same sample count + auto const samples = [&info] { + auto const of = [](RenderingAttachment const& attachment) { return attachment.view->image().config().samples; }; + if(!info.colorAttachments.empty()) return of(info.colorAttachments.front()); + if(info.depthAttachment.has_value()) return of(*info.depthAttachment); + if(info.stencilAttachment.has_value()) return of(*info.stencilAttachment); + return VK_SAMPLE_COUNT_1_BIT; + }; + + return RenderingInheritance{ + .colorFormats = {std::from_range, info.colorAttachments | std::views::transform(format)}, + .depthFormat = info.depthAttachment.has_value() ? format(*info.depthAttachment) : VK_FORMAT_UNDEFINED, + .stencilFormat = info.stencilAttachment.has_value() ? format(*info.stencilAttachment) : VK_FORMAT_UNDEFINED, + .samples = samples(), + .viewMask = info.viewMask, + .flags = info.flags & ~VK_RENDERING_CONTENTS_SECONDARY_COMMAND_BUFFERS_BIT, + .attachmentMapping = info.attachmentMapping, + }; +} } namespace cvk { @@ -610,6 +627,6 @@ void SecondaryCmdBuffer::begin(Inheritance const& inheritance, VkCommandBufferUs .flags = usage | VK_COMMAND_BUFFER_USAGE_RENDER_PASS_CONTINUE_BIT, .pInheritanceInfo = &inheritanceInfo, }; - BaseCmdBuffer::begin(info, &inheritance); + BaseCmdBuffer::begin(info); } } diff --git a/cvk/src/render/sync/barrier.cpp b/cvk/src/render/sync/barrier.cpp index dcb48ff6..50d8accd 100644 --- a/cvk/src/render/sync/barrier.cpp +++ b/cvk/src/render/sync/barrier.cpp @@ -3,6 +3,7 @@ #include "cvk/base/core.hpp" #include "cvk/data/buffer/base_buffer.hpp" #include "cvk/data/img/image.hpp" +#include "cvk/data/img/image_view.hpp" #include "cvk/render/cmd/cmd_buffer.hpp" #include @@ -102,7 +103,7 @@ namespace { } void Barrier::debug_check_rendering( - BaseCmdBuffer::Rendering const& rendering, + RenderingInfo const& rendering, std::span memoryBarriers, std::span bufferBarriers, std::span imageBarriers @@ -127,7 +128,7 @@ void Barrier::debug_check_rendering( "barriers inside a rendering need VK_DEPENDENCY_BY_REGION_BIT" ) {} CTH_CRITICAL( - std::popcount(rendering.inheritance.viewMask) > 1 && (flags & VK_DEPENDENCY_VIEW_LOCAL_BIT) == 0, + std::popcount(rendering.viewMask) > 1 && (flags & VK_DEPENDENCY_VIEW_LOCAL_BIT) == 0, "barriers inside a rendering with more than one view need VK_DEPENDENCY_VIEW_LOCAL_BIT" ) {} auto const transfer = [](VkBufferMemoryBarrier2 const& barrier) { @@ -138,19 +139,24 @@ void Barrier::debug_check_rendering( "barriers inside a rendering have no queue family ownership transfer" ) {} + auto const attached = [&rendering](Image const& image) { + auto const uses = [&image](RenderingAttachment const& attachment) { + return &attachment.view->image() == &image + || (attachment.resolveView != nullptr && &attachment.resolveView->image() == &image); + }; + return std::ranges::any_of(rendering.colorAttachments, uses) + || (rendering.depthAttachment.has_value() && uses(*rendering.depthAttachment)) + || (rendering.stencilAttachment.has_value() && uses(*rendering.stencilAttachment)); + }; + for(auto const [barrier, entry] : std::views::zip(imageBarriers, images)) { CTH_CRITICAL(barrier.oldLayout != barrier.newLayout, "barriers inside a rendering have no layout transition") {} CTH_CRITICAL( barrier.srcQueueFamilyIndex != barrier.dstQueueFamilyIndex, "barriers inside a rendering have no queue family ownership transfer" ) {} - bool const attachment = std::ranges::contains( - rendering.attachments, - entry.image.get(), - ¬_null_ptr::get - ); CTH_CRITICAL( - attachment && barrier.newLayout != VK_IMAGE_LAYOUT_RENDERING_LOCAL_READ_KHR + attached(*entry.image) && barrier.newLayout != VK_IMAGE_LAYOUT_RENDERING_LOCAL_READ_KHR && barrier.newLayout != VK_IMAGE_LAYOUT_GENERAL, "attachments inside a rendering must be in the rendering local read or general layout" ) { diff --git a/cvk/tests/src/data/descriptor/descriptor_draw_test.cpp b/cvk/tests/src/data/descriptor/descriptor_draw_test.cpp index 29141bea..e29e8bd4 100644 --- a/cvk/tests/src/data/descriptor/descriptor_draw_test.cpp +++ b/cvk/tests/src/data/descriptor/descriptor_draw_test.cpp @@ -133,8 +133,6 @@ namespace { } ); cmd.bindPipeline(fill.pipeline); - cmd.setViewport(); - cmd.setScissor(); cmd.bindVertexBuffer(0, fill.vertices); draw(cmd); cmd.draw(3); diff --git a/cvk/tests/src/render/cmd/cmd_buffer_test.cpp b/cvk/tests/src/render/cmd/cmd_buffer_test.cpp index 4ec40b6c..4eff2401 100644 --- a/cvk/tests/src/render/cmd/cmd_buffer_test.cpp +++ b/cvk/tests/src/render/cmd/cmd_buffer_test.cpp @@ -4,7 +4,10 @@ #include "cvk/render/cmd/cmd_buffer.hpp" #include +#include +#include #include +#include namespace cvk { @@ -27,6 +30,18 @@ CVK_TEST(CmdBuffer, inheritance_rendering_info) { }()); } +CVK_TEST(CmdBuffer, push_constant_data) { + static_assert(push_constant_range>); + static_assert(push_constant_range>); + static_assert(push_constant_range>); + static_assert(!push_constant_range>); + + static_assert(push_constant_data); + static_assert(push_constant_data>); + static_assert(!push_constant_data); + static_assert(!push_constant_data>); +} + CVK_TEST(CmdBuffer, delete_only_through_the_derived_class) { static_assert(!std::is_destructible_v); static_assert(std::is_destructible_v); diff --git a/cvk/tests/src/render/cmd/draw_test.cpp b/cvk/tests/src/render/cmd/draw_test.cpp index 7d07517b..4e7a23e8 100644 --- a/cvk/tests/src/render/cmd/draw_test.cpp +++ b/cvk/tests/src/render/cmd/draw_test.cpp @@ -15,6 +15,7 @@ #include #include #include +#include namespace cvk { @@ -111,10 +112,9 @@ namespace { ); cmd.bindPipeline(fill.pipeline); - cmd.setViewport(); - cmd.setScissor(); cmd.bindVertexBuffer(0, fill.vertices); - cmd.pushConstants(fill.layout, VK_SHADER_STAGE_FRAGMENT_BIT, GREEN); + // a span pushes its elements, not itself + cmd.pushConstants(fill.layout, VK_SHADER_STAGE_FRAGMENT_BIT, std::span{GREEN}); draw(cmd); cmd.endRendering(); diff --git a/cvk/tests/src/render/cmd/local_read_test.cpp b/cvk/tests/src/render/cmd/local_read_test.cpp index ffc03f2d..9e107855 100644 --- a/cvk/tests/src/render/cmd/local_read_test.cpp +++ b/cvk/tests/src/render/cmd/local_read_test.cpp @@ -91,7 +91,7 @@ namespace { /** * @brief both attachments cleared to black */ - [[nodiscard]] RenderingInfo renderingInfo(VkRenderingFlags flags = 0) const { + [[nodiscard]] RenderingInfo renderingInfo(VkRenderingFlags flags = 0, AttachmentMapping mapping = {}) const { auto const clear = [](ImageView const& view, VkImageLayout layout) { return RenderingAttachment{ .view = &view, @@ -107,6 +107,7 @@ namespace { clear(secondView, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL), }, .flags = flags, + .attachmentMapping = std::move(mapping), }; } @@ -250,13 +251,11 @@ CVK_TEST(LocalRead, draw_reads_an_attachment_of_the_same_rendering) { test::record_and_submit(context, [&](PrimaryCmdBuffer& cmd) { attachments.toRendering(cmd); - cmd.beginRendering(attachments.renderingInfo()); - cmd.setViewport(); - cmd.setScissor(); + // the begin sets the mapping of the first draw, the bind of the second pipeline its own + cmd.beginRendering(attachments.renderingInfo(0, write_first())); + EXPECT_EQ(cmd.rendering()->attachmentMapping, write_first()); - // the binds set the attachment mapping of the pipelines draws.drawWrite(cmd); - EXPECT_EQ(cmd.rendering()->inheritance.attachmentMapping, write_first()); Barrier toInput{VK_DEPENDENCY_BY_REGION_BIT}; toInput.add( @@ -271,7 +270,7 @@ CVK_TEST(LocalRead, draw_reads_an_attachment_of_the_same_rendering) { toInput.execute(cmd); cmd.bindPipeline(draws.read); - EXPECT_EQ(cmd.rendering()->inheritance.attachmentMapping, copy_first_to_second()); + EXPECT_EQ(cmd.rendering()->attachmentMapping, copy_first_to_second()); cmd.bindDescriptorSet(draws.input, draws.readLayout, 0); cmd.draw(3); cmd.endRendering(); @@ -310,15 +309,16 @@ CVK_TEST(LocalRead, secondary_inherits_the_mapping) { test::record_and_submit(context, [&](PrimaryCmdBuffer& cmd) { attachments.toRendering(cmd); cmd.beginRendering(attachments.renderingInfo(VK_RENDERING_CONTENTS_SECONDARY_COMMAND_BUFFERS_BIT)); - EXPECT_EQ(cmd.rendering()->inheritance.attachmentMapping, AttachmentMapping{}); + EXPECT_EQ(cmd.rendering()->attachmentMapping, AttachmentMapping{}); cmd.setAttachmentMapping(write_first()); - EXPECT_EQ(cmd.rendering()->inheritance.attachmentMapping, write_first()); + EXPECT_EQ(cmd.rendering()->attachmentMapping, write_first()); // the draw needs the mapping of the write pipeline in the secondary auto& secondary = secondaries[0]; - secondary.begin(cmd.rendering()->inheritance, VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT); - EXPECT_EQ(secondary.rendering()->inheritance.attachmentMapping, write_first()); + auto const inheritance = cmd.inheritance(); + EXPECT_EQ(inheritance.attachmentMapping, write_first()); + secondary.begin(inheritance, VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT); secondary.setViewport( {0.f, 0.f, static_cast(EXTENT.width), static_cast(EXTENT.height), 0.f, 1.f} ); diff --git a/cvk/tests/src/render/cmd/rendering_test.cpp b/cvk/tests/src/render/cmd/rendering_test.cpp index 43618b81..b7d0478b 100644 --- a/cvk/tests/src/render/cmd/rendering_test.cpp +++ b/cvk/tests/src/render/cmd/rendering_test.cpp @@ -101,11 +101,15 @@ CVK_TEST(Rendering, tracks_the_current_rendering) { cmd.beginRendering({.colorAttachments = {clear_red(view)}}); auto const* rendering = cmd.rendering(); ASSERT_NE(rendering, nullptr); - EXPECT_FALSE(rendering->inherited); - ASSERT_EQ(rendering->inheritance.colorFormats.size(), 1); - EXPECT_EQ(rendering->inheritance.colorFormats[0], COLOR_CONFIG.format); - ASSERT_EQ(rendering->attachments.size(), 1); - EXPECT_EQ(rendering->attachments[0].get(), &image); + ASSERT_EQ(rendering->colorAttachments.size(), 1); + EXPECT_EQ(rendering->colorAttachments[0].view.get(), &view); + ASSERT_TRUE(rendering->area.has_value()); + EXPECT_EQ(rendering->area->extent.width, EXTENT.width); + EXPECT_EQ(rendering->area->extent.height, EXTENT.height); + + auto const inheritance = cmd.inheritance(); + ASSERT_EQ(inheritance.colorFormats.size(), 1); + EXPECT_EQ(inheritance.colorFormats[0], COLOR_CONFIG.format); cmd.endRendering(); EXPECT_EQ(cmd.rendering(), nullptr); @@ -132,7 +136,7 @@ CVK_TEST(Rendering, clear_a_depth_attachment) { }, } ); - EXPECT_EQ(cmd.rendering()->inheritance.depthFormat, VK_FORMAT_D16_UNORM); + EXPECT_EQ(cmd.inheritance().depthFormat, VK_FORMAT_D16_UNORM); cmd.endRendering(); }); } @@ -153,9 +157,9 @@ CVK_TEST(Rendering, secondary_inherits_the_rendering) { ); auto& secondary = secondaries[0]; - secondary.begin(cmd.rendering()->inheritance, VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT); - ASSERT_NE(secondary.rendering(), nullptr); - EXPECT_TRUE(secondary.rendering()->inherited); + secondary.begin(cmd.inheritance(), VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT); + EXPECT_TRUE(secondary.recording()); + EXPECT_EQ(secondary.rendering(), nullptr); secondary.end(); cmd.execute(secondaries); diff --git a/lib/cth b/lib/cth index e6a384d7..85b058be 160000 --- a/lib/cth +++ b/lib/cth @@ -1 +1 @@ -Subproject commit e6a384d73dd08f758b8a6446827bf1ea099dcbea +Subproject commit 85b058bea610416c1d710687f3328f12efa234b0 From e57a7c14fdd284726c3ac78ae532c0eacc484f4d Mon Sep 17 00:00:00 2001 From: lukas <89145039+codethinki@users.noreply.github.com> Date: Mon, 19 Oct 2026 19:54:52 +0200 Subject: [PATCH 067/135] cvk: fix failed core create, device queues after wrap, messenger wrap - fix: a throw of the Device or the Allocator in Core::create() left the physical device and the queue set index set, the pick returns them now and create() sets the members after the last step that can throw - fix: a failed queue retrieval threw after Device::create() created the device, vkGetDeviceQueue() has no failure, a null queue is a debug check now - fix: a wrapped Device had no queues, Core::wrap() then had none either, Device::State has the queue family indices, wrap() retrieves the queues - fix: DebugMessenger had release() and State but no wrap(), wrap() and the wrap constructor added --- cvk/incl/cvk/base/core.hpp | 19 ++++++--- cvk/incl/cvk/base/device.hpp | 10 ++++- cvk/incl/cvk/debug/debug_messenger.hpp | 16 ++++++++ cvk/src/base/core.cpp | 41 +++++++++++--------- cvk/src/base/device.cpp | 8 +++- cvk/src/debug/debug_messenger.cpp | 11 ++++++ cvk/tests/src/base/device_test.cpp | 6 ++- cvk/tests/src/debug/debug_messenger_test.cpp | 18 +++++++++ 8 files changed, 99 insertions(+), 30 deletions(-) diff --git a/cvk/incl/cvk/base/core.hpp b/cvk/incl/cvk/base/core.hpp index a1782528..cded378f 100644 --- a/cvk/incl/cvk/base/core.hpp +++ b/cvk/incl/cvk/base/core.hpp @@ -139,9 +139,18 @@ class Core { [[nodiscard]] constexpr std::span queues() const { return _queues; } private: - struct queue_mappings { + /** + * @brief physical device for a queue set and the mapping of the queues + */ + struct device_pick { + std::unique_ptr physicalDevice; std::vector uniqueQueueFamilyIndices; std::vector queuesToUniqueQueues; + + /** + * @brief std::nullopt -> the config had no queue sets + */ + std::optional queueSetIndex = std::nullopt; }; struct queue_set_relations { std::vector uniqueQueues; @@ -151,10 +160,10 @@ class Core { void forget(); /** - * @post sets @ref _physicalDevice and @ref _queueSetIndex + * @brief picks a physical device for the first possible queue set * @throws cvk::cvk_exception reason: no physical device meets the requirements */ - [[nodiscard]] queue_mappings createPhysicalDevice(std::span surfaces, Config const&); + [[nodiscard]] device_pick pickPhysicalDevice(std::span surfaces, Config const&) const; [[nodiscard]] static queue_set_relations createQueueSetRelations( queue_set_t const& queueSet, @@ -164,11 +173,11 @@ class Core { /** * @return std::nullopt -> no physical device supports the queue set */ - [[nodiscard]] std::optional tryCreatePhysicalDevice( + [[nodiscard]] std::optional tryPickPhysicalDevice( std::span surfaces, Config const&, queue_set_t const& queueSet - ); + ) const; void createQueues(std::span queuesToUniqueQueues); diff --git a/cvk/incl/cvk/base/device.hpp b/cvk/incl/cvk/base/device.hpp index f761d6ac..c9d98297 100644 --- a/cvk/incl/cvk/base/device.hpp +++ b/cvk/incl/cvk/base/device.hpp @@ -60,11 +60,11 @@ class Device { ~Device() { optDestroy(); } /** - * @brief wraps the state + * @brief wraps the state and retrieves the queues of @ref State::queueFamilyIndices * @details nullptr @ref State::functionTable -> loads the functions + * @pre the device has the queues of @ref State::queueFamilyIndices * @post @ref created() * @note calls @ref optDestroy() - * @note does not retrieve queues */ void wrap(State); @@ -136,6 +136,7 @@ class Device { /** * @brief retrieves the queues * @param familyIndices family index of each queue + * @pre the device has the queues */ void createQueues(std::span familyIndices); @@ -167,6 +168,11 @@ struct Device::State { * @details nullptr -> @ref wrap() loads it */ std::unique_ptr functionTable; + + /** + * @brief family index of each queue, see @ref Config::queueFamilyIndices + */ + std::vector queueFamilyIndices{}; }; } diff --git a/cvk/incl/cvk/debug/debug_messenger.hpp b/cvk/incl/cvk/debug/debug_messenger.hpp index b51d9002..b7ebedf7 100644 --- a/cvk/incl/cvk/debug/debug_messenger.hpp +++ b/cvk/incl/cvk/debug/debug_messenger.hpp @@ -29,6 +29,14 @@ class DebugMessenger { */ DebugMessenger(Instance const&, Config); + /** + * @brief constructs and wraps + * @details calls: + * - @ref DebugMessenger(Instance const&, Config) + * - @ref wrap() + */ + DebugMessenger(Instance const&, Config, State const&); + /** * @brief constructs and creates * @details calls: @@ -43,6 +51,14 @@ class DebugMessenger { */ ~DebugMessenger() { optDestroy(); } + /** + * @brief wraps the state + * @pre the instance created the messenger + * @post @ref created() + * @note calls @ref optDestroy() + */ + void wrap(State const&); + /** * @brief creates the messenger * @pre the instance enables VK_EXT_debug_utils diff --git a/cvk/src/base/core.cpp b/cvk/src/base/core.cpp index 2d5bdc8a..5a8da9c4 100644 --- a/cvk/src/base/core.cpp +++ b/cvk/src/base/core.cpp @@ -57,20 +57,21 @@ void Core::create(span surfaces, Config const& config) { optDestroy(); - auto [uniqueQueueFamilyIndices, queuesToUniqueQueues] = createPhysicalDevice(surfaces, config); - + // the members change after the last step that can throw, a throw leaves the core not created + auto pick = pickPhysicalDevice(surfaces, config); auto device = std::make_unique( *_instance, - *_physicalDevice, - Device::Config{std::move(uniqueQueueFamilyIndices)} + *pick.physicalDevice, + Device::Config{std::move(pick.uniqueQueueFamilyIndices)} ); - // a throw destroys the device, the core stays not created auto allocator = std::make_unique(*device, cvk::create); + _physicalDevice = std::move(pick.physicalDevice); _device = std::move(device); _allocator = std::move(allocator); + _queueSetIndex = pick.queueSetIndex; - createQueues(queuesToUniqueQueues); + createQueues(pick.queuesToUniqueQueues); } void Core::destroy() { @@ -129,19 +130,19 @@ void Core::forget() { _queueSetIndex = std::nullopt; } -auto Core::createPhysicalDevice(span surfaces, Config const& config) -> queue_mappings { +auto Core::pickPhysicalDevice(span surfaces, Config const& config) const -> device_pick { if(config.queueSets.empty()) { - auto mappings = tryCreatePhysicalDevice(surfaces, config, queue_set_t{config.queueProperties.size()}); - CVK_STABLE_THROW(!mappings, "no physical device supports the queues") {} - return *std::move(mappings); + auto pick = tryPickPhysicalDevice(surfaces, config, queue_set_t{config.queueProperties.size()}); + CVK_STABLE_THROW(!pick, "no physical device supports the queues") {} + return *std::move(pick); } for(auto const& [setIndex, queueSet] : std::views::enumerate(config.queueSets)) { - auto mappings = tryCreatePhysicalDevice(surfaces, config, queueSet); - if(!mappings) continue; + auto pick = tryPickPhysicalDevice(surfaces, config, queueSet); + if(!pick) continue; - _queueSetIndex = static_cast(setIndex); - return *std::move(mappings); + pick->queueSetIndex = static_cast(setIndex); + return *std::move(pick); } CVK_STABLE_THROW(true, "no physical device supports any queue set") {} @@ -173,11 +174,11 @@ auto Core::createQueueSetRelations( return {std::move(uniqueQueues), std::move(queueToUniqueQueue)}; } -auto Core::tryCreatePhysicalDevice( +auto Core::tryPickPhysicalDevice( span surfaces, Config const& config, queue_set_t const& queueSet -) -> std::optional { +) const -> std::optional { CTH_CRITICAL(queueSet.size() != config.queueProperties.size(), "queue set must span all queues") {} auto [uniqueQueues, queueToUniqueQueue] = createQueueSetRelations(queueSet, config.queueProperties); @@ -191,9 +192,11 @@ auto Core::tryCreatePhysicalDevice( ); if(!result) return std::nullopt; - _physicalDevice = std::make_unique(std::move(result->device)); - - return queue_mappings{std::move(result->queueFamilyIndices), std::move(queueToUniqueQueue)}; + return device_pick{ + .physicalDevice = std::make_unique(std::move(result->device)), + .uniqueQueueFamilyIndices = std::move(result->queueFamilyIndices), + .queuesToUniqueQueues = std::move(queueToUniqueQueue), + }; } void Core::createQueues(span queuesToUniqueQueues) { diff --git a/cvk/src/base/device.cpp b/cvk/src/base/device.cpp index ec61f250..dcb1d334 100644 --- a/cvk/src/base/device.cpp +++ b/cvk/src/base/device.cpp @@ -8,6 +8,7 @@ #include #include +#include namespace cvk { @@ -41,6 +42,7 @@ void Device::wrap(State state) { } _handle.reset(state.vkDevice.get()); + createQueues(state.queueFamilyIndices); } void Device::create(Config const& config) { @@ -66,7 +68,8 @@ Device::State Device::release() { State state{ .vkDevice = _handle.release(), - .functionTable = std::move(_functionTable) + .functionTable = std::move(_functionTable), + .queueFamilyIndices = {std::from_range, _queues | std::views::transform(&Queue::familyIndex)}, }; forget(); return state; @@ -178,7 +181,8 @@ void Device::createQueues(span familyIndices) { VkQueue ptr = VK_NULL_HANDLE; table()->vkGetDeviceQueue(_handle.get(), familyIndex, queueIndex, &ptr); - CVK_STABLE_THROW(ptr == VK_NULL_HANDLE, "failed to get device queue") {} + // vkGetDeviceQueue() has no failure, a null queue means the device has no such queue + CTH_CRITICAL(ptr == VK_NULL_HANDLE, "the device has no queue {} in family {}", queueIndex, familyIndex) {} _queues.emplace_back( _physicalDevice->queueFamily(familyIndex)->properties, diff --git a/cvk/src/debug/debug_messenger.cpp b/cvk/src/debug/debug_messenger.cpp index 9b009125..5abaa597 100644 --- a/cvk/src/debug/debug_messenger.cpp +++ b/cvk/src/debug/debug_messenger.cpp @@ -12,11 +12,22 @@ namespace cvk { DebugMessenger::DebugMessenger(Instance const& instance, Config config) : _config{std::move(config)}, _handle{instance.get()} { Instance::debug_check(instance); } +DebugMessenger::DebugMessenger(Instance const& instance, Config config, State const& state) : DebugMessenger{ + instance, + std::move(config) +} { wrap(state); } + DebugMessenger::DebugMessenger(Instance const& instance, Config config, create_t) : DebugMessenger{ instance, std::move(config) } { create(); } +void DebugMessenger::wrap(State const& state) { + optDestroy(); + + _handle.reset(state.vkMessenger.get(), _handle.vkInstance()); +} + void DebugMessenger::create() { optDestroy(); diff --git a/cvk/tests/src/base/device_test.cpp b/cvk/tests/src/base/device_test.cpp index d7cc4940..9fe79785 100644 --- a/cvk/tests/src/base/device_test.cpp +++ b/cvk/tests/src/base/device_test.cpp @@ -131,13 +131,15 @@ CVK_TEST(Device, release_and_wrap) { EXPECT_TRUE(device.queues().empty()); EXPECT_EQ(state.vkDevice.get(), vkDevice); ASSERT_NE(state.functionTable, nullptr); + EXPECT_EQ(state.queueFamilyIndices, familyIndices); Device const wrapped{instance, physicalDevice, std::move(state)}; ASSERT_TRUE(wrapped.created()); EXPECT_EQ(wrapped.get(), vkDevice); - EXPECT_TRUE(wrapped.queues().empty()); - wrapped.waitIdle(); + ASSERT_EQ(wrapped.queues().size(), familyIndices.size()); + for(auto const& queue : wrapped.queues()) + queue.wait(); } CVK_TEST(Device, wrap_without_table_loads_it) { diff --git a/cvk/tests/src/debug/debug_messenger_test.cpp b/cvk/tests/src/debug/debug_messenger_test.cpp index 128858f2..c7ae35ea 100644 --- a/cvk/tests/src/debug/debug_messenger_test.cpp +++ b/cvk/tests/src/debug/debug_messenger_test.cpp @@ -117,6 +117,24 @@ CVK_TEST(DebugMessenger, release_keeps_the_messenger) { DebugMessenger::destroy(instance.get(), state.vkMessenger.get()); } +CVK_TEST(DebugMessenger, release_and_wrap) { + if constexpr(!constants::ENABLE_VALIDATION_LAYERS) GTEST_SKIP() << "needs the validation layers"; + + Instance const instance{APP_NAME, NO_EXTENSIONS, std::nullopt}; + DebugMessenger messenger{instance, DebugMessenger::Config::Default(count_test_messages), create}; + auto const vkMessenger = messenger.get(); + + auto const state = messenger.release(); + DebugMessenger const wrapped{instance, messenger.config(), state}; + + ASSERT_TRUE(wrapped.created()); + EXPECT_EQ(wrapped.get(), vkMessenger); + + received = 0; + submit_test_message(instance); + EXPECT_EQ(received, 1); +} + CVK_TEST(DebugMessenger, move_assignment_destroys_the_old_messenger) { if constexpr(!constants::ENABLE_VALIDATION_LAYERS) GTEST_SKIP() << "needs the validation layers"; From ada251dd2420fefd45093db3f12e59def9c24651 Mon Sep 17 00:00:00 2001 From: lukas <89145039+codethinki@users.noreply.github.com> Date: Mon, 19 Oct 2026 20:29:25 +0200 Subject: [PATCH 068/135] cvk: DeviceFeatures, physical device holds queried data only - DeviceFeatures: one bool per Vulkan feature bit, merge(), missing() with the names (boost-pfr until C++26 reflection), a true bit of an extension struct enables its extension - dev::FeatureChain: the Vulkan structs, one feature_struct trait per struct, extensions_of() - PhysicalDevice: queried extensions, features, properties and queue families, no requirements, no create()/wrap(); enumerate_physical_devices() and pick_physical_device() as free functions - Device::Config holds the features and extensions to enable, the device stores the enabled set, Device::State carries it - Core builds the base set, the config and the present set once - Allocator::flags() reads the enabled set of the device - fix: Device::create() set swapchainMaintenance1 as a special case, the present set has the bit now - AGENTS: device feature rule --- AGENTS.md | 3 +- cvk/incl/cvk/base/allocator.hpp | 8 +- cvk/incl/cvk/base/core.hpp | 21 +- cvk/incl/cvk/base/core_config.hpp | 12 +- cvk/incl/cvk/base/device.hpp | 29 +- cvk/incl/cvk/base/device_config.hpp | 18 +- cvk/incl/cvk/base/physical_device.hpp | 260 +++-------- .../cvk/utility/constant/device_constants.hpp | 83 ++-- .../cvk/utility/device/device_features.hpp | 206 +++++++++ cvk/incl/cvk/utility/device/feature_chain.hpp | 225 ++++++++++ .../device/physical_device_features.hpp | 237 ---------- cvk/src/base/allocator.cpp | 27 +- cvk/src/base/core.cpp | 46 +- cvk/src/base/device.cpp | 30 +- cvk/src/base/physical_device.cpp | 422 +++++++----------- cvk/src/utility/device/device_features.cpp | 20 + cvk/src/utility/device/feature_chain.cpp | 96 ++++ .../device/physical_device_features.cpp | 89 ---- cvk/tests/src/base/allocator_test.cpp | 8 +- cvk/tests/src/base/core_test.cpp | 3 +- cvk/tests/src/base/device_test.cpp | 19 +- cvk/tests/src/base/physical_device_test.cpp | 88 ++-- cvk/tests/src/headless.cpp | 18 +- cvk/tests/src/headless.hpp | 5 +- cvk/tests/src/render/sync/fence_test.cpp | 2 +- .../utility/device/device_features_test.cpp | 42 ++ .../src/utility/device/feature_chain_test.cpp | 127 ++++++ .../device/physical_device_features_test.cpp | 166 ------- 28 files changed, 1180 insertions(+), 1130 deletions(-) create mode 100644 cvk/incl/cvk/utility/device/device_features.hpp create mode 100644 cvk/incl/cvk/utility/device/feature_chain.hpp delete mode 100644 cvk/incl/cvk/utility/device/physical_device_features.hpp create mode 100644 cvk/src/utility/device/device_features.cpp create mode 100644 cvk/src/utility/device/feature_chain.cpp delete mode 100644 cvk/src/utility/device/physical_device_features.cpp create mode 100644 cvk/tests/src/utility/device/device_features_test.cpp create mode 100644 cvk/tests/src/utility/device/feature_chain_test.cpp delete mode 100644 cvk/tests/src/utility/device/physical_device_features_test.cpp diff --git a/AGENTS.md b/AGENTS.md index e402d992..2dafccb0 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -17,7 +17,8 @@ Read it before you change a file. Match the existing code. | `lib/cmake` | one cmake file per library that adds it: `.cmake` | - Vulkan 1.3 is the baseline. Do not port code that 1.3 makes unnecessary (render passes, framebuffers, the old sync API). -- Functionality beyond 1.3 comes from required extensions, not from a higher version: `VK_KHR_dynamic_rendering_local_read` gives tile-based GPUs subpass-like local reads in a dynamic rendering. Extension feature structs go into `utils::PhysicalDeviceFeatures`, `link()` puts them in the chain only with their extension. +- Functionality beyond 1.3 comes from required extensions, not from a higher version: `VK_KHR_dynamic_rendering_local_read` gives tile-based GPUs subpass-like local reads in a dynamic rendering. +- Device features are the `bool` fields of `DeviceFeatures` (`cvk/utility/device/device_features.hpp`). A true bit of an extension struct enables its extension. A new extension struct: an entry in `dev::feature_structs_t`, a `dev::feature_struct` specialization, its fields in `DeviceFeatures` in the same order. - Use the Vulkan C API through volk. Do not use Vulkan-Hpp. - Memory comes from VMA. - `cvk` destroys immediately. It has no destruction queue. Deferred destruction belongs to `jvk`. diff --git a/cvk/incl/cvk/base/allocator.hpp b/cvk/incl/cvk/base/allocator.hpp index a438261e..e6f470dc 100644 --- a/cvk/incl/cvk/base/allocator.hpp +++ b/cvk/incl/cvk/base/allocator.hpp @@ -11,7 +11,6 @@ namespace cvk { class Device; -class PhysicalDevice; /** * @brief VmaAllocator wrapper, allocates the memory of one device @@ -61,8 +60,7 @@ class Allocator { /** * @brief creates the allocator - * @details the flags follow the device: @ref flags() - * @pre the device enabled the required extensions and features of its physical device + * @details the flags follow the enabled features and extensions of the device: @ref flags() * @post @ref created() * @note calls @ref optDestroy() * @throws cvk::vk_result_exception result of vmaImportVulkanFunctionsFromVolk() @@ -91,10 +89,10 @@ class Allocator { static void destroy(VmaAllocator); /** - * @return VMA flags for the required extensions and features of the physical device + * @return VMA flags for the enabled features and extensions of the device * @details bufferDeviceAddress, maintenance4, VK_EXT_memory_budget */ - [[nodiscard]] static VmaAllocatorCreateFlags flags(PhysicalDevice const&); + [[nodiscard]] static VmaAllocatorCreateFlags flags(Device const&); [[nodiscard]] constexpr bool created() const { return static_cast(_handle); } [[nodiscard]] constexpr vk_handle_t get() const { return _handle.get(); } diff --git a/cvk/incl/cvk/base/core.hpp b/cvk/incl/cvk/base/core.hpp index cded378f..269a23a0 100644 --- a/cvk/incl/cvk/base/core.hpp +++ b/cvk/incl/cvk/base/core.hpp @@ -12,6 +12,7 @@ #include #include #include +#include #include @@ -139,6 +140,14 @@ class Core { [[nodiscard]] constexpr std::span queues() const { return _queues; } private: + /** + * @brief features and extensions of the device + */ + struct device_requirements { + DeviceFeatures features; + std::vector extensions; + }; + /** * @brief physical device for a queue set and the mapping of the queues */ @@ -159,11 +168,20 @@ class Core { void forget(); + /** + * @return the base set, the config and the present set with a present queue + */ + [[nodiscard]] static device_requirements requirements(Config const&); + /** * @brief picks a physical device for the first possible queue set * @throws cvk::cvk_exception reason: no physical device meets the requirements */ - [[nodiscard]] device_pick pickPhysicalDevice(std::span surfaces, Config const&) const; + [[nodiscard]] device_pick pickPhysicalDevice( + std::span surfaces, + Config const&, + device_requirements const& + ) const; [[nodiscard]] static queue_set_relations createQueueSetRelations( queue_set_t const& queueSet, @@ -176,6 +194,7 @@ class Core { [[nodiscard]] std::optional tryPickPhysicalDevice( std::span surfaces, Config const&, + device_requirements const&, queue_set_t const& queueSet ) const; diff --git a/cvk/incl/cvk/base/core_config.hpp b/cvk/incl/cvk/base/core_config.hpp index 6b19c927..7774133e 100644 --- a/cvk/incl/cvk/base/core_config.hpp +++ b/cvk/incl/cvk/base/core_config.hpp @@ -1,7 +1,7 @@ #pragma once #include "queue/queue_family.hpp" -#include "cvk/utility/device/physical_device_features.hpp" +#include "cvk/utility/device/device_features.hpp" #include #include @@ -29,14 +29,16 @@ struct CoreConfig { std::vector queueSets{}; /** - * @brief device extensions in addition to the defaults of @ref PhysicalDevice::AutoPick() + * @brief device features besides @ref constants::BASE_DEVICE_FEATURES + * @details a present queue adds @ref constants::PRESENT_DEVICE_FEATURES */ - std::vector requiredExtensions{}; + DeviceFeatures features{}; /** - * @brief device features in addition to the defaults of @ref PhysicalDevice::AutoPick() + * @brief device extensions besides the extensions of the features + * @details a present queue adds @ref constants::PRESENT_DEVICE_EXTENSIONS */ - utils::PhysicalDeviceFeatures requiredFeatures{}; + std::vector extensions{}; static constexpr void debug_check(CoreConfig const& config); }; diff --git a/cvk/incl/cvk/base/device.hpp b/cvk/incl/cvk/base/device.hpp index c9d98297..ccaf06b0 100644 --- a/cvk/incl/cvk/base/device.hpp +++ b/cvk/incl/cvk/base/device.hpp @@ -12,6 +12,7 @@ #include #include +#include #include #include @@ -69,9 +70,8 @@ class Device { void wrap(State); /** - * @brief creates the device and retrieves the queues - * @details enables the required extensions and features of the physical device, - * VK_KHR_swapchain_maintenance1 required -> enables the swapchainMaintenance1 feature + * @brief creates the device with the features and extensions of the config and retrieves the queues + * @pre the physical device has the features and the extensions of the config * @post @ref created() * @note calls @ref optDestroy() * @throws cvk::vk_result_exception result of vkCreateDevice() @@ -117,6 +117,16 @@ class Device { [[nodiscard]] constexpr Instance const& instance() const { return *_instance; } [[nodiscard]] constexpr PhysicalDevice const& physicalDevice() const { return *_physicalDevice; } + /** + * @return the enabled features + */ + [[nodiscard]] constexpr DeviceFeatures const& features() const { return _features; } + + /** + * @return the enabled extensions, the extensions of the features included + */ + [[nodiscard]] constexpr std::span extensions() const { return _extensions; } + [[nodiscard]] constexpr bool created() const { return static_cast(_handle); } private: @@ -127,6 +137,7 @@ class Device { ); /** + * @brief creates the device with @ref _features and @ref _extensions * @throws cvk::vk_result_exception result of vkCreateDevice() */ void createLogicalDevice(queue_families_queue_counts_t const& queueFamiliesQueueCounts); @@ -147,6 +158,8 @@ class Device { unique_handle> _handle; std::unique_ptr _functionTable = std::make_unique(); + DeviceFeatures _features{}; + std::vector _extensions{}; std::vector _queues{}; public: @@ -173,6 +186,16 @@ struct Device::State { * @brief family index of each queue, see @ref Config::queueFamilyIndices */ std::vector queueFamilyIndices{}; + + /** + * @brief the features that the device enables + */ + DeviceFeatures features{}; + + /** + * @brief the extensions that the device enables, the extensions of the features included + */ + std::vector extensions{}; }; } diff --git a/cvk/incl/cvk/base/device_config.hpp b/cvk/incl/cvk/base/device_config.hpp index 97f4a827..61686c58 100644 --- a/cvk/incl/cvk/base/device_config.hpp +++ b/cvk/incl/cvk/base/device_config.hpp @@ -1,14 +1,30 @@ #pragma once #include "queue/queue_family.hpp" +#include "cvk/utility/device/device_features.hpp" + +#include #include namespace cvk { +/** + * @brief queues, features and extensions of a device + * @details cvk needs @ref constants::BASE_DEVICE_FEATURES, @ref Core adds them + */ struct DeviceConfig { /** * @brief family index of every queue to create - * @details see @ref PhysicalDevice::Create() */ std::vector queueFamilyIndices; + + /** + * @brief features to enable, a true bit of an extension struct enables its extension + */ + DeviceFeatures features{}; + + /** + * @brief extensions to enable, besides the extensions of the features + */ + std::vector extensions{}; }; } diff --git a/cvk/incl/cvk/base/physical_device.hpp b/cvk/incl/cvk/base/physical_device.hpp index 54b732f3..b367ff8c 100644 --- a/cvk/incl/cvk/base/physical_device.hpp +++ b/cvk/incl/cvk/base/physical_device.hpp @@ -1,7 +1,7 @@ #pragma once #include "cvk/base/queue/queue_family.hpp" +#include "cvk/utility/device/device_features.hpp" #include "cvk/utility/types.hpp" -#include "cvk/utility/device/physical_device_features.hpp" #include @@ -18,210 +18,78 @@ class Instance; class Surface; /** - * @brief VkPhysicalDevice wrapper with the queried properties + * @brief VkPhysicalDevice with its queried extensions, features, properties and queue families * @details copyable, the instance owns the physical device */ class PhysicalDevice { public: using vk_handle_t = VkPhysicalDevice; - struct State; - struct CreateResult; - - /** - * @brief base constructor - * @pre @ref Instance::created() - */ - explicit PhysicalDevice( - Instance const&, - utils::PhysicalDeviceFeatures requiredFeatures, - std::span requiredExtensions - ); - - /** - * @brief constructs and creates - * @details calls: - * - @ref PhysicalDevice(Instance const&, utils::PhysicalDeviceFeatures, std::span) - * - @ref create(std::span, vk_not_null) - */ - explicit PhysicalDevice( - Instance const&, - utils::PhysicalDeviceFeatures const& requiredFeatures, - std::span requiredExtensions, - std::span surfaces, - vk_not_null vkDevice - ); - - /** - * @brief constructs and wraps - * @details calls: - * - @ref PhysicalDevice(Instance const&, utils::PhysicalDeviceFeatures, std::span) - * - @ref wrap() - */ - explicit PhysicalDevice( - Instance const&, - utils::PhysicalDeviceFeatures const& requiredFeatures, - std::span requiredExtensions, - State const& - ); - - ~PhysicalDevice() = default; - - /** - * @brief constructs and creates if the device meets the requirements - * @param queueProperties passed to @ref suitable() - * @return std::nullopt -> not @ref suitable() - */ - [[nodiscard]] static std::optional Create( - Instance const&, - std::span surfaces, - std::span queueProperties, - std::span requiredExtensions, - utils::PhysicalDeviceFeatures const& requiredFeatures, - vk_not_null vkDevice - ); - - /** - * @brief wraps the state - * @details empty or std::nullopt members of the state -> queries them - * @post @ref created() - */ - void wrap(State const&); /** * @brief queries the device - * @param surfaces to query the present support of the queue families, empty -> no present support - * @post @ref created() - */ - void create(std::span surfaces, vk_not_null vkDevice); - - /** - * @brief queries the device - * @param queueFamilies of the device - * @post @ref created() - */ - void create(std::span queueFamilies, vk_not_null vkDevice); - - /** - * @brief enumerates the devices and picks the first that meets the requirements - * @param queueProperties queues the device must support - * @return std::nullopt -> no device meets the requirements - * @details adds to the requirements: - * - @ref constants::REQUIRED_DEVICE_EXTENSIONS and @ref constants::required_device_features() - * - a PRESENT queue -> @ref constants::PRESENT_DEVICE_EXTENSIONS - */ - [[nodiscard]] static std::optional AutoPick( - Instance const&, - std::span surfaces, - std::span queueProperties, - std::span requiredExtensions = {}, - utils::PhysicalDeviceFeatures const& requiredFeatures = {} - ); - - /** - * @return the device has the required features, the required extensions and the queues - * @pre @ref created() - */ - [[nodiscard]] bool suitable(std::span queueProperties) const; - - /** - * @return required features the device does not have - * @pre @ref created() + * @param surfaces a family presents only if it presents to every surface, empty -> no present support + * @pre the instance of the device is created + * @throws cvk::vk_result_exception result of vkEnumerateDeviceExtensionProperties() + * @throws cvk::vk_result_exception result of vkGetPhysicalDeviceSurfaceSupportKHR() */ - [[nodiscard]] std::vector supports( - utils::PhysicalDeviceFeatures const& requiredFeatures - ) const; + explicit PhysicalDevice(vk_not_null, std::span surfaces = {}); - /** - * @return required extensions the device does not have - */ - [[nodiscard]] std::vector supports(std::span requiredExtensions) const; + ~PhysicalDevice() = default; /** - * @return first of @ref candidates that supports @ref features with @ref tiling - * @pre @ref created() - * @throws cvk::cvk_exception reason: no candidate supports the features + * @param features to require + * @param extensions to require, besides the extensions of the features + * @return names of the features and the extensions that the device does not have */ - [[nodiscard]] VkFormat findSupportedFormat( - std::span candidates, - VkImageTiling tiling, - VkFormatFeatureFlags features + [[nodiscard]] std::vector missing( + DeviceFeatures const& features, + std::span extensions = {} ) const; /** * @brief finds queue families for the requested queues * @param queueProperties requested queues * @return family index per requested queue, empty -> no combination - * @pre @ref created() */ [[nodiscard]] std::vector queueFamilyIndices( std::span queueProperties ) const; /** - * @pre @ref created() - */ - [[nodiscard]] bool supportsQueueSet(std::span queueProperties) const; - - /** - * @throws cvk::vk_result_exception result of vkEnumeratePhysicalDevices() - */ - [[nodiscard]] static std::vector enumerateVkDevices(vk_not_null); - - /** - * @throws cvk::vk_result_exception result of vkEnumerateDeviceExtensionProperties() + * @return first of the candidates that supports the features with the tiling + * @throws cvk::cvk_exception reason: no candidate supports the features */ - [[nodiscard]] static std::vector queryExtensions(vk_not_null vkDevice); - - [[nodiscard]] static VkPhysicalDeviceProperties queryProperties(vk_not_null vkDevice); - - [[nodiscard]] static VkPhysicalDeviceMemoryProperties queryMemoryProperties( - vk_not_null vkDevice - ); + [[nodiscard]] VkFormat findSupportedFormat( + std::span candidates, + VkImageTiling tiling, + VkFormatFeatureFlags features + ) const; /** - * @param surfaces a family presents only if it presents to every surface, empty -> no present support + * @param idx queue family index + * @return nullptr -> no family with the index */ - [[nodiscard]] static std::vector queryQueueFamilies( - std::span surfaces, - vk_not_null vkDevice - ); - - [[nodiscard]] static VkSampleCountFlagBits evalMaxSampleCount(VkPhysicalDeviceProperties const& properties); + [[nodiscard]] QueueFamily const* queueFamily(queue_family_index_t idx) const; - [[nodiscard]] constexpr bool created() const { return _handle != nullptr; } [[nodiscard]] constexpr vk_handle_t get() const { return _handle.get(); } - [[nodiscard]] constexpr auto const& features() const { return _features; } - [[nodiscard]] constexpr auto const& requiredFeatures() const { return _requiredFeatures; } [[nodiscard]] constexpr std::span extensions() const { return _extensions; } - [[nodiscard]] constexpr std::span requiredExtensions() const { return _requiredExtensions; } - [[nodiscard]] constexpr auto const& properties() const { return _properties; } - [[nodiscard]] constexpr auto const& memProperties() const { return _memProperties; } - [[nodiscard]] constexpr VkSampleCountFlagBits maxSampleCount() const { return _maxSampleCount; } + [[nodiscard]] constexpr DeviceFeatures const& features() const { return _features; } + [[nodiscard]] constexpr VkPhysicalDeviceProperties const& properties() const { return _properties; } [[nodiscard]] constexpr VkPhysicalDeviceLimits const& limits() const { return _properties.limits; } - - /** - * @param idx queue family index - * @return nullptr -> no family with @ref idx - */ - [[nodiscard]] QueueFamily const* queueFamily(queue_family_index_t idx) const; - + [[nodiscard]] constexpr VkPhysicalDeviceMemoryProperties const& memoryProperties() const { + return _memoryProperties; + } + [[nodiscard]] constexpr VkSampleCountFlagBits maxSampleCount() const { return _maxSampleCount; } [[nodiscard]] constexpr std::span queueFamilies() const { return _queueFamilies; } private: - not_null_ptr _instance; - utils::PhysicalDeviceFeatures _requiredFeatures; - std::vector _requiredExtensions{}; - move_ptr _handle; - - utils::PhysicalDeviceFeatures _features{}; - std::vector _extensions{}; - VkPhysicalDeviceProperties _properties{}; - VkSampleCountFlagBits _maxSampleCount = VK_SAMPLE_COUNT_FLAG_BITS_MAX_ENUM; - - VkPhysicalDeviceMemoryProperties _memProperties{}; - - std::vector _queueFamilies{}; + std::vector _extensions; + DeviceFeatures _features; + VkPhysicalDeviceProperties _properties; + VkPhysicalDeviceMemoryProperties _memoryProperties; + VkSampleCountFlagBits _maxSampleCount; + std::vector _queueFamilies; public: PhysicalDevice(PhysicalDevice const& other) = default; @@ -229,37 +97,47 @@ class PhysicalDevice { PhysicalDevice& operator=(PhysicalDevice const& other) = default; PhysicalDevice& operator=(PhysicalDevice&& other) noexcept = default; - static constexpr void debug_check(PhysicalDevice const& device); - static constexpr void debug_check_handle(vk_not_null vkDevice); -}; -} - -namespace cvk { -struct PhysicalDevice::State { - vk_not_null vkDevice; - /** - * @details empty -> queried without present support - */ - std::vector queueFamilies; - - std::optional features = std::nullopt; - std::vector extensions{}; - std::optional properties = std::nullopt; - VkSampleCountFlagBits maxSampleCount = VK_SAMPLE_COUNT_FLAG_BITS_MAX_ENUM; - std::optional memProperties = std::nullopt; + static constexpr void debug_check(PhysicalDevice const&); }; -struct PhysicalDevice::CreateResult { +/** + * @brief physical device for the requested queues, with the family index of each queue + */ +struct PhysicalDevicePick { PhysicalDevice device; std::vector queueFamilyIndices; }; + +/** + * @brief queries every physical device of the instance + * @param surfaces a family presents only if it presents to every surface, empty -> no present support + * @pre @ref Instance::created() + * @throws cvk::vk_result_exception result of vkEnumeratePhysicalDevices() + */ +[[nodiscard]] std::vector enumerate_physical_devices( + Instance const&, + std::span surfaces = {} +); + +/** + * @brief picks the first physical device with the features, the extensions and the queues + * @param surfaces a family presents only if it presents to every surface + * @param extensions to require, besides the extensions of the features + * @return std::nullopt -> no device meets the requirements + * @pre @ref Instance::created() + * @throws cvk::vk_result_exception result of vkEnumeratePhysicalDevices() + */ +[[nodiscard]] std::optional pick_physical_device( + Instance const&, + std::span surfaces, + std::span queueProperties, + DeviceFeatures const& features, + std::span extensions = {} +); } namespace cvk { constexpr void PhysicalDevice::debug_check(PhysicalDevice const& device) { - CTH_CRITICAL(!device.created(), "physical device must be created") {} - debug_check_handle(device.get()); + CTH_CRITICAL(device.get() == VK_NULL_HANDLE, "physical device must not be moved from") {} } - -constexpr void PhysicalDevice::debug_check_handle([[maybe_unused]] vk_not_null vkDevice) {} } diff --git a/cvk/incl/cvk/utility/constant/device_constants.hpp b/cvk/incl/cvk/utility/constant/device_constants.hpp index f8308595..63c16ca8 100644 --- a/cvk/incl/cvk/utility/constant/device_constants.hpp +++ b/cvk/incl/cvk/utility/constant/device_constants.hpp @@ -1,5 +1,5 @@ #pragma once -#include "cvk/utility/device/physical_device_features.hpp" +#include "cvk/utility/device/device_features.hpp" #include @@ -8,19 +8,46 @@ namespace cvk::constants { /** - * @brief extensions that every device enables - * @details VK_KHR_dynamic_rendering_local_read: tile local reads in a dynamic rendering, like subpasses + * @brief features that every device enables + * @details bindless: descriptorIndexing guarantees the minimum set of descriptor indexing features, they are enabled, + * descriptorBindingVariableDescriptorCount is not part of the set, the one extra requirement, + * dynamicRenderingLocalRead: tile local reads in a dynamic rendering, like subpasses */ -inline constexpr std::array REQUIRED_DEVICE_EXTENSIONS{ - VK_KHR_DYNAMIC_RENDERING_LOCAL_READ_EXTENSION_NAME, +inline constexpr DeviceFeatures BASE_DEVICE_FEATURES{ + .samplerAnisotropy = true, + .shaderSampledImageArrayDynamicIndexing = true, + .shaderStorageBufferArrayDynamicIndexing = true, + .descriptorIndexing = true, + .shaderUniformTexelBufferArrayDynamicIndexing = true, + .shaderStorageTexelBufferArrayDynamicIndexing = true, + .shaderSampledImageArrayNonUniformIndexing = true, + .shaderStorageBufferArrayNonUniformIndexing = true, + .shaderUniformTexelBufferArrayNonUniformIndexing = true, + .descriptorBindingSampledImageUpdateAfterBind = true, + .descriptorBindingStorageImageUpdateAfterBind = true, + .descriptorBindingStorageBufferUpdateAfterBind = true, + .descriptorBindingUniformTexelBufferUpdateAfterBind = true, + .descriptorBindingStorageTexelBufferUpdateAfterBind = true, + .descriptorBindingUpdateUnusedWhilePending = true, + .descriptorBindingPartiallyBound = true, + .descriptorBindingVariableDescriptorCount = true, + .runtimeDescriptorArray = true, + .timelineSemaphore = true, + .synchronization2 = true, + .dynamicRendering = true, + .dynamicRenderingLocalRead = true, }; /** - * @brief extensions that a device with a present queue enables + * @brief features that a device with a present queue enables + */ +inline constexpr DeviceFeatures PRESENT_DEVICE_FEATURES{.swapchainMaintenance1 = true}; + +/** + * @brief extensions without feature bits that a device with a present queue enables */ -inline constexpr std::array PRESENT_DEVICE_EXTENSIONS{ +inline constexpr std::array PRESENT_DEVICE_EXTENSIONS{ VK_KHR_SWAPCHAIN_EXTENSION_NAME, - VK_KHR_SWAPCHAIN_MAINTENANCE_1_EXTENSION_NAME, }; /** @@ -31,44 +58,4 @@ inline constexpr std::array PRESENT_INSTANCE_EXTENSIONS{ VK_KHR_GET_SURFACE_CAPABILITIES_2_EXTENSION_NAME, VK_KHR_SURFACE_MAINTENANCE_1_EXTENSION_NAME, }; - -/** - * @brief features that every device enables - * @details bindless: descriptorIndexing guarantees the minimum set of descriptor indexing features, they are enabled, - * descriptorBindingVariableDescriptorCount is not part of the set, the one extra requirement - */ -[[nodiscard]] inline utils::PhysicalDeviceFeatures required_device_features() { - utils::PhysicalDeviceFeatures features{}; - - auto& features10 = features.features().features; - features10.samplerAnisotropy = VK_TRUE; - features10.shaderSampledImageArrayDynamicIndexing = VK_TRUE; - features10.shaderStorageBufferArrayDynamicIndexing = VK_TRUE; - - auto& features12 = features.features(); - features12.timelineSemaphore = VK_TRUE; - features12.descriptorIndexing = VK_TRUE; - features12.shaderUniformTexelBufferArrayDynamicIndexing = VK_TRUE; - features12.shaderStorageTexelBufferArrayDynamicIndexing = VK_TRUE; - features12.shaderSampledImageArrayNonUniformIndexing = VK_TRUE; - features12.shaderStorageBufferArrayNonUniformIndexing = VK_TRUE; - features12.shaderUniformTexelBufferArrayNonUniformIndexing = VK_TRUE; - features12.descriptorBindingSampledImageUpdateAfterBind = VK_TRUE; - features12.descriptorBindingStorageImageUpdateAfterBind = VK_TRUE; - features12.descriptorBindingStorageBufferUpdateAfterBind = VK_TRUE; - features12.descriptorBindingUniformTexelBufferUpdateAfterBind = VK_TRUE; - features12.descriptorBindingStorageTexelBufferUpdateAfterBind = VK_TRUE; - features12.descriptorBindingUpdateUnusedWhilePending = VK_TRUE; - features12.descriptorBindingPartiallyBound = VK_TRUE; - features12.runtimeDescriptorArray = VK_TRUE; - features12.descriptorBindingVariableDescriptorCount = VK_TRUE; - - auto& features13 = features.features(); - features13.synchronization2 = VK_TRUE; - features13.dynamicRendering = VK_TRUE; - - features.features().dynamicRenderingLocalRead = VK_TRUE; - - return features; -} } diff --git a/cvk/incl/cvk/utility/device/device_features.hpp b/cvk/incl/cvk/utility/device/device_features.hpp new file mode 100644 index 00000000..b1695cdd --- /dev/null +++ b/cvk/incl/cvk/utility/device/device_features.hpp @@ -0,0 +1,206 @@ +#pragma once +#include +#include +#include +#include +#include +#include +#include + + +namespace cvk { +/** + * @brief feature bits of a device, one bool per Vulkan feature + * @details the fields follow the Vulkan feature structs in order, see @ref dev::feature_structs, + * a true bit of an extension struct enables its extension, see @ref dev::extensions_of() + */ +struct DeviceFeatures { + // Vulkan 1.0, VkPhysicalDeviceFeatures + bool robustBufferAccess = false; + bool fullDrawIndexUint32 = false; + bool imageCubeArray = false; + bool independentBlend = false; + bool geometryShader = false; + bool tessellationShader = false; + bool sampleRateShading = false; + bool dualSrcBlend = false; + bool logicOp = false; + bool multiDrawIndirect = false; + bool drawIndirectFirstInstance = false; + bool depthClamp = false; + bool depthBiasClamp = false; + bool fillModeNonSolid = false; + bool depthBounds = false; + bool wideLines = false; + bool largePoints = false; + bool alphaToOne = false; + bool multiViewport = false; + bool samplerAnisotropy = false; + bool textureCompressionETC2 = false; + bool textureCompressionASTC_LDR = false; + bool textureCompressionBC = false; + bool occlusionQueryPrecise = false; + bool pipelineStatisticsQuery = false; + bool vertexPipelineStoresAndAtomics = false; + bool fragmentStoresAndAtomics = false; + bool shaderTessellationAndGeometryPointSize = false; + bool shaderImageGatherExtended = false; + bool shaderStorageImageExtendedFormats = false; + bool shaderStorageImageMultisample = false; + bool shaderStorageImageReadWithoutFormat = false; + bool shaderStorageImageWriteWithoutFormat = false; + bool shaderUniformBufferArrayDynamicIndexing = false; + bool shaderSampledImageArrayDynamicIndexing = false; + bool shaderStorageBufferArrayDynamicIndexing = false; + bool shaderStorageImageArrayDynamicIndexing = false; + bool shaderClipDistance = false; + bool shaderCullDistance = false; + bool shaderFloat64 = false; + bool shaderInt64 = false; + bool shaderInt16 = false; + bool shaderResourceResidency = false; + bool shaderResourceMinLod = false; + bool sparseBinding = false; + bool sparseResidencyBuffer = false; + bool sparseResidencyImage2D = false; + bool sparseResidencyImage3D = false; + bool sparseResidency2Samples = false; + bool sparseResidency4Samples = false; + bool sparseResidency8Samples = false; + bool sparseResidency16Samples = false; + bool sparseResidencyAliased = false; + bool variableMultisampleRate = false; + bool inheritedQueries = false; + + // Vulkan 1.1, VkPhysicalDeviceVulkan11Features + bool storageBuffer16BitAccess = false; + bool uniformAndStorageBuffer16BitAccess = false; + bool storagePushConstant16 = false; + bool storageInputOutput16 = false; + bool multiview = false; + bool multiviewGeometryShader = false; + bool multiviewTessellationShader = false; + bool variablePointersStorageBuffer = false; + bool variablePointers = false; + bool protectedMemory = false; + bool samplerYcbcrConversion = false; + bool shaderDrawParameters = false; + + // Vulkan 1.2, VkPhysicalDeviceVulkan12Features + bool samplerMirrorClampToEdge = false; + bool drawIndirectCount = false; + bool storageBuffer8BitAccess = false; + bool uniformAndStorageBuffer8BitAccess = false; + bool storagePushConstant8 = false; + bool shaderBufferInt64Atomics = false; + bool shaderSharedInt64Atomics = false; + bool shaderFloat16 = false; + bool shaderInt8 = false; + bool descriptorIndexing = false; + bool shaderInputAttachmentArrayDynamicIndexing = false; + bool shaderUniformTexelBufferArrayDynamicIndexing = false; + bool shaderStorageTexelBufferArrayDynamicIndexing = false; + bool shaderUniformBufferArrayNonUniformIndexing = false; + bool shaderSampledImageArrayNonUniformIndexing = false; + bool shaderStorageBufferArrayNonUniformIndexing = false; + bool shaderStorageImageArrayNonUniformIndexing = false; + bool shaderInputAttachmentArrayNonUniformIndexing = false; + bool shaderUniformTexelBufferArrayNonUniformIndexing = false; + bool shaderStorageTexelBufferArrayNonUniformIndexing = false; + bool descriptorBindingUniformBufferUpdateAfterBind = false; + bool descriptorBindingSampledImageUpdateAfterBind = false; + bool descriptorBindingStorageImageUpdateAfterBind = false; + bool descriptorBindingStorageBufferUpdateAfterBind = false; + bool descriptorBindingUniformTexelBufferUpdateAfterBind = false; + bool descriptorBindingStorageTexelBufferUpdateAfterBind = false; + bool descriptorBindingUpdateUnusedWhilePending = false; + bool descriptorBindingPartiallyBound = false; + bool descriptorBindingVariableDescriptorCount = false; + bool runtimeDescriptorArray = false; + bool samplerFilterMinmax = false; + bool scalarBlockLayout = false; + bool imagelessFramebuffer = false; + bool uniformBufferStandardLayout = false; + bool shaderSubgroupExtendedTypes = false; + bool separateDepthStencilLayouts = false; + bool hostQueryReset = false; + bool timelineSemaphore = false; + bool bufferDeviceAddress = false; + bool bufferDeviceAddressCaptureReplay = false; + bool bufferDeviceAddressMultiDevice = false; + bool vulkanMemoryModel = false; + bool vulkanMemoryModelDeviceScope = false; + bool vulkanMemoryModelAvailabilityVisibilityChains = false; + bool shaderOutputViewportIndex = false; + bool shaderOutputLayer = false; + bool subgroupBroadcastDynamicId = false; + + // Vulkan 1.3, VkPhysicalDeviceVulkan13Features + bool robustImageAccess = false; + bool inlineUniformBlock = false; + bool descriptorBindingInlineUniformBlockUpdateAfterBind = false; + bool pipelineCreationCacheControl = false; + bool privateData = false; + bool shaderDemoteToHelperInvocation = false; + bool shaderTerminateInvocation = false; + bool subgroupSizeControl = false; + bool computeFullSubgroups = false; + bool synchronization2 = false; + bool textureCompressionASTC_HDR = false; + bool shaderZeroInitializeWorkgroupMemory = false; + bool dynamicRendering = false; + bool shaderIntegerDotProduct = false; + bool maintenance4 = false; + + // VK_KHR_swapchain_maintenance1, VkPhysicalDeviceSwapchainMaintenance1FeaturesKHR + bool swapchainMaintenance1 = false; + + // VK_KHR_dynamic_rendering_local_read, VkPhysicalDeviceDynamicRenderingLocalReadFeaturesKHR + bool dynamicRenderingLocalRead = false; + + /** + * @brief enables every feature of other + */ + constexpr DeviceFeatures& merge(DeviceFeatures const& other); + + /** + * @return names of the features that required enables and this does not + */ + [[nodiscard]] std::vector missing(DeviceFeatures const& required) const; + + [[nodiscard]] constexpr bool operator==(DeviceFeatures const&) const = default; +}; +} + +namespace cvk::dev { +/** + * @brief feature bits in @ref DeviceFeatures, every field is a bool without padding + */ +inline constexpr size_t FEATURE_COUNT = sizeof(DeviceFeatures); + +using feature_bits_t = std::array; + +/** + * @return field i of the features as element i + */ +[[nodiscard]] constexpr feature_bits_t to_bits(DeviceFeatures const& features) { + return std::bit_cast(features); +} + +/** + * @return element i of the bits as field i + */ +[[nodiscard]] constexpr DeviceFeatures to_features(feature_bits_t const& bits) { + return std::bit_cast(bits); +} +} + +namespace cvk { +constexpr DeviceFeatures& DeviceFeatures::merge(DeviceFeatures const& other) { + auto bits = dev::to_bits(*this); + std::ranges::transform(bits, dev::to_bits(other), bits.begin(), std::logical_or{}); + + *this = dev::to_features(bits); + return *this; +} +} diff --git a/cvk/incl/cvk/utility/device/feature_chain.hpp b/cvk/incl/cvk/utility/device/feature_chain.hpp new file mode 100644 index 00000000..c21bc0ff --- /dev/null +++ b/cvk/incl/cvk/utility/device/feature_chain.hpp @@ -0,0 +1,225 @@ +#pragma once +#include "device_features.hpp" + +#include "cvk/utility/types.hpp" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + + +namespace cvk::dev { +/** + * @brief the Vulkan feature structs of @ref DeviceFeatures, in the order of its fields + * @details a new struct: an entry here, a @ref feature_struct specialization, its fields in @ref DeviceFeatures + */ +using feature_structs_t = std::tuple< + VkPhysicalDeviceFeatures2, + VkPhysicalDeviceVulkan11Features, + VkPhysicalDeviceVulkan12Features, + VkPhysicalDeviceVulkan13Features, + VkPhysicalDeviceSwapchainMaintenance1FeaturesKHR, + VkPhysicalDeviceDynamicRenderingLocalReadFeaturesKHR +>; + +inline constexpr size_t FEATURE_STRUCTS = std::tuple_size_v; + +/** + * @brief sType, VkBool32 flags and extension of a feature struct + * @details OFFSET: byte offset of the first flag, COUNT: flags, EXTENSION: empty -> core struct + */ +template +struct feature_struct; + +template<> +struct feature_struct { + static constexpr VkStructureType TYPE = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2; + static constexpr size_t OFFSET = offsetof(VkPhysicalDeviceFeatures2, features); + static constexpr size_t COUNT = sizeof(VkPhysicalDeviceFeatures) / sizeof(VkBool32); + static constexpr std::string_view EXTENSION{}; +}; + +template<> +struct feature_struct { + static constexpr VkStructureType TYPE = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_1_FEATURES; + static constexpr size_t OFFSET = offsetof(VkPhysicalDeviceVulkan11Features, storageBuffer16BitAccess); + static constexpr size_t COUNT = + (offsetof(VkPhysicalDeviceVulkan11Features, shaderDrawParameters) - OFFSET) / sizeof(VkBool32) + 1; + static constexpr std::string_view EXTENSION{}; +}; + +template<> +struct feature_struct { + static constexpr VkStructureType TYPE = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_2_FEATURES; + static constexpr size_t OFFSET = offsetof(VkPhysicalDeviceVulkan12Features, samplerMirrorClampToEdge); + static constexpr size_t COUNT = + (offsetof(VkPhysicalDeviceVulkan12Features, subgroupBroadcastDynamicId) - OFFSET) / sizeof(VkBool32) + 1; + static constexpr std::string_view EXTENSION{}; +}; + +template<> +struct feature_struct { + static constexpr VkStructureType TYPE = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_3_FEATURES; + static constexpr size_t OFFSET = offsetof(VkPhysicalDeviceVulkan13Features, robustImageAccess); + static constexpr size_t COUNT = + (offsetof(VkPhysicalDeviceVulkan13Features, maintenance4) - OFFSET) / sizeof(VkBool32) + 1; + static constexpr std::string_view EXTENSION{}; +}; + +template<> +struct feature_struct { + static constexpr VkStructureType TYPE = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SWAPCHAIN_MAINTENANCE_1_FEATURES_KHR; + static constexpr size_t OFFSET = offsetof(VkPhysicalDeviceSwapchainMaintenance1FeaturesKHR, swapchainMaintenance1); + static constexpr size_t COUNT = 1; + static constexpr std::string_view EXTENSION = VK_KHR_SWAPCHAIN_MAINTENANCE_1_EXTENSION_NAME; +}; + +template<> +struct feature_struct { + static constexpr VkStructureType TYPE = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DYNAMIC_RENDERING_LOCAL_READ_FEATURES_KHR; + static constexpr size_t OFFSET = + offsetof(VkPhysicalDeviceDynamicRenderingLocalReadFeaturesKHR, dynamicRenderingLocalRead); + static constexpr size_t COUNT = 1; + static constexpr std::string_view EXTENSION = VK_KHR_DYNAMIC_RENDERING_LOCAL_READ_EXTENSION_NAME; +}; + +static_assert(feature_struct::COUNT == 55); +static_assert(feature_struct::COUNT == 12); +static_assert(feature_struct::COUNT == 47); +static_assert(feature_struct::COUNT == 15); + +/** + * @brief index of the first field of T in @ref DeviceFeatures + */ +template +inline constexpr size_t feature_offset = [](std::type_identity>) { + size_t offset = 0; + bool found = false; + ((found = found || std::same_as, offset += found ? 0 : feature_struct::COUNT), ...); + return offset; +}(std::type_identity{}); + +static_assert( + [](std::type_identity>) { + return (feature_struct::COUNT + ...); + }(std::type_identity{}) == FEATURE_COUNT, + "DeviceFeatures must have one field per flag of the feature structs" +); +static_assert(alignof(DeviceFeatures) == 1, "DeviceFeatures must have bool fields only"); + +/** + * @return the extensions of the structs with a true bit in the features + */ +[[nodiscard]] constexpr std::vector extensions_of(DeviceFeatures const& features) { + auto const bits = to_bits(features); + std::vector extensions{}; + + [&](std::type_identity>) { + ([&] { + if constexpr(!feature_struct::EXTENSION.empty()) { + auto const flags = std::span{bits}.subspan(feature_offset, feature_struct::COUNT); + if(std::ranges::contains(flags, true)) extensions.push_back(feature_struct::EXTENSION); + } + }(), ...); + }(std::type_identity{}); + + return extensions; +} + +/** + * @brief the feature structs as one pNext chain for Vulkan + * @details copyable, copy and move link the chain again, + * an extension struct is in the chain only with its extension + */ +class FeatureChain { +public: + /** + * @brief every feature false + * @param extensions of the device, the chain includes their structs + */ + explicit FeatureChain(std::span extensions); + + /** + * @brief the bits of the features + * @param extensions of the device, the chain includes their structs + * @pre the extensions contain @ref extensions_of() the features + */ + FeatureChain(DeviceFeatures const&, std::span extensions); + + ~FeatureChain() = default; + + /** + * @return the bits of the structs, a struct out of the chain -> false + */ + [[nodiscard]] DeviceFeatures features() const; + + /** + * @return the struct is in the chain + * @tparam T one of @ref feature_structs_t + */ + template + [[nodiscard]] constexpr bool linked() const; + + /** + * @brief start of the pNext chain + */ + [[nodiscard]] constexpr auto* get(this auto& self) { return &std::get(self._structs); } + +private: + /** + * @brief links the pNext members of the structs in @ref _linked + */ + void relink(); + + feature_structs_t _structs; + std::array _linked{}; + +public: + FeatureChain(FeatureChain const& other) : _structs{other._structs}, _linked{other._linked} { relink(); } + FeatureChain(FeatureChain&& other) noexcept : _structs{other._structs}, _linked{other._linked} { relink(); } + FeatureChain& operator=(FeatureChain const& other) { + _structs = other._structs; + _linked = other._linked; + relink(); + return *this; + } + FeatureChain& operator=(FeatureChain&& other) noexcept { + _structs = other._structs; + _linked = other._linked; + relink(); + return *this; + } +}; + +/** + * @brief queries the features of the device + * @param extensions of the device, the query includes their structs + */ +[[nodiscard]] DeviceFeatures query_features(vk_not_null, std::span extensions); +} + +namespace cvk::dev { +/** + * @brief position of T in @ref feature_structs_t + */ +template +inline constexpr size_t feature_index = [](std::type_identity>) { + constexpr std::array same{std::same_as...}; + return static_cast(std::ranges::find(same, true) - same.begin()); +}(std::type_identity{}); + +template +constexpr bool FeatureChain::linked() const { + static_assert(feature_index < FEATURE_STRUCTS, "T must be a feature struct"); + return _linked[feature_index]; +} +} diff --git a/cvk/incl/cvk/utility/device/physical_device_features.hpp b/cvk/incl/cvk/utility/device/physical_device_features.hpp deleted file mode 100644 index 5ec57181..00000000 --- a/cvk/incl/cvk/utility/device/physical_device_features.hpp +++ /dev/null @@ -1,237 +0,0 @@ -#pragma once -#include "cvk/utility/format.hpp" -#include "cvk/utility/types.hpp" - -#include - -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include - - -namespace cvk::utils { -/** - * @brief feature that a device does not have - * @details @ref index is the position in the VkBool32 flags of the struct with @ref sType - */ -struct MissingFeature { - VkStructureType sType; - size_t index; -}; - -/** - * @brief features of the vulkan 1.0 to 1.3 core and of extensions as one pNext chain - * @details copyable, copy and move link the chain again, - * an extension struct is in the chain only after @ref link() with its extension - */ -class PhysicalDeviceFeatures { -public: - using chain_t = std::tuple< - VkPhysicalDeviceFeatures2, - VkPhysicalDeviceVulkan11Features, - VkPhysicalDeviceVulkan12Features, - VkPhysicalDeviceVulkan13Features, - VkPhysicalDeviceSwapchainMaintenance1FeaturesKHR, - VkPhysicalDeviceDynamicRenderingLocalReadFeaturesKHR - >; - - static constexpr size_t STRUCTS = std::tuple_size_v; - - /** - * @brief every feature false - * @post the core structs are in the chain - */ - PhysicalDeviceFeatures(); - - /** - * @brief queries the features of the device - * @param extensions of the device, the query includes their structs - * @post the core structs and the structs of the extensions are in the chain - */ - PhysicalDeviceFeatures(vk_not_null, std::span extensions); - - ~PhysicalDeviceFeatures() = default; - - /** - * @return features that @ref requiredFeatures enables and this does not - */ - [[nodiscard]] std::vector supports(PhysicalDeviceFeatures const& requiredFeatures) const; - - /** - * @brief enables every feature that @ref other enables - */ - void merge(PhysicalDeviceFeatures const& other); - - /** - * @brief puts the core structs and the structs of the extensions in the chain - * @details the structs of other extensions stay out, Vulkan forbids them - */ - void link(std::span extensions); - - /** - * @return the struct is in the chain - * @tparam T one of @ref chain_t - */ - template - [[nodiscard]] constexpr bool linked() const; - - /** - * @tparam T one of @ref chain_t - */ - template - [[nodiscard]] constexpr auto& features(this auto& self) { return std::get(self._chain); } - - /** - * @brief start of the pNext chain, for VkDeviceCreateInfo::pNext - */ - [[nodiscard]] constexpr VkPhysicalDeviceFeatures2 const* get() const { - return &std::get(_chain); - } - -private: - /** - * @brief links the pNext members of the structs in @ref _linked - */ - void relink(); - - chain_t _chain; - std::array _linked{}; - -public: - PhysicalDeviceFeatures(PhysicalDeviceFeatures const& other) : _chain{other._chain}, _linked{other._linked} { - relink(); - } - PhysicalDeviceFeatures(PhysicalDeviceFeatures&& other) noexcept : _chain{other._chain}, _linked{other._linked} { - relink(); - } - PhysicalDeviceFeatures& operator=(PhysicalDeviceFeatures const& other) { - _chain = other._chain; - _linked = other._linked; - relink(); - return *this; - } - PhysicalDeviceFeatures& operator=(PhysicalDeviceFeatures&& other) noexcept { - _chain = other._chain; - _linked = other._linked; - relink(); - return *this; - } -}; -} - -namespace cvk::utils::dev { -/** - * @brief position of the VkBool32 flags in a feature struct - */ -template -struct feature_flags; - -template<> -struct feature_flags { - static constexpr size_t OFFSET = offsetof(VkPhysicalDeviceFeatures2, features); - static constexpr size_t COUNT = sizeof(VkPhysicalDeviceFeatures) / sizeof(VkBool32); -}; - -template<> -struct feature_flags { - static constexpr size_t OFFSET = offsetof(VkPhysicalDeviceVulkan11Features, storageBuffer16BitAccess); - static constexpr size_t COUNT = - (offsetof(VkPhysicalDeviceVulkan11Features, shaderDrawParameters) - OFFSET) / sizeof(VkBool32) + 1; -}; - -template<> -struct feature_flags { - static constexpr size_t OFFSET = offsetof(VkPhysicalDeviceVulkan12Features, samplerMirrorClampToEdge); - static constexpr size_t COUNT = - (offsetof(VkPhysicalDeviceVulkan12Features, subgroupBroadcastDynamicId) - OFFSET) / sizeof(VkBool32) + 1; -}; - -template<> -struct feature_flags { - static constexpr size_t OFFSET = offsetof(VkPhysicalDeviceVulkan13Features, robustImageAccess); - static constexpr size_t COUNT = - (offsetof(VkPhysicalDeviceVulkan13Features, maintenance4) - OFFSET) / sizeof(VkBool32) + 1; -}; - -template<> -struct feature_flags { - static constexpr size_t OFFSET = offsetof(VkPhysicalDeviceSwapchainMaintenance1FeaturesKHR, swapchainMaintenance1); - static constexpr size_t COUNT = 1; -}; - -template<> -struct feature_flags { - static constexpr size_t OFFSET = - offsetof(VkPhysicalDeviceDynamicRenderingLocalReadFeaturesKHR, dynamicRenderingLocalRead); - static constexpr size_t COUNT = 1; -}; - -static_assert(feature_flags::COUNT == 55); -static_assert(feature_flags::COUNT == 12); -static_assert(feature_flags::COUNT == 47); -static_assert(feature_flags::COUNT == 15); - -/** - * @brief extension of a feature struct, core struct -> empty - */ -template -struct feature_extension { - static constexpr std::string_view NAME{}; -}; - -template<> -struct feature_extension { - static constexpr std::string_view NAME = VK_KHR_SWAPCHAIN_MAINTENANCE_1_EXTENSION_NAME; -}; - -template<> -struct feature_extension { - static constexpr std::string_view NAME = VK_KHR_DYNAMIC_RENDERING_LOCAL_READ_EXTENSION_NAME; -}; - -/** - * @brief position of T in the chain - */ -template -constexpr size_t chain_index = [](std::type_identity>) { - constexpr std::array same{std::same_as...}; - return static_cast(std::ranges::find(same, true) - same.begin()); -}(std::type_identity{}); - -/** - * @return the VkBool32 flags of @ref features - */ -template -[[nodiscard]] auto flags(T& features) { - using feature_t = std::remove_const_t; - using bool_t = std::conditional_t, VkBool32 const, VkBool32>; - using byte_t = std::conditional_t, std::byte const, std::byte>; - - constexpr size_t count = feature_flags::COUNT; - - auto* const begin = reinterpret_cast(&features) + feature_flags::OFFSET; - return std::span{reinterpret_cast(begin), count}; -} -} - -namespace cvk::utils { -template -constexpr bool PhysicalDeviceFeatures::linked() const { - static_assert(dev::chain_index < STRUCTS, "T must be in the chain"); - return _linked[dev::chain_index]; -} -} - -CTH_FORMAT_CLASS( - cvk::utils::MissingFeature, - "{}, flag: {}", - [](cvk::utils::MissingFeature const& feature) { return std::tie(feature.sType, feature.index); } -); diff --git a/cvk/src/base/allocator.cpp b/cvk/src/base/allocator.cpp index 7a3fda69..71d96497 100644 --- a/cvk/src/base/allocator.cpp +++ b/cvk/src/base/allocator.cpp @@ -25,11 +25,9 @@ void Allocator::wrap(State const& state) { void Allocator::create() { optDestroy(); - auto const& physicalDevice = _device->physicalDevice(); - VmaAllocatorCreateInfo info{ - .flags = flags(physicalDevice), - .physicalDevice = physicalDevice.get(), + .flags = flags(*_device), + .physicalDevice = _device->physicalDevice().get(), .device = _device->get(), .preferredLargeHeapBlockSize = 0, .pAllocationCallbacks = nullptr, @@ -82,23 +80,14 @@ void Allocator::destroy(VmaAllocator vmaAllocator) { vmaDestroyAllocator(vmaAllocator); } -VmaAllocatorCreateFlags Allocator::flags(PhysicalDevice const& physicalDevice) { - auto const& requiredFeatures = physicalDevice.requiredFeatures(); - auto const requiredExtensions = physicalDevice.requiredExtensions(); - - auto const enabled = [requiredExtensions](std::string_view extension) { - return std::ranges::contains(requiredExtensions, extension); - }; +VmaAllocatorCreateFlags Allocator::flags(Device const& device) { + auto const& features = device.features(); VmaAllocatorCreateFlags flags = 0; - - if(requiredFeatures.features().bufferDeviceAddress == VK_TRUE) - flags |= VMA_ALLOCATOR_CREATE_BUFFER_DEVICE_ADDRESS_BIT; - - if(requiredFeatures.features().maintenance4 == VK_TRUE) - flags |= VMA_ALLOCATOR_CREATE_KHR_MAINTENANCE4_BIT; - - if(enabled(VK_EXT_MEMORY_BUDGET_EXTENSION_NAME)) flags |= VMA_ALLOCATOR_CREATE_EXT_MEMORY_BUDGET_BIT; + if(features.bufferDeviceAddress) flags |= VMA_ALLOCATOR_CREATE_BUFFER_DEVICE_ADDRESS_BIT; + if(features.maintenance4) flags |= VMA_ALLOCATOR_CREATE_KHR_MAINTENANCE4_BIT; + if(std::ranges::contains(device.extensions(), std::string_view{VK_EXT_MEMORY_BUDGET_EXTENSION_NAME})) + flags |= VMA_ALLOCATOR_CREATE_EXT_MEMORY_BUDGET_BIT; return flags; } diff --git a/cvk/src/base/core.cpp b/cvk/src/base/core.cpp index 5a8da9c4..cad7295d 100644 --- a/cvk/src/base/core.cpp +++ b/cvk/src/base/core.cpp @@ -5,6 +5,7 @@ #include "cvk/base/instance.hpp" #include "cvk/base/physical_device.hpp" #include "cvk/surface/surface.hpp" +#include "cvk/utility/constant/device_constants.hpp" #include "cvk/utility/vk_exceptions.hpp" #include @@ -58,11 +59,16 @@ void Core::create(span surfaces, Config const& config) { optDestroy(); // the members change after the last step that can throw, a throw leaves the core not created - auto pick = pickPhysicalDevice(surfaces, config); + auto required = requirements(config); + auto pick = pickPhysicalDevice(surfaces, config, required); auto device = std::make_unique( *_instance, *pick.physicalDevice, - Device::Config{std::move(pick.uniqueQueueFamilyIndices)} + Device::Config{ + .queueFamilyIndices = std::move(pick.uniqueQueueFamilyIndices), + .features = required.features, + .extensions = std::move(required.extensions), + } ); auto allocator = std::make_unique(*device, cvk::create); @@ -130,15 +136,36 @@ void Core::forget() { _queueSetIndex = std::nullopt; } -auto Core::pickPhysicalDevice(span surfaces, Config const& config) const -> device_pick { +auto Core::requirements(Config const& config) -> device_requirements { + device_requirements required{.features = constants::BASE_DEVICE_FEATURES, .extensions = config.extensions}; + required.features.merge(config.features); + + bool const present = std::ranges::any_of( + config.queueProperties, + [](QueueFamilyProperties properties) { return contains(properties, QueueFamilyProperties::PRESENT); } + ); + if(!present) return required; + + required.features.merge(constants::PRESENT_DEVICE_FEATURES); + for(auto const extension : constants::PRESENT_DEVICE_EXTENSIONS) + if(!std::ranges::contains(required.extensions, extension)) required.extensions.emplace_back(extension); + + return required; +} + +auto Core::pickPhysicalDevice( + span surfaces, + Config const& config, + device_requirements const& required +) const -> device_pick { if(config.queueSets.empty()) { - auto pick = tryPickPhysicalDevice(surfaces, config, queue_set_t{config.queueProperties.size()}); + auto pick = tryPickPhysicalDevice(surfaces, config, required, queue_set_t{config.queueProperties.size()}); CVK_STABLE_THROW(!pick, "no physical device supports the queues") {} return *std::move(pick); } for(auto const& [setIndex, queueSet] : std::views::enumerate(config.queueSets)) { - auto pick = tryPickPhysicalDevice(surfaces, config, queueSet); + auto pick = tryPickPhysicalDevice(surfaces, config, required, queueSet); if(!pick) continue; pick->queueSetIndex = static_cast(setIndex); @@ -177,19 +204,14 @@ auto Core::createQueueSetRelations( auto Core::tryPickPhysicalDevice( span surfaces, Config const& config, + device_requirements const& required, queue_set_t const& queueSet ) const -> std::optional { CTH_CRITICAL(queueSet.size() != config.queueProperties.size(), "queue set must span all queues") {} auto [uniqueQueues, queueToUniqueQueue] = createQueueSetRelations(queueSet, config.queueProperties); - auto result = PhysicalDevice::AutoPick( - *_instance, - surfaces, - uniqueQueues, - config.requiredExtensions, - config.requiredFeatures - ); + auto result = pick_physical_device(*_instance, surfaces, uniqueQueues, required.features, required.extensions); if(!result) return std::nullopt; return device_pick{ diff --git a/cvk/src/base/device.cpp b/cvk/src/base/device.cpp index dcb1d334..40ef183b 100644 --- a/cvk/src/base/device.cpp +++ b/cvk/src/base/device.cpp @@ -2,6 +2,7 @@ #include "cvk/base/instance.hpp" #include "cvk/base/physical_device.hpp" +#include "cvk/utility/device/feature_chain.hpp" #include "cvk/utility/vk_exceptions.hpp" #include @@ -42,12 +43,19 @@ void Device::wrap(State state) { } _handle.reset(state.vkDevice.get()); + _features = state.features; + _extensions = std::move(state.extensions); createQueues(state.queueFamilyIndices); } void Device::create(Config const& config) { optDestroy(); + _features = config.features; + _extensions = config.extensions; + for(auto const extension : dev::extensions_of(config.features)) + if(!std::ranges::contains(_extensions, extension)) _extensions.emplace_back(extension); + auto const& familyIndices = config.queueFamilyIndices; auto const queueCounts = calcQueueFamiliesQueueCounts(familyIndices); @@ -70,6 +78,8 @@ Device::State Device::release() { .vkDevice = _handle.release(), .functionTable = std::move(_functionTable), .queueFamilyIndices = {std::from_range, _queues | std::views::transform(&Queue::familyIndex)}, + .features = _features, + .extensions = std::move(_extensions), }; forget(); return state; @@ -93,6 +103,8 @@ void Device::destroy(VkDevice vkDevice) { void Device::forget() { _queues.clear(); + _features = {}; + _extensions.clear(); if(_functionTable == nullptr) _functionTable = std::make_unique(); std::memset(_functionTable.get(), 0, sizeof(VolkDeviceTable)); @@ -131,18 +143,8 @@ void Device::createLogicalDevice(queue_families_queue_counts_t const& queueFamil } ); } - auto const requiredExtensions = cth::str::to_c_str_vector(_physicalDevice->requiredExtensions()); - - auto features = _physicalDevice->requiredFeatures(); - - bool const maintenance1 = std::ranges::contains( - _physicalDevice->requiredExtensions(), - std::string_view{VK_KHR_SWAPCHAIN_MAINTENANCE_1_EXTENSION_NAME} - ); - if(maintenance1) - features.features().swapchainMaintenance1 = VK_TRUE; - - features.link(_physicalDevice->requiredExtensions()); + auto const extensions = cth::str::to_c_str_vector(_extensions); + dev::FeatureChain const features{_features, _extensions}; VkDeviceCreateInfo const createInfo{ .sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO, @@ -152,8 +154,8 @@ void Device::createLogicalDevice(queue_families_queue_counts_t const& queueFamil .pQueueCreateInfos = queueCreateInfos.data(), .enabledLayerCount = 0, .ppEnabledLayerNames = nullptr, - .enabledExtensionCount = static_cast(requiredExtensions.size()), - .ppEnabledExtensionNames = requiredExtensions.data(), + .enabledExtensionCount = static_cast(extensions.size()), + .ppEnabledExtensionNames = extensions.data(), .pEnabledFeatures = nullptr, }; VkDevice ptr = VK_NULL_HANDLE; diff --git a/cvk/src/base/physical_device.cpp b/cvk/src/base/physical_device.cpp index f8362776..0e147385 100644 --- a/cvk/src/base/physical_device.cpp +++ b/cvk/src/base/physical_device.cpp @@ -2,10 +2,11 @@ #include "cvk/base/instance.hpp" #include "cvk/surface/surface.hpp" -#include "cvk/utility/constant/device_constants.hpp" +#include "cvk/utility/device/feature_chain.hpp" #include "cvk/utility/vk_exceptions.hpp" #include +#include #include #include @@ -16,166 +17,145 @@ using std::span; using std::string; using std::vector; -PhysicalDevice::PhysicalDevice( - Instance const& instance, - utils::PhysicalDeviceFeatures requiredFeatures, - span requiredExtensions -) : _instance{&instance}, - _requiredFeatures{std::move(requiredFeatures)}, - _requiredExtensions{std::from_range, requiredExtensions} {} - -PhysicalDevice::PhysicalDevice( - Instance const& instance, - utils::PhysicalDeviceFeatures const& requiredFeatures, - span requiredExtensions, - span surfaces, - vk_not_null vkDevice -) : PhysicalDevice{instance, requiredFeatures, requiredExtensions} { create(surfaces, vkDevice); } - -PhysicalDevice::PhysicalDevice( - Instance const& instance, - utils::PhysicalDeviceFeatures const& requiredFeatures, - span requiredExtensions, - State const& state -) : PhysicalDevice{instance, requiredFeatures, requiredExtensions} { wrap(state); } - - -auto PhysicalDevice::Create( - Instance const& instance, - span surfaces, - span queueProperties, - span requiredExtensions, - utils::PhysicalDeviceFeatures const& requiredFeatures, - vk_not_null vkDevice -) -> std::optional { - PhysicalDevice physicalDevice{instance, requiredFeatures, requiredExtensions, surfaces, vkDevice}; - - if(!physicalDevice.suitable(queueProperties)) - return std::nullopt; - - auto indices = physicalDevice.queueFamilyIndices(queueProperties); - return CreateResult{ - std::move(physicalDevice), - std::move(indices) - }; -} - -void PhysicalDevice::wrap(State const& state) { - auto const device = state.vkDevice; - debug_check_handle(device); - - _handle = device.get(); - - _extensions = !state.extensions.empty() ? state.extensions : queryExtensions(device); - _features = state.features.has_value() ? *state.features : utils::PhysicalDeviceFeatures{device, _extensions}; - _properties = state.properties.has_value() ? *state.properties : queryProperties(device); +namespace { + [[nodiscard]] vector query_extensions(vk_not_null vkDevice) { + uint32_t count = 0; + auto const countResult = vkEnumerateDeviceExtensionProperties(vkDevice.get(), nullptr, &count, nullptr); + CVK_RESULT_STABLE_THROW(countResult != VK_SUCCESS, countResult, "failed to count device extensions") {} + + vector properties(count); + auto const writeResult = vkEnumerateDeviceExtensionProperties( + vkDevice.get(), + nullptr, + &count, + properties.data() + ); + CVK_RESULT_STABLE_THROW(writeResult != VK_SUCCESS, writeResult, "failed to enumerate device extensions") {} + + return { + std::from_range, + properties | std::views::transform([](VkExtensionProperties const& extension) { + return string{extension.extensionName}; + }) + }; + } - _maxSampleCount = state.maxSampleCount != VK_SAMPLE_COUNT_FLAG_BITS_MAX_ENUM - ? state.maxSampleCount - : evalMaxSampleCount(_properties); + [[nodiscard]] VkPhysicalDeviceProperties query_properties(vk_not_null vkDevice) { + VkPhysicalDeviceProperties properties{}; + vkGetPhysicalDeviceProperties(vkDevice.get(), &properties); + return properties; + } - _memProperties = state.memProperties.has_value() ? *state.memProperties : queryMemoryProperties(device); - _queueFamilies = !state.queueFamilies.empty() ? state.queueFamilies : queryQueueFamilies({}, device); -} + [[nodiscard]] VkPhysicalDeviceMemoryProperties query_memory_properties(vk_not_null vkDevice) { + VkPhysicalDeviceMemoryProperties properties{}; + vkGetPhysicalDeviceMemoryProperties(vkDevice.get(), &properties); + return properties; + } -void PhysicalDevice::create(span surfaces, vk_not_null vkDevice) { - create(queryQueueFamilies(surfaces, vkDevice), vkDevice); -} + /** + * @param surfaces a family presents only if it presents to every surface, empty -> no present support + */ + [[nodiscard]] vector query_queue_families( + span surfaces, + vk_not_null vkDevice + ) { + uint32_t count = 0; + vkGetPhysicalDeviceQueueFamilyProperties(vkDevice.get(), &count, nullptr); + vector familyProperties(count); + vkGetPhysicalDeviceQueueFamilyProperties(vkDevice.get(), &count, familyProperties.data()); + + vector queueFamilies{}; + queueFamilies.reserve(familyProperties.size()); + + for(uint32_t i = 0; i < familyProperties.size(); i++) { + bool presentSupport = !surfaces.empty(); + + for(auto const& surface : surfaces) { + VkBool32 support = VK_FALSE; + auto const result = vkGetPhysicalDeviceSurfaceSupportKHR(vkDevice.get(), i, surface.get(), &support); + CVK_RESULT_STABLE_THROW(result != VK_SUCCESS, result, "device-surface support query failed") {} + + presentSupport = presentSupport && support == VK_TRUE; + } + + queueFamilies.push_back(QueueFamily::Vk(i, familyProperties[i], presentSupport)); + } -void PhysicalDevice::create(span queueFamilies, vk_not_null vkDevice) { - debug_check_handle(vkDevice); + return queueFamilies; + } - _handle = vkDevice.get(); - _extensions = queryExtensions(vkDevice); - _features = utils::PhysicalDeviceFeatures{vkDevice, _extensions}; - _properties = queryProperties(vkDevice); - _maxSampleCount = evalMaxSampleCount(_properties); - _memProperties = queryMemoryProperties(vkDevice); - _queueFamilies = {std::from_range, queueFamilies}; + [[nodiscard]] VkSampleCountFlagBits max_sample_count(VkPhysicalDeviceProperties const& properties) { + VkSampleCountFlags const supportedSamples = properties.limits.framebufferColorSampleCounts + & properties.limits.framebufferDepthSampleCounts; + + constexpr std::array sampleFlagBits = { + VK_SAMPLE_COUNT_64_BIT, + VK_SAMPLE_COUNT_32_BIT, + VK_SAMPLE_COUNT_16_BIT, + VK_SAMPLE_COUNT_8_BIT, + VK_SAMPLE_COUNT_4_BIT, + VK_SAMPLE_COUNT_2_BIT, + VK_SAMPLE_COUNT_1_BIT + }; + + for(auto const sampleFlagBit : sampleFlagBits) + if(supportedSamples & sampleFlagBit) + return sampleFlagBit; + + CTH_CRITICAL(true, "invalid state, sample count not supported") {} + return VK_SAMPLE_COUNT_1_BIT; + } } +PhysicalDevice::PhysicalDevice(vk_not_null vkDevice, span surfaces) : + _handle{vkDevice.get()}, + _extensions{query_extensions(vkDevice)}, + _features{dev::query_features(vkDevice, _extensions)}, + _properties{query_properties(vkDevice)}, + _memoryProperties{query_memory_properties(vkDevice)}, + _maxSampleCount{max_sample_count(_properties)}, + _queueFamilies{query_queue_families(surfaces, vkDevice)} {} -bool PhysicalDevice::suitable(span queueProperties) const { +vector PhysicalDevice::missing(DeviceFeatures const& features, span extensions) const { debug_check(*this); - auto const missingFeatures = supports(_requiredFeatures); - auto const missingExtensions = supports(_requiredExtensions); - auto const queueIndices = queueFamilyIndices(queueProperties); - bool const suitable = missingFeatures.empty() && missingExtensions.empty() && !queueIndices.empty(); - - CTH_WARN(!suitable, "physical device ({}) is not suitable", _properties.deviceName) { - if(queueIndices.empty()) - details->add("missing queue families"); + vector missing{}; + for(auto const name : _features.missing(features)) + missing.emplace_back(name); - for(auto const& missingExtension : missingExtensions) - details->add("missing extension: {}", missingExtension); - - for(auto const& missingFeature : missingFeatures) - details->add("missing feature: {}", missingFeature); - } + auto const add = [&](std::string_view extension) { + if(!std::ranges::contains(_extensions, extension) && !std::ranges::contains(missing, extension)) + missing.emplace_back(extension); + }; + for(auto const& extension : extensions) + add(extension); + for(auto const extension : dev::extensions_of(features)) + add(extension); - return suitable; + return missing; } -auto PhysicalDevice::AutoPick( - Instance const& instance, - span surfaces, - span queueProperties, - span requiredExtensions, - utils::PhysicalDeviceFeatures const& requiredFeatures -) -> std::optional { - Instance::debug_check(instance); - - auto const devices = enumerateVkDevices(instance.get()); - - auto const toString = [](std::string_view extension) { return string{extension}; }; - - vector extensions{std::from_range, requiredExtensions}; - extensions.append_range(constants::REQUIRED_DEVICE_EXTENSIONS | std::views::transform(toString)); - - bool const present = std::ranges::any_of( - queueProperties, - [](QueueFamilyProperties properties) { return contains(properties, QueueFamilyProperties::PRESENT); } - ); - if(present) extensions.append_range(constants::PRESENT_DEVICE_EXTENSIONS | std::views::transform(toString)); - - auto features = constants::required_device_features(); - features.merge(requiredFeatures); - - - std::optional chosen{}; - - for(auto const& device : devices) { - auto createResult = Create(instance, surfaces, queueProperties, extensions, features, device); - - if(createResult.has_value()) { - chosen = std::move(createResult); - break; - } - } - - if(!chosen) - return std::nullopt; +auto PhysicalDevice::queueFamilyIndices( + span queueProperties +) const -> vector { + debug_check(*this); - log::msg("chosen physical device: {}", chosen->device._properties.deviceName); + if(queueProperties.empty()) + return {}; - return chosen; -} + vector> queueIndices{queueProperties.size()}; -auto PhysicalDevice::supports( - utils::PhysicalDeviceFeatures const& requiredFeatures -) const -> vector { - debug_check(*this); + for(auto [requiredProperties, indices] : std::views::zip(queueProperties, queueIndices)) + for(auto const& family : _queueFamilies) + if((family.properties & requiredProperties) == requiredProperties) + indices.push_back(family.index); - return _features.supports(requiredFeatures); -} + vector familiesMaxQueues(_queueFamilies.size()); -auto PhysicalDevice::supports(span requiredExtensions) const -> vector { - vector missingExtensions{}; - for(auto const& requiredExtension : requiredExtensions) - if(!std::ranges::contains(_extensions, requiredExtension)) missingExtensions.emplace_back(requiredExtension); + for(auto [src, dst] : std::views::zip(_queueFamilies, familiesMaxQueues)) + dst = src.vkProperties.queueCount; - return missingExtensions; + return cth::alg::assign(queueIndices, familiesMaxQueues); } auto PhysicalDevice::findSupportedFormat( @@ -198,149 +178,57 @@ auto PhysicalDevice::findSupportedFormat( return VK_FORMAT_MAX_ENUM; } -auto PhysicalDevice::queueFamilyIndices( - span queueProperties -) const -> vector { - debug_check(*this); - - if(queueProperties.empty()) - return {}; - - vector> queueIndices{queueProperties.size()}; - - for(auto [requiredProperties, indices] : std::views::zip(queueProperties, queueIndices)) - for(auto const& family : _queueFamilies) - if((family.properties & requiredProperties) == requiredProperties) - indices.push_back(family.index); - - vector familiesMaxQueues(_queueFamilies.size()); - - for(auto [src, dst] : std::views::zip(_queueFamilies, familiesMaxQueues)) - dst = src.vkProperties.queueCount; - - return cth::alg::assign(queueIndices, familiesMaxQueues); -} +QueueFamily const* PhysicalDevice::queueFamily(queue_family_index_t idx) const { + auto const it = std::ranges::find(_queueFamilies, idx, &QueueFamily::index); -bool PhysicalDevice::supportsQueueSet(span queueProperties) const { - debug_check(*this); + if(it == _queueFamilies.end()) + return nullptr; - return !queueFamilyIndices(queueProperties).empty(); + return &*it; } -vector PhysicalDevice::enumerateVkDevices(vk_not_null vkInstance) { - Instance::debug_check_handle(vkInstance); +vector enumerate_physical_devices(Instance const& instance, span surfaces) { + Instance::debug_check(instance); - uint32_t deviceCount = 0; - auto const countResult = vkEnumeratePhysicalDevices(vkInstance.get(), &deviceCount, nullptr); + uint32_t count = 0; + auto const countResult = vkEnumeratePhysicalDevices(instance.get(), &count, nullptr); CVK_RESULT_STABLE_THROW(countResult != VK_SUCCESS, countResult, "failed to count physical devices") {} - vector devices(deviceCount); - auto const writeResult = vkEnumeratePhysicalDevices(vkInstance.get(), &deviceCount, devices.data()); + vector vkDevices(count); + auto const writeResult = vkEnumeratePhysicalDevices(instance.get(), &count, vkDevices.data()); CVK_RESULT_STABLE_THROW(writeResult != VK_SUCCESS, writeResult, "failed to enumerate physical devices") {} - return devices; -} - -vector PhysicalDevice::queryExtensions(vk_not_null vkDevice) { - uint32_t extensionCount = 0; - auto const countResult = vkEnumerateDeviceExtensionProperties(vkDevice.get(), nullptr, &extensionCount, nullptr); - CVK_RESULT_STABLE_THROW(countResult != VK_SUCCESS, countResult, "failed to count device extensions") {} - - vector availableExtensions(extensionCount); - auto const writeResult = vkEnumerateDeviceExtensionProperties( - vkDevice.get(), - nullptr, - &extensionCount, - availableExtensions.data() - ); - CVK_RESULT_STABLE_THROW( - writeResult != VK_SUCCESS, - writeResult, - "failed to enumerate device extension properties" - ) {} - - vector extensions(extensionCount); - std::ranges::transform( - availableExtensions, - extensions.begin(), - [](VkExtensionProperties const& extension) { return extension.extensionName; } - ); - - return extensions; -} - -VkPhysicalDeviceProperties PhysicalDevice::queryProperties(vk_not_null vkDevice) { - VkPhysicalDeviceProperties properties{}; - vkGetPhysicalDeviceProperties(vkDevice.get(), &properties); - return properties; -} - -VkPhysicalDeviceMemoryProperties PhysicalDevice::queryMemoryProperties(vk_not_null vkDevice) { - VkPhysicalDeviceMemoryProperties memProperties{}; - vkGetPhysicalDeviceMemoryProperties(vkDevice.get(), &memProperties); - return memProperties; + return { + std::from_range, + vkDevices | std::views::transform([surfaces](VkPhysicalDevice vkDevice) { + return PhysicalDevice{vkDevice, surfaces}; + }) + }; } -vector PhysicalDevice::queryQueueFamilies( +std::optional pick_physical_device( + Instance const& instance, span surfaces, - vk_not_null vkDevice + span queueProperties, + DeviceFeatures const& features, + span extensions ) { - uint32_t count = 0; - vkGetPhysicalDeviceQueueFamilyProperties(vkDevice.get(), &count, nullptr); - vector familyProperties(count); - vkGetPhysicalDeviceQueueFamilyProperties(vkDevice.get(), &count, familyProperties.data()); - - vector queueFamilies{}; - queueFamilies.reserve(familyProperties.size()); - - for(uint32_t i = 0; i < familyProperties.size(); i++) { - bool presentSupport = !surfaces.empty(); + for(auto& device : enumerate_physical_devices(instance, surfaces)) { + auto const missing = device.missing(features, extensions); + auto indices = device.queueFamilyIndices(queueProperties); - for(auto const& surface : surfaces) { - VkBool32 support = VK_FALSE; - auto const result = vkGetPhysicalDeviceSurfaceSupportKHR(vkDevice.get(), i, surface.get(), &support); - CVK_RESULT_STABLE_THROW(result != VK_SUCCESS, result, "device-surface support query failed") {} - - presentSupport = presentSupport && support == VK_TRUE; + if(missing.empty() && !indices.empty()) { + log::msg("chosen physical device: {}", device.properties().deviceName); + return PhysicalDevicePick{std::move(device), std::move(indices)}; } - queueFamilies.push_back(QueueFamily::Vk(i, familyProperties[i], presentSupport)); + cth::dt::string_joiner reasons{", "}; + if(indices.empty()) reasons += "queue families"; + for(auto const& name : missing) + reasons += name; + log::msg("physical device {} misses: {}", device.properties().deviceName, std::string{reasons}); } - return queueFamilies; -} - -VkSampleCountFlagBits PhysicalDevice::evalMaxSampleCount(VkPhysicalDeviceProperties const& properties) { - VkSampleCountFlags const supportedSamples = properties.limits.framebufferColorSampleCounts - & properties.limits.framebufferDepthSampleCounts; - - constexpr std::array sampleFlagBits = { - VK_SAMPLE_COUNT_64_BIT, - VK_SAMPLE_COUNT_32_BIT, - VK_SAMPLE_COUNT_16_BIT, - VK_SAMPLE_COUNT_8_BIT, - VK_SAMPLE_COUNT_4_BIT, - VK_SAMPLE_COUNT_2_BIT, - VK_SAMPLE_COUNT_1_BIT - }; - - for(auto const sampleFlagBit : sampleFlagBits) - if(supportedSamples & sampleFlagBit) - return sampleFlagBit; - - CTH_CRITICAL(true, "invalid state, sample count not supported") {} - return VK_SAMPLE_COUNT_1_BIT; -} - -QueueFamily const* PhysicalDevice::queueFamily(queue_family_index_t idx) const { - auto const it = std::ranges::find_if( - _queueFamilies, - [idx](QueueFamily const& family) { return family.index == idx; } - ); - - if(it == _queueFamilies.end()) - return nullptr; - - return &*it; + return std::nullopt; } } diff --git a/cvk/src/utility/device/device_features.cpp b/cvk/src/utility/device/device_features.cpp new file mode 100644 index 00000000..579f10eb --- /dev/null +++ b/cvk/src/utility/device/device_features.cpp @@ -0,0 +1,20 @@ +#include "cvk/utility/device/device_features.hpp" + +#include + + +namespace cvk { +std::vector DeviceFeatures::missing(DeviceFeatures const& required) const { + //IMPLEMENT C++26 reflection gives the names, boost-pfr goes + static constexpr auto NAMES = boost::pfr::names_as_array(); + + auto const available = dev::to_bits(*this); + auto const needed = dev::to_bits(required); + + std::vector names{}; + for(size_t i = 0; i < dev::FEATURE_COUNT; ++i) + if(needed[i] && !available[i]) names.push_back(NAMES[i]); + + return names; +} +} diff --git a/cvk/src/utility/device/feature_chain.cpp b/cvk/src/utility/device/feature_chain.cpp new file mode 100644 index 00000000..a98f8fb3 --- /dev/null +++ b/cvk/src/utility/device/feature_chain.cpp @@ -0,0 +1,96 @@ +#include "cvk/utility/device/feature_chain.hpp" + +#include +#include +#include + + +namespace cvk::dev { +using std::span; + +namespace { + /** + * @return the VkBool32 flags of the feature struct + */ + template + [[nodiscard]] auto flags(T& features) { + using feature_t = std::remove_const_t; + using bool_t = std::conditional_t, VkBool32 const, VkBool32>; + using byte_t = std::conditional_t, std::byte const, std::byte>; + + constexpr size_t count = feature_struct::COUNT; + + auto* const begin = reinterpret_cast(&features) + feature_struct::OFFSET; + return std::span{reinterpret_cast(begin), count}; + } + + template + [[nodiscard]] feature_structs_t empty_structs(std::type_identity>) { + return feature_structs_t{Ts{.sType = feature_struct::TYPE, .pNext = nullptr}...}; + } +} + +FeatureChain::FeatureChain(span extensions) : + _structs{empty_structs(std::type_identity{})} { + [&](std::index_sequence) { + ((_linked[Is] = [&] { + constexpr auto name = feature_struct>::EXTENSION; + return name.empty() || std::ranges::contains(extensions, name); + }()), ...); + }(std::make_index_sequence{}); + + relink(); +} + +FeatureChain::FeatureChain(DeviceFeatures const& features, span extensions) : + FeatureChain{extensions} { + auto const bits = to_bits(features); + + std::apply( + [&](Ts&... structs) { + ([&] { + for(auto const [index, flag] : std::views::enumerate(flags(structs))) + flag = bits[feature_offset + static_cast(index)] ? VK_TRUE : VK_FALSE; + }(), ...); + }, + _structs + ); +} + +DeviceFeatures FeatureChain::features() const { + feature_bits_t bits{}; + + std::apply( + [&](Ts const&... structs) { + ([&] { + for(auto const [index, flag] : std::views::enumerate(flags(structs))) + bits[feature_offset + static_cast(index)] = flag == VK_TRUE; + }(), ...); + }, + _structs + ); + + return to_features(bits); +} + +void FeatureChain::relink() { + void* next = nullptr; + + // back to front: each linked struct points to the next linked one + [&](std::index_sequence) { + ([&] { + constexpr size_t index = FEATURE_STRUCTS - 1 - Is; + auto& features = std::get(_structs); + + features.pNext = _linked[index] ? next : nullptr; + if(_linked[index]) next = &features; + }(), ...); + }(std::make_index_sequence{}); +} + +DeviceFeatures query_features(vk_not_null vkDevice, span extensions) { + FeatureChain chain{extensions}; + vkGetPhysicalDeviceFeatures2(vkDevice.get(), chain.get()); + return chain.features(); +} +} diff --git a/cvk/src/utility/device/physical_device_features.cpp b/cvk/src/utility/device/physical_device_features.cpp deleted file mode 100644 index 7f403eea..00000000 --- a/cvk/src/utility/device/physical_device_features.cpp +++ /dev/null @@ -1,89 +0,0 @@ -#include "cvk/utility/device/physical_device_features.hpp" - -#include -#include -#include - - -namespace cvk::utils { -PhysicalDeviceFeatures::PhysicalDeviceFeatures() : _chain{ - VkPhysicalDeviceFeatures2{.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2, .pNext = nullptr, .features{}}, - VkPhysicalDeviceVulkan11Features{.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_1_FEATURES, .pNext = nullptr}, - VkPhysicalDeviceVulkan12Features{.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_2_FEATURES, .pNext = nullptr}, - VkPhysicalDeviceVulkan13Features{.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_3_FEATURES, .pNext = nullptr}, - VkPhysicalDeviceSwapchainMaintenance1FeaturesKHR{ - .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SWAPCHAIN_MAINTENANCE_1_FEATURES_KHR, - .pNext = nullptr, - .swapchainMaintenance1 = VK_FALSE, - }, - VkPhysicalDeviceDynamicRenderingLocalReadFeaturesKHR{ - .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DYNAMIC_RENDERING_LOCAL_READ_FEATURES_KHR, - .pNext = nullptr, - .dynamicRenderingLocalRead = VK_FALSE, - }, -} { link({}); } - -PhysicalDeviceFeatures::PhysicalDeviceFeatures( - vk_not_null vkDevice, - std::span extensions -) : PhysicalDeviceFeatures{} { - link(extensions); - vkGetPhysicalDeviceFeatures2(vkDevice.get(), &features()); -} - -auto PhysicalDeviceFeatures::supports( - PhysicalDeviceFeatures const& requiredFeatures -) const -> std::vector { - std::vector missingFeatures{}; - - auto const check = [&](T const& available, T const& required) { - auto const flags = std::views::zip(dev::flags(available), dev::flags(required)); - - for(auto const [index, flag] : std::views::enumerate(flags)) - if(std::get<1>(flag) && !std::get<0>(flag)) - missingFeatures.emplace_back(available.sType, static_cast(index)); - }; - - std::apply( - [&](Ts const&... available) { (check(available, std::get(requiredFeatures._chain)), ...); }, - _chain - ); - - return missingFeatures; -} - -void PhysicalDeviceFeatures::merge(PhysicalDeviceFeatures const& other) { - auto const merge = [&](T& to, T const& from) { - for(auto [toFlag, fromFlag] : std::views::zip(dev::flags(to), dev::flags(from))) - toFlag |= fromFlag; - }; - - std::apply([&](Ts&... to) { (merge(to, std::get(other._chain)), ...); }, _chain); -} - -void PhysicalDeviceFeatures::link(std::span extensions) { - [&](std::index_sequence) { - ((_linked[Is] = [&] { - constexpr auto name = dev::feature_extension>::NAME; - return name.empty() || std::ranges::contains(extensions, name); - }()), ...); - }(std::make_index_sequence{}); - - relink(); -} - -void PhysicalDeviceFeatures::relink() { - void* next = nullptr; - - // back to front: each linked struct points to the next linked one - [&](std::index_sequence) { - ([&] { - constexpr size_t index = STRUCTS - 1 - Is; - auto& features = std::get(_chain); - - features.pNext = _linked[index] ? next : nullptr; - if(_linked[index]) next = &features; - }(), ...); - }(std::make_index_sequence{}); -} -} diff --git a/cvk/tests/src/base/allocator_test.cpp b/cvk/tests/src/base/allocator_test.cpp index 3f478b0f..49f18000 100644 --- a/cvk/tests/src/base/allocator_test.cpp +++ b/cvk/tests/src/base/allocator_test.cpp @@ -58,12 +58,12 @@ CVK_TEST(Allocator, move_assignment_destroys_the_old_allocator) { EXPECT_FALSE(other.created()); } -CVK_TEST(Allocator, flags_follow_the_required_features) { +CVK_TEST(Allocator, flags_follow_the_enabled_features) { auto config = test::Context::CONFIG; - config.requiredFeatures.features().bufferDeviceAddress = VK_TRUE; + config.features.bufferDeviceAddress = true; test::Context const context{config}; - auto const flags = Allocator::flags(context.core.physicalDevice()); + auto const flags = Allocator::flags(context.core.device()); EXPECT_NE(flags & VMA_ALLOCATOR_CREATE_BUFFER_DEVICE_ADDRESS_BIT, 0u); } @@ -71,7 +71,7 @@ CVK_TEST(Allocator, flags_follow_the_required_features) { CVK_TEST(Allocator, no_flags_without_the_features) { test::Context const context{}; - auto const flags = Allocator::flags(context.core.physicalDevice()); + auto const flags = Allocator::flags(context.core.device()); EXPECT_EQ(flags & VMA_ALLOCATOR_CREATE_BUFFER_DEVICE_ADDRESS_BIT, 0u); } diff --git a/cvk/tests/src/base/core_test.cpp b/cvk/tests/src/base/core_test.cpp index 40db6b62..afff61df 100644 --- a/cvk/tests/src/base/core_test.cpp +++ b/cvk/tests/src/base/core_test.cpp @@ -7,6 +7,7 @@ #include "cvk/base/device.hpp" #include "cvk/base/instance.hpp" #include "cvk/base/physical_device.hpp" +#include "cvk/utility/constant/device_constants.hpp" #include "cvk/utility/constants.hpp" #include "cvk/utility/vk_exceptions.hpp" @@ -72,7 +73,7 @@ CVK_TEST(Core, create_without_queue_sets) { CVK_TEST(Core, create_one_queue_per_entry) { Instance const instance{APP_NAME, NO_EXTENSIONS, std::nullopt}; - if(!PhysicalDevice::AutoPick(instance, {}, TWO_QUEUES.queueProperties)) + if(!pick_physical_device(instance, {}, TWO_QUEUES.queueProperties, constants::BASE_DEVICE_FEATURES)) GTEST_SKIP() << "needs a device with two queues"; Core const core{instance, {}, TWO_QUEUES}; diff --git a/cvk/tests/src/base/device_test.cpp b/cvk/tests/src/base/device_test.cpp index 9fe79785..88d2fe65 100644 --- a/cvk/tests/src/base/device_test.cpp +++ b/cvk/tests/src/base/device_test.cpp @@ -5,11 +5,13 @@ #include "cvk/base/device.hpp" #include "cvk/base/instance.hpp" #include "cvk/base/physical_device.hpp" +#include "cvk/utility/constant/device_constants.hpp" #include "cvk/utility/constants.hpp" #include #include #include +#include namespace cvk { @@ -64,7 +66,7 @@ CVK_TEST(Device, create_retrieves_the_queue) { CVK_TEST(Device, create_retrieves_the_queues) { Instance const instance{APP_NAME, NO_EXTENSIONS, std::nullopt}; - auto const result = PhysicalDevice::AutoPick(instance, {}, THREE_QUEUES); + auto const result = pick_physical_device(instance, {}, THREE_QUEUES, {}); if(!result) GTEST_SKIP() << "needs a device with three queues"; auto const& [physicalDevice, familyIndices] = *result; @@ -179,11 +181,24 @@ CVK_TEST(Device, present_enables_swapchain_maintenance1) { auto const [physicalDevice, familyIndices] = test::pick(instance, queues, surfaces); - Device const device{instance, physicalDevice, Device::Config{familyIndices}}; + Device const device{ + instance, + physicalDevice, + Device::Config{ + .queueFamilyIndices = familyIndices, + .features = constants::PRESENT_DEVICE_FEATURES, + .extensions = {VK_KHR_SWAPCHAIN_EXTENSION_NAME}, + } + }; ASSERT_TRUE(device.created()); ASSERT_EQ(device.queues().size(), 1); EXPECT_TRUE(device.queues()[0].can_present()); + EXPECT_TRUE(device.features().swapchainMaintenance1); + + // the feature bit adds its extension + std::string const maintenance1{VK_KHR_SWAPCHAIN_MAINTENANCE_1_EXTENSION_NAME}; + EXPECT_TRUE(std::ranges::contains(device.extensions(), maintenance1)); EXPECT_NE(device.functions()->vkCreateSwapchainKHR, nullptr); EXPECT_NE(device.functions()->vkReleaseSwapchainImagesKHR, nullptr); } diff --git a/cvk/tests/src/base/physical_device_test.cpp b/cvk/tests/src/base/physical_device_test.cpp index 63322dd6..a9b2f87b 100644 --- a/cvk/tests/src/base/physical_device_test.cpp +++ b/cvk/tests/src/base/physical_device_test.cpp @@ -24,20 +24,16 @@ namespace { }; constexpr std::array PRESENT_QUEUES{QueueFamilyProperties::GRAPHICS | QueueFamilyProperties::PRESENT}; - bool requires_extension(PhysicalDevice const& device, std::string_view extension) { - return std::ranges::contains(device.requiredExtensions(), extension); - } + std::vector const UNKNOWN_EXTENSION{"VK_CVK_extension_that_does_not_exist"}; } -CVK_TEST(PhysicalDevice, auto_pick_finds_the_queues) { +CVK_TEST(PhysicalDevice, pick_finds_the_queues) { Instance const instance{APP_NAME, NO_EXTENSIONS, std::nullopt}; - auto const result = PhysicalDevice::AutoPick(instance, {}, QUEUES); - - ASSERT_TRUE(result.has_value()); - auto const& [device, familyIndices] = *result; + auto const pick = pick_physical_device(instance, {}, QUEUES, constants::BASE_DEVICE_FEATURES); - ASSERT_TRUE(device.created()); + ASSERT_TRUE(pick.has_value()); + auto const& [device, familyIndices] = *pick; ASSERT_EQ(familyIndices.size(), QUEUES.size()); for(auto const [familyIndex, properties] : std::views::zip(familyIndices, QUEUES)) { @@ -47,83 +43,55 @@ CVK_TEST(PhysicalDevice, auto_pick_finds_the_queues) { } } -CVK_TEST(PhysicalDevice, auto_pick_adds_the_defaults) { +CVK_TEST(PhysicalDevice, picked_device_has_the_features) { Instance const instance{APP_NAME, NO_EXTENSIONS, std::nullopt}; auto const [device, _] = test::pick(instance, QUEUES); - EXPECT_TRUE(device.supports(constants::required_device_features()).empty()); - EXPECT_TRUE(device.requiredFeatures().supports(constants::required_device_features()).empty()); - - for(auto const extension : constants::PRESENT_DEVICE_EXTENSIONS) - EXPECT_FALSE(requires_extension(device, extension)) << extension; - for(auto const extension : constants::REQUIRED_DEVICE_EXTENSIONS) - EXPECT_TRUE(requires_extension(device, extension)) << extension; + EXPECT_TRUE(device.missing(constants::BASE_DEVICE_FEATURES).empty()); + EXPECT_TRUE(device.features().dynamicRenderingLocalRead); + std::string const localRead{VK_KHR_DYNAMIC_RENDERING_LOCAL_READ_EXTENSION_NAME}; + EXPECT_TRUE(std::ranges::contains(device.extensions(), localRead)); } -CVK_TEST(PhysicalDevice, queries_the_features_of_its_extensions) { +CVK_TEST(PhysicalDevice, missing_names_the_extensions) { Instance const instance{APP_NAME, NO_EXTENSIONS, std::nullopt}; - auto const [device, _] = test::pick(instance, QUEUES); - using local_read_t = VkPhysicalDeviceDynamicRenderingLocalReadFeaturesKHR; - EXPECT_TRUE(device.features().linked()); - EXPECT_TRUE(device.features().features().dynamicRenderingLocalRead); + EXPECT_EQ(device.missing({}, UNKNOWN_EXTENSION), UNKNOWN_EXTENSION); +} + +CVK_TEST(PhysicalDevice, pick_with_a_missing_extension_finds_nothing) { + Instance const instance{APP_NAME, NO_EXTENSIONS, std::nullopt}; + + EXPECT_FALSE(pick_physical_device(instance, {}, QUEUES, {}, UNKNOWN_EXTENSION).has_value()); } -CVK_TEST(PhysicalDevice, auto_pick_present_without_surfaces_finds_nothing) { +CVK_TEST(PhysicalDevice, pick_present_without_surfaces_finds_nothing) { Instance const instance{APP_NAME, NO_EXTENSIONS, std::nullopt}; - EXPECT_FALSE(PhysicalDevice::AutoPick(instance, {}, PRESENT_QUEUES).has_value()); + EXPECT_FALSE(pick_physical_device(instance, {}, PRESENT_QUEUES, {}).has_value()); } -CVK_TEST(PhysicalDevice, auto_pick_present_requires_the_present_extensions) { +CVK_TEST(PhysicalDevice, pick_present_finds_a_family_of_the_surface) { Instance const instance{APP_NAME, test::headless_extensions(), std::nullopt}; std::array const surfaces{test::headless_surface(instance)}; auto const [device, familyIndices] = test::pick(instance, PRESENT_QUEUES, surfaces); - for(auto const extension : constants::PRESENT_DEVICE_EXTENSIONS) - EXPECT_TRUE(requires_extension(device, extension)) << extension; - + EXPECT_TRUE(device.features().swapchainMaintenance1); ASSERT_EQ(familyIndices.size(), 1); EXPECT_TRUE(surfaces[0].supportsFamily(device, familyIndices[0])); } -CVK_TEST(PhysicalDevice, auto_pick_missing_extension_finds_nothing) { +CVK_TEST(PhysicalDevice, enumerate_without_surfaces_has_no_present) { Instance const instance{APP_NAME, NO_EXTENSIONS, std::nullopt}; - std::vector const extensions{"VK_CVK_extension_that_does_not_exist"}; - EXPECT_FALSE(PhysicalDevice::AutoPick(instance, {}, QUEUES, extensions).has_value()); -} - -CVK_TEST(PhysicalDevice, query_queue_families_without_surfaces_has_no_present) { - Instance const instance{APP_NAME, NO_EXTENSIONS, std::nullopt}; - - for(auto const vkDevice : PhysicalDevice::enumerateVkDevices(instance.get())) - for(auto const& family : PhysicalDevice::queryQueueFamilies({}, vkDevice)) + for(auto const& device : enumerate_physical_devices(instance)) + for(auto const& family : device.queueFamilies()) EXPECT_FALSE(contains(family.properties, QueueFamilyProperties::PRESENT)); } -CVK_TEST(PhysicalDevice, wrap_queries_the_missing_members) { - Instance const instance{APP_NAME, NO_EXTENSIONS, std::nullopt}; - auto const [picked, _] = test::pick(instance, QUEUES); - - PhysicalDevice const device{ - instance, - picked.requiredFeatures(), - picked.requiredExtensions(), - PhysicalDevice::State{.vkDevice = picked.get(), .queueFamilies = {}} - }; - - ASSERT_TRUE(device.created()); - EXPECT_EQ(device.get(), picked.get()); - EXPECT_EQ(device.maxSampleCount(), picked.maxSampleCount()); - EXPECT_EQ(device.queueFamilies().size(), picked.queueFamilies().size()); - EXPECT_TRUE(std::ranges::equal(device.extensions(), picked.extensions())); - EXPECT_STREQ(device.properties().deviceName, picked.properties().deviceName); -} - CVK_TEST(PhysicalDevice, copy_keeps_the_queried_members) { Instance const instance{APP_NAME, NO_EXTENSIONS, std::nullopt}; auto const [device, _] = test::pick(instance, QUEUES); @@ -131,9 +99,11 @@ CVK_TEST(PhysicalDevice, copy_keeps_the_queried_members) { PhysicalDevice const copy{device}; EXPECT_EQ(copy.get(), device.get()); + EXPECT_EQ(copy.features(), device.features()); + EXPECT_TRUE(std::ranges::equal(copy.extensions(), device.extensions())); EXPECT_TRUE(std::ranges::equal(copy.queueFamilies(), device.queueFamilies())); - EXPECT_TRUE(copy.supports(device.requiredFeatures()).empty()); - EXPECT_EQ(copy.features().get()->pNext, ©.features().features()); + EXPECT_EQ(copy.maxSampleCount(), device.maxSampleCount()); + EXPECT_STREQ(copy.properties().deviceName, device.properties().deviceName); } CVK_TEST(PhysicalDevice, find_supported_format) { diff --git a/cvk/tests/src/headless.cpp b/cvk/tests/src/headless.cpp index 23024fd0..0f75b859 100644 --- a/cvk/tests/src/headless.cpp +++ b/cvk/tests/src/headless.cpp @@ -2,6 +2,7 @@ #include "cvk/utility/constant/device_constants.hpp" +#include #include @@ -31,11 +32,24 @@ Surface headless_surface(Instance const& instance, Surface::Config config) { return Surface{instance, std::move(config), Surface::State{vkSurface}}; } -PhysicalDevice::CreateResult pick( +PhysicalDevicePick pick( Instance const& instance, std::span queueProperties, std::span surfaces ) { - return PhysicalDevice::AutoPick(instance, surfaces, queueProperties).value(); + auto features = constants::BASE_DEVICE_FEATURES; + std::vector extensions{}; + + bool const present = std::ranges::any_of( + queueProperties, + [](QueueFamilyProperties properties) { return contains(properties, QueueFamilyProperties::PRESENT); } + ); + if(present) { + features.merge(constants::PRESENT_DEVICE_FEATURES); + for(auto const extension : constants::PRESENT_DEVICE_EXTENSIONS) + extensions.emplace_back(extension); + } + + return pick_physical_device(instance, surfaces, queueProperties, features, extensions).value(); } } diff --git a/cvk/tests/src/headless.hpp b/cvk/tests/src/headless.hpp index f3cfb1fa..7b0b3bd5 100644 --- a/cvk/tests/src/headless.hpp +++ b/cvk/tests/src/headless.hpp @@ -21,10 +21,11 @@ namespace cvk::test { [[nodiscard]] Surface headless_surface(Instance const&, Surface::Config = {}); /** - * @brief @ref PhysicalDevice::AutoPick() for tests + * @brief @ref pick_physical_device() with @ref constants::BASE_DEVICE_FEATURES + * @details a present queue adds @ref constants::PRESENT_DEVICE_FEATURES and @ref constants::PRESENT_DEVICE_EXTENSIONS * @throws std::bad_optional_access no device meets the requirements */ -[[nodiscard]] PhysicalDevice::CreateResult pick( +[[nodiscard]] PhysicalDevicePick pick( Instance const&, std::span queueProperties, std::span surfaces = {} diff --git a/cvk/tests/src/render/sync/fence_test.cpp b/cvk/tests/src/render/sync/fence_test.cpp index 31ae507b..611ec86b 100644 --- a/cvk/tests/src/render/sync/fence_test.cpp +++ b/cvk/tests/src/render/sync/fence_test.cpp @@ -29,7 +29,7 @@ namespace { Core::Config os_fence_config() { auto config = test::Context::CONFIG; - config.requiredExtensions.append_range( + config.extensions.append_range( constants::OS_FENCE_DEVICE_EXTENSIONS | std::views::transform( [](std::string_view extension) { return std::string{extension}; } ) diff --git a/cvk/tests/src/utility/device/device_features_test.cpp b/cvk/tests/src/utility/device/device_features_test.cpp new file mode 100644 index 00000000..91830df5 --- /dev/null +++ b/cvk/tests/src/utility/device/device_features_test.cpp @@ -0,0 +1,42 @@ +#include "test.hpp" + +#include "cvk/utility/constant/device_constants.hpp" +#include "cvk/utility/device/device_features.hpp" + +#include +#include +#include + + +namespace cvk { +CVK_TEST(DeviceFeatures, default_is_false) { + static_assert(std::ranges::none_of(dev::to_bits(DeviceFeatures{}), std::identity{})); +} + +CVK_TEST(DeviceFeatures, bits_follow_the_field_order) { + static_assert(dev::to_bits(DeviceFeatures{.robustBufferAccess = true})[0]); + static_assert(dev::to_bits(DeviceFeatures{.dynamicRenderingLocalRead = true})[dev::FEATURE_COUNT - 1]); + static_assert(dev::to_features(dev::to_bits(constants::BASE_DEVICE_FEATURES)) == constants::BASE_DEVICE_FEATURES); +} + +CVK_TEST(DeviceFeatures, merge_enables_the_features_of_both) { + static_assert([] { + DeviceFeatures features{.samplerAnisotropy = true}; + features.merge(DeviceFeatures{.synchronization2 = true}); + + return features.samplerAnisotropy && features.synchronization2 && !features.dynamicRendering; + }()); +} + +CVK_TEST(DeviceFeatures, missing_names_the_features) { + DeviceFeatures const available{.timelineSemaphore = true}; + DeviceFeatures const required{.samplerAnisotropy = true, .timelineSemaphore = true, .synchronization2 = true}; + + std::vector const expected{"samplerAnisotropy", "synchronization2"}; + EXPECT_EQ(available.missing(required), expected); +} + +CVK_TEST(DeviceFeatures, nothing_misses_in_itself) { + EXPECT_TRUE(constants::BASE_DEVICE_FEATURES.missing(constants::BASE_DEVICE_FEATURES).empty()); +} +} diff --git a/cvk/tests/src/utility/device/feature_chain_test.cpp b/cvk/tests/src/utility/device/feature_chain_test.cpp new file mode 100644 index 00000000..dfc79544 --- /dev/null +++ b/cvk/tests/src/utility/device/feature_chain_test.cpp @@ -0,0 +1,127 @@ +#include "test.hpp" + +#include "cvk/utility/constant/device_constants.hpp" +#include "cvk/utility/device/feature_chain.hpp" + +#include +#include + + +namespace cvk::dev { +namespace { + using local_read_t = VkPhysicalDeviceDynamicRenderingLocalReadFeaturesKHR; + using maintenance1_t = VkPhysicalDeviceSwapchainMaintenance1FeaturesKHR; + + std::vector const LOCAL_READ{VK_KHR_DYNAMIC_RENDERING_LOCAL_READ_EXTENSION_NAME}; + std::vector const BOTH{ + VK_KHR_SWAPCHAIN_MAINTENANCE_1_EXTENSION_NAME, + VK_KHR_DYNAMIC_RENDERING_LOCAL_READ_EXTENSION_NAME, + }; + + std::vector const CORE{ + VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2, + VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_1_FEATURES, + VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_2_FEATURES, + VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_3_FEATURES, + }; + + VkBaseInStructure const* first(FeatureChain const& chain) { + return reinterpret_cast(chain.get()); + } + + /** + * @return the structs of the chain in order, every struct lies in the chain object + */ + std::vector chain_types(FeatureChain const& chain) { + std::vector types{}; + auto const* const begin = reinterpret_cast(&chain); + auto const* const end = begin + sizeof(FeatureChain); + + for(auto const* node = first(chain); node != nullptr; node = node->pNext) { + auto const* const address = reinterpret_cast(node); + EXPECT_TRUE(address >= begin && address < end) << "the chain points out of its object"; + types.push_back(node->sType); + } + return types; + } + + /** + * @return the struct of the type in the chain, nullptr -> not in the chain + */ + template + T const* find(FeatureChain const& chain) { + for(auto const* node = first(chain); node != nullptr; node = node->pNext) + if(node->sType == feature_struct::TYPE) return reinterpret_cast(node); + return nullptr; + } +} + +CVK_TEST(FeatureChain, without_extensions_links_the_core_structs) { + FeatureChain const chain{std::vector{}}; + + EXPECT_EQ(chain_types(chain), CORE); + EXPECT_TRUE(chain.linked()); + EXPECT_FALSE(chain.linked()); + EXPECT_FALSE(chain.linked()); +} + +CVK_TEST(FeatureChain, links_the_structs_of_the_extensions) { + FeatureChain const localRead{LOCAL_READ}; + auto expected = CORE; + expected.push_back(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DYNAMIC_RENDERING_LOCAL_READ_FEATURES_KHR); + EXPECT_EQ(chain_types(localRead), expected); + + FeatureChain const both{BOTH}; + expected = CORE; + expected.push_back(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SWAPCHAIN_MAINTENANCE_1_FEATURES_KHR); + expected.push_back(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DYNAMIC_RENDERING_LOCAL_READ_FEATURES_KHR); + EXPECT_EQ(chain_types(both), expected); +} + +CVK_TEST(FeatureChain, copy_and_move_link_their_own_chain) { + FeatureChain const chain{DeviceFeatures{.multiview = true}, LOCAL_READ}; + + FeatureChain const copy{chain}; + EXPECT_EQ(chain_types(copy), chain_types(chain)); + EXPECT_EQ(copy.features(), chain.features()); + + FeatureChain moved{FeatureChain{chain}}; + EXPECT_EQ(chain_types(moved), chain_types(chain)); + + moved = copy; + EXPECT_EQ(chain_types(moved), chain_types(chain)); +} + +CVK_TEST(FeatureChain, bits_go_into_their_vulkan_member) { + FeatureChain const chain{ + DeviceFeatures{.samplerAnisotropy = true, .synchronization2 = true, .dynamicRenderingLocalRead = true}, + LOCAL_READ + }; + + EXPECT_EQ(chain.get()->features.samplerAnisotropy, VK_TRUE); + EXPECT_EQ(chain.get()->features.robustBufferAccess, VK_FALSE); + + auto const* const features13 = find(chain); + ASSERT_NE(features13, nullptr); + EXPECT_EQ(features13->synchronization2, VK_TRUE); + EXPECT_EQ(features13->dynamicRendering, VK_FALSE); + + auto const* const localRead = find(chain); + ASSERT_NE(localRead, nullptr); + EXPECT_EQ(localRead->dynamicRenderingLocalRead, VK_TRUE); +} + +CVK_TEST(FeatureChain, features_round_trip) { + FeatureChain const chain{constants::BASE_DEVICE_FEATURES, LOCAL_READ}; + + EXPECT_EQ(chain.features(), constants::BASE_DEVICE_FEATURES); +} + +CVK_TEST(FeatureChain, extensions_of_the_features) { + static_assert(extensions_of(DeviceFeatures{.synchronization2 = true}).empty()); + static_assert(extensions_of(constants::PRESENT_DEVICE_FEATURES).size() == 1); + + std::vector const expected{VK_KHR_DYNAMIC_RENDERING_LOCAL_READ_EXTENSION_NAME}; + EXPECT_EQ(extensions_of(constants::BASE_DEVICE_FEATURES), expected); +} +} diff --git a/cvk/tests/src/utility/device/physical_device_features_test.cpp b/cvk/tests/src/utility/device/physical_device_features_test.cpp deleted file mode 100644 index 05bbfabc..00000000 --- a/cvk/tests/src/utility/device/physical_device_features_test.cpp +++ /dev/null @@ -1,166 +0,0 @@ -#include "test.hpp" - -#include "cvk/utility/device/physical_device_features.hpp" - -#include -#include -#include - - -namespace cvk::utils { -namespace { - void expect_linked(PhysicalDeviceFeatures const& features) { - auto const* const features2 = features.get(); - - EXPECT_EQ(features2, &features.features()); - EXPECT_EQ(features2->sType, VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2); - EXPECT_EQ(features2->pNext, &features.features()); - EXPECT_EQ( - features.features().pNext, - &features.features() - ); - EXPECT_EQ( - features.features().pNext, - &features.features() - ); - EXPECT_EQ(features.features().pNext, nullptr); - } - - using local_read_t = VkPhysicalDeviceDynamicRenderingLocalReadFeaturesKHR; - using maintenance1_t = VkPhysicalDeviceSwapchainMaintenance1FeaturesKHR; - - std::vector const LOCAL_READ{VK_KHR_DYNAMIC_RENDERING_LOCAL_READ_EXTENSION_NAME}; - std::vector const BOTH{ - VK_KHR_SWAPCHAIN_MAINTENANCE_1_EXTENSION_NAME, - VK_KHR_DYNAMIC_RENDERING_LOCAL_READ_EXTENSION_NAME, - }; - - constexpr size_t SAMPLER_ANISOTROPY_INDEX = - offsetof(VkPhysicalDeviceFeatures, samplerAnisotropy) / sizeof(VkBool32); - constexpr size_t SYNCHRONIZATION2_INDEX = - (offsetof(VkPhysicalDeviceVulkan13Features, synchronization2) - - dev::feature_flags::OFFSET) / sizeof(VkBool32); -} - -CVK_TEST(PhysicalDeviceFeatures, default_is_linked_and_false) { - PhysicalDeviceFeatures const features{}; - - expect_linked(features); - - EXPECT_FALSE(features.features().features.samplerAnisotropy); - EXPECT_FALSE(features.features().synchronization2); -} - -CVK_TEST(PhysicalDeviceFeatures, copy_links_its_own_chain) { - PhysicalDeviceFeatures features{}; - features.features().timelineSemaphore = VK_TRUE; - - PhysicalDeviceFeatures const copy{features}; - expect_linked(copy); - EXPECT_TRUE(copy.features().timelineSemaphore); - - PhysicalDeviceFeatures assigned{}; - assigned = copy; - expect_linked(assigned); - EXPECT_TRUE(assigned.features().timelineSemaphore); -} - -CVK_TEST(PhysicalDeviceFeatures, move_links_its_own_chain) { - PhysicalDeviceFeatures features{}; - features.features().multiview = VK_TRUE; - - PhysicalDeviceFeatures const moved{std::move(features)}; - expect_linked(moved); - EXPECT_TRUE(moved.features().multiview); -} - -CVK_TEST(PhysicalDeviceFeatures, extension_structs_are_not_linked_by_default) { - PhysicalDeviceFeatures const features{}; - - EXPECT_TRUE(features.linked()); - EXPECT_FALSE(features.linked()); - EXPECT_FALSE(features.linked()); -} - -CVK_TEST(PhysicalDeviceFeatures, link_adds_the_structs_of_the_extensions) { - PhysicalDeviceFeatures features{}; - - features.link(LOCAL_READ); - EXPECT_TRUE(features.linked()); - EXPECT_FALSE(features.linked()); - EXPECT_EQ(features.features().pNext, &features.features()); - EXPECT_EQ(features.features().pNext, nullptr); - - features.link(BOTH); - EXPECT_EQ(features.features().pNext, &features.features()); - EXPECT_EQ(features.features().pNext, &features.features()); - EXPECT_EQ(features.features().pNext, nullptr); - - features.link({}); - expect_linked(features); - EXPECT_FALSE(features.linked()); -} - -CVK_TEST(PhysicalDeviceFeatures, copy_keeps_the_linked_extensions) { - PhysicalDeviceFeatures features{}; - features.link(LOCAL_READ); - - PhysicalDeviceFeatures const copy{features}; - - EXPECT_TRUE(copy.linked()); - EXPECT_EQ(copy.features().pNext, ©.features()); -} - -CVK_TEST(PhysicalDeviceFeatures, supports_reports_missing_extension_features) { - PhysicalDeviceFeatures const available{}; - PhysicalDeviceFeatures required{}; - required.features().dynamicRenderingLocalRead = VK_TRUE; - - auto const missing = available.supports(required); - - ASSERT_EQ(missing.size(), 1); - EXPECT_EQ(missing[0].sType, VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DYNAMIC_RENDERING_LOCAL_READ_FEATURES_KHR); - EXPECT_EQ(missing[0].index, 0); -} - -CVK_TEST(PhysicalDeviceFeatures, supports_reports_missing_features) { - PhysicalDeviceFeatures available{}; - available.features().timelineSemaphore = VK_TRUE; - - PhysicalDeviceFeatures required{}; - required.features().features.samplerAnisotropy = VK_TRUE; - required.features().timelineSemaphore = VK_TRUE; - required.features().synchronization2 = VK_TRUE; - - auto const missing = available.supports(required); - - ASSERT_EQ(missing.size(), 2); - EXPECT_EQ(missing[0].sType, VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2); - EXPECT_EQ(missing[0].index, SAMPLER_ANISOTROPY_INDEX); - EXPECT_EQ(missing[1].sType, VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_3_FEATURES); - EXPECT_EQ(missing[1].index, SYNCHRONIZATION2_INDEX); -} - -CVK_TEST(PhysicalDeviceFeatures, supports_itself) { - PhysicalDeviceFeatures features{}; - features.features().features.samplerAnisotropy = VK_TRUE; - features.features().dynamicRendering = VK_TRUE; - - EXPECT_TRUE(features.supports(features).empty()); -} - -CVK_TEST(PhysicalDeviceFeatures, merge_enables_the_features_of_both) { - PhysicalDeviceFeatures features{}; - features.features().features.samplerAnisotropy = VK_TRUE; - - PhysicalDeviceFeatures other{}; - other.features().synchronization2 = VK_TRUE; - - features.merge(other); - - EXPECT_TRUE(features.features().features.samplerAnisotropy); - EXPECT_TRUE(features.features().synchronization2); - EXPECT_FALSE(features.features().dynamicRendering); - expect_linked(features); -} -} From 10f78c04c3c2bcdced1521613355f4c56bd0f4ec Mon Sep 17 00:00:00 2001 From: lukas <89145039+codethinki@users.noreply.github.com> Date: Mon, 19 Oct 2026 20:31:12 +0200 Subject: [PATCH 069/135] cvk: handle headers into utility/handle --- AGENTS.md | 8 ++++---- cvk/incl/cvk/base/allocator.hpp | 2 +- cvk/incl/cvk/base/device.hpp | 2 +- cvk/incl/cvk/base/instance.hpp | 2 +- cvk/incl/cvk/data/buffer/base_buffer.hpp | 2 +- cvk/incl/cvk/data/descriptor/descriptor_pool.hpp | 2 +- cvk/incl/cvk/data/descriptor/descriptor_set.hpp | 2 +- cvk/incl/cvk/data/img/image.hpp | 2 +- cvk/incl/cvk/data/img/image_view.hpp | 2 +- cvk/incl/cvk/data/img/sampler.hpp | 2 +- cvk/incl/cvk/data/memory/memory.hpp | 2 +- cvk/incl/cvk/debug/debug_messenger.hpp | 2 +- cvk/incl/cvk/render/cmd/cmd_buffer.hpp | 2 +- cvk/incl/cvk/render/cmd/cmd_pool.hpp | 2 +- .../cvk/render/pipeline/layout/descriptor_set_layout.hpp | 2 +- cvk/incl/cvk/render/pipeline/layout/pipeline_layout.hpp | 2 +- cvk/incl/cvk/render/pipeline/pipeline.hpp | 2 +- cvk/incl/cvk/render/pipeline/shader/shader.hpp | 2 +- cvk/incl/cvk/render/sync/fence.hpp | 2 +- cvk/incl/cvk/render/sync/semaphore.hpp | 2 +- cvk/incl/cvk/surface/surface.hpp | 2 +- cvk/incl/cvk/surface/swapchain.hpp | 2 +- cvk/incl/cvk/utility/{ => handle}/core_handle.hpp | 3 ++- cvk/incl/cvk/utility/{ => handle}/instance_handle.hpp | 3 ++- cvk/incl/cvk/utility/{ => handle}/unique_handle.hpp | 0 cvk/incl/cvk/utility/{ => handle}/vma_handle.hpp | 5 +++-- cvk/tests/src/utility/{ => handle}/unique_handle_test.cpp | 8 ++++---- 27 files changed, 36 insertions(+), 33 deletions(-) rename cvk/incl/cvk/utility/{ => handle}/core_handle.hpp (95%) rename cvk/incl/cvk/utility/{ => handle}/instance_handle.hpp (96%) rename cvk/incl/cvk/utility/{ => handle}/unique_handle.hpp (100%) rename cvk/incl/cvk/utility/{ => handle}/vma_handle.hpp (91%) rename cvk/tests/src/utility/{ => handle}/unique_handle_test.cpp (98%) diff --git a/AGENTS.md b/AGENTS.md index 2dafccb0..8d83e9e8 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -171,12 +171,12 @@ public: - The last public section holds what every class has and readers rarely need: copy and move members, `debug_check` functions. - Classes that own a handle are move only. A class that differs (copyable, not movable, custom move) says so in its `@brief`. - Every class of a Vulkan or VMA object declares `using vk_handle_t = VkX;` as its first type, also classes that do not own their handle. `get()` returns `vk_handle_t`. -- They store the handle in `cvk::unique_handle` (`cvk/utility/unique_handle.hpp`): a `std::unique_ptr` with a deleter that holds the destroy context and calls the static `destroy`. +- They store the handle in `cvk::unique_handle` (`cvk/utility/handle/unique_handle.hpp`): a `std::unique_ptr` with a deleter that holds the destroy context and calls the static `destroy`. Deleters: `core_deleter` (the core), `vma_deleter` (the core, destroys through its allocator), `instance_deleter` (the `VkInstance`), `handle_deleter` (no context), a deleter derived from them or one of its own (`BaseCmdBuffer` frees through its pool). The deleters require `vk_handle_owner`: `T` declares `vk_handle_t`. - Device objects use `core_handle` (`cvk/utility/core_handle.hpp`), short for `unique_handle>`. - Objects with VMA memory use `vma_handle` (`cvk/utility/vma_handle.hpp`), short for `unique_handle>`. - Instance objects use `instance_handle` (`cvk/utility/instance_handle.hpp`), short for `unique_handle>`. + Device objects use `core_handle` (`cvk/utility/handle/core_handle.hpp`), short for `unique_handle>`. + Objects with VMA memory use `vma_handle` (`cvk/utility/handle/vma_handle.hpp`), short for `unique_handle>`. + Instance objects use `instance_handle` (`cvk/utility/handle/instance_handle.hpp`), short for `unique_handle>`. Other deleters: `unique_handle>`. - Handle and context go in together: `_handle.reset(ptr, core())`. A missing context argument does not compile. - Move constructor and move assignment are `= default`. The move assignment destroys the old handle with the old context. diff --git a/cvk/incl/cvk/base/allocator.hpp b/cvk/incl/cvk/base/allocator.hpp index e6f470dc..90d447aa 100644 --- a/cvk/incl/cvk/base/allocator.hpp +++ b/cvk/incl/cvk/base/allocator.hpp @@ -1,7 +1,7 @@ #pragma once #include "cvk/utility/constants.hpp" +#include "cvk/utility/handle/unique_handle.hpp" #include "cvk/utility/types.hpp" -#include "cvk/utility/unique_handle.hpp" #include #include diff --git a/cvk/incl/cvk/base/device.hpp b/cvk/incl/cvk/base/device.hpp index ccaf06b0..eec36f28 100644 --- a/cvk/incl/cvk/base/device.hpp +++ b/cvk/incl/cvk/base/device.hpp @@ -3,8 +3,8 @@ #include "device_table.hpp" #include "cvk/base/queue/queue.hpp" +#include "cvk/utility/handle/unique_handle.hpp" #include "cvk/utility/types.hpp" -#include "cvk/utility/unique_handle.hpp" #include diff --git a/cvk/incl/cvk/base/instance.hpp b/cvk/incl/cvk/base/instance.hpp index 9cd0d377..fefb4bb6 100644 --- a/cvk/incl/cvk/base/instance.hpp +++ b/cvk/incl/cvk/base/instance.hpp @@ -2,8 +2,8 @@ #include "cvk/debug/debug_messenger.hpp" #include "cvk/utility/constants.hpp" +#include "cvk/utility/handle/unique_handle.hpp" #include "cvk/utility/types.hpp" -#include "cvk/utility/unique_handle.hpp" #include diff --git a/cvk/incl/cvk/data/buffer/base_buffer.hpp b/cvk/incl/cvk/data/buffer/base_buffer.hpp index a21ec95e..44460179 100644 --- a/cvk/incl/cvk/data/buffer/base_buffer.hpp +++ b/cvk/incl/cvk/data/buffer/base_buffer.hpp @@ -3,7 +3,7 @@ #include "cvk/base/device_table.hpp" #include "cvk/data/memory/memory.hpp" -#include "cvk/utility/core_handle.hpp" +#include "cvk/utility/handle/core_handle.hpp" #include "cvk/utility/types.hpp" #include diff --git a/cvk/incl/cvk/data/descriptor/descriptor_pool.hpp b/cvk/incl/cvk/data/descriptor/descriptor_pool.hpp index 0a1bac70..c472e163 100644 --- a/cvk/incl/cvk/data/descriptor/descriptor_pool.hpp +++ b/cvk/incl/cvk/data/descriptor/descriptor_pool.hpp @@ -3,7 +3,7 @@ #include "cvk/base/device_table.hpp" #include "cvk/utility/constants.hpp" -#include "cvk/utility/core_handle.hpp" +#include "cvk/utility/handle/core_handle.hpp" #include "cvk/utility/types.hpp" #include diff --git a/cvk/incl/cvk/data/descriptor/descriptor_set.hpp b/cvk/incl/cvk/data/descriptor/descriptor_set.hpp index 0b45676a..fb8a4e1a 100644 --- a/cvk/incl/cvk/data/descriptor/descriptor_set.hpp +++ b/cvk/incl/cvk/data/descriptor/descriptor_set.hpp @@ -5,8 +5,8 @@ #include "cvk/base/device_table.hpp" #include "cvk/render/pipeline/layout/descriptor_set_layout.hpp" #include "cvk/utility/constants.hpp" +#include "cvk/utility/handle/unique_handle.hpp" #include "cvk/utility/types.hpp" -#include "cvk/utility/unique_handle.hpp" #include diff --git a/cvk/incl/cvk/data/img/image.hpp b/cvk/incl/cvk/data/img/image.hpp index 6753d940..e298245f 100644 --- a/cvk/incl/cvk/data/img/image.hpp +++ b/cvk/incl/cvk/data/img/image.hpp @@ -4,7 +4,7 @@ #include "cvk/base/device_table.hpp" #include "cvk/data/memory/memory.hpp" -#include "cvk/utility/core_handle.hpp" +#include "cvk/utility/handle/core_handle.hpp" #include "cvk/utility/types.hpp" #include diff --git a/cvk/incl/cvk/data/img/image_view.hpp b/cvk/incl/cvk/data/img/image_view.hpp index 65fabdba..f6c7c20a 100644 --- a/cvk/incl/cvk/data/img/image_view.hpp +++ b/cvk/incl/cvk/data/img/image_view.hpp @@ -4,7 +4,7 @@ #include "cvk/base/device_table.hpp" #include "cvk/data/img/image.hpp" #include "cvk/utility/constants.hpp" -#include "cvk/utility/core_handle.hpp" +#include "cvk/utility/handle/core_handle.hpp" #include "cvk/utility/types.hpp" #include diff --git a/cvk/incl/cvk/data/img/sampler.hpp b/cvk/incl/cvk/data/img/sampler.hpp index f2dd0874..5077c255 100644 --- a/cvk/incl/cvk/data/img/sampler.hpp +++ b/cvk/incl/cvk/data/img/sampler.hpp @@ -3,7 +3,7 @@ #include "cvk/base/device_table.hpp" #include "cvk/utility/constants.hpp" -#include "cvk/utility/core_handle.hpp" +#include "cvk/utility/handle/core_handle.hpp" #include "cvk/utility/types.hpp" #include diff --git a/cvk/incl/cvk/data/memory/memory.hpp b/cvk/incl/cvk/data/memory/memory.hpp index 20a9eef0..28204f42 100644 --- a/cvk/incl/cvk/data/memory/memory.hpp +++ b/cvk/incl/cvk/data/memory/memory.hpp @@ -1,8 +1,8 @@ #pragma once #include "memory_config.hpp" +#include "cvk/utility/handle/vma_handle.hpp" #include "cvk/utility/types.hpp" -#include "cvk/utility/vma_handle.hpp" #include #include diff --git a/cvk/incl/cvk/debug/debug_messenger.hpp b/cvk/incl/cvk/debug/debug_messenger.hpp index b7ebedf7..f66d2e00 100644 --- a/cvk/incl/cvk/debug/debug_messenger.hpp +++ b/cvk/incl/cvk/debug/debug_messenger.hpp @@ -1,6 +1,6 @@ #pragma once #include "cvk/utility/constants.hpp" -#include "cvk/utility/instance_handle.hpp" +#include "cvk/utility/handle/instance_handle.hpp" #include "cvk/utility/types.hpp" #include diff --git a/cvk/incl/cvk/render/cmd/cmd_buffer.hpp b/cvk/incl/cvk/render/cmd/cmd_buffer.hpp index 56d08e50..223cdd98 100644 --- a/cvk/incl/cvk/render/cmd/cmd_buffer.hpp +++ b/cvk/incl/cvk/render/cmd/cmd_buffer.hpp @@ -6,8 +6,8 @@ #include "cvk/base/device_table.hpp" #include "cvk/utility/constants.hpp" +#include "cvk/utility/handle/unique_handle.hpp" #include "cvk/utility/types.hpp" -#include "cvk/utility/unique_handle.hpp" #include diff --git a/cvk/incl/cvk/render/cmd/cmd_pool.hpp b/cvk/incl/cvk/render/cmd/cmd_pool.hpp index c96465bc..276dbb10 100644 --- a/cvk/incl/cvk/render/cmd/cmd_pool.hpp +++ b/cvk/incl/cvk/render/cmd/cmd_pool.hpp @@ -3,7 +3,7 @@ #include "cvk/base/device_table.hpp" #include "cvk/utility/constants.hpp" -#include "cvk/utility/core_handle.hpp" +#include "cvk/utility/handle/core_handle.hpp" #include "cvk/utility/types.hpp" #include diff --git a/cvk/incl/cvk/render/pipeline/layout/descriptor_set_layout.hpp b/cvk/incl/cvk/render/pipeline/layout/descriptor_set_layout.hpp index b5ec5e4e..bccf6a96 100644 --- a/cvk/incl/cvk/render/pipeline/layout/descriptor_set_layout.hpp +++ b/cvk/incl/cvk/render/pipeline/layout/descriptor_set_layout.hpp @@ -1,7 +1,7 @@ #pragma once #include "cvk/base/device_table.hpp" #include "cvk/utility/constants.hpp" -#include "cvk/utility/core_handle.hpp" +#include "cvk/utility/handle/core_handle.hpp" #include "cvk/utility/types.hpp" #include diff --git a/cvk/incl/cvk/render/pipeline/layout/pipeline_layout.hpp b/cvk/incl/cvk/render/pipeline/layout/pipeline_layout.hpp index 631602c2..142dda38 100644 --- a/cvk/incl/cvk/render/pipeline/layout/pipeline_layout.hpp +++ b/cvk/incl/cvk/render/pipeline/layout/pipeline_layout.hpp @@ -1,7 +1,7 @@ #pragma once #include "cvk/base/device_table.hpp" #include "cvk/utility/constants.hpp" -#include "cvk/utility/core_handle.hpp" +#include "cvk/utility/handle/core_handle.hpp" #include "cvk/utility/types.hpp" #include diff --git a/cvk/incl/cvk/render/pipeline/pipeline.hpp b/cvk/incl/cvk/render/pipeline/pipeline.hpp index 5369325a..ef936f64 100644 --- a/cvk/incl/cvk/render/pipeline/pipeline.hpp +++ b/cvk/incl/cvk/render/pipeline/pipeline.hpp @@ -3,7 +3,7 @@ #include "cvk/base/device_table.hpp" #include "cvk/utility/constants.hpp" -#include "cvk/utility/core_handle.hpp" +#include "cvk/utility/handle/core_handle.hpp" #include "cvk/utility/types.hpp" #include diff --git a/cvk/incl/cvk/render/pipeline/shader/shader.hpp b/cvk/incl/cvk/render/pipeline/shader/shader.hpp index 863e1341..05fd3793 100644 --- a/cvk/incl/cvk/render/pipeline/shader/shader.hpp +++ b/cvk/incl/cvk/render/pipeline/shader/shader.hpp @@ -1,6 +1,6 @@ #pragma once #include "cvk/base/device_table.hpp" -#include "cvk/utility/core_handle.hpp" +#include "cvk/utility/handle/core_handle.hpp" #include "cvk/utility/types.hpp" #include diff --git a/cvk/incl/cvk/render/sync/fence.hpp b/cvk/incl/cvk/render/sync/fence.hpp index 6a21c914..d420d516 100644 --- a/cvk/incl/cvk/render/sync/fence.hpp +++ b/cvk/incl/cvk/render/sync/fence.hpp @@ -1,6 +1,6 @@ #pragma once #include "cvk/base/device_table.hpp" -#include "cvk/utility/core_handle.hpp" +#include "cvk/utility/handle/core_handle.hpp" #include "cvk/utility/types.hpp" #include diff --git a/cvk/incl/cvk/render/sync/semaphore.hpp b/cvk/incl/cvk/render/sync/semaphore.hpp index 540940a6..f4f08578 100644 --- a/cvk/incl/cvk/render/sync/semaphore.hpp +++ b/cvk/incl/cvk/render/sync/semaphore.hpp @@ -1,7 +1,7 @@ #pragma once #include "cvk/base/device_table.hpp" #include "cvk/utility/constants.hpp" -#include "cvk/utility/core_handle.hpp" +#include "cvk/utility/handle/core_handle.hpp" #include "cvk/utility/types.hpp" #include diff --git a/cvk/incl/cvk/surface/surface.hpp b/cvk/incl/cvk/surface/surface.hpp index 02cda310..ef637ac3 100644 --- a/cvk/incl/cvk/surface/surface.hpp +++ b/cvk/incl/cvk/surface/surface.hpp @@ -1,7 +1,7 @@ #pragma once #include "surface_config.hpp" -#include "cvk/utility/instance_handle.hpp" +#include "cvk/utility/handle/instance_handle.hpp" #include "cvk/utility/types.hpp" #include diff --git a/cvk/incl/cvk/surface/swapchain.hpp b/cvk/incl/cvk/surface/swapchain.hpp index 4c8486d4..9ab09b84 100644 --- a/cvk/incl/cvk/surface/swapchain.hpp +++ b/cvk/incl/cvk/surface/swapchain.hpp @@ -6,7 +6,7 @@ #include "cvk/data/img/image_view.hpp" #include "cvk/render/sync/fence.hpp" #include "cvk/render/sync/semaphore.hpp" -#include "cvk/utility/core_handle.hpp" +#include "cvk/utility/handle/core_handle.hpp" #include "cvk/utility/types.hpp" #include diff --git a/cvk/incl/cvk/utility/core_handle.hpp b/cvk/incl/cvk/utility/handle/core_handle.hpp similarity index 95% rename from cvk/incl/cvk/utility/core_handle.hpp rename to cvk/incl/cvk/utility/handle/core_handle.hpp index ee9176ad..2e6511f3 100644 --- a/cvk/incl/cvk/utility/core_handle.hpp +++ b/cvk/incl/cvk/utility/handle/core_handle.hpp @@ -1,7 +1,8 @@ #pragma once +#include "unique_handle.hpp" + #include "cvk/base/core.hpp" #include "cvk/utility/types.hpp" -#include "cvk/utility/unique_handle.hpp" namespace cvk { diff --git a/cvk/incl/cvk/utility/instance_handle.hpp b/cvk/incl/cvk/utility/handle/instance_handle.hpp similarity index 96% rename from cvk/incl/cvk/utility/instance_handle.hpp rename to cvk/incl/cvk/utility/handle/instance_handle.hpp index 0b3826bf..34efd2b9 100644 --- a/cvk/incl/cvk/utility/instance_handle.hpp +++ b/cvk/incl/cvk/utility/handle/instance_handle.hpp @@ -1,6 +1,7 @@ #pragma once +#include "unique_handle.hpp" + #include "cvk/utility/types.hpp" -#include "cvk/utility/unique_handle.hpp" #include diff --git a/cvk/incl/cvk/utility/unique_handle.hpp b/cvk/incl/cvk/utility/handle/unique_handle.hpp similarity index 100% rename from cvk/incl/cvk/utility/unique_handle.hpp rename to cvk/incl/cvk/utility/handle/unique_handle.hpp diff --git a/cvk/incl/cvk/utility/vma_handle.hpp b/cvk/incl/cvk/utility/handle/vma_handle.hpp similarity index 91% rename from cvk/incl/cvk/utility/vma_handle.hpp rename to cvk/incl/cvk/utility/handle/vma_handle.hpp index 85df7c27..1789bdec 100644 --- a/cvk/incl/cvk/utility/vma_handle.hpp +++ b/cvk/incl/cvk/utility/handle/vma_handle.hpp @@ -1,8 +1,9 @@ #pragma once +#include "core_handle.hpp" +#include "unique_handle.hpp" + #include "cvk/base/allocator.hpp" #include "cvk/base/core.hpp" -#include "cvk/utility/core_handle.hpp" -#include "cvk/utility/unique_handle.hpp" #include #include diff --git a/cvk/tests/src/utility/unique_handle_test.cpp b/cvk/tests/src/utility/handle/unique_handle_test.cpp similarity index 98% rename from cvk/tests/src/utility/unique_handle_test.cpp rename to cvk/tests/src/utility/handle/unique_handle_test.cpp index 7e93ae73..abfd08ba 100644 --- a/cvk/tests/src/utility/unique_handle_test.cpp +++ b/cvk/tests/src/utility/handle/unique_handle_test.cpp @@ -9,10 +9,10 @@ #include "cvk/render/sync/timeline_semaphore.hpp" #include "cvk/surface/surface.hpp" #include "cvk/utility/constants.hpp" -#include "cvk/utility/core_handle.hpp" -#include "cvk/utility/instance_handle.hpp" -#include "cvk/utility/unique_handle.hpp" -#include "cvk/utility/vma_handle.hpp" +#include "cvk/utility/handle/core_handle.hpp" +#include "cvk/utility/handle/instance_handle.hpp" +#include "cvk/utility/handle/unique_handle.hpp" +#include "cvk/utility/handle/vma_handle.hpp" #include #include From d0a0d011d15b06e0feefa8d21b7d0ddd4aa9187f Mon Sep 17 00:00:00 2001 From: lukas <89145039+codethinki@users.noreply.github.com> Date: Mon, 19 Oct 2026 20:32:25 +0200 Subject: [PATCH 070/135] cvk: FeatureChain not copyable or movable, auto for the struct types Only locals use the chain, a copy pointed into the old object until the relink, a self assignment relinked itself. --- cvk/incl/cvk/utility/device/feature_chain.hpp | 37 +++++++------------ cvk/src/utility/device/feature_chain.cpp | 4 +- .../src/utility/device/feature_chain_test.cpp | 16 ++------ 3 files changed, 19 insertions(+), 38 deletions(-) diff --git a/cvk/incl/cvk/utility/device/feature_chain.hpp b/cvk/incl/cvk/utility/device/feature_chain.hpp index c21bc0ff..3c94cd2f 100644 --- a/cvk/incl/cvk/utility/device/feature_chain.hpp +++ b/cvk/incl/cvk/utility/device/feature_chain.hpp @@ -42,7 +42,7 @@ struct feature_struct; template<> struct feature_struct { - static constexpr VkStructureType TYPE = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2; + static constexpr auto TYPE = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2; static constexpr size_t OFFSET = offsetof(VkPhysicalDeviceFeatures2, features); static constexpr size_t COUNT = sizeof(VkPhysicalDeviceFeatures) / sizeof(VkBool32); static constexpr std::string_view EXTENSION{}; @@ -50,7 +50,7 @@ struct feature_struct { template<> struct feature_struct { - static constexpr VkStructureType TYPE = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_1_FEATURES; + static constexpr auto TYPE = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_1_FEATURES; static constexpr size_t OFFSET = offsetof(VkPhysicalDeviceVulkan11Features, storageBuffer16BitAccess); static constexpr size_t COUNT = (offsetof(VkPhysicalDeviceVulkan11Features, shaderDrawParameters) - OFFSET) / sizeof(VkBool32) + 1; @@ -59,7 +59,7 @@ struct feature_struct { template<> struct feature_struct { - static constexpr VkStructureType TYPE = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_2_FEATURES; + static constexpr auto TYPE = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_2_FEATURES; static constexpr size_t OFFSET = offsetof(VkPhysicalDeviceVulkan12Features, samplerMirrorClampToEdge); static constexpr size_t COUNT = (offsetof(VkPhysicalDeviceVulkan12Features, subgroupBroadcastDynamicId) - OFFSET) / sizeof(VkBool32) + 1; @@ -68,7 +68,7 @@ struct feature_struct { template<> struct feature_struct { - static constexpr VkStructureType TYPE = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_3_FEATURES; + static constexpr auto TYPE = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_3_FEATURES; static constexpr size_t OFFSET = offsetof(VkPhysicalDeviceVulkan13Features, robustImageAccess); static constexpr size_t COUNT = (offsetof(VkPhysicalDeviceVulkan13Features, maintenance4) - OFFSET) / sizeof(VkBool32) + 1; @@ -77,7 +77,7 @@ struct feature_struct { template<> struct feature_struct { - static constexpr VkStructureType TYPE = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SWAPCHAIN_MAINTENANCE_1_FEATURES_KHR; + static constexpr auto TYPE = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SWAPCHAIN_MAINTENANCE_1_FEATURES_KHR; static constexpr size_t OFFSET = offsetof(VkPhysicalDeviceSwapchainMaintenance1FeaturesKHR, swapchainMaintenance1); static constexpr size_t COUNT = 1; static constexpr std::string_view EXTENSION = VK_KHR_SWAPCHAIN_MAINTENANCE_1_EXTENSION_NAME; @@ -85,7 +85,7 @@ struct feature_struct { template<> struct feature_struct { - static constexpr VkStructureType TYPE = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DYNAMIC_RENDERING_LOCAL_READ_FEATURES_KHR; + static constexpr auto TYPE = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DYNAMIC_RENDERING_LOCAL_READ_FEATURES_KHR; static constexpr size_t OFFSET = offsetof(VkPhysicalDeviceDynamicRenderingLocalReadFeaturesKHR, dynamicRenderingLocalRead); static constexpr size_t COUNT = 1; @@ -136,9 +136,8 @@ static_assert(alignof(DeviceFeatures) == 1, "DeviceFeatures must have bool field } /** - * @brief the feature structs as one pNext chain for Vulkan - * @details copyable, copy and move link the chain again, - * an extension struct is in the chain only with its extension + * @brief the feature structs as one pNext chain for Vulkan, not copyable, not movable: the chain points into the object + * @details an extension struct is in the chain only with its extension */ class FeatureChain { public: @@ -178,26 +177,16 @@ class FeatureChain { /** * @brief links the pNext members of the structs in @ref _linked */ - void relink(); + void link(); feature_structs_t _structs; std::array _linked{}; public: - FeatureChain(FeatureChain const& other) : _structs{other._structs}, _linked{other._linked} { relink(); } - FeatureChain(FeatureChain&& other) noexcept : _structs{other._structs}, _linked{other._linked} { relink(); } - FeatureChain& operator=(FeatureChain const& other) { - _structs = other._structs; - _linked = other._linked; - relink(); - return *this; - } - FeatureChain& operator=(FeatureChain&& other) noexcept { - _structs = other._structs; - _linked = other._linked; - relink(); - return *this; - } + FeatureChain(FeatureChain const& other) = delete; + FeatureChain(FeatureChain&& other) = delete; + FeatureChain& operator=(FeatureChain const& other) = delete; + FeatureChain& operator=(FeatureChain&& other) = delete; }; /** diff --git a/cvk/src/utility/device/feature_chain.cpp b/cvk/src/utility/device/feature_chain.cpp index a98f8fb3..88416747 100644 --- a/cvk/src/utility/device/feature_chain.cpp +++ b/cvk/src/utility/device/feature_chain.cpp @@ -39,7 +39,7 @@ FeatureChain::FeatureChain(span extensions) : }()), ...); }(std::make_index_sequence{}); - relink(); + link(); } FeatureChain::FeatureChain(DeviceFeatures const& features, span extensions) : @@ -73,7 +73,7 @@ DeviceFeatures FeatureChain::features() const { return to_features(bits); } -void FeatureChain::relink() { +void FeatureChain::link() { void* next = nullptr; // back to front: each linked struct points to the next linked one diff --git a/cvk/tests/src/utility/device/feature_chain_test.cpp b/cvk/tests/src/utility/device/feature_chain_test.cpp index dfc79544..6278deab 100644 --- a/cvk/tests/src/utility/device/feature_chain_test.cpp +++ b/cvk/tests/src/utility/device/feature_chain_test.cpp @@ -4,6 +4,7 @@ #include "cvk/utility/device/feature_chain.hpp" #include +#include #include @@ -78,18 +79,9 @@ CVK_TEST(FeatureChain, links_the_structs_of_the_extensions) { EXPECT_EQ(chain_types(both), expected); } -CVK_TEST(FeatureChain, copy_and_move_link_their_own_chain) { - FeatureChain const chain{DeviceFeatures{.multiview = true}, LOCAL_READ}; - - FeatureChain const copy{chain}; - EXPECT_EQ(chain_types(copy), chain_types(chain)); - EXPECT_EQ(copy.features(), chain.features()); - - FeatureChain moved{FeatureChain{chain}}; - EXPECT_EQ(chain_types(moved), chain_types(chain)); - - moved = copy; - EXPECT_EQ(chain_types(moved), chain_types(chain)); +CVK_TEST(FeatureChain, is_not_copyable_or_movable) { + static_assert(!std::is_copy_constructible_v && !std::is_move_constructible_v); + static_assert(!std::is_copy_assignable_v && !std::is_move_assignable_v); } CVK_TEST(FeatureChain, bits_go_into_their_vulkan_member) { From 6179aad3877d504028d547841dd84ef028e23889 Mon Sep 17 00:00:00 2001 From: lukas <89145039+codethinki@users.noreply.github.com> Date: Mon, 19 Oct 2026 20:51:08 +0200 Subject: [PATCH 071/135] AGENTS: the three cases of classes that differ from move only --- AGENTS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AGENTS.md b/AGENTS.md index 8d83e9e8..64d83385 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -169,7 +169,7 @@ public: - The first public section holds the API: types, constants, lifecycle, operations, accessors. - The last public section holds what every class has and readers rarely need: copy and move members, `debug_check` functions. -- Classes that own a handle are move only. A class that differs (copyable, not movable, custom move) says so in its `@brief`. +- Classes that own a handle are move only. A class that differs says so in its `@brief`: copyable, neither copyable nor movable, or a custom move. - Every class of a Vulkan or VMA object declares `using vk_handle_t = VkX;` as its first type, also classes that do not own their handle. `get()` returns `vk_handle_t`. - They store the handle in `cvk::unique_handle` (`cvk/utility/handle/unique_handle.hpp`): a `std::unique_ptr` with a deleter that holds the destroy context and calls the static `destroy`. Deleters: `core_deleter` (the core), `vma_deleter` (the core, destroys through its allocator), `instance_deleter` (the `VkInstance`), `handle_deleter` (no context), a deleter derived from them or one of its own (`BaseCmdBuffer` frees through its pool). From 0eca5128fb92ab6b1d0edd201f051fad81647702 Mon Sep 17 00:00:00 2001 From: lukas <89145039+codethinki@users.noreply.github.com> Date: Mon, 19 Oct 2026 21:09:38 +0200 Subject: [PATCH 072/135] AGENTS: named constructors are PascalCase --- AGENTS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/AGENTS.md b/AGENTS.md index 64d83385..a48fa2cd 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -73,6 +73,7 @@ ctest --test-dir out/build/gcc_release --output-on-failure | generic library type, trait, concept | `snake_case` | `basic_deletable`, `handle_t`, `destroyable` | | type alias that renames a type | `snake_case_t` | `callback_t`, `queue_set_t` | | member function | `camelCase` | `optDestroy()`, `createInfo()` | +| named constructor (static factory) | `PascalCase` | `Default()`, `DepthBuffer()`, `LayoutTransition()` | | free function | `snake_case` | `to_queue_properties()`, `default_debug_callback()` | | static check function | `debug_check`, `debug_check_handle` | | | function parameter | `camelCase` | `appName`, `requiredExtensions` | From ee4c606cd1ffedf3c6081f83a9e070e94d4580e4 Mon Sep 17 00:00:00 2001 From: lukas <89145039+codethinki@users.noreply.github.com> Date: Mon, 19 Oct 2026 21:09:39 +0200 Subject: [PATCH 073/135] cvk: Instance::Config, checks in create(), create_t constructor - Instance::Config: app name and version, engine name and version, extensions - the base constructor stores the config, create() checks the extensions and layers and throws cvk_exception (the base constructor threw before) - construct and create with create_t: Instance{config} did not create, Instance{config, std::nullopt} did - the constructor steps are private or free functions: available_instance_extensions(), available_layers() - extensions() returns a span of the enabled extensions, State carries them - the messenger is an std::optional, debugMessenger() returns it - removed: the empty REQUIRED_INSTANCE_EXTENSIONS, a second enableValidationLayers() added the extensions twice - tests: test::instance_config() --- cvk/incl/cvk/base/instance.hpp | 105 ++++---- cvk/incl/cvk/base/instance_config.hpp | 39 +++ cvk/src/base/instance.cpp | 248 +++++++----------- cvk/tests/src/base/core_test.cpp | 27 +- cvk/tests/src/base/device_test.cpp | 23 +- cvk/tests/src/base/instance_test.cpp | 53 ++-- cvk/tests/src/base/physical_device_test.cpp | 21 +- cvk/tests/src/base/queue/queue_test.cpp | 13 +- cvk/tests/src/context.cpp | 4 +- cvk/tests/src/debug/debug_messenger_test.cpp | 15 +- cvk/tests/src/surface/surface_test.cpp | 16 +- cvk/tests/src/test.hpp | 9 +- .../src/utility/handle/unique_handle_test.cpp | 2 +- 13 files changed, 286 insertions(+), 289 deletions(-) create mode 100644 cvk/incl/cvk/base/instance_config.hpp diff --git a/cvk/incl/cvk/base/instance.hpp b/cvk/incl/cvk/base/instance.hpp index fefb4bb6..0eb40a5d 100644 --- a/cvk/incl/cvk/base/instance.hpp +++ b/cvk/incl/cvk/base/instance.hpp @@ -1,4 +1,5 @@ #pragma once +#include "instance_config.hpp" #include "cvk/debug/debug_messenger.hpp" #include "cvk/utility/constants.hpp" @@ -10,15 +11,12 @@ #include #include -#include #include #include #include -#include #include - namespace cvk { /** * @brief VkInstance wrapper, owns the debug messenger @@ -26,6 +24,7 @@ namespace cvk { class Instance { public: using vk_handle_t = VkInstance; + using Config = InstanceConfig; struct State; static constexpr std::array VALIDATION_LAYERS = { @@ -51,35 +50,26 @@ class Instance { }, }; - static constexpr std::array REQUIRED_INSTANCE_EXTENSIONS{}; - /** * @brief base constructor - * @details debug builds enable the validation layers with @ref VALIDATION_LAYER_SETTINGS - * @throws cth::except::default_exception reason: required instance extension missing - * @throws cth::except::default_exception reason: required validation layer missing */ - Instance(std::string_view appName, std::span requiredExtensions); + explicit Instance(Config); /** - * @brief constructs and creates + * @brief constructs and wraps * @details calls: - * - @ref Instance(std::string_view, std::span) - * - @ref create() + * - @ref Instance(Config) + * - @ref wrap() */ - Instance( - std::string_view appName, - std::span requiredExtensions, - std::optional const& messengerConfig - ); + Instance(Config, State); /** - * @brief constructs and wraps + * @brief constructs and creates * @details calls: - * - @ref Instance(std::string_view, std::span) - * - @ref wrap() + * - @ref Instance(Config) + * - @ref create() */ - Instance(std::string_view appName, std::span requiredExtensions, State); + explicit Instance(Config, create_t, std::optional messengerConfig = std::nullopt); /** * @note calls @ref optDestroy() @@ -95,13 +85,15 @@ class Instance { void wrap(State); /** - * @brief creates the instance + * @brief creates the instance with the extensions of the config, debug builds with the validation layers * @param messengerConfig std::nullopt -> no messenger, debug builds use @ref DebugMessenger::Config::Default() * @pre @ref Cvk::initialized() * @post @ref created() * @post volk uses the functions of this instance * @note calls @ref optDestroy() * @note no validation layers -> no messenger + * @throws cvk::cvk_exception reason: instance extension missing + * @throws cvk::cvk_exception reason: validation layer missing * @throws cvk::vk_result_exception result of vkCreateInstance() */ void create(std::optional messengerConfig = std::nullopt); @@ -124,51 +116,38 @@ class Instance { State release(); /** - * @throws cth::except::default_exception reason: required extension missing + * @pre volk loaded the instance functions, @ref create() and @ref wrap() load them */ - void checkInstanceExtensionSupport(); + static void destroy(VkInstance); - /** - * @throws cth::except::default_exception reason: required validation layer missing - */ - void checkValidationLayerSupport(); + [[nodiscard]] constexpr bool created() const { return static_cast(_handle); } + [[nodiscard]] constexpr vk_handle_t get() const { return _handle.get(); } + [[nodiscard]] constexpr Config const& config() const { return _config; } /** - * @brief adds the extensions of the validation layers to the required extensions - * @note calls @ref checkValidationLayerSupport() + * @return the enabled extensions, debug builds include @ref VALIDATION_LAYER_EXTENSIONS */ - void enableValidationLayers(); - - [[nodiscard]] static std::vector getAvailableValidationLayers(); - [[nodiscard]] static std::vector getAvailableInstanceExtensions(); - [[nodiscard]] VkApplicationInfo appInfo() const; + [[nodiscard]] constexpr std::span extensions() const { return _extensions; } /** - * @pre volk loaded the instance functions, @ref create() and @ref wrap() load them + * @return nullptr -> no messenger */ - static void destroy(VkInstance); - - [[nodiscard]] constexpr bool created() const { return static_cast(_handle); } - [[nodiscard]] constexpr vk_handle_t get() const { return _handle.get(); } - [[nodiscard]] constexpr std::vector availableExtensions() const { return _availableExt; } - [[nodiscard]] constexpr std::vector requiredExtensions() const { return _requiredExt; } - [[nodiscard]] constexpr std::vector availableValidationLayers() const { return _availableLayers; } + [[nodiscard]] constexpr DebugMessenger const* debugMessenger() const { + return _debugMessenger.has_value() ? &*_debugMessenger : nullptr; + } private: void forget(); - std::string _name; + static void loadInstanceFunctions(vk_not_null); - std::vector _requiredExt{}; - std::vector _availableExt; - std::vector _availableLayers{}; + Config _config; + std::vector _extensions{}; // declared before the handle: move assignment destroys the old messenger before the old instance - std::unique_ptr _debugMessenger = nullptr; + std::optional _debugMessenger = std::nullopt; unique_handle> _handle; - static void loadInstanceFunctions(vk_not_null); - public: Instance(Instance const& other) = delete; Instance& operator=(Instance const& other) = delete; @@ -178,6 +157,18 @@ class Instance { static constexpr void debug_check(Instance const&); static constexpr void debug_check_handle(vk_not_null); }; + +/** + * @brief queries the instance extensions of the loader and the implicit layers + * @pre @ref Cvk::initialized() + */ +[[nodiscard]] std::vector available_instance_extensions(); + +/** + * @brief queries the instance layers + * @pre @ref Cvk::initialized() + */ +[[nodiscard]] std::vector available_layers(); } @@ -186,14 +177,20 @@ struct Instance::State { vk_not_null vkInstance; /** - * @brief messenger of @ref vkInstance - * @note may be nullptr + * @brief messenger of @ref vkInstance, std::nullopt -> no messenger */ - std::unique_ptr debugMessenger; + std::optional debugMessenger = std::nullopt; + + /** + * @brief the extensions that the instance enables + */ + std::vector extensions{}; }; } namespace cvk { -constexpr void Instance::debug_check(Instance const& instance) { debug_check_handle(instance.get()); } +constexpr void Instance::debug_check(Instance const& instance) { + CTH_CRITICAL(!instance.created(), "instance must be created") {} +} constexpr void Instance::debug_check_handle([[maybe_unused]] vk_not_null vkInstance) {} } diff --git a/cvk/incl/cvk/base/instance_config.hpp b/cvk/incl/cvk/base/instance_config.hpp new file mode 100644 index 00000000..4abbdd5a --- /dev/null +++ b/cvk/incl/cvk/base/instance_config.hpp @@ -0,0 +1,39 @@ +#pragma once +#include + +#include +#include +#include + + +namespace cvk { +/** + * @brief application, engine and extensions of an instance + */ +struct InstanceConfig { + std::string appName; + uint32_t appVersion = 0; + std::string engineName = "cvk"; + uint32_t engineVersion = 0; + + /** + * @brief instance extensions, debug builds add @ref Instance::VALIDATION_LAYER_EXTENSIONS + */ + std::vector extensions{}; + + /** + * @attention the names point into the config + */ + [[nodiscard]] constexpr VkApplicationInfo appInfo() const { + return VkApplicationInfo{ + .sType = VK_STRUCTURE_TYPE_APPLICATION_INFO, + .pNext = nullptr, + .pApplicationName = appName.c_str(), + .applicationVersion = appVersion, + .pEngineName = engineName.c_str(), + .engineVersion = engineVersion, + .apiVersion = VK_API_VERSION_1_3, + }; + } +}; +} diff --git a/cvk/src/base/instance.cpp b/cvk/src/base/instance.cpp index a6c37db6..11a4281f 100644 --- a/cvk/src/base/instance.cpp +++ b/cvk/src/base/instance.cpp @@ -3,54 +3,45 @@ #include "cvk/debug/debug_messenger.hpp" #include "cvk/utility/vk_exceptions.hpp" +#include + #include #include - +#include namespace cvk { -using std::span; using std::string; using std::string_view; using std::vector; +namespace { + /** + * @return the names that are not available + */ + [[nodiscard]] vector missing(std::ranges::range auto const& names, vector const& available) { + vector missing{}; + for(string_view const name : names) + if(!std::ranges::contains(available, name)) missing.emplace_back(name); + return missing; + } +} -Instance::Instance(string_view appName, span requiredExtensions) : _name(appName), - _availableExt{getAvailableInstanceExtensions()} { - _requiredExt.reserve(requiredExtensions.size() + REQUIRED_INSTANCE_EXTENSIONS.size()); - _requiredExt.append_range(requiredExtensions); - _requiredExt.append_range( - REQUIRED_INSTANCE_EXTENSIONS | std::views::transform( - [](string_view const& view) { return string{view}; } - ) - ); - - checkInstanceExtensionSupport(); +Instance::Instance(Config config) : _config{std::move(config)} {} +Instance::Instance(Config config, State state) : Instance{std::move(config)} { wrap(std::move(state)); } - if constexpr(constants::ENABLE_VALIDATION_LAYERS) enableValidationLayers(); +Instance::Instance(Config config, create_t, std::optional messengerConfig) : + Instance{std::move(config)} { + create(std::move(messengerConfig)); } -Instance::Instance( - string_view appName, - span requiredExtensions, - std::optional const& messengerConfig -) : Instance{appName, requiredExtensions} { create(messengerConfig); } - -Instance::Instance( - string_view appName, - span requiredExtensions, - State state -) : Instance{ - appName, - requiredExtensions -} { wrap(std::move(state)); } - void Instance::wrap(State state) { optDestroy(); _handle.reset(state.vkInstance.get()); _debugMessenger = std::move(state.debugMessenger); + _extensions = std::move(state.extensions); loadInstanceFunctions(state.vkInstance); } @@ -58,21 +49,34 @@ void Instance::wrap(State state) { void Instance::create(std::optional messengerConfig) { optDestroy(); - if constexpr(constants::COMPILATION_MODE == CompilationMode::DEBUG) - if(messengerConfig == std::nullopt) - messengerConfig = DebugMessenger::Config::Default(); + _extensions = _config.extensions; + if constexpr(constants::ENABLE_VALIDATION_LAYERS) { + for(string_view const extension : VALIDATION_LAYER_EXTENSIONS) + if(!std::ranges::contains(_extensions, extension)) _extensions.emplace_back(extension); - vector requiredExtVec(_requiredExt.size()); - std::ranges::transform(_requiredExt, requiredExtVec.begin(), [](auto const& str) { return str.data(); }); + if(!messengerConfig.has_value()) messengerConfig = DebugMessenger::Config::Default(); + } - auto const appInfo = this->appInfo(); - VkDebugUtilsMessengerCreateInfoEXT debugCreateInfo{}; - VkLayerSettingsCreateInfoEXT const layerSettings{ - .sType = VK_STRUCTURE_TYPE_LAYER_SETTINGS_CREATE_INFO_EXT, - .pNext = nullptr, - .settingCount = static_cast(VALIDATION_LAYER_SETTINGS.size()), - .pSettings = VALIDATION_LAYER_SETTINGS.data(), - }; + auto const missingExtensions = missing(_extensions, available_instance_extensions()); + CTH_STABLE_ERR(!missingExtensions.empty(), "instance extensions missing") { + for(auto const& extension : missingExtensions) + details->add(extension); + forget(); + throw cvk::cvk_exception{details->exception()}; + } + + if constexpr(constants::ENABLE_VALIDATION_LAYERS) { + auto const missingLayers = missing(VALIDATION_LAYERS, available_layers()); + CTH_STABLE_ERR(!missingLayers.empty(), "validation layers missing") { + for(auto const& layer : missingLayers) + details->add(layer); + forget(); + throw cvk::cvk_exception{details->exception()}; + } + } + + auto const extensions = cth::str::to_c_str_vector(_extensions); + auto const appInfo = _config.appInfo(); VkInstanceCreateInfo createInfo{ .sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO, @@ -81,41 +85,47 @@ void Instance::create(std::optional messengerConfig) { .pApplicationInfo = &appInfo, .enabledLayerCount = 0, .ppEnabledLayerNames = nullptr, + .enabledExtensionCount = static_cast(extensions.size()), + .ppEnabledExtensionNames = extensions.data(), + }; - .enabledExtensionCount = static_cast(requiredExtVec.size()), - .ppEnabledExtensionNames = requiredExtVec.data(), + VkLayerSettingsCreateInfoEXT const layerSettings{ + .sType = VK_STRUCTURE_TYPE_LAYER_SETTINGS_CREATE_INFO_EXT, + .pNext = nullptr, + .settingCount = static_cast(VALIDATION_LAYER_SETTINGS.size()), + .pSettings = VALIDATION_LAYER_SETTINGS.data(), }; + VkDebugUtilsMessengerCreateInfoEXT debugCreateInfo{}; - if constexpr(constants::ENABLE_VALIDATION_LAYERS) - if(messengerConfig != std::nullopt) { - createInfo.enabledLayerCount = static_cast(VALIDATION_LAYERS.size()); - createInfo.ppEnabledLayerNames = VALIDATION_LAYERS.data(); + if constexpr(constants::ENABLE_VALIDATION_LAYERS) { + createInfo.enabledLayerCount = static_cast(VALIDATION_LAYERS.size()); + createInfo.ppEnabledLayerNames = VALIDATION_LAYERS.data(); - debugCreateInfo = messengerConfig->createInfo(); - debugCreateInfo.pNext = &layerSettings; - createInfo.pNext = &debugCreateInfo; - } + // the messenger of the create info reports the messages of vkCreateInstance() and vkDestroyInstance() + debugCreateInfo = messengerConfig->createInfo(); + debugCreateInfo.pNext = &layerSettings; + createInfo.pNext = &debugCreateInfo; + } VkInstance ptr = VK_NULL_HANDLE; - auto const createInstanceResult = vkCreateInstance(&createInfo, nullptr, &ptr); + auto const result = vkCreateInstance(&createInfo, nullptr, &ptr); - CTH_STABLE_ERR(createInstanceResult != VK_SUCCESS, "failed to create instance!") { + CTH_STABLE_ERR(result != VK_SUCCESS, "failed to create instance") { forget(); - throw cvk::vk_result_exception{createInstanceResult, details->exception()}; + throw cvk::vk_result_exception{result, details->exception()}; } _handle.reset(ptr); loadInstanceFunctions(ptr); if constexpr(constants::ENABLE_VALIDATION_LAYERS) - if(messengerConfig != std::nullopt) - _debugMessenger = std::make_unique(*this, *messengerConfig, cvk::create); + _debugMessenger.emplace(*this, *messengerConfig, cvk::create); } void Instance::destroy() { debug_check(*this); - _debugMessenger = nullptr; + _debugMessenger.reset(); _handle.reset(); forget(); @@ -124,109 +134,55 @@ void Instance::destroy() { Instance::State Instance::release() { debug_check(*this); - State state{_handle.release(), std::move(_debugMessenger)}; + State state{ + .vkInstance = _handle.release(), + .debugMessenger = std::move(_debugMessenger), + .extensions = std::move(_extensions), + }; forget(); return state; } +void Instance::destroy(VkInstance vkInstance) { + CTH_WARN(vkInstance == nullptr, "vkInstance invalid") {} -void Instance::checkInstanceExtensionSupport() { - vector missingExtensions{}; - - for(auto& extension : _requiredExt) - if(!std::ranges::contains(_availableExt, extension)) missingExtensions.emplace_back(extension); - - CTH_STABLE_ERR(!missingExtensions.empty(), "instance extensions missing") { - std::ranges::for_each(missingExtensions, [&details](string_view extension) { details->add(extension); }); - - throw details->exception(); - } -} - -void Instance::checkValidationLayerSupport() { - if constexpr(constants::ENABLE_VALIDATION_LAYERS) { - vector missingLayers{}; - - std::ranges::for_each( - VALIDATION_LAYERS, - [&](string_view layer) { - if(!std::ranges::contains(_availableLayers, layer)) missingLayers.emplace_back(layer); - } - ); - CTH_STABLE_ERR(!missingLayers.empty(), "validation layers missing") { - std::ranges::for_each(missingLayers, [&details](string_view layer) { details->add(layer); }); - - throw details->exception(); - } - } -} - -void Instance::enableValidationLayers() { - _availableLayers = getAvailableValidationLayers(); - checkValidationLayerSupport(); + vkDestroyInstance(vkInstance, nullptr); - _requiredExt.insert( - _requiredExt.begin(), - VALIDATION_LAYER_EXTENSIONS.begin(), - VALIDATION_LAYER_EXTENSIONS.end() - ); + log::msg("destroyed instance"); } -vector Instance::getAvailableValidationLayers() { - uint32_t layerCount = 0; - vkEnumerateInstanceLayerProperties(&layerCount, nullptr); - - std::vector availableLayers(layerCount); - vkEnumerateInstanceLayerProperties(&layerCount, availableLayers.data()); - - vector layers(availableLayers.size()); - std::ranges::transform( - availableLayers, - layers.begin(), - [](VkLayerProperties const& layer) { return string(layer.layerName); } - ); - - return layers; +void Instance::forget() { + _debugMessenger.reset(); + _extensions.clear(); } -vector Instance::getAvailableInstanceExtensions() { - uint32_t extensionCount = 0; - vkEnumerateInstanceExtensionProperties(nullptr, &extensionCount, nullptr); - std::vector availableExtensions(extensionCount); - vkEnumerateInstanceExtensionProperties(nullptr, &extensionCount, availableExtensions.data()); - - vector availableExtensionsStr(availableExtensions.size()); - std::ranges::transform( - availableExtensions, - availableExtensionsStr.begin(), - [](VkExtensionProperties const& ext) { return ext.extensionName; } - ); - - - return availableExtensionsStr; +void Instance::loadInstanceFunctions(vk_not_null vkInstance) { volkLoadInstanceOnly(vkInstance.get()); } } -VkApplicationInfo Instance::appInfo() const { - return VkApplicationInfo{ - .sType = VK_STRUCTURE_TYPE_APPLICATION_INFO, - .pNext = nullptr, - .pApplicationName = _name.c_str(), - .applicationVersion = 0, - .pEngineName = _name.c_str(), - .engineVersion = 0, - .apiVersion = VK_API_VERSION_1_3, +namespace cvk { +vector available_instance_extensions() { + uint32_t count = 0; + vkEnumerateInstanceExtensionProperties(nullptr, &count, nullptr); + vector properties(count); + vkEnumerateInstanceExtensionProperties(nullptr, &count, properties.data()); + + return { + std::from_range, + properties | std::views::transform([](VkExtensionProperties const& extension) { + return string{extension.extensionName}; + }) }; } -void Instance::destroy(VkInstance vkInstance) { - CTH_WARN(vkInstance == nullptr, "vkInstance invalid") {} +vector available_layers() { + uint32_t count = 0; + vkEnumerateInstanceLayerProperties(&count, nullptr); + vector properties(count); + vkEnumerateInstanceLayerProperties(&count, properties.data()); - vkDestroyInstance(vkInstance, nullptr); - - log::msg("destroyed instance"); + return { + std::from_range, + properties | std::views::transform([](VkLayerProperties const& layer) { return string{layer.layerName}; }) + }; } - -void Instance::forget() { _debugMessenger = nullptr; } - -void Instance::loadInstanceFunctions(vk_not_null vkInstance) { volkLoadInstanceOnly(vkInstance.get()); } } diff --git a/cvk/tests/src/base/core_test.cpp b/cvk/tests/src/base/core_test.cpp index afff61df..5e11222c 100644 --- a/cvk/tests/src/base/core_test.cpp +++ b/cvk/tests/src/base/core_test.cpp @@ -17,9 +17,6 @@ namespace cvk { -using test::APP_NAME; -using test::NO_EXTENSIONS; - namespace { /** * @brief one queue, some devices have one queue only @@ -38,7 +35,7 @@ namespace { } CVK_TEST(Core, base_constructor_does_not_create) { - Instance const instance{APP_NAME, NO_EXTENSIONS, std::nullopt}; + Instance const instance{test::instance_config(), create}; Core const core{instance}; @@ -47,7 +44,7 @@ CVK_TEST(Core, base_constructor_does_not_create) { } CVK_TEST(Core, create_without_queue_sets) { - Instance const instance{APP_NAME, NO_EXTENSIONS, std::nullopt}; + Instance const instance{test::instance_config(), create}; Core const core{instance, {}, CONFIG}; @@ -71,7 +68,7 @@ CVK_TEST(Core, create_without_queue_sets) { } CVK_TEST(Core, create_one_queue_per_entry) { - Instance const instance{APP_NAME, NO_EXTENSIONS, std::nullopt}; + Instance const instance{test::instance_config(), create}; if(!pick_physical_device(instance, {}, TWO_QUEUES.queueProperties, constants::BASE_DEVICE_FEATURES)) GTEST_SKIP() << "needs a device with two queues"; @@ -84,7 +81,7 @@ CVK_TEST(Core, create_one_queue_per_entry) { } CVK_TEST(Core, queue_set_group_shares_a_queue) { - Instance const instance{APP_NAME, NO_EXTENSIONS, std::nullopt}; + Instance const instance{test::instance_config(), create}; auto config = TWO_QUEUES; config.queueSets.push_back(one_group(config.queueProperties.size())); @@ -101,7 +98,7 @@ CVK_TEST(Core, queue_set_group_shares_a_queue) { } CVK_TEST(Core, first_possible_queue_set_wins) { - Instance const instance{APP_NAME, NO_EXTENSIONS, std::nullopt}; + Instance const instance{test::instance_config(), create}; // no device has this many compute queues constexpr size_t queues = 256; @@ -120,7 +117,7 @@ CVK_TEST(Core, first_possible_queue_set_wins) { } CVK_TEST(Core, no_suitable_device_throws) { - Instance const instance{APP_NAME, NO_EXTENSIONS, std::nullopt}; + Instance const instance{test::instance_config(), create}; Core::Config const config{.queueProperties = {QueueFamilyProperties::PRESENT}}; auto const construct = [&] { Core const core{instance, {}, config}; }; @@ -129,7 +126,7 @@ CVK_TEST(Core, no_suitable_device_throws) { } CVK_TEST(Core, release_and_wrap) { - Instance const instance{APP_NAME, NO_EXTENSIONS, std::nullopt}; + Instance const instance{test::instance_config(), create}; Core core{instance, {}, CONFIG}; auto const vkDevice = core.vkDevice(); @@ -149,7 +146,7 @@ CVK_TEST(Core, release_and_wrap) { } CVK_TEST(Core, wrap_without_queues_takes_the_device_queues) { - Instance const instance{APP_NAME, NO_EXTENSIONS, std::nullopt}; + Instance const instance{test::instance_config(), create}; Core core{instance, {}, CONFIG}; auto state = core.release(); @@ -161,7 +158,7 @@ CVK_TEST(Core, wrap_without_queues_takes_the_device_queues) { } CVK_TEST(Core, wrap_without_allocator_creates_one) { - Instance const instance{APP_NAME, NO_EXTENSIONS, std::nullopt}; + Instance const instance{test::instance_config(), create}; Core core{instance, {}, CONFIG}; auto state = core.release(); @@ -174,7 +171,7 @@ CVK_TEST(Core, wrap_without_allocator_creates_one) { } CVK_TEST(Core, queues_work_after_move) { - Instance const instance{APP_NAME, NO_EXTENSIONS, std::nullopt}; + Instance const instance{test::instance_config(), create}; Core core{instance, {}, CONFIG}; Core const moved{std::move(core)}; @@ -186,7 +183,7 @@ CVK_TEST(Core, queues_work_after_move) { } CVK_TEST(Core, present_queue_with_headless_surface) { - Instance const instance{APP_NAME, test::headless_extensions(), std::nullopt}; + Instance const instance{test::instance_config(test::headless_extensions()), create}; std::array const surfaces{test::headless_surface(instance)}; Core::Config const config{ @@ -205,7 +202,7 @@ CVK_TEST(Core, move_assignment_destroys_the_old_core) { test::reset_validation_errors(); { - Instance const instance{APP_NAME, NO_EXTENSIONS, test::counting_messenger()}; + Instance const instance{test::instance_config(), create, test::counting_messenger()}; Core core{instance, {}, CONFIG}; Core other{instance, {}, CONFIG}; diff --git a/cvk/tests/src/base/device_test.cpp b/cvk/tests/src/base/device_test.cpp index 88d2fe65..d352988d 100644 --- a/cvk/tests/src/base/device_test.cpp +++ b/cvk/tests/src/base/device_test.cpp @@ -15,9 +15,6 @@ namespace cvk { -using test::APP_NAME; -using test::NO_EXTENSIONS; - namespace { /** * @brief one queue, some devices have one queue only @@ -33,7 +30,7 @@ namespace { } CVK_TEST(Device, base_constructor_does_not_create) { - Instance const instance{APP_NAME, NO_EXTENSIONS, std::nullopt}; + Instance const instance{test::instance_config(), create}; auto const [physicalDevice, _] = test::pick(instance, QUEUES); Device const device{instance, physicalDevice}; @@ -43,7 +40,7 @@ CVK_TEST(Device, base_constructor_does_not_create) { } CVK_TEST(Device, create_retrieves_the_queue) { - Instance const instance{APP_NAME, NO_EXTENSIONS, std::nullopt}; + Instance const instance{test::instance_config(), create}; auto const [physicalDevice, familyIndices] = test::pick(instance, QUEUES); Device const device{instance, physicalDevice, Device::Config{familyIndices}}; @@ -64,7 +61,7 @@ CVK_TEST(Device, create_retrieves_the_queue) { } CVK_TEST(Device, create_retrieves_the_queues) { - Instance const instance{APP_NAME, NO_EXTENSIONS, std::nullopt}; + Instance const instance{test::instance_config(), create}; auto const result = pick_physical_device(instance, {}, THREE_QUEUES, {}); if(!result) GTEST_SKIP() << "needs a device with three queues"; @@ -88,7 +85,7 @@ CVK_TEST(Device, create_retrieves_the_queues) { } CVK_TEST(Device, queues_of_one_family_get_increasing_indices) { - Instance const instance{APP_NAME, NO_EXTENSIONS, std::nullopt}; + Instance const instance{test::instance_config(), create}; auto const [physicalDevice, _] = test::pick(instance, QUEUES); auto const families = physicalDevice.queueFamilies(); @@ -104,7 +101,7 @@ CVK_TEST(Device, queues_of_one_family_get_increasing_indices) { } CVK_TEST(Device, queues_work_after_move) { - Instance const instance{APP_NAME, NO_EXTENSIONS, std::nullopt}; + Instance const instance{test::instance_config(), create}; auto const [physicalDevice, familyIndices] = test::pick(instance, QUEUES); Device device{instance, physicalDevice, Device::Config{familyIndices}}; @@ -121,7 +118,7 @@ CVK_TEST(Device, queues_work_after_move) { } CVK_TEST(Device, release_and_wrap) { - Instance const instance{APP_NAME, NO_EXTENSIONS, std::nullopt}; + Instance const instance{test::instance_config(), create}; auto const [physicalDevice, familyIndices] = test::pick(instance, QUEUES); Device device{instance, physicalDevice, Device::Config{familyIndices}}; @@ -145,7 +142,7 @@ CVK_TEST(Device, release_and_wrap) { } CVK_TEST(Device, wrap_without_table_loads_it) { - Instance const instance{APP_NAME, NO_EXTENSIONS, std::nullopt}; + Instance const instance{test::instance_config(), create}; auto const [physicalDevice, familyIndices] = test::pick(instance, QUEUES); Device device{instance, physicalDevice, Device::Config{familyIndices}}; @@ -160,7 +157,7 @@ CVK_TEST(Device, wrap_without_table_loads_it) { } CVK_TEST(Device, release_twice_is_safe) { - Instance const instance{APP_NAME, NO_EXTENSIONS, std::nullopt}; + Instance const instance{test::instance_config(), create}; auto const [physicalDevice, familyIndices] = test::pick(instance, QUEUES); Device device{instance, physicalDevice, Device::Config{familyIndices}}; @@ -175,7 +172,7 @@ CVK_TEST(Device, release_twice_is_safe) { } CVK_TEST(Device, present_enables_swapchain_maintenance1) { - Instance const instance{APP_NAME, test::headless_extensions(), std::nullopt}; + Instance const instance{test::instance_config(test::headless_extensions()), create}; std::array const surfaces{test::headless_surface(instance)}; std::array const queues{QueueFamilyProperties::GRAPHICS | QueueFamilyProperties::PRESENT}; @@ -208,7 +205,7 @@ CVK_TEST(Device, move_assignment_destroys_the_old_device) { test::reset_validation_errors(); { - Instance const instance{APP_NAME, NO_EXTENSIONS, test::counting_messenger()}; + Instance const instance{test::instance_config(), create, test::counting_messenger()}; auto const [physicalDevice, familyIndices] = test::pick(instance, QUEUES); Device device{instance, physicalDevice, Device::Config{familyIndices}}; diff --git a/cvk/tests/src/base/instance_test.cpp b/cvk/tests/src/base/instance_test.cpp index a6c122eb..a56612a7 100644 --- a/cvk/tests/src/base/instance_test.cpp +++ b/cvk/tests/src/base/instance_test.cpp @@ -2,6 +2,7 @@ #include "validation.hpp" #include "cvk/base/instance.hpp" +#include "cvk/utility/constants.hpp" #include #include @@ -10,16 +11,15 @@ namespace cvk { using test::APP_NAME; -using test::NO_EXTENSIONS; CVK_TEST(Instance, base_constructor_does_not_create) { - Instance const instance{APP_NAME, NO_EXTENSIONS}; + Instance const instance{test::instance_config()}; EXPECT_FALSE(instance.created()); } CVK_TEST(Instance, create_and_destroy) { - Instance instance{APP_NAME, NO_EXTENSIONS}; + Instance instance{test::instance_config()}; instance.create(); ASSERT_TRUE(instance.created()); @@ -30,13 +30,13 @@ CVK_TEST(Instance, create_and_destroy) { } CVK_TEST(Instance, constructor_creates) { - Instance const instance{APP_NAME, NO_EXTENSIONS, std::nullopt}; + Instance const instance{test::instance_config(), create}; EXPECT_TRUE(instance.created()); } CVK_TEST(Instance, create_twice_loads_the_new_instance) { - Instance instance{APP_NAME, NO_EXTENSIONS, std::nullopt}; + Instance instance{test::instance_config(), create}; instance.create(); @@ -44,20 +44,29 @@ CVK_TEST(Instance, create_twice_loads_the_new_instance) { EXPECT_EQ(volkGetLoadedInstance(), instance.get()); } -CVK_TEST(Instance, reports_required_extensions) { - std::vector const required{VK_KHR_SURFACE_EXTENSION_NAME}; +CVK_TEST(Instance, create_enables_the_extensions_of_the_config) { + std::string const surface{VK_KHR_SURFACE_EXTENSION_NAME}; + Instance const instance{test::instance_config({surface}), create}; - Instance const instance{APP_NAME, required}; - - EXPECT_TRUE(std::ranges::contains(instance.requiredExtensions(), std::string{VK_KHR_SURFACE_EXTENSION_NAME})); + EXPECT_TRUE(std::ranges::contains(instance.extensions(), surface)); + if constexpr(constants::ENABLE_VALIDATION_LAYERS) { + std::string const debugUtils{VK_EXT_DEBUG_UTILS_EXTENSION_NAME}; + EXPECT_TRUE(std::ranges::contains(instance.extensions(), debugUtils)); + EXPECT_NE(instance.debugMessenger(), nullptr); + } } -CVK_TEST(Instance, missing_extension_throws) { - std::vector const required{"VK_CVK_extension_that_does_not_exist"}; +CVK_TEST(Instance, create_with_a_missing_extension_throws_and_forgets) { + Instance instance{test::instance_config({"VK_CVK_extension_that_does_not_exist"})}; - auto const construct = [&] { Instance const instance{APP_NAME, required}; }; + EXPECT_THROW(instance.create(), cth::except::default_exception); + EXPECT_FALSE(instance.created()); + EXPECT_TRUE(instance.extensions().empty()); +} - EXPECT_THROW(construct(), cth::except::default_exception); +CVK_TEST(Instance, available_extensions_contain_the_surface) { + std::string const surface{VK_KHR_SURFACE_EXTENSION_NAME}; + EXPECT_TRUE(std::ranges::contains(available_instance_extensions(), surface)); } CVK_TEST(Instance, wrap_takes_ownership) { @@ -83,7 +92,7 @@ CVK_TEST(Instance, wrap_takes_ownership) { VkInstance vkInstance = VK_NULL_HANDLE; ASSERT_EQ(vkCreateInstance(&createInfo, nullptr, &vkInstance), VK_SUCCESS); - Instance instance{APP_NAME, NO_EXTENSIONS, Instance::State{vkInstance, nullptr}}; + Instance instance{test::instance_config(), Instance::State{vkInstance}}; ASSERT_TRUE(instance.created()); EXPECT_EQ(instance.get(), vkInstance); @@ -96,8 +105,8 @@ CVK_TEST(Instance, wrap_takes_ownership) { CVK_TEST(Instance, move_assignment_takes_the_other_instance) { test::reset_validation_errors(); { - Instance instance{APP_NAME, NO_EXTENSIONS, test::counting_messenger()}; - Instance other{APP_NAME, NO_EXTENSIONS, test::counting_messenger()}; + Instance instance{test::instance_config(), create, test::counting_messenger()}; + Instance other{test::instance_config(), create, test::counting_messenger()}; auto const vkInstance = other.get(); instance = std::move(other); @@ -109,15 +118,21 @@ CVK_TEST(Instance, move_assignment_takes_the_other_instance) { } CVK_TEST(Instance, release_and_wrap) { - Instance instance{APP_NAME, NO_EXTENSIONS, std::nullopt}; + Instance instance{test::instance_config(), create}; auto const vkInstance = instance.get(); + auto const extensions = std::vector{std::from_range, instance.extensions()}; + auto state = instance.release(); EXPECT_FALSE(instance.created()); + EXPECT_EQ(instance.debugMessenger(), nullptr); EXPECT_EQ(state.vkInstance.get(), vkInstance); + EXPECT_EQ(state.extensions, extensions); - Instance const wrapped{APP_NAME, NO_EXTENSIONS, std::move(state)}; + Instance const wrapped{test::instance_config(), std::move(state)}; ASSERT_TRUE(wrapped.created()); EXPECT_EQ(wrapped.get(), vkInstance); + EXPECT_TRUE(std::ranges::equal(wrapped.extensions(), extensions)); + if constexpr(constants::ENABLE_VALIDATION_LAYERS) EXPECT_NE(wrapped.debugMessenger(), nullptr); } } diff --git a/cvk/tests/src/base/physical_device_test.cpp b/cvk/tests/src/base/physical_device_test.cpp index a9b2f87b..000f81ae 100644 --- a/cvk/tests/src/base/physical_device_test.cpp +++ b/cvk/tests/src/base/physical_device_test.cpp @@ -12,9 +12,6 @@ namespace cvk { -using test::APP_NAME; -using test::NO_EXTENSIONS; - namespace { /** * @brief one queue, some devices have one queue only @@ -28,7 +25,7 @@ namespace { } CVK_TEST(PhysicalDevice, pick_finds_the_queues) { - Instance const instance{APP_NAME, NO_EXTENSIONS, std::nullopt}; + Instance const instance{test::instance_config(), create}; auto const pick = pick_physical_device(instance, {}, QUEUES, constants::BASE_DEVICE_FEATURES); @@ -44,7 +41,7 @@ CVK_TEST(PhysicalDevice, pick_finds_the_queues) { } CVK_TEST(PhysicalDevice, picked_device_has_the_features) { - Instance const instance{APP_NAME, NO_EXTENSIONS, std::nullopt}; + Instance const instance{test::instance_config(), create}; auto const [device, _] = test::pick(instance, QUEUES); @@ -55,26 +52,26 @@ CVK_TEST(PhysicalDevice, picked_device_has_the_features) { } CVK_TEST(PhysicalDevice, missing_names_the_extensions) { - Instance const instance{APP_NAME, NO_EXTENSIONS, std::nullopt}; + Instance const instance{test::instance_config(), create}; auto const [device, _] = test::pick(instance, QUEUES); EXPECT_EQ(device.missing({}, UNKNOWN_EXTENSION), UNKNOWN_EXTENSION); } CVK_TEST(PhysicalDevice, pick_with_a_missing_extension_finds_nothing) { - Instance const instance{APP_NAME, NO_EXTENSIONS, std::nullopt}; + Instance const instance{test::instance_config(), create}; EXPECT_FALSE(pick_physical_device(instance, {}, QUEUES, {}, UNKNOWN_EXTENSION).has_value()); } CVK_TEST(PhysicalDevice, pick_present_without_surfaces_finds_nothing) { - Instance const instance{APP_NAME, NO_EXTENSIONS, std::nullopt}; + Instance const instance{test::instance_config(), create}; EXPECT_FALSE(pick_physical_device(instance, {}, PRESENT_QUEUES, {}).has_value()); } CVK_TEST(PhysicalDevice, pick_present_finds_a_family_of_the_surface) { - Instance const instance{APP_NAME, test::headless_extensions(), std::nullopt}; + Instance const instance{test::instance_config(test::headless_extensions()), create}; std::array const surfaces{test::headless_surface(instance)}; auto const [device, familyIndices] = test::pick(instance, PRESENT_QUEUES, surfaces); @@ -85,7 +82,7 @@ CVK_TEST(PhysicalDevice, pick_present_finds_a_family_of_the_surface) { } CVK_TEST(PhysicalDevice, enumerate_without_surfaces_has_no_present) { - Instance const instance{APP_NAME, NO_EXTENSIONS, std::nullopt}; + Instance const instance{test::instance_config(), create}; for(auto const& device : enumerate_physical_devices(instance)) for(auto const& family : device.queueFamilies()) @@ -93,7 +90,7 @@ CVK_TEST(PhysicalDevice, enumerate_without_surfaces_has_no_present) { } CVK_TEST(PhysicalDevice, copy_keeps_the_queried_members) { - Instance const instance{APP_NAME, NO_EXTENSIONS, std::nullopt}; + Instance const instance{test::instance_config(), create}; auto const [device, _] = test::pick(instance, QUEUES); PhysicalDevice const copy{device}; @@ -107,7 +104,7 @@ CVK_TEST(PhysicalDevice, copy_keeps_the_queried_members) { } CVK_TEST(PhysicalDevice, find_supported_format) { - Instance const instance{APP_NAME, NO_EXTENSIONS, std::nullopt}; + Instance const instance{test::instance_config(), create}; auto const [device, _] = test::pick(instance, QUEUES); // R8G8B8A8_UNORM sampled images with optimal tiling are mandatory diff --git a/cvk/tests/src/base/queue/queue_test.cpp b/cvk/tests/src/base/queue/queue_test.cpp index 9a458054..20a82c69 100644 --- a/cvk/tests/src/base/queue/queue_test.cpp +++ b/cvk/tests/src/base/queue/queue_test.cpp @@ -10,9 +10,6 @@ namespace cvk { -using test::APP_NAME; -using test::NO_EXTENSIONS; - namespace { constexpr std::array QUEUES{QueueFamilyProperties::GRAPHICS}; @@ -37,7 +34,7 @@ CVK_TEST(Queue, base_constructor_does_not_create) { } CVK_TEST(Queue, copies_share_the_queue) { - Instance const instance{APP_NAME, NO_EXTENSIONS, std::nullopt}; + Instance const instance{test::instance_config(), create}; auto const [physicalDevice, familyIndices] = test::pick(instance, QUEUES); Device const device{instance, physicalDevice, Device::Config{familyIndices}}; @@ -52,7 +49,7 @@ CVK_TEST(Queue, copies_share_the_queue) { } CVK_TEST(Queue, raw_submit_signals_the_fence) { - Instance const instance{APP_NAME, NO_EXTENSIONS, std::nullopt}; + Instance const instance{test::instance_config(), create}; auto const [physicalDevice, familyIndices] = test::pick(instance, QUEUES); Device const device{instance, physicalDevice, Device::Config{familyIndices}}; auto const table = device.table(); @@ -70,7 +67,7 @@ CVK_TEST(Queue, raw_submit_signals_the_fence) { } CVK_TEST(Queue, raw_submit_without_fence) { - Instance const instance{APP_NAME, NO_EXTENSIONS, std::nullopt}; + Instance const instance{test::instance_config(), create}; auto const [physicalDevice, familyIndices] = test::pick(instance, QUEUES); Device const device{instance, physicalDevice, Device::Config{familyIndices}}; @@ -80,7 +77,7 @@ CVK_TEST(Queue, raw_submit_without_fence) { } CVK_TEST(Queue, release_and_wrap) { - Instance const instance{APP_NAME, NO_EXTENSIONS, std::nullopt}; + Instance const instance{test::instance_config(), create}; auto const [physicalDevice, familyIndices] = test::pick(instance, QUEUES); Device const device{instance, physicalDevice, Device::Config{familyIndices}}; @@ -98,7 +95,7 @@ CVK_TEST(Queue, release_and_wrap) { } CVK_TEST(Queue, present_family_can_present) { - Instance const instance{APP_NAME, test::headless_extensions(), std::nullopt}; + Instance const instance{test::instance_config(test::headless_extensions()), create}; std::array const surfaces{test::headless_surface(instance)}; std::array const queues{QueueFamilyProperties::GRAPHICS | QueueFamilyProperties::PRESENT}; diff --git a/cvk/tests/src/context.cpp b/cvk/tests/src/context.cpp index 57149aea..b481cfe2 100644 --- a/cvk/tests/src/context.cpp +++ b/cvk/tests/src/context.cpp @@ -11,14 +11,14 @@ Core::Config const Context::CONFIG{ .queueProperties = {QueueFamilyProperties::GRAPHICS | QueueFamilyProperties::COMPUTE}, }; -Context::Context(Core::Config const& config) : instance{APP_NAME, NO_EXTENSIONS, counting_messenger()}, +Context::Context(Core::Config const& config) : instance{test::instance_config(), create, test::counting_messenger()}, core{instance, {}, config} {} Core::Config const PresentContext::CONFIG{ .queueProperties = {QueueFamilyProperties::GRAPHICS | QueueFamilyProperties::PRESENT}, }; -PresentContext::PresentContext() : instance{APP_NAME, headless_extensions(), counting_messenger()}, +PresentContext::PresentContext() : instance{test::instance_config(test::headless_extensions()), create, test::counting_messenger()}, surfaces{headless_surface(instance)}, core{instance, surfaces, CONFIG} {} Context::ValidationCheck::ValidationCheck() { reset_validation_errors(); } diff --git a/cvk/tests/src/debug/debug_messenger_test.cpp b/cvk/tests/src/debug/debug_messenger_test.cpp index c7ae35ea..67689b01 100644 --- a/cvk/tests/src/debug/debug_messenger_test.cpp +++ b/cvk/tests/src/debug/debug_messenger_test.cpp @@ -8,9 +8,6 @@ namespace cvk { -using test::APP_NAME; -using test::NO_EXTENSIONS; - namespace { constexpr std::string_view TEST_MESSAGE = "cvk test message"; @@ -54,7 +51,7 @@ namespace { CVK_TEST(DebugMessenger, forwards_messages_to_the_callback) { if constexpr(!constants::ENABLE_VALIDATION_LAYERS) GTEST_SKIP() << "needs the validation layers"; - Instance const instance{APP_NAME, NO_EXTENSIONS, std::nullopt}; + Instance const instance{test::instance_config(), create}; DebugMessenger const messenger{instance, DebugMessenger::Config::Default(count_test_messages), create}; ASSERT_TRUE(messenger.created()); @@ -67,7 +64,7 @@ CVK_TEST(DebugMessenger, forwards_messages_to_the_callback) { CVK_TEST(DebugMessenger, accepts_a_lambda) { if constexpr(!constants::ENABLE_VALIDATION_LAYERS) GTEST_SKIP() << "needs the validation layers"; - Instance const instance{APP_NAME, NO_EXTENSIONS, std::nullopt}; + Instance const instance{test::instance_config(), create}; DebugMessenger::callback_t const callback = []( VkDebugUtilsMessageSeverityFlagBitsEXT, @@ -89,7 +86,7 @@ CVK_TEST(DebugMessenger, accepts_a_lambda) { CVK_TEST(DebugMessenger, destroy_resets) { if constexpr(!constants::ENABLE_VALIDATION_LAYERS) GTEST_SKIP() << "needs the validation layers"; - Instance const instance{APP_NAME, NO_EXTENSIONS, std::nullopt}; + Instance const instance{test::instance_config(), create}; DebugMessenger messenger{instance, DebugMessenger::Config::Default(count_test_messages), create}; messenger.destroy(); @@ -104,7 +101,7 @@ CVK_TEST(DebugMessenger, destroy_resets) { CVK_TEST(DebugMessenger, release_keeps_the_messenger) { if constexpr(!constants::ENABLE_VALIDATION_LAYERS) GTEST_SKIP() << "needs the validation layers"; - Instance const instance{APP_NAME, NO_EXTENSIONS, std::nullopt}; + Instance const instance{test::instance_config(), create}; DebugMessenger messenger{instance, DebugMessenger::Config::Default(count_test_messages), create}; auto const state = messenger.release(); @@ -120,7 +117,7 @@ CVK_TEST(DebugMessenger, release_keeps_the_messenger) { CVK_TEST(DebugMessenger, release_and_wrap) { if constexpr(!constants::ENABLE_VALIDATION_LAYERS) GTEST_SKIP() << "needs the validation layers"; - Instance const instance{APP_NAME, NO_EXTENSIONS, std::nullopt}; + Instance const instance{test::instance_config(), create}; DebugMessenger messenger{instance, DebugMessenger::Config::Default(count_test_messages), create}; auto const vkMessenger = messenger.get(); @@ -140,7 +137,7 @@ CVK_TEST(DebugMessenger, move_assignment_destroys_the_old_messenger) { test::reset_validation_errors(); { - Instance const instance{APP_NAME, NO_EXTENSIONS, test::counting_messenger()}; + Instance const instance{test::instance_config(), create, test::counting_messenger()}; DebugMessenger messenger{instance, DebugMessenger::Config::Default(count_test_messages), create}; DebugMessenger other{instance, DebugMessenger::Config::Default(count_test_messages), create}; auto const vkMessenger = other.get(); diff --git a/cvk/tests/src/surface/surface_test.cpp b/cvk/tests/src/surface/surface_test.cpp index 8375851f..c3c1b2b7 100644 --- a/cvk/tests/src/surface/surface_test.cpp +++ b/cvk/tests/src/surface/surface_test.cpp @@ -12,8 +12,6 @@ namespace cvk { -using test::APP_NAME; - namespace { constexpr std::array PRESENT_QUEUES{QueueFamilyProperties::GRAPHICS | QueueFamilyProperties::PRESENT}; @@ -24,7 +22,7 @@ namespace { } CVK_TEST(Surface, wrap_takes_ownership) { - Instance const instance{APP_NAME, test::headless_extensions(), std::nullopt}; + Instance const instance{test::instance_config(test::headless_extensions()), create}; auto surface = test::headless_surface(instance); @@ -36,7 +34,7 @@ CVK_TEST(Surface, wrap_takes_ownership) { } CVK_TEST(Surface, release_keeps_the_surface) { - Instance const instance{APP_NAME, test::headless_extensions(), std::nullopt}; + Instance const instance{test::instance_config(test::headless_extensions()), create}; auto surface = test::headless_surface(instance); auto const state = surface.release(); @@ -46,7 +44,7 @@ CVK_TEST(Surface, release_keeps_the_surface) { } CVK_TEST(Surface, empty_config_allows_everything) { - Instance const instance{APP_NAME, test::headless_extensions(), std::nullopt}; + Instance const instance{test::instance_config(test::headless_extensions()), create}; std::array const surfaces{test::headless_surface(instance, ALLOW_ALL)}; auto const [device, _] = test::pick(instance, PRESENT_QUEUES, surfaces); @@ -58,7 +56,7 @@ CVK_TEST(Surface, empty_config_allows_everything) { } CVK_TEST(Surface, formats_keep_the_allowed_order) { - Instance const instance{APP_NAME, test::headless_extensions(), std::nullopt}; + Instance const instance{test::instance_config(test::headless_extensions()), create}; std::array const surfaces{test::headless_surface(instance, ALLOW_ALL)}; auto const [device, _] = test::pick(instance, PRESENT_QUEUES, surfaces); @@ -79,7 +77,7 @@ CVK_TEST(Surface, formats_keep_the_allowed_order) { } CVK_TEST(Surface, no_allowed_format_throws) { - Instance const instance{APP_NAME, test::headless_extensions(), std::nullopt}; + Instance const instance{test::instance_config(test::headless_extensions()), create}; std::array const surfaces{test::headless_surface(instance, ALLOW_ALL)}; auto const [device, _] = test::pick(instance, PRESENT_QUEUES, surfaces); @@ -94,7 +92,7 @@ CVK_TEST(Surface, no_allowed_format_throws) { } CVK_TEST(Surface, capabilities) { - Instance const instance{APP_NAME, test::headless_extensions(), std::nullopt}; + Instance const instance{test::instance_config(test::headless_extensions()), create}; std::array const surfaces{test::headless_surface(instance)}; auto const [device, _] = test::pick(instance, PRESENT_QUEUES, surfaces); @@ -108,7 +106,7 @@ CVK_TEST(Surface, move_assignment_destroys_the_old_surface) { test::reset_validation_errors(); { - Instance const instance{APP_NAME, test::headless_extensions(), test::counting_messenger()}; + Instance const instance{test::instance_config(test::headless_extensions()), create, test::counting_messenger()}; auto surface = test::headless_surface(instance); auto other = test::headless_surface(instance); auto const vkSurface = other.get(); diff --git a/cvk/tests/src/test.hpp b/cvk/tests/src/test.hpp index d2c4a67a..7d0c9020 100644 --- a/cvk/tests/src/test.hpp +++ b/cvk/tests/src/test.hpp @@ -1,4 +1,6 @@ #pragma once +#include "cvk/base/instance_config.hpp" + #include #include @@ -11,7 +13,12 @@ namespace cvk::test { constexpr std::string_view APP_NAME = "cvk_tests"; -inline std::vector const NO_EXTENSIONS{}; +/** + * @brief instance config with @ref APP_NAME + */ +[[nodiscard]] inline InstanceConfig instance_config(std::vector extensions = {}) { + return InstanceConfig{.appName = std::string{APP_NAME}, .extensions = std::move(extensions)}; +} /** * @brief glsl sources of the test shaders diff --git a/cvk/tests/src/utility/handle/unique_handle_test.cpp b/cvk/tests/src/utility/handle/unique_handle_test.cpp index abfd08ba..3808d902 100644 --- a/cvk/tests/src/utility/handle/unique_handle_test.cpp +++ b/cvk/tests/src/utility/handle/unique_handle_test.cpp @@ -236,7 +236,7 @@ CVK_TEST(unique_handle, instance_deleter) { test::reset_validation_errors(); { - Instance const instance{test::APP_NAME, test::headless_extensions(), test::counting_messenger()}; + Instance const instance{test::instance_config(test::headless_extensions()), create, test::counting_messenger()}; auto surface = test::headless_surface(instance); instance_handle handle{surface.release().vkSurface.get(), instance.get()}; From 7131374510b700bbd0ede9af828bcd813b07c416 Mon Sep 17 00:00:00 2001 From: lukas <89145039+codethinki@users.noreply.github.com> Date: Mon, 19 Oct 2026 22:29:56 +0200 Subject: [PATCH 074/135] cvk: queue without lifecycle, queue mutexes through device release, waitIdle locks - Queue: the device owns the vulkan queue, wrap(), destroy(), optDestroy(), release(), the base constructor and created() removed, the device constructs the queues - names: rawSubmit(), rawPresent(), canPresent(), waitIdle() (was wait(), like Device::waitIdle()) - mutex(): the shared mutex, for code outside of cvk that uses get() - fix: Device::release() + wrap() created new mutexes, copies of the old queues locked another mutex than the new queues of the same VkQueue, Device::State::queueMutexes carries them, empty -> new mutexes - fix: Device::waitIdle() called vkDeviceWaitIdle() without the queue mutexes, Vulkan requires the host access to every queue synchronized, it locks them in address order - QueueState has no default mutex, QueueFamily::operator== defaulted, cth::en::flag_val qualified instead of a using in the header --- cvk/incl/cvk/base/device.hpp | 18 ++++- cvk/incl/cvk/base/queue/queue.hpp | 68 ++++++------------- cvk/incl/cvk/base/queue/queue_family.hpp | 16 ++--- cvk/incl/cvk/base/queue/queue_state.hpp | 2 +- cvk/src/base/device.cpp | 33 +++++++-- cvk/src/base/queue/queue.cpp | 46 +++---------- cvk/src/surface/swapchain.cpp | 2 +- cvk/tests/src/base/core_test.cpp | 12 ++-- cvk/tests/src/base/device_test.cpp | 16 ++--- cvk/tests/src/base/queue/queue_test.cpp | 49 ++++++------- cvk/tests/src/base/queue/submit_test.cpp | 4 +- cvk/tests/src/render/sync/fence_test.cpp | 8 +-- cvk/tests/src/render/sync/semaphore_test.cpp | 4 +- .../render/sync/timeline_semaphore_test.cpp | 2 +- 14 files changed, 133 insertions(+), 147 deletions(-) diff --git a/cvk/incl/cvk/base/device.hpp b/cvk/incl/cvk/base/device.hpp index eec36f28..ccc7db88 100644 --- a/cvk/incl/cvk/base/device.hpp +++ b/cvk/incl/cvk/base/device.hpp @@ -11,6 +11,7 @@ #include #include +#include #include #include #include @@ -63,7 +64,9 @@ class Device { /** * @brief wraps the state and retrieves the queues of @ref State::queueFamilyIndices * @details nullptr @ref State::functionTable -> loads the functions + * @details empty @ref State::queueMutexes -> creates a mutex per queue * @pre the device has the queues of @ref State::queueFamilyIndices + * @pre empty @ref State::queueMutexes or one per queue * @post @ref created() * @note calls @ref optDestroy() */ @@ -97,6 +100,7 @@ class Device { /** * @brief blocks until every queue of the device finished its work + * @details locks the mutexes of every queue, Vulkan requires it * @pre @ref created() * @throws cvk::vk_result_exception result of vkDeviceWaitIdle() */ @@ -147,9 +151,14 @@ class Device { /** * @brief retrieves the queues * @param familyIndices family index of each queue + * @param mutexes mutex of each queue, empty -> creates one per queue * @pre the device has the queues + * @pre empty mutexes or one per queue */ - void createQueues(std::span familyIndices); + void createQueues( + std::span familyIndices, + std::span const> mutexes = {} + ); not_null_ptr _instance; @@ -187,6 +196,13 @@ struct Device::State { */ std::vector queueFamilyIndices{}; + /** + * @brief mutex of each queue, the copies of a released queue keep locking the same mutex + * @details empty -> @ref wrap() creates a mutex per queue + * @pre empty or one per entry of @ref queueFamilyIndices + */ + std::vector> queueMutexes{}; + /** * @brief the features that the device enables */ diff --git a/cvk/incl/cvk/base/queue/queue.hpp b/cvk/incl/cvk/base/queue/queue.hpp index 47bbef98..4f9b2adf 100644 --- a/cvk/incl/cvk/base/queue/queue.hpp +++ b/cvk/incl/cvk/base/queue/queue.hpp @@ -7,6 +7,8 @@ #include +#include +#include #include @@ -14,42 +16,19 @@ namespace cvk { class Fence; /** - * @brief VkQueue wrapper - * @details copyable, thread safe, copies share the mutex of the vulkan queue + * @brief VkQueue of a device + * @details copyable, thread safe: copies share the mutex of the vulkan queue, + * the device owns the vulkan queue, a queue has no lifecycle */ class Queue { public: using vk_handle_t = VkQueue; using State = QueueState; - explicit Queue(QueueFamilyProperties familyProperties) : _familyProperties{familyProperties} {} - Queue(QueueFamilyProperties familyProperties, State state) : Queue{familyProperties} { wrap(std::move(state)); } - ~Queue() { optDestroy(); } - - /** - * @brief wraps the vulkan queue - * @details the device calls this, not the user - * @post @ref created() - * @note calls @ref optDestroy() - */ - void wrap(State); - /** - * @brief forgets, the device owns the vulkan queue - * @pre @ref created() + * @brief the device constructs its queues */ - void destroy(); - - /** - * @brief if @ref created() -> calls @ref destroy() - */ - void optDestroy() { if(created()) destroy(); } - - /** - * @brief releases ownership, returns the state and forgets - * @pre @ref created() - */ - State release(); + Queue(QueueFamilyProperties familyProperties, State const&); /** * @brief submits the infos in one vkQueueSubmit2() @@ -69,41 +48,41 @@ class Queue { void submit(SubmitInfo const& info, Fence const* fence = nullptr) const { submit({&info, 1}, fence); } /** - * @brief submits to the vulkan queue + * @brief submits to the vulkan queue, for what @ref SubmitInfo does not cover * @param fence VK_NULL_HANDLE -> no fence - * @pre @ref created() * @throws cvk::vk_result_exception result of vkQueueSubmit2() */ - void raw_submit(VkSubmitInfo2 const& submitInfo, VkFence fence = VK_NULL_HANDLE) const; + void rawSubmit(VkSubmitInfo2 const& submitInfo, VkFence fence = VK_NULL_HANDLE) const; /** * @brief presents through vkQueuePresentKHR() * @return VK_SUCCESS, VK_SUBOPTIMAL_KHR or VK_ERROR_OUT_OF_DATE_KHR - * @pre @ref created() - * @pre @ref can_present() + * @pre @ref canPresent() * @throws cvk::vk_result_exception other result of vkQueuePresentKHR() */ - [[nodiscard]] VkResult raw_present(VkPresentInfoKHR const& presentInfo) const; + [[nodiscard]] VkResult rawPresent(VkPresentInfoKHR const& presentInfo) const; /** * @brief blocks until the queue finished all its work - * @pre @ref created() * @throws cvk::vk_result_exception result of vkQueueWaitIdle() */ - void wait() const; + void waitIdle() const; - [[nodiscard]] constexpr bool can_present() const { + [[nodiscard]] constexpr bool canPresent() const { return contains(_familyProperties, QueueFamilyProperties::PRESENT); } - [[nodiscard]] constexpr bool created() const { return _handle != VK_NULL_HANDLE; } [[nodiscard]] constexpr vk_handle_t get() const { return _handle.get(); } [[nodiscard]] constexpr uint32_t index() const { return _queueIndex; } [[nodiscard]] constexpr uint32_t familyIndex() const { return _familyIndex; } [[nodiscard]] constexpr QueueFamilyProperties familyProperties() const { return _familyProperties; } -private: - void forget(); + /** + * @brief guards the vulkan queue, shared by every copy + * @details code outside of cvk that uses @ref get() locks it + */ + [[nodiscard]] constexpr std::shared_ptr const& mutex() const { return _handleMtx; } +private: QueueFamilyProperties _familyProperties; VolkDeviceTable const* _functions = nullptr; @@ -121,21 +100,16 @@ class Queue { static constexpr void debug_check(Queue const&); static constexpr void debug_check_present(Queue const&); - static constexpr void debug_check_handle(VkQueue); }; } namespace cvk { constexpr void Queue::debug_check(Queue const& queue) { - CTH_CRITICAL(!queue.created(), "queue must be created") {} + CTH_CRITICAL(queue.get() == VK_NULL_HANDLE, "queue must not be moved from") {} } constexpr void Queue::debug_check_present(Queue const& queue) { debug_check(queue); - CTH_CRITICAL(!queue.can_present(), "queue is not a present queue") {} -} - -constexpr void Queue::debug_check_handle(VkQueue vkQueue) { - CTH_CRITICAL(vkQueue == VK_NULL_HANDLE, "vkQueue must not be invalid (VK_NULL_HANDLE)") {} + CTH_CRITICAL(!queue.canPresent(), "queue is not a present queue") {} } } diff --git a/cvk/incl/cvk/base/queue/queue_family.hpp b/cvk/incl/cvk/base/queue/queue_family.hpp index a7d647ed..9ef5e054 100644 --- a/cvk/incl/cvk/base/queue/queue_family.hpp +++ b/cvk/incl/cvk/base/queue/queue_family.hpp @@ -8,14 +8,12 @@ namespace cvk { -using cth::en::flag_val; - enum class QueueFamilyProperties { NONE = 0, - GRAPHICS = flag_val(0), - COMPUTE = flag_val(1), - TRANSFER = flag_val(2), - PRESENT = flag_val(3) + GRAPHICS = cth::en::flag_val(0), + COMPUTE = cth::en::flag_val(1), + TRANSFER = cth::en::flag_val(2), + PRESENT = cth::en::flag_val(3) }; } @@ -49,11 +47,9 @@ struct QueueFamily { .vkProperties = vkProperties }; } -}; -constexpr bool operator==(QueueFamily const& l, QueueFamily const& r) { - return l.index == r.index && l.properties == r.properties && l.vkProperties == r.vkProperties; -} + [[nodiscard]] constexpr bool operator==(QueueFamily const&) const = default; +}; } CTH_HASH_AGGREGATE(cvk::QueueFamily) diff --git a/cvk/incl/cvk/base/queue/queue_state.hpp b/cvk/incl/cvk/base/queue/queue_state.hpp index 33a216e5..12234c76 100644 --- a/cvk/incl/cvk/base/queue/queue_state.hpp +++ b/cvk/incl/cvk/base/queue/queue_state.hpp @@ -29,7 +29,7 @@ struct QueueState { * @brief guards the vulkan queue, shared by every copy of the queue * @pre not nullptr */ - std::shared_ptr vkQueueMtx = std::make_shared(); + std::shared_ptr vkQueueMtx; static constexpr void debug_check(QueueState const& state) { diff --git a/cvk/src/base/device.cpp b/cvk/src/base/device.cpp index 40ef183b..807f29f2 100644 --- a/cvk/src/base/device.cpp +++ b/cvk/src/base/device.cpp @@ -9,7 +9,9 @@ #include #include +#include #include +#include namespace cvk { @@ -45,7 +47,7 @@ void Device::wrap(State state) { _handle.reset(state.vkDevice.get()); _features = state.features; _extensions = std::move(state.extensions); - createQueues(state.queueFamilyIndices); + createQueues(state.queueFamilyIndices, state.queueMutexes); } void Device::create(Config const& config) { @@ -78,6 +80,7 @@ Device::State Device::release() { .vkDevice = _handle.release(), .functionTable = std::move(_functionTable), .queueFamilyIndices = {std::from_range, _queues | std::views::transform(&Queue::familyIndex)}, + .queueMutexes = {std::from_range, _queues | std::views::transform(&Queue::mutex)}, .features = _features, .extensions = std::move(_extensions), }; @@ -88,6 +91,19 @@ Device::State Device::release() { void Device::waitIdle() const { debug_check(*this); + // one lock per mutex, in address order: no deadlock with another waitIdle() + std::vector mutexes{std::from_range, _queues | std::views::transform([](Queue const& queue) { + return queue.mutex().get(); + })}; + std::ranges::sort(mutexes); + auto const [end, _] = std::ranges::unique(mutexes); + mutexes.erase(end, mutexes.end()); + + std::vector> locks{}; + locks.reserve(mutexes.size()); + for(auto* const mutex : mutexes) + locks.emplace_back(*mutex); + auto const result = functions()->vkDeviceWaitIdle(_handle.get()); CVK_RESULT_STABLE_THROW(result != VK_SUCCESS, result, "failed to wait for device") {} } @@ -172,12 +188,20 @@ void Device::createLogicalDevice(queue_families_queue_counts_t const& queueFamil void Device::loadFunctionTable(VkDevice vkDevice) const { volkLoadDeviceTable(_functionTable.get(), vkDevice); } -void Device::createQueues(span familyIndices) { +void Device::createQueues( + span familyIndices, + span const> mutexes +) { + CTH_CRITICAL( + !mutexes.empty() && mutexes.size() != familyIndices.size(), + "one mutex per queue or no mutexes" + ) {} + std::unordered_map queueCounts{}; _queues.reserve(familyIndices.size()); - for(auto const familyIndex : familyIndices) { + for(auto const [i, familyIndex] : std::views::enumerate(familyIndices)) { auto& queueIndex = queueCounts[familyIndex]; VkQueue ptr = VK_NULL_HANDLE; @@ -186,9 +210,10 @@ void Device::createQueues(span familyIndices) { // vkGetDeviceQueue() has no failure, a null queue means the device has no such queue CTH_CRITICAL(ptr == VK_NULL_HANDLE, "the device has no queue {} in family {}", queueIndex, familyIndex) {} + auto mutex = mutexes.empty() ? std::make_shared() : mutexes[static_cast(i)]; _queues.emplace_back( _physicalDevice->queueFamily(familyIndex)->properties, - Queue::State{_functionTable.get(), ptr, familyIndex, queueIndex++, std::make_shared()} + Queue::State{_functionTable.get(), ptr, familyIndex, queueIndex++, std::move(mutex)} ); } } diff --git a/cvk/src/base/queue/queue.cpp b/cvk/src/base/queue/queue.cpp index ed50dc60..804208ec 100644 --- a/cvk/src/base/queue/queue.cpp +++ b/cvk/src/base/queue/queue.cpp @@ -48,33 +48,13 @@ namespace { } } -void Queue::wrap(State state) { +Queue::Queue(QueueFamilyProperties familyProperties, State const& state) : _familyProperties{familyProperties}, + _functions{state.functions.get()}, + _handle{state.vkQueue.get()}, + _handleMtx{state.vkQueueMtx}, + _familyIndex{state.familyIndex}, + _queueIndex{state.queueIndex} { State::debug_check(state); - optDestroy(); - - _handle = state.vkQueue.get(); - _functions = state.functions.get(); - _handleMtx = std::move(state.vkQueueMtx); - _familyIndex = state.familyIndex; - _queueIndex = state.queueIndex; -} - -void Queue::destroy() { - debug_check(*this); - forget(); -} - -Queue::State Queue::release() { - debug_check(*this); - State state{ - _functions, - _handle.get(), - _familyIndex, - _queueIndex, - std::move(_handleMtx), - }; - forget(); - return state; } void Queue::submit(span infos, Fence const* fence) const { @@ -152,7 +132,7 @@ void Queue::submit(span infos, Fence const* fence) const { CVK_RESULT_STABLE_THROW(result != VK_SUCCESS, result, "failed to submit to queue") {} } -void Queue::raw_submit(VkSubmitInfo2 const& submitInfo, VkFence fence) const { +void Queue::rawSubmit(VkSubmitInfo2 const& submitInfo, VkFence fence) const { debug_check(*this); VkResult result; @@ -164,7 +144,7 @@ void Queue::raw_submit(VkSubmitInfo2 const& submitInfo, VkFence fence) const { CVK_RESULT_STABLE_THROW(result != VK_SUCCESS, result, "failed to submit to queue") {} } -VkResult Queue::raw_present(VkPresentInfoKHR const& presentInfo) const { +VkResult Queue::rawPresent(VkPresentInfoKHR const& presentInfo) const { debug_check_present(*this); VkResult result; @@ -182,7 +162,7 @@ VkResult Queue::raw_present(VkPresentInfoKHR const& presentInfo) const { return result; } -void Queue::wait() const { +void Queue::waitIdle() const { debug_check(*this); VkResult result; @@ -193,12 +173,4 @@ void Queue::wait() const { CVK_RESULT_STABLE_THROW(result != VK_SUCCESS, result, "failed to wait for queue") {} } - -void Queue::forget() { - _functions = nullptr; - _handle = VK_NULL_HANDLE; - _handleMtx = nullptr; - _familyIndex = 0; - _queueIndex = 0; -} } diff --git a/cvk/src/surface/swapchain.cpp b/cvk/src/surface/swapchain.cpp index a2e7efdc..40015f44 100644 --- a/cvk/src/surface/swapchain.cpp +++ b/cvk/src/surface/swapchain.cpp @@ -174,7 +174,7 @@ VkResult Swapchain::present(uint32_t imageIndex) { .pResults = nullptr, }; - auto const result = _presentQueue->raw_present(info); + auto const result = _presentQueue->rawPresent(info); _acquired[imageIndex] = false; return result; } diff --git a/cvk/tests/src/base/core_test.cpp b/cvk/tests/src/base/core_test.cpp index 5e11222c..3b3162fc 100644 --- a/cvk/tests/src/base/core_test.cpp +++ b/cvk/tests/src/base/core_test.cpp @@ -61,9 +61,9 @@ CVK_TEST(Core, create_without_queue_sets) { for(size_t i = 0; i < CONFIG.queueProperties.size(); i++) { auto const& queue = core.queue(i); - ASSERT_TRUE(queue.created()); + ASSERT_NE(queue.get(), VK_NULL_HANDLE); EXPECT_TRUE(contains(queue.familyProperties(), CONFIG.queueProperties[i])); - queue.wait(); + queue.waitIdle(); } } @@ -142,7 +142,7 @@ CVK_TEST(Core, release_and_wrap) { EXPECT_EQ(wrapped.vkDevice(), vkDevice); EXPECT_EQ(wrapped.allocator().get(), vmaAllocator); ASSERT_EQ(wrapped.queues().size(), CONFIG.queueProperties.size()); - wrapped.queue(0).wait(); + wrapped.queue(0).waitIdle(); } CVK_TEST(Core, wrap_without_queues_takes_the_device_queues) { @@ -179,7 +179,7 @@ CVK_TEST(Core, queues_work_after_move) { EXPECT_FALSE(core.created()); ASSERT_TRUE(moved.created()); for(auto const& queue : moved.queues()) - queue.wait(); + queue.waitIdle(); } CVK_TEST(Core, present_queue_with_headless_surface) { @@ -193,7 +193,7 @@ CVK_TEST(Core, present_queue_with_headless_surface) { Core const core{instance, surfaces, config}; ASSERT_TRUE(core.created()); - EXPECT_TRUE(core.queue(0).can_present()); + EXPECT_TRUE(core.queue(0).canPresent()); EXPECT_TRUE(surfaces[0].supportsFamily(core.physicalDevice(), core.queue(0).familyIndex())); } @@ -212,7 +212,7 @@ CVK_TEST(Core, move_assignment_destroys_the_old_core) { ASSERT_EQ(core.vkDevice(), vkDevice); EXPECT_FALSE(other.created()); - core.queue(0).wait(); + core.queue(0).waitIdle(); } EXPECT_EQ(test::validation_errors(), 0); } diff --git a/cvk/tests/src/base/device_test.cpp b/cvk/tests/src/base/device_test.cpp index d352988d..b9b4324a 100644 --- a/cvk/tests/src/base/device_test.cpp +++ b/cvk/tests/src/base/device_test.cpp @@ -51,11 +51,11 @@ CVK_TEST(Device, create_retrieves_the_queue) { ASSERT_EQ(device.queues().size(), 1); auto const& queue = device.queues()[0]; - ASSERT_TRUE(queue.created()); + ASSERT_NE(queue.get(), VK_NULL_HANDLE); EXPECT_EQ(queue.familyIndex(), familyIndices[0]); EXPECT_EQ(queue.index(), 0); EXPECT_TRUE(contains(queue.familyProperties(), QUEUES[0])); - queue.wait(); + queue.waitIdle(); device.waitIdle(); } @@ -74,11 +74,11 @@ CVK_TEST(Device, create_retrieves_the_queues) { std::map queueIndices{}; for(auto const& [queue, familyIndex, properties] : std::views::zip(device.queues(), familyIndices, THREE_QUEUES)) { - ASSERT_TRUE(queue.created()); + ASSERT_NE(queue.get(), VK_NULL_HANDLE); EXPECT_EQ(queue.familyIndex(), familyIndex); EXPECT_EQ(queue.index(), queueIndices[familyIndex]++); EXPECT_TRUE(contains(queue.familyProperties(), properties)); - queue.wait(); + queue.waitIdle(); } device.waitIdle(); @@ -114,7 +114,7 @@ CVK_TEST(Device, queues_work_after_move) { EXPECT_EQ(moved.get(), vkDevice); for(auto const& queue : moved.queues()) - queue.wait(); + queue.waitIdle(); } CVK_TEST(Device, release_and_wrap) { @@ -138,7 +138,7 @@ CVK_TEST(Device, release_and_wrap) { EXPECT_EQ(wrapped.get(), vkDevice); ASSERT_EQ(wrapped.queues().size(), familyIndices.size()); for(auto const& queue : wrapped.queues()) - queue.wait(); + queue.waitIdle(); } CVK_TEST(Device, wrap_without_table_loads_it) { @@ -190,7 +190,7 @@ CVK_TEST(Device, present_enables_swapchain_maintenance1) { ASSERT_TRUE(device.created()); ASSERT_EQ(device.queues().size(), 1); - EXPECT_TRUE(device.queues()[0].can_present()); + EXPECT_TRUE(device.queues()[0].canPresent()); EXPECT_TRUE(device.features().swapchainMaintenance1); // the feature bit adds its extension @@ -217,7 +217,7 @@ CVK_TEST(Device, move_assignment_destroys_the_old_device) { ASSERT_EQ(device.get(), vkDevice); EXPECT_FALSE(other.created()); ASSERT_EQ(device.queues().size(), 1); - device.queues()[0].wait(); + device.queues()[0].waitIdle(); device.waitIdle(); } EXPECT_EQ(test::validation_errors(), 0); diff --git a/cvk/tests/src/base/queue/queue_test.cpp b/cvk/tests/src/base/queue/queue_test.cpp index 20a82c69..8d07b671 100644 --- a/cvk/tests/src/base/queue/queue_test.cpp +++ b/cvk/tests/src/base/queue/queue_test.cpp @@ -26,13 +26,6 @@ namespace { }; } -CVK_TEST(Queue, base_constructor_does_not_create) { - Queue const queue{QueueFamilyProperties::GRAPHICS}; - - EXPECT_FALSE(queue.created()); - EXPECT_FALSE(queue.can_present()); -} - CVK_TEST(Queue, copies_share_the_queue) { Instance const instance{test::instance_config(), create}; auto const [physicalDevice, familyIndices] = test::pick(instance, QUEUES); @@ -41,11 +34,11 @@ CVK_TEST(Queue, copies_share_the_queue) { auto const& queue = device.queues()[0]; Queue const copy{queue}; - ASSERT_TRUE(copy.created()); EXPECT_EQ(copy.get(), queue.get()); + EXPECT_EQ(copy.mutex(), queue.mutex()); EXPECT_EQ(copy.familyIndex(), queue.familyIndex()); EXPECT_EQ(copy.index(), queue.index()); - copy.wait(); + copy.waitIdle(); } CVK_TEST(Queue, raw_submit_signals_the_fence) { @@ -59,7 +52,7 @@ CVK_TEST(Queue, raw_submit_signals_the_fence) { ASSERT_EQ(table->vkCreateFence(table.device(), &fenceInfo, nullptr, &fence), VK_SUCCESS); auto const& queue = device.queues()[0]; - queue.raw_submit(EMPTY_SUBMIT, fence); + queue.rawSubmit(EMPTY_SUBMIT, fence); EXPECT_EQ(table->vkWaitForFences(table.device(), 1, &fence, VK_TRUE, UINT64_MAX), VK_SUCCESS); @@ -72,26 +65,36 @@ CVK_TEST(Queue, raw_submit_without_fence) { Device const device{instance, physicalDevice, Device::Config{familyIndices}}; auto const& queue = device.queues()[0]; - queue.raw_submit(EMPTY_SUBMIT); - queue.wait(); + queue.rawSubmit(EMPTY_SUBMIT); + queue.waitIdle(); } -CVK_TEST(Queue, release_and_wrap) { +CVK_TEST(Queue, copies_keep_their_mutex_through_a_device_release_and_wrap) { Instance const instance{test::instance_config(), create}; auto const [physicalDevice, familyIndices] = test::pick(instance, QUEUES); - Device const device{instance, physicalDevice, Device::Config{familyIndices}}; + Device device{instance, physicalDevice, Device::Config{familyIndices}}; - Queue copy{device.queues()[0]}; - auto const vkQueue = copy.get(); + Queue const copy{device.queues()[0]}; + Device const wrapped{instance, physicalDevice, device.release()}; - auto state = copy.release(); - EXPECT_FALSE(copy.created()); + ASSERT_EQ(wrapped.queues().size(), 1); + EXPECT_EQ(wrapped.queues()[0].get(), copy.get()); + EXPECT_EQ(wrapped.queues()[0].mutex(), copy.mutex()); + copy.waitIdle(); +} + +CVK_TEST(Queue, wrap_without_mutexes_creates_them) { + Instance const instance{test::instance_config(), create}; + auto const [physicalDevice, familyIndices] = test::pick(instance, QUEUES); + Device device{instance, physicalDevice, Device::Config{familyIndices}}; - Queue const wrapped{copy.familyProperties(), std::move(state)}; + auto state = device.release(); + state.queueMutexes.clear(); + Device const wrapped{instance, physicalDevice, std::move(state)}; - ASSERT_TRUE(wrapped.created()); - EXPECT_EQ(wrapped.get(), vkQueue); - wrapped.wait(); + ASSERT_EQ(wrapped.queues().size(), 1); + EXPECT_NE(wrapped.queues()[0].mutex(), nullptr); + wrapped.waitIdle(); } CVK_TEST(Queue, present_family_can_present) { @@ -102,6 +105,6 @@ CVK_TEST(Queue, present_family_can_present) { auto const [physicalDevice, familyIndices] = test::pick(instance, queues, surfaces); Device const device{instance, physicalDevice, Device::Config{familyIndices}}; - EXPECT_TRUE(device.queues()[0].can_present()); + EXPECT_TRUE(device.queues()[0].canPresent()); } } diff --git a/cvk/tests/src/base/queue/submit_test.cpp b/cvk/tests/src/base/queue/submit_test.cpp index 23bb54d0..0c577b3f 100644 --- a/cvk/tests/src/base/queue/submit_test.cpp +++ b/cvk/tests/src/base/queue/submit_test.cpp @@ -39,7 +39,7 @@ CVK_TEST(Submit, command_buffer_without_fence) { context.core.queue(0).submit({.cmdBuffers = {&primary}}); - context.core.queue(0).wait(); + context.core.queue(0).waitIdle(); } CVK_TEST(Submit, binary_semaphore_orders_two_infos) { @@ -90,7 +90,7 @@ CVK_TEST(Submit, explicit_timeline_value_keeps_the_counter) { context.core.queue(0).submit({.signals = {{timeline, VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT, 5}}}); - context.core.queue(0).wait(); + context.core.queue(0).waitIdle(); EXPECT_EQ(timeline.gpuValue(), 5); EXPECT_EQ(timeline.value(), 0); } diff --git a/cvk/tests/src/render/sync/fence_test.cpp b/cvk/tests/src/render/sync/fence_test.cpp index 611ec86b..abbd145d 100644 --- a/cvk/tests/src/render/sync/fence_test.cpp +++ b/cvk/tests/src/render/sync/fence_test.cpp @@ -87,7 +87,7 @@ CVK_TEST(Fence, submit_signals) { test::Context const context{}; Fence const fence{context.core, 0}; - context.core.queue(0).raw_submit(EMPTY_SUBMIT, fence.get()); + context.core.queue(0).rawSubmit(EMPTY_SUBMIT, fence.get()); fence.wait(); EXPECT_TRUE(fence.signaled()); @@ -103,7 +103,7 @@ CVK_TEST(Fence, wait_reset_timeout_keeps_the_state) { EXPECT_EQ(fence.waitReset(0), VK_TIMEOUT); EXPECT_FALSE(fence.signaled()); - context.core.queue(0).raw_submit(EMPTY_SUBMIT, fence.get()); + context.core.queue(0).rawSubmit(EMPTY_SUBMIT, fence.get()); EXPECT_EQ(fence.waitReset(UINT64_MAX), VK_SUCCESS); EXPECT_FALSE(fence.signaled()); @@ -146,7 +146,7 @@ CVK_TEST(Fence, extract_os_handle_waits_for_the_submit) { EXPECT_EQ(fence.exportTypes(), constants::OS_FENCE_HANDLE_TYPE); auto const& queue = context->core.queue(0); - queue.raw_submit(EMPTY_SUBMIT, fence.get()); + queue.rawSubmit(EMPTY_SUBMIT, fence.get()); auto const handle = fence.extractOsHandle(); if(handle) EXPECT_EQ(cth::os::wait(handle.get(), std::chrono::seconds{5}), cth::os::wait_result::WAITED); @@ -156,7 +156,7 @@ CVK_TEST(Fence, extract_os_handle_waits_for_the_submit) { EXPECT_FALSE(fence.signaled()); #endif - queue.wait(); + queue.waitIdle(); } CVK_TEST(Fence, extract_os_handle_of_a_signaled_fence) { diff --git a/cvk/tests/src/render/sync/semaphore_test.cpp b/cvk/tests/src/render/sync/semaphore_test.cpp index 9bafe459..2084fa6f 100644 --- a/cvk/tests/src/render/sync/semaphore_test.cpp +++ b/cvk/tests/src/render/sync/semaphore_test.cpp @@ -69,8 +69,8 @@ CVK_TEST(Semaphore, submit_signals_and_waits) { .pSignalSemaphoreInfos = nullptr, }; - queue.raw_submit(signalSubmit); - queue.raw_submit(waitSubmit, fence.get()); + queue.rawSubmit(signalSubmit); + queue.rawSubmit(waitSubmit, fence.get()); fence.wait(); } diff --git a/cvk/tests/src/render/sync/timeline_semaphore_test.cpp b/cvk/tests/src/render/sync/timeline_semaphore_test.cpp index 1316e01f..ed58fb5c 100644 --- a/cvk/tests/src/render/sync/timeline_semaphore_test.cpp +++ b/cvk/tests/src/render/sync/timeline_semaphore_test.cpp @@ -71,7 +71,7 @@ CVK_TEST(TimelineSemaphore, submit_signals_the_next_value) { .pSignalSemaphoreInfos = &signalInfo, }; - context.core.queue(0).raw_submit(submit); + context.core.queue(0).rawSubmit(submit); semaphore.wait(); EXPECT_EQ(semaphore.gpuValue(), signalInfo.value); From ecb504d890491be82ca21b28d8fbbccd210615cf Mon Sep 17 00:00:00 2001 From: lukas <89145039+codethinki@users.noreply.github.com> Date: Tue, 20 Oct 2026 00:19:32 +0200 Subject: [PATCH 075/135] cvk: descriptor write variant, buffer descriptor info in BufferDescriptor - DescriptorWrite = std::variant, a write has only the descriptors of its kind, before one of images and buffers stayed empty - DescriptorSet::write(DescriptorWrite const&) for one write - BufferDescriptor::info() builds the VkDescriptorBufferInfo, the range check of BaseBuffer::descriptorInfo() moved there, BaseBuffer::descriptorInfo() and Buffer::descriptorInfo() removed - DescriptorPool: add() and remove() doc like CmdPool, the owner of a released set may count it --- cvk/incl/cvk/data/buffer/base_buffer.hpp | 22 -------- cvk/incl/cvk/data/buffer/buffer.hpp | 13 ----- .../cvk/data/descriptor/descriptor_pool.hpp | 6 ++- .../cvk/data/descriptor/descriptor_set.hpp | 6 +++ .../cvk/data/descriptor/descriptor_write.hpp | 38 ++++++++++++-- cvk/src/data/descriptor/descriptor_set.cpp | 51 ++++++++++++------- .../src/data/buffer/base_buffer_test.cpp | 17 ------- cvk/tests/src/data/buffer/buffer_test.cpp | 17 ------- .../data/descriptor/descriptor_draw_test.cpp | 6 +-- .../data/descriptor/descriptor_set_test.cpp | 34 +++++++++---- cvk/tests/src/render/cmd/local_read_test.cpp | 8 ++- 11 files changed, 106 insertions(+), 112 deletions(-) diff --git a/cvk/incl/cvk/data/buffer/base_buffer.hpp b/cvk/incl/cvk/data/buffer/base_buffer.hpp index 44460179..44c4fcb4 100644 --- a/cvk/incl/cvk/data/buffer/base_buffer.hpp +++ b/cvk/incl/cvk/data/buffer/base_buffer.hpp @@ -118,14 +118,6 @@ class BaseBuffer { */ void stage(BaseCmdBuffer const&, BaseBuffer const& stagingBuffer, size_t dstOffset = 0) const; - /** - * @param size in bytes (VK_WHOLE_SIZE => to the end) - * @param offset in bytes - * @pre @ref created() - * @pre offset + size <= @ref size() - */ - [[nodiscard]] constexpr VkDescriptorBufferInfo descriptorInfo(size_t size = VK_WHOLE_SIZE, size_t offset = 0) const; - static void destroy(DeviceTable table, VkBuffer); [[nodiscard]] constexpr bool created() const { return static_cast(_handle); } @@ -176,20 +168,6 @@ struct BaseBuffer::State { }; } -namespace cvk { -constexpr VkDescriptorBufferInfo BaseBuffer::descriptorInfo(size_t size, size_t offset) const { - debug_check(*this); - CTH_CRITICAL(offset > _size, "offset must lie in the buffer") {} - CTH_CRITICAL(size != VK_WHOLE_SIZE && size > _size - offset, "range must lie in the buffer") {} - - return VkDescriptorBufferInfo{ - .buffer = get(), - .offset = offset, - .range = size == VK_WHOLE_SIZE ? _size - offset : size, - }; -} -} - namespace cvk { constexpr void BaseBuffer::debug_check(BaseBuffer const& buffer) { CTH_CRITICAL(!buffer.created(), "buffer must be created") {} diff --git a/cvk/incl/cvk/data/buffer/buffer.hpp b/cvk/incl/cvk/data/buffer/buffer.hpp index f767d315..dc6e3589 100644 --- a/cvk/incl/cvk/data/buffer/buffer.hpp +++ b/cvk/incl/cvk/data/buffer/buffer.hpp @@ -154,19 +154,6 @@ class Buffer : public BaseBuffer { BaseBuffer::stage(cmdBuffer, stagingBuffer, dstOffset * sizeof(T)); } - /** - * @param count (VK_WHOLE_SIZE => to the end) - * @param offset in elements - * @pre offset + count <= @ref elements() - * @details calls @ref BaseBuffer::descriptorInfo() - */ - [[nodiscard]] constexpr VkDescriptorBufferInfo descriptorInfo( - size_t count = VK_WHOLE_SIZE, - size_t offset = 0 - ) const { - return BaseBuffer::descriptorInfo(bytes(count), offset * sizeof(T)); - } - /** * @brief copies the data to the mapped elements * @pre data.size() <= mapped.size() diff --git a/cvk/incl/cvk/data/descriptor/descriptor_pool.hpp b/cvk/incl/cvk/data/descriptor/descriptor_pool.hpp index c472e163..6eadb0b4 100644 --- a/cvk/incl/cvk/data/descriptor/descriptor_pool.hpp +++ b/cvk/incl/cvk/data/descriptor/descriptor_pool.hpp @@ -99,13 +99,15 @@ class DescriptorPool { /** * @brief counts a set of the pool - * @details @ref DescriptorSet::create() and @ref DescriptorSet::wrap() call it + * @details @ref DescriptorSet::create() and @ref DescriptorSet::wrap() call it, + * code that keeps a released set of the pool may call it too */ void add() const; /** * @brief stops to count a set of the pool - * @details @ref DescriptorSet::destroy() and @ref DescriptorSet::release() call it + * @details @ref DescriptorSet::destroy() and @ref DescriptorSet::release() call it, + * code that frees a counted set itself calls it too * @pre !@ref unused() */ void remove() const; diff --git a/cvk/incl/cvk/data/descriptor/descriptor_set.hpp b/cvk/incl/cvk/data/descriptor/descriptor_set.hpp index fb8a4e1a..731e9108 100644 --- a/cvk/incl/cvk/data/descriptor/descriptor_set.hpp +++ b/cvk/incl/cvk/data/descriptor/descriptor_set.hpp @@ -114,6 +114,12 @@ class DescriptorSet { */ void write(std::span) const; + /** + * @brief writes the descriptors into the set + * @details calls @ref write(std::span) const + */ + void write(DescriptorWrite const& descriptorWrite) const { write({&descriptorWrite, 1}); } + static void destroy(DeviceTable table, VkDescriptorPool, VkDescriptorSet); /** diff --git a/cvk/incl/cvk/data/descriptor/descriptor_write.hpp b/cvk/incl/cvk/data/descriptor/descriptor_write.hpp index 3dad3227..b5bc591e 100644 --- a/cvk/incl/cvk/data/descriptor/descriptor_write.hpp +++ b/cvk/incl/cvk/data/descriptor/descriptor_write.hpp @@ -6,7 +6,10 @@ #include +#include + #include +#include #include @@ -56,18 +59,43 @@ struct BufferDescriptor { */ size_t size = VK_WHOLE_SIZE; - [[nodiscard]] constexpr VkDescriptorBufferInfo info() const { return buffer->descriptorInfo(size, offset); } + /** + * @pre @ref BaseBuffer::created() + * @pre the range lies in the buffer + */ + [[nodiscard]] constexpr VkDescriptorBufferInfo info() const { + BaseBuffer::debug_check(*buffer); + CTH_CRITICAL(offset > buffer->size(), "offset must lie in the buffer") {} + CTH_CRITICAL(size != VK_WHOLE_SIZE && size > buffer->size() - offset, "range must lie in the buffer") {} + + return VkDescriptorBufferInfo{ + .buffer = buffer->get(), + .offset = offset, + .range = size == VK_WHOLE_SIZE ? buffer->size() - offset : size, + }; + } }; /** - * @brief descriptors for consecutive array elements of one binding - * @details image bindings take @ref images, buffer bindings take @ref buffers + * @brief image descriptors for consecutive array elements of one image binding */ -struct DescriptorWrite { +struct ImageDescriptorWrite { uint32_t binding = 0; uint32_t arrayElement = 0; - std::vector images{}; +}; + +/** + * @brief buffer descriptors for consecutive array elements of one buffer binding + */ +struct BufferDescriptorWrite { + uint32_t binding = 0; + uint32_t arrayElement = 0; std::vector buffers{}; }; + +/** + * @brief descriptors for consecutive array elements of one binding, the kind matches the binding type + */ +using DescriptorWrite = std::variant; } diff --git a/cvk/src/data/descriptor/descriptor_set.cpp b/cvk/src/data/descriptor/descriptor_set.cpp index 45783551..694e73c5 100644 --- a/cvk/src/data/descriptor/descriptor_set.cpp +++ b/cvk/src/data/descriptor/descriptor_set.cpp @@ -3,6 +3,9 @@ #include "cvk/base/core.hpp" #include "cvk/utility/vk_exceptions.hpp" +#include +#include +#include #include @@ -144,11 +147,20 @@ DescriptorSet::State DescriptorSet::release() { void DescriptorSet::write(span writes) const { debug_check(*this); + auto const images = [](DescriptorWrite const& write) -> std::span { + auto const* imageWrite = std::get_if(&write); + return imageWrite != nullptr ? imageWrite->images : std::span{}; + }; + auto const buffers = [](DescriptorWrite const& write) -> std::span { + auto const* bufferWrite = std::get_if(&write); + return bufferWrite != nullptr ? bufferWrite->buffers : std::span{}; + }; + size_t imageCount = 0; size_t bufferCount = 0; for(auto const& write : writes) { - imageCount += write.images.size(); - bufferCount += write.buffers.size(); + imageCount += images(write).size(); + bufferCount += buffers(write).size(); } // reserved: the writes point into the infos @@ -160,35 +172,38 @@ void DescriptorSet::write(span writes) const { vkWrites.reserve(writes.size()); for(auto const& write : writes) { - auto const* binding = _layout->binding(write.binding); - CTH_CRITICAL(binding == nullptr, "the layout has no binding {}", write.binding) {} + auto const [bindingIndex, arrayElement] = std::visit( + [](auto const& w) { return std::pair{w.binding, w.arrayElement}; }, + write + ); + auto const* binding = _layout->binding(bindingIndex); + CTH_CRITICAL(binding == nullptr, "the layout has no binding {}", bindingIndex) {} auto const type = binding->descriptorType; auto const kind = descriptor_kind(type); - auto const count = static_cast(kind == DescriptorKind::IMAGE ? write.images.size() - : write.buffers.size()); + bool const imageWrite = std::holds_alternative(write); + auto const count = static_cast(imageWrite ? images(write).size() : buffers(write).size()); CTH_CRITICAL(kind == DescriptorKind::UNSUPPORTED, "descriptor type not supported") { - details->add("binding: {}, type: {}", write.binding, type); + details->add("binding: {}, type: {}", bindingIndex, type); } CTH_CRITICAL( - (kind == DescriptorKind::IMAGE && !write.buffers.empty()) - || (kind == DescriptorKind::BUFFER && !write.images.empty()), - "the descriptors must match the type of the binding" + (kind == DescriptorKind::IMAGE) != imageWrite, + "the kind of the write must match the type of the binding" ) { - details->add("binding: {}, type: {}", write.binding, type); + details->add("binding: {}, type: {}", bindingIndex, type); } CTH_CRITICAL(count == 0, "a write needs descriptors") {} - CTH_CRITICAL(write.arrayElement + count > capacity(*binding), "the descriptors must lie in the binding") { - details->add("binding: {}, first: {}, count: {}", write.binding, write.arrayElement, count); + CTH_CRITICAL(arrayElement + count > capacity(*binding), "the descriptors must lie in the binding") { + details->add("binding: {}, first: {}, count: {}", bindingIndex, arrayElement, count); } VkWriteDescriptorSet vkWrite{ .sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, .pNext = nullptr, .dstSet = get(), - .dstBinding = write.binding, - .dstArrayElement = write.arrayElement, + .dstBinding = bindingIndex, + .dstArrayElement = arrayElement, .descriptorCount = count, .descriptorType = type, .pImageInfo = nullptr, @@ -196,15 +211,15 @@ void DescriptorSet::write(span writes) const { .pTexelBufferView = nullptr, }; - if(kind == DescriptorKind::IMAGE) { + if(imageWrite) { vkWrite.pImageInfo = imageInfos.data() + imageInfos.size(); - for(auto const& descriptor : write.images) { + for(auto const& descriptor : images(write)) { debug_check_image(descriptor, type); imageInfos.push_back(descriptor.info()); } } else { vkWrite.pBufferInfo = bufferInfos.data() + bufferInfos.size(); - for(auto const& descriptor : write.buffers) + for(auto const& descriptor : buffers(write)) bufferInfos.push_back(descriptor.info()); } diff --git a/cvk/tests/src/data/buffer/base_buffer_test.cpp b/cvk/tests/src/data/buffer/base_buffer_test.cpp index f68b6ae6..00d80149 100644 --- a/cvk/tests/src/data/buffer/base_buffer_test.cpp +++ b/cvk/tests/src/data/buffer/base_buffer_test.cpp @@ -191,23 +191,6 @@ CVK_TEST(BaseBuffer, stage_the_whole_staging_buffer) { readback.memory().unmap(); } -CVK_TEST(BaseBuffer, descriptor_info) { - test::Context const context{}; - BaseBuffer const buffer{context.core, DEVICE_CONFIG, SIZE}; - - auto const whole = buffer.descriptorInfo(); - EXPECT_EQ(whole.buffer, buffer.get()); - EXPECT_EQ(whole.offset, 0); - EXPECT_EQ(whole.range, SIZE); - - auto const end = buffer.descriptorInfo(VK_WHOLE_SIZE, 64); - EXPECT_EQ(end.offset, 64); - EXPECT_EQ(end.range, SIZE - 64); - - auto const part = buffer.descriptorInfo(16, 32); - EXPECT_EQ(part.range, 16); -} - CVK_TEST(BaseBuffer, release_and_wrap) { test::Context const context{}; BaseBuffer buffer{context.core, DEVICE_CONFIG, SIZE}; diff --git a/cvk/tests/src/data/buffer/buffer_test.cpp b/cvk/tests/src/data/buffer/buffer_test.cpp index 87b260f5..5546bed8 100644 --- a/cvk/tests/src/data/buffer/buffer_test.cpp +++ b/cvk/tests/src/data/buffer/buffer_test.cpp @@ -145,23 +145,6 @@ CVK_TEST(Buffer, stage_elements) { readback.unmap(); } -CVK_TEST(Buffer, descriptor_info_in_elements) { - test::Context const context{}; - Buffer const buffer{context.core, HOST_CONFIG, COUNT}; - - auto const whole = buffer.descriptorInfo(); - EXPECT_EQ(whole.offset, 0); - EXPECT_EQ(whole.range, COUNT * sizeof(Vertex)); - - auto const end = buffer.descriptorInfo(VK_WHOLE_SIZE, 3); - EXPECT_EQ(end.offset, 3 * sizeof(Vertex)); - EXPECT_EQ(end.range, (COUNT - 3) * sizeof(Vertex)); - - auto const part = buffer.descriptorInfo(2, 1); - EXPECT_EQ(part.offset, sizeof(Vertex)); - EXPECT_EQ(part.range, 2 * sizeof(Vertex)); -} - CVK_TEST(Buffer, release_and_wrap_as_bytes) { test::Context const context{}; Buffer buffer{context.core, HOST_CONFIG, COUNT}; diff --git a/cvk/tests/src/data/descriptor/descriptor_draw_test.cpp b/cvk/tests/src/data/descriptor/descriptor_draw_test.cpp index e29e8bd4..20cb9910 100644 --- a/cvk/tests/src/data/descriptor/descriptor_draw_test.cpp +++ b/cvk/tests/src/data/descriptor/descriptor_draw_test.cpp @@ -181,7 +181,7 @@ CVK_TEST(DescriptorDraw, uniform_buffer_color) { DescriptorPool const pool{context.core, DescriptorPool::Config{{&setLayout}}, create}; DescriptorSet const set{pool, setLayout, create}; - set.write(std::array{DescriptorWrite{.binding = 0, .buffers = {{&color}}}}); + set.write(BufferDescriptorWrite{.binding = 0, .buffers = {{&color}}}); test::record_and_submit(context, [&](PrimaryCmdBuffer& cmd) { record_fill(context, cmd, fill, target, [&](BaseCmdBuffer& c) { c.bindDescriptorSet(set, fill.layout, 0); }); @@ -246,9 +246,7 @@ CVK_TEST(DescriptorDraw, bindless_texture_written_after_bind) { // update after bind: the bind is recorded, the descriptor comes now set.write( - std::array{ - DescriptorWrite{.binding = 0, .arrayElement = INDEX, .images = {{.view = &view, .sampler = &sampler}}}, - } + ImageDescriptorWrite{.binding = 0, .arrayElement = INDEX, .images = {{.view = &view, .sampler = &sampler}}} ); test::submit_and_wait(context, cmd); diff --git a/cvk/tests/src/data/descriptor/descriptor_set_test.cpp b/cvk/tests/src/data/descriptor/descriptor_set_test.cpp index d82393d4..69aeecfd 100644 --- a/cvk/tests/src/data/descriptor/descriptor_set_test.cpp +++ b/cvk/tests/src/data/descriptor/descriptor_set_test.cpp @@ -115,13 +115,31 @@ CVK_TEST(DescriptorSet, write_buffer_and_image) { Sampler const sampler{context.core, {}, create}; set.write( - std::array{ - DescriptorWrite{.binding = 0, .buffers = {{&buffer, 0, 16}}}, - DescriptorWrite{.binding = 1, .images = {{.view = &view, .sampler = &sampler}}}, + std::array{ + BufferDescriptorWrite{.binding = 0, .buffers = {{&buffer, 0, 16}}}, + ImageDescriptorWrite{.binding = 1, .images = {{.view = &view, .sampler = &sampler}}}, } ); } +CVK_TEST(DescriptorSet, buffer_descriptor_info) { + test::Context const context{}; + BaseBuffer const buffer{context.core, {.usage = VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT}, 256}; + + auto const whole = BufferDescriptor{&buffer}.info(); + EXPECT_EQ(whole.buffer, buffer.get()); + EXPECT_EQ(whole.offset, 0); + EXPECT_EQ(whole.range, 256); + + auto const end = BufferDescriptor{.buffer = &buffer, .offset = 64}.info(); + EXPECT_EQ(end.offset, 64); + EXPECT_EQ(end.range, 256 - 64); + + auto const part = BufferDescriptor{.buffer = &buffer, .offset = 32, .size = 16}.info(); + EXPECT_EQ(part.offset, 32); + EXPECT_EQ(part.range, 16); +} + CVK_TEST(DescriptorSet, variable_count) { test::Context const context{}; DescriptorSetLayout const layout{context.core, bindless(), create}; @@ -134,12 +152,10 @@ CVK_TEST(DescriptorSet, variable_count) { EXPECT_EQ(set.variableCount(), 8); set.write( - std::array{ - DescriptorWrite{ - .binding = 0, - .arrayElement = 6, - .images = {{.view = &view, .sampler = &sampler}, {.view = &view, .sampler = &sampler}}, - }, + ImageDescriptorWrite{ + .binding = 0, + .arrayElement = 6, + .images = {{.view = &view, .sampler = &sampler}, {.view = &view, .sampler = &sampler}}, } ); } diff --git a/cvk/tests/src/render/cmd/local_read_test.cpp b/cvk/tests/src/render/cmd/local_read_test.cpp index 9e107855..fe97e934 100644 --- a/cvk/tests/src/render/cmd/local_read_test.cpp +++ b/cvk/tests/src/render/cmd/local_read_test.cpp @@ -163,11 +163,9 @@ namespace { FULLSCREEN.size() } { input.write( - std::array{ - DescriptorWrite{ - .binding = 0, - .images = {{.view = &first, .layout = VK_IMAGE_LAYOUT_RENDERING_LOCAL_READ_KHR}}, - }, + ImageDescriptorWrite{ + .binding = 0, + .images = {{.view = &first, .layout = VK_IMAGE_LAYOUT_RENDERING_LOCAL_READ_KHR}}, } ); vertices.write(FULLSCREEN); From 59019620d1d177814d476031f9cf5eb0e9f6d4a2 Mon Sep 17 00:00:00 2001 From: lukas <89145039+codethinki@users.noreply.github.com> Date: Tue, 20 Oct 2026 09:39:54 +0200 Subject: [PATCH 076/135] cvk: DescriptorWrite from the resources, the binding decides the rest - DescriptorWrite: one descriptor, the constructors take the resources (buffer with a range, view, view and sampler, sampler) and the element, the kind lives inside the write - the binding type decides the descriptor type and the image layout: storage image -> GENERAL, input attachment -> RENDERING_LOCAL_READ, else SHADER_READ_ONLY_OPTIMAL - set.write({{0, ubo}, {1, view, sampler}}) writes a batch with one vkUpdateDescriptorSets(), set.write({0, ubo}) one descriptor - Range: offset and size, for the buffer range and later the memory - removed: ImageDescriptor, BufferDescriptor, ImageDescriptorWrite, BufferDescriptorWrite - new check: a sampler for a descriptor type that takes none --- .../cvk/data/descriptor/descriptor_set.hpp | 16 ++- .../cvk/data/descriptor/descriptor_write.hpp | 113 ++++++---------- cvk/incl/cvk/utility/range.hpp | 20 +++ cvk/src/data/descriptor/descriptor_set.cpp | 123 +++++++++--------- .../data/descriptor/descriptor_draw_test.cpp | 6 +- .../data/descriptor/descriptor_set_test.cpp | 34 ++--- cvk/tests/src/render/cmd/local_read_test.cpp | 8 +- 7 files changed, 151 insertions(+), 169 deletions(-) create mode 100644 cvk/incl/cvk/utility/range.hpp diff --git a/cvk/incl/cvk/data/descriptor/descriptor_set.hpp b/cvk/incl/cvk/data/descriptor/descriptor_set.hpp index 731e9108..17d440cd 100644 --- a/cvk/incl/cvk/data/descriptor/descriptor_set.hpp +++ b/cvk/incl/cvk/data/descriptor/descriptor_set.hpp @@ -13,6 +13,7 @@ #include #include +#include #include @@ -106,19 +107,26 @@ class DescriptorSet { State release(); /** - * @brief writes the descriptors into the set + * @brief writes the descriptors into the set with one vkUpdateDescriptorSets() * @pre @ref created() - * @pre the bindings exist, the descriptors match their types and lie in them + * @pre the bindings exist, the kind of each write matches the binding type + * @pre the elements lie in the bindings, the buffer ranges in the buffers * @pre the gpu does not use the written descriptors, except with VK_DESCRIPTOR_BINDING_UPDATE_AFTER_BIND_BIT * @attention texel buffer, inline uniform block and acceleration structure descriptors are not supported */ void write(std::span) const; /** - * @brief writes the descriptors into the set + * @brief writes the descriptors into the set, for a braced list: set.write({{0, ubo}, {1, view, sampler}}) * @details calls @ref write(std::span) const */ - void write(DescriptorWrite const& descriptorWrite) const { write({&descriptorWrite, 1}); } + void write(std::initializer_list writes) const { write(std::span{writes.begin(), writes.size()}); } + + /** + * @brief writes one descriptor into the set: set.write({0, ubo}) + * @details calls @ref write(std::span) const + */ + void write(DescriptorWrite const& descriptorWrite) const { write(std::span{&descriptorWrite, 1}); } static void destroy(DeviceTable table, VkDescriptorPool, VkDescriptorSet); diff --git a/cvk/incl/cvk/data/descriptor/descriptor_write.hpp b/cvk/incl/cvk/data/descriptor/descriptor_write.hpp index b5bc591e..1069f6e3 100644 --- a/cvk/incl/cvk/data/descriptor/descriptor_write.hpp +++ b/cvk/incl/cvk/data/descriptor/descriptor_write.hpp @@ -1,101 +1,74 @@ #pragma once -#include "cvk/data/buffer/base_buffer.hpp" -#include "cvk/data/img/image_view.hpp" -#include "cvk/data/img/sampler.hpp" +#include "cvk/utility/range.hpp" #include "cvk/utility/types.hpp" #include -#include - #include #include -#include namespace cvk { +class BaseBuffer; +class DescriptorSet; +class ImageView; +class Sampler; + /** - * @brief image, sampler or both for an image descriptor, the binding decides the descriptor type + * @brief one descriptor of one binding, the constructor decides the kind + * @details the binding type in the layout of the set decides the descriptor type and the image layout: + * storage image -> VK_IMAGE_LAYOUT_GENERAL, input attachment -> VK_IMAGE_LAYOUT_RENDERING_LOCAL_READ_KHR, + * else VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL */ -struct ImageDescriptor { +class DescriptorWrite { +public: /** - * @brief nullptr -> no view, VK_DESCRIPTOR_TYPE_SAMPLER + * @brief buffer descriptor: uniform or storage buffer, dynamic or not + * @param range in bytes + * @param element array element of the binding */ - ImageView const* view = nullptr; + constexpr DescriptorWrite(uint32_t binding, BaseBuffer const& buffer, Range range = {}, uint32_t element = 0) : + _binding{binding}, _element{element}, _resource{BufferResource{&buffer, range}} {} /** - * @brief layout of the view when the gpu reads it - * @details VK_DESCRIPTOR_TYPE_STORAGE_IMAGE -> VK_IMAGE_LAYOUT_GENERAL + * @brief image descriptor without sampler: sampled image, storage image, input attachment + * @param element array element of the binding */ - VkImageLayout layout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; + constexpr DescriptorWrite(uint32_t binding, ImageView const& view, uint32_t element = 0) : + _binding{binding}, _element{element}, _resource{ImageResource{&view, nullptr}} {} /** - * @brief nullptr -> no sampler, the descriptor type needs none + * @brief combined image sampler descriptor + * @param element array element of the binding */ - Sampler const* sampler = nullptr; - - [[nodiscard]] constexpr VkDescriptorImageInfo info() const { - return VkDescriptorImageInfo{ - .sampler = sampler != nullptr ? sampler->get() : VK_NULL_HANDLE, - .imageView = view != nullptr ? view->get() : VK_NULL_HANDLE, - .imageLayout = view != nullptr ? layout : VK_IMAGE_LAYOUT_UNDEFINED, - }; - } -}; - -/** - * @brief buffer range for a buffer descriptor, the binding decides the descriptor type - */ -struct BufferDescriptor { - not_null_ptr buffer; + constexpr DescriptorWrite(uint32_t binding, ImageView const& view, Sampler const& sampler, uint32_t element = 0) : + _binding{binding}, _element{element}, _resource{ImageResource{&view, &sampler}} {} /** - * @brief in bytes + * @brief sampler descriptor + * @param element array element of the binding */ - size_t offset = 0; + constexpr DescriptorWrite(uint32_t binding, Sampler const& sampler, uint32_t element = 0) : + _binding{binding}, _element{element}, _resource{ImageResource{nullptr, &sampler}} {} - /** - * @brief in bytes (VK_WHOLE_SIZE => to the end) - */ - size_t size = VK_WHOLE_SIZE; +private: + struct BufferResource { + not_null_ptr buffer; + Range range; + }; /** - * @pre @ref BaseBuffer::created() - * @pre the range lies in the buffer + * @brief view nullptr -> sampler descriptor, sampler nullptr -> no sampler */ - [[nodiscard]] constexpr VkDescriptorBufferInfo info() const { - BaseBuffer::debug_check(*buffer); - CTH_CRITICAL(offset > buffer->size(), "offset must lie in the buffer") {} - CTH_CRITICAL(size != VK_WHOLE_SIZE && size > buffer->size() - offset, "range must lie in the buffer") {} + struct ImageResource { + ImageView const* view; + Sampler const* sampler; + }; - return VkDescriptorBufferInfo{ - .buffer = buffer->get(), - .offset = offset, - .range = size == VK_WHOLE_SIZE ? buffer->size() - offset : size, - }; - } -}; + uint32_t _binding; + uint32_t _element; + std::variant _resource; -/** - * @brief image descriptors for consecutive array elements of one image binding - */ -struct ImageDescriptorWrite { - uint32_t binding = 0; - uint32_t arrayElement = 0; - std::vector images{}; + friend DescriptorSet; }; - -/** - * @brief buffer descriptors for consecutive array elements of one buffer binding - */ -struct BufferDescriptorWrite { - uint32_t binding = 0; - uint32_t arrayElement = 0; - std::vector buffers{}; -}; - -/** - * @brief descriptors for consecutive array elements of one binding, the kind matches the binding type - */ -using DescriptorWrite = std::variant; } diff --git a/cvk/incl/cvk/utility/range.hpp b/cvk/incl/cvk/utility/range.hpp new file mode 100644 index 00000000..3b5c9fe2 --- /dev/null +++ b/cvk/incl/cvk/utility/range.hpp @@ -0,0 +1,20 @@ +#pragma once +#include + +#include + + +namespace cvk { +/** + * @brief offset and size of a part of a buffer or a memory + * @details in bytes, in elements for a typed buffer + */ +struct Range { + size_t offset = 0; + + /** + * @brief (VK_WHOLE_SIZE => to the end) + */ + size_t size = VK_WHOLE_SIZE; +}; +} diff --git a/cvk/src/data/descriptor/descriptor_set.cpp b/cvk/src/data/descriptor/descriptor_set.cpp index 694e73c5..29362177 100644 --- a/cvk/src/data/descriptor/descriptor_set.cpp +++ b/cvk/src/data/descriptor/descriptor_set.cpp @@ -1,10 +1,12 @@ #include "cvk/data/descriptor/descriptor_set.hpp" #include "cvk/base/core.hpp" +#include "cvk/data/buffer/base_buffer.hpp" +#include "cvk/data/img/image_view.hpp" +#include "cvk/data/img/sampler.hpp" #include "cvk/utility/vk_exceptions.hpp" #include -#include #include #include @@ -34,22 +36,33 @@ namespace { } } - void debug_check_image(ImageDescriptor const& descriptor, VkDescriptorType type) { - CTH_CRITICAL( - (type == VK_DESCRIPTOR_TYPE_SAMPLER || type == VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER) - && descriptor.sampler == nullptr, - "sampler descriptors need a sampler" - ) {} - CTH_CRITICAL( - type != VK_DESCRIPTOR_TYPE_SAMPLER && descriptor.view == nullptr, - "image descriptors need a view" - ) {} + /** + * @return the layout of a view in a descriptor of the type + */ + constexpr VkImageLayout descriptor_layout(VkDescriptorType type) { + switch(type) { + case VK_DESCRIPTOR_TYPE_STORAGE_IMAGE: return VK_IMAGE_LAYOUT_GENERAL; + case VK_DESCRIPTOR_TYPE_INPUT_ATTACHMENT: return VK_IMAGE_LAYOUT_RENDERING_LOCAL_READ_KHR; + default: return VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; + } + } + + void debug_check_image(ImageView const* view, Sampler const* sampler, VkDescriptorType type) { + bool const samples = type == VK_DESCRIPTOR_TYPE_SAMPLER || type == VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; + CTH_CRITICAL(samples && sampler == nullptr, "sampler descriptors need a sampler") {} + CTH_CRITICAL(!samples && sampler != nullptr, "the descriptor type takes no sampler") {} + CTH_CRITICAL(type != VK_DESCRIPTOR_TYPE_SAMPLER && view == nullptr, "image descriptors need a view") {} + if(view != nullptr) ImageView::debug_check(*view); + if(sampler != nullptr) Sampler::debug_check(*sampler); + } + + void debug_check_buffer(BaseBuffer const& buffer, Range range) { + BaseBuffer::debug_check(buffer); + CTH_CRITICAL(range.offset > buffer.size(), "offset must lie in the buffer") {} CTH_CRITICAL( - type == VK_DESCRIPTOR_TYPE_STORAGE_IMAGE && descriptor.layout != VK_IMAGE_LAYOUT_GENERAL, - "storage image descriptors need VK_IMAGE_LAYOUT_GENERAL" + range.size != VK_WHOLE_SIZE && range.size > buffer.size() - range.offset, + "range must lie in the buffer" ) {} - if(descriptor.view != nullptr) ImageView::debug_check(*descriptor.view); - if(descriptor.sampler != nullptr) Sampler::debug_check(*descriptor.sampler); } } @@ -147,80 +160,72 @@ DescriptorSet::State DescriptorSet::release() { void DescriptorSet::write(span writes) const { debug_check(*this); - auto const images = [](DescriptorWrite const& write) -> std::span { - auto const* imageWrite = std::get_if(&write); - return imageWrite != nullptr ? imageWrite->images : std::span{}; - }; - auto const buffers = [](DescriptorWrite const& write) -> std::span { - auto const* bufferWrite = std::get_if(&write); - return bufferWrite != nullptr ? bufferWrite->buffers : std::span{}; - }; - - size_t imageCount = 0; - size_t bufferCount = 0; - for(auto const& write : writes) { - imageCount += images(write).size(); - bufferCount += buffers(write).size(); - } - // reserved: the writes point into the infos std::vector imageInfos{}; - imageInfos.reserve(imageCount); + imageInfos.reserve(writes.size()); std::vector bufferInfos{}; - bufferInfos.reserve(bufferCount); + bufferInfos.reserve(writes.size()); std::vector vkWrites{}; vkWrites.reserve(writes.size()); for(auto const& write : writes) { - auto const [bindingIndex, arrayElement] = std::visit( - [](auto const& w) { return std::pair{w.binding, w.arrayElement}; }, - write - ); - auto const* binding = _layout->binding(bindingIndex); - CTH_CRITICAL(binding == nullptr, "the layout has no binding {}", bindingIndex) {} + auto const* binding = _layout->binding(write._binding); + CTH_CRITICAL(binding == nullptr, "the layout has no binding {}", write._binding) {} auto const type = binding->descriptorType; auto const kind = descriptor_kind(type); - bool const imageWrite = std::holds_alternative(write); - auto const count = static_cast(imageWrite ? images(write).size() : buffers(write).size()); + auto const* buffer = std::get_if(&write._resource); CTH_CRITICAL(kind == DescriptorKind::UNSUPPORTED, "descriptor type not supported") { - details->add("binding: {}, type: {}", bindingIndex, type); + details->add("binding: {}, type: {}", write._binding, type); } CTH_CRITICAL( - (kind == DescriptorKind::IMAGE) != imageWrite, + (kind == DescriptorKind::BUFFER) != (buffer != nullptr), "the kind of the write must match the type of the binding" ) { - details->add("binding: {}, type: {}", bindingIndex, type); + details->add("binding: {}, type: {}", write._binding, type); } - CTH_CRITICAL(count == 0, "a write needs descriptors") {} - CTH_CRITICAL(arrayElement + count > capacity(*binding), "the descriptors must lie in the binding") { - details->add("binding: {}, first: {}, count: {}", bindingIndex, arrayElement, count); + CTH_CRITICAL(write._element >= capacity(*binding), "the element must lie in the binding") { + details->add("binding: {}, element: {}", write._binding, write._element); } VkWriteDescriptorSet vkWrite{ .sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, .pNext = nullptr, .dstSet = get(), - .dstBinding = bindingIndex, - .dstArrayElement = arrayElement, - .descriptorCount = count, + .dstBinding = write._binding, + .dstArrayElement = write._element, + .descriptorCount = 1, .descriptorType = type, .pImageInfo = nullptr, .pBufferInfo = nullptr, .pTexelBufferView = nullptr, }; - if(imageWrite) { - vkWrite.pImageInfo = imageInfos.data() + imageInfos.size(); - for(auto const& descriptor : images(write)) { - debug_check_image(descriptor, type); - imageInfos.push_back(descriptor.info()); - } + if(buffer != nullptr) { + debug_check_buffer(*buffer->buffer, buffer->range); + + auto const [offset, size] = buffer->range; + bufferInfos.push_back( + VkDescriptorBufferInfo{ + .buffer = buffer->buffer->get(), + .offset = offset, + .range = size == VK_WHOLE_SIZE ? buffer->buffer->size() - offset : size, + } + ); + vkWrite.pBufferInfo = &bufferInfos.back(); } else { - vkWrite.pBufferInfo = bufferInfos.data() + bufferInfos.size(); - for(auto const& descriptor : buffers(write)) - bufferInfos.push_back(descriptor.info()); + auto const& [view, sampler] = std::get(write._resource); + debug_check_image(view, sampler, type); + + imageInfos.push_back( + VkDescriptorImageInfo{ + .sampler = sampler != nullptr ? sampler->get() : VK_NULL_HANDLE, + .imageView = view != nullptr ? view->get() : VK_NULL_HANDLE, + .imageLayout = view != nullptr ? descriptor_layout(type) : VK_IMAGE_LAYOUT_UNDEFINED, + } + ); + vkWrite.pImageInfo = &imageInfos.back(); } vkWrites.push_back(vkWrite); diff --git a/cvk/tests/src/data/descriptor/descriptor_draw_test.cpp b/cvk/tests/src/data/descriptor/descriptor_draw_test.cpp index 20cb9910..236b390a 100644 --- a/cvk/tests/src/data/descriptor/descriptor_draw_test.cpp +++ b/cvk/tests/src/data/descriptor/descriptor_draw_test.cpp @@ -181,7 +181,7 @@ CVK_TEST(DescriptorDraw, uniform_buffer_color) { DescriptorPool const pool{context.core, DescriptorPool::Config{{&setLayout}}, create}; DescriptorSet const set{pool, setLayout, create}; - set.write(BufferDescriptorWrite{.binding = 0, .buffers = {{&color}}}); + set.write({0, color}); test::record_and_submit(context, [&](PrimaryCmdBuffer& cmd) { record_fill(context, cmd, fill, target, [&](BaseCmdBuffer& c) { c.bindDescriptorSet(set, fill.layout, 0); }); @@ -245,9 +245,7 @@ CVK_TEST(DescriptorDraw, bindless_texture_written_after_bind) { cmd.end(); // update after bind: the bind is recorded, the descriptor comes now - set.write( - ImageDescriptorWrite{.binding = 0, .arrayElement = INDEX, .images = {{.view = &view, .sampler = &sampler}}} - ); + set.write({0, view, sampler, INDEX}); test::submit_and_wait(context, cmd); expect_green(target); diff --git a/cvk/tests/src/data/descriptor/descriptor_set_test.cpp b/cvk/tests/src/data/descriptor/descriptor_set_test.cpp index 69aeecfd..ccbececc 100644 --- a/cvk/tests/src/data/descriptor/descriptor_set_test.cpp +++ b/cvk/tests/src/data/descriptor/descriptor_set_test.cpp @@ -114,30 +114,18 @@ CVK_TEST(DescriptorSet, write_buffer_and_image) { ImageView const view{image, {}, create}; Sampler const sampler{context.core, {}, create}; - set.write( - std::array{ - BufferDescriptorWrite{.binding = 0, .buffers = {{&buffer, 0, 16}}}, - ImageDescriptorWrite{.binding = 1, .images = {{.view = &view, .sampler = &sampler}}}, - } - ); + set.write({{0, buffer, {.offset = 0, .size = 16}}, {1, view, sampler}}); } -CVK_TEST(DescriptorSet, buffer_descriptor_info) { +CVK_TEST(DescriptorSet, write_one_descriptor_and_the_whole_buffer) { test::Context const context{}; - BaseBuffer const buffer{context.core, {.usage = VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT}, 256}; - - auto const whole = BufferDescriptor{&buffer}.info(); - EXPECT_EQ(whole.buffer, buffer.get()); - EXPECT_EQ(whole.offset, 0); - EXPECT_EQ(whole.range, 256); + DescriptorSetLayout const layout{context.core, buffer_and_texture(), create}; + DescriptorPool const pool{context.core, DescriptorPool::Config{{&layout}}, create}; + DescriptorSet const set{pool, layout, create}; - auto const end = BufferDescriptor{.buffer = &buffer, .offset = 64}.info(); - EXPECT_EQ(end.offset, 64); - EXPECT_EQ(end.range, 256 - 64); + BaseBuffer const buffer{context.core, {.usage = VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT}, 64}; - auto const part = BufferDescriptor{.buffer = &buffer, .offset = 32, .size = 16}.info(); - EXPECT_EQ(part.offset, 32); - EXPECT_EQ(part.range, 16); + set.write({0, buffer}); } CVK_TEST(DescriptorSet, variable_count) { @@ -151,13 +139,7 @@ CVK_TEST(DescriptorSet, variable_count) { Sampler const sampler{context.core, {}, create}; EXPECT_EQ(set.variableCount(), 8); - set.write( - ImageDescriptorWrite{ - .binding = 0, - .arrayElement = 6, - .images = {{.view = &view, .sampler = &sampler}, {.view = &view, .sampler = &sampler}}, - } - ); + set.write({{0, view, sampler, 6}, {0, view, sampler, 7}}); } CVK_TEST(DescriptorSet, release_and_wrap) { diff --git a/cvk/tests/src/render/cmd/local_read_test.cpp b/cvk/tests/src/render/cmd/local_read_test.cpp index fe97e934..f3195897 100644 --- a/cvk/tests/src/render/cmd/local_read_test.cpp +++ b/cvk/tests/src/render/cmd/local_read_test.cpp @@ -162,12 +162,8 @@ namespace { }, FULLSCREEN.size() } { - input.write( - ImageDescriptorWrite{ - .binding = 0, - .images = {{.view = &first, .layout = VK_IMAGE_LAYOUT_RENDERING_LOCAL_READ_KHR}}, - } - ); + // input attachment: VK_IMAGE_LAYOUT_RENDERING_LOCAL_READ_KHR from the binding type + input.write({0, first}); vertices.write(FULLSCREEN); } From 81dcb3981642cd551f62920f4cfa289f7926160f Mon Sep 17 00:00:00 2001 From: lukas <89145039+codethinki@users.noreply.github.com> Date: Tue, 20 Oct 2026 10:32:29 +0200 Subject: [PATCH 077/135] cvk: image layout of a descriptor write, default from the binding type - the view constructors of DescriptorWrite take the layout of the view at the gpu access (VK_IMAGE_LAYOUT_MAX_ENUM => the default of the binding type), for example DEPTH_READ_ONLY_OPTIMAL for a depth image that is the read only depth attachment of the same rendering - test: a texture in GENERAL with the layout in the write, through a sampler2D: the layers do not check the layout of dynamically indexed runtime array descriptors --- .../cvk/data/descriptor/descriptor_write.hpp | 24 ++- cvk/src/data/descriptor/descriptor_set.cpp | 6 +- cvk/tests/shaders/texture.frag | 7 + .../data/descriptor/descriptor_draw_test.cpp | 142 +++++++++++------- cvk/tests/src/shaders.hpp | 20 +++ 5 files changed, 136 insertions(+), 63 deletions(-) create mode 100644 cvk/tests/shaders/texture.frag diff --git a/cvk/incl/cvk/data/descriptor/descriptor_write.hpp b/cvk/incl/cvk/data/descriptor/descriptor_write.hpp index 1069f6e3..55f55c2d 100644 --- a/cvk/incl/cvk/data/descriptor/descriptor_write.hpp +++ b/cvk/incl/cvk/data/descriptor/descriptor_write.hpp @@ -16,7 +16,7 @@ class Sampler; /** * @brief one descriptor of one binding, the constructor decides the kind - * @details the binding type in the layout of the set decides the descriptor type and the image layout: + * @details the binding type in the layout of the set decides the descriptor type and the default image layout: * storage image -> VK_IMAGE_LAYOUT_GENERAL, input attachment -> VK_IMAGE_LAYOUT_RENDERING_LOCAL_READ_KHR, * else VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL */ @@ -33,23 +33,34 @@ class DescriptorWrite { /** * @brief image descriptor without sampler: sampled image, storage image, input attachment * @param element array element of the binding + * @param layout of the view at the gpu access (VK_IMAGE_LAYOUT_MAX_ENUM => the default of the binding type) */ - constexpr DescriptorWrite(uint32_t binding, ImageView const& view, uint32_t element = 0) : - _binding{binding}, _element{element}, _resource{ImageResource{&view, nullptr}} {} + constexpr DescriptorWrite( + uint32_t binding, + ImageView const& view, + uint32_t element = 0, + VkImageLayout layout = VK_IMAGE_LAYOUT_MAX_ENUM + ) : _binding{binding}, _element{element}, _resource{ImageResource{&view, nullptr, layout}} {} /** * @brief combined image sampler descriptor * @param element array element of the binding + * @param layout of the view at the gpu access (VK_IMAGE_LAYOUT_MAX_ENUM => the default of the binding type) */ - constexpr DescriptorWrite(uint32_t binding, ImageView const& view, Sampler const& sampler, uint32_t element = 0) : - _binding{binding}, _element{element}, _resource{ImageResource{&view, &sampler}} {} + constexpr DescriptorWrite( + uint32_t binding, + ImageView const& view, + Sampler const& sampler, + uint32_t element = 0, + VkImageLayout layout = VK_IMAGE_LAYOUT_MAX_ENUM + ) : _binding{binding}, _element{element}, _resource{ImageResource{&view, &sampler, layout}} {} /** * @brief sampler descriptor * @param element array element of the binding */ constexpr DescriptorWrite(uint32_t binding, Sampler const& sampler, uint32_t element = 0) : - _binding{binding}, _element{element}, _resource{ImageResource{nullptr, &sampler}} {} + _binding{binding}, _element{element}, _resource{ImageResource{nullptr, &sampler, VK_IMAGE_LAYOUT_UNDEFINED}} {} private: struct BufferResource { @@ -63,6 +74,7 @@ class DescriptorWrite { struct ImageResource { ImageView const* view; Sampler const* sampler; + VkImageLayout layout; }; uint32_t _binding; diff --git a/cvk/src/data/descriptor/descriptor_set.cpp b/cvk/src/data/descriptor/descriptor_set.cpp index 29362177..2071eed9 100644 --- a/cvk/src/data/descriptor/descriptor_set.cpp +++ b/cvk/src/data/descriptor/descriptor_set.cpp @@ -215,14 +215,16 @@ void DescriptorSet::write(span writes) const { ); vkWrite.pBufferInfo = &bufferInfos.back(); } else { - auto const& [view, sampler] = std::get(write._resource); + auto const& [view, sampler, layout] = std::get(write._resource); debug_check_image(view, sampler, type); imageInfos.push_back( VkDescriptorImageInfo{ .sampler = sampler != nullptr ? sampler->get() : VK_NULL_HANDLE, .imageView = view != nullptr ? view->get() : VK_NULL_HANDLE, - .imageLayout = view != nullptr ? descriptor_layout(type) : VK_IMAGE_LAYOUT_UNDEFINED, + .imageLayout = view == nullptr ? VK_IMAGE_LAYOUT_UNDEFINED + : layout != VK_IMAGE_LAYOUT_MAX_ENUM ? layout + : descriptor_layout(type), } ); vkWrite.pImageInfo = &imageInfos.back(); diff --git a/cvk/tests/shaders/texture.frag b/cvk/tests/shaders/texture.frag new file mode 100644 index 00000000..b7888abc --- /dev/null +++ b/cvk/tests/shaders/texture.frag @@ -0,0 +1,7 @@ +#version 450 + +layout(set = 0, binding = 0) uniform sampler2D tex; + +layout(location = 0) out vec4 outColor; + +void main() { outColor = texture(tex, vec2(0.5)); } diff --git a/cvk/tests/src/data/descriptor/descriptor_draw_test.cpp b/cvk/tests/src/data/descriptor/descriptor_draw_test.cpp index 236b390a..39d3b135 100644 --- a/cvk/tests/src/data/descriptor/descriptor_draw_test.cpp +++ b/cvk/tests/src/data/descriptor/descriptor_draw_test.cpp @@ -190,64 +190,96 @@ CVK_TEST(DescriptorDraw, uniform_buffer_color) { expect_green(target); } -CVK_TEST(DescriptorDraw, bindless_texture_written_after_bind) { - test::Context const context{}; - constexpr uint32_t INDEX = 17; +namespace { + /** + * @brief draws a green texture + * @param layout of the texture at the draw (VK_IMAGE_LAYOUT_MAX_ENUM => the default, shader read only) + * @param afterBind true -> bindless binding of 64, the descriptor at element 17 comes after the recording, + * false -> one sampler2D, the descriptor comes before the recording, the layers check its layout at the draw + */ + void draw_texture(test::Context const& context, VkImageLayout layout, bool afterBind) { + constexpr VkDescriptorBindingFlags BINDLESS = VK_DESCRIPTOR_BINDING_PARTIALLY_BOUND_BIT + | VK_DESCRIPTOR_BINDING_UPDATE_AFTER_BIND_BIT + | VK_DESCRIPTOR_BINDING_VARIABLE_DESCRIPTOR_COUNT_BIT; + uint32_t const index = afterBind ? 17 : 0; + auto const textureLayout = layout != VK_IMAGE_LAYOUT_MAX_ENUM ? layout + : VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; - DescriptorSetLayout::Config setConfig{}; - setConfig.add( - 0, - VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, - VK_SHADER_STAGE_FRAGMENT_BIT, - 64, - VK_DESCRIPTOR_BINDING_PARTIALLY_BOUND_BIT | VK_DESCRIPTOR_BINDING_UPDATE_AFTER_BIND_BIT - | VK_DESCRIPTOR_BINDING_VARIABLE_DESCRIPTOR_COUNT_BIT - ); - DescriptorSetLayout const setLayout{context.core, setConfig, create}; - Fill const fill{ - context.core, - setLayout, - test::shaders::BINDLESS_FRAG, - {{VK_SHADER_STAGE_FRAGMENT_BIT, 0, sizeof(uint32_t)}} - }; - Target const target{context.core}; + DescriptorSetLayout::Config setConfig{}; + setConfig.add( + 0, + VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, + VK_SHADER_STAGE_FRAGMENT_BIT, + afterBind ? 64 : 1, + afterBind ? BINDLESS : 0 + ); + DescriptorSetLayout const setLayout{context.core, setConfig, create}; + Fill const fill{ + context.core, + setLayout, + afterBind ? std::span{test::shaders::BINDLESS_FRAG} + : std::span{test::shaders::TEXTURE_FRAG}, + {{VK_SHADER_STAGE_FRAGMENT_BIT, 0, sizeof(uint32_t)}} + }; + Target const target{context.core}; - // 1x1 green texture - Image const texture{ - context.core, - {.format = VK_FORMAT_R8G8B8A8_UNORM, .usage = VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT}, - VkExtent2D{1, 1} - }; - ImageView const view{texture, {}, create}; - Sampler const sampler{context.core, {}, create}; - Buffer staging{context.core, host_written(VK_BUFFER_USAGE_TRANSFER_SRC_BIT), 4}; - staging.write(GREEN_PIXEL); - - DescriptorPool const pool{context.core, {{&setLayout, 1, 32}}, create}; - DescriptorSet const set{pool, setLayout, 32, create}; - - CmdPool const cmdPool{context.core, CmdPool::Config{context.core.queue(0)}, create}; - PrimaryCmdBuffer cmd{cmdPool, create}; - cmd.begin(VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT); - - Barrier toTransfer{}; - toTransfer.addTransition(texture, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_UNDEFINED); - toTransfer.execute(cmd); - texture.copy(cmd, staging); - Barrier toShader{}; - toShader.addTransition(texture, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL); - toShader.execute(cmd); - - record_fill(context, cmd, fill, target, [&](BaseCmdBuffer& c) { - c.bindDescriptorSet(set, fill.layout, 0); - c.pushConstants(fill.layout, VK_SHADER_STAGE_FRAGMENT_BIT, INDEX); - }); - cmd.end(); + // 1x1 green texture + Image const texture{ + context.core, + {.format = VK_FORMAT_R8G8B8A8_UNORM, .usage = VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT}, + VkExtent2D{1, 1} + }; + ImageView const view{texture, {}, create}; + Sampler const sampler{context.core, {}, create}; + Buffer staging{context.core, host_written(VK_BUFFER_USAGE_TRANSFER_SRC_BIT), 4}; + staging.write(GREEN_PIXEL); - // update after bind: the bind is recorded, the descriptor comes now - set.write({0, view, sampler, INDEX}); - test::submit_and_wait(context, cmd); + uint32_t const variableCount = afterBind ? 32 : 0; + DescriptorPool const pool{context.core, {{&setLayout, 1, variableCount}}, create}; + DescriptorSet const set{pool, setLayout, variableCount, create}; + if(!afterBind) set.write({0, view, sampler, index, layout}); - expect_green(target); + CmdPool const cmdPool{context.core, CmdPool::Config{context.core.queue(0)}, create}; + PrimaryCmdBuffer cmd{cmdPool, create}; + cmd.begin(VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT); + + Barrier toTransfer{}; + toTransfer.addTransition(texture, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_UNDEFINED); + toTransfer.execute(cmd); + texture.copy(cmd, staging); + Barrier toShader{}; + toShader.add( + texture, + ImageBarrierInfo::LayoutTransition( + textureLayout, + VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, + {VK_PIPELINE_STAGE_2_TRANSFER_BIT, VK_ACCESS_2_TRANSFER_WRITE_BIT}, + {VK_PIPELINE_STAGE_2_FRAGMENT_SHADER_BIT, VK_ACCESS_2_SHADER_READ_BIT} + ) + ); + toShader.execute(cmd); + + record_fill(context, cmd, fill, target, [&](BaseCmdBuffer& c) { + c.bindDescriptorSet(set, fill.layout, 0); + c.pushConstants(fill.layout, VK_SHADER_STAGE_FRAGMENT_BIT, index); + }); + cmd.end(); + + // update after bind: the bind is recorded, the descriptor comes now + if(afterBind) set.write({0, view, sampler, index, layout}); + test::submit_and_wait(context, cmd); + + expect_green(target); + } +} + +CVK_TEST(DescriptorDraw, bindless_texture_written_after_bind) { + test::Context const context{}; + draw_texture(context, VK_IMAGE_LAYOUT_MAX_ENUM, true); +} + +CVK_TEST(DescriptorDraw, texture_in_the_layout_of_the_write) { + test::Context const context{}; + draw_texture(context, VK_IMAGE_LAYOUT_GENERAL, false); } } diff --git a/cvk/tests/src/shaders.hpp b/cvk/tests/src/shaders.hpp index 83f000dc..9546c023 100644 --- a/cvk/tests/src/shaders.hpp +++ b/cvk/tests/src/shaders.hpp @@ -156,4 +156,24 @@ inline constexpr std::array LOCAL_READ_FRAG{ 0x00000005, 0x0004003d, 0x0000000a, 0x0000000d, 0x0000000c, 0x00050062, 0x00000007, 0x00000012, 0x0000000d, 0x00000011, 0x0003003e, 0x00000009, 0x00000012, 0x000100fd, 0x00010038, }; + +/** + * @brief fragment shader: set 0 binding 0 combined image sampler, location 0 color output of its center + */ +inline constexpr std::array TEXTURE_FRAG{ + 0x07230203, 0x00010600, 0x000d000b, 0x00000013, 0x00000000, 0x00020011, 0x00000001, 0x0006000b, + 0x00000001, 0x4c534c47, 0x6474732e, 0x3035342e, 0x00000000, 0x0003000e, 0x00000000, 0x00000001, + 0x0007000f, 0x00000004, 0x00000004, 0x6e69616d, 0x00000000, 0x00000009, 0x0000000d, 0x00030010, + 0x00000004, 0x00000007, 0x00040047, 0x00000009, 0x0000001e, 0x00000000, 0x00040047, 0x0000000d, + 0x00000021, 0x00000000, 0x00040047, 0x0000000d, 0x00000022, 0x00000000, 0x00020013, 0x00000002, + 0x00030021, 0x00000003, 0x00000002, 0x00030016, 0x00000006, 0x00000020, 0x00040017, 0x00000007, + 0x00000006, 0x00000004, 0x00040020, 0x00000008, 0x00000003, 0x00000007, 0x0004003b, 0x00000008, + 0x00000009, 0x00000003, 0x00090019, 0x0000000a, 0x00000006, 0x00000001, 0x00000000, 0x00000000, + 0x00000000, 0x00000001, 0x00000000, 0x0003001b, 0x0000000b, 0x0000000a, 0x00040020, 0x0000000c, + 0x00000000, 0x0000000b, 0x0004003b, 0x0000000c, 0x0000000d, 0x00000000, 0x00040017, 0x0000000f, + 0x00000006, 0x00000002, 0x0004002b, 0x00000006, 0x00000010, 0x3f000000, 0x0005002c, 0x0000000f, + 0x00000011, 0x00000010, 0x00000010, 0x00050036, 0x00000002, 0x00000004, 0x00000000, 0x00000003, + 0x000200f8, 0x00000005, 0x0004003d, 0x0000000b, 0x0000000e, 0x0000000d, 0x00050057, 0x00000007, + 0x00000012, 0x0000000e, 0x00000011, 0x0003003e, 0x00000009, 0x00000012, 0x000100fd, 0x00010038, +}; } From 8fdc9b0fafb97f281d7d43618e66a1144af516c3 Mon Sep 17 00:00:00 2001 From: lukas <89145039+codethinki@users.noreply.github.com> Date: Tue, 20 Oct 2026 10:51:28 +0200 Subject: [PATCH 078/135] cvk: Mapping, Range and BufferCopy, std::byte, optional memory - Mapping: the mapped range of a memory, the destructor unmaps, as() turns the bytes into elements, Memory::unmap() is private - Memory::map(), flush(), invalidate() take a Range, the memory works in std::byte - BaseBuffer forwards map(), flush(), invalidate() and write() like the legacy buffer, Buffer does the same in elements - copy() takes a BufferCopy {srcOffset, dstOffset, size}, the positional size came before the offsets - std::optional in BaseBuffer and Image, memory() returns nullptr without own memory (swapchain images), before a not created Memory stood for it - removed: stage() (copy() with a dstOffset does it), the static write(data, mapped) of Memory and Buffer - flush() and invalidate() document that the memory must be mapped, vkInvalidateMappedMemoryRanges() requires it --- cvk/incl/cvk/data/buffer/base_buffer.hpp | 85 ++++++++++--- cvk/incl/cvk/data/buffer/buffer.hpp | 114 +++++------------- cvk/incl/cvk/data/img/image.hpp | 10 +- cvk/incl/cvk/data/memory/mapping.hpp | 57 +++++++++ cvk/incl/cvk/data/memory/memory.hpp | 82 +++++++------ cvk/src/data/buffer/base_buffer.cpp | 52 +++++--- cvk/src/data/img/image.cpp | 18 +-- cvk/src/data/memory/memory.cpp | 37 +++--- .../src/data/buffer/base_buffer_test.cpp | 78 ++++++------ cvk/tests/src/data/buffer/buffer_test.cpp | 32 ++--- .../data/descriptor/descriptor_draw_test.cpp | 8 +- cvk/tests/src/data/img/image_test.cpp | 28 ++--- cvk/tests/src/data/memory/memory_test.cpp | 77 +++++++----- cvk/tests/src/render/cmd/draw_test.cpp | 9 +- cvk/tests/src/render/cmd/local_read_test.cpp | 8 +- cvk/tests/src/render/cmd/rendering_test.cpp | 9 +- 16 files changed, 397 insertions(+), 307 deletions(-) create mode 100644 cvk/incl/cvk/data/memory/mapping.hpp diff --git a/cvk/incl/cvk/data/buffer/base_buffer.hpp b/cvk/incl/cvk/data/buffer/base_buffer.hpp index 44c4fcb4..74fb72e1 100644 --- a/cvk/incl/cvk/data/buffer/base_buffer.hpp +++ b/cvk/incl/cvk/data/buffer/base_buffer.hpp @@ -4,23 +4,40 @@ #include "cvk/base/device_table.hpp" #include "cvk/data/memory/memory.hpp" #include "cvk/utility/handle/core_handle.hpp" +#include "cvk/utility/range.hpp" #include "cvk/utility/types.hpp" #include #include +#include #include +#include namespace cvk { class BaseCmdBuffer; class Core; +/** + * @brief offsets and size of a buffer copy + * @details in bytes, in elements for a typed buffer + */ +struct BufferCopy { + size_t srcOffset = 0; + size_t dstOffset = 0; + + /** + * @brief (VK_WHOLE_SIZE => as much as fits into both buffers) + */ + size_t size = VK_WHOLE_SIZE; +}; + /** * @brief VkBuffer wrapper, owns the buffer and its memory, sizes in bytes * @details @ref create() creates both with vmaCreateBuffer(), - * map, flush and write through @ref memory(), + * map, flush, invalidate and write forward to the memory, * @ref Buffer adds an element type */ class BaseBuffer { @@ -58,7 +75,7 @@ class BaseBuffer { /** * @brief wraps the state - * @details std::nullopt @ref State::memory -> @ref destroy() only destroys the buffer + * @details std::nullopt @ref State::memory -> no own memory, @ref destroy() only destroys the buffer * @pre the buffer is bound to its memory * @pre the buffer was created with the usage of the config * @post @ref created() @@ -95,28 +112,46 @@ class BaseBuffer { /** * @brief records a copy from the source buffer - * @param size in bytes (VK_WHOLE_SIZE => as much as fits into both buffers) - * @param srcOffset in bytes - * @param dstOffset in bytes + * @param region in bytes * @pre @ref created() and src created * @pre @ref BaseCmdBuffer::recording() * @pre src usage has VK_BUFFER_USAGE_TRANSFER_SRC_BIT, the usage has VK_BUFFER_USAGE_TRANSFER_DST_BIT * @pre 0 < copied size, the ranges lie in the buffers */ - void copy( - BaseCmdBuffer const&, - BaseBuffer const& src, - size_t size = VK_WHOLE_SIZE, - size_t srcOffset = 0, - size_t dstOffset = 0 - ) const; + void copy(BaseCmdBuffer const&, BaseBuffer const& src, BufferCopy region = {}) const; /** - * @brief records a copy of the whole staging buffer - * @param dstOffset in bytes - * @details calls @ref copy() + * @brief maps the range of the memory + * @param range in bytes + * @return the mapped range, its destructor unmaps + * @pre @ref memory() != nullptr + * @details calls @ref Memory::map() */ - void stage(BaseCmdBuffer const&, BaseBuffer const& stagingBuffer, size_t dstOffset = 0) const; + [[nodiscard]] Mapping map(Range range = {}) const; + + /** + * @brief makes host writes visible to the device + * @param range in bytes + * @pre @ref memory() != nullptr, the memory is mapped + * @details calls @ref Memory::flush() + */ + void flush(Range range = {}) const; + + /** + * @brief makes device writes visible to the host + * @param range in bytes + * @pre @ref memory() != nullptr, the memory is mapped + * @details calls @ref Memory::invalidate() + */ + void invalidate(Range range = {}) const; + + /** + * @brief copies the data to the buffer + * @param offset in bytes + * @pre @ref memory() != nullptr + * @details calls @ref Memory::write() + */ + void write(std::span data, size_t offset = 0) const; static void destroy(DeviceTable table, VkBuffer); @@ -131,16 +166,23 @@ class BaseBuffer { [[nodiscard]] constexpr size_t size() const { return _size; } /** - * @return the memory of the buffer, not created -> the buffer does not own its memory + * @return nullptr -> the buffer does not own its memory */ - [[nodiscard]] constexpr Memory const& memory() const { return _memory; } + [[nodiscard]] constexpr Memory const* memory() const { + return _memory.has_value() && _memory->created() ? &*_memory : nullptr; + } private: void forget(); + /** + * @pre @ref memory() != nullptr + */ + [[nodiscard]] constexpr Memory const& ownMemory() const; + // declared before the memory: move assignment destroys the old buffer before its memory core_handle _handle; - Memory _memory; + std::optional _memory = std::nullopt; Config _config; size_t _size = 0; @@ -169,6 +211,11 @@ struct BaseBuffer::State { } namespace cvk { +constexpr Memory const& BaseBuffer::ownMemory() const { + CTH_CRITICAL(memory() == nullptr, "the buffer must own its memory") {} + return *_memory; +} + constexpr void BaseBuffer::debug_check(BaseBuffer const& buffer) { CTH_CRITICAL(!buffer.created(), "buffer must be created") {} } diff --git a/cvk/incl/cvk/data/buffer/buffer.hpp b/cvk/incl/cvk/data/buffer/buffer.hpp index dc6e3589..9b69d20f 100644 --- a/cvk/incl/cvk/data/buffer/buffer.hpp +++ b/cvk/incl/cvk/data/buffer/buffer.hpp @@ -4,6 +4,7 @@ #include #include +#include #include #include @@ -31,7 +32,7 @@ constexpr VkIndexType index_type = std::same_as ? VK_INDEX_TYPE_UIN * @tparam T element, the gpu copies it byte by byte * @details adds no members: a @ref BaseBuffer reference to it sees the same buffer in bytes */ -template requires std::is_trivially_copyable_v +template class Buffer : public BaseBuffer { public: /** @@ -74,93 +75,51 @@ class Buffer : public BaseBuffer { void create(size_t count) { BaseBuffer::create(count * sizeof(T)); } /** - * @brief maps the memory as elements - * @param count (VK_WHOLE_SIZE => to the end) - * @param offset in elements - * @return the mapped elements - * @pre offset + count <= @ref elements() - * @note each call needs one @ref unmap() - * @details calls @ref Memory::map() + * @brief maps the range of the memory as elements + * @param range in elements + * @return the mapped elements, the destructor unmaps + * @details calls @ref BaseBuffer::map() */ - [[nodiscard]] std::span map(size_t count = VK_WHOLE_SIZE, size_t offset = 0) const { - CTH_CRITICAL(offset > elements(), "offset must lie in the buffer") {} - size_t const mapCount = count == VK_WHOLE_SIZE ? elements() - offset : count; - CTH_CRITICAL(mapCount > elements() - offset, "range must lie in the buffer") {} - - return as_elements(memory().map(mapCount * sizeof(T), offset * sizeof(T))); - } + [[nodiscard]] Mapping map(Range range = {}) const { return BaseBuffer::map(bytes(range)).template as(); } /** - * @brief unmaps the memory once - * @details calls @ref Memory::unmap() + * @brief makes host writes visible to the device + * @param range in elements + * @details calls @ref BaseBuffer::flush() */ - void unmap() const { memory().unmap(); } + void flush(Range range = {}) const { BaseBuffer::flush(bytes(range)); } /** - * @brief copies the data to the buffer - * @param offset in elements - * @pre offset + data.size() <= @ref elements() - * @details calls @ref Memory::write() + * @brief makes device writes visible to the host + * @param range in elements + * @details calls @ref BaseBuffer::invalidate() */ - void write(std::span data, size_t offset = 0) const { - CTH_CRITICAL(offset > elements() || data.size() > elements() - offset, "range must lie in the buffer") {} - - memory().write(std::span{reinterpret_cast(data.data()), data.size_bytes()}, offset * sizeof(T)); - } + void invalidate(Range range = {}) const { BaseBuffer::invalidate(bytes(range)); } /** - * @brief makes host writes visible to the device - * @param count (VK_WHOLE_SIZE => to the end) - * @param offset in elements - * @details calls @ref Memory::flush() - */ - void flush(size_t count = VK_WHOLE_SIZE, size_t offset = 0) const { - memory().flush(bytes(count), offset * sizeof(T)); - } - - /** - * @brief makes device writes visible to the host - * @param count (VK_WHOLE_SIZE => to the end) + * @brief copies the data to the buffer * @param offset in elements - * @details calls @ref Memory::invalidate() + * @details calls @ref BaseBuffer::write() */ - void invalidate(size_t count = VK_WHOLE_SIZE, size_t offset = 0) const { - memory().invalidate(bytes(count), offset * sizeof(T)); + void write(std::span data, size_t offset = 0) const { + BaseBuffer::write(std::as_bytes(data), offset * sizeof(T)); } /** * @brief records a copy from the source buffer - * @param count (VK_WHOLE_SIZE => as much as fits into both buffers) - * @param srcOffset in elements - * @param dstOffset in elements + * @param region in elements * @details calls @ref BaseBuffer::copy() */ - void copy( - BaseCmdBuffer const& cmdBuffer, - Buffer const& src, - size_t count = VK_WHOLE_SIZE, - size_t srcOffset = 0, - size_t dstOffset = 0 - ) const { - BaseBuffer::copy(cmdBuffer, src, bytes(count), srcOffset * sizeof(T), dstOffset * sizeof(T)); - } - - /** - * @brief records a copy of the whole staging buffer - * @param dstOffset in elements - * @details calls @ref BaseBuffer::stage() - */ - void stage(BaseCmdBuffer const& cmdBuffer, Buffer const& stagingBuffer, size_t dstOffset = 0) const { - BaseBuffer::stage(cmdBuffer, stagingBuffer, dstOffset * sizeof(T)); - } - - /** - * @brief copies the data to the mapped elements - * @pre data.size() <= mapped.size() - */ - static constexpr void write(std::span data, std::span mapped) { - CTH_CRITICAL(data.size() > mapped.size(), "data must fit into the mapped range") {} - std::ranges::copy(data, mapped.begin()); + void copy(BaseCmdBuffer const& cmdBuffer, Buffer const& src, BufferCopy region = {}) const { + BaseBuffer::copy( + cmdBuffer, + src, + { + .srcOffset = region.srcOffset * sizeof(T), + .dstOffset = region.dstOffset * sizeof(T), + .size = bytes(region.size), + } + ); } [[nodiscard]] constexpr size_t elements() const { return size() / sizeof(T); } @@ -173,17 +132,8 @@ class Buffer : public BaseBuffer { return count == VK_WHOLE_SIZE ? VK_WHOLE_SIZE : count * sizeof(T); } - /** - * @brief starts the lifetime of the elements in the mapped bytes, keeps their values - */ - [[nodiscard]] static std::span as_elements(std::span mapped) { - size_t const count = mapped.size() / sizeof(T); -#ifdef __cpp_lib_start_lifetime_as - return {std::start_lifetime_as_array(mapped.data(), count), count}; -#else - //TODO remove when the MSVC STL has start_lifetime_as on clang (microsoft/STL GH-6169) - return {reinterpret_cast(mapped.data()), count}; -#endif + [[nodiscard]] static constexpr Range bytes(Range range) { + return {.offset = range.offset * sizeof(T), .size = bytes(range.size)}; } public: diff --git a/cvk/incl/cvk/data/img/image.hpp b/cvk/incl/cvk/data/img/image.hpp index e298245f..0dacab8b 100644 --- a/cvk/incl/cvk/data/img/image.hpp +++ b/cvk/incl/cvk/data/img/image.hpp @@ -62,7 +62,7 @@ class Image { /** * @brief wraps the state - * @details std::nullopt @ref State::memory -> @ref destroy() only destroys the image + * @details std::nullopt @ref State::memory -> no own memory, @ref destroy() only destroys the image * @pre the image is bound to its memory * @pre the image was created with the config * @post @ref created() @@ -140,16 +140,18 @@ class Image { [[nodiscard]] constexpr uint32_t mipLevels() const { return _config.mipLevels; } /** - * @return the memory of the image, not created -> the image does not own its memory + * @return nullptr -> the image does not own its memory */ - [[nodiscard]] constexpr Memory const& memory() const { return _memory; } + [[nodiscard]] constexpr Memory const* memory() const { + return _memory.has_value() && _memory->created() ? &*_memory : nullptr; + } private: void forget(); // declared before the memory: move assignment destroys the old image before its memory core_handle _handle; - Memory _memory; + std::optional _memory = std::nullopt; Config _config; VkExtent2D _extent{}; diff --git a/cvk/incl/cvk/data/memory/mapping.hpp b/cvk/incl/cvk/data/memory/mapping.hpp new file mode 100644 index 00000000..d50bf6f9 --- /dev/null +++ b/cvk/incl/cvk/data/memory/mapping.hpp @@ -0,0 +1,57 @@ +#pragma once +#include "cvk/utility/types.hpp" + +#include +#include +#include + + +namespace cvk { +class Memory; + +/** + * @brief mapped range of a memory, the destructor unmaps + * @tparam T element, std::byte for the bytes of a memory + * @details move constructible, not copyable, not assignable, a moved from mapping does not unmap + */ +template +class Mapping { +public: + /** + * @brief takes over one map of the memory, @ref Memory::map() constructs it + * @pre the range lies in the mapped memory + */ + constexpr Mapping(Memory const& memory, std::span range) : _memory{&memory}, _range{range} {} + + /** + * @brief unmaps the memory once + */ + ~Mapping(); + + /** + * @brief the range as elements of U, takes over the map + * @details starts the lifetime of the elements, keeps their values + * @pre the size is a multiple of sizeof(U), the data is aligned for U + */ + template requires std::same_as + [[nodiscard]] Mapping as() &&; + + [[nodiscard]] constexpr std::span span() const { return _range; } + [[nodiscard]] constexpr T* data() const { return _range.data(); } + [[nodiscard]] constexpr size_t size() const { return _range.size(); } + [[nodiscard]] constexpr bool empty() const { return _range.empty(); } + [[nodiscard]] constexpr auto begin() const { return _range.begin(); } + [[nodiscard]] constexpr auto end() const { return _range.end(); } + [[nodiscard]] constexpr T& operator[](size_t index) const { return _range[index]; } + +private: + move_ptr _memory; + std::span _range; + +public: + Mapping(Mapping const& other) = delete; + Mapping(Mapping&& other) noexcept = default; + Mapping& operator=(Mapping const& other) = delete; + Mapping& operator=(Mapping&& other) = delete; +}; +} diff --git a/cvk/incl/cvk/data/memory/memory.hpp b/cvk/incl/cvk/data/memory/memory.hpp index 28204f42..90eea394 100644 --- a/cvk/incl/cvk/data/memory/memory.hpp +++ b/cvk/incl/cvk/data/memory/memory.hpp @@ -1,7 +1,9 @@ #pragma once +#include "mapping.hpp" #include "memory_config.hpp" #include "cvk/utility/handle/vma_handle.hpp" +#include "cvk/utility/range.hpp" #include "cvk/utility/types.hpp" #include @@ -9,7 +11,9 @@ #include -#include +#include +#include +#include #include @@ -90,61 +94,48 @@ class Memory { State release(); /** - * @brief maps the memory - * @param size (VK_WHOLE_SIZE => to the end) - * @param offset from the start of the memory - * @return the mapped range + * @brief maps the range of the memory + * @param range in bytes + * @return the mapped range, its destructor unmaps * @pre @ref created() * @pre @ref properties() has VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT - * @pre offset + size <= @ref size() - * @note VMA counts the maps, each call needs one @ref unmap() + * @pre the range lies in the memory + * @note VMA counts the maps, a map while a mapping lives returns the same memory * @throws cvk::vk_result_exception result of vmaMapMemory() */ - [[nodiscard]] std::span map(size_t size = VK_WHOLE_SIZE, size_t offset = 0) const; - - /** - * @brief unmaps the memory once - * @pre @ref map() was called more often than @ref unmap() - */ - void unmap() const; + [[nodiscard]] Mapping map(Range range = {}) const; /** * @brief makes host writes visible to the device - * @param size (VK_WHOLE_SIZE => to the end) - * @param offset from the start of the memory + * @param range in bytes * @details host coherent memory -> does nothing * @pre @ref created() + * @pre the memory is mapped: a @ref Mapping lives or VMA_ALLOCATION_CREATE_MAPPED_BIT * @throws cvk::vk_result_exception result of vmaFlushAllocation() */ - void flush(size_t size = VK_WHOLE_SIZE, size_t offset = 0) const; + void flush(Range range = {}) const; /** * @brief makes device writes visible to the host - * @param size (VK_WHOLE_SIZE => to the end) - * @param offset from the start of the memory + * @param range in bytes * @details host coherent memory -> does nothing * @pre @ref created() + * @pre the memory is mapped: a @ref Mapping lives or VMA_ALLOCATION_CREATE_MAPPED_BIT * @throws cvk::vk_result_exception result of vmaInvalidateAllocation() */ - void invalidate(size_t size = VK_WHOLE_SIZE, size_t offset = 0) const; + void invalidate(Range range = {}) const; /** * @brief copies the data to the memory * @details maps, copies, flushes and unmaps - * @param offset from the start of the memory + * @param offset in bytes * @pre @ref created() * @pre @ref properties() has VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT - * @pre offset + data.size() <= @ref size() + * @pre the data fits into the memory after the offset * @throws cvk::vk_result_exception result of vmaMapMemory() * @throws cvk::vk_result_exception result of vmaFlushAllocation() */ - void write(std::span data, size_t offset = 0) const; - - /** - * @brief copies the data to the mapped range - * @pre data.size() <= mapped.size() - */ - static constexpr void write(std::span data, std::span mapped); + void write(std::span data, size_t offset = 0) const; static void destroy(VmaAllocator, VmaAllocation); @@ -172,12 +163,20 @@ class Memory { /** * @return the whole memory if mapped, not mapped -> empty - * @details @ref map() or VMA_ALLOCATION_CREATE_MAPPED_BIT maps it + * @details a living @ref Mapping or VMA_ALLOCATION_CREATE_MAPPED_BIT maps it * @pre @ref created() */ - [[nodiscard]] std::span mapped() const; + [[nodiscard]] std::span mapped() const; private: + template + friend class Mapping; + + /** + * @brief unmaps the memory once, the destructor of a @ref Mapping calls it + */ + void unmap() const; + void forget(); [[nodiscard]] VmaAllocator allocator() const; @@ -202,9 +201,24 @@ struct Memory::State { } namespace cvk { -constexpr void Memory::write(std::span data, std::span mapped) { - CTH_CRITICAL(data.size() > mapped.size(), "data must fit into the mapped range") {} - std::ranges::copy(data, mapped.begin()); +template +Mapping::~Mapping() { + if(_memory != nullptr) _memory->unmap(); +} + +template +template requires std::same_as +Mapping Mapping::as() && { + CTH_CRITICAL(_range.size() % sizeof(U) != 0, "the size must be a multiple of the element size") {} + + size_t const count = _range.size() / sizeof(U); + auto const& memory = *_memory.release(); +#ifdef __cpp_lib_start_lifetime_as + return Mapping{memory, {std::start_lifetime_as_array(_range.data(), count), count}}; +#else + //TODO remove when the MSVC STL has start_lifetime_as on clang (microsoft/STL GH-6169) + return Mapping{memory, {reinterpret_cast(_range.data()), count}}; +#endif } } diff --git a/cvk/src/data/buffer/base_buffer.cpp b/cvk/src/data/buffer/base_buffer.cpp index c0127b22..169ed0fc 100644 --- a/cvk/src/data/buffer/base_buffer.cpp +++ b/cvk/src/data/buffer/base_buffer.cpp @@ -9,8 +9,7 @@ namespace cvk { -BaseBuffer::BaseBuffer(Core const& core, Config const& config) : _handle{core}, _memory{core, config.memory}, - _config{config} { +BaseBuffer::BaseBuffer(Core const& core, Config const& config) : _handle{core}, _config{config} { Core::debug_check(core); Config::debug_check(config); } @@ -25,7 +24,7 @@ void BaseBuffer::wrap(State const& state) { optDestroy(); _handle.reset(state.vkBuffer.get(), core()); - if(state.memory.has_value()) _memory.wrap(*state.memory); + if(state.memory.has_value()) _memory.emplace(core(), _config.memory, *state.memory); _size = state.size; } @@ -33,7 +32,7 @@ void BaseBuffer::create(size_t size) { optDestroy(); auto const bufferInfo = _config.createInfo(size); - auto const allocationInfo = _memory.config().createInfo(); + auto const allocationInfo = _config.memory.createInfo(); VkBuffer vkBuffer = VK_NULL_HANDLE; VmaAllocation vmaAllocation = VK_NULL_HANDLE; @@ -52,7 +51,7 @@ void BaseBuffer::create(size_t size) { } _handle.reset(vkBuffer, core()); - _memory.wrap(Memory::State{vmaAllocation}); + _memory.emplace(core(), _config.memory, Memory::State{vmaAllocation}); _size = size; } @@ -60,7 +59,7 @@ void BaseBuffer::destroy() { debug_check(*this); _handle.reset(); - _memory.optDestroy(); + _memory.reset(); forget(); } @@ -69,19 +68,15 @@ BaseBuffer::State BaseBuffer::release() { debug_check(*this); State state{.vkBuffer = _handle.release(), .size = _size, .memory = std::nullopt}; - if(_memory.created()) state.memory = _memory.release(); + if(_memory.has_value()) state.memory = _memory->release(); forget(); return state; } -void BaseBuffer::copy( - BaseCmdBuffer const& cmdBuffer, - BaseBuffer const& src, - size_t size, - size_t srcOffset, - size_t dstOffset -) const { +void BaseBuffer::copy(BaseCmdBuffer const& cmdBuffer, BaseBuffer const& src, BufferCopy region) const { + auto const [srcOffset, dstOffset, size] = region; + debug_check(*this); debug_check(src); CTH_CRITICAL(!cmdBuffer.recording(), "command buffer must be recording") {} @@ -106,7 +101,7 @@ void BaseBuffer::copy( details->add("dst: {} + {} > {} (offset + size > dst size)", dstOffset, copySize, _size); } - VkBufferCopy2 const region{ + VkBufferCopy2 const vkRegion{ .sType = VK_STRUCTURE_TYPE_BUFFER_COPY_2, .pNext = nullptr, .srcOffset = srcOffset, @@ -119,13 +114,29 @@ void BaseBuffer::copy( .srcBuffer = src.get(), .dstBuffer = get(), .regionCount = 1, - .pRegions = ®ion, + .pRegions = &vkRegion, }; core().functions()->vkCmdCopyBuffer2(cmdBuffer.get(), &info); } -void BaseBuffer::stage(BaseCmdBuffer const& cmdBuffer, BaseBuffer const& stagingBuffer, size_t dstOffset) const { - copy(cmdBuffer, stagingBuffer, stagingBuffer.size(), 0, dstOffset); +Mapping BaseBuffer::map(Range range) const { + debug_check(*this); + return ownMemory().map(range); +} + +void BaseBuffer::flush(Range range) const { + debug_check(*this); + ownMemory().flush(range); +} + +void BaseBuffer::invalidate(Range range) const { + debug_check(*this); + ownMemory().invalidate(range); +} + +void BaseBuffer::write(std::span data, size_t offset) const { + debug_check(*this); + ownMemory().write(data, offset); } void BaseBuffer::destroy(DeviceTable table, VkBuffer vkBuffer) { @@ -134,5 +145,8 @@ void BaseBuffer::destroy(DeviceTable table, VkBuffer vkBuffer) { table->vkDestroyBuffer(table.device(), vkBuffer, nullptr); } -void BaseBuffer::forget() { _size = 0; } +void BaseBuffer::forget() { + _memory.reset(); + _size = 0; +} } diff --git a/cvk/src/data/img/image.cpp b/cvk/src/data/img/image.cpp index 65b08962..8675b19c 100644 --- a/cvk/src/data/img/image.cpp +++ b/cvk/src/data/img/image.cpp @@ -11,8 +11,7 @@ namespace cvk { -Image::Image(Core const& core, Config const& config) : _handle{core}, _memory{core, config.memory}, - _config{config} { +Image::Image(Core const& core, Config const& config) : _handle{core}, _config{config} { Core::debug_check(core); Config::debug_check(config); } @@ -25,7 +24,7 @@ void Image::wrap(State const& state) { optDestroy(); _handle.reset(state.vkImage.get(), core()); - if(state.memory.has_value()) _memory.wrap(*state.memory); + if(state.memory.has_value()) _memory.emplace(core(), _config.memory, *state.memory); _extent = state.extent; } @@ -33,7 +32,7 @@ void Image::create(VkExtent2D extent) { optDestroy(); auto const imageInfo = _config.createInfo(extent); - auto const allocationInfo = _memory.config().createInfo(); + auto const allocationInfo = _config.memory.createInfo(); VkImage vkImage = VK_NULL_HANDLE; VmaAllocation vmaAllocation = VK_NULL_HANDLE; @@ -52,7 +51,7 @@ void Image::create(VkExtent2D extent) { } _handle.reset(vkImage, core()); - _memory.wrap(Memory::State{vmaAllocation}); + _memory.emplace(core(), _config.memory, Memory::State{vmaAllocation}); _extent = extent; } @@ -60,7 +59,7 @@ void Image::destroy() { debug_check(*this); _handle.reset(); - _memory.optDestroy(); + _memory.reset(); forget(); } @@ -73,7 +72,7 @@ Image::State Image::release() { .extent = _extent, .memory = std::nullopt, }; - if(_memory.created()) state.memory = _memory.release(); + if(_memory.has_value()) state.memory = _memory->release(); forget(); return state; @@ -140,5 +139,8 @@ void Image::destroy(DeviceTable table, VkImage vkImage) { table->vkDestroyImage(table.device(), vkImage, nullptr); } -void Image::forget() { _extent = {}; } +void Image::forget() { + _memory.reset(); + _extent = {}; +} } diff --git a/cvk/src/data/memory/memory.cpp b/cvk/src/data/memory/memory.cpp index 7958c52b..82540f10 100644 --- a/cvk/src/data/memory/memory.cpp +++ b/cvk/src/data/memory/memory.cpp @@ -4,6 +4,8 @@ #include "cvk/base/core.hpp" #include "cvk/utility/vk_exceptions.hpp" +#include + namespace cvk { Memory::Memory(Core const& core, Config const& config) : _handle{core}, _config{config} { Core::debug_check(core); } @@ -55,21 +57,21 @@ Memory::State Memory::release() { return state; } -std::span Memory::map(size_t size, size_t offset) const { +Mapping Memory::map(Range range) const { debug_check(*this); CTH_CRITICAL((properties() & VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT) == 0, "memory must be host visible") {} - size_t const memorySize = this->size(); - CTH_CRITICAL(offset > memorySize, "offset must lie in the memory") {} + size_t const memorySize = size(); + CTH_CRITICAL(range.offset > memorySize, "offset must lie in the memory") {} - size_t const mapSize = size == VK_WHOLE_SIZE ? memorySize - offset : size; - CTH_CRITICAL(mapSize > memorySize - offset, "range must lie in the memory") {} + size_t const mapSize = range.size == VK_WHOLE_SIZE ? memorySize - range.offset : range.size; + CTH_CRITICAL(mapSize > memorySize - range.offset, "range must lie in the memory") {} void* data = nullptr; auto const result = vmaMapMemory(allocator(), get(), &data); CVK_RESULT_STABLE_THROW(result != VK_SUCCESS, result, "failed to map memory") {} - return {static_cast(data) + offset, mapSize}; + return Mapping{*this, {static_cast(data) + range.offset, mapSize}}; } void Memory::unmap() const { @@ -78,21 +80,21 @@ void Memory::unmap() const { vmaUnmapMemory(allocator(), get()); } -void Memory::flush(size_t size, size_t offset) const { +void Memory::flush(Range range) const { debug_check(*this); - auto const result = vmaFlushAllocation(allocator(), get(), offset, size); + auto const result = vmaFlushAllocation(allocator(), get(), range.offset, range.size); CVK_RESULT_STABLE_THROW(result != VK_SUCCESS, result, "failed to flush memory") {} } -void Memory::invalidate(size_t size, size_t offset) const { +void Memory::invalidate(Range range) const { debug_check(*this); - auto const result = vmaInvalidateAllocation(allocator(), get(), offset, size); + auto const result = vmaInvalidateAllocation(allocator(), get(), range.offset, range.size); CVK_RESULT_STABLE_THROW(result != VK_SUCCESS, result, "failed to invalidate memory") {} } -void Memory::write(std::span data, size_t offset) const { +void Memory::write(std::span data, size_t offset) const { debug_check(*this); CTH_CRITICAL((properties() & VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT) == 0, "memory must be host visible") {} CTH_CRITICAL(offset > size() || data.size() > size() - offset, "range must lie in the memory") {} @@ -100,9 +102,12 @@ void Memory::write(std::span data, size_t offset) const { if(data.empty()) return; // not vmaCopyMemoryToAllocation(): it flushes after the unmap, non coherent memory must be mapped to flush - write(data, map(data.size(), offset)); - auto const result = vmaFlushAllocation(allocator(), get(), offset, data.size()); - unmap(); + VkResult result = VK_SUCCESS; + { + auto const mapping = map({.offset = offset, .size = data.size()}); + std::ranges::copy(data, mapping.begin()); + result = vmaFlushAllocation(allocator(), get(), offset, data.size()); + } CVK_RESULT_STABLE_THROW(result != VK_SUCCESS, result, "failed to flush memory") {} } @@ -121,11 +126,11 @@ VmaAllocationInfo Memory::info() const { return info; } -std::span Memory::mapped() const { +std::span Memory::mapped() const { auto const allocationInfo = info(); if(allocationInfo.pMappedData == nullptr) return {}; - return {static_cast(allocationInfo.pMappedData), allocationInfo.size}; + return {static_cast(allocationInfo.pMappedData), allocationInfo.size}; } VkMemoryPropertyFlags Memory::properties() const { diff --git a/cvk/tests/src/data/buffer/base_buffer_test.cpp b/cvk/tests/src/data/buffer/base_buffer_test.cpp index 00d80149..24ece861 100644 --- a/cvk/tests/src/data/buffer/base_buffer_test.cpp +++ b/cvk/tests/src/data/buffer/base_buffer_test.cpp @@ -7,6 +7,7 @@ #include #include +#include namespace cvk { @@ -80,7 +81,7 @@ CVK_TEST(BaseBuffer, base_constructor_does_not_create) { BaseBuffer const buffer{context.core, STAGING_CONFIG}; EXPECT_FALSE(buffer.created()); - EXPECT_FALSE(buffer.memory().created()); + EXPECT_EQ(buffer.memory(), nullptr); } CVK_TEST(BaseBuffer, create_and_destroy) { @@ -90,12 +91,12 @@ CVK_TEST(BaseBuffer, create_and_destroy) { ASSERT_TRUE(buffer.created()); EXPECT_EQ(&buffer.core(), &context.core); EXPECT_EQ(buffer.size(), SIZE); - ASSERT_TRUE(buffer.memory().created()); - EXPECT_GE(buffer.memory().size(), SIZE); + ASSERT_NE(buffer.memory(), nullptr); + EXPECT_GE(buffer.memory()->size(), SIZE); buffer.destroy(); EXPECT_FALSE(buffer.created()); - EXPECT_FALSE(buffer.memory().created()); + EXPECT_EQ(buffer.memory(), nullptr); EXPECT_EQ(buffer.size(), 0); } @@ -117,94 +118,91 @@ CVK_TEST(BaseBuffer, auto_usage_picks_host_visible_memory_for_host_access) { BaseBuffer const buffer{context.core, STAGING_CONFIG, SIZE}; - EXPECT_NE(buffer.memory().properties() & VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT, 0u); + EXPECT_NE(buffer.memory()->properties() & VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT, 0u); } -CVK_TEST(BaseBuffer, write_and_read_through_the_memory) { +CVK_TEST(BaseBuffer, write_and_read) { test::Context const context{}; BaseBuffer const buffer{context.core, READBACK_CONFIG, SIZE}; - std::array data{}; - std::ranges::fill(data, 'x'); + std::array data{}; + std::ranges::fill(data, std::byte{'x'}); - buffer.memory().write(data); + buffer.write(data); - auto const mapped = buffer.memory().map(SIZE); - buffer.memory().invalidate(); + auto const mapped = buffer.map(); + buffer.invalidate(); EXPECT_TRUE(std::ranges::equal(mapped, data)); - buffer.memory().unmap(); } CVK_TEST(BaseBuffer, copy_a_range) { test::Context const context{}; BaseBuffer const staging{context.core, STAGING_CONFIG, SIZE}; BaseBuffer const readback{context.core, READBACK_CONFIG, SIZE}; - std::array data{}; - std::ranges::fill(data, 'c'); - staging.memory().write(data, 16); + std::array data{}; + std::ranges::fill(data, std::byte{'c'}); + staging.write(data, 16); test::record_and_submit(context, [&](PrimaryCmdBuffer const& primary) { - readback.copy(primary, staging, data.size(), 16, 32); + readback.copy(primary, staging, {.srcOffset = 16, .dstOffset = 32, .size = data.size()}); test::host_read_barrier(context, primary); }); - auto const mapped = readback.memory().map(data.size(), 32); - readback.memory().invalidate(); + auto const mapped = readback.map({.offset = 32, .size = data.size()}); + readback.invalidate(); EXPECT_TRUE(std::ranges::equal(mapped, data)); - readback.memory().unmap(); } CVK_TEST(BaseBuffer, copy_whole_size_fits_both_buffers) { test::Context const context{}; BaseBuffer const staging{context.core, STAGING_CONFIG, SIZE}; BaseBuffer const readback{context.core, READBACK_CONFIG, SIZE / 2}; - std::array data{}; - std::ranges::fill(data, 'w'); - staging.memory().write(data); + std::array data{}; + std::ranges::fill(data, std::byte{'w'}); + staging.write(data); test::record_and_submit(context, [&](PrimaryCmdBuffer const& primary) { readback.copy(primary, staging); test::host_read_barrier(context, primary); }); - auto const mapped = readback.memory().map(SIZE / 2); - readback.memory().invalidate(); - EXPECT_TRUE(std::ranges::all_of(mapped, [](char const c) { return c == 'w'; })); - readback.memory().unmap(); + auto const mapped = readback.map(); + readback.invalidate(); + EXPECT_TRUE(std::ranges::all_of(mapped, [](std::byte b) { return b == std::byte{'w'}; })); } -CVK_TEST(BaseBuffer, stage_the_whole_staging_buffer) { +CVK_TEST(BaseBuffer, copy_the_whole_source_to_an_offset) { test::Context const context{}; BaseBuffer const staging{context.core, STAGING_CONFIG, 64}; BaseBuffer const readback{context.core, READBACK_CONFIG, SIZE}; - std::array data{}; - std::ranges::fill(data, 's'); - staging.memory().write(data); + std::array data{}; + std::ranges::fill(data, std::byte{'s'}); + staging.write(data); test::record_and_submit(context, [&](PrimaryCmdBuffer const& primary) { - readback.stage(primary, staging, 8); + readback.copy(primary, staging, {.dstOffset = 8}); test::host_read_barrier(context, primary); }); - auto const mapped = readback.memory().map(data.size(), 8); - readback.memory().invalidate(); + auto const mapped = readback.map({.offset = 8, .size = data.size()}); + readback.invalidate(); EXPECT_TRUE(std::ranges::equal(mapped, data)); - readback.memory().unmap(); } CVK_TEST(BaseBuffer, release_and_wrap) { test::Context const context{}; BaseBuffer buffer{context.core, DEVICE_CONFIG, SIZE}; auto const vkBuffer = buffer.get(); - auto const vmaAllocation = buffer.memory().get(); + auto const vmaAllocation = buffer.memory()->get(); auto const state = buffer.release(); EXPECT_FALSE(buffer.created()); - EXPECT_FALSE(buffer.memory().created()); + EXPECT_EQ(buffer.memory(), nullptr); ASSERT_TRUE(state.memory.has_value()); BaseBuffer const wrapped{context.core, DEVICE_CONFIG, state}; EXPECT_EQ(wrapped.get(), vkBuffer); - EXPECT_EQ(wrapped.memory().get(), vmaAllocation); + ASSERT_NE(wrapped.memory(), nullptr); + EXPECT_EQ(wrapped.memory()->get(), vmaAllocation); EXPECT_EQ(wrapped.size(), SIZE); } @@ -219,7 +217,7 @@ CVK_TEST(BaseBuffer, wrap_without_memory_destroys_only_the_buffer) { { BaseBuffer const wrapped{context.core, DEVICE_CONFIG, state}; - EXPECT_FALSE(wrapped.memory().created()); + EXPECT_EQ(wrapped.memory(), nullptr); EXPECT_EQ(wrapped.size(), SIZE); } @@ -235,8 +233,8 @@ CVK_TEST(BaseBuffer, move_assignment_destroys_the_old_buffer) { buffer = std::move(other); EXPECT_EQ(buffer.get(), vkBuffer); - EXPECT_TRUE(buffer.memory().created()); + EXPECT_NE(buffer.memory(), nullptr); EXPECT_FALSE(other.created()); - EXPECT_FALSE(other.memory().created()); + EXPECT_EQ(other.memory(), nullptr); } } diff --git a/cvk/tests/src/data/buffer/buffer_test.cpp b/cvk/tests/src/data/buffer/buffer_test.cpp index 5546bed8..ec650254 100644 --- a/cvk/tests/src/data/buffer/buffer_test.cpp +++ b/cvk/tests/src/data/buffer/buffer_test.cpp @@ -61,14 +61,6 @@ CVK_TEST(Buffer, elements_must_be_trivially_copyable) { CVK_TEST(Buffer, adds_no_members) { static_assert(sizeof(Buffer) == sizeof(BaseBuffer)); } -CVK_TEST(Buffer, write_to_mapped_elements_is_constexpr) { - static_assert([] { - std::array mapped{}; - Buffer::write(TRIANGLE, mapped); - return mapped[2] == TRIANGLE[2] && mapped[3] == Vertex{}; - }()); -} - CVK_TEST(Buffer, create_counts_elements) { test::Context const context{}; @@ -86,10 +78,9 @@ CVK_TEST(Buffer, write_and_map) { buffer.write(TRIANGLE, 2); - auto const mapped = buffer.map(TRIANGLE.size(), 2); + auto const mapped = buffer.map({.offset = 2, .size = TRIANGLE.size()}); buffer.invalidate(); EXPECT_TRUE(std::ranges::equal(mapped, TRIANGLE)); - buffer.unmap(); } CVK_TEST(Buffer, map_whole_buffer) { @@ -99,16 +90,13 @@ CVK_TEST(Buffer, map_whole_buffer) { auto const whole = buffer.map(); EXPECT_EQ(whole.size(), COUNT); - Buffer::write(TRIANGLE, whole); + std::ranges::copy(TRIANGLE, whole.begin()); buffer.flush(); - buffer.flush(1, 2); + buffer.flush({.offset = 2, .size = 1}); - auto const end = buffer.map(VK_WHOLE_SIZE, 5); + auto const end = buffer.map({.offset = 5}); EXPECT_EQ(end.size(), COUNT - 5); EXPECT_EQ(end.data(), whole.data() + 5); - - buffer.unmap(); - buffer.unmap(); } CVK_TEST(Buffer, copy_elements) { @@ -118,31 +106,29 @@ CVK_TEST(Buffer, copy_elements) { staging.write(TRIANGLE, 1); test::record_and_submit(context, [&](PrimaryCmdBuffer const& primary) { - readback.copy(primary, staging, TRIANGLE.size(), 1, 4); + readback.copy(primary, staging, {.srcOffset = 1, .dstOffset = 4, .size = TRIANGLE.size()}); test::host_read_barrier(context, primary); }); - auto const mapped = readback.map(TRIANGLE.size(), 4); + auto const mapped = readback.map({.offset = 4, .size = TRIANGLE.size()}); readback.invalidate(); EXPECT_TRUE(std::ranges::equal(mapped, TRIANGLE)); - readback.unmap(); } -CVK_TEST(Buffer, stage_elements) { +CVK_TEST(Buffer, copy_the_whole_source_to_an_element) { test::Context const context{}; Buffer const staging{context.core, STAGING_CONFIG, TRIANGLE.size()}; Buffer const readback{context.core, READBACK_CONFIG, COUNT}; staging.write(TRIANGLE); test::record_and_submit(context, [&](PrimaryCmdBuffer const& primary) { - readback.stage(primary, staging, 5); + readback.copy(primary, staging, {.dstOffset = 5}); test::host_read_barrier(context, primary); }); - auto const mapped = readback.map(TRIANGLE.size(), 5); + auto const mapped = readback.map({.offset = 5, .size = TRIANGLE.size()}); readback.invalidate(); EXPECT_TRUE(std::ranges::equal(mapped, TRIANGLE)); - readback.unmap(); } CVK_TEST(Buffer, release_and_wrap_as_bytes) { diff --git a/cvk/tests/src/data/descriptor/descriptor_draw_test.cpp b/cvk/tests/src/data/descriptor/descriptor_draw_test.cpp index 39d3b135..6b401dea 100644 --- a/cvk/tests/src/data/descriptor/descriptor_draw_test.cpp +++ b/cvk/tests/src/data/descriptor/descriptor_draw_test.cpp @@ -18,6 +18,7 @@ #include #include +#include #include #include #include @@ -155,12 +156,11 @@ namespace { } void expect_green(Target const& target) { - auto const pixels = target.readback.memory().map(target.readback.size()); - target.readback.memory().invalidate(); - auto const asByte = [](char c) { return static_cast(c); }; + auto const pixels = target.readback.map(); + target.readback.invalidate(); + auto const asByte = [](std::byte b) { return std::to_integer(b); }; for(auto const pixel : pixels | std::views::chunk(4)) EXPECT_TRUE(std::ranges::equal(pixel, GREEN_PIXEL, {}, asByte)); - target.readback.memory().unmap(); } } diff --git a/cvk/tests/src/data/img/image_test.cpp b/cvk/tests/src/data/img/image_test.cpp index ac08c9e2..4848b810 100644 --- a/cvk/tests/src/data/img/image_test.cpp +++ b/cvk/tests/src/data/img/image_test.cpp @@ -6,6 +6,7 @@ #include "cvk/render/sync/barrier.hpp" #include +#include #include @@ -119,7 +120,7 @@ CVK_TEST(Image, base_constructor_does_not_create) { Image const image{context.core, TEXTURE_CONFIG}; EXPECT_FALSE(image.created()); - EXPECT_FALSE(image.memory().created()); + EXPECT_EQ(image.memory(), nullptr); } CVK_TEST(Image, create_and_destroy) { @@ -131,11 +132,11 @@ CVK_TEST(Image, create_and_destroy) { EXPECT_EQ(image.extent().width, EXTENT.width); EXPECT_EQ(image.extent().height, EXTENT.height); EXPECT_EQ(image.mipLevels(), 7); - EXPECT_TRUE(image.memory().created()); + EXPECT_NE(image.memory(), nullptr); image.destroy(); EXPECT_FALSE(image.created()); - EXPECT_FALSE(image.memory().created()); + EXPECT_EQ(image.memory(), nullptr); EXPECT_EQ(image.extent().width, 0); } @@ -157,10 +158,10 @@ CVK_TEST(Image, copy_from_a_buffer_into_two_mip_levels) { BaseBuffer const staging{context.core, STAGING_CONFIG, mip0 + mip1}; BaseBuffer const readback{context.core, READBACK_CONFIG, mip0 + mip1}; - std::vector pixels(mip0 + mip1); + std::vector pixels(mip0 + mip1); for(size_t i = 0; i < pixels.size(); ++i) - pixels[i] = static_cast(i % 251); - staging.memory().write(pixels); + pixels[i] = static_cast(i % 251); + staging.write(pixels); test::record_and_submit(context, [&](PrimaryCmdBuffer const& primary) { Barrier toDst{}; @@ -178,26 +179,25 @@ CVK_TEST(Image, copy_from_a_buffer_into_two_mip_levels) { test::host_read_barrier(context, primary); }); - auto const mapped = readback.memory().map(pixels.size()); - readback.memory().invalidate(); + auto const mapped = readback.map({.size = pixels.size()}); + readback.invalidate(); EXPECT_TRUE(std::ranges::equal(mapped, pixels)); - readback.memory().unmap(); } CVK_TEST(Image, release_and_wrap) { test::Context const context{}; Image image{context.core, TEXTURE_CONFIG, EXTENT}; auto const vkImage = image.get(); - auto const vmaAllocation = image.memory().get(); + auto const vmaAllocation = image.memory()->get(); auto const state = image.release(); EXPECT_FALSE(image.created()); - EXPECT_FALSE(image.memory().created()); + EXPECT_EQ(image.memory(), nullptr); ASSERT_TRUE(state.memory.has_value()); Image const wrapped{context.core, TEXTURE_CONFIG, state}; EXPECT_EQ(wrapped.get(), vkImage); - EXPECT_EQ(wrapped.memory().get(), vmaAllocation); + EXPECT_EQ(wrapped.memory()->get(), vmaAllocation); EXPECT_EQ(wrapped.extent().width, EXTENT.width); } @@ -212,7 +212,7 @@ CVK_TEST(Image, wrap_without_memory_destroys_only_the_image) { { Image const wrapped{context.core, TEXTURE_CONFIG, state}; - EXPECT_FALSE(wrapped.memory().created()); + EXPECT_EQ(wrapped.memory(), nullptr); } EXPECT_TRUE(memory.created()); @@ -227,7 +227,7 @@ CVK_TEST(Image, move_assignment_destroys_the_old_image) { image = std::move(other); EXPECT_EQ(image.get(), vkImage); - EXPECT_TRUE(image.memory().created()); + EXPECT_NE(image.memory(), nullptr); EXPECT_FALSE(other.created()); } } diff --git a/cvk/tests/src/data/memory/memory_test.cpp b/cvk/tests/src/data/memory/memory_test.cpp index 2185c527..a4b4969d 100644 --- a/cvk/tests/src/data/memory/memory_test.cpp +++ b/cvk/tests/src/data/memory/memory_test.cpp @@ -5,6 +5,8 @@ #include #include +#include +#include #include @@ -80,16 +82,16 @@ CVK_TEST(Memory, map_write_and_read) { test::Context const context{}; Memory const memory{context.core, HOST_CONFIG, REQUIREMENTS}; - auto const written = memory.map(); - ASSERT_EQ(written.size(), memory.size()); - std::ranges::fill(written, 'x'); - memory.flush(); - memory.unmap(); + { + auto const written = memory.map(); + ASSERT_EQ(written.size(), memory.size()); + std::ranges::fill(written, std::byte{'x'}); + memory.flush(); + } auto const read = memory.map(); memory.invalidate(); - EXPECT_TRUE(std::ranges::all_of(read, [](char const c) { return c == 'x'; })); - memory.unmap(); + EXPECT_TRUE(std::ranges::all_of(read, [](std::byte b) { return b == std::byte{'x'}; })); } CVK_TEST(Memory, map_a_range) { @@ -97,43 +99,63 @@ CVK_TEST(Memory, map_a_range) { Memory const memory{context.core, HOST_CONFIG, REQUIREMENTS}; auto const whole = memory.map(); - auto const part = memory.map(16, 32); + auto const part = memory.map({.offset = 32, .size = 16}); + auto const end = memory.map({.offset = 32}); EXPECT_EQ(part.data(), whole.data() + 32); EXPECT_EQ(part.size(), 16); - - auto const end = memory.map(VK_WHOLE_SIZE, 32); EXPECT_EQ(end.size(), memory.size() - 32); - - memory.unmap(); - memory.unmap(); - memory.unmap(); } CVK_TEST(Memory, write_copies_the_data) { test::Context const context{}; Memory const memory{context.core, HOST_CONFIG, REQUIREMENTS}; - std::array data{}; - std::ranges::fill(data, 'y'); + std::array data{}; + std::ranges::fill(data, std::byte{'y'}); memory.write(data, 32); - auto const mapped = memory.map(data.size(), 32); + auto const mapped = memory.map({.offset = 32, .size = data.size()}); EXPECT_TRUE(std::ranges::equal(mapped, data)); - memory.unmap(); } -CVK_TEST(Memory, mapped_follows_map_and_unmap) { +CVK_TEST(Memory, the_mapping_unmaps) { test::Context const context{}; Memory const memory{context.core, HOST_CONFIG, REQUIREMENTS}; EXPECT_TRUE(memory.mapped().empty()); + { + auto const mapping = memory.map(); + EXPECT_EQ(memory.mapped().data(), mapping.data()); + EXPECT_EQ(memory.mapped().size(), memory.size()); + } + EXPECT_TRUE(memory.mapped().empty()); +} - auto const data = memory.map(); - EXPECT_EQ(memory.mapped().data(), data.data()); - EXPECT_EQ(memory.mapped().size(), memory.size()); +CVK_TEST(Memory, a_moved_mapping_unmaps_once) { + test::Context const context{}; + Memory const memory{context.core, HOST_CONFIG, REQUIREMENTS}; - memory.unmap(); + { + auto mapping = memory.map(); + Mapping const moved{std::move(mapping)}; + EXPECT_FALSE(memory.mapped().empty()); + } + EXPECT_TRUE(memory.mapped().empty()); +} + +CVK_TEST(Memory, mapping_as_elements) { + test::Context const context{}; + Memory const memory{context.core, HOST_CONFIG, REQUIREMENTS}; + + { + auto bytes = memory.map({.offset = 0, .size = 64}); + auto const* data = bytes.data(); + auto const elements = std::move(bytes).as(); + + EXPECT_EQ(elements.size(), 16); + EXPECT_EQ(static_cast(elements.data()), static_cast(data)); + } EXPECT_TRUE(memory.mapped().empty()); } @@ -147,15 +169,6 @@ CVK_TEST(Memory, mapped_bit_maps_persistently) { EXPECT_FALSE(memory.mapped().empty()); } -CVK_TEST(Memory, write_to_a_mapped_range_is_constexpr) { - static_assert([] { - std::array mapped{}; - std::array const data{'a', 'b'}; - Memory::write(data, mapped); - return mapped[0] == 'a' && mapped[1] == 'b' && mapped[2] == 0; - }()); -} - CVK_TEST(Memory, release_and_wrap) { test::Context const context{}; Memory memory{context.core, HOST_CONFIG, REQUIREMENTS}; diff --git a/cvk/tests/src/render/cmd/draw_test.cpp b/cvk/tests/src/render/cmd/draw_test.cpp index 4e7a23e8..04725021 100644 --- a/cvk/tests/src/render/cmd/draw_test.cpp +++ b/cvk/tests/src/render/cmd/draw_test.cpp @@ -13,6 +13,7 @@ #include #include +#include #include #include #include @@ -135,12 +136,12 @@ namespace { test::host_read_barrier(context, cmd); }); - auto const pixels = readback.memory().map(readback.size()); - readback.memory().invalidate(); + auto const pixels = readback.map(); + readback.invalidate(); constexpr std::array green{0, 255, 0, 255}; + auto const asByte = [](std::byte b) { return std::to_integer(b); }; for(auto const pixel : pixels | std::views::chunk(4)) - EXPECT_TRUE(std::ranges::equal(pixel, green, {}, [](char c) { return static_cast(c); })); - readback.memory().unmap(); + EXPECT_TRUE(std::ranges::equal(pixel, green, {}, asByte)); } } diff --git a/cvk/tests/src/render/cmd/local_read_test.cpp b/cvk/tests/src/render/cmd/local_read_test.cpp index f3195897..06411b4c 100644 --- a/cvk/tests/src/render/cmd/local_read_test.cpp +++ b/cvk/tests/src/render/cmd/local_read_test.cpp @@ -17,6 +17,7 @@ #include #include +#include #include #include @@ -285,12 +286,11 @@ CVK_TEST(LocalRead, draw_reads_an_attachment_of_the_same_rendering) { test::host_read_barrier(context, cmd); }); - auto const pixels = readback.memory().map(readback.size()); - readback.memory().invalidate(); - auto const asByte = [](char c) { return static_cast(c); }; + auto const pixels = readback.map(); + readback.invalidate(); + auto const asByte = [](std::byte b) { return std::to_integer(b); }; for(auto const pixel : pixels | std::views::chunk(4)) EXPECT_TRUE(std::ranges::equal(pixel, GREEN_PIXEL, {}, asByte)); - readback.memory().unmap(); } CVK_TEST(LocalRead, secondary_inherits_the_mapping) { diff --git a/cvk/tests/src/render/cmd/rendering_test.cpp b/cvk/tests/src/render/cmd/rendering_test.cpp index b7d0478b..b6d91e50 100644 --- a/cvk/tests/src/render/cmd/rendering_test.cpp +++ b/cvk/tests/src/render/cmd/rendering_test.cpp @@ -9,6 +9,7 @@ #include #include +#include #include #include @@ -79,12 +80,12 @@ CVK_TEST(Rendering, clear_a_color_attachment) { test::host_read_barrier(context, cmd); }); - auto const pixels = readback.memory().map(readback.size()); - readback.memory().invalidate(); + auto const pixels = readback.map(); + readback.invalidate(); auto const red = std::array{255, 0, 0, 255}; + auto const asByte = [](std::byte b) { return std::to_integer(b); }; for(auto const pixel : pixels | std::views::chunk(4)) - EXPECT_TRUE(std::ranges::equal(pixel, red, {}, [](char c) { return static_cast(c); })); - readback.memory().unmap(); + EXPECT_TRUE(std::ranges::equal(pixel, red, {}, asByte)); } CVK_TEST(Rendering, tracks_the_current_rendering) { From b1d0499b0cac79df09436db21dc31a1dbf61bb69 Mon Sep 17 00:00:00 2001 From: lukas <89145039+codethinki@users.noreply.github.com> Date: Tue, 20 Oct 2026 21:37:56 +0200 Subject: [PATCH 079/135] cvk: layout_transition takes the new layout first, the target first rule in AGENTS --- AGENTS.md | 1 + cvk/incl/cvk/render/sync/barrier.hpp | 36 +++++++++++----------- cvk/tests/src/render/sync/barrier_test.cpp | 6 ++-- 3 files changed, 22 insertions(+), 21 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index a48fa2cd..1bb87ff6 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -101,6 +101,7 @@ ctest --test-dir out/build/gcc_release --output-on-failure - Initialize Vulkan structs with designated initializers. Always write `.sType` and `.pNext`. - Data structs (`SubmitInfo`, `Barrier`, the info structs) are aggregates: the caller can set everything with designated initializers. Member functions like `add()` are optional shortcuts, checks run where the data is used. - Commands that change the state of a command buffer (binds, dynamic state, push constants, renderings, draws) are members of the command buffer: `cmd.bindPipeline(pipeline)`. Commands on the data of an object (barriers, copies, uploads) are members of the object and take the command buffer: `barrier.execute(cmd)`, `image.copy(cmd, staging)`. +- The target comes first, like `std::memcpy(dst, src, ...)`: a new value before the old one, `layout_transition(newLayout, oldLayout)`. The object of a copy is the destination: `dst.copy(cmd, src)`. - Use deducing `this` (`this auto& self`) where a function must work on the derived type. - Qualify `std::` names. A `.cpp` file can declare `using std::span;` and similar at the top of its namespace. - No raw `new` and `delete`. Use `std::unique_ptr`, `cth::move_ptr` and `cth::not_null`. diff --git a/cvk/incl/cvk/render/sync/barrier.hpp b/cvk/incl/cvk/render/sync/barrier.hpp index f700e64e..1b591c45 100644 --- a/cvk/incl/cvk/render/sync/barrier.hpp +++ b/cvk/incl/cvk/render/sync/barrier.hpp @@ -130,8 +130,8 @@ struct LayoutTransition { * @return the scopes of the transition, no common transition -> std::nullopt */ [[nodiscard]] constexpr std::optional layout_transition( - VkImageLayout oldLayout, - VkImageLayout newLayout + VkImageLayout newLayout, + VkImageLayout oldLayout ); @@ -247,7 +247,7 @@ constexpr void Barrier::addTransition( uint32_t firstMipLevel, uint32_t mipLevels ) { - auto const transition = layout_transition(oldLayout, newLayout); + auto const transition = layout_transition(newLayout, oldLayout); CTH_CRITICAL(!transition.has_value(), "layout transition has no table entry, use add() with scopes") { details->add("transition: {} -> {}", oldLayout, newLayout); } @@ -260,10 +260,10 @@ constexpr void Barrier::addTransition( } namespace cvk { -constexpr std::optional layout_transition(VkImageLayout oldLayout, VkImageLayout newLayout) { +constexpr std::optional layout_transition(VkImageLayout newLayout, VkImageLayout oldLayout) { struct Entry { - VkImageLayout oldLayout; VkImageLayout newLayout; + VkImageLayout oldLayout; LayoutTransition transition; }; @@ -299,66 +299,66 @@ constexpr std::optional layout_transition(VkImageLayout oldLay constexpr std::array entries{ Entry{ - VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, + VK_IMAGE_LAYOUT_UNDEFINED, {stagesOf(transferWrite), transferWrite}, }, Entry{ - VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, + VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, {transferWrite, fragmentRead}, }, Entry{ - VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, + VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, {transferWrite, transferRead}, }, Entry{ - VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, + VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, {transferRead, fragmentRead}, }, Entry{ - VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, + VK_IMAGE_LAYOUT_UNDEFINED, {stagesOf(color), color}, }, Entry{ - VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_OPTIMAL, + VK_IMAGE_LAYOUT_UNDEFINED, {stagesOf(depth), depth}, }, Entry{ - VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, + VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, {colorWrite, fragmentRead}, }, Entry{ - VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, VK_IMAGE_LAYOUT_PRESENT_SRC_KHR, + VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, {colorWrite, none}, }, Entry{ - VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_RENDERING_LOCAL_READ_KHR, + VK_IMAGE_LAYOUT_UNDEFINED, {stagesOf(localRead), localRead}, }, Entry{ - VK_IMAGE_LAYOUT_RENDERING_LOCAL_READ_KHR, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, + VK_IMAGE_LAYOUT_RENDERING_LOCAL_READ_KHR, {localReadWrite, fragmentRead}, }, Entry{ - VK_IMAGE_LAYOUT_RENDERING_LOCAL_READ_KHR, VK_IMAGE_LAYOUT_PRESENT_SRC_KHR, + VK_IMAGE_LAYOUT_RENDERING_LOCAL_READ_KHR, {colorWrite, none}, }, }; - auto const it = std::ranges::find_if(entries, [oldLayout, newLayout](Entry const& entry) { - return entry.oldLayout == oldLayout && entry.newLayout == newLayout; + auto const it = std::ranges::find_if(entries, [newLayout, oldLayout](Entry const& entry) { + return entry.newLayout == newLayout && entry.oldLayout == oldLayout; }); if(it == entries.end()) return std::nullopt; return it->transition; diff --git a/cvk/tests/src/render/sync/barrier_test.cpp b/cvk/tests/src/render/sync/barrier_test.cpp index 4e002471..853b52a9 100644 --- a/cvk/tests/src/render/sync/barrier_test.cpp +++ b/cvk/tests/src/render/sync/barrier_test.cpp @@ -23,17 +23,17 @@ namespace { } CVK_TEST(Barrier, layout_transition_table) { - constexpr auto toTransfer = layout_transition(VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL); + constexpr auto toTransfer = layout_transition(VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_UNDEFINED); static_assert(toTransfer.has_value()); static_assert(toTransfer->src.stages == VK_PIPELINE_STAGE_2_TRANSFER_BIT); static_assert(toTransfer->src.access == VK_ACCESS_2_NONE); static_assert(toTransfer->dst.access == VK_ACCESS_2_TRANSFER_WRITE_BIT); - constexpr auto toLocalRead = layout_transition(VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_RENDERING_LOCAL_READ_KHR); + constexpr auto toLocalRead = layout_transition(VK_IMAGE_LAYOUT_RENDERING_LOCAL_READ_KHR, VK_IMAGE_LAYOUT_UNDEFINED); static_assert(toLocalRead.has_value()); static_assert((toLocalRead->dst.access & VK_ACCESS_2_INPUT_ATTACHMENT_READ_BIT) != 0); - static_assert(!layout_transition(VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_IMAGE_LAYOUT_UNDEFINED).has_value()); + static_assert(!layout_transition(VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL).has_value()); } CVK_TEST(Barrier, layout_transition_info_covers_the_remaining_mip_levels) { From 7c88e2f0a99f1acd10ece8dd4ca52b3627e877e5 Mon Sep 17 00:00:00 2001 From: lukas <89145039+codethinki@users.noreply.github.com> Date: Tue, 20 Oct 2026 21:40:24 +0200 Subject: [PATCH 080/135] cvk: LayoutTransition takes new, old, dst, src --- cvk/incl/cvk/render/sync/barrier.hpp | 4 ++-- cvk/tests/src/data/descriptor/descriptor_draw_test.cpp | 8 ++++---- cvk/tests/src/render/cmd/draw_test.cpp | 4 ++-- cvk/tests/src/render/cmd/local_read_test.cpp | 4 ++-- cvk/tests/src/render/cmd/rendering_test.cpp | 4 ++-- cvk/tests/src/render/sync/barrier_test.cpp | 10 ++++++---- 6 files changed, 18 insertions(+), 16 deletions(-) diff --git a/cvk/incl/cvk/render/sync/barrier.hpp b/cvk/incl/cvk/render/sync/barrier.hpp index 1b591c45..5de43212 100644 --- a/cvk/incl/cvk/render/sync/barrier.hpp +++ b/cvk/incl/cvk/render/sync/barrier.hpp @@ -65,8 +65,8 @@ struct ImageBarrierInfo { [[nodiscard]] static constexpr ImageBarrierInfo LayoutTransition( VkImageLayout newLayout, VkImageLayout oldLayout, - BarrierScope const& src, BarrierScope const& dst, + BarrierScope const& src, uint32_t firstMipLevel = 0, uint32_t mipLevels = VK_REMAINING_MIP_LEVELS ) { @@ -254,7 +254,7 @@ constexpr void Barrier::addTransition( add( image, - ImageInfo::LayoutTransition(newLayout, oldLayout, transition->src, transition->dst, firstMipLevel, mipLevels) + ImageInfo::LayoutTransition(newLayout, oldLayout, transition->dst, transition->src, firstMipLevel, mipLevels) ); } } diff --git a/cvk/tests/src/data/descriptor/descriptor_draw_test.cpp b/cvk/tests/src/data/descriptor/descriptor_draw_test.cpp index 6b401dea..78519030 100644 --- a/cvk/tests/src/data/descriptor/descriptor_draw_test.cpp +++ b/cvk/tests/src/data/descriptor/descriptor_draw_test.cpp @@ -145,8 +145,8 @@ namespace { ImageBarrierInfo::LayoutTransition( VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, - {VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT, VK_ACCESS_2_COLOR_ATTACHMENT_WRITE_BIT}, - {VK_PIPELINE_STAGE_2_TRANSFER_BIT, VK_ACCESS_2_TRANSFER_READ_BIT} + {VK_PIPELINE_STAGE_2_TRANSFER_BIT, VK_ACCESS_2_TRANSFER_READ_BIT}, + {VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT, VK_ACCESS_2_COLOR_ATTACHMENT_WRITE_BIT} ) ); toTransfer.execute(cmd); @@ -253,8 +253,8 @@ namespace { ImageBarrierInfo::LayoutTransition( textureLayout, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, - {VK_PIPELINE_STAGE_2_TRANSFER_BIT, VK_ACCESS_2_TRANSFER_WRITE_BIT}, - {VK_PIPELINE_STAGE_2_FRAGMENT_SHADER_BIT, VK_ACCESS_2_SHADER_READ_BIT} + {VK_PIPELINE_STAGE_2_FRAGMENT_SHADER_BIT, VK_ACCESS_2_SHADER_READ_BIT}, + {VK_PIPELINE_STAGE_2_TRANSFER_BIT, VK_ACCESS_2_TRANSFER_WRITE_BIT} ) ); toShader.execute(cmd); diff --git a/cvk/tests/src/render/cmd/draw_test.cpp b/cvk/tests/src/render/cmd/draw_test.cpp index 04725021..4702ba67 100644 --- a/cvk/tests/src/render/cmd/draw_test.cpp +++ b/cvk/tests/src/render/cmd/draw_test.cpp @@ -126,8 +126,8 @@ namespace { ImageBarrierInfo::LayoutTransition( VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, - {VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT, VK_ACCESS_2_COLOR_ATTACHMENT_WRITE_BIT}, - {VK_PIPELINE_STAGE_2_TRANSFER_BIT, VK_ACCESS_2_TRANSFER_READ_BIT} + {VK_PIPELINE_STAGE_2_TRANSFER_BIT, VK_ACCESS_2_TRANSFER_READ_BIT}, + {VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT, VK_ACCESS_2_COLOR_ATTACHMENT_WRITE_BIT} ) ); barrier.execute(cmd); diff --git a/cvk/tests/src/render/cmd/local_read_test.cpp b/cvk/tests/src/render/cmd/local_read_test.cpp index 06411b4c..069cac49 100644 --- a/cvk/tests/src/render/cmd/local_read_test.cpp +++ b/cvk/tests/src/render/cmd/local_read_test.cpp @@ -276,8 +276,8 @@ CVK_TEST(LocalRead, draw_reads_an_attachment_of_the_same_rendering) { ImageBarrierInfo::LayoutTransition( VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, - COLOR_WRITE, - {VK_PIPELINE_STAGE_2_TRANSFER_BIT, VK_ACCESS_2_TRANSFER_READ_BIT} + {VK_PIPELINE_STAGE_2_TRANSFER_BIT, VK_ACCESS_2_TRANSFER_READ_BIT}, + COLOR_WRITE ) ); toTransfer.execute(cmd); diff --git a/cvk/tests/src/render/cmd/rendering_test.cpp b/cvk/tests/src/render/cmd/rendering_test.cpp index b6d91e50..b5b0a6ed 100644 --- a/cvk/tests/src/render/cmd/rendering_test.cpp +++ b/cvk/tests/src/render/cmd/rendering_test.cpp @@ -70,8 +70,8 @@ CVK_TEST(Rendering, clear_a_color_attachment) { ImageBarrierInfo::LayoutTransition( VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, - COLOR_WRITE, - {VK_PIPELINE_STAGE_2_TRANSFER_BIT, VK_ACCESS_2_TRANSFER_READ_BIT} + {VK_PIPELINE_STAGE_2_TRANSFER_BIT, VK_ACCESS_2_TRANSFER_READ_BIT}, + COLOR_WRITE ) ); barrier.execute(cmd); diff --git a/cvk/tests/src/render/sync/barrier_test.cpp b/cvk/tests/src/render/sync/barrier_test.cpp index 853b52a9..a783fe00 100644 --- a/cvk/tests/src/render/sync/barrier_test.cpp +++ b/cvk/tests/src/render/sync/barrier_test.cpp @@ -40,14 +40,16 @@ CVK_TEST(Barrier, layout_transition_info_covers_the_remaining_mip_levels) { constexpr auto info = ImageBarrierInfo::LayoutTransition( VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_UNDEFINED, - BarrierScope{}, - TRANSFER_WRITE + TRANSFER_WRITE, + BarrierScope{} ); static_assert(info.firstMipLevel == 0); static_assert(info.mipLevels == VK_REMAINING_MIP_LEVELS); static_assert(info.newLayout == VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL); static_assert(info.oldLayout == VK_IMAGE_LAYOUT_UNDEFINED); + static_assert(info.dst.access == VK_ACCESS_2_TRANSFER_WRITE_BIT); + static_assert(info.src.access == VK_ACCESS_2_NONE); } CVK_TEST(Barrier, queue_transition_info_keeps_the_layout) { @@ -115,8 +117,8 @@ CVK_TEST(Barrier, image_layout_transition) { ImageBarrierInfo::LayoutTransition( VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_UNDEFINED, - {}, - TRANSFER_WRITE + TRANSFER_WRITE, + {} ), }, }, From b2fd0b5dd1dbc6c3910894d8ced0bbce22bd9647 Mon Sep 17 00:00:00 2001 From: lukas <89145039+codethinki@users.noreply.github.com> Date: Tue, 20 Oct 2026 21:42:49 +0200 Subject: [PATCH 081/135] cvk: barrier scope parameters dst first, fields keep the Vulkan order --- AGENTS.md | 2 +- cvk/incl/cvk/render/sync/barrier.hpp | 14 ++++++++------ cvk/tests/src/render/sync/barrier_test.cpp | 17 ++++++++++++----- 3 files changed, 21 insertions(+), 12 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 1bb87ff6..89ee3e2b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -101,7 +101,7 @@ ctest --test-dir out/build/gcc_release --output-on-failure - Initialize Vulkan structs with designated initializers. Always write `.sType` and `.pNext`. - Data structs (`SubmitInfo`, `Barrier`, the info structs) are aggregates: the caller can set everything with designated initializers. Member functions like `add()` are optional shortcuts, checks run where the data is used. - Commands that change the state of a command buffer (binds, dynamic state, push constants, renderings, draws) are members of the command buffer: `cmd.bindPipeline(pipeline)`. Commands on the data of an object (barriers, copies, uploads) are members of the object and take the command buffer: `barrier.execute(cmd)`, `image.copy(cmd, staging)`. -- The target comes first, like `std::memcpy(dst, src, ...)`: a new value before the old one, `layout_transition(newLayout, oldLayout)`. The object of a copy is the destination: `dst.copy(cmd, src)`. +- The target comes first, like `std::memcpy(dst, src, ...)`: new before old, dst before src, `LayoutTransition(newLayout, oldLayout, dst, src)`. The object of a copy is the destination: `dst.copy(cmd, src)`. Exception: `src` and `dst` fields keep the order of the Vulkan struct (`BarrierScope src, dst`, `BufferCopy{srcOffset, dstOffset}`), designated initializers follow it. - Use deducing `this` (`this auto& self`) where a function must work on the derived type. - Qualify `std::` names. A `.cpp` file can declare `using std::span;` and similar at the top of its namespace. - No raw `new` and `delete`. Use `std::unique_ptr`, `cth::move_ptr` and `cth::not_null`. diff --git a/cvk/incl/cvk/render/sync/barrier.hpp b/cvk/incl/cvk/render/sync/barrier.hpp index 5de43212..37188e24 100644 --- a/cvk/incl/cvk/render/sync/barrier.hpp +++ b/cvk/incl/cvk/render/sync/barrier.hpp @@ -81,13 +81,13 @@ struct ImageBarrierInfo { } /** - * @brief queue family ownership transfer without layout transition, src and dst name the families + * @brief queue family ownership transfer without layout transition, dst and src name the families * @param layout of the image */ [[nodiscard]] static constexpr ImageBarrierInfo QueueTransition( VkImageLayout layout, - BarrierScope const& src, - BarrierScope const& dst + BarrierScope const& dst, + BarrierScope const& src ) { return ImageBarrierInfo{.src = src, .dst = dst, .newLayout = layout, .oldLayout = layout}; } @@ -111,9 +111,9 @@ struct BufferBarrierInfo { size_t size = VK_WHOLE_SIZE; /** - * @brief queue family ownership transfer, src and dst name the families + * @brief queue family ownership transfer, dst and src name the families */ - [[nodiscard]] static constexpr BufferBarrierInfo QueueTransition(BarrierScope const& src, BarrierScope const& dst) { + [[nodiscard]] static constexpr BufferBarrierInfo QueueTransition(BarrierScope const& dst, BarrierScope const& src) { return BufferBarrierInfo{.src = src, .dst = dst}; } }; @@ -176,7 +176,9 @@ struct Barrier { /** * @brief adds a global memory barrier */ - constexpr void add(BarrierScope const& src, BarrierScope const& dst) { memory.push_back({src, dst}); } + constexpr void add(BarrierScope const& dst, BarrierScope const& src) { + memory.push_back({.src = src, .dst = dst}); + } /** * @brief adds an image barrier diff --git a/cvk/tests/src/render/sync/barrier_test.cpp b/cvk/tests/src/render/sync/barrier_test.cpp index a783fe00..9c637e4c 100644 --- a/cvk/tests/src/render/sync/barrier_test.cpp +++ b/cvk/tests/src/render/sync/barrier_test.cpp @@ -53,19 +53,26 @@ CVK_TEST(Barrier, layout_transition_info_covers_the_remaining_mip_levels) { } CVK_TEST(Barrier, queue_transition_info_keeps_the_layout) { - constexpr auto info = ImageBarrierInfo::QueueTransition(VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, {}, {}); + constexpr auto info = ImageBarrierInfo::QueueTransition( + VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, + {.queueFamilyIndex = 1}, + {.queueFamilyIndex = 0} + ); static_assert(info.newLayout == VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL); static_assert(info.oldLayout == info.newLayout); + static_assert(info.dst.queueFamilyIndex == 1); + static_assert(info.src.queueFamilyIndex == 0); } CVK_TEST(Barrier, memory_barriers_are_constexpr) { static_assert([] { Barrier barrier{VK_DEPENDENCY_BY_REGION_BIT}; - barrier.add(TRANSFER_WRITE, FRAGMENT_READ); + barrier.add(FRAGMENT_READ, TRANSFER_WRITE); bool const added = !barrier.empty(); + bool const ordered = barrier.memory.front().dst.access == FRAGMENT_READ.access; barrier.clear(); - return added && barrier.empty(); + return added && ordered && barrier.empty(); }()); } @@ -86,7 +93,7 @@ CVK_TEST(Barrier, add_and_clear) { Barrier barrier{}; EXPECT_TRUE(barrier.empty()); - barrier.add(TRANSFER_WRITE, FRAGMENT_READ); + barrier.add(FRAGMENT_READ, TRANSFER_WRITE); barrier.add(buffer, {.src = TRANSFER_WRITE, .dst = FRAGMENT_READ}); EXPECT_FALSE(barrier.empty()); @@ -98,7 +105,7 @@ CVK_TEST(Barrier, execute_memory_and_buffer_barriers) { test::Context const context{}; BaseBuffer const buffer{context.core, {.usage = VK_BUFFER_USAGE_TRANSFER_DST_BIT}, 64}; Barrier barrier{VK_DEPENDENCY_BY_REGION_BIT}; - barrier.add(TRANSFER_WRITE, FRAGMENT_READ); + barrier.add(FRAGMENT_READ, TRANSFER_WRITE); barrier.add(buffer, {.src = TRANSFER_WRITE, .dst = FRAGMENT_READ, .offset = 16, .size = 32}); test::record_and_submit(context, [&](PrimaryCmdBuffer const& primary) { barrier.execute(primary); }); From 541b6739de4b360469ec8e19f22ba2a136526fb6 Mon Sep 17 00:00:00 2001 From: lukas <89145039+codethinki@users.noreply.github.com> Date: Tue, 20 Oct 2026 23:27:16 +0200 Subject: [PATCH 082/135] cvk: Core keeps the queue index per entry, queue() returns the queue of the device --- cvk/incl/cvk/base/core.hpp | 33 ++++++++++++++++++++------------ cvk/src/base/core.cpp | 25 +++++++++++------------- cvk/tests/src/base/core_test.cpp | 26 ++++++++++++++----------- 3 files changed, 47 insertions(+), 37 deletions(-) diff --git a/cvk/incl/cvk/base/core.hpp b/cvk/incl/cvk/base/core.hpp index 269a23a0..cfad8040 100644 --- a/cvk/incl/cvk/base/core.hpp +++ b/cvk/incl/cvk/base/core.hpp @@ -62,7 +62,7 @@ class Core { /** * @brief wraps the state - * @details empty @ref State::queues -> copies the queues of the device, + * @details empty @ref State::queueIndices -> entry i uses queue i of the device, * nullptr @ref State::allocator -> creates the allocator * @post @ref created() * @note calls @ref optDestroy() @@ -130,14 +130,25 @@ class Core { /** * @param idx index in @ref Config::queueProperties + * @return the queue of the device, entries with the same @ref queueIndex() return the same queue * @pre @ref created() */ - [[nodiscard]] constexpr auto& queue(this auto& self, size_t idx) { - debug_check(self); - return self._queues[idx]; + [[nodiscard]] Queue const& queue(size_t idx) const; + + /** + * @param idx index in @ref Config::queueProperties + * @return index of the queue in @ref Device::queues(), entries with the same index share the VkQueue + * @pre @ref created() + */ + [[nodiscard]] constexpr size_t queueIndex(size_t idx) const { + debug_check(*this); + return _queueIndices[idx]; } - [[nodiscard]] constexpr std::span queues() const { return _queues; } + /** + * @return @ref queueIndex() per entry of @ref Config::queueProperties + */ + [[nodiscard]] constexpr std::span queueIndices() const { return _queueIndices; } private: /** @@ -198,9 +209,6 @@ class Core { queue_set_t const& queueSet ) const; - void createQueues(std::span queuesToUniqueQueues); - - not_null_ptr _instance; std::unique_ptr _physicalDevice; @@ -208,7 +216,7 @@ class Core { std::unique_ptr _allocator; std::unique_ptr _device; std::optional _queueSetIndex; - std::vector _queues; + std::vector _queueIndices; public: Core(Core const& other) = delete; @@ -238,10 +246,11 @@ struct Core::State { */ std::unique_ptr allocator; /** - * @brief queue per entry of @ref Config::queueProperties - * @details empty -> the queues of @ref device + * @brief index in @ref Device::queues() per entry of @ref Config::queueProperties + * @details empty -> entry i uses queue i of @ref device + * @pre every index < queue count of @ref device */ - std::vector queues{}; + std::vector queueIndices{}; std::optional queueSetIndex = std::nullopt; }; } diff --git a/cvk/src/base/core.cpp b/cvk/src/base/core.cpp index cad7295d..4b0d7329 100644 --- a/cvk/src/base/core.cpp +++ b/cvk/src/base/core.cpp @@ -40,6 +40,10 @@ void Core::wrap(State state) { "allocator must be created with the device of the state" ) {} } + CTH_CRITICAL( + std::ranges::any_of(state.queueIndices, [&](size_t index) { return index >= state.device->queues().size(); }), + "queue indices must be < queue count of the device" + ) {} optDestroy(); @@ -49,8 +53,8 @@ void Core::wrap(State state) { if(_allocator == nullptr) _allocator = std::make_unique(*_device, cvk::create); _queueSetIndex = state.queueSetIndex; - if(state.queues.empty()) _queues.assign_range(_device->queues()); - else _queues = std::move(state.queues); + if(state.queueIndices.empty()) _queueIndices.assign_range(std::views::iota(0uz, _device->queues().size())); + else _queueIndices = std::move(state.queueIndices); } void Core::create(span surfaces, Config const& config) { @@ -76,8 +80,7 @@ void Core::create(span surfaces, Config const& config) { _device = std::move(device); _allocator = std::move(allocator); _queueSetIndex = pick.queueSetIndex; - - createQueues(pick.queuesToUniqueQueues); + _queueIndices = std::move(pick.queuesToUniqueQueues); } void Core::destroy() { @@ -92,7 +95,7 @@ Core::State Core::release() { .physicalDevice = std::move(_physicalDevice), .device = std::move(_device), .allocator = std::move(_allocator), - .queues = std::move(_queues), + .queueIndices = std::move(_queueIndices), .queueSetIndex = _queueSetIndex, }; forget(); @@ -128,8 +131,10 @@ Allocator const& Core::allocator() const { return *_allocator; } +Queue const& Core::queue(size_t idx) const { return device().queues()[queueIndex(idx)]; } + void Core::forget() { - _queues.clear(); + _queueIndices.clear(); _allocator = nullptr; _device = nullptr; _physicalDevice = nullptr; @@ -221,14 +226,6 @@ auto Core::tryPickPhysicalDevice( }; } -void Core::createQueues(span queuesToUniqueQueues) { - auto const uniqueQueues = _device->queues(); - - _queues.reserve(queuesToUniqueQueues.size()); - for(auto const uniqueQueueIdx : queuesToUniqueQueues) - _queues.push_back(uniqueQueues[uniqueQueueIdx]); -} - Core::Core(Core&& other) noexcept = default; Core& Core::operator=(Core&& other) noexcept = default; } diff --git a/cvk/tests/src/base/core_test.cpp b/cvk/tests/src/base/core_test.cpp index 3b3162fc..64d2f957 100644 --- a/cvk/tests/src/base/core_test.cpp +++ b/cvk/tests/src/base/core_test.cpp @@ -56,7 +56,7 @@ CVK_TEST(Core, create_without_queue_sets) { EXPECT_EQ(core.deviceTable().device(), core.vkDevice()); EXPECT_EQ(&core.device().physicalDevice(), &core.physicalDevice()); - ASSERT_EQ(core.queues().size(), CONFIG.queueProperties.size()); + ASSERT_EQ(core.queueIndices().size(), CONFIG.queueProperties.size()); EXPECT_EQ(core.device().queues().size(), CONFIG.queueProperties.size()); for(size_t i = 0; i < CONFIG.queueProperties.size(); i++) { @@ -75,8 +75,9 @@ CVK_TEST(Core, create_one_queue_per_entry) { Core const core{instance, {}, TWO_QUEUES}; - ASSERT_EQ(core.queues().size(), 2); + ASSERT_EQ(core.queueIndices().size(), 2); EXPECT_EQ(core.device().queues().size(), 2); + EXPECT_NE(core.queueIndex(0), core.queueIndex(1)); EXPECT_NE(core.queue(0).get(), core.queue(1).get()); } @@ -91,7 +92,8 @@ CVK_TEST(Core, queue_set_group_shares_a_queue) { ASSERT_TRUE(core.created()); EXPECT_EQ(core.queueSetIndex(), 0); EXPECT_EQ(core.device().queues().size(), 1); - EXPECT_EQ(core.queue(0).get(), core.queue(1).get()); + EXPECT_EQ(core.queueIndex(0), core.queueIndex(1)); + EXPECT_EQ(&core.queue(0), &core.queue(1)); EXPECT_TRUE(contains(core.queue(0).familyProperties(), QueueFamilyProperties::GRAPHICS)); EXPECT_TRUE(contains(core.queue(0).familyProperties(), QueueFamilyProperties::COMPUTE)); @@ -113,7 +115,7 @@ CVK_TEST(Core, first_possible_queue_set_wins) { ASSERT_TRUE(core.created()); EXPECT_EQ(core.queueSetIndex(), 1); EXPECT_EQ(core.device().queues().size(), 1); - EXPECT_EQ(core.queues().size(), queues); + EXPECT_EQ(core.queueIndices().size(), queues); } CVK_TEST(Core, no_suitable_device_throws) { @@ -134,27 +136,29 @@ CVK_TEST(Core, release_and_wrap) { auto state = core.release(); EXPECT_FALSE(core.created()); - EXPECT_TRUE(core.queues().empty()); + EXPECT_TRUE(core.queueIndices().empty()); Core const wrapped{instance, std::move(state)}; ASSERT_TRUE(wrapped.created()); EXPECT_EQ(wrapped.vkDevice(), vkDevice); EXPECT_EQ(wrapped.allocator().get(), vmaAllocator); - ASSERT_EQ(wrapped.queues().size(), CONFIG.queueProperties.size()); + ASSERT_EQ(wrapped.queueIndices().size(), CONFIG.queueProperties.size()); wrapped.queue(0).waitIdle(); } -CVK_TEST(Core, wrap_without_queues_takes_the_device_queues) { +CVK_TEST(Core, wrap_without_queue_indices_takes_the_device_queues) { Instance const instance{test::instance_config(), create}; Core core{instance, {}, CONFIG}; auto state = core.release(); - state.queues.clear(); + state.queueIndices.clear(); Core const wrapped{instance, std::move(state)}; - EXPECT_EQ(wrapped.queues().size(), wrapped.device().queues().size()); + ASSERT_EQ(wrapped.queueIndices().size(), wrapped.device().queues().size()); + for(size_t i = 0; i < wrapped.queueIndices().size(); i++) + EXPECT_EQ(&wrapped.queue(i), &wrapped.device().queues()[i]); } CVK_TEST(Core, wrap_without_allocator_creates_one) { @@ -178,8 +182,8 @@ CVK_TEST(Core, queues_work_after_move) { EXPECT_FALSE(core.created()); ASSERT_TRUE(moved.created()); - for(auto const& queue : moved.queues()) - queue.waitIdle(); + for(size_t i = 0; i < moved.queueIndices().size(); i++) + moved.queue(i).waitIdle(); } CVK_TEST(Core, present_queue_with_headless_surface) { From 5d0d51b1a8ddc254cfe4417a1c249b1cec00fbca Mon Sep 17 00:00:00 2001 From: lukas <89145039+codethinki@users.noreply.github.com> Date: Tue, 20 Oct 2026 23:29:49 +0200 Subject: [PATCH 083/135] cvk: no queue mutexes, the caller synchronizes the queues --- cvk/incl/cvk/base/device.hpp | 19 ++---------- cvk/incl/cvk/base/queue/queue.hpp | 12 ++------ cvk/incl/cvk/base/queue/queue_state.hpp | 16 ---------- cvk/src/base/device.cpp | 32 +++----------------- cvk/src/base/queue/queue.cpp | 39 +++++++------------------ cvk/tests/src/base/queue/queue_test.cpp | 18 +----------- 6 files changed, 19 insertions(+), 117 deletions(-) diff --git a/cvk/incl/cvk/base/device.hpp b/cvk/incl/cvk/base/device.hpp index ccc7db88..734db803 100644 --- a/cvk/incl/cvk/base/device.hpp +++ b/cvk/incl/cvk/base/device.hpp @@ -11,7 +11,6 @@ #include #include -#include #include #include #include @@ -64,9 +63,7 @@ class Device { /** * @brief wraps the state and retrieves the queues of @ref State::queueFamilyIndices * @details nullptr @ref State::functionTable -> loads the functions - * @details empty @ref State::queueMutexes -> creates a mutex per queue * @pre the device has the queues of @ref State::queueFamilyIndices - * @pre empty @ref State::queueMutexes or one per queue * @post @ref created() * @note calls @ref optDestroy() */ @@ -100,8 +97,8 @@ class Device { /** * @brief blocks until every queue of the device finished its work - * @details locks the mutexes of every queue, Vulkan requires it * @pre @ref created() + * @pre no other thread uses a queue of the device, Vulkan requires it * @throws cvk::vk_result_exception result of vkDeviceWaitIdle() */ void waitIdle() const; @@ -151,14 +148,9 @@ class Device { /** * @brief retrieves the queues * @param familyIndices family index of each queue - * @param mutexes mutex of each queue, empty -> creates one per queue * @pre the device has the queues - * @pre empty mutexes or one per queue */ - void createQueues( - std::span familyIndices, - std::span const> mutexes = {} - ); + void createQueues(std::span familyIndices); not_null_ptr _instance; @@ -196,13 +188,6 @@ struct Device::State { */ std::vector queueFamilyIndices{}; - /** - * @brief mutex of each queue, the copies of a released queue keep locking the same mutex - * @details empty -> @ref wrap() creates a mutex per queue - * @pre empty or one per entry of @ref queueFamilyIndices - */ - std::vector> queueMutexes{}; - /** * @brief the features that the device enables */ diff --git a/cvk/incl/cvk/base/queue/queue.hpp b/cvk/incl/cvk/base/queue/queue.hpp index 4f9b2adf..21bb2da4 100644 --- a/cvk/incl/cvk/base/queue/queue.hpp +++ b/cvk/incl/cvk/base/queue/queue.hpp @@ -7,8 +7,6 @@ #include -#include -#include #include @@ -17,8 +15,9 @@ class Fence; /** * @brief VkQueue of a device - * @details copyable, thread safe: copies share the mutex of the vulkan queue, + * @details copyable, copies share the vulkan queue, * the device owns the vulkan queue, a queue has no lifecycle + * @attention not thread safe: one thread at a time per vulkan queue, Vulkan requires it */ class Queue { public: @@ -76,18 +75,11 @@ class Queue { [[nodiscard]] constexpr uint32_t familyIndex() const { return _familyIndex; } [[nodiscard]] constexpr QueueFamilyProperties familyProperties() const { return _familyProperties; } - /** - * @brief guards the vulkan queue, shared by every copy - * @details code outside of cvk that uses @ref get() locks it - */ - [[nodiscard]] constexpr std::shared_ptr const& mutex() const { return _handleMtx; } - private: QueueFamilyProperties _familyProperties; VolkDeviceTable const* _functions = nullptr; move_ptr _handle = VK_NULL_HANDLE; - std::shared_ptr _handleMtx{}; uint32_t _familyIndex = 0; uint32_t _queueIndex = 0; diff --git a/cvk/incl/cvk/base/queue/queue_state.hpp b/cvk/incl/cvk/base/queue/queue_state.hpp index 12234c76..a7d87585 100644 --- a/cvk/incl/cvk/base/queue/queue_state.hpp +++ b/cvk/incl/cvk/base/queue/queue_state.hpp @@ -3,11 +3,6 @@ #include -#include - -#include -#include - namespace cvk { struct QueueState { /** @@ -24,16 +19,5 @@ struct QueueState { * @brief index in the family */ uint32_t queueIndex; - - /** - * @brief guards the vulkan queue, shared by every copy of the queue - * @pre not nullptr - */ - std::shared_ptr vkQueueMtx; - - - static constexpr void debug_check(QueueState const& state) { - CTH_CRITICAL(state.vkQueueMtx == nullptr, "mutex must not be nullptr") {} - } }; } diff --git a/cvk/src/base/device.cpp b/cvk/src/base/device.cpp index 807f29f2..71130b68 100644 --- a/cvk/src/base/device.cpp +++ b/cvk/src/base/device.cpp @@ -9,7 +9,6 @@ #include #include -#include #include #include @@ -47,7 +46,7 @@ void Device::wrap(State state) { _handle.reset(state.vkDevice.get()); _features = state.features; _extensions = std::move(state.extensions); - createQueues(state.queueFamilyIndices, state.queueMutexes); + createQueues(state.queueFamilyIndices); } void Device::create(Config const& config) { @@ -80,7 +79,6 @@ Device::State Device::release() { .vkDevice = _handle.release(), .functionTable = std::move(_functionTable), .queueFamilyIndices = {std::from_range, _queues | std::views::transform(&Queue::familyIndex)}, - .queueMutexes = {std::from_range, _queues | std::views::transform(&Queue::mutex)}, .features = _features, .extensions = std::move(_extensions), }; @@ -91,19 +89,6 @@ Device::State Device::release() { void Device::waitIdle() const { debug_check(*this); - // one lock per mutex, in address order: no deadlock with another waitIdle() - std::vector mutexes{std::from_range, _queues | std::views::transform([](Queue const& queue) { - return queue.mutex().get(); - })}; - std::ranges::sort(mutexes); - auto const [end, _] = std::ranges::unique(mutexes); - mutexes.erase(end, mutexes.end()); - - std::vector> locks{}; - locks.reserve(mutexes.size()); - for(auto* const mutex : mutexes) - locks.emplace_back(*mutex); - auto const result = functions()->vkDeviceWaitIdle(_handle.get()); CVK_RESULT_STABLE_THROW(result != VK_SUCCESS, result, "failed to wait for device") {} } @@ -188,20 +173,12 @@ void Device::createLogicalDevice(queue_families_queue_counts_t const& queueFamil void Device::loadFunctionTable(VkDevice vkDevice) const { volkLoadDeviceTable(_functionTable.get(), vkDevice); } -void Device::createQueues( - span familyIndices, - span const> mutexes -) { - CTH_CRITICAL( - !mutexes.empty() && mutexes.size() != familyIndices.size(), - "one mutex per queue or no mutexes" - ) {} - +void Device::createQueues(span familyIndices) { std::unordered_map queueCounts{}; _queues.reserve(familyIndices.size()); - for(auto const [i, familyIndex] : std::views::enumerate(familyIndices)) { + for(auto const familyIndex : familyIndices) { auto& queueIndex = queueCounts[familyIndex]; VkQueue ptr = VK_NULL_HANDLE; @@ -210,10 +187,9 @@ void Device::createQueues( // vkGetDeviceQueue() has no failure, a null queue means the device has no such queue CTH_CRITICAL(ptr == VK_NULL_HANDLE, "the device has no queue {} in family {}", queueIndex, familyIndex) {} - auto mutex = mutexes.empty() ? std::make_shared() : mutexes[static_cast(i)]; _queues.emplace_back( _physicalDevice->queueFamily(familyIndex)->properties, - Queue::State{_functionTable.get(), ptr, familyIndex, queueIndex++, std::move(mutex)} + Queue::State{_functionTable.get(), ptr, familyIndex, queueIndex++} ); } } diff --git a/cvk/src/base/queue/queue.cpp b/cvk/src/base/queue/queue.cpp index 804208ec..43243952 100644 --- a/cvk/src/base/queue/queue.cpp +++ b/cvk/src/base/queue/queue.cpp @@ -51,11 +51,8 @@ namespace { Queue::Queue(QueueFamilyProperties familyProperties, State const& state) : _familyProperties{familyProperties}, _functions{state.functions.get()}, _handle{state.vkQueue.get()}, - _handleMtx{state.vkQueueMtx}, _familyIndex{state.familyIndex}, - _queueIndex{state.queueIndex} { - State::debug_check(state); -} + _queueIndex{state.queueIndex} {} void Queue::submit(span infos, Fence const* fence) const { debug_check(*this); @@ -118,16 +115,12 @@ void Queue::submit(span infos, Fence const* fence) const { ); } - VkResult result; - { - std::scoped_lock const lock{*_handleMtx}; - result = _functions->vkQueueSubmit2( - _handle.get(), - static_cast(submits.size()), - submits.data(), - fence != nullptr ? fence->get() : VK_NULL_HANDLE - ); - } + auto const result = _functions->vkQueueSubmit2( + _handle.get(), + static_cast(submits.size()), + submits.data(), + fence != nullptr ? fence->get() : VK_NULL_HANDLE + ); CVK_RESULT_STABLE_THROW(result != VK_SUCCESS, result, "failed to submit to queue") {} } @@ -135,11 +128,7 @@ void Queue::submit(span infos, Fence const* fence) const { void Queue::rawSubmit(VkSubmitInfo2 const& submitInfo, VkFence fence) const { debug_check(*this); - VkResult result; - { - std::scoped_lock const lock{*_handleMtx}; - result = _functions->vkQueueSubmit2(_handle.get(), 1, &submitInfo, fence); - } + auto const result = _functions->vkQueueSubmit2(_handle.get(), 1, &submitInfo, fence); CVK_RESULT_STABLE_THROW(result != VK_SUCCESS, result, "failed to submit to queue") {} } @@ -147,11 +136,7 @@ void Queue::rawSubmit(VkSubmitInfo2 const& submitInfo, VkFence fence) const { VkResult Queue::rawPresent(VkPresentInfoKHR const& presentInfo) const { debug_check_present(*this); - VkResult result; - { - std::scoped_lock const lock{*_handleMtx}; - result = _functions->vkQueuePresentKHR(_handle.get(), &presentInfo); - } + auto const result = _functions->vkQueuePresentKHR(_handle.get(), &presentInfo); CVK_RESULT_STABLE_THROW( result != VK_SUCCESS && result != VK_SUBOPTIMAL_KHR && result != VK_ERROR_OUT_OF_DATE_KHR, @@ -165,11 +150,7 @@ VkResult Queue::rawPresent(VkPresentInfoKHR const& presentInfo) const { void Queue::waitIdle() const { debug_check(*this); - VkResult result; - { - std::scoped_lock const lock{*_handleMtx}; - result = _functions->vkQueueWaitIdle(_handle.get()); - } + auto const result = _functions->vkQueueWaitIdle(_handle.get()); CVK_RESULT_STABLE_THROW(result != VK_SUCCESS, result, "failed to wait for queue") {} } diff --git a/cvk/tests/src/base/queue/queue_test.cpp b/cvk/tests/src/base/queue/queue_test.cpp index 8d07b671..e0934e1d 100644 --- a/cvk/tests/src/base/queue/queue_test.cpp +++ b/cvk/tests/src/base/queue/queue_test.cpp @@ -35,7 +35,6 @@ CVK_TEST(Queue, copies_share_the_queue) { Queue const copy{queue}; EXPECT_EQ(copy.get(), queue.get()); - EXPECT_EQ(copy.mutex(), queue.mutex()); EXPECT_EQ(copy.familyIndex(), queue.familyIndex()); EXPECT_EQ(copy.index(), queue.index()); copy.waitIdle(); @@ -69,7 +68,7 @@ CVK_TEST(Queue, raw_submit_without_fence) { queue.waitIdle(); } -CVK_TEST(Queue, copies_keep_their_mutex_through_a_device_release_and_wrap) { +CVK_TEST(Queue, copies_keep_the_queue_through_a_device_release_and_wrap) { Instance const instance{test::instance_config(), create}; auto const [physicalDevice, familyIndices] = test::pick(instance, QUEUES); Device device{instance, physicalDevice, Device::Config{familyIndices}}; @@ -79,24 +78,9 @@ CVK_TEST(Queue, copies_keep_their_mutex_through_a_device_release_and_wrap) { ASSERT_EQ(wrapped.queues().size(), 1); EXPECT_EQ(wrapped.queues()[0].get(), copy.get()); - EXPECT_EQ(wrapped.queues()[0].mutex(), copy.mutex()); copy.waitIdle(); } -CVK_TEST(Queue, wrap_without_mutexes_creates_them) { - Instance const instance{test::instance_config(), create}; - auto const [physicalDevice, familyIndices] = test::pick(instance, QUEUES); - Device device{instance, physicalDevice, Device::Config{familyIndices}}; - - auto state = device.release(); - state.queueMutexes.clear(); - Device const wrapped{instance, physicalDevice, std::move(state)}; - - ASSERT_EQ(wrapped.queues().size(), 1); - EXPECT_NE(wrapped.queues()[0].mutex(), nullptr); - wrapped.waitIdle(); -} - CVK_TEST(Queue, present_family_can_present) { Instance const instance{test::instance_config(test::headless_extensions()), create}; std::array const surfaces{test::headless_surface(instance)}; From c581e3966316723ca35896b9763ac704460e2a31 Mon Sep 17 00:00:00 2001 From: lukas <89145039+codethinki@users.noreply.github.com> Date: Tue, 20 Oct 2026 23:31:09 +0200 Subject: [PATCH 084/135] cvk: timeline counter without atomic, default move, signal order in the docs --- .../cvk/render/sync/timeline_semaphore.hpp | 20 ++++++++----------- cvk/src/render/sync/timeline_semaphore.cpp | 3 ++- .../render/sync/timeline_semaphore_test.cpp | 1 - 3 files changed, 10 insertions(+), 14 deletions(-) diff --git a/cvk/incl/cvk/render/sync/timeline_semaphore.hpp b/cvk/incl/cvk/render/sync/timeline_semaphore.hpp index de1774c7..f7704b82 100644 --- a/cvk/incl/cvk/render/sync/timeline_semaphore.hpp +++ b/cvk/incl/cvk/render/sync/timeline_semaphore.hpp @@ -1,7 +1,6 @@ #pragma once #include "semaphore.hpp" -#include #include @@ -9,8 +8,8 @@ namespace cvk { /** * @brief timeline VkSemaphore with a host counter * @details the counter is the last value that @ref next() or @ref signal() gave out, - * private base: a timeline semaphore must not pass as a binary @ref Semaphore, - * move members are custom: the counter is atomic + * private base: a timeline semaphore must not pass as a binary @ref Semaphore + * @attention not thread safe: @ref next() and @ref signal() change the counter */ class TimelineSemaphore : Semaphore { public: @@ -73,6 +72,7 @@ class TimelineSemaphore : Semaphore { * @brief advances the counter and signals the semaphore from the host * @return the signaled value * @pre @ref created() + * @pre no signal of the semaphore is pending * @throws cvk::vk_result_exception result of vkSignalSemaphore() */ uint64_t signal(); @@ -96,7 +96,8 @@ class TimelineSemaphore : Semaphore { /** * @brief advances the counter * @return value for the next signal operation - * @details thread safe + * @attention signal the values in counter order: one queue keeps the order, + * two queues need a wait between their submits */ [[nodiscard]] constexpr uint64_t next() const { return ++_value; } @@ -117,18 +118,13 @@ class TimelineSemaphore : Semaphore { using Semaphore::get; private: - mutable std::atomic _value = 0; + mutable uint64_t _value = 0; public: TimelineSemaphore(TimelineSemaphore const& other) = delete; - TimelineSemaphore(TimelineSemaphore&& other) noexcept : Semaphore{std::move(other)}, - _value{other._value.exchange(0)} {} + TimelineSemaphore(TimelineSemaphore&& other) noexcept = default; TimelineSemaphore& operator=(TimelineSemaphore const& other) = delete; - TimelineSemaphore& operator=(TimelineSemaphore&& other) noexcept { - Semaphore::operator=(std::move(other)); - _value = other._value.exchange(0); - return *this; - } + TimelineSemaphore& operator=(TimelineSemaphore&& other) noexcept = default; static constexpr void debug_check(TimelineSemaphore const& semaphore); }; diff --git a/cvk/src/render/sync/timeline_semaphore.cpp b/cvk/src/render/sync/timeline_semaphore.cpp index 123f1bc1..26e2b158 100644 --- a/cvk/src/render/sync/timeline_semaphore.cpp +++ b/cvk/src/render/sync/timeline_semaphore.cpp @@ -4,6 +4,7 @@ #include "cvk/utility/vk_exceptions.hpp" #include +#include namespace cvk { @@ -39,7 +40,7 @@ void TimelineSemaphore::create(uint64_t initialValue) { TimelineSemaphore::State TimelineSemaphore::release() { auto const [vkSemaphore] = Semaphore::release(); - return State{vkSemaphore, _value.exchange(0)}; + return State{vkSemaphore, std::exchange(_value, 0)}; } uint64_t TimelineSemaphore::signal() { diff --git a/cvk/tests/src/render/sync/timeline_semaphore_test.cpp b/cvk/tests/src/render/sync/timeline_semaphore_test.cpp index ed58fb5c..6d16790b 100644 --- a/cvk/tests/src/render/sync/timeline_semaphore_test.cpp +++ b/cvk/tests/src/render/sync/timeline_semaphore_test.cpp @@ -101,7 +101,6 @@ CVK_TEST(TimelineSemaphore, move_keeps_the_counter) { TimelineSemaphore const moved{std::move(semaphore)}; EXPECT_FALSE(semaphore.created()); - EXPECT_EQ(semaphore.value(), 0); EXPECT_EQ(moved.value(), 3); EXPECT_EQ(moved.gpuValue(), 3); } From 7aa6924cb5d4c6220d8b4711503521603cba5db9 Mon Sep 17 00:00:00 2001 From: lukas <89145039+codethinki@users.noreply.github.com> Date: Tue, 20 Oct 2026 23:50:55 +0200 Subject: [PATCH 085/135] cvk: swapchain keeps a copy of the present queue --- cvk/incl/cvk/surface/swapchain.hpp | 4 ++-- cvk/src/surface/swapchain.cpp | 5 ++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/cvk/incl/cvk/surface/swapchain.hpp b/cvk/incl/cvk/surface/swapchain.hpp index 9ab09b84..7769943e 100644 --- a/cvk/incl/cvk/surface/swapchain.hpp +++ b/cvk/incl/cvk/surface/swapchain.hpp @@ -2,6 +2,7 @@ #include "swapchain_config.hpp" #include "cvk/base/device_table.hpp" +#include "cvk/base/queue/queue.hpp" #include "cvk/data/img/image.hpp" #include "cvk/data/img/image_view.hpp" #include "cvk/render/sync/fence.hpp" @@ -22,7 +23,6 @@ namespace cvk { class Core; -class Queue; class Surface; /** @@ -253,7 +253,7 @@ class Swapchain { void forget(); - not_null_ptr _presentQueue; + Queue _presentQueue; not_null_ptr _surface; Config _config; diff --git a/cvk/src/surface/swapchain.cpp b/cvk/src/surface/swapchain.cpp index 40015f44..79a7c9e6 100644 --- a/cvk/src/surface/swapchain.cpp +++ b/cvk/src/surface/swapchain.cpp @@ -2,7 +2,6 @@ #include "cvk/base/core.hpp" #include "cvk/base/physical_device.hpp" -#include "cvk/base/queue/queue.hpp" #include "cvk/surface/surface.hpp" #include "cvk/utility/vk_exceptions.hpp" @@ -14,7 +13,7 @@ using std::span; using std::vector; Swapchain::Swapchain(Core const& core, Queue const& presentQueue, Surface const& surface, Config config) : - _presentQueue{&presentQueue}, _surface{&surface}, _config{std::move(config)}, _handle{core} { + _presentQueue{presentQueue}, _surface{&surface}, _config{std::move(config)}, _handle{core} { Core::debug_check(core); Surface::debug_check(surface); Queue::debug_check_present(presentQueue); @@ -174,7 +173,7 @@ VkResult Swapchain::present(uint32_t imageIndex) { .pResults = nullptr, }; - auto const result = _presentQueue->rawPresent(info); + auto const result = _presentQueue.rawPresent(info); _acquired[imageIndex] = false; return result; } From 7262ef1757541c4b4c976b8ce35269df0c4a5919 Mon Sep 17 00:00:00 2001 From: lukas <89145039+codethinki@users.noreply.github.com> Date: Wed, 21 Oct 2026 08:22:06 +0200 Subject: [PATCH 086/135] cvk: one barrier scope per layout instead of the transition table, shader stages override --- cvk/incl/cvk/render/sync/barrier.hpp | 260 ++++++++++---------- cvk/src/render/sync/barrier.cpp | 19 ++ cvk/tests/src/render/cmd/draw_test.cpp | 10 +- cvk/tests/src/render/cmd/rendering_test.cpp | 25 ++ cvk/tests/src/render/sync/barrier_test.cpp | 130 +++++++++- 5 files changed, 301 insertions(+), 143 deletions(-) diff --git a/cvk/incl/cvk/render/sync/barrier.hpp b/cvk/incl/cvk/render/sync/barrier.hpp index 37188e24..1acf5913 100644 --- a/cvk/incl/cvk/render/sync/barrier.hpp +++ b/cvk/incl/cvk/render/sync/barrier.hpp @@ -8,8 +8,6 @@ #include -#include -#include #include #include #include @@ -118,23 +116,6 @@ struct BufferBarrierInfo { } }; -/** - * @brief scopes of a common layout transition - */ -struct LayoutTransition { - BarrierScope src; - BarrierScope dst; -}; - -/** - * @return the scopes of the transition, no common transition -> std::nullopt - */ -[[nodiscard]] constexpr std::optional layout_transition( - VkImageLayout newLayout, - VkImageLayout oldLayout -); - - /** * @brief global memory part of a barrier */ @@ -154,6 +135,11 @@ struct Barrier { using ImageInfo = ImageBarrierInfo; using BufferInfo = BufferBarrierInfo; + /** + * @brief shader stages of @ref addTransition(): the layout decides, fragment shader for shader reads + */ + static constexpr VkPipelineStageFlags2 INFERRED_SHADER_STAGES = VK_PIPELINE_STAGE_2_NONE; + struct BufferEntry { not_null_ptr buffer; BufferInfo info; @@ -187,13 +173,15 @@ struct Barrier { /** * @brief adds a layout transition with the scopes of @ref layout_transition() + * @param shaderStages stages of the shader reads in the layouts (@ref INFERRED_SHADER_STAGES => fragment shader) * @param mipLevels (VK_REMAINING_MIP_LEVELS => to the last mip level) - * @pre @ref layout_transition() knows the transition + * @pre @ref layout_scope() knows both layouts */ - constexpr void addTransition( + void addTransition( Image const&, VkImageLayout newLayout, VkImageLayout oldLayout, + VkPipelineStageFlags2 shaderStages = INFERRED_SHADER_STAGES, uint32_t firstMipLevel = 0, uint32_t mipLevels = VK_REMAINING_MIP_LEVELS ); @@ -242,127 +230,143 @@ struct Barrier { } namespace cvk { -constexpr void Barrier::addTransition( - Image const& image, +/** + * @brief scopes of a layout transition + */ +struct LayoutTransition { + BarrierScope src; + BarrierScope dst; +}; + +/** + * @brief what the gpu does with an image in the layout + * @details input attachments: fragment shader only, general: all commands + * @param aspects of the image, pick color or depth for the attachment, read only and local read layouts + * @param shaderStages stages of the shader reads (@ref Barrier::INFERRED_SHADER_STAGES => fragment shader) + * @return std::nullopt -> no scope for the layout: undefined, preinitialized or not known + * @pre shaderStages has only shader stages + */ +[[nodiscard]] constexpr std::optional layout_scope( + VkImageLayout, + VkImageAspectFlags aspects, + VkPipelineStageFlags2 shaderStages = Barrier::INFERRED_SHADER_STAGES +); + +/** + * @brief scopes of the transition: src from the old layout, dst from the new layout + * @details src keeps the writes of the old layout, + * old layout undefined, preinitialized or present -> src waits for the dst stages without access + * @return std::nullopt -> no scope for a layout + * @pre shaderStages has only shader stages + */ +[[nodiscard]] constexpr std::optional layout_transition( VkImageLayout newLayout, VkImageLayout oldLayout, - uint32_t firstMipLevel, - uint32_t mipLevels -) { - auto const transition = layout_transition(newLayout, oldLayout); - CTH_CRITICAL(!transition.has_value(), "layout transition has no table entry, use add() with scopes") { - details->add("transition: {} -> {}", oldLayout, newLayout); - } - - add( - image, - ImageInfo::LayoutTransition(newLayout, oldLayout, transition->dst, transition->src, firstMipLevel, mipLevels) - ); -} + VkImageAspectFlags aspects, + VkPipelineStageFlags2 shaderStages = Barrier::INFERRED_SHADER_STAGES +); } namespace cvk { -constexpr std::optional layout_transition(VkImageLayout newLayout, VkImageLayout oldLayout) { - struct Entry { - VkImageLayout newLayout; - VkImageLayout oldLayout; - LayoutTransition transition; - }; +constexpr std::optional layout_scope( + VkImageLayout layout, + VkImageAspectFlags aspects, + VkPipelineStageFlags2 shaderStages +) { + constexpr VkPipelineStageFlags2 shaderStageMask = VK_PIPELINE_STAGE_2_VERTEX_SHADER_BIT + | VK_PIPELINE_STAGE_2_TESSELLATION_CONTROL_SHADER_BIT + | VK_PIPELINE_STAGE_2_TESSELLATION_EVALUATION_SHADER_BIT + | VK_PIPELINE_STAGE_2_GEOMETRY_SHADER_BIT + | VK_PIPELINE_STAGE_2_FRAGMENT_SHADER_BIT + | VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT + | VK_PIPELINE_STAGE_2_PRE_RASTERIZATION_SHADERS_BIT + | VK_PIPELINE_STAGE_2_TASK_SHADER_BIT_EXT + | VK_PIPELINE_STAGE_2_MESH_SHADER_BIT_EXT + | VK_PIPELINE_STAGE_2_RAY_TRACING_SHADER_BIT_KHR; + CTH_CRITICAL((shaderStages & ~shaderStageMask) != 0, "shader stages must only have shader stage bits") { + details->add("shader stages: {:#x}", shaderStages); + } - constexpr BarrierScope none{}; - constexpr BarrierScope transferWrite{VK_PIPELINE_STAGE_2_TRANSFER_BIT, VK_ACCESS_2_TRANSFER_WRITE_BIT}; - constexpr BarrierScope transferRead{VK_PIPELINE_STAGE_2_TRANSFER_BIT, VK_ACCESS_2_TRANSFER_READ_BIT}; - constexpr BarrierScope fragmentRead{VK_PIPELINE_STAGE_2_FRAGMENT_SHADER_BIT, VK_ACCESS_2_SHADER_READ_BIT}; - constexpr BarrierScope colorWrite{ - VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT, - VK_ACCESS_2_COLOR_ATTACHMENT_WRITE_BIT - }; - constexpr BarrierScope color{ + auto const shaders = shaderStages != Barrier::INFERRED_SHADER_STAGES + ? shaderStages + : VK_PIPELINE_STAGE_2_FRAGMENT_SHADER_BIT; + bool const color = (aspects & VK_IMAGE_ASPECT_COLOR_BIT) != 0; + + constexpr VkPipelineStageFlags2 tests = VK_PIPELINE_STAGE_2_EARLY_FRAGMENT_TESTS_BIT + | VK_PIPELINE_STAGE_2_LATE_FRAGMENT_TESTS_BIT; + constexpr BarrierScope colorAttachment{ VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT, VK_ACCESS_2_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_2_COLOR_ATTACHMENT_WRITE_BIT }; - constexpr BarrierScope depth{ - VK_PIPELINE_STAGE_2_EARLY_FRAGMENT_TESTS_BIT | VK_PIPELINE_STAGE_2_LATE_FRAGMENT_TESTS_BIT, + constexpr BarrierScope depthAttachment{ + tests, VK_ACCESS_2_DEPTH_STENCIL_ATTACHMENT_READ_BIT | VK_ACCESS_2_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT }; - // attachments that the rendering also reads as input attachments - constexpr BarrierScope localRead{ - color.stages | depth.stages | VK_PIPELINE_STAGE_2_FRAGMENT_SHADER_BIT, - color.access | depth.access | VK_ACCESS_2_INPUT_ATTACHMENT_READ_BIT + BarrierScope const shaderRead{shaders, VK_ACCESS_2_SHADER_READ_BIT}; + BarrierScope const depthRead{ + tests | shaders, + VK_ACCESS_2_DEPTH_STENCIL_ATTACHMENT_READ_BIT | VK_ACCESS_2_SHADER_READ_BIT }; - constexpr BarrierScope localReadWrite{ - color.stages | VK_PIPELINE_STAGE_2_LATE_FRAGMENT_TESTS_BIT, - VK_ACCESS_2_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_2_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT + // attachments that the rendering also reads as input attachments + constexpr auto localRead = [](BarrierScope const& attachment) { + return BarrierScope{ + attachment.stages | VK_PIPELINE_STAGE_2_FRAGMENT_SHADER_BIT, + attachment.access | VK_ACCESS_2_INPUT_ATTACHMENT_READ_BIT + }; }; - // from VK_IMAGE_LAYOUT_UNDEFINED: src waits for the dst stages without access, - // orders the transition after their last use and after an acquire semaphore wait on these stages - constexpr auto stagesOf = [](BarrierScope const& scope) { return BarrierScope{scope.stages}; }; - - constexpr std::array entries{ - Entry{ - VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, - VK_IMAGE_LAYOUT_UNDEFINED, - {stagesOf(transferWrite), transferWrite}, - }, - Entry{ - VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, - VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, - {transferWrite, fragmentRead}, - }, - Entry{ - VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, - VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, - {transferWrite, transferRead}, - }, - Entry{ - VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, - VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, - {transferRead, fragmentRead}, - }, - - Entry{ - VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, - VK_IMAGE_LAYOUT_UNDEFINED, - {stagesOf(color), color}, - }, - Entry{ - VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_OPTIMAL, - VK_IMAGE_LAYOUT_UNDEFINED, - {stagesOf(depth), depth}, - }, - Entry{ - VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, - VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, - {colorWrite, fragmentRead}, - }, - Entry{ - VK_IMAGE_LAYOUT_PRESENT_SRC_KHR, - VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, - {colorWrite, none}, - }, - - Entry{ - VK_IMAGE_LAYOUT_RENDERING_LOCAL_READ_KHR, - VK_IMAGE_LAYOUT_UNDEFINED, - {stagesOf(localRead), localRead}, - }, - Entry{ - VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, - VK_IMAGE_LAYOUT_RENDERING_LOCAL_READ_KHR, - {localReadWrite, fragmentRead}, - }, - Entry{ - VK_IMAGE_LAYOUT_PRESENT_SRC_KHR, - VK_IMAGE_LAYOUT_RENDERING_LOCAL_READ_KHR, - {colorWrite, none}, - }, - }; + switch(layout) { + case VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL: + return BarrierScope{VK_PIPELINE_STAGE_2_TRANSFER_BIT, VK_ACCESS_2_TRANSFER_READ_BIT}; + case VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL: + return BarrierScope{VK_PIPELINE_STAGE_2_TRANSFER_BIT, VK_ACCESS_2_TRANSFER_WRITE_BIT}; + case VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL: return shaderRead; + case VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL: return colorAttachment; + case VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_OPTIMAL: + case VK_IMAGE_LAYOUT_STENCIL_ATTACHMENT_OPTIMAL: + case VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL: + case VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_STENCIL_READ_ONLY_OPTIMAL: + case VK_IMAGE_LAYOUT_DEPTH_READ_ONLY_STENCIL_ATTACHMENT_OPTIMAL: return depthAttachment; + case VK_IMAGE_LAYOUT_DEPTH_READ_ONLY_OPTIMAL: + case VK_IMAGE_LAYOUT_STENCIL_READ_ONLY_OPTIMAL: + case VK_IMAGE_LAYOUT_DEPTH_STENCIL_READ_ONLY_OPTIMAL: return depthRead; + case VK_IMAGE_LAYOUT_ATTACHMENT_OPTIMAL: return color ? colorAttachment : depthAttachment; + case VK_IMAGE_LAYOUT_READ_ONLY_OPTIMAL: return color ? shaderRead : depthRead; + case VK_IMAGE_LAYOUT_RENDERING_LOCAL_READ_KHR: return localRead(color ? colorAttachment : depthAttachment); + case VK_IMAGE_LAYOUT_GENERAL: + return BarrierScope{ + VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT, + VK_ACCESS_2_MEMORY_READ_BIT | VK_ACCESS_2_MEMORY_WRITE_BIT + }; + case VK_IMAGE_LAYOUT_PRESENT_SRC_KHR: return BarrierScope{}; + default: return std::nullopt; + } +} + +constexpr std::optional layout_transition( + VkImageLayout newLayout, + VkImageLayout oldLayout, + VkImageAspectFlags aspects, + VkPipelineStageFlags2 shaderStages +) { + auto const dst = layout_scope(newLayout, aspects, shaderStages); + if(!dst.has_value()) return std::nullopt; - auto const it = std::ranges::find_if(entries, [newLayout, oldLayout](Entry const& entry) { - return entry.newLayout == newLayout && entry.oldLayout == oldLayout; - }); - if(it == entries.end()) return std::nullopt; - return it->transition; + // no content to keep: src waits for the dst stages without access, + // orders the transition after their last use and after an acquire semaphore wait on these stages + if(oldLayout == VK_IMAGE_LAYOUT_UNDEFINED || oldLayout == VK_IMAGE_LAYOUT_PREINITIALIZED + || oldLayout == VK_IMAGE_LAYOUT_PRESENT_SRC_KHR) + return LayoutTransition{.src = {dst->stages}, .dst = *dst}; + + auto const src = layout_scope(oldLayout, aspects, shaderStages); + if(!src.has_value()) return std::nullopt; + + // reads leave nothing to make visible, only their stages finish first + constexpr VkAccessFlags2 writes = VK_ACCESS_2_TRANSFER_WRITE_BIT + | VK_ACCESS_2_COLOR_ATTACHMENT_WRITE_BIT + | VK_ACCESS_2_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT + | VK_ACCESS_2_SHADER_WRITE_BIT + | VK_ACCESS_2_MEMORY_WRITE_BIT; + return LayoutTransition{.src = {src->stages, src->access & writes}, .dst = *dst}; } } diff --git a/cvk/src/render/sync/barrier.cpp b/cvk/src/render/sync/barrier.cpp index 50d8accd..71fbc887 100644 --- a/cvk/src/render/sync/barrier.cpp +++ b/cvk/src/render/sync/barrier.cpp @@ -165,6 +165,25 @@ void Barrier::debug_check_rendering( } } +void Barrier::addTransition( + Image const& image, + VkImageLayout newLayout, + VkImageLayout oldLayout, + VkPipelineStageFlags2 shaderStages, + uint32_t firstMipLevel, + uint32_t mipLevels +) { + auto const transition = layout_transition(newLayout, oldLayout, image.aspectMask(), shaderStages); + CTH_CRITICAL(!transition.has_value(), "layout transition has no scope for a layout, use add() with scopes") { + details->add("transition: {} -> {}", oldLayout, newLayout); + } + + add( + image, + ImageInfo::LayoutTransition(newLayout, oldLayout, transition->dst, transition->src, firstMipLevel, mipLevels) + ); +} + void Barrier::execute(BaseCmdBuffer const& cmdBuffer) const { BaseCmdBuffer::debug_check(cmdBuffer); CTH_CRITICAL(!cmdBuffer.recording(), "command buffer must be recording") {} diff --git a/cvk/tests/src/render/cmd/draw_test.cpp b/cvk/tests/src/render/cmd/draw_test.cpp index 4702ba67..bf525220 100644 --- a/cvk/tests/src/render/cmd/draw_test.cpp +++ b/cvk/tests/src/render/cmd/draw_test.cpp @@ -121,15 +121,7 @@ namespace { cmd.endRendering(); Barrier barrier{}; - barrier.add( - image, - ImageBarrierInfo::LayoutTransition( - VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, - VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, - {VK_PIPELINE_STAGE_2_TRANSFER_BIT, VK_ACCESS_2_TRANSFER_READ_BIT}, - {VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT, VK_ACCESS_2_COLOR_ATTACHMENT_WRITE_BIT} - ) - ); + barrier.addTransition(image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL); barrier.execute(cmd); test::copy_to_buffer(context, cmd, image, readback, 0, 0); diff --git a/cvk/tests/src/render/cmd/rendering_test.cpp b/cvk/tests/src/render/cmd/rendering_test.cpp index b5b0a6ed..e8f950e3 100644 --- a/cvk/tests/src/render/cmd/rendering_test.cpp +++ b/cvk/tests/src/render/cmd/rendering_test.cpp @@ -142,6 +142,31 @@ CVK_TEST(Rendering, clear_a_depth_attachment) { }); } +CVK_TEST(Rendering, depth_attachment_in_the_default_layout) { + test::Context const context{}; + Image const image{context.core, Image::Config::DepthBuffer(VK_FORMAT_D16_UNORM, VK_SAMPLE_COUNT_1_BIT), EXTENT}; + ImageView const view{image, {}, create}; + + test::record_and_submit(context, [&](PrimaryCmdBuffer& cmd) { + // the aspect of the image decides the scopes of VK_IMAGE_LAYOUT_ATTACHMENT_OPTIMAL + Barrier transition{}; + transition.addTransition(image, VK_IMAGE_LAYOUT_ATTACHMENT_OPTIMAL, VK_IMAGE_LAYOUT_UNDEFINED); + transition.execute(cmd); + + cmd.beginRendering( + { + .depthAttachment = RenderingAttachment{ + .view = &view, + .loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR, + .storeOp = VK_ATTACHMENT_STORE_OP_STORE, + .clearValue = {.depthStencil = {1.f, 0}}, + }, + } + ); + cmd.endRendering(); + }); +} + CVK_TEST(Rendering, secondary_inherits_the_rendering) { test::Context const context{}; Image const image{context.core, COLOR_CONFIG, EXTENT}; diff --git a/cvk/tests/src/render/sync/barrier_test.cpp b/cvk/tests/src/render/sync/barrier_test.cpp index 9c637e4c..9bc427a2 100644 --- a/cvk/tests/src/render/sync/barrier_test.cpp +++ b/cvk/tests/src/render/sync/barrier_test.cpp @@ -6,6 +6,8 @@ #include "cvk/render/cmd/cmd_buffer.hpp" #include "cvk/render/sync/barrier.hpp" +#include + namespace cvk { namespace { @@ -20,20 +22,123 @@ namespace { constexpr BarrierScope TRANSFER_WRITE{VK_PIPELINE_STAGE_2_TRANSFER_BIT, VK_ACCESS_2_TRANSFER_WRITE_BIT}; constexpr BarrierScope FRAGMENT_READ{VK_PIPELINE_STAGE_2_FRAGMENT_SHADER_BIT, VK_ACCESS_2_SHADER_READ_BIT}; + + constexpr VkImageAspectFlags COLOR = VK_IMAGE_ASPECT_COLOR_BIT; + constexpr VkImageAspectFlags DEPTH = VK_IMAGE_ASPECT_DEPTH_BIT; + constexpr VkPipelineStageFlags2 TESTS = VK_PIPELINE_STAGE_2_EARLY_FRAGMENT_TESTS_BIT + | VK_PIPELINE_STAGE_2_LATE_FRAGMENT_TESTS_BIT; } -CVK_TEST(Barrier, layout_transition_table) { - constexpr auto toTransfer = layout_transition(VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_UNDEFINED); +CVK_TEST(Barrier, layout_transition_from_undefined_waits_for_the_dst_stages) { + constexpr auto toTransfer = layout_transition(VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_UNDEFINED, COLOR); static_assert(toTransfer.has_value()); static_assert(toTransfer->src.stages == VK_PIPELINE_STAGE_2_TRANSFER_BIT); static_assert(toTransfer->src.access == VK_ACCESS_2_NONE); static_assert(toTransfer->dst.access == VK_ACCESS_2_TRANSFER_WRITE_BIT); - constexpr auto toLocalRead = layout_transition(VK_IMAGE_LAYOUT_RENDERING_LOCAL_READ_KHR, VK_IMAGE_LAYOUT_UNDEFINED); + constexpr auto toLocalRead = layout_transition( + VK_IMAGE_LAYOUT_RENDERING_LOCAL_READ_KHR, + VK_IMAGE_LAYOUT_UNDEFINED, + COLOR + ); static_assert(toLocalRead.has_value()); static_assert((toLocalRead->dst.access & VK_ACCESS_2_INPUT_ATTACHMENT_READ_BIT) != 0); - static_assert(!layout_transition(VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL).has_value()); + constexpr auto toUndefined = layout_transition( + VK_IMAGE_LAYOUT_UNDEFINED, + VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, + COLOR + ); + static_assert(!toUndefined.has_value()); +} + +CVK_TEST(Barrier, layout_transition_src_from_the_old_dst_from_the_new_layout) { + constexpr auto readback = layout_transition( + VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, + VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, + COLOR + ); + static_assert(readback.has_value()); + static_assert(readback->src.stages == VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT); + static_assert(readback->src.access == VK_ACCESS_2_COLOR_ATTACHMENT_WRITE_BIT); + static_assert(readback->dst.stages == VK_PIPELINE_STAGE_2_TRANSFER_BIT); + static_assert(readback->dst.access == VK_ACCESS_2_TRANSFER_READ_BIT); +} + +CVK_TEST(Barrier, layout_transition_src_keeps_only_the_writes) { + constexpr auto renderAgain = layout_transition( + VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, + VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, + COLOR + ); + static_assert(renderAgain.has_value()); + static_assert(renderAgain->src.stages == VK_PIPELINE_STAGE_2_FRAGMENT_SHADER_BIT); + static_assert(renderAgain->src.access == VK_ACCESS_2_NONE); +} + +CVK_TEST(Barrier, layout_transition_aspects_pick_color_or_depth) { + constexpr auto color = layout_transition(VK_IMAGE_LAYOUT_ATTACHMENT_OPTIMAL, VK_IMAGE_LAYOUT_UNDEFINED, COLOR); + constexpr auto depth = layout_transition(VK_IMAGE_LAYOUT_ATTACHMENT_OPTIMAL, VK_IMAGE_LAYOUT_UNDEFINED, DEPTH); + static_assert(color->dst.stages == VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT); + static_assert(depth->dst.stages == TESTS); + + constexpr auto depthLocalRead = layout_scope(VK_IMAGE_LAYOUT_RENDERING_LOCAL_READ_KHR, DEPTH); + static_assert(depthLocalRead->stages == (TESTS | VK_PIPELINE_STAGE_2_FRAGMENT_SHADER_BIT)); +} + +CVK_TEST(Barrier, layout_transition_shader_stages_replace_the_fragment_shader) { + constexpr auto toCompute = layout_transition( + VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, + VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, + COLOR, + VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT + ); + static_assert(toCompute->dst.stages == VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT); + + constexpr auto fromCompute = layout_transition( + VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, + VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, + COLOR, + VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT + ); + static_assert(fromCompute->src.stages == VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT); + + // input attachments are fragment shader only + constexpr auto localRead = layout_scope( + VK_IMAGE_LAYOUT_RENDERING_LOCAL_READ_KHR, + COLOR, + VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT + ); + static_assert((localRead->stages & VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT) == 0); +} + +CVK_TEST(Barrier, every_pair_of_known_layouts_has_a_transition) { + static_assert([] { + constexpr std::array layouts{ + VK_IMAGE_LAYOUT_GENERAL, + VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, + VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL, + VK_IMAGE_LAYOUT_DEPTH_STENCIL_READ_ONLY_OPTIMAL, + VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, + VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, + VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, + VK_IMAGE_LAYOUT_DEPTH_READ_ONLY_STENCIL_ATTACHMENT_OPTIMAL, + VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_STENCIL_READ_ONLY_OPTIMAL, + VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_OPTIMAL, + VK_IMAGE_LAYOUT_DEPTH_READ_ONLY_OPTIMAL, + VK_IMAGE_LAYOUT_STENCIL_ATTACHMENT_OPTIMAL, + VK_IMAGE_LAYOUT_STENCIL_READ_ONLY_OPTIMAL, + VK_IMAGE_LAYOUT_READ_ONLY_OPTIMAL, + VK_IMAGE_LAYOUT_ATTACHMENT_OPTIMAL, + VK_IMAGE_LAYOUT_RENDERING_LOCAL_READ_KHR, + VK_IMAGE_LAYOUT_PRESENT_SRC_KHR, + }; + for(auto const aspects : {COLOR, DEPTH}) + for(auto const newLayout : layouts) + for(auto const oldLayout : layouts) + if(!layout_transition(newLayout, oldLayout, aspects).has_value()) return false; + return true; + }()); } CVK_TEST(Barrier, layout_transition_info_covers_the_remaining_mip_levels) { @@ -148,14 +253,27 @@ CVK_TEST(Barrier, add_transition_on_a_mip_range) { image, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, + Barrier::INFERRED_SHADER_STAGES, 1, 2 ); - toRead.addTransition(image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 3); + toRead.addTransition( + image, + VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, + VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, + Barrier::INFERRED_SHADER_STAGES, + 3 + ); toRead.execute(primary); Barrier back{}; - back.addTransition(image, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, 3); + back.addTransition( + image, + VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, + VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, + Barrier::INFERRED_SHADER_STAGES, + 3 + ); back.execute(primary); }); } From 183df424398ac500182efd5de847a7d7ad653ebf Mon Sep 17 00:00:00 2001 From: lukas <89145039+codethinki@users.noreply.github.com> Date: Wed, 21 Oct 2026 09:04:52 +0200 Subject: [PATCH 087/135] cvk: TimelineSemaphore waits for a value --- cvk/incl/cvk/render/sync/timeline_semaphore.hpp | 14 ++++++++++---- cvk/src/render/sync/timeline_semaphore.cpp | 7 +++---- .../src/render/sync/timeline_semaphore_test.cpp | 17 +++++++++++++++-- 3 files changed, 28 insertions(+), 10 deletions(-) diff --git a/cvk/incl/cvk/render/sync/timeline_semaphore.hpp b/cvk/incl/cvk/render/sync/timeline_semaphore.hpp index f7704b82..28ab832d 100644 --- a/cvk/incl/cvk/render/sync/timeline_semaphore.hpp +++ b/cvk/incl/cvk/render/sync/timeline_semaphore.hpp @@ -78,20 +78,26 @@ class TimelineSemaphore : Semaphore { uint64_t signal(); /** - * @brief blocks until the semaphore reaches @ref value() or the timeout ends + * @brief blocks until the semaphore reaches the value or the timeout ends * @param timeout in nanoseconds * @return VK_SUCCESS or VK_TIMEOUT * @pre @ref created() * @throws cvk::vk_result_exception other result of vkWaitSemaphores() */ - [[nodiscard]] VkResult wait(uint64_t timeout) const; + [[nodiscard]] VkResult wait(uint64_t value, uint64_t timeout) const; /** - * @brief blocks until the semaphore reaches @ref value() + * @brief blocks until the semaphore reaches the value * @pre @ref created() * @throws cvk::vk_result_exception result of vkWaitSemaphores() */ - void wait() const; + void wait(uint64_t value) const; + + /** + * @brief blocks until the semaphore reaches @ref value() + * @details calls @ref wait(uint64_t) const + */ + void wait() const { wait(_value); } /** * @brief advances the counter diff --git a/cvk/src/render/sync/timeline_semaphore.cpp b/cvk/src/render/sync/timeline_semaphore.cpp index 26e2b158..b15b62e3 100644 --- a/cvk/src/render/sync/timeline_semaphore.cpp +++ b/cvk/src/render/sync/timeline_semaphore.cpp @@ -60,12 +60,11 @@ uint64_t TimelineSemaphore::signal() { return info.value; } -VkResult TimelineSemaphore::wait(uint64_t timeout) const { +VkResult TimelineSemaphore::wait(uint64_t value, uint64_t timeout) const { debug_check(*this); auto const table = core().deviceTable(); auto const vkSemaphore = get(); - uint64_t const value = _value; VkSemaphoreWaitInfo const info{ .sType = VK_STRUCTURE_TYPE_SEMAPHORE_WAIT_INFO, @@ -86,8 +85,8 @@ VkResult TimelineSemaphore::wait(uint64_t timeout) const { return result; } -void TimelineSemaphore::wait() const { - [[maybe_unused]] auto const result = wait(std::numeric_limits::max()); +void TimelineSemaphore::wait(uint64_t value) const { + [[maybe_unused]] auto const result = wait(value, std::numeric_limits::max()); } uint64_t TimelineSemaphore::gpuValue() const { diff --git a/cvk/tests/src/render/sync/timeline_semaphore_test.cpp b/cvk/tests/src/render/sync/timeline_semaphore_test.cpp index 6d16790b..3f504338 100644 --- a/cvk/tests/src/render/sync/timeline_semaphore_test.cpp +++ b/cvk/tests/src/render/sync/timeline_semaphore_test.cpp @@ -24,7 +24,7 @@ CVK_TEST(TimelineSemaphore, create_sets_the_initial_value) { ASSERT_TRUE(semaphore.created()); EXPECT_EQ(semaphore.value(), 5); EXPECT_EQ(semaphore.gpuValue(), 5); - EXPECT_EQ(semaphore.wait(0), VK_SUCCESS); + EXPECT_EQ(semaphore.wait(semaphore.value(), 0), VK_SUCCESS); } CVK_TEST(TimelineSemaphore, signal_advances_the_counter) { @@ -43,10 +43,23 @@ CVK_TEST(TimelineSemaphore, wait_for_a_value_not_signaled_times_out) { EXPECT_EQ(semaphore.next(), 1); - EXPECT_EQ(semaphore.wait(0), VK_TIMEOUT); + EXPECT_EQ(semaphore.wait(semaphore.value(), 0), VK_TIMEOUT); EXPECT_EQ(semaphore.gpuValue(), 0); } +CVK_TEST(TimelineSemaphore, wait_for_an_older_value) { + test::Context const context{}; + TimelineSemaphore semaphore{context.core, create}; + + EXPECT_EQ(semaphore.signal(), 1); + EXPECT_EQ(semaphore.next(), 2); + + // the counter is ahead of the semaphore, an older value is reached + EXPECT_EQ(semaphore.wait(1, 0), VK_SUCCESS); + EXPECT_EQ(semaphore.wait(2, 0), VK_TIMEOUT); + semaphore.wait(1); +} + CVK_TEST(TimelineSemaphore, submit_signals_the_next_value) { test::Context const context{}; TimelineSemaphore const semaphore{context.core, create}; From d21ae8f33b0a64c0c968f48a3721dd4f24c19ca2 Mon Sep 17 00:00:00 2001 From: lukas <89145039+codethinki@users.noreply.github.com> Date: Wed, 21 Oct 2026 09:10:20 +0200 Subject: [PATCH 088/135] cvk: Image::copyTo into a buffer, the buffer copy from an image calls it --- AGENTS.md | 2 +- cvk/incl/cvk/data/buffer/base_buffer.hpp | 14 +++++ cvk/incl/cvk/data/buffer/buffer.hpp | 15 +++++ cvk/incl/cvk/data/img/image.hpp | 30 +++++++++ cvk/src/data/buffer/base_buffer.cpp | 11 ++++ cvk/src/data/img/image.cpp | 80 +++++++++++++++++++----- cvk/tests/src/data/img/image_test.cpp | 70 +++++++++++++++++++++ 7 files changed, 204 insertions(+), 18 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 89ee3e2b..9d624c59 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -101,7 +101,7 @@ ctest --test-dir out/build/gcc_release --output-on-failure - Initialize Vulkan structs with designated initializers. Always write `.sType` and `.pNext`. - Data structs (`SubmitInfo`, `Barrier`, the info structs) are aggregates: the caller can set everything with designated initializers. Member functions like `add()` are optional shortcuts, checks run where the data is used. - Commands that change the state of a command buffer (binds, dynamic state, push constants, renderings, draws) are members of the command buffer: `cmd.bindPipeline(pipeline)`. Commands on the data of an object (barriers, copies, uploads) are members of the object and take the command buffer: `barrier.execute(cmd)`, `image.copy(cmd, staging)`. -- The target comes first, like `std::memcpy(dst, src, ...)`: new before old, dst before src, `LayoutTransition(newLayout, oldLayout, dst, src)`. The object of a copy is the destination: `dst.copy(cmd, src)`. Exception: `src` and `dst` fields keep the order of the Vulkan struct (`BarrierScope src, dst`, `BufferCopy{srcOffset, dstOffset}`), designated initializers follow it. +- The target comes first, like `std::memcpy(dst, src, ...)`: new before old, dst before src, `LayoutTransition(newLayout, oldLayout, dst, src)`. The object of a copy is the destination: `dst.copy(cmd, src)`. `copyTo(cmd, dst)` copies with the object as the source, where the code belongs to the source: `image.copyTo(cmd, buffer)`, the mirror `buffer.copy(cmd, image)` calls it. Exception: `src` and `dst` fields keep the order of the Vulkan struct (`BarrierScope src, dst`, `BufferCopy{srcOffset, dstOffset}`), designated initializers follow it. - Use deducing `this` (`this auto& self`) where a function must work on the derived type. - Qualify `std::` names. A `.cpp` file can declare `using std::span;` and similar at the top of its namespace. - No raw `new` and `delete`. Use `std::unique_ptr`, `cth::move_ptr` and `cth::not_null`. diff --git a/cvk/incl/cvk/data/buffer/base_buffer.hpp b/cvk/incl/cvk/data/buffer/base_buffer.hpp index 74fb72e1..ede10c1c 100644 --- a/cvk/incl/cvk/data/buffer/base_buffer.hpp +++ b/cvk/incl/cvk/data/buffer/base_buffer.hpp @@ -19,6 +19,7 @@ namespace cvk { class BaseCmdBuffer; class Core; +class Image; /** * @brief offsets and size of a buffer copy @@ -120,6 +121,19 @@ class BaseBuffer { */ void copy(BaseCmdBuffer const&, BaseBuffer const& src, BufferCopy region = {}) const; + /** + * @brief records a copy from the mip level of the image + * @param dstOffset in bytes + * @details calls @ref Image::copyTo() + */ + void copy( + BaseCmdBuffer const&, + Image const& src, + size_t dstOffset = 0, + uint32_t mipLevel = 0, + VkImageLayout layout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL + ) const; + /** * @brief maps the range of the memory * @param range in bytes diff --git a/cvk/incl/cvk/data/buffer/buffer.hpp b/cvk/incl/cvk/data/buffer/buffer.hpp index 9b69d20f..2c409923 100644 --- a/cvk/incl/cvk/data/buffer/buffer.hpp +++ b/cvk/incl/cvk/data/buffer/buffer.hpp @@ -122,6 +122,21 @@ class Buffer : public BaseBuffer { ); } + /** + * @brief records a copy from the mip level of the image + * @param dstOffset in elements + * @details calls @ref BaseBuffer::copy(BaseCmdBuffer const&, Image const&, size_t, uint32_t, VkImageLayout) const + */ + void copy( + BaseCmdBuffer const& cmdBuffer, + Image const& src, + size_t dstOffset = 0, + uint32_t mipLevel = 0, + VkImageLayout layout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL + ) const { + BaseBuffer::copy(cmdBuffer, src, dstOffset * sizeof(T), mipLevel, layout); + } + [[nodiscard]] constexpr size_t elements() const { return size() / sizeof(T); } private: diff --git a/cvk/incl/cvk/data/img/image.hpp b/cvk/incl/cvk/data/img/image.hpp index 0dacab8b..226043b3 100644 --- a/cvk/incl/cvk/data/img/image.hpp +++ b/cvk/incl/cvk/data/img/image.hpp @@ -114,6 +114,25 @@ class Image { VkImageLayout layout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL ) const; + /** + * @brief records a copy from the mip level into the buffer + * @param dstOffset in bytes + * @param layout of the mip level, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL or VK_IMAGE_LAYOUT_GENERAL + * @pre @ref created() and dst created + * @pre @ref BaseCmdBuffer::recording() + * @pre mipLevel < @ref mipLevels() + * @pre @ref aspectMask() has one bit + * @pre the usage has VK_IMAGE_USAGE_TRANSFER_SRC_BIT, dst usage has VK_BUFFER_USAGE_TRANSFER_DST_BIT + * @pre the mip level fits into dst after dstOffset + */ + void copyTo( + BaseCmdBuffer const&, + BaseBuffer const& dst, + size_t dstOffset = 0, + uint32_t mipLevel = 0, + VkImageLayout layout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL + ) const; + /** * @return mip levels down to 1x1 * @pre width > 0, height > 0 @@ -147,6 +166,17 @@ class Image { } private: + /** + * @brief checks the preconditions that both copy directions share + * @return region of the whole mip level at the buffer offset + */ + [[nodiscard]] VkBufferImageCopy2 bufferCopy( + BaseCmdBuffer const&, + BaseBuffer const& buffer, + size_t bufferOffset, + uint32_t mipLevel + ) const; + void forget(); // declared before the memory: move assignment destroys the old image before its memory diff --git a/cvk/src/data/buffer/base_buffer.cpp b/cvk/src/data/buffer/base_buffer.cpp index 169ed0fc..dfbb58a5 100644 --- a/cvk/src/data/buffer/base_buffer.cpp +++ b/cvk/src/data/buffer/base_buffer.cpp @@ -2,6 +2,7 @@ #include "cvk/base/allocator.hpp" #include "cvk/base/core.hpp" +#include "cvk/data/img/image.hpp" #include "cvk/render/cmd/cmd_buffer.hpp" #include "cvk/utility/vk_exceptions.hpp" @@ -119,6 +120,16 @@ void BaseBuffer::copy(BaseCmdBuffer const& cmdBuffer, BaseBuffer const& src, Buf core().functions()->vkCmdCopyBuffer2(cmdBuffer.get(), &info); } +void BaseBuffer::copy( + BaseCmdBuffer const& cmdBuffer, + Image const& src, + size_t dstOffset, + uint32_t mipLevel, + VkImageLayout layout +) const { + src.copyTo(cmdBuffer, *this, dstOffset, mipLevel, layout); +} + Mapping BaseBuffer::map(Range range) const { debug_check(*this); return ownMemory().map(range); diff --git a/cvk/src/data/img/image.cpp b/cvk/src/data/img/image.cpp index 8675b19c..68e74bd2 100644 --- a/cvk/src/data/img/image.cpp +++ b/cvk/src/data/img/image.cpp @@ -85,11 +85,7 @@ void Image::copy( uint32_t mipLevel, VkImageLayout layout ) const { - debug_check(*this); - BaseBuffer::debug_check(src); - CTH_CRITICAL(!cmdBuffer.recording(), "command buffer must be recording") {} - CTH_CRITICAL(mipLevel >= mipLevels(), "mip level must lie in the image") {} - CTH_CRITICAL(!std::has_single_bit(aspectMask()), "a copy needs exactly one aspect") {} + auto const region = bufferCopy(cmdBuffer, src, srcOffset, mipLevel); CTH_CRITICAL( (src.config().usage & VK_BUFFER_USAGE_TRANSFER_SRC_BIT) == 0, "src usage must have VK_BUFFER_USAGE_TRANSFER_SRC_BIT" @@ -106,10 +102,70 @@ void Image::copy( details->add("layout: {}", layout); } - VkBufferImageCopy2 const region{ + VkCopyBufferToImageInfo2 const info{ + .sType = VK_STRUCTURE_TYPE_COPY_BUFFER_TO_IMAGE_INFO_2, + .pNext = nullptr, + .srcBuffer = src.get(), + .dstImage = get(), + .dstImageLayout = layout, + .regionCount = 1, + .pRegions = ®ion, + }; + core().functions()->vkCmdCopyBufferToImage2(cmdBuffer.get(), &info); +} + +void Image::copyTo( + BaseCmdBuffer const& cmdBuffer, + BaseBuffer const& dst, + size_t dstOffset, + uint32_t mipLevel, + VkImageLayout layout +) const { + auto const region = bufferCopy(cmdBuffer, dst, dstOffset, mipLevel); + CTH_CRITICAL( + (dst.config().usage & VK_BUFFER_USAGE_TRANSFER_DST_BIT) == 0, + "dst usage must have VK_BUFFER_USAGE_TRANSFER_DST_BIT" + ) {} + CTH_CRITICAL( + (_config.usage & VK_IMAGE_USAGE_TRANSFER_SRC_BIT) == 0, + "image usage must have VK_IMAGE_USAGE_TRANSFER_SRC_BIT" + ) {} + + CTH_CRITICAL( + layout != VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL && layout != VK_IMAGE_LAYOUT_GENERAL, + "mip level must be in VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL or VK_IMAGE_LAYOUT_GENERAL" + ) { + details->add("layout: {}", layout); + } + + VkCopyImageToBufferInfo2 const info{ + .sType = VK_STRUCTURE_TYPE_COPY_IMAGE_TO_BUFFER_INFO_2, + .pNext = nullptr, + .srcImage = get(), + .srcImageLayout = layout, + .dstBuffer = dst.get(), + .regionCount = 1, + .pRegions = ®ion, + }; + core().functions()->vkCmdCopyImageToBuffer2(cmdBuffer.get(), &info); +} + +VkBufferImageCopy2 Image::bufferCopy( + BaseCmdBuffer const& cmdBuffer, + BaseBuffer const& buffer, + size_t bufferOffset, + uint32_t mipLevel +) const { + debug_check(*this); + BaseBuffer::debug_check(buffer); + CTH_CRITICAL(!cmdBuffer.recording(), "command buffer must be recording") {} + CTH_CRITICAL(mipLevel >= mipLevels(), "mip level must lie in the image") {} + CTH_CRITICAL(!std::has_single_bit(aspectMask()), "a copy needs exactly one aspect") {} + + return VkBufferImageCopy2{ .sType = VK_STRUCTURE_TYPE_BUFFER_IMAGE_COPY_2, .pNext = nullptr, - .bufferOffset = srcOffset, + .bufferOffset = bufferOffset, .bufferRowLength = 0, .bufferImageHeight = 0, .imageSubresource = { @@ -121,16 +177,6 @@ void Image::copy( .imageOffset = {0, 0, 0}, .imageExtent = {std::max(_extent.width >> mipLevel, 1u), std::max(_extent.height >> mipLevel, 1u), 1}, }; - VkCopyBufferToImageInfo2 const info{ - .sType = VK_STRUCTURE_TYPE_COPY_BUFFER_TO_IMAGE_INFO_2, - .pNext = nullptr, - .srcBuffer = src.get(), - .dstImage = get(), - .dstImageLayout = layout, - .regionCount = 1, - .pRegions = ®ion, - }; - core().functions()->vkCmdCopyBufferToImage2(cmdBuffer.get(), &info); } void Image::destroy(DeviceTable table, VkImage vkImage) { diff --git a/cvk/tests/src/data/img/image_test.cpp b/cvk/tests/src/data/img/image_test.cpp index 4848b810..f54a69fd 100644 --- a/cvk/tests/src/data/img/image_test.cpp +++ b/cvk/tests/src/data/img/image_test.cpp @@ -2,11 +2,14 @@ #include "test.hpp" #include "cvk/data/buffer/base_buffer.hpp" +#include "cvk/data/buffer/buffer.hpp" #include "cvk/data/img/image.hpp" #include "cvk/render/sync/barrier.hpp" #include #include +#include +#include #include @@ -184,6 +187,73 @@ CVK_TEST(Image, copy_from_a_buffer_into_two_mip_levels) { EXPECT_TRUE(std::ranges::equal(mapped, pixels)); } +CVK_TEST(Image, copy_two_mip_levels_to_a_buffer) { + test::Context const context{}; + Image const image{context.core, COPY_CONFIG, EXTENT}; + + size_t const mip0 = size_t{EXTENT.width} * EXTENT.height * 4; + size_t const mip1 = mip0 / 4; + BaseBuffer const staging{context.core, STAGING_CONFIG, mip0 + mip1}; + BaseBuffer const readback{context.core, READBACK_CONFIG, mip0 + mip1}; + + std::vector pixels(mip0 + mip1); + for(size_t i = 0; i < pixels.size(); ++i) + pixels[i] = static_cast(i % 251); + staging.write(pixels); + + test::record_and_submit(context, [&](PrimaryCmdBuffer const& primary) { + Barrier toDst{}; + toDst.addTransition(image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_UNDEFINED); + toDst.execute(primary); + + image.copy(primary, staging, 0, 0); + image.copy(primary, staging, mip0, 1); + + Barrier toSrc{}; + toSrc.addTransition(image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL); + toSrc.execute(primary); + image.copyTo(primary, readback, 0, 0); + readback.copy(primary, image, mip0, 1); + test::host_read_barrier(context, primary); + }); + + auto const mapped = readback.map({.size = pixels.size()}); + readback.invalidate(); + EXPECT_TRUE(std::ranges::equal(mapped, pixels)); +} + +CVK_TEST(Image, typed_buffer_copies_a_mip_level_in_elements) { + test::Context const context{}; + Image const image{context.core, COPY_CONFIG, EXTENT}; + + // one R8G8B8A8 texel per element + size_t const mip1 = size_t{EXTENT.width / 2} * (EXTENT.height / 2); + BaseBuffer const staging{context.core, STAGING_CONFIG, mip1 * sizeof(uint32_t)}; + Buffer const readback{context.core, READBACK_CONFIG, mip1 + 1}; + + std::vector texels(mip1); + for(size_t i = 0; i < texels.size(); ++i) + texels[i] = static_cast(i * 2654435761u); + staging.write(std::as_bytes(std::span{texels})); + + test::record_and_submit(context, [&](PrimaryCmdBuffer const& primary) { + Barrier toDst{}; + toDst.addTransition(image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_UNDEFINED); + toDst.execute(primary); + image.copy(primary, staging, 0, 1); + + Barrier toSrc{}; + toSrc.addTransition(image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL); + toSrc.execute(primary); + readback.copy(primary, image, 1, 1); + test::host_read_barrier(context, primary); + }); + + auto const mapped = readback.map({.offset = 1}); + readback.invalidate(); + EXPECT_TRUE(std::ranges::equal(mapped, texels)); +} + CVK_TEST(Image, release_and_wrap) { test::Context const context{}; Image image{context.core, TEXTURE_CONFIG, EXTENT}; From 4e2b313fe56933f13db93a0da5dda4e742442199 Mon Sep 17 00:00:00 2001 From: lukas <89145039+codethinki@users.noreply.github.com> Date: Wed, 21 Oct 2026 09:34:06 +0200 Subject: [PATCH 089/135] cvk: Image::blitMipLevels fills the mip chain, every level ends in transfer src --- cvk/incl/cvk/data/img/image.hpp | 19 +++++++ cvk/src/data/img/image.cpp | 71 +++++++++++++++++++++++++++ cvk/tests/src/data/img/image_test.cpp | 43 ++++++++++++++++ 3 files changed, 133 insertions(+) diff --git a/cvk/incl/cvk/data/img/image.hpp b/cvk/incl/cvk/data/img/image.hpp index 226043b3..a6d5b6fe 100644 --- a/cvk/incl/cvk/data/img/image.hpp +++ b/cvk/incl/cvk/data/img/image.hpp @@ -133,6 +133,25 @@ class Image { VkImageLayout layout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL ) const; + /** + * @brief records blits that fill each mip level from the level before it + * @param firstMipLevel first level to fill, the blits read from firstMipLevel - 1 + * @param mipLevels (VK_REMAINING_MIP_LEVELS => to the last mip level) + * @param filter depth formats need VK_FILTER_NEAREST + * @pre @ref created(), @ref BaseCmdBuffer::recording() + * @pre 0 < firstMipLevel, the levels lie in the image + * @pre the usage has VK_IMAGE_USAGE_TRANSFER_SRC_BIT and VK_IMAGE_USAGE_TRANSFER_DST_BIT + * @pre the format supports blits and the filter + * @pre the levels from firstMipLevel - 1 to the last filled level are in VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL + * @post the same levels are in VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL + */ + void blitMipLevels( + BaseCmdBuffer const&, + uint32_t firstMipLevel = 1, + uint32_t mipLevels = VK_REMAINING_MIP_LEVELS, + VkFilter filter = VK_FILTER_LINEAR + ) const; + /** * @return mip levels down to 1x1 * @pre width > 0, height > 0 diff --git a/cvk/src/data/img/image.cpp b/cvk/src/data/img/image.cpp index 68e74bd2..730827ad 100644 --- a/cvk/src/data/img/image.cpp +++ b/cvk/src/data/img/image.cpp @@ -4,6 +4,7 @@ #include "cvk/base/core.hpp" #include "cvk/data/buffer/base_buffer.hpp" #include "cvk/render/cmd/cmd_buffer.hpp" +#include "cvk/render/sync/barrier.hpp" #include "cvk/utility/vk_exceptions.hpp" #include @@ -150,6 +151,76 @@ void Image::copyTo( core().functions()->vkCmdCopyImageToBuffer2(cmdBuffer.get(), &info); } +void Image::blitMipLevels( + BaseCmdBuffer const& cmdBuffer, + uint32_t firstMipLevel, + uint32_t mipLevels, + VkFilter filter +) const { + debug_check(*this); + CTH_CRITICAL(!cmdBuffer.recording(), "command buffer must be recording") {} + CTH_CRITICAL( + firstMipLevel == 0 || firstMipLevel >= _config.mipLevels, + "first mip level must lie in [1, mip levels)" + ) {} + + auto const levels = mipLevels == VK_REMAINING_MIP_LEVELS ? _config.mipLevels - firstMipLevel : mipLevels; + CTH_CRITICAL(levels == 0 || levels > _config.mipLevels - firstMipLevel, "mip levels must lie in the image") {} + + constexpr VkImageUsageFlags transfer = VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT; + CTH_CRITICAL( + (_config.usage & transfer) != transfer, + "image usage must have VK_IMAGE_USAGE_TRANSFER_SRC_BIT and VK_IMAGE_USAGE_TRANSFER_DST_BIT" + ) {} + + auto const corner = [this](uint32_t level) { + return VkOffset3D{ + static_cast(std::max(_extent.width >> level, 1u)), + static_cast(std::max(_extent.height >> level, 1u)), + 1, + }; + }; + // the level is written, the next blit reads it + auto const toSrc = [&](uint32_t level) { + Barrier barrier{}; + barrier.addTransition( + *this, + VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, + VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, + Barrier::INFERRED_SHADER_STAGES, + level, + 1 + ); + barrier.execute(cmdBuffer); + }; + + for(uint32_t level = firstMipLevel; level < firstMipLevel + levels; ++level) { + toSrc(level - 1); + + VkImageBlit2 const region{ + .sType = VK_STRUCTURE_TYPE_IMAGE_BLIT_2, + .pNext = nullptr, + .srcSubresource = {.aspectMask = aspectMask(), .mipLevel = level - 1, .baseArrayLayer = 0, .layerCount = 1}, + .srcOffsets = {{0, 0, 0}, corner(level - 1)}, + .dstSubresource = {.aspectMask = aspectMask(), .mipLevel = level, .baseArrayLayer = 0, .layerCount = 1}, + .dstOffsets = {{0, 0, 0}, corner(level)}, + }; + VkBlitImageInfo2 const info{ + .sType = VK_STRUCTURE_TYPE_BLIT_IMAGE_INFO_2, + .pNext = nullptr, + .srcImage = get(), + .srcImageLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, + .dstImage = get(), + .dstImageLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, + .regionCount = 1, + .pRegions = ®ion, + .filter = filter, + }; + core().functions()->vkCmdBlitImage2(cmdBuffer.get(), &info); + } + toSrc(firstMipLevel + levels - 1); +} + VkBufferImageCopy2 Image::bufferCopy( BaseCmdBuffer const& cmdBuffer, BaseBuffer const& buffer, diff --git a/cvk/tests/src/data/img/image_test.cpp b/cvk/tests/src/data/img/image_test.cpp index f54a69fd..60cd1f56 100644 --- a/cvk/tests/src/data/img/image_test.cpp +++ b/cvk/tests/src/data/img/image_test.cpp @@ -37,6 +37,16 @@ namespace { .mipLevels = 2, }; + /** + * @brief image that fills its mip chain with blits + */ + constexpr Image::Config BLIT_CONFIG{ + .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, + .format = VK_FORMAT_R8G8B8A8_UNORM, + .usage = VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT, + .mipLevels = Image::evalMipLevelCount(EXTENT), + }; + constexpr BaseBuffer::Config STAGING_CONFIG{ .usage = VK_BUFFER_USAGE_TRANSFER_SRC_BIT, .memory = {.flags = VMA_ALLOCATION_CREATE_HOST_ACCESS_SEQUENTIAL_WRITE_BIT}, @@ -254,6 +264,39 @@ CVK_TEST(Image, typed_buffer_copies_a_mip_level_in_elements) { EXPECT_TRUE(std::ranges::equal(mapped, texels)); } +CVK_TEST(Image, blit_mip_levels_fills_the_mip_chain) { + test::Context const context{}; + Image const image{context.core, BLIT_CONFIG, EXTENT}; + + // one color: linear blits keep it in every level + constexpr uint32_t color = 0x80'40'20'10; + std::vector const texels(size_t{EXTENT.width} * EXTENT.height, color); + BaseBuffer const staging{context.core, STAGING_CONFIG, texels.size() * sizeof(uint32_t)}; + staging.write(std::as_bytes(std::span{texels})); + + uint32_t const last = BLIT_CONFIG.mipLevels - 1; + size_t const level2 = size_t{EXTENT.width >> 2} * (EXTENT.height >> 2); + Buffer const readback{context.core, READBACK_CONFIG, level2 + 1}; + + test::record_and_submit(context, [&](PrimaryCmdBuffer const& primary) { + Barrier toDst{}; + toDst.addTransition(image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_UNDEFINED); + toDst.execute(primary); + image.copy(primary, staging); + + image.blitMipLevels(primary); + + // every level is in VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL + readback.copy(primary, image, 0, 2); + readback.copy(primary, image, level2, last); + test::host_read_barrier(context, primary); + }); + + auto const mapped = readback.map(); + readback.invalidate(); + EXPECT_TRUE(std::ranges::all_of(mapped, [](uint32_t texel) { return texel == color; })); +} + CVK_TEST(Image, release_and_wrap) { test::Context const context{}; Image image{context.core, TEXTURE_CONFIG, EXTENT}; From 235356283b185d559193a579960fe39304be4118 Mon Sep 17 00:00:00 2001 From: lukas <89145039+codethinki@users.noreply.github.com> Date: Wed, 21 Oct 2026 09:42:35 +0200 Subject: [PATCH 090/135] cvk tests: one shared instance and device per process, one ctest process, queue tests enable the base features --- cvk/tests/CMakeLists.txt | 4 +- cvk/tests/src/base/queue/queue_test.cpp | 23 +++++-- cvk/tests/src/context.cpp | 79 +++++++++++++++++++++++-- cvk/tests/src/context.hpp | 55 +++++++++++++---- cvk/tests/src/environment.cpp | 7 ++- 5 files changed, 146 insertions(+), 22 deletions(-) diff --git a/cvk/tests/CMakeLists.txt b/cvk/tests/CMakeLists.txt index 6ccc7793..e8e66ebf 100644 --- a/cvk/tests/CMakeLists.txt +++ b/cvk/tests/CMakeLists.txt @@ -21,5 +21,5 @@ find_package(GTest CONFIG REQUIRED) target_link_libraries(cvk_tests PRIVATE GTest::gtest_main cvk) -include(GoogleTest) -gtest_discover_tests(cvk_tests) +# one process: the tests share their instance and device +add_test(NAME cvk_tests COMMAND cvk_tests) diff --git a/cvk/tests/src/base/queue/queue_test.cpp b/cvk/tests/src/base/queue/queue_test.cpp index e0934e1d..97633da7 100644 --- a/cvk/tests/src/base/queue/queue_test.cpp +++ b/cvk/tests/src/base/queue/queue_test.cpp @@ -4,15 +4,28 @@ #include "cvk/base/device.hpp" #include "cvk/base/instance.hpp" #include "cvk/base/physical_device.hpp" +#include "cvk/utility/constant/device_constants.hpp" #include #include +#include +#include namespace cvk { namespace { constexpr std::array QUEUES{QueueFamilyProperties::GRAPHICS}; + /** + * @brief device config with the base features, vkQueueSubmit2() needs synchronization2 + */ + Device::Config device_config(std::vector familyIndices) { + return Device::Config{ + .queueFamilyIndices = std::move(familyIndices), + .features = constants::BASE_DEVICE_FEATURES, + }; + } + VkSubmitInfo2 const EMPTY_SUBMIT{ .sType = VK_STRUCTURE_TYPE_SUBMIT_INFO_2, .pNext = nullptr, @@ -29,7 +42,7 @@ namespace { CVK_TEST(Queue, copies_share_the_queue) { Instance const instance{test::instance_config(), create}; auto const [physicalDevice, familyIndices] = test::pick(instance, QUEUES); - Device const device{instance, physicalDevice, Device::Config{familyIndices}}; + Device const device{instance, physicalDevice, device_config(familyIndices)}; auto const& queue = device.queues()[0]; Queue const copy{queue}; @@ -43,7 +56,7 @@ CVK_TEST(Queue, copies_share_the_queue) { CVK_TEST(Queue, raw_submit_signals_the_fence) { Instance const instance{test::instance_config(), create}; auto const [physicalDevice, familyIndices] = test::pick(instance, QUEUES); - Device const device{instance, physicalDevice, Device::Config{familyIndices}}; + Device const device{instance, physicalDevice, device_config(familyIndices)}; auto const table = device.table(); VkFenceCreateInfo const fenceInfo{.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO, .pNext = nullptr, .flags = 0}; @@ -61,7 +74,7 @@ CVK_TEST(Queue, raw_submit_signals_the_fence) { CVK_TEST(Queue, raw_submit_without_fence) { Instance const instance{test::instance_config(), create}; auto const [physicalDevice, familyIndices] = test::pick(instance, QUEUES); - Device const device{instance, physicalDevice, Device::Config{familyIndices}}; + Device const device{instance, physicalDevice, device_config(familyIndices)}; auto const& queue = device.queues()[0]; queue.rawSubmit(EMPTY_SUBMIT); @@ -71,7 +84,7 @@ CVK_TEST(Queue, raw_submit_without_fence) { CVK_TEST(Queue, copies_keep_the_queue_through_a_device_release_and_wrap) { Instance const instance{test::instance_config(), create}; auto const [physicalDevice, familyIndices] = test::pick(instance, QUEUES); - Device device{instance, physicalDevice, Device::Config{familyIndices}}; + Device device{instance, physicalDevice, device_config(familyIndices)}; Queue const copy{device.queues()[0]}; Device const wrapped{instance, physicalDevice, device.release()}; @@ -87,7 +100,7 @@ CVK_TEST(Queue, present_family_can_present) { std::array const queues{QueueFamilyProperties::GRAPHICS | QueueFamilyProperties::PRESENT}; auto const [physicalDevice, familyIndices] = test::pick(instance, queues, surfaces); - Device const device{instance, physicalDevice, Device::Config{familyIndices}}; + Device const device{instance, physicalDevice, device_config(familyIndices)}; EXPECT_TRUE(device.queues()[0].canPresent()); } diff --git a/cvk/tests/src/context.cpp b/cvk/tests/src/context.cpp index b481cfe2..982be751 100644 --- a/cvk/tests/src/context.cpp +++ b/cvk/tests/src/context.cpp @@ -5,21 +5,92 @@ #include "cvk/render/cmd/cmd_buffer.hpp" #include "cvk/render/sync/fence.hpp" +#include +#include + namespace cvk::test { Core::Config const Context::CONFIG{ .queueProperties = {QueueFamilyProperties::GRAPHICS | QueueFamilyProperties::COMPUTE}, }; -Context::Context(Core::Config const& config) : instance{test::instance_config(), create, test::counting_messenger()}, - core{instance, {}, config} {} +struct Context::Owned { + explicit Owned(Core::Config const& config) : instance{instance_config(), create, counting_messenger()}, + core{instance, {}, config} {} + + Instance instance; + Core core; +}; Core::Config const PresentContext::CONFIG{ .queueProperties = {QueueFamilyProperties::GRAPHICS | QueueFamilyProperties::PRESENT}, }; -PresentContext::PresentContext() : instance{test::instance_config(test::headless_extensions()), create, test::counting_messenger()}, - surfaces{headless_surface(instance)}, core{instance, surfaces, CONFIG} {} +namespace { + /** + * @brief one instance for both shared contexts, the cores and the surface come at their first use + * @details volk holds the functions of one instance, cvk assumes one instance per process + */ + struct SharedContexts { + Instance instance{instance_config(headless_extensions()), create, counting_messenger()}; + std::optional core{}; + std::optional surface{}; + std::optional presentCore{}; + }; + + std::optional shared{}; + + SharedContexts& shared_contexts() { + if(!shared.has_value()) shared.emplace(); + // a test with an own instance loaded its functions into volk + volkLoadInstanceOnly(shared->instance.get()); + return *shared; + } + + Core const& shared_core() { + auto& contexts = shared_contexts(); + if(!contexts.core.has_value()) + contexts.core.emplace(contexts.instance, std::span{}, Context::CONFIG); + return *contexts.core; + } + + Surface const& shared_surface() { + auto& contexts = shared_contexts(); + if(!contexts.surface.has_value()) contexts.surface.emplace(headless_surface(contexts.instance)); + return *contexts.surface; + } + + Core const& shared_present_core() { + auto& contexts = shared_contexts(); + if(!contexts.presentCore.has_value()) { + contexts.presentCore.emplace( + contexts.instance, + std::span{&shared_surface(), 1}, + PresentContext::CONFIG + ); + } + return *contexts.presentCore; + } +} + +Context::Context() : instance{shared_contexts().instance}, core{shared_core()} {} + +Context::Context(Core::Config const& config) : _owned{std::make_unique(config)}, + instance{_owned->instance}, core{_owned->core} {} + +Context::~Context() = default; + +PresentContext::PresentContext() : instance{shared_contexts().instance}, core{shared_present_core()}, + _surface{shared_surface()} {} + +void destroy_shared_contexts() { + if(!shared.has_value()) return; + + volkLoadInstanceOnly(shared->instance.get()); + reset_validation_errors(); + shared.reset(); + EXPECT_EQ(validation_errors(), 0) << "errors at the destruction of the shared contexts, e.g. leaks"; +} Context::ValidationCheck::ValidationCheck() { reset_validation_errors(); } diff --git a/cvk/tests/src/context.hpp b/cvk/tests/src/context.hpp index 00509d8f..4099b9d5 100644 --- a/cvk/tests/src/context.hpp +++ b/cvk/tests/src/context.hpp @@ -9,13 +9,15 @@ #include "cvk/render/cmd/cmd_buffer.hpp" #include +#include #include namespace cvk::test { /** * @brief instance and core for tests, fails the test on validation errors - * @details members destroy in reverse order -> the check sees the errors of vkDestroyInstance() too, e.g. leaks + * @details the default is the shared instance and core of the process, made at the first use, + * @ref destroy_shared_contexts() destroys them, the leaks of all tests show up there */ struct Context { /** @@ -23,15 +25,37 @@ struct Context { */ static Core::Config const CONFIG; - explicit Context(Core::Config const& = CONFIG); - struct ValidationCheck { ValidationCheck(); ~ValidationCheck(); - } validationCheck; + }; + + /** + * @brief instance and core that a context owns + */ + struct Owned; + + /** + * @brief the shared instance and core + */ + Context(); + + /** + * @brief own instance and core with the config + * @details destroys them at the end -> the check sees the errors of vkDestroyInstance() too, e.g. leaks + */ + explicit Context(Core::Config const&); + + ~Context(); + + ValidationCheck validationCheck; - Instance instance; - Core core; +private: + std::unique_ptr _owned; + +public: + Instance const& instance; + Core const& core; Context(Context const& other) = delete; Context(Context&& other) = delete; @@ -41,6 +65,7 @@ struct Context { /** * @brief instance, headless surface and core with a present queue, fails the test on validation errors + * @details the shared ones of the process, made at the first use, see @ref Context */ struct PresentContext { /** @@ -52,19 +77,29 @@ struct PresentContext { Context::ValidationCheck validationCheck; - Instance instance; - std::array surfaces; - Core core; + Instance const& instance; + Core const& core; - [[nodiscard]] Surface const& surface() const { return surfaces[0]; } + [[nodiscard]] Surface const& surface() const { return _surface; } [[nodiscard]] Queue const& queue() const { return core.queue(0); } +private: + Surface const& _surface; + +public: + PresentContext(PresentContext const& other) = delete; PresentContext(PresentContext&& other) = delete; PresentContext& operator=(PresentContext const& other) = delete; PresentContext& operator=(PresentContext&& other) = delete; }; +/** + * @brief destroys the shared contexts, fails on their validation errors + * @details the test environment calls it before the loader unloads + */ +void destroy_shared_contexts(); + /** * @brief submits the command buffer to the first queue and waits for the gpu * @pre the command buffer is executable diff --git a/cvk/tests/src/environment.cpp b/cvk/tests/src/environment.cpp index 4346f2e5..a69985b7 100644 --- a/cvk/tests/src/environment.cpp +++ b/cvk/tests/src/environment.cpp @@ -1,3 +1,5 @@ +#include "context.hpp" + #include "cvk/base/cvk.hpp" #include @@ -11,7 +13,10 @@ namespace { class Environment : public testing::Environment { public: void SetUp() override { Cvk::init(); } - void TearDown() override { Cvk::terminate(); } + void TearDown() override { + destroy_shared_contexts(); + Cvk::terminate(); + } }; [[maybe_unused]] testing::Environment* const ENVIRONMENT = testing::AddGlobalTestEnvironment(new Environment{}); From c778fb0263360f9f84a74ab8b13720606d634896 Mon Sep 17 00:00:00 2001 From: lukas <89145039+codethinki@users.noreply.github.com> Date: Wed, 21 Oct 2026 09:52:06 +0200 Subject: [PATCH 091/135] cmake: link with lld when it is installed --- CMakeLists.txt | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index e0bcc98a..60b09d33 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -10,6 +10,12 @@ cth_set_latest_c_cpp_standard() set(CMAKE_CXX_SCAN_FOR_MODULES OFF) +# lld links much faster than the default linker +find_program(LLD_LINKER ld.lld) +if(LLD_LINKER AND NOT MSVC) + set(CMAKE_LINKER_TYPE LLD) +endif() + include(lib/cmake/cth.cmake) include(lib/cmake/gtl.cmake) From bb42f03d8efdefbb51400a8262c62e25439ec1cb Mon Sep 17 00:00:00 2001 From: lukas <89145039+codethinki@users.noreply.github.com> Date: Wed, 21 Oct 2026 10:01:09 +0200 Subject: [PATCH 092/135] cvk: array layers in images, views, barriers, copies, blits and renderings, cube map config --- cvk/incl/cvk/data/img/image.hpp | 11 ++--- cvk/incl/cvk/data/img/image_config.hpp | 35 +++++++++++++-- cvk/incl/cvk/data/img/image_view.hpp | 27 +++++++++++- cvk/incl/cvk/data/img/image_view_config.hpp | 18 ++++++-- cvk/incl/cvk/render/cmd/rendering_info.hpp | 10 +++++ cvk/incl/cvk/render/sync/barrier.hpp | 9 +++- cvk/src/data/img/image.cpp | 21 +++++++-- cvk/src/data/img/image_view.cpp | 8 +++- cvk/src/render/cmd/cmd_buffer.cpp | 8 +++- cvk/src/render/sync/barrier.cpp | 11 ++++- cvk/tests/src/data/img/image_test.cpp | 49 +++++++++++++++++++++ cvk/tests/src/data/img/image_view_test.cpp | 25 ++++++++++- cvk/tests/src/render/cmd/rendering_test.cpp | 36 +++++++++++++++ 13 files changed, 246 insertions(+), 22 deletions(-) diff --git a/cvk/incl/cvk/data/img/image.hpp b/cvk/incl/cvk/data/img/image.hpp index a6d5b6fe..c2a51172 100644 --- a/cvk/incl/cvk/data/img/image.hpp +++ b/cvk/incl/cvk/data/img/image.hpp @@ -97,8 +97,8 @@ class Image { State release(); /** - * @brief records a copy from the buffer into the mip level - * @param srcOffset in bytes + * @brief records a copy from the buffer into every layer of the mip level + * @param srcOffset in bytes, the layers follow each other in the buffer * @param layout of the mip level, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL or VK_IMAGE_LAYOUT_GENERAL * @pre @ref created() and src created * @pre @ref BaseCmdBuffer::recording() @@ -115,8 +115,8 @@ class Image { ) const; /** - * @brief records a copy from the mip level into the buffer - * @param dstOffset in bytes + * @brief records a copy from every layer of the mip level into the buffer + * @param dstOffset in bytes, the layers follow each other in the buffer * @param layout of the mip level, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL or VK_IMAGE_LAYOUT_GENERAL * @pre @ref created() and dst created * @pre @ref BaseCmdBuffer::recording() @@ -134,7 +134,7 @@ class Image { ) const; /** - * @brief records blits that fill each mip level from the level before it + * @brief records blits that fill each mip level from the level before it, every layer * @param firstMipLevel first level to fill, the blits read from firstMipLevel - 1 * @param mipLevels (VK_REMAINING_MIP_LEVELS => to the last mip level) * @param filter depth formats need VK_FILTER_NEAREST @@ -176,6 +176,7 @@ class Image { return _config.aspectMask != 0 ? _config.aspectMask : format_aspects(_config.format); } [[nodiscard]] constexpr uint32_t mipLevels() const { return _config.mipLevels; } + [[nodiscard]] constexpr uint32_t arrayLayers() const { return _config.arrayLayers; } /** * @return nullptr -> the image does not own its memory diff --git a/cvk/incl/cvk/data/img/image_config.hpp b/cvk/incl/cvk/data/img/image_config.hpp index 889ca11f..f296c2f6 100644 --- a/cvk/incl/cvk/data/img/image_config.hpp +++ b/cvk/incl/cvk/data/img/image_config.hpp @@ -1,4 +1,4 @@ -#pragma once + #pragma once #include "image_format.hpp" #include "cvk/base/queue/queue_family.hpp" @@ -14,7 +14,7 @@ namespace cvk { /** - * @brief 2D image: format, usage, mip levels, who uses it and where its memory lives + * @brief 2D image: format, usage, mip levels, layers, who uses it and where its memory lives */ struct ImageConfig { /** @@ -45,6 +45,12 @@ struct ImageConfig { */ uint32_t mipLevels = 1; + /** + * @pre >= 1 + * @pre VK_IMAGE_CREATE_CUBE_COMPATIBLE_BIT -> a multiple of 6, one layer per face + */ + uint32_t arrayLayers = 1; + VkSampleCountFlagBits samples = VK_SAMPLE_COUNT_1_BIT; /** @@ -76,6 +82,22 @@ struct ImageConfig { }; } + /** + * @brief sampled cube map, the six faces are the layers + * @param mipLevels > 1 -> transfer src usage for @ref Image::blitMipLevels() + * @details the faces: +x, -x, +y, -y, +z, -z + */ + [[nodiscard]] static constexpr ImageConfig CubeMap(VkFormat format, uint32_t mipLevels = 1) { + return ImageConfig{ + .format = format, + .usage = VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT + | (mipLevels > 1 ? VK_IMAGE_USAGE_TRANSFER_SRC_BIT : VkImageUsageFlags{0}), + .flags = VK_IMAGE_CREATE_CUBE_COMPATIBLE_BIT, + .mipLevels = mipLevels, + .arrayLayers = 6, + }; + } + /** * @attention pQueueFamilyIndices points into @ref queueFamilies */ @@ -88,7 +110,7 @@ struct ImageConfig { .format = format, .extent = {extent.width, extent.height, 1}, .mipLevels = mipLevels, - .arrayLayers = 1, + .arrayLayers = arrayLayers, .samples = samples, .tiling = tiling, .usage = usage, @@ -112,6 +134,13 @@ constexpr void ImageConfig::debug_check(ImageConfig const& config) { ) {} CTH_CRITICAL(config.usage == 0, "usage must not be 0") {} CTH_CRITICAL(config.mipLevels == 0, "at least one mip level is required") {} + CTH_CRITICAL(config.arrayLayers == 0, "at least one layer is required") {} + CTH_CRITICAL( + (config.flags & VK_IMAGE_CREATE_CUBE_COMPATIBLE_BIT) != 0 && config.arrayLayers % 6 != 0, + "a cube compatible image needs a multiple of 6 layers" + ) { + details->add("layers: {}", config.arrayLayers); + } CTH_CRITICAL( config.initialLayout != VK_IMAGE_LAYOUT_UNDEFINED && config.initialLayout != VK_IMAGE_LAYOUT_PREINITIALIZED, "initial layout must be undefined or preinitialized" diff --git a/cvk/incl/cvk/data/img/image_view.hpp b/cvk/incl/cvk/data/img/image_view.hpp index f6c7c20a..56db7972 100644 --- a/cvk/incl/cvk/data/img/image_view.hpp +++ b/cvk/incl/cvk/data/img/image_view.hpp @@ -14,7 +14,7 @@ namespace cvk { /** - * @brief VkImageView wrapper, 2D view of a mip range of its image + * @brief VkImageView wrapper, view of a mip and layer range of its image */ class ImageView { public: @@ -60,7 +60,8 @@ class ImageView { /** * @brief creates the view * @pre @ref Image::created() - * @pre the mip range lies in the image + * @pre the mip range and the layer range lie in the image + * @pre the layers fit the view type * @post @ref created() * @note calls @ref optDestroy() * @throws cvk::vk_result_exception result of vkCreateImageView() @@ -98,6 +99,28 @@ class ImageView { */ [[nodiscard]] constexpr VkImageSubresourceRange range() const { return _config.range(_image->aspectMask()); } + /** + * @return the layers of the view + */ + [[nodiscard]] constexpr uint32_t arrayLayers() const { + return _config.arrayLayers == VK_REMAINING_ARRAY_LAYERS ? _image->arrayLayers() - _config.baseArrayLayer + : _config.arrayLayers; + } + + /** + * @return @ref Config::viewType, VK_IMAGE_VIEW_TYPE_MAX_ENUM -> inferred: + * one layer -> 2D, cube compatible image with 6 layers -> cube, a multiple of 6 -> cube array, else 2D array + */ + [[nodiscard]] constexpr VkImageViewType viewType() const { + if(_config.viewType != VK_IMAGE_VIEW_TYPE_MAX_ENUM) return _config.viewType; + + auto const layers = arrayLayers(); + if(layers == 1) return VK_IMAGE_VIEW_TYPE_2D; + if((_image->config().flags & VK_IMAGE_CREATE_CUBE_COMPATIBLE_BIT) == 0 || layers % 6 != 0) + return VK_IMAGE_VIEW_TYPE_2D_ARRAY; + return layers == 6 ? VK_IMAGE_VIEW_TYPE_CUBE : VK_IMAGE_VIEW_TYPE_CUBE_ARRAY; + } + private: void forget(); diff --git a/cvk/incl/cvk/data/img/image_view_config.hpp b/cvk/incl/cvk/data/img/image_view_config.hpp index 04003be3..ec07d419 100644 --- a/cvk/incl/cvk/data/img/image_view_config.hpp +++ b/cvk/incl/cvk/data/img/image_view_config.hpp @@ -4,9 +4,14 @@ namespace cvk { /** - * @brief mip range and aspect of an image view + * @brief type, mip range, layer range and aspect of an image view */ struct ImageViewConfig { + /** + * @brief VK_IMAGE_VIEW_TYPE_MAX_ENUM -> inferred, see @ref ImageView::viewType() + */ + VkImageViewType viewType = VK_IMAGE_VIEW_TYPE_MAX_ENUM; + uint32_t baseMipLevel = 0; /** @@ -19,6 +24,13 @@ struct ImageViewConfig { */ VkImageAspectFlags aspectMask = 0; + uint32_t baseArrayLayer = 0; + + /** + * @brief VK_REMAINING_ARRAY_LAYERS -> to the last layer of the image + */ + uint32_t arrayLayers = VK_REMAINING_ARRAY_LAYERS; + /** * @param imageAspectMask aspect mask of the image */ @@ -27,8 +39,8 @@ struct ImageViewConfig { .aspectMask = aspectMask != 0 ? aspectMask : imageAspectMask, .baseMipLevel = baseMipLevel, .levelCount = mipLevels, - .baseArrayLayer = 0, - .layerCount = 1, + .baseArrayLayer = baseArrayLayer, + .layerCount = arrayLayers, }; } }; diff --git a/cvk/incl/cvk/render/cmd/rendering_info.hpp b/cvk/incl/cvk/render/cmd/rendering_info.hpp index 7e35edc8..69f4d3b6 100644 --- a/cvk/incl/cvk/render/cmd/rendering_info.hpp +++ b/cvk/incl/cvk/render/cmd/rendering_info.hpp @@ -169,6 +169,16 @@ struct RenderingInfo { */ std::optional area = std::nullopt; + /** + * @brief layers that the rendering writes, viewMask != 0 -> ignored + * @pre the attachment views have the layers + */ + uint32_t layerCount = 1; + + /** + * @brief multiview, bit i -> view i renders into layer i + * @pre the attachment views have a layer per view + */ uint32_t viewMask = 0; /** diff --git a/cvk/incl/cvk/render/sync/barrier.hpp b/cvk/incl/cvk/render/sync/barrier.hpp index 1acf5913..f107cbbc 100644 --- a/cvk/incl/cvk/render/sync/barrier.hpp +++ b/cvk/incl/cvk/render/sync/barrier.hpp @@ -55,6 +55,13 @@ struct ImageBarrierInfo { */ uint32_t mipLevels = VK_REMAINING_MIP_LEVELS; + uint32_t firstArrayLayer = 0; + + /** + * @brief VK_REMAINING_ARRAY_LAYERS -> to the last layer + */ + uint32_t arrayLayers = VK_REMAINING_ARRAY_LAYERS; + /** * @brief 0 -> the aspect mask of the image */ @@ -172,7 +179,7 @@ struct Barrier { constexpr void add(Image const& image, ImageInfo const& info) { images.push_back({&image, info}); } /** - * @brief adds a layout transition with the scopes of @ref layout_transition() + * @brief adds a layout transition of every layer with the scopes of @ref layout_transition() * @param shaderStages stages of the shader reads in the layouts (@ref INFERRED_SHADER_STAGES => fragment shader) * @param mipLevels (VK_REMAINING_MIP_LEVELS => to the last mip level) * @pre @ref layout_scope() knows both layouts diff --git a/cvk/src/data/img/image.cpp b/cvk/src/data/img/image.cpp index 730827ad..36dd1ca8 100644 --- a/cvk/src/data/img/image.cpp +++ b/cvk/src/data/img/image.cpp @@ -30,6 +30,11 @@ void Image::wrap(State const& state) { } void Image::create(VkExtent2D extent) { + CTH_CRITICAL( + (_config.flags & VK_IMAGE_CREATE_CUBE_COMPATIBLE_BIT) != 0 && extent.width != extent.height, + "a cube compatible image needs a square extent" + ) {} + optDestroy(); auto const imageInfo = _config.createInfo(extent); @@ -200,9 +205,19 @@ void Image::blitMipLevels( VkImageBlit2 const region{ .sType = VK_STRUCTURE_TYPE_IMAGE_BLIT_2, .pNext = nullptr, - .srcSubresource = {.aspectMask = aspectMask(), .mipLevel = level - 1, .baseArrayLayer = 0, .layerCount = 1}, + .srcSubresource = { + .aspectMask = aspectMask(), + .mipLevel = level - 1, + .baseArrayLayer = 0, + .layerCount = arrayLayers(), + }, .srcOffsets = {{0, 0, 0}, corner(level - 1)}, - .dstSubresource = {.aspectMask = aspectMask(), .mipLevel = level, .baseArrayLayer = 0, .layerCount = 1}, + .dstSubresource = { + .aspectMask = aspectMask(), + .mipLevel = level, + .baseArrayLayer = 0, + .layerCount = arrayLayers(), + }, .dstOffsets = {{0, 0, 0}, corner(level)}, }; VkBlitImageInfo2 const info{ @@ -243,7 +258,7 @@ VkBufferImageCopy2 Image::bufferCopy( .aspectMask = aspectMask(), .mipLevel = mipLevel, .baseArrayLayer = 0, - .layerCount = 1, + .layerCount = arrayLayers(), }, .imageOffset = {0, 0, 0}, .imageExtent = {std::max(_extent.width >> mipLevel, 1u), std::max(_extent.height >> mipLevel, 1u), 1}, diff --git a/cvk/src/data/img/image_view.cpp b/cvk/src/data/img/image_view.cpp index ec50ee84..734fbc92 100644 --- a/cvk/src/data/img/image_view.cpp +++ b/cvk/src/data/img/image_view.cpp @@ -27,6 +27,12 @@ void ImageView::create() { _config.mipLevels != VK_REMAINING_MIP_LEVELS && _config.mipLevels > _image->mipLevels() - _config.baseMipLevel, "mip levels must lie in the image" ) {} + CTH_CRITICAL(_config.baseArrayLayer >= _image->arrayLayers(), "base array layer must lie in the image") {} + CTH_CRITICAL( + _config.arrayLayers != VK_REMAINING_ARRAY_LAYERS + && _config.arrayLayers > _image->arrayLayers() - _config.baseArrayLayer, + "array layers must lie in the image" + ) {} optDestroy(); @@ -35,7 +41,7 @@ void ImageView::create() { .pNext = nullptr, .flags = 0, .image = _image->get(), - .viewType = VK_IMAGE_VIEW_TYPE_2D, + .viewType = viewType(), .format = _image->format(), .components = { .r = VK_COMPONENT_SWIZZLE_IDENTITY, diff --git a/cvk/src/render/cmd/cmd_buffer.cpp b/cvk/src/render/cmd/cmd_buffer.cpp index 0fc52f00..a49d9584 100644 --- a/cvk/src/render/cmd/cmd_buffer.cpp +++ b/cvk/src/render/cmd/cmd_buffer.cpp @@ -10,6 +10,7 @@ #include "cvk/utility/vk_exceptions.hpp" #include +#include #include @@ -176,6 +177,11 @@ void BaseCmdBuffer::beginRendering(RenderingInfo const& info) { ImageView const& view = *attachment.view; ImageView::debug_check(view); CTH_CRITICAL(view_mip_levels(view) != 1, "attachment views must have one mip level") {} + // multiview: view i renders into layer i + auto const layers = info.viewMask != 0 ? static_cast(std::bit_width(info.viewMask)) : info.layerCount; + CTH_CRITICAL(view.arrayLayers() < layers, "attachment views need a layer per rendered layer or view") { + details->add("view layers: {}, needed: {}", view.arrayLayers(), layers); + } auto const viewExtent = view_extent(view); extent = extent.has_value() @@ -253,7 +259,7 @@ void BaseCmdBuffer::beginRendering(RenderingInfo const& info) { .pNext = nullptr, .flags = info.flags, .renderArea = info.area.value_or(VkRect2D{.offset = {0, 0}, .extent = extent.value_or(VkExtent2D{})}), - .layerCount = 1, + .layerCount = info.layerCount, .viewMask = info.viewMask, .colorAttachmentCount = static_cast(colors.size()), .pColorAttachments = colors.data(), diff --git a/cvk/src/render/sync/barrier.cpp b/cvk/src/render/sync/barrier.cpp index 71fbc887..e2837a3c 100644 --- a/cvk/src/render/sync/barrier.cpp +++ b/cvk/src/render/sync/barrier.cpp @@ -69,6 +69,13 @@ namespace { info.mipLevels != VK_REMAINING_MIP_LEVELS && info.mipLevels > image.mipLevels() - info.firstMipLevel, "mip levels must lie in the image" ) {} + CTH_CRITICAL(info.arrayLayers == 0, "array layers must not be 0") {} + CTH_CRITICAL(info.firstArrayLayer >= image.arrayLayers(), "first array layer must lie in the image") {} + CTH_CRITICAL( + info.arrayLayers != VK_REMAINING_ARRAY_LAYERS + && info.arrayLayers > image.arrayLayers() - info.firstArrayLayer, + "array layers must lie in the image" + ) {} bool const transfer = info.src.queueFamilyIndex != info.dst.queueFamilyIndex; CTH_CRITICAL( @@ -95,8 +102,8 @@ namespace { .aspectMask = info.aspectMask != 0 ? info.aspectMask : image.aspectMask(), .baseMipLevel = info.firstMipLevel, .levelCount = info.mipLevels, - .baseArrayLayer = 0, - .layerCount = 1, + .baseArrayLayer = info.firstArrayLayer, + .layerCount = info.arrayLayers, }, }; } diff --git a/cvk/tests/src/data/img/image_test.cpp b/cvk/tests/src/data/img/image_test.cpp index 60cd1f56..eb3d498c 100644 --- a/cvk/tests/src/data/img/image_test.cpp +++ b/cvk/tests/src/data/img/image_test.cpp @@ -127,6 +127,16 @@ CVK_TEST(Image, config_depth_buffer) { static_assert((Image::Config::debug_check(config), true)); } +CVK_TEST(Image, config_cube_map) { + constexpr auto config = Image::Config::CubeMap(VK_FORMAT_R8G8B8A8_UNORM, 3); + static_assert(config.arrayLayers == 6); + static_assert((config.flags & VK_IMAGE_CREATE_CUBE_COMPATIBLE_BIT) != 0); + static_assert((config.usage & VK_IMAGE_USAGE_TRANSFER_SRC_BIT) != 0); + static_assert((Image::Config::CubeMap(VK_FORMAT_R8G8B8A8_UNORM).usage & VK_IMAGE_USAGE_TRANSFER_SRC_BIT) == 0); + static_assert(config.createInfo({8, 8}).arrayLayers == 6); + static_assert((Image::Config::debug_check(config), true)); +} + CVK_TEST(Image, base_constructor_does_not_create) { test::Context const context{}; @@ -297,6 +307,45 @@ CVK_TEST(Image, blit_mip_levels_fills_the_mip_chain) { EXPECT_TRUE(std::ranges::all_of(mapped, [](uint32_t texel) { return texel == color; })); } +CVK_TEST(Image, cube_map_copies_and_blits_every_face) { + test::Context const context{}; + constexpr VkExtent2D extent{8, 8}; + Image const image{ + context.core, + Image::Config::CubeMap(VK_FORMAT_R8G8B8A8_UNORM, Image::evalMipLevelCount(extent)), + extent + }; + + // one color per face, linear blits keep it, the faces follow each other in the buffer + auto const color = [](uint32_t face) { return 0x01'01'01'01u * (face + 1) * 30; }; + size_t const faceTexels = size_t{extent.width} * extent.height; + std::vector texels(6 * faceTexels); + for(uint32_t face = 0; face < 6; ++face) + std::ranges::fill(std::span{texels}.subspan(face * faceTexels, faceTexels), color(face)); + + BaseBuffer const staging{context.core, STAGING_CONFIG, texels.size() * sizeof(uint32_t)}; + staging.write(std::as_bytes(std::span{texels})); + Buffer const readback{context.core, READBACK_CONFIG, 6}; + + test::record_and_submit(context, [&](PrimaryCmdBuffer const& primary) { + Barrier toDst{}; + toDst.addTransition(image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_UNDEFINED); + toDst.execute(primary); + + image.copy(primary, staging); + image.blitMipLevels(primary); + + // the last level has one texel per face + image.copyTo(primary, readback, 0, image.mipLevels() - 1); + test::host_read_barrier(context, primary); + }); + + auto const mapped = readback.map(); + readback.invalidate(); + for(uint32_t face = 0; face < 6; ++face) + EXPECT_EQ(mapped[face], color(face)); +} + CVK_TEST(Image, release_and_wrap) { test::Context const context{}; Image image{context.core, TEXTURE_CONFIG, EXTENT}; diff --git a/cvk/tests/src/data/img/image_view_test.cpp b/cvk/tests/src/data/img/image_view_test.cpp index bf252a32..c6e1a98e 100644 --- a/cvk/tests/src/data/img/image_view_test.cpp +++ b/cvk/tests/src/data/img/image_view_test.cpp @@ -24,12 +24,35 @@ CVK_TEST(ImageView, config_range) { static_assert(range.aspectMask == VK_IMAGE_ASPECT_COLOR_BIT); static_assert(range.baseMipLevel == 2); static_assert(range.levelCount == VK_REMAINING_MIP_LEVELS); - static_assert(range.layerCount == 1); + static_assert(range.baseArrayLayer == 0); + static_assert(range.layerCount == VK_REMAINING_ARRAY_LAYERS); constexpr ImageView::Config depth{.aspectMask = VK_IMAGE_ASPECT_DEPTH_BIT}; static_assert(depth.range(VK_IMAGE_ASPECT_COLOR_BIT).aspectMask == VK_IMAGE_ASPECT_DEPTH_BIT); } +CVK_TEST(ImageView, view_type_follows_the_layers) { + test::Context const context{}; + Image const flat{context.core, TEXTURE_CONFIG, EXTENT}; + auto layeredConfig = TEXTURE_CONFIG; + layeredConfig.arrayLayers = 4; + Image const layered{context.core, layeredConfig, EXTENT}; + Image const cube{context.core, Image::Config::CubeMap(VK_FORMAT_R8G8B8A8_UNORM), EXTENT}; + + ImageView const flatView{flat, {}, create}; + ImageView const arrayView{layered, {}, create}; + ImageView const cubeView{cube, {}, create}; + ImageView const faceView{cube, {.baseArrayLayer = 2, .arrayLayers = 1}, create}; + ImageView const facesView{cube, {.viewType = VK_IMAGE_VIEW_TYPE_2D_ARRAY}, create}; + + EXPECT_EQ(flatView.viewType(), VK_IMAGE_VIEW_TYPE_2D); + EXPECT_EQ(arrayView.viewType(), VK_IMAGE_VIEW_TYPE_2D_ARRAY); + EXPECT_EQ(arrayView.arrayLayers(), 4); + EXPECT_EQ(cubeView.viewType(), VK_IMAGE_VIEW_TYPE_CUBE); + EXPECT_EQ(faceView.viewType(), VK_IMAGE_VIEW_TYPE_2D); + EXPECT_EQ(facesView.viewType(), VK_IMAGE_VIEW_TYPE_2D_ARRAY); +} + CVK_TEST(ImageView, base_constructor_does_not_create) { test::Context const context{}; Image const image{context.core, TEXTURE_CONFIG, EXTENT}; diff --git a/cvk/tests/src/render/cmd/rendering_test.cpp b/cvk/tests/src/render/cmd/rendering_test.cpp index e8f950e3..db7d35d9 100644 --- a/cvk/tests/src/render/cmd/rendering_test.cpp +++ b/cvk/tests/src/render/cmd/rendering_test.cpp @@ -88,6 +88,42 @@ CVK_TEST(Rendering, clear_a_color_attachment) { EXPECT_TRUE(std::ranges::equal(pixel, red, {}, asByte)); } +CVK_TEST(Rendering, clear_every_layer) { + test::Context const context{}; + auto config = COLOR_CONFIG; + config.arrayLayers = 2; + Image const image{context.core, config, EXTENT}; + ImageView const view{image, {}, create}; + size_t const layerSize = size_t{EXTENT.width} * EXTENT.height * 4; + BaseBuffer const readback{ + context.core, + {.usage = VK_BUFFER_USAGE_TRANSFER_DST_BIT, .memory = {.flags = VMA_ALLOCATION_CREATE_HOST_ACCESS_RANDOM_BIT}}, + 2 * layerSize + }; + + test::record_and_submit(context, [&](PrimaryCmdBuffer& cmd) { + Barrier transition{}; + transition.addTransition(image, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, VK_IMAGE_LAYOUT_UNDEFINED); + transition.execute(cmd); + + cmd.beginRendering({.colorAttachments = {clear_red(view)}, .layerCount = 2}); + cmd.endRendering(); + + Barrier toTransfer{}; + toTransfer.addTransition(image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL); + toTransfer.execute(cmd); + image.copyTo(cmd, readback); + test::host_read_barrier(context, cmd); + }); + + auto const pixels = readback.map(); + readback.invalidate(); + auto const red = std::array{255, 0, 0, 255}; + auto const asByte = [](std::byte b) { return std::to_integer(b); }; + for(auto const pixel : pixels | std::views::chunk(4)) + EXPECT_TRUE(std::ranges::equal(pixel, red, {}, asByte)); +} + CVK_TEST(Rendering, tracks_the_current_rendering) { test::Context const context{}; Image const image{context.core, COLOR_CONFIG, EXTENT}; From e21d70398d3181b97d4dad0dfd360389a3699ca8 Mon Sep 17 00:00:00 2001 From: lukas <89145039+codethinki@users.noreply.github.com> Date: Wed, 21 Oct 2026 10:07:55 +0200 Subject: [PATCH 093/135] cvk: check values in the cth log messages instead of details lines --- cvk/incl/cvk/data/img/image_config.hpp | 7 ++- cvk/incl/cvk/render/cmd/rendering_info.hpp | 16 +++---- .../pipeline/layout/descriptor_set_layout.hpp | 14 +++--- cvk/incl/cvk/render/sync/barrier.hpp | 8 ++-- cvk/src/data/buffer/base_buffer.cpp | 13 +++-- cvk/src/data/descriptor/descriptor_pool.cpp | 8 +--- cvk/src/data/descriptor/descriptor_set.cpp | 26 ++++++---- cvk/src/data/img/image.cpp | 14 +++--- cvk/src/render/cmd/cmd_buffer.cpp | 47 ++++++++++--------- cvk/src/render/cmd/cmd_pool.cpp | 8 +--- .../pipeline/layout/pipeline_layout.cpp | 8 +++- cvk/src/render/pipeline/shader/shader.cpp | 25 +++++----- cvk/src/render/sync/barrier.cpp | 25 +++++----- cvk/src/surface/surface.cpp | 6 +-- cvk/src/surface/swapchain.cpp | 8 +--- 15 files changed, 117 insertions(+), 116 deletions(-) diff --git a/cvk/incl/cvk/data/img/image_config.hpp b/cvk/incl/cvk/data/img/image_config.hpp index f296c2f6..34ebeb11 100644 --- a/cvk/incl/cvk/data/img/image_config.hpp +++ b/cvk/incl/cvk/data/img/image_config.hpp @@ -137,10 +137,9 @@ constexpr void ImageConfig::debug_check(ImageConfig const& config) { CTH_CRITICAL(config.arrayLayers == 0, "at least one layer is required") {} CTH_CRITICAL( (config.flags & VK_IMAGE_CREATE_CUBE_COMPATIBLE_BIT) != 0 && config.arrayLayers % 6 != 0, - "a cube compatible image needs a multiple of 6 layers" - ) { - details->add("layers: {}", config.arrayLayers); - } + "a cube compatible image needs a multiple of 6 layers, got {}", + config.arrayLayers + ) {} CTH_CRITICAL( config.initialLayout != VK_IMAGE_LAYOUT_UNDEFINED && config.initialLayout != VK_IMAGE_LAYOUT_PREINITIALIZED, "initial layout must be undefined or preinitialized" diff --git a/cvk/incl/cvk/render/cmd/rendering_info.hpp b/cvk/incl/cvk/render/cmd/rendering_info.hpp index 69f4d3b6..f48336df 100644 --- a/cvk/incl/cvk/render/cmd/rendering_info.hpp +++ b/cvk/incl/cvk/render/cmd/rendering_info.hpp @@ -211,15 +211,15 @@ constexpr bool AttachmentMapping::operator==(AttachmentMapping const& other) con constexpr void AttachmentMapping::debug_check(AttachmentMapping const& mapping, size_t colorAttachments) { CTH_CRITICAL( !mapping.locations.empty() && mapping.locations.size() != colorAttachments, - "one location per color attachment" - ) { - details->add("locations: {}, color attachments: {}", mapping.locations.size(), colorAttachments); - } + "one location per color attachment, got {} for {}", + mapping.locations.size(), + colorAttachments + ) {} CTH_CRITICAL( !mapping.colorInputIndices.empty() && mapping.colorInputIndices.size() != colorAttachments, - "one input attachment index per color attachment" - ) { - details->add("indices: {}, color attachments: {}", mapping.colorInputIndices.size(), colorAttachments); - } + "one input attachment index per color attachment, got {} for {}", + mapping.colorInputIndices.size(), + colorAttachments + ) {} } } diff --git a/cvk/incl/cvk/render/pipeline/layout/descriptor_set_layout.hpp b/cvk/incl/cvk/render/pipeline/layout/descriptor_set_layout.hpp index bccf6a96..5e845e8f 100644 --- a/cvk/incl/cvk/render/pipeline/layout/descriptor_set_layout.hpp +++ b/cvk/incl/cvk/render/pipeline/layout/descriptor_set_layout.hpp @@ -238,17 +238,15 @@ constexpr void DescriptorSetLayout::Config::debug_check(Config const& config) { CTH_CRITICAL( (flags & VK_DESCRIPTOR_BINDING_VARIABLE_DESCRIPTOR_COUNT_BIT) != 0 && static_cast(index) + 1 != config.bindings.size(), - "only the binding with the highest number can have a variable descriptor count" - ) { - details->add("binding: {}", binding.binding); - } + "only the binding with the highest number can have a variable descriptor count, got binding {}", + binding.binding + ) {} CTH_CRITICAL( dynamic && (flags & (VK_DESCRIPTOR_BINDING_UPDATE_AFTER_BIND_BIT | VK_DESCRIPTOR_BINDING_VARIABLE_DESCRIPTOR_COUNT_BIT)) != 0, - "dynamic buffer bindings cannot update after bind or have a variable descriptor count" - ) { - details->add("binding: {}", binding.binding); - } + "dynamic buffer bindings cannot update after bind or have a variable descriptor count, got binding {}", + binding.binding + ) {} } } diff --git a/cvk/incl/cvk/render/sync/barrier.hpp b/cvk/incl/cvk/render/sync/barrier.hpp index f107cbbc..53585d06 100644 --- a/cvk/incl/cvk/render/sync/barrier.hpp +++ b/cvk/incl/cvk/render/sync/barrier.hpp @@ -290,9 +290,11 @@ constexpr std::optional layout_scope( | VK_PIPELINE_STAGE_2_TASK_SHADER_BIT_EXT | VK_PIPELINE_STAGE_2_MESH_SHADER_BIT_EXT | VK_PIPELINE_STAGE_2_RAY_TRACING_SHADER_BIT_KHR; - CTH_CRITICAL((shaderStages & ~shaderStageMask) != 0, "shader stages must only have shader stage bits") { - details->add("shader stages: {:#x}", shaderStages); - } + CTH_CRITICAL( + (shaderStages & ~shaderStageMask) != 0, + "shader stages must only have shader stage bits, got {:#x}", + shaderStages + ) {} auto const shaders = shaderStages != Barrier::INFERRED_SHADER_STAGES ? shaderStages diff --git a/cvk/src/data/buffer/base_buffer.cpp b/cvk/src/data/buffer/base_buffer.cpp index dfbb58a5..2af0b0a6 100644 --- a/cvk/src/data/buffer/base_buffer.cpp +++ b/cvk/src/data/buffer/base_buffer.cpp @@ -96,11 +96,14 @@ void BaseBuffer::copy(BaseCmdBuffer const& cmdBuffer, BaseBuffer const& src, Buf CTH_CRITICAL(copySize == 0, "copy size must not be 0") {} CTH_CRITICAL( copySize > src._size - srcOffset || copySize > _size - dstOffset, - "copy range must lie in the buffers" - ) { - details->add("src: {} + {} > {} (offset + size > src size)", srcOffset, copySize, src._size); - details->add("dst: {} + {} > {} (offset + size > dst size)", dstOffset, copySize, _size); - } + "copy range must lie in the buffers, got src {} + {} of {}, dst {} + {} of {}", + srcOffset, + copySize, + src._size, + dstOffset, + copySize, + _size + ) {} VkBufferCopy2 const vkRegion{ .sType = VK_STRUCTURE_TYPE_BUFFER_COPY_2, diff --git a/cvk/src/data/descriptor/descriptor_pool.cpp b/cvk/src/data/descriptor/descriptor_pool.cpp index 0ac5fec4..da5fa673 100644 --- a/cvk/src/data/descriptor/descriptor_pool.cpp +++ b/cvk/src/data/descriptor/descriptor_pool.cpp @@ -43,9 +43,7 @@ void DescriptorPool::create() { void DescriptorPool::destroy() { debug_check(*this); - CTH_CRITICAL(!unused(), "descriptor sets of the pool must be destroyed first") { - details->add("descriptor sets: {}", _sets); - } + CTH_CRITICAL(!unused(), "descriptor sets of the pool must be destroyed first, got {}", _sets) {} _handle.reset(); @@ -54,9 +52,7 @@ void DescriptorPool::destroy() { DescriptorPool::State DescriptorPool::release() { debug_check(*this); - CTH_CRITICAL(!unused(), "descriptor sets of the pool must be destroyed first") { - details->add("descriptor sets: {}", _sets); - } + CTH_CRITICAL(!unused(), "descriptor sets of the pool must be destroyed first, got {}", _sets) {} State const state{_handle.release()}; forget(); diff --git a/cvk/src/data/descriptor/descriptor_set.cpp b/cvk/src/data/descriptor/descriptor_set.cpp index 2071eed9..02a31de4 100644 --- a/cvk/src/data/descriptor/descriptor_set.cpp +++ b/cvk/src/data/descriptor/descriptor_set.cpp @@ -176,18 +176,24 @@ void DescriptorSet::write(span writes) const { auto const kind = descriptor_kind(type); auto const* buffer = std::get_if(&write._resource); - CTH_CRITICAL(kind == DescriptorKind::UNSUPPORTED, "descriptor type not supported") { - details->add("binding: {}, type: {}", write._binding, type); - } + CTH_CRITICAL( + kind == DescriptorKind::UNSUPPORTED, + "descriptor type {} of binding {} not supported", + type, + write._binding + ) {} CTH_CRITICAL( (kind == DescriptorKind::BUFFER) != (buffer != nullptr), - "the kind of the write must match the type of the binding" - ) { - details->add("binding: {}, type: {}", write._binding, type); - } - CTH_CRITICAL(write._element >= capacity(*binding), "the element must lie in the binding") { - details->add("binding: {}, element: {}", write._binding, write._element); - } + "the kind of the write must match the type of the binding, got binding {} of type {}", + write._binding, + type + ) {} + CTH_CRITICAL( + write._element >= capacity(*binding), + "the element must lie in the binding, got element {} of binding {}", + write._element, + write._binding + ) {} VkWriteDescriptorSet vkWrite{ .sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, diff --git a/cvk/src/data/img/image.cpp b/cvk/src/data/img/image.cpp index 36dd1ca8..e3795623 100644 --- a/cvk/src/data/img/image.cpp +++ b/cvk/src/data/img/image.cpp @@ -103,10 +103,9 @@ void Image::copy( CTH_CRITICAL( layout != VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL && layout != VK_IMAGE_LAYOUT_GENERAL, - "mip level must be in VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL or VK_IMAGE_LAYOUT_GENERAL" - ) { - details->add("layout: {}", layout); - } + "mip level must be in VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL or VK_IMAGE_LAYOUT_GENERAL, got {}", + layout + ) {} VkCopyBufferToImageInfo2 const info{ .sType = VK_STRUCTURE_TYPE_COPY_BUFFER_TO_IMAGE_INFO_2, @@ -139,10 +138,9 @@ void Image::copyTo( CTH_CRITICAL( layout != VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL && layout != VK_IMAGE_LAYOUT_GENERAL, - "mip level must be in VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL or VK_IMAGE_LAYOUT_GENERAL" - ) { - details->add("layout: {}", layout); - } + "mip level must be in VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL or VK_IMAGE_LAYOUT_GENERAL, got {}", + layout + ) {} VkCopyImageToBufferInfo2 const info{ .sType = VK_STRUCTURE_TYPE_COPY_IMAGE_TO_BUFFER_INFO_2, diff --git a/cvk/src/render/cmd/cmd_buffer.cpp b/cvk/src/render/cmd/cmd_buffer.cpp index a49d9584..b59ad82d 100644 --- a/cvk/src/render/cmd/cmd_buffer.cpp +++ b/cvk/src/render/cmd/cmd_buffer.cpp @@ -23,10 +23,9 @@ namespace { layout == VK_IMAGE_LAYOUT_UNDEFINED || layout == VK_IMAGE_LAYOUT_PREINITIALIZED || layout == VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL || layout == VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL || layout == VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL || layout == VK_IMAGE_LAYOUT_PRESENT_SRC_KHR, - "attachment must be in an attachment layout" - ) { - details->add("layout: {}", layout); - } + "attachment must be in an attachment layout, got {}", + layout + ) {} } constexpr bool dynamic_buffer(VkDescriptorType type) { @@ -179,9 +178,12 @@ void BaseCmdBuffer::beginRendering(RenderingInfo const& info) { CTH_CRITICAL(view_mip_levels(view) != 1, "attachment views must have one mip level") {} // multiview: view i renders into layer i auto const layers = info.viewMask != 0 ? static_cast(std::bit_width(info.viewMask)) : info.layerCount; - CTH_CRITICAL(view.arrayLayers() < layers, "attachment views need a layer per rendered layer or view") { - details->add("view layers: {}, needed: {}", view.arrayLayers(), layers); - } + CTH_CRITICAL( + view.arrayLayers() < layers, + "attachment views need a layer per rendered layer or view, got {} for {}", + view.arrayLayers(), + layers + ) {} auto const viewExtent = view_extent(view); extent = extent.has_value() @@ -221,9 +223,11 @@ void BaseCmdBuffer::beginRendering(RenderingInfo const& info) { colors.push_back(attachmentInfo(attachment)); auto const format = attachment.view->image().format(); - CTH_CRITICAL(format_aspects(format) != VK_IMAGE_ASPECT_COLOR_BIT, "color attachments need a color format") { - details->add("format: {}", format); - } + CTH_CRITICAL( + format_aspects(format) != VK_IMAGE_ASPECT_COLOR_BIT, + "color attachments need a color format, got {}", + format + ) {} } std::optional depth{}; @@ -233,10 +237,9 @@ void BaseCmdBuffer::beginRendering(RenderingInfo const& info) { auto const format = info.depthAttachment->view->image().format(); CTH_CRITICAL( (format_aspects(format) & VK_IMAGE_ASPECT_DEPTH_BIT) == 0, - "the depth attachment needs a depth format" - ) { - details->add("format: {}", format); - } + "the depth attachment needs a depth format, got {}", + format + ) {} } std::optional stencil{}; @@ -246,10 +249,9 @@ void BaseCmdBuffer::beginRendering(RenderingInfo const& info) { auto const format = info.stencilAttachment->view->image().format(); CTH_CRITICAL( (format_aspects(format) & VK_IMAGE_ASPECT_STENCIL_BIT) == 0, - "the stencil attachment needs a stencil format" - ) { - details->add("format: {}", format); - } + "the stencil attachment needs a stencil format, got {}", + format + ) {} } CTH_CRITICAL(!extent.has_value() && !info.area.has_value(), "a rendering without attachments needs an area") {} @@ -354,9 +356,12 @@ void BaseCmdBuffer::bindDescriptorSet( return dynamic_buffer(binding.descriptorType) ? sum + binding.descriptorCount : sum; } ); - CTH_CRITICAL(dynamicOffsets.size() != dynamicCount, "one dynamic offset per dynamic buffer descriptor") { - details->add("dynamic descriptors: {}, offsets: {}", dynamicCount, dynamicOffsets.size()); - } + CTH_CRITICAL( + dynamicOffsets.size() != dynamicCount, + "one dynamic offset per dynamic buffer descriptor, got {} for {}", + dynamicOffsets.size(), + dynamicCount + ) {} VkDescriptorSet const vkSet = set.get(); core().functions()->vkCmdBindDescriptorSets( diff --git a/cvk/src/render/cmd/cmd_pool.cpp b/cvk/src/render/cmd/cmd_pool.cpp index 10315534..808f93cf 100644 --- a/cvk/src/render/cmd/cmd_pool.cpp +++ b/cvk/src/render/cmd/cmd_pool.cpp @@ -37,9 +37,7 @@ void CmdPool::create() { void CmdPool::destroy() { debug_check(*this); - CTH_CRITICAL(!unused(), "command buffers of the pool must be destroyed first") { - details->add("command buffers: {}", _buffers); - } + CTH_CRITICAL(!unused(), "command buffers of the pool must be destroyed first, got {}", _buffers) {} _handle.reset(); @@ -48,9 +46,7 @@ void CmdPool::destroy() { CmdPool::State CmdPool::release() { debug_check(*this); - CTH_CRITICAL(!unused(), "command buffers of the pool must be destroyed first") { - details->add("command buffers: {}", _buffers); - } + CTH_CRITICAL(!unused(), "command buffers of the pool must be destroyed first, got {}", _buffers) {} State const state{_handle.release()}; forget(); diff --git a/cvk/src/render/pipeline/layout/pipeline_layout.cpp b/cvk/src/render/pipeline/layout/pipeline_layout.cpp index ca95d976..9148c2b2 100644 --- a/cvk/src/render/pipeline/layout/pipeline_layout.cpp +++ b/cvk/src/render/pipeline/layout/pipeline_layout.cpp @@ -13,8 +13,12 @@ PipelineLayout::PipelineLayout(Core const& core, Config const& config) : _handle _pushConstantRanges{config.pushConstantRanges} { Core::debug_check(core); auto const maxSets = core.physicalDevice().limits().maxBoundDescriptorSets; - CTH_STABLE_ERR(config.setLayouts.size() > maxSets, "more set layouts than maxBoundDescriptorSets") { - details->add("set layouts: {}, max: {}", config.setLayouts.size(), maxSets); + CTH_STABLE_ERR( + config.setLayouts.size() > maxSets, + "more set layouts than maxBoundDescriptorSets, got {} for {}", + config.setLayouts.size(), + maxSets + ) { throw details->exception(); } diff --git a/cvk/src/render/pipeline/shader/shader.cpp b/cvk/src/render/pipeline/shader/shader.cpp index ec0f6781..10c07819 100644 --- a/cvk/src/render/pipeline/shader/shader.cpp +++ b/cvk/src/render/pipeline/shader/shader.cpp @@ -111,20 +111,21 @@ VkPipelineShaderStageCreateInfo Shader::stageInfo( } std::vector Shader::load(path_t const& spvPath) { - CVK_STABLE_THROW(!std::filesystem::exists(spvPath), "SPIR-V file does not exist") { - details->add("file: {}", spvPath.string()); - } + CVK_STABLE_THROW(!std::filesystem::exists(spvPath), "SPIR-V file {} does not exist", spvPath.string()) {} auto const size = std::filesystem::file_size(spvPath); - CVK_STABLE_THROW(size == 0 || size % sizeof(uint32_t) != 0, "SPIR-V file size must be a multiple of 4") { - details->add("file: {}, size: {}", spvPath.string(), size); - } + CVK_STABLE_THROW( + size == 0 || size % sizeof(uint32_t) != 0, + "SPIR-V file size must be a multiple of 4, got {} for {}", + size, + spvPath.string() + ) {} std::vector spv(size / sizeof(uint32_t)); std::ifstream file{spvPath, std::ios::binary}; file.read(reinterpret_cast(spv.data()), static_cast(size)); - CVK_STABLE_THROW(!file, "failed to read SPIR-V file") { details->add("file: {}", spvPath.string()); } + CVK_STABLE_THROW(!file, "failed to read SPIR-V file {}", spvPath.string()) {} return spv; } @@ -135,9 +136,7 @@ void Shader::compile( path_t const& compilerPath, std::string_view flags ) { - CVK_STABLE_THROW(!std::filesystem::exists(glslPath), "glsl file does not exist") { - details->add("file: {}", glslPath.string()); - } + CVK_STABLE_THROW(!std::filesystem::exists(glslPath), "glsl file {} does not exist", glslPath.string()) {} #ifdef CTH_PLATFORM_WINDOWS constexpr std::string_view nullDevice = "NUL"; @@ -170,14 +169,12 @@ void Shader::compile( std::filesystem::remove(logPath); } - CVK_STABLE_THROW(result != 0, "shader compilation failed") { - details->add("file: {}", glslPath.string()); + CVK_STABLE_THROW(result != 0, "shader compilation of {} failed", glslPath.string()) { details->add("command: {}", command); for(auto const& line : output) details->add("{}", line); } - CTH_STABLE_WARN(!output.empty(), "shader compiler output") { - details->add("file: {}", glslPath.string()); + CTH_STABLE_WARN(!output.empty(), "shader compiler output for {}", glslPath.string()) { for(auto const& line : output) details->add("{}", line); } } diff --git a/cvk/src/render/sync/barrier.cpp b/cvk/src/render/sync/barrier.cpp index e2837a3c..d6773eab 100644 --- a/cvk/src/render/sync/barrier.cpp +++ b/cvk/src/render/sync/barrier.cpp @@ -81,10 +81,11 @@ namespace { CTH_CRITICAL( (info.oldLayout != info.newLayout || transfer) && (info.newLayout == VK_IMAGE_LAYOUT_UNDEFINED || info.newLayout == VK_IMAGE_LAYOUT_PREINITIALIZED), - "a transition or queue family transfer must not have an undefined or preinitialized new layout" - ) { - details->add("transition: {} -> {}", info.oldLayout, info.newLayout); - } + "a transition or queue family transfer must not have an undefined or preinitialized new layout, " + "got {} -> {}", + info.oldLayout, + info.newLayout + ) {} return VkImageMemoryBarrier2{ .sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER_2, @@ -165,10 +166,9 @@ void Barrier::debug_check_rendering( CTH_CRITICAL( attached(*entry.image) && barrier.newLayout != VK_IMAGE_LAYOUT_RENDERING_LOCAL_READ_KHR && barrier.newLayout != VK_IMAGE_LAYOUT_GENERAL, - "attachments inside a rendering must be in the rendering local read or general layout" - ) { - details->add("layout: {}", barrier.newLayout); - } + "attachments inside a rendering must be in the rendering local read or general layout, got {}", + barrier.newLayout + ) {} } } @@ -181,9 +181,12 @@ void Barrier::addTransition( uint32_t mipLevels ) { auto const transition = layout_transition(newLayout, oldLayout, image.aspectMask(), shaderStages); - CTH_CRITICAL(!transition.has_value(), "layout transition has no scope for a layout, use add() with scopes") { - details->add("transition: {} -> {}", oldLayout, newLayout); - } + CTH_CRITICAL( + !transition.has_value(), + "layout transition {} -> {} has no scope for a layout, use add() with scopes", + oldLayout, + newLayout + ) {} add( image, diff --git a/cvk/src/surface/surface.cpp b/cvk/src/surface/surface.cpp index bcb34737..53136f03 100644 --- a/cvk/src/surface/surface.cpp +++ b/cvk/src/surface/surface.cpp @@ -120,8 +120,7 @@ auto Surface::formats(PhysicalDevice const& physicalDevice) const -> vector
format_t { auto const availableFormats = formats(physicalDevice); - CTH_STABLE_ERR(availableFormats.empty(), "no suitable format found") { - details->add("allowed: {}", _config.allowedSurfaceFormats); + CTH_STABLE_ERR(availableFormats.empty(), "no suitable format found, allowed: {}", _config.allowedSurfaceFormats) { throw details->exception(); } return availableFormats[0]; @@ -143,8 +142,7 @@ VkSurfaceCapabilitiesKHR Surface::capabilities(PhysicalDevice const& physicalDev auto Surface::presentMode(PhysicalDevice const& physicalDevice) const -> present_mode_t { auto const availableModes = presentModes(physicalDevice); - CTH_STABLE_ERR(availableModes.empty(), "no suitable present mode found") { - details->add("allowed: {}", _config.allowedPresentModes); + CTH_STABLE_ERR(availableModes.empty(), "no suitable present mode found, allowed: {}", _config.allowedPresentModes) { throw details->exception(); } return availableModes[0]; diff --git a/cvk/src/surface/swapchain.cpp b/cvk/src/surface/swapchain.cpp index 79a7c9e6..1def3846 100644 --- a/cvk/src/surface/swapchain.cpp +++ b/cvk/src/surface/swapchain.cpp @@ -142,9 +142,7 @@ Swapchain::Acquired Swapchain::acquire(Semaphore const& signal, uint64_t timeout VkResult Swapchain::present(uint32_t imageIndex) { debug_check(*this); - CTH_CRITICAL(imageIndex >= imageCount() || !_acquired[imageIndex], "the image must be acquired") { - details->add("image index: {}", imageIndex); - } + CTH_CRITICAL(imageIndex >= imageCount() || !_acquired[imageIndex], "image {} must be acquired", imageIndex) {} collectRetired(); @@ -181,9 +179,7 @@ VkResult Swapchain::present(uint32_t imageIndex) { void Swapchain::unacquire(span imageIndices) { debug_check(*this); for(auto const index : imageIndices) - CTH_CRITICAL(index >= imageCount() || !_acquired[index], "the images must be acquired") { - details->add("image index: {}", index); - } + CTH_CRITICAL(index >= imageCount() || !_acquired[index], "image {} must be acquired", index) {} if(imageIndices.empty()) return; From 4ed16965b5745d8c3d7ed87b250f8ba0f838150f Mon Sep 17 00:00:00 2001 From: lukas <89145039+codethinki@users.noreply.github.com> Date: Wed, 21 Oct 2026 10:19:27 +0200 Subject: [PATCH 094/135] cvk: BasePipeline with GraphicsPipeline (was Pipeline) and ComputePipeline, dispatch and dispatchIndirect --- cvk/incl/cvk/render/cmd/cmd_buffer.hpp | 28 +++- .../cvk/render/pipeline/base_pipeline.hpp | 117 +++++++++++++ .../cvk/render/pipeline/compute_pipeline.hpp | 73 +++++++++ .../cvk/render/pipeline/graphics_pipeline.hpp | 78 +++++++++ ...onfig.hpp => graphics_pipeline_config.hpp} | 18 +- cvk/incl/cvk/render/pipeline/pipeline.hpp | 148 ----------------- cvk/src/render/cmd/cmd_buffer.cpp | 32 +++- cvk/src/render/pipeline/base_pipeline.cpp | 91 +++++++++++ cvk/src/render/pipeline/compute_pipeline.cpp | 38 +++++ .../{pipeline.cpp => graphics_pipeline.cpp} | 97 +++-------- ...onfig.cpp => graphics_pipeline_config.cpp} | 4 +- cvk/tests/shaders/values.comp | 9 + .../data/descriptor/descriptor_draw_test.cpp | 8 +- cvk/tests/src/render/cmd/draw_test.cpp | 8 +- cvk/tests/src/render/cmd/local_read_test.cpp | 10 +- .../render/pipeline/compute_pipeline_test.cpp | 154 ++++++++++++++++++ ...ne_test.cpp => graphics_pipeline_test.cpp} | 55 ++++--- cvk/tests/src/shaders.hpp | 24 +++ 18 files changed, 712 insertions(+), 280 deletions(-) create mode 100644 cvk/incl/cvk/render/pipeline/base_pipeline.hpp create mode 100644 cvk/incl/cvk/render/pipeline/compute_pipeline.hpp create mode 100644 cvk/incl/cvk/render/pipeline/graphics_pipeline.hpp rename cvk/incl/cvk/render/pipeline/{pipeline_config.hpp => graphics_pipeline_config.hpp} (87%) delete mode 100644 cvk/incl/cvk/render/pipeline/pipeline.hpp create mode 100644 cvk/src/render/pipeline/base_pipeline.cpp create mode 100644 cvk/src/render/pipeline/compute_pipeline.cpp rename cvk/src/render/pipeline/{pipeline.cpp => graphics_pipeline.cpp} (64%) rename cvk/src/render/pipeline/{pipeline_config.cpp => graphics_pipeline_config.cpp} (63%) create mode 100644 cvk/tests/shaders/values.comp create mode 100644 cvk/tests/src/render/pipeline/compute_pipeline_test.cpp rename cvk/tests/src/render/pipeline/{pipeline_test.cpp => graphics_pipeline_test.cpp} (72%) diff --git a/cvk/incl/cvk/render/cmd/cmd_buffer.hpp b/cvk/incl/cvk/render/cmd/cmd_buffer.hpp index 223cdd98..2910ca7d 100644 --- a/cvk/incl/cvk/render/cmd/cmd_buffer.hpp +++ b/cvk/incl/cvk/render/cmd/cmd_buffer.hpp @@ -23,9 +23,10 @@ namespace cvk { +class ComputePipeline; class Core; class DescriptorSet; -class Pipeline; +class GraphicsPipeline; class PipelineLayout; /** @@ -148,11 +149,17 @@ class BaseCmdBuffer { /** * @brief binds the pipeline to the graphics bind point * @details @ref rendering() -> sets the attachment mapping of the pipeline if the current one differs - * @pre @ref recording(), @ref Pipeline::created() + * @pre @ref recording(), @ref GraphicsPipeline::created() * @pre a secondary inside the rendering of a primary -> the attachment mapping of the pipeline == the inherited one * @note outside of a rendering: sets no attachment mapping, the draws need the default of @ref beginRendering() */ - void bindPipeline(Pipeline const&); + void bindPipeline(GraphicsPipeline const&); + + /** + * @brief binds the pipeline to the compute bind point + * @pre @ref recording(), @ref ComputePipeline::created() + */ + void bindPipeline(ComputePipeline const&) const; /** * @brief binds the set to the set number of the pipeline layout @@ -262,6 +269,21 @@ class BaseCmdBuffer { uint32_t stride = sizeof(VkDrawIndexedIndirectCommand) ) const; + /** + * @brief records a dispatch of the bound compute pipeline + * @param groupCountX work groups in x, the same for y and z + * @pre @ref recording(), outside of a rendering + */ + void dispatch(uint32_t groupCountX, uint32_t groupCountY = 1, uint32_t groupCountZ = 1) const; + + /** + * @brief dispatches with the VkDispatchIndirectCommand in the buffer + * @param offset in bytes + * @pre the preconditions of @ref dispatch(), buffer created with VK_BUFFER_USAGE_INDIRECT_BUFFER_BIT + * @pre the command lies in the buffer + */ + void dispatchIndirect(BaseBuffer const&, size_t offset = 0) const; + static void destroy(DeviceTable table, VkCommandPool, VkCommandBuffer); /** diff --git a/cvk/incl/cvk/render/pipeline/base_pipeline.hpp b/cvk/incl/cvk/render/pipeline/base_pipeline.hpp new file mode 100644 index 00000000..e306ce65 --- /dev/null +++ b/cvk/incl/cvk/render/pipeline/base_pipeline.hpp @@ -0,0 +1,117 @@ +#pragma once +#include "cvk/base/device_table.hpp" +#include "cvk/utility/handle/core_handle.hpp" +#include "cvk/utility/types.hpp" + +#include + +#include + + +namespace cvk { +class Core; +class PipelineLayout; + +/** + * @brief VkPipeline wrapper, base of @ref GraphicsPipeline and @ref ComputePipeline + * @details the derived classes build the create info, the base creates the pipeline, + * the layout is a parent of the pipeline: it outlives the pipeline, + * the destructor is protected: delete through the derived class + */ +class BasePipeline { +public: + using vk_handle_t = VkPipeline; + struct State; + + /** + * @brief wraps the state + * @pre the pipeline was created with the layout and the config of the derived class + * @post @ref created() + * @note calls @ref optDestroy() + */ + void wrap(State const&); + + /** + * @brief destroys and forgets + * @pre @ref created() + * @pre the gpu does not use the pipeline + */ + void destroy(); + + /** + * @brief if @ref created() -> calls @ref destroy() + */ + void optDestroy() { if(created()) destroy(); } + + /** + * @brief releases ownership, returns the state and forgets + * @pre @ref created() + */ + State release(); + + static void destroy(DeviceTable table, VkPipeline); + + [[nodiscard]] constexpr bool created() const { return static_cast(_handle); } + [[nodiscard]] constexpr vk_handle_t get() const { return _handle.get(); } + [[nodiscard]] constexpr Core const& core() const { return _handle.core(); } + [[nodiscard]] constexpr PipelineLayout const& layout() const { return *_layout; } + [[nodiscard]] constexpr VkPipelineBindPoint bindPoint() const { return _bindPoint; } + +protected: + /** + * @brief base constructor, keeps the layout + * @pre @ref Core::created() + */ + BasePipeline(Core const&, PipelineLayout const&, VkPipelineBindPoint); + + /** + * @note calls @ref optDestroy() + */ + ~BasePipeline() { optDestroy(); } + + /** + * @brief creates the pipeline with the info + * @pre the layout of the info is @ref layout() + * @post @ref created() + * @note calls @ref optDestroy() + * @throws cvk::vk_result_exception result of vkCreateGraphicsPipelines() + */ + void create(VkGraphicsPipelineCreateInfo const&); + + /** + * @brief creates the pipeline with the info + * @pre the layout of the info is @ref layout() + * @post @ref created() + * @note calls @ref optDestroy() + * @throws cvk::vk_result_exception result of vkCreateComputePipelines() + */ + void create(VkComputePipelineCreateInfo const&); + +private: + void forget(); + + core_handle _handle; + not_null_ptr _layout; + VkPipelineBindPoint _bindPoint; + +public: + BasePipeline(BasePipeline const& other) = delete; + BasePipeline(BasePipeline&& other) noexcept = default; + BasePipeline& operator=(BasePipeline const& other) = delete; + BasePipeline& operator=(BasePipeline&& other) noexcept = default; + + static constexpr void debug_check(BasePipeline const&); +}; +} + +namespace cvk { +struct BasePipeline::State { + vk_not_null vkPipeline; +}; +} + +namespace cvk { +constexpr void BasePipeline::debug_check(BasePipeline const& pipeline) { + CTH_CRITICAL(!pipeline.created(), "pipeline must be created") {} +} +} diff --git a/cvk/incl/cvk/render/pipeline/compute_pipeline.hpp b/cvk/incl/cvk/render/pipeline/compute_pipeline.hpp new file mode 100644 index 00000000..4ac775be --- /dev/null +++ b/cvk/incl/cvk/render/pipeline/compute_pipeline.hpp @@ -0,0 +1,73 @@ +#pragma once +#include "base_pipeline.hpp" + +#include "cvk/utility/constants.hpp" + +#include + + +namespace cvk { +class Core; +class PipelineLayout; + +/** + * @brief shader stage and flags of a compute pipeline + */ +struct ComputePipelineConfig { + /** + * @brief stage of a compute shader, see @ref Shader::stageInfo() + * @attention the specialization info and the entry point name outlive @ref ComputePipeline::create() + */ + VkPipelineShaderStageCreateInfo stage{}; + + /** + * @brief VK_PIPELINE_CREATE_DISPATCH_BASE_BIT, ... + */ + VkPipelineCreateFlags flags = 0; +}; + +/** + * @brief compute VkPipeline wrapper + */ +class ComputePipeline : public BasePipeline { +public: + using Config = ComputePipelineConfig; + + /** + * @brief base constructor, keeps the layout + * @pre @ref Core::created() + */ + ComputePipeline(Core const&, PipelineLayout const&, Config const&); + + /** + * @brief constructs and wraps + * @details calls: + * - @ref ComputePipeline(Core const&, PipelineLayout const&, Config const&) + * - @ref wrap() + */ + ComputePipeline(Core const&, PipelineLayout const&, Config const&, State const&); + + /** + * @brief constructs and creates + * @details calls: + * - @ref ComputePipeline(Core const&, PipelineLayout const&, Config const&) + * - @ref create() + */ + explicit ComputePipeline(Core const&, PipelineLayout const&, Config const&, create_t); + + /** + * @brief creates the pipeline + * @pre the stage of the config is a compute shader + * @pre the layout and the shader module of the config are not destroyed + * @post @ref created() + * @note calls @ref optDestroy() + * @throws cvk::vk_result_exception result of vkCreateComputePipelines() + */ + void create(); + + [[nodiscard]] constexpr Config const& config() const { return _config; } + +private: + Config _config; +}; +} diff --git a/cvk/incl/cvk/render/pipeline/graphics_pipeline.hpp b/cvk/incl/cvk/render/pipeline/graphics_pipeline.hpp new file mode 100644 index 00000000..984005bb --- /dev/null +++ b/cvk/incl/cvk/render/pipeline/graphics_pipeline.hpp @@ -0,0 +1,78 @@ +#pragma once +#include "base_pipeline.hpp" +#include "graphics_pipeline_config.hpp" + +#include "cvk/utility/constants.hpp" + +#include + + +namespace cvk { +class Core; +class PipelineLayout; + +/** + * @brief graphics VkPipeline wrapper for dynamic rendering + */ +class GraphicsPipeline : public BasePipeline { +public: + using Config = GraphicsPipelineConfig; + + /** + * @brief base constructor, keeps the layout + * @pre @ref Core::created() + */ + GraphicsPipeline(Core const&, PipelineLayout const&, Config); + + /** + * @brief constructs and wraps + * @details calls: + * - @ref GraphicsPipeline(Core const&, PipelineLayout const&, Config) + * - @ref wrap() + */ + GraphicsPipeline(Core const&, PipelineLayout const&, Config, State const&); + + /** + * @brief constructs and creates + * @details calls: + * - @ref GraphicsPipeline(Core const&, PipelineLayout const&, Config) + * - @ref create() + */ + explicit GraphicsPipeline(Core const&, PipelineLayout const&, Config, create_t); + + /** + * @brief base constructor of a derivative pipeline, keeps the layout and copies the handle of the parent + * @pre @ref Core::created() + * @pre @ref created() of the parent, with VK_PIPELINE_CREATE_ALLOW_DERIVATIVES_BIT in its flags + */ + GraphicsPipeline(Core const&, GraphicsPipeline const& parent, Config); + + /** + * @brief constructs and creates a derivative pipeline + * @details calls: + * - @ref GraphicsPipeline(Core const&, GraphicsPipeline const&, Config) + * - @ref create() + */ + explicit GraphicsPipeline(Core const&, GraphicsPipeline const& parent, Config, create_t); + + /** + * @brief creates the pipeline + * @details derivative pipeline -> adds VK_PIPELINE_CREATE_DERIVATIVE_BIT + * @pre the layout, the parent and the shader modules of the config are not destroyed + * @post @ref created() + * @note calls @ref optDestroy() + * @throws cvk::vk_result_exception result of vkCreateGraphicsPipelines() + */ + void create(); + + /** + * @return VK_NULL_HANDLE -> not a derivative pipeline + */ + [[nodiscard]] constexpr VkPipeline parent() const { return _vkParent; } + [[nodiscard]] constexpr Config const& config() const { return _config; } + +private: + VkPipeline _vkParent = VK_NULL_HANDLE; + Config _config; +}; +} diff --git a/cvk/incl/cvk/render/pipeline/pipeline_config.hpp b/cvk/incl/cvk/render/pipeline/graphics_pipeline_config.hpp similarity index 87% rename from cvk/incl/cvk/render/pipeline/pipeline_config.hpp rename to cvk/incl/cvk/render/pipeline/graphics_pipeline_config.hpp index e2d8f0c7..6b9d4fb1 100644 --- a/cvk/incl/cvk/render/pipeline/pipeline_config.hpp +++ b/cvk/incl/cvk/render/pipeline/graphics_pipeline_config.hpp @@ -15,9 +15,9 @@ class Shader; /** * @brief graphics pipeline state, the defaults are the legacy defaults - * @details @ref Pipeline::create() sets the pointer and count members of the Vulkan structs from the vectors + * @details @ref GraphicsPipeline::create() sets the pointer and count members of the Vulkan structs from the vectors */ -struct PipelineConfig { +struct GraphicsPipelineConfig { /** * @brief alpha blending: src alpha, one minus src alpha */ @@ -33,13 +33,13 @@ struct PipelineConfig { | VK_COLOR_COMPONENT_A_BIT, }; - constexpr PipelineConfig() = default; + constexpr GraphicsPipelineConfig() = default; /** * @brief one @ref ALPHA_BLENDING attachment per color format * @param depthFormat VK_FORMAT_UNDEFINED -> no depth attachment */ - explicit constexpr PipelineConfig( + explicit constexpr GraphicsPipelineConfig( std::span colorFormats, VkFormat depthFormat = VK_FORMAT_UNDEFINED ); @@ -47,16 +47,16 @@ struct PipelineConfig { /** * @brief adds the stage of the shader * @param specialization nullptr -> no specialization constants - * @attention @ref specialization must outlive @ref Pipeline::create() + * @attention @ref specialization must outlive @ref GraphicsPipeline::create() * @pre @ref Shader::created() * @pre no stage of the same type */ - PipelineConfig& addStage(Shader const&, VkSpecializationInfo const* specialization = nullptr); + GraphicsPipelineConfig& addStage(Shader const&, VkSpecializationInfo const* specialization = nullptr); /** * @pre a stage of the type exists */ - constexpr PipelineConfig& removeStage(VkShaderStageFlagBits stage); + constexpr GraphicsPipelineConfig& removeStage(VkShaderStageFlagBits stage); /** * @brief VK_PIPELINE_CREATE_ALLOW_DERIVATIVES_BIT -> the pipeline can be a parent @@ -161,12 +161,12 @@ struct PipelineConfig { } namespace cvk { -constexpr PipelineConfig::PipelineConfig(std::span colorFormats, VkFormat depthFormat) : +constexpr GraphicsPipelineConfig::GraphicsPipelineConfig(std::span colorFormats, VkFormat depthFormat) : colorBlendAttachments(colorFormats.size(), ALPHA_BLENDING), colorFormats{std::from_range, colorFormats}, depthFormat{depthFormat} {} -constexpr PipelineConfig& PipelineConfig::removeStage(VkShaderStageFlagBits stage) { +constexpr GraphicsPipelineConfig& GraphicsPipelineConfig::removeStage(VkShaderStageFlagBits stage) { auto const removed = std::erase_if( stages, [stage](VkPipelineShaderStageCreateInfo const& info) { return info.stage == stage; } diff --git a/cvk/incl/cvk/render/pipeline/pipeline.hpp b/cvk/incl/cvk/render/pipeline/pipeline.hpp deleted file mode 100644 index ef936f64..00000000 --- a/cvk/incl/cvk/render/pipeline/pipeline.hpp +++ /dev/null @@ -1,148 +0,0 @@ -#pragma once -#include "pipeline_config.hpp" - -#include "cvk/base/device_table.hpp" -#include "cvk/utility/constants.hpp" -#include "cvk/utility/handle/core_handle.hpp" -#include "cvk/utility/types.hpp" - -#include - -#include - - - -namespace cvk { -class Core; -class PipelineLayout; - -/** - * @brief graphics VkPipeline wrapper for dynamic rendering - * @details the layout is a parent of the pipeline: it outlives the pipeline - */ -class Pipeline { -public: - using vk_handle_t = VkPipeline; - using Config = PipelineConfig; - struct State; - - /** - * @brief base constructor, keeps the layout - * @pre @ref Core::created() - */ - Pipeline(Core const&, PipelineLayout const&, Config); - - /** - * @brief constructs and wraps - * @details calls: - * - @ref Pipeline(Core const&, PipelineLayout const&, Config) - * - @ref wrap() - */ - Pipeline(Core const&, PipelineLayout const&, Config, State const&); - - /** - * @brief constructs and creates - * @details calls: - * - @ref Pipeline(Core const&, PipelineLayout const&, Config) - * - @ref create() - */ - explicit Pipeline(Core const&, PipelineLayout const&, Config, create_t); - - /** - * @brief base constructor of a derivative pipeline, keeps the layout and copies the handle of the parent - * @pre @ref Core::created() - * @pre @ref created() of the parent, with VK_PIPELINE_CREATE_ALLOW_DERIVATIVES_BIT in its flags - */ - Pipeline(Core const&, Pipeline const& parent, Config); - - /** - * @brief constructs and creates a derivative pipeline - * @details calls: - * - @ref Pipeline(Core const&, Pipeline const&, Config) - * - @ref create() - */ - explicit Pipeline(Core const&, Pipeline const& parent, Config, create_t); - - /** - * @note calls @ref optDestroy() - */ - ~Pipeline() { optDestroy(); } - - /** - * @brief wraps the state - * @pre the pipeline was created with the config and the layout - * @post @ref created() - * @note calls @ref optDestroy() - */ - void wrap(State const&); - - /** - * @brief creates the pipeline - * @details derivative pipeline -> adds VK_PIPELINE_CREATE_DERIVATIVE_BIT - * @pre the layout, the parent and the shader modules of the config are not destroyed - * @post @ref created() - * @note calls @ref optDestroy() - * @throws cvk::vk_result_exception result of vkCreateGraphicsPipelines() - */ - void create(); - - /** - * @brief destroys and forgets - * @pre @ref created() - * @pre the gpu does not use the pipeline - */ - void destroy(); - - /** - * @brief if @ref created() -> calls @ref destroy() - */ - void optDestroy() { if(created()) destroy(); } - - /** - * @brief releases ownership, returns the state and forgets - * @pre @ref created() - */ - State release(); - - static void destroy(DeviceTable table, VkPipeline); - - [[nodiscard]] constexpr bool created() const { return static_cast(_handle); } - [[nodiscard]] constexpr vk_handle_t get() const { return _handle.get(); } - [[nodiscard]] constexpr PipelineLayout const& layout() const { return *_layout; } - - /** - * @return VK_NULL_HANDLE -> not a derivative pipeline - */ - [[nodiscard]] constexpr VkPipeline parent() const { return _vkParent; } - [[nodiscard]] constexpr Config const& config() const { return _config; } - [[nodiscard]] constexpr Core const& core() const { return _handle.core(); } - -private: - void forget(); - - core_handle _handle; - not_null_ptr _layout; - VkPipeline _vkParent = VK_NULL_HANDLE; - Config _config; - -public: - Pipeline(Pipeline const& other) = delete; - Pipeline(Pipeline&& other) noexcept = default; - Pipeline& operator=(Pipeline const& other) = delete; - Pipeline& operator=(Pipeline&& other) noexcept = default; - - static constexpr void debug_check(Pipeline const&); -}; -} - -namespace cvk { -struct Pipeline::State { - vk_not_null vkPipeline; -}; -} - -namespace cvk { -constexpr void Pipeline::debug_check(Pipeline const& pipeline) { - CTH_CRITICAL(!pipeline.created(), "pipeline must be created") {} -} -} diff --git a/cvk/src/render/cmd/cmd_buffer.cpp b/cvk/src/render/cmd/cmd_buffer.cpp index b59ad82d..e553229b 100644 --- a/cvk/src/render/cmd/cmd_buffer.cpp +++ b/cvk/src/render/cmd/cmd_buffer.cpp @@ -5,8 +5,9 @@ #include "cvk/data/img/image.hpp" #include "cvk/data/img/image_format.hpp" #include "cvk/data/img/image_view.hpp" +#include "cvk/render/pipeline/compute_pipeline.hpp" +#include "cvk/render/pipeline/graphics_pipeline.hpp" #include "cvk/render/pipeline/layout/pipeline_layout.hpp" -#include "cvk/render/pipeline/pipeline.hpp" #include "cvk/utility/vk_exceptions.hpp" #include @@ -325,10 +326,10 @@ void BaseCmdBuffer::setScissor(VkRect2D const& scissor) const { core().functions()->vkCmdSetScissor(get(), 0, 1, &scissor); } -void BaseCmdBuffer::bindPipeline(Pipeline const& pipeline) { +void BaseCmdBuffer::bindPipeline(GraphicsPipeline const& pipeline) { debug_check(*this); CTH_CRITICAL(!recording(), "command buffer must be recording") {} - Pipeline::debug_check(pipeline); + GraphicsPipeline::debug_check(pipeline); auto const& mapping = pipeline.config().attachmentMapping; if(rendering() != nullptr && mapping != rendering()->attachmentMapping) setAttachmentMapping(mapping); @@ -336,6 +337,14 @@ void BaseCmdBuffer::bindPipeline(Pipeline const& pipeline) { core().functions()->vkCmdBindPipeline(get(), VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline.get()); } +void BaseCmdBuffer::bindPipeline(ComputePipeline const& pipeline) const { + debug_check(*this); + CTH_CRITICAL(!recording(), "command buffer must be recording") {} + ComputePipeline::debug_check(pipeline); + + core().functions()->vkCmdBindPipeline(get(), VK_PIPELINE_BIND_POINT_COMPUTE, pipeline.get()); +} + void BaseCmdBuffer::bindDescriptorSet( DescriptorSet const& set, PipelineLayout const& layout, @@ -472,6 +481,23 @@ void BaseCmdBuffer::drawIndexedIndirect( core().functions()->vkCmdDrawIndexedIndirect(get(), buffer.get(), offset, drawCount, stride); } +void BaseCmdBuffer::dispatch(uint32_t groupCountX, uint32_t groupCountY, uint32_t groupCountZ) const { + debug_check(*this); + CTH_CRITICAL(!recording(), "command buffer must be recording") {} + CTH_CRITICAL(rendering() != nullptr, "dispatches must be outside of a rendering") {} + + core().functions()->vkCmdDispatch(get(), groupCountX, groupCountY, groupCountZ); +} + +void BaseCmdBuffer::dispatchIndirect(BaseBuffer const& buffer, size_t offset) const { + debug_check(*this); + CTH_CRITICAL(!recording(), "command buffer must be recording") {} + CTH_CRITICAL(rendering() != nullptr, "dispatches must be outside of a rendering") {} + debug_check_indirect(buffer, offset, 1, 0, sizeof(VkDispatchIndirectCommand)); + + core().functions()->vkCmdDispatchIndirect(get(), buffer.get(), offset); +} + void BaseCmdBuffer::destroy(DeviceTable table, VkCommandPool vkPool, VkCommandBuffer vkBuffer) { CTH_WARN(vkBuffer == VK_NULL_HANDLE, "vkBuffer invalid (VK_NULL_HANDLE)") {} diff --git a/cvk/src/render/pipeline/base_pipeline.cpp b/cvk/src/render/pipeline/base_pipeline.cpp new file mode 100644 index 00000000..7d506661 --- /dev/null +++ b/cvk/src/render/pipeline/base_pipeline.cpp @@ -0,0 +1,91 @@ +#include "cvk/render/pipeline/base_pipeline.hpp" + +#include "cvk/base/core.hpp" +#include "cvk/render/pipeline/layout/pipeline_layout.hpp" +#include "cvk/utility/vk_exceptions.hpp" + + +namespace cvk { +BasePipeline::BasePipeline(Core const& core, PipelineLayout const& layout, VkPipelineBindPoint bindPoint) : + _handle{core}, _layout{&layout}, _bindPoint{bindPoint} { + Core::debug_check(core); +} + +void BasePipeline::wrap(State const& state) { + optDestroy(); + + _handle.reset(state.vkPipeline.get(), core()); +} + +void BasePipeline::destroy() { + debug_check(*this); + + _handle.reset(); + + forget(); +} + +BasePipeline::State BasePipeline::release() { + debug_check(*this); + + State const state{_handle.release()}; + forget(); + return state; +} + +void BasePipeline::destroy(DeviceTable table, VkPipeline vkPipeline) { + CTH_WARN(vkPipeline == VK_NULL_HANDLE, "vkPipeline invalid (VK_NULL_HANDLE)") {} + + table->vkDestroyPipeline(table.device(), vkPipeline, nullptr); +} + +void BasePipeline::create(VkGraphicsPipelineCreateInfo const& info) { + PipelineLayout::debug_check(*_layout); + CTH_CRITICAL(info.layout != _layout->get(), "the create info must use the layout of the pipeline") {} + + optDestroy(); + + VkPipeline ptr = VK_NULL_HANDLE; + auto const result = core().functions()->vkCreateGraphicsPipelines( + core().vkDevice(), + VK_NULL_HANDLE, + 1, + &info, + nullptr, + &ptr + ); + + CTH_STABLE_ERR(result != VK_SUCCESS, "failed to create graphics pipeline") { + forget(); + throw cvk::vk_result_exception{result, details->exception()}; + } + + _handle.reset(ptr, core()); +} + +void BasePipeline::create(VkComputePipelineCreateInfo const& info) { + PipelineLayout::debug_check(*_layout); + CTH_CRITICAL(info.layout != _layout->get(), "the create info must use the layout of the pipeline") {} + + optDestroy(); + + VkPipeline ptr = VK_NULL_HANDLE; + auto const result = core().functions()->vkCreateComputePipelines( + core().vkDevice(), + VK_NULL_HANDLE, + 1, + &info, + nullptr, + &ptr + ); + + CTH_STABLE_ERR(result != VK_SUCCESS, "failed to create compute pipeline") { + forget(); + throw cvk::vk_result_exception{result, details->exception()}; + } + + _handle.reset(ptr, core()); +} + +void BasePipeline::forget() {} +} diff --git a/cvk/src/render/pipeline/compute_pipeline.cpp b/cvk/src/render/pipeline/compute_pipeline.cpp new file mode 100644 index 00000000..332dc356 --- /dev/null +++ b/cvk/src/render/pipeline/compute_pipeline.cpp @@ -0,0 +1,38 @@ +#include "cvk/render/pipeline/compute_pipeline.hpp" + +#include "cvk/render/pipeline/layout/pipeline_layout.hpp" + + +namespace cvk { +ComputePipeline::ComputePipeline(Core const& core, PipelineLayout const& layout, Config const& config) : + BasePipeline{core, layout, VK_PIPELINE_BIND_POINT_COMPUTE}, _config{config} {} + +ComputePipeline::ComputePipeline( + Core const& core, + PipelineLayout const& layout, + Config const& config, + State const& state +) : ComputePipeline{core, layout, config} { wrap(state); } + +ComputePipeline::ComputePipeline(Core const& core, PipelineLayout const& layout, Config const& config, create_t) : + ComputePipeline{core, layout, config} { + create(); +} + +void ComputePipeline::create() { + CTH_CRITICAL(_config.stage.stage != VK_SHADER_STAGE_COMPUTE_BIT, "the stage must be a compute shader") {} + CTH_CRITICAL(_config.stage.module == VK_NULL_HANDLE, "the stage needs a shader module") {} + + VkComputePipelineCreateInfo const info{ + .sType = VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO, + .pNext = nullptr, + .flags = _config.flags, + .stage = _config.stage, + .layout = layout().get(), + .basePipelineHandle = VK_NULL_HANDLE, + .basePipelineIndex = -1, + }; + + BasePipeline::create(info); +} +} diff --git a/cvk/src/render/pipeline/pipeline.cpp b/cvk/src/render/pipeline/graphics_pipeline.cpp similarity index 64% rename from cvk/src/render/pipeline/pipeline.cpp rename to cvk/src/render/pipeline/graphics_pipeline.cpp index eae49c92..0a38947d 100644 --- a/cvk/src/render/pipeline/pipeline.cpp +++ b/cvk/src/render/pipeline/graphics_pipeline.cpp @@ -1,4 +1,4 @@ -#include "cvk/render/pipeline/pipeline.hpp" +#include "cvk/render/pipeline/graphics_pipeline.hpp" #include "cvk/base/core.hpp" #include "cvk/render/pipeline/layout/pipeline_layout.hpp" @@ -6,27 +6,23 @@ namespace cvk { -Pipeline::Pipeline(Core const& core, PipelineLayout const& layout, Config config) : _handle{core}, - _layout{&layout}, - _config{std::move(config)} { Core::debug_check(core); } - -Pipeline::Pipeline(Core const& core, PipelineLayout const& layout, Config config, State const& state) : Pipeline{ - core, - layout, - std::move(config) -} { wrap(state); } - -Pipeline::Pipeline(Core const& core, PipelineLayout const& layout, Config config, create_t) : Pipeline{ - core, - layout, - std::move(config) -} { create(); } - -Pipeline::Pipeline(Core const& core, Pipeline const& parent, Config config) : _handle{core}, - _layout{&parent.layout()}, +GraphicsPipeline::GraphicsPipeline(Core const& core, PipelineLayout const& layout, Config config) : + BasePipeline{core, layout, VK_PIPELINE_BIND_POINT_GRAPHICS}, _config{std::move(config)} {} + +GraphicsPipeline::GraphicsPipeline(Core const& core, PipelineLayout const& layout, Config config, State const& state) : + GraphicsPipeline{core, layout, std::move(config)} { + wrap(state); +} + +GraphicsPipeline::GraphicsPipeline(Core const& core, PipelineLayout const& layout, Config config, create_t) : + GraphicsPipeline{core, layout, std::move(config)} { + create(); +} + +GraphicsPipeline::GraphicsPipeline(Core const& core, GraphicsPipeline const& parent, Config config) : + BasePipeline{core, parent.layout(), VK_PIPELINE_BIND_POINT_GRAPHICS}, _vkParent{parent.get()}, _config{std::move(config)} { - Core::debug_check(core); debug_check(parent); CTH_CRITICAL( (parent.config().flags & VK_PIPELINE_CREATE_ALLOW_DERIVATIVES_BIT) == 0, @@ -34,28 +30,18 @@ Pipeline::Pipeline(Core const& core, Pipeline const& parent, Config config) : _h ) {} } -Pipeline::Pipeline(Core const& core, Pipeline const& parent, Config config, create_t) : Pipeline{ - core, - parent, - std::move(config) -} { create(); } - -void Pipeline::wrap(State const& state) { - optDestroy(); - - _handle.reset(state.vkPipeline.get(), core()); +GraphicsPipeline::GraphicsPipeline(Core const& core, GraphicsPipeline const& parent, Config config, create_t) : + GraphicsPipeline{core, parent, std::move(config)} { + create(); } -void Pipeline::create() { - PipelineLayout::debug_check(*_layout); +void GraphicsPipeline::create() { CTH_CRITICAL( _config.colorBlendAttachments.size() != _config.colorFormats.size(), "one color blend attachment per color format required" ) {} AttachmentMapping::debug_check(_config.attachmentMapping, _config.colorFormats.size()); - optDestroy(); - auto const& config = _config; VkPipelineVertexInputStateCreateInfo const vertexInput{ @@ -120,52 +106,13 @@ void Pipeline::create() { .pDepthStencilState = &config.depthStencil, .pColorBlendState = &colorBlend, .pDynamicState = &dynamicState, - .layout = _layout->get(), + .layout = layout().get(), .renderPass = VK_NULL_HANDLE, .subpass = 0, .basePipelineHandle = _vkParent, .basePipelineIndex = -1, }; - VkPipeline ptr = VK_NULL_HANDLE; - auto const result = core().functions()->vkCreateGraphicsPipelines( - core().vkDevice(), - VK_NULL_HANDLE, - 1, - &info, - nullptr, - &ptr - ); - - CTH_STABLE_ERR(result != VK_SUCCESS, "failed to create graphics pipeline") { - forget(); - throw cvk::vk_result_exception{result, details->exception()}; - } - - _handle.reset(ptr, core()); + BasePipeline::create(info); } - -void Pipeline::destroy() { - debug_check(*this); - - _handle.reset(); - - forget(); -} - -Pipeline::State Pipeline::release() { - debug_check(*this); - - State const state{_handle.release()}; - forget(); - return state; -} - -void Pipeline::destroy(DeviceTable table, VkPipeline vkPipeline) { - CTH_WARN(vkPipeline == VK_NULL_HANDLE, "vkPipeline invalid (VK_NULL_HANDLE)") {} - - table->vkDestroyPipeline(table.device(), vkPipeline, nullptr); -} - -void Pipeline::forget() {} } diff --git a/cvk/src/render/pipeline/pipeline_config.cpp b/cvk/src/render/pipeline/graphics_pipeline_config.cpp similarity index 63% rename from cvk/src/render/pipeline/pipeline_config.cpp rename to cvk/src/render/pipeline/graphics_pipeline_config.cpp index cde0b705..7915dfaa 100644 --- a/cvk/src/render/pipeline/pipeline_config.cpp +++ b/cvk/src/render/pipeline/graphics_pipeline_config.cpp @@ -1,4 +1,4 @@ -#include "cvk/render/pipeline/pipeline_config.hpp" +#include "cvk/render/pipeline/graphics_pipeline_config.hpp" #include "cvk/render/pipeline/shader/shader.hpp" @@ -6,7 +6,7 @@ namespace cvk { -PipelineConfig& PipelineConfig::addStage(Shader const& shader, VkSpecializationInfo const* specialization) { +GraphicsPipelineConfig& GraphicsPipelineConfig::addStage(Shader const& shader, VkSpecializationInfo const* specialization) { CTH_CRITICAL( std::ranges::contains(stages, shader.stage(), &VkPipelineShaderStageCreateInfo::stage), "stage already added" diff --git a/cvk/tests/shaders/values.comp b/cvk/tests/shaders/values.comp new file mode 100644 index 00000000..9772e6df --- /dev/null +++ b/cvk/tests/shaders/values.comp @@ -0,0 +1,9 @@ +#version 450 + +layout(local_size_x = 64) in; + +layout(set = 0, binding = 0) buffer Values { + uint values[]; +}; + +void main() { values[gl_GlobalInvocationID.x] = gl_GlobalInvocationID.x * 3u; } diff --git a/cvk/tests/src/data/descriptor/descriptor_draw_test.cpp b/cvk/tests/src/data/descriptor/descriptor_draw_test.cpp index 78519030..e2b4ad13 100644 --- a/cvk/tests/src/data/descriptor/descriptor_draw_test.cpp +++ b/cvk/tests/src/data/descriptor/descriptor_draw_test.cpp @@ -10,9 +10,9 @@ #include "cvk/data/img/sampler.hpp" #include "cvk/render/cmd/cmd_buffer.hpp" #include "cvk/render/cmd/cmd_pool.hpp" +#include "cvk/render/pipeline/graphics_pipeline.hpp" #include "cvk/render/pipeline/layout/descriptor_set_layout.hpp" #include "cvk/render/pipeline/layout/pipeline_layout.hpp" -#include "cvk/render/pipeline/pipeline.hpp" #include "cvk/render/pipeline/shader/shader.hpp" #include "cvk/render/sync/barrier.hpp" @@ -63,8 +63,8 @@ namespace { vertices.write(FULLSCREEN); } - [[nodiscard]] Pipeline::Config config() const { - Pipeline::Config config{COLOR_FORMATS}; + [[nodiscard]] GraphicsPipeline::Config config() const { + GraphicsPipeline::Config config{COLOR_FORMATS}; config.addStage(vertex).addStage(fragment); config.vertexBindings = {{.binding = 0, .stride = sizeof(Vec3), .inputRate = VK_VERTEX_INPUT_RATE_VERTEX}}; config.vertexAttributes = { @@ -77,7 +77,7 @@ namespace { PipelineLayout layout; Shader vertex; Shader fragment; - Pipeline pipeline; + GraphicsPipeline pipeline; Buffer vertices; }; diff --git a/cvk/tests/src/render/cmd/draw_test.cpp b/cvk/tests/src/render/cmd/draw_test.cpp index bf525220..0a9f3cb0 100644 --- a/cvk/tests/src/render/cmd/draw_test.cpp +++ b/cvk/tests/src/render/cmd/draw_test.cpp @@ -6,8 +6,8 @@ #include "cvk/data/img/image.hpp" #include "cvk/data/img/image_view.hpp" #include "cvk/render/cmd/cmd_buffer.hpp" +#include "cvk/render/pipeline/graphics_pipeline.hpp" #include "cvk/render/pipeline/layout/pipeline_layout.hpp" -#include "cvk/render/pipeline/pipeline.hpp" #include "cvk/render/pipeline/shader/shader.hpp" #include "cvk/render/sync/barrier.hpp" @@ -60,8 +60,8 @@ namespace { vertices.write(FULLSCREEN); } - [[nodiscard]] Pipeline::Config config() const { - Pipeline::Config config{COLOR_FORMATS}; + [[nodiscard]] GraphicsPipeline::Config config() const { + GraphicsPipeline::Config config{COLOR_FORMATS}; config.addStage(vertex).addStage(fragment); config.vertexBindings = {{.binding = 0, .stride = sizeof(Vec3), .inputRate = VK_VERTEX_INPUT_RATE_VERTEX}}; config.vertexAttributes = { @@ -74,7 +74,7 @@ namespace { PipelineLayout layout; Shader vertex; Shader fragment; - Pipeline pipeline; + GraphicsPipeline pipeline; Buffer vertices; }; diff --git a/cvk/tests/src/render/cmd/local_read_test.cpp b/cvk/tests/src/render/cmd/local_read_test.cpp index 069cac49..0ff07a79 100644 --- a/cvk/tests/src/render/cmd/local_read_test.cpp +++ b/cvk/tests/src/render/cmd/local_read_test.cpp @@ -9,9 +9,9 @@ #include "cvk/data/img/image_view.hpp" #include "cvk/render/cmd/cmd_buffer.hpp" #include "cvk/render/cmd/cmd_pool.hpp" +#include "cvk/render/pipeline/graphics_pipeline.hpp" #include "cvk/render/pipeline/layout/descriptor_set_layout.hpp" #include "cvk/render/pipeline/layout/pipeline_layout.hpp" -#include "cvk/render/pipeline/pipeline.hpp" #include "cvk/render/pipeline/shader/shader.hpp" #include "cvk/render/sync/barrier.hpp" @@ -121,8 +121,8 @@ namespace { /** * @brief fullscreen pipeline for the attachments */ - Pipeline::Config fullscreen(Shader const& vertex, Shader const& fragment, AttachmentMapping mapping) { - Pipeline::Config config{COLOR_FORMATS}; + GraphicsPipeline::Config fullscreen(Shader const& vertex, Shader const& fragment, AttachmentMapping mapping) { + GraphicsPipeline::Config config{COLOR_FORMATS}; config.addStage(vertex).addStage(fragment); config.vertexBindings = {{.binding = 0, .stride = sizeof(Vec3), .inputRate = VK_VERTEX_INPUT_RATE_VERTEX}}; config.vertexAttributes = {{.location = 0, .binding = 0, .format = VK_FORMAT_R32G32B32_SFLOAT, .offset = 0}}; @@ -183,10 +183,10 @@ namespace { Shader color; Shader localRead; PipelineLayout writeLayout; - Pipeline write; + GraphicsPipeline write; DescriptorSetLayout inputLayout; PipelineLayout readLayout; - Pipeline read; + GraphicsPipeline read; DescriptorPool pool; DescriptorSet input; Buffer vertices; diff --git a/cvk/tests/src/render/pipeline/compute_pipeline_test.cpp b/cvk/tests/src/render/pipeline/compute_pipeline_test.cpp new file mode 100644 index 00000000..d6641fdb --- /dev/null +++ b/cvk/tests/src/render/pipeline/compute_pipeline_test.cpp @@ -0,0 +1,154 @@ +#include "context.hpp" +#include "shaders.hpp" +#include "test.hpp" + +#include "cvk/data/buffer/buffer.hpp" +#include "cvk/data/descriptor/descriptor_pool.hpp" +#include "cvk/data/descriptor/descriptor_set.hpp" +#include "cvk/render/cmd/cmd_buffer.hpp" +#include "cvk/render/pipeline/compute_pipeline.hpp" +#include "cvk/render/pipeline/layout/descriptor_set_layout.hpp" +#include "cvk/render/pipeline/layout/pipeline_layout.hpp" +#include "cvk/render/pipeline/shader/shader.hpp" +#include "cvk/render/sync/barrier.hpp" + +#include +#include + + +namespace cvk { +namespace { + /** + * @brief work groups of the values shader, 64 invocations each + */ + constexpr uint32_t GROUPS = 4; + constexpr uint32_t VALUES = GROUPS * 64; + + constexpr Buffer::Config OUTPUT_CONFIG{ + .usage = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT, + .memory = {.flags = VMA_ALLOCATION_CREATE_HOST_ACCESS_RANDOM_BIT}, + }; + + /** + * @brief set layout, pipeline layout and shader of the values shader + */ + struct Values { + explicit Values(Core const& core) : setLayout{core, set_config(), create}, + layout{core, PipelineLayout::Config{.setLayouts = {&setLayout}}, create}, + shader{core, VK_SHADER_STAGE_COMPUTE_BIT, test::shaders::VALUES_COMP} {} + + [[nodiscard]] ComputePipeline::Config config() const { return {.stage = shader.stageInfo()}; } + + static DescriptorSetLayout::Config set_config() { + DescriptorSetLayout::Config config{}; + config.add(0, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, VK_SHADER_STAGE_COMPUTE_BIT); + return config; + } + + DescriptorSetLayout setLayout; + PipelineLayout layout; + Shader shader; + }; + + /** + * @brief binds the values pipeline and its storage buffer, records the dispatch and checks the values + * @param dispatch records the dispatch with the command buffer + */ + void expect_values(test::Context const& context, auto&& dispatch) { + Values const values{context.core}; + ComputePipeline const pipeline{context.core, values.layout, values.config(), create}; + Buffer const output{context.core, OUTPUT_CONFIG, VALUES}; + DescriptorPool const pool{context.core, DescriptorPool::Config{{&values.setLayout}}, create}; + DescriptorSet const set{pool, values.setLayout, create}; + set.write({0, output}); + + test::record_and_submit(context, [&](PrimaryCmdBuffer const& cmd) { + cmd.bindPipeline(pipeline); + cmd.bindDescriptorSet(set, values.layout, 0, VK_PIPELINE_BIND_POINT_COMPUTE); + dispatch(cmd); + + Barrier toHost{}; + toHost.add( + {VK_PIPELINE_STAGE_2_HOST_BIT, VK_ACCESS_2_HOST_READ_BIT}, + {VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT, VK_ACCESS_2_SHADER_WRITE_BIT} + ); + toHost.execute(cmd); + }); + + auto const mapped = output.map(); + output.invalidate(); + for(uint32_t i = 0; i < VALUES; ++i) + EXPECT_EQ(mapped[i], 3 * i); + } +} + +CVK_TEST(ComputePipeline, base_constructor_does_not_create) { + test::Context const context{}; + Values const values{context.core}; + + ComputePipeline const pipeline{context.core, values.layout, values.config()}; + + EXPECT_FALSE(pipeline.created()); + EXPECT_EQ(&pipeline.layout(), &values.layout); + EXPECT_EQ(pipeline.bindPoint(), VK_PIPELINE_BIND_POINT_COMPUTE); +} + +CVK_TEST(ComputePipeline, create_and_destroy) { + test::Context const context{}; + Values const values{context.core}; + + ComputePipeline pipeline{context.core, values.layout, values.config(), create}; + ASSERT_TRUE(pipeline.created()); + EXPECT_EQ(&pipeline.core(), &context.core); + + pipeline.destroy(); + EXPECT_FALSE(pipeline.created()); +} + +CVK_TEST(ComputePipeline, release_and_wrap) { + test::Context const context{}; + Values const values{context.core}; + ComputePipeline pipeline{context.core, values.layout, values.config(), create}; + auto const vkPipeline = pipeline.get(); + + auto const state = pipeline.release(); + EXPECT_FALSE(pipeline.created()); + + ComputePipeline const wrapped{context.core, values.layout, values.config(), state}; + EXPECT_EQ(wrapped.get(), vkPipeline); +} + +CVK_TEST(ComputePipeline, move_assignment_destroys_the_old_pipeline) { + test::Context const context{}; + Values const values{context.core}; + ComputePipeline pipeline{context.core, values.layout, values.config(), create}; + ComputePipeline other{context.core, values.layout, values.config(), create}; + auto const vkPipeline = other.get(); + + pipeline = std::move(other); + + EXPECT_EQ(pipeline.get(), vkPipeline); + EXPECT_FALSE(other.created()); +} + +CVK_TEST(ComputePipeline, dispatch_fills_a_storage_buffer) { + test::Context const context{}; + + expect_values(context, [](BaseCmdBuffer const& cmd) { cmd.dispatch(GROUPS); }); +} + +CVK_TEST(ComputePipeline, dispatch_indirect_fills_a_storage_buffer) { + test::Context const context{}; + Buffer const indirect{ + context.core, + { + .usage = VK_BUFFER_USAGE_INDIRECT_BUFFER_BIT, + .memory = {.flags = VMA_ALLOCATION_CREATE_HOST_ACCESS_SEQUENTIAL_WRITE_BIT}, + }, + 1 + }; + indirect.write(std::array{VkDispatchIndirectCommand{GROUPS, 1, 1}}); + + expect_values(context, [&](BaseCmdBuffer const& cmd) { cmd.dispatchIndirect(indirect); }); +} +} diff --git a/cvk/tests/src/render/pipeline/pipeline_test.cpp b/cvk/tests/src/render/pipeline/graphics_pipeline_test.cpp similarity index 72% rename from cvk/tests/src/render/pipeline/pipeline_test.cpp rename to cvk/tests/src/render/pipeline/graphics_pipeline_test.cpp index 17f921b9..6d9bbda4 100644 --- a/cvk/tests/src/render/pipeline/pipeline_test.cpp +++ b/cvk/tests/src/render/pipeline/graphics_pipeline_test.cpp @@ -3,9 +3,9 @@ #include "test.hpp" #include "cvk/render/cmd/cmd_buffer.hpp" +#include "cvk/render/pipeline/graphics_pipeline.hpp" #include "cvk/render/pipeline/layout/descriptor_set_layout.hpp" #include "cvk/render/pipeline/layout/pipeline_layout.hpp" -#include "cvk/render/pipeline/pipeline.hpp" #include "cvk/render/pipeline/shader/shader.hpp" #include @@ -33,8 +33,8 @@ namespace { vertex{core, VK_SHADER_STAGE_VERTEX_BIT, test::shaders::TRIANGLE_VERT}, fragment{core, VK_SHADER_STAGE_FRAGMENT_BIT, test::shaders::TRIANGLE_FRAG} {} - [[nodiscard]] Pipeline::Config config() const { - Pipeline::Config config{COLOR_FORMATS, DEPTH_FORMAT}; + [[nodiscard]] GraphicsPipeline::Config config() const { + GraphicsPipeline::Config config{COLOR_FORMATS, DEPTH_FORMAT}; config.addStage(vertex).addStage(fragment); config.vertexBindings = { {.binding = 0, .stride = 3 * sizeof(float), .inputRate = VK_VERTEX_INPUT_RATE_VERTEX}, @@ -58,8 +58,8 @@ namespace { }; } -CVK_TEST(Pipeline, config_from_the_formats) { - Pipeline::Config const config{COLOR_FORMATS, DEPTH_FORMAT}; +CVK_TEST(GraphicsPipeline, config_from_the_formats) { + GraphicsPipeline::Config const config{COLOR_FORMATS, DEPTH_FORMAT}; EXPECT_TRUE(std::ranges::equal(config.colorFormats, COLOR_FORMATS)); ASSERT_EQ(config.colorBlendAttachments.size(), 1); @@ -67,9 +67,9 @@ CVK_TEST(Pipeline, config_from_the_formats) { EXPECT_EQ(config.depthFormat, DEPTH_FORMAT); } -CVK_TEST(Pipeline, config_is_constexpr) { +CVK_TEST(GraphicsPipeline, config_is_constexpr) { static_assert([] { - Pipeline::Config config{COLOR_FORMATS, DEPTH_FORMAT}; + GraphicsPipeline::Config config{COLOR_FORMATS, DEPTH_FORMAT}; config.stages.push_back( VkPipelineShaderStageCreateInfo{ .sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO, @@ -86,7 +86,7 @@ CVK_TEST(Pipeline, config_is_constexpr) { }()); } -CVK_TEST(Pipeline, config_stages) { +CVK_TEST(GraphicsPipeline, config_stages) { test::Context const context{}; Triangle const triangle{context.core}; @@ -99,10 +99,10 @@ CVK_TEST(Pipeline, config_stages) { EXPECT_EQ(config.stages[0].stage, VK_SHADER_STAGE_FRAGMENT_BIT); } -CVK_TEST(Pipeline, bind) { +CVK_TEST(GraphicsPipeline, bind) { test::Context const context{}; Triangle const triangle{context.core}; - Pipeline const pipeline{context.core, triangle.layout, triangle.config(), create}; + GraphicsPipeline const pipeline{context.core, triangle.layout, triangle.config(), create}; CmdPool const pool{context.core, CmdPool::Config{context.core.queue(0)}, create}; PrimaryCmdBuffer primary{pool, create}; @@ -113,67 +113,68 @@ CVK_TEST(Pipeline, bind) { test::submit_and_wait(context, primary); } -CVK_TEST(Pipeline, base_constructor_does_not_create) { +CVK_TEST(GraphicsPipeline, base_constructor_does_not_create) { test::Context const context{}; Triangle const triangle{context.core}; - Pipeline const pipeline{context.core, triangle.layout, triangle.config()}; + GraphicsPipeline const pipeline{context.core, triangle.layout, triangle.config()}; EXPECT_FALSE(pipeline.created()); EXPECT_EQ(&pipeline.layout(), &triangle.layout); + EXPECT_EQ(pipeline.bindPoint(), VK_PIPELINE_BIND_POINT_GRAPHICS); } -CVK_TEST(Pipeline, create) { +CVK_TEST(GraphicsPipeline, create) { test::Context const context{}; Triangle const triangle{context.core}; - Pipeline const pipeline{context.core, triangle.layout, triangle.config(), create}; + GraphicsPipeline const pipeline{context.core, triangle.layout, triangle.config(), create}; EXPECT_TRUE(pipeline.created()); EXPECT_EQ(pipeline.config().stages.size(), 2); } -CVK_TEST(Pipeline, create_without_depth) { +CVK_TEST(GraphicsPipeline, create_without_depth) { test::Context const context{}; Triangle const triangle{context.core}; auto config = triangle.config(); config.depthFormat = VK_FORMAT_UNDEFINED; - Pipeline const pipeline{context.core, triangle.layout, std::move(config), create}; + GraphicsPipeline const pipeline{context.core, triangle.layout, std::move(config), create}; EXPECT_TRUE(pipeline.created()); } -CVK_TEST(Pipeline, shaders_can_be_destroyed_after_create) { +CVK_TEST(GraphicsPipeline, shaders_can_be_destroyed_after_create) { test::Context const context{}; auto triangle = std::make_unique(context.core); - Pipeline pipeline{context.core, triangle->layout, triangle->config(), create}; + GraphicsPipeline pipeline{context.core, triangle->layout, triangle->config(), create}; triangle->vertex.destroy(); triangle->fragment.destroy(); EXPECT_TRUE(pipeline.created()); } -CVK_TEST(Pipeline, release_and_wrap) { +CVK_TEST(GraphicsPipeline, release_and_wrap) { test::Context const context{}; Triangle const triangle{context.core}; - Pipeline pipeline{context.core, triangle.layout, triangle.config(), create}; + GraphicsPipeline pipeline{context.core, triangle.layout, triangle.config(), create}; auto const vkPipeline = pipeline.get(); auto const state = pipeline.release(); EXPECT_FALSE(pipeline.created()); - Pipeline const wrapped{context.core, triangle.layout, triangle.config(), state}; + GraphicsPipeline const wrapped{context.core, triangle.layout, triangle.config(), state}; EXPECT_EQ(wrapped.get(), vkPipeline); } -CVK_TEST(Pipeline, move_assignment_destroys_the_old_pipeline) { +CVK_TEST(GraphicsPipeline, move_assignment_destroys_the_old_pipeline) { test::Context const context{}; Triangle const triangle{context.core}; - Pipeline pipeline{context.core, triangle.layout, triangle.config(), create}; - Pipeline other{context.core, triangle.layout, triangle.config(), create}; + GraphicsPipeline pipeline{context.core, triangle.layout, triangle.config(), create}; + GraphicsPipeline other{context.core, triangle.layout, triangle.config(), create}; auto const vkPipeline = other.get(); pipeline = std::move(other); @@ -182,17 +183,17 @@ CVK_TEST(Pipeline, move_assignment_destroys_the_old_pipeline) { EXPECT_FALSE(other.created()); } -CVK_TEST(Pipeline, derivative) { +CVK_TEST(GraphicsPipeline, derivative) { test::Context const context{}; Triangle const triangle{context.core}; auto parentConfig = triangle.config(); parentConfig.flags = VK_PIPELINE_CREATE_ALLOW_DERIVATIVES_BIT; - Pipeline const parent{context.core, triangle.layout, std::move(parentConfig), create}; + GraphicsPipeline const parent{context.core, triangle.layout, std::move(parentConfig), create}; auto childConfig = triangle.config(); childConfig.rasterization.cullMode = VK_CULL_MODE_NONE; - Pipeline const child{context.core, parent, std::move(childConfig), create}; + GraphicsPipeline const child{context.core, parent, std::move(childConfig), create}; ASSERT_TRUE(child.created()); EXPECT_EQ(child.parent(), parent.get()); diff --git a/cvk/tests/src/shaders.hpp b/cvk/tests/src/shaders.hpp index 9546c023..0bda3bfb 100644 --- a/cvk/tests/src/shaders.hpp +++ b/cvk/tests/src/shaders.hpp @@ -176,4 +176,28 @@ inline constexpr std::array TEXTURE_FRAG{ 0x000200f8, 0x00000005, 0x0004003d, 0x0000000b, 0x0000000e, 0x0000000d, 0x00050057, 0x00000007, 0x00000012, 0x0000000e, 0x00000011, 0x0003003e, 0x00000009, 0x00000012, 0x000100fd, 0x00010038, }; + +/** + * @brief compute shader: local size 64, set 0 binding 0 storage buffer uint values[], value i = 3 * i + */ +inline constexpr std::array VALUES_COMP{ + 0x07230203, 0x00010600, 0x000d000b, 0x0000001a, 0x00000000, 0x00020011, 0x00000001, 0x0006000b, + 0x00000001, 0x4c534c47, 0x6474732e, 0x3035342e, 0x00000000, 0x0003000e, 0x00000000, 0x00000001, + 0x0007000f, 0x00000005, 0x00000004, 0x6e69616d, 0x00000000, 0x0000000a, 0x0000000f, 0x00060010, + 0x00000004, 0x00000011, 0x00000040, 0x00000001, 0x00000001, 0x00040047, 0x00000007, 0x00000006, + 0x00000004, 0x00030047, 0x00000008, 0x00000002, 0x00050048, 0x00000008, 0x00000000, 0x00000023, + 0x00000000, 0x00040047, 0x0000000a, 0x00000021, 0x00000000, 0x00040047, 0x0000000a, 0x00000022, + 0x00000000, 0x00040047, 0x0000000f, 0x0000000b, 0x0000001c, 0x00020013, 0x00000002, 0x00030021, + 0x00000003, 0x00000002, 0x00040015, 0x00000006, 0x00000020, 0x00000000, 0x0003001d, 0x00000007, + 0x00000006, 0x0003001e, 0x00000008, 0x00000007, 0x00040020, 0x00000009, 0x0000000c, 0x00000008, + 0x0004003b, 0x00000009, 0x0000000a, 0x0000000c, 0x00040015, 0x0000000b, 0x00000020, 0x00000001, + 0x0004002b, 0x0000000b, 0x0000000c, 0x00000000, 0x00040017, 0x0000000d, 0x00000006, 0x00000003, + 0x00040020, 0x0000000e, 0x00000001, 0x0000000d, 0x0004003b, 0x0000000e, 0x0000000f, 0x00000001, + 0x0004002b, 0x00000006, 0x00000010, 0x00000000, 0x00040020, 0x00000011, 0x00000001, 0x00000006, + 0x0004002b, 0x00000006, 0x00000016, 0x00000003, 0x00040020, 0x00000018, 0x0000000c, 0x00000006, + 0x00050036, 0x00000002, 0x00000004, 0x00000000, 0x00000003, 0x000200f8, 0x00000005, 0x00050041, + 0x00000011, 0x00000012, 0x0000000f, 0x00000010, 0x0004003d, 0x00000006, 0x00000013, 0x00000012, + 0x00050084, 0x00000006, 0x00000017, 0x00000013, 0x00000016, 0x00060041, 0x00000018, 0x00000019, + 0x0000000a, 0x0000000c, 0x00000013, 0x0003003e, 0x00000019, 0x00000017, 0x000100fd, 0x00010038, +}; } From 57c1380a1c42f1c574fe017c254cf583be059159 Mon Sep 17 00:00:00 2001 From: lukas <89145039+codethinki@users.noreply.github.com> Date: Wed, 21 Oct 2026 10:23:13 +0200 Subject: [PATCH 095/135] cvk: pipelines take the core from their layout --- .../cvk/render/pipeline/base_pipeline.hpp | 6 ++--- .../cvk/render/pipeline/compute_pipeline.hpp | 13 +++++------ .../cvk/render/pipeline/graphics_pipeline.hpp | 20 ++++++++--------- cvk/src/render/pipeline/base_pipeline.cpp | 6 ++--- cvk/src/render/pipeline/compute_pipeline.cpp | 22 +++++++++---------- cvk/src/render/pipeline/graphics_pipeline.cpp | 20 ++++++++--------- .../data/descriptor/descriptor_draw_test.cpp | 2 +- cvk/tests/src/render/cmd/draw_test.cpp | 2 +- cvk/tests/src/render/cmd/local_read_test.cpp | 4 ++-- .../render/pipeline/compute_pipeline_test.cpp | 14 ++++++------ .../pipeline/graphics_pipeline_test.cpp | 22 +++++++++---------- 11 files changed, 63 insertions(+), 68 deletions(-) diff --git a/cvk/incl/cvk/render/pipeline/base_pipeline.hpp b/cvk/incl/cvk/render/pipeline/base_pipeline.hpp index e306ce65..60508423 100644 --- a/cvk/incl/cvk/render/pipeline/base_pipeline.hpp +++ b/cvk/incl/cvk/render/pipeline/base_pipeline.hpp @@ -59,10 +59,10 @@ class BasePipeline { protected: /** - * @brief base constructor, keeps the layout - * @pre @ref Core::created() + * @brief base constructor, keeps the layout, the core comes from the layout + * @pre @ref Core::created() of the layout */ - BasePipeline(Core const&, PipelineLayout const&, VkPipelineBindPoint); + BasePipeline(PipelineLayout const&, VkPipelineBindPoint); /** * @note calls @ref optDestroy() diff --git a/cvk/incl/cvk/render/pipeline/compute_pipeline.hpp b/cvk/incl/cvk/render/pipeline/compute_pipeline.hpp index 4ac775be..04d0d171 100644 --- a/cvk/incl/cvk/render/pipeline/compute_pipeline.hpp +++ b/cvk/incl/cvk/render/pipeline/compute_pipeline.hpp @@ -7,7 +7,6 @@ namespace cvk { -class Core; class PipelineLayout; /** @@ -35,25 +34,25 @@ class ComputePipeline : public BasePipeline { /** * @brief base constructor, keeps the layout - * @pre @ref Core::created() + * @pre @ref Core::created() of the layout */ - ComputePipeline(Core const&, PipelineLayout const&, Config const&); + ComputePipeline(PipelineLayout const&, Config const&); /** * @brief constructs and wraps * @details calls: - * - @ref ComputePipeline(Core const&, PipelineLayout const&, Config const&) + * - @ref ComputePipeline(PipelineLayout const&, Config const&) * - @ref wrap() */ - ComputePipeline(Core const&, PipelineLayout const&, Config const&, State const&); + ComputePipeline(PipelineLayout const&, Config const&, State const&); /** * @brief constructs and creates * @details calls: - * - @ref ComputePipeline(Core const&, PipelineLayout const&, Config const&) + * - @ref ComputePipeline(PipelineLayout const&, Config const&) * - @ref create() */ - explicit ComputePipeline(Core const&, PipelineLayout const&, Config const&, create_t); + explicit ComputePipeline(PipelineLayout const&, Config const&, create_t); /** * @brief creates the pipeline diff --git a/cvk/incl/cvk/render/pipeline/graphics_pipeline.hpp b/cvk/incl/cvk/render/pipeline/graphics_pipeline.hpp index 984005bb..cf1a117a 100644 --- a/cvk/incl/cvk/render/pipeline/graphics_pipeline.hpp +++ b/cvk/incl/cvk/render/pipeline/graphics_pipeline.hpp @@ -8,7 +8,6 @@ namespace cvk { -class Core; class PipelineLayout; /** @@ -20,40 +19,39 @@ class GraphicsPipeline : public BasePipeline { /** * @brief base constructor, keeps the layout - * @pre @ref Core::created() + * @pre @ref Core::created() of the layout */ - GraphicsPipeline(Core const&, PipelineLayout const&, Config); + GraphicsPipeline(PipelineLayout const&, Config); /** * @brief constructs and wraps * @details calls: - * - @ref GraphicsPipeline(Core const&, PipelineLayout const&, Config) + * - @ref GraphicsPipeline(PipelineLayout const&, Config) * - @ref wrap() */ - GraphicsPipeline(Core const&, PipelineLayout const&, Config, State const&); + GraphicsPipeline(PipelineLayout const&, Config, State const&); /** * @brief constructs and creates * @details calls: - * - @ref GraphicsPipeline(Core const&, PipelineLayout const&, Config) + * - @ref GraphicsPipeline(PipelineLayout const&, Config) * - @ref create() */ - explicit GraphicsPipeline(Core const&, PipelineLayout const&, Config, create_t); + explicit GraphicsPipeline(PipelineLayout const&, Config, create_t); /** * @brief base constructor of a derivative pipeline, keeps the layout and copies the handle of the parent - * @pre @ref Core::created() * @pre @ref created() of the parent, with VK_PIPELINE_CREATE_ALLOW_DERIVATIVES_BIT in its flags */ - GraphicsPipeline(Core const&, GraphicsPipeline const& parent, Config); + GraphicsPipeline(GraphicsPipeline const& parent, Config); /** * @brief constructs and creates a derivative pipeline * @details calls: - * - @ref GraphicsPipeline(Core const&, GraphicsPipeline const&, Config) + * - @ref GraphicsPipeline(GraphicsPipeline const&, Config) * - @ref create() */ - explicit GraphicsPipeline(Core const&, GraphicsPipeline const& parent, Config, create_t); + explicit GraphicsPipeline(GraphicsPipeline const& parent, Config, create_t); /** * @brief creates the pipeline diff --git a/cvk/src/render/pipeline/base_pipeline.cpp b/cvk/src/render/pipeline/base_pipeline.cpp index 7d506661..d0a36fc3 100644 --- a/cvk/src/render/pipeline/base_pipeline.cpp +++ b/cvk/src/render/pipeline/base_pipeline.cpp @@ -6,9 +6,9 @@ namespace cvk { -BasePipeline::BasePipeline(Core const& core, PipelineLayout const& layout, VkPipelineBindPoint bindPoint) : - _handle{core}, _layout{&layout}, _bindPoint{bindPoint} { - Core::debug_check(core); +BasePipeline::BasePipeline(PipelineLayout const& layout, VkPipelineBindPoint bindPoint) : _handle{layout.core()}, + _layout{&layout}, _bindPoint{bindPoint} { + Core::debug_check(layout.core()); } void BasePipeline::wrap(State const& state) { diff --git a/cvk/src/render/pipeline/compute_pipeline.cpp b/cvk/src/render/pipeline/compute_pipeline.cpp index 332dc356..26bfb684 100644 --- a/cvk/src/render/pipeline/compute_pipeline.cpp +++ b/cvk/src/render/pipeline/compute_pipeline.cpp @@ -4,18 +4,16 @@ namespace cvk { -ComputePipeline::ComputePipeline(Core const& core, PipelineLayout const& layout, Config const& config) : - BasePipeline{core, layout, VK_PIPELINE_BIND_POINT_COMPUTE}, _config{config} {} - -ComputePipeline::ComputePipeline( - Core const& core, - PipelineLayout const& layout, - Config const& config, - State const& state -) : ComputePipeline{core, layout, config} { wrap(state); } - -ComputePipeline::ComputePipeline(Core const& core, PipelineLayout const& layout, Config const& config, create_t) : - ComputePipeline{core, layout, config} { +ComputePipeline::ComputePipeline(PipelineLayout const& layout, Config const& config) : + BasePipeline{layout, VK_PIPELINE_BIND_POINT_COMPUTE}, _config{config} {} + +ComputePipeline::ComputePipeline(PipelineLayout const& layout, Config const& config, State const& state) : + ComputePipeline{layout, config} { + wrap(state); +} + +ComputePipeline::ComputePipeline(PipelineLayout const& layout, Config const& config, create_t) : + ComputePipeline{layout, config} { create(); } diff --git a/cvk/src/render/pipeline/graphics_pipeline.cpp b/cvk/src/render/pipeline/graphics_pipeline.cpp index 0a38947d..5c0ecea9 100644 --- a/cvk/src/render/pipeline/graphics_pipeline.cpp +++ b/cvk/src/render/pipeline/graphics_pipeline.cpp @@ -6,21 +6,21 @@ namespace cvk { -GraphicsPipeline::GraphicsPipeline(Core const& core, PipelineLayout const& layout, Config config) : - BasePipeline{core, layout, VK_PIPELINE_BIND_POINT_GRAPHICS}, _config{std::move(config)} {} +GraphicsPipeline::GraphicsPipeline(PipelineLayout const& layout, Config config) : + BasePipeline{layout, VK_PIPELINE_BIND_POINT_GRAPHICS}, _config{std::move(config)} {} -GraphicsPipeline::GraphicsPipeline(Core const& core, PipelineLayout const& layout, Config config, State const& state) : - GraphicsPipeline{core, layout, std::move(config)} { +GraphicsPipeline::GraphicsPipeline(PipelineLayout const& layout, Config config, State const& state) : + GraphicsPipeline{layout, std::move(config)} { wrap(state); } -GraphicsPipeline::GraphicsPipeline(Core const& core, PipelineLayout const& layout, Config config, create_t) : - GraphicsPipeline{core, layout, std::move(config)} { +GraphicsPipeline::GraphicsPipeline(PipelineLayout const& layout, Config config, create_t) : + GraphicsPipeline{layout, std::move(config)} { create(); } -GraphicsPipeline::GraphicsPipeline(Core const& core, GraphicsPipeline const& parent, Config config) : - BasePipeline{core, parent.layout(), VK_PIPELINE_BIND_POINT_GRAPHICS}, +GraphicsPipeline::GraphicsPipeline(GraphicsPipeline const& parent, Config config) : + BasePipeline{parent.layout(), VK_PIPELINE_BIND_POINT_GRAPHICS}, _vkParent{parent.get()}, _config{std::move(config)} { debug_check(parent); @@ -30,8 +30,8 @@ GraphicsPipeline::GraphicsPipeline(Core const& core, GraphicsPipeline const& par ) {} } -GraphicsPipeline::GraphicsPipeline(Core const& core, GraphicsPipeline const& parent, Config config, create_t) : - GraphicsPipeline{core, parent, std::move(config)} { +GraphicsPipeline::GraphicsPipeline(GraphicsPipeline const& parent, Config config, create_t) : + GraphicsPipeline{parent, std::move(config)} { create(); } diff --git a/cvk/tests/src/data/descriptor/descriptor_draw_test.cpp b/cvk/tests/src/data/descriptor/descriptor_draw_test.cpp index e2b4ad13..fa177d7c 100644 --- a/cvk/tests/src/data/descriptor/descriptor_draw_test.cpp +++ b/cvk/tests/src/data/descriptor/descriptor_draw_test.cpp @@ -58,7 +58,7 @@ namespace { }, vertex{core, VK_SHADER_STAGE_VERTEX_BIT, test::shaders::FILL_VERT}, fragment{core, VK_SHADER_STAGE_FRAGMENT_BIT, fragmentCode}, - pipeline{core, layout, config(), create}, + pipeline{layout, config(), create}, vertices{core, host_written(VK_BUFFER_USAGE_VERTEX_BUFFER_BIT), FULLSCREEN.size()} { vertices.write(FULLSCREEN); } diff --git a/cvk/tests/src/render/cmd/draw_test.cpp b/cvk/tests/src/render/cmd/draw_test.cpp index 0a9f3cb0..a4d12443 100644 --- a/cvk/tests/src/render/cmd/draw_test.cpp +++ b/cvk/tests/src/render/cmd/draw_test.cpp @@ -55,7 +55,7 @@ namespace { }, vertex{core, VK_SHADER_STAGE_VERTEX_BIT, test::shaders::FILL_VERT}, fragment{core, VK_SHADER_STAGE_FRAGMENT_BIT, test::shaders::TRIANGLE_FRAG}, - pipeline{core, layout, config(), create}, + pipeline{layout, config(), create}, vertices{core, host_written(VK_BUFFER_USAGE_VERTEX_BUFFER_BIT), FULLSCREEN.size()} { vertices.write(FULLSCREEN); } diff --git a/cvk/tests/src/render/cmd/local_read_test.cpp b/cvk/tests/src/render/cmd/local_read_test.cpp index 0ff07a79..b2bd60ca 100644 --- a/cvk/tests/src/render/cmd/local_read_test.cpp +++ b/cvk/tests/src/render/cmd/local_read_test.cpp @@ -149,10 +149,10 @@ namespace { color{core, VK_SHADER_STAGE_FRAGMENT_BIT, test::shaders::TRIANGLE_FRAG}, localRead{core, VK_SHADER_STAGE_FRAGMENT_BIT, test::shaders::LOCAL_READ_FRAG}, writeLayout{core, {.pushConstantRanges = {{VK_SHADER_STAGE_FRAGMENT_BIT, 0, sizeof(GREEN)}}}, create}, - write{core, writeLayout, fullscreen(vertex, color, write_first()), create}, + write{writeLayout, fullscreen(vertex, color, write_first()), create}, inputLayout{core, input_attachment(), create}, readLayout{core, {.setLayouts = {&inputLayout}}, create}, - read{core, readLayout, fullscreen(vertex, localRead, copy_first_to_second()), create}, + read{readLayout, fullscreen(vertex, localRead, copy_first_to_second()), create}, pool{core, {{&inputLayout}}, create}, input{pool, inputLayout, create}, vertices{ diff --git a/cvk/tests/src/render/pipeline/compute_pipeline_test.cpp b/cvk/tests/src/render/pipeline/compute_pipeline_test.cpp index d6641fdb..dd7c02b6 100644 --- a/cvk/tests/src/render/pipeline/compute_pipeline_test.cpp +++ b/cvk/tests/src/render/pipeline/compute_pipeline_test.cpp @@ -56,7 +56,7 @@ namespace { */ void expect_values(test::Context const& context, auto&& dispatch) { Values const values{context.core}; - ComputePipeline const pipeline{context.core, values.layout, values.config(), create}; + ComputePipeline const pipeline{values.layout, values.config(), create}; Buffer const output{context.core, OUTPUT_CONFIG, VALUES}; DescriptorPool const pool{context.core, DescriptorPool::Config{{&values.setLayout}}, create}; DescriptorSet const set{pool, values.setLayout, create}; @@ -86,7 +86,7 @@ CVK_TEST(ComputePipeline, base_constructor_does_not_create) { test::Context const context{}; Values const values{context.core}; - ComputePipeline const pipeline{context.core, values.layout, values.config()}; + ComputePipeline const pipeline{values.layout, values.config()}; EXPECT_FALSE(pipeline.created()); EXPECT_EQ(&pipeline.layout(), &values.layout); @@ -97,7 +97,7 @@ CVK_TEST(ComputePipeline, create_and_destroy) { test::Context const context{}; Values const values{context.core}; - ComputePipeline pipeline{context.core, values.layout, values.config(), create}; + ComputePipeline pipeline{values.layout, values.config(), create}; ASSERT_TRUE(pipeline.created()); EXPECT_EQ(&pipeline.core(), &context.core); @@ -108,21 +108,21 @@ CVK_TEST(ComputePipeline, create_and_destroy) { CVK_TEST(ComputePipeline, release_and_wrap) { test::Context const context{}; Values const values{context.core}; - ComputePipeline pipeline{context.core, values.layout, values.config(), create}; + ComputePipeline pipeline{values.layout, values.config(), create}; auto const vkPipeline = pipeline.get(); auto const state = pipeline.release(); EXPECT_FALSE(pipeline.created()); - ComputePipeline const wrapped{context.core, values.layout, values.config(), state}; + ComputePipeline const wrapped{values.layout, values.config(), state}; EXPECT_EQ(wrapped.get(), vkPipeline); } CVK_TEST(ComputePipeline, move_assignment_destroys_the_old_pipeline) { test::Context const context{}; Values const values{context.core}; - ComputePipeline pipeline{context.core, values.layout, values.config(), create}; - ComputePipeline other{context.core, values.layout, values.config(), create}; + ComputePipeline pipeline{values.layout, values.config(), create}; + ComputePipeline other{values.layout, values.config(), create}; auto const vkPipeline = other.get(); pipeline = std::move(other); diff --git a/cvk/tests/src/render/pipeline/graphics_pipeline_test.cpp b/cvk/tests/src/render/pipeline/graphics_pipeline_test.cpp index 6d9bbda4..70d0afb0 100644 --- a/cvk/tests/src/render/pipeline/graphics_pipeline_test.cpp +++ b/cvk/tests/src/render/pipeline/graphics_pipeline_test.cpp @@ -102,7 +102,7 @@ CVK_TEST(GraphicsPipeline, config_stages) { CVK_TEST(GraphicsPipeline, bind) { test::Context const context{}; Triangle const triangle{context.core}; - GraphicsPipeline const pipeline{context.core, triangle.layout, triangle.config(), create}; + GraphicsPipeline const pipeline{triangle.layout, triangle.config(), create}; CmdPool const pool{context.core, CmdPool::Config{context.core.queue(0)}, create}; PrimaryCmdBuffer primary{pool, create}; @@ -117,7 +117,7 @@ CVK_TEST(GraphicsPipeline, base_constructor_does_not_create) { test::Context const context{}; Triangle const triangle{context.core}; - GraphicsPipeline const pipeline{context.core, triangle.layout, triangle.config()}; + GraphicsPipeline const pipeline{triangle.layout, triangle.config()}; EXPECT_FALSE(pipeline.created()); EXPECT_EQ(&pipeline.layout(), &triangle.layout); @@ -128,7 +128,7 @@ CVK_TEST(GraphicsPipeline, create) { test::Context const context{}; Triangle const triangle{context.core}; - GraphicsPipeline const pipeline{context.core, triangle.layout, triangle.config(), create}; + GraphicsPipeline const pipeline{triangle.layout, triangle.config(), create}; EXPECT_TRUE(pipeline.created()); EXPECT_EQ(pipeline.config().stages.size(), 2); @@ -141,7 +141,7 @@ CVK_TEST(GraphicsPipeline, create_without_depth) { auto config = triangle.config(); config.depthFormat = VK_FORMAT_UNDEFINED; - GraphicsPipeline const pipeline{context.core, triangle.layout, std::move(config), create}; + GraphicsPipeline const pipeline{triangle.layout, std::move(config), create}; EXPECT_TRUE(pipeline.created()); } @@ -150,7 +150,7 @@ CVK_TEST(GraphicsPipeline, shaders_can_be_destroyed_after_create) { test::Context const context{}; auto triangle = std::make_unique(context.core); - GraphicsPipeline pipeline{context.core, triangle->layout, triangle->config(), create}; + GraphicsPipeline pipeline{triangle->layout, triangle->config(), create}; triangle->vertex.destroy(); triangle->fragment.destroy(); @@ -160,21 +160,21 @@ CVK_TEST(GraphicsPipeline, shaders_can_be_destroyed_after_create) { CVK_TEST(GraphicsPipeline, release_and_wrap) { test::Context const context{}; Triangle const triangle{context.core}; - GraphicsPipeline pipeline{context.core, triangle.layout, triangle.config(), create}; + GraphicsPipeline pipeline{triangle.layout, triangle.config(), create}; auto const vkPipeline = pipeline.get(); auto const state = pipeline.release(); EXPECT_FALSE(pipeline.created()); - GraphicsPipeline const wrapped{context.core, triangle.layout, triangle.config(), state}; + GraphicsPipeline const wrapped{triangle.layout, triangle.config(), state}; EXPECT_EQ(wrapped.get(), vkPipeline); } CVK_TEST(GraphicsPipeline, move_assignment_destroys_the_old_pipeline) { test::Context const context{}; Triangle const triangle{context.core}; - GraphicsPipeline pipeline{context.core, triangle.layout, triangle.config(), create}; - GraphicsPipeline other{context.core, triangle.layout, triangle.config(), create}; + GraphicsPipeline pipeline{triangle.layout, triangle.config(), create}; + GraphicsPipeline other{triangle.layout, triangle.config(), create}; auto const vkPipeline = other.get(); pipeline = std::move(other); @@ -189,11 +189,11 @@ CVK_TEST(GraphicsPipeline, derivative) { auto parentConfig = triangle.config(); parentConfig.flags = VK_PIPELINE_CREATE_ALLOW_DERIVATIVES_BIT; - GraphicsPipeline const parent{context.core, triangle.layout, std::move(parentConfig), create}; + GraphicsPipeline const parent{triangle.layout, std::move(parentConfig), create}; auto childConfig = triangle.config(); childConfig.rasterization.cullMode = VK_CULL_MODE_NONE; - GraphicsPipeline const child{context.core, parent, std::move(childConfig), create}; + GraphicsPipeline const child{parent, std::move(childConfig), create}; ASSERT_TRUE(child.created()); EXPECT_EQ(child.parent(), parent.get()); From fdbb2aefd2a3c906c70f2c4fec3b8ee0918ed7c6 Mon Sep 17 00:00:00 2001 From: lukas <89145039+codethinki@users.noreply.github.com> Date: Wed, 21 Oct 2026 10:35:36 +0200 Subject: [PATCH 096/135] cvk: CmdPoolConfig, GraphicsPipelineConfig and DescriptorPoolConfig are aggregates --- .../descriptor/descriptor_pool_config.hpp | 12 +++-- cvk/incl/cvk/render/cmd/cmd_pool.hpp | 1 + cvk/incl/cvk/render/cmd/cmd_pool_config.hpp | 19 ++----- .../cvk/render/pipeline/graphics_pipeline.hpp | 1 + .../pipeline/graphics_pipeline_config.hpp | 22 +------- cvk/src/render/cmd/cmd_pool.cpp | 2 + cvk/src/render/pipeline/graphics_pipeline.cpp | 18 +++++-- cvk/tests/src/base/queue/submit_test.cpp | 2 +- cvk/tests/src/context.hpp | 2 +- .../data/descriptor/descriptor_draw_test.cpp | 16 ++++-- .../data/descriptor/descriptor_pool_test.cpp | 22 +++++--- .../data/descriptor/descriptor_set_test.cpp | 16 +++--- cvk/tests/src/render/cmd/cmd_buffer_test.cpp | 2 +- cvk/tests/src/render/cmd/cmd_pool_test.cpp | 20 ++++--- cvk/tests/src/render/cmd/draw_test.cpp | 8 ++- cvk/tests/src/render/cmd/local_read_test.cpp | 6 +-- cvk/tests/src/render/cmd/rendering_test.cpp | 2 +- .../render/pipeline/compute_pipeline_test.cpp | 2 +- .../pipeline/graphics_pipeline_test.cpp | 54 ++++++++++++++++--- cvk/tests/src/surface/swapchain_test.cpp | 2 +- 20 files changed, 139 insertions(+), 90 deletions(-) diff --git a/cvk/incl/cvk/data/descriptor/descriptor_pool_config.hpp b/cvk/incl/cvk/data/descriptor/descriptor_pool_config.hpp index 40907289..7d80a2b6 100644 --- a/cvk/incl/cvk/data/descriptor/descriptor_pool_config.hpp +++ b/cvk/incl/cvk/data/descriptor/descriptor_pool_config.hpp @@ -29,17 +29,19 @@ struct DescriptorPoolConfig { uint32_t variableCount = 0; }; - constexpr DescriptorPoolConfig() = default; - /** * @brief counts the sets and descriptors of the layouts * @param flags update after bind layouts add VK_DESCRIPTOR_POOL_CREATE_UPDATE_AFTER_BIND_BIT * @details calls @ref add() per entry */ - constexpr DescriptorPoolConfig(std::initializer_list sets, VkDescriptorPoolCreateFlags flags = 0) : - flags{flags} { + [[nodiscard]] static constexpr DescriptorPoolConfig ForLayouts( + std::initializer_list sets, + VkDescriptorPoolCreateFlags flags = 0 + ) { + DescriptorPoolConfig config{.flags = flags}; for(auto const& [layout, count, variableCount] : sets) - add(*layout, count, variableCount); + config.add(*layout, count, variableCount); + return config; } /** diff --git a/cvk/incl/cvk/render/cmd/cmd_pool.hpp b/cvk/incl/cvk/render/cmd/cmd_pool.hpp index 276dbb10..592722e2 100644 --- a/cvk/incl/cvk/render/cmd/cmd_pool.hpp +++ b/cvk/incl/cvk/render/cmd/cmd_pool.hpp @@ -61,6 +61,7 @@ class CmdPool { /** * @brief creates the pool + * @pre the config has a queue family (!= VK_QUEUE_FAMILY_IGNORED) * @post @ref created() * @note calls @ref optDestroy() * @throws cvk::vk_result_exception result of vkCreateCommandPool() diff --git a/cvk/incl/cvk/render/cmd/cmd_pool_config.hpp b/cvk/incl/cvk/render/cmd/cmd_pool_config.hpp index ede0848b..15024c38 100644 --- a/cvk/incl/cvk/render/cmd/cmd_pool_config.hpp +++ b/cvk/incl/cvk/render/cmd/cmd_pool_config.hpp @@ -1,5 +1,4 @@ #pragma once -#include "cvk/base/queue/queue.hpp" #include "cvk/base/queue/queue_family.hpp" #include @@ -13,23 +12,11 @@ struct CmdPoolConfig { static constexpr VkCommandPoolCreateFlags DEFAULT_FLAGS = VK_COMMAND_POOL_CREATE_TRANSIENT_BIT | VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT; - constexpr CmdPoolConfig() = default; - - explicit constexpr CmdPoolConfig( - queue_family_index_t queueFamilyIndex, - VkCommandPoolCreateFlags flags = DEFAULT_FLAGS - ) : queueFamilyIndex{queueFamilyIndex}, flags{flags} {} - - /** - * @brief config for the family of the queue - */ - explicit constexpr CmdPoolConfig(Queue const& queue, VkCommandPoolCreateFlags flags = DEFAULT_FLAGS) : - CmdPoolConfig{queue.familyIndex(), flags} {} - /** - * @brief the command buffers of the pool are submitted to queues of this family + * @brief the family of the queues that the command buffers are submitted to + * @details VK_QUEUE_FAMILY_IGNORED is not allowed at create */ - queue_family_index_t queueFamilyIndex = 0; + queue_family_index_t queueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; /** * @brief VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT -> @ref BaseCmdBuffer::reset() is allowed diff --git a/cvk/incl/cvk/render/pipeline/graphics_pipeline.hpp b/cvk/incl/cvk/render/pipeline/graphics_pipeline.hpp index cf1a117a..e879695a 100644 --- a/cvk/incl/cvk/render/pipeline/graphics_pipeline.hpp +++ b/cvk/incl/cvk/render/pipeline/graphics_pipeline.hpp @@ -56,6 +56,7 @@ class GraphicsPipeline : public BasePipeline { /** * @brief creates the pipeline * @details derivative pipeline -> adds VK_PIPELINE_CREATE_DERIVATIVE_BIT + * empty @ref Config::colorBlendAttachments -> @ref Config::ALPHA_BLENDING per color format, the config is kept * @pre the layout, the parent and the shader modules of the config are not destroyed * @post @ref created() * @note calls @ref optDestroy() diff --git a/cvk/incl/cvk/render/pipeline/graphics_pipeline_config.hpp b/cvk/incl/cvk/render/pipeline/graphics_pipeline_config.hpp index 6b9d4fb1..134cb35c 100644 --- a/cvk/incl/cvk/render/pipeline/graphics_pipeline_config.hpp +++ b/cvk/incl/cvk/render/pipeline/graphics_pipeline_config.hpp @@ -5,8 +5,6 @@ #include -#include -#include #include @@ -33,17 +31,6 @@ struct GraphicsPipelineConfig { | VK_COLOR_COMPONENT_A_BIT, }; - constexpr GraphicsPipelineConfig() = default; - - /** - * @brief one @ref ALPHA_BLENDING attachment per color format - * @param depthFormat VK_FORMAT_UNDEFINED -> no depth attachment - */ - explicit constexpr GraphicsPipelineConfig( - std::span colorFormats, - VkFormat depthFormat = VK_FORMAT_UNDEFINED - ); - /** * @brief adds the stage of the shader * @param specialization nullptr -> no specialization constants @@ -134,8 +121,8 @@ struct GraphicsPipelineConfig { }; /** - * @brief blend state per color attachment - * @pre size == @ref colorFormats size + * @brief blend state per color attachment, empty -> @ref ALPHA_BLENDING per color format + * @pre empty or size == @ref colorFormats size */ std::vector colorBlendAttachments{}; @@ -161,11 +148,6 @@ struct GraphicsPipelineConfig { } namespace cvk { -constexpr GraphicsPipelineConfig::GraphicsPipelineConfig(std::span colorFormats, VkFormat depthFormat) : - colorBlendAttachments(colorFormats.size(), ALPHA_BLENDING), - colorFormats{std::from_range, colorFormats}, - depthFormat{depthFormat} {} - constexpr GraphicsPipelineConfig& GraphicsPipelineConfig::removeStage(VkShaderStageFlagBits stage) { auto const removed = std::erase_if( stages, diff --git a/cvk/src/render/cmd/cmd_pool.cpp b/cvk/src/render/cmd/cmd_pool.cpp index 808f93cf..74ff89cf 100644 --- a/cvk/src/render/cmd/cmd_pool.cpp +++ b/cvk/src/render/cmd/cmd_pool.cpp @@ -20,6 +20,8 @@ void CmdPool::wrap(State const& state) { } void CmdPool::create() { + CTH_CRITICAL(_config.queueFamilyIndex == VK_QUEUE_FAMILY_IGNORED, "the config needs a queue family") {} + optDestroy(); auto const info = _config.createInfo(); diff --git a/cvk/src/render/pipeline/graphics_pipeline.cpp b/cvk/src/render/pipeline/graphics_pipeline.cpp index 5c0ecea9..08bc928f 100644 --- a/cvk/src/render/pipeline/graphics_pipeline.cpp +++ b/cvk/src/render/pipeline/graphics_pipeline.cpp @@ -37,8 +37,10 @@ GraphicsPipeline::GraphicsPipeline(GraphicsPipeline const& parent, Config config void GraphicsPipeline::create() { CTH_CRITICAL( - _config.colorBlendAttachments.size() != _config.colorFormats.size(), - "one color blend attachment per color format required" + !_config.colorBlendAttachments.empty() && _config.colorBlendAttachments.size() != _config.colorFormats.size(), + "one color blend attachment per color format required, got {} for {}", + _config.colorBlendAttachments.size(), + _config.colorFormats.size() ) {} AttachmentMapping::debug_check(_config.attachmentMapping, _config.colorFormats.size()); @@ -64,9 +66,17 @@ void GraphicsPipeline::create() { .pScissors = nullptr, }; + std::vector const defaultBlendAttachments( + config.colorBlendAttachments.empty() ? config.colorFormats.size() : 0, + Config::ALPHA_BLENDING + ); + auto const& blendAttachments = config.colorBlendAttachments.empty() + ? defaultBlendAttachments + : config.colorBlendAttachments; + auto colorBlend = config.colorBlend; - colorBlend.attachmentCount = static_cast(config.colorBlendAttachments.size()); - colorBlend.pAttachments = config.colorBlendAttachments.data(); + colorBlend.attachmentCount = static_cast(blendAttachments.size()); + colorBlend.pAttachments = blendAttachments.data(); VkPipelineDynamicStateCreateInfo const dynamicState{ .sType = VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO, diff --git a/cvk/tests/src/base/queue/submit_test.cpp b/cvk/tests/src/base/queue/submit_test.cpp index 0c577b3f..983927ea 100644 --- a/cvk/tests/src/base/queue/submit_test.cpp +++ b/cvk/tests/src/base/queue/submit_test.cpp @@ -32,7 +32,7 @@ CVK_TEST(Submit, semaphore_info_takes_the_kind_from_the_semaphore) { CVK_TEST(Submit, command_buffer_without_fence) { test::Context const context{}; - CmdPool const pool{context.core, CmdPool::Config{context.core.queue(0)}, create}; + CmdPool const pool{context.core, CmdPool::Config{.queueFamilyIndex = context.core.queue(0).familyIndex()}, create}; PrimaryCmdBuffer primary{pool, create}; primary.begin(VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT); primary.end(); diff --git a/cvk/tests/src/context.hpp b/cvk/tests/src/context.hpp index 4099b9d5..f08173db 100644 --- a/cvk/tests/src/context.hpp +++ b/cvk/tests/src/context.hpp @@ -133,7 +133,7 @@ void copy_to_buffer( */ template void record_and_submit(Context const& context, Record&& record) { - CmdPool const pool{context.core, CmdPool::Config{context.core.queue(0)}, create}; + CmdPool const pool{context.core, CmdPool::Config{.queueFamilyIndex = context.core.queue(0).familyIndex()}, create}; PrimaryCmdBuffer primary{pool, create}; primary.begin(VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT); diff --git a/cvk/tests/src/data/descriptor/descriptor_draw_test.cpp b/cvk/tests/src/data/descriptor/descriptor_draw_test.cpp index fa177d7c..78b42c29 100644 --- a/cvk/tests/src/data/descriptor/descriptor_draw_test.cpp +++ b/cvk/tests/src/data/descriptor/descriptor_draw_test.cpp @@ -64,7 +64,7 @@ namespace { } [[nodiscard]] GraphicsPipeline::Config config() const { - GraphicsPipeline::Config config{COLOR_FORMATS}; + GraphicsPipeline::Config config{.colorFormats = {std::from_range, COLOR_FORMATS}}; config.addStage(vertex).addStage(fragment); config.vertexBindings = {{.binding = 0, .stride = sizeof(Vec3), .inputRate = VK_VERTEX_INPUT_RATE_VERTEX}}; config.vertexAttributes = { @@ -179,7 +179,7 @@ CVK_TEST(DescriptorDraw, uniform_buffer_color) { }; color.write(std::array, 1>{GREEN}); - DescriptorPool const pool{context.core, DescriptorPool::Config{{&setLayout}}, create}; + DescriptorPool const pool{context.core, DescriptorPool::Config::ForLayouts({{&setLayout}}), create}; DescriptorSet const set{pool, setLayout, create}; set.write({0, color}); @@ -235,11 +235,19 @@ namespace { staging.write(GREEN_PIXEL); uint32_t const variableCount = afterBind ? 32 : 0; - DescriptorPool const pool{context.core, {{&setLayout, 1, variableCount}}, create}; + DescriptorPool const pool{ + context.core, + DescriptorPool::Config::ForLayouts({{&setLayout, 1, variableCount}}), + create + }; DescriptorSet const set{pool, setLayout, variableCount, create}; if(!afterBind) set.write({0, view, sampler, index, layout}); - CmdPool const cmdPool{context.core, CmdPool::Config{context.core.queue(0)}, create}; + CmdPool const cmdPool{ + context.core, + CmdPool::Config{.queueFamilyIndex = context.core.queue(0).familyIndex()}, + create + }; PrimaryCmdBuffer cmd{cmdPool, create}; cmd.begin(VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT); diff --git a/cvk/tests/src/data/descriptor/descriptor_pool_test.cpp b/cvk/tests/src/data/descriptor/descriptor_pool_test.cpp index f8d3de71..564645bc 100644 --- a/cvk/tests/src/data/descriptor/descriptor_pool_test.cpp +++ b/cvk/tests/src/data/descriptor/descriptor_pool_test.cpp @@ -64,15 +64,25 @@ CVK_TEST(DescriptorPool, config_add_takes_the_variable_count_and_update_after_bi EXPECT_NE(config.flags & VK_DESCRIPTOR_POOL_CREATE_UPDATE_AFTER_BIND_BIT, 0); } -CVK_TEST(DescriptorPool, constructor_counts_every_layout) { +CVK_TEST(DescriptorPool, config_is_an_aggregate) { + static_assert(std::is_aggregate_v); + + DescriptorPool::Config const config{.maxSets = 2, .flags = VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT}; + + EXPECT_EQ(config.maxSets, 2); + EXPECT_TRUE(config.sizes.empty()); + EXPECT_EQ(config.flags, VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT); +} + +CVK_TEST(DescriptorPool, for_layouts_counts_every_layout) { test::Context const context{}; DescriptorSetLayout const layout{context.core, buffers_and_texture(), create}; DescriptorSetLayout const textures{context.core, bindless(), create}; - DescriptorPool::Config const config{ + auto const config = DescriptorPool::Config::ForLayouts( {{&layout, 2}, {&textures, 1, 100}}, VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT - }; + ); EXPECT_EQ(config.maxSets, 3); EXPECT_EQ(count_of(config, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER), 4); @@ -85,7 +95,7 @@ CVK_TEST(DescriptorPool, create_and_destroy) { test::Context const context{}; DescriptorSetLayout const layout{context.core, buffers_and_texture(), create}; - DescriptorPool pool{context.core, DescriptorPool::Config{{&layout, 4}}, create}; + DescriptorPool pool{context.core, DescriptorPool::Config::ForLayouts({{&layout, 4}}), create}; ASSERT_TRUE(pool.created()); EXPECT_TRUE(pool.unused()); @@ -98,7 +108,7 @@ CVK_TEST(DescriptorPool, create_and_destroy) { CVK_TEST(DescriptorPool, reset_counts) { test::Context const context{}; DescriptorSetLayout const layout{context.core, buffers_and_texture(), create}; - DescriptorPool const pool{context.core, DescriptorPool::Config{{&layout}}, create}; + DescriptorPool const pool{context.core, DescriptorPool::Config::ForLayouts({{&layout}}), create}; pool.reset(); pool.reset(); @@ -109,7 +119,7 @@ CVK_TEST(DescriptorPool, reset_counts) { CVK_TEST(DescriptorPool, release_and_wrap) { test::Context const context{}; DescriptorSetLayout const layout{context.core, buffers_and_texture(), create}; - auto const config = DescriptorPool::Config{{&layout}}; + auto const config = DescriptorPool::Config::ForLayouts({{&layout}}); DescriptorPool pool{context.core, config, create}; auto const vkPool = pool.get(); diff --git a/cvk/tests/src/data/descriptor/descriptor_set_test.cpp b/cvk/tests/src/data/descriptor/descriptor_set_test.cpp index ccbececc..f09a50c6 100644 --- a/cvk/tests/src/data/descriptor/descriptor_set_test.cpp +++ b/cvk/tests/src/data/descriptor/descriptor_set_test.cpp @@ -43,7 +43,7 @@ namespace { CVK_TEST(DescriptorSet, base_constructor_does_not_create) { test::Context const context{}; DescriptorSetLayout const layout{context.core, buffer_and_texture(), create}; - DescriptorPool const pool{context.core, DescriptorPool::Config{{&layout}}, create}; + DescriptorPool const pool{context.core, DescriptorPool::Config::ForLayouts({{&layout}}), create}; DescriptorSet const set{pool, layout}; @@ -58,7 +58,7 @@ CVK_TEST(DescriptorSet, destroy_frees_with_a_freeing_pool) { DescriptorSetLayout const layout{context.core, buffer_and_texture(), create}; DescriptorPool const pool{ context.core, - DescriptorPool::Config{{{&layout}}, VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT}, + DescriptorPool::Config::ForLayouts({{&layout}}, VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT), create }; @@ -78,7 +78,7 @@ CVK_TEST(DescriptorSet, destroy_frees_with_a_freeing_pool) { CVK_TEST(DescriptorSet, destroy_without_free_only_forgets) { test::Context const context{}; DescriptorSetLayout const layout{context.core, buffer_and_texture(), create}; - DescriptorPool const pool{context.core, DescriptorPool::Config{{&layout}}, create}; + DescriptorPool const pool{context.core, DescriptorPool::Config::ForLayouts({{&layout}}), create}; DescriptorSet set{pool, layout, create}; set.destroy(); @@ -91,7 +91,7 @@ CVK_TEST(DescriptorSet, destroy_without_free_only_forgets) { CVK_TEST(DescriptorSet, pool_reset_frees_the_set) { test::Context const context{}; DescriptorSetLayout const layout{context.core, buffer_and_texture(), create}; - DescriptorPool const pool{context.core, DescriptorPool::Config{{&layout}}, create}; + DescriptorPool const pool{context.core, DescriptorPool::Config::ForLayouts({{&layout}}), create}; DescriptorSet set{pool, layout, create}; pool.reset(); @@ -106,7 +106,7 @@ CVK_TEST(DescriptorSet, pool_reset_frees_the_set) { CVK_TEST(DescriptorSet, write_buffer_and_image) { test::Context const context{}; DescriptorSetLayout const layout{context.core, buffer_and_texture(), create}; - DescriptorPool const pool{context.core, DescriptorPool::Config{{&layout}}, create}; + DescriptorPool const pool{context.core, DescriptorPool::Config::ForLayouts({{&layout}}), create}; DescriptorSet const set{pool, layout, create}; BaseBuffer const buffer{context.core, {.usage = VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT}, 64}; @@ -120,7 +120,7 @@ CVK_TEST(DescriptorSet, write_buffer_and_image) { CVK_TEST(DescriptorSet, write_one_descriptor_and_the_whole_buffer) { test::Context const context{}; DescriptorSetLayout const layout{context.core, buffer_and_texture(), create}; - DescriptorPool const pool{context.core, DescriptorPool::Config{{&layout}}, create}; + DescriptorPool const pool{context.core, DescriptorPool::Config::ForLayouts({{&layout}}), create}; DescriptorSet const set{pool, layout, create}; BaseBuffer const buffer{context.core, {.usage = VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT}, 64}; @@ -131,7 +131,7 @@ CVK_TEST(DescriptorSet, write_one_descriptor_and_the_whole_buffer) { CVK_TEST(DescriptorSet, variable_count) { test::Context const context{}; DescriptorSetLayout const layout{context.core, bindless(), create}; - DescriptorPool const pool{context.core, DescriptorPool::Config{{&layout, 1, 8}}, create}; + DescriptorPool const pool{context.core, DescriptorPool::Config::ForLayouts({{&layout, 1, 8}}), create}; DescriptorSet const set{pool, layout, 8, create}; Image const image{context.core, TEXTURE_CONFIG, VkExtent2D{4, 4}}; @@ -147,7 +147,7 @@ CVK_TEST(DescriptorSet, release_and_wrap) { DescriptorSetLayout const layout{context.core, buffer_and_texture(), create}; DescriptorPool const pool{ context.core, - DescriptorPool::Config{{{&layout}}, VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT}, + DescriptorPool::Config::ForLayouts({{&layout}}, VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT), create }; DescriptorSet set{pool, layout, create}; diff --git a/cvk/tests/src/render/cmd/cmd_buffer_test.cpp b/cvk/tests/src/render/cmd/cmd_buffer_test.cpp index 4eff2401..b646873e 100644 --- a/cvk/tests/src/render/cmd/cmd_buffer_test.cpp +++ b/cvk/tests/src/render/cmd/cmd_buffer_test.cpp @@ -13,7 +13,7 @@ namespace cvk { namespace { CmdPool::Config pool_config(test::Context const& context) { - return CmdPool::Config{context.core.queue(0)}; + return CmdPool::Config{.queueFamilyIndex = context.core.queue(0).familyIndex()}; } } diff --git a/cvk/tests/src/render/cmd/cmd_pool_test.cpp b/cvk/tests/src/render/cmd/cmd_pool_test.cpp index 6da85720..fd56b945 100644 --- a/cvk/tests/src/render/cmd/cmd_pool_test.cpp +++ b/cvk/tests/src/render/cmd/cmd_pool_test.cpp @@ -4,30 +4,34 @@ #include "cvk/render/cmd/cmd_buffer.hpp" #include "cvk/render/cmd/cmd_pool.hpp" +#include + namespace cvk { namespace { CmdPool::Config pool_config(test::Context const& context) { - return CmdPool::Config{context.core.queue(0)}; + return CmdPool::Config{.queueFamilyIndex = context.core.queue(0).familyIndex()}; } } CVK_TEST(CmdPool, config_create_info) { - constexpr CmdPool::Config config{2, VK_COMMAND_POOL_CREATE_TRANSIENT_BIT}; + constexpr CmdPool::Config config{.queueFamilyIndex = 2, .flags = VK_COMMAND_POOL_CREATE_TRANSIENT_BIT}; constexpr auto info = config.createInfo(); static_assert(info.queueFamilyIndex == 2); static_assert(info.flags == VK_COMMAND_POOL_CREATE_TRANSIENT_BIT); } -CVK_TEST(CmdPool, config_from_the_queue_uses_its_family) { - test::Context const context{}; - auto const& queue = context.core.queue(0); +CVK_TEST(CmdPool, config_is_an_aggregate_with_defaults) { + static_assert(std::is_aggregate_v); - CmdPool::Config const config{queue}; + constexpr CmdPool::Config empty{}; + static_assert(empty.queueFamilyIndex == VK_QUEUE_FAMILY_IGNORED); + static_assert(empty.flags == CmdPool::Config::DEFAULT_FLAGS); - EXPECT_EQ(config.queueFamilyIndex, queue.familyIndex()); - EXPECT_NE(config.flags & VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT, 0u); + constexpr CmdPool::Config withFamily{.queueFamilyIndex = 3}; + static_assert(withFamily.queueFamilyIndex == 3); + static_assert((withFamily.flags & VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT) != 0); } CVK_TEST(CmdPool, base_constructor_does_not_create) { diff --git a/cvk/tests/src/render/cmd/draw_test.cpp b/cvk/tests/src/render/cmd/draw_test.cpp index a4d12443..7452f834 100644 --- a/cvk/tests/src/render/cmd/draw_test.cpp +++ b/cvk/tests/src/render/cmd/draw_test.cpp @@ -61,7 +61,7 @@ namespace { } [[nodiscard]] GraphicsPipeline::Config config() const { - GraphicsPipeline::Config config{COLOR_FORMATS}; + GraphicsPipeline::Config config{.colorFormats = {std::from_range, COLOR_FORMATS}}; config.addStage(vertex).addStage(fragment); config.vertexBindings = {{.binding = 0, .stride = sizeof(Vec3), .inputRate = VK_VERTEX_INPUT_RATE_VERTEX}}; config.vertexAttributes = { @@ -121,7 +121,11 @@ namespace { cmd.endRendering(); Barrier barrier{}; - barrier.addTransition(image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL); + barrier.addTransition( + image, + VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, + VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL + ); barrier.execute(cmd); test::copy_to_buffer(context, cmd, image, readback, 0, 0); diff --git a/cvk/tests/src/render/cmd/local_read_test.cpp b/cvk/tests/src/render/cmd/local_read_test.cpp index b2bd60ca..fb88eafa 100644 --- a/cvk/tests/src/render/cmd/local_read_test.cpp +++ b/cvk/tests/src/render/cmd/local_read_test.cpp @@ -122,7 +122,7 @@ namespace { * @brief fullscreen pipeline for the attachments */ GraphicsPipeline::Config fullscreen(Shader const& vertex, Shader const& fragment, AttachmentMapping mapping) { - GraphicsPipeline::Config config{COLOR_FORMATS}; + GraphicsPipeline::Config config{.colorFormats = {std::from_range, COLOR_FORMATS}}; config.addStage(vertex).addStage(fragment); config.vertexBindings = {{.binding = 0, .stride = sizeof(Vec3), .inputRate = VK_VERTEX_INPUT_RATE_VERTEX}}; config.vertexAttributes = {{.location = 0, .binding = 0, .format = VK_FORMAT_R32G32B32_SFLOAT, .offset = 0}}; @@ -153,7 +153,7 @@ namespace { inputLayout{core, input_attachment(), create}, readLayout{core, {.setLayouts = {&inputLayout}}, create}, read{readLayout, fullscreen(vertex, localRead, copy_first_to_second()), create}, - pool{core, {{&inputLayout}}, create}, + pool{core, DescriptorPool::Config::ForLayouts({{&inputLayout}}), create}, input{pool, inputLayout, create}, vertices{ core, @@ -297,7 +297,7 @@ CVK_TEST(LocalRead, secondary_inherits_the_mapping) { test::Context const context{}; Attachments const attachments{context.core}; Draws const draws{context.core, attachments.firstView}; - CmdPool const pool{context.core, CmdPool::Config{context.core.queue(0)}, create}; + CmdPool const pool{context.core, CmdPool::Config{.queueFamilyIndex = context.core.queue(0).familyIndex()}, create}; std::array secondaries{SecondaryCmdBuffer{pool, create}}; test::record_and_submit(context, [&](PrimaryCmdBuffer& cmd) { diff --git a/cvk/tests/src/render/cmd/rendering_test.cpp b/cvk/tests/src/render/cmd/rendering_test.cpp index db7d35d9..517575d3 100644 --- a/cvk/tests/src/render/cmd/rendering_test.cpp +++ b/cvk/tests/src/render/cmd/rendering_test.cpp @@ -207,7 +207,7 @@ CVK_TEST(Rendering, secondary_inherits_the_rendering) { test::Context const context{}; Image const image{context.core, COLOR_CONFIG, EXTENT}; ImageView const view{image, {}, create}; - CmdPool const pool{context.core, CmdPool::Config{context.core.queue(0)}, create}; + CmdPool const pool{context.core, CmdPool::Config{.queueFamilyIndex = context.core.queue(0).familyIndex()}, create}; std::array secondaries{SecondaryCmdBuffer{pool, create}}; test::record_and_submit(context, [&](PrimaryCmdBuffer& cmd) { diff --git a/cvk/tests/src/render/pipeline/compute_pipeline_test.cpp b/cvk/tests/src/render/pipeline/compute_pipeline_test.cpp index dd7c02b6..da472fa0 100644 --- a/cvk/tests/src/render/pipeline/compute_pipeline_test.cpp +++ b/cvk/tests/src/render/pipeline/compute_pipeline_test.cpp @@ -58,7 +58,7 @@ namespace { Values const values{context.core}; ComputePipeline const pipeline{values.layout, values.config(), create}; Buffer const output{context.core, OUTPUT_CONFIG, VALUES}; - DescriptorPool const pool{context.core, DescriptorPool::Config{{&values.setLayout}}, create}; + DescriptorPool const pool{context.core, DescriptorPool::Config::ForLayouts({{&values.setLayout}}), create}; DescriptorSet const set{pool, values.setLayout, create}; set.write({0, output}); diff --git a/cvk/tests/src/render/pipeline/graphics_pipeline_test.cpp b/cvk/tests/src/render/pipeline/graphics_pipeline_test.cpp index 70d0afb0..e592f266 100644 --- a/cvk/tests/src/render/pipeline/graphics_pipeline_test.cpp +++ b/cvk/tests/src/render/pipeline/graphics_pipeline_test.cpp @@ -10,6 +10,8 @@ #include #include +#include +#include namespace cvk { @@ -34,7 +36,10 @@ namespace { fragment{core, VK_SHADER_STAGE_FRAGMENT_BIT, test::shaders::TRIANGLE_FRAG} {} [[nodiscard]] GraphicsPipeline::Config config() const { - GraphicsPipeline::Config config{COLOR_FORMATS, DEPTH_FORMAT}; + GraphicsPipeline::Config config{ + .colorFormats = {std::from_range, COLOR_FORMATS}, + .depthFormat = DEPTH_FORMAT, + }; config.addStage(vertex).addStage(fragment); config.vertexBindings = { {.binding = 0, .stride = 3 * sizeof(float), .inputRate = VK_VERTEX_INPUT_RATE_VERTEX}, @@ -58,18 +63,25 @@ namespace { }; } -CVK_TEST(GraphicsPipeline, config_from_the_formats) { - GraphicsPipeline::Config const config{COLOR_FORMATS, DEPTH_FORMAT}; +CVK_TEST(GraphicsPipeline, config_is_an_aggregate_without_blend_attachments) { + static_assert(std::is_aggregate_v); + + GraphicsPipeline::Config const config{ + .colorFormats = {std::from_range, COLOR_FORMATS}, + .depthFormat = DEPTH_FORMAT, + }; EXPECT_TRUE(std::ranges::equal(config.colorFormats, COLOR_FORMATS)); - ASSERT_EQ(config.colorBlendAttachments.size(), 1); - EXPECT_EQ(config.colorBlendAttachments[0].blendEnable, VK_TRUE); + EXPECT_TRUE(config.colorBlendAttachments.empty()); EXPECT_EQ(config.depthFormat, DEPTH_FORMAT); } CVK_TEST(GraphicsPipeline, config_is_constexpr) { static_assert([] { - GraphicsPipeline::Config config{COLOR_FORMATS, DEPTH_FORMAT}; + GraphicsPipeline::Config config{ + .colorFormats = {std::from_range, COLOR_FORMATS}, + .depthFormat = DEPTH_FORMAT, + }; config.stages.push_back( VkPipelineShaderStageCreateInfo{ .sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO, @@ -82,7 +94,7 @@ CVK_TEST(GraphicsPipeline, config_is_constexpr) { } ); config.removeStage(VK_SHADER_STAGE_VERTEX_BIT); - return config.colorBlendAttachments.size() == 1 && config.stages.empty(); + return config.colorFormats.size() == 1 && config.colorBlendAttachments.empty() && config.stages.empty(); }()); } @@ -103,7 +115,7 @@ CVK_TEST(GraphicsPipeline, bind) { test::Context const context{}; Triangle const triangle{context.core}; GraphicsPipeline const pipeline{triangle.layout, triangle.config(), create}; - CmdPool const pool{context.core, CmdPool::Config{context.core.queue(0)}, create}; + CmdPool const pool{context.core, CmdPool::Config{.queueFamilyIndex = context.core.queue(0).familyIndex()}, create}; PrimaryCmdBuffer primary{pool, create}; primary.begin(VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT); @@ -134,6 +146,32 @@ CVK_TEST(GraphicsPipeline, create) { EXPECT_EQ(pipeline.config().stages.size(), 2); } +CVK_TEST(GraphicsPipeline, create_keeps_the_empty_blend_attachments) { + test::Context const context{}; + Triangle const triangle{context.core}; + + GraphicsPipeline const pipeline{triangle.layout, triangle.config(), create}; + + EXPECT_TRUE(pipeline.created()); + EXPECT_TRUE(pipeline.config().colorBlendAttachments.empty()); +} + +CVK_TEST(GraphicsPipeline, create_with_a_blend_attachment_per_color_format) { + test::Context const context{}; + Triangle const triangle{context.core}; + + auto config = triangle.config(); + config.colorBlendAttachments = { + {.blendEnable = VK_FALSE, .colorWriteMask = VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT}, + }; + + GraphicsPipeline const pipeline{triangle.layout, std::move(config), create}; + + EXPECT_TRUE(pipeline.created()); + ASSERT_EQ(pipeline.config().colorBlendAttachments.size(), 1); + EXPECT_EQ(pipeline.config().colorBlendAttachments[0].blendEnable, VK_FALSE); +} + CVK_TEST(GraphicsPipeline, create_without_depth) { test::Context const context{}; Triangle const triangle{context.core}; diff --git a/cvk/tests/src/surface/swapchain_test.cpp b/cvk/tests/src/surface/swapchain_test.cpp index 2da1c5d9..7b6630f3 100644 --- a/cvk/tests/src/surface/swapchain_test.cpp +++ b/cvk/tests/src/surface/swapchain_test.cpp @@ -51,7 +51,7 @@ namespace { if(!index.has_value()) return result; auto const& image = swapchain.images()[*index]; - CmdPool const pool{context.core, CmdPool::Config{context.queue()}, create}; + CmdPool const pool{context.core, CmdPool::Config{.queueFamilyIndex = context.queue().familyIndex()}, create}; PrimaryCmdBuffer cmd{pool, create}; cmd.begin(VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT); From 7202c7d9bedf040866514c94857144494f32b47a Mon Sep 17 00:00:00 2001 From: lukas <89145039+codethinki@users.noreply.github.com> Date: Wed, 21 Oct 2026 10:35:36 +0200 Subject: [PATCH 097/135] cvk: every cvk throw is a cvk_exception, the pipeline layout checks its limit at create --- AGENTS.md | 5 +++-- .../pipeline/layout/pipeline_layout.hpp | 2 +- cvk/incl/cvk/surface/surface.hpp | 4 ++-- .../pipeline/layout/pipeline_layout.cpp | 20 ++++++++++--------- cvk/src/surface/surface.cpp | 16 +++++++++------ cvk/tests/src/base/instance_test.cpp | 3 ++- cvk/tests/src/base/physical_device_test.cpp | 3 ++- .../pipeline/layout/pipeline_layout_test.cpp | 5 +++-- cvk/tests/src/surface/surface_test.cpp | 3 ++- 9 files changed, 36 insertions(+), 25 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 9d624c59..7facedf8 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -224,10 +224,11 @@ inline void Semaphore::debug_check(Semaphore const& semaphore) { ### Errors - `CTH_CRITICAL(condition, "message") {}`: precondition check, active in debug builds only. -- `CTH_STABLE_ERR(condition, "message") { forget(); throw details->exception(); }`: error that can happen at run time. +- `CTH_STABLE_ERR(condition, "message") { forget(); throw cvk::cvk_exception{details->exception()}; }`: error that can happen at run time. - `CTH_WARN(condition, "message") {}`: suspicious state that is not an error. - `cvk` stores state only where it needs the state to work. A check that needs more state is left to the validation layers. - `CVK_RESULT_STABLE_THROW(result != VK_SUCCESS, result, "message") {}`: throws `cvk::vk_result_exception` with the `VkResult`. +- `cvk` throws `cvk::cvk_exception` or a derived type: `CVK_STABLE_THROW`, `CVK_RESULT_STABLE_THROW`, or `throw cvk::cvk_exception{details->exception()}` inside a `CTH_STABLE_ERR` block. - The condition is the error case: `CTH_CRITICAL(!created(), ...)`, not the expected case. ### Comment markers @@ -268,7 +269,7 @@ Documentation uses doxygen comments and follows ASD-STE100 (Simplified Technical | `@attention` | danger that is not a precondition: undefined behavior, not thread safe | - Write preconditions as `@pre`, not as `@attention requires ...`. -- For an exception that cvk throws itself, write the cause after `reason:`: `@throws cth::except::default_exception reason: required extension missing`. +- For an exception that cvk throws itself, write the cause after `reason:`: `@throws cvk::cvk_exception reason: required extension missing`. ### Math like notation diff --git a/cvk/incl/cvk/render/pipeline/layout/pipeline_layout.hpp b/cvk/incl/cvk/render/pipeline/layout/pipeline_layout.hpp index 142dda38..5e75a008 100644 --- a/cvk/incl/cvk/render/pipeline/layout/pipeline_layout.hpp +++ b/cvk/incl/cvk/render/pipeline/layout/pipeline_layout.hpp @@ -29,7 +29,6 @@ class PipelineLayout { * @brief base constructor, copies the handles of the set layouts * @pre @ref Core::created() * @pre the set layouts are created - * @throws cth::except::default_exception reason: more set layouts than maxBoundDescriptorSets */ PipelineLayout(Core const&, Config const&); @@ -67,6 +66,7 @@ class PipelineLayout { * @pre the set layouts of the config are not destroyed * @post @ref created() * @note calls @ref optDestroy() + * @throws cvk::cvk_exception reason: more set layouts than maxBoundDescriptorSets * @throws cvk::vk_result_exception result of vkCreatePipelineLayout() */ void create(); diff --git a/cvk/incl/cvk/surface/surface.hpp b/cvk/incl/cvk/surface/surface.hpp index ef637ac3..33eaaf6a 100644 --- a/cvk/incl/cvk/surface/surface.hpp +++ b/cvk/incl/cvk/surface/surface.hpp @@ -96,13 +96,13 @@ class Surface { /** * @return the most preferred allowed and available format - * @throws cth::except::default_exception reason: no allowed format available + * @throws cvk::cvk_exception reason: no allowed format available */ [[nodiscard]] format_t format(PhysicalDevice const&) const; /** * @return the most preferred allowed and available present mode - * @throws cth::except::default_exception reason: no allowed present mode available + * @throws cvk::cvk_exception reason: no allowed present mode available */ [[nodiscard]] present_mode_t presentMode(PhysicalDevice const&) const; diff --git a/cvk/src/render/pipeline/layout/pipeline_layout.cpp b/cvk/src/render/pipeline/layout/pipeline_layout.cpp index 9148c2b2..5a18037a 100644 --- a/cvk/src/render/pipeline/layout/pipeline_layout.cpp +++ b/cvk/src/render/pipeline/layout/pipeline_layout.cpp @@ -12,15 +12,6 @@ namespace cvk { PipelineLayout::PipelineLayout(Core const& core, Config const& config) : _handle{core}, _pushConstantRanges{config.pushConstantRanges} { Core::debug_check(core); - auto const maxSets = core.physicalDevice().limits().maxBoundDescriptorSets; - CTH_STABLE_ERR( - config.setLayouts.size() > maxSets, - "more set layouts than maxBoundDescriptorSets, got {} for {}", - config.setLayouts.size(), - maxSets - ) { - throw details->exception(); - } _vkSetLayouts.reserve(config.setLayouts.size()); for(auto const& layout : config.setLayouts) { @@ -47,6 +38,17 @@ void PipelineLayout::wrap(State const& state) { void PipelineLayout::create() { optDestroy(); + auto const maxSets = core().physicalDevice().limits().maxBoundDescriptorSets; + CTH_STABLE_ERR( + _vkSetLayouts.size() > maxSets, + "more set layouts than maxBoundDescriptorSets, got {} for {}", + _vkSetLayouts.size(), + maxSets + ) { + forget(); + throw cvk::cvk_exception{details->exception()}; + } + VkPipelineLayoutCreateInfo const info{ .sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO, .pNext = nullptr, diff --git a/cvk/src/surface/surface.cpp b/cvk/src/surface/surface.cpp index 53136f03..be56fd36 100644 --- a/cvk/src/surface/surface.cpp +++ b/cvk/src/surface/surface.cpp @@ -120,9 +120,11 @@ auto Surface::formats(PhysicalDevice const& physicalDevice) const -> vector format_t { auto const availableFormats = formats(physicalDevice); - CTH_STABLE_ERR(availableFormats.empty(), "no suitable format found, allowed: {}", _config.allowedSurfaceFormats) { - throw details->exception(); - } + CVK_STABLE_THROW( + availableFormats.empty(), + "no suitable format found, allowed: {}", + _config.allowedSurfaceFormats + ) {} return availableFormats[0]; } @@ -142,9 +144,11 @@ VkSurfaceCapabilitiesKHR Surface::capabilities(PhysicalDevice const& physicalDev auto Surface::presentMode(PhysicalDevice const& physicalDevice) const -> present_mode_t { auto const availableModes = presentModes(physicalDevice); - CTH_STABLE_ERR(availableModes.empty(), "no suitable present mode found, allowed: {}", _config.allowedPresentModes) { - throw details->exception(); - } + CVK_STABLE_THROW( + availableModes.empty(), + "no suitable present mode found, allowed: {}", + _config.allowedPresentModes + ) {} return availableModes[0]; } diff --git a/cvk/tests/src/base/instance_test.cpp b/cvk/tests/src/base/instance_test.cpp index a56612a7..91d02c91 100644 --- a/cvk/tests/src/base/instance_test.cpp +++ b/cvk/tests/src/base/instance_test.cpp @@ -3,6 +3,7 @@ #include "cvk/base/instance.hpp" #include "cvk/utility/constants.hpp" +#include "cvk/utility/vk_exceptions.hpp" #include #include @@ -59,7 +60,7 @@ CVK_TEST(Instance, create_enables_the_extensions_of_the_config) { CVK_TEST(Instance, create_with_a_missing_extension_throws_and_forgets) { Instance instance{test::instance_config({"VK_CVK_extension_that_does_not_exist"})}; - EXPECT_THROW(instance.create(), cth::except::default_exception); + EXPECT_THROW(instance.create(), cvk_exception); EXPECT_FALSE(instance.created()); EXPECT_TRUE(instance.extensions().empty()); } diff --git a/cvk/tests/src/base/physical_device_test.cpp b/cvk/tests/src/base/physical_device_test.cpp index 000f81ae..8da77957 100644 --- a/cvk/tests/src/base/physical_device_test.cpp +++ b/cvk/tests/src/base/physical_device_test.cpp @@ -4,6 +4,7 @@ #include "cvk/base/instance.hpp" #include "cvk/base/physical_device.hpp" #include "cvk/utility/constant/device_constants.hpp" +#include "cvk/utility/vk_exceptions.hpp" #include #include @@ -124,6 +125,6 @@ CVK_TEST(PhysicalDevice, find_supported_format) { VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT ); }; - EXPECT_THROW(findNone(), cth::except::default_exception); + EXPECT_THROW(findNone(), cvk_exception); } } diff --git a/cvk/tests/src/render/pipeline/layout/pipeline_layout_test.cpp b/cvk/tests/src/render/pipeline/layout/pipeline_layout_test.cpp index 8327622a..098394ad 100644 --- a/cvk/tests/src/render/pipeline/layout/pipeline_layout_test.cpp +++ b/cvk/tests/src/render/pipeline/layout/pipeline_layout_test.cpp @@ -4,6 +4,7 @@ #include "cvk/base/physical_device.hpp" #include "cvk/render/pipeline/layout/descriptor_set_layout.hpp" #include "cvk/render/pipeline/layout/pipeline_layout.hpp" +#include "cvk/utility/vk_exceptions.hpp" namespace cvk { @@ -47,9 +48,9 @@ CVK_TEST(PipelineLayout, too_many_sets_throws) { .setLayouts = std::vector>(maxSets + 1, &set), }; - auto const construct = [&] { PipelineLayout const layout{context.core, config}; }; + auto const construct = [&] { PipelineLayout const layout{context.core, config, create}; }; - EXPECT_THROW(construct(), cth::except::default_exception); + EXPECT_THROW(construct(), cvk_exception); } CVK_TEST(PipelineLayout, set_layouts_can_be_destroyed_after_create) { diff --git a/cvk/tests/src/surface/surface_test.cpp b/cvk/tests/src/surface/surface_test.cpp index c3c1b2b7..58657ef9 100644 --- a/cvk/tests/src/surface/surface_test.cpp +++ b/cvk/tests/src/surface/surface_test.cpp @@ -6,6 +6,7 @@ #include "cvk/base/physical_device.hpp" #include "cvk/surface/surface.hpp" #include "cvk/utility/constants.hpp" +#include "cvk/utility/vk_exceptions.hpp" #include #include @@ -88,7 +89,7 @@ CVK_TEST(Surface, no_allowed_format_throws) { auto const surface = test::headless_surface(instance, config); EXPECT_TRUE(surface.formats(device).empty()); - EXPECT_THROW([[maybe_unused]] auto const f = surface.format(device), cth::except::default_exception); + EXPECT_THROW([[maybe_unused]] auto const f = surface.format(device), cvk_exception); } CVK_TEST(Surface, capabilities) { From 2e3d7d5775340d57c7eb0e612d44f403d5fe5e24 Mon Sep 17 00:00:00 2001 From: lukas <89145039+codethinki@users.noreply.github.com> Date: Wed, 21 Oct 2026 11:00:37 +0200 Subject: [PATCH 098/135] cvk: create constructors take a create_t tag when create() has no required parameter, Fence and TimelineSemaphore follow it --- AGENTS.md | 1 + cvk/incl/cvk/render/sync/fence.hpp | 3 ++- .../cvk/render/sync/timeline_semaphore.hpp | 4 ++-- cvk/src/render/sync/fence.cpp | 3 ++- cvk/src/render/sync/timeline_semaphore.cpp | 4 +++- cvk/src/surface/swapchain.cpp | 2 +- cvk/tests/src/base/queue/submit_test.cpp | 2 +- cvk/tests/src/context.cpp | 2 +- cvk/tests/src/render/sync/fence_test.cpp | 20 +++++++++---------- cvk/tests/src/render/sync/semaphore_test.cpp | 2 +- .../render/sync/timeline_semaphore_test.cpp | 10 ++++++++++ cvk/tests/src/surface/swapchain_test.cpp | 4 ++-- 12 files changed, 36 insertions(+), 21 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 7facedf8..9457de8c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -220,6 +220,7 @@ inline void Semaphore::debug_check(Semaphore const& semaphore) { - A parent (instance, core, device, pool, ...) does not move and is not destroyed while objects depend on it. Code does not have to support it. - A failed `create()` calls `forget()` before it throws. - `release()` exists so that an owner can use an object it must not destroy (for example swapchain images). +- A constructor that creates takes the parameters of the base constructor, then the parameters of `create()`. When `create()` has no required parameter, a `create_t` tag comes before them: `Fence(core, create_t, flags = 0)`, `Image(core, config, extent)`. ### Errors diff --git a/cvk/incl/cvk/render/sync/fence.hpp b/cvk/incl/cvk/render/sync/fence.hpp index d420d516..df8ca013 100644 --- a/cvk/incl/cvk/render/sync/fence.hpp +++ b/cvk/incl/cvk/render/sync/fence.hpp @@ -1,5 +1,6 @@ #pragma once #include "cvk/base/device_table.hpp" +#include "cvk/utility/constants.hpp" #include "cvk/utility/handle/core_handle.hpp" #include "cvk/utility/types.hpp" @@ -42,7 +43,7 @@ class Fence { * - @ref Fence(Core const&) * - @ref create() */ - Fence(Core const&, VkFenceCreateFlags flags, VkExternalFenceHandleTypeFlags exportTypes = 0); + explicit Fence(Core const&, create_t, VkFenceCreateFlags flags = 0, VkExternalFenceHandleTypeFlags exportTypes = 0); /** * @note calls @ref optDestroy() diff --git a/cvk/incl/cvk/render/sync/timeline_semaphore.hpp b/cvk/incl/cvk/render/sync/timeline_semaphore.hpp index 28ab832d..a873c182 100644 --- a/cvk/incl/cvk/render/sync/timeline_semaphore.hpp +++ b/cvk/incl/cvk/render/sync/timeline_semaphore.hpp @@ -34,9 +34,9 @@ class TimelineSemaphore : Semaphore { * @brief constructs and creates * @details calls: * - @ref TimelineSemaphore(Core const&) - * - @ref create() + * - @ref create() with the initial value */ - explicit TimelineSemaphore(Core const&, create_t); + explicit TimelineSemaphore(Core const&, create_t, uint64_t initialValue = 0); /** * @note calls @ref optDestroy() diff --git a/cvk/src/render/sync/fence.cpp b/cvk/src/render/sync/fence.cpp index 6be9390d..c5838943 100644 --- a/cvk/src/render/sync/fence.cpp +++ b/cvk/src/render/sync/fence.cpp @@ -20,7 +20,8 @@ Fence::Fence(Core const& core) : _handle{core} { Core::debug_check(core); } Fence::Fence(Core const& core, State const& state) : Fence{core} { wrap(state); } -Fence::Fence(Core const& core, VkFenceCreateFlags flags, VkExternalFenceHandleTypeFlags exportTypes) : Fence{core} { +Fence::Fence(Core const& core, create_t, VkFenceCreateFlags flags, VkExternalFenceHandleTypeFlags exportTypes) : + Fence{core} { create(flags, exportTypes); } diff --git a/cvk/src/render/sync/timeline_semaphore.cpp b/cvk/src/render/sync/timeline_semaphore.cpp index b15b62e3..7d768710 100644 --- a/cvk/src/render/sync/timeline_semaphore.cpp +++ b/cvk/src/render/sync/timeline_semaphore.cpp @@ -14,7 +14,9 @@ TimelineSemaphore::TimelineSemaphore(Core const& core, State const& state) : Tim wrap(state); } -TimelineSemaphore::TimelineSemaphore(Core const& core, create_t) : TimelineSemaphore{core} { create(); } +TimelineSemaphore::TimelineSemaphore(Core const& core, create_t, uint64_t initialValue) : TimelineSemaphore{core} { + create(initialValue); +} void TimelineSemaphore::wrap(State const& state) { Semaphore::wrap(Semaphore::State{state.vkSemaphore}); diff --git a/cvk/src/surface/swapchain.cpp b/cvk/src/surface/swapchain.cpp index 1def3846..9f671004 100644 --- a/cvk/src/surface/swapchain.cpp +++ b/cvk/src/surface/swapchain.cpp @@ -269,7 +269,7 @@ void Swapchain::createImages() { _presentFences.reserve(count); for(uint32_t index = 0; index < count; ++index) { _presentSemaphores.emplace_back(core(), cvk::create); - _presentFences.emplace_back(core(), VK_FENCE_CREATE_SIGNALED_BIT); + _presentFences.emplace_back(core(), cvk::create, VK_FENCE_CREATE_SIGNALED_BIT); } _acquired.assign(count, false); diff --git a/cvk/tests/src/base/queue/submit_test.cpp b/cvk/tests/src/base/queue/submit_test.cpp index 983927ea..70032eda 100644 --- a/cvk/tests/src/base/queue/submit_test.cpp +++ b/cvk/tests/src/base/queue/submit_test.cpp @@ -45,7 +45,7 @@ CVK_TEST(Submit, command_buffer_without_fence) { CVK_TEST(Submit, binary_semaphore_orders_two_infos) { test::Context const context{}; Semaphore const semaphore{context.core, create}; - Fence const fence{context.core, 0}; + Fence const fence{context.core, create}; std::array const infos{ SubmitInfo{.signals = {semaphore}}, diff --git a/cvk/tests/src/context.cpp b/cvk/tests/src/context.cpp index 982be751..0c3134fd 100644 --- a/cvk/tests/src/context.cpp +++ b/cvk/tests/src/context.cpp @@ -97,7 +97,7 @@ Context::ValidationCheck::ValidationCheck() { reset_validation_errors(); } Context::ValidationCheck::~ValidationCheck() { EXPECT_EQ(validation_errors(), 0); } void submit_and_wait(Context const& context, PrimaryCmdBuffer const& primary) { - Fence const fence{context.core, 0}; + Fence const fence{context.core, create}; context.core.queue(0).submit({.cmdBuffers = {&primary}}, &fence); fence.wait(); diff --git a/cvk/tests/src/render/sync/fence_test.cpp b/cvk/tests/src/render/sync/fence_test.cpp index abbd145d..046f849a 100644 --- a/cvk/tests/src/render/sync/fence_test.cpp +++ b/cvk/tests/src/render/sync/fence_test.cpp @@ -63,7 +63,7 @@ CVK_TEST(Fence, base_constructor_does_not_create) { CVK_TEST(Fence, create_not_signaled) { test::Context const context{}; - Fence const fence{context.core, 0}; + Fence const fence{context.core, create}; ASSERT_TRUE(fence.created()); EXPECT_EQ(fence.status(), VK_NOT_READY); @@ -74,7 +74,7 @@ CVK_TEST(Fence, create_not_signaled) { CVK_TEST(Fence, create_signaled_and_reset) { test::Context const context{}; - Fence const fence{context.core, VK_FENCE_CREATE_SIGNALED_BIT}; + Fence const fence{context.core, create, VK_FENCE_CREATE_SIGNALED_BIT}; ASSERT_TRUE(fence.signaled()); fence.wait(); @@ -85,7 +85,7 @@ CVK_TEST(Fence, create_signaled_and_reset) { CVK_TEST(Fence, submit_signals) { test::Context const context{}; - Fence const fence{context.core, 0}; + Fence const fence{context.core, create}; context.core.queue(0).rawSubmit(EMPTY_SUBMIT, fence.get()); @@ -98,7 +98,7 @@ CVK_TEST(Fence, submit_signals) { CVK_TEST(Fence, wait_reset_timeout_keeps_the_state) { test::Context const context{}; - Fence const fence{context.core, 0}; + Fence const fence{context.core, create}; EXPECT_EQ(fence.waitReset(0), VK_TIMEOUT); EXPECT_FALSE(fence.signaled()); @@ -111,7 +111,7 @@ CVK_TEST(Fence, wait_reset_timeout_keeps_the_state) { CVK_TEST(Fence, release_and_wrap) { test::Context const context{}; - Fence fence{context.core, VK_FENCE_CREATE_SIGNALED_BIT}; + Fence fence{context.core, create, VK_FENCE_CREATE_SIGNALED_BIT}; auto const vkFence = fence.get(); auto const state = fence.release(); @@ -126,8 +126,8 @@ CVK_TEST(Fence, release_and_wrap) { CVK_TEST(Fence, move_assignment_destroys_the_old_fence) { test::Context const context{}; - Fence fence{context.core, 0}; - Fence other{context.core, VK_FENCE_CREATE_SIGNALED_BIT}; + Fence fence{context.core, create}; + Fence other{context.core, create, VK_FENCE_CREATE_SIGNALED_BIT}; auto const vkFence = other.get(); fence = std::move(other); @@ -142,7 +142,7 @@ CVK_TEST(Fence, extract_os_handle_waits_for_the_submit) { try { context.emplace(os_fence_config()); } catch(cvk_exception const&) { GTEST_SKIP() << "no os fence extension"; } if(!os_fence_exportable(context->core)) GTEST_SKIP() << "os fence handle type not exportable"; - Fence const fence{context->core, 0, constants::OS_FENCE_HANDLE_TYPE}; + Fence const fence{context->core, create, 0, constants::OS_FENCE_HANDLE_TYPE}; EXPECT_EQ(fence.exportTypes(), constants::OS_FENCE_HANDLE_TYPE); auto const& queue = context->core.queue(0); @@ -164,7 +164,7 @@ CVK_TEST(Fence, extract_os_handle_of_a_signaled_fence) { try { context.emplace(os_fence_config()); } catch(cvk_exception const&) { GTEST_SKIP() << "no os fence extension"; } if(!os_fence_exportable(context->core)) GTEST_SKIP() << "os fence handle type not exportable"; - Fence const fence{context->core, VK_FENCE_CREATE_SIGNALED_BIT, constants::OS_FENCE_HANDLE_TYPE}; + Fence const fence{context->core, create, VK_FENCE_CREATE_SIGNALED_BIT, constants::OS_FENCE_HANDLE_TYPE}; auto const handle = fence.extractOsHandle(); @@ -174,7 +174,7 @@ CVK_TEST(Fence, extract_os_handle_of_a_signaled_fence) { CVK_TEST(Fence, release_and_wrap_keep_the_export_types) { test::Context const context{}; - Fence fence{context.core, 0, constants::OS_FENCE_HANDLE_TYPE}; + Fence fence{context.core, create, 0, constants::OS_FENCE_HANDLE_TYPE}; auto const state = fence.release(); EXPECT_EQ(state.exportTypes, constants::OS_FENCE_HANDLE_TYPE); diff --git a/cvk/tests/src/render/sync/semaphore_test.cpp b/cvk/tests/src/render/sync/semaphore_test.cpp index 2084fa6f..e56d1e2c 100644 --- a/cvk/tests/src/render/sync/semaphore_test.cpp +++ b/cvk/tests/src/render/sync/semaphore_test.cpp @@ -41,7 +41,7 @@ CVK_TEST(Semaphore, create_and_destroy) { CVK_TEST(Semaphore, submit_signals_and_waits) { test::Context const context{}; Semaphore const semaphore{context.core, create}; - Fence const fence{context.core, 0}; + Fence const fence{context.core, create}; auto const& queue = context.core.queue(0); auto const info = semaphore_info(semaphore); diff --git a/cvk/tests/src/render/sync/timeline_semaphore_test.cpp b/cvk/tests/src/render/sync/timeline_semaphore_test.cpp index 3f504338..e7a97dde 100644 --- a/cvk/tests/src/render/sync/timeline_semaphore_test.cpp +++ b/cvk/tests/src/render/sync/timeline_semaphore_test.cpp @@ -27,6 +27,16 @@ CVK_TEST(TimelineSemaphore, create_sets_the_initial_value) { EXPECT_EQ(semaphore.wait(semaphore.value(), 0), VK_SUCCESS); } +CVK_TEST(TimelineSemaphore, construct_with_an_initial_value) { + test::Context const context{}; + + TimelineSemaphore const semaphore{context.core, create, 5}; + + ASSERT_TRUE(semaphore.created()); + EXPECT_EQ(semaphore.value(), 5); + EXPECT_EQ(semaphore.gpuValue(), 5); +} + CVK_TEST(TimelineSemaphore, signal_advances_the_counter) { test::Context const context{}; TimelineSemaphore semaphore{context.core, create}; diff --git a/cvk/tests/src/surface/swapchain_test.cpp b/cvk/tests/src/surface/swapchain_test.cpp index 7b6630f3..dae98805 100644 --- a/cvk/tests/src/surface/swapchain_test.cpp +++ b/cvk/tests/src/surface/swapchain_test.cpp @@ -79,7 +79,7 @@ namespace { cmd.end(); - Fence const fence{context.core, 0}; + Fence const fence{context.core, create}; context.queue().submit( { .cmdBuffers = {&cmd}, @@ -98,7 +98,7 @@ namespace { * @brief waits for the acquire semaphore of an image that was not presented */ void consume(test::PresentContext const& context, Semaphore const& acquired) { - Fence const fence{context.core, 0}; + Fence const fence{context.core, create}; context.queue().submit({.waits = {acquired}}, &fence); fence.wait(); } From 1bc2c3908a285229e6e5035fa3491c24d846c388 Mon Sep 17 00:00:00 2001 From: lukas <89145039+codethinki@users.noreply.github.com> Date: Wed, 21 Oct 2026 11:00:37 +0200 Subject: [PATCH 099/135] cvk: execute secondaries from one array, through pointers or one by one --- cvk/incl/cvk/render/cmd/cmd_buffer.hpp | 21 ++++++++++ cvk/src/render/cmd/cmd_buffer.cpp | 44 +++++++++++++------- cvk/tests/src/render/cmd/cmd_buffer_test.cpp | 18 ++++++++ cvk/tests/src/render/cmd/local_read_test.cpp | 5 +-- cvk/tests/src/render/cmd/rendering_test.cpp | 5 +-- 5 files changed, 72 insertions(+), 21 deletions(-) diff --git a/cvk/incl/cvk/render/cmd/cmd_buffer.hpp b/cvk/incl/cvk/render/cmd/cmd_buffer.hpp index 2910ca7d..7617b44f 100644 --- a/cvk/incl/cvk/render/cmd/cmd_buffer.hpp +++ b/cvk/incl/cvk/render/cmd/cmd_buffer.hpp @@ -446,6 +446,19 @@ class PrimaryCmdBuffer : public BaseCmdBuffer { */ void execute(std::span secondaries) const; + /** + * @brief records the secondary command buffers, the secondaries can lie anywhere + * @pre the preconditions of @ref execute(std::span) const + * @pre no pointer is nullptr + */ + void execute(std::span secondaries) const; + + /** + * @brief records the secondary command buffer + * @details calls @ref execute(std::span) const + */ + void execute(SecondaryCmdBuffer const& secondary) const; + /** * @brief what a secondary inherits from the rendering * @details the formats and samples of the attachments, the flags without @@ -453,6 +466,14 @@ class PrimaryCmdBuffer : public BaseCmdBuffer { * @pre @ref rendering() */ [[nodiscard]] RenderingInheritance inheritance() const; + +private: + /** + * @brief records vkCmdExecuteCommands() with the command buffers + * @pre @ref recording() + * @pre @ref rendering() -> VK_RENDERING_CONTENTS_SECONDARY_COMMAND_BUFFERS_BIT + */ + void recordExecute(std::span) const; }; } diff --git a/cvk/src/render/cmd/cmd_buffer.cpp b/cvk/src/render/cmd/cmd_buffer.cpp index e553229b..009bacb2 100644 --- a/cvk/src/render/cmd/cmd_buffer.cpp +++ b/cvk/src/render/cmd/cmd_buffer.cpp @@ -79,6 +79,10 @@ namespace { auto const extent = view.image().extent(); return {std::max(extent.width >> mip, 1u), std::max(extent.height >> mip, 1u)}; } + + void debug_check_secondary(SecondaryCmdBuffer const& secondary) { + CTH_CRITICAL(!secondary.created() || secondary.recording(), "secondary must be created and not recording") {} + } } BaseCmdBuffer::BaseCmdBuffer(CmdPool const& pool, VkCommandBufferLevel level) : _handle{pool}, _level{level} { @@ -561,26 +565,25 @@ void PrimaryCmdBuffer::begin(VkCommandBufferUsageFlags usage) { } void PrimaryCmdBuffer::execute(span secondaries) const { - debug_check(*this); - CTH_CRITICAL(!recording(), "command buffer must be recording") {} - CTH_CRITICAL( - std::ranges::any_of( - secondaries, - [](SecondaryCmdBuffer const& secondary) { return !secondary.created() || secondary.recording(); } - ), - "secondaries must be created and not recording" - ) {} - - CTH_CRITICAL( - rendering() != nullptr && (rendering()->flags & VK_RENDERING_CONTENTS_SECONDARY_COMMAND_BUFFERS_BIT) == 0, - "the rendering must have VK_RENDERING_CONTENTS_SECONDARY_COMMAND_BUFFERS_BIT" - ) {} + for(auto const& secondary : secondaries) + debug_check_secondary(secondary); std::vector const vkBuffers{std::from_range, secondaries | std::views::transform(&SecondaryCmdBuffer::get)}; + recordExecute(vkBuffers); +} - core().functions()->vkCmdExecuteCommands(get(), static_cast(vkBuffers.size()), vkBuffers.data()); +void PrimaryCmdBuffer::execute(span secondaries) const { + for(auto const* secondary : secondaries) { + CTH_CRITICAL(secondary == nullptr, "secondary must not be nullptr") {} + debug_check_secondary(*secondary); + } + + std::vector const vkBuffers{std::from_range, secondaries | std::views::transform(&SecondaryCmdBuffer::get)}; + recordExecute(vkBuffers); } +void PrimaryCmdBuffer::execute(SecondaryCmdBuffer const& secondary) const { execute({&secondary, 1}); } + RenderingInheritance PrimaryCmdBuffer::inheritance() const { debug_check(*this); CTH_CRITICAL(rendering() == nullptr, "command buffer must be inside its own rendering") {} @@ -607,6 +610,17 @@ RenderingInheritance PrimaryCmdBuffer::inheritance() const { .attachmentMapping = info.attachmentMapping, }; } + +void PrimaryCmdBuffer::recordExecute(span vkBuffers) const { + debug_check(*this); + CTH_CRITICAL(!recording(), "command buffer must be recording") {} + CTH_CRITICAL( + rendering() != nullptr && (rendering()->flags & VK_RENDERING_CONTENTS_SECONDARY_COMMAND_BUFFERS_BIT) == 0, + "the rendering must have VK_RENDERING_CONTENTS_SECONDARY_COMMAND_BUFFERS_BIT" + ) {} + + core().functions()->vkCmdExecuteCommands(get(), static_cast(vkBuffers.size()), vkBuffers.data()); +} } namespace cvk { diff --git a/cvk/tests/src/render/cmd/cmd_buffer_test.cpp b/cvk/tests/src/render/cmd/cmd_buffer_test.cpp index b646873e..3f06ee6f 100644 --- a/cvk/tests/src/render/cmd/cmd_buffer_test.cpp +++ b/cvk/tests/src/render/cmd/cmd_buffer_test.cpp @@ -131,6 +131,24 @@ CVK_TEST(CmdBuffer, execute_secondaries) { test::submit_and_wait(context, primary); } +CVK_TEST(CmdBuffer, execute_secondaries_through_pointers) { + test::Context const context{}; + CmdPool const pool{context.core, pool_config(context), create}; + SecondaryCmdBuffer first{pool, create}; + SecondaryCmdBuffer second{pool, create}; + for(auto* secondary : {&first, &second}) { + secondary->begin(); + secondary->end(); + } + + PrimaryCmdBuffer primary{pool, create}; + primary.begin(VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT); + primary.execute(std::array{&first, &second}); + primary.end(); + + test::submit_and_wait(context, primary); +} + CVK_TEST(CmdBuffer, secondary_begin_with_rendering_inheritance) { test::Context const context{}; CmdPool const pool{context.core, pool_config(context), create}; diff --git a/cvk/tests/src/render/cmd/local_read_test.cpp b/cvk/tests/src/render/cmd/local_read_test.cpp index fb88eafa..fccb60ad 100644 --- a/cvk/tests/src/render/cmd/local_read_test.cpp +++ b/cvk/tests/src/render/cmd/local_read_test.cpp @@ -298,7 +298,7 @@ CVK_TEST(LocalRead, secondary_inherits_the_mapping) { Attachments const attachments{context.core}; Draws const draws{context.core, attachments.firstView}; CmdPool const pool{context.core, CmdPool::Config{.queueFamilyIndex = context.core.queue(0).familyIndex()}, create}; - std::array secondaries{SecondaryCmdBuffer{pool, create}}; + SecondaryCmdBuffer secondary{pool, create}; test::record_and_submit(context, [&](PrimaryCmdBuffer& cmd) { attachments.toRendering(cmd); @@ -309,7 +309,6 @@ CVK_TEST(LocalRead, secondary_inherits_the_mapping) { EXPECT_EQ(cmd.rendering()->attachmentMapping, write_first()); // the draw needs the mapping of the write pipeline in the secondary - auto& secondary = secondaries[0]; auto const inheritance = cmd.inheritance(); EXPECT_EQ(inheritance.attachmentMapping, write_first()); secondary.begin(inheritance, VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT); @@ -320,7 +319,7 @@ CVK_TEST(LocalRead, secondary_inherits_the_mapping) { draws.drawWrite(secondary); secondary.end(); - cmd.execute(secondaries); + cmd.execute(secondary); cmd.endRendering(); }); } diff --git a/cvk/tests/src/render/cmd/rendering_test.cpp b/cvk/tests/src/render/cmd/rendering_test.cpp index 517575d3..6d1cd9b2 100644 --- a/cvk/tests/src/render/cmd/rendering_test.cpp +++ b/cvk/tests/src/render/cmd/rendering_test.cpp @@ -208,7 +208,7 @@ CVK_TEST(Rendering, secondary_inherits_the_rendering) { Image const image{context.core, COLOR_CONFIG, EXTENT}; ImageView const view{image, {}, create}; CmdPool const pool{context.core, CmdPool::Config{.queueFamilyIndex = context.core.queue(0).familyIndex()}, create}; - std::array secondaries{SecondaryCmdBuffer{pool, create}}; + SecondaryCmdBuffer secondary{pool, create}; test::record_and_submit(context, [&](PrimaryCmdBuffer& cmd) { Barrier transition{}; @@ -218,13 +218,12 @@ CVK_TEST(Rendering, secondary_inherits_the_rendering) { {.colorAttachments = {clear_red(view)}, .flags = VK_RENDERING_CONTENTS_SECONDARY_COMMAND_BUFFERS_BIT} ); - auto& secondary = secondaries[0]; secondary.begin(cmd.inheritance(), VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT); EXPECT_TRUE(secondary.recording()); EXPECT_EQ(secondary.rendering(), nullptr); secondary.end(); - cmd.execute(secondaries); + cmd.execute(secondary); cmd.endRendering(); }); } From e5780f7adc499331c0e0395b1199294e8328cdaf Mon Sep 17 00:00:00 2001 From: lukas <89145039+codethinki@users.noreply.github.com> Date: Wed, 21 Oct 2026 11:00:37 +0200 Subject: [PATCH 100/135] cvk: vectors instead of unordered maps for the queue family counts and the queue set roots --- cvk/incl/cvk/base/device.hpp | 16 +++++++++------- cvk/src/base/core.cpp | 10 ++++------ cvk/src/base/device.cpp | 32 +++++++++++++++++++++++++------- 3 files changed, 38 insertions(+), 20 deletions(-) diff --git a/cvk/incl/cvk/base/device.hpp b/cvk/incl/cvk/base/device.hpp index 734db803..2fd08d3d 100644 --- a/cvk/incl/cvk/base/device.hpp +++ b/cvk/incl/cvk/base/device.hpp @@ -13,7 +13,6 @@ #include #include #include -#include #include @@ -25,8 +24,6 @@ class PhysicalDevice; * @brief VkDevice wrapper, owns the function table and the queues */ class Device { - using queue_families_queue_counts_t = std::unordered_map; - public: using vk_handle_t = VkDevice; using Config = DeviceConfig; @@ -133,15 +130,20 @@ class Device { private: void forget(); - [[nodiscard]] static queue_families_queue_counts_t calcQueueFamiliesQueueCounts( - std::span familyIndices - ); + /** + * @param familyIndices family index of each queue + * @return count of queues per family, one entry per family of the physical device + * @pre the physical device has a family for each index + */ + [[nodiscard]] std::vector calcQueueFamiliesQueueCounts(std::span familyIndices) const; /** * @brief creates the device with @ref _features and @ref _extensions + * @param queueFamiliesQueueCounts count of queues per family, the index is the family index + * @pre some count > 0 * @throws cvk::vk_result_exception result of vkCreateDevice() */ - void createLogicalDevice(queue_families_queue_counts_t const& queueFamiliesQueueCounts); + void createLogicalDevice(std::span queueFamiliesQueueCounts); void loadFunctionTable(VkDevice vkDevice) const; diff --git a/cvk/src/base/core.cpp b/cvk/src/base/core.cpp index 4b0d7329..4c190a37 100644 --- a/cvk/src/base/core.cpp +++ b/cvk/src/base/core.cpp @@ -9,7 +9,6 @@ #include "cvk/utility/vk_exceptions.hpp" #include -#include namespace cvk { @@ -189,16 +188,15 @@ auto Core::createQueueSetRelations( auto const roots = queueSet.roots(); - std::unordered_map const rootToQueue{ - std::from_range, - std::views::zip(roots, std::views::iota(0uz)) - }; + vector rootToQueue(queueSet.size()); + for(auto const [root, queue] : std::views::zip(roots, std::views::iota(0uz))) + rootToQueue[root] = queue; vector uniqueQueues(roots.size()); vector queueToUniqueQueue(queueProperties.size()); for(index_type i = 0; i < queueSet.size(); i++) { - auto const queueIdx = rootToQueue.at(queueSet.find(i)); + auto const queueIdx = rootToQueue[queueSet.find(i)]; queueToUniqueQueue[i] = queueIdx; uniqueQueues[queueIdx] |= queueProperties[i]; } diff --git a/cvk/src/base/device.cpp b/cvk/src/base/device.cpp index 71130b68..9854dd21 100644 --- a/cvk/src/base/device.cpp +++ b/cvk/src/base/device.cpp @@ -111,17 +111,27 @@ void Device::forget() { std::memset(_functionTable.get(), 0, sizeof(VolkDeviceTable)); } -auto Device::calcQueueFamiliesQueueCounts(span familyIndices) -> queue_families_queue_counts_t { - queue_families_queue_counts_t queueCounts{}; +vector Device::calcQueueFamiliesQueueCounts(span familyIndices) const { + vector queueCounts(_physicalDevice->queueFamilies().size()); - for(auto const& familyIndex : familyIndices) + for(auto const familyIndex : familyIndices) { + CTH_CRITICAL( + familyIndex >= queueCounts.size(), + "queue family index must be less than the family count {}, got {}", + queueCounts.size(), + familyIndex + ) {} ++queueCounts[familyIndex]; + } return queueCounts; } -void Device::createLogicalDevice(queue_families_queue_counts_t const& queueFamiliesQueueCounts) { - CTH_CRITICAL(queueFamiliesQueueCounts.empty(), "at least one queue is required") {} +void Device::createLogicalDevice(span queueFamiliesQueueCounts) { + CTH_CRITICAL( + std::ranges::all_of(queueFamiliesQueueCounts, [](uint32_t count) { return count == 0; }), + "at least one queue is required" + ) {} CTH_CRITICAL(created(), "device was already created") {} vector queueCreateInfos{}; @@ -130,7 +140,9 @@ void Device::createLogicalDevice(queue_families_queue_counts_t const& queueFamil vector> queuePriorities{}; queuePriorities.reserve(queueFamiliesQueueCounts.size()); - for(auto const [queueFamily, queueCount] : queueFamiliesQueueCounts) { + for(auto const [queueFamily, queueCount] : std::views::zip(std::views::iota(0u), queueFamiliesQueueCounts)) { + if(queueCount == 0) continue; + queuePriorities.emplace_back(queueCount, 1.0f); queueCreateInfos.push_back( @@ -174,11 +186,17 @@ void Device::createLogicalDevice(queue_families_queue_counts_t const& queueFamil void Device::loadFunctionTable(VkDevice vkDevice) const { volkLoadDeviceTable(_functionTable.get(), vkDevice); } void Device::createQueues(span familyIndices) { - std::unordered_map queueCounts{}; + vector queueCounts(_physicalDevice->queueFamilies().size()); _queues.reserve(familyIndices.size()); for(auto const familyIndex : familyIndices) { + CTH_CRITICAL( + familyIndex >= queueCounts.size(), + "queue family index must be less than the family count {}, got {}", + queueCounts.size(), + familyIndex + ) {} auto& queueIndex = queueCounts[familyIndex]; VkQueue ptr = VK_NULL_HANDLE; From 6b8d01b640922c0fb7b79027fd3ea37422e97bc5 Mon Sep 17 00:00:00 2001 From: lukas <89145039+codethinki@users.noreply.github.com> Date: Wed, 21 Oct 2026 11:13:01 +0200 Subject: [PATCH 101/135] cvk: execute secondaries from a braced list of pointers, explicit DescriptorSet tag constructors --- cvk/incl/cvk/data/descriptor/descriptor_set.hpp | 4 ++-- cvk/incl/cvk/render/cmd/cmd_buffer.hpp | 8 ++++++++ cvk/src/render/cmd/cmd_buffer.cpp | 4 ++++ cvk/tests/src/render/cmd/cmd_buffer_test.cpp | 12 ++++++++---- 4 files changed, 22 insertions(+), 6 deletions(-) diff --git a/cvk/incl/cvk/data/descriptor/descriptor_set.hpp b/cvk/incl/cvk/data/descriptor/descriptor_set.hpp index 17d440cd..8102dd8c 100644 --- a/cvk/incl/cvk/data/descriptor/descriptor_set.hpp +++ b/cvk/incl/cvk/data/descriptor/descriptor_set.hpp @@ -51,13 +51,13 @@ class DescriptorSet { * - @ref DescriptorSet(DescriptorPool const&, DescriptorSetLayout const&, uint32_t) * - @ref create() */ - DescriptorSet(DescriptorPool const&, DescriptorSetLayout const&, uint32_t variableCount, create_t); + explicit DescriptorSet(DescriptorPool const&, DescriptorSetLayout const&, uint32_t variableCount, create_t); /** * @brief constructs and creates with the maximum variable count * @details calls @ref DescriptorSet(DescriptorPool const&, DescriptorSetLayout const&, uint32_t, create_t) */ - DescriptorSet(DescriptorPool const& pool, DescriptorSetLayout const& layout, create_t) : DescriptorSet{ + explicit DescriptorSet(DescriptorPool const& pool, DescriptorSetLayout const& layout, create_t) : DescriptorSet{ pool, layout, 0, diff --git a/cvk/incl/cvk/render/cmd/cmd_buffer.hpp b/cvk/incl/cvk/render/cmd/cmd_buffer.hpp index 7617b44f..3dbcde30 100644 --- a/cvk/incl/cvk/render/cmd/cmd_buffer.hpp +++ b/cvk/incl/cvk/render/cmd/cmd_buffer.hpp @@ -15,6 +15,7 @@ #include #include +#include #include #include #include @@ -453,6 +454,13 @@ class PrimaryCmdBuffer : public BaseCmdBuffer { */ void execute(std::span secondaries) const; + /** + * @brief records the secondary command buffers of a braced list: execute({&a, &b}) + * @details calls @ref execute(std::span) const, + * without this overload {&a, &b} binds to std::span as iterator pair -> records [&a, &b) + */ + void execute(std::initializer_list secondaries) const; + /** * @brief records the secondary command buffer * @details calls @ref execute(std::span) const diff --git a/cvk/src/render/cmd/cmd_buffer.cpp b/cvk/src/render/cmd/cmd_buffer.cpp index 009bacb2..2c148182 100644 --- a/cvk/src/render/cmd/cmd_buffer.cpp +++ b/cvk/src/render/cmd/cmd_buffer.cpp @@ -582,6 +582,10 @@ void PrimaryCmdBuffer::execute(span secondaries recordExecute(vkBuffers); } +void PrimaryCmdBuffer::execute(std::initializer_list secondaries) const { + execute(span{secondaries.begin(), secondaries.size()}); +} + void PrimaryCmdBuffer::execute(SecondaryCmdBuffer const& secondary) const { execute({&secondary, 1}); } RenderingInheritance PrimaryCmdBuffer::inheritance() const { diff --git a/cvk/tests/src/render/cmd/cmd_buffer_test.cpp b/cvk/tests/src/render/cmd/cmd_buffer_test.cpp index 3f06ee6f..fe504830 100644 --- a/cvk/tests/src/render/cmd/cmd_buffer_test.cpp +++ b/cvk/tests/src/render/cmd/cmd_buffer_test.cpp @@ -134,16 +134,20 @@ CVK_TEST(CmdBuffer, execute_secondaries) { CVK_TEST(CmdBuffer, execute_secondaries_through_pointers) { test::Context const context{}; CmdPool const pool{context.core, pool_config(context), create}; - SecondaryCmdBuffer first{pool, create}; - SecondaryCmdBuffer second{pool, create}; - for(auto* secondary : {&first, &second}) { + // secondaries[1] is not recorded, the iterator pair [&secondaries[0], &secondaries[2]) would execute it + std::array secondaries{ + SecondaryCmdBuffer{pool, create}, + SecondaryCmdBuffer{pool, create}, + SecondaryCmdBuffer{pool, create}, + }; + for(auto* secondary : {&secondaries[0], &secondaries[2]}) { secondary->begin(); secondary->end(); } PrimaryCmdBuffer primary{pool, create}; primary.begin(VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT); - primary.execute(std::array{&first, &second}); + primary.execute({&secondaries[0], &secondaries[2]}); primary.end(); test::submit_and_wait(context, primary); From bfb32097c6f9054d6389dd02f3fec7728c55fd53 Mon Sep 17 00:00:00 2001 From: lukas <89145039+codethinki@users.noreply.github.com> Date: Wed, 21 Oct 2026 11:35:27 +0200 Subject: [PATCH 102/135] lib: cth with gtl hash maps, cth_cmake with find modules, flat_hash_map aliases the cth one, no own gtl.cmake --- AGENTS.md | 2 +- CMakeLists.txt | 1 - jvk/prework/flat_hash_map.hpp | 4 ++-- jvk/prework/image_layouts.hpp | 2 +- lib/cmake/gtl.cmake | 7 ------- lib/cth | 2 +- lib/cth_cmake | 2 +- 7 files changed, 6 insertions(+), 14 deletions(-) delete mode 100644 lib/cmake/gtl.cmake diff --git a/AGENTS.md b/AGENTS.md index 9457de8c..923f9827 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -23,7 +23,7 @@ Read it before you change a file. Match the existing code. - Memory comes from VMA. - `cvk` destroys immediately. It has no destruction queue. Deferred destruction belongs to `jvk`. - `cvk` tracks no image layouts. Every function that uses a layout takes it as a value, with a default where the use fixes it. Layout tracking belongs to `jvk` (`jvk/prework/image_layouts.hpp`). -- Hash maps come from gtl (`jvk/prework/flat_hash_map.hpp`). gtl installs no cmake package, `lib/cmake/gtl.cmake` creates the target `gtl::gtl`. +- Hash maps come from cth: `cth::dt::flat_hash_map` and `cth::dt::flat_hash_set` (gtl, no pointer stability), `jvk/prework/flat_hash_map.hpp` aliases the map. No `std::unordered_map`. - Commits have no `Co-Authored-By` or "Generated with" lines. ## Branches diff --git a/CMakeLists.txt b/CMakeLists.txt index 60b09d33..59184d0b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -17,7 +17,6 @@ if(LLD_LINKER AND NOT MSVC) endif() include(lib/cmake/cth.cmake) -include(lib/cmake/gtl.cmake) if(JOLLY_ENABLE_TESTS) enable_testing() diff --git a/jvk/prework/flat_hash_map.hpp b/jvk/prework/flat_hash_map.hpp index 666f6a35..28df9519 100644 --- a/jvk/prework/flat_hash_map.hpp +++ b/jvk/prework/flat_hash_map.hpp @@ -1,5 +1,5 @@ #pragma once -#include +#include namespace cvk { @@ -7,5 +7,5 @@ namespace cvk { * @brief hash map without pointer stability, a rehash moves the elements */ template -using flat_hash_map_t = gtl::flat_hash_map; +using flat_hash_map = cth::dt::flat_hash_map; } diff --git a/jvk/prework/image_layouts.hpp b/jvk/prework/image_layouts.hpp index 259da692..c449b48e 100644 --- a/jvk/prework/image_layouts.hpp +++ b/jvk/prework/image_layouts.hpp @@ -57,6 +57,6 @@ class ImageLayouts { private: //IMPLEMENT key (image, mip level) -> layout: no vector per image, no mip count check - flat_hash_map_t> _layouts{}; + flat_hash_map> _layouts{}; }; } diff --git a/lib/cmake/gtl.cmake b/lib/cmake/gtl.cmake deleted file mode 100644 index 35fa86d5..00000000 --- a/lib/cmake/gtl.cmake +++ /dev/null @@ -1,7 +0,0 @@ -# gtl installs only its headers and no cmake package, this creates the target gtl::gtl -if(NOT TARGET gtl::gtl) - find_path(GTL_INCLUDE_DIR gtl/phmap.hpp REQUIRED) - - add_library(gtl::gtl INTERFACE IMPORTED) - target_include_directories(gtl::gtl SYSTEM INTERFACE ${GTL_INCLUDE_DIR}) -endif() diff --git a/lib/cth b/lib/cth index 85b058be..9b0417a3 160000 --- a/lib/cth +++ b/lib/cth @@ -1 +1 @@ -Subproject commit 85b058bea610416c1d710687f3328f12efa234b0 +Subproject commit 9b0417a3e10bce820361f91a51a7b064425c4db4 diff --git a/lib/cth_cmake b/lib/cth_cmake index 09a6c5d0..79a61917 160000 --- a/lib/cth_cmake +++ b/lib/cth_cmake @@ -1 +1 @@ -Subproject commit 09a6c5d02421cba58fcf33bc3e4fd3d4fb646002 +Subproject commit 79a619176696cebe86d5d56059681f369a8b50e6 From 33dea8521f1e2d20bda82a25cf911a105755512f Mon Sep 17 00:00:00 2001 From: lukas <89145039+codethinki@users.noreply.github.com> Date: Wed, 21 Oct 2026 12:11:09 +0200 Subject: [PATCH 103/135] AGENTS: shorter rules, the override rule --- AGENTS.md | 45 ++++++++++++++++++++++++--------------------- 1 file changed, 24 insertions(+), 21 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 923f9827..dea393ff 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -17,13 +17,14 @@ Read it before you change a file. Match the existing code. | `lib/cmake` | one cmake file per library that adds it: `.cmake` | - Vulkan 1.3 is the baseline. Do not port code that 1.3 makes unnecessary (render passes, framebuffers, the old sync API). -- Functionality beyond 1.3 comes from required extensions, not from a higher version: `VK_KHR_dynamic_rendering_local_read` gives tile-based GPUs subpass-like local reads in a dynamic rendering. -- Device features are the `bool` fields of `DeviceFeatures` (`cvk/utility/device/device_features.hpp`). A true bit of an extension struct enables its extension. A new extension struct: an entry in `dev::feature_structs_t`, a `dev::feature_struct` specialization, its fields in `DeviceFeatures` in the same order. +- Features beyond 1.3 come from required extensions, not from a higher version: `VK_KHR_dynamic_rendering_local_read` (tile-based local reads). +- Device features are the `bool` fields of `DeviceFeatures`. A true bit of an extension struct enables its extension. + New extension struct: entry in `dev::feature_structs_t`, `dev::feature_struct` specialization, fields in `DeviceFeatures` in the same order. - Use the Vulkan C API through volk. Do not use Vulkan-Hpp. - Memory comes from VMA. - `cvk` destroys immediately. It has no destruction queue. Deferred destruction belongs to `jvk`. -- `cvk` tracks no image layouts. Every function that uses a layout takes it as a value, with a default where the use fixes it. Layout tracking belongs to `jvk` (`jvk/prework/image_layouts.hpp`). -- Hash maps come from cth: `cth::dt::flat_hash_map` and `cth::dt::flat_hash_set` (gtl, no pointer stability), `jvk/prework/flat_hash_map.hpp` aliases the map. No `std::unordered_map`. +- `cvk` tracks no image layouts: functions take the layout as a value, with a default where the use fixes it. Tracking belongs to `jvk`. +- Hash maps: `cth::dt::flat_hash_map`, `cth::dt::flat_hash_set`. No `std::unordered_map`. - Commits have no `Co-Authored-By` or "Generated with" lines. ## Branches @@ -85,8 +86,8 @@ ctest --test-dir out/build/gcc_release --output-on-failure | namespace | short lower case | `cvk`, `cvk::constants`, `cvk::dev` | - A raw Vulkan handle in a variable or parameter has the prefix `vk`: `vkInstance`, `vkSemaphore`. A raw VMA handle has the prefix `vma`: `vmaAllocator`, `vmaAllocation`. -- `_t` marks an alias that only renames a type: `callback_t` is a function pointer, `queue_set_t` a `union_find`. An alias that is a type of its own gets a plain std style name, like `std::string` for `std::basic_string`. -- A declaration leaves out a parameter name that repeats the type: `void wrap(State const&)`, `destroy(DeviceTable table, VkSemaphore)`, not `VkSemaphore vkSemaphore`. The definition keeps the name. +- `_t` only for aliases that rename a type: `callback_t`, `queue_set_t`. An alias that is a type of its own gets a std style name, like `std::string`. +- Declarations leave out parameter names that repeat the type: `wrap(State const&)`, not `VkSemaphore vkSemaphore`. Definitions keep them. - Function parameters were `snake_case` in the legacy code. New code uses `camelCase`. Rename them when you port. - `cvk::dev` holds implementation details that are not part of the API. @@ -94,18 +95,24 @@ ctest --test-dir out/build/gcc_release --output-on-failure - Use `constexpr`, `consteval` and `decltype(auto)`. The macros `cxpr`, `cval` and `declauto` do not exist anymore. - Mark every function `constexpr` that can run at compile time, also when no code uses it there yet: accessors, `debug_check` functions, configs, value types. -- Functions that call Vulkan, VMA or the OS are not `constexpr`. Neither are the constructors, the destructor and the lifecycle functions of a class that owns a handle: the object cannot exist at compile time. +- Not `constexpr`: functions that call Vulkan, VMA or the OS, and the constructors, destructor and lifecycle of a handle owner. - Put `[[nodiscard]]` on every function that returns a value the caller must use. - Put `explicit` on constructors with one parameter and on tag constructors. - Use `auto const` for locals that do not change. - Initialize Vulkan structs with designated initializers. Always write `.sType` and `.pNext`. -- Data structs (`SubmitInfo`, `Barrier`, the info structs) are aggregates: the caller can set everything with designated initializers. Member functions like `add()` are optional shortcuts, checks run where the data is used. -- Commands that change the state of a command buffer (binds, dynamic state, push constants, renderings, draws) are members of the command buffer: `cmd.bindPipeline(pipeline)`. Commands on the data of an object (barriers, copies, uploads) are members of the object and take the command buffer: `barrier.execute(cmd)`, `image.copy(cmd, staging)`. -- The target comes first, like `std::memcpy(dst, src, ...)`: new before old, dst before src, `LayoutTransition(newLayout, oldLayout, dst, src)`. The object of a copy is the destination: `dst.copy(cmd, src)`. `copyTo(cmd, dst)` copies with the object as the source, where the code belongs to the source: `image.copyTo(cmd, buffer)`, the mirror `buffer.copy(cmd, image)` calls it. Exception: `src` and `dst` fields keep the order of the Vulkan struct (`BarrierScope src, dst`, `BufferCopy{srcOffset, dstOffset}`), designated initializers follow it. +- Data structs (`SubmitInfo`, `Barrier`, info structs) are aggregates. Member functions like `add()` are optional shortcuts, checks run at the use. +- Commands on the command buffer state (binds, dynamic state, push constants, renderings, draws) are command buffer members: `cmd.bindPipeline(pipeline)`. + Commands on the data of an object (barriers, copies, uploads) are object members: `barrier.execute(cmd)`. +- Target first, like `std::memcpy`: new before old, dst before src: `LayoutTransition(newLayout, oldLayout, dst, src)`. + - copies: `dst.copy(cmd, src)`, `src.copyTo(cmd, dst)` where the code belongs to the source: `image.copyTo(cmd, buffer)` + - struct fields keep the Vulkan order: `BarrierScope src, dst` - Use deducing `this` (`this auto& self`) where a function must work on the derived type. - Qualify `std::` names. A `.cpp` file can declare `using std::span;` and similar at the top of its namespace. - No raw `new` and `delete`. Use `std::unique_ptr`, `cth::move_ptr` and `cth::not_null`. - Prefer compile time checks (concepts, `static_assert`) to run time checks. +- `cvk` offers an override of an automatically chosen value if: + - the choice is not guaranteed to be optimal + - there is a legitimate interest: debugging, user preference - Never erase types into integers or `void*`. ### Includes @@ -173,16 +180,12 @@ public: - The last public section holds what every class has and readers rarely need: copy and move members, `debug_check` functions. - Classes that own a handle are move only. A class that differs says so in its `@brief`: copyable, neither copyable nor movable, or a custom move. - Every class of a Vulkan or VMA object declares `using vk_handle_t = VkX;` as its first type, also classes that do not own their handle. `get()` returns `vk_handle_t`. -- They store the handle in `cvk::unique_handle` (`cvk/utility/handle/unique_handle.hpp`): a `std::unique_ptr` with a deleter that holds the destroy context and calls the static `destroy`. - Deleters: `core_deleter` (the core), `vma_deleter` (the core, destroys through its allocator), `instance_deleter` (the `VkInstance`), `handle_deleter` (no context), a deleter derived from them or one of its own (`BaseCmdBuffer` frees through its pool). - The deleters require `vk_handle_owner`: `T` declares `vk_handle_t`. - Device objects use `core_handle` (`cvk/utility/handle/core_handle.hpp`), short for `unique_handle>`. - Objects with VMA memory use `vma_handle` (`cvk/utility/handle/vma_handle.hpp`), short for `unique_handle>`. - Instance objects use `instance_handle` (`cvk/utility/handle/instance_handle.hpp`), short for `unique_handle>`. - Other deleters: `unique_handle>`. +- The handle lives in `cvk::unique_handle`: a `std::unique_ptr` whose deleter holds the destroy context and calls the static `destroy`. + Short forms: `core_handle` (device objects), `vma_handle` (VMA memory), `instance_handle` (instance objects). + Other deleters: `handle_deleter` (no context) or an own one (`BaseCmdBuffer` frees through its pool). - Handle and context go in together: `_handle.reset(ptr, core())`. A missing context argument does not compile. - Move constructor and move assignment are `= default`. The move assignment destroys the old handle with the old context. -- A class that owns more than one object declares the objects that depend on its handle first. `= default` assigns in declaration order: `Instance` declares `_debugMessenger` before `_handle`. +- A class that owns several objects declares the objects that depend on its handle first, `= default` assigns in that order: `_debugMessenger` before `_handle`. - Classes that do not own their handle (`Queue`, `PhysicalDevice`) keep `cth::move_ptr`. After the class, in the same header: @@ -215,12 +218,12 @@ inline void Semaphore::debug_check(Semaphore const& semaphore) { | `forget()` | sets the members to the not created state, does not destroy | - The destructor calls `optDestroy()`. -- `reset` keeps its std meaning (`std::unique_ptr::reset` destroys). `reset` and `invalidate` name Vulkan operations (`vkResetFences`, `vkInvalidateMappedMemoryRanges`). The lifecycle uses `forget()`. +- `reset` and `invalidate` keep their std and Vulkan meanings (`unique_ptr::reset`, `vkResetFences`). The lifecycle uses `forget()`. - Device objects take `Core const&` in the base constructor. The `core_deleter` of the handle stores it, `core()` returns it. - A parent (instance, core, device, pool, ...) does not move and is not destroyed while objects depend on it. Code does not have to support it. - A failed `create()` calls `forget()` before it throws. - `release()` exists so that an owner can use an object it must not destroy (for example swapchain images). -- A constructor that creates takes the parameters of the base constructor, then the parameters of `create()`. When `create()` has no required parameter, a `create_t` tag comes before them: `Fence(core, create_t, flags = 0)`, `Image(core, config, extent)`. +- Create constructor: the base constructor parameters, then the `create()` parameters. `create()` without required parameters -> `create_t` first: `Fence(core, create_t, flags = 0)`. ### Errors @@ -229,7 +232,7 @@ inline void Semaphore::debug_check(Semaphore const& semaphore) { - `CTH_WARN(condition, "message") {}`: suspicious state that is not an error. - `cvk` stores state only where it needs the state to work. A check that needs more state is left to the validation layers. - `CVK_RESULT_STABLE_THROW(result != VK_SUCCESS, result, "message") {}`: throws `cvk::vk_result_exception` with the `VkResult`. -- `cvk` throws `cvk::cvk_exception` or a derived type: `CVK_STABLE_THROW`, `CVK_RESULT_STABLE_THROW`, or `throw cvk::cvk_exception{details->exception()}` inside a `CTH_STABLE_ERR` block. +- `cvk` throws only `cvk::cvk_exception` and derived types. - The condition is the error case: `CTH_CRITICAL(!created(), ...)`, not the expected case. ### Comment markers From 1cb0b08611528bff2e4ab84ecc14fb21a25037b9 Mon Sep 17 00:00:00 2001 From: lukas <89145039+codethinki@users.noreply.github.com> Date: Wed, 21 Oct 2026 12:14:06 +0200 Subject: [PATCH 104/135] AGENTS: std call patterns, exception type per module --- AGENTS.md | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index dea393ff..1f91e6e5 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -103,9 +103,7 @@ ctest --test-dir out/build/gcc_release --output-on-failure - Data structs (`SubmitInfo`, `Barrier`, info structs) are aggregates. Member functions like `add()` are optional shortcuts, checks run at the use. - Commands on the command buffer state (binds, dynamic state, push constants, renderings, draws) are command buffer members: `cmd.bindPipeline(pipeline)`. Commands on the data of an object (barriers, copies, uploads) are object members: `barrier.execute(cmd)`. -- Target first, like `std::memcpy`: new before old, dst before src: `LayoutTransition(newLayout, oldLayout, dst, src)`. - - copies: `dst.copy(cmd, src)`, `src.copyTo(cmd, dst)` where the code belongs to the source: `image.copyTo(cmd, buffer)` - - struct fields keep the Vulkan order: `BarrierScope src, dst` +- Call patterns follow the modern std library conventions. - Use deducing `this` (`this auto& self`) where a function must work on the derived type. - Qualify `std::` names. A `.cpp` file can declare `using std::span;` and similar at the top of its namespace. - No raw `new` and `delete`. Use `std::unique_ptr`, `cth::move_ptr` and `cth::not_null`. @@ -232,7 +230,7 @@ inline void Semaphore::debug_check(Semaphore const& semaphore) { - `CTH_WARN(condition, "message") {}`: suspicious state that is not an error. - `cvk` stores state only where it needs the state to work. A check that needs more state is left to the validation layers. - `CVK_RESULT_STABLE_THROW(result != VK_SUCCESS, result, "message") {}`: throws `cvk::vk_result_exception` with the `VkResult`. -- `cvk` throws only `cvk::cvk_exception` and derived types. +- A module of cth_engine throws only its exception type `::_exception` or a derived type: `cvk::cvk_exception`. - The condition is the error case: `CTH_CRITICAL(!created(), ...)`, not the expected case. ### Comment markers From 10d5b5ac7d7b239da3163b85a15573a8cdd24ffc Mon Sep 17 00:00:00 2001 From: lukas <89145039+codethinki@users.noreply.github.com> Date: Wed, 21 Oct 2026 12:19:40 +0200 Subject: [PATCH 105/135] AGENTS: general rules instead of lists of cases --- AGENTS.md | 38 +++++++++++++++++--------------------- 1 file changed, 17 insertions(+), 21 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 1f91e6e5..2ca0a1b5 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -17,13 +17,14 @@ Read it before you change a file. Match the existing code. | `lib/cmake` | one cmake file per library that adds it: `.cmake` | - Vulkan 1.3 is the baseline. Do not port code that 1.3 makes unnecessary (render passes, framebuffers, the old sync API). -- Features beyond 1.3 come from required extensions, not from a higher version: `VK_KHR_dynamic_rendering_local_read` (tile-based local reads). +- Features beyond 1.3 come from required extensions, not from a higher version. - Device features are the `bool` fields of `DeviceFeatures`. A true bit of an extension struct enables its extension. New extension struct: entry in `dev::feature_structs_t`, `dev::feature_struct` specialization, fields in `DeviceFeatures` in the same order. - Use the Vulkan C API through volk. Do not use Vulkan-Hpp. - Memory comes from VMA. -- `cvk` destroys immediately. It has no destruction queue. Deferred destruction belongs to `jvk`. -- `cvk` tracks no image layouts: functions take the layout as a value, with a default where the use fixes it. Tracking belongs to `jvk`. +- `cvk` keeps only the state it needs to work: no destruction queue, no layout tracking, no thread sync. + - other state is a parameter, with a default where the use fixes it: the image layout + - `jvk` tracks it, the validation layers do the checks that need it - Hash maps: `cth::dt::flat_hash_map`, `cth::dt::flat_hash_set`. No `std::unordered_map`. - Commits have no `Co-Authored-By` or "Generated with" lines. @@ -85,30 +86,27 @@ ctest --test-dir out/build/gcc_release --output-on-failure | macro | `UPPER_SNAKE_CASE` with library prefix | `CVK_RESULT_STABLE_THROW` | | namespace | short lower case | `cvk`, `cvk::constants`, `cvk::dev` | -- A raw Vulkan handle in a variable or parameter has the prefix `vk`: `vkInstance`, `vkSemaphore`. A raw VMA handle has the prefix `vma`: `vmaAllocator`, `vmaAllocation`. +- A raw handle in a variable or parameter has the prefix of its API: `vkInstance`, `vmaAllocation`. - `_t` only for aliases that rename a type: `callback_t`, `queue_set_t`. An alias that is a type of its own gets a std style name, like `std::string`. - Declarations leave out parameter names that repeat the type: `wrap(State const&)`, not `VkSemaphore vkSemaphore`. Definitions keep them. -- Function parameters were `snake_case` in the legacy code. New code uses `camelCase`. Rename them when you port. -- `cvk::dev` holds implementation details that are not part of the API. +- `::dev` holds implementation details that are not part of the API. ### Language -- Use `constexpr`, `consteval` and `decltype(auto)`. The macros `cxpr`, `cval` and `declauto` do not exist anymore. -- Mark every function `constexpr` that can run at compile time, also when no code uses it there yet: accessors, `debug_check` functions, configs, value types. -- Not `constexpr`: functions that call Vulkan, VMA or the OS, and the constructors, destructor and lifecycle of a handle owner. +- Mark every function `constexpr` that can run at compile time, also when nothing uses it there yet. + A handle owner cannot exist at compile time: its lifecycle is not `constexpr`. - Put `[[nodiscard]]` on every function that returns a value the caller must use. - Put `explicit` on constructors with one parameter and on tag constructors. - Use `auto const` for locals that do not change. -- Initialize Vulkan structs with designated initializers. Always write `.sType` and `.pNext`. +- Initialize aggregates with designated initializers. Vulkan structs always get `.sType` and `.pNext`. - Data structs (`SubmitInfo`, `Barrier`, info structs) are aggregates. Member functions like `add()` are optional shortcuts, checks run at the use. -- Commands on the command buffer state (binds, dynamic state, push constants, renderings, draws) are command buffer members: `cmd.bindPipeline(pipeline)`. - Commands on the data of an object (barriers, copies, uploads) are object members: `barrier.execute(cmd)`. +- A command is a member of the object it acts on: the command buffer for its own state (`cmd.bindPipeline(pipeline)`), else the object (`barrier.execute(cmd)`). - Call patterns follow the modern std library conventions. - Use deducing `this` (`this auto& self`) where a function must work on the derived type. - Qualify `std::` names. A `.cpp` file can declare `using std::span;` and similar at the top of its namespace. - No raw `new` and `delete`. Use `std::unique_ptr`, `cth::move_ptr` and `cth::not_null`. - Prefer compile time checks (concepts, `static_assert`) to run time checks. -- `cvk` offers an override of an automatically chosen value if: +- An automatically chosen value has an override if: - the choice is not guaranteed to be optimal - there is a legitimate interest: debugging, user preference - Never erase types into integers or `void*`. @@ -176,11 +174,10 @@ public: - The first public section holds the API: types, constants, lifecycle, operations, accessors. - The last public section holds what every class has and readers rarely need: copy and move members, `debug_check` functions. -- Classes that own a handle are move only. A class that differs says so in its `@brief`: copyable, neither copyable nor movable, or a custom move. +- Classes that own a handle are move only. A class that differs says so in its `@brief`. - Every class of a Vulkan or VMA object declares `using vk_handle_t = VkX;` as its first type, also classes that do not own their handle. `get()` returns `vk_handle_t`. - The handle lives in `cvk::unique_handle`: a `std::unique_ptr` whose deleter holds the destroy context and calls the static `destroy`. - Short forms: `core_handle` (device objects), `vma_handle` (VMA memory), `instance_handle` (instance objects). - Other deleters: `handle_deleter` (no context) or an own one (`BaseCmdBuffer` frees through its pool). + `cvk/utility/handle/` has the deleters and the short forms (`core_handle`). - Handle and context go in together: `_handle.reset(ptr, core())`. A missing context argument does not compile. - Move constructor and move assignment are `= default`. The move assignment destroys the old handle with the old context. - A class that owns several objects declares the objects that depend on its handle first, `= default` assigns in that order: `_debugMessenger` before `_handle`. @@ -216,7 +213,7 @@ inline void Semaphore::debug_check(Semaphore const& semaphore) { | `forget()` | sets the members to the not created state, does not destroy | - The destructor calls `optDestroy()`. -- `reset` and `invalidate` keep their std and Vulkan meanings (`unique_ptr::reset`, `vkResetFences`). The lifecycle uses `forget()`. +- std and Vulkan names keep their meaning: the lifecycle uses `forget()`, not `reset()`. - Device objects take `Core const&` in the base constructor. The `core_deleter` of the handle stores it, `core()` returns it. - A parent (instance, core, device, pool, ...) does not move and is not destroyed while objects depend on it. Code does not have to support it. - A failed `create()` calls `forget()` before it throws. @@ -226,9 +223,8 @@ inline void Semaphore::debug_check(Semaphore const& semaphore) { ### Errors - `CTH_CRITICAL(condition, "message") {}`: precondition check, active in debug builds only. -- `CTH_STABLE_ERR(condition, "message") { forget(); throw cvk::cvk_exception{details->exception()}; }`: error that can happen at run time. +- `CTH_STABLE_ERR(condition, "message") { throw ::_exception{details->exception()}; }`: error that can happen at run time. - `CTH_WARN(condition, "message") {}`: suspicious state that is not an error. -- `cvk` stores state only where it needs the state to work. A check that needs more state is left to the validation layers. - `CVK_RESULT_STABLE_THROW(result != VK_SUCCESS, result, "message") {}`: throws `cvk::vk_result_exception` with the `VkResult`. - A module of cth_engine throws only its exception type `::_exception` or a derived type: `cvk::cvk_exception`. - The condition is the error case: `CTH_CRITICAL(!created(), ...)`, not the expected case. @@ -271,7 +267,7 @@ Documentation uses doxygen comments and follows ASD-STE100 (Simplified Technical | `@attention` | danger that is not a precondition: undefined behavior, not thread safe | - Write preconditions as `@pre`, not as `@attention requires ...`. -- For an exception that cvk throws itself, write the cause after `reason:`: `@throws cvk::cvk_exception reason: required extension missing`. +- For an exception of the module itself, write the cause after `reason:`: `@throws cvk::cvk_exception reason: required extension missing`. ### Math like notation @@ -320,8 +316,8 @@ void create(std::optional messengerConfig = std::nullopt ### When you port legacy code +- Apply this guide to ported code. The legacy macros `cxpr`, `cval` and `declauto` become keywords. - Check every comment against the code. The legacy comments are not always up to date. -- Rewrite comments to this style. - Report bugs that you find. Do not fix a behavior change silently. ### Repository text From ef27af4b73a1c281537770b3e30c5a5ce633af62 Mon Sep 17 00:00:00 2001 From: lukas <89145039+codethinki@users.noreply.github.com> Date: Wed, 21 Oct 2026 12:21:26 +0200 Subject: [PATCH 106/135] cvk: the swapchain picks format and present mode from its config, recreate(extent, config), surface without config --- cvk/incl/cvk/surface/surface.hpp | 33 ++----- cvk/incl/cvk/surface/surface_config.hpp | 28 ------ cvk/incl/cvk/surface/swapchain.hpp | 46 ++++++++- cvk/incl/cvk/surface/swapchain_config.hpp | 18 +++- cvk/src/surface/surface.cpp | 56 ++--------- cvk/src/surface/swapchain.cpp | 109 ++++++++++++++++------ cvk/tests/src/headless.cpp | 4 +- cvk/tests/src/headless.hpp | 2 +- cvk/tests/src/surface/surface_test.cpp | 74 ++++++--------- cvk/tests/src/surface/swapchain_test.cpp | 90 +++++++++++++++++- 10 files changed, 272 insertions(+), 188 deletions(-) delete mode 100644 cvk/incl/cvk/surface/surface_config.hpp diff --git a/cvk/incl/cvk/surface/surface.hpp b/cvk/incl/cvk/surface/surface.hpp index 33eaaf6a..227b785f 100644 --- a/cvk/incl/cvk/surface/surface.hpp +++ b/cvk/incl/cvk/surface/surface.hpp @@ -1,6 +1,4 @@ #pragma once -#include "surface_config.hpp" - #include "cvk/utility/handle/instance_handle.hpp" #include "cvk/utility/types.hpp" @@ -22,24 +20,21 @@ class PhysicalDevice; class Surface { public: using vk_handle_t = VkSurfaceKHR; - using Config = SurfaceConfig; - using format_t = Config::format_t; - using present_mode_t = Config::present_mode_t; struct State; /** * @brief base constructor * @pre @ref Instance::created() */ - explicit Surface(Instance const&, Config = {}); + explicit Surface(Instance const&); /** * @brief constructs and wraps * @details calls: - * - @ref Surface(Instance const&, Config) + * - @ref Surface(Instance const&) * - @ref wrap() */ - Surface(Instance const&, Config, State const&); + Surface(Instance const&, State const&); /** * @note calls @ref optDestroy() @@ -77,10 +72,10 @@ class Surface { [[nodiscard]] bool supportsFamily(PhysicalDevice const&, uint32_t familyIndex) const; /** - * @return allowed and available present modes, in the order of @ref Config::allowedPresentModes + * @return present modes the physical device offers for the surface * @throws cvk::vk_result_exception result of vkGetPhysicalDeviceSurfacePresentModesKHR() */ - [[nodiscard]] std::vector presentModes(PhysicalDevice const&) const; + [[nodiscard]] std::vector presentModes(PhysicalDevice const&) const; /** * @throws cvk::vk_result_exception result of vkGetPhysicalDeviceSurfaceCapabilitiesKHR() @@ -88,34 +83,20 @@ class Surface { [[nodiscard]] VkSurfaceCapabilitiesKHR capabilities(PhysicalDevice const&) const; /** - * @return allowed and available formats, in the order of @ref Config::allowedSurfaceFormats + * @return formats the physical device offers for the surface * @attention no format -> the surface is not usable with the physical device * @throws cvk::vk_result_exception result of vkGetPhysicalDeviceSurfaceFormatsKHR() */ - [[nodiscard]] std::vector formats(PhysicalDevice const&) const; - - /** - * @return the most preferred allowed and available format - * @throws cvk::cvk_exception reason: no allowed format available - */ - [[nodiscard]] format_t format(PhysicalDevice const&) const; - - /** - * @return the most preferred allowed and available present mode - * @throws cvk::cvk_exception reason: no allowed present mode available - */ - [[nodiscard]] present_mode_t presentMode(PhysicalDevice const&) const; + [[nodiscard]] std::vector formats(PhysicalDevice const&) const; static void destroy(vk_not_null, VkSurfaceKHR); [[nodiscard]] constexpr bool created() const { return static_cast(_handle); } [[nodiscard]] constexpr vk_handle_t get() const { return _handle.get(); } - [[nodiscard]] constexpr Config const& config() const { return _config; } private: void forget(); - Config _config; instance_handle _handle; public: diff --git a/cvk/incl/cvk/surface/surface_config.hpp b/cvk/incl/cvk/surface/surface_config.hpp deleted file mode 100644 index 6bd030f1..00000000 --- a/cvk/incl/cvk/surface/surface_config.hpp +++ /dev/null @@ -1,28 +0,0 @@ -#pragma once -#include - -#include -#include - -namespace cvk { -struct SurfaceConfig { - using format_t = VkSurfaceFormatKHR; - using present_mode_t = VkPresentModeKHR; - static constexpr std::array DEFAULT_ALLOWED_FORMATS{ - {{VK_FORMAT_B8G8R8A8_SRGB, VK_COLOR_SPACE_SRGB_NONLINEAR_KHR}} - }; - static constexpr std::array DEFAULT_ALLOWED_PRESENT_MODES{{VK_PRESENT_MODE_FIFO_KHR}}; - - /** - * @brief formats the surface allows, the first is the most preferred - * @details empty -> every available format - */ - std::vector allowedSurfaceFormats{std::from_range, DEFAULT_ALLOWED_FORMATS}; - - /** - * @brief present modes the surface allows, the first is the most preferred - * @details empty -> every available present mode - */ - std::vector allowedPresentModes{std::from_range, DEFAULT_ALLOWED_PRESENT_MODES}; -}; -} diff --git a/cvk/incl/cvk/surface/swapchain.hpp b/cvk/incl/cvk/surface/swapchain.hpp index 7769943e..86bb6fc0 100644 --- a/cvk/incl/cvk/surface/swapchain.hpp +++ b/cvk/incl/cvk/surface/swapchain.hpp @@ -18,6 +18,7 @@ #include #include #include +#include #include @@ -28,7 +29,8 @@ class Surface; /** * @brief VkSwapchainKHR wrapper, owns its images, their views, a present semaphore and a present fence per image * @details the images are wrapped without memory and released before the swapchain is destroyed, - * @ref resize() retires the old swapchain, the swapchain destroys it once its present fences signal, + * @ref resize() and @ref recreate() retire the old swapchain, + * the swapchain destroys it once its present fences signal, * custom move assignment: destroys first, a default one would destroy the swapchain images * @attention not thread safe */ @@ -87,6 +89,7 @@ class Swapchain { /** * @brief wraps the state, gets the images, creates their views, present semaphores and present fences + * @details the format and the present mode come from the state, not from the config * @pre the swapchain was created for the surface with the config and the state * @post @ref created() * @note calls @ref optDestroy() @@ -95,11 +98,13 @@ class Swapchain { void wrap(State const&); /** - * @brief creates the swapchain with the format and present mode of the surface + * @brief picks the format and the present mode of the config, creates the swapchain * @param extent of the window, a fixed currentExtent of the surface wins + * @details the first entries of @ref Config::formats and @ref Config::presentModes that the surface offers win * @pre extent width and height > 0 * @post @ref created() * @note calls @ref optDestroy() + * @throws cvk::cvk_exception reason: the surface offers no allowed format or present mode * @throws cvk::vk_result_exception result of vkCreateSwapchainKHR() */ void create(VkExtent2D extent); @@ -107,15 +112,31 @@ class Swapchain { /** * @brief creates a new swapchain with the old one as oldSwapchain, the old one retires * @param extent of the window, a fixed currentExtent of the surface wins - * @details unacquires the images that are still acquired, - * the swapchain destroys the retired one once its present fences signal + * @details keeps the format and the present mode, queries the surface capabilities again, + * unacquires the images that are still acquired, + * the swapchain destroys the retired one once its present fences signal, + * a format or present mode change needs @ref recreate() * @pre @ref created() * @pre extent width and height > 0 + * @pre the surface still supports the format and the present mode of the swapchain * @pre no present semaphore is signaled without its present * @throws cvk::vk_result_exception result of vkCreateSwapchainKHR() */ void resize(VkExtent2D extent); + /** + * @brief applies the config, e.g. another present mode, retires the old swapchain like @ref resize() + * @param extent of the window, a fixed currentExtent of the surface wins + * @details picks the format and the present mode of the config again, see @ref create() + * @pre @ref created() + * @pre extent width and height > 0 + * @pre no present semaphore is signaled without its present + * @note a new format needs new pipelines for the swapchain images + * @throws cvk::cvk_exception reason: the surface offers no allowed format or present mode, nothing changes + * @throws cvk::vk_result_exception result of vkCreateSwapchainKHR() + */ + void recreate(VkExtent2D extent, Config); + /** * @brief waits for the present fences, destroys the swapchain and forgets * @details releases the images, destroys the views, present semaphores, present fences and retired swapchains @@ -222,7 +243,7 @@ class Swapchain { private: /** - * @brief old swapchain after @ref resize(), the images are released + * @brief old swapchain after @ref resize() or @ref recreate(), the images are released * @details members destroy in reverse order: the swapchain last */ struct Retired { @@ -233,9 +254,24 @@ class Swapchain { std::vector presentFences; }; + /** + * @return the first allowed format and present mode of the config that the surface offers + * @throws cvk::cvk_exception reason: the surface offers no allowed format or present mode + */ + [[nodiscard]] std::pair pick(Config const&) const; + + /** + * @brief creates the swapchain and its images with @ref _surfaceFormat and @ref _presentMode + */ void createSwapchain(VkExtent2D extent, VkSwapchainKHR oldSwapchain); void createImages(); + /** + * @brief unacquires the acquired images, moves the swapchain and its images to the retired swapchains + * @return the handle of the retired swapchain + */ + [[nodiscard]] VkSwapchainKHR retire(); + /** * @brief waits for the present fences, releases the images, destroys the views, semaphores and fences */ diff --git a/cvk/incl/cvk/surface/swapchain_config.hpp b/cvk/incl/cvk/surface/swapchain_config.hpp index 231c7e26..22db5e5b 100644 --- a/cvk/incl/cvk/surface/swapchain_config.hpp +++ b/cvk/incl/cvk/surface/swapchain_config.hpp @@ -11,9 +11,25 @@ namespace cvk { /** - * @brief image count, usage and sharing of a swapchain, the surface decides format and present mode + * @brief allowed formats and present modes, image count, usage and sharing of a swapchain + * @details @ref formats and @ref presentModes are preferences, one entry -> exactly this entry or a throw */ struct SwapchainConfig { + /** + * @brief allowed formats, the first one the surface offers wins + * @details empty -> the first format of the surface + */ + std::vector formats{ + {VK_FORMAT_B8G8R8A8_SRGB, VK_COLOR_SPACE_SRGB_NONLINEAR_KHR}, + {VK_FORMAT_R8G8B8A8_SRGB, VK_COLOR_SPACE_SRGB_NONLINEAR_KHR}, + }; + + /** + * @brief allowed present modes, the first one the surface offers wins + * @details empty -> VK_PRESENT_MODE_FIFO_KHR (always available) + */ + std::vector presentModes{VK_PRESENT_MODE_FIFO_KHR}; + /** * @brief 0 -> minImageCount of the surface + 1, clamped to the limits of the surface */ diff --git a/cvk/src/surface/surface.cpp b/cvk/src/surface/surface.cpp index be56fd36..a138a0f5 100644 --- a/cvk/src/surface/surface.cpp +++ b/cvk/src/surface/surface.cpp @@ -3,34 +3,14 @@ #include "cvk/base/instance.hpp" #include "cvk/base/physical_device.hpp" #include "cvk/utility/vk_exceptions.hpp" -#include "cvk/utility/vk_overloads.hpp" - -#include namespace cvk { using std::vector; -namespace { - /** - * @return @ref allowed in its order, filtered by @ref available, empty @ref allowed -> @ref available - */ - template - [[nodiscard]] vector filter_allowed(vector available, std::span allowed) { - if(allowed.empty()) return available; - - vector result{}; - for(auto const& value : allowed) - if(std::ranges::contains(available, value)) result.push_back(value); - return result; - } -} - -Surface::Surface(Instance const& instance, Config config) : _config{std::move(config)}, _handle{instance.get()} {} +Surface::Surface(Instance const& instance) : _handle{instance.get()} {} -Surface::Surface(Instance const& instance, Config config, State const& state) : Surface{instance, std::move(config)} { - wrap(state); -} +Surface::Surface(Instance const& instance, State const& state) : Surface{instance} { wrap(state); } void Surface::wrap(State const& state) { debug_check_handle(state.vkSurface); @@ -70,7 +50,7 @@ bool Surface::supportsFamily(PhysicalDevice const& physicalDevice, uint32_t fami return support; } -auto Surface::presentModes(PhysicalDevice const& physicalDevice) const -> vector { +vector Surface::presentModes(PhysicalDevice const& physicalDevice) const { debug_check(*this); uint32_t size = 0; @@ -82,7 +62,7 @@ auto Surface::presentModes(PhysicalDevice const& physicalDevice) const -> vector ); CVK_RESULT_STABLE_THROW(countResult != VK_SUCCESS, countResult, "device-surface present modes query failed") {} - vector modes(size); + vector modes(size); auto const writeResult = vkGetPhysicalDeviceSurfacePresentModesKHR( physicalDevice.get(), _handle.get(), @@ -91,10 +71,10 @@ auto Surface::presentModes(PhysicalDevice const& physicalDevice) const -> vector ); CVK_RESULT_STABLE_THROW(writeResult != VK_SUCCESS, writeResult, "device-surface present modes query failed") {} - return filter_allowed(std::move(modes), _config.allowedPresentModes); + return modes; } -auto Surface::formats(PhysicalDevice const& physicalDevice) const -> vector { +vector Surface::formats(PhysicalDevice const& physicalDevice) const { debug_check(*this); uint32_t size = 0; @@ -106,7 +86,7 @@ auto Surface::formats(PhysicalDevice const& physicalDevice) const -> vector formats(size); + vector formats(size); auto const writeResult = vkGetPhysicalDeviceSurfaceFormatsKHR( physicalDevice.get(), _handle.get(), @@ -115,17 +95,7 @@ auto Surface::formats(PhysicalDevice const& physicalDevice) const -> vector(std::move(formats), _config.allowedSurfaceFormats); -} - -auto Surface::format(PhysicalDevice const& physicalDevice) const -> format_t { - auto const availableFormats = formats(physicalDevice); - CVK_STABLE_THROW( - availableFormats.empty(), - "no suitable format found, allowed: {}", - _config.allowedSurfaceFormats - ) {} - return availableFormats[0]; + return formats; } VkSurfaceCapabilitiesKHR Surface::capabilities(PhysicalDevice const& physicalDevice) const { @@ -142,16 +112,6 @@ VkSurfaceCapabilitiesKHR Surface::capabilities(PhysicalDevice const& physicalDev return capabilities; } -auto Surface::presentMode(PhysicalDevice const& physicalDevice) const -> present_mode_t { - auto const availableModes = presentModes(physicalDevice); - CVK_STABLE_THROW( - availableModes.empty(), - "no suitable present mode found, allowed: {}", - _config.allowedPresentModes - ) {} - return availableModes[0]; -} - void Surface::destroy(vk_not_null vkInstance, VkSurfaceKHR vkSurface) { CTH_WARN(vkSurface == VK_NULL_HANDLE, "vkSurface invalid (VK_NULL_HANDLE)") {} diff --git a/cvk/src/surface/swapchain.cpp b/cvk/src/surface/swapchain.cpp index 9f671004..35865371 100644 --- a/cvk/src/surface/swapchain.cpp +++ b/cvk/src/surface/swapchain.cpp @@ -4,6 +4,7 @@ #include "cvk/base/physical_device.hpp" #include "cvk/surface/surface.hpp" #include "cvk/utility/vk_exceptions.hpp" +#include "cvk/utility/vk_overloads.hpp" #include @@ -48,39 +49,34 @@ void Swapchain::wrap(State const& state) { } void Swapchain::create(VkExtent2D extent) { + CTH_CRITICAL(extent.width == 0 || extent.height == 0, "extent width and height must be > 0, got {}", extent) {} optDestroy(); + auto const [surfaceFormat, presentMode] = pick(_config); + _surfaceFormat = surfaceFormat; + _presentMode = presentMode; createSwapchain(extent, VK_NULL_HANDLE); } void Swapchain::resize(VkExtent2D extent) { debug_check(*this); + CTH_CRITICAL(extent.width == 0 || extent.height == 0, "extent width and height must be > 0, got {}", extent) {} - vector acquiredImages{}; - for(uint32_t index = 0; index < imageCount(); ++index) - if(_acquired[index]) acquiredImages.push_back(index); - unacquire(acquiredImages); + auto const oldSwapchain = retire(); + createSwapchain(extent, oldSwapchain); +} - // the swapchain destroys its images - for(auto& image : _images) - image.release(); +void Swapchain::recreate(VkExtent2D extent, Config config) { + debug_check(*this); + CTH_CRITICAL(extent.width == 0 || extent.height == 0, "extent width and height must be > 0, got {}", extent) {} + Config::debug_check(config); - auto const oldSwapchain = get(); - _retired.push_back( - Retired{ - .handle = std::move(_handle), - .images = std::move(_images), - .views = std::move(_views), - .presentSemaphores = std::move(_presentSemaphores), - .presentFences = std::move(_presentFences), - } - ); - _images.clear(); - _views.clear(); - _presentSemaphores.clear(); - _presentFences.clear(); - _acquired.clear(); + auto const [surfaceFormat, presentMode] = pick(config); + auto const oldSwapchain = retire(); + _config = std::move(config); + _surfaceFormat = surfaceFormat; + _presentMode = presentMode; createSwapchain(extent, oldSwapchain); } @@ -204,20 +200,44 @@ void Swapchain::destroy(DeviceTable table, VkSwapchainKHR vkSwapchain) { table->vkDestroySwapchainKHR(table.device(), vkSwapchain, nullptr); } +std::pair Swapchain::pick(Config const& config) const { + auto const& physicalDevice = core().physicalDevice(); + + auto const formats = _surface->formats(physicalDevice); + auto const& allowedFormats = config.formats.empty() ? formats : config.formats; + auto const formatIt = std::ranges::find_first_of(allowedFormats, formats); + CVK_STABLE_THROW( + formatIt == allowedFormats.end(), + "the surface offers no allowed format, allowed: {}, offered: {}", + config.formats, + formats + ) {} + + if(config.presentModes.empty()) return {*formatIt, VK_PRESENT_MODE_FIFO_KHR}; + + auto const presentModes = _surface->presentModes(physicalDevice); + auto const presentModeIt = std::ranges::find_first_of(config.presentModes, presentModes); + CVK_STABLE_THROW( + presentModeIt == config.presentModes.end(), + "the surface offers no allowed present mode, allowed: {}, offered: {}", + config.presentModes, + presentModes + ) {} + + return {*formatIt, *presentModeIt}; +} + void Swapchain::createSwapchain(VkExtent2D extent, VkSwapchainKHR oldSwapchain) { Surface::debug_check(*_surface); - CTH_CRITICAL(extent.width == 0 || extent.height == 0, "extent width and height must be > 0") {} - auto const& physicalDevice = core().physicalDevice(); - auto const capabilities = _surface->capabilities(physicalDevice); - auto const surfaceFormat = _surface->format(physicalDevice); - auto const presentMode = _surface->presentMode(physicalDevice); + auto const capabilities = _surface->capabilities(core().physicalDevice()); auto const imageExtent = evalExtent(extent, capabilities); auto const info = _config.createInfo( _surface->get(), - surfaceFormat, - presentMode, + _surfaceFormat, + _presentMode, + //TODO override of the pre transform, for pre-rotation on rotated mobile displays capabilities.currentTransform, imageExtent, evalImageCount(_config.minImageCount, capabilities), @@ -234,8 +254,6 @@ void Swapchain::createSwapchain(VkExtent2D extent, VkSwapchainKHR oldSwapchain) _handle.reset(ptr, core()); _extent = imageExtent; - _surfaceFormat = surfaceFormat; - _presentMode = presentMode; createImages(); } @@ -275,6 +293,35 @@ void Swapchain::createImages() { _acquired.assign(count, false); } +VkSwapchainKHR Swapchain::retire() { + vector acquiredImages{}; + for(uint32_t index = 0; index < imageCount(); ++index) + if(_acquired[index]) acquiredImages.push_back(index); + unacquire(acquiredImages); + + // the swapchain destroys its images + for(auto& image : _images) + image.release(); + + auto const oldSwapchain = get(); + _retired.push_back( + Retired{ + .handle = std::move(_handle), + .images = std::move(_images), + .views = std::move(_views), + .presentSemaphores = std::move(_presentSemaphores), + .presentFences = std::move(_presentFences), + } + ); + _images.clear(); + _views.clear(); + _presentSemaphores.clear(); + _presentFences.clear(); + _acquired.clear(); + + return oldSwapchain; +} + void Swapchain::destroyImages() { for(auto const& fence : _presentFences) fence.wait(); diff --git a/cvk/tests/src/headless.cpp b/cvk/tests/src/headless.cpp index 0f75b859..15f5ea0f 100644 --- a/cvk/tests/src/headless.cpp +++ b/cvk/tests/src/headless.cpp @@ -18,7 +18,7 @@ std::vector headless_extensions() { return extensions; } -Surface headless_surface(Instance const& instance, Surface::Config config) { +Surface headless_surface(Instance const& instance) { VkHeadlessSurfaceCreateInfoEXT const createInfo{ .sType = VK_STRUCTURE_TYPE_HEADLESS_SURFACE_CREATE_INFO_EXT, .pNext = nullptr, @@ -29,7 +29,7 @@ Surface headless_surface(Instance const& instance, Surface::Config config) { auto const result = vkCreateHeadlessSurfaceEXT(instance.get(), &createInfo, nullptr, &vkSurface); CTH_STABLE_THROW(result != VK_SUCCESS, "failed to create headless surface") {} - return Surface{instance, std::move(config), Surface::State{vkSurface}}; + return Surface{instance, Surface::State{vkSurface}}; } PhysicalDevicePick pick( diff --git a/cvk/tests/src/headless.hpp b/cvk/tests/src/headless.hpp index 7b0b3bd5..e92f14e1 100644 --- a/cvk/tests/src/headless.hpp +++ b/cvk/tests/src/headless.hpp @@ -18,7 +18,7 @@ namespace cvk::test { * @brief creates a headless surface * @pre the instance enables @ref headless_extensions() */ -[[nodiscard]] Surface headless_surface(Instance const&, Surface::Config = {}); +[[nodiscard]] Surface headless_surface(Instance const&); /** * @brief @ref pick_physical_device() with @ref constants::BASE_DEVICE_FEATURES diff --git a/cvk/tests/src/surface/surface_test.cpp b/cvk/tests/src/surface/surface_test.cpp index 58657ef9..52f086bd 100644 --- a/cvk/tests/src/surface/surface_test.cpp +++ b/cvk/tests/src/surface/surface_test.cpp @@ -1,3 +1,4 @@ +#include "context.hpp" #include "headless.hpp" #include "test.hpp" #include "validation.hpp" @@ -6,8 +7,9 @@ #include "cvk/base/physical_device.hpp" #include "cvk/surface/surface.hpp" #include "cvk/utility/constants.hpp" -#include "cvk/utility/vk_exceptions.hpp" +#include "cvk/utility/vk_overloads.hpp" +#include #include #include @@ -15,11 +17,6 @@ namespace cvk { namespace { constexpr std::array PRESENT_QUEUES{QueueFamilyProperties::GRAPHICS | QueueFamilyProperties::PRESENT}; - - /** - * @brief config that allows everything - */ - Surface::Config const ALLOW_ALL{.allowedSurfaceFormats = {}, .allowedPresentModes = {}}; } CVK_TEST(Surface, wrap_takes_ownership) { @@ -44,52 +41,39 @@ CVK_TEST(Surface, release_keeps_the_surface) { Surface::destroy(instance.get(), state.vkSurface.get()); } -CVK_TEST(Surface, empty_config_allows_everything) { - Instance const instance{test::instance_config(test::headless_extensions()), create}; - std::array const surfaces{test::headless_surface(instance, ALLOW_ALL)}; - auto const [device, _] = test::pick(instance, PRESENT_QUEUES, surfaces); - - auto const& surface = surfaces[0]; - - EXPECT_FALSE(surface.formats(device).empty()); - EXPECT_FALSE(surface.presentModes(device).empty()); - EXPECT_TRUE(std::ranges::contains(surface.presentModes(device), VK_PRESENT_MODE_FIFO_KHR)); -} - -CVK_TEST(Surface, formats_keep_the_allowed_order) { - Instance const instance{test::instance_config(test::headless_extensions()), create}; - std::array const surfaces{test::headless_surface(instance, ALLOW_ALL)}; - auto const [device, _] = test::pick(instance, PRESENT_QUEUES, surfaces); +CVK_TEST(Surface, formats_are_every_offered_format) { + test::PresentContext const context{}; + auto const vkPhysicalDevice = context.core.physicalDevice().get(); + auto const vkSurface = context.surface().get(); - auto const available = surfaces[0].formats(device); - if(available.size() < 2) GTEST_SKIP() << "needs two surface formats"; + uint32_t count = 0; + ASSERT_EQ(vkGetPhysicalDeviceSurfaceFormatsKHR(vkPhysicalDevice, vkSurface, &count, nullptr), VK_SUCCESS); + std::vector offered(count); + ASSERT_EQ(vkGetPhysicalDeviceSurfaceFormatsKHR(vkPhysicalDevice, vkSurface, &count, offered.data()), VK_SUCCESS); - Surface::Config const config{ - .allowedSurfaceFormats = {available[1], available[0]}, - .allowedPresentModes = {}, - }; - auto const surface = test::headless_surface(instance, config); + auto const formats = context.surface().formats(context.core.physicalDevice()); - auto const formats = surface.formats(device); - ASSERT_EQ(formats.size(), 2); - EXPECT_EQ(formats[0], available[1]); - EXPECT_EQ(formats[1], available[0]); - EXPECT_EQ(surface.format(device), available[1]); + EXPECT_FALSE(formats.empty()); + EXPECT_EQ(formats, offered); } -CVK_TEST(Surface, no_allowed_format_throws) { - Instance const instance{test::instance_config(test::headless_extensions()), create}; - std::array const surfaces{test::headless_surface(instance, ALLOW_ALL)}; - auto const [device, _] = test::pick(instance, PRESENT_QUEUES, surfaces); +CVK_TEST(Surface, present_modes_are_every_offered_present_mode) { + test::PresentContext const context{}; + auto const vkPhysicalDevice = context.core.physicalDevice().get(); + auto const vkSurface = context.surface().get(); + + uint32_t count = 0; + ASSERT_EQ(vkGetPhysicalDeviceSurfacePresentModesKHR(vkPhysicalDevice, vkSurface, &count, nullptr), VK_SUCCESS); + std::vector offered(count); + ASSERT_EQ( + vkGetPhysicalDeviceSurfacePresentModesKHR(vkPhysicalDevice, vkSurface, &count, offered.data()), + VK_SUCCESS + ); - Surface::Config const config{ - .allowedSurfaceFormats = {{VK_FORMAT_R64G64B64A64_SFLOAT, VK_COLOR_SPACE_SRGB_NONLINEAR_KHR}}, - .allowedPresentModes = {}, - }; - auto const surface = test::headless_surface(instance, config); + auto const presentModes = context.surface().presentModes(context.core.physicalDevice()); - EXPECT_TRUE(surface.formats(device).empty()); - EXPECT_THROW([[maybe_unused]] auto const f = surface.format(device), cvk_exception); + EXPECT_TRUE(std::ranges::contains(presentModes, VK_PRESENT_MODE_FIFO_KHR)); + EXPECT_EQ(presentModes, offered); } CVK_TEST(Surface, capabilities) { diff --git a/cvk/tests/src/surface/swapchain_test.cpp b/cvk/tests/src/surface/swapchain_test.cpp index dae98805..558320a7 100644 --- a/cvk/tests/src/surface/swapchain_test.cpp +++ b/cvk/tests/src/surface/swapchain_test.cpp @@ -10,10 +10,13 @@ #include "cvk/render/sync/semaphore.hpp" #include "cvk/surface/surface.hpp" #include "cvk/surface/swapchain.hpp" +#include "cvk/utility/vk_exceptions.hpp" +#include #include #include #include +#include namespace cvk { @@ -137,7 +140,8 @@ CVK_TEST(Swapchain, create_and_destroy) { EXPECT_GE(swapchain.imageCount(), capabilities.minImageCount); EXPECT_EQ(swapchain.views().size(), swapchain.imageCount()); EXPECT_EQ(swapchain.extent().width, Swapchain::evalExtent(EXTENT, capabilities).width); - EXPECT_EQ(swapchain.format(), context.surface().format(context.core.physicalDevice()).format); + auto const offered = context.surface().formats(context.core.physicalDevice()); + EXPECT_TRUE(std::ranges::contains(offered, VkSurfaceFormatKHR{swapchain.format(), swapchain.colorSpace()})); EXPECT_EQ(swapchain.images()[0].aspectMask(), VK_IMAGE_ASPECT_COLOR_BIT); swapchain.destroy(); @@ -145,6 +149,50 @@ CVK_TEST(Swapchain, create_and_destroy) { EXPECT_EQ(swapchain.imageCount(), 0); } +CVK_TEST(Swapchain, create_picks_the_first_allowed_format_the_surface_offers) { + test::PresentContext const context{}; + auto const offered = context.surface().formats(context.core.physicalDevice()); + if(offered.size() < 2) GTEST_SKIP() << "needs two surface formats"; + + Swapchain::Config const config{ + .formats = {{VK_FORMAT_R64G64B64A64_SFLOAT, VK_COLOR_SPACE_SRGB_NONLINEAR_KHR}, offered[1], offered[0]}, + }; + Swapchain const swapchain{context.core, context.queue(), context.surface(), config, EXTENT}; + + EXPECT_EQ(swapchain.format(), offered[1].format); + EXPECT_EQ(swapchain.colorSpace(), offered[1].colorSpace); +} + +CVK_TEST(Swapchain, create_without_an_offered_format_throws) { + test::PresentContext const context{}; + Swapchain::Config const config{.formats = {{VK_FORMAT_R64G64B64A64_SFLOAT, VK_COLOR_SPACE_SRGB_NONLINEAR_KHR}}}; + Swapchain swapchain{context.core, context.queue(), context.surface(), config}; + + EXPECT_THROW(swapchain.create(EXTENT), cvk_exception); + EXPECT_FALSE(swapchain.created()); +} + +CVK_TEST(Swapchain, create_without_an_offered_present_mode_throws) { + test::PresentContext const context{}; + Swapchain::Config const config{.presentModes = {VK_PRESENT_MODE_SHARED_DEMAND_REFRESH_KHR}}; + Swapchain swapchain{context.core, context.queue(), context.surface(), config}; + + EXPECT_THROW(swapchain.create(EXTENT), cvk_exception); + EXPECT_FALSE(swapchain.created()); +} + +CVK_TEST(Swapchain, empty_lists_take_the_first_format_and_fifo) { + test::PresentContext const context{}; + auto const offered = context.surface().formats(context.core.physicalDevice()); + + Swapchain::Config const config{.formats = {}, .presentModes = {}}; + Swapchain const swapchain{context.core, context.queue(), context.surface(), config, EXTENT}; + + EXPECT_EQ(swapchain.format(), offered[0].format); + EXPECT_EQ(swapchain.colorSpace(), offered[0].colorSpace); + EXPECT_EQ(swapchain.presentMode(), VK_PRESENT_MODE_FIFO_KHR); +} + CVK_TEST(Swapchain, acquire_clear_present) { test::PresentContext const context{}; Swapchain swapchain{context.core, context.queue(), context.surface(), {}, EXTENT}; @@ -167,6 +215,46 @@ CVK_TEST(Swapchain, resize_retires_the_old_swapchain) { EXPECT_EQ(present_cleared_image(context, swapchain), VK_SUCCESS); } +CVK_TEST(Swapchain, resize_keeps_the_format_and_the_present_mode) { + test::PresentContext const context{}; + auto const offered = context.surface().formats(context.core.physicalDevice()); + if(offered.size() < 2) GTEST_SKIP() << "needs two surface formats"; + + Swapchain created{context.core, context.queue(), context.surface(), {.formats = {offered[1]}}, EXTENT}; + auto const state = created.release(); + // the config prefers another format -> a resize that picks again changes the format + Swapchain swapchain{context.core, context.queue(), context.surface(), {.formats = {offered[0]}}, state}; + + swapchain.resize({32, 32}); + + EXPECT_EQ(swapchain.format(), offered[1].format); + EXPECT_EQ(swapchain.colorSpace(), offered[1].colorSpace); + EXPECT_EQ(swapchain.presentMode(), state.presentMode); + EXPECT_EQ(present_cleared_image(context, swapchain), VK_SUCCESS); +} + +CVK_TEST(Swapchain, recreate_applies_the_config) { + test::PresentContext const context{}; + auto const& physicalDevice = context.core.physicalDevice(); + auto const formats = context.surface().formats(physicalDevice); + auto const presentModes = context.surface().presentModes(physicalDevice); + if(formats.size() < 2 || presentModes.size() < 2) GTEST_SKIP() << "needs two surface formats and present modes"; + + Swapchain::Config const config{.formats = {formats[0]}, .presentModes = {presentModes[0]}}; + Swapchain swapchain{context.core, context.queue(), context.surface(), config, EXTENT}; + EXPECT_EQ(present_cleared_image(context, swapchain), VK_SUCCESS); + + swapchain.recreate({32, 32}, {.formats = {formats[1]}, .presentModes = {presentModes[1]}}); + + EXPECT_EQ(swapchain.format(), formats[1].format); + EXPECT_EQ(swapchain.colorSpace(), formats[1].colorSpace); + EXPECT_EQ(swapchain.presentMode(), presentModes[1]); + EXPECT_EQ(swapchain.config().presentModes, std::vector{presentModes[1]}); + EXPECT_EQ(swapchain.retired(), 1); + EXPECT_EQ(swapchain.extent().width, 32); + EXPECT_EQ(present_cleared_image(context, swapchain), VK_SUCCESS); +} + CVK_TEST(Swapchain, unacquire_gives_the_image_back) { test::PresentContext const context{}; Swapchain swapchain{context.core, context.queue(), context.surface(), {}, EXTENT}; From 7ee758138fe261d11bd884c6e0a4e92e420b0a68 Mon Sep 17 00:00:00 2001 From: lukas <89145039+codethinki@users.noreply.github.com> Date: Wed, 21 Oct 2026 12:30:30 +0200 Subject: [PATCH 107/135] cvk tests: a failed recreate keeps the swapchain --- cvk/tests/src/surface/swapchain_test.cpp | 25 ++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/cvk/tests/src/surface/swapchain_test.cpp b/cvk/tests/src/surface/swapchain_test.cpp index 558320a7..4ae937e5 100644 --- a/cvk/tests/src/surface/swapchain_test.cpp +++ b/cvk/tests/src/surface/swapchain_test.cpp @@ -255,6 +255,31 @@ CVK_TEST(Swapchain, recreate_applies_the_config) { EXPECT_EQ(present_cleared_image(context, swapchain), VK_SUCCESS); } +CVK_TEST(Swapchain, recreate_without_an_offered_format_throws_and_keeps_the_swapchain) { + test::PresentContext const context{}; + Swapchain::Config const config{}; + Swapchain swapchain{context.core, context.queue(), context.surface(), config, EXTENT}; + auto const vkSwapchain = swapchain.get(); + auto const extent = swapchain.extent(); + auto const format = swapchain.format(); + auto const colorSpace = swapchain.colorSpace(); + auto const presentMode = swapchain.presentMode(); + + Swapchain::Config const unoffered{.formats = {{VK_FORMAT_R64G64B64A64_SFLOAT, VK_COLOR_SPACE_SRGB_NONLINEAR_KHR}}}; + EXPECT_THROW(swapchain.recreate({32, 32}, unoffered), cvk_exception); + + ASSERT_TRUE(swapchain.created()); + EXPECT_EQ(swapchain.get(), vkSwapchain); + EXPECT_EQ(swapchain.extent(), extent); + EXPECT_EQ(swapchain.format(), format); + EXPECT_EQ(swapchain.colorSpace(), colorSpace); + EXPECT_EQ(swapchain.presentMode(), presentMode); + EXPECT_EQ(swapchain.config().formats, config.formats); + EXPECT_EQ(swapchain.config().presentModes, config.presentModes); + EXPECT_EQ(swapchain.retired(), 0); + EXPECT_EQ(present_cleared_image(context, swapchain), VK_SUCCESS); +} + CVK_TEST(Swapchain, unacquire_gives_the_image_back) { test::PresentContext const context{}; Swapchain swapchain{context.core, context.queue(), context.surface(), {}, EXTENT}; From a251b655e316207b18c0872d10e0485cc6b10f2a Mon Sep 17 00:00:00 2001 From: lukas <89145039+codethinki@users.noreply.github.com> Date: Wed, 21 Oct 2026 12:30:30 +0200 Subject: [PATCH 108/135] cvk: the sampler warns when it clamps maxAnisotropy --- cvk/incl/cvk/data/img/sampler.hpp | 2 +- cvk/src/data/img/sampler.cpp | 10 +++++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/cvk/incl/cvk/data/img/sampler.hpp b/cvk/incl/cvk/data/img/sampler.hpp index 5077c255..49be3996 100644 --- a/cvk/incl/cvk/data/img/sampler.hpp +++ b/cvk/incl/cvk/data/img/sampler.hpp @@ -25,7 +25,7 @@ class Sampler { /** * @brief base constructor - * @details clamps @ref Config::maxAnisotropy to maxSamplerAnisotropy of the device + * @details clamps @ref Config::maxAnisotropy to maxSamplerAnisotropy of the device, warns if it clamps * @pre @ref Core::created() */ Sampler(Core const&, Config const&); diff --git a/cvk/src/data/img/sampler.cpp b/cvk/src/data/img/sampler.cpp index 53eb4f3d..2271c8e8 100644 --- a/cvk/src/data/img/sampler.cpp +++ b/cvk/src/data/img/sampler.cpp @@ -10,7 +10,15 @@ namespace cvk { Sampler::Sampler(Core const& core, Config const& config) : _handle{core}, _config{config} { Core::debug_check(core); - _config.maxAnisotropy = std::min(_config.maxAnisotropy, core.physicalDevice().limits().maxSamplerAnisotropy); + + auto const limit = core.physicalDevice().limits().maxSamplerAnisotropy; + CTH_WARN( + _config.maxAnisotropy > limit, + "maxAnisotropy clamped to the device limit {}, got {}", + limit, + _config.maxAnisotropy + ) {} + _config.maxAnisotropy = std::min(_config.maxAnisotropy, limit); } Sampler::Sampler(Core const& core, Config const& config, State const& state) : Sampler{core, config} { wrap(state); } From 5ef7eff7055ac0fa12793ab4fc4c6844136a5ab0 Mon Sep 17 00:00:00 2001 From: lukas <89145039+codethinki@users.noreply.github.com> Date: Wed, 21 Oct 2026 12:30:30 +0200 Subject: [PATCH 109/135] cvk: Core::Config::device picks a device by uuid, the automatic pick prefers discrete GPUs --- cvk/incl/cvk/base/core.hpp | 4 ++ cvk/incl/cvk/base/core_config.hpp | 8 ++++ cvk/incl/cvk/base/physical_device.hpp | 32 +++++++++++++- cvk/src/base/core.cpp | 35 ++++++++++++++- cvk/src/base/physical_device.cpp | 48 ++++++++++++++++----- cvk/tests/src/base/core_test.cpp | 37 ++++++++++++++++ cvk/tests/src/base/physical_device_test.cpp | 29 +++++++++++++ 7 files changed, 179 insertions(+), 14 deletions(-) diff --git a/cvk/incl/cvk/base/core.hpp b/cvk/incl/cvk/base/core.hpp index cfad8040..17d6279f 100644 --- a/cvk/incl/cvk/base/core.hpp +++ b/cvk/incl/cvk/base/core.hpp @@ -72,9 +72,12 @@ class Core { /** * @brief picks a physical device for the first possible queue set, creates the device, the allocator and the queues * @param surfaces the present queues present to every surface + * @details the first queue set that a physical device supports wins, + * then the best physical device for this set, see @ref pick_physical_device() and @ref Config::device * @post @ref created() * @note calls @ref optDestroy() * @throws cvk::cvk_exception reason: no physical device meets the requirements + * @throws cvk::cvk_exception reason: @ref Config::device is missing or does not meet the requirements * @throws cvk::vk_result_exception result of vkCreateDevice() * @throws cvk::vk_result_exception result of vmaCreateAllocator() */ @@ -187,6 +190,7 @@ class Core { /** * @brief picks a physical device for the first possible queue set * @throws cvk::cvk_exception reason: no physical device meets the requirements + * @throws cvk::cvk_exception reason: @ref Config::device is missing or does not meet the requirements */ [[nodiscard]] device_pick pickPhysicalDevice( std::span surfaces, diff --git a/cvk/incl/cvk/base/core_config.hpp b/cvk/incl/cvk/base/core_config.hpp index 7774133e..e2c4ced4 100644 --- a/cvk/incl/cvk/base/core_config.hpp +++ b/cvk/incl/cvk/base/core_config.hpp @@ -1,4 +1,5 @@ #pragma once +#include "physical_device.hpp" #include "queue/queue_family.hpp" #include "cvk/utility/device/device_features.hpp" @@ -7,6 +8,7 @@ #include #include +#include #include #include @@ -40,6 +42,12 @@ struct CoreConfig { */ std::vector extensions{}; + /** + * @brief std::nullopt -> the best suitable device + * @details set -> exactly this device, see @ref PhysicalDevice::uuid() + */ + std::optional device = std::nullopt; + static constexpr void debug_check(CoreConfig const& config); }; } diff --git a/cvk/incl/cvk/base/physical_device.hpp b/cvk/incl/cvk/base/physical_device.hpp index b367ff8c..7916d2bf 100644 --- a/cvk/incl/cvk/base/physical_device.hpp +++ b/cvk/incl/cvk/base/physical_device.hpp @@ -7,6 +7,8 @@ #include +#include +#include #include #include #include @@ -17,6 +19,8 @@ namespace cvk { class Instance; class Surface; +using device_uuid_t = std::array; + /** * @brief VkPhysicalDevice with its queried extensions, features, properties and queue families * @details copyable, the instance owns the physical device @@ -76,6 +80,11 @@ class PhysicalDevice { [[nodiscard]] constexpr DeviceFeatures const& features() const { return _features; } [[nodiscard]] constexpr VkPhysicalDeviceProperties const& properties() const { return _properties; } [[nodiscard]] constexpr VkPhysicalDeviceLimits const& limits() const { return _properties.limits; } + + /** + * @return deviceUUID of VkPhysicalDeviceIDProperties, the same across instances and processes + */ + [[nodiscard]] constexpr device_uuid_t const& uuid() const { return _uuid; } [[nodiscard]] constexpr VkPhysicalDeviceMemoryProperties const& memoryProperties() const { return _memoryProperties; } @@ -87,6 +96,7 @@ class PhysicalDevice { std::vector _extensions; DeviceFeatures _features; VkPhysicalDeviceProperties _properties; + device_uuid_t _uuid; VkPhysicalDeviceMemoryProperties _memoryProperties; VkSampleCountFlagBits _maxSampleCount; std::vector _queueFamilies; @@ -120,9 +130,11 @@ struct PhysicalDevicePick { ); /** - * @brief picks the first physical device with the features, the extensions and the queues + * @brief picks the best physical device with the features, the extensions and the queues * @param surfaces a family presents only if it presents to every surface * @param extensions to require, besides the extensions of the features + * @param uuid std::nullopt -> every device counts, set -> only the device with this uuid counts + * @details best: the lowest @ref dev::device_type_rank(), ties go to the first in enumeration order * @return std::nullopt -> no device meets the requirements * @pre @ref Instance::created() * @throws cvk::vk_result_exception result of vkEnumeratePhysicalDevices() @@ -132,10 +144,26 @@ struct PhysicalDevicePick { std::span surfaces, std::span queueProperties, DeviceFeatures const& features, - std::span extensions = {} + std::span extensions = {}, + std::optional const& uuid = std::nullopt ); } +namespace cvk::dev { +/** + * @return rank of the device type for the pick, 0 is the best: discrete, integrated, virtual, cpu, other + */ +[[nodiscard]] constexpr uint32_t device_type_rank(VkPhysicalDeviceType type) { + switch(type) { + case VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU: return 0; + case VK_PHYSICAL_DEVICE_TYPE_INTEGRATED_GPU: return 1; + case VK_PHYSICAL_DEVICE_TYPE_VIRTUAL_GPU: return 2; + case VK_PHYSICAL_DEVICE_TYPE_CPU: return 3; + default: return 4; + } +} +} + namespace cvk { constexpr void PhysicalDevice::debug_check(PhysicalDevice const& device) { CTH_CRITICAL(device.get() == VK_NULL_HANDLE, "physical device must not be moved from") {} diff --git a/cvk/src/base/core.cpp b/cvk/src/base/core.cpp index 4c190a37..55f65d4c 100644 --- a/cvk/src/base/core.cpp +++ b/cvk/src/base/core.cpp @@ -8,13 +8,29 @@ #include "cvk/utility/constant/device_constants.hpp" #include "cvk/utility/vk_exceptions.hpp" +#include #include +#include namespace cvk { using std::span; using std::vector; +namespace { + /** + * @return the uuid as hex in the 8-4-4-4-12 form + */ + [[nodiscard]] std::string uuid_string(device_uuid_t const& uuid) { + std::string result{}; + for(auto const [index, byte] : std::views::enumerate(uuid)) { + if(index == 4 || index == 6 || index == 8 || index == 10) result += '-'; + result += std::format("{:02x}", byte); + } + return result; + } +} + Core::Core(Instance const& instance) : _instance{&instance} {} Core::Core(Instance const& instance, span surfaces, Config const& config) : Core{instance} { @@ -164,6 +180,11 @@ auto Core::pickPhysicalDevice( ) const -> device_pick { if(config.queueSets.empty()) { auto pick = tryPickPhysicalDevice(surfaces, config, required, queue_set_t{config.queueProperties.size()}); + CVK_STABLE_THROW( + !pick && config.device.has_value(), + "physical device {} is missing or does not support the queues", + uuid_string(*config.device) + ) {} CVK_STABLE_THROW(!pick, "no physical device supports the queues") {} return *std::move(pick); } @@ -176,6 +197,11 @@ auto Core::pickPhysicalDevice( return *std::move(pick); } + CVK_STABLE_THROW( + config.device.has_value(), + "physical device {} is missing or does not support any queue set", + uuid_string(*config.device) + ) {} CVK_STABLE_THROW(true, "no physical device supports any queue set") {} std::unreachable(); } @@ -214,7 +240,14 @@ auto Core::tryPickPhysicalDevice( auto [uniqueQueues, queueToUniqueQueue] = createQueueSetRelations(queueSet, config.queueProperties); - auto result = pick_physical_device(*_instance, surfaces, uniqueQueues, required.features, required.extensions); + auto result = pick_physical_device( + *_instance, + surfaces, + uniqueQueues, + required.features, + required.extensions, + config.device + ); if(!result) return std::nullopt; return device_pick{ diff --git a/cvk/src/base/physical_device.cpp b/cvk/src/base/physical_device.cpp index 0e147385..571f9605 100644 --- a/cvk/src/base/physical_device.cpp +++ b/cvk/src/base/physical_device.cpp @@ -40,10 +40,25 @@ namespace { }; } - [[nodiscard]] VkPhysicalDeviceProperties query_properties(vk_not_null vkDevice) { - VkPhysicalDeviceProperties properties{}; - vkGetPhysicalDeviceProperties(vkDevice.get(), &properties); - return properties; + struct queried_properties { + VkPhysicalDeviceProperties properties; + device_uuid_t uuid; + }; + + [[nodiscard]] queried_properties query_properties(vk_not_null vkDevice) { + VkPhysicalDeviceIDProperties idProperties{ + .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_ID_PROPERTIES, + .pNext = nullptr, + }; + VkPhysicalDeviceProperties2 properties{ + .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2, + .pNext = &idProperties, + }; + vkGetPhysicalDeviceProperties2(vkDevice.get(), &properties); + + queried_properties result{.properties = properties.properties, .uuid = {}}; + std::ranges::copy(idProperties.deviceUUID, result.uuid.begin()); + return result; } [[nodiscard]] VkPhysicalDeviceMemoryProperties query_memory_properties(vk_not_null vkDevice) { @@ -111,10 +126,13 @@ PhysicalDevice::PhysicalDevice(vk_not_null vkDevice, span PhysicalDevice::missing(DeviceFeatures const& features, span extensions) const { debug_check(*this); @@ -211,15 +229,22 @@ std::optional pick_physical_device( span surfaces, span queueProperties, DeviceFeatures const& features, - span extensions + span extensions, + std::optional const& uuid ) { + std::optional best{}; + for(auto& device : enumerate_physical_devices(instance, surfaces)) { + if(uuid.has_value() && device.uuid() != *uuid) continue; + auto const missing = device.missing(features, extensions); auto indices = device.queueFamilyIndices(queueProperties); if(missing.empty() && !indices.empty()) { - log::msg("chosen physical device: {}", device.properties().deviceName); - return PhysicalDevicePick{std::move(device), std::move(indices)}; + auto const rank = dev::device_type_rank(device.properties().deviceType); + if(!best.has_value() || rank < dev::device_type_rank(best->device.properties().deviceType)) + best = PhysicalDevicePick{std::move(device), std::move(indices)}; + continue; } cth::dt::string_joiner reasons{", "}; @@ -229,6 +254,7 @@ std::optional pick_physical_device( log::msg("physical device {} misses: {}", device.properties().deviceName, std::string{reasons}); } - return std::nullopt; + if(best.has_value()) log::msg("chosen physical device: {}", best->device.properties().deviceName); + return best; } } diff --git a/cvk/tests/src/base/core_test.cpp b/cvk/tests/src/base/core_test.cpp index 64d2f957..1c65040f 100644 --- a/cvk/tests/src/base/core_test.cpp +++ b/cvk/tests/src/base/core_test.cpp @@ -1,3 +1,4 @@ +#include "context.hpp" #include "headless.hpp" #include "test.hpp" #include "validation.hpp" @@ -127,6 +128,42 @@ CVK_TEST(Core, no_suitable_device_throws) { EXPECT_THROW(construct(), cvk_exception); } +CVK_TEST(Core, device_uuid_picks_exactly_this_device) { + test::Context const context{}; + auto config = test::Context::CONFIG; + + // every suitable device, not only the one that the automatic pick takes + for(auto const& device : enumerate_physical_devices(context.instance)) { + bool const suitable = device.missing(constants::BASE_DEVICE_FEATURES).empty() + && !device.queueFamilyIndices(config.queueProperties).empty(); + if(!suitable) continue; + + config.device = device.uuid(); + Core const core{context.instance, {}, config}; + + EXPECT_EQ(core.physicalDevice().uuid(), device.uuid()); + EXPECT_EQ(core.vkPhysicalDevice(), device.get()); + } +} + +CVK_TEST(Core, missing_device_uuid_throws) { + test::Context const context{}; + device_uuid_t unknown{}; + unknown.fill(0xFF); + + Core::Config const withoutSets{.queueProperties = test::Context::CONFIG.queueProperties, .device = unknown}; + Core::Config const withSets{ + .queueProperties = test::Context::CONFIG.queueProperties, + .queueSets = {Core::queue_set_t{1}}, + .device = unknown, + }; + + auto const construct = [&](Core::Config const& config) { Core const core{context.instance, {}, config}; }; + + EXPECT_THROW(construct(withoutSets), cvk_exception); + EXPECT_THROW(construct(withSets), cvk_exception); +} + CVK_TEST(Core, release_and_wrap) { Instance const instance{test::instance_config(), create}; diff --git a/cvk/tests/src/base/physical_device_test.cpp b/cvk/tests/src/base/physical_device_test.cpp index 8da77957..92533a03 100644 --- a/cvk/tests/src/base/physical_device_test.cpp +++ b/cvk/tests/src/base/physical_device_test.cpp @@ -1,3 +1,4 @@ +#include "context.hpp" #include "headless.hpp" #include "test.hpp" @@ -102,6 +103,34 @@ CVK_TEST(PhysicalDevice, copy_keeps_the_queried_members) { EXPECT_TRUE(std::ranges::equal(copy.queueFamilies(), device.queueFamilies())); EXPECT_EQ(copy.maxSampleCount(), device.maxSampleCount()); EXPECT_STREQ(copy.properties().deviceName, device.properties().deviceName); + EXPECT_EQ(copy.uuid(), device.uuid()); +} + +CVK_TEST(PhysicalDevice, uuid_is_the_device_uuid) { + test::Context const context{}; + auto const& device = context.core.physicalDevice(); + + VkPhysicalDeviceIDProperties idProperties{ + .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_ID_PROPERTIES, + .pNext = nullptr, + }; + VkPhysicalDeviceProperties2 properties{ + .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2, + .pNext = &idProperties, + }; + vkGetPhysicalDeviceProperties2(device.get(), &properties); + + EXPECT_TRUE(std::ranges::equal(device.uuid(), idProperties.deviceUUID)); + EXPECT_STREQ(device.properties().deviceName, properties.properties.deviceName); +} + +CVK_TEST(PhysicalDevice, type_rank_prefers_discrete_integrated_virtual_cpu_other) { + constexpr auto rank = dev::device_type_rank; + static_assert(rank(VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU) == 0); + static_assert(rank(VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU) < rank(VK_PHYSICAL_DEVICE_TYPE_INTEGRATED_GPU)); + static_assert(rank(VK_PHYSICAL_DEVICE_TYPE_INTEGRATED_GPU) < rank(VK_PHYSICAL_DEVICE_TYPE_VIRTUAL_GPU)); + static_assert(rank(VK_PHYSICAL_DEVICE_TYPE_VIRTUAL_GPU) < rank(VK_PHYSICAL_DEVICE_TYPE_CPU)); + static_assert(rank(VK_PHYSICAL_DEVICE_TYPE_CPU) < rank(VK_PHYSICAL_DEVICE_TYPE_OTHER)); } CVK_TEST(PhysicalDevice, find_supported_format) { From aa420c2892f511b6e997cf7bb4d618f494c43ab1 Mon Sep 17 00:00:00 2001 From: lukas <89145039+codethinki@users.noreply.github.com> Date: Wed, 21 Oct 2026 12:37:26 +0200 Subject: [PATCH 110/135] cvk: TODO for a reflection based swapchain move assignment --- cvk/incl/cvk/surface/swapchain.hpp | 1 + 1 file changed, 1 insertion(+) diff --git a/cvk/incl/cvk/surface/swapchain.hpp b/cvk/incl/cvk/surface/swapchain.hpp index 86bb6fc0..556c5c4f 100644 --- a/cvk/incl/cvk/surface/swapchain.hpp +++ b/cvk/incl/cvk/surface/swapchain.hpp @@ -310,6 +310,7 @@ class Swapchain { Swapchain(Swapchain&& other) noexcept = default; Swapchain& operator=(Swapchain const& other) = delete; //TODO the custom move assignment is ugly, find a design where the default one works + //TODO generate the member moves with reflection, only optDestroy(), destroyRetired() and forget() stay Swapchain& operator=(Swapchain&& other) noexcept; static constexpr void debug_check(Swapchain const&); From db201e59adaf55f32b08b681dbb29b600875072a Mon Sep 17 00:00:00 2001 From: lukas <89145039+codethinki@users.noreply.github.com> Date: Wed, 21 Oct 2026 12:37:46 +0200 Subject: [PATCH 111/135] cvk: dev::enumerate for the two-call queries, repeats on VK_INCOMPLETE and shrinks to the returned count --- cvk/incl/cvk/base/instance.hpp | 4 + cvk/incl/cvk/utility/enumerate.hpp | 84 ++++++++++++++ cvk/src/base/instance.cpp | 11 +- cvk/src/base/physical_device.cpp | 32 ++---- cvk/src/surface/surface.cpp | 42 +------ cvk/src/surface/swapchain.cpp | 17 ++- cvk/tests/src/utility/enumerate_test.cpp | 133 +++++++++++++++++++++++ 7 files changed, 249 insertions(+), 74 deletions(-) create mode 100644 cvk/incl/cvk/utility/enumerate.hpp create mode 100644 cvk/tests/src/utility/enumerate_test.cpp diff --git a/cvk/incl/cvk/base/instance.hpp b/cvk/incl/cvk/base/instance.hpp index 0eb40a5d..b2cf6901 100644 --- a/cvk/incl/cvk/base/instance.hpp +++ b/cvk/incl/cvk/base/instance.hpp @@ -94,6 +94,8 @@ class Instance { * @note no validation layers -> no messenger * @throws cvk::cvk_exception reason: instance extension missing * @throws cvk::cvk_exception reason: validation layer missing + * @throws cvk::vk_result_exception result of vkEnumerateInstanceExtensionProperties() + * @throws cvk::vk_result_exception result of vkEnumerateInstanceLayerProperties() * @throws cvk::vk_result_exception result of vkCreateInstance() */ void create(std::optional messengerConfig = std::nullopt); @@ -161,12 +163,14 @@ class Instance { /** * @brief queries the instance extensions of the loader and the implicit layers * @pre @ref Cvk::initialized() + * @throws cvk::vk_result_exception result of vkEnumerateInstanceExtensionProperties() */ [[nodiscard]] std::vector available_instance_extensions(); /** * @brief queries the instance layers * @pre @ref Cvk::initialized() + * @throws cvk::vk_result_exception result of vkEnumerateInstanceLayerProperties() */ [[nodiscard]] std::vector available_layers(); } diff --git a/cvk/incl/cvk/utility/enumerate.hpp b/cvk/incl/cvk/utility/enumerate.hpp new file mode 100644 index 00000000..5608afd5 --- /dev/null +++ b/cvk/incl/cvk/utility/enumerate.hpp @@ -0,0 +1,84 @@ +#pragma once +#include "cvk/utility/vk_exceptions.hpp" + +#include + +#include +#include +#include +#include +#include + + +namespace cvk::dev { +/** + * @brief element type of a two-call query: the pointee of its last parameter + */ +template +struct enumerate_element {}; + +template +struct enumerate_element { + using type = std::remove_pointer_t>>; +}; + +template +using enumerate_element_t = enumerate_element::type; + +/** + * @brief max count and write rounds of @ref enumerate() while the result is VK_INCOMPLETE + */ +constexpr uint32_t MAX_QUERY_ATTEMPTS = 4; + +/** + * @brief calls a two-call query as fn(args..., &count, data), first for the count, then for the elements + * @param what name of the elements for the error message + * @param fn function pointer: a volk global or a VolkDeviceTable member + * @param args arguments before the count + * @details VkResult fn -> repeats both calls while the result is VK_INCOMPLETE, + * at most @ref MAX_QUERY_ATTEMPTS rounds, + * void fn -> one count and one write call + * @return the elements, as many as the last call wrote + * @throws cvk::vk_result_exception result of fn other than VK_SUCCESS or VK_INCOMPLETE + * @throws cvk::vk_result_exception VK_INCOMPLETE in all @ref MAX_QUERY_ATTEMPTS rounds + */ +template +[[nodiscard]] std::vector> enumerate( + [[maybe_unused]] std::string_view what, + Fn fn, + Args const&... args +) { + using element_t = enumerate_element_t; + + std::vector elements{}; + uint32_t count = 0; + + if constexpr(std::is_void_v>) { + fn(args..., &count, nullptr); + elements.resize(count); + fn(args..., &count, elements.data()); + } else { + VkResult result = VK_INCOMPLETE; + for(uint32_t attempt = 0; attempt < MAX_QUERY_ATTEMPTS && result == VK_INCOMPLETE; ++attempt) { + auto const countResult = fn(args..., &count, nullptr); + CVK_RESULT_STABLE_THROW(countResult != VK_SUCCESS, countResult, "failed to query {}", what) {} + // data() of an empty vector can be nullptr, a second count call could return a new count + if(count == 0) return {}; + + elements.resize(count); + result = fn(args..., &count, elements.data()); + } + CVK_RESULT_STABLE_THROW( + result == VK_INCOMPLETE, + result, + "failed to query {}, the count changed on every attempt, got {} attempts", + what, + MAX_QUERY_ATTEMPTS + ) {} + CVK_RESULT_STABLE_THROW(result != VK_SUCCESS, result, "failed to query {}", what) {} + } + + elements.resize(count); + return elements; +} +} diff --git a/cvk/src/base/instance.cpp b/cvk/src/base/instance.cpp index 11a4281f..42680d14 100644 --- a/cvk/src/base/instance.cpp +++ b/cvk/src/base/instance.cpp @@ -1,6 +1,7 @@ #include "cvk/base/instance.hpp" #include "cvk/debug/debug_messenger.hpp" +#include "cvk/utility/enumerate.hpp" #include "cvk/utility/vk_exceptions.hpp" #include @@ -161,10 +162,7 @@ void Instance::loadInstanceFunctions(vk_not_null vkInstance) { volkL namespace cvk { vector available_instance_extensions() { - uint32_t count = 0; - vkEnumerateInstanceExtensionProperties(nullptr, &count, nullptr); - vector properties(count); - vkEnumerateInstanceExtensionProperties(nullptr, &count, properties.data()); + auto const properties = dev::enumerate("instance extensions", vkEnumerateInstanceExtensionProperties, nullptr); return { std::from_range, @@ -175,10 +173,7 @@ vector available_instance_extensions() { } vector available_layers() { - uint32_t count = 0; - vkEnumerateInstanceLayerProperties(&count, nullptr); - vector properties(count); - vkEnumerateInstanceLayerProperties(&count, properties.data()); + auto const properties = dev::enumerate("instance layers", vkEnumerateInstanceLayerProperties); return { std::from_range, diff --git a/cvk/src/base/physical_device.cpp b/cvk/src/base/physical_device.cpp index 571f9605..7091f15b 100644 --- a/cvk/src/base/physical_device.cpp +++ b/cvk/src/base/physical_device.cpp @@ -3,6 +3,7 @@ #include "cvk/base/instance.hpp" #include "cvk/surface/surface.hpp" #include "cvk/utility/device/feature_chain.hpp" +#include "cvk/utility/enumerate.hpp" #include "cvk/utility/vk_exceptions.hpp" #include @@ -19,18 +20,12 @@ using std::vector; namespace { [[nodiscard]] vector query_extensions(vk_not_null vkDevice) { - uint32_t count = 0; - auto const countResult = vkEnumerateDeviceExtensionProperties(vkDevice.get(), nullptr, &count, nullptr); - CVK_RESULT_STABLE_THROW(countResult != VK_SUCCESS, countResult, "failed to count device extensions") {} - - vector properties(count); - auto const writeResult = vkEnumerateDeviceExtensionProperties( + auto const properties = dev::enumerate( + "device extensions", + vkEnumerateDeviceExtensionProperties, vkDevice.get(), - nullptr, - &count, - properties.data() + nullptr ); - CVK_RESULT_STABLE_THROW(writeResult != VK_SUCCESS, writeResult, "failed to enumerate device extensions") {} return { std::from_range, @@ -74,10 +69,11 @@ namespace { span surfaces, vk_not_null vkDevice ) { - uint32_t count = 0; - vkGetPhysicalDeviceQueueFamilyProperties(vkDevice.get(), &count, nullptr); - vector familyProperties(count); - vkGetPhysicalDeviceQueueFamilyProperties(vkDevice.get(), &count, familyProperties.data()); + auto const familyProperties = dev::enumerate( + "queue families", + vkGetPhysicalDeviceQueueFamilyProperties, + vkDevice.get() + ); vector queueFamilies{}; queueFamilies.reserve(familyProperties.size()); @@ -208,13 +204,7 @@ QueueFamily const* PhysicalDevice::queueFamily(queue_family_index_t idx) const { vector enumerate_physical_devices(Instance const& instance, span surfaces) { Instance::debug_check(instance); - uint32_t count = 0; - auto const countResult = vkEnumeratePhysicalDevices(instance.get(), &count, nullptr); - CVK_RESULT_STABLE_THROW(countResult != VK_SUCCESS, countResult, "failed to count physical devices") {} - - vector vkDevices(count); - auto const writeResult = vkEnumeratePhysicalDevices(instance.get(), &count, vkDevices.data()); - CVK_RESULT_STABLE_THROW(writeResult != VK_SUCCESS, writeResult, "failed to enumerate physical devices") {} + auto const vkDevices = dev::enumerate("physical devices", vkEnumeratePhysicalDevices, instance.get()); return { std::from_range, diff --git a/cvk/src/surface/surface.cpp b/cvk/src/surface/surface.cpp index a138a0f5..c43664ba 100644 --- a/cvk/src/surface/surface.cpp +++ b/cvk/src/surface/surface.cpp @@ -2,6 +2,7 @@ #include "cvk/base/instance.hpp" #include "cvk/base/physical_device.hpp" +#include "cvk/utility/enumerate.hpp" #include "cvk/utility/vk_exceptions.hpp" @@ -53,49 +54,18 @@ bool Surface::supportsFamily(PhysicalDevice const& physicalDevice, uint32_t fami vector Surface::presentModes(PhysicalDevice const& physicalDevice) const { debug_check(*this); - uint32_t size = 0; - auto const countResult = vkGetPhysicalDeviceSurfacePresentModesKHR( + return dev::enumerate( + "surface present modes", + vkGetPhysicalDeviceSurfacePresentModesKHR, physicalDevice.get(), - _handle.get(), - &size, - nullptr + _handle.get() ); - CVK_RESULT_STABLE_THROW(countResult != VK_SUCCESS, countResult, "device-surface present modes query failed") {} - - vector modes(size); - auto const writeResult = vkGetPhysicalDeviceSurfacePresentModesKHR( - physicalDevice.get(), - _handle.get(), - &size, - modes.data() - ); - CVK_RESULT_STABLE_THROW(writeResult != VK_SUCCESS, writeResult, "device-surface present modes query failed") {} - - return modes; } vector Surface::formats(PhysicalDevice const& physicalDevice) const { debug_check(*this); - uint32_t size = 0; - auto const countResult = vkGetPhysicalDeviceSurfaceFormatsKHR( - physicalDevice.get(), - _handle.get(), - &size, - nullptr - ); - CVK_RESULT_STABLE_THROW(countResult != VK_SUCCESS, countResult, "device-surface formats query failed") {} - - vector formats(size); - auto const writeResult = vkGetPhysicalDeviceSurfaceFormatsKHR( - physicalDevice.get(), - _handle.get(), - &size, - formats.data() - ); - CVK_RESULT_STABLE_THROW(writeResult != VK_SUCCESS, writeResult, "device-surface formats query failed") {} - - return formats; + return dev::enumerate("surface formats", vkGetPhysicalDeviceSurfaceFormatsKHR, physicalDevice.get(), _handle.get()); } VkSurfaceCapabilitiesKHR Surface::capabilities(PhysicalDevice const& physicalDevice) const { diff --git a/cvk/src/surface/swapchain.cpp b/cvk/src/surface/swapchain.cpp index 35865371..a0540143 100644 --- a/cvk/src/surface/swapchain.cpp +++ b/cvk/src/surface/swapchain.cpp @@ -3,6 +3,7 @@ #include "cvk/base/core.hpp" #include "cvk/base/physical_device.hpp" #include "cvk/surface/surface.hpp" +#include "cvk/utility/enumerate.hpp" #include "cvk/utility/vk_exceptions.hpp" #include "cvk/utility/vk_overloads.hpp" @@ -259,15 +260,13 @@ void Swapchain::createSwapchain(VkExtent2D extent, VkSwapchainKHR oldSwapchain) } void Swapchain::createImages() { - auto const* functions = core().functions(); - - uint32_t count = 0; - auto const countResult = functions->vkGetSwapchainImagesKHR(core().vkDevice(), get(), &count, nullptr); - CVK_RESULT_STABLE_THROW(countResult != VK_SUCCESS, countResult, "failed to get the swapchain image count") {} - - vector vkImages(count); - auto const imagesResult = functions->vkGetSwapchainImagesKHR(core().vkDevice(), get(), &count, vkImages.data()); - CVK_RESULT_STABLE_THROW(imagesResult != VK_SUCCESS, imagesResult, "failed to get the swapchain images") {} + auto const vkImages = dev::enumerate( + "swapchain images", + core().functions()->vkGetSwapchainImagesKHR, + core().vkDevice(), + get() + ); + auto const count = static_cast(vkImages.size()); Image::Config const imageConfig{ .format = _surfaceFormat.format, diff --git a/cvk/tests/src/utility/enumerate_test.cpp b/cvk/tests/src/utility/enumerate_test.cpp new file mode 100644 index 00000000..02ed0e52 --- /dev/null +++ b/cvk/tests/src/utility/enumerate_test.cpp @@ -0,0 +1,133 @@ +#include "test.hpp" + +#include "cvk/utility/enumerate.hpp" +#include "cvk/utility/vk_exceptions.hpp" + +#include + +#include +#include +#include +#include +#include + + +namespace cvk { +namespace { + constexpr std::array ELEMENTS{1, 2, 3, 4}; + + /** + * @brief acts like a Vulkan query with the available elements + * @return VK_INCOMPLETE -> data has less space than available + */ + VkResult query(std::span available, uint32_t* count, int* data) { + if(data == nullptr) { + *count = static_cast(available.size()); + return VK_SUCCESS; + } + + auto const written = std::min(*count, available.size()); + std::ranges::copy(available.first(written), data); + *count = static_cast(written); + return written < available.size() ? VK_INCOMPLETE : VK_SUCCESS; + } + + /** + * @brief 2 elements at the first call, 4 at the later calls -> the first write is incomplete + */ + VkResult growing(uint32_t* calls, uint32_t* count, int* data) { + auto const call = (*calls)++; + return query(std::span{ELEMENTS}.first(call == 0 ? 2 : 4), count, data); + } + + /** + * @brief 4 elements at the count call, 2 at the write call + */ + VkResult shrinking(uint32_t* calls, uint32_t* count, int* data) { + auto const call = (*calls)++; + return query(std::span{ELEMENTS}.first(call == 0 ? 4 : 2), count, data); + } + + /** + * @brief one element at every count call, VK_INCOMPLETE at every write call + */ + VkResult always_incomplete(uint32_t* calls, uint32_t* count, int* data) { + ++*calls; + if(data != nullptr) return VK_INCOMPLETE; + + *count = 1; + return VK_SUCCESS; + } + + /** + * @return VK_ERROR_OUT_OF_HOST_MEMORY at the call with the index failingCall + */ + VkResult failing(uint32_t failingCall, uint32_t* calls, uint32_t* count, int* data) { + if((*calls)++ == failingCall) return VK_ERROR_OUT_OF_HOST_MEMORY; + return query(ELEMENTS, count, data); + } + + /** + * @brief writes the first elementCount elements, like vkGetPhysicalDeviceQueueFamilyProperties() + */ + void void_query(uint32_t elementCount, uint32_t* count, int* data) { + [[maybe_unused]] auto const result = query(std::span{ELEMENTS}.first(elementCount), count, data); + } +} + +CVK_TEST(enumerate, element_type_is_the_pointee_of_the_last_parameter) { + static_assert(std::same_as, int>); + static_assert(std::same_as, VkPhysicalDevice>); + static_assert(std::same_as, VkImage>); +} + +CVK_TEST(enumerate, incomplete_repeats_until_complete) { + uint32_t calls = 0; + + auto const elements = dev::enumerate("fake elements", growing, &calls); + + EXPECT_EQ(calls, 4); + EXPECT_TRUE(std::ranges::equal(elements, ELEMENTS)); +} + +CVK_TEST(enumerate, incomplete_in_every_attempt_throws) { + uint32_t calls = 0; + + try { + [[maybe_unused]] auto const elements = dev::enumerate("fake elements", always_incomplete, &calls); + ADD_FAILURE() << "no throw"; + } catch(vk_result_exception const& exception) { + EXPECT_EQ(exception.vkResult(), VK_INCOMPLETE); + } + EXPECT_EQ(calls, 2 * dev::MAX_QUERY_ATTEMPTS); +} + +CVK_TEST(enumerate, fewer_written_elements_shrink_the_result) { + uint32_t calls = 0; + + auto const elements = dev::enumerate("fake elements", shrinking, &calls); + + EXPECT_EQ(calls, 2); + EXPECT_TRUE(std::ranges::equal(elements, std::span{ELEMENTS}.first(2))); +} + +CVK_TEST(enumerate, error_throws_the_result) { + // count call, write call + for(uint32_t const failingCall : {0u, 1u}) { + uint32_t calls = 0; + try { + [[maybe_unused]] auto const elements = dev::enumerate("fake elements", failing, failingCall, &calls); + ADD_FAILURE() << "no throw at call " << failingCall; + } catch(vk_result_exception const& exception) { + EXPECT_EQ(exception.vkResult(), VK_ERROR_OUT_OF_HOST_MEMORY); + } + EXPECT_EQ(calls, failingCall + 1); + } +} + +CVK_TEST(enumerate, void_function_returns_its_elements) { + auto const elements = dev::enumerate("fake elements", void_query, 3u); + + EXPECT_TRUE(std::ranges::equal(elements, std::span{ELEMENTS}.first(3))); +} +} From 004e92ab6aa51bd0f9ec01c6286fdf50d4b12860 Mon Sep 17 00:00:00 2001 From: lukas <89145039+codethinki@users.noreply.github.com> Date: Wed, 21 Oct 2026 12:43:57 +0200 Subject: [PATCH 112/135] cvk: inline constexpr create tag, explicit cvk_exception conversion, no COMPILATION_MODE --- cvk/incl/cvk/utility/constants.hpp | 8 ++------ cvk/incl/cvk/utility/types.hpp | 2 -- cvk/incl/cvk/utility/vk_exceptions.hpp | 2 +- 3 files changed, 3 insertions(+), 9 deletions(-) diff --git a/cvk/incl/cvk/utility/constants.hpp b/cvk/incl/cvk/utility/constants.hpp index e2385c24..e3680c1f 100644 --- a/cvk/incl/cvk/utility/constants.hpp +++ b/cvk/incl/cvk/utility/constants.hpp @@ -5,16 +5,12 @@ #include -#include - namespace cvk { struct create_t {}; -static constexpr create_t create{}; +inline constexpr create_t create{}; } namespace cvk::constants { constexpr bool ENABLE_VALIDATION_LAYERS = DEBUG_MODE; - -constexpr auto COMPILATION_MODE = cth::COMPILATION_MODE; -} // namespace cvk::constants +} diff --git a/cvk/incl/cvk/utility/types.hpp b/cvk/incl/cvk/utility/types.hpp index b94d0c0f..95c6a600 100644 --- a/cvk/incl/cvk/utility/types.hpp +++ b/cvk/incl/cvk/utility/types.hpp @@ -15,6 +15,4 @@ using move_ptr = cth::move_ptr; template using unique_not_null = cth::unique_not_null; - -using CompilationMode = cth::CompilationMode; } diff --git a/cvk/incl/cvk/utility/vk_exceptions.hpp b/cvk/incl/cvk/utility/vk_exceptions.hpp index 39cf20ea..166a6121 100644 --- a/cvk/incl/cvk/utility/vk_exceptions.hpp +++ b/cvk/incl/cvk/utility/vk_exceptions.hpp @@ -27,7 +27,7 @@ class cvk_exception : public cth::except::default_exception { std::move(trace) } {} - cvk_exception(cth::except::default_exception defaultException) + explicit cvk_exception(cth::except::default_exception defaultException) : default_exception{std::move(defaultException)} { prepend(PREFIX); } }; From e546326819f54b558741a5d260695922534297dc Mon Sep 17 00:00:00 2001 From: lukas <89145039+codethinki@users.noreply.github.com> Date: Wed, 21 Oct 2026 12:43:57 +0200 Subject: [PATCH 113/135] cvk: addStage takes the entry point --- .../render/pipeline/graphics_pipeline_config.hpp | 9 +++++++-- .../render/pipeline/graphics_pipeline_config.cpp | 8 ++++++-- .../src/render/pipeline/graphics_pipeline_test.cpp | 13 +++++++++++++ 3 files changed, 26 insertions(+), 4 deletions(-) diff --git a/cvk/incl/cvk/render/pipeline/graphics_pipeline_config.hpp b/cvk/incl/cvk/render/pipeline/graphics_pipeline_config.hpp index 134cb35c..cb218992 100644 --- a/cvk/incl/cvk/render/pipeline/graphics_pipeline_config.hpp +++ b/cvk/incl/cvk/render/pipeline/graphics_pipeline_config.hpp @@ -34,11 +34,16 @@ struct GraphicsPipelineConfig { /** * @brief adds the stage of the shader * @param specialization nullptr -> no specialization constants - * @attention @ref specialization must outlive @ref GraphicsPipeline::create() + * @param entryPoint name of the entry function + * @attention @ref specialization and @ref entryPoint must outlive @ref GraphicsPipeline::create() * @pre @ref Shader::created() * @pre no stage of the same type */ - GraphicsPipelineConfig& addStage(Shader const&, VkSpecializationInfo const* specialization = nullptr); + GraphicsPipelineConfig& addStage( + Shader const&, + VkSpecializationInfo const* specialization = nullptr, + char const* entryPoint = "main" + ); /** * @pre a stage of the type exists diff --git a/cvk/src/render/pipeline/graphics_pipeline_config.cpp b/cvk/src/render/pipeline/graphics_pipeline_config.cpp index 7915dfaa..5880c255 100644 --- a/cvk/src/render/pipeline/graphics_pipeline_config.cpp +++ b/cvk/src/render/pipeline/graphics_pipeline_config.cpp @@ -6,13 +6,17 @@ namespace cvk { -GraphicsPipelineConfig& GraphicsPipelineConfig::addStage(Shader const& shader, VkSpecializationInfo const* specialization) { +GraphicsPipelineConfig& GraphicsPipelineConfig::addStage( + Shader const& shader, + VkSpecializationInfo const* specialization, + char const* entryPoint +) { CTH_CRITICAL( std::ranges::contains(stages, shader.stage(), &VkPipelineShaderStageCreateInfo::stage), "stage already added" ) {} - stages.push_back(shader.stageInfo(specialization)); + stages.push_back(shader.stageInfo(specialization, entryPoint)); return *this; } } diff --git a/cvk/tests/src/render/pipeline/graphics_pipeline_test.cpp b/cvk/tests/src/render/pipeline/graphics_pipeline_test.cpp index e592f266..43695fff 100644 --- a/cvk/tests/src/render/pipeline/graphics_pipeline_test.cpp +++ b/cvk/tests/src/render/pipeline/graphics_pipeline_test.cpp @@ -111,6 +111,19 @@ CVK_TEST(GraphicsPipeline, config_stages) { EXPECT_EQ(config.stages[0].stage, VK_SHADER_STAGE_FRAGMENT_BIT); } +CVK_TEST(GraphicsPipeline, config_add_stage_takes_the_entry_point) { + test::Context const context{}; + Triangle const triangle{context.core}; + char const* const entryPoint = "other"; + + GraphicsPipeline::Config config{}; + config.addStage(triangle.vertex).addStage(triangle.fragment, nullptr, entryPoint); + + ASSERT_EQ(config.stages.size(), 2); + EXPECT_STREQ(config.stages[0].pName, "main"); + EXPECT_EQ(config.stages[1].pName, entryPoint); +} + CVK_TEST(GraphicsPipeline, bind) { test::Context const context{}; Triangle const triangle{context.core}; From fcab959862ae08ce8f2aa1ff9b5638cb3cd25565 Mon Sep 17 00:00:00 2001 From: lukas <89145039+codethinki@users.noreply.github.com> Date: Wed, 21 Oct 2026 12:53:36 +0200 Subject: [PATCH 114/135] cvk: the swapchain keeps its generations in one vector, default move assignment --- cvk/incl/cvk/surface/swapchain.hpp | 101 ++++++++------ cvk/src/surface/swapchain.cpp | 162 ++++++++--------------- cvk/tests/src/surface/swapchain_test.cpp | 38 ++++++ 3 files changed, 152 insertions(+), 149 deletions(-) diff --git a/cvk/incl/cvk/surface/swapchain.hpp b/cvk/incl/cvk/surface/swapchain.hpp index 556c5c4f..36b3409f 100644 --- a/cvk/incl/cvk/surface/swapchain.hpp +++ b/cvk/incl/cvk/surface/swapchain.hpp @@ -30,8 +30,7 @@ class Surface; * @brief VkSwapchainKHR wrapper, owns its images, their views, a present semaphore and a present fence per image * @details the images are wrapped without memory and released before the swapchain is destroyed, * @ref resize() and @ref recreate() retire the old swapchain, - * the swapchain destroys it once its present fences signal, - * custom move assignment: destroys first, a default one would destroy the swapchain images + * the swapchain destroys it once its present fences signal * @attention not thread safe */ class Swapchain { @@ -80,12 +79,8 @@ class Swapchain { /** * @note calls @ref optDestroy() - * @note waits for the retired swapchains and destroys them */ - ~Swapchain() { - optDestroy(); - destroyRetired(); - } + ~Swapchain() { optDestroy(); } /** * @brief wraps the state, gets the images, creates their views, present semaphores and present fences @@ -120,7 +115,7 @@ class Swapchain { * @pre extent width and height > 0 * @pre the surface still supports the format and the present mode of the swapchain * @pre no present semaphore is signaled without its present - * @throws cvk::vk_result_exception result of vkCreateSwapchainKHR() + * @throws cvk::vk_result_exception result of vkCreateSwapchainKHR(), destroys the old swapchains and forgets */ void resize(VkExtent2D extent); @@ -133,7 +128,7 @@ class Swapchain { * @pre no present semaphore is signaled without its present * @note a new format needs new pipelines for the swapchain images * @throws cvk::cvk_exception reason: the surface offers no allowed format or present mode, nothing changes - * @throws cvk::vk_result_exception result of vkCreateSwapchainKHR() + * @throws cvk::vk_result_exception result of vkCreateSwapchainKHR(), destroys the old swapchains and forgets */ void recreate(VkExtent2D extent, Config); @@ -201,16 +196,18 @@ class Swapchain { */ [[nodiscard]] static constexpr uint32_t evalImageCount(uint32_t minImageCount, VkSurfaceCapabilitiesKHR const&); - [[nodiscard]] constexpr bool created() const { return static_cast(_handle); } - [[nodiscard]] constexpr vk_handle_t get() const { return _handle.get(); } - [[nodiscard]] constexpr Core const& core() const { return _handle.core(); } + [[nodiscard]] constexpr bool created() const { return !_generations.empty(); } + [[nodiscard]] constexpr vk_handle_t get() const { return created() ? current().handle.get() : VK_NULL_HANDLE; } + [[nodiscard]] constexpr Core const& core() const { return *_core; } [[nodiscard]] constexpr Config const& config() const { return _config; } [[nodiscard]] constexpr VkExtent2D extent() const { return _extent; } [[nodiscard]] constexpr VkFormat format() const { return _surfaceFormat.format; } [[nodiscard]] constexpr VkColorSpaceKHR colorSpace() const { return _surfaceFormat.colorSpace; } [[nodiscard]] constexpr VkPresentModeKHR presentMode() const { return _presentMode; } - [[nodiscard]] constexpr uint32_t imageCount() const { return static_cast(_images.size()); } + [[nodiscard]] constexpr uint32_t imageCount() const { + return created() ? static_cast(current().images.size()) : 0; + } /** * @return width / height @@ -220,40 +217,66 @@ class Swapchain { return static_cast(_extent.width) / static_cast(_extent.height); } - [[nodiscard]] constexpr std::span images() const { return _images; } - [[nodiscard]] constexpr std::span views() const { return _views; } + [[nodiscard]] constexpr std::span images() const { + return created() ? std::span{current().images} : std::span{}; + } + [[nodiscard]] constexpr std::span views() const { + return created() ? std::span{current().views} : std::span{}; + } /** * @return the semaphore that @ref present() of the image waits for, a submit signals it * @pre imageIndex < @ref imageCount() */ [[nodiscard]] constexpr Semaphore const& presentSemaphore(uint32_t imageIndex) const { - return _presentSemaphores[imageIndex]; + return current().presentSemaphores[imageIndex]; } /** * @pre imageIndex < @ref imageCount() */ - [[nodiscard]] constexpr bool acquired(uint32_t imageIndex) const { return _acquired[imageIndex]; } + [[nodiscard]] constexpr bool acquired(uint32_t imageIndex) const { return current().acquired[imageIndex]; } /** * @return count of the retired swapchains that wait for their present fences */ - [[nodiscard]] constexpr size_t retired() const { return _retired.size(); } + [[nodiscard]] constexpr size_t retired() const { return created() ? _generations.size() - 1 : 0; } private: /** - * @brief old swapchain after @ref resize() or @ref recreate(), the images are released - * @details members destroy in reverse order: the swapchain last + * @brief one VkSwapchainKHR with its images, views, present semaphores, present fences and acquire states + * @details the destructor waits for the present fences and releases the images, + * then the members destroy in reverse order: the swapchain last, + * a moved from generation is empty, its destructor does nothing, + * no move assignment: a member wise one destroys the images without the wait and the release */ - struct Retired { + struct Generation { + explicit Generation(core_handle swapchain) : handle{std::move(swapchain)} {} + ~Generation(); + core_handle handle; std::vector images; std::vector views; std::vector presentSemaphores; std::vector presentFences; + std::vector acquired; + + Generation(Generation const& other) = delete; + Generation(Generation&& other) noexcept = default; + Generation& operator=(Generation const& other) = delete; + Generation& operator=(Generation&& other) = delete; }; + /** + * @pre @ref created() + */ + [[nodiscard]] constexpr Generation& current() { return _generations.back(); } + + /** + * @pre @ref created() + */ + [[nodiscard]] constexpr Generation const& current() const { return _generations.back(); } + /** * @return the first allowed format and present mode of the config that the surface offers * @throws cvk::cvk_exception reason: the surface offers no allowed format or present mode @@ -261,45 +284,37 @@ class Swapchain { [[nodiscard]] std::pair pick(Config const&) const; /** - * @brief creates the swapchain and its images with @ref _surfaceFormat and @ref _presentMode + * @brief creates a swapchain with @ref _surfaceFormat and @ref _presentMode as the new current generation + * @details a failure destroys every generation and forgets */ void createSwapchain(VkExtent2D extent, VkSwapchainKHR oldSwapchain); - void createImages(); /** - * @brief unacquires the acquired images, moves the swapchain and its images to the retired swapchains - * @return the handle of the retired swapchain + * @brief gets the images of the current generation, creates their views, present semaphores and present fences */ - [[nodiscard]] VkSwapchainKHR retire(); + void createImages(); /** - * @brief waits for the present fences, releases the images, destroys the views, semaphores and fences + * @brief unacquires the acquired images of the current generation */ - void destroyImages(); + void unacquireAll(); /** - * @brief destroys the retired swapchains with signaled present fences + * @brief destroys the retired generations with signaled present fences */ void collectRetired(); - /** - * @brief waits for the present fences of the retired swapchains and destroys them - */ - void destroyRetired(); - void forget(); Queue _presentQueue; not_null_ptr _surface; + not_null_ptr _core; Config _config; - core_handle _handle; - std::vector _images; - std::vector _views; - std::vector _presentSemaphores; - std::vector _presentFences; - std::vector _acquired; - std::vector _retired; + /** + * @brief back() is the current swapchain, the others are retired, empty -> not created + */ + std::vector _generations; VkExtent2D _extent{}; VkSurfaceFormatKHR _surfaceFormat{}; @@ -309,9 +324,7 @@ class Swapchain { Swapchain(Swapchain const& other) = delete; Swapchain(Swapchain&& other) noexcept = default; Swapchain& operator=(Swapchain const& other) = delete; - //TODO the custom move assignment is ugly, find a design where the default one works - //TODO generate the member moves with reflection, only optDestroy(), destroyRetired() and forget() stay - Swapchain& operator=(Swapchain&& other) noexcept; + Swapchain& operator=(Swapchain&& other) noexcept = default; static constexpr void debug_check(Swapchain const&); }; diff --git a/cvk/src/surface/swapchain.cpp b/cvk/src/surface/swapchain.cpp index a0540143..560fed3e 100644 --- a/cvk/src/surface/swapchain.cpp +++ b/cvk/src/surface/swapchain.cpp @@ -8,6 +8,7 @@ #include "cvk/utility/vk_overloads.hpp" #include +#include namespace cvk { @@ -15,7 +16,7 @@ using std::span; using std::vector; Swapchain::Swapchain(Core const& core, Queue const& presentQueue, Surface const& surface, Config config) : - _presentQueue{presentQueue}, _surface{&surface}, _config{std::move(config)}, _handle{core} { + _presentQueue{presentQueue}, _surface{&surface}, _core{&core}, _config{std::move(config)} { Core::debug_check(core); Surface::debug_check(surface); Queue::debug_check_present(presentQueue); @@ -41,7 +42,7 @@ Swapchain::Swapchain( void Swapchain::wrap(State const& state) { optDestroy(); - _handle.reset(state.vkSwapchain.get(), core()); + _generations.emplace_back(core_handle{state.vkSwapchain.get(), core()}); _extent = state.extent; _surfaceFormat = state.surfaceFormat; _presentMode = state.presentMode; @@ -63,8 +64,8 @@ void Swapchain::resize(VkExtent2D extent) { debug_check(*this); CTH_CRITICAL(extent.width == 0 || extent.height == 0, "extent width and height must be > 0, got {}", extent) {} - auto const oldSwapchain = retire(); - createSwapchain(extent, oldSwapchain); + unacquireAll(); + createSwapchain(extent, get()); } void Swapchain::recreate(VkExtent2D extent, Config config) { @@ -74,19 +75,17 @@ void Swapchain::recreate(VkExtent2D extent, Config config) { auto const [surfaceFormat, presentMode] = pick(config); - auto const oldSwapchain = retire(); + unacquireAll(); _config = std::move(config); _surfaceFormat = surfaceFormat; _presentMode = presentMode; - createSwapchain(extent, oldSwapchain); + createSwapchain(extent, get()); } void Swapchain::destroy() { debug_check(*this); - destroyImages(); - destroyRetired(); - _handle.reset(); + _generations.clear(); forget(); } @@ -94,15 +93,15 @@ void Swapchain::destroy() { Swapchain::State Swapchain::release() { debug_check(*this); - destroyImages(); - destroyRetired(); - State const state{ - .vkSwapchain = _handle.release(), + .vkSwapchain = current().handle.release(), .extent = _extent, .surfaceFormat = _surfaceFormat, .presentMode = _presentMode, }; + // waits for the present fences, releases the images, the current generation has no handle to destroy + _generations.clear(); + forget(); return state; } @@ -124,7 +123,7 @@ Swapchain::Acquired Swapchain::acquire(Semaphore const& signal, uint64_t timeout ); if(result == VK_SUCCESS || result == VK_SUBOPTIMAL_KHR) { - _acquired[imageIndex] = true; + current().acquired[imageIndex] = true; return {result, imageIndex}; } @@ -139,12 +138,14 @@ Swapchain::Acquired Swapchain::acquire(Semaphore const& signal, uint64_t timeout VkResult Swapchain::present(uint32_t imageIndex) { debug_check(*this); - CTH_CRITICAL(imageIndex >= imageCount() || !_acquired[imageIndex], "image {} must be acquired", imageIndex) {} + CTH_CRITICAL(imageIndex >= imageCount() || !acquired(imageIndex), "image {} must be acquired", imageIndex) {} collectRetired(); + auto& generation = current(); + // signaled by the last present of the image or created signaled - auto const& fence = _presentFences[imageIndex]; + auto const& fence = generation.presentFences[imageIndex]; fence.waitReset(); VkFence const vkFence = fence.get(); @@ -155,8 +156,8 @@ VkResult Swapchain::present(uint32_t imageIndex) { .pFences = &vkFence, }; - VkSemaphore const vkSemaphore = _presentSemaphores[imageIndex].get(); - VkSwapchainKHR const vkSwapchain = get(); + VkSemaphore const vkSemaphore = generation.presentSemaphores[imageIndex].get(); + VkSwapchainKHR const vkSwapchain = generation.handle.get(); VkPresentInfoKHR const info{ .sType = VK_STRUCTURE_TYPE_PRESENT_INFO_KHR, .pNext = &fenceInfo, @@ -169,14 +170,14 @@ VkResult Swapchain::present(uint32_t imageIndex) { }; auto const result = _presentQueue.rawPresent(info); - _acquired[imageIndex] = false; + generation.acquired[imageIndex] = false; return result; } void Swapchain::unacquire(span imageIndices) { debug_check(*this); for(auto const index : imageIndices) - CTH_CRITICAL(index >= imageCount() || !_acquired[index], "image {} must be acquired", index) {} + CTH_CRITICAL(index >= imageCount() || !acquired(index), "image {} must be acquired", index) {} if(imageIndices.empty()) return; @@ -192,7 +193,7 @@ void Swapchain::unacquire(span imageIndices) { CVK_RESULT_STABLE_THROW(result != VK_SUCCESS, result, "failed to unacquire swapchain images") {} for(auto const index : imageIndices) - _acquired[index] = false; + current().acquired[index] = false; } void Swapchain::destroy(DeviceTable table, VkSwapchainKHR vkSwapchain) { @@ -201,6 +202,15 @@ void Swapchain::destroy(DeviceTable table, VkSwapchainKHR vkSwapchain) { table->vkDestroySwapchainKHR(table.device(), vkSwapchain, nullptr); } +Swapchain::Generation::~Generation() { + for(auto const& fence : presentFences) + fence.wait(); + + // the swapchain destroys its images + for(auto& image : images) + image.release(); +} + std::pair Swapchain::pick(Config const& config) const { auto const& physicalDevice = core().physicalDevice(); @@ -249,22 +259,26 @@ void Swapchain::createSwapchain(VkExtent2D extent, VkSwapchainKHR oldSwapchain) auto const result = core().functions()->vkCreateSwapchainKHR(core().vkDevice(), &info, nullptr, &ptr); CTH_STABLE_ERR(result != VK_SUCCESS, "failed to create swapchain") { + // the old swapchain retires also when the creation fails, no generation stays current + _generations.clear(); forget(); throw cvk::vk_result_exception{result, details->exception()}; } - _handle.reset(ptr, core()); + _generations.emplace_back(core_handle{ptr, core()}); _extent = imageExtent; createImages(); } void Swapchain::createImages() { + auto& generation = current(); + auto const vkImages = dev::enumerate( "swapchain images", core().functions()->vkGetSwapchainImagesKHR, core().vkDevice(), - get() + generation.handle.get() ); auto const count = static_cast(vkImages.size()); @@ -274,80 +288,42 @@ void Swapchain::createImages() { .queueFamilies = _config.queueFamilies, }; - _images.reserve(count); + generation.images.reserve(count); for(auto const vkImage : vkImages) - _images.emplace_back(core(), imageConfig, Image::State{.vkImage = vkImage, .extent = _extent}); + generation.images.emplace_back(core(), imageConfig, Image::State{.vkImage = vkImage, .extent = _extent}); - _views.reserve(count); - for(auto const& image : _images) - _views.emplace_back(image, ImageView::Config{}, cvk::create); + generation.views.reserve(count); + for(auto const& image : generation.images) + generation.views.emplace_back(image, ImageView::Config{}, cvk::create); - _presentSemaphores.reserve(count); - _presentFences.reserve(count); + generation.presentSemaphores.reserve(count); + generation.presentFences.reserve(count); for(uint32_t index = 0; index < count; ++index) { - _presentSemaphores.emplace_back(core(), cvk::create); - _presentFences.emplace_back(core(), cvk::create, VK_FENCE_CREATE_SIGNALED_BIT); + generation.presentSemaphores.emplace_back(core(), cvk::create); + generation.presentFences.emplace_back(core(), cvk::create, VK_FENCE_CREATE_SIGNALED_BIT); } - _acquired.assign(count, false); + generation.acquired.assign(count, false); } -VkSwapchainKHR Swapchain::retire() { +void Swapchain::unacquireAll() { vector acquiredImages{}; for(uint32_t index = 0; index < imageCount(); ++index) - if(_acquired[index]) acquiredImages.push_back(index); + if(acquired(index)) acquiredImages.push_back(index); unacquire(acquiredImages); - - // the swapchain destroys its images - for(auto& image : _images) - image.release(); - - auto const oldSwapchain = get(); - _retired.push_back( - Retired{ - .handle = std::move(_handle), - .images = std::move(_images), - .views = std::move(_views), - .presentSemaphores = std::move(_presentSemaphores), - .presentFences = std::move(_presentFences), - } - ); - _images.clear(); - _views.clear(); - _presentSemaphores.clear(); - _presentFences.clear(); - _acquired.clear(); - - return oldSwapchain; -} - -void Swapchain::destroyImages() { - for(auto const& fence : _presentFences) - fence.wait(); - - _views.clear(); - // the swapchain destroys its images - for(auto& image : _images) - image.release(); - _images.clear(); - - _presentSemaphores.clear(); - _presentFences.clear(); - _acquired.clear(); } void Swapchain::collectRetired() { - std::erase_if(_retired, [](Retired const& retired) { - return std::ranges::all_of(retired.presentFences, &Fence::signaled); - }); -} + if(_generations.size() < 2) return; -void Swapchain::destroyRetired() { - for(auto const& retired : _retired) - for(auto const& fence : retired.presentFences) - fence.wait(); + // the generations have no move assignment, the kept ones move into a new vector + vector kept{}; + kept.reserve(_generations.size()); + for(auto& generation : _generations | std::views::take(_generations.size() - 1)) + if(!std::ranges::all_of(generation.presentFences, &Fence::signaled)) kept.push_back(std::move(generation)); + kept.push_back(std::move(current())); - _retired.clear(); + _generations = std::move(kept); } void Swapchain::forget() { @@ -355,28 +331,4 @@ void Swapchain::forget() { _surfaceFormat = {}; _presentMode = VK_PRESENT_MODE_FIFO_KHR; } - -Swapchain& Swapchain::operator=(Swapchain&& other) noexcept { - if(this == &other) return *this; - - optDestroy(); - destroyRetired(); - - _presentQueue = other._presentQueue; - _surface = other._surface; - _config = std::move(other._config); - _handle = std::move(other._handle); - _images = std::move(other._images); - _views = std::move(other._views); - _presentSemaphores = std::move(other._presentSemaphores); - _presentFences = std::move(other._presentFences); - _acquired = std::move(other._acquired); - _retired = std::move(other._retired); - _extent = other._extent; - _surfaceFormat = other._surfaceFormat; - _presentMode = other._presentMode; - - other.forget(); - return *this; -} } diff --git a/cvk/tests/src/surface/swapchain_test.cpp b/cvk/tests/src/surface/swapchain_test.cpp index 4ae937e5..06738d58 100644 --- a/cvk/tests/src/surface/swapchain_test.cpp +++ b/cvk/tests/src/surface/swapchain_test.cpp @@ -280,6 +280,22 @@ CVK_TEST(Swapchain, recreate_without_an_offered_format_throws_and_keeps_the_swap EXPECT_EQ(present_cleared_image(context, swapchain), VK_SUCCESS); } +CVK_TEST(Swapchain, presents_destroy_the_retired_swapchain_after_its_present_fences_signal) { + test::PresentContext const context{}; + Swapchain swapchain{context.core, context.queue(), context.surface(), {}, EXTENT}; + EXPECT_EQ(present_cleared_image(context, swapchain), VK_SUCCESS); + swapchain.resize({32, 32}); + auto const vkSwapchain = swapchain.get(); + ASSERT_EQ(swapchain.retired(), 1); + + // the headless surface completes presents at once, the bound only ends a broken loop + for(uint32_t frame = 0; frame < 16 && swapchain.retired() != 0; ++frame) + EXPECT_EQ(present_cleared_image(context, swapchain), VK_SUCCESS); + + EXPECT_EQ(swapchain.retired(), 0); + EXPECT_EQ(swapchain.get(), vkSwapchain); +} + CVK_TEST(Swapchain, unacquire_gives_the_image_back) { test::PresentContext const context{}; Swapchain swapchain{context.core, context.queue(), context.surface(), {}, EXTENT}; @@ -337,4 +353,26 @@ CVK_TEST(Swapchain, move_assignment_destroys_the_old_swapchain) { EXPECT_FALSE(moved.created()); EXPECT_EQ(present_cleared_image(context, swapchain), VK_SUCCESS); } + +CVK_TEST(Swapchain, move_assignment_over_a_retired_swapchain_with_a_present_in_flight) { + test::PresentContext const context{}; + auto const other = test::headless_surface(context.instance); + Swapchain swapchain{context.core, context.queue(), context.surface(), {}, EXTENT}; + // no acquire or present after the resize: the retired swapchain stays with its present fence + EXPECT_EQ(present_cleared_image(context, swapchain), VK_SUCCESS); + swapchain.resize({32, 32}); + ASSERT_EQ(swapchain.retired(), 1); + + Swapchain moved{context.core, context.queue(), other, {}, EXTENT}; + auto const vkSwapchain = moved.get(); + + swapchain = std::move(moved); + + ASSERT_TRUE(swapchain.created()); + EXPECT_EQ(swapchain.get(), vkSwapchain); + EXPECT_EQ(swapchain.retired(), 0); + EXPECT_EQ(swapchain.extent().width, EXTENT.width); + EXPECT_FALSE(moved.created()); + EXPECT_EQ(present_cleared_image(context, swapchain), VK_SUCCESS); +} } From ceaa20f61d4683de62ece9bf10e291f07ff23816 Mon Sep 17 00:00:00 2001 From: lukas <89145039+codethinki@users.noreply.github.com> Date: Wed, 21 Oct 2026 12:53:36 +0200 Subject: [PATCH 115/135] cvk: a failed Instance::create leaves the instance not created, no closing namespace comments --- cvk/incl/cvk/base/instance.hpp | 1 + cvk/incl/cvk/debug/debug_messenger.hpp | 2 +- .../cvk/utility/constant/debug_constants.hpp | 2 +- cvk/src/base/instance.cpp | 21 ++++++++++++------- cvk/src/debug/debug_messenger.cpp | 2 +- cvk/tests/src/base/instance_test.cpp | 6 +++++- 6 files changed, 23 insertions(+), 11 deletions(-) diff --git a/cvk/incl/cvk/base/instance.hpp b/cvk/incl/cvk/base/instance.hpp index b2cf6901..cfb34cdc 100644 --- a/cvk/incl/cvk/base/instance.hpp +++ b/cvk/incl/cvk/base/instance.hpp @@ -97,6 +97,7 @@ class Instance { * @throws cvk::vk_result_exception result of vkEnumerateInstanceExtensionProperties() * @throws cvk::vk_result_exception result of vkEnumerateInstanceLayerProperties() * @throws cvk::vk_result_exception result of vkCreateInstance() + * @throws cvk::vk_result_exception result of vkCreateDebugUtilsMessengerEXT(), destroys the instance */ void create(std::optional messengerConfig = std::nullopt); diff --git a/cvk/incl/cvk/debug/debug_messenger.hpp b/cvk/incl/cvk/debug/debug_messenger.hpp index f66d2e00..e4459dfa 100644 --- a/cvk/incl/cvk/debug/debug_messenger.hpp +++ b/cvk/incl/cvk/debug/debug_messenger.hpp @@ -128,7 +128,7 @@ class DebugMessenger { static constexpr void debug_check(DebugMessenger const&); }; -} // namespace cvk +} namespace cvk { struct DebugMessenger::State { diff --git a/cvk/incl/cvk/utility/constant/debug_constants.hpp b/cvk/incl/cvk/utility/constant/debug_constants.hpp index e3c4eccf..5e40f3f1 100644 --- a/cvk/incl/cvk/utility/constant/debug_constants.hpp +++ b/cvk/incl/cvk/utility/constant/debug_constants.hpp @@ -17,4 +17,4 @@ constexpr VkDebugUtilsMessageTypeFlagsEXT DEBUG_MESSAGE_TYPE = VK_DEBUG_UTILS_MESSAGE_TYPE_GENERAL_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_TYPE_VALIDATION_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_TYPE_PERFORMANCE_BIT_EXT; -} // namespace cvk::constants +} diff --git a/cvk/src/base/instance.cpp b/cvk/src/base/instance.cpp index 42680d14..29db244a 100644 --- a/cvk/src/base/instance.cpp +++ b/cvk/src/base/instance.cpp @@ -50,19 +50,19 @@ void Instance::wrap(State state) { void Instance::create(std::optional messengerConfig) { optDestroy(); - _extensions = _config.extensions; + // the members change after the checks, a throw from them finds the members forgotten + auto requiredExtensions = _config.extensions; if constexpr(constants::ENABLE_VALIDATION_LAYERS) { for(string_view const extension : VALIDATION_LAYER_EXTENSIONS) - if(!std::ranges::contains(_extensions, extension)) _extensions.emplace_back(extension); + if(!std::ranges::contains(requiredExtensions, extension)) requiredExtensions.emplace_back(extension); if(!messengerConfig.has_value()) messengerConfig = DebugMessenger::Config::Default(); } - auto const missingExtensions = missing(_extensions, available_instance_extensions()); + auto const missingExtensions = missing(requiredExtensions, available_instance_extensions()); CTH_STABLE_ERR(!missingExtensions.empty(), "instance extensions missing") { for(auto const& extension : missingExtensions) details->add(extension); - forget(); throw cvk::cvk_exception{details->exception()}; } @@ -71,11 +71,11 @@ void Instance::create(std::optional messengerConfig) { CTH_STABLE_ERR(!missingLayers.empty(), "validation layers missing") { for(auto const& layer : missingLayers) details->add(layer); - forget(); throw cvk::cvk_exception{details->exception()}; } } + _extensions = std::move(requiredExtensions); auto const extensions = cth::str::to_c_str_vector(_extensions); auto const appInfo = _config.appInfo(); @@ -119,8 +119,15 @@ void Instance::create(std::optional messengerConfig) { _handle.reset(ptr); loadInstanceFunctions(ptr); - if constexpr(constants::ENABLE_VALIDATION_LAYERS) - _debugMessenger.emplace(*this, *messengerConfig, cvk::create); + if constexpr(constants::ENABLE_VALIDATION_LAYERS) { + // the messenger needs the created instance, only a catch can destroy the instance when the messenger fails + try { + _debugMessenger.emplace(*this, *messengerConfig, cvk::create); + } catch(...) { + destroy(); + throw; + } + } } void Instance::destroy() { diff --git a/cvk/src/debug/debug_messenger.cpp b/cvk/src/debug/debug_messenger.cpp index 5abaa597..e502f49d 100644 --- a/cvk/src/debug/debug_messenger.cpp +++ b/cvk/src/debug/debug_messenger.cpp @@ -69,7 +69,7 @@ DebugMessenger::State DebugMessenger::release() { } void DebugMessenger::forget() {} -} // namespace cvk +} namespace cvk::dev { VKAPI_ATTR VkBool32 VKAPI_CALL default_debug_callback( diff --git a/cvk/tests/src/base/instance_test.cpp b/cvk/tests/src/base/instance_test.cpp index 91d02c91..5fdb00a7 100644 --- a/cvk/tests/src/base/instance_test.cpp +++ b/cvk/tests/src/base/instance_test.cpp @@ -58,11 +58,15 @@ CVK_TEST(Instance, create_enables_the_extensions_of_the_config) { } CVK_TEST(Instance, create_with_a_missing_extension_throws_and_forgets) { - Instance instance{test::instance_config({"VK_CVK_extension_that_does_not_exist"})}; + // a created instance with the config of the missing extension: create() destroys it first + Instance created{test::instance_config(), create}; + Instance instance{test::instance_config({"VK_CVK_extension_that_does_not_exist"}), created.release()}; + ASSERT_TRUE(instance.created()); EXPECT_THROW(instance.create(), cvk_exception); EXPECT_FALSE(instance.created()); EXPECT_TRUE(instance.extensions().empty()); + EXPECT_EQ(instance.debugMessenger(), nullptr); } CVK_TEST(Instance, available_extensions_contain_the_surface) { From 7edea625a45b966c6c06166a20851abdcd43ecdb Mon Sep 17 00:00:00 2001 From: lukas <89145039+codethinki@users.noreply.github.com> Date: Wed, 21 Oct 2026 13:04:13 +0200 Subject: [PATCH 116/135] cvk: messenger config without Default(), default_debug_callback, private config, constexpr createInfo --- cvk/incl/cvk/base/instance.hpp | 2 +- cvk/incl/cvk/debug/debug_messenger.hpp | 47 +++++++++++--------- cvk/src/base/instance.cpp | 2 +- cvk/src/debug/debug_messenger.cpp | 39 +++------------- cvk/tests/src/debug/debug_messenger_test.cpp | 23 +++++++--- cvk/tests/src/validation.cpp | 4 +- 6 files changed, 52 insertions(+), 65 deletions(-) diff --git a/cvk/incl/cvk/base/instance.hpp b/cvk/incl/cvk/base/instance.hpp index cfb34cdc..f6142a93 100644 --- a/cvk/incl/cvk/base/instance.hpp +++ b/cvk/incl/cvk/base/instance.hpp @@ -86,7 +86,7 @@ class Instance { /** * @brief creates the instance with the extensions of the config, debug builds with the validation layers - * @param messengerConfig std::nullopt -> no messenger, debug builds use @ref DebugMessenger::Config::Default() + * @param messengerConfig std::nullopt -> no messenger, debug builds use the default @ref DebugMessenger::Config * @pre @ref Cvk::initialized() * @post @ref created() * @post volk uses the functions of this instance diff --git a/cvk/incl/cvk/debug/debug_messenger.hpp b/cvk/incl/cvk/debug/debug_messenger.hpp index e4459dfa..68b9d717 100644 --- a/cvk/incl/cvk/debug/debug_messenger.hpp +++ b/cvk/incl/cvk/debug/debug_messenger.hpp @@ -12,6 +12,17 @@ namespace cvk { class Instance; +/** + * @brief writes the message to cth::log + * @return VK_FALSE, the call that caused the message continues + */ +VKAPI_ATTR VkBool32 VKAPI_CALL default_debug_callback( + VkDebugUtilsMessageSeverityFlagBitsEXT, + VkDebugUtilsMessageTypeFlagsEXT, + VkDebugUtilsMessengerCallbackDataEXT const*, + void* userData +); + /** * @brief VkDebugUtilsMessengerEXT wrapper */ @@ -22,7 +33,6 @@ class DebugMessenger { struct State; using callback_t = PFN_vkDebugUtilsMessengerCallbackEXT; - /** * @brief base constructor * @pre @ref Instance::created() @@ -45,7 +55,6 @@ class DebugMessenger { */ explicit DebugMessenger(Instance const&, Config, create_t); - /** * @note calls @ref optDestroy() */ @@ -68,14 +77,12 @@ class DebugMessenger { */ void create(); - /** * @brief destroys and forgets * @pre @ref created() */ void destroy(); - /** * @brief if @ref created() -> calls @ref destroy() */ @@ -89,35 +96,35 @@ class DebugMessenger { */ State release(); - struct Config { - callback_t callback = defaultCallback(); - VkDebugUtilsMessageSeverityFlagsEXT messageSeverities = constants::DEBUG_MESSAGE_SEVERITY; - VkDebugUtilsMessageTypeFlagsEXT messageTypes = constants::DEBUG_MESSAGE_TYPE; - /** - * @param callback nullptr -> @ref defaultCallback() + * @pre != nullptr */ - static Config Default(callback_t callback = nullptr); - - /** - * @brief writes the message to cth::log - */ - static callback_t defaultCallback(); + callback_t callback = &default_debug_callback; + VkDebugUtilsMessageSeverityFlagsEXT messageSeverities = constants::DEBUG_MESSAGE_SEVERITY; + VkDebugUtilsMessageTypeFlagsEXT messageTypes = constants::DEBUG_MESSAGE_TYPE; - [[nodiscard]] VkDebugUtilsMessengerCreateInfoEXT createInfo() const; + [[nodiscard]] constexpr VkDebugUtilsMessengerCreateInfoEXT createInfo() const { + return VkDebugUtilsMessengerCreateInfoEXT{ + .sType = VK_STRUCTURE_TYPE_DEBUG_UTILS_MESSENGER_CREATE_INFO_EXT, + .pNext = nullptr, + .flags = 0, + .messageSeverity = messageSeverities, + .messageType = messageTypes, + .pfnUserCallback = callback, + .pUserData = nullptr, + }; + } }; [[nodiscard]] constexpr bool created() const { return static_cast(_handle); } [[nodiscard]] constexpr vk_handle_t get() const { return _handle.get(); } [[nodiscard]] constexpr Config config() const { return _config; } -protected: - Config _config; - private: void forget(); + Config _config; instance_handle _handle; public: diff --git a/cvk/src/base/instance.cpp b/cvk/src/base/instance.cpp index 29db244a..d62ad8ee 100644 --- a/cvk/src/base/instance.cpp +++ b/cvk/src/base/instance.cpp @@ -56,7 +56,7 @@ void Instance::create(std::optional messengerConfig) { for(string_view const extension : VALIDATION_LAYER_EXTENSIONS) if(!std::ranges::contains(requiredExtensions, extension)) requiredExtensions.emplace_back(extension); - if(!messengerConfig.has_value()) messengerConfig = DebugMessenger::Config::Default(); + if(!messengerConfig.has_value()) messengerConfig = DebugMessenger::Config{}; } auto const missingExtensions = missing(requiredExtensions, available_instance_extensions()); diff --git a/cvk/src/debug/debug_messenger.cpp b/cvk/src/debug/debug_messenger.cpp index e502f49d..cccb8baa 100644 --- a/cvk/src/debug/debug_messenger.cpp +++ b/cvk/src/debug/debug_messenger.cpp @@ -71,35 +71,6 @@ DebugMessenger::State DebugMessenger::release() { void DebugMessenger::forget() {} } -namespace cvk::dev { -VKAPI_ATTR VkBool32 VKAPI_CALL default_debug_callback( - VkDebugUtilsMessageSeverityFlagBitsEXT messageSeverity, - VkDebugUtilsMessageTypeFlagsEXT messageType, - VkDebugUtilsMessengerCallbackDataEXT const* callbackData, - void* userData -); -} - -namespace cvk { -DebugMessenger::Config DebugMessenger::Config::Default(callback_t callback) { - return Config{callback == nullptr ? defaultCallback() : callback}; -} - -DebugMessenger::callback_t DebugMessenger::Config::defaultCallback() { return dev::default_debug_callback; } - -VkDebugUtilsMessengerCreateInfoEXT DebugMessenger::Config::createInfo() const { - return VkDebugUtilsMessengerCreateInfoEXT{ - .sType = VK_STRUCTURE_TYPE_DEBUG_UTILS_MESSENGER_CREATE_INFO_EXT, - .pNext = nullptr, - .flags = 0, - .messageSeverity = messageSeverities, - .messageType = messageTypes, - .pfnUserCallback = callback, - .pUserData = nullptr, - }; -} -} - namespace cvk::dev { struct component_info { @@ -150,20 +121,21 @@ namespace { return joiner; } } +} +namespace cvk { VKAPI_ATTR VkBool32 VKAPI_CALL default_debug_callback( VkDebugUtilsMessageSeverityFlagBitsEXT messageSeverity, VkDebugUtilsMessageTypeFlagsEXT messageType, VkDebugUtilsMessengerCallbackDataEXT const* callbackData, [[maybe_unused]] void* userData ) { - auto const severity = to_severity(messageSeverity); + auto const severity = dev::to_severity(messageSeverity); - auto type = to_msg(messageType); + auto type = dev::to_msg(messageType); if(type.empty()) type = "UNKNOWN"; - - std::vector objects{}; + std::vector objects{}; objects.reserve(callbackData->objectCount); for(auto const& object : std::span{callbackData->pObjects, callbackData->objectCount}) { @@ -188,7 +160,6 @@ VKAPI_ATTR VkBool32 VKAPI_CALL default_debug_callback( objects ); - return VK_FALSE; } } diff --git a/cvk/tests/src/debug/debug_messenger_test.cpp b/cvk/tests/src/debug/debug_messenger_test.cpp index 67689b01..bf4f8189 100644 --- a/cvk/tests/src/debug/debug_messenger_test.cpp +++ b/cvk/tests/src/debug/debug_messenger_test.cpp @@ -48,11 +48,20 @@ namespace { } } +CVK_TEST(DebugMessenger, default_config_create_info) { + constexpr auto info = DebugMessenger::Config{}.createInfo(); + + static_assert(info.sType == VK_STRUCTURE_TYPE_DEBUG_UTILS_MESSENGER_CREATE_INFO_EXT); + static_assert(info.pfnUserCallback == &default_debug_callback); + static_assert(info.messageSeverity == constants::DEBUG_MESSAGE_SEVERITY); + static_assert(info.messageType == constants::DEBUG_MESSAGE_TYPE); +} + CVK_TEST(DebugMessenger, forwards_messages_to_the_callback) { if constexpr(!constants::ENABLE_VALIDATION_LAYERS) GTEST_SKIP() << "needs the validation layers"; Instance const instance{test::instance_config(), create}; - DebugMessenger const messenger{instance, DebugMessenger::Config::Default(count_test_messages), create}; + DebugMessenger const messenger{instance, DebugMessenger::Config{.callback = count_test_messages}, create}; ASSERT_TRUE(messenger.created()); received = 0; @@ -75,7 +84,7 @@ CVK_TEST(DebugMessenger, accepts_a_lambda) { if(callbackData->pMessage == TEST_MESSAGE) ++received; return VK_FALSE; }; - DebugMessenger const messenger{instance, DebugMessenger::Config::Default(callback), create}; + DebugMessenger const messenger{instance, DebugMessenger::Config{.callback = callback}, create}; received = 0; submit_test_message(instance); @@ -87,7 +96,7 @@ CVK_TEST(DebugMessenger, destroy_resets) { if constexpr(!constants::ENABLE_VALIDATION_LAYERS) GTEST_SKIP() << "needs the validation layers"; Instance const instance{test::instance_config(), create}; - DebugMessenger messenger{instance, DebugMessenger::Config::Default(count_test_messages), create}; + DebugMessenger messenger{instance, DebugMessenger::Config{.callback = count_test_messages}, create}; messenger.destroy(); EXPECT_FALSE(messenger.created()); @@ -102,7 +111,7 @@ CVK_TEST(DebugMessenger, release_keeps_the_messenger) { if constexpr(!constants::ENABLE_VALIDATION_LAYERS) GTEST_SKIP() << "needs the validation layers"; Instance const instance{test::instance_config(), create}; - DebugMessenger messenger{instance, DebugMessenger::Config::Default(count_test_messages), create}; + DebugMessenger messenger{instance, DebugMessenger::Config{.callback = count_test_messages}, create}; auto const state = messenger.release(); EXPECT_FALSE(messenger.created()); @@ -118,7 +127,7 @@ CVK_TEST(DebugMessenger, release_and_wrap) { if constexpr(!constants::ENABLE_VALIDATION_LAYERS) GTEST_SKIP() << "needs the validation layers"; Instance const instance{test::instance_config(), create}; - DebugMessenger messenger{instance, DebugMessenger::Config::Default(count_test_messages), create}; + DebugMessenger messenger{instance, DebugMessenger::Config{.callback = count_test_messages}, create}; auto const vkMessenger = messenger.get(); auto const state = messenger.release(); @@ -138,8 +147,8 @@ CVK_TEST(DebugMessenger, move_assignment_destroys_the_old_messenger) { test::reset_validation_errors(); { Instance const instance{test::instance_config(), create, test::counting_messenger()}; - DebugMessenger messenger{instance, DebugMessenger::Config::Default(count_test_messages), create}; - DebugMessenger other{instance, DebugMessenger::Config::Default(count_test_messages), create}; + DebugMessenger messenger{instance, DebugMessenger::Config{.callback = count_test_messages}, create}; + DebugMessenger other{instance, DebugMessenger::Config{.callback = count_test_messages}, create}; auto const vkMessenger = other.get(); messenger = std::move(other); diff --git a/cvk/tests/src/validation.cpp b/cvk/tests/src/validation.cpp index c8e9783a..e2a053a8 100644 --- a/cvk/tests/src/validation.cpp +++ b/cvk/tests/src/validation.cpp @@ -12,11 +12,11 @@ namespace { void* userData ) { if(severity >= VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT) ++errors; - return DebugMessenger::Config::defaultCallback()(severity, types, callbackData, userData); + return default_debug_callback(severity, types, callbackData, userData); } } -DebugMessenger::Config counting_messenger() { return DebugMessenger::Config::Default(count_errors); } +DebugMessenger::Config counting_messenger() { return DebugMessenger::Config{.callback = count_errors}; } size_t validation_errors() { return errors; } From 0061f19317c32bf845df2d83b39e3159821c8f01 Mon Sep 17 00:00:00 2001 From: lukas <89145039+codethinki@users.noreply.github.com> Date: Wed, 21 Oct 2026 13:04:13 +0200 Subject: [PATCH 117/135] cvk: free constexpr mip_level_count and mip_extent --- cvk/incl/cvk/data/img/image.hpp | 29 +++++++++++++----- cvk/src/data/img/image.cpp | 10 +++---- cvk/src/render/cmd/cmd_buffer.cpp | 4 +-- cvk/tests/src/data/img/image_test.cpp | 34 ++++++++++++++++------ cvk/tests/src/data/img/image_view_test.cpp | 2 +- cvk/tests/src/render/sync/barrier_test.cpp | 2 +- jvk/prework/image_layouts_test.cpp | 2 +- 7 files changed, 54 insertions(+), 29 deletions(-) diff --git a/cvk/incl/cvk/data/img/image.hpp b/cvk/incl/cvk/data/img/image.hpp index c2a51172..b29456f2 100644 --- a/cvk/incl/cvk/data/img/image.hpp +++ b/cvk/incl/cvk/data/img/image.hpp @@ -13,6 +13,8 @@ #include #include +#include +#include #include @@ -152,14 +154,6 @@ class Image { VkFilter filter = VK_FILTER_LINEAR ) const; - /** - * @return mip levels down to 1x1 - * @pre width > 0, height > 0 - */ - [[nodiscard]] static constexpr uint32_t evalMipLevelCount(VkExtent2D extent) { - return static_cast(std::bit_width(std::max(extent.width, extent.height))); - } - static void destroy(DeviceTable table, VkImage); [[nodiscard]] constexpr bool created() const { return static_cast(_handle); } @@ -213,6 +207,25 @@ class Image { static constexpr void debug_check(Image const&); }; + +/** + * @return mip levels down to 1x1 + * @pre width > 0, height > 0 + */ +[[nodiscard]] constexpr uint32_t mip_level_count(VkExtent2D extent) { + return static_cast(std::bit_width(std::max(extent.width, extent.height))); +} + +/** + * @return extent of the mip level, at least 1x1, also beyond @ref mip_level_count() + */ +[[nodiscard]] constexpr VkExtent2D mip_extent(VkExtent2D extent, uint32_t mipLevel) { + // a shift by the bit count or more is undefined + auto const shrink = [mipLevel](uint32_t size) { + return mipLevel < std::numeric_limits::digits ? std::max(size >> mipLevel, 1u) : 1u; + }; + return VkExtent2D{shrink(extent.width), shrink(extent.height)}; +} } namespace cvk { diff --git a/cvk/src/data/img/image.cpp b/cvk/src/data/img/image.cpp index e3795623..26866c6f 100644 --- a/cvk/src/data/img/image.cpp +++ b/cvk/src/data/img/image.cpp @@ -177,11 +177,8 @@ void Image::blitMipLevels( ) {} auto const corner = [this](uint32_t level) { - return VkOffset3D{ - static_cast(std::max(_extent.width >> level, 1u)), - static_cast(std::max(_extent.height >> level, 1u)), - 1, - }; + auto const extent = mip_extent(_extent, level); + return VkOffset3D{static_cast(extent.width), static_cast(extent.height), 1}; }; // the level is written, the next blit reads it auto const toSrc = [&](uint32_t level) { @@ -246,6 +243,7 @@ VkBufferImageCopy2 Image::bufferCopy( CTH_CRITICAL(mipLevel >= mipLevels(), "mip level must lie in the image") {} CTH_CRITICAL(!std::has_single_bit(aspectMask()), "a copy needs exactly one aspect") {} + auto const extent = mip_extent(_extent, mipLevel); return VkBufferImageCopy2{ .sType = VK_STRUCTURE_TYPE_BUFFER_IMAGE_COPY_2, .pNext = nullptr, @@ -259,7 +257,7 @@ VkBufferImageCopy2 Image::bufferCopy( .layerCount = arrayLayers(), }, .imageOffset = {0, 0, 0}, - .imageExtent = {std::max(_extent.width >> mipLevel, 1u), std::max(_extent.height >> mipLevel, 1u), 1}, + .imageExtent = {extent.width, extent.height, 1}, }; } diff --git a/cvk/src/render/cmd/cmd_buffer.cpp b/cvk/src/render/cmd/cmd_buffer.cpp index 2c148182..a7452b0b 100644 --- a/cvk/src/render/cmd/cmd_buffer.cpp +++ b/cvk/src/render/cmd/cmd_buffer.cpp @@ -75,9 +75,7 @@ namespace { } VkExtent2D view_extent(ImageView const& view) { - auto const mip = view.range().baseMipLevel; - auto const extent = view.image().extent(); - return {std::max(extent.width >> mip, 1u), std::max(extent.height >> mip, 1u)}; + return mip_extent(view.image().extent(), view.range().baseMipLevel); } void debug_check_secondary(SecondaryCmdBuffer const& secondary) { diff --git a/cvk/tests/src/data/img/image_test.cpp b/cvk/tests/src/data/img/image_test.cpp index eb3d498c..4a67f954 100644 --- a/cvk/tests/src/data/img/image_test.cpp +++ b/cvk/tests/src/data/img/image_test.cpp @@ -24,7 +24,7 @@ namespace { .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, .format = VK_FORMAT_R8G8B8A8_UNORM, .usage = VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT, - .mipLevels = Image::evalMipLevelCount(EXTENT), + .mipLevels = mip_level_count(EXTENT), }; /** @@ -44,7 +44,7 @@ namespace { .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, .format = VK_FORMAT_R8G8B8A8_UNORM, .usage = VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT, - .mipLevels = Image::evalMipLevelCount(EXTENT), + .mipLevels = mip_level_count(EXTENT), }; constexpr BaseBuffer::Config STAGING_CONFIG{ @@ -58,12 +58,28 @@ namespace { }; } -CVK_TEST(Image, eval_mip_level_count) { - static_assert(Image::evalMipLevelCount({1, 1}) == 1); - static_assert(Image::evalMipLevelCount({2, 1}) == 2); - static_assert(Image::evalMipLevelCount({256, 256}) == 9); - static_assert(Image::evalMipLevelCount({300, 200}) == 9); - static_assert(Image::evalMipLevelCount({1, 1024}) == 11); +CVK_TEST(Image, mip_level_count) { + static_assert(mip_level_count({1, 1}) == 1); + static_assert(mip_level_count({2, 1}) == 2); + static_assert(mip_level_count({256, 256}) == 9); + static_assert(mip_level_count({300, 200}) == 9); + static_assert(mip_level_count({1, 1024}) == 11); +} + +CVK_TEST(Image, mip_extent) { + static_assert(mip_extent({1, 1}, 0) == VkExtent2D{1, 1}); + static_assert(mip_extent({1, 1}, 3) == VkExtent2D{1, 1}); + static_assert(mip_extent({256, 256}, 0) == VkExtent2D{256, 256}); + static_assert(mip_extent({256, 256}, 8) == VkExtent2D{1, 1}); + static_assert(mip_extent({300, 200}, 1) == VkExtent2D{150, 100}); + static_assert(mip_extent({300, 200}, 3) == VkExtent2D{37, 25}); + static_assert(mip_extent({1, 1024}, 4) == VkExtent2D{1, 64}); + static_assert(mip_extent({1024, 4}, 4) == VkExtent2D{64, 1}); + + // levels beyond the count, also beyond the bit count of uint32_t + static_assert(mip_extent({300, 200}, mip_level_count({300, 200})) == VkExtent2D{1, 1}); + static_assert(mip_extent({4096, 4096}, 32) == VkExtent2D{1, 1}); + static_assert(mip_extent({4096, 4096}, 100) == VkExtent2D{1, 1}); } CVK_TEST(Image, format_aspects) { @@ -312,7 +328,7 @@ CVK_TEST(Image, cube_map_copies_and_blits_every_face) { constexpr VkExtent2D extent{8, 8}; Image const image{ context.core, - Image::Config::CubeMap(VK_FORMAT_R8G8B8A8_UNORM, Image::evalMipLevelCount(extent)), + Image::Config::CubeMap(VK_FORMAT_R8G8B8A8_UNORM, mip_level_count(extent)), extent }; diff --git a/cvk/tests/src/data/img/image_view_test.cpp b/cvk/tests/src/data/img/image_view_test.cpp index c6e1a98e..d6990fb6 100644 --- a/cvk/tests/src/data/img/image_view_test.cpp +++ b/cvk/tests/src/data/img/image_view_test.cpp @@ -13,7 +13,7 @@ namespace { .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, .format = VK_FORMAT_R8G8B8A8_UNORM, .usage = VK_IMAGE_USAGE_SAMPLED_BIT, - .mipLevels = Image::evalMipLevelCount(EXTENT), + .mipLevels = mip_level_count(EXTENT), }; } diff --git a/cvk/tests/src/render/sync/barrier_test.cpp b/cvk/tests/src/render/sync/barrier_test.cpp index 9bc427a2..b0a2d6ca 100644 --- a/cvk/tests/src/render/sync/barrier_test.cpp +++ b/cvk/tests/src/render/sync/barrier_test.cpp @@ -17,7 +17,7 @@ namespace { .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, .format = VK_FORMAT_R8G8B8A8_UNORM, .usage = VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT, - .mipLevels = Image::evalMipLevelCount(EXTENT), + .mipLevels = mip_level_count(EXTENT), }; constexpr BarrierScope TRANSFER_WRITE{VK_PIPELINE_STAGE_2_TRANSFER_BIT, VK_ACCESS_2_TRANSFER_WRITE_BIT}; diff --git a/jvk/prework/image_layouts_test.cpp b/jvk/prework/image_layouts_test.cpp index c1fda747..080ce3f4 100644 --- a/jvk/prework/image_layouts_test.cpp +++ b/jvk/prework/image_layouts_test.cpp @@ -15,7 +15,7 @@ namespace { .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, .format = VK_FORMAT_R8G8B8A8_UNORM, .usage = VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT, - .mipLevels = Image::evalMipLevelCount(EXTENT), + .mipLevels = mip_level_count(EXTENT), }; } From 680887f7fddec5486fcbdfd6c0f6d9c42557c698 Mon Sep 17 00:00:00 2001 From: lukas <89145039+codethinki@users.noreply.github.com> Date: Wed, 21 Oct 2026 13:04:13 +0200 Subject: [PATCH 118/135] cvk: Cvk is a reference counted loader object on cth::co::global_switch --- cvk/incl/cvk/base/cvk.hpp | 38 ++++++++++++++++++++++++--------- cvk/src/base/cvk.cpp | 23 ++++++-------------- cvk/tests/src/base/cvk_test.cpp | 33 ++++++++++++++++++++++++++++ cvk/tests/src/environment.cpp | 12 ++++++++--- 4 files changed, 77 insertions(+), 29 deletions(-) create mode 100644 cvk/tests/src/base/cvk_test.cpp diff --git a/cvk/incl/cvk/base/cvk.hpp b/cvk/incl/cvk/base/cvk.hpp index 8aa76154..4cf3a6cd 100644 --- a/cvk/incl/cvk/base/cvk.hpp +++ b/cvk/incl/cvk/base/cvk.hpp @@ -1,29 +1,47 @@ #pragma once +#include namespace cvk { /** - * @brief loads and unloads the vulkan loader + * @brief loads the vulkan loader while a Cvk exists, neither copyable nor movable + * @details reference counted like std::ios_base::Init: every Cvk shares the one loader of the process, + * the first Cvk loads it, the last one unloads it, + * the Cvk outlives every instance + * @attention not thread safe */ class Cvk { public: /** - * @brief loads the vulkan loader through volk + * @brief the first Cvk loads the vulkan loader through volk * @post @ref initialized() * @throws cvk::vk_result_exception result of volkInitialize() */ - static void init(); + Cvk(); /** - * @brief unloads the vulkan loader - * @post !@ref initialized() + * @brief the last Cvk unloads the vulkan loader + * @pre the last Cvk -> no instance exists */ - static void terminate(); + ~Cvk(); - [[nodiscard]] static bool initialized() { return _initialized; } + /** + * @return a Cvk exists + */ + [[nodiscard]] static bool initialized() { return _loader.acquired(); } private: - inline static bool _initialized = false; - static void terminateVolk(); - static void initVolk(); + /** + * @throws cvk::vk_result_exception result of volkInitialize() + */ + static void load(); + static void unload(); + + inline static cth::co::global_switch _loader{&load, &unload}; + +public: + Cvk(Cvk const& other) = delete; + Cvk(Cvk&& other) = delete; + Cvk& operator=(Cvk const& other) = delete; + Cvk& operator=(Cvk&& other) = delete; }; } diff --git a/cvk/src/base/cvk.cpp b/cvk/src/base/cvk.cpp index 8a3fe6e8..d7ae2a61 100644 --- a/cvk/src/base/cvk.cpp +++ b/cvk/src/base/cvk.cpp @@ -6,29 +6,20 @@ namespace cvk { -void Cvk::terminate() { - log::msg("terminating cvk..."); +Cvk::Cvk() { _loader.acquire(); } - terminateVolk(); +Cvk::~Cvk() { _loader.release(); } - log::msg("cvk terminated"); - _initialized = false; -} - -void Cvk::terminateVolk() { volkFinalize(); } - -void Cvk::initVolk() { +void Cvk::load() { auto const result = volkInitialize(); CVK_RESULT_STABLE_THROW(result != VK_SUCCESS, result, "failed to load vulkan") {} - log::msg("loaded vulkan (volk)"); + log::msg("cvk initialized, loaded vulkan (volk)"); } -void Cvk::init() { - log::msg("cvk initializing..."); - initVolk(); +void Cvk::unload() { + volkFinalize(); - log::msg("cvk initialized"); - _initialized = true; + log::msg("cvk terminated, unloaded vulkan (volk)"); } } diff --git a/cvk/tests/src/base/cvk_test.cpp b/cvk/tests/src/base/cvk_test.cpp new file mode 100644 index 00000000..39c64cbc --- /dev/null +++ b/cvk/tests/src/base/cvk_test.cpp @@ -0,0 +1,33 @@ +#include "test.hpp" + +#include "cvk/base/cvk.hpp" + +#include + +#include + + +namespace cvk { +CVK_TEST(Cvk, is_not_copyable_or_movable) { + static_assert(!std::is_copy_constructible_v && !std::is_copy_assignable_v); + static_assert(!std::is_move_constructible_v && !std::is_move_assignable_v); +} + +CVK_TEST(Cvk, nested_objects_keep_the_loader) { + // the test environment holds a Cvk, the count never reaches 0 here + ASSERT_TRUE(Cvk::initialized()); + { + Cvk const outer{}; + { + Cvk const inner{}; + EXPECT_TRUE(Cvk::initialized()); + } + EXPECT_TRUE(Cvk::initialized()); + } + EXPECT_TRUE(Cvk::initialized()); + + uint32_t version = 0; + ASSERT_NE(vkEnumerateInstanceVersion, nullptr); + EXPECT_EQ(vkEnumerateInstanceVersion(&version), VK_SUCCESS); +} +} diff --git a/cvk/tests/src/environment.cpp b/cvk/tests/src/environment.cpp index a69985b7..29d66f29 100644 --- a/cvk/tests/src/environment.cpp +++ b/cvk/tests/src/environment.cpp @@ -4,19 +4,25 @@ #include +#include + namespace cvk::test { namespace { /** - * @brief loads the vulkan loader once for all tests + * @brief holds the vulkan loader for all tests + * @details the shared contexts are destroyed before the loader */ class Environment : public testing::Environment { public: - void SetUp() override { Cvk::init(); } + void SetUp() override { _cvk.emplace(); } void TearDown() override { destroy_shared_contexts(); - Cvk::terminate(); + _cvk.reset(); } + + private: + std::optional _cvk{}; }; [[maybe_unused]] testing::Environment* const ENVIRONMENT = testing::AddGlobalTestEnvironment(new Environment{}); From c686ae34f8ea5bb3702992b377ed2c22884fa179 Mon Sep 17 00:00:00 2001 From: lukas <89145039+codethinki@users.noreply.github.com> Date: Wed, 21 Oct 2026 13:04:18 +0200 Subject: [PATCH 119/135] AGENTS: CubeMap() as the named constructor example --- AGENTS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AGENTS.md b/AGENTS.md index 2ca0a1b5..e7cc2af5 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -75,7 +75,7 @@ ctest --test-dir out/build/gcc_release --output-on-failure | generic library type, trait, concept | `snake_case` | `basic_deletable`, `handle_t`, `destroyable` | | type alias that renames a type | `snake_case_t` | `callback_t`, `queue_set_t` | | member function | `camelCase` | `optDestroy()`, `createInfo()` | -| named constructor (static factory) | `PascalCase` | `Default()`, `DepthBuffer()`, `LayoutTransition()` | +| named constructor (static factory) | `PascalCase` | `CubeMap()`, `DepthBuffer()`, `LayoutTransition()` | | free function | `snake_case` | `to_queue_properties()`, `default_debug_callback()` | | static check function | `debug_check`, `debug_check_handle` | | | function parameter | `camelCase` | `appName`, `requiredExtensions` | From 9c182da4a95c8e710f95f098817c82735726f8d5 Mon Sep 17 00:00:00 2001 From: lukas <89145039+codethinki@users.noreply.github.com> Date: Wed, 21 Oct 2026 13:07:53 +0200 Subject: [PATCH 120/135] cvk: the Cvk loader switch is a function local static, no static init order problem --- cvk/incl/cvk/base/cvk.hpp | 15 +++++++++++---- cvk/src/base/cvk.cpp | 13 +++++++++++-- 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/cvk/incl/cvk/base/cvk.hpp b/cvk/incl/cvk/base/cvk.hpp index 4cf3a6cd..40c6b0e7 100644 --- a/cvk/incl/cvk/base/cvk.hpp +++ b/cvk/incl/cvk/base/cvk.hpp @@ -1,5 +1,8 @@ #pragma once -#include + +namespace cth::co { +class global_switch; +} namespace cvk { /** @@ -27,17 +30,21 @@ class Cvk { /** * @return a Cvk exists */ - [[nodiscard]] static bool initialized() { return _loader.acquired(); } + [[nodiscard]] static bool initialized(); private: + /** + * @return the switch that loads and unloads the loader + * @details a function local static: a Cvk at namespace scope in another translation unit finds it constructed + */ + [[nodiscard]] static cth::co::global_switch& loader(); + /** * @throws cvk::vk_result_exception result of volkInitialize() */ static void load(); static void unload(); - inline static cth::co::global_switch _loader{&load, &unload}; - public: Cvk(Cvk const& other) = delete; Cvk(Cvk&& other) = delete; diff --git a/cvk/src/base/cvk.cpp b/cvk/src/base/cvk.cpp index d7ae2a61..429f27c9 100644 --- a/cvk/src/base/cvk.cpp +++ b/cvk/src/base/cvk.cpp @@ -2,13 +2,22 @@ #include "cvk/utility/vk_exceptions.hpp" +#include #include namespace cvk { -Cvk::Cvk() { _loader.acquire(); } +Cvk::Cvk() { loader().acquire(); } -Cvk::~Cvk() { _loader.release(); } +Cvk::~Cvk() { loader().release(); } + +bool Cvk::initialized() { return loader().acquired(); } + +cth::co::global_switch& Cvk::loader() { + // constructed at the first call, also when the first call comes from the static initialization of another unit + static cth::co::global_switch loaderSwitch{&load, &unload}; + return loaderSwitch; +} void Cvk::load() { auto const result = volkInitialize(); From 752716f9cc855cbcd2f092c5b61ec05a191e1f41 Mon Sep 17 00:00:00 2001 From: lukas <89145039+codethinki@users.noreply.github.com> Date: Wed, 21 Oct 2026 13:07:53 +0200 Subject: [PATCH 121/135] cvk: the messenger config checks its callback, barrier test line width --- cvk/incl/cvk/debug/debug_messenger.hpp | 6 ++++++ cvk/src/debug/debug_messenger.cpp | 5 ++++- cvk/tests/src/render/sync/barrier_test.cpp | 6 +++++- 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/cvk/incl/cvk/debug/debug_messenger.hpp b/cvk/incl/cvk/debug/debug_messenger.hpp index 68b9d717..ac62631e 100644 --- a/cvk/incl/cvk/debug/debug_messenger.hpp +++ b/cvk/incl/cvk/debug/debug_messenger.hpp @@ -115,6 +115,8 @@ class DebugMessenger { .pUserData = nullptr, }; } + + static constexpr void debug_check(Config const&); }; [[nodiscard]] constexpr bool created() const { return static_cast(_handle); } @@ -147,4 +149,8 @@ namespace cvk { constexpr void DebugMessenger::debug_check(DebugMessenger const& debugMessenger) { CTH_CRITICAL(!debugMessenger.created(), "debug_messenger not created") {} } + +constexpr void DebugMessenger::Config::debug_check(Config const& config) { + CTH_CRITICAL(config.callback == nullptr, "callback must not be nullptr") {} +} } diff --git a/cvk/src/debug/debug_messenger.cpp b/cvk/src/debug/debug_messenger.cpp index cccb8baa..796bf2e5 100644 --- a/cvk/src/debug/debug_messenger.cpp +++ b/cvk/src/debug/debug_messenger.cpp @@ -10,7 +10,10 @@ namespace cvk { DebugMessenger::DebugMessenger(Instance const& instance, Config config) : _config{std::move(config)}, - _handle{instance.get()} { Instance::debug_check(instance); } + _handle{instance.get()} { + Instance::debug_check(instance); + Config::debug_check(_config); +} DebugMessenger::DebugMessenger(Instance const& instance, Config config, State const& state) : DebugMessenger{ instance, diff --git a/cvk/tests/src/render/sync/barrier_test.cpp b/cvk/tests/src/render/sync/barrier_test.cpp index b0a2d6ca..c85ac3db 100644 --- a/cvk/tests/src/render/sync/barrier_test.cpp +++ b/cvk/tests/src/render/sync/barrier_test.cpp @@ -30,7 +30,11 @@ namespace { } CVK_TEST(Barrier, layout_transition_from_undefined_waits_for_the_dst_stages) { - constexpr auto toTransfer = layout_transition(VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_UNDEFINED, COLOR); + constexpr auto toTransfer = layout_transition( + VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, + VK_IMAGE_LAYOUT_UNDEFINED, + COLOR + ); static_assert(toTransfer.has_value()); static_assert(toTransfer->src.stages == VK_PIPELINE_STAGE_2_TRANSFER_BIT); static_assert(toTransfer->src.access == VK_ACCESS_2_NONE); From 26b3a90e69bb0c90fd4474ad3d300568d8bb4ae3 Mon Sep 17 00:00:00 2001 From: lukas <89145039+codethinki@users.noreply.github.com> Date: Wed, 21 Oct 2026 13:19:55 +0200 Subject: [PATCH 122/135] lib: cth with the thread safe global_switch, Cvk is thread safe --- cvk/incl/cvk/base/cvk.hpp | 3 +-- lib/cth | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/cvk/incl/cvk/base/cvk.hpp b/cvk/incl/cvk/base/cvk.hpp index 40c6b0e7..14cb9762 100644 --- a/cvk/incl/cvk/base/cvk.hpp +++ b/cvk/incl/cvk/base/cvk.hpp @@ -8,9 +8,8 @@ namespace cvk { /** * @brief loads the vulkan loader while a Cvk exists, neither copyable nor movable * @details reference counted like std::ios_base::Init: every Cvk shares the one loader of the process, - * the first Cvk loads it, the last one unloads it, + * the first Cvk loads it, the last one unloads it, thread safe, * the Cvk outlives every instance - * @attention not thread safe */ class Cvk { public: diff --git a/lib/cth b/lib/cth index 9b0417a3..9e94b4a5 160000 --- a/lib/cth +++ b/lib/cth @@ -1 +1 @@ -Subproject commit 9b0417a3e10bce820361f91a51a7b064425c4db4 +Subproject commit 9e94b4a5b245883b2087a60d8614adc0290c8d11 From a943e9e679be3b5cf539e9ae58370a40f583298c Mon Sep 17 00:00:00 2001 From: lukas <89145039+codethinki@users.noreply.github.com> Date: Wed, 21 Oct 2026 13:43:42 +0200 Subject: [PATCH 123/135] ci: GitHub Actions on the cth pattern, linux tests on lavapipe, windows build only --- .github/actions/build-container/action.yml | 48 +++++++++++++ .github/actions/build-engine/action.yml | 49 +++++++++++++ .github/actions/restore-cache/action.yml | 73 ++++++++++++++++++++ .github/actions/save-cache/action.yml | 58 ++++++++++++++++ .github/actions/setup-vcpkg-cache/action.yml | 42 +++++++++++ .github/actions/setup-windows/action.yml | 47 +++++++++++++ .github/actions/test-engine/action.yml | 13 ++++ .github/workflows/build-test.yml | 36 ++++++++++ .github/workflows/linux-pipeline.yml | 62 +++++++++++++++++ .github/workflows/windows-pipeline.yml | 36 ++++++++++ ci/docker/linux/Dockerfile | 29 ++++++++ 11 files changed, 493 insertions(+) create mode 100644 .github/actions/build-container/action.yml create mode 100644 .github/actions/build-engine/action.yml create mode 100644 .github/actions/restore-cache/action.yml create mode 100644 .github/actions/save-cache/action.yml create mode 100644 .github/actions/setup-vcpkg-cache/action.yml create mode 100644 .github/actions/setup-windows/action.yml create mode 100644 .github/actions/test-engine/action.yml create mode 100644 .github/workflows/build-test.yml create mode 100644 .github/workflows/linux-pipeline.yml create mode 100644 .github/workflows/windows-pipeline.yml create mode 100644 ci/docker/linux/Dockerfile diff --git a/.github/actions/build-container/action.yml b/.github/actions/build-container/action.yml new file mode 100644 index 00000000..a2921688 --- /dev/null +++ b/.github/actions/build-container/action.yml @@ -0,0 +1,48 @@ +name: 'Build Container' +description: 'Builds and pushes the Linux Docker image' +inputs: + dockerfile: + description: 'Path to the Dockerfile' + required: true + tag: + description: 'Tag suffix for the image' + required: true + image-subdir: + description: 'Subdirectory to push image to' + default: 'cache' + +outputs: + container-image-adress: + description: Address of container image + value: ${{ steps.set-image.outputs.image }} + +runs: + using: "composite" + steps: + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v4 + + - name: Set image names + id: set-image + shell: bash + run: | + image="ghcr.io/${{ github.repository }}/${{ inputs.image-subdir }}:${{ inputs.tag }}" + echo "image=$image" >> $GITHUB_OUTPUT + echo "cache_image=${image}-cache" >> $GITHUB_OUTPUT + + - name: Log in to the Container registry + uses: docker/login-action@v4 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ github.token }} + + - name: Build Docker image + uses: docker/build-push-action@v7 + with: + context: . + file: ${{ inputs.dockerfile }} + push: true + tags: ${{ steps.set-image.outputs.image }} + cache-from: type=registry,ref=${{ steps.set-image.outputs.cache_image }} + cache-to: type=registry,ref=${{ steps.set-image.outputs.cache_image }},mode=max \ No newline at end of file diff --git a/.github/actions/build-engine/action.yml b/.github/actions/build-engine/action.yml new file mode 100644 index 00000000..a9fb274b --- /dev/null +++ b/.github/actions/build-engine/action.yml @@ -0,0 +1,49 @@ +name: 'Build engine' +description: 'Handles Caching, Configuration, and Compilation' +inputs: + cache_prefix: + description: 'Cache Prefix e.g. OS' + required: true + preset: + description: 'CI preset from ci/CMakeUserPresets.json' + required: true + +runs: + using: "composite" + steps: + # --- 1. setup vcpkg cache --- + - name: setup vcpkg + uses: ./.github/actions/setup-vcpkg-cache + + # --- 2. configure --- + - name: Configure CMake + shell: bash + run: | + [ -f ci/CMakeUserPresets.json ] && cp ci/CMakeUserPresets.json CMakeUserPresets.json + cmake --preset ${{ inputs.preset }} + + # --- 3. restore BuildCache --- + - name: Restore BuildCache + id: buildcache + uses: ./.github/actions/restore-cache + with: + path: .buildcache + key: buildcache-${{ inputs.cache_prefix }}-${{ inputs.preset }}-${{ github.run_id }} + restore-keys: buildcache-${{ inputs.cache_prefix }}-${{ inputs.preset }}- + global-tag: buildcache-${{ inputs.cache_prefix }}-${{ inputs.preset }} + gh-token: ${{ github.token }} + + # --- 4. build --- + - name: Build + shell: bash + run: cmake --build --preset ${{ inputs.preset }} + + # --- 5. save BuildCache --- + - name: Save BuildCache + if: always() + uses: ./.github/actions/save-cache + with: + path: .buildcache + key: ${{ steps.buildcache.outputs.primary-key }} + global-tag: buildcache-${{ inputs.cache_prefix }}-${{ inputs.preset }} + gh-token: ${{ github.token }} diff --git a/.github/actions/restore-cache/action.yml b/.github/actions/restore-cache/action.yml new file mode 100644 index 00000000..c165d23f --- /dev/null +++ b/.github/actions/restore-cache/action.yml @@ -0,0 +1,73 @@ +name: 'Restore Cache' +description: 'Restores from GitHub Cache or GHCR' +inputs: + path: + description: 'File path to restore' + required: true + key: + description: 'Primary key for GitHub Cache' + required: true + restore-keys: + description: 'Fallback keys for GitHub Cache' + required: false + global-tag: + description: 'OCI Tag for GHCR' + required: true + gh-token: + description: 'GitHub Token for GHCR auth' + required: true + default: ${{ github.token }} + +outputs: + cache-hit: + description: 'true if cache was found in either github or GHCR' + value: ${{ steps.local-cache.outputs.cache-hit == 'true' || steps.global-cache.outputs.hit == 'true' }} + primary-key: + description: 'The key used for saving later' + value: ${{ steps.local-cache.outputs.cache-primary-key }} + +runs: + using: "composite" + steps: + # --- Local GitHub cache --- + - name: Restore local cache + id: local-cache + uses: actions/cache/restore@v6 + with: + path: ${{ inputs.path }} + key: ${{ inputs.key }} + restore-keys: ${{ inputs.restore-keys }} + + # --- Setup ORAS (Only if local missed) --- + - name: Set up ORAS + if: steps.local-cache.outputs.cache-matched-key == '' + uses: oras-project/setup-oras@v2 + + # --- Login --- + - name: Login to GHCR (Native ORAS) + if: steps.local-cache.outputs.cache-matched-key == '' + shell: bash + run: | + echo "${{ inputs.gh-token }}" | oras login ghcr.io \ + --username "${{ github.actor }}" \ + --password-stdin + + # --- Global GHCR cache --- + - name: Restore global cache + id: global-cache + if: steps.local-cache.outputs.cache-matched-key == '' + shell: bash + env: + REPO: ghcr.io/${{ github.repository }} + TAG: master-${{ inputs.global-tag }} + TARGET_PATH: ${{ inputs.path }} + run: | + echo "Local miss. Pulling from GHCR: $TAG" + if oras pull "$REPO/cache:$TAG"; then + echo "hit=true" >> $GITHUB_OUTPUT + tar -xzf cache-archive.tar.gz + rm cache-archive.tar.gz + else + echo "hit=false" >> $GITHUB_OUTPUT + echo "Global cache miss." + fi \ No newline at end of file diff --git a/.github/actions/save-cache/action.yml b/.github/actions/save-cache/action.yml new file mode 100644 index 00000000..990f2276 --- /dev/null +++ b/.github/actions/save-cache/action.yml @@ -0,0 +1,58 @@ +name: 'Save Cache' +description: 'Saves to GitHub Cache (Always) and GHCR (Only on master)' +inputs: + path: + description: 'File path to save' + required: true + key: + description: 'Key used in the restore step' + required: true + global-tag: + description: 'OCI Tag for GHCR' + required: true + gh-token: + description: 'GitHub Token for GHCR auth' + required: true + default: ${{ github.token }} + +runs: + using: "composite" + steps: + # --- Local github cache --- + - name: Save Local Cache + uses: actions/cache/save@v6 + with: + path: ${{ inputs.path }} + key: ${{ inputs.key }} + + # --- Setup for Global (Only on master) --- + - name: Set up ORAS + if: github.ref == 'refs/heads/master' + uses: oras-project/setup-oras@v2 + + # --- Login --- + - name: Login to GHCR (Native ORAS) + if: github.ref == 'refs/heads/master' + shell: bash + run: | + echo "${{ inputs.gh-token }}" | oras login ghcr.io \ + --username "${{ github.actor }}" \ + --password-stdin + + # --- Global GHCR cache (master only) --- + - name: Save Global Cache + if: github.ref == 'refs/heads/master' + shell: bash + env: + REPO: ghcr.io/${{ github.repository }} + TAG: master-${{ inputs.global-tag }} + TARGET_PATH: ${{ inputs.path }} + run: | + if [ -d "$TARGET_PATH" ]; then + echo "Compressing for Global Cache..." + tar -czf cache-archive.tar.gz "$TARGET_PATH" + + echo "Pushing to GHCR..." + oras push "$REPO/cache:$TAG" cache-archive.tar.gz + rm cache-archive.tar.gz + fi \ No newline at end of file diff --git a/.github/actions/setup-vcpkg-cache/action.yml b/.github/actions/setup-vcpkg-cache/action.yml new file mode 100644 index 00000000..b51bd6c4 --- /dev/null +++ b/.github/actions/setup-vcpkg-cache/action.yml @@ -0,0 +1,42 @@ +name: 'Vcpkg Cache' +description: 'sets up vcpkg to use NuGet for caching' +inputs: + gh-token: + description: 'GitHub Token for GHCR auth' + required: true + default: ${{ github.token }} + +runs: + using: "composite" + steps: + # --- WINDOWS: (dotnet nuget) --- + - name: setup vcpkg nuget source + if: runner.os == 'Windows' + shell: bash + run: | + dotnet nuget add source "https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json" \ + --name "GitHub" \ + --username "${{ github.repository_owner }}" \ + --password "${{ inputs.gh-token }}" \ + --store-password-in-clear-text + + # --- other: (mono + nuget.exe) --- + - name: setup vcpkg nuget source + if: runner.os != 'Windows' + shell: bash + run: | + # download nuget.exe + wget -qO nuget.exe https://dist.nuget.org/win-x86-commandline/latest/nuget.exe + NUGET_PATH="$(pwd)/nuget.exe" + + # add source + mono "$NUGET_PATH" sources add \ + -Name "GitHub" \ + -Source "https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json" \ + -UserName "${{ github.repository_owner }}" \ + -Password "${{ inputs.gh-token }}" \ + -StorePasswordInClearText + + # set API key for push + mono "$NUGET_PATH" setapikey "${{ inputs.gh-token }}" \ + -Source "https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json" \ No newline at end of file diff --git a/.github/actions/setup-windows/action.yml b/.github/actions/setup-windows/action.yml new file mode 100644 index 00000000..647a7ff0 --- /dev/null +++ b/.github/actions/setup-windows/action.yml @@ -0,0 +1,47 @@ +name: 'Setup Windows Environment' +description: 'Sets up MSVC, Vcpkg, BuildCache, CMake and Ninja' +inputs: + compiler: + description: 'compiler of the CI preset (msvc or clang)' + required: true + +runs: + using: "composite" + steps: + - uses: ilammy/msvc-dev-cmd@v1 + + - name: Setup Vcpkg Environment + shell: pwsh + run: | + $vcpkgPath = $env:VCPKG_INSTALLATION_ROOT + if (-not (Test-Path "$vcpkgPath")) { + Write-Error "Vcpkg not found at VCPKG_INSTALLATION_ROOT ($vcpkgPath)" + exit 1 + } + "VCPKG_ROOT=$vcpkgPath" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append + + - name: Install BuildCache + shell: pwsh + run: | + Invoke-WebRequest -Uri "https://gitlab.com/bits-n-bites/buildcache/-/releases/v0.31.7/downloads/buildcache-windows.zip" -OutFile "buildcache.zip" + Expand-Archive buildcache.zip -DestinationPath c:\buildcache + echo "c:\buildcache\buildcache\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append + + - name: Install LLVM + if: inputs.compiler == 'clang' + shell: pwsh + run: | + $version = "23.1.2" + $name = "clang+llvm-$version-x86_64-pc-windows-msvc" + curl.exe -fsSL -o "$env:RUNNER_TEMP\llvm.tar.xz" "https://github.com/llvm/llvm-project/releases/download/llvmorg-$version/$name.tar.xz" + cmd /c "7z x %RUNNER_TEMP%\llvm.tar.xz -so | 7z x -si -ttar -oc:\llvm > nul" + & "c:\llvm\$name\bin\clang++.exe" --version + echo "c:\llvm\$name\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append + + - uses: actions/setup-python@v7 + with: + python-version: "3.x" + + - name: Install CMake & Ninja + run: pip install --upgrade cmake ninja + shell: powershell diff --git a/.github/actions/test-engine/action.yml b/.github/actions/test-engine/action.yml new file mode 100644 index 00000000..2aaf9dba --- /dev/null +++ b/.github/actions/test-engine/action.yml @@ -0,0 +1,13 @@ +name: 'Test engine' +description: 'Runs CTest with the given CI test preset' +inputs: + preset: + description: 'CI preset from ci/CMakeUserPresets.json' + required: true + +runs: + using: "composite" + steps: + - name: Test + shell: bash + run: ctest --preset ${{ inputs.preset }} --output-on-failure diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml new file mode 100644 index 00000000..c58a0e49 --- /dev/null +++ b/.github/workflows/build-test.yml @@ -0,0 +1,36 @@ +name: Build & Test + +on: + push: { branches: ["master", "v2", "_v2/jvk_rebuild"] } + pull_request: { branches: ["master"] } + workflow_dispatch: + +permissions: { contents: read, packages: write } + +jobs: + linux: + name: Linux (${{ matrix.compiler }} ${{ matrix.config }}) + strategy: + fail-fast: false + matrix: + compiler: [gcc, clang] + # debug runs the validation layers with synchronization validation + config: [debug, release] + + uses: ./.github/workflows/linux-pipeline.yml + with: + compiler: ${{ matrix.compiler }} + config: ${{ matrix.config }} + secrets: inherit + + windows: + name: Windows (${{ matrix.compiler }}) + strategy: + fail-fast: false + matrix: + compiler: [msvc, clang] + + uses: ./.github/workflows/windows-pipeline.yml + with: + compiler: ${{ matrix.compiler }} + secrets: inherit diff --git a/.github/workflows/linux-pipeline.yml b/.github/workflows/linux-pipeline.yml new file mode 100644 index 00000000..e77e9ffd --- /dev/null +++ b/.github/workflows/linux-pipeline.yml @@ -0,0 +1,62 @@ +name: Linux Pipeline + +on: + workflow_call: + inputs: + compiler: + required: true + type: string + config: + required: true + type: string + +permissions: + contents: read + packages: write + +jobs: + prepare: + name: Build Container + runs-on: ubuntu-latest + outputs: + container-image-adress: ${{ steps.build.outputs.container-image-adress }} + steps: + - uses: actions/checkout@v7 + - id: build + name: Build & Push + uses: ./.github/actions/build-container + with: + dockerfile: ci/docker/linux/Dockerfile + tag: linux-container + + build: + name: Linux (${{ inputs.compiler }} ${{ inputs.config }}) + needs: prepare + runs-on: ubuntu-latest + # the container has no gpu: the tests run on lavapipe (VK_DRIVER_FILES in the Dockerfile), + # tests that need more queues than lavapipe has skip + container: + image: ${{ needs.prepare.outputs.container-image-adress }} + credentials: + username: ${{ github.actor }} + password: ${{ github.token }} + options: >- + -v /usr/local/share/vcpkg:/vcpkg + -e VCPKG_ROOT=/vcpkg + env: + VCPKG_ROOT: /vcpkg + steps: + - uses: actions/checkout@v7 + with: + submodules: recursive + + - name: Build + uses: ./.github/actions/build-engine + with: + cache_prefix: engine-linux + preset: ci_linux_${{ inputs.compiler }}_${{ inputs.config }} + + - name: Test + uses: ./.github/actions/test-engine + with: + preset: ci_linux_${{ inputs.compiler }}_${{ inputs.config }} diff --git a/.github/workflows/windows-pipeline.yml b/.github/workflows/windows-pipeline.yml new file mode 100644 index 00000000..da8c03e7 --- /dev/null +++ b/.github/workflows/windows-pipeline.yml @@ -0,0 +1,36 @@ +name: Windows Pipeline + +on: + workflow_call: + inputs: + compiler: + required: true + type: string + +permissions: + contents: read + packages: write + +jobs: + # build only: the runners have no vulkan driver, the tests need a device + build: + name: Windows (${{ inputs.compiler }}) + runs-on: windows-latest + defaults: + run: + shell: bash # Forces Git Bash for consistency with Linux scripts + + steps: + - uses: actions/checkout@v7 + with: + submodules: recursive + + - uses: ./.github/actions/setup-windows + with: + compiler: ${{ inputs.compiler }} + + - name: Build + uses: ./.github/actions/build-engine + with: + cache_prefix: engine-windows + preset: ci_windows_${{ inputs.compiler }} diff --git a/ci/docker/linux/Dockerfile b/ci/docker/linux/Dockerfile new file mode 100644 index 00000000..37aaa6b2 --- /dev/null +++ b/ci/docker/linux/Dockerfile @@ -0,0 +1,29 @@ +FROM cachyos/cachyos:latest + +# vulkan-swrast: lavapipe, the cpu vulkan driver of mesa +# vulkan-validation-layers: debug builds enable them with synchronization validation +# shaderc: glslc for the shader compile tests +RUN pacman -Syu --noconfirm && \ + pacman -S --noconfirm \ + base-devel \ + cmake \ + ninja \ + git \ + gcc \ + clang \ + lld \ + python \ + curl \ + wget \ + zip \ + unzip \ + tar \ + mono \ + buildcache-git \ + vulkan-icd-loader \ + vulkan-swrast \ + vulkan-validation-layers \ + shaderc + +# the tests run on lavapipe, also if another driver is installed +ENV VK_DRIVER_FILES=/usr/share/vulkan/icd.d/lvp_icd.json From 2e815b087e3d635a8798d4847d9158622ae687fa Mon Sep 17 00:00:00 2001 From: lukas <89145039+codethinki@users.noreply.github.com> Date: Wed, 21 Oct 2026 14:03:49 +0200 Subject: [PATCH 124/135] ci: commit the CI presets, the gitignore covers only the root CMakeUserPresets.json --- .gitignore | 2 +- ci/CMakeUserPresets.json | 160 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 161 insertions(+), 1 deletion(-) create mode 100644 ci/CMakeUserPresets.json diff --git a/.gitignore b/.gitignore index f6f178e0..e7bb914a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ vcpkg_installed/ out/ .vs/ -CMakeUserPresets.json +/CMakeUserPresets.json diff --git a/ci/CMakeUserPresets.json b/ci/CMakeUserPresets.json new file mode 100644 index 00000000..05bc52e3 --- /dev/null +++ b/ci/CMakeUserPresets.json @@ -0,0 +1,160 @@ +{ + "version": 3, + "configurePresets": [ + { + "name": "ci-vcpkg", + "hidden": true, + "cacheVariables": { + "VCPKG_INSTALLED_DIR": "${sourceDir}/vcpkg_installed" + }, + "environment": { + "VCPKG_BINARY_SOURCES": "clear;nuget,GitHub,readwrite" + } + }, + { + "name": "ci-buildcache", + "hidden": true, + "environment": { + "BUILDCACHE_DIR": "${sourceDir}/.buildcache", + "BUILDCACHE_ACCURACY": "SLOPPY", + "BUILDCACHE_MAX_CACHE_SIZE": "524288000" + } + }, + { + "name": "ci-config-base", + "hidden": true, + "inherits": [ + "ci-vcpkg", + "ci-buildcache" + ] + }, + { + "name": "ci_linux_gcc_debug", + "inherits": [ + "ci-config-base", + "gcc_debug" + ] + }, + { + "name": "ci_linux_gcc_release", + "inherits": [ + "ci-config-base", + "gcc_release" + ] + }, + { + "name": "ci_linux_clang_debug", + "inherits": [ + "ci-config-base", + "clang_debug" + ] + }, + { + "name": "ci_linux_clang_release", + "inherits": [ + "ci-config-base", + "clang_release" + ] + }, + { + "name": "ci_windows_msvc", + "inherits": [ + "ci-config-base", + "msvc_release" + ] + }, + { + "name": "ci_windows_clang", + "inherits": [ + "ci-config-base", + "clang_release" + ] + } + ], + "buildPresets": [ + { + "name": "ci-build-debug", + "hidden": true, + "configuration": "Debug" + }, + { + "name": "ci-build-release", + "hidden": true, + "configuration": "Release" + }, + { + "name": "ci_linux_gcc_debug", + "configurePreset": "ci_linux_gcc_debug", + "inherits": "ci-build-debug" + }, + { + "name": "ci_linux_gcc_release", + "configurePreset": "ci_linux_gcc_release", + "inherits": "ci-build-release" + }, + { + "name": "ci_linux_clang_debug", + "configurePreset": "ci_linux_clang_debug", + "inherits": "ci-build-debug" + }, + { + "name": "ci_linux_clang_release", + "configurePreset": "ci_linux_clang_release", + "inherits": "ci-build-release" + }, + { + "name": "ci_windows_msvc", + "configurePreset": "ci_windows_msvc", + "inherits": "ci-build-release" + }, + { + "name": "ci_windows_clang", + "configurePreset": "ci_windows_clang", + "inherits": "ci-build-release" + } + ], + "testPresets": [ + { + "name": "ci-test-base", + "hidden": true, + "output": { + "outputOnFailure": true + }, + "execution": { + "noTestsAction": "error" + } + }, + { + "name": "ci-test-debug", + "hidden": true, + "inherits": "ci-test-base", + "configuration": "Debug" + }, + { + "name": "ci-test-release", + "hidden": true, + "inherits": "ci-test-base", + "configuration": "Release" + }, + { + "name": "ci_linux_gcc_debug", + "configurePreset": "ci_linux_gcc_debug", + "inherits": "ci-test-debug" + }, + { + "name": "ci_linux_gcc_release", + "configurePreset": "ci_linux_gcc_release", + "inherits": "ci-test-release" + }, + { + "name": "ci_linux_clang_debug", + "configurePreset": "ci_linux_clang_debug", + "inherits": "ci-test-debug" + }, + { + "name": "ci_linux_clang_release", + "configurePreset": "ci_linux_clang_release", + "inherits": "ci-test-release" + } + ] +} From 5af92ff7cc3f5df74e72bb8ccf16b67a22375138 Mon Sep 17 00:00:00 2001 From: lukas <89145039+codethinki@users.noreply.github.com> Date: Wed, 21 Oct 2026 23:54:24 +0200 Subject: [PATCH 125/135] lib: RGFW fork as submodule on branch cth_engine, from the 1.8.1 release --- .gitmodules | 4 ++++ lib/rgfw | 1 + 2 files changed, 5 insertions(+) create mode 160000 lib/rgfw diff --git a/.gitmodules b/.gitmodules index 897a6dea..1008123d 100644 --- a/.gitmodules +++ b/.gitmodules @@ -5,3 +5,7 @@ path = lib/cth url = https://github.com/codethinki/cth branch = master +[submodule "lib/rgfw"] + path = lib/rgfw + url = https://github.com/codethinki/RGFW + branch = cth_engine diff --git a/lib/rgfw b/lib/rgfw new file mode 160000 index 00000000..d96684e6 --- /dev/null +++ b/lib/rgfw @@ -0,0 +1 @@ +Subproject commit d96684e6877d3ef11c731b122b8949942ed071c9 From 775fefc6d643e2b1bbeadab6b25bab5f7a3d92ec Mon Sep 17 00:00:00 2001 From: lukas <89145039+codethinki@users.noreply.github.com> Date: Wed, 21 Oct 2026 23:54:31 +0200 Subject: [PATCH 126/135] AGENTS: lib/rgfw --- AGENTS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/AGENTS.md b/AGENTS.md index e7cc2af5..2557c081 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -14,6 +14,7 @@ Read it before you change a file. Match the existing code. | `tests/` | loader smoke tests | | `lib/cth` | cth (submodule), `lib/cmake/cth.cmake` adds it with only the components the engine needs | | `lib/cth_cmake` | cmake utilities (submodule) | +| `lib/rgfw` | RGFW windowing library, our fork (submodule) on branch `cth_engine`, fixes are commits there | | `lib/cmake` | one cmake file per library that adds it: `.cmake` | - Vulkan 1.3 is the baseline. Do not port code that 1.3 makes unnecessary (render passes, framebuffers, the old sync API). From 4318b6d0502272d39634f1612afa0df74efd22b9 Mon Sep 17 00:00:00 2001 From: lukas <89145039+codethinki@users.noreply.github.com> Date: Wed, 21 Oct 2026 23:55:09 +0200 Subject: [PATCH 127/135] cvk tests: shared instance with validation checks, InstanceContext for leak tests, shared test helpers, per-process shader temp dir --- cvk/tests/src/base/core_test.cpp | 115 ++++++++--------- cvk/tests/src/base/device_test.cpp | 104 +++++++-------- cvk/tests/src/base/instance_test.cpp | 43 +++--- cvk/tests/src/base/physical_device_test.cpp | 52 ++++---- cvk/tests/src/base/queue/queue_test.cpp | 55 ++------ cvk/tests/src/base/queue/submit_test.cpp | 2 +- cvk/tests/src/context.cpp | 57 +++++--- cvk/tests/src/context.hpp | 122 ++++++++++++++++-- .../src/data/buffer/base_buffer_test.cpp | 21 +-- cvk/tests/src/data/buffer/buffer_test.cpp | 13 +- .../data/descriptor/descriptor_draw_test.cpp | 83 +++--------- .../data/descriptor/descriptor_pool_test.cpp | 6 +- .../data/descriptor/descriptor_set_test.cpp | 6 +- cvk/tests/src/data/img/image_test.cpp | 13 +- cvk/tests/src/debug/debug_messenger_test.cpp | 62 +++++---- cvk/tests/src/render/cmd/cmd_buffer_test.cpp | 28 ++-- cvk/tests/src/render/cmd/cmd_pool_test.cpp | 28 ++-- cvk/tests/src/render/cmd/draw_test.cpp | 60 ++------- cvk/tests/src/render/cmd/local_read_test.cpp | 49 ++----- cvk/tests/src/render/cmd/rendering_test.cpp | 36 ++---- .../render/pipeline/compute_pipeline_test.cpp | 5 +- .../pipeline/graphics_pipeline_test.cpp | 2 +- .../layout/descriptor_set_layout_test.cpp | 4 +- .../render/pipeline/shader/shader_test.cpp | 67 ++++++++-- cvk/tests/src/render/sync/barrier_test.cpp | 5 +- cvk/tests/src/render/sync/fence_test.cpp | 1 + cvk/tests/src/surface/surface_test.cpp | 40 ++---- cvk/tests/src/surface/swapchain_test.cpp | 2 +- .../src/utility/handle/unique_handle_test.cpp | 13 +- 29 files changed, 505 insertions(+), 589 deletions(-) diff --git a/cvk/tests/src/base/core_test.cpp b/cvk/tests/src/base/core_test.cpp index 1c65040f..a7df5c26 100644 --- a/cvk/tests/src/base/core_test.cpp +++ b/cvk/tests/src/base/core_test.cpp @@ -1,28 +1,25 @@ #include "context.hpp" -#include "headless.hpp" #include "test.hpp" -#include "validation.hpp" #include "cvk/base/allocator.hpp" #include "cvk/base/core.hpp" #include "cvk/base/device.hpp" #include "cvk/base/instance.hpp" #include "cvk/base/physical_device.hpp" +#include "cvk/surface/surface.hpp" #include "cvk/utility/constant/device_constants.hpp" #include "cvk/utility/constants.hpp" #include "cvk/utility/vk_exceptions.hpp" -#include #include +#include #include namespace cvk { namespace { - /** - * @brief one queue, some devices have one queue only - */ - Core::Config const CONFIG{.queueProperties = {QueueFamilyProperties::GRAPHICS | QueueFamilyProperties::COMPUTE}}; + using test::Context; + Core::Config const TWO_QUEUES{.queueProperties = {QueueFamilyProperties::GRAPHICS, QueueFamilyProperties::COMPUTE}}; /** @@ -36,45 +33,45 @@ namespace { } CVK_TEST(Core, base_constructor_does_not_create) { - Instance const instance{test::instance_config(), create}; + Context const context{}; - Core const core{instance}; + Core const core{context.instance}; EXPECT_FALSE(core.created()); - EXPECT_EQ(&core.instance(), &instance); + EXPECT_EQ(&core.instance(), &context.instance); } CVK_TEST(Core, create_without_queue_sets) { - Instance const instance{test::instance_config(), create}; + Context const context{}; - Core const core{instance, {}, CONFIG}; + Core const core{context.instance, {}, Context::CONFIG}; ASSERT_TRUE(core.created()); - EXPECT_EQ(core.vkInstance(), instance.get()); + EXPECT_EQ(core.vkInstance(), context.instance.get()); EXPECT_EQ(core.vkPhysicalDevice(), core.physicalDevice().get()); EXPECT_EQ(core.vkDevice(), core.device().get()); EXPECT_EQ(core.functions(), core.device().functions()); EXPECT_EQ(core.deviceTable().device(), core.vkDevice()); EXPECT_EQ(&core.device().physicalDevice(), &core.physicalDevice()); - ASSERT_EQ(core.queueIndices().size(), CONFIG.queueProperties.size()); - EXPECT_EQ(core.device().queues().size(), CONFIG.queueProperties.size()); + ASSERT_EQ(core.queueIndices().size(), Context::CONFIG.queueProperties.size()); + EXPECT_EQ(core.device().queues().size(), Context::CONFIG.queueProperties.size()); - for(size_t i = 0; i < CONFIG.queueProperties.size(); i++) { + for(size_t i = 0; i < Context::CONFIG.queueProperties.size(); i++) { auto const& queue = core.queue(i); ASSERT_NE(queue.get(), VK_NULL_HANDLE); - EXPECT_TRUE(contains(queue.familyProperties(), CONFIG.queueProperties[i])); + EXPECT_TRUE(contains(queue.familyProperties(), Context::CONFIG.queueProperties[i])); queue.waitIdle(); } } CVK_TEST(Core, create_one_queue_per_entry) { - Instance const instance{test::instance_config(), create}; + Context const context{}; - if(!pick_physical_device(instance, {}, TWO_QUEUES.queueProperties, constants::BASE_DEVICE_FEATURES)) + if(!pick_physical_device(context.instance, {}, TWO_QUEUES.queueProperties, constants::BASE_DEVICE_FEATURES)) GTEST_SKIP() << "needs a device with two queues"; - Core const core{instance, {}, TWO_QUEUES}; + Core const core{context.instance, {}, TWO_QUEUES}; ASSERT_EQ(core.queueIndices().size(), 2); EXPECT_EQ(core.device().queues().size(), 2); @@ -83,12 +80,12 @@ CVK_TEST(Core, create_one_queue_per_entry) { } CVK_TEST(Core, queue_set_group_shares_a_queue) { - Instance const instance{test::instance_config(), create}; + Context const context{}; auto config = TWO_QUEUES; config.queueSets.push_back(one_group(config.queueProperties.size())); - Core const core{instance, {}, config}; + Core const core{context.instance, {}, config}; ASSERT_TRUE(core.created()); EXPECT_EQ(core.queueSetIndex(), 0); @@ -101,7 +98,7 @@ CVK_TEST(Core, queue_set_group_shares_a_queue) { } CVK_TEST(Core, first_possible_queue_set_wins) { - Instance const instance{test::instance_config(), create}; + Context const context{}; // no device has this many compute queues constexpr size_t queues = 256; @@ -111,7 +108,7 @@ CVK_TEST(Core, first_possible_queue_set_wins) { .queueSets = {Core::queue_set_t{queues}, one_group(queues)}, }; - Core const core{instance, {}, config}; + Core const core{context.instance, {}, config}; ASSERT_TRUE(core.created()); EXPECT_EQ(core.queueSetIndex(), 1); @@ -120,17 +117,17 @@ CVK_TEST(Core, first_possible_queue_set_wins) { } CVK_TEST(Core, no_suitable_device_throws) { - Instance const instance{test::instance_config(), create}; + Context const context{}; Core::Config const config{.queueProperties = {QueueFamilyProperties::PRESENT}}; - auto const construct = [&] { Core const core{instance, {}, config}; }; + auto const construct = [&] { Core const core{context.instance, {}, config}; }; EXPECT_THROW(construct(), cvk_exception); } CVK_TEST(Core, device_uuid_picks_exactly_this_device) { - test::Context const context{}; - auto config = test::Context::CONFIG; + Context const context{}; + auto config = Context::CONFIG; // every suitable device, not only the one that the automatic pick takes for(auto const& device : enumerate_physical_devices(context.instance)) { @@ -147,13 +144,13 @@ CVK_TEST(Core, device_uuid_picks_exactly_this_device) { } CVK_TEST(Core, missing_device_uuid_throws) { - test::Context const context{}; + Context const context{}; device_uuid_t unknown{}; unknown.fill(0xFF); - Core::Config const withoutSets{.queueProperties = test::Context::CONFIG.queueProperties, .device = unknown}; + Core::Config const withoutSets{.queueProperties = Context::CONFIG.queueProperties, .device = unknown}; Core::Config const withSets{ - .queueProperties = test::Context::CONFIG.queueProperties, + .queueProperties = Context::CONFIG.queueProperties, .queueSets = {Core::queue_set_t{1}}, .device = unknown, }; @@ -165,9 +162,9 @@ CVK_TEST(Core, missing_device_uuid_throws) { } CVK_TEST(Core, release_and_wrap) { - Instance const instance{test::instance_config(), create}; + Context const context{}; - Core core{instance, {}, CONFIG}; + Core core{context.instance, {}, Context::CONFIG}; auto const vkDevice = core.vkDevice(); auto const vmaAllocator = core.allocator().get(); @@ -175,23 +172,23 @@ CVK_TEST(Core, release_and_wrap) { EXPECT_FALSE(core.created()); EXPECT_TRUE(core.queueIndices().empty()); - Core const wrapped{instance, std::move(state)}; + Core const wrapped{context.instance, std::move(state)}; ASSERT_TRUE(wrapped.created()); EXPECT_EQ(wrapped.vkDevice(), vkDevice); EXPECT_EQ(wrapped.allocator().get(), vmaAllocator); - ASSERT_EQ(wrapped.queueIndices().size(), CONFIG.queueProperties.size()); + ASSERT_EQ(wrapped.queueIndices().size(), Context::CONFIG.queueProperties.size()); wrapped.queue(0).waitIdle(); } CVK_TEST(Core, wrap_without_queue_indices_takes_the_device_queues) { - Instance const instance{test::instance_config(), create}; + Context const context{}; - Core core{instance, {}, CONFIG}; + Core core{context.instance, {}, Context::CONFIG}; auto state = core.release(); state.queueIndices.clear(); - Core const wrapped{instance, std::move(state)}; + Core const wrapped{context.instance, std::move(state)}; ASSERT_EQ(wrapped.queueIndices().size(), wrapped.device().queues().size()); for(size_t i = 0; i < wrapped.queueIndices().size(); i++) @@ -199,22 +196,22 @@ CVK_TEST(Core, wrap_without_queue_indices_takes_the_device_queues) { } CVK_TEST(Core, wrap_without_allocator_creates_one) { - Instance const instance{test::instance_config(), create}; + Context const context{}; - Core core{instance, {}, CONFIG}; + Core core{context.instance, {}, Context::CONFIG}; auto state = core.release(); state.allocator = nullptr; - Core const wrapped{instance, std::move(state)}; + Core const wrapped{context.instance, std::move(state)}; EXPECT_TRUE(wrapped.allocator().created()); EXPECT_EQ(&wrapped.allocator().device(), &wrapped.device()); } CVK_TEST(Core, queues_work_after_move) { - Instance const instance{test::instance_config(), create}; + Context const context{}; - Core core{instance, {}, CONFIG}; + Core core{context.instance, {}, Context::CONFIG}; Core const moved{std::move(core)}; EXPECT_FALSE(core.created()); @@ -224,37 +221,29 @@ CVK_TEST(Core, queues_work_after_move) { } CVK_TEST(Core, present_queue_with_headless_surface) { - Instance const instance{test::instance_config(test::headless_extensions()), create}; - std::array const surfaces{test::headless_surface(instance)}; + test::PresentContext const context{}; + std::span const surfaces{&context.surface(), 1}; - Core::Config const config{ - .queueProperties = {QueueFamilyProperties::GRAPHICS | QueueFamilyProperties::PRESENT} - }; - - Core const core{instance, surfaces, config}; + Core const core{context.instance, surfaces, test::PresentContext::CONFIG}; ASSERT_TRUE(core.created()); EXPECT_TRUE(core.queue(0).canPresent()); - EXPECT_TRUE(surfaces[0].supportsFamily(core.physicalDevice(), core.queue(0).familyIndex())); + EXPECT_TRUE(context.surface().supportsFamily(core.physicalDevice(), core.queue(0).familyIndex())); } CVK_TEST(Core, move_assignment_destroys_the_old_core) { if constexpr(!constants::ENABLE_VALIDATION_LAYERS) GTEST_SKIP() << "needs the validation layers"; - test::reset_validation_errors(); - { - Instance const instance{test::instance_config(), create, test::counting_messenger()}; + test::InstanceContext const context{}; - Core core{instance, {}, CONFIG}; - Core other{instance, {}, CONFIG}; - auto const vkDevice = other.vkDevice(); + Core core{context.instance, {}, Context::CONFIG}; + Core other{context.instance, {}, Context::CONFIG}; + auto const vkDevice = other.vkDevice(); - core = std::move(other); + core = std::move(other); - ASSERT_EQ(core.vkDevice(), vkDevice); - EXPECT_FALSE(other.created()); - core.queue(0).waitIdle(); - } - EXPECT_EQ(test::validation_errors(), 0); + ASSERT_EQ(core.vkDevice(), vkDevice); + EXPECT_FALSE(other.created()); + core.queue(0).waitIdle(); } } diff --git a/cvk/tests/src/base/device_test.cpp b/cvk/tests/src/base/device_test.cpp index b9b4324a..483202f2 100644 --- a/cvk/tests/src/base/device_test.cpp +++ b/cvk/tests/src/base/device_test.cpp @@ -1,6 +1,6 @@ +#include "context.hpp" #include "headless.hpp" #include "test.hpp" -#include "validation.hpp" #include "cvk/base/device.hpp" #include "cvk/base/instance.hpp" @@ -11,17 +11,15 @@ #include #include #include +#include #include namespace cvk { namespace { - /** - * @brief one queue, some devices have one queue only - */ - constexpr std::array QUEUES{ - QueueFamilyProperties::GRAPHICS | QueueFamilyProperties::COMPUTE | QueueFamilyProperties::TRANSFER - }; + using test::Context; + using test::QUEUES; + constexpr std::array THREE_QUEUES{ QueueFamilyProperties::GRAPHICS, QueueFamilyProperties::COMPUTE, @@ -30,24 +28,24 @@ namespace { } CVK_TEST(Device, base_constructor_does_not_create) { - Instance const instance{test::instance_config(), create}; - auto const [physicalDevice, _] = test::pick(instance, QUEUES); + Context const context{}; + auto const [physicalDevice, _] = test::pick(context.instance, QUEUES); - Device const device{instance, physicalDevice}; + Device const device{context.instance, physicalDevice}; EXPECT_FALSE(device.created()); EXPECT_TRUE(device.queues().empty()); } CVK_TEST(Device, create_retrieves_the_queue) { - Instance const instance{test::instance_config(), create}; - auto const [physicalDevice, familyIndices] = test::pick(instance, QUEUES); + Context const context{}; + auto const [physicalDevice, familyIndices] = test::pick(context.instance, QUEUES); - Device const device{instance, physicalDevice, Device::Config{familyIndices}}; + Device const device{context.instance, physicalDevice, Device::Config{familyIndices}}; ASSERT_TRUE(device.created()); EXPECT_EQ(&device.physicalDevice(), &physicalDevice); - EXPECT_EQ(&device.instance(), &instance); + EXPECT_EQ(&device.instance(), &context.instance); ASSERT_EQ(device.queues().size(), 1); auto const& queue = device.queues()[0]; @@ -61,13 +59,13 @@ CVK_TEST(Device, create_retrieves_the_queue) { } CVK_TEST(Device, create_retrieves_the_queues) { - Instance const instance{test::instance_config(), create}; + Context const context{}; - auto const result = pick_physical_device(instance, {}, THREE_QUEUES, {}); + auto const result = pick_physical_device(context.instance, {}, THREE_QUEUES, {}); if(!result) GTEST_SKIP() << "needs a device with three queues"; auto const& [physicalDevice, familyIndices] = *result; - Device const device{instance, physicalDevice, Device::Config{familyIndices}}; + Device const device{context.instance, physicalDevice, Device::Config{familyIndices}}; ASSERT_TRUE(device.created()); ASSERT_EQ(device.queues().size(), THREE_QUEUES.size()); @@ -85,14 +83,14 @@ CVK_TEST(Device, create_retrieves_the_queues) { } CVK_TEST(Device, queues_of_one_family_get_increasing_indices) { - Instance const instance{test::instance_config(), create}; - auto const [physicalDevice, _] = test::pick(instance, QUEUES); + Context const context{}; + auto const [physicalDevice, _] = test::pick(context.instance, QUEUES); auto const families = physicalDevice.queueFamilies(); auto const family = std::ranges::find_if(families, [](auto const& f) { return f.vkProperties.queueCount >= 2; }); if(family == families.end()) GTEST_SKIP() << "needs a queue family with two queues"; - Device const device{instance, physicalDevice, Device::Config{{family->index, family->index}}}; + Device const device{context.instance, physicalDevice, Device::Config{{family->index, family->index}}}; ASSERT_EQ(device.queues().size(), 2); EXPECT_EQ(device.queues()[0].index(), 0); @@ -101,10 +99,10 @@ CVK_TEST(Device, queues_of_one_family_get_increasing_indices) { } CVK_TEST(Device, queues_work_after_move) { - Instance const instance{test::instance_config(), create}; - auto const [physicalDevice, familyIndices] = test::pick(instance, QUEUES); + Context const context{}; + auto const [physicalDevice, familyIndices] = test::pick(context.instance, QUEUES); - Device device{instance, physicalDevice, Device::Config{familyIndices}}; + Device device{context.instance, physicalDevice, Device::Config{familyIndices}}; auto const vkDevice = device.get(); Device const moved{std::move(device)}; @@ -118,10 +116,10 @@ CVK_TEST(Device, queues_work_after_move) { } CVK_TEST(Device, release_and_wrap) { - Instance const instance{test::instance_config(), create}; - auto const [physicalDevice, familyIndices] = test::pick(instance, QUEUES); + Context const context{}; + auto const [physicalDevice, familyIndices] = test::pick(context.instance, QUEUES); - Device device{instance, physicalDevice, Device::Config{familyIndices}}; + Device device{context.instance, physicalDevice, Device::Config{familyIndices}}; auto const vkDevice = device.get(); auto state = device.release(); @@ -132,7 +130,7 @@ CVK_TEST(Device, release_and_wrap) { ASSERT_NE(state.functionTable, nullptr); EXPECT_EQ(state.queueFamilyIndices, familyIndices); - Device const wrapped{instance, physicalDevice, std::move(state)}; + Device const wrapped{context.instance, physicalDevice, std::move(state)}; ASSERT_TRUE(wrapped.created()); EXPECT_EQ(wrapped.get(), vkDevice); @@ -142,14 +140,14 @@ CVK_TEST(Device, release_and_wrap) { } CVK_TEST(Device, wrap_without_table_loads_it) { - Instance const instance{test::instance_config(), create}; - auto const [physicalDevice, familyIndices] = test::pick(instance, QUEUES); + Context const context{}; + auto const [physicalDevice, familyIndices] = test::pick(context.instance, QUEUES); - Device device{instance, physicalDevice, Device::Config{familyIndices}}; + Device device{context.instance, physicalDevice, Device::Config{familyIndices}}; auto state = device.release(); state.functionTable = nullptr; - Device const wrapped{instance, physicalDevice, std::move(state)}; + Device const wrapped{context.instance, physicalDevice, std::move(state)}; ASSERT_TRUE(wrapped.created()); EXPECT_NE(wrapped.functions()->vkDestroyDevice, nullptr); @@ -157,29 +155,29 @@ CVK_TEST(Device, wrap_without_table_loads_it) { } CVK_TEST(Device, release_twice_is_safe) { - Instance const instance{test::instance_config(), create}; - auto const [physicalDevice, familyIndices] = test::pick(instance, QUEUES); + Context const context{}; + auto const [physicalDevice, familyIndices] = test::pick(context.instance, QUEUES); - Device device{instance, physicalDevice, Device::Config{familyIndices}}; - Device first{instance, physicalDevice, device.release()}; + Device device{context.instance, physicalDevice, Device::Config{familyIndices}}; + Device first{context.instance, physicalDevice, device.release()}; device.create(Device::Config{familyIndices}); ASSERT_TRUE(device.created()); EXPECT_NE(device.functions()->vkDestroyDevice, nullptr); - Device const second{instance, physicalDevice, device.release()}; + Device const second{context.instance, physicalDevice, device.release()}; EXPECT_TRUE(second.created()); } CVK_TEST(Device, present_enables_swapchain_maintenance1) { - Instance const instance{test::instance_config(test::headless_extensions()), create}; - std::array const surfaces{test::headless_surface(instance)}; + test::PresentContext const context{}; + std::span const surfaces{&context.surface(), 1}; std::array const queues{QueueFamilyProperties::GRAPHICS | QueueFamilyProperties::PRESENT}; - auto const [physicalDevice, familyIndices] = test::pick(instance, queues, surfaces); + auto const [physicalDevice, familyIndices] = test::pick(context.instance, queues, surfaces); Device const device{ - instance, + context.instance, physicalDevice, Device::Config{ .queueFamilyIndices = familyIndices, @@ -203,23 +201,19 @@ CVK_TEST(Device, present_enables_swapchain_maintenance1) { CVK_TEST(Device, move_assignment_destroys_the_old_device) { if constexpr(!constants::ENABLE_VALIDATION_LAYERS) GTEST_SKIP() << "needs the validation layers"; - test::reset_validation_errors(); - { - Instance const instance{test::instance_config(), create, test::counting_messenger()}; - auto const [physicalDevice, familyIndices] = test::pick(instance, QUEUES); + test::InstanceContext const context{}; + auto const [physicalDevice, familyIndices] = test::pick(context.instance, QUEUES); - Device device{instance, physicalDevice, Device::Config{familyIndices}}; - Device other{instance, physicalDevice, Device::Config{familyIndices}}; - auto const vkDevice = other.get(); + Device device{context.instance, physicalDevice, Device::Config{familyIndices}}; + Device other{context.instance, physicalDevice, Device::Config{familyIndices}}; + auto const vkDevice = other.get(); - device = std::move(other); + device = std::move(other); - ASSERT_EQ(device.get(), vkDevice); - EXPECT_FALSE(other.created()); - ASSERT_EQ(device.queues().size(), 1); - device.queues()[0].waitIdle(); - device.waitIdle(); - } - EXPECT_EQ(test::validation_errors(), 0); + ASSERT_EQ(device.get(), vkDevice); + EXPECT_FALSE(other.created()); + ASSERT_EQ(device.queues().size(), 1); + device.queues()[0].waitIdle(); + device.waitIdle(); } } diff --git a/cvk/tests/src/base/instance_test.cpp b/cvk/tests/src/base/instance_test.cpp index 5fdb00a7..e783eac0 100644 --- a/cvk/tests/src/base/instance_test.cpp +++ b/cvk/tests/src/base/instance_test.cpp @@ -1,3 +1,4 @@ +#include "context.hpp" #include "test.hpp" #include "validation.hpp" @@ -20,9 +21,10 @@ CVK_TEST(Instance, base_constructor_does_not_create) { } CVK_TEST(Instance, create_and_destroy) { + test::Context::ValidationCheck const validationCheck{}; Instance instance{test::instance_config()}; - instance.create(); + instance.create(test::counting_messenger()); ASSERT_TRUE(instance.created()); EXPECT_EQ(volkGetLoadedInstance(), instance.get()); @@ -31,24 +33,28 @@ CVK_TEST(Instance, create_and_destroy) { } CVK_TEST(Instance, constructor_creates) { - Instance const instance{test::instance_config(), create}; + test::Context::ValidationCheck const validationCheck{}; + + Instance const instance{test::instance_config(), create, test::counting_messenger()}; EXPECT_TRUE(instance.created()); } CVK_TEST(Instance, create_twice_loads_the_new_instance) { - Instance instance{test::instance_config(), create}; + test::InstanceContext context{}; - instance.create(); + context.instance.create(test::counting_messenger()); - ASSERT_TRUE(instance.created()); - EXPECT_EQ(volkGetLoadedInstance(), instance.get()); + ASSERT_TRUE(context.instance.created()); + EXPECT_EQ(volkGetLoadedInstance(), context.instance.get()); } CVK_TEST(Instance, create_enables_the_extensions_of_the_config) { std::string const surface{VK_KHR_SURFACE_EXTENSION_NAME}; - Instance const instance{test::instance_config({surface}), create}; + test::InstanceContext const context{{surface}}; + + auto const& instance = context.instance; EXPECT_TRUE(std::ranges::contains(instance.extensions(), surface)); if constexpr(constants::ENABLE_VALIDATION_LAYERS) { std::string const debugUtils{VK_EXT_DEBUG_UTILS_EXTENSION_NAME}; @@ -59,8 +65,8 @@ CVK_TEST(Instance, create_enables_the_extensions_of_the_config) { CVK_TEST(Instance, create_with_a_missing_extension_throws_and_forgets) { // a created instance with the config of the missing extension: create() destroys it first - Instance created{test::instance_config(), create}; - Instance instance{test::instance_config({"VK_CVK_extension_that_does_not_exist"}), created.release()}; + test::InstanceContext context{}; + Instance instance{test::instance_config({"VK_CVK_extension_that_does_not_exist"}), context.instance.release()}; ASSERT_TRUE(instance.created()); EXPECT_THROW(instance.create(), cvk_exception); @@ -108,22 +114,19 @@ CVK_TEST(Instance, wrap_takes_ownership) { } CVK_TEST(Instance, move_assignment_takes_the_other_instance) { - test::reset_validation_errors(); - { - Instance instance{test::instance_config(), create, test::counting_messenger()}; - Instance other{test::instance_config(), create, test::counting_messenger()}; - auto const vkInstance = other.get(); + test::InstanceContext context{}; + Instance other{test::instance_config(), create, test::counting_messenger()}; + auto const vkInstance = other.get(); - instance = std::move(other); + context.instance = std::move(other); - EXPECT_EQ(instance.get(), vkInstance); - EXPECT_FALSE(other.created()); - } - EXPECT_EQ(test::validation_errors(), 0); + EXPECT_EQ(context.instance.get(), vkInstance); + EXPECT_FALSE(other.created()); } CVK_TEST(Instance, release_and_wrap) { - Instance instance{test::instance_config(), create}; + test::InstanceContext context{}; + auto& instance = context.instance; auto const vkInstance = instance.get(); auto const extensions = std::vector{std::from_range, instance.extensions()}; diff --git a/cvk/tests/src/base/physical_device_test.cpp b/cvk/tests/src/base/physical_device_test.cpp index 92533a03..c9480863 100644 --- a/cvk/tests/src/base/physical_device_test.cpp +++ b/cvk/tests/src/base/physical_device_test.cpp @@ -9,27 +9,25 @@ #include #include +#include #include #include namespace cvk { namespace { - /** - * @brief one queue, some devices have one queue only - */ - constexpr std::array QUEUES{ - QueueFamilyProperties::GRAPHICS | QueueFamilyProperties::COMPUTE | QueueFamilyProperties::TRANSFER - }; + using test::Context; + using test::QUEUES; + constexpr std::array PRESENT_QUEUES{QueueFamilyProperties::GRAPHICS | QueueFamilyProperties::PRESENT}; std::vector const UNKNOWN_EXTENSION{"VK_CVK_extension_that_does_not_exist"}; } CVK_TEST(PhysicalDevice, pick_finds_the_queues) { - Instance const instance{test::instance_config(), create}; + Context const context{}; - auto const pick = pick_physical_device(instance, {}, QUEUES, constants::BASE_DEVICE_FEATURES); + auto const pick = pick_physical_device(context.instance, {}, QUEUES, constants::BASE_DEVICE_FEATURES); ASSERT_TRUE(pick.has_value()); auto const& [device, familyIndices] = *pick; @@ -43,9 +41,9 @@ CVK_TEST(PhysicalDevice, pick_finds_the_queues) { } CVK_TEST(PhysicalDevice, picked_device_has_the_features) { - Instance const instance{test::instance_config(), create}; + Context const context{}; - auto const [device, _] = test::pick(instance, QUEUES); + auto const [device, _] = test::pick(context.instance, QUEUES); EXPECT_TRUE(device.missing(constants::BASE_DEVICE_FEATURES).empty()); EXPECT_TRUE(device.features().dynamicRenderingLocalRead); @@ -54,46 +52,46 @@ CVK_TEST(PhysicalDevice, picked_device_has_the_features) { } CVK_TEST(PhysicalDevice, missing_names_the_extensions) { - Instance const instance{test::instance_config(), create}; - auto const [device, _] = test::pick(instance, QUEUES); + Context const context{}; + auto const [device, _] = test::pick(context.instance, QUEUES); EXPECT_EQ(device.missing({}, UNKNOWN_EXTENSION), UNKNOWN_EXTENSION); } CVK_TEST(PhysicalDevice, pick_with_a_missing_extension_finds_nothing) { - Instance const instance{test::instance_config(), create}; + Context const context{}; - EXPECT_FALSE(pick_physical_device(instance, {}, QUEUES, {}, UNKNOWN_EXTENSION).has_value()); + EXPECT_FALSE(pick_physical_device(context.instance, {}, QUEUES, {}, UNKNOWN_EXTENSION).has_value()); } CVK_TEST(PhysicalDevice, pick_present_without_surfaces_finds_nothing) { - Instance const instance{test::instance_config(), create}; + Context const context{}; - EXPECT_FALSE(pick_physical_device(instance, {}, PRESENT_QUEUES, {}).has_value()); + EXPECT_FALSE(pick_physical_device(context.instance, {}, PRESENT_QUEUES, {}).has_value()); } CVK_TEST(PhysicalDevice, pick_present_finds_a_family_of_the_surface) { - Instance const instance{test::instance_config(test::headless_extensions()), create}; - std::array const surfaces{test::headless_surface(instance)}; + test::PresentContext const context{}; + std::span const surfaces{&context.surface(), 1}; - auto const [device, familyIndices] = test::pick(instance, PRESENT_QUEUES, surfaces); + auto const [device, familyIndices] = test::pick(context.instance, PRESENT_QUEUES, surfaces); EXPECT_TRUE(device.features().swapchainMaintenance1); ASSERT_EQ(familyIndices.size(), 1); - EXPECT_TRUE(surfaces[0].supportsFamily(device, familyIndices[0])); + EXPECT_TRUE(context.surface().supportsFamily(device, familyIndices[0])); } CVK_TEST(PhysicalDevice, enumerate_without_surfaces_has_no_present) { - Instance const instance{test::instance_config(), create}; + Context const context{}; - for(auto const& device : enumerate_physical_devices(instance)) + for(auto const& device : enumerate_physical_devices(context.instance)) for(auto const& family : device.queueFamilies()) EXPECT_FALSE(contains(family.properties, QueueFamilyProperties::PRESENT)); } CVK_TEST(PhysicalDevice, copy_keeps_the_queried_members) { - Instance const instance{test::instance_config(), create}; - auto const [device, _] = test::pick(instance, QUEUES); + Context const context{}; + auto const [device, _] = test::pick(context.instance, QUEUES); PhysicalDevice const copy{device}; @@ -107,7 +105,7 @@ CVK_TEST(PhysicalDevice, copy_keeps_the_queried_members) { } CVK_TEST(PhysicalDevice, uuid_is_the_device_uuid) { - test::Context const context{}; + Context const context{}; auto const& device = context.core.physicalDevice(); VkPhysicalDeviceIDProperties idProperties{ @@ -134,8 +132,8 @@ CVK_TEST(PhysicalDevice, type_rank_prefers_discrete_integrated_virtual_cpu_other } CVK_TEST(PhysicalDevice, find_supported_format) { - Instance const instance{test::instance_config(), create}; - auto const [device, _] = test::pick(instance, QUEUES); + Context const context{}; + auto const& device = context.core.physicalDevice(); // R8G8B8A8_UNORM sampled images with optimal tiling are mandatory std::array const candidates{VK_FORMAT_R8G8B8A8_UNORM}; diff --git a/cvk/tests/src/base/queue/queue_test.cpp b/cvk/tests/src/base/queue/queue_test.cpp index 97633da7..7cbf07ec 100644 --- a/cvk/tests/src/base/queue/queue_test.cpp +++ b/cvk/tests/src/base/queue/queue_test.cpp @@ -1,31 +1,16 @@ +#include "context.hpp" #include "headless.hpp" #include "test.hpp" #include "cvk/base/device.hpp" #include "cvk/base/instance.hpp" #include "cvk/base/physical_device.hpp" -#include "cvk/utility/constant/device_constants.hpp" -#include #include -#include -#include namespace cvk { namespace { - constexpr std::array QUEUES{QueueFamilyProperties::GRAPHICS}; - - /** - * @brief device config with the base features, vkQueueSubmit2() needs synchronization2 - */ - Device::Config device_config(std::vector familyIndices) { - return Device::Config{ - .queueFamilyIndices = std::move(familyIndices), - .features = constants::BASE_DEVICE_FEATURES, - }; - } - VkSubmitInfo2 const EMPTY_SUBMIT{ .sType = VK_STRUCTURE_TYPE_SUBMIT_INFO_2, .pNext = nullptr, @@ -40,11 +25,9 @@ namespace { } CVK_TEST(Queue, copies_share_the_queue) { - Instance const instance{test::instance_config(), create}; - auto const [physicalDevice, familyIndices] = test::pick(instance, QUEUES); - Device const device{instance, physicalDevice, device_config(familyIndices)}; + test::Context const context{}; + auto const& queue = context.core.queue(0); - auto const& queue = device.queues()[0]; Queue const copy{queue}; EXPECT_EQ(copy.get(), queue.get()); @@ -54,17 +37,14 @@ CVK_TEST(Queue, copies_share_the_queue) { } CVK_TEST(Queue, raw_submit_signals_the_fence) { - Instance const instance{test::instance_config(), create}; - auto const [physicalDevice, familyIndices] = test::pick(instance, QUEUES); - Device const device{instance, physicalDevice, device_config(familyIndices)}; - auto const table = device.table(); + test::Context const context{}; + auto const table = context.core.deviceTable(); VkFenceCreateInfo const fenceInfo{.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO, .pNext = nullptr, .flags = 0}; VkFence fence = VK_NULL_HANDLE; ASSERT_EQ(table->vkCreateFence(table.device(), &fenceInfo, nullptr, &fence), VK_SUCCESS); - auto const& queue = device.queues()[0]; - queue.rawSubmit(EMPTY_SUBMIT, fence); + context.core.queue(0).rawSubmit(EMPTY_SUBMIT, fence); EXPECT_EQ(table->vkWaitForFences(table.device(), 1, &fence, VK_TRUE, UINT64_MAX), VK_SUCCESS); @@ -72,22 +52,20 @@ CVK_TEST(Queue, raw_submit_signals_the_fence) { } CVK_TEST(Queue, raw_submit_without_fence) { - Instance const instance{test::instance_config(), create}; - auto const [physicalDevice, familyIndices] = test::pick(instance, QUEUES); - Device const device{instance, physicalDevice, device_config(familyIndices)}; + test::Context const context{}; + auto const& queue = context.core.queue(0); - auto const& queue = device.queues()[0]; queue.rawSubmit(EMPTY_SUBMIT); queue.waitIdle(); } CVK_TEST(Queue, copies_keep_the_queue_through_a_device_release_and_wrap) { - Instance const instance{test::instance_config(), create}; - auto const [physicalDevice, familyIndices] = test::pick(instance, QUEUES); - Device device{instance, physicalDevice, device_config(familyIndices)}; + test::Context const context{}; + auto const [physicalDevice, familyIndices] = test::pick(context.instance, test::QUEUES); + Device device{context.instance, physicalDevice, Device::Config{familyIndices}}; Queue const copy{device.queues()[0]}; - Device const wrapped{instance, physicalDevice, device.release()}; + Device const wrapped{context.instance, physicalDevice, device.release()}; ASSERT_EQ(wrapped.queues().size(), 1); EXPECT_EQ(wrapped.queues()[0].get(), copy.get()); @@ -95,13 +73,8 @@ CVK_TEST(Queue, copies_keep_the_queue_through_a_device_release_and_wrap) { } CVK_TEST(Queue, present_family_can_present) { - Instance const instance{test::instance_config(test::headless_extensions()), create}; - std::array const surfaces{test::headless_surface(instance)}; - std::array const queues{QueueFamilyProperties::GRAPHICS | QueueFamilyProperties::PRESENT}; - - auto const [physicalDevice, familyIndices] = test::pick(instance, queues, surfaces); - Device const device{instance, physicalDevice, device_config(familyIndices)}; + test::PresentContext const context{}; - EXPECT_TRUE(device.queues()[0].canPresent()); + EXPECT_TRUE(context.queue().canPresent()); } } diff --git a/cvk/tests/src/base/queue/submit_test.cpp b/cvk/tests/src/base/queue/submit_test.cpp index 70032eda..0ab16db7 100644 --- a/cvk/tests/src/base/queue/submit_test.cpp +++ b/cvk/tests/src/base/queue/submit_test.cpp @@ -32,7 +32,7 @@ CVK_TEST(Submit, semaphore_info_takes_the_kind_from_the_semaphore) { CVK_TEST(Submit, command_buffer_without_fence) { test::Context const context{}; - CmdPool const pool{context.core, CmdPool::Config{.queueFamilyIndex = context.core.queue(0).familyIndex()}, create}; + CmdPool const pool{context.core, test::pool_config(context.core), create}; PrimaryCmdBuffer primary{pool, create}; primary.begin(VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT); primary.end(); diff --git a/cvk/tests/src/context.cpp b/cvk/tests/src/context.cpp index 0c3134fd..ff15f40e 100644 --- a/cvk/tests/src/context.cpp +++ b/cvk/tests/src/context.cpp @@ -3,24 +3,18 @@ #include "validation.hpp" #include "cvk/render/cmd/cmd_buffer.hpp" +#include "cvk/render/pipeline/shader/shader.hpp" #include "cvk/render/sync/fence.hpp" +#include +#include #include +#include #include namespace cvk::test { -Core::Config const Context::CONFIG{ - .queueProperties = {QueueFamilyProperties::GRAPHICS | QueueFamilyProperties::COMPUTE}, -}; - -struct Context::Owned { - explicit Owned(Core::Config const& config) : instance{instance_config(), create, counting_messenger()}, - core{instance, {}, config} {} - - Instance instance; - Core core; -}; +Core::Config const Context::CONFIG{.queueProperties = {std::from_range, QUEUES}}; Core::Config const PresentContext::CONFIG{ .queueProperties = {QueueFamilyProperties::GRAPHICS | QueueFamilyProperties::PRESENT}, @@ -75,14 +69,41 @@ namespace { Context::Context() : instance{shared_contexts().instance}, core{shared_core()} {} -Context::Context(Core::Config const& config) : _owned{std::make_unique(config)}, - instance{_owned->instance}, core{_owned->core} {} - -Context::~Context() = default; +Context::Context(Core::Config const& config) : + _core{std::in_place, shared_contexts().instance, std::span{}, config}, + instance{shared_contexts().instance}, core{*_core} {} PresentContext::PresentContext() : instance{shared_contexts().instance}, core{shared_present_core()}, _surface{shared_surface()} {} +InstanceContext::InstanceContext(std::vector extensions) : + instance{instance_config(std::move(extensions)), create, counting_messenger()} {} + +CmdPool::Config pool_config(Core const& core) { return {.queueFamilyIndex = core.queue(0).familyIndex()}; } + +GraphicsPipeline::Config fullscreen_config( + Shader const& vertex, + Shader const& fragment, + std::span colorFormats +) { + GraphicsPipeline::Config config{.colorFormats = {std::from_range, colorFormats}}; + config.addStage(vertex).addStage(fragment); + config.vertexBindings = {{.binding = 0, .stride = sizeof(Vec3), .inputRate = VK_VERTEX_INPUT_RATE_VERTEX}}; + config.vertexAttributes = {{.location = 0, .binding = 0, .format = VK_FORMAT_R32G32B32_SFLOAT, .offset = 0}}; + config.rasterization.cullMode = VK_CULL_MODE_NONE; + return config; +} + +bool every_pixel_is(BaseBuffer const& readback, std::array pixel) { + auto const pixels = readback.map(); + readback.invalidate(); + auto const asByte = [](std::byte b) { return std::to_integer(b); }; + return std::ranges::all_of( + pixels | std::views::chunk(4), + [&](auto const& bytes) { return std::ranges::equal(bytes, pixel, {}, asByte); } + ); +} + void destroy_shared_contexts() { if(!shared.has_value()) return; @@ -143,7 +164,11 @@ void copy_to_buffer( .bufferImageHeight = 0, .imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, mipLevel, 0, 1}, .imageOffset = {0, 0, 0}, - .imageExtent = {image.extent().width >> mipLevel, image.extent().height >> mipLevel, 1}, + .imageExtent = { + std::max(image.extent().width >> mipLevel, 1u), + std::max(image.extent().height >> mipLevel, 1u), + 1, + }, }; VkCopyImageToBufferInfo2 const info{ .sType = VK_STRUCTURE_TYPE_COPY_IMAGE_TO_BUFFER_INFO_2, diff --git a/cvk/tests/src/context.hpp b/cvk/tests/src/context.hpp index f08173db..5cf2ce3e 100644 --- a/cvk/tests/src/context.hpp +++ b/cvk/tests/src/context.hpp @@ -7,13 +7,92 @@ #include "cvk/data/buffer/base_buffer.hpp" #include "cvk/data/img/image.hpp" #include "cvk/render/cmd/cmd_buffer.hpp" +#include "cvk/render/cmd/cmd_pool.hpp" +#include "cvk/render/pipeline/graphics_pipeline.hpp" #include -#include +#include +#include +#include #include +#include +namespace cvk { +class Shader; +} + namespace cvk::test { +/** + * @brief one queue, some devices have one queue only + */ +inline constexpr std::array QUEUES{QueueFamilyProperties::GRAPHICS | QueueFamilyProperties::COMPUTE}; + +/** + * @brief buffer the cpu writes once + */ +[[nodiscard]] constexpr BaseBuffer::Config host_written(VkBufferUsageFlags usage) { + return {.usage = usage, .memory = {.flags = VMA_ALLOCATION_CREATE_HOST_ACCESS_SEQUENTIAL_WRITE_BIT}}; +} + +/** + * @brief buffer the cpu writes once and the gpu copies from + */ +inline constexpr BaseBuffer::Config STAGING_CONFIG = host_written(VK_BUFFER_USAGE_TRANSFER_SRC_BIT); + +/** + * @brief buffer the gpu copies into and the cpu reads back + */ +inline constexpr BaseBuffer::Config READBACK_CONFIG{ + .usage = VK_BUFFER_USAGE_TRANSFER_DST_BIT, + .memory = {.flags = VMA_ALLOCATION_CREATE_HOST_ACCESS_RANDOM_BIT}, +}; + +/** + * @brief binding flags of a bindless descriptor array + */ +inline constexpr VkDescriptorBindingFlags BINDLESS = VK_DESCRIPTOR_BINDING_PARTIALLY_BOUND_BIT + | VK_DESCRIPTOR_BINDING_UPDATE_AFTER_BIND_BIT + | VK_DESCRIPTOR_BINDING_VARIABLE_DESCRIPTOR_COUNT_BIT; + +/** + * @brief vertex of @ref FULLSCREEN, the vec3 position at location 0 of the test vertex shaders + */ +struct Vec3 { + float x, y, z; +}; + +/** + * @brief one triangle that covers the whole viewport + */ +inline constexpr std::array FULLSCREEN{Vec3{-1.f, -1.f, 0.f}, Vec3{3.f, -1.f, 0.f}, Vec3{-1.f, 3.f, 0.f}}; + +/** + * @brief color of the fill draws + */ +inline constexpr std::array GREEN{0.f, 1.f, 0.f, 1.f}; + +/** + * @brief @ref GREEN as a VK_FORMAT_R8G8B8A8_UNORM pixel + */ +inline constexpr std::array GREEN_PIXEL{0, 255, 0, 255}; + +/** + * @brief pipeline config that draws @ref FULLSCREEN from vertex binding 0 without culling + * @pre the shaders are created + */ +[[nodiscard]] GraphicsPipeline::Config fullscreen_config( + Shader const& vertex, + Shader const& fragment, + std::span colorFormats +); + +/** + * @return every VK_FORMAT_R8G8B8A8_UNORM pixel of the buffer is the pixel + * @pre the buffer is host visible, the gpu writes are visible to the host + */ +[[nodiscard]] bool every_pixel_is(BaseBuffer const& readback, std::array pixel); + /** * @brief instance and core for tests, fails the test on validation errors * @details the default is the shared instance and core of the process, made at the first use, @@ -21,7 +100,7 @@ namespace cvk::test { */ struct Context { /** - * @brief one queue, some devices have one queue only + * @brief the queues of @ref QUEUES */ static Core::Config const CONFIG; @@ -30,28 +109,22 @@ struct Context { ~ValidationCheck(); }; - /** - * @brief instance and core that a context owns - */ - struct Owned; - /** * @brief the shared instance and core */ Context(); /** - * @brief own instance and core with the config - * @details destroys them at the end -> the check sees the errors of vkDestroyInstance() too, e.g. leaks + * @brief the shared instance and an own core with the config + * @details destroys the core at the end -> the check sees the errors of vkDestroyDevice() too, e.g. leaks + * @throws cvk::cvk_exception reason: no physical device meets the requirements */ explicit Context(Core::Config const&); - ~Context(); - ValidationCheck validationCheck; private: - std::unique_ptr _owned; + std::optional _core{}; public: Instance const& instance; @@ -87,13 +160,34 @@ struct PresentContext { Surface const& _surface; public: - PresentContext(PresentContext const& other) = delete; PresentContext(PresentContext&& other) = delete; PresentContext& operator=(PresentContext const& other) = delete; PresentContext& operator=(PresentContext&& other) = delete; }; +/** + * @brief own instance with @ref counting_messenger(), fails the test on validation errors + * @details for the tests that need an own instance, e.g. the leak reports of vkDestroyInstance(): + * the check runs after the instance is destroyed + */ +struct InstanceContext { + explicit InstanceContext(std::vector extensions = {}); + + Context::ValidationCheck validationCheck; + Instance instance; + + InstanceContext(InstanceContext const& other) = delete; + InstanceContext(InstanceContext&& other) = delete; + InstanceContext& operator=(InstanceContext const& other) = delete; + InstanceContext& operator=(InstanceContext&& other) = delete; +}; + +/** + * @brief pool config for the first queue of the core + */ +[[nodiscard]] CmdPool::Config pool_config(Core const&); + /** * @brief destroys the shared contexts, fails on their validation errors * @details the test environment calls it before the loader unloads @@ -133,7 +227,7 @@ void copy_to_buffer( */ template void record_and_submit(Context const& context, Record&& record) { - CmdPool const pool{context.core, CmdPool::Config{.queueFamilyIndex = context.core.queue(0).familyIndex()}, create}; + CmdPool const pool{context.core, pool_config(context.core), create}; PrimaryCmdBuffer primary{pool, create}; primary.begin(VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT); diff --git a/cvk/tests/src/data/buffer/base_buffer_test.cpp b/cvk/tests/src/data/buffer/base_buffer_test.cpp index 24ece861..e575b777 100644 --- a/cvk/tests/src/data/buffer/base_buffer_test.cpp +++ b/cvk/tests/src/data/buffer/base_buffer_test.cpp @@ -12,23 +12,10 @@ namespace cvk { namespace { - constexpr size_t SIZE = 256; - - /** - * @brief staging buffer, the cpu writes it once - */ - constexpr BaseBuffer::Config STAGING_CONFIG{ - .usage = VK_BUFFER_USAGE_TRANSFER_SRC_BIT, - .memory = {.flags = VMA_ALLOCATION_CREATE_HOST_ACCESS_SEQUENTIAL_WRITE_BIT}, - }; + using test::READBACK_CONFIG; + using test::STAGING_CONFIG; - /** - * @brief buffer the cpu reads back - */ - constexpr BaseBuffer::Config READBACK_CONFIG{ - .usage = VK_BUFFER_USAGE_TRANSFER_DST_BIT, - .memory = {.flags = VMA_ALLOCATION_CREATE_HOST_ACCESS_RANDOM_BIT}, - }; + constexpr size_t SIZE = 256; /** * @brief gpu only buffer @@ -43,7 +30,7 @@ CVK_TEST(BaseBuffer, config_create_info) { static_assert(info.usage == VK_BUFFER_USAGE_TRANSFER_SRC_BIT); static_assert(info.flags == 0); static_assert(info.sharingMode == VK_SHARING_MODE_EXCLUSIVE); - static_assert(info.queueFamilyIndexCount == 0 && info.pQueueFamilyIndices == nullptr); + static_assert(info.queueFamilyIndexCount == 0 && info.pQueueFamilyIndices == STAGING_CONFIG.queueFamilies.data()); static_assert(STAGING_CONFIG.memory.autoUsage()); } diff --git a/cvk/tests/src/data/buffer/buffer_test.cpp b/cvk/tests/src/data/buffer/buffer_test.cpp index ec650254..efcf41ad 100644 --- a/cvk/tests/src/data/buffer/buffer_test.cpp +++ b/cvk/tests/src/data/buffer/buffer_test.cpp @@ -11,6 +11,9 @@ namespace cvk { namespace { + using test::READBACK_CONFIG; + using test::STAGING_CONFIG; + struct Vertex { float x, y, z; uint32_t color; @@ -34,16 +37,6 @@ namespace { .memory = {.flags = VMA_ALLOCATION_CREATE_HOST_ACCESS_RANDOM_BIT}, }; - constexpr Buffer::Config STAGING_CONFIG{ - .usage = VK_BUFFER_USAGE_TRANSFER_SRC_BIT, - .memory = {.flags = VMA_ALLOCATION_CREATE_HOST_ACCESS_SEQUENTIAL_WRITE_BIT}, - }; - - constexpr Buffer::Config READBACK_CONFIG{ - .usage = VK_BUFFER_USAGE_TRANSFER_DST_BIT, - .memory = {.flags = VMA_ALLOCATION_CREATE_HOST_ACCESS_RANDOM_BIT}, - }; - /** * @brief Buffer is a valid type */ diff --git a/cvk/tests/src/data/descriptor/descriptor_draw_test.cpp b/cvk/tests/src/data/descriptor/descriptor_draw_test.cpp index 78b42c29..7235eb1f 100644 --- a/cvk/tests/src/data/descriptor/descriptor_draw_test.cpp +++ b/cvk/tests/src/data/descriptor/descriptor_draw_test.cpp @@ -16,35 +16,21 @@ #include "cvk/render/pipeline/shader/shader.hpp" #include "cvk/render/sync/barrier.hpp" -#include #include #include #include #include -#include #include +#include namespace cvk { namespace { + using test::host_written; + using test::Vec3; + constexpr VkExtent2D EXTENT{16, 16}; constexpr std::array COLOR_FORMATS{VK_FORMAT_R8G8B8A8_UNORM}; - constexpr std::array GREEN{0.f, 1.f, 0.f, 1.f}; - constexpr std::array GREEN_PIXEL{0, 255, 0, 255}; - - struct Vec3 { - float x, y, z; - }; - - /** - * @brief one triangle that covers the whole viewport - */ - constexpr std::array FULLSCREEN{Vec3{-1.f, -1.f, 0.f}, Vec3{3.f, -1.f, 0.f}, Vec3{-1.f, 3.f, 0.f}}; - - template - typename Buffer::Config host_written(VkBufferUsageFlags usage) { - return {.usage = usage, .memory = {.flags = VMA_ALLOCATION_CREATE_HOST_ACCESS_SEQUENTIAL_WRITE_BIT}}; - } /** * @brief fullscreen pipeline with one descriptor set and the fragment shader @@ -58,20 +44,9 @@ namespace { }, vertex{core, VK_SHADER_STAGE_VERTEX_BIT, test::shaders::FILL_VERT}, fragment{core, VK_SHADER_STAGE_FRAGMENT_BIT, fragmentCode}, - pipeline{layout, config(), create}, - vertices{core, host_written(VK_BUFFER_USAGE_VERTEX_BUFFER_BIT), FULLSCREEN.size()} { - vertices.write(FULLSCREEN); - } - - [[nodiscard]] GraphicsPipeline::Config config() const { - GraphicsPipeline::Config config{.colorFormats = {std::from_range, COLOR_FORMATS}}; - config.addStage(vertex).addStage(fragment); - config.vertexBindings = {{.binding = 0, .stride = sizeof(Vec3), .inputRate = VK_VERTEX_INPUT_RATE_VERTEX}}; - config.vertexAttributes = { - {.location = 0, .binding = 0, .format = VK_FORMAT_R32G32B32_SFLOAT, .offset = 0}, - }; - config.rasterization.cullMode = VK_CULL_MODE_NONE; - return config; + pipeline{layout, test::fullscreen_config(vertex, fragment, COLOR_FORMATS), create}, + vertices{core, host_written(VK_BUFFER_USAGE_VERTEX_BUFFER_BIT), test::FULLSCREEN.size()} { + vertices.write(test::FULLSCREEN); } PipelineLayout layout; @@ -94,14 +69,7 @@ namespace { EXTENT }, view{image, {}, create}, - readback{ - core, - { - .usage = VK_BUFFER_USAGE_TRANSFER_DST_BIT, - .memory = {.flags = VMA_ALLOCATION_CREATE_HOST_ACCESS_RANDOM_BIT}, - }, - size_t{EXTENT.width} * EXTENT.height * 4 - } {} + readback{core, test::READBACK_CONFIG, size_t{EXTENT.width} * EXTENT.height * 4} {} Image image; ImageView view; @@ -154,14 +122,6 @@ namespace { test::copy_to_buffer(context, cmd, target.image, target.readback, 0, 0); test::host_read_barrier(context, cmd); } - - void expect_green(Target const& target) { - auto const pixels = target.readback.map(); - target.readback.invalidate(); - auto const asByte = [](std::byte b) { return std::to_integer(b); }; - for(auto const pixel : pixels | std::views::chunk(4)) - EXPECT_TRUE(std::ranges::equal(pixel, GREEN_PIXEL, {}, asByte)); - } } CVK_TEST(DescriptorDraw, uniform_buffer_color) { @@ -172,12 +132,8 @@ CVK_TEST(DescriptorDraw, uniform_buffer_color) { Fill const fill{context.core, setLayout, test::shaders::UNIFORM_COLOR_FRAG}; Target const target{context.core}; - Buffer> color{ - context.core, - host_written>(VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT), - 1 - }; - color.write(std::array, 1>{GREEN}); + Buffer> color{context.core, host_written(VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT), 1}; + color.write(std::array, 1>{test::GREEN}); DescriptorPool const pool{context.core, DescriptorPool::Config::ForLayouts({{&setLayout}}), create}; DescriptorSet const set{pool, setLayout, create}; @@ -187,7 +143,7 @@ CVK_TEST(DescriptorDraw, uniform_buffer_color) { record_fill(context, cmd, fill, target, [&](BaseCmdBuffer& c) { c.bindDescriptorSet(set, fill.layout, 0); }); }); - expect_green(target); + EXPECT_TRUE(test::every_pixel_is(target.readback, test::GREEN_PIXEL)); } namespace { @@ -198,9 +154,6 @@ namespace { * false -> one sampler2D, the descriptor comes before the recording, the layers check its layout at the draw */ void draw_texture(test::Context const& context, VkImageLayout layout, bool afterBind) { - constexpr VkDescriptorBindingFlags BINDLESS = VK_DESCRIPTOR_BINDING_PARTIALLY_BOUND_BIT - | VK_DESCRIPTOR_BINDING_UPDATE_AFTER_BIND_BIT - | VK_DESCRIPTOR_BINDING_VARIABLE_DESCRIPTOR_COUNT_BIT; uint32_t const index = afterBind ? 17 : 0; auto const textureLayout = layout != VK_IMAGE_LAYOUT_MAX_ENUM ? layout : VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; @@ -211,7 +164,7 @@ namespace { VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, VK_SHADER_STAGE_FRAGMENT_BIT, afterBind ? 64 : 1, - afterBind ? BINDLESS : 0 + afterBind ? test::BINDLESS : 0 ); DescriptorSetLayout const setLayout{context.core, setConfig, create}; Fill const fill{ @@ -231,8 +184,8 @@ namespace { }; ImageView const view{texture, {}, create}; Sampler const sampler{context.core, {}, create}; - Buffer staging{context.core, host_written(VK_BUFFER_USAGE_TRANSFER_SRC_BIT), 4}; - staging.write(GREEN_PIXEL); + Buffer staging{context.core, test::STAGING_CONFIG, 4}; + staging.write(test::GREEN_PIXEL); uint32_t const variableCount = afterBind ? 32 : 0; DescriptorPool const pool{ @@ -243,11 +196,7 @@ namespace { DescriptorSet const set{pool, setLayout, variableCount, create}; if(!afterBind) set.write({0, view, sampler, index, layout}); - CmdPool const cmdPool{ - context.core, - CmdPool::Config{.queueFamilyIndex = context.core.queue(0).familyIndex()}, - create - }; + CmdPool const cmdPool{context.core, test::pool_config(context.core), create}; PrimaryCmdBuffer cmd{cmdPool, create}; cmd.begin(VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT); @@ -277,7 +226,7 @@ namespace { if(afterBind) set.write({0, view, sampler, index, layout}); test::submit_and_wait(context, cmd); - expect_green(target); + EXPECT_TRUE(test::every_pixel_is(target.readback, test::GREEN_PIXEL)); } } diff --git a/cvk/tests/src/data/descriptor/descriptor_pool_test.cpp b/cvk/tests/src/data/descriptor/descriptor_pool_test.cpp index 564645bc..44c7ad7e 100644 --- a/cvk/tests/src/data/descriptor/descriptor_pool_test.cpp +++ b/cvk/tests/src/data/descriptor/descriptor_pool_test.cpp @@ -9,10 +9,6 @@ namespace cvk { namespace { - constexpr VkDescriptorBindingFlags BINDLESS = VK_DESCRIPTOR_BINDING_PARTIALLY_BOUND_BIT - | VK_DESCRIPTOR_BINDING_UPDATE_AFTER_BIND_BIT - | VK_DESCRIPTOR_BINDING_VARIABLE_DESCRIPTOR_COUNT_BIT; - /** * @brief binding 0: 2 uniform buffers, binding 1: 1 combined image sampler */ @@ -28,7 +24,7 @@ namespace { */ DescriptorSetLayout::Config bindless() { DescriptorSetLayout::Config config{}; - config.add(0, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, VK_SHADER_STAGE_FRAGMENT_BIT, 1024, BINDLESS); + config.add(0, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, VK_SHADER_STAGE_FRAGMENT_BIT, 1024, test::BINDLESS); return config; } diff --git a/cvk/tests/src/data/descriptor/descriptor_set_test.cpp b/cvk/tests/src/data/descriptor/descriptor_set_test.cpp index f09a50c6..3121bd9a 100644 --- a/cvk/tests/src/data/descriptor/descriptor_set_test.cpp +++ b/cvk/tests/src/data/descriptor/descriptor_set_test.cpp @@ -14,10 +14,6 @@ namespace cvk { namespace { - constexpr VkDescriptorBindingFlags BINDLESS = VK_DESCRIPTOR_BINDING_PARTIALLY_BOUND_BIT - | VK_DESCRIPTOR_BINDING_UPDATE_AFTER_BIND_BIT - | VK_DESCRIPTOR_BINDING_VARIABLE_DESCRIPTOR_COUNT_BIT; - /** * @brief binding 0: uniform buffer, binding 1: combined image sampler */ @@ -33,7 +29,7 @@ namespace { */ DescriptorSetLayout::Config bindless() { DescriptorSetLayout::Config config{}; - config.add(0, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, VK_SHADER_STAGE_FRAGMENT_BIT, 64, BINDLESS); + config.add(0, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, VK_SHADER_STAGE_FRAGMENT_BIT, 64, test::BINDLESS); return config; } diff --git a/cvk/tests/src/data/img/image_test.cpp b/cvk/tests/src/data/img/image_test.cpp index 4a67f954..aadc2bb8 100644 --- a/cvk/tests/src/data/img/image_test.cpp +++ b/cvk/tests/src/data/img/image_test.cpp @@ -15,6 +15,9 @@ namespace cvk { namespace { + using test::READBACK_CONFIG; + using test::STAGING_CONFIG; + constexpr VkExtent2D EXTENT{64, 32}; /** @@ -46,16 +49,6 @@ namespace { .usage = VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT, .mipLevels = mip_level_count(EXTENT), }; - - constexpr BaseBuffer::Config STAGING_CONFIG{ - .usage = VK_BUFFER_USAGE_TRANSFER_SRC_BIT, - .memory = {.flags = VMA_ALLOCATION_CREATE_HOST_ACCESS_SEQUENTIAL_WRITE_BIT}, - }; - - constexpr BaseBuffer::Config READBACK_CONFIG{ - .usage = VK_BUFFER_USAGE_TRANSFER_DST_BIT, - .memory = {.flags = VMA_ALLOCATION_CREATE_HOST_ACCESS_RANDOM_BIT}, - }; } CVK_TEST(Image, mip_level_count) { diff --git a/cvk/tests/src/debug/debug_messenger_test.cpp b/cvk/tests/src/debug/debug_messenger_test.cpp index bf4f8189..d7da01d1 100644 --- a/cvk/tests/src/debug/debug_messenger_test.cpp +++ b/cvk/tests/src/debug/debug_messenger_test.cpp @@ -1,5 +1,5 @@ +#include "context.hpp" #include "test.hpp" -#include "validation.hpp" #include "cvk/base/instance.hpp" #include "cvk/debug/debug_messenger.hpp" @@ -60,12 +60,12 @@ CVK_TEST(DebugMessenger, default_config_create_info) { CVK_TEST(DebugMessenger, forwards_messages_to_the_callback) { if constexpr(!constants::ENABLE_VALIDATION_LAYERS) GTEST_SKIP() << "needs the validation layers"; - Instance const instance{test::instance_config(), create}; - DebugMessenger const messenger{instance, DebugMessenger::Config{.callback = count_test_messages}, create}; + test::Context const context{}; + DebugMessenger const messenger{context.instance, DebugMessenger::Config{.callback = count_test_messages}, create}; ASSERT_TRUE(messenger.created()); received = 0; - submit_test_message(instance); + submit_test_message(context.instance); EXPECT_EQ(received, 1); } @@ -73,7 +73,7 @@ CVK_TEST(DebugMessenger, forwards_messages_to_the_callback) { CVK_TEST(DebugMessenger, accepts_a_lambda) { if constexpr(!constants::ENABLE_VALIDATION_LAYERS) GTEST_SKIP() << "needs the validation layers"; - Instance const instance{test::instance_config(), create}; + test::Context const context{}; DebugMessenger::callback_t const callback = []( VkDebugUtilsMessageSeverityFlagBitsEXT, @@ -84,25 +84,25 @@ CVK_TEST(DebugMessenger, accepts_a_lambda) { if(callbackData->pMessage == TEST_MESSAGE) ++received; return VK_FALSE; }; - DebugMessenger const messenger{instance, DebugMessenger::Config{.callback = callback}, create}; + DebugMessenger const messenger{context.instance, DebugMessenger::Config{.callback = callback}, create}; received = 0; - submit_test_message(instance); + submit_test_message(context.instance); EXPECT_EQ(received, 1); } -CVK_TEST(DebugMessenger, destroy_resets) { +CVK_TEST(DebugMessenger, destroy_stops_the_messages) { if constexpr(!constants::ENABLE_VALIDATION_LAYERS) GTEST_SKIP() << "needs the validation layers"; - Instance const instance{test::instance_config(), create}; - DebugMessenger messenger{instance, DebugMessenger::Config{.callback = count_test_messages}, create}; + test::Context const context{}; + DebugMessenger messenger{context.instance, DebugMessenger::Config{.callback = count_test_messages}, create}; messenger.destroy(); EXPECT_FALSE(messenger.created()); received = 0; - submit_test_message(instance); + submit_test_message(context.instance); EXPECT_EQ(received, 0); } @@ -110,56 +110,52 @@ CVK_TEST(DebugMessenger, destroy_resets) { CVK_TEST(DebugMessenger, release_keeps_the_messenger) { if constexpr(!constants::ENABLE_VALIDATION_LAYERS) GTEST_SKIP() << "needs the validation layers"; - Instance const instance{test::instance_config(), create}; - DebugMessenger messenger{instance, DebugMessenger::Config{.callback = count_test_messages}, create}; + test::Context const context{}; + DebugMessenger messenger{context.instance, DebugMessenger::Config{.callback = count_test_messages}, create}; auto const state = messenger.release(); EXPECT_FALSE(messenger.created()); received = 0; - submit_test_message(instance); + submit_test_message(context.instance); EXPECT_EQ(received, 1); - DebugMessenger::destroy(instance.get(), state.vkMessenger.get()); + DebugMessenger::destroy(context.instance.get(), state.vkMessenger.get()); } CVK_TEST(DebugMessenger, release_and_wrap) { if constexpr(!constants::ENABLE_VALIDATION_LAYERS) GTEST_SKIP() << "needs the validation layers"; - Instance const instance{test::instance_config(), create}; - DebugMessenger messenger{instance, DebugMessenger::Config{.callback = count_test_messages}, create}; + test::Context const context{}; + DebugMessenger messenger{context.instance, DebugMessenger::Config{.callback = count_test_messages}, create}; auto const vkMessenger = messenger.get(); auto const state = messenger.release(); - DebugMessenger const wrapped{instance, messenger.config(), state}; + DebugMessenger const wrapped{context.instance, messenger.config(), state}; ASSERT_TRUE(wrapped.created()); EXPECT_EQ(wrapped.get(), vkMessenger); received = 0; - submit_test_message(instance); + submit_test_message(context.instance); EXPECT_EQ(received, 1); } CVK_TEST(DebugMessenger, move_assignment_destroys_the_old_messenger) { if constexpr(!constants::ENABLE_VALIDATION_LAYERS) GTEST_SKIP() << "needs the validation layers"; - test::reset_validation_errors(); - { - Instance const instance{test::instance_config(), create, test::counting_messenger()}; - DebugMessenger messenger{instance, DebugMessenger::Config{.callback = count_test_messages}, create}; - DebugMessenger other{instance, DebugMessenger::Config{.callback = count_test_messages}, create}; - auto const vkMessenger = other.get(); + test::InstanceContext const context{}; + DebugMessenger messenger{context.instance, DebugMessenger::Config{.callback = count_test_messages}, create}; + DebugMessenger other{context.instance, DebugMessenger::Config{.callback = count_test_messages}, create}; + auto const vkMessenger = other.get(); - messenger = std::move(other); + messenger = std::move(other); - EXPECT_EQ(messenger.get(), vkMessenger); - EXPECT_FALSE(other.created()); + EXPECT_EQ(messenger.get(), vkMessenger); + EXPECT_FALSE(other.created()); - received = 0; - submit_test_message(instance); - EXPECT_EQ(received, 1); - } - EXPECT_EQ(test::validation_errors(), 0); + received = 0; + submit_test_message(context.instance); + EXPECT_EQ(received, 1); } } diff --git a/cvk/tests/src/render/cmd/cmd_buffer_test.cpp b/cvk/tests/src/render/cmd/cmd_buffer_test.cpp index fe504830..53af016f 100644 --- a/cvk/tests/src/render/cmd/cmd_buffer_test.cpp +++ b/cvk/tests/src/render/cmd/cmd_buffer_test.cpp @@ -11,11 +11,7 @@ namespace cvk { -namespace { - CmdPool::Config pool_config(test::Context const& context) { - return CmdPool::Config{.queueFamilyIndex = context.core.queue(0).familyIndex()}; - } -} +using test::pool_config; CVK_TEST(CmdBuffer, inheritance_rendering_info) { static_assert([] { @@ -50,7 +46,7 @@ CVK_TEST(CmdBuffer, delete_only_through_the_derived_class) { CVK_TEST(CmdBuffer, base_constructor_does_not_create) { test::Context const context{}; - CmdPool const pool{context.core, pool_config(context), create}; + CmdPool const pool{context.core, pool_config(context.core), create}; PrimaryCmdBuffer const primary{pool}; SecondaryCmdBuffer const secondary{pool}; @@ -64,7 +60,7 @@ CVK_TEST(CmdBuffer, base_constructor_does_not_create) { CVK_TEST(CmdBuffer, create_and_destroy) { test::Context const context{}; - CmdPool const pool{context.core, pool_config(context), create}; + CmdPool const pool{context.core, pool_config(context.core), create}; PrimaryCmdBuffer primary{pool, create}; ASSERT_TRUE(primary.created()); @@ -77,7 +73,7 @@ CVK_TEST(CmdBuffer, create_and_destroy) { CVK_TEST(CmdBuffer, begin_and_end) { test::Context const context{}; - CmdPool const pool{context.core, pool_config(context), create}; + CmdPool const pool{context.core, pool_config(context.core), create}; PrimaryCmdBuffer primary{pool, create}; primary.begin(VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT); @@ -89,7 +85,7 @@ CVK_TEST(CmdBuffer, begin_and_end) { CVK_TEST(CmdBuffer, submit) { test::Context const context{}; - CmdPool const pool{context.core, pool_config(context), create}; + CmdPool const pool{context.core, pool_config(context.core), create}; PrimaryCmdBuffer primary{pool, create}; primary.begin(VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT); primary.end(); @@ -99,7 +95,7 @@ CVK_TEST(CmdBuffer, submit) { CVK_TEST(CmdBuffer, reset_and_record_again) { test::Context const context{}; - CmdPool const pool{context.core, pool_config(context), create}; + CmdPool const pool{context.core, pool_config(context.core), create}; PrimaryCmdBuffer primary{pool, create}; primary.begin(); @@ -113,7 +109,7 @@ CVK_TEST(CmdBuffer, reset_and_record_again) { CVK_TEST(CmdBuffer, execute_secondaries) { test::Context const context{}; - CmdPool const pool{context.core, pool_config(context), create}; + CmdPool const pool{context.core, pool_config(context.core), create}; std::array secondaries{ SecondaryCmdBuffer{pool, create}, SecondaryCmdBuffer{pool, create}, @@ -133,7 +129,7 @@ CVK_TEST(CmdBuffer, execute_secondaries) { CVK_TEST(CmdBuffer, execute_secondaries_through_pointers) { test::Context const context{}; - CmdPool const pool{context.core, pool_config(context), create}; + CmdPool const pool{context.core, pool_config(context.core), create}; // secondaries[1] is not recorded, the iterator pair [&secondaries[0], &secondaries[2]) would execute it std::array secondaries{ SecondaryCmdBuffer{pool, create}, @@ -155,7 +151,7 @@ CVK_TEST(CmdBuffer, execute_secondaries_through_pointers) { CVK_TEST(CmdBuffer, secondary_begin_with_rendering_inheritance) { test::Context const context{}; - CmdPool const pool{context.core, pool_config(context), create}; + CmdPool const pool{context.core, pool_config(context.core), create}; SecondaryCmdBuffer secondary{pool, create}; secondary.begin( @@ -171,7 +167,7 @@ CVK_TEST(CmdBuffer, secondary_begin_with_rendering_inheritance) { CVK_TEST(CmdBuffer, release_and_wrap) { test::Context const context{}; - CmdPool const pool{context.core, pool_config(context), create}; + CmdPool const pool{context.core, pool_config(context.core), create}; PrimaryCmdBuffer primary{pool, create}; auto const vkBuffer = primary.get(); @@ -185,7 +181,7 @@ CVK_TEST(CmdBuffer, release_and_wrap) { CVK_TEST(CmdBuffer, moved_from_buffer_does_not_record) { test::Context const context{}; - CmdPool const pool{context.core, pool_config(context), create}; + CmdPool const pool{context.core, pool_config(context.core), create}; PrimaryCmdBuffer primary{pool, create}; primary.begin(); @@ -202,7 +198,7 @@ CVK_TEST(CmdBuffer, moved_from_buffer_does_not_record) { CVK_TEST(CmdBuffer, move_assignment_frees_the_old_buffer) { test::Context const context{}; - CmdPool const pool{context.core, pool_config(context), create}; + CmdPool const pool{context.core, pool_config(context.core), create}; PrimaryCmdBuffer primary{pool, create}; PrimaryCmdBuffer other{pool, create}; auto const vkBuffer = other.get(); diff --git a/cvk/tests/src/render/cmd/cmd_pool_test.cpp b/cvk/tests/src/render/cmd/cmd_pool_test.cpp index fd56b945..e144d75b 100644 --- a/cvk/tests/src/render/cmd/cmd_pool_test.cpp +++ b/cvk/tests/src/render/cmd/cmd_pool_test.cpp @@ -8,11 +8,7 @@ namespace cvk { -namespace { - CmdPool::Config pool_config(test::Context const& context) { - return CmdPool::Config{.queueFamilyIndex = context.core.queue(0).familyIndex()}; - } -} +using test::pool_config; CVK_TEST(CmdPool, config_create_info) { constexpr CmdPool::Config config{.queueFamilyIndex = 2, .flags = VK_COMMAND_POOL_CREATE_TRANSIENT_BIT}; @@ -37,7 +33,7 @@ CVK_TEST(CmdPool, config_is_an_aggregate_with_defaults) { CVK_TEST(CmdPool, base_constructor_does_not_create) { test::Context const context{}; - CmdPool const pool{context.core, pool_config(context)}; + CmdPool const pool{context.core, pool_config(context.core)}; EXPECT_FALSE(pool.created()); EXPECT_TRUE(pool.unused()); @@ -45,7 +41,7 @@ CVK_TEST(CmdPool, base_constructor_does_not_create) { CVK_TEST(CmdPool, create_and_destroy) { test::Context const context{}; - CmdPool pool{context.core, pool_config(context), create}; + CmdPool pool{context.core, pool_config(context.core), create}; ASSERT_TRUE(pool.created()); EXPECT_EQ(&pool.core(), &context.core); @@ -56,7 +52,7 @@ CVK_TEST(CmdPool, create_and_destroy) { CVK_TEST(CmdPool, counts_its_command_buffers) { test::Context const context{}; - CmdPool const pool{context.core, pool_config(context), create}; + CmdPool const pool{context.core, pool_config(context.core), create}; { PrimaryCmdBuffer primary{pool, create}; @@ -72,7 +68,7 @@ CVK_TEST(CmdPool, counts_its_command_buffers) { CVK_TEST(CmdPool, release_of_a_buffer_stops_the_count) { test::Context const context{}; - CmdPool const pool{context.core, pool_config(context), create}; + CmdPool const pool{context.core, pool_config(context.core), create}; PrimaryCmdBuffer primary{pool, create}; auto const state = primary.release(); @@ -83,7 +79,7 @@ CVK_TEST(CmdPool, release_of_a_buffer_stops_the_count) { CVK_TEST(CmdPool, owner_of_a_released_buffer_counts_it) { test::Context const context{}; - CmdPool const pool{context.core, pool_config(context), create}; + CmdPool const pool{context.core, pool_config(context.core), create}; PrimaryCmdBuffer primary{pool, create}; auto const state = primary.release(); @@ -97,7 +93,7 @@ CVK_TEST(CmdPool, owner_of_a_released_buffer_counts_it) { CVK_TEST(CmdPool, reset_returns_the_buffers_to_the_initial_state) { test::Context const context{}; - CmdPool const pool{context.core, pool_config(context), create}; + CmdPool const pool{context.core, pool_config(context.core), create}; PrimaryCmdBuffer primary{pool, create}; primary.begin(); primary.end(); @@ -111,7 +107,7 @@ CVK_TEST(CmdPool, reset_returns_the_buffers_to_the_initial_state) { CVK_TEST(CmdPool, reset_ends_the_recording) { test::Context const context{}; - CmdPool const pool{context.core, pool_config(context), create}; + CmdPool const pool{context.core, pool_config(context.core), create}; PrimaryCmdBuffer primary{pool, create}; SecondaryCmdBuffer secondary{pool, create}; primary.begin(); @@ -129,20 +125,20 @@ CVK_TEST(CmdPool, reset_ends_the_recording) { CVK_TEST(CmdPool, release_and_wrap) { test::Context const context{}; - CmdPool pool{context.core, pool_config(context), create}; + CmdPool pool{context.core, pool_config(context.core), create}; auto const vkPool = pool.get(); auto const state = pool.release(); EXPECT_FALSE(pool.created()); - CmdPool const wrapped{context.core, pool_config(context), state}; + CmdPool const wrapped{context.core, pool_config(context.core), state}; EXPECT_EQ(wrapped.get(), vkPool); } CVK_TEST(CmdPool, move_assignment_destroys_the_old_pool) { test::Context const context{}; - CmdPool pool{context.core, pool_config(context), create}; - CmdPool other{context.core, pool_config(context), create}; + CmdPool pool{context.core, pool_config(context.core), create}; + CmdPool other{context.core, pool_config(context.core), create}; auto const vkPool = other.get(); pool = std::move(other); diff --git a/cvk/tests/src/render/cmd/draw_test.cpp b/cvk/tests/src/render/cmd/draw_test.cpp index 7452f834..c8e43eba 100644 --- a/cvk/tests/src/render/cmd/draw_test.cpp +++ b/cvk/tests/src/render/cmd/draw_test.cpp @@ -11,38 +11,20 @@ #include "cvk/render/pipeline/shader/shader.hpp" #include "cvk/render/sync/barrier.hpp" -#include #include #include #include -#include #include namespace cvk { namespace { + using test::GREEN; + using test::host_written; + using test::Vec3; + constexpr VkExtent2D EXTENT{16, 16}; constexpr std::array COLOR_FORMATS{VK_FORMAT_R8G8B8A8_UNORM}; - constexpr std::array GREEN{0.f, 1.f, 0.f, 1.f}; - - constexpr BaseBuffer::Config READBACK_CONFIG{ - .usage = VK_BUFFER_USAGE_TRANSFER_DST_BIT, - .memory = {.flags = VMA_ALLOCATION_CREATE_HOST_ACCESS_RANDOM_BIT}, - }; - - struct Vec3 { - float x, y, z; - }; - - /** - * @brief one triangle that covers the whole viewport - */ - constexpr std::array FULLSCREEN{Vec3{-1.f, -1.f, 0.f}, Vec3{3.f, -1.f, 0.f}, Vec3{-1.f, 3.f, 0.f}}; - - template - constexpr Buffer::Config host_written(VkBufferUsageFlags usage) { - return {.usage = usage, .memory = {.flags = VMA_ALLOCATION_CREATE_HOST_ACCESS_SEQUENTIAL_WRITE_BIT}}; - } /** * @brief pipeline that fills the triangle with the push constant color @@ -55,20 +37,9 @@ namespace { }, vertex{core, VK_SHADER_STAGE_VERTEX_BIT, test::shaders::FILL_VERT}, fragment{core, VK_SHADER_STAGE_FRAGMENT_BIT, test::shaders::TRIANGLE_FRAG}, - pipeline{layout, config(), create}, - vertices{core, host_written(VK_BUFFER_USAGE_VERTEX_BUFFER_BIT), FULLSCREEN.size()} { - vertices.write(FULLSCREEN); - } - - [[nodiscard]] GraphicsPipeline::Config config() const { - GraphicsPipeline::Config config{.colorFormats = {std::from_range, COLOR_FORMATS}}; - config.addStage(vertex).addStage(fragment); - config.vertexBindings = {{.binding = 0, .stride = sizeof(Vec3), .inputRate = VK_VERTEX_INPUT_RATE_VERTEX}}; - config.vertexAttributes = { - {.location = 0, .binding = 0, .format = VK_FORMAT_R32G32B32_SFLOAT, .offset = 0}, - }; - config.rasterization.cullMode = VK_CULL_MODE_NONE; - return config; + pipeline{layout, test::fullscreen_config(vertex, fragment, COLOR_FORMATS), create}, + vertices{core, host_written(VK_BUFFER_USAGE_VERTEX_BUFFER_BIT), test::FULLSCREEN.size()} { + vertices.write(test::FULLSCREEN); } PipelineLayout layout; @@ -93,7 +64,7 @@ namespace { EXTENT }; ImageView const view{image, {}, create}; - BaseBuffer const readback{context.core, READBACK_CONFIG, size_t{EXTENT.width} * EXTENT.height * 4}; + BaseBuffer const readback{context.core, test::READBACK_CONFIG, size_t{EXTENT.width} * EXTENT.height * 4}; test::record_and_submit(context, [&](PrimaryCmdBuffer& cmd) { Barrier transition{}; @@ -132,12 +103,7 @@ namespace { test::host_read_barrier(context, cmd); }); - auto const pixels = readback.map(); - readback.invalidate(); - constexpr std::array green{0, 255, 0, 255}; - auto const asByte = [](std::byte b) { return std::to_integer(b); }; - for(auto const pixel : pixels | std::views::chunk(4)) - EXPECT_TRUE(std::ranges::equal(pixel, green, {}, asByte)); + EXPECT_TRUE(test::every_pixel_is(readback, test::GREEN_PIXEL)); } } @@ -158,7 +124,7 @@ CVK_TEST(Draw, draw) { CVK_TEST(Draw, draw_indexed) { test::Context const context{}; Fill const fill{context.core}; - Buffer indices{context.core, host_written(VK_BUFFER_USAGE_INDEX_BUFFER_BIT), 3}; + Buffer indices{context.core, host_written(VK_BUFFER_USAGE_INDEX_BUFFER_BIT), 3}; indices.write(std::array{0, 1, 2}); expect_filled(context, fill, [&](PrimaryCmdBuffer const& cmd) { @@ -172,7 +138,7 @@ CVK_TEST(Draw, draw_indirect) { Fill const fill{context.core}; Buffer commands{ context.core, - host_written(VK_BUFFER_USAGE_INDIRECT_BUFFER_BIT), + host_written(VK_BUFFER_USAGE_INDIRECT_BUFFER_BIT), 1 }; commands.write( @@ -185,11 +151,11 @@ CVK_TEST(Draw, draw_indirect) { CVK_TEST(Draw, draw_indexed_indirect) { test::Context const context{}; Fill const fill{context.core}; - Buffer indices{context.core, host_written(VK_BUFFER_USAGE_INDEX_BUFFER_BIT), 3}; + Buffer indices{context.core, host_written(VK_BUFFER_USAGE_INDEX_BUFFER_BIT), 3}; indices.write(std::array{0, 1, 2}); Buffer commands{ context.core, - host_written(VK_BUFFER_USAGE_INDIRECT_BUFFER_BIT), + host_written(VK_BUFFER_USAGE_INDIRECT_BUFFER_BIT), 1 }; commands.write( diff --git a/cvk/tests/src/render/cmd/local_read_test.cpp b/cvk/tests/src/render/cmd/local_read_test.cpp index fccb60ad..aa51d7bd 100644 --- a/cvk/tests/src/render/cmd/local_read_test.cpp +++ b/cvk/tests/src/render/cmd/local_read_test.cpp @@ -15,19 +15,18 @@ #include "cvk/render/pipeline/shader/shader.hpp" #include "cvk/render/sync/barrier.hpp" -#include #include #include -#include -#include +#include namespace cvk { namespace { + using test::GREEN; + using test::Vec3; + constexpr VkExtent2D EXTENT{16, 16}; constexpr std::array COLOR_FORMATS{VK_FORMAT_R8G8B8A8_UNORM, VK_FORMAT_R8G8B8A8_UNORM}; - constexpr std::array GREEN{0.f, 1.f, 0.f, 1.f}; - constexpr std::array GREEN_PIXEL{0, 255, 0, 255}; constexpr BarrierScope COLOR_WRITE{ VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT, @@ -35,15 +34,6 @@ namespace { }; constexpr BarrierScope INPUT_READ{VK_PIPELINE_STAGE_2_FRAGMENT_SHADER_BIT, VK_ACCESS_2_INPUT_ATTACHMENT_READ_BIT}; - struct Vec3 { - float x, y, z; - }; - - /** - * @brief one triangle that covers the whole viewport - */ - constexpr std::array FULLSCREEN{Vec3{-1.f, -1.f, 0.f}, Vec3{3.f, -1.f, 0.f}, Vec3{-1.f, 3.f, 0.f}}; - /** * @brief location 0 -> attachment 0, attachment 1 without output */ @@ -122,11 +112,7 @@ namespace { * @brief fullscreen pipeline for the attachments */ GraphicsPipeline::Config fullscreen(Shader const& vertex, Shader const& fragment, AttachmentMapping mapping) { - GraphicsPipeline::Config config{.colorFormats = {std::from_range, COLOR_FORMATS}}; - config.addStage(vertex).addStage(fragment); - config.vertexBindings = {{.binding = 0, .stride = sizeof(Vec3), .inputRate = VK_VERTEX_INPUT_RATE_VERTEX}}; - config.vertexAttributes = {{.location = 0, .binding = 0, .format = VK_FORMAT_R32G32B32_SFLOAT, .offset = 0}}; - config.rasterization.cullMode = VK_CULL_MODE_NONE; + auto config = test::fullscreen_config(vertex, fragment, COLOR_FORMATS); config.attachmentMapping = std::move(mapping); return config; } @@ -155,17 +141,10 @@ namespace { read{readLayout, fullscreen(vertex, localRead, copy_first_to_second()), create}, pool{core, DescriptorPool::Config::ForLayouts({{&inputLayout}}), create}, input{pool, inputLayout, create}, - vertices{ - core, - { - .usage = VK_BUFFER_USAGE_VERTEX_BUFFER_BIT, - .memory = {.flags = VMA_ALLOCATION_CREATE_HOST_ACCESS_SEQUENTIAL_WRITE_BIT}, - }, - FULLSCREEN.size() - } { + vertices{core, test::host_written(VK_BUFFER_USAGE_VERTEX_BUFFER_BIT), test::FULLSCREEN.size()} { // input attachment: VK_IMAGE_LAYOUT_RENDERING_LOCAL_READ_KHR from the binding type input.write({0, first}); - vertices.write(FULLSCREEN); + vertices.write(test::FULLSCREEN); } /** @@ -237,11 +216,7 @@ CVK_TEST(LocalRead, draw_reads_an_attachment_of_the_same_rendering) { test::Context const context{}; Attachments const attachments{context.core}; Draws const draws{context.core, attachments.firstView}; - BaseBuffer const readback{ - context.core, - {.usage = VK_BUFFER_USAGE_TRANSFER_DST_BIT, .memory = {.flags = VMA_ALLOCATION_CREATE_HOST_ACCESS_RANDOM_BIT}}, - size_t{EXTENT.width} * EXTENT.height * 4 - }; + BaseBuffer const readback{context.core, test::READBACK_CONFIG, size_t{EXTENT.width} * EXTENT.height * 4}; test::record_and_submit(context, [&](PrimaryCmdBuffer& cmd) { attachments.toRendering(cmd); @@ -286,18 +261,14 @@ CVK_TEST(LocalRead, draw_reads_an_attachment_of_the_same_rendering) { test::host_read_barrier(context, cmd); }); - auto const pixels = readback.map(); - readback.invalidate(); - auto const asByte = [](std::byte b) { return std::to_integer(b); }; - for(auto const pixel : pixels | std::views::chunk(4)) - EXPECT_TRUE(std::ranges::equal(pixel, GREEN_PIXEL, {}, asByte)); + EXPECT_TRUE(test::every_pixel_is(readback, test::GREEN_PIXEL)); } CVK_TEST(LocalRead, secondary_inherits_the_mapping) { test::Context const context{}; Attachments const attachments{context.core}; Draws const draws{context.core, attachments.firstView}; - CmdPool const pool{context.core, CmdPool::Config{.queueFamilyIndex = context.core.queue(0).familyIndex()}, create}; + CmdPool const pool{context.core, test::pool_config(context.core), create}; SecondaryCmdBuffer secondary{pool, create}; test::record_and_submit(context, [&](PrimaryCmdBuffer& cmd) { diff --git a/cvk/tests/src/render/cmd/rendering_test.cpp b/cvk/tests/src/render/cmd/rendering_test.cpp index 6d1cd9b2..1377e96b 100644 --- a/cvk/tests/src/render/cmd/rendering_test.cpp +++ b/cvk/tests/src/render/cmd/rendering_test.cpp @@ -7,11 +7,8 @@ #include "cvk/render/cmd/cmd_buffer.hpp" #include "cvk/render/sync/barrier.hpp" -#include #include #include -#include -#include namespace cvk { @@ -35,6 +32,11 @@ namespace { VK_ACCESS_2_COLOR_ATTACHMENT_WRITE_BIT }; + /** + * @brief the clear color of @ref clear_red() as a VK_FORMAT_R8G8B8A8_UNORM pixel + */ + constexpr std::array RED_PIXEL{255, 0, 0, 255}; + RenderingAttachment clear_red(ImageView const& view, VkImageLayout layout = VK_IMAGE_LAYOUT_ATTACHMENT_OPTIMAL) { return RenderingAttachment{ .view = &view, @@ -50,11 +52,7 @@ CVK_TEST(Rendering, clear_a_color_attachment) { test::Context const context{}; Image const image{context.core, COLOR_CONFIG, EXTENT}; ImageView const view{image, {}, create}; - BaseBuffer const readback{ - context.core, - {.usage = VK_BUFFER_USAGE_TRANSFER_DST_BIT, .memory = {.flags = VMA_ALLOCATION_CREATE_HOST_ACCESS_RANDOM_BIT}}, - size_t{EXTENT.width} * EXTENT.height * 4 - }; + BaseBuffer const readback{context.core, test::READBACK_CONFIG, size_t{EXTENT.width} * EXTENT.height * 4}; test::record_and_submit(context, [&](PrimaryCmdBuffer& cmd) { Barrier transition{}; @@ -80,12 +78,7 @@ CVK_TEST(Rendering, clear_a_color_attachment) { test::host_read_barrier(context, cmd); }); - auto const pixels = readback.map(); - readback.invalidate(); - auto const red = std::array{255, 0, 0, 255}; - auto const asByte = [](std::byte b) { return std::to_integer(b); }; - for(auto const pixel : pixels | std::views::chunk(4)) - EXPECT_TRUE(std::ranges::equal(pixel, red, {}, asByte)); + EXPECT_TRUE(test::every_pixel_is(readback, RED_PIXEL)); } CVK_TEST(Rendering, clear_every_layer) { @@ -95,11 +88,7 @@ CVK_TEST(Rendering, clear_every_layer) { Image const image{context.core, config, EXTENT}; ImageView const view{image, {}, create}; size_t const layerSize = size_t{EXTENT.width} * EXTENT.height * 4; - BaseBuffer const readback{ - context.core, - {.usage = VK_BUFFER_USAGE_TRANSFER_DST_BIT, .memory = {.flags = VMA_ALLOCATION_CREATE_HOST_ACCESS_RANDOM_BIT}}, - 2 * layerSize - }; + BaseBuffer const readback{context.core, test::READBACK_CONFIG, 2 * layerSize}; test::record_and_submit(context, [&](PrimaryCmdBuffer& cmd) { Barrier transition{}; @@ -116,12 +105,7 @@ CVK_TEST(Rendering, clear_every_layer) { test::host_read_barrier(context, cmd); }); - auto const pixels = readback.map(); - readback.invalidate(); - auto const red = std::array{255, 0, 0, 255}; - auto const asByte = [](std::byte b) { return std::to_integer(b); }; - for(auto const pixel : pixels | std::views::chunk(4)) - EXPECT_TRUE(std::ranges::equal(pixel, red, {}, asByte)); + EXPECT_TRUE(test::every_pixel_is(readback, RED_PIXEL)); } CVK_TEST(Rendering, tracks_the_current_rendering) { @@ -207,7 +191,7 @@ CVK_TEST(Rendering, secondary_inherits_the_rendering) { test::Context const context{}; Image const image{context.core, COLOR_CONFIG, EXTENT}; ImageView const view{image, {}, create}; - CmdPool const pool{context.core, CmdPool::Config{.queueFamilyIndex = context.core.queue(0).familyIndex()}, create}; + CmdPool const pool{context.core, test::pool_config(context.core), create}; SecondaryCmdBuffer secondary{pool, create}; test::record_and_submit(context, [&](PrimaryCmdBuffer& cmd) { diff --git a/cvk/tests/src/render/pipeline/compute_pipeline_test.cpp b/cvk/tests/src/render/pipeline/compute_pipeline_test.cpp index da472fa0..543584e1 100644 --- a/cvk/tests/src/render/pipeline/compute_pipeline_test.cpp +++ b/cvk/tests/src/render/pipeline/compute_pipeline_test.cpp @@ -141,10 +141,7 @@ CVK_TEST(ComputePipeline, dispatch_indirect_fills_a_storage_buffer) { test::Context const context{}; Buffer const indirect{ context.core, - { - .usage = VK_BUFFER_USAGE_INDIRECT_BUFFER_BIT, - .memory = {.flags = VMA_ALLOCATION_CREATE_HOST_ACCESS_SEQUENTIAL_WRITE_BIT}, - }, + test::host_written(VK_BUFFER_USAGE_INDIRECT_BUFFER_BIT), 1 }; indirect.write(std::array{VkDispatchIndirectCommand{GROUPS, 1, 1}}); diff --git a/cvk/tests/src/render/pipeline/graphics_pipeline_test.cpp b/cvk/tests/src/render/pipeline/graphics_pipeline_test.cpp index 43695fff..0e4ec7dd 100644 --- a/cvk/tests/src/render/pipeline/graphics_pipeline_test.cpp +++ b/cvk/tests/src/render/pipeline/graphics_pipeline_test.cpp @@ -128,7 +128,7 @@ CVK_TEST(GraphicsPipeline, bind) { test::Context const context{}; Triangle const triangle{context.core}; GraphicsPipeline const pipeline{triangle.layout, triangle.config(), create}; - CmdPool const pool{context.core, CmdPool::Config{.queueFamilyIndex = context.core.queue(0).familyIndex()}, create}; + CmdPool const pool{context.core, test::pool_config(context.core), create}; PrimaryCmdBuffer primary{pool, create}; primary.begin(VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT); diff --git a/cvk/tests/src/render/pipeline/layout/descriptor_set_layout_test.cpp b/cvk/tests/src/render/pipeline/layout/descriptor_set_layout_test.cpp index 2d42eb99..426f2849 100644 --- a/cvk/tests/src/render/pipeline/layout/descriptor_set_layout_test.cpp +++ b/cvk/tests/src/render/pipeline/layout/descriptor_set_layout_test.cpp @@ -126,9 +126,7 @@ CVK_TEST(DescriptorSetLayout, config_without_flags_keeps_no_flags) { CVK_TEST(DescriptorSetLayout, create_bindless) { test::Context const context{}; - constexpr VkDescriptorBindingFlags BINDLESS = VK_DESCRIPTOR_BINDING_PARTIALLY_BOUND_BIT - | VK_DESCRIPTOR_BINDING_UPDATE_AFTER_BIND_BIT - | VK_DESCRIPTOR_BINDING_VARIABLE_DESCRIPTOR_COUNT_BIT; + using test::BINDLESS; DescriptorSetLayout::Config config{}; config.add(0, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, VK_SHADER_STAGE_VERTEX_BIT) diff --git a/cvk/tests/src/render/pipeline/shader/shader_test.cpp b/cvk/tests/src/render/pipeline/shader/shader_test.cpp index 205a7ed2..a0ceb4cb 100644 --- a/cvk/tests/src/render/pipeline/shader/shader_test.cpp +++ b/cvk/tests/src/render/pipeline/shader/shader_test.cpp @@ -5,26 +5,67 @@ #include "cvk/render/pipeline/shader/shader.hpp" #include "cvk/utility/vk_exceptions.hpp" -#include -#include -#include +#include -#ifdef _WIN32 +#ifdef CTH_PLATFORM_WINDOWS +#include #define CVK_NULL_DEVICE "NUL" #else +#include #define CVK_NULL_DEVICE "/dev/null" #endif +#include +#include +#include +#include +#include + namespace cvk { namespace { using test::shaders::TRIANGLE_VERT; /** - * @brief writes bytes to a file in the temp directory + * @return glslc runs, the check runs once per process + */ + bool glslc_available() { + static bool const available = std::system("glslc --version >" CVK_NULL_DEVICE " 2>&1") == 0; + return available; + } + + auto process_id() { +#ifdef CTH_PLATFORM_WINDOWS + return _getpid(); +#else + return getpid(); +#endif + } + + /** + * @return directory of this process for the temp files, the process exit removes it + * @details one directory per process: test processes can run at the same time + */ + std::filesystem::path const& temp_dir() { + struct Directory { + Directory() { std::filesystem::create_directories(path); } + ~Directory() { + std::error_code error{}; + std::filesystem::remove_all(path, error); + } + + std::filesystem::path path = std::filesystem::temp_directory_path() + / std::format("cvk_tests_{}", process_id()); + }; + static Directory const directory{}; + return directory.path; + } + + /** + * @brief writes bytes to a file in @ref temp_dir() */ std::filesystem::path write_temp(std::string_view name, std::span bytes) { - auto path = std::filesystem::temp_directory_path() / name; + auto path = temp_dir() / name; std::ofstream file{path, std::ios::binary}; file.write(bytes.data(), static_cast(bytes.size())); return path; @@ -69,8 +110,7 @@ CVK_TEST(Shader, create_from_file) { } CVK_TEST(Shader, load_rejects_bad_files) { - auto const missing = std::filesystem::temp_directory_path() / "cvk_test_missing.spv"; - std::filesystem::remove(missing); + auto const missing = temp_dir() / "cvk_test_missing.spv"; EXPECT_THROW([[maybe_unused]] auto const spv = Shader::load(missing), cvk_exception); constexpr std::array notWords{1, 2, 3, 4, 5}; @@ -109,9 +149,9 @@ CVK_TEST(Shader, move_assignment_destroys_the_old_shader) { } CVK_TEST(Shader, compile_and_create) { - if(std::system("glslc --version >" CVK_NULL_DEVICE " 2>&1") != 0) GTEST_SKIP() << "glslc not found"; + if(!glslc_available()) GTEST_SKIP() << "glslc not found"; test::Context const context{}; - auto const spvPath = std::filesystem::temp_directory_path() / "cvk_test_compiled.vert.spv"; + auto const spvPath = temp_dir() / "cvk_test_compiled.vert.spv"; Shader const shader{context.core, VK_SHADER_STAGE_VERTEX_BIT, spvPath, test::SHADER_DIR / "triangle.vert"}; @@ -122,10 +162,10 @@ CVK_TEST(Shader, compile_and_create) { } CVK_TEST(Shader, compile_error_throws) { - if(std::system("glslc --version >" CVK_NULL_DEVICE " 2>&1") != 0) GTEST_SKIP() << "glslc not found"; + if(!glslc_available()) GTEST_SKIP() << "glslc not found"; constexpr std::string_view broken = "#version 450\nvoid main() { undeclared = 1; }\n"; auto const glslPath = write_temp("cvk_test_broken.vert", broken); - auto const spvPath = std::filesystem::temp_directory_path() / "cvk_test_broken.vert.spv"; + auto const spvPath = temp_dir() / "cvk_test_broken.vert.spv"; EXPECT_THROW(Shader::compile(glslPath, spvPath), cvk_exception); @@ -133,8 +173,7 @@ CVK_TEST(Shader, compile_error_throws) { } CVK_TEST(Shader, compile_missing_glsl_throws) { - auto const missing = std::filesystem::temp_directory_path() / "cvk_test_missing.vert"; - std::filesystem::remove(missing); + auto const missing = temp_dir() / "cvk_test_missing.vert"; EXPECT_THROW(Shader::compile(missing, missing.string() + ".spv"), cvk_exception); } diff --git a/cvk/tests/src/render/sync/barrier_test.cpp b/cvk/tests/src/render/sync/barrier_test.cpp index c85ac3db..10b666fd 100644 --- a/cvk/tests/src/render/sync/barrier_test.cpp +++ b/cvk/tests/src/render/sync/barrier_test.cpp @@ -199,7 +199,7 @@ CVK_TEST(Barrier, aggregate_is_constexpr) { CVK_TEST(Barrier, add_and_clear) { test::Context const context{}; BaseBuffer const buffer{context.core, {.usage = VK_BUFFER_USAGE_TRANSFER_DST_BIT}, 64}; - Barrier barrier{}; + Barrier barrier{VK_DEPENDENCY_BY_REGION_BIT}; EXPECT_TRUE(barrier.empty()); barrier.add(FRAGMENT_READ, TRANSFER_WRITE); @@ -208,6 +208,7 @@ CVK_TEST(Barrier, add_and_clear) { barrier.clear(); EXPECT_TRUE(barrier.empty()); + EXPECT_EQ(barrier.flags, VK_DEPENDENCY_BY_REGION_BIT); } CVK_TEST(Barrier, execute_memory_and_buffer_barriers) { @@ -218,8 +219,6 @@ CVK_TEST(Barrier, execute_memory_and_buffer_barriers) { barrier.add(buffer, {.src = TRANSFER_WRITE, .dst = FRAGMENT_READ, .offset = 16, .size = 32}); test::record_and_submit(context, [&](PrimaryCmdBuffer const& primary) { barrier.execute(primary); }); - - EXPECT_EQ(barrier.flags, VK_DEPENDENCY_BY_REGION_BIT); } CVK_TEST(Barrier, image_layout_transition) { diff --git a/cvk/tests/src/render/sync/fence_test.cpp b/cvk/tests/src/render/sync/fence_test.cpp index 046f849a..d40e76de 100644 --- a/cvk/tests/src/render/sync/fence_test.cpp +++ b/cvk/tests/src/render/sync/fence_test.cpp @@ -174,6 +174,7 @@ CVK_TEST(Fence, extract_os_handle_of_a_signaled_fence) { CVK_TEST(Fence, release_and_wrap_keep_the_export_types) { test::Context const context{}; + if(!os_fence_exportable(context.core)) GTEST_SKIP() << "os fence handle type not exportable"; Fence fence{context.core, create, 0, constants::OS_FENCE_HANDLE_TYPE}; auto const state = fence.release(); diff --git a/cvk/tests/src/surface/surface_test.cpp b/cvk/tests/src/surface/surface_test.cpp index 52f086bd..f0d8f76b 100644 --- a/cvk/tests/src/surface/surface_test.cpp +++ b/cvk/tests/src/surface/surface_test.cpp @@ -1,7 +1,6 @@ #include "context.hpp" #include "headless.hpp" #include "test.hpp" -#include "validation.hpp" #include "cvk/base/instance.hpp" #include "cvk/base/physical_device.hpp" @@ -10,19 +9,14 @@ #include "cvk/utility/vk_overloads.hpp" #include -#include #include namespace cvk { -namespace { - constexpr std::array PRESENT_QUEUES{QueueFamilyProperties::GRAPHICS | QueueFamilyProperties::PRESENT}; -} - CVK_TEST(Surface, wrap_takes_ownership) { - Instance const instance{test::instance_config(test::headless_extensions()), create}; + test::Context const context{}; - auto surface = test::headless_surface(instance); + auto surface = test::headless_surface(context.instance); ASSERT_TRUE(surface.created()); EXPECT_NE(surface.get(), VK_NULL_HANDLE); @@ -32,13 +26,13 @@ CVK_TEST(Surface, wrap_takes_ownership) { } CVK_TEST(Surface, release_keeps_the_surface) { - Instance const instance{test::instance_config(test::headless_extensions()), create}; - auto surface = test::headless_surface(instance); + test::Context const context{}; + auto surface = test::headless_surface(context.instance); auto const state = surface.release(); EXPECT_FALSE(surface.created()); - Surface::destroy(instance.get(), state.vkSurface.get()); + Surface::destroy(context.instance.get(), state.vkSurface.get()); } CVK_TEST(Surface, formats_are_every_offered_format) { @@ -77,11 +71,9 @@ CVK_TEST(Surface, present_modes_are_every_offered_present_mode) { } CVK_TEST(Surface, capabilities) { - Instance const instance{test::instance_config(test::headless_extensions()), create}; - std::array const surfaces{test::headless_surface(instance)}; - auto const [device, _] = test::pick(instance, PRESENT_QUEUES, surfaces); + test::PresentContext const context{}; - auto const capabilities = surfaces[0].capabilities(device); + auto const capabilities = context.surface().capabilities(context.core.physicalDevice()); EXPECT_GE(capabilities.minImageCount, 1); } @@ -89,18 +81,14 @@ CVK_TEST(Surface, capabilities) { CVK_TEST(Surface, move_assignment_destroys_the_old_surface) { if constexpr(!constants::ENABLE_VALIDATION_LAYERS) GTEST_SKIP() << "needs the validation layers"; - test::reset_validation_errors(); - { - Instance const instance{test::instance_config(test::headless_extensions()), create, test::counting_messenger()}; - auto surface = test::headless_surface(instance); - auto other = test::headless_surface(instance); - auto const vkSurface = other.get(); + test::InstanceContext const context{test::headless_extensions()}; + auto surface = test::headless_surface(context.instance); + auto other = test::headless_surface(context.instance); + auto const vkSurface = other.get(); - surface = std::move(other); + surface = std::move(other); - EXPECT_EQ(surface.get(), vkSurface); - EXPECT_FALSE(other.created()); - } - EXPECT_EQ(test::validation_errors(), 0); + EXPECT_EQ(surface.get(), vkSurface); + EXPECT_FALSE(other.created()); } } diff --git a/cvk/tests/src/surface/swapchain_test.cpp b/cvk/tests/src/surface/swapchain_test.cpp index 06738d58..70a017dd 100644 --- a/cvk/tests/src/surface/swapchain_test.cpp +++ b/cvk/tests/src/surface/swapchain_test.cpp @@ -54,7 +54,7 @@ namespace { if(!index.has_value()) return result; auto const& image = swapchain.images()[*index]; - CmdPool const pool{context.core, CmdPool::Config{.queueFamilyIndex = context.queue().familyIndex()}, create}; + CmdPool const pool{context.core, test::pool_config(context.core), create}; PrimaryCmdBuffer cmd{pool, create}; cmd.begin(VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT); diff --git a/cvk/tests/src/utility/handle/unique_handle_test.cpp b/cvk/tests/src/utility/handle/unique_handle_test.cpp index 3808d902..5a8cc8dd 100644 --- a/cvk/tests/src/utility/handle/unique_handle_test.cpp +++ b/cvk/tests/src/utility/handle/unique_handle_test.cpp @@ -1,7 +1,6 @@ #include "context.hpp" #include "headless.hpp" #include "test.hpp" -#include "validation.hpp" #include "cvk/base/instance.hpp" #include "cvk/data/memory/memory.hpp" @@ -234,16 +233,12 @@ CVK_TEST(unique_handle, release_keeps_the_handle) { CVK_TEST(unique_handle, instance_deleter) { if constexpr(!constants::ENABLE_VALIDATION_LAYERS) GTEST_SKIP() << "needs the validation layers"; - test::reset_validation_errors(); - { - Instance const instance{test::instance_config(test::headless_extensions()), create, test::counting_messenger()}; - auto surface = test::headless_surface(instance); + test::InstanceContext const context{test::headless_extensions()}; + auto surface = test::headless_surface(context.instance); - instance_handle handle{surface.release().vkSurface.get(), instance.get()}; + instance_handle handle{surface.release().vkSurface.get(), context.instance.get()}; - EXPECT_EQ(handle.vkInstance(), instance.get()); - } - EXPECT_EQ(test::validation_errors(), 0); + EXPECT_EQ(handle.vkInstance(), context.instance.get()); } CVK_TEST(unique_handle, handle_deleter) { From e94ea02a6f3173f71825bcfb52aed5cbab51e825 Mon Sep 17 00:00:00 2001 From: lukas <89145039+codethinki@users.noreply.github.com> Date: Wed, 21 Oct 2026 23:59:03 +0200 Subject: [PATCH 128/135] cvk: docs for the guarantees the tests check --- cvk/incl/cvk/base/core.hpp | 3 ++- cvk/incl/cvk/base/device.hpp | 2 ++ cvk/incl/cvk/base/queue/queue.hpp | 4 +++- cvk/incl/cvk/data/memory/memory_config.hpp | 3 +++ cvk/incl/cvk/render/cmd/cmd_buffer.hpp | 2 +- cvk/incl/cvk/utility/handle/unique_handle.hpp | 3 ++- 6 files changed, 13 insertions(+), 4 deletions(-) diff --git a/cvk/incl/cvk/base/core.hpp b/cvk/incl/cvk/base/core.hpp index 17d6279f..565a7da1 100644 --- a/cvk/incl/cvk/base/core.hpp +++ b/cvk/incl/cvk/base/core.hpp @@ -25,7 +25,8 @@ class Surface; /** * @brief picks the physical device and owns it with the device, the allocator and the queues - * @details the instance outlives the core + * @details the instance outlives the core, + * the queues stay valid when the core moves */ class Core { public: diff --git a/cvk/incl/cvk/base/device.hpp b/cvk/incl/cvk/base/device.hpp index 2fd08d3d..aef4a2e7 100644 --- a/cvk/incl/cvk/base/device.hpp +++ b/cvk/incl/cvk/base/device.hpp @@ -22,6 +22,7 @@ class PhysicalDevice; /** * @brief VkDevice wrapper, owns the function table and the queues + * @details the queues stay valid when the device moves */ class Device { public: @@ -68,6 +69,7 @@ class Device { /** * @brief creates the device with the features and extensions of the config and retrieves the queues + * @details queues of one family get the indices 0, 1, ... in the order of @ref Config::queueFamilyIndices * @pre the physical device has the features and the extensions of the config * @post @ref created() * @note calls @ref optDestroy() diff --git a/cvk/incl/cvk/base/queue/queue.hpp b/cvk/incl/cvk/base/queue/queue.hpp index 21bb2da4..ae71b4bc 100644 --- a/cvk/incl/cvk/base/queue/queue.hpp +++ b/cvk/incl/cvk/base/queue/queue.hpp @@ -16,7 +16,9 @@ class Fence; /** * @brief VkQueue of a device * @details copyable, copies share the vulkan queue, - * the device owns the vulkan queue, a queue has no lifecycle + * the device owns the vulkan queue, a queue has no lifecycle, + * a copy stays valid while the VkDevice and its function table exist, + * also through a release and wrap of the device with the function table of the state * @attention not thread safe: one thread at a time per vulkan queue, Vulkan requires it */ class Queue { diff --git a/cvk/incl/cvk/data/memory/memory_config.hpp b/cvk/incl/cvk/data/memory/memory_config.hpp index bebb0703..0b9fe3e6 100644 --- a/cvk/incl/cvk/data/memory/memory_config.hpp +++ b/cvk/incl/cvk/data/memory/memory_config.hpp @@ -28,6 +28,9 @@ struct MemoryConfig { */ VkMemoryPropertyFlags preferredFlags = 0; + /** + * @return create info with the members of the config, no pool + */ [[nodiscard]] constexpr VmaAllocationCreateInfo createInfo() const { return VmaAllocationCreateInfo{ .flags = flags, diff --git a/cvk/incl/cvk/render/cmd/cmd_buffer.hpp b/cvk/incl/cvk/render/cmd/cmd_buffer.hpp index 3dbcde30..2e3eac9b 100644 --- a/cvk/incl/cvk/render/cmd/cmd_buffer.hpp +++ b/cvk/incl/cvk/render/cmd/cmd_buffer.hpp @@ -299,7 +299,7 @@ class BaseCmdBuffer { [[nodiscard]] constexpr VkCommandBufferLevel level() const { return _level; } /** - * @return recording, a reset of the pool ends it + * @return recording, a reset of the pool ends it, a moved from command buffer does not record */ [[nodiscard]] constexpr bool recording() const { return created() && _recording && _beginResets == pool().resets(); diff --git a/cvk/incl/cvk/utility/handle/unique_handle.hpp b/cvk/incl/cvk/utility/handle/unique_handle.hpp index 943b1967..dd521196 100644 --- a/cvk/incl/cvk/utility/handle/unique_handle.hpp +++ b/cvk/incl/cvk/utility/handle/unique_handle.hpp @@ -32,7 +32,8 @@ struct handle_deleter { * a deleter derived from them or one of its own * @details a handle comes only together with its context: the constructors and reset() construct the deleter, * a missing context argument does not compile, - * move assignment destroys the old handle with the old context, then takes handle and context + * move assignment destroys the old handle with the old context, then takes handle and context, + * the size is the size of the handle plus the size of the context */ template class unique_handle { From d95b5eede77f2d31df11ba43e67140dc9b4b164a Mon Sep 17 00:00:00 2001 From: lukas <89145039+codethinki@users.noreply.github.com> Date: Wed, 21 Oct 2026 23:59:03 +0200 Subject: [PATCH 129/135] cvk: graphics and compute families count as transfer, shader log next to the spv, style fixes --- cvk/incl/cvk/base/queue/queue_family.hpp | 10 +++++++++- cvk/incl/cvk/data/img/image_config.hpp | 2 +- cvk/incl/cvk/debug/debug_messenger.hpp | 6 +++--- cvk/incl/cvk/render/pipeline/shader/shader.hpp | 3 ++- cvk/incl/cvk/utility/format.hpp | 2 +- cvk/src/base/device.cpp | 2 +- cvk/src/render/pipeline/shader/shader.cpp | 5 +++-- cvk/tests/src/base/queue/queue_test.cpp | 8 ++++++++ cvk/tests/src/render/pipeline/shader/shader_test.cpp | 2 ++ 9 files changed, 30 insertions(+), 10 deletions(-) diff --git a/cvk/incl/cvk/base/queue/queue_family.hpp b/cvk/incl/cvk/base/queue/queue_family.hpp index 9ef5e054..0d9402ca 100644 --- a/cvk/incl/cvk/base/queue/queue_family.hpp +++ b/cvk/incl/cvk/base/queue/queue_family.hpp @@ -20,11 +20,19 @@ enum class QueueFamilyProperties { CTH_GEN_ENUM_FLAG_OVERLOADS(cvk::QueueFamilyProperties) namespace cvk { +/** + * @param presentSupport the family presents to the surfaces + * @return the properties of a queue family with the flags + * @details graphics or compute -> transfer, also without VK_QUEUE_TRANSFER_BIT: + * Vulkan lets graphics and compute families leave the bit out + */ constexpr QueueFamilyProperties to_queue_properties(VkQueueFlags flags, bool presentSupport) { + constexpr VkQueueFlags transfer = VK_QUEUE_TRANSFER_BIT | VK_QUEUE_GRAPHICS_BIT | VK_QUEUE_COMPUTE_BIT; + auto result = QueueFamilyProperties::NONE; if(flags & VK_QUEUE_GRAPHICS_BIT) result |= QueueFamilyProperties::GRAPHICS; if(flags & VK_QUEUE_COMPUTE_BIT) result |= QueueFamilyProperties::COMPUTE; - if(flags & VK_QUEUE_TRANSFER_BIT) result |= QueueFamilyProperties::TRANSFER; + if(flags & transfer) result |= QueueFamilyProperties::TRANSFER; if(presentSupport) result |= QueueFamilyProperties::PRESENT; return result; } diff --git a/cvk/incl/cvk/data/img/image_config.hpp b/cvk/incl/cvk/data/img/image_config.hpp index 34ebeb11..345e7c6f 100644 --- a/cvk/incl/cvk/data/img/image_config.hpp +++ b/cvk/incl/cvk/data/img/image_config.hpp @@ -1,4 +1,4 @@ - #pragma once +#pragma once #include "image_format.hpp" #include "cvk/base/queue/queue_family.hpp" diff --git a/cvk/incl/cvk/debug/debug_messenger.hpp b/cvk/incl/cvk/debug/debug_messenger.hpp index ac62631e..cbc2d921 100644 --- a/cvk/incl/cvk/debug/debug_messenger.hpp +++ b/cvk/incl/cvk/debug/debug_messenger.hpp @@ -91,7 +91,7 @@ class DebugMessenger { static void destroy(vk_not_null, VkDebugUtilsMessengerEXT vkMessenger); /** - * @brief releases ownership, returns state and forgets + * @brief releases ownership, returns the state and forgets * @pre @ref created() */ State release(); @@ -121,7 +121,7 @@ class DebugMessenger { [[nodiscard]] constexpr bool created() const { return static_cast(_handle); } [[nodiscard]] constexpr vk_handle_t get() const { return _handle.get(); } - [[nodiscard]] constexpr Config config() const { return _config; } + [[nodiscard]] constexpr Config const& config() const { return _config; } private: void forget(); @@ -147,7 +147,7 @@ struct DebugMessenger::State { namespace cvk { constexpr void DebugMessenger::debug_check(DebugMessenger const& debugMessenger) { - CTH_CRITICAL(!debugMessenger.created(), "debug_messenger not created") {} + CTH_CRITICAL(!debugMessenger.created(), "debug messenger must be created") {} } constexpr void DebugMessenger::Config::debug_check(Config const& config) { diff --git a/cvk/incl/cvk/render/pipeline/shader/shader.hpp b/cvk/incl/cvk/render/pipeline/shader/shader.hpp index 05fd3793..69623bb0 100644 --- a/cvk/incl/cvk/render/pipeline/shader/shader.hpp +++ b/cvk/incl/cvk/render/pipeline/shader/shader.hpp @@ -144,7 +144,8 @@ class Shader { * @brief compiles a glsl file to a SPIR-V file * @param compilerPath glslc compatible compiler * @param flags for the compiler - * @details compiler output without error -> logged as warning + * @details the compiler writes its output to the file spvPath + ".log", the call reads and removes the file, + * compiler output without error -> logged as warning * @throws cvk::cvk_exception reason: glsl file missing, compiler failed, details: compiler output */ static void compile( diff --git a/cvk/incl/cvk/utility/format.hpp b/cvk/incl/cvk/utility/format.hpp index 55eb6234..2c3e0c60 100644 --- a/cvk/incl/cvk/utility/format.hpp +++ b/cvk/incl/cvk/utility/format.hpp @@ -1,5 +1,5 @@ -// ReSharper disable CppClangTidyCertDcl58Cpp #pragma once +// ReSharper disable CppClangTidyCertDcl58Cpp #include #include diff --git a/cvk/src/base/device.cpp b/cvk/src/base/device.cpp index 9854dd21..f659d027 100644 --- a/cvk/src/base/device.cpp +++ b/cvk/src/base/device.cpp @@ -132,7 +132,7 @@ void Device::createLogicalDevice(span queueFamiliesQueueCounts) std::ranges::all_of(queueFamiliesQueueCounts, [](uint32_t count) { return count == 0; }), "at least one queue is required" ) {} - CTH_CRITICAL(created(), "device was already created") {} + CTH_CRITICAL(created(), "device must not be created") {} vector queueCreateInfos{}; queueCreateInfos.reserve(queueFamiliesQueueCounts.size()); diff --git a/cvk/src/render/pipeline/shader/shader.cpp b/cvk/src/render/pipeline/shader/shader.cpp index 10c07819..86f37539 100644 --- a/cvk/src/render/pipeline/shader/shader.cpp +++ b/cvk/src/render/pipeline/shader/shader.cpp @@ -143,8 +143,9 @@ void Shader::compile( #else constexpr std::string_view nullDevice = "/dev/null"; #endif - auto const logName = std::format("cvk_{}.log", spvPath.filename().string()); - auto const logPath = std::filesystem::temp_directory_path() / logName; + // one log per SPIR-V path: two processes that compile into other directories do not share a log + auto logPath = spvPath; + logPath += ".log"; auto command = std::format( R"("{}" {} -c "{}" -o "{}" >{} 2>"{}")", diff --git a/cvk/tests/src/base/queue/queue_test.cpp b/cvk/tests/src/base/queue/queue_test.cpp index 7cbf07ec..b1ade939 100644 --- a/cvk/tests/src/base/queue/queue_test.cpp +++ b/cvk/tests/src/base/queue/queue_test.cpp @@ -24,6 +24,14 @@ namespace { }; } +CVK_TEST(QueueFamily, graphics_or_compute_supports_transfer) { + using enum QueueFamilyProperties; + static_assert(to_queue_properties(VK_QUEUE_GRAPHICS_BIT, false) == (GRAPHICS | TRANSFER)); + static_assert(to_queue_properties(VK_QUEUE_COMPUTE_BIT, false) == (COMPUTE | TRANSFER)); + static_assert(to_queue_properties(VK_QUEUE_TRANSFER_BIT, true) == (TRANSFER | PRESENT)); + static_assert(to_queue_properties(VK_QUEUE_SPARSE_BINDING_BIT, false) == NONE); +} + CVK_TEST(Queue, copies_share_the_queue) { test::Context const context{}; auto const& queue = context.core.queue(0); diff --git a/cvk/tests/src/render/pipeline/shader/shader_test.cpp b/cvk/tests/src/render/pipeline/shader/shader_test.cpp index a0ceb4cb..7b0ebcea 100644 --- a/cvk/tests/src/render/pipeline/shader/shader_test.cpp +++ b/cvk/tests/src/render/pipeline/shader/shader_test.cpp @@ -157,6 +157,7 @@ CVK_TEST(Shader, compile_and_create) { EXPECT_TRUE(shader.created()); EXPECT_TRUE(std::filesystem::exists(spvPath)); + EXPECT_FALSE(std::filesystem::exists(spvPath.string() + ".log")); std::filesystem::remove(spvPath); } @@ -168,6 +169,7 @@ CVK_TEST(Shader, compile_error_throws) { auto const spvPath = temp_dir() / "cvk_test_broken.vert.spv"; EXPECT_THROW(Shader::compile(glslPath, spvPath), cvk_exception); + EXPECT_FALSE(std::filesystem::exists(spvPath.string() + ".log")); std::filesystem::remove(glslPath); } From 44251b4b6b82fd22d0a1e58e6ac0d53718ee37f1 Mon Sep 17 00:00:00 2001 From: lukas <89145039+codethinki@users.noreply.github.com> Date: Thu, 22 Oct 2026 00:48:05 +0200 Subject: [PATCH 130/135] lib: RGFW with Wayland and Win32 only, wayland protocols generated at build time, five fixes on the fork branch --- ci/docker/linux/Dockerfile | 5 ++- lib/cmake/rgfw.cmake | 82 ++++++++++++++++++++++++++++++++++++++ lib/rgfw | 2 +- vcpkg.json | 10 ++++- 4 files changed, 96 insertions(+), 3 deletions(-) create mode 100644 lib/cmake/rgfw.cmake diff --git a/ci/docker/linux/Dockerfile b/ci/docker/linux/Dockerfile index 37aaa6b2..24ab254d 100644 --- a/ci/docker/linux/Dockerfile +++ b/ci/docker/linux/Dockerfile @@ -3,6 +3,7 @@ FROM cachyos/cachyos:latest # vulkan-swrast: lavapipe, the cpu vulkan driver of mesa # vulkan-validation-layers: debug builds enable them with synchronization validation # shaderc: glslc for the shader compile tests +# wayland, libxkbcommon: the RGFW Wayland backend of cgfw, the vcpkg ports of them are empty on linux RUN pacman -Syu --noconfirm && \ pacman -S --noconfirm \ base-devel \ @@ -23,7 +24,9 @@ RUN pacman -Syu --noconfirm && \ vulkan-icd-loader \ vulkan-swrast \ vulkan-validation-layers \ - shaderc + shaderc \ + wayland \ + libxkbcommon # the tests run on lavapipe, also if another driver is installed ENV VK_DRIVER_FILES=/usr/share/vulkan/icd.d/lvp_icd.json diff --git a/lib/cmake/rgfw.cmake b/lib/cmake/rgfw.cmake new file mode 100644 index 00000000..85368f8e --- /dev/null +++ b/lib/cmake/rgfw.cmake @@ -0,0 +1,82 @@ +# RGFW from the lib/rgfw submodule: the header with its platform libraries +# cgfw compiles the implementation (RGFW_IMPLEMENTATION) in one C source file +# platforms: Win32 and Wayland, no X11, no OpenGL and no EGL +if(NOT TARGET rgfw::rgfw) + set(RGFW_DIR "${CMAKE_CURRENT_LIST_DIR}/../rgfw") + if(NOT EXISTS "${RGFW_DIR}/RGFW.h") + message(FATAL_ERROR "lib/rgfw is empty, run: git submodule update --init lib/rgfw") + endif() + + add_library(rgfw INTERFACE) + add_library(rgfw::rgfw ALIAS rgfw) + + # the implementation and the generated protocol code are C, + # cth_set_latest_c_cpp_standard() sets the C standard to the C++ one, C has no 26 + set(CMAKE_C_STANDARD 17) + enable_language(C) + + target_include_directories(rgfw INTERFACE "${RGFW_DIR}") + # RGFWDEF empty: the functions have external linkage, the default inline needs the definitions in every user + target_compile_definitions(rgfw INTERFACE RGFW_VULKAN "RGFWDEF=") + + if(WIN32) + # RGFW includes windows.h: no min and max macros + target_compile_definitions(rgfw INTERFACE NOMINMAX) + # RGFW loads winmm, dwmapi, shcore and opengl32 at run time + target_link_libraries(rgfw INTERFACE gdi32 user32 shell32) + elseif(CMAKE_SYSTEM_NAME STREQUAL "Linux") + target_compile_definitions(rgfw INTERFACE RGFW_WAYLAND RGFW_NO_X11) + + # wayland and xkbcommon come from the system, the vcpkg ports of them are empty on linux + find_package(PkgConfig REQUIRED) + pkg_check_modules(RGFW_WAYLAND REQUIRED IMPORTED_TARGET wayland-client wayland-cursor xkbcommon) + find_program(WAYLAND_SCANNER wayland-scanner REQUIRED) + + # the protocol XML files of the vcpkg port wayland-protocols + pkg_get_variable(WAYLAND_PROTOCOLS_DIR wayland-protocols pkgdatadir) + if(NOT WAYLAND_PROTOCOLS_DIR) + message(FATAL_ERROR "wayland-protocols not found, vcpkg installs it on linux") + endif() + + # the protocols that RGFW includes, see the xdg-shell.c rule in lib/rgfw/Makefile + set(RGFW_WAYLAND_PROTOCOLS + "stable/xdg-shell/xdg-shell.xml" + "unstable/xdg-decoration/xdg-decoration-unstable-v1.xml" + "staging/xdg-toplevel-icon/xdg-toplevel-icon-v1.xml" + "unstable/xdg-output/xdg-output-unstable-v1.xml" + "unstable/relative-pointer/relative-pointer-unstable-v1.xml" + "unstable/pointer-constraints/pointer-constraints-unstable-v1.xml" + ) + + set(RGFW_PROTOCOL_DIR "${CMAKE_BINARY_DIR}/lib/rgfw/wayland") + file(MAKE_DIRECTORY "${RGFW_PROTOCOL_DIR}") + + set(RGFW_PROTOCOL_FILES "") + foreach(protocol IN LISTS RGFW_WAYLAND_PROTOCOLS) + get_filename_component(name "${protocol}" NAME_WE) + set(xml "${WAYLAND_PROTOCOLS_DIR}/${protocol}") + set(header "${RGFW_PROTOCOL_DIR}/${name}.h") + set(code "${RGFW_PROTOCOL_DIR}/${name}.c") + + add_custom_command( + OUTPUT "${header}" "${code}" + COMMAND "${WAYLAND_SCANNER}" client-header "${xml}" "${header}" + COMMAND "${WAYLAND_SCANNER}" public-code "${xml}" "${code}" + DEPENDS "${xml}" + COMMENT "wayland-scanner ${name}" + VERBATIM + ) + list(APPEND RGFW_PROTOCOL_FILES "${header}" "${code}") + endforeach() + + add_library(rgfw_wayland_protocols STATIC ${RGFW_PROTOCOL_FILES}) + target_include_directories(rgfw_wayland_protocols PUBLIC "${RGFW_PROTOCOL_DIR}") + target_link_libraries(rgfw_wayland_protocols PUBLIC PkgConfig::RGFW_WAYLAND) + + target_link_libraries(rgfw INTERFACE rgfw_wayland_protocols) + + #TODO libdecor for frames on GNOME, GNOME has no xdg-decoration (KDE and sway have it), vcpkg has no port + else() + message(FATAL_ERROR "RGFW: only Windows and Linux (Wayland) are supported, got ${CMAKE_SYSTEM_NAME}") + endif() +endif() diff --git a/lib/rgfw b/lib/rgfw index d96684e6..951c486a 160000 --- a/lib/rgfw +++ b/lib/rgfw @@ -1 +1 @@ -Subproject commit d96684e6877d3ef11c731b122b8949942ed071c9 +Subproject commit 951c486a8145f63b93aff62f3072c456d2fca3ab diff --git a/vcpkg.json b/vcpkg.json index 74ddfe1d..33d4ab7c 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -5,6 +5,14 @@ "gtest", "volk", "vulkan-memory-allocator", - "vulkan-utility-libraries" + "vulkan-utility-libraries", + { + "name": "wayland", + "platform": "linux" + }, + { + "name": "wayland-protocols", + "platform": "linux" + } ] } From ffad759fb034881f8974a4a6374d5169ad376c26 Mon Sep 17 00:00:00 2001 From: lukas <89145039+codethinki@users.noreply.github.com> Date: Thu, 22 Oct 2026 00:48:05 +0200 Subject: [PATCH 131/135] cgfw: window module over RGFW, Vulkan surface into cvk, behavior tests that skip without a display --- AGENTS.md | 1 + CMakeLists.txt | 2 + cgfw/CMakeLists.txt | 34 ++++ cgfw/incl/cgfw/utility/exception.hpp | 38 +++++ cgfw/incl/cgfw/window/window.hpp | 224 ++++++++++++++++++++++++++ cgfw/src/pch/glob/cth.hpp | 6 + cgfw/src/pch/glob/std.hpp | 9 ++ cgfw/src/pch/glob/volk.hpp | 2 + cgfw/src/pch/pch.hpp | 6 + cgfw/src/window/rgfw.c | 8 + cgfw/src/window/window.cpp | 122 ++++++++++++++ cgfw/tests/CMakeLists.txt | 23 +++ cgfw/tests/src/context.cpp | 40 +++++ cgfw/tests/src/context.hpp | 40 +++++ cgfw/tests/src/environment.cpp | 24 +++ cgfw/tests/src/pch/glob/gtest.hpp | 2 + cgfw/tests/src/pch/pch.hpp | 5 + cgfw/tests/src/test.hpp | 41 +++++ cgfw/tests/src/window/window_test.cpp | 125 ++++++++++++++ 19 files changed, 752 insertions(+) create mode 100644 cgfw/CMakeLists.txt create mode 100644 cgfw/incl/cgfw/utility/exception.hpp create mode 100644 cgfw/incl/cgfw/window/window.hpp create mode 100644 cgfw/src/pch/glob/cth.hpp create mode 100644 cgfw/src/pch/glob/std.hpp create mode 100644 cgfw/src/pch/glob/volk.hpp create mode 100644 cgfw/src/pch/pch.hpp create mode 100644 cgfw/src/window/rgfw.c create mode 100644 cgfw/src/window/window.cpp create mode 100644 cgfw/tests/CMakeLists.txt create mode 100644 cgfw/tests/src/context.cpp create mode 100644 cgfw/tests/src/context.hpp create mode 100644 cgfw/tests/src/environment.cpp create mode 100644 cgfw/tests/src/pch/glob/gtest.hpp create mode 100644 cgfw/tests/src/pch/pch.hpp create mode 100644 cgfw/tests/src/test.hpp create mode 100644 cgfw/tests/src/window/window_test.cpp diff --git a/AGENTS.md b/AGENTS.md index 2557c081..d43dc66c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -8,6 +8,7 @@ Read it before you change a file. Match the existing code. | Path | Content | |---|---| | `cvk/` | C++ wrapper of the Vulkan C API: one class per Vulkan object, objects own their handle | +| `cgfw/` | C++ wrapper of RGFW: windows of the platform (Win32, Wayland) and their Vulkan surface | | `jvk/` | coroutine layer on top of `cvk` (not started) | | `jvk/prework/` | parts for `jvk` that left `cvk`, not built | | `legacy/` | the old `jvk`, `jolly` and `demo` code, reference only, not built | diff --git a/CMakeLists.txt b/CMakeLists.txt index 59184d0b..d1eb2b72 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -17,6 +17,7 @@ if(LLD_LINKER AND NOT MSVC) endif() include(lib/cmake/cth.cmake) +include(lib/cmake/rgfw.cmake) if(JOLLY_ENABLE_TESTS) enable_testing() @@ -24,6 +25,7 @@ endif() # Add subprojects add_subdirectory(cvk) +add_subdirectory(cgfw) # Tests if(JOLLY_ENABLE_TESTS) diff --git a/cgfw/CMakeLists.txt b/cgfw/CMakeLists.txt new file mode 100644 index 00000000..99bac6fc --- /dev/null +++ b/cgfw/CMakeLists.txt @@ -0,0 +1,34 @@ +cth_glob_cpp(CGFW_SRC "src") +cth_glob_cpp(CGFW_INCL "incl") + +add_library(cgfw STATIC ${CGFW_SRC} ${CGFW_INCL}) + +cth_target_enable_build_cache(cgfw OPTIONAL) + +target_include_directories( + cgfw + PUBLIC incl + PRIVATE src +) + +target_precompile_headers( + cgfw + PRIVATE + src/pch/pch.hpp +) + +# the RGFW implementation is C, cth_glob_cpp() skips .c files, the pch is C++ +target_sources(cgfw PRIVATE src/window/rgfw.c) +set_source_files_properties(src/window/rgfw.c PROPERTIES SKIP_PRECOMPILE_HEADERS ON) + +# the public headers do not include RGFW.h +target_link_libraries( + cgfw + PUBLIC cvk + PRIVATE rgfw::rgfw +) + +# tests +if(JOLLY_ENABLE_TESTS) + add_subdirectory(tests) +endif() diff --git a/cgfw/incl/cgfw/utility/exception.hpp b/cgfw/incl/cgfw/utility/exception.hpp new file mode 100644 index 00000000..4a103cec --- /dev/null +++ b/cgfw/incl/cgfw/utility/exception.hpp @@ -0,0 +1,38 @@ +#pragma once +#include +#include + +#include +#include +#include +#include +#include + + +namespace cgfw { +/** + * @brief exception of cgfw, the message starts with "cgfw: " + */ +class cgfw_exception : public cth::except::default_exception { + static constexpr auto PREFIX = "cgfw: "; + +public: + cgfw_exception( + std::string_view msg, + cth::except::Severity const severity, + std::source_location loc, + std::stacktrace trace + ) : default_exception{ + std::format("{}{}", PREFIX, msg), + severity, + std::move(loc), + std::move(trace) + } {} + + explicit cgfw_exception(cth::except::default_exception defaultException) + : default_exception{std::move(defaultException)} { prepend(PREFIX); } +}; +} + +#define CGFW_STABLE_THROW(expression, msg, ...) \ + CTH_STABLE_THROW_T(cgfw::cgfw_exception, expression, msg, __VA_ARGS__) diff --git a/cgfw/incl/cgfw/window/window.hpp b/cgfw/incl/cgfw/window/window.hpp new file mode 100644 index 00000000..640d3234 --- /dev/null +++ b/cgfw/incl/cgfw/window/window.hpp @@ -0,0 +1,224 @@ +#pragma once +#include "cvk/surface/surface.hpp" +#include "cvk/utility/constants.hpp" +#include "cvk/utility/handle/unique_handle.hpp" +#include "cvk/utility/types.hpp" + +#include + +#include +#include + +#include +#include +#include + + +struct RGFW_window; + +namespace cvk { +class Instance; +} + +namespace cgfw { +using cvk::create; +using cvk::create_t; + +/** + * @brief creation flags of a @ref Window, the values of the RGFW_windowFlags + */ +enum class WindowFlags : uint32_t { + NONE = 0, + NO_BORDER = cth::en::flag_val(0), + NO_RESIZE = cth::en::flag_val(1), + FULLSCREEN = cth::en::flag_val(4), + CENTER = cth::en::flag_val(6), + SCALE_TO_MONITOR = cth::en::flag_val(8), + /** + * @brief the window is not visible + */ + HIDE = cth::en::flag_val(9), + MAXIMIZE = cth::en::flag_val(10), + FLOATING = cth::en::flag_val(12), + FOCUS_ON_SHOW = cth::en::flag_val(13), + MINIMIZE = cth::en::flag_val(14), +}; +} + +CTH_GEN_ENUM_FLAG_OVERLOADS(cgfw::WindowFlags) + +namespace cgfw { +namespace dev { + /** + * @brief deleter of a window, without context: RGFW keeps one global state + * @details calls @ref Window::destroy(RGFW_window*) + */ + struct window_deleter { + void operator()(RGFW_window*) const; + }; +} + +/** + * @brief RGFW_window wrapper, a window of the platform: Win32 or Wayland + * @details RGFW keeps one global state: the first window initializes it, the destruction of the last one + * deinitializes it, @ref pollEvents() handles the events of every window + * @attention not thread safe: RGFW keeps one global state + */ +class Window { +public: + using handle_t = RGFW_window*; + struct State; + + /** + * @brief title, size and flags of the window + */ + struct Config { + std::string title{}; + + /** + * @brief requested size of the window + * @pre width > 0, height > 0 + */ + VkExtent2D extent{}; + + WindowFlags flags = WindowFlags::NONE; + + static constexpr void debug_check(Config const&); + }; + + /** + * @brief base constructor + */ + explicit Window(Config); + + /** + * @brief constructs and wraps + * @details calls: + * - @ref Window(Config) + * - @ref wrap() + */ + Window(Config, State const&); + + /** + * @brief constructs and creates + * @details calls: + * - @ref Window(Config) + * - @ref create() + */ + explicit Window(Config, create_t); + + /** + * @note calls @ref optDestroy() + */ + ~Window() { optDestroy(); } + + /** + * @brief wraps the state + * @pre RGFW_createWindow() created the window with the config + * @post @ref created() + * @note calls @ref optDestroy() + */ + void wrap(State const&); + + /** + * @brief creates the window with the config + * @details no window exists -> initializes RGFW first + * @pre @ref Config::debug_check() passes for the config + * @post @ref created() + * @note calls @ref optDestroy() + * @throws cgfw::cgfw_exception reason: RGFW_createWindow() failed, e.g. no display + */ + void create(); + + /** + * @brief destroys and forgets + * @pre @ref created() + * @pre every surface of the window is destroyed + * @note the last window deinitializes RGFW + */ + void destroy(); + + /** + * @brief if @ref created() -> calls @ref destroy() + */ + void optDestroy() { if(created()) destroy(); } + + /** + * @brief releases ownership, returns the state and forgets + * @pre @ref created() + */ + State release(); + + /** + * @brief creates a surface of the window + * @pre @ref created() + * @pre the instance enables @ref instance_extensions() + * @throws cgfw::cgfw_exception reason: RGFW_window_createSurface_Vulkan() failed, the message has the VkResult + */ + [[nodiscard]] cvk::Surface surface(cvk::Instance const&) const; + + /** + * @brief handles the pending events of every window + * @details RGFW handles the events of all windows together + * @pre @ref created() + */ + void pollEvents() const; + + /** + * @return the user or the platform requested to close the window + * @details @ref pollEvents() receives the requests + * @pre @ref created() + */ + [[nodiscard]] bool shouldClose() const; + + /** + * @brief closes the window and frees it + * @pre the window is not nullptr + */ + static void destroy(RGFW_window*); + + [[nodiscard]] constexpr bool created() const { return static_cast(_handle); } + [[nodiscard]] constexpr handle_t get() const { return _handle.get(); } + [[nodiscard]] constexpr Config const& config() const { return _config; } + +private: + void forget(); + + Config _config; + cvk::unique_handle _handle{}; + +public: + Window(Window const& other) = delete; + Window(Window&& other) noexcept = default; + Window& operator=(Window const& other) = delete; + Window& operator=(Window&& other) noexcept = default; + + static constexpr void debug_check(Window const&); +}; + +/** + * @return the instance extensions of @ref Window::surface(): VK_KHR_surface and the surface extension of the platform + */ +[[nodiscard]] std::vector instance_extensions(); +} + +namespace cgfw { +struct Window::State { + cvk::not_null_ptr rgfwWindow; +}; +} + +namespace cgfw { +constexpr void Window::Config::debug_check(Config const& config) { + CTH_CRITICAL( + config.extent.width == 0 || config.extent.height == 0, + "the extent must not be 0, got {}x{}", + config.extent.width, + config.extent.height + ) {} +} + +constexpr void Window::debug_check(Window const& window) { + CTH_CRITICAL(!window.created(), "window must be created") {} +} +} diff --git a/cgfw/src/pch/glob/cth.hpp b/cgfw/src/pch/glob/cth.hpp new file mode 100644 index 00000000..7d66d320 --- /dev/null +++ b/cgfw/src/pch/glob/cth.hpp @@ -0,0 +1,6 @@ +#pragma once +#include +#include +#include +#include +#include diff --git a/cgfw/src/pch/glob/std.hpp b/cgfw/src/pch/glob/std.hpp new file mode 100644 index 00000000..52f7dc07 --- /dev/null +++ b/cgfw/src/pch/glob/std.hpp @@ -0,0 +1,9 @@ +#pragma once +#include +#include +#include +#include +#include +#include +#include +#include diff --git a/cgfw/src/pch/glob/volk.hpp b/cgfw/src/pch/glob/volk.hpp new file mode 100644 index 00000000..e78ae406 --- /dev/null +++ b/cgfw/src/pch/glob/volk.hpp @@ -0,0 +1,2 @@ +#pragma once +#include diff --git a/cgfw/src/pch/pch.hpp b/cgfw/src/pch/pch.hpp new file mode 100644 index 00000000..5aeb856c --- /dev/null +++ b/cgfw/src/pch/pch.hpp @@ -0,0 +1,6 @@ +#pragma once + +#include "glob/cth.hpp" +#include "glob/std.hpp" + +#include "glob/volk.hpp" diff --git a/cgfw/src/window/rgfw.c b/cgfw/src/window/rgfw.c new file mode 100644 index 00000000..40827fc9 --- /dev/null +++ b/cgfw/src/window/rgfw.c @@ -0,0 +1,8 @@ +// the RGFW implementation, no other source file defines RGFW_IMPLEMENTATION +// C: the Wayland part of RGFW is no valid C++ (void* conversions, designator order) + +// volk first: RGFW includes vulkan.h, volk needs VK_NO_PROTOTYPES before it +#include + +#define RGFW_IMPLEMENTATION +#include diff --git a/cgfw/src/window/window.cpp b/cgfw/src/window/window.cpp new file mode 100644 index 00000000..658660dd --- /dev/null +++ b/cgfw/src/window/window.cpp @@ -0,0 +1,122 @@ +#include "cgfw/window/window.hpp" + +#include "cgfw/utility/exception.hpp" + +#include "cvk/base/instance.hpp" +#include "cvk/utility/format.hpp" + +#include + +#include + + +namespace cgfw { +namespace { + constexpr bool same_flag(WindowFlags flag, RGFW_windowFlags rgfwFlag) { + return static_cast(flag) == rgfwFlag; + } + + static_assert(same_flag(WindowFlags::NO_BORDER, RGFW_windowNoBorder)); + static_assert(same_flag(WindowFlags::NO_RESIZE, RGFW_windowNoResize)); + static_assert(same_flag(WindowFlags::FULLSCREEN, RGFW_windowFullscreen)); + static_assert(same_flag(WindowFlags::CENTER, RGFW_windowCenter)); + static_assert(same_flag(WindowFlags::SCALE_TO_MONITOR, RGFW_windowScaleToMonitor)); + static_assert(same_flag(WindowFlags::HIDE, RGFW_windowHide)); + static_assert(same_flag(WindowFlags::MAXIMIZE, RGFW_windowMaximize)); + static_assert(same_flag(WindowFlags::FLOATING, RGFW_windowFloating)); + static_assert(same_flag(WindowFlags::FOCUS_ON_SHOW, RGFW_windowFocusOnShow)); + static_assert(same_flag(WindowFlags::MINIMIZE, RGFW_windowMinimize)); +} + +void dev::window_deleter::operator()(RGFW_window* rgfwWindow) const { Window::destroy(rgfwWindow); } + +Window::Window(Config config) : _config{std::move(config)} {} + +Window::Window(Config config, State const& state) : Window{std::move(config)} { wrap(state); } + +Window::Window(Config config, create_t) : Window{std::move(config)} { create(); } + +void Window::wrap(State const& state) { + optDestroy(); + + _handle.reset(state.rgfwWindow.get()); +} + +void Window::create() { + Config::debug_check(_config); + optDestroy(); + + auto* const rgfwWindow = RGFW_createWindow( + _config.title.c_str(), + 0, + 0, + static_cast(_config.extent.width), + static_cast(_config.extent.height), + static_cast(_config.flags) + ); + CGFW_STABLE_THROW(rgfwWindow == nullptr, "failed to create the window {}, e.g. no display", _config.title) { + forget(); + } + + _handle.reset(rgfwWindow); +} + +void Window::destroy() { + debug_check(*this); + + _handle.reset(); + + forget(); +} + +Window::State Window::release() { + debug_check(*this); + + State const state{_handle.release()}; + forget(); + return state; +} + +cvk::Surface Window::surface(cvk::Instance const& instance) const { + debug_check(*this); + cvk::Instance::debug_check(instance); + + VkSurfaceKHR vkSurface = VK_NULL_HANDLE; + auto const result = RGFW_window_createSurface_Vulkan(_handle.get(), instance.get(), &vkSurface); + CGFW_STABLE_THROW( + result != VK_SUCCESS, + "failed to create the surface of the window {}, got {}", + _config.title, + result + ) {} + + return cvk::Surface{instance, cvk::Surface::State{vkSurface}}; +} + +void Window::pollEvents() const { + debug_check(*this); + + RGFW_pollEvents(); +} + +bool Window::shouldClose() const { + debug_check(*this); + + return RGFW_window_shouldClose(_handle.get()) != RGFW_FALSE; +} + +void Window::destroy(RGFW_window* rgfwWindow) { + CTH_CRITICAL(rgfwWindow == nullptr, "rgfwWindow must not be nullptr") {} + + RGFW_window_close(rgfwWindow); +} + +void Window::forget() {} + +std::vector instance_extensions() { + size_t count = 0; + char const** const extensions = RGFW_getRequiredInstanceExtensions_Vulkan(&count); + + return std::vector{std::from_range, std::span{extensions, count}}; +} +} diff --git a/cgfw/tests/CMakeLists.txt b/cgfw/tests/CMakeLists.txt new file mode 100644 index 00000000..773945cd --- /dev/null +++ b/cgfw/tests/CMakeLists.txt @@ -0,0 +1,23 @@ +cth_glob_cpp(CGFW_TESTS_SRC "src") + +add_executable(cgfw_tests ${CGFW_TESTS_SRC}) + +cth_target_enable_build_cache(cgfw_tests OPTIONAL) + +target_include_directories( + cgfw_tests + PRIVATE src +) + +target_precompile_headers( + cgfw_tests + PRIVATE + src/pch/pch.hpp +) + +find_package(GTest CONFIG REQUIRED) + +target_link_libraries(cgfw_tests PRIVATE GTest::gtest_main cgfw) + +# one process: RGFW keeps one global state +add_test(NAME cgfw_tests COMMAND cgfw_tests) diff --git a/cgfw/tests/src/context.cpp b/cgfw/tests/src/context.cpp new file mode 100644 index 00000000..76a85049 --- /dev/null +++ b/cgfw/tests/src/context.cpp @@ -0,0 +1,40 @@ +#include "context.hpp" + +#include + +#include +#include + + +namespace cgfw::test { +namespace { + size_t errors = 0; + + VKAPI_ATTR VkBool32 VKAPI_CALL count_errors( + VkDebugUtilsMessageSeverityFlagBitsEXT severity, + VkDebugUtilsMessageTypeFlagsEXT types, + VkDebugUtilsMessengerCallbackDataEXT const* callbackData, + void* userData + ) { + if(severity >= VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT) ++errors; + return cvk::default_debug_callback(severity, types, callbackData, userData); + } +} + +bool display_available() { +#ifdef CTH_PLATFORM_WINDOWS + return true; +#else + //TODO a headless weston in the CI container, the tests skip there + return std::getenv("WAYLAND_DISPLAY") != nullptr; +#endif +} + +cvk::DebugMessenger::Config counting_messenger() { return cvk::DebugMessenger::Config{.callback = count_errors}; } + +ValidationCheck::ValidationCheck() { errors = 0; } + +ValidationCheck::~ValidationCheck() { EXPECT_EQ(errors, 0); } + +InstanceContext::InstanceContext() : instance{instance_config(instance_extensions()), cvk::create, counting_messenger()} {} +} diff --git a/cgfw/tests/src/context.hpp b/cgfw/tests/src/context.hpp new file mode 100644 index 00000000..baa4324d --- /dev/null +++ b/cgfw/tests/src/context.hpp @@ -0,0 +1,40 @@ +#pragma once +#include "test.hpp" + +#include "cvk/base/instance.hpp" +#include "cvk/debug/debug_messenger.hpp" + + + +namespace cgfw::test { +/** + * @brief messenger config that logs every message and counts the errors + * @details a small copy of the cvk tests, the instance create info gets it too: + * counts the errors of vkDestroyInstance(), e.g. leaked surfaces + */ +[[nodiscard]] cvk::DebugMessenger::Config counting_messenger(); + +/** + * @brief resets the error count, the destructor fails the test on errors + */ +struct ValidationCheck { + ValidationCheck(); + ~ValidationCheck(); +}; + +/** + * @brief own instance with @ref instance_extensions() and @ref counting_messenger(), fails the test on validation errors + * @details the check runs after the instance is destroyed + */ +struct InstanceContext { + InstanceContext(); + + ValidationCheck validationCheck; + cvk::Instance instance; + + InstanceContext(InstanceContext const& other) = delete; + InstanceContext(InstanceContext&& other) = delete; + InstanceContext& operator=(InstanceContext const& other) = delete; + InstanceContext& operator=(InstanceContext&& other) = delete; +}; +} diff --git a/cgfw/tests/src/environment.cpp b/cgfw/tests/src/environment.cpp new file mode 100644 index 00000000..4f372d57 --- /dev/null +++ b/cgfw/tests/src/environment.cpp @@ -0,0 +1,24 @@ +#include "cvk/base/cvk.hpp" + +#include + +#include + + +namespace cgfw::test { +namespace { + /** + * @brief holds the vulkan loader for all tests + */ + class Environment : public testing::Environment { + public: + void SetUp() override { _cvk.emplace(); } + void TearDown() override { _cvk.reset(); } + + private: + std::optional _cvk{}; + }; + + [[maybe_unused]] testing::Environment* const ENVIRONMENT = testing::AddGlobalTestEnvironment(new Environment{}); +} +} diff --git a/cgfw/tests/src/pch/glob/gtest.hpp b/cgfw/tests/src/pch/glob/gtest.hpp new file mode 100644 index 00000000..e469bb31 --- /dev/null +++ b/cgfw/tests/src/pch/glob/gtest.hpp @@ -0,0 +1,2 @@ +#pragma once +#include diff --git a/cgfw/tests/src/pch/pch.hpp b/cgfw/tests/src/pch/pch.hpp new file mode 100644 index 00000000..d97ada7c --- /dev/null +++ b/cgfw/tests/src/pch/pch.hpp @@ -0,0 +1,5 @@ +#pragma once +#include "glob/gtest.hpp" + +#include "context.hpp" +#include "test.hpp" diff --git a/cgfw/tests/src/test.hpp b/cgfw/tests/src/test.hpp new file mode 100644 index 00000000..596e447c --- /dev/null +++ b/cgfw/tests/src/test.hpp @@ -0,0 +1,41 @@ +#pragma once +#include "cgfw/window/window.hpp" + +#include "cvk/base/instance_config.hpp" + +#include + +#include +#include +#include + +#define CGFW_TEST(suite, test_name) S_TEST(cgfw, suite, test_name) + +/** + * @brief skips the test without a display + */ +#define CGFW_SKIP_WITHOUT_DISPLAY() \ + if(!cgfw::test::display_available()) GTEST_SKIP() << "no display" + +namespace cgfw::test { +constexpr std::string_view APP_NAME = "cgfw_tests"; + +/** + * @return a display exists, linux: WAYLAND_DISPLAY is set + */ +[[nodiscard]] bool display_available(); + +/** + * @brief hidden window: a test run shows no window + */ +[[nodiscard]] inline Window::Config window_config(std::string title = "cgfw test") { + return Window::Config{.title = std::move(title), .extent = {64, 64}, .flags = WindowFlags::HIDE}; +} + +/** + * @brief instance config with @ref APP_NAME + */ +[[nodiscard]] inline cvk::InstanceConfig instance_config(std::vector extensions = {}) { + return cvk::InstanceConfig{.appName = std::string{APP_NAME}, .extensions = std::move(extensions)}; +} +} diff --git a/cgfw/tests/src/window/window_test.cpp b/cgfw/tests/src/window/window_test.cpp new file mode 100644 index 00000000..b994eb04 --- /dev/null +++ b/cgfw/tests/src/window/window_test.cpp @@ -0,0 +1,125 @@ +#include "context.hpp" +#include "test.hpp" + +#include "cgfw/window/window.hpp" + +#include "cvk/surface/surface.hpp" + +#include + +#include +#include +#include + + +namespace cgfw { +CGFW_TEST(Window, base_constructor_does_not_create) { + Window const window{test::window_config()}; + + EXPECT_FALSE(window.created()); + EXPECT_EQ(window.get(), nullptr); + EXPECT_EQ(window.config().title, test::window_config().title); +} + +CGFW_TEST(Window, create_and_destroy) { + CGFW_SKIP_WITHOUT_DISPLAY(); + Window window{test::window_config()}; + + window.create(); + ASSERT_TRUE(window.created()); + EXPECT_NE(window.get(), nullptr); + + window.destroy(); + EXPECT_FALSE(window.created()); +} + +CGFW_TEST(Window, constructor_creates) { + CGFW_SKIP_WITHOUT_DISPLAY(); + + Window const window{test::window_config(), create}; + + EXPECT_TRUE(window.created()); + EXPECT_FALSE(window.shouldClose()); +} + +CGFW_TEST(Window, release_and_wrap) { + CGFW_SKIP_WITHOUT_DISPLAY(); + Window window{test::window_config(), create}; + auto const rgfwWindow = window.get(); + + auto const state = window.release(); + EXPECT_FALSE(window.created()); + EXPECT_EQ(state.rgfwWindow.get(), rgfwWindow); + + Window const wrapped{test::window_config(), state}; + EXPECT_TRUE(wrapped.created()); + EXPECT_EQ(wrapped.get(), rgfwWindow); +} + +CGFW_TEST(Window, move_keeps_the_window) { + CGFW_SKIP_WITHOUT_DISPLAY(); + Window window{test::window_config(), create}; + auto const rgfwWindow = window.get(); + + Window const moved{std::move(window)}; + + EXPECT_FALSE(window.created()); + EXPECT_EQ(moved.get(), rgfwWindow); + moved.pollEvents(); + EXPECT_FALSE(moved.shouldClose()); +} + +CGFW_TEST(Window, move_assignment_destroys_the_old_window) { + CGFW_SKIP_WITHOUT_DISPLAY(); + Window window{test::window_config("old"), create}; + Window other{test::window_config("new"), create}; + auto const rgfwWindow = other.get(); + + window = std::move(other); + + EXPECT_EQ(window.get(), rgfwWindow); + EXPECT_EQ(window.config().title, "new"); + EXPECT_FALSE(other.created()); +} + +CGFW_TEST(Window, windows_exist_at_the_same_time) { + CGFW_SKIP_WITHOUT_DISPLAY(); + + std::array const windows{ + Window{test::window_config("first"), create}, + Window{test::window_config("second"), create}, + Window{test::window_config("third"), create}, + }; + + windows[0].pollEvents(); + for(auto const& window : windows) { + EXPECT_TRUE(window.created()); + EXPECT_FALSE(window.shouldClose()); + } + EXPECT_NE(windows[0].get(), windows[1].get()); + EXPECT_NE(windows[1].get(), windows[2].get()); + EXPECT_NE(windows[0].get(), windows[2].get()); +} + +CGFW_TEST(Window, surface_of_an_instance_with_the_extensions) { + CGFW_SKIP_WITHOUT_DISPLAY(); + test::InstanceContext const context{}; + Window const window{test::window_config(), create}; + + auto const surface = window.surface(context.instance); + + EXPECT_TRUE(surface.created()); +} + +CGFW_TEST(instance_extensions, contain_the_surface_extensions) { + auto const extensions = instance_extensions(); + + ASSERT_FALSE(extensions.empty()); + EXPECT_TRUE(std::ranges::contains(extensions, std::string{VK_KHR_SURFACE_EXTENSION_NAME})); +#ifdef CTH_PLATFORM_WINDOWS + EXPECT_TRUE(std::ranges::contains(extensions, std::string{"VK_KHR_win32_surface"})); +#else + EXPECT_TRUE(std::ranges::contains(extensions, std::string{"VK_KHR_wayland_surface"})); +#endif +} +} From 48ca94747e117c774dcec6c74105c1d1e6ebf24e Mon Sep 17 00:00:00 2001 From: lukas <89145039+codethinki@users.noreply.github.com> Date: Thu, 22 Oct 2026 15:09:10 +0200 Subject: [PATCH 132/135] lib: cth_cmake with C23, rgfw.cmake needs no C17 override --- lib/cmake/rgfw.cmake | 4 +--- lib/cth_cmake | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/lib/cmake/rgfw.cmake b/lib/cmake/rgfw.cmake index 85368f8e..cefa0a64 100644 --- a/lib/cmake/rgfw.cmake +++ b/lib/cmake/rgfw.cmake @@ -10,9 +10,7 @@ if(NOT TARGET rgfw::rgfw) add_library(rgfw INTERFACE) add_library(rgfw::rgfw ALIAS rgfw) - # the implementation and the generated protocol code are C, - # cth_set_latest_c_cpp_standard() sets the C standard to the C++ one, C has no 26 - set(CMAKE_C_STANDARD 17) + # the implementation and the generated protocol code are C enable_language(C) target_include_directories(rgfw INTERFACE "${RGFW_DIR}") diff --git a/lib/cth_cmake b/lib/cth_cmake index 79a61917..f81d24a7 160000 --- a/lib/cth_cmake +++ b/lib/cth_cmake @@ -1 +1 @@ -Subproject commit 79a619176696cebe86d5d56059681f369a8b50e6 +Subproject commit f81d24a7b99bf0fad8a1c887ef85dced779441b6 From b260a9daf3f0baa1e9ad7ab53407cf8bfc6523c0 Mon Sep 17 00:00:00 2001 From: lukas <89145039+codethinki@users.noreply.github.com> Date: Thu, 22 Oct 2026 15:11:39 +0200 Subject: [PATCH 133/135] AGENTS: opaque OS handles are void* --- AGENTS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AGENTS.md b/AGENTS.md index d43dc66c..863a965f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -111,7 +111,7 @@ ctest --test-dir out/build/gcc_release --output-on-failure - An automatically chosen value has an override if: - the choice is not guaranteed to be optimal - there is a legitimate interest: debugging, user preference -- Never erase types into integers or `void*`. +- Never erase types into integers or `void*`. Exception: opaque OS handles are `void*`, like in `cth::os`. ### Includes From 13eb99de5ff882444150416a3503705db6e1994d Mon Sep 17 00:00:00 2001 From: lukas <89145039+codethinki@users.noreply.github.com> Date: Thu, 22 Oct 2026 15:13:28 +0200 Subject: [PATCH 134/135] cgfw: native_window with opaque handles, Window::nativeWindow() --- cgfw/incl/cgfw/window/window.hpp | 17 +++++++++++++++++ cgfw/src/window/window.cpp | 13 +++++++++++++ cgfw/tests/src/window/window_test.cpp | 22 ++++++++++++++++++++++ 3 files changed, 52 insertions(+) diff --git a/cgfw/incl/cgfw/window/window.hpp b/cgfw/incl/cgfw/window/window.hpp index 640d3234..9b4bade1 100644 --- a/cgfw/incl/cgfw/window/window.hpp +++ b/cgfw/incl/cgfw/window/window.hpp @@ -48,6 +48,17 @@ enum class WindowFlags : uint32_t { CTH_GEN_ENUM_FLAG_OVERLOADS(cgfw::WindowFlags) namespace cgfw { +/** + * @brief opaque native handles of a window + * @details windows: display = HINSTANCE, window = HWND + * wayland: display = wl_display*, window = wl_surface* + * windows that exist at the same time have the same display + */ +struct native_window { + void* display; + void* window; +}; + namespace dev { /** * @brief deleter of a window, without context: RGFW keeps one global state @@ -171,6 +182,12 @@ class Window { */ [[nodiscard]] bool shouldClose() const; + /** + * @return the native handles of the window + * @pre @ref created() + */ + [[nodiscard]] native_window nativeWindow() const; + /** * @brief closes the window and frees it * @pre the window is not nullptr diff --git a/cgfw/src/window/window.cpp b/cgfw/src/window/window.cpp index 658660dd..e288e7ca 100644 --- a/cgfw/src/window/window.cpp +++ b/cgfw/src/window/window.cpp @@ -7,6 +7,8 @@ #include +#include + #include @@ -105,6 +107,17 @@ bool Window::shouldClose() const { return RGFW_window_shouldClose(_handle.get()) != RGFW_FALSE; } +native_window Window::nativeWindow() const { + debug_check(*this); + +#ifdef CTH_PLATFORM_WINDOWS + // RGFW has no getter, it gives the Win32 surface this HINSTANCE too + return native_window{.display = GetModuleHandleW(nullptr), .window = RGFW_window_getHWND(_handle.get())}; +#else + return native_window{.display = RGFW_getDisplay_Wayland(), .window = RGFW_window_getWindow_Wayland(_handle.get())}; +#endif +} + void Window::destroy(RGFW_window* rgfwWindow) { CTH_CRITICAL(rgfwWindow == nullptr, "rgfwWindow must not be nullptr") {} diff --git a/cgfw/tests/src/window/window_test.cpp b/cgfw/tests/src/window/window_test.cpp index b994eb04..ba618f39 100644 --- a/cgfw/tests/src/window/window_test.cpp +++ b/cgfw/tests/src/window/window_test.cpp @@ -101,6 +101,28 @@ CGFW_TEST(Window, windows_exist_at_the_same_time) { EXPECT_NE(windows[0].get(), windows[2].get()); } +CGFW_TEST(Window, native_handles_are_not_nullptr) { + CGFW_SKIP_WITHOUT_DISPLAY(); + Window const window{test::window_config(), create}; + + auto const [display, nativeWindow] = window.nativeWindow(); + + EXPECT_NE(display, nullptr); + EXPECT_NE(nativeWindow, nullptr); +} + +CGFW_TEST(Window, windows_at_the_same_time_have_the_same_display) { + CGFW_SKIP_WITHOUT_DISPLAY(); + Window const first{test::window_config("first"), create}; + Window const second{test::window_config("second"), create}; + + auto const firstNative = first.nativeWindow(); + auto const secondNative = second.nativeWindow(); + + EXPECT_EQ(firstNative.display, secondNative.display); + EXPECT_NE(firstNative.window, secondNative.window); +} + CGFW_TEST(Window, surface_of_an_instance_with_the_extensions) { CGFW_SKIP_WITHOUT_DISPLAY(); test::InstanceContext const context{}; From 042e10b25927a863fc5694de8fcef5f72ab0122e Mon Sep 17 00:00:00 2001 From: lukas <89145039+codethinki@users.noreply.github.com> Date: Fri, 23 Oct 2026 23:07:16 +0200 Subject: [PATCH 135/135] ci: runs on pushes and pull requests to master and v2, like cth --- .github/workflows/build-test.yml | 4 ++-- AGENTS.md | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index c58a0e49..ab36f70b 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -1,8 +1,8 @@ name: Build & Test on: - push: { branches: ["master", "v2", "_v2/jvk_rebuild"] } - pull_request: { branches: ["master"] } + push: { branches: ["master", "v2"] } + pull_request: { branches: ["master", "v2"] } workflow_dispatch: permissions: { contents: read, packages: write } diff --git a/AGENTS.md b/AGENTS.md index 863a965f..114eed13 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -38,6 +38,7 @@ Read it before you change a file. Match the existing code. - Topic branches off the work branch are named `_v2/_jvk_rebuild/`. - `dev` and the `_dev/...` branches are old. Do not use them. - Work locally. Do not open pull requests for single steps. +- CI runs on pushes and pull requests to `master` and `v2`. The work branch has a draft pull request into `v2`, it runs the CI on every push. ## Build and test