From 3d7daa9f6d382d96f1aeabc4e3cec12b1d06fe94 Mon Sep 17 00:00:00 2001 From: kindem Date: Sun, 28 Jun 2026 14:33:33 +0800 Subject: [PATCH 01/24] refactor: copy first-party runtime deps via producer post-build, batch the rest Replace the per-DLL copy targets with two simpler owners so each shared Binaries destination is still written exactly once (no parallel-build race) without flooding the IDE target list: - a first-party shared library copies itself into the shared dir from a POST_BUILD step on its producer. Each producer writes only its own file, so destinations never overlap, and POST_BUILD adds no edges to the graph so it cannot close a build cycle. Works on every generator. - imported targets, plain-path third-party files and resources keep going through the single per-sub-project assets target; imported targets are resolved to concrete paths first since their $ cannot be evaluated in that source-scope target. Only one CopyDistAssets target per sub-project remains. --- CMake/Target.cmake | 121 ++++++++++++++++++++++++++++++--------------- 1 file changed, 82 insertions(+), 39 deletions(-) diff --git a/CMake/Target.cmake b/CMake/Target.cmake index fc6cfe5f..7f5e2737 100644 --- a/CMake/Target.cmake +++ b/CMake/Target.cmake @@ -86,41 +86,79 @@ function(exp_gather_target_runtime_dependencies_recurse) set(${arg_OUT_DEP_TARGET} ${result_dep_target} PARENT_SCOPE) endfunction() -# Every runtime file lands in the single shared per-sub-project Binaries directory. To keep the copies generator-agnostic -# and race-free, each destination is owned by exactly one copy step rather than being re-copied by every consumer: -# - files referenced through a target ($) get a per-file owner created in the consumer's scope, so the -# generator expression resolves where the target is visible (imported third-party targets such as Qt are -# directory-scoped and would be invisible in a global aggregate target). A first-party owner additionally waits on its -# producing target; merging these into one target is impossible because a build tool such as MirrorTool consumes a dll -# while another dll's producer transitively depends on the tool, which would close a build cycle; -# - prebuilt third-party files given as plain paths, plus resources, carry no target and are batched (deduplicated) into -# one per-sub-project assets target whose copies run sequentially (see exp_finalize_dist_assets). -function(exp_add_runtime_dep_copy) +# All runtime files share one per-sub-project Binaries directory, so each must have a single copy owner or parallel builds +# race on the same destination: a first-party shared library copies itself via a POST_BUILD step on its producer (see +# exp_add_library), while imported targets, plain-path third-party files and resources go through one batched assets +# target (see exp_finalize_dist_assets). An imported target's $ is not resolvable in that source-scope +# batch target, so its file is resolved to a concrete path here first. + +# Resolve an imported target's on-disk file for a config, trying the per-config location, its config mapping, the +# config-less IMPORTED_LOCATION, then any available imported configuration. +function(exp_get_imported_location) set(options "") - set(singleValueArgs KEY SRC PRODUCER OUTPUT_TARGET) + set(singleValueArgs TARGET CONFIG OUTPUT) set(multiValueArgs "") cmake_parse_arguments(arg "${options}" "${singleValueArgs}" "${multiValueArgs}" ${ARGN}) - string(MAKE_C_IDENTIFIER "${arg_KEY}" key_id) - set(registry_property EXP_RUNTIME_DEP_COPY_${SUB_PROJECT_NAME}_${key_id}) - - get_property(copy_target GLOBAL PROPERTY ${registry_property}) - if (NOT copy_target) - exp_get_runtime_output_dir(OUTPUT out_dir) - set(copy_target ${SUB_PROJECT_NAME}.CopyDll.${key_id}) - add_custom_target( - ${copy_target} - COMMAND ${CMAKE_COMMAND} -E make_directory ${out_dir} - COMMAND ${CMAKE_COMMAND} -E copy_if_different ${arg_SRC} ${out_dir} - ) - set_target_properties(${copy_target} PROPERTIES FOLDER ${AUX_TARGETS_FOLDER}) - if (arg_PRODUCER) - add_dependencies(${copy_target} ${arg_PRODUCER}) + set(loc "loc-NOTFOUND") + if (NOT "${arg_CONFIG}" STREQUAL "") + string(TOUPPER "${arg_CONFIG}" config_upper) + get_target_property(loc ${arg_TARGET} IMPORTED_LOCATION_${config_upper}) + if (NOT loc) + get_target_property(mapped_configs ${arg_TARGET} MAP_IMPORTED_CONFIG_${config_upper}) + if (mapped_configs) + foreach (mapped ${mapped_configs}) + string(TOUPPER "${mapped}" mapped_upper) + get_target_property(loc ${arg_TARGET} IMPORTED_LOCATION_${mapped_upper}) + if (loc) + break() + endif () + endforeach () + endif () endif () - set_property(GLOBAL PROPERTY ${registry_property} ${copy_target}) + endif () + if (NOT loc) + get_target_property(loc ${arg_TARGET} IMPORTED_LOCATION) + endif () + if (NOT loc) + get_target_property(imported_configs ${arg_TARGET} IMPORTED_CONFIGURATIONS) + if (imported_configs) + list(GET imported_configs 0 first_config) + string(TOUPPER "${first_config}" first_upper) + get_target_property(loc ${arg_TARGET} IMPORTED_LOCATION_${first_upper}) + endif () + endif () + if (NOT loc) + set(loc "") endif () - set(${arg_OUTPUT_TARGET} ${copy_target} PARENT_SCOPE) + set(${arg_OUTPUT} ${loc} PARENT_SCOPE) +endfunction() + +# Resolve an imported target's runtime file to a concrete path the source-scope assets target can copy: a plain path for +# single-config generators, or a per-config $<$:path> of literals (no target reference) for multi-config ones. +function(exp_resolve_imported_runtime_file) + set(options "") + set(singleValueArgs TARGET OUTPUT_SRC) + set(multiValueArgs "") + cmake_parse_arguments(arg "${options}" "${singleValueArgs}" "${multiValueArgs}" ${ARGN}) + + set(src_expr "") + if (with_multi_config_generator) + foreach (config ${CMAKE_CONFIGURATION_TYPES}) + exp_get_imported_location(TARGET ${arg_TARGET} CONFIG ${config} OUTPUT loc) + if (loc) + string(APPEND src_expr "$<$:${loc}>") + endif () + endforeach () + else () + exp_get_imported_location(TARGET ${arg_TARGET} CONFIG "${CMAKE_BUILD_TYPE}" OUTPUT loc) + if (loc) + set(src_expr ${loc}) + endif () + endif () + + set(${arg_OUTPUT_SRC} ${src_expr} PARENT_SCOPE) endfunction() function(exp_schedule_dist_assets_finalize) @@ -207,19 +245,14 @@ function(exp_process_runtime_dependencies) endif () if (referenced AND TARGET ${referenced}) - set(producer "") get_target_property(referenced_imported ${referenced} IMPORTED) - if (NOT referenced_imported) - set(producer ${referenced}) + if (referenced_imported) + exp_resolve_imported_runtime_file(TARGET ${referenced} OUTPUT_SRC src) + if (src) + set_property(GLOBAL APPEND PROPERTY EXP_DIST_ASSET_FILES_${SUB_PROJECT_NAME} ${src}) + endif () endif () - - exp_add_runtime_dep_copy( - KEY ${r} - SRC ${r} - PRODUCER ${producer} - OUTPUT_TARGET copy_target - ) - add_dependencies(${arg_NAME} ${copy_target}) + # first-party shared libs copy themselves via a POST_BUILD step in exp_add_library; nothing to wire up here else () set_property(GLOBAL APPEND PROPERTY EXP_DIST_ASSET_FILES_${SUB_PROJECT_NAME} ${r}) endif () @@ -561,6 +594,16 @@ function(exp_add_library) ARCHIVE_OUTPUT_DIRECTORY ${archive_output_directory} ) + if ("${arg_TYPE}" STREQUAL "SHARED") + exp_get_runtime_output_dir(OUTPUT dist_dir) + add_custom_command( + TARGET ${arg_NAME} POST_BUILD + COMMAND ${CMAKE_COMMAND} -E make_directory ${dist_dir} + COMMAND ${CMAKE_COMMAND} -E copy_if_different $ ${dist_dir} + VERBATIM + ) + endif () + foreach (inc ${arg_PUBLIC_INC}) get_filename_component(absolute_inc ${inc} ABSOLUTE) list(APPEND public_build_inc $) From 507c811991f70d0eea5340703bf5e4b09149cdce Mon Sep 17 00:00:00 2001 From: FlyAntNotDown <461425614@qq.com> Date: Mon, 29 Jun 2026 16:49:04 +0800 Subject: [PATCH 02/24] feat: support push constants in rhi Wire push / root constants through the RHI: each PipelineConstantLayout now carries an explicit DirectX12 register (hlslBinding / hlslBindingSpace) alongside the Vulkan byte offset, and ComputePass / RasterPass recorders gain SetPipelineConstants addressed by pipeline-constant index. Vulkan resolves the index to its push constant range and calls vkCmdPushConstants; DirectX12 appends a root 32-bit-constants parameter per range and calls Set{Graphics,Compute}Root32BitConstants. Dummy gets no-op overrides. --- .../Include/RHI/DirectX12/BindGroupLayout.h | 2 ++ .../Include/RHI/DirectX12/CommandRecorder.h | 2 ++ .../Include/RHI/DirectX12/PipelineLayout.h | 3 +++ .../RHI-DirectX12/Src/CommandRecorder.cpp | 14 +++++++++++ .../RHI-DirectX12/Src/PipelineLayout.cpp | 21 +++++++++++++++- .../Include/RHI/Dummy/CommandRecorder.h | 2 ++ .../Source/RHI-Dummy/Src/CommandRecorder.cpp | 8 ++++++ .../Include/RHI/Vulkan/CommandRecorder.h | 2 ++ .../Include/RHI/Vulkan/PipelineLayout.h | 4 +++ .../Source/RHI-Vulkan/Src/CommandRecorder.cpp | 14 +++++++++++ .../Source/RHI-Vulkan/Src/PipelineLayout.cpp | 17 ++++++++----- .../Source/RHI/Include/RHI/CommandRecorder.h | 2 ++ .../Source/RHI/Include/RHI/PipelineLayout.h | 21 +++++++++++++--- Engine/Source/RHI/Src/PipelineLayout.cpp | 25 +++++++++++++------ 14 files changed, 120 insertions(+), 17 deletions(-) diff --git a/Engine/Source/RHI-DirectX12/Include/RHI/DirectX12/BindGroupLayout.h b/Engine/Source/RHI-DirectX12/Include/RHI/DirectX12/BindGroupLayout.h index 9042fd2b..85234dde 100644 --- a/Engine/Source/RHI-DirectX12/Include/RHI/DirectX12/BindGroupLayout.h +++ b/Engine/Source/RHI-DirectX12/Include/RHI/DirectX12/BindGroupLayout.h @@ -11,6 +11,8 @@ namespace RHI::DirectX12 { class DX12Device; + D3D12_SHADER_VISIBILITY GetShaderVisibility(ShaderStageFlags shaderStageFlags); + struct RootParameterKeyInfo { BindingType bindingType; uint8_t layoutIndex; diff --git a/Engine/Source/RHI-DirectX12/Include/RHI/DirectX12/CommandRecorder.h b/Engine/Source/RHI-DirectX12/Include/RHI/DirectX12/CommandRecorder.h index ed512d16..0382899f 100644 --- a/Engine/Source/RHI-DirectX12/Include/RHI/DirectX12/CommandRecorder.h +++ b/Engine/Source/RHI-DirectX12/Include/RHI/DirectX12/CommandRecorder.h @@ -70,6 +70,7 @@ namespace RHI::DirectX12 { // ComputePassCommandRecorder void SetPipeline(ComputePipeline* inPipeline) override; void SetBindGroup(uint8_t inLayoutIndex, BindGroup* inBindGroup) override; + void SetPipelineConstants(uint32_t inPipelineConstantIndex, const void* inData, uint32_t inSize) override; void Dispatch(size_t inGroupCountX, size_t inGroupCountY, size_t inGroupCountZ) override; void EndPass() override; @@ -94,6 +95,7 @@ namespace RHI::DirectX12 { // RasterPassCommandRecorder void SetPipeline(RasterPipeline* inPipeline) override; void SetBindGroup(uint8_t inLayoutIndex, BindGroup* inBindGroup) override; + void SetPipelineConstants(uint32_t inPipelineConstantIndex, const void* inData, uint32_t inSize) override; void SetIndexBuffer(BufferView* inBufferView) override; void SetVertexBuffer(size_t inSlot, BufferView* inBufferView) override; void Draw(size_t inVertexCount, size_t inInstanceCount, size_t inFirstVertex, size_t inFirstInstance) override; diff --git a/Engine/Source/RHI-DirectX12/Include/RHI/DirectX12/PipelineLayout.h b/Engine/Source/RHI-DirectX12/Include/RHI/DirectX12/PipelineLayout.h index f6144c69..8be0ace6 100644 --- a/Engine/Source/RHI-DirectX12/Include/RHI/DirectX12/PipelineLayout.h +++ b/Engine/Source/RHI-DirectX12/Include/RHI/DirectX12/PipelineLayout.h @@ -6,6 +6,7 @@ #include #include +#include #include @@ -38,6 +39,7 @@ namespace RHI::DirectX12 { ~DX12PipelineLayout() override; std::optional QueryRootDescriptorParameterIndex(uint8_t inLayoutIndex, const HlslBinding& inBinding); + RootParameterIndex QueryRootConstantParameterIndex(uint32_t inPipelineConstantIndex) const; ID3D12RootSignature* GetNative() const; private: @@ -45,5 +47,6 @@ namespace RHI::DirectX12 { ComPtr nativeRootSignature; RootParameterIndexMap rootParameterIndexMap; + std::vector rootConstantParameterIndices; }; } diff --git a/Engine/Source/RHI-DirectX12/Src/CommandRecorder.cpp b/Engine/Source/RHI-DirectX12/Src/CommandRecorder.cpp index 9dd0c129..e02a8a9a 100644 --- a/Engine/Source/RHI-DirectX12/Src/CommandRecorder.cpp +++ b/Engine/Source/RHI-DirectX12/Src/CommandRecorder.cpp @@ -224,6 +224,13 @@ namespace RHI::DirectX12 { } } + void DX12ComputePassCommandRecorder::SetPipelineConstants(const uint32_t inPipelineConstantIndex, const void* inData, const uint32_t inSize) + { + Assert(inSize % 4 == 0); + const auto rootParameterIndex = computePipeline->GetPipelineLayout().QueryRootConstantParameterIndex(inPipelineConstantIndex); + commandBuffer.GetNativeCmdList()->SetComputeRoot32BitConstants(rootParameterIndex, inSize / 4, inData, 0); + } + void DX12ComputePassCommandRecorder::Dispatch(const size_t inGroupCountX, const size_t inGroupCountY, const size_t inGroupCountZ) { commandBuffer.GetNativeCmdList()->Dispatch(inGroupCountX, inGroupCountY, inGroupCountZ); @@ -317,6 +324,13 @@ namespace RHI::DirectX12 { } } + void DX12RasterPassCommandRecorder::SetPipelineConstants(const uint32_t inPipelineConstantIndex, const void* inData, const uint32_t inSize) + { + Assert(inSize % 4 == 0); + const auto rootParameterIndex = rasterPipeline->GetPipelineLayout().QueryRootConstantParameterIndex(inPipelineConstantIndex); + commandBuffer.GetNativeCmdList()->SetGraphicsRoot32BitConstants(rootParameterIndex, inSize / 4, inData, 0); + } + void DX12RasterPassCommandRecorder::SetIndexBuffer(BufferView* inBufferView) { const auto* bufferView = static_cast(inBufferView); diff --git a/Engine/Source/RHI-DirectX12/Src/PipelineLayout.cpp b/Engine/Source/RHI-DirectX12/Src/PipelineLayout.cpp index 1f21de04..4948da8c 100644 --- a/Engine/Source/RHI-DirectX12/Src/PipelineLayout.cpp +++ b/Engine/Source/RHI-DirectX12/Src/PipelineLayout.cpp @@ -40,6 +40,11 @@ namespace RHI::DirectX12 { return iter->second; } + RootParameterIndex DX12PipelineLayout::QueryRootConstantParameterIndex(const uint32_t inPipelineConstantIndex) const + { + return rootConstantParameterIndices[inPipelineConstantIndex]; + } + ID3D12RootSignature* DX12PipelineLayout::GetNative() const { return nativeRootSignature.Get(); @@ -66,7 +71,21 @@ namespace RHI::DirectX12 { rootParameterIndexMap[rootParameterKey] = { keyInfo.bindingType, index }; } } - // TODO root constants + rootConstantParameterIndices.reserve(inCreateInfo.pipelineConstantLayouts.size()); + for (const auto& pipelineConstantLayout : inCreateInfo.pipelineConstantLayouts) { + Assert(pipelineConstantLayout.size % 4 == 0); + const auto& hlslBinding = std::get(pipelineConstantLayout.platformBinding); + const auto index = static_cast(rootParameters.size()); + + rootParameters.emplace_back(); + rootParameters.back().InitAsConstants( + pipelineConstantLayout.size / 4, + hlslBinding.binding, + hlslBinding.bindingSpace, + GetShaderVisibility(pipelineConstantLayout.stageFlags)); + + rootConstantParameterIndices.emplace_back(index); + } CD3DX12_VERSIONED_ROOT_SIGNATURE_DESC rootSignatureDesc; rootSignatureDesc.Init_1_1(rootParameters.size(), rootParameters.data(), 0, nullptr, D3D12_ROOT_SIGNATURE_FLAG_ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUT); diff --git a/Engine/Source/RHI-Dummy/Include/RHI/Dummy/CommandRecorder.h b/Engine/Source/RHI-Dummy/Include/RHI/Dummy/CommandRecorder.h index 0d6b007e..55ad9b38 100644 --- a/Engine/Source/RHI-Dummy/Include/RHI/Dummy/CommandRecorder.h +++ b/Engine/Source/RHI-Dummy/Include/RHI/Dummy/CommandRecorder.h @@ -60,6 +60,7 @@ namespace RHI::Dummy { // ComputePassCommandRecorder void SetPipeline(ComputePipeline* pipeline) override; void SetBindGroup(uint8_t layoutIndex, BindGroup *bindGroup) override; + void SetPipelineConstants(uint32_t pipelineConstantIndex, const void* data, uint32_t size) override; void Dispatch(size_t groupCountX, size_t groupCountY, size_t groupCountZ) override; void EndPass() override; }; @@ -78,6 +79,7 @@ namespace RHI::Dummy { // RasterPassCommandRecorder void SetPipeline(RasterPipeline* pipeline) override; void SetBindGroup(uint8_t layoutIndex, BindGroup* bindGroup) override; + void SetPipelineConstants(uint32_t pipelineConstantIndex, const void* data, uint32_t size) override; void SetIndexBuffer(BufferView* bufferView) override; void SetVertexBuffer(size_t slot, BufferView* bufferView) override; void Draw(size_t vertexCount, size_t instanceCount, size_t firstVertex, size_t firstInstance) override; diff --git a/Engine/Source/RHI-Dummy/Src/CommandRecorder.cpp b/Engine/Source/RHI-Dummy/Src/CommandRecorder.cpp index 4d891b08..b8c72603 100644 --- a/Engine/Source/RHI-Dummy/Src/CommandRecorder.cpp +++ b/Engine/Source/RHI-Dummy/Src/CommandRecorder.cpp @@ -71,6 +71,10 @@ namespace RHI::Dummy { { } + void DummyComputePassCommandRecorder::SetPipelineConstants(uint32_t pipelineConstantIndex, const void* data, uint32_t size) + { + } + void DummyComputePassCommandRecorder::Dispatch(size_t groupCountX, size_t groupCountY, size_t groupCountZ) { } @@ -105,6 +109,10 @@ namespace RHI::Dummy { { } + void DummyRasterPassCommandRecorder::SetPipelineConstants(uint32_t pipelineConstantIndex, const void* data, uint32_t size) + { + } + void DummyRasterPassCommandRecorder::SetIndexBuffer(BufferView* bufferView) { } diff --git a/Engine/Source/RHI-Vulkan/Include/RHI/Vulkan/CommandRecorder.h b/Engine/Source/RHI-Vulkan/Include/RHI/Vulkan/CommandRecorder.h index 8a297530..93659156 100644 --- a/Engine/Source/RHI-Vulkan/Include/RHI/Vulkan/CommandRecorder.h +++ b/Engine/Source/RHI-Vulkan/Include/RHI/Vulkan/CommandRecorder.h @@ -72,6 +72,7 @@ namespace RHI::Vulkan { // ComputePassCommandRecorder void SetPipeline(ComputePipeline* inPipeline) override; void SetBindGroup(uint8_t inLayoutIndex, BindGroup* inBindGroup) override; + void SetPipelineConstants(uint32_t inPipelineConstantIndex, const void* inData, uint32_t inSize) override; void Dispatch(size_t inGroupCountX, size_t inGroupCountY, size_t inGroupCountZ) override; void EndPass() override; @@ -96,6 +97,7 @@ namespace RHI::Vulkan { // RasterPassCommandRecorder void SetPipeline(RasterPipeline* inPipeline) override; void SetBindGroup(uint8_t inLayoutIndex, BindGroup* inBindGroup) override; + void SetPipelineConstants(uint32_t inPipelineConstantIndex, const void* inData, uint32_t inSize) override; void SetIndexBuffer(BufferView* inBufferView) override; void SetVertexBuffer(size_t inSlot, BufferView* inBufferView) override; void Draw(size_t inVertexCount, size_t inInstanceCount, size_t inFirstVertex, size_t inFirstInstance) override; diff --git a/Engine/Source/RHI-Vulkan/Include/RHI/Vulkan/PipelineLayout.h b/Engine/Source/RHI-Vulkan/Include/RHI/Vulkan/PipelineLayout.h index 68f3a124..6cd70ad8 100644 --- a/Engine/Source/RHI-Vulkan/Include/RHI/Vulkan/PipelineLayout.h +++ b/Engine/Source/RHI-Vulkan/Include/RHI/Vulkan/PipelineLayout.h @@ -4,6 +4,8 @@ #pragma once +#include + #include #include @@ -18,11 +20,13 @@ namespace RHI::Vulkan { ~VulkanPipelineLayout() override; VkPipelineLayout GetNative() const; + const VkPushConstantRange& GetPushConstantRange(uint32_t inPipelineConstantIndex) const; private: void CreateNativePipelineLayout(const PipelineLayoutCreateInfo& inCreateInfo); VulkanDevice& device; VkPipelineLayout nativePipelineLayout; + std::vector pushConstantRanges; }; } \ No newline at end of file diff --git a/Engine/Source/RHI-Vulkan/Src/CommandRecorder.cpp b/Engine/Source/RHI-Vulkan/Src/CommandRecorder.cpp index dfb093ba..14a11070 100644 --- a/Engine/Source/RHI-Vulkan/Src/CommandRecorder.cpp +++ b/Engine/Source/RHI-Vulkan/Src/CommandRecorder.cpp @@ -365,6 +365,13 @@ namespace RHI::Vulkan { vkCmdBindDescriptorSets(commandBuffer.GetNative(), VK_PIPELINE_BIND_POINT_COMPUTE, layout, inLayoutIndex, 1, &descriptorSet, 0, nullptr); } + void VulkanComputePassCommandRecorder::SetPipelineConstants(uint32_t inPipelineConstantIndex, const void* inData, uint32_t inSize) + { + const auto* pipelineLayout = computePipeline->GetPipelineLayout(); + const auto& range = pipelineLayout->GetPushConstantRange(inPipelineConstantIndex); + vkCmdPushConstants(commandBuffer.GetNative(), pipelineLayout->GetNative(), range.stageFlags, range.offset, inSize, inData); + } + void VulkanComputePassCommandRecorder::Dispatch(size_t inGroupCountX, size_t inGroupCountY, size_t inGroupCountZ) { vkCmdDispatch(commandBuffer.GetNative(), inGroupCountX, inGroupCountY, inGroupCountZ); @@ -475,6 +482,13 @@ namespace RHI::Vulkan { vkCmdBindDescriptorSets(commandBuffer.GetNative(), VK_PIPELINE_BIND_POINT_GRAPHICS, layout, inLayoutIndex, 1, &descriptorSet, 0, nullptr); } + void VulkanRasterPassCommandRecorder::SetPipelineConstants(uint32_t inPipelineConstantIndex, const void* inData, uint32_t inSize) + { + const auto* pipelineLayout = rasterPipeline->GetPipelineLayout(); + const auto& range = pipelineLayout->GetPushConstantRange(inPipelineConstantIndex); + vkCmdPushConstants(commandBuffer.GetNative(), pipelineLayout->GetNative(), range.stageFlags, range.offset, inSize, inData); + } + void VulkanRasterPassCommandRecorder::SetIndexBuffer(BufferView *inBufferView) { const auto* mBufferView = static_cast(inBufferView); diff --git a/Engine/Source/RHI-Vulkan/Src/PipelineLayout.cpp b/Engine/Source/RHI-Vulkan/Src/PipelineLayout.cpp index bbe9c031..1a2ddd52 100644 --- a/Engine/Source/RHI-Vulkan/Src/PipelineLayout.cpp +++ b/Engine/Source/RHI-Vulkan/Src/PipelineLayout.cpp @@ -28,6 +28,11 @@ namespace RHI::Vulkan { return nativePipelineLayout; } + const VkPushConstantRange& VulkanPipelineLayout::GetPushConstantRange(const uint32_t inPipelineConstantIndex) const + { + return pushConstantRanges[inPipelineConstantIndex]; + } + void VulkanPipelineLayout::CreateNativePipelineLayout(const PipelineLayoutCreateInfo& inCreateInfo) { std::vector setLayouts(inCreateInfo.bindGroupLayouts.size()); @@ -36,20 +41,20 @@ namespace RHI::Vulkan { setLayouts[i] = vulkanBindGroup->GetNative(); } - std::vector pushConstants(inCreateInfo.pipelineConstantLayouts.size()); + pushConstantRanges.resize(inCreateInfo.pipelineConstantLayouts.size()); for (uint32_t i = 0; i < inCreateInfo.pipelineConstantLayouts.size(); ++i) { const auto& constantInfo = inCreateInfo.pipelineConstantLayouts[i]; - pushConstants[i].stageFlags = FlagsCast(constantInfo.stageFlags); - pushConstants[i].offset = constantInfo.offset; - pushConstants[i].size = constantInfo.size; + pushConstantRanges[i].stageFlags = FlagsCast(constantInfo.stageFlags); + pushConstantRanges[i].offset = std::get(constantInfo.platformBinding).offset; + pushConstantRanges[i].size = constantInfo.size; } VkPipelineLayoutCreateInfo pipelineLayoutInfo = {}; pipelineLayoutInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO; pipelineLayoutInfo.setLayoutCount = setLayouts.size(); pipelineLayoutInfo.pSetLayouts = setLayouts.data(); - pipelineLayoutInfo.pushConstantRangeCount = pushConstants.size(); - pipelineLayoutInfo.pPushConstantRanges = pushConstants.data(); + pipelineLayoutInfo.pushConstantRangeCount = pushConstantRanges.size(); + pipelineLayoutInfo.pPushConstantRanges = pushConstantRanges.data(); Assert(vkCreatePipelineLayout(device.GetNative(), &pipelineLayoutInfo, nullptr, &nativePipelineLayout) == VK_SUCCESS); #if BUILD_CONFIG_DEBUG diff --git a/Engine/Source/RHI/Include/RHI/CommandRecorder.h b/Engine/Source/RHI/Include/RHI/CommandRecorder.h index 3b9a4a28..160f01fc 100644 --- a/Engine/Source/RHI/Include/RHI/CommandRecorder.h +++ b/Engine/Source/RHI/Include/RHI/CommandRecorder.h @@ -227,6 +227,7 @@ namespace RHI { virtual void SetPipeline(ComputePipeline* pipeline) = 0; virtual void SetBindGroup(uint8_t layoutIndex, BindGroup* bindGroup) = 0; + virtual void SetPipelineConstants(uint32_t pipelineConstantIndex, const void* data, uint32_t size) = 0; virtual void Dispatch(size_t groupCountX, size_t groupCountY, size_t groupCountZ) = 0; virtual void EndPass() = 0; @@ -241,6 +242,7 @@ namespace RHI { virtual void SetPipeline(RasterPipeline* pipeline) = 0; virtual void SetBindGroup(uint8_t layoutIndex, BindGroup* bindGroup) = 0; + virtual void SetPipelineConstants(uint32_t pipelineConstantIndex, const void* data, uint32_t size) = 0; virtual void SetIndexBuffer(BufferView* bufferView) = 0; virtual void SetVertexBuffer(size_t slot, BufferView* bufferView) = 0; virtual void Draw(size_t vertexCount, size_t instanceCount, size_t firstVertex, size_t firstInstance) = 0; diff --git a/Engine/Source/RHI/Include/RHI/PipelineLayout.h b/Engine/Source/RHI/Include/RHI/PipelineLayout.h index 7ef07035..9f1821f1 100644 --- a/Engine/Source/RHI/Include/RHI/PipelineLayout.h +++ b/Engine/Source/RHI/Include/RHI/PipelineLayout.h @@ -5,22 +5,37 @@ #pragma once #include +#include + #include #include namespace RHI { class BindGroupLayout; + struct HlslPipelineConstantBinding { + uint8_t binding; + uint8_t bindingSpace; + + HlslPipelineConstantBinding(uint8_t inBinding, uint8_t inBindingSpace); + }; + + struct GlslPipelineConstantBinding { + uint32_t offset; + + explicit GlslPipelineConstantBinding(uint32_t inOffset); + }; + struct PipelineConstantLayout { ShaderStageFlags stageFlags; - uint32_t offset; uint32_t size; + std::variant platformBinding; PipelineConstantLayout(); - PipelineConstantLayout(ShaderStageFlags inStageFlags, uint32_t inOffset, uint32_t inSize); + PipelineConstantLayout(ShaderStageFlags inStageFlags, uint32_t inSize, const std::variant& inPlatformBinding); PipelineConstantLayout& SetStageFlags(ShaderStageFlags inStageFlags); - PipelineConstantLayout& SetOffset(uint32_t inOffset); PipelineConstantLayout& SetSize(uint32_t inSize); + PipelineConstantLayout& SetPlatformBinding(const std::variant& inPlatformBinding); }; struct PipelineLayoutCreateInfo { diff --git a/Engine/Source/RHI/Src/PipelineLayout.cpp b/Engine/Source/RHI/Src/PipelineLayout.cpp index cfe45694..d2b4bcc6 100644 --- a/Engine/Source/RHI/Src/PipelineLayout.cpp +++ b/Engine/Source/RHI/Src/PipelineLayout.cpp @@ -5,17 +5,28 @@ #include namespace RHI { + HlslPipelineConstantBinding::HlslPipelineConstantBinding(const uint8_t inBinding, const uint8_t inBindingSpace) + : binding(inBinding) + , bindingSpace(inBindingSpace) + { + } + + GlslPipelineConstantBinding::GlslPipelineConstantBinding(const uint32_t inOffset) + : offset(inOffset) + { + } + PipelineConstantLayout::PipelineConstantLayout() : stageFlags(ShaderStageFlags::null) - , offset(0) , size(0) + , platformBinding(HlslPipelineConstantBinding(0, 0)) { } - PipelineConstantLayout::PipelineConstantLayout(const ShaderStageFlags inStageFlags, const uint32_t inOffset, const uint32_t inSize) + PipelineConstantLayout::PipelineConstantLayout(const ShaderStageFlags inStageFlags, const uint32_t inSize, const std::variant& inPlatformBinding) : stageFlags(inStageFlags) - , offset(inOffset) , size(inSize) + , platformBinding(inPlatformBinding) { } @@ -25,15 +36,15 @@ namespace RHI { return *this; } - PipelineConstantLayout& PipelineConstantLayout::SetOffset(const uint32_t inOffset) + PipelineConstantLayout& PipelineConstantLayout::SetSize(const uint32_t inSize) { - offset = inOffset; + size = inSize; return *this; } - PipelineConstantLayout& PipelineConstantLayout::SetSize(const uint32_t inSize) + PipelineConstantLayout& PipelineConstantLayout::SetPlatformBinding(const std::variant& inPlatformBinding) { - size = inSize; + platformBinding = inPlatformBinding; return *this; } From 4d6dae3c264f2529a4f999e59b47e7ec14380b5e Mon Sep 17 00:00:00 2001 From: kindem Date: Mon, 29 Jun 2026 22:19:48 +0800 Subject: [PATCH 03/24] feat: support indirect dispatch in rhi --- .../Include/RHI/DirectX12/CommandRecorder.h | 1 + .../RHI-DirectX12/Include/RHI/DirectX12/Device.h | 4 +++- Engine/Source/RHI-DirectX12/Src/CommandRecorder.cpp | 6 ++++++ Engine/Source/RHI-DirectX12/Src/Device.cpp | 10 ++++++++-- .../RHI-Dummy/Include/RHI/Dummy/CommandRecorder.h | 3 ++- Engine/Source/RHI-Dummy/Src/CommandRecorder.cpp | 4 ++++ .../RHI-Vulkan/Include/RHI/Vulkan/CommandRecorder.h | 1 + Engine/Source/RHI-Vulkan/Src/CommandRecorder.cpp | 6 ++++++ Engine/Source/RHI/Include/RHI/CommandRecorder.h | 7 +++++++ 9 files changed, 38 insertions(+), 4 deletions(-) diff --git a/Engine/Source/RHI-DirectX12/Include/RHI/DirectX12/CommandRecorder.h b/Engine/Source/RHI-DirectX12/Include/RHI/DirectX12/CommandRecorder.h index 0382899f..20f3bbbd 100644 --- a/Engine/Source/RHI-DirectX12/Include/RHI/DirectX12/CommandRecorder.h +++ b/Engine/Source/RHI-DirectX12/Include/RHI/DirectX12/CommandRecorder.h @@ -72,6 +72,7 @@ namespace RHI::DirectX12 { void SetBindGroup(uint8_t inLayoutIndex, BindGroup* inBindGroup) override; void SetPipelineConstants(uint32_t inPipelineConstantIndex, const void* inData, uint32_t inSize) override; void Dispatch(size_t inGroupCountX, size_t inGroupCountY, size_t inGroupCountZ) override; + void DispatchIndirect(Buffer* inIndirectBuffer, size_t inOffset) override; void EndPass() override; private: diff --git a/Engine/Source/RHI-DirectX12/Include/RHI/DirectX12/Device.h b/Engine/Source/RHI-DirectX12/Include/RHI/DirectX12/Device.h index 35692a15..df322afe 100644 --- a/Engine/Source/RHI-DirectX12/Include/RHI/DirectX12/Device.h +++ b/Engine/Source/RHI-DirectX12/Include/RHI/DirectX12/Device.h @@ -97,6 +97,7 @@ namespace RHI::DirectX12 { ID3D12Device* GetNative() const; ID3D12CommandSignature* GetDrawIndirectCommandSignature() const; ID3D12CommandSignature* GetDrawIndexedIndirectCommandSignature() const; + ID3D12CommandSignature* GetDispatchIndirectCommandSignature() const; Common::UniquePtr AllocateRtvDescriptor() const; Common::UniquePtr AllocateCbvSrvUavDescriptor() const; Common::UniquePtr AllocateSamplerDescriptor() const; @@ -107,7 +108,7 @@ namespace RHI::DirectX12 { void CreateNativeQueues(const DeviceCreateInfo& inCreateInfo); void QueryNativeDescriptorSize(); void CreateDescriptorPools(); - void CreateDrawIndirectCommandSignatures(); + void CreateIndirectCommandSignatures(); #if BUILD_CONFIG_DEBUG void RegisterNativeDebugLayerExceptionHandler(); void UnregisterNativeDebugLayerExceptionHandler(); @@ -127,5 +128,6 @@ namespace RHI::DirectX12 { ComPtr nativeDevice; ComPtr drawIndirectCommandSignature; ComPtr drawIndexedIndirectCommandSignature; + ComPtr dispatchIndirectCommandSignature; }; } diff --git a/Engine/Source/RHI-DirectX12/Src/CommandRecorder.cpp b/Engine/Source/RHI-DirectX12/Src/CommandRecorder.cpp index e02a8a9a..febed841 100644 --- a/Engine/Source/RHI-DirectX12/Src/CommandRecorder.cpp +++ b/Engine/Source/RHI-DirectX12/Src/CommandRecorder.cpp @@ -236,6 +236,12 @@ namespace RHI::DirectX12 { commandBuffer.GetNativeCmdList()->Dispatch(inGroupCountX, inGroupCountY, inGroupCountZ); } + void DX12ComputePassCommandRecorder::DispatchIndirect(Buffer* inIndirectBuffer, const size_t inOffset) + { + const auto* indirectBuffer = static_cast(inIndirectBuffer); + commandBuffer.GetNativeCmdList()->ExecuteIndirect(device.GetDispatchIndirectCommandSignature(), 1, indirectBuffer->GetNative(), inOffset, nullptr, 0); + } + void DX12ComputePassCommandRecorder::EndPass() { } diff --git a/Engine/Source/RHI-DirectX12/Src/Device.cpp b/Engine/Source/RHI-DirectX12/Src/Device.cpp index eb07ff72..0162d75e 100644 --- a/Engine/Source/RHI-DirectX12/Src/Device.cpp +++ b/Engine/Source/RHI-DirectX12/Src/Device.cpp @@ -153,7 +153,7 @@ namespace RHI::DirectX12 { CreateNativeQueues(inCreateInfo); QueryNativeDescriptorSize(); CreateDescriptorPools(); - CreateDrawIndirectCommandSignatures(); + CreateIndirectCommandSignatures(); #if BUILD_CONFIG_DEBUG RegisterNativeDebugLayerExceptionHandler(); #endif @@ -299,6 +299,11 @@ namespace RHI::DirectX12 { return drawIndexedIndirectCommandSignature.Get(); } + ID3D12CommandSignature* DX12Device::GetDispatchIndirectCommandSignature() const + { + return dispatchIndirectCommandSignature.Get(); + } + Common::UniquePtr DX12Device::AllocateRtvDescriptor() const { return rtvDescriptorPool->Allocate(); @@ -368,7 +373,7 @@ namespace RHI::DirectX12 { dsvDescriptorPool = Common::MakeUnique(*this, D3D12_DESCRIPTOR_HEAP_TYPE_DSV, nativeDsvDescriptorSize, 16); } - void DX12Device::CreateDrawIndirectCommandSignatures() + void DX12Device::CreateIndirectCommandSignatures() { const auto createSignature = [this](const D3D12_INDIRECT_ARGUMENT_TYPE inArgumentType, const uint32_t inStride) -> ComPtr { D3D12_INDIRECT_ARGUMENT_DESC argumentDesc {}; @@ -388,6 +393,7 @@ namespace RHI::DirectX12 { drawIndirectCommandSignature = createSignature(D3D12_INDIRECT_ARGUMENT_TYPE_DRAW, sizeof(DrawIndirectArguments)); drawIndexedIndirectCommandSignature = createSignature(D3D12_INDIRECT_ARGUMENT_TYPE_DRAW_INDEXED, sizeof(DrawIndexedIndirectArguments)); + dispatchIndirectCommandSignature = createSignature(D3D12_INDIRECT_ARGUMENT_TYPE_DISPATCH, sizeof(DispatchIndirectArguments)); } #if BUILD_CONFIG_DEBUG diff --git a/Engine/Source/RHI-Dummy/Include/RHI/Dummy/CommandRecorder.h b/Engine/Source/RHI-Dummy/Include/RHI/Dummy/CommandRecorder.h index 55ad9b38..12957310 100644 --- a/Engine/Source/RHI-Dummy/Include/RHI/Dummy/CommandRecorder.h +++ b/Engine/Source/RHI-Dummy/Include/RHI/Dummy/CommandRecorder.h @@ -62,9 +62,10 @@ namespace RHI::Dummy { void SetBindGroup(uint8_t layoutIndex, BindGroup *bindGroup) override; void SetPipelineConstants(uint32_t pipelineConstantIndex, const void* data, uint32_t size) override; void Dispatch(size_t groupCountX, size_t groupCountY, size_t groupCountZ) override; + void DispatchIndirect(Buffer* indirectBuffer, size_t offset) override; void EndPass() override; }; - + class DummyRasterPassCommandRecorder final : public RasterPassCommandRecorder { public: NonCopyable(DummyRasterPassCommandRecorder) diff --git a/Engine/Source/RHI-Dummy/Src/CommandRecorder.cpp b/Engine/Source/RHI-Dummy/Src/CommandRecorder.cpp index b8c72603..25d3bd4d 100644 --- a/Engine/Source/RHI-Dummy/Src/CommandRecorder.cpp +++ b/Engine/Source/RHI-Dummy/Src/CommandRecorder.cpp @@ -79,6 +79,10 @@ namespace RHI::Dummy { { } + void DummyComputePassCommandRecorder::DispatchIndirect(Buffer* indirectBuffer, size_t offset) + { + } + void DummyComputePassCommandRecorder::EndPass() { } diff --git a/Engine/Source/RHI-Vulkan/Include/RHI/Vulkan/CommandRecorder.h b/Engine/Source/RHI-Vulkan/Include/RHI/Vulkan/CommandRecorder.h index 93659156..40c3677d 100644 --- a/Engine/Source/RHI-Vulkan/Include/RHI/Vulkan/CommandRecorder.h +++ b/Engine/Source/RHI-Vulkan/Include/RHI/Vulkan/CommandRecorder.h @@ -74,6 +74,7 @@ namespace RHI::Vulkan { void SetBindGroup(uint8_t inLayoutIndex, BindGroup* inBindGroup) override; void SetPipelineConstants(uint32_t inPipelineConstantIndex, const void* inData, uint32_t inSize) override; void Dispatch(size_t inGroupCountX, size_t inGroupCountY, size_t inGroupCountZ) override; + void DispatchIndirect(Buffer* inIndirectBuffer, size_t inOffset) override; void EndPass() override; private: diff --git a/Engine/Source/RHI-Vulkan/Src/CommandRecorder.cpp b/Engine/Source/RHI-Vulkan/Src/CommandRecorder.cpp index 14a11070..66b381fe 100644 --- a/Engine/Source/RHI-Vulkan/Src/CommandRecorder.cpp +++ b/Engine/Source/RHI-Vulkan/Src/CommandRecorder.cpp @@ -377,6 +377,12 @@ namespace RHI::Vulkan { vkCmdDispatch(commandBuffer.GetNative(), inGroupCountX, inGroupCountY, inGroupCountZ); } + void VulkanComputePassCommandRecorder::DispatchIndirect(Buffer* inIndirectBuffer, const size_t inOffset) + { + const auto* indirectBuffer = static_cast(inIndirectBuffer); + vkCmdDispatchIndirect(commandBuffer.GetNative(), indirectBuffer->GetNative(), inOffset); + } + void VulkanComputePassCommandRecorder::EndPass() { diff --git a/Engine/Source/RHI/Include/RHI/CommandRecorder.h b/Engine/Source/RHI/Include/RHI/CommandRecorder.h index 160f01fc..b57a2b42 100644 --- a/Engine/Source/RHI/Include/RHI/CommandRecorder.h +++ b/Engine/Source/RHI/Include/RHI/CommandRecorder.h @@ -108,6 +108,12 @@ namespace RHI { uint32_t firstInstance = 0; }; + struct DispatchIndirectArguments { + uint32_t groupCountX = 0; + uint32_t groupCountY = 0; + uint32_t groupCountZ = 0; + }; + template struct ColorAttachmentBase { LoadOp loadOp; @@ -229,6 +235,7 @@ namespace RHI { virtual void SetBindGroup(uint8_t layoutIndex, BindGroup* bindGroup) = 0; virtual void SetPipelineConstants(uint32_t pipelineConstantIndex, const void* data, uint32_t size) = 0; virtual void Dispatch(size_t groupCountX, size_t groupCountY, size_t groupCountZ) = 0; + virtual void DispatchIndirect(Buffer* indirectBuffer, size_t offset) = 0; virtual void EndPass() = 0; protected: From a8846ae9a6dad0a20b178d8707b7ff25fede6710 Mon Sep 17 00:00:00 2001 From: kindem Date: Tue, 30 Jun 2026 23:05:25 +0800 Subject: [PATCH 04/24] feat: support timestamp and occlusion queries in rhi --- .../Include/RHI/DirectX12/CommandRecorder.h | 8 +++ .../Include/RHI/DirectX12/Common.h | 10 ++++ .../Include/RHI/DirectX12/Device.h | 1 + .../Include/RHI/DirectX12/QuerySet.h | 30 +++++++++++ .../Include/RHI/DirectX12/Queue.h | 1 + .../RHI-DirectX12/Src/CommandRecorder.cpp | 36 +++++++++++++ Engine/Source/RHI-DirectX12/Src/Device.cpp | 6 +++ Engine/Source/RHI-DirectX12/Src/QuerySet.cpp | 33 ++++++++++++ Engine/Source/RHI-DirectX12/Src/Queue.cpp | 7 +++ .../Include/RHI/Dummy/CommandRecorder.h | 5 ++ .../RHI-Dummy/Include/RHI/Dummy/Device.h | 1 + .../RHI-Dummy/Include/RHI/Dummy/QuerySet.h | 16 ++++++ .../RHI-Dummy/Include/RHI/Dummy/Queue.h | 1 + .../Source/RHI-Dummy/Src/CommandRecorder.cpp | 20 ++++++++ Engine/Source/RHI-Dummy/Src/Device.cpp | 6 +++ Engine/Source/RHI-Dummy/Src/QuerySet.cpp | 14 ++++++ Engine/Source/RHI-Dummy/Src/Queue.cpp | 5 ++ .../Include/RHI/Vulkan/CommandRecorder.h | 8 +++ .../RHI-Vulkan/Include/RHI/Vulkan/Common.h | 6 +++ .../RHI-Vulkan/Include/RHI/Vulkan/Device.h | 1 + .../RHI-Vulkan/Include/RHI/Vulkan/QuerySet.h | 28 +++++++++++ .../RHI-Vulkan/Include/RHI/Vulkan/Queue.h | 2 + .../Source/RHI-Vulkan/Src/CommandRecorder.cpp | 38 ++++++++++++++ Engine/Source/RHI-Vulkan/Src/Device.cpp | 7 +++ Engine/Source/RHI-Vulkan/Src/QuerySet.cpp | 38 ++++++++++++++ Engine/Source/RHI-Vulkan/Src/Queue.cpp | 14 +++++- .../Source/RHI/Include/RHI/CommandRecorder.h | 6 +++ Engine/Source/RHI/Include/RHI/Common.h | 6 +++ Engine/Source/RHI/Include/RHI/Device.h | 3 ++ Engine/Source/RHI/Include/RHI/QuerySet.h | 38 ++++++++++++++ Engine/Source/RHI/Include/RHI/Queue.h | 1 + Engine/Source/RHI/Include/RHI/RHI.h | 1 + Engine/Source/RHI/Src/QuerySet.cpp | 50 +++++++++++++++++++ 33 files changed, 445 insertions(+), 2 deletions(-) create mode 100644 Engine/Source/RHI-DirectX12/Include/RHI/DirectX12/QuerySet.h create mode 100644 Engine/Source/RHI-DirectX12/Src/QuerySet.cpp create mode 100644 Engine/Source/RHI-Dummy/Include/RHI/Dummy/QuerySet.h create mode 100644 Engine/Source/RHI-Dummy/Src/QuerySet.cpp create mode 100644 Engine/Source/RHI-Vulkan/Include/RHI/Vulkan/QuerySet.h create mode 100644 Engine/Source/RHI-Vulkan/Src/QuerySet.cpp create mode 100644 Engine/Source/RHI/Include/RHI/QuerySet.h create mode 100644 Engine/Source/RHI/Src/QuerySet.cpp diff --git a/Engine/Source/RHI-DirectX12/Include/RHI/DirectX12/CommandRecorder.h b/Engine/Source/RHI-DirectX12/Include/RHI/DirectX12/CommandRecorder.h index 20f3bbbd..b0fd0dc7 100644 --- a/Engine/Source/RHI-DirectX12/Include/RHI/DirectX12/CommandRecorder.h +++ b/Engine/Source/RHI-DirectX12/Include/RHI/DirectX12/CommandRecorder.h @@ -12,6 +12,7 @@ namespace RHI::DirectX12 { class DX12ComputePipeline; class DX12RasterPipeline; class DX12PipelineLayout; + class DX12QuerySet; class DX12CommandRecorder final : public CommandRecorder { public: @@ -25,6 +26,9 @@ namespace RHI::DirectX12 { Common::UniquePtr BeginCopyPass() override; Common::UniquePtr BeginComputePass() override; Common::UniquePtr BeginRasterPass(const RasterPassBeginInfo& inBeginInfo) override; + void WriteTimestamp(QuerySet* inQuerySet, uint32_t inQueryIndex) override; + void ResetQuerySet(QuerySet* inQuerySet, uint32_t inFirstQuery, uint32_t inQueryCount) override; + void ResolveQuery(QuerySet* inQuerySet, uint32_t inFirstQuery, uint32_t inQueryCount, Buffer* inDstBuffer, size_t inDstOffset) override; void End() override; private: @@ -110,6 +114,8 @@ namespace RHI::DirectX12 { void DrawIndexedIndirect(Buffer* inIndirectBuffer, size_t inOffset) override; void MultiDrawIndirect(Buffer* inIndirectBuffer, size_t inOffset, size_t inDrawCount) override; void MultiDrawIndexedIndirect(Buffer* inIndirectBuffer, size_t inOffset, size_t inDrawCount) override; + void BeginOcclusionQuery(QuerySet* inQuerySet, uint32_t inQueryIndex) override; + void EndOcclusionQuery() override; void EndPass() override; private: @@ -117,5 +123,7 @@ namespace RHI::DirectX12 { DX12CommandRecorder& commandRecorder; DX12RasterPipeline* rasterPipeline; DX12CommandBuffer& commandBuffer; + DX12QuerySet* activeOcclusionQuerySet; + uint32_t activeOcclusionQueryIndex; }; } diff --git a/Engine/Source/RHI-DirectX12/Include/RHI/DirectX12/Common.h b/Engine/Source/RHI-DirectX12/Include/RHI/DirectX12/Common.h index e6e39994..5e468fe6 100644 --- a/Engine/Source/RHI-DirectX12/Include/RHI/DirectX12/Common.h +++ b/Engine/Source/RHI-DirectX12/Include/RHI/DirectX12/Common.h @@ -273,6 +273,16 @@ namespace RHI::DirectX12 { ECIMPL_ITEM(TextureState::present, D3D12_RESOURCE_STATE_PRESENT) ECIMPL_END(D3D12_RESOURCE_STATES) + ECIMPL_BEGIN(QueryType, D3D12_QUERY_HEAP_TYPE) + ECIMPL_ITEM(QueryType::occlusion, D3D12_QUERY_HEAP_TYPE_OCCLUSION) + ECIMPL_ITEM(QueryType::timestamp, D3D12_QUERY_HEAP_TYPE_TIMESTAMP) + ECIMPL_END(D3D12_QUERY_HEAP_TYPE) + + ECIMPL_BEGIN(QueryType, D3D12_QUERY_TYPE) + ECIMPL_ITEM(QueryType::occlusion, D3D12_QUERY_TYPE_OCCLUSION) + ECIMPL_ITEM(QueryType::timestamp, D3D12_QUERY_TYPE_TIMESTAMP) + ECIMPL_END(D3D12_QUERY_TYPE) + FCIMPL_BEGIN(ColorWriteFlags, uint8_t) FCIMPL_ITEM(ColorWriteBits::red, D3D12_COLOR_WRITE_ENABLE_RED) FCIMPL_ITEM(ColorWriteBits::green, D3D12_COLOR_WRITE_ENABLE_GREEN) diff --git a/Engine/Source/RHI-DirectX12/Include/RHI/DirectX12/Device.h b/Engine/Source/RHI-DirectX12/Include/RHI/DirectX12/Device.h index df322afe..bdf13206 100644 --- a/Engine/Source/RHI-DirectX12/Include/RHI/DirectX12/Device.h +++ b/Engine/Source/RHI-DirectX12/Include/RHI/DirectX12/Device.h @@ -90,6 +90,7 @@ namespace RHI::DirectX12 { Common::UniquePtr CreateCommandBuffer() override; Common::UniquePtr CreateFence(bool inInitAsSignaled) override; Common::UniquePtr CreateSemaphore() override; + Common::UniquePtr CreateQuerySet(const QuerySetCreateInfo& inCreateInfo) override; bool CheckSwapChainFormatSupport(Surface* inSurface, PixelFormat inFormat) override; TextureSubResourceCopyFootprint GetTextureSubResourceCopyFootprint(const Texture& texture, const TextureSubResourceInfo& subResourceInfo) override; diff --git a/Engine/Source/RHI-DirectX12/Include/RHI/DirectX12/QuerySet.h b/Engine/Source/RHI-DirectX12/Include/RHI/DirectX12/QuerySet.h new file mode 100644 index 00000000..267b876c --- /dev/null +++ b/Engine/Source/RHI-DirectX12/Include/RHI/DirectX12/QuerySet.h @@ -0,0 +1,30 @@ +// +// Created by johnk on 2026/6/30. +// + +#pragma once + +#include +#include + +#include + +using Microsoft::WRL::ComPtr; + +namespace RHI::DirectX12 { + class DX12Device; + + class DX12QuerySet final : public QuerySet { + public: + NonCopyable(DX12QuerySet) + DX12QuerySet(DX12Device& inDevice, const QuerySetCreateInfo& inCreateInfo); + ~DX12QuerySet() override; + + ID3D12QueryHeap* GetNative() const; + + private: + void CreateNativeQueryHeap(DX12Device& inDevice, const QuerySetCreateInfo& inCreateInfo); + + ComPtr nativeQueryHeap; + }; +} diff --git a/Engine/Source/RHI-DirectX12/Include/RHI/DirectX12/Queue.h b/Engine/Source/RHI-DirectX12/Include/RHI/DirectX12/Queue.h index f2b165d9..875f8aa9 100644 --- a/Engine/Source/RHI-DirectX12/Include/RHI/DirectX12/Queue.h +++ b/Engine/Source/RHI-DirectX12/Include/RHI/DirectX12/Queue.h @@ -20,6 +20,7 @@ namespace RHI::DirectX12 { void Submit(CommandBuffer* inCmdBuffer, const QueueSubmitInfo& inSubmitInfo) override; void Flush(Fence* inFenceToSignal) override; + float GetTimestampPeriod() override; ID3D12CommandQueue* GetNative() const; diff --git a/Engine/Source/RHI-DirectX12/Src/CommandRecorder.cpp b/Engine/Source/RHI-DirectX12/Src/CommandRecorder.cpp index febed841..8d196ffe 100644 --- a/Engine/Source/RHI-DirectX12/Src/CommandRecorder.cpp +++ b/Engine/Source/RHI-DirectX12/Src/CommandRecorder.cpp @@ -17,6 +17,7 @@ #include #include #include +#include #include namespace RHI::DirectX12 { @@ -251,6 +252,8 @@ namespace RHI::DirectX12 { , commandRecorder(inCmdRecorder) , rasterPipeline(nullptr) , commandBuffer(inCmdBuffer) + , activeOcclusionQuerySet(nullptr) + , activeOcclusionQueryIndex(0) { // set render targets std::vector rtvHandles(inBeginInfo.colorAttachments.size()); @@ -409,6 +412,19 @@ namespace RHI::DirectX12 { commandBuffer.GetNativeCmdList()->ExecuteIndirect(device.GetDrawIndexedIndirectCommandSignature(), inDrawCount, indirectBuffer->GetNative(), inOffset, nullptr, 0); } + void DX12RasterPassCommandRecorder::BeginOcclusionQuery(QuerySet* inQuerySet, const uint32_t inQueryIndex) + { + activeOcclusionQuerySet = static_cast(inQuerySet); + activeOcclusionQueryIndex = inQueryIndex; + commandBuffer.GetNativeCmdList()->BeginQuery(activeOcclusionQuerySet->GetNative(), D3D12_QUERY_TYPE_OCCLUSION, inQueryIndex); + } + + void DX12RasterPassCommandRecorder::EndOcclusionQuery() + { + Assert(activeOcclusionQuerySet != nullptr); + commandBuffer.GetNativeCmdList()->EndQuery(activeOcclusionQuerySet->GetNative(), D3D12_QUERY_TYPE_OCCLUSION, activeOcclusionQueryIndex); + } + void DX12RasterPassCommandRecorder::EndPass() { } @@ -492,6 +508,26 @@ namespace RHI::DirectX12 { return Common::UniquePtr(new DX12RasterPassCommandRecorder(device, *this, commandBuffer, inBeginInfo)); } + void DX12CommandRecorder::WriteTimestamp(QuerySet* inQuerySet, const uint32_t inQueryIndex) + { + const auto* querySet = static_cast(inQuerySet); + const auto queryType = EnumCast(querySet->GetCreateInfo().type); + commandBuffer.GetNativeCmdList()->EndQuery(querySet->GetNative(), queryType, inQueryIndex); + } + + void DX12CommandRecorder::ResetQuerySet(QuerySet* inQuerySet, uint32_t inFirstQuery, uint32_t inQueryCount) + { + // DirectX12 query heaps need no reset before reuse. + } + + void DX12CommandRecorder::ResolveQuery(QuerySet* inQuerySet, const uint32_t inFirstQuery, const uint32_t inQueryCount, Buffer* inDstBuffer, const size_t inDstOffset) + { + const auto* querySet = static_cast(inQuerySet); + const auto* dstBuffer = static_cast(inDstBuffer); + const auto queryType = EnumCast(querySet->GetCreateInfo().type); + commandBuffer.GetNativeCmdList()->ResolveQueryData(querySet->GetNative(), queryType, inFirstQuery, inQueryCount, dstBuffer->GetNative(), inDstOffset); + } + void DX12CommandRecorder::End() { Assert(SUCCEEDED(commandBuffer.GetNativeCmdList()->Close())); diff --git a/Engine/Source/RHI-DirectX12/Src/Device.cpp b/Engine/Source/RHI-DirectX12/Src/Device.cpp index 0162d75e..cf3fa51b 100644 --- a/Engine/Source/RHI-DirectX12/Src/Device.cpp +++ b/Engine/Source/RHI-DirectX12/Src/Device.cpp @@ -24,6 +24,7 @@ #include #include #include +#include #include #include @@ -253,6 +254,11 @@ namespace RHI::DirectX12 { return { new DX12Semaphore(*this) }; } + Common::UniquePtr DX12Device::CreateQuerySet(const QuerySetCreateInfo& inCreateInfo) + { + return { new DX12QuerySet(*this, inCreateInfo) }; + } + bool DX12Device::CheckSwapChainFormatSupport(Surface* inSurface, PixelFormat inFormat) { static std::unordered_set supportedFormats = { diff --git a/Engine/Source/RHI-DirectX12/Src/QuerySet.cpp b/Engine/Source/RHI-DirectX12/Src/QuerySet.cpp new file mode 100644 index 00000000..b4d3149e --- /dev/null +++ b/Engine/Source/RHI-DirectX12/Src/QuerySet.cpp @@ -0,0 +1,33 @@ +// +// Created by johnk on 2026/6/30. +// + +#include +#include +#include +#include + +namespace RHI::DirectX12 { + DX12QuerySet::DX12QuerySet(DX12Device& inDevice, const QuerySetCreateInfo& inCreateInfo) + : QuerySet(inCreateInfo) + { + CreateNativeQueryHeap(inDevice, inCreateInfo); + } + + DX12QuerySet::~DX12QuerySet() = default; + + ID3D12QueryHeap* DX12QuerySet::GetNative() const + { + return nativeQueryHeap.Get(); + } + + void DX12QuerySet::CreateNativeQueryHeap(DX12Device& inDevice, const QuerySetCreateInfo& inCreateInfo) + { + D3D12_QUERY_HEAP_DESC desc = {}; + desc.Type = EnumCast(inCreateInfo.type); + desc.Count = inCreateInfo.count; + desc.NodeMask = 0; + + Assert(SUCCEEDED(inDevice.GetNative()->CreateQueryHeap(&desc, IID_PPV_ARGS(&nativeQueryHeap)))); + } +} diff --git a/Engine/Source/RHI-DirectX12/Src/Queue.cpp b/Engine/Source/RHI-DirectX12/Src/Queue.cpp index ca700b26..cd4d5c65 100644 --- a/Engine/Source/RHI-DirectX12/Src/Queue.cpp +++ b/Engine/Source/RHI-DirectX12/Src/Queue.cpp @@ -44,6 +44,13 @@ namespace RHI::DirectX12 { } } + float DX12Queue::GetTimestampPeriod() + { + UINT64 frequency; + Assert(SUCCEEDED(nativeCmdQueue->GetTimestampFrequency(&frequency))); + return 1.0e9f / static_cast(frequency); + } + ID3D12CommandQueue* DX12Queue::GetNative() const { return nativeCmdQueue.Get(); diff --git a/Engine/Source/RHI-Dummy/Include/RHI/Dummy/CommandRecorder.h b/Engine/Source/RHI-Dummy/Include/RHI/Dummy/CommandRecorder.h index 12957310..a4687454 100644 --- a/Engine/Source/RHI-Dummy/Include/RHI/Dummy/CommandRecorder.h +++ b/Engine/Source/RHI-Dummy/Include/RHI/Dummy/CommandRecorder.h @@ -21,6 +21,9 @@ namespace RHI::Dummy { Common::UniquePtr BeginCopyPass() override; Common::UniquePtr BeginComputePass() override; Common::UniquePtr BeginRasterPass(const RasterPassBeginInfo& beginInfo) override; + void WriteTimestamp(QuerySet* querySet, uint32_t queryIndex) override; + void ResetQuerySet(QuerySet* querySet, uint32_t firstQuery, uint32_t queryCount) override; + void ResolveQuery(QuerySet* querySet, uint32_t firstQuery, uint32_t queryCount, Buffer* dstBuffer, size_t dstOffset) override; void End() override; private: @@ -94,6 +97,8 @@ namespace RHI::Dummy { void DrawIndexedIndirect(Buffer* indirectBuffer, size_t offset) override; void MultiDrawIndirect(Buffer* indirectBuffer, size_t offset, size_t drawCount) override; void MultiDrawIndexedIndirect(Buffer* indirectBuffer, size_t offset, size_t drawCount) override; + void BeginOcclusionQuery(QuerySet* querySet, uint32_t queryIndex) override; + void EndOcclusionQuery() override; void EndPass() override; }; } diff --git a/Engine/Source/RHI-Dummy/Include/RHI/Dummy/Device.h b/Engine/Source/RHI-Dummy/Include/RHI/Dummy/Device.h index 2f9268f4..fa686219 100644 --- a/Engine/Source/RHI-Dummy/Include/RHI/Dummy/Device.h +++ b/Engine/Source/RHI-Dummy/Include/RHI/Dummy/Device.h @@ -33,6 +33,7 @@ namespace RHI::Dummy { Common::UniquePtr CreateCommandBuffer() override; Common::UniquePtr CreateFence(bool bInitAsSignaled) override; Common::UniquePtr CreateSemaphore() override; + Common::UniquePtr CreateQuerySet(const QuerySetCreateInfo& createInfo) override; bool CheckSwapChainFormatSupport(Surface *surface, PixelFormat format) override; TextureSubResourceCopyFootprint GetTextureSubResourceCopyFootprint(const Texture& texture, const TextureSubResourceInfo& subResourceInfo) override; diff --git a/Engine/Source/RHI-Dummy/Include/RHI/Dummy/QuerySet.h b/Engine/Source/RHI-Dummy/Include/RHI/Dummy/QuerySet.h new file mode 100644 index 00000000..c9c3fac3 --- /dev/null +++ b/Engine/Source/RHI-Dummy/Include/RHI/Dummy/QuerySet.h @@ -0,0 +1,16 @@ +// +// Created by johnk on 2026/6/30. +// + +#pragma once + +#include + +namespace RHI::Dummy { + class DummyQuerySet final : public QuerySet { + public: + NonCopyable(DummyQuerySet) + explicit DummyQuerySet(const QuerySetCreateInfo& createInfo); + ~DummyQuerySet() override; + }; +} diff --git a/Engine/Source/RHI-Dummy/Include/RHI/Dummy/Queue.h b/Engine/Source/RHI-Dummy/Include/RHI/Dummy/Queue.h index 24aa57c6..47a5def2 100644 --- a/Engine/Source/RHI-Dummy/Include/RHI/Dummy/Queue.h +++ b/Engine/Source/RHI-Dummy/Include/RHI/Dummy/Queue.h @@ -15,5 +15,6 @@ namespace RHI::Dummy { void Submit(RHI::CommandBuffer* commandBuffer, const RHI::QueueSubmitInfo& submitInfo) override; void Flush(RHI::Fence* fenceToSignal) override; + float GetTimestampPeriod() override; }; } diff --git a/Engine/Source/RHI-Dummy/Src/CommandRecorder.cpp b/Engine/Source/RHI-Dummy/Src/CommandRecorder.cpp index 25d3bd4d..ada6bbdb 100644 --- a/Engine/Source/RHI-Dummy/Src/CommandRecorder.cpp +++ b/Engine/Source/RHI-Dummy/Src/CommandRecorder.cpp @@ -169,6 +169,14 @@ namespace RHI::Dummy { { } + void DummyRasterPassCommandRecorder::BeginOcclusionQuery(QuerySet* querySet, uint32_t queryIndex) + { + } + + void DummyRasterPassCommandRecorder::EndOcclusionQuery() + { + } + void DummyRasterPassCommandRecorder::EndPass() { } @@ -207,6 +215,18 @@ namespace RHI::Dummy { return Common::UniquePtr(new DummyRasterPassCommandRecorder(dummyCommandBuffer)); } + void DummyCommandRecorder::WriteTimestamp(QuerySet* querySet, uint32_t queryIndex) + { + } + + void DummyCommandRecorder::ResetQuerySet(QuerySet* querySet, uint32_t firstQuery, uint32_t queryCount) + { + } + + void DummyCommandRecorder::ResolveQuery(QuerySet* querySet, uint32_t firstQuery, uint32_t queryCount, Buffer* dstBuffer, size_t dstOffset) + { + } + void DummyCommandRecorder::End() { } diff --git a/Engine/Source/RHI-Dummy/Src/Device.cpp b/Engine/Source/RHI-Dummy/Src/Device.cpp index c4cbd46d..7caa2b3c 100644 --- a/Engine/Source/RHI-Dummy/Src/Device.cpp +++ b/Engine/Source/RHI-Dummy/Src/Device.cpp @@ -16,6 +16,7 @@ #include #include #include +#include #include namespace RHI::Dummy { @@ -114,6 +115,11 @@ namespace RHI::Dummy { return { new DummySemaphore(*this) }; } + Common::UniquePtr DummyDevice::CreateQuerySet(const QuerySetCreateInfo& createInfo) + { + return { new DummyQuerySet(createInfo) }; + } + bool DummyDevice::CheckSwapChainFormatSupport(Surface* surface, PixelFormat format) { return true; diff --git a/Engine/Source/RHI-Dummy/Src/QuerySet.cpp b/Engine/Source/RHI-Dummy/Src/QuerySet.cpp new file mode 100644 index 00000000..577b4809 --- /dev/null +++ b/Engine/Source/RHI-Dummy/Src/QuerySet.cpp @@ -0,0 +1,14 @@ +// +// Created by johnk on 2026/6/30. +// + +#include + +namespace RHI::Dummy { + DummyQuerySet::DummyQuerySet(const QuerySetCreateInfo& createInfo) + : QuerySet(createInfo) + { + } + + DummyQuerySet::~DummyQuerySet() = default; +} diff --git a/Engine/Source/RHI-Dummy/Src/Queue.cpp b/Engine/Source/RHI-Dummy/Src/Queue.cpp index 48d3a866..3f6a3660 100644 --- a/Engine/Source/RHI-Dummy/Src/Queue.cpp +++ b/Engine/Source/RHI-Dummy/Src/Queue.cpp @@ -16,4 +16,9 @@ namespace RHI::Dummy { void DummyQueue::Flush(RHI::Fence* fenceToSignal) { } + + float DummyQueue::GetTimestampPeriod() + { + return 1.0f; + } } diff --git a/Engine/Source/RHI-Vulkan/Include/RHI/Vulkan/CommandRecorder.h b/Engine/Source/RHI-Vulkan/Include/RHI/Vulkan/CommandRecorder.h index 40c3677d..ff8a83fc 100644 --- a/Engine/Source/RHI-Vulkan/Include/RHI/Vulkan/CommandRecorder.h +++ b/Engine/Source/RHI-Vulkan/Include/RHI/Vulkan/CommandRecorder.h @@ -14,6 +14,7 @@ namespace RHI::Vulkan { class VulkanCommandBuffer; class VulkanRasterPipeline; class VulkanComputePipeline; + class VulkanQuerySet; class VulkanCommandRecorder final : public CommandRecorder { public: @@ -27,6 +28,9 @@ namespace RHI::Vulkan { Common::UniquePtr BeginCopyPass() override; Common::UniquePtr BeginComputePass() override; Common::UniquePtr BeginRasterPass(const RasterPassBeginInfo& inBeginInfo) override; + void WriteTimestamp(QuerySet* inQuerySet, uint32_t inQueryIndex) override; + void ResetQuerySet(QuerySet* inQuerySet, uint32_t inFirstQuery, uint32_t inQueryCount) override; + void ResolveQuery(QuerySet* inQuerySet, uint32_t inFirstQuery, uint32_t inQueryCount, Buffer* inDstBuffer, size_t inDstOffset) override; void End() override; private: @@ -112,6 +116,8 @@ namespace RHI::Vulkan { void DrawIndexedIndirect(Buffer* inIndirectBuffer, size_t inOffset) override; void MultiDrawIndirect(Buffer* inIndirectBuffer, size_t inOffset, size_t inDrawCount) override; void MultiDrawIndexedIndirect(Buffer* inIndirectBuffer, size_t inOffset, size_t inDrawCount) override; + void BeginOcclusionQuery(QuerySet* inQuerySet, uint32_t inQueryIndex) override; + void EndOcclusionQuery() override; void EndPass() override; private: @@ -119,6 +125,8 @@ namespace RHI::Vulkan { VulkanCommandRecorder& commandRecorder; VulkanCommandBuffer& commandBuffer; VulkanRasterPipeline* rasterPipeline; + VulkanQuerySet* activeOcclusionQuerySet; + uint32_t activeOcclusionQueryIndex; }; } diff --git a/Engine/Source/RHI-Vulkan/Include/RHI/Vulkan/Common.h b/Engine/Source/RHI-Vulkan/Include/RHI/Vulkan/Common.h index 105f980c..76344dfc 100644 --- a/Engine/Source/RHI-Vulkan/Include/RHI/Vulkan/Common.h +++ b/Engine/Source/RHI-Vulkan/Include/RHI/Vulkan/Common.h @@ -264,6 +264,11 @@ namespace RHI::Vulkan { ECIMPL_ITEM(PresentMode::max, VK_PRESENT_MODE_IMMEDIATE_KHR) // TODO Set the default present mode to immediate? ECIMPL_END(VkPresentModeKHR) + ECIMPL_BEGIN(QueryType, VkQueryType) + ECIMPL_ITEM(QueryType::occlusion, VK_QUERY_TYPE_OCCLUSION) + ECIMPL_ITEM(QueryType::timestamp, VK_QUERY_TYPE_TIMESTAMP) + ECIMPL_END(VkQueryType) + ECIMPL_BEGIN(TextureAspect, VkImageAspectFlags) ECIMPL_ITEM(TextureAspect::color, VK_IMAGE_ASPECT_COLOR_BIT) ECIMPL_ITEM(TextureAspect::depth, VK_IMAGE_ASPECT_DEPTH_BIT) @@ -289,6 +294,7 @@ namespace RHI::Vulkan { FCIMPL_ITEM(BufferUsageBits::storage, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT) FCIMPL_ITEM(BufferUsageBits::rwStorage, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT) FCIMPL_ITEM(BufferUsageBits::indirect, VK_BUFFER_USAGE_INDIRECT_BUFFER_BIT) + FCIMPL_ITEM(BufferUsageBits::queryResolve, VK_BUFFER_USAGE_TRANSFER_DST_BIT) FCIMPL_END(VkBufferUsageFlagBits) FCIMPL_BEGIN(TextureUsageFlags, VkImageUsageFlags) diff --git a/Engine/Source/RHI-Vulkan/Include/RHI/Vulkan/Device.h b/Engine/Source/RHI-Vulkan/Include/RHI/Vulkan/Device.h index 7071a00f..26c5b837 100644 --- a/Engine/Source/RHI-Vulkan/Include/RHI/Vulkan/Device.h +++ b/Engine/Source/RHI-Vulkan/Include/RHI/Vulkan/Device.h @@ -40,6 +40,7 @@ namespace RHI::Vulkan { Common::UniquePtr CreateCommandBuffer() override; Common::UniquePtr CreateFence(bool initAsSignaled) override; Common::UniquePtr CreateSemaphore() override; + Common::UniquePtr CreateQuerySet(const QuerySetCreateInfo& inCreateInfo) override; bool CheckSwapChainFormatSupport(Surface* inSurface, PixelFormat inFormat) override; TextureSubResourceCopyFootprint GetTextureSubResourceCopyFootprint(const Texture& texture, const TextureSubResourceInfo& subResourceInfo) override; diff --git a/Engine/Source/RHI-Vulkan/Include/RHI/Vulkan/QuerySet.h b/Engine/Source/RHI-Vulkan/Include/RHI/Vulkan/QuerySet.h new file mode 100644 index 00000000..86b9b3ae --- /dev/null +++ b/Engine/Source/RHI-Vulkan/Include/RHI/Vulkan/QuerySet.h @@ -0,0 +1,28 @@ +// +// Created by johnk on 2026/6/30. +// + +#pragma once + +#include + +#include + +namespace RHI::Vulkan { + class VulkanDevice; + + class VulkanQuerySet final : public QuerySet { + public: + NonCopyable(VulkanQuerySet) + VulkanQuerySet(VulkanDevice& inDevice, const QuerySetCreateInfo& inCreateInfo); + ~VulkanQuerySet() override; + + VkQueryPool GetNative() const; + + private: + void CreateNativeQueryPool(const QuerySetCreateInfo& inCreateInfo); + + VulkanDevice& device; + VkQueryPool nativeQueryPool; + }; +} diff --git a/Engine/Source/RHI-Vulkan/Include/RHI/Vulkan/Queue.h b/Engine/Source/RHI-Vulkan/Include/RHI/Vulkan/Queue.h index 082d32d8..6c288ea2 100644 --- a/Engine/Source/RHI-Vulkan/Include/RHI/Vulkan/Queue.h +++ b/Engine/Source/RHI-Vulkan/Include/RHI/Vulkan/Queue.h @@ -20,10 +20,12 @@ namespace RHI::Vulkan { void Submit(CommandBuffer* inCmdBuffer, const QueueSubmitInfo& inSubmitInfo) override; void Flush(Fence* inFenceToSignal) override; + float GetTimestampPeriod() override; VkQueue GetNative() const; private: + VulkanDevice& device; VkQueue nativeQueue; }; } diff --git a/Engine/Source/RHI-Vulkan/Src/CommandRecorder.cpp b/Engine/Source/RHI-Vulkan/Src/CommandRecorder.cpp index 66b381fe..d5099531 100644 --- a/Engine/Source/RHI-Vulkan/Src/CommandRecorder.cpp +++ b/Engine/Source/RHI-Vulkan/Src/CommandRecorder.cpp @@ -15,6 +15,7 @@ #include #include #include +#include #include namespace RHI::Vulkan { @@ -244,6 +245,28 @@ namespace RHI::Vulkan { return Common::UniquePtr(new VulkanRasterPassCommandRecorder(device, *this, commandBuffer, inBeginInfo)); } + void VulkanCommandRecorder::WriteTimestamp(QuerySet* inQuerySet, const uint32_t inQueryIndex) + { + const auto* querySet = static_cast(inQuerySet); + vkCmdWriteTimestamp(commandBuffer.GetNative(), VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, querySet->GetNative(), inQueryIndex); + } + + void VulkanCommandRecorder::ResetQuerySet(QuerySet* inQuerySet, const uint32_t inFirstQuery, const uint32_t inQueryCount) + { + const auto* querySet = static_cast(inQuerySet); + vkCmdResetQueryPool(commandBuffer.GetNative(), querySet->GetNative(), inFirstQuery, inQueryCount); + } + + void VulkanCommandRecorder::ResolveQuery(QuerySet* inQuerySet, const uint32_t inFirstQuery, const uint32_t inQueryCount, Buffer* inDstBuffer, const size_t inDstOffset) + { + const auto* querySet = static_cast(inQuerySet); + const auto* dstBuffer = static_cast(inDstBuffer); + vkCmdCopyQueryPoolResults( + commandBuffer.GetNative(), querySet->GetNative(), inFirstQuery, inQueryCount, + dstBuffer->GetNative(), inDstOffset, sizeof(uint64_t), + VK_QUERY_RESULT_64_BIT | VK_QUERY_RESULT_WAIT_BIT); + } + void VulkanCommandRecorder::End() { vkEndCommandBuffer(commandBuffer.GetNative()); @@ -393,6 +416,8 @@ namespace RHI::Vulkan { , commandRecorder(inCmdRecorder) , commandBuffer(inCmdBuffer) , rasterPipeline(nullptr) + , activeOcclusionQuerySet(nullptr) + , activeOcclusionQueryIndex(0) { std::vector colorAttachmentInfos(inBeginInfo.colorAttachments.size()); for (size_t i = 0; i < inBeginInfo.colorAttachments.size(); i++) @@ -588,6 +613,19 @@ namespace RHI::Vulkan { vkCmdDrawIndexedIndirect(commandBuffer.GetNative(), indirectBuffer->GetNative(), inOffset, inDrawCount, sizeof(DrawIndexedIndirectArguments)); } + void VulkanRasterPassCommandRecorder::BeginOcclusionQuery(QuerySet* inQuerySet, const uint32_t inQueryIndex) + { + activeOcclusionQuerySet = static_cast(inQuerySet); + activeOcclusionQueryIndex = inQueryIndex; + vkCmdBeginQuery(commandBuffer.GetNative(), activeOcclusionQuerySet->GetNative(), inQueryIndex, VK_QUERY_CONTROL_PRECISE_BIT); + } + + void VulkanRasterPassCommandRecorder::EndOcclusionQuery() + { + Assert(activeOcclusionQuerySet != nullptr); + vkCmdEndQuery(commandBuffer.GetNative(), activeOcclusionQuerySet->GetNative(), activeOcclusionQueryIndex); + } + void VulkanRasterPassCommandRecorder::EndPass() { auto* pfn = device.GetGpu().GetInstance().FindOrGetTypedDynamicFuncPointer("vkCmdEndRenderingKHR"); diff --git a/Engine/Source/RHI-Vulkan/Src/Device.cpp b/Engine/Source/RHI-Vulkan/Src/Device.cpp index 1826cf85..20b6260c 100644 --- a/Engine/Source/RHI-Vulkan/Src/Device.cpp +++ b/Engine/Source/RHI-Vulkan/Src/Device.cpp @@ -22,6 +22,7 @@ #include #include #include +#include namespace RHI::Vulkan { const std::vector requiredExtensions = { @@ -147,6 +148,11 @@ namespace RHI::Vulkan { return { new VulkanSemaphore(*this) }; } + Common::UniquePtr VulkanDevice::CreateQuerySet(const QuerySetCreateInfo& inCreateInfo) + { + return { new VulkanQuerySet(*this, inCreateInfo) }; + } + bool VulkanDevice::CheckSwapChainFormatSupport(Surface* inSurface, const PixelFormat inFormat) { const auto* vkSurface = static_cast(inSurface); @@ -248,6 +254,7 @@ namespace RHI::Vulkan { VkPhysicalDeviceFeatures deviceFeatures = {}; deviceFeatures.samplerAnisotropy = VK_TRUE; + deviceFeatures.occlusionQueryPrecise = VK_TRUE; VkDeviceCreateInfo deviceCreateInfo = {}; deviceCreateInfo.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO; diff --git a/Engine/Source/RHI-Vulkan/Src/QuerySet.cpp b/Engine/Source/RHI-Vulkan/Src/QuerySet.cpp new file mode 100644 index 00000000..e5b093c6 --- /dev/null +++ b/Engine/Source/RHI-Vulkan/Src/QuerySet.cpp @@ -0,0 +1,38 @@ +// +// Created by johnk on 2026/6/30. +// + +#include +#include +#include +#include + +namespace RHI::Vulkan { + VulkanQuerySet::VulkanQuerySet(VulkanDevice& inDevice, const QuerySetCreateInfo& inCreateInfo) + : QuerySet(inCreateInfo) + , device(inDevice) + , nativeQueryPool(VK_NULL_HANDLE) + { + CreateNativeQueryPool(inCreateInfo); + } + + VulkanQuerySet::~VulkanQuerySet() + { + vkDestroyQueryPool(device.GetNative(), nativeQueryPool, nullptr); + } + + VkQueryPool VulkanQuerySet::GetNative() const + { + return nativeQueryPool; + } + + void VulkanQuerySet::CreateNativeQueryPool(const QuerySetCreateInfo& inCreateInfo) + { + VkQueryPoolCreateInfo poolInfo = {}; + poolInfo.sType = VK_STRUCTURE_TYPE_QUERY_POOL_CREATE_INFO; + poolInfo.queryType = EnumCast(inCreateInfo.type); + poolInfo.queryCount = inCreateInfo.count; + + Assert(vkCreateQueryPool(device.GetNative(), &poolInfo, nullptr, &nativeQueryPool) == VK_SUCCESS); + } +} diff --git a/Engine/Source/RHI-Vulkan/Src/Queue.cpp b/Engine/Source/RHI-Vulkan/Src/Queue.cpp index 11477f96..82a7d9fd 100644 --- a/Engine/Source/RHI-Vulkan/Src/Queue.cpp +++ b/Engine/Source/RHI-Vulkan/Src/Queue.cpp @@ -8,10 +8,13 @@ #include #include #include +#include +#include namespace RHI::Vulkan { - VulkanQueue::VulkanQueue(VulkanDevice&, const VkQueue inNativeQueue) - : nativeQueue(inNativeQueue) + VulkanQueue::VulkanQueue(VulkanDevice& inDevice, const VkQueue inNativeQueue) + : device(inDevice) + , nativeQueue(inNativeQueue) { } @@ -69,6 +72,13 @@ namespace RHI::Vulkan { Assert(vkQueueSubmit(nativeQueue, 1, &vkSubmitInfo, vkFence->GetNative()) == VK_SUCCESS); } + float VulkanQueue::GetTimestampPeriod() + { + VkPhysicalDeviceProperties properties; + vkGetPhysicalDeviceProperties(device.GetGpu().GetNative(), &properties); + return properties.limits.timestampPeriod; + } + VkQueue VulkanQueue::GetNative() const { return nativeQueue; diff --git a/Engine/Source/RHI/Include/RHI/CommandRecorder.h b/Engine/Source/RHI/Include/RHI/CommandRecorder.h index b57a2b42..160925e4 100644 --- a/Engine/Source/RHI/Include/RHI/CommandRecorder.h +++ b/Engine/Source/RHI/Include/RHI/CommandRecorder.h @@ -22,6 +22,7 @@ namespace RHI { class TextureView; class BindGroup; class SwapChain; + class QuerySet; struct Barrier; struct TextureSubResourceInfo { @@ -263,6 +264,8 @@ namespace RHI { virtual void DrawIndexedIndirect(Buffer* indirectBuffer, size_t offset) = 0; virtual void MultiDrawIndirect(Buffer* indirectBuffer, size_t offset, size_t drawCount) = 0; virtual void MultiDrawIndexedIndirect(Buffer* indirectBuffer, size_t offset, size_t drawCount) = 0; + virtual void BeginOcclusionQuery(QuerySet* querySet, uint32_t queryIndex) = 0; + virtual void EndOcclusionQuery() = 0; virtual void EndPass() = 0; protected: @@ -277,6 +280,9 @@ namespace RHI { virtual Common::UniquePtr BeginCopyPass() = 0; virtual Common::UniquePtr BeginComputePass() = 0; virtual Common::UniquePtr BeginRasterPass(const RasterPassBeginInfo& beginInfo) = 0; + virtual void WriteTimestamp(QuerySet* querySet, uint32_t queryIndex) = 0; + virtual void ResetQuerySet(QuerySet* querySet, uint32_t firstQuery, uint32_t queryCount) = 0; + virtual void ResolveQuery(QuerySet* querySet, uint32_t firstQuery, uint32_t queryCount, Buffer* dstBuffer, size_t dstOffset) = 0; virtual void End() = 0; protected: diff --git a/Engine/Source/RHI/Include/RHI/Common.h b/Engine/Source/RHI/Include/RHI/Common.h index ea4c46cf..877b59f3 100644 --- a/Engine/Source/RHI/Include/RHI/Common.h +++ b/Engine/Source/RHI/Include/RHI/Common.h @@ -408,6 +408,12 @@ namespace RHI { present, max }; + + enum class QueryType : uint8_t { + occlusion, + timestamp, + max + }; } namespace RHI { diff --git a/Engine/Source/RHI/Include/RHI/Device.h b/Engine/Source/RHI/Include/RHI/Device.h index 816a2fea..fd174101 100644 --- a/Engine/Source/RHI/Include/RHI/Device.h +++ b/Engine/Source/RHI/Include/RHI/Device.h @@ -26,9 +26,11 @@ namespace RHI { struct RasterPipelineCreateInfo; struct SwapChainCreateInfo; struct SurfaceCreateInfo; + struct QuerySetCreateInfo; struct TextureSubResourceCopyFootprint; struct TextureSubResourceInfo; class Queue; + class QuerySet; class Buffer; class Texture; class Sampler; @@ -80,6 +82,7 @@ namespace RHI { virtual Common::UniquePtr CreateCommandBuffer() = 0; virtual Common::UniquePtr CreateFence(bool bInitAsSignaled) = 0; virtual Common::UniquePtr CreateSemaphore() = 0; + virtual Common::UniquePtr CreateQuerySet(const QuerySetCreateInfo& createInfo) = 0; virtual bool CheckSwapChainFormatSupport(Surface* surface, PixelFormat format) = 0; virtual TextureSubResourceCopyFootprint GetTextureSubResourceCopyFootprint(const Texture& texture, const TextureSubResourceInfo& subResourceInfo) = 0; diff --git a/Engine/Source/RHI/Include/RHI/QuerySet.h b/Engine/Source/RHI/Include/RHI/QuerySet.h new file mode 100644 index 00000000..6d42d695 --- /dev/null +++ b/Engine/Source/RHI/Include/RHI/QuerySet.h @@ -0,0 +1,38 @@ +// +// Created by johnk on 2026/6/30. +// + +#pragma once + +#include + +#include +#include + +namespace RHI { + struct QuerySetCreateInfo { + QueryType type; + uint32_t count; + std::string debugName; + + QuerySetCreateInfo(); + QuerySetCreateInfo(QueryType inType, uint32_t inCount, std::string inDebugName = ""); + + QuerySetCreateInfo& SetType(QueryType inType); + QuerySetCreateInfo& SetCount(uint32_t inCount); + QuerySetCreateInfo& SetDebugName(std::string inDebugName); + }; + + class QuerySet { + public: + NonCopyable(QuerySet) + virtual ~QuerySet(); + + const QuerySetCreateInfo& GetCreateInfo() const; + + protected: + explicit QuerySet(const QuerySetCreateInfo& inCreateInfo); + + QuerySetCreateInfo createInfo; + }; +} diff --git a/Engine/Source/RHI/Include/RHI/Queue.h b/Engine/Source/RHI/Include/RHI/Queue.h index e3e602e9..db80e37c 100644 --- a/Engine/Source/RHI/Include/RHI/Queue.h +++ b/Engine/Source/RHI/Include/RHI/Queue.h @@ -34,6 +34,7 @@ namespace RHI { virtual void Submit(CommandBuffer* commandBuffer, const QueueSubmitInfo& submitInfo) = 0; virtual void Flush(Fence* fenceToSignal) = 0; + virtual float GetTimestampPeriod() = 0; protected: Queue(); diff --git a/Engine/Source/RHI/Include/RHI/RHI.h b/Engine/Source/RHI/Include/RHI/RHI.h index eb8c080c..9b1ce213 100644 --- a/Engine/Source/RHI/Include/RHI/RHI.h +++ b/Engine/Source/RHI/Include/RHI/RHI.h @@ -24,6 +24,7 @@ #include #include #include +#include #if PLATFORM_WINDOWS #undef CreateSemaphore diff --git a/Engine/Source/RHI/Src/QuerySet.cpp b/Engine/Source/RHI/Src/QuerySet.cpp new file mode 100644 index 00000000..ac67c476 --- /dev/null +++ b/Engine/Source/RHI/Src/QuerySet.cpp @@ -0,0 +1,50 @@ +// +// Created by johnk on 2026/6/30. +// + +#include + +namespace RHI { + QuerySetCreateInfo::QuerySetCreateInfo() + : type(QueryType::max) + , count(0) + { + } + + QuerySetCreateInfo::QuerySetCreateInfo(const QueryType inType, const uint32_t inCount, std::string inDebugName) + : type(inType) + , count(inCount) + , debugName(std::move(inDebugName)) + { + } + + QuerySetCreateInfo& QuerySetCreateInfo::SetType(const QueryType inType) + { + type = inType; + return *this; + } + + QuerySetCreateInfo& QuerySetCreateInfo::SetCount(const uint32_t inCount) + { + count = inCount; + return *this; + } + + QuerySetCreateInfo& QuerySetCreateInfo::SetDebugName(std::string inDebugName) + { + debugName = std::move(inDebugName); + return *this; + } + + QuerySet::QuerySet(const QuerySetCreateInfo& inCreateInfo) + : createInfo(inCreateInfo) + { + } + + QuerySet::~QuerySet() = default; + + const QuerySetCreateInfo& QuerySet::GetCreateInfo() const + { + return createInfo; + } +} From 4a0cf43dec0d4e23dcdba9f075c04d5326d9c812 Mon Sep 17 00:00:00 2001 From: FlyAntNotDown <461425614@qq.com> Date: Wed, 1 Jul 2026 19:29:22 +0800 Subject: [PATCH 05/24] feat: support read-only and read-write storage textures in rhi Mirror the buffer storage/rwStorage split for textures: storage* means read-only (SRV), rwStorage* means read-write (UAV). Add rwStorageTexture binding type, rwStorageBinding texture view type / usage bit, and rwStorage texture state; drop the unused StorageTextureAccess enum. Also fix the Vulkan storage-image descriptor layout (GENERAL instead of SHADER_READ_ONLY_OPTIMAL) and give DX12 rw storage textures the ALLOW_UNORDERED_ACCESS resource flag. --- .../Include/RHI/DirectX12/Common.h | 4 +++- Engine/Source/RHI-DirectX12/Src/BindGroup.cpp | 3 ++- Engine/Source/RHI-DirectX12/Src/TextureView.cpp | 4 ++-- .../RHI-Vulkan/Include/RHI/Vulkan/Common.h | 2 ++ Engine/Source/RHI-Vulkan/Src/BindGroup.cpp | 11 ++++++++--- .../Source/RHI-Vulkan/Src/CommandRecorder.cpp | 6 +++++- Engine/Source/RHI/Include/RHI/Common.h | 15 +++++++-------- .../Source/Render/Include/Render/RenderGraph.h | 1 + Engine/Source/Render/Src/RenderGraph.cpp | 17 ++++++++++++++++- Engine/Source/Render/Src/ShaderCompiler.cpp | 5 +++-- Engine/Source/Render/Test/ResourcePoolTest.cpp | 2 +- 11 files changed, 50 insertions(+), 20 deletions(-) diff --git a/Engine/Source/RHI-DirectX12/Include/RHI/DirectX12/Common.h b/Engine/Source/RHI-DirectX12/Include/RHI/DirectX12/Common.h index 5e468fe6..e1fa52e2 100644 --- a/Engine/Source/RHI-DirectX12/Include/RHI/DirectX12/Common.h +++ b/Engine/Source/RHI-DirectX12/Include/RHI/DirectX12/Common.h @@ -267,7 +267,8 @@ namespace RHI::DirectX12 { ECIMPL_ITEM(TextureState::copyDst, D3D12_RESOURCE_STATE_COPY_DEST) ECIMPL_ITEM(TextureState::shaderReadOnly, D3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCE | D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE) ECIMPL_ITEM(TextureState::renderTarget, D3D12_RESOURCE_STATE_RENDER_TARGET) - ECIMPL_ITEM(TextureState::storage, D3D12_RESOURCE_STATE_UNORDERED_ACCESS) + ECIMPL_ITEM(TextureState::storage, D3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCE | D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE) + ECIMPL_ITEM(TextureState::rwStorage, D3D12_RESOURCE_STATE_UNORDERED_ACCESS) ECIMPL_ITEM(TextureState::depthStencilReadonly, D3D12_RESOURCE_STATE_DEPTH_READ) ECIMPL_ITEM(TextureState::depthStencilWrite, D3D12_RESOURCE_STATE_DEPTH_WRITE) ECIMPL_ITEM(TextureState::present, D3D12_RESOURCE_STATE_PRESENT) @@ -295,6 +296,7 @@ namespace RHI::DirectX12 { FCIMPL_ITEM(TextureUsageBits::copyDst, D3D12_RESOURCE_FLAG_NONE) FCIMPL_ITEM(TextureUsageBits::textureBinding, D3D12_RESOURCE_FLAG_NONE) FCIMPL_ITEM(TextureUsageBits::storageBinding, D3D12_RESOURCE_FLAG_NONE) + FCIMPL_ITEM(TextureUsageBits::rwStorageBinding, D3D12_RESOURCE_FLAG_ALLOW_UNORDERED_ACCESS) FCIMPL_ITEM(TextureUsageBits::renderAttachment, D3D12_RESOURCE_FLAG_ALLOW_RENDER_TARGET) FCIMPL_ITEM(TextureUsageBits::depthStencilAttachment, D3D12_RESOURCE_FLAG_ALLOW_DEPTH_STENCIL) FCIMPL_END(D3D12_RESOURCE_FLAGS) diff --git a/Engine/Source/RHI-DirectX12/Src/BindGroup.cpp b/Engine/Source/RHI-DirectX12/Src/BindGroup.cpp index 8985bcdc..84438414 100644 --- a/Engine/Source/RHI-DirectX12/Src/BindGroup.cpp +++ b/Engine/Source/RHI-DirectX12/Src/BindGroup.cpp @@ -19,7 +19,8 @@ namespace RHI::DirectX12 { return bufferView->GetNativeCpuDescriptorHandle(); } if (entry.binding.type == BindingType::texture || - entry.binding.type == BindingType::storageTexture) { + entry.binding.type == BindingType::storageTexture || + entry.binding.type == BindingType::rwStorageTexture) { const auto* textureView = static_cast(std::get(entry.entity)); return textureView->GetNativeCpuDescriptorHandle(); } diff --git a/Engine/Source/RHI-DirectX12/Src/TextureView.cpp b/Engine/Source/RHI-DirectX12/Src/TextureView.cpp index 6f83a457..164dec0c 100644 --- a/Engine/Source/RHI-DirectX12/Src/TextureView.cpp +++ b/Engine/Source/RHI-DirectX12/Src/TextureView.cpp @@ -11,12 +11,12 @@ namespace RHI::DirectX12 { static bool IsShaderResource(const TextureViewType type) { - return (type == TextureViewType::textureBinding); + return (type == TextureViewType::textureBinding) || (type == TextureViewType::storageBinding); } static bool IsUnorderedAccess(const TextureViewType type) { - return (type == TextureViewType::storageBinding); + return (type == TextureViewType::rwStorageBinding); } static bool IsRenderTarget(const TextureViewType type) diff --git a/Engine/Source/RHI-Vulkan/Include/RHI/Vulkan/Common.h b/Engine/Source/RHI-Vulkan/Include/RHI/Vulkan/Common.h index 76344dfc..3d4691d1 100644 --- a/Engine/Source/RHI-Vulkan/Include/RHI/Vulkan/Common.h +++ b/Engine/Source/RHI-Vulkan/Include/RHI/Vulkan/Common.h @@ -193,6 +193,7 @@ namespace RHI::Vulkan { ECIMPL_ITEM(BindingType::sampler, VK_DESCRIPTOR_TYPE_SAMPLER) ECIMPL_ITEM(BindingType::texture, VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE) ECIMPL_ITEM(BindingType::storageTexture, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE) + ECIMPL_ITEM(BindingType::rwStorageTexture, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE) ECIMPL_END(VkDescriptorType) ECIMPL_BEGIN(AddressMode, VkSamplerAddressMode) @@ -302,6 +303,7 @@ namespace RHI::Vulkan { FCIMPL_ITEM(TextureUsageBits::copyDst, VK_IMAGE_USAGE_TRANSFER_DST_BIT) FCIMPL_ITEM(TextureUsageBits::textureBinding, VK_IMAGE_USAGE_SAMPLED_BIT) FCIMPL_ITEM(TextureUsageBits::storageBinding, VK_IMAGE_USAGE_STORAGE_BIT) + FCIMPL_ITEM(TextureUsageBits::rwStorageBinding, VK_IMAGE_USAGE_STORAGE_BIT) FCIMPL_ITEM(TextureUsageBits::renderAttachment, VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT) FCIMPL_ITEM(TextureUsageBits::depthStencilAttachment, VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT) FCIMPL_END(VkImageUsageFlagBits) diff --git a/Engine/Source/RHI-Vulkan/Src/BindGroup.cpp b/Engine/Source/RHI-Vulkan/Src/BindGroup.cpp index a0d24505..008b9792 100644 --- a/Engine/Source/RHI-Vulkan/Src/BindGroup.cpp +++ b/Engine/Source/RHI-Vulkan/Src/BindGroup.cpp @@ -87,7 +87,8 @@ namespace RHI::Vulkan { bufferInfosNum++; } else if (entry.binding.type == BindingType::sampler || entry.binding.type == BindingType::texture - ||entry.binding.type == BindingType::storageTexture) { + || entry.binding.type == BindingType::storageTexture + || entry.binding.type == BindingType::rwStorageTexture) { imageInfosNum++; } } @@ -121,11 +122,15 @@ namespace RHI::Vulkan { imageInfos.back().sampler = sampler->GetNative(); descriptorWrites[i].pImageInfo = &imageInfos.back(); - } else if (entry.binding.type == BindingType::texture || entry.binding.type == BindingType::storageTexture) { + } else if (entry.binding.type == BindingType::texture + || entry.binding.type == BindingType::storageTexture + || entry.binding.type == BindingType::rwStorageTexture) { const auto* textureView = static_cast(std::get(entry.entity)); imageInfos.emplace_back(); - imageInfos.back().imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; + imageInfos.back().imageLayout = entry.binding.type == BindingType::texture + ? VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL + : VK_IMAGE_LAYOUT_GENERAL; imageInfos.back().imageView = textureView->GetNative(); descriptorWrites[i].pImageInfo = &imageInfos.back(); diff --git a/Engine/Source/RHI-Vulkan/Src/CommandRecorder.cpp b/Engine/Source/RHI-Vulkan/Src/CommandRecorder.cpp index d5099531..34082ca9 100644 --- a/Engine/Source/RHI-Vulkan/Src/CommandRecorder.cpp +++ b/Engine/Source/RHI-Vulkan/Src/CommandRecorder.cpp @@ -72,7 +72,8 @@ namespace RHI::Vulkan { { TextureState::copyDst, VK_ACCESS_TRANSFER_WRITE_BIT }, { TextureState::shaderReadOnly, VK_ACCESS_SHADER_READ_BIT }, { TextureState::renderTarget, VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT }, - { TextureState::storage, VK_ACCESS_SHADER_WRITE_BIT }, + { TextureState::storage, VK_ACCESS_SHADER_READ_BIT }, + { TextureState::rwStorage, VK_ACCESS_SHADER_WRITE_BIT }, { TextureState::depthStencilReadonly, VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_READ_BIT }, { TextureState::depthStencilWrite, VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT }, { TextureState::present, VK_ACCESS_MEMORY_READ_BIT } @@ -89,6 +90,7 @@ namespace RHI::Vulkan { { TextureState::shaderReadOnly, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT | VK_PIPELINE_STAGE_ALL_GRAPHICS_BIT }, { TextureState::renderTarget, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT }, { TextureState::storage, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT | VK_PIPELINE_STAGE_ALL_GRAPHICS_BIT }, + { TextureState::rwStorage, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT | VK_PIPELINE_STAGE_ALL_GRAPHICS_BIT }, { TextureState::depthStencilReadonly, VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT | VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT }, { TextureState::depthStencilWrite, VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT | VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT }, { TextureState::present, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT } @@ -105,6 +107,7 @@ namespace RHI::Vulkan { { TextureState::shaderReadOnly, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT | VK_PIPELINE_STAGE_ALL_GRAPHICS_BIT }, { TextureState::renderTarget, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT }, { TextureState::storage, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT | VK_PIPELINE_STAGE_ALL_GRAPHICS_BIT }, + { TextureState::rwStorage, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT | VK_PIPELINE_STAGE_ALL_GRAPHICS_BIT }, { TextureState::depthStencilReadonly, VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT | VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT }, { TextureState::depthStencilWrite, VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT | VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT }, { TextureState::present, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT } @@ -121,6 +124,7 @@ namespace RHI::Vulkan { { TextureState::shaderReadOnly, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL }, { TextureState::renderTarget, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL }, { TextureState::storage, VK_IMAGE_LAYOUT_GENERAL }, + { TextureState::rwStorage, VK_IMAGE_LAYOUT_GENERAL }, { TextureState::depthStencilReadonly, VK_IMAGE_LAYOUT_DEPTH_STENCIL_READ_ONLY_OPTIMAL }, { TextureState::depthStencilWrite, VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL }, { TextureState::present, VK_IMAGE_LAYOUT_PRESENT_SRC_KHR } diff --git a/Engine/Source/RHI/Include/RHI/Common.h b/Engine/Source/RHI/Include/RHI/Common.h index 877b59f3..6ac01f92 100644 --- a/Engine/Source/RHI/Include/RHI/Common.h +++ b/Engine/Source/RHI/Include/RHI/Common.h @@ -193,6 +193,7 @@ namespace RHI { enum class TextureViewType : uint8_t { textureBinding, storageBinding, + rwStorageBinding, colorAttachment, depthStencil, max @@ -247,6 +248,7 @@ namespace RHI { sampler, texture, storageTexture, + rwStorageTexture, max }; @@ -266,11 +268,6 @@ namespace RHI { max }; - enum class StorageTextureAccess : uint8_t { - writeOnly, - max - }; - enum class VertexStepMode : uint8_t { perVertex, perInstance, @@ -403,6 +400,7 @@ namespace RHI { shaderReadOnly, renderTarget, storage, + rwStorage, depthStencilReadonly, depthStencilWrite, present, @@ -439,9 +437,10 @@ namespace RHI { copyDst = 0x2, textureBinding = 0x4, storageBinding = 0x8, - renderAttachment = 0x10, - depthStencilAttachment = 0x20, - max = 0x40 + rwStorageBinding = 0x10, + renderAttachment = 0x20, + depthStencilAttachment = 0x40, + max = 0x80 }; using TextureUsageFlags = Common::Flags; DECLARE_FLAG_BITS_OP(TextureUsageFlags, TextureUsageBits) diff --git a/Engine/Source/Render/Include/Render/RenderGraph.h b/Engine/Source/Render/Include/Render/RenderGraph.h index 7a799ace..2aa5ae91 100644 --- a/Engine/Source/Render/Include/Render/RenderGraph.h +++ b/Engine/Source/Render/Include/Render/RenderGraph.h @@ -218,6 +218,7 @@ namespace Render { RGBindGroupDesc& RwStorageBuffer(std::string inName, RGBufferViewRef bufferView); RGBindGroupDesc& Texture(std::string inName, RGTextureViewRef textureView); RGBindGroupDesc& StorageTexture(std::string inName, RGTextureViewRef textureView); + RGBindGroupDesc& RwStorageTexture(std::string inName, RGTextureViewRef textureView); }; class RGBindGroup { diff --git a/Engine/Source/Render/Src/RenderGraph.cpp b/Engine/Source/Render/Src/RenderGraph.cpp index 247777cf..fd1ee17c 100644 --- a/Engine/Source/Render/Src/RenderGraph.cpp +++ b/Engine/Source/Render/Src/RenderGraph.cpp @@ -21,6 +21,8 @@ namespace Render::Internal { } else if (type == RHI::BindingType::texture) { outReads.emplace(std::get(view)->GetResource()); } else if (type == RHI::BindingType::storageTexture) { + outReads.emplace(std::get(view)->GetResource()); + } else if (type == RHI::BindingType::rwStorageTexture) { outWrites.emplace(std::get(view)->GetResource()); } else if (type == RHI::BindingType::sampler){ return; @@ -311,6 +313,17 @@ namespace Render { return *this; } + RGBindGroupDesc& RGBindGroupDesc::RwStorageTexture(std::string inName, RGTextureViewRef textureView) + { + Assert(!items.contains(inName)); + + RGBindItemDesc item; + item.type = RHI::BindingType::rwStorageTexture; + item.view = textureView; + items.emplace(std::make_pair(std::move(inName), item)); + return *this; + } + RGBindGroup::RGBindGroup(RGBindGroupDesc inDesc) : desc(std::move(inDesc)) { @@ -950,7 +963,7 @@ namespace Render { devirtualizedResourceViews.emplace(std::make_pair(bufferView, ResourceViewCache::Get(device).GetOrCreate(GetRHI(bufferView->GetBuffer()), bufferView->desc))); } createInfo.AddEntry(RHI::BindGroupEntry(*binding, GetRHI(bufferView))); - } else if (item.type == RHI::BindingType::texture || item.type == RHI::BindingType::storageTexture) { + } else if (item.type == RHI::BindingType::texture || item.type == RHI::BindingType::storageTexture || item.type == RHI::BindingType::rwStorageTexture) { auto* textureView = std::get(item.view); if (!devirtualizedResourceViews.contains(textureView)) { devirtualizedResourceViews.emplace(std::make_pair(textureView, ResourceViewCache::Get(device).GetOrCreate(GetRHI(textureView->GetTexture()), textureView->desc))); @@ -1053,6 +1066,8 @@ namespace Render { TransitionTexture(inRecoder, std::get(view)->GetTexture(), RHI::TextureState::shaderReadOnly); } else if (type == RHI::BindingType::storageTexture) { TransitionTexture(inRecoder, std::get(view)->GetTexture(), RHI::TextureState::storage); + } else if (type == RHI::BindingType::rwStorageTexture) { + TransitionTexture(inRecoder, std::get(view)->GetTexture(), RHI::TextureState::rwStorage); } else if (type == RHI::BindingType::sampler) {} else { Unimplement(); } diff --git a/Engine/Source/Render/Src/ShaderCompiler.cpp b/Engine/Source/Render/Src/ShaderCompiler.cpp index 4ea7e989..eb43101a 100644 --- a/Engine/Source/Render/Src/ShaderCompiler.cpp +++ b/Engine/Source/Render/Src/ShaderCompiler.cpp @@ -61,7 +61,7 @@ namespace Render { { D3D_SIT_CBUFFER, RHI::BindingType::uniformBuffer }, { D3D_SIT_TEXTURE, RHI::BindingType::texture }, { D3D_SIT_SAMPLER, RHI::BindingType::sampler }, - { D3D_SIT_UAV_RWTYPED, RHI::BindingType::storageTexture }, + { D3D_SIT_UAV_RWTYPED, RHI::BindingType::rwStorageTexture }, { D3D_SIT_STRUCTURED, RHI::BindingType::storageBuffer }, { D3D_SIT_UAV_RWSTRUCTURED, RHI::BindingType::rwStorageBuffer } }; @@ -242,7 +242,8 @@ namespace Render { resourceBindings.emplace_back(&buffer, RHI::BindingType::storageBuffer); } for (const spirv_cross::Resource& image : shaderResources.storage_images) { - resourceBindings.emplace_back(&image, RHI::BindingType::storageTexture); + const bool readOnly = compiler.has_decoration(image.id, spv::DecorationNonWritable); + resourceBindings.emplace_back(&image, readOnly ? RHI::BindingType::storageTexture : RHI::BindingType::rwStorageTexture); } for (const auto& iter : resourceBindings) { // NOLINT diff --git a/Engine/Source/Render/Test/ResourcePoolTest.cpp b/Engine/Source/Render/Test/ResourcePoolTest.cpp index 471d6cfb..627571ae 100644 --- a/Engine/Source/Render/Test/ResourcePoolTest.cpp +++ b/Engine/Source/Render/Test/ResourcePoolTest.cpp @@ -33,7 +33,7 @@ TEST_F(ResourcePoolTest, BasicTest) textureDesc.height = 1080; textureDesc.depthOrArraySize = 1; textureDesc.format = RHI::PixelFormat::rgba8Unorm; - textureDesc.usages = RHI::TextureUsageBits::renderAttachment | RHI::TextureUsageBits::storageBinding; + textureDesc.usages = RHI::TextureUsageBits::renderAttachment | RHI::TextureUsageBits::rwStorageBinding; textureDesc.mipLevels = 1; textureDesc.samples = 1; textureDesc.initialState = RHI::TextureState::undefined; From 4117f10aec0f5d864d76b8dc8db52bea4b6cc66f Mon Sep 17 00:00:00 2001 From: FlyAntNotDown <461425614@qq.com> Date: Thu, 2 Jul 2026 10:21:35 +0800 Subject: [PATCH 06/24] feat: support pipeline cache in rhi --- .../Include/RHI/DirectX12/Device.h | 1 + .../Include/RHI/DirectX12/PipelineCache.h | 34 +++++++++++ Engine/Source/RHI-DirectX12/Src/Device.cpp | 6 ++ Engine/Source/RHI-DirectX12/Src/Pipeline.cpp | 15 ++++- .../RHI-DirectX12/Src/PipelineCache.cpp | 56 +++++++++++++++++++ .../RHI-Dummy/Include/RHI/Dummy/Device.h | 1 + .../Include/RHI/Dummy/PipelineCache.h | 18 ++++++ Engine/Source/RHI-Dummy/Src/Device.cpp | 6 ++ Engine/Source/RHI-Dummy/Src/PipelineCache.cpp | 19 +++++++ .../RHI-Vulkan/Include/RHI/Vulkan/Device.h | 1 + .../Include/RHI/Vulkan/PipelineCache.h | 29 ++++++++++ Engine/Source/RHI-Vulkan/Src/Device.cpp | 6 ++ Engine/Source/RHI-Vulkan/Src/Pipeline.cpp | 11 +++- .../Source/RHI-Vulkan/Src/PipelineCache.cpp | 56 +++++++++++++++++++ Engine/Source/RHI/Include/RHI/Device.h | 3 + Engine/Source/RHI/Include/RHI/Pipeline.h | 5 ++ Engine/Source/RHI/Include/RHI/PipelineCache.h | 38 +++++++++++++ Engine/Source/RHI/Include/RHI/RHI.h | 1 + Engine/Source/RHI/Src/Pipeline.cpp | 14 +++++ Engine/Source/RHI/Src/PipelineCache.cpp | 51 +++++++++++++++++ 20 files changed, 367 insertions(+), 4 deletions(-) create mode 100644 Engine/Source/RHI-DirectX12/Include/RHI/DirectX12/PipelineCache.h create mode 100644 Engine/Source/RHI-DirectX12/Src/PipelineCache.cpp create mode 100644 Engine/Source/RHI-Dummy/Include/RHI/Dummy/PipelineCache.h create mode 100644 Engine/Source/RHI-Dummy/Src/PipelineCache.cpp create mode 100644 Engine/Source/RHI-Vulkan/Include/RHI/Vulkan/PipelineCache.h create mode 100644 Engine/Source/RHI-Vulkan/Src/PipelineCache.cpp create mode 100644 Engine/Source/RHI/Include/RHI/PipelineCache.h create mode 100644 Engine/Source/RHI/Src/PipelineCache.cpp diff --git a/Engine/Source/RHI-DirectX12/Include/RHI/DirectX12/Device.h b/Engine/Source/RHI-DirectX12/Include/RHI/DirectX12/Device.h index bdf13206..a224ecff 100644 --- a/Engine/Source/RHI-DirectX12/Include/RHI/DirectX12/Device.h +++ b/Engine/Source/RHI-DirectX12/Include/RHI/DirectX12/Device.h @@ -85,6 +85,7 @@ namespace RHI::DirectX12 { Common::UniquePtr CreateBindGroup(const BindGroupCreateInfo& inCreateInfo) override; Common::UniquePtr CreatePipelineLayout(const PipelineLayoutCreateInfo& inCreateInfo) override; Common::UniquePtr CreateShaderModule(const ShaderModuleCreateInfo& inCreateInfo) override; + Common::UniquePtr CreatePipelineCache(const PipelineCacheCreateInfo& inCreateInfo) override; Common::UniquePtr CreateComputePipeline(const ComputePipelineCreateInfo& inCreateInfo) override; Common::UniquePtr CreateRasterPipeline(const RasterPipelineCreateInfo& inCreateInfo) override; Common::UniquePtr CreateCommandBuffer() override; diff --git a/Engine/Source/RHI-DirectX12/Include/RHI/DirectX12/PipelineCache.h b/Engine/Source/RHI-DirectX12/Include/RHI/DirectX12/PipelineCache.h new file mode 100644 index 00000000..6ae57793 --- /dev/null +++ b/Engine/Source/RHI-DirectX12/Include/RHI/DirectX12/PipelineCache.h @@ -0,0 +1,34 @@ +// +// Created by johnk on 2026/7/2. +// + +#pragma once + +#include + +#include +#include + +#include + +using Microsoft::WRL::ComPtr; + +namespace RHI::DirectX12 { + class DX12Device; + + class DX12PipelineCache final : public PipelineCache { + public: + NonCopyable(DX12PipelineCache) + DX12PipelineCache(DX12Device& inDevice, const PipelineCacheCreateInfo& inCreateInfo); + ~DX12PipelineCache() override; + + std::vector GetData() override; + ID3D12PipelineLibrary* GetNative() const; + + private: + void CreateNativePipelineLibrary(DX12Device& inDevice, const PipelineCacheCreateInfo& inCreateInfo); + + std::vector initialData; + ComPtr nativePipelineLibrary; + }; +} diff --git a/Engine/Source/RHI-DirectX12/Src/Device.cpp b/Engine/Source/RHI-DirectX12/Src/Device.cpp index cf3fa51b..fdddc915 100644 --- a/Engine/Source/RHI-DirectX12/Src/Device.cpp +++ b/Engine/Source/RHI-DirectX12/Src/Device.cpp @@ -25,6 +25,7 @@ #include #include #include +#include #include #include @@ -259,6 +260,11 @@ namespace RHI::DirectX12 { return { new DX12QuerySet(*this, inCreateInfo) }; } + Common::UniquePtr DX12Device::CreatePipelineCache(const PipelineCacheCreateInfo& inCreateInfo) + { + return { new DX12PipelineCache(*this, inCreateInfo) }; + } + bool DX12Device::CheckSwapChainFormatSupport(Surface* inSurface, PixelFormat inFormat) { static std::unordered_set supportedFormats = { diff --git a/Engine/Source/RHI-DirectX12/Src/Pipeline.cpp b/Engine/Source/RHI-DirectX12/Src/Pipeline.cpp index 43083881..cee31a71 100644 --- a/Engine/Source/RHI-DirectX12/Src/Pipeline.cpp +++ b/Engine/Source/RHI-DirectX12/Src/Pipeline.cpp @@ -10,6 +10,7 @@ #include #include #include +#include namespace RHI::DirectX12 { D3D12_RENDER_TARGET_BLEND_DESC GetDX12RenderTargetBlendDesc(const ColorTargetState& colorTargetState) @@ -224,8 +225,18 @@ namespace RHI::DirectX12 { auto inputElements = GetDX12InputElements(inCreateInfo); UpdateDX12InputLayoutDesc(desc, inputElements); - bool success = SUCCEEDED(inDevice.GetNative()->CreateGraphicsPipelineState(&desc, IID_PPV_ARGS(&nativePipelineState))); - Assert(success); + auto* pipelineCache = static_cast(inCreateInfo.pipelineCache); + ID3D12PipelineLibrary* nativePipelineLibrary = pipelineCache != nullptr ? pipelineCache->GetNative() : nullptr; + const bool cacheable = nativePipelineLibrary != nullptr && !inCreateInfo.debugName.empty(); + const std::wstring cacheKey = cacheable ? Common::StringUtils::ToWideString(inCreateInfo.debugName) : std::wstring {}; + + bool loaded = cacheable && SUCCEEDED(nativePipelineLibrary->LoadGraphicsPipeline(cacheKey.c_str(), &desc, IID_PPV_ARGS(&nativePipelineState))); + if (!loaded) { + Assert(SUCCEEDED(inDevice.GetNative()->CreateGraphicsPipelineState(&desc, IID_PPV_ARGS(&nativePipelineState)))); + if (cacheable) { + nativePipelineLibrary->StorePipeline(cacheKey.c_str(), nativePipelineState.Get()); + } + } #if BUILD_CONFIG_DEBUG if (!inCreateInfo.debugName.empty()) { diff --git a/Engine/Source/RHI-DirectX12/Src/PipelineCache.cpp b/Engine/Source/RHI-DirectX12/Src/PipelineCache.cpp new file mode 100644 index 00000000..140d2019 --- /dev/null +++ b/Engine/Source/RHI-DirectX12/Src/PipelineCache.cpp @@ -0,0 +1,56 @@ +// +// Created by johnk on 2026/7/2. +// + +#include + +#include +#include +#include + +namespace RHI::DirectX12 { + DX12PipelineCache::DX12PipelineCache(DX12Device& inDevice, const PipelineCacheCreateInfo& inCreateInfo) + : PipelineCache(inCreateInfo) + { + CreateNativePipelineLibrary(inDevice, inCreateInfo); + } + + DX12PipelineCache::~DX12PipelineCache() = default; + + std::vector DX12PipelineCache::GetData() + { + if (nativePipelineLibrary == nullptr) { + return {}; + } + + const SIZE_T size = nativePipelineLibrary->GetSerializedSize(); + std::vector result(size); + Assert(SUCCEEDED(nativePipelineLibrary->Serialize(result.data(), size))); + return result; + } + + ID3D12PipelineLibrary* DX12PipelineCache::GetNative() const + { + return nativePipelineLibrary.Get(); + } + + void DX12PipelineCache::CreateNativePipelineLibrary(DX12Device& inDevice, const PipelineCacheCreateInfo& inCreateInfo) + { + ComPtr nativeDevice1; + if (FAILED(inDevice.GetNative()->QueryInterface(IID_PPV_ARGS(&nativeDevice1)))) { + return; + } + + // CreatePipelineLibrary does not copy the blob, so it must outlive the library object. + if (inCreateInfo.initialDataSize > 0) { + initialData.resize(inCreateInfo.initialDataSize); + memcpy(initialData.data(), inCreateInfo.initialData, inCreateInfo.initialDataSize); + } + + // A stale or driver-incompatible blob makes CreatePipelineLibrary fail; fall back to an empty library then. + if (FAILED(nativeDevice1->CreatePipelineLibrary(initialData.data(), initialData.size(), IID_PPV_ARGS(&nativePipelineLibrary)))) { + initialData.clear(); + Assert(SUCCEEDED(nativeDevice1->CreatePipelineLibrary(nullptr, 0, IID_PPV_ARGS(&nativePipelineLibrary)))); + } + } +} diff --git a/Engine/Source/RHI-Dummy/Include/RHI/Dummy/Device.h b/Engine/Source/RHI-Dummy/Include/RHI/Dummy/Device.h index fa686219..6917291b 100644 --- a/Engine/Source/RHI-Dummy/Include/RHI/Dummy/Device.h +++ b/Engine/Source/RHI-Dummy/Include/RHI/Dummy/Device.h @@ -28,6 +28,7 @@ namespace RHI::Dummy { Common::UniquePtr CreateBindGroup(const BindGroupCreateInfo& createInfo) override; Common::UniquePtr CreatePipelineLayout(const PipelineLayoutCreateInfo& createInfo) override; Common::UniquePtr CreateShaderModule(const ShaderModuleCreateInfo& createInfo) override; + Common::UniquePtr CreatePipelineCache(const PipelineCacheCreateInfo& createInfo) override; Common::UniquePtr CreateComputePipeline(const ComputePipelineCreateInfo& createInfo) override; Common::UniquePtr CreateRasterPipeline(const RasterPipelineCreateInfo& createInfo) override; Common::UniquePtr CreateCommandBuffer() override; diff --git a/Engine/Source/RHI-Dummy/Include/RHI/Dummy/PipelineCache.h b/Engine/Source/RHI-Dummy/Include/RHI/Dummy/PipelineCache.h new file mode 100644 index 00000000..7286fa12 --- /dev/null +++ b/Engine/Source/RHI-Dummy/Include/RHI/Dummy/PipelineCache.h @@ -0,0 +1,18 @@ +// +// Created by johnk on 2026/7/2. +// + +#pragma once + +#include + +namespace RHI::Dummy { + class DummyPipelineCache final : public PipelineCache { + public: + NonCopyable(DummyPipelineCache) + explicit DummyPipelineCache(const PipelineCacheCreateInfo& createInfo); + ~DummyPipelineCache() override; + + std::vector GetData() override; + }; +} diff --git a/Engine/Source/RHI-Dummy/Src/Device.cpp b/Engine/Source/RHI-Dummy/Src/Device.cpp index 7caa2b3c..4a240c6e 100644 --- a/Engine/Source/RHI-Dummy/Src/Device.cpp +++ b/Engine/Source/RHI-Dummy/Src/Device.cpp @@ -17,6 +17,7 @@ #include #include #include +#include #include namespace RHI::Dummy { @@ -120,6 +121,11 @@ namespace RHI::Dummy { return { new DummyQuerySet(createInfo) }; } + Common::UniquePtr DummyDevice::CreatePipelineCache(const PipelineCacheCreateInfo& createInfo) + { + return { new DummyPipelineCache(createInfo) }; + } + bool DummyDevice::CheckSwapChainFormatSupport(Surface* surface, PixelFormat format) { return true; diff --git a/Engine/Source/RHI-Dummy/Src/PipelineCache.cpp b/Engine/Source/RHI-Dummy/Src/PipelineCache.cpp new file mode 100644 index 00000000..b4b4f506 --- /dev/null +++ b/Engine/Source/RHI-Dummy/Src/PipelineCache.cpp @@ -0,0 +1,19 @@ +// +// Created by johnk on 2026/7/2. +// + +#include + +namespace RHI::Dummy { + DummyPipelineCache::DummyPipelineCache(const PipelineCacheCreateInfo& createInfo) + : PipelineCache(createInfo) + { + } + + DummyPipelineCache::~DummyPipelineCache() = default; + + std::vector DummyPipelineCache::GetData() + { + return {}; + } +} diff --git a/Engine/Source/RHI-Vulkan/Include/RHI/Vulkan/Device.h b/Engine/Source/RHI-Vulkan/Include/RHI/Vulkan/Device.h index 26c5b837..c52c5e11 100644 --- a/Engine/Source/RHI-Vulkan/Include/RHI/Vulkan/Device.h +++ b/Engine/Source/RHI-Vulkan/Include/RHI/Vulkan/Device.h @@ -35,6 +35,7 @@ namespace RHI::Vulkan { Common::UniquePtr CreateBindGroup(const BindGroupCreateInfo& inCreateInfo) override; Common::UniquePtr CreatePipelineLayout(const PipelineLayoutCreateInfo& inCreateInfo) override; Common::UniquePtr CreateShaderModule(const ShaderModuleCreateInfo& inCreateInfo) override; + Common::UniquePtr CreatePipelineCache(const PipelineCacheCreateInfo& inCreateInfo) override; Common::UniquePtr CreateComputePipeline(const ComputePipelineCreateInfo& inCreateInfo) override; Common::UniquePtr CreateRasterPipeline(const RasterPipelineCreateInfo& inCreateInfo) override; Common::UniquePtr CreateCommandBuffer() override; diff --git a/Engine/Source/RHI-Vulkan/Include/RHI/Vulkan/PipelineCache.h b/Engine/Source/RHI-Vulkan/Include/RHI/Vulkan/PipelineCache.h new file mode 100644 index 00000000..0f05c1dc --- /dev/null +++ b/Engine/Source/RHI-Vulkan/Include/RHI/Vulkan/PipelineCache.h @@ -0,0 +1,29 @@ +// +// Created by johnk on 2026/7/2. +// + +#pragma once + +#include + +#include + +namespace RHI::Vulkan { + class VulkanDevice; + + class VulkanPipelineCache final : public PipelineCache { + public: + NonCopyable(VulkanPipelineCache) + VulkanPipelineCache(VulkanDevice& inDevice, const PipelineCacheCreateInfo& inCreateInfo); + ~VulkanPipelineCache() override; + + std::vector GetData() override; + VkPipelineCache GetNative() const; + + private: + void CreateNativePipelineCache(const PipelineCacheCreateInfo& inCreateInfo); + + VulkanDevice& device; + VkPipelineCache nativePipelineCache; + }; +} diff --git a/Engine/Source/RHI-Vulkan/Src/Device.cpp b/Engine/Source/RHI-Vulkan/Src/Device.cpp index 20b6260c..945633aa 100644 --- a/Engine/Source/RHI-Vulkan/Src/Device.cpp +++ b/Engine/Source/RHI-Vulkan/Src/Device.cpp @@ -23,6 +23,7 @@ #include #include #include +#include namespace RHI::Vulkan { const std::vector requiredExtensions = { @@ -153,6 +154,11 @@ namespace RHI::Vulkan { return { new VulkanQuerySet(*this, inCreateInfo) }; } + Common::UniquePtr VulkanDevice::CreatePipelineCache(const PipelineCacheCreateInfo& inCreateInfo) + { + return { new VulkanPipelineCache(*this, inCreateInfo) }; + } + bool VulkanDevice::CheckSwapChainFormatSupport(Surface* inSurface, const PixelFormat inFormat) { const auto* vkSurface = static_cast(inSurface); diff --git a/Engine/Source/RHI-Vulkan/Src/Pipeline.cpp b/Engine/Source/RHI-Vulkan/Src/Pipeline.cpp index a1536a5c..b7704a8d 100644 --- a/Engine/Source/RHI-Vulkan/Src/Pipeline.cpp +++ b/Engine/Source/RHI-Vulkan/Src/Pipeline.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include namespace RHI::Vulkan { @@ -255,7 +256,10 @@ namespace RHI::Vulkan { pipelineCreateInfo.pVertexInputState = &vtxInput; pipelineCreateInfo.pNext = &pipelineRenderingCreateInfo; - Assert(vkCreateGraphicsPipelines(device.GetNative(), VK_NULL_HANDLE, 1, &pipelineCreateInfo, nullptr, &nativePipeline) == VK_SUCCESS); + const VkPipelineCache nativePipelineCache = inCreateInfo.pipelineCache != nullptr + ? static_cast(inCreateInfo.pipelineCache)->GetNative() + : VK_NULL_HANDLE; + Assert(vkCreateGraphicsPipelines(device.GetNative(), nativePipelineCache, 1, &pipelineCreateInfo, nullptr, &nativePipeline) == VK_SUCCESS); #if BUILD_CONFIG_DEBUG if (!inCreateInfo.debugName.empty()) { @@ -310,7 +314,10 @@ namespace RHI::Vulkan { pipelineInfo.layout = pipelineLayout->GetNative(); pipelineInfo.stage = stageInfo; - Assert(vkCreateComputePipelines(device.GetNative(), VK_NULL_HANDLE, 1, &pipelineInfo, nullptr, &nativePipeline) == VK_SUCCESS); + const VkPipelineCache nativePipelineCache = inCreateInfo.pipelineCache != nullptr + ? static_cast(inCreateInfo.pipelineCache)->GetNative() + : VK_NULL_HANDLE; + Assert(vkCreateComputePipelines(device.GetNative(), nativePipelineCache, 1, &pipelineInfo, nullptr, &nativePipeline) == VK_SUCCESS); } VkPipeline VulkanComputePipeline::GetNative() const diff --git a/Engine/Source/RHI-Vulkan/Src/PipelineCache.cpp b/Engine/Source/RHI-Vulkan/Src/PipelineCache.cpp new file mode 100644 index 00000000..366dbdb4 --- /dev/null +++ b/Engine/Source/RHI-Vulkan/Src/PipelineCache.cpp @@ -0,0 +1,56 @@ +// +// Created by johnk on 2026/7/2. +// + +#include +#include +#include +#include + +namespace RHI::Vulkan { + VulkanPipelineCache::VulkanPipelineCache(VulkanDevice& inDevice, const PipelineCacheCreateInfo& inCreateInfo) + : PipelineCache(inCreateInfo) + , device(inDevice) + , nativePipelineCache(VK_NULL_HANDLE) + { + CreateNativePipelineCache(inCreateInfo); + } + + VulkanPipelineCache::~VulkanPipelineCache() + { + if (nativePipelineCache != VK_NULL_HANDLE) { + vkDestroyPipelineCache(device.GetNative(), nativePipelineCache, nullptr); + } + } + + std::vector VulkanPipelineCache::GetData() + { + size_t size = 0; + Assert(vkGetPipelineCacheData(device.GetNative(), nativePipelineCache, &size, nullptr) == VK_SUCCESS); + + std::vector result(size); + Assert(vkGetPipelineCacheData(device.GetNative(), nativePipelineCache, &size, result.data()) == VK_SUCCESS); + return result; + } + + VkPipelineCache VulkanPipelineCache::GetNative() const + { + return nativePipelineCache; + } + + void VulkanPipelineCache::CreateNativePipelineCache(const PipelineCacheCreateInfo& inCreateInfo) + { + VkPipelineCacheCreateInfo cacheInfo = {}; + cacheInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_CACHE_CREATE_INFO; + cacheInfo.initialDataSize = inCreateInfo.initialDataSize; + cacheInfo.pInitialData = inCreateInfo.initialData; + + Assert(vkCreatePipelineCache(device.GetNative(), &cacheInfo, nullptr, &nativePipelineCache) == VK_SUCCESS); + +#if BUILD_CONFIG_DEBUG + if (!inCreateInfo.debugName.empty()) { + device.SetObjectName(VK_OBJECT_TYPE_PIPELINE_CACHE, reinterpret_cast(nativePipelineCache), inCreateInfo.debugName.c_str()); + } +#endif + } +} diff --git a/Engine/Source/RHI/Include/RHI/Device.h b/Engine/Source/RHI/Include/RHI/Device.h index fd174101..3a45c333 100644 --- a/Engine/Source/RHI/Include/RHI/Device.h +++ b/Engine/Source/RHI/Include/RHI/Device.h @@ -24,6 +24,7 @@ namespace RHI { struct ShaderModuleCreateInfo; struct ComputePipelineCreateInfo; struct RasterPipelineCreateInfo; + struct PipelineCacheCreateInfo; struct SwapChainCreateInfo; struct SurfaceCreateInfo; struct QuerySetCreateInfo; @@ -40,6 +41,7 @@ namespace RHI { class ShaderModule; class ComputePipeline; class RasterPipeline; + class PipelineCache; class CommandBuffer; class SwapChain; class Fence; @@ -77,6 +79,7 @@ namespace RHI { virtual Common::UniquePtr CreateBindGroup(const BindGroupCreateInfo& createInfo) = 0; virtual Common::UniquePtr CreatePipelineLayout(const PipelineLayoutCreateInfo& createInfo) = 0; virtual Common::UniquePtr CreateShaderModule(const ShaderModuleCreateInfo& createInfo) = 0; + virtual Common::UniquePtr CreatePipelineCache(const PipelineCacheCreateInfo& createInfo) = 0; virtual Common::UniquePtr CreateComputePipeline(const ComputePipelineCreateInfo& createInfo) = 0; virtual Common::UniquePtr CreateRasterPipeline(const RasterPipelineCreateInfo& createInfo) = 0; virtual Common::UniquePtr CreateCommandBuffer() = 0; diff --git a/Engine/Source/RHI/Include/RHI/Pipeline.h b/Engine/Source/RHI/Include/RHI/Pipeline.h index ca946d9b..574405ea 100644 --- a/Engine/Source/RHI/Include/RHI/Pipeline.h +++ b/Engine/Source/RHI/Include/RHI/Pipeline.h @@ -14,6 +14,7 @@ namespace RHI { class BindGroupLayout; class PipelineLayout; class ShaderModule; + class PipelineCache; struct HlslVertexBinding { std::string semanticName; @@ -219,10 +220,12 @@ namespace RHI { struct ComputePipelineCreateInfo { PipelineLayout* layout; ShaderModule* computeShader; + PipelineCache* pipelineCache; ComputePipelineCreateInfo(); ComputePipelineCreateInfo& SetLayout(PipelineLayout* inLayout); ComputePipelineCreateInfo& SetComputeShader(ShaderModule* inComputeShader); + ComputePipelineCreateInfo& SetPipelineCache(PipelineCache* inPipelineCache); }; struct RasterPipelineCreateInfo { @@ -240,6 +243,7 @@ namespace RHI { MultiSampleState multiSampleState; FragmentState fragmentState; + PipelineCache* pipelineCache; std::string debugName; explicit RasterPipelineCreateInfo(PipelineLayout* inLayout = nullptr); @@ -254,6 +258,7 @@ namespace RHI { RasterPipelineCreateInfo& SetDepthStencilState(const DepthStencilState& inDepthStencilState); RasterPipelineCreateInfo& SetMultiSampleState(const MultiSampleState& inMultiSampleState); RasterPipelineCreateInfo& SetFragmentState(const FragmentState& inFragmentState); + RasterPipelineCreateInfo& SetPipelineCache(PipelineCache* inPipelineCache); RasterPipelineCreateInfo& SetDebugName(std::string inDebugName); }; diff --git a/Engine/Source/RHI/Include/RHI/PipelineCache.h b/Engine/Source/RHI/Include/RHI/PipelineCache.h new file mode 100644 index 00000000..c65cc635 --- /dev/null +++ b/Engine/Source/RHI/Include/RHI/PipelineCache.h @@ -0,0 +1,38 @@ +// +// Created by johnk on 2026/7/2. +// + +#pragma once + +#include +#include +#include + +#include + +namespace RHI { + struct PipelineCacheCreateInfo { + const void* initialData; + size_t initialDataSize; + std::string debugName; + + PipelineCacheCreateInfo(); + explicit PipelineCacheCreateInfo(const void* inInitialData, size_t inInitialDataSize, std::string inDebugName = ""); + explicit PipelineCacheCreateInfo(const std::vector& inInitialData, std::string inDebugName = ""); + + PipelineCacheCreateInfo& SetInitialData(const void* inInitialData); + PipelineCacheCreateInfo& SetInitialDataSize(size_t inInitialDataSize); + PipelineCacheCreateInfo& SetDebugName(std::string inDebugName); + }; + + class PipelineCache { + public: + NonCopyable(PipelineCache) + virtual ~PipelineCache(); + + virtual std::vector GetData() = 0; + + protected: + explicit PipelineCache(const PipelineCacheCreateInfo& inCreateInfo); + }; +} diff --git a/Engine/Source/RHI/Include/RHI/RHI.h b/Engine/Source/RHI/Include/RHI/RHI.h index 9b1ce213..b5798d63 100644 --- a/Engine/Source/RHI/Include/RHI/RHI.h +++ b/Engine/Source/RHI/Include/RHI/RHI.h @@ -25,6 +25,7 @@ #include #include #include +#include #if PLATFORM_WINDOWS #undef CreateSemaphore diff --git a/Engine/Source/RHI/Src/Pipeline.cpp b/Engine/Source/RHI/Src/Pipeline.cpp index e5e09a16..b73d5356 100644 --- a/Engine/Source/RHI/Src/Pipeline.cpp +++ b/Engine/Source/RHI/Src/Pipeline.cpp @@ -304,6 +304,7 @@ namespace RHI { ComputePipelineCreateInfo::ComputePipelineCreateInfo() : layout(nullptr) , computeShader(nullptr) + , pipelineCache(nullptr) { } @@ -319,6 +320,12 @@ namespace RHI { return *this; } + ComputePipelineCreateInfo& ComputePipelineCreateInfo::SetPipelineCache(PipelineCache* inPipelineCache) + { + pipelineCache = inPipelineCache; + return *this; + } + RasterPipelineCreateInfo::RasterPipelineCreateInfo(PipelineLayout* inLayout) : layout(inLayout) , vertexShader(nullptr) @@ -326,6 +333,7 @@ namespace RHI { , geometryShader(nullptr) , domainShader(nullptr) , hullShader(nullptr) + , pipelineCache(nullptr) { } @@ -397,6 +405,12 @@ namespace RHI { return *this; } + RasterPipelineCreateInfo& RasterPipelineCreateInfo::SetPipelineCache(PipelineCache* inPipelineCache) + { + pipelineCache = inPipelineCache; + return *this; + } + RasterPipelineCreateInfo& RasterPipelineCreateInfo::SetDebugName(std::string inDebugName) { debugName = std::move(inDebugName); diff --git a/Engine/Source/RHI/Src/PipelineCache.cpp b/Engine/Source/RHI/Src/PipelineCache.cpp new file mode 100644 index 00000000..afdad1d4 --- /dev/null +++ b/Engine/Source/RHI/Src/PipelineCache.cpp @@ -0,0 +1,51 @@ +// +// Created by johnk on 2026/7/2. +// + +#include + +#include + +namespace RHI { + PipelineCacheCreateInfo::PipelineCacheCreateInfo() + : initialData(nullptr) + , initialDataSize(0) + { + } + + PipelineCacheCreateInfo::PipelineCacheCreateInfo(const void* inInitialData, const size_t inInitialDataSize, std::string inDebugName) + : initialData(inInitialData) + , initialDataSize(inInitialDataSize) + , debugName(std::move(inDebugName)) + { + } + + PipelineCacheCreateInfo::PipelineCacheCreateInfo(const std::vector& inInitialData, std::string inDebugName) + : initialData(inInitialData.data()) + , initialDataSize(inInitialData.size()) + , debugName(std::move(inDebugName)) + { + } + + PipelineCacheCreateInfo& PipelineCacheCreateInfo::SetInitialData(const void* inInitialData) + { + initialData = inInitialData; + return *this; + } + + PipelineCacheCreateInfo& PipelineCacheCreateInfo::SetInitialDataSize(const size_t inInitialDataSize) + { + initialDataSize = inInitialDataSize; + return *this; + } + + PipelineCacheCreateInfo& PipelineCacheCreateInfo::SetDebugName(std::string inDebugName) + { + debugName = std::move(inDebugName); + return *this; + } + + PipelineCache::PipelineCache(const PipelineCacheCreateInfo&) {} + + PipelineCache::~PipelineCache() = default; +} From 9b51da07211d5b0cfcc107601a47686ac22ec309 Mon Sep 17 00:00:00 2001 From: FlyAntNotDown <461425614@qq.com> Date: Thu, 2 Jul 2026 14:59:15 +0800 Subject: [PATCH 07/24] feat: support hdr color space control and fix swapchain present mode Add a ColorSpace enum (srgbNonLinear, hdr10St2084) and a colorSpace field on SwapChainCreateInfo, thread it through CheckSwapChainFormatSupport, and apply it per backend: Vulkan maps it to VkColorSpaceKHR (and enables the optional VK_EXT_swapchain_colorspace instance extension so HDR spaces get enumerated), DirectX12 maps it to DXGI_COLOR_SPACE_TYPE via SetColorSpace1. Also fix present-mode handling: expand PresentMode to immediately/vsync/ mailbox/fifoRelaxed, make Vulkan fall back to the guaranteed FIFO mode instead of asserting when the requested mode is unsupported, and give DirectX12 real tearing for immediately via ALLOW_TEARING so the mode means the same thing on both backends. --- Engine/Source/Launch/Src/GameViewport.cpp | 2 +- .../Include/RHI/DirectX12/Common.h | 5 +++ .../Include/RHI/DirectX12/Device.h | 2 +- .../Include/RHI/DirectX12/SwapChain.h | 1 + Engine/Source/RHI-DirectX12/Src/Device.cpp | 12 +++--- Engine/Source/RHI-DirectX12/Src/SwapChain.cpp | 37 +++++++++++++++++-- .../RHI-Dummy/Include/RHI/Dummy/Device.h | 2 +- Engine/Source/RHI-Dummy/Src/Device.cpp | 2 +- .../RHI-Vulkan/Include/RHI/Vulkan/Common.h | 8 +++- .../RHI-Vulkan/Include/RHI/Vulkan/Device.h | 2 +- .../RHI-Vulkan/Include/RHI/Vulkan/Instance.h | 1 + Engine/Source/RHI-Vulkan/Src/Device.cpp | 5 +-- Engine/Source/RHI-Vulkan/Src/Instance.cpp | 29 +++++++++++---- Engine/Source/RHI-Vulkan/Src/SwapChain.cpp | 17 ++++----- Engine/Source/RHI/Include/RHI/Common.h | 15 +++++--- Engine/Source/RHI/Include/RHI/Device.h | 2 +- Engine/Source/RHI/Include/RHI/SwapChain.h | 2 + Engine/Source/RHI/Src/SwapChain.cpp | 7 ++++ Sample/RHI-SSAO/SSAOApplication.cpp | 2 +- Sample/RHI-TexSampling/TexSampling.cpp | 2 +- Sample/RHI-Triangle/Triangle.cpp | 2 +- Sample/Rendering-BaseTexture/BaseTexture.cpp | 2 +- Sample/Rendering-SSAO/SSAOApplication.cpp | 2 +- Sample/Rendering-Triangle/Triangle.cpp | 2 +- 24 files changed, 116 insertions(+), 47 deletions(-) diff --git a/Engine/Source/Launch/Src/GameViewport.cpp b/Engine/Source/Launch/Src/GameViewport.cpp index 5d7ca22e..b84d1231 100644 --- a/Engine/Source/Launch/Src/GameViewport.cpp +++ b/Engine/Source/Launch/Src/GameViewport.cpp @@ -102,7 +102,7 @@ namespace Launch { std::optional pixelFormat = {}; for (const auto format : formatQualifiers) { - if (device->CheckSwapChainFormatSupport(surface.Get(), format)) { + if (device->CheckSwapChainFormatSupport(surface.Get(), format, RHI::ColorSpace::srgbNonLinear)) { pixelFormat = format; break; } diff --git a/Engine/Source/RHI-DirectX12/Include/RHI/DirectX12/Common.h b/Engine/Source/RHI-DirectX12/Include/RHI/DirectX12/Common.h index e1fa52e2..a2e49ae6 100644 --- a/Engine/Source/RHI-DirectX12/Include/RHI/DirectX12/Common.h +++ b/Engine/Source/RHI-DirectX12/Include/RHI/DirectX12/Common.h @@ -284,6 +284,11 @@ namespace RHI::DirectX12 { ECIMPL_ITEM(QueryType::timestamp, D3D12_QUERY_TYPE_TIMESTAMP) ECIMPL_END(D3D12_QUERY_TYPE) + ECIMPL_BEGIN(ColorSpace, DXGI_COLOR_SPACE_TYPE) + ECIMPL_ITEM(ColorSpace::srgbNonLinear, DXGI_COLOR_SPACE_RGB_FULL_G22_NONE_P709) + ECIMPL_ITEM(ColorSpace::hdr10St2084, DXGI_COLOR_SPACE_RGB_FULL_G2084_NONE_P2020) + ECIMPL_END(DXGI_COLOR_SPACE_TYPE) + FCIMPL_BEGIN(ColorWriteFlags, uint8_t) FCIMPL_ITEM(ColorWriteBits::red, D3D12_COLOR_WRITE_ENABLE_RED) FCIMPL_ITEM(ColorWriteBits::green, D3D12_COLOR_WRITE_ENABLE_GREEN) diff --git a/Engine/Source/RHI-DirectX12/Include/RHI/DirectX12/Device.h b/Engine/Source/RHI-DirectX12/Include/RHI/DirectX12/Device.h index a224ecff..010b3211 100644 --- a/Engine/Source/RHI-DirectX12/Include/RHI/DirectX12/Device.h +++ b/Engine/Source/RHI-DirectX12/Include/RHI/DirectX12/Device.h @@ -93,7 +93,7 @@ namespace RHI::DirectX12 { Common::UniquePtr CreateSemaphore() override; Common::UniquePtr CreateQuerySet(const QuerySetCreateInfo& inCreateInfo) override; - bool CheckSwapChainFormatSupport(Surface* inSurface, PixelFormat inFormat) override; + bool CheckSwapChainFormatSupport(Surface* inSurface, PixelFormat inFormat, ColorSpace inColorSpace) override; TextureSubResourceCopyFootprint GetTextureSubResourceCopyFootprint(const Texture& texture, const TextureSubResourceInfo& subResourceInfo) override; ID3D12Device* GetNative() const; diff --git a/Engine/Source/RHI-DirectX12/Include/RHI/DirectX12/SwapChain.h b/Engine/Source/RHI-DirectX12/Include/RHI/DirectX12/SwapChain.h index eb2a20d2..48d0d3af 100644 --- a/Engine/Source/RHI-DirectX12/Include/RHI/DirectX12/SwapChain.h +++ b/Engine/Source/RHI-DirectX12/Include/RHI/DirectX12/SwapChain.h @@ -33,6 +33,7 @@ namespace RHI::DirectX12 { DX12Queue& queue; uint8_t textureNum; PresentMode presentMode; + bool allowTearing; ComPtr nativeSwapChain; std::vector> textures; }; diff --git a/Engine/Source/RHI-DirectX12/Src/Device.cpp b/Engine/Source/RHI-DirectX12/Src/Device.cpp index fdddc915..28d05f67 100644 --- a/Engine/Source/RHI-DirectX12/Src/Device.cpp +++ b/Engine/Source/RHI-DirectX12/Src/Device.cpp @@ -265,14 +265,14 @@ namespace RHI::DirectX12 { return { new DX12PipelineCache(*this, inCreateInfo) }; } - bool DX12Device::CheckSwapChainFormatSupport(Surface* inSurface, PixelFormat inFormat) + bool DX12Device::CheckSwapChainFormatSupport(Surface* inSurface, PixelFormat inFormat, ColorSpace inColorSpace) { - static std::unordered_set supportedFormats = { - PixelFormat::rgba8Unorm, - PixelFormat::bgra8Unorm, - // TODO HDR + static std::unordered_map> supportedFormats = { + { ColorSpace::srgbNonLinear, { PixelFormat::rgba8Unorm, PixelFormat::bgra8Unorm } }, + { ColorSpace::hdr10St2084, { PixelFormat::rgb10A2Unorm } } }; - return supportedFormats.contains(inFormat); + const auto iter = supportedFormats.find(inColorSpace); + return iter != supportedFormats.end() && iter->second.contains(inFormat); } TextureSubResourceCopyFootprint DX12Device::GetTextureSubResourceCopyFootprint(const Texture& texture, const TextureSubResourceInfo& subResourceInfo) diff --git a/Engine/Source/RHI-DirectX12/Src/SwapChain.cpp b/Engine/Source/RHI-DirectX12/Src/SwapChain.cpp index 2c20adbe..972f4fd5 100644 --- a/Engine/Source/RHI-DirectX12/Src/SwapChain.cpp +++ b/Engine/Source/RHI-DirectX12/Src/SwapChain.cpp @@ -3,6 +3,7 @@ // #include +#include #include #include @@ -15,10 +16,23 @@ #include namespace RHI::DirectX12 { - static uint8_t GetSyncInterval(PresentMode presentMode) + // DXGI only exposes the vblank sync interval as a knob, so every vsynced mode (vsync/mailbox/fifoRelaxed) maps to 1 and + // only immediately maps to 0. Tearing (true uncapped present) is then opted into separately via the ALLOW_TEARING flags. + static uint8_t GetSyncInterval(const PresentMode presentMode) { return presentMode == PresentMode::immediately ? 0 : 1; } + + static bool CheckAllowTearingSupport(IDXGIFactory4* inFactory) + { + ComPtr factory5; + if (FAILED(inFactory->QueryInterface(IID_PPV_ARGS(&factory5)))) { + return false; + } + BOOL allowTearing = FALSE; + return SUCCEEDED(factory5->CheckFeatureSupport(DXGI_FEATURE_PRESENT_ALLOW_TEARING, &allowTearing, sizeof(allowTearing))) + && allowTearing == TRUE; + } } namespace RHI::DirectX12 { @@ -26,8 +40,9 @@ namespace RHI::DirectX12 { : SwapChain(inCreateInfo) , device(inDevice) , queue(static_cast(*inCreateInfo.presentQueue)) - , presentMode(inCreateInfo.presentMode) , textureNum(inCreateInfo.textureNum) + , presentMode(inCreateInfo.presentMode) + , allowTearing(false) { CreateDX12SwapChain(inCreateInfo); FetchTextures(inCreateInfo); @@ -59,7 +74,11 @@ namespace RHI::DirectX12 { { auto& dx12WaitSemaphore = static_cast(*inWaitSemaphore); Assert(SUCCEEDED(queue.GetNative()->Wait(dx12WaitSemaphore.GetNative(), 1))); - Assert(SUCCEEDED(nativeSwapChain->Present(GetSyncInterval(presentMode), false))); + + const auto syncInterval = GetSyncInterval(presentMode); + // DXGI_PRESENT_ALLOW_TEARING is only legal with a sync interval of 0 and an ALLOW_TEARING swap chain. + const UINT presentFlags = syncInterval == 0 && allowTearing ? DXGI_PRESENT_ALLOW_TEARING : 0; + Assert(SUCCEEDED(nativeSwapChain->Present(syncInterval, presentFlags))); } void DX12SwapChain::CreateDX12SwapChain(const SwapChainCreateInfo& inCreateInfo) @@ -70,6 +89,10 @@ namespace RHI::DirectX12 { const auto* dx12Surface = static_cast(inCreateInfo.surface); Assert(dx12Surface != nullptr); + // Tearing (uncapped, IMMEDIATE-like present) needs both the ALLOW_TEARING swap-chain flag here and the matching + // present flag later, and is only usable when the DXGI factory reports support for it. + allowTearing = inCreateInfo.presentMode == PresentMode::immediately && CheckAllowTearingSupport(instance.GetNative()); + DXGI_SWAP_CHAIN_DESC1 desc {}; desc.BufferCount = inCreateInfo.textureNum; desc.Width = inCreateInfo.width; @@ -78,6 +101,7 @@ namespace RHI::DirectX12 { desc.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT; desc.SwapEffect = DXGI_SWAP_EFFECT_FLIP_DISCARD; desc.SampleDesc.Count = 1; + desc.Flags = allowTearing ? DXGI_SWAP_CHAIN_FLAG_ALLOW_TEARING : 0; ComPtr dx12SwapChain1; bool success = SUCCEEDED(instance.GetNative()->CreateSwapChainForHwnd( @@ -91,6 +115,13 @@ namespace RHI::DirectX12 { success = SUCCEEDED(dx12SwapChain1.As(&nativeSwapChain)); Assert(success); + + const auto dxgiColorSpace = EnumCast(inCreateInfo.colorSpace); + UINT colorSpaceSupport = 0; + if (SUCCEEDED(nativeSwapChain->CheckColorSpaceSupport(dxgiColorSpace, &colorSpaceSupport)) + && (colorSpaceSupport & DXGI_SWAP_CHAIN_COLOR_SPACE_SUPPORT_FLAG_PRESENT)) { + Assert(SUCCEEDED(nativeSwapChain->SetColorSpace1(dxgiColorSpace))); + } } void DX12SwapChain::FetchTextures(const SwapChainCreateInfo& inCreateInfo) diff --git a/Engine/Source/RHI-Dummy/Include/RHI/Dummy/Device.h b/Engine/Source/RHI-Dummy/Include/RHI/Dummy/Device.h index 6917291b..9ecd45b7 100644 --- a/Engine/Source/RHI-Dummy/Include/RHI/Dummy/Device.h +++ b/Engine/Source/RHI-Dummy/Include/RHI/Dummy/Device.h @@ -36,7 +36,7 @@ namespace RHI::Dummy { Common::UniquePtr CreateSemaphore() override; Common::UniquePtr CreateQuerySet(const QuerySetCreateInfo& createInfo) override; - bool CheckSwapChainFormatSupport(Surface *surface, PixelFormat format) override; + bool CheckSwapChainFormatSupport(Surface *surface, PixelFormat format, ColorSpace colorSpace) override; TextureSubResourceCopyFootprint GetTextureSubResourceCopyFootprint(const Texture& texture, const TextureSubResourceInfo& subResourceInfo) override; private: diff --git a/Engine/Source/RHI-Dummy/Src/Device.cpp b/Engine/Source/RHI-Dummy/Src/Device.cpp index 4a240c6e..6c1a0b06 100644 --- a/Engine/Source/RHI-Dummy/Src/Device.cpp +++ b/Engine/Source/RHI-Dummy/Src/Device.cpp @@ -126,7 +126,7 @@ namespace RHI::Dummy { return { new DummyPipelineCache(createInfo) }; } - bool DummyDevice::CheckSwapChainFormatSupport(Surface* surface, PixelFormat format) + bool DummyDevice::CheckSwapChainFormatSupport(Surface* surface, PixelFormat format, ColorSpace colorSpace) { return true; } diff --git a/Engine/Source/RHI-Vulkan/Include/RHI/Vulkan/Common.h b/Engine/Source/RHI-Vulkan/Include/RHI/Vulkan/Common.h index 3d4691d1..7454d1ad 100644 --- a/Engine/Source/RHI-Vulkan/Include/RHI/Vulkan/Common.h +++ b/Engine/Source/RHI-Vulkan/Include/RHI/Vulkan/Common.h @@ -262,9 +262,15 @@ namespace RHI::Vulkan { ECIMPL_BEGIN(PresentMode, VkPresentModeKHR) ECIMPL_ITEM(PresentMode::immediately, VK_PRESENT_MODE_IMMEDIATE_KHR) ECIMPL_ITEM(PresentMode::vsync, VK_PRESENT_MODE_FIFO_KHR) - ECIMPL_ITEM(PresentMode::max, VK_PRESENT_MODE_IMMEDIATE_KHR) // TODO Set the default present mode to immediate? + ECIMPL_ITEM(PresentMode::mailbox, VK_PRESENT_MODE_MAILBOX_KHR) + ECIMPL_ITEM(PresentMode::fifoRelaxed, VK_PRESENT_MODE_FIFO_RELAXED_KHR) ECIMPL_END(VkPresentModeKHR) + ECIMPL_BEGIN(ColorSpace, VkColorSpaceKHR) + ECIMPL_ITEM(ColorSpace::srgbNonLinear, VK_COLOR_SPACE_SRGB_NONLINEAR_KHR) + ECIMPL_ITEM(ColorSpace::hdr10St2084, VK_COLOR_SPACE_HDR10_ST2084_EXT) + ECIMPL_END(VkColorSpaceKHR) + ECIMPL_BEGIN(QueryType, VkQueryType) ECIMPL_ITEM(QueryType::occlusion, VK_QUERY_TYPE_OCCLUSION) ECIMPL_ITEM(QueryType::timestamp, VK_QUERY_TYPE_TIMESTAMP) diff --git a/Engine/Source/RHI-Vulkan/Include/RHI/Vulkan/Device.h b/Engine/Source/RHI-Vulkan/Include/RHI/Vulkan/Device.h index c52c5e11..b79759d9 100644 --- a/Engine/Source/RHI-Vulkan/Include/RHI/Vulkan/Device.h +++ b/Engine/Source/RHI-Vulkan/Include/RHI/Vulkan/Device.h @@ -43,7 +43,7 @@ namespace RHI::Vulkan { Common::UniquePtr CreateSemaphore() override; Common::UniquePtr CreateQuerySet(const QuerySetCreateInfo& inCreateInfo) override; - bool CheckSwapChainFormatSupport(Surface* inSurface, PixelFormat inFormat) override; + bool CheckSwapChainFormatSupport(Surface* inSurface, PixelFormat inFormat, ColorSpace inColorSpace) override; TextureSubResourceCopyFootprint GetTextureSubResourceCopyFootprint(const Texture& texture, const TextureSubResourceInfo& subResourceInfo) override; VkDevice GetNative() const; diff --git a/Engine/Source/RHI-Vulkan/Include/RHI/Vulkan/Instance.h b/Engine/Source/RHI-Vulkan/Include/RHI/Vulkan/Instance.h index 3ff02b2b..c844928c 100644 --- a/Engine/Source/RHI-Vulkan/Include/RHI/Vulkan/Instance.h +++ b/Engine/Source/RHI-Vulkan/Include/RHI/Vulkan/Instance.h @@ -53,6 +53,7 @@ namespace RHI::Vulkan { VkDebugUtilsMessengerEXT nativeDebugMessenger; #endif VkInstance nativeInstance; + std::vector enabledExtensionNames; std::vector nativePhysicalDevices; std::vector> gpus; std::unordered_map dynamicFuncPointers; diff --git a/Engine/Source/RHI-Vulkan/Src/Device.cpp b/Engine/Source/RHI-Vulkan/Src/Device.cpp index 945633aa..0c805d7e 100644 --- a/Engine/Source/RHI-Vulkan/Src/Device.cpp +++ b/Engine/Source/RHI-Vulkan/Src/Device.cpp @@ -159,10 +159,9 @@ namespace RHI::Vulkan { return { new VulkanPipelineCache(*this, inCreateInfo) }; } - bool VulkanDevice::CheckSwapChainFormatSupport(Surface* inSurface, const PixelFormat inFormat) + bool VulkanDevice::CheckSwapChainFormatSupport(Surface* inSurface, const PixelFormat inFormat, const ColorSpace inColorSpace) { const auto* vkSurface = static_cast(inSurface); - VkColorSpaceKHR colorSpace = VK_COLORSPACE_SRGB_NONLINEAR_KHR; uint32_t formatCount = 0; std::vector surfaceFormats; @@ -173,7 +172,7 @@ namespace RHI::Vulkan { const auto iter = std::ranges::find_if( surfaceFormats, - [format = EnumCast(inFormat), colorSpace](const VkSurfaceFormatKHR surfaceFormat) { + [format = EnumCast(inFormat), colorSpace = EnumCast(inColorSpace)](const VkSurfaceFormatKHR surfaceFormat) { return format == surfaceFormat.format && colorSpace == surfaceFormat.colorSpace; }); return iter != surfaceFormats.end(); diff --git a/Engine/Source/RHI-Vulkan/Src/Instance.cpp b/Engine/Source/RHI-Vulkan/Src/Instance.cpp index 803d12e3..edcbf971 100644 --- a/Engine/Source/RHI-Vulkan/Src/Instance.cpp +++ b/Engine/Source/RHI-Vulkan/Src/Instance.cpp @@ -31,6 +31,12 @@ namespace RHI::Vulkan { #endif }; + // Optional instance extensions: enabled only when the driver reports support. VK_EXT_swapchain_colorspace is what makes + // vkGetPhysicalDeviceSurfaceFormatsKHR report the HDR color spaces (e.g. HDR10 ST.2084) in addition to plain sRGB. + static std::vector optionalExtensionNames = { + "VK_EXT_swapchain_colorspace" + }; + #if BUILD_CONFIG_DEBUG static VKAPI_ATTR VkBool32 VKAPI_CALL DebugCallback( VkDebugUtilsMessageSeverityFlagBitsEXT messageSeverity, @@ -117,14 +123,23 @@ namespace RHI::Vulkan { std::vector supportedExtensions(supportedExtensionCount); vkEnumerateInstanceExtensionProperties(nullptr, &supportedExtensionCount, supportedExtensions.data()); + const auto isExtensionSupported = [&supportedExtensions](const char* inName) -> bool { + return std::ranges::find_if( + supportedExtensions, + [inName](const auto& elem) -> bool { return std::string(inName) == elem.extensionName; } + ) != supportedExtensions.end(); + }; + for (auto&& requiredExtensionName : requiredExtensionNames) { - if (const auto iter = std::ranges::find_if( - supportedExtensions, - [&requiredExtensionName](const auto& elem) -> bool { return std::string(requiredExtensionName) == elem.extensionName; } - ); - iter == supportedExtensions.end()) { + if (!isExtensionSupported(requiredExtensionName)) { QuickFailWithReason("required vulkan extensions is not support"); } + enabledExtensionNames.emplace_back(requiredExtensionName); + } + for (auto&& optionalExtensionName : optionalExtensionNames) { + if (isExtensionSupported(optionalExtensionName)) { + enabledExtensionNames.emplace_back(optionalExtensionName); + } } } @@ -140,8 +155,8 @@ namespace RHI::Vulkan { VkInstanceCreateInfo createInfo = {}; createInfo.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO; createInfo.pApplicationInfo = &applicationInfo; - createInfo.enabledExtensionCount = requiredExtensionNames.size(); - createInfo.ppEnabledExtensionNames = requiredExtensionNames.data(); + createInfo.enabledExtensionCount = enabledExtensionNames.size(); + createInfo.ppEnabledExtensionNames = enabledExtensionNames.data(); #if BUILD_CONFIG_DEBUG createInfo.enabledLayerCount = requiredLayerNames.size(); createInfo.ppEnabledLayerNames = requiredLayerNames.data(); diff --git a/Engine/Source/RHI-Vulkan/Src/SwapChain.cpp b/Engine/Source/RHI-Vulkan/Src/SwapChain.cpp index d32bbdc3..86e37e97 100644 --- a/Engine/Source/RHI-Vulkan/Src/SwapChain.cpp +++ b/Engine/Source/RHI-Vulkan/Src/SwapChain.cpp @@ -89,9 +89,9 @@ namespace RHI::Vulkan { extent.width = std::clamp(extent.width, surfaceCap.minImageExtent.width, surfaceCap.maxImageExtent.width); extent.height = std::clamp(extent.height, surfaceCap.minImageExtent.height, surfaceCap.maxImageExtent.height); - Assert(device.CheckSwapChainFormatSupport(vkSurface, inCreateInfo.format)); + Assert(device.CheckSwapChainFormatSupport(vkSurface, inCreateInfo.format, inCreateInfo.colorSpace)); auto supportedFormat = EnumCast(inCreateInfo.format); - auto colorSpace = VK_COLORSPACE_SRGB_NONLINEAR_KHR; + auto colorSpace = EnumCast(inCreateInfo.colorSpace); uint32_t presentModeCount = 0; std::vector presentModes; @@ -100,14 +100,11 @@ namespace RHI::Vulkan { presentModes.resize(presentModeCount); vkGetPhysicalDeviceSurfacePresentModesKHR(device.GetGpu().GetNative(), surface, &presentModeCount, presentModes.data()); - VkPresentModeKHR supportedMode = EnumCast(inCreateInfo.presentMode); - { - Assert(!presentModes.empty()); - const auto iter = std::ranges::find_if( - presentModes, - [supportedMode](const VkPresentModeKHR mode) { return mode == supportedMode; }); - Assert(iter != presentModes.end()); - } + // Only VK_PRESENT_MODE_FIFO_KHR is guaranteed by the spec, so fall back to it when the requested mode is unavailable. + const VkPresentModeKHR requestedMode = EnumCast(inCreateInfo.presentMode); + const VkPresentModeKHR supportedMode = std::ranges::find(presentModes, requestedMode) != presentModes.end() + ? requestedMode + : VK_PRESENT_MODE_FIFO_KHR; swapChainImageCount = std::clamp(static_cast(inCreateInfo.textureNum), surfaceCap.minImageCount, surfaceCap.maxImageCount); VkSwapchainCreateInfoKHR swapChainInfo = {}; diff --git a/Engine/Source/RHI/Include/RHI/Common.h b/Engine/Source/RHI/Include/RHI/Common.h index 6ac01f92..d2b1dac7 100644 --- a/Engine/Source/RHI/Include/RHI/Common.h +++ b/Engine/Source/RHI/Include/RHI/Common.h @@ -367,11 +367,16 @@ namespace RHI { }; enum class PresentMode : uint8_t { - // TODO check this - // 1. DirectX SwapEffect #see https://docs.microsoft.com/en-us/windows/win32/api/dxgi/ne-dxgi-dxgi_swap_effect - // 2. Vulkan VkPresentModeKHR #see https://www.khronos.org/registry/vulkan/specs/1.3-extensions/man/html/VkPresentModeKHR.html - immediately, - vsync, + immediately, // no synchronization, lowest latency, allows tearing + vsync, // hard vertical sync, no tearing, always supported + mailbox, // vertical sync without tearing, keeps only the latest frame (low latency) + fifoRelaxed, // vertical sync that allows tearing when the app misses a refresh + max + }; + + enum class ColorSpace : uint8_t { + srgbNonLinear, // standard SDR sRGB (gamma ~2.2) + hdr10St2084, // HDR10: Rec.2020 primaries with the ST.2084 (PQ) transfer function max }; diff --git a/Engine/Source/RHI/Include/RHI/Device.h b/Engine/Source/RHI/Include/RHI/Device.h index 3a45c333..a9573034 100644 --- a/Engine/Source/RHI/Include/RHI/Device.h +++ b/Engine/Source/RHI/Include/RHI/Device.h @@ -87,7 +87,7 @@ namespace RHI { virtual Common::UniquePtr CreateSemaphore() = 0; virtual Common::UniquePtr CreateQuerySet(const QuerySetCreateInfo& createInfo) = 0; - virtual bool CheckSwapChainFormatSupport(Surface* surface, PixelFormat format) = 0; + virtual bool CheckSwapChainFormatSupport(Surface* surface, PixelFormat format, ColorSpace colorSpace) = 0; virtual TextureSubResourceCopyFootprint GetTextureSubResourceCopyFootprint(const Texture& texture, const TextureSubResourceInfo& subResourceInfo) = 0; protected: diff --git a/Engine/Source/RHI/Include/RHI/SwapChain.h b/Engine/Source/RHI/Include/RHI/SwapChain.h index 8da93789..edf66365 100644 --- a/Engine/Source/RHI/Include/RHI/SwapChain.h +++ b/Engine/Source/RHI/Include/RHI/SwapChain.h @@ -21,6 +21,7 @@ namespace RHI { Surface* surface; uint8_t textureNum; PixelFormat format; + ColorSpace colorSpace; uint32_t width; uint32_t height; PresentMode presentMode; @@ -30,6 +31,7 @@ namespace RHI { SwapChainCreateInfo& SetSurface(Surface* inSurface); SwapChainCreateInfo& SetTextureNum(uint8_t inTextureNum); SwapChainCreateInfo& SetFormat(PixelFormat inFormat); + SwapChainCreateInfo& SetColorSpace(ColorSpace inColorSpace); SwapChainCreateInfo& SetWidth(uint32_t inWidth); SwapChainCreateInfo& SetHeight(uint32_t inHeight); SwapChainCreateInfo& SetPresentMode(PresentMode inMode); diff --git a/Engine/Source/RHI/Src/SwapChain.cpp b/Engine/Source/RHI/Src/SwapChain.cpp index 65cf8d33..28169e3c 100644 --- a/Engine/Source/RHI/Src/SwapChain.cpp +++ b/Engine/Source/RHI/Src/SwapChain.cpp @@ -10,6 +10,7 @@ namespace RHI { , surface(nullptr) , textureNum(0) , format(PixelFormat::max) + , colorSpace(ColorSpace::srgbNonLinear) , width(0) , height(0) , presentMode(PresentMode::max) @@ -40,6 +41,12 @@ namespace RHI { return *this; } + SwapChainCreateInfo& SwapChainCreateInfo::SetColorSpace(const ColorSpace inColorSpace) + { + colorSpace = inColorSpace; + return *this; + } + SwapChainCreateInfo& SwapChainCreateInfo::SetWidth(const uint32_t inWidth) { width = inWidth; diff --git a/Sample/RHI-SSAO/SSAOApplication.cpp b/Sample/RHI-SSAO/SSAOApplication.cpp index 6f665631..a637477d 100644 --- a/Sample/RHI-SSAO/SSAOApplication.cpp +++ b/Sample/RHI-SSAO/SSAOApplication.cpp @@ -414,7 +414,7 @@ class SSAOApplication final : public Application { surface = device->CreateSurface(SurfaceCreateInfo(GetPlatformWindow())); for (const auto format : swapChainFormatQualifiers) { - if (device->CheckSwapChainFormatSupport(surface.Get(), format)) { + if (device->CheckSwapChainFormatSupport(surface.Get(), format, ColorSpace::srgbNonLinear)) { swapChainFormat = format; break; } diff --git a/Sample/RHI-TexSampling/TexSampling.cpp b/Sample/RHI-TexSampling/TexSampling.cpp index 96f50ef7..259e65a4 100644 --- a/Sample/RHI-TexSampling/TexSampling.cpp +++ b/Sample/RHI-TexSampling/TexSampling.cpp @@ -113,7 +113,7 @@ class TexSamplingApplication final : public Application { surface = device->CreateSurface(SurfaceCreateInfo(GetPlatformWindow())); for (const auto format : swapChainFormatQualifiers) { - if (device->CheckSwapChainFormatSupport(surface.Get(), format)) { + if (device->CheckSwapChainFormatSupport(surface.Get(), format, ColorSpace::srgbNonLinear)) { swapChainFormat = format; break; } diff --git a/Sample/RHI-Triangle/Triangle.cpp b/Sample/RHI-Triangle/Triangle.cpp index 221d3f80..fd029c61 100644 --- a/Sample/RHI-Triangle/Triangle.cpp +++ b/Sample/RHI-Triangle/Triangle.cpp @@ -104,7 +104,7 @@ class TriangleApplication final : public Application { surface = device->CreateSurface(SurfaceCreateInfo(GetPlatformWindow())); for (const auto format : swapChainFormatQualifiers) { - if (device->CheckSwapChainFormatSupport(surface.Get(), format)) { + if (device->CheckSwapChainFormatSupport(surface.Get(), format, ColorSpace::srgbNonLinear)) { swapChainFormat = format; break; } diff --git a/Sample/Rendering-BaseTexture/BaseTexture.cpp b/Sample/Rendering-BaseTexture/BaseTexture.cpp index 931d757d..b14d880d 100644 --- a/Sample/Rendering-BaseTexture/BaseTexture.cpp +++ b/Sample/Rendering-BaseTexture/BaseTexture.cpp @@ -263,7 +263,7 @@ void BaseTexApp::CreateSwapChain() surface = device->CreateSurface(SurfaceCreateInfo(GetPlatformWindow())); for (const auto format : swapChainFormatQualifiers) { - if (device->CheckSwapChainFormatSupport(surface.Get(), format)) { + if (device->CheckSwapChainFormatSupport(surface.Get(), format, ColorSpace::srgbNonLinear)) { swapChainFormat = format; break; } diff --git a/Sample/Rendering-SSAO/SSAOApplication.cpp b/Sample/Rendering-SSAO/SSAOApplication.cpp index d7a9b9a3..24b9b395 100644 --- a/Sample/Rendering-SSAO/SSAOApplication.cpp +++ b/Sample/Rendering-SSAO/SSAOApplication.cpp @@ -569,7 +569,7 @@ class SSAOApp final : public Application { }; for (const auto format : swapChainFormatQualifiers) { - if (device->CheckSwapChainFormatSupport(surface.Get(), format)) { + if (device->CheckSwapChainFormatSupport(surface.Get(), format, ColorSpace::srgbNonLinear)) { swapChainFormat = format; break; } diff --git a/Sample/Rendering-Triangle/Triangle.cpp b/Sample/Rendering-Triangle/Triangle.cpp index a67f8720..fe52d1e1 100644 --- a/Sample/Rendering-Triangle/Triangle.cpp +++ b/Sample/Rendering-Triangle/Triangle.cpp @@ -246,7 +246,7 @@ void TriangleApplication::CreateSwapChain() }; for (const auto format : swapChainFormatQualifiers) { - if (device->CheckSwapChainFormatSupport(surface.Get(), format)) { + if (device->CheckSwapChainFormatSupport(surface.Get(), format, ColorSpace::srgbNonLinear)) { swapChainFormat = format; break; } From 46462d148f63fb2687caab9a3dd6cfddb2788e3f Mon Sep 17 00:00:00 2001 From: kindem Date: Sat, 4 Jul 2026 17:49:02 +0800 Subject: [PATCH 08/24] fix: repair rendering samples and prototype widget after rhi pipeline cache and hdr changes --- Editor/Src/Widget/Prototype.cpp | 2 +- Sample/Rendering-BaseTexture/BaseTexture.cpp | 4 ++-- Sample/Rendering-SSAO/SSAOApplication.cpp | 10 +++++----- Sample/Rendering-Triangle/Triangle.cpp | 4 ++-- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/Editor/Src/Widget/Prototype.cpp b/Editor/Src/Widget/Prototype.cpp index c202d5e0..26b99b4c 100644 --- a/Editor/Src/Widget/Prototype.cpp +++ b/Editor/Src/Widget/Prototype.cpp @@ -190,7 +190,7 @@ namespace Editor { std::optional pixelFormat = {}; for (const auto format : formatQualifiers) { - if (device->CheckSwapChainFormatSupport(surface.Get(), format)) { + if (device->CheckSwapChainFormatSupport(surface.Get(), format, RHI::ColorSpace::srgbNonLinear)) { pixelFormat = format; break; } diff --git a/Sample/Rendering-BaseTexture/BaseTexture.cpp b/Sample/Rendering-BaseTexture/BaseTexture.cpp index b14d880d..fa742e6d 100644 --- a/Sample/Rendering-BaseTexture/BaseTexture.cpp +++ b/Sample/Rendering-BaseTexture/BaseTexture.cpp @@ -123,7 +123,7 @@ void BaseTexApp::OnDrawFrame() frameFence->Reset(); const auto backTextureIndex = swapChain->AcquireBackTexture(imageReadySemaphore.Get()); - auto* pso = PipelineCache::Get(*device).GetOrCreate( + auto* pso = Render::PipelineCache::Get(*device).GetOrCreate( RasterPipelineStateDesc() .SetVertexShader(vs) .SetPixelShader(ps) @@ -210,7 +210,7 @@ void BaseTexApp::OnDestroy() fence->Wait(); BindGroupCache::Get(*device).Invalidate(); - PipelineCache::Get(*device).Invalidate(); + Render::PipelineCache::Get(*device).Invalidate(); BufferPool::Get(*device).Invalidate(); TexturePool::Get(*device).Invalidate(); ShaderMap::Get(*device).Invalidate(); diff --git a/Sample/Rendering-SSAO/SSAOApplication.cpp b/Sample/Rendering-SSAO/SSAOApplication.cpp index 24b9b395..26b034fe 100644 --- a/Sample/Rendering-SSAO/SSAOApplication.cpp +++ b/Sample/Rendering-SSAO/SSAOApplication.cpp @@ -227,7 +227,7 @@ class SSAOApp final : public Application { auto* ssaoKernelView = builder.CreateBufferView(ssaoKernelBuffer, RGBufferViewDesc(BufferViewType::uniformBinding, ssaoKernelSize * sizeof(FVec4))); // 1. G-Buffer Pass - auto* gBufferPipeline = PipelineCache::Get(*device).GetOrCreate( + auto* gBufferPipeline = Render::PipelineCache::Get(*device).GetOrCreate( RasterPipelineStateDesc() .SetVertexShader(gBufferVS) .SetPixelShader(gBufferPS) @@ -288,7 +288,7 @@ class SSAOApp final : public Application { }); // 2. SSAO Pass - auto* ssaoPipeline = PipelineCache::Get(*device).GetOrCreate( + auto* ssaoPipeline = Render::PipelineCache::Get(*device).GetOrCreate( RasterPipelineStateDesc() .SetVertexShader(ssaoVS) .SetPixelShader(ssaoPS) @@ -330,7 +330,7 @@ class SSAOApp final : public Application { }); // 3. SSAO Blur Pass - auto* ssaoBlurPipeline = PipelineCache::Get(*device).GetOrCreate( + auto* ssaoBlurPipeline = Render::PipelineCache::Get(*device).GetOrCreate( RasterPipelineStateDesc() .SetVertexShader(blurVS) .SetPixelShader(blurPS) @@ -367,7 +367,7 @@ class SSAOApp final : public Application { }); // 4. Composition Pass - auto* compositionPipeline = PipelineCache::Get(*device).GetOrCreate( + auto* compositionPipeline = Render::PipelineCache::Get(*device).GetOrCreate( RasterPipelineStateDesc() .SetVertexShader(compositionVS) .SetPixelShader(compositionPS) @@ -440,7 +440,7 @@ class SSAOApp final : public Application { fence->Wait(); BindGroupCache::Get(*device).Invalidate(); - PipelineCache::Get(*device).Invalidate(); + Render::PipelineCache::Get(*device).Invalidate(); BufferPool::Get(*device).Invalidate(); TexturePool::Get(*device).Invalidate(); ShaderMap::Get(*device).Invalidate(); diff --git a/Sample/Rendering-Triangle/Triangle.cpp b/Sample/Rendering-Triangle/Triangle.cpp index fe52d1e1..4784f60d 100644 --- a/Sample/Rendering-Triangle/Triangle.cpp +++ b/Sample/Rendering-Triangle/Triangle.cpp @@ -114,7 +114,7 @@ void TriangleApplication::OnDrawFrame() frameFence->Reset(); const auto backTextureIndex = swapChain->AcquireBackTexture(imageReadySemaphore.Get()); - auto* pso = PipelineCache::Get(*device).GetOrCreate( + auto* pso = Render::PipelineCache::Get(*device).GetOrCreate( RasterPipelineStateDesc() .SetVertexShader(triangleVS) .SetPixelShader(trianglePS) @@ -195,7 +195,7 @@ void TriangleApplication::OnDestroy() fence->Wait(); BindGroupCache::Get(*device).Invalidate(); - PipelineCache::Get(*device).Invalidate(); + Render::PipelineCache::Get(*device).Invalidate(); BufferPool::Get(*device).Invalidate(); TexturePool::Get(*device).Invalidate(); ShaderMap::Get(*device).Invalidate(); From 178350f97cbed1867c43dd248737f2f5f0c4d456 Mon Sep 17 00:00:00 2001 From: kindem Date: Sat, 4 Jul 2026 17:49:02 +0800 Subject: [PATCH 09/24] feat: add viewport present and editor world activation groundwork --- Engine/Source/Core/Include/Core/Log.h | 30 +++++++---- Engine/Source/Core/Src/Log.cpp | 52 ++++++++++++------- .../Source/Launch/Include/Launch/GameClient.h | 2 +- .../Launch/Include/Launch/GameViewport.h | 1 + Engine/Source/Launch/Src/GameClient.cpp | 4 +- Engine/Source/Launch/Src/GameViewport.cpp | 5 ++ .../Render/Include/Render/RenderModule.h | 2 + .../Source/Render/Include/Render/Renderer.h | 4 +- Engine/Source/Render/Include/Render/Scene.h | 2 +- .../Source/Render/SharedSrc/RenderModule.cpp | 11 ++++ Engine/Source/Render/Src/Renderer.cpp | 25 ++++++++- .../Source/Runtime/Include/Runtime/Client.h | 4 +- Engine/Source/Runtime/Include/Runtime/ECS.h | 6 +++ .../Runtime/Include/Runtime/System/Player.h | 13 +++-- .../Runtime/Include/Runtime/System/Render.h | 2 +- .../Source/Runtime/Include/Runtime/Viewport.h | 3 ++ Engine/Source/Runtime/Include/Runtime/World.h | 8 +++ Engine/Source/Runtime/Src/ECS.cpp | 3 ++ Engine/Source/Runtime/Src/Engine.cpp | 5 +- Engine/Source/Runtime/Src/System/Player.cpp | 1 + Engine/Source/Runtime/Src/System/Render.cpp | 12 +++-- Engine/Source/Runtime/Src/World.cpp | 32 ++++++++++++ 22 files changed, 182 insertions(+), 45 deletions(-) diff --git a/Engine/Source/Core/Include/Core/Log.h b/Engine/Source/Core/Include/Core/Log.h index 8e94e24d..b06f4c02 100644 --- a/Engine/Source/Core/Include/Core/Log.h +++ b/Engine/Source/Core/Include/Core/Log.h @@ -19,10 +19,28 @@ #define LogError(tag, ...) Core::Logger::Get().Log(#tag, Core::LogLevel::error, std::format(__VA_ARGS__)) namespace Core { - class LogStream { + enum class LogLevel : uint8_t { + verbose, + info, + warning, + error, + max + }; + + struct LogEntry { + std::string time; + std::string tag; + LogLevel level; + std::string content; + }; + + class CORE_API LogStream { public: virtual ~LogStream() = default; virtual void Write(const std::string& inString) = 0; + // structured entry point used by Logger, the default implementation formats the entry and forwards to + // Write(string), override it when the stream wants the raw fields (e.g. the editor log panel) + virtual void Write(const LogEntry& inEntry); virtual void Flush() = 0; }; @@ -53,14 +71,6 @@ namespace Core { std::ofstream file; }; - enum class LogLevel : uint8_t { - verbose, - info, - warning, - error, - max - }; - class CORE_API Logger { public: static Logger& Get(); @@ -76,7 +86,7 @@ namespace Core { private: Logger(); - void LogInternal(const std::string& inString); + void LogInternal(const LogEntry& inEntry); float lastFlushTimeSec; std::vector> streams; diff --git a/Engine/Source/Core/Src/Log.cpp b/Engine/Source/Core/Src/Log.cpp index 84b63b4d..37f62f7a 100644 --- a/Engine/Source/Core/Src/Log.cpp +++ b/Engine/Source/Core/Src/Log.cpp @@ -6,7 +6,33 @@ #include #include +namespace Core::Internal { + static std::string FormatLogEntry(const LogEntry& inEntry) + { + static std::unordered_map logLevelStringMap = { + { LogLevel::verbose, "Verbose" }, + { LogLevel::info, "Info" }, + { LogLevel::warning, "Warning" }, + { LogLevel::error, "Error" } + }; + + static std::unordered_map logLevelColorStr = { + { LogLevel::verbose, "" }, + { LogLevel::info, "" }, + { LogLevel::warning, "\033[33m" }, + { LogLevel::error, "\033[31m" } + }; + + return std::format("{}[{}][{}][{}] {}\033[0m", logLevelColorStr.at(inEntry.level), inEntry.time, inEntry.tag, logLevelStringMap.at(inEntry.level), inEntry.content); + } +} + namespace Core { + void LogStream::Write(const LogEntry& inEntry) + { + Write(Internal::FormatLogEntry(inEntry)); + } + COutLogStream::COutLogStream() = default; COutLogStream::~COutLogStream() @@ -61,22 +87,12 @@ namespace Core { void Logger::Log(const std::string& inTag, LogLevel inLevel, const std::string& inContent) { - static std::unordered_map LogLevelStringMap = { - { LogLevel::verbose, "Verbose" }, - { LogLevel::info, "Info" }, - { LogLevel::warning, "Warning" }, - { LogLevel::error, "Error" } - }; - - static std::unordered_map LogLevelColorStr = { - { LogLevel::verbose, "" }, - { LogLevel::info, "" }, - { LogLevel::warning, "\033[33m" }, - { LogLevel::error, "\033[31m" } - }; - - const auto time = Common::AccurateTime(Common::TimePoint::Now()); - LogInternal(std::format("{}[{}][{}][{}] {}\033[0m", LogLevelColorStr.at(inLevel), time.ToString("hh-mm-ss:mss"), inTag, LogLevelStringMap.at(inLevel), inContent)); + LogEntry entry; + entry.time = Common::AccurateTime(Common::TimePoint::Now()).ToString("hh-mm-ss:mss"); + entry.tag = inTag; + entry.level = inLevel; + entry.content = inContent; + LogInternal(entry); } void Logger::Attach(Common::UniquePtr&& inStream) @@ -97,7 +113,7 @@ namespace Core { Attach(new COutLogStream()); } - void Logger::LogInternal(const std::string& inString) // NOLINT + void Logger::LogInternal(const LogEntry& inEntry) // NOLINT { #if BUILD_CONFIG_DEBUG const bool needFlush = true; // NOLINT @@ -108,7 +124,7 @@ namespace Core { #endif for (const auto& stream : streams) { - stream->Write(inString); + stream->Write(inEntry); if (needFlush) { stream->Flush(); } diff --git a/Engine/Source/Launch/Include/Launch/GameClient.h b/Engine/Source/Launch/Include/Launch/GameClient.h index 76829934..13ac53ea 100644 --- a/Engine/Source/Launch/Include/Launch/GameClient.h +++ b/Engine/Source/Launch/Include/Launch/GameClient.h @@ -15,7 +15,7 @@ namespace Launch { explicit GameClient(GameViewport& inViewport); ~GameClient() override; - Runtime::Viewport& GetViewport() override; + Runtime::Viewport* GetViewport() override; Runtime::World& GetWorld() override; private: diff --git a/Engine/Source/Launch/Include/Launch/GameViewport.h b/Engine/Source/Launch/Include/Launch/GameViewport.h index c053993b..0ceb413e 100644 --- a/Engine/Source/Launch/Include/Launch/GameViewport.h +++ b/Engine/Source/Launch/Include/Launch/GameViewport.h @@ -34,6 +34,7 @@ namespace Launch { uint32_t GetWidth() const override; uint32_t GetHeight() const override; void Resize(uint32_t inWidth, uint32_t inHeight) override; + void Present(const Runtime::PresentInfo& inPresentInfo) override; bool ShouldClose() const; void PollEvents() const; diff --git a/Engine/Source/Launch/Src/GameClient.cpp b/Engine/Source/Launch/Src/GameClient.cpp index eadd1b7c..3161611e 100644 --- a/Engine/Source/Launch/Src/GameClient.cpp +++ b/Engine/Source/Launch/Src/GameClient.cpp @@ -14,9 +14,9 @@ namespace Launch { GameClient::~GameClient() = default; - Runtime::Viewport& GameClient::GetViewport() + Runtime::Viewport* GameClient::GetViewport() { - return viewport; + return &viewport; } Runtime::World& GameClient::GetWorld() diff --git a/Engine/Source/Launch/Src/GameViewport.cpp b/Engine/Source/Launch/Src/GameViewport.cpp index b84d1231..0984d833 100644 --- a/Engine/Source/Launch/Src/GameViewport.cpp +++ b/Engine/Source/Launch/Src/GameViewport.cpp @@ -88,6 +88,11 @@ namespace Launch { glfwPollEvents(); } + void GameViewport::Present(const Runtime::PresentInfo& inPresentInfo) + { + swapChain->Present(inPresentInfo.renderFinishedSemaphore); + } + void GameViewport::RecreateSwapChain(uint32_t inWidth, uint32_t inHeight) { static std::vector formatQualifiers = { diff --git a/Engine/Source/Render/Include/Render/RenderModule.h b/Engine/Source/Render/Include/Render/RenderModule.h index ec749f14..7507cd2d 100644 --- a/Engine/Source/Render/Include/Render/RenderModule.h +++ b/Engine/Source/Render/Include/Render/RenderModule.h @@ -28,6 +28,8 @@ namespace Render { void Initialize(const RenderModuleInitParams& inParams); void DeInitialize(); + // called on render thread once per frame, reclaims pooled resources/caches not used for several frames + void ForfeitFrameResources() const; RHI::Device* GetDevice() const; Render::RenderThread& GetRenderThread() const; Scene* NewScene() const; diff --git a/Engine/Source/Render/Include/Render/Renderer.h b/Engine/Source/Render/Include/Render/Renderer.h index 200ab0a1..8a330ff2 100644 --- a/Engine/Source/Render/Include/Render/Renderer.h +++ b/Engine/Source/Render/Include/Render/Renderer.h @@ -20,7 +20,7 @@ namespace Render { struct Params { RHI::Device* device; const Scene* scene; - const RHI::Texture* surface; + RHI::Texture* surface; Common::UVec2 surfaceExtent; std::vector views; RHI::Semaphore* waitSemaphore; @@ -36,7 +36,7 @@ namespace Render { protected: RHI::Device* device; const Scene* scene; - const RHI::Texture* surface; + RHI::Texture* surface; Common::UVec2 surfaceExtent; std::vector views; RHI::Semaphore* waitSemaphore; diff --git a/Engine/Source/Render/Include/Render/Scene.h b/Engine/Source/Render/Include/Render/Scene.h index d2cbb599..f3ba5308 100644 --- a/Engine/Source/Render/Include/Render/Scene.h +++ b/Engine/Source/Render/Include/Render/Scene.h @@ -27,7 +27,7 @@ namespace Render { template void Remove(EntityId inEntity); private: - template using SceneProxyContainer = std::unordered_map; + template using SceneProxyContainer = std::unordered_map; template SceneProxyContainer& GetSceneProxyContainer(); template const SceneProxyContainer& GetSceneProxyContainer() const; diff --git a/Engine/Source/Render/SharedSrc/RenderModule.cpp b/Engine/Source/Render/SharedSrc/RenderModule.cpp index 8cbc5475..f08431da 100644 --- a/Engine/Source/Render/SharedSrc/RenderModule.cpp +++ b/Engine/Source/Render/SharedSrc/RenderModule.cpp @@ -4,6 +4,8 @@ #include #include +#include +#include #include namespace Render { @@ -57,6 +59,15 @@ namespace Render { initialized = false; } + void RenderModule::ForfeitFrameResources() const + { + Assert(Core::ThreadContext::IsRenderThread()); + BufferPool::Get(*rhiDevice).Forfeit(); + TexturePool::Get(*rhiDevice).Forfeit(); + ResourceViewCache::Get(*rhiDevice).Forfeit(); + BindGroupCache::Get(*rhiDevice).Forfeit(); + } + RHI::Device* RenderModule::GetDevice() const { return rhiDevice.Get(); diff --git a/Engine/Source/Render/Src/Renderer.cpp b/Engine/Source/Render/Src/Renderer.cpp index 971b2474..9c3526e3 100644 --- a/Engine/Source/Render/Src/Renderer.cpp +++ b/Engine/Source/Render/Src/Renderer.cpp @@ -4,6 +4,10 @@ #include +namespace Render::Internal { + const Common::LinearColor surfaceClearColor = { 0.1f, 0.1f, 0.12f, 1.0f }; +} + namespace Render { Renderer::Renderer(const Params& inParams) : device(inParams.device) @@ -29,7 +33,26 @@ namespace Render { void StandardRenderer::Render(float inDeltaTimeSeconds) { - // TODO + auto* backTexture = rgBuilder.ImportTexture(surface, RHI::TextureState::present); + auto* backTextureView = rgBuilder.CreateTextureView(backTexture, RGTextureViewDesc(RHI::TextureViewType::colorAttachment, RHI::TextureViewDimension::tv2D)); + + rgBuilder.AddRasterPass( + "BasePass", + RGRasterPassDesc() + .AddColorAttachment(RGColorAttachment(backTextureView, RHI::LoadOp::clear, RHI::StoreOp::store, Internal::surfaceClearColor)), + {}, + [](const RGBuilder&, RHI::RasterPassCommandRecorder&) -> void {}, + {}, + [backTexture](const RGBuilder& rg, RHI::CommandRecorder& recorder) -> void { + recorder.ResourceBarrier(RHI::Barrier::Transition(rg.GetRHI(backTexture), RHI::TextureState::renderTarget, RHI::TextureState::present)); + }); + + RGExecuteInfo executeInfo; + executeInfo.semaphoresToWait = { waitSemaphore }; + executeInfo.semaphoresToSignal = { signalSemaphore }; + executeInfo.inFenceToSignal = signalFence; + rgBuilder.Execute(executeInfo); + FinalizeViews(); } diff --git a/Engine/Source/Runtime/Include/Runtime/Client.h b/Engine/Source/Runtime/Include/Runtime/Client.h index 1c821cf6..cd05f425 100644 --- a/Engine/Source/Runtime/Include/Runtime/Client.h +++ b/Engine/Source/Runtime/Include/Runtime/Client.h @@ -14,7 +14,9 @@ namespace Runtime { virtual ~Client(); virtual World& GetWorld() = 0; - virtual Viewport& GetViewport() = 0; + // maybe nullptr when the client has no active viewport (e.g. headless world or editor viewport closed), + // rendering is skipped in that case + virtual Viewport* GetViewport() = 0; protected: Client(); diff --git a/Engine/Source/Runtime/Include/Runtime/ECS.h b/Engine/Source/Runtime/Include/Runtime/ECS.h index 542fbb49..af7e20ba 100644 --- a/Engine/Source/Runtime/Include/Runtime/ECS.h +++ b/Engine/Source/Runtime/Include/Runtime/ECS.h @@ -409,6 +409,12 @@ namespace Runtime { Observer removedObserver; }; + // entities carrying this component are skipped entirely by ECRegistry::Save, use it to mark runtime-created + // entities (e.g. players spawned by systems) that must not be serialized into a level + struct RUNTIME_API EClass(transient) TransientTag { + EClassBody(TransientTag) + }; + struct RUNTIME_API EClass() EntityArchive { EClassBody(EntityArchive) diff --git a/Engine/Source/Runtime/Include/Runtime/System/Player.h b/Engine/Source/Runtime/Include/Runtime/System/Player.h index 2f0604f9..2cc746c4 100644 --- a/Engine/Source/Runtime/Include/Runtime/System/Player.h +++ b/Engine/Source/Runtime/Include/Runtime/System/Player.h @@ -28,6 +28,7 @@ namespace Runtime { template void CreatePlayerSpecialPart(T& inPlayer); uint8_t activeLocalPlayerNum; + PlayType playType; Render::RenderModule& renderModule; }; } @@ -36,13 +37,19 @@ namespace Runtime { template Entity PlayerSystem::CreatePlayer() { + // a game world requires exactly one player start, an editor world tolerates an empty level and falls back to + // spawning the player at the identity transform const auto& playerStartQuery = registry.View().All(); - Assert(playerStartQuery.size() == 1); - const auto& [playerStartEntity, playerStart, playerStartTransform] = playerStartQuery[0]; + Assert(playType == PlayType::editor ? playerStartQuery.size() <= 1 : playerStartQuery.size() == 1); const auto playerEntity = registry.Create(); + registry.Emplace(playerEntity); registry.Emplace(playerEntity); - registry.Emplace(playerEntity, playerStartTransform); + if (playerStartQuery.empty()) { + registry.Emplace(playerEntity); + } else { + registry.Emplace(playerEntity, std::get<2>(playerStartQuery[0])); + } auto& player = registry.Emplace(playerEntity); player.viewState = renderModule.NewViewState(); diff --git a/Engine/Source/Runtime/Include/Runtime/System/Render.h b/Engine/Source/Runtime/Include/Runtime/System/Render.h index 8b1dfb55..a47f3c4c 100644 --- a/Engine/Source/Runtime/Include/Runtime/System/Render.h +++ b/Engine/Source/Runtime/Include/Runtime/System/Render.h @@ -46,7 +46,7 @@ namespace Runtime { const auto& player = registry.Get(inEntity); const auto& camera = registry.Get(inEntity); const auto& worldTransform = registry.Get(inEntity); - const auto& clientViewport = client->GetViewport(); + const auto& clientViewport = *client->GetViewport(); const auto width = clientViewport.GetWidth(); const auto height = clientViewport.GetHeight(); diff --git a/Engine/Source/Runtime/Include/Runtime/Viewport.h b/Engine/Source/Runtime/Include/Runtime/Viewport.h index 7bb0adc1..28f8ecf4 100644 --- a/Engine/Source/Runtime/Include/Runtime/Viewport.h +++ b/Engine/Source/Runtime/Include/Runtime/Viewport.h @@ -27,6 +27,9 @@ namespace Runtime { virtual uint32_t GetWidth() const = 0; virtual uint32_t GetHeight() const = 0; virtual void Resize(uint32_t inWidth, uint32_t inHeight) = 0; + // called on render thread after rendering finished, implementations present the back texture acquired by the + // matching GetNextPresentInfo() call, waiting inPresentInfo.renderFinishedSemaphore + virtual void Present(const PresentInfo& inPresentInfo) = 0; // TODO mouse keyboard inputs etc. protected: diff --git a/Engine/Source/Runtime/Include/Runtime/World.h b/Engine/Source/Runtime/Include/Runtime/World.h index b229f5e2..4dc0c1c3 100644 --- a/Engine/Source/Runtime/Include/Runtime/World.h +++ b/Engine/Source/Runtime/Include/Runtime/World.h @@ -36,6 +36,14 @@ namespace Runtime { void Resume(); void Pause(); void Stop(); + // editor-only world lifecycle: builds/destroys the system executor while staying stopped, so an editor world + // ticks its systems (transform/scene/render) for editing without entering play + void Activate(); + void Deactivate(); + bool Activated() const; + bool ShouldTick() const; + ECRegistry& GetRegistry(); + const ECRegistry& GetRegistry() const; void LoadFrom(AssetPtr inLevel); void SaveTo(AssetPtr inLevel); diff --git a/Engine/Source/Runtime/Src/ECS.cpp b/Engine/Source/Runtime/Src/ECS.cpp index 55a4afac..dccd8bbd 100644 --- a/Engine/Source/Runtime/Src/ECS.cpp +++ b/Engine/Source/Runtime/Src/ECS.cpp @@ -805,6 +805,9 @@ namespace Runtime { outArchive = {}; outArchive.entities.reserve(Count()); Each([&](Entity entity) -> void { + if (Has(entity)) { + return; + } outArchive.entities.emplace(entity, EntityArchive {}); auto& comps = outArchive.entities.at(entity).comps; comps.reserve(CompCount(entity)); diff --git a/Engine/Source/Runtime/Src/Engine.cpp b/Engine/Source/Runtime/Src/Engine.cpp index 612713b6..d13e521c 100644 --- a/Engine/Source/Runtime/Src/Engine.cpp +++ b/Engine/Source/Runtime/Src/Engine.cpp @@ -66,14 +66,15 @@ namespace Runtime { Core::ThreadContext::IncFrameNumber(); auto& renderThread = renderModule->GetRenderThread(); - renderThread.EmplaceTask([]() -> void { + renderThread.EmplaceTask([renderModule = renderModule]() -> void { Core::ThreadContext::IncFrameNumber(); Core::Console::Get().PerformRenderThreadSettingsCopy(); Render::ShaderArtifactRegistry::Get().PerformThreadCopy(); + renderModule->ForfeitFrameResources(); }); for (auto* world : worlds) { - if (!world->Playing()) { + if (!world->ShouldTick()) { continue; } world->Tick(inDeltaTimeSeconds); diff --git a/Engine/Source/Runtime/Src/System/Player.cpp b/Engine/Source/Runtime/Src/System/Player.cpp index 388c727e..81d39b9c 100644 --- a/Engine/Source/Runtime/Src/System/Player.cpp +++ b/Engine/Source/Runtime/Src/System/Player.cpp @@ -11,6 +11,7 @@ namespace Runtime { PlayerSystem::PlayerSystem(ECRegistry& inRegistry, const SystemSetupContext& inContext) : System(inRegistry, inContext) , activeLocalPlayerNum(0) + , playType(inContext.playType) , renderModule(EngineHolder::Get().GetRenderModule()) { auto& playersInfo = registry.GEmplace(); diff --git a/Engine/Source/Runtime/Src/System/Render.cpp b/Engine/Source/Runtime/Src/System/Render.cpp index eb3eedd6..2d4f5279 100644 --- a/Engine/Source/Runtime/Src/System/Render.cpp +++ b/Engine/Source/Runtime/Src/System/Render.cpp @@ -28,14 +28,19 @@ namespace Runtime { void RenderSystem::Tick(float inDeltaTimeSeconds) { - auto& clientViewport = client->GetViewport(); + auto* clientViewport = client != nullptr ? client->GetViewport() : nullptr; + if (clientViewport == nullptr) { + return; + } + renderModule.GetRenderThread().EmplaceTask( [ fence = lastFrameFence, views = BuildViews(), scene = registry.GGet().scene.Get(), - surfaceExtent = Common::UVec2(clientViewport.GetWidth(), clientViewport.GetHeight()), - presentInfo = clientViewport.GetNextPresentInfo(), + surfaceExtent = Common::UVec2(clientViewport->GetWidth(), clientViewport->GetHeight()), + presentInfo = clientViewport->GetNextPresentInfo(), + viewport = clientViewport, renderModule = &renderModule, inDeltaTimeSeconds ]() -> void { @@ -54,6 +59,7 @@ namespace Runtime { auto renderer = renderModule->CreateStandardRenderer(rendererParams); renderer.Render(inDeltaTimeSeconds); + viewport->Present(presentInfo); }); } diff --git a/Engine/Source/Runtime/Src/World.cpp b/Engine/Source/Runtime/Src/World.cpp index 9fa201ae..0e72f789 100644 --- a/Engine/Source/Runtime/Src/World.cpp +++ b/Engine/Source/Runtime/Src/World.cpp @@ -75,6 +75,38 @@ namespace Runtime { executor.reset(); } + void World::Activate() + { + Assert(systemSetupContext.playType == PlayType::editor && Stopped() && !executor.has_value()); + executor.emplace(ecRegistry, systemGraph, systemSetupContext); + } + + void World::Deactivate() + { + Assert(systemSetupContext.playType == PlayType::editor && Stopped() && executor.has_value()); + executor.reset(); + } + + bool World::Activated() const + { + return executor.has_value(); + } + + bool World::ShouldTick() const + { + return executor.has_value() && !Paused(); + } + + ECRegistry& World::GetRegistry() + { + return ecRegistry; + } + + const ECRegistry& World::GetRegistry() const + { + return ecRegistry; + } + void World::LoadFrom(AssetPtr inLevel) { Assert(Stopped()); From aede374f655405a44241ffbb3024ad77e8d5a702 Mon Sep 17 00:00:00 2001 From: kindem Date: Sat, 4 Jul 2026 18:04:13 +0800 Subject: [PATCH 10/24] feat: add editor viewport, client and main window with engine tick loop --- Editor/Include/Editor/EditorClient.h | 29 ++++ Editor/Include/Editor/EditorContext.h | 27 ++++ Editor/Include/Editor/Widget/Editor.h | 21 ++- Editor/Include/Editor/Widget/EditorViewport.h | 42 ++++++ Editor/Src/EditorClient.cpp | 38 +++++ Editor/Src/EditorContext.cpp | 21 +++ Editor/Src/Widget/Editor.cpp | 59 +++++++- Editor/Src/Widget/EditorViewport.cpp | 139 ++++++++++++++++++ Editor/Src/Widget/ProjectHub.cpp | 27 +++- .../Source/Runtime/Include/Runtime/Client.h | 3 +- .../Runtime/Include/Runtime/RenderThreadPtr.h | 8 +- .../Source/Runtime/Include/Runtime/Viewport.h | 5 +- Engine/Source/Runtime/Src/System/Render.cpp | 9 +- 13 files changed, 411 insertions(+), 17 deletions(-) create mode 100644 Editor/Include/Editor/EditorClient.h create mode 100644 Editor/Include/Editor/EditorContext.h create mode 100644 Editor/Include/Editor/Widget/EditorViewport.h create mode 100644 Editor/Src/EditorClient.cpp create mode 100644 Editor/Src/EditorContext.cpp create mode 100644 Editor/Src/Widget/EditorViewport.cpp diff --git a/Editor/Include/Editor/EditorClient.h b/Editor/Include/Editor/EditorClient.h new file mode 100644 index 00000000..2bece00b --- /dev/null +++ b/Editor/Include/Editor/EditorClient.h @@ -0,0 +1,29 @@ +// +// Created by johnk on 2026/7/4. +// + +#pragma once + +#include +#include + +namespace Editor { + // connects the edited world with the editor viewport: owns the editor-play-type world and keeps it activated + // (systems ticking without entering play), the viewport widget registers itself here on construction + class EditorClient final : public Runtime::Client { + public: + EditorClient(); + ~EditorClient() override; + + NonCopyable(EditorClient) + NonMovable(EditorClient) + + Runtime::World& GetWorld() override; + Runtime::Viewport* GetViewport() override; + void SetViewport(Runtime::Viewport* inViewport); + + private: + Runtime::World world; + Runtime::Viewport* viewport; + }; +} diff --git a/Editor/Include/Editor/EditorContext.h b/Editor/Include/Editor/EditorContext.h new file mode 100644 index 00000000..6d8a7763 --- /dev/null +++ b/Editor/Include/Editor/EditorContext.h @@ -0,0 +1,27 @@ +// +// Created by johnk on 2026/7/4. +// + +#pragma once + +#include + +#include +#include + +namespace Editor { + // single source of truth for one open editor session: owns the edited world's client, later phases add the + // selection state and change notifications shared across panels + class EditorContext final : public QObject { + Q_OBJECT + + public: + explicit EditorContext(QObject* inParent = nullptr); + ~EditorContext() override; + + EditorClient& GetClient() const; + + private: + Common::UniquePtr client; + }; +} diff --git a/Editor/Include/Editor/Widget/Editor.h b/Editor/Include/Editor/Widget/Editor.h index a11b6353..8c012fd6 100644 --- a/Editor/Include/Editor/Widget/Editor.h +++ b/Editor/Include/Editor/Widget/Editor.h @@ -4,13 +4,30 @@ #pragma once -#include +#include +#include +#include + +#include namespace Editor { - class ExplosionEditor final : public QWidget { + class EditorViewport; + + class ExplosionEditor final : public QMainWindow { Q_OBJECT public: ExplosionEditor(); + ~ExplosionEditor() override; + + private: + void SetupWindow(); + void StartEngineTick(); + void TickEngine(); + + EditorContext* context; + EditorViewport* viewport; + QTimer* tickTimer; + QElapsedTimer frameTimer; }; } diff --git a/Editor/Include/Editor/Widget/EditorViewport.h b/Editor/Include/Editor/Widget/EditorViewport.h new file mode 100644 index 00000000..95e9398e --- /dev/null +++ b/Editor/Include/Editor/Widget/EditorViewport.h @@ -0,0 +1,42 @@ +// +// Created by johnk on 2026/7/4. +// + +#pragma once + +#include +#include +#include + +namespace Editor { + // qt widget hosting the engine-rendered scene: implements Runtime::Viewport over a swapchain created on the + // widget's native window, RenderSystem acquires/presents through this seam every engine tick + class EditorViewport final : public GraphicsWidget, public Runtime::Viewport { + Q_OBJECT + + public: + explicit EditorViewport(EditorClient& inClient, QWidget* inParent = nullptr); + ~EditorViewport() override; + + Runtime::Client& GetClient() override; + Runtime::PresentInfo GetNextPresentInfo() override; + uint32_t GetWidth() const override; + uint32_t GetHeight() const override; + void Resize(uint32_t inWidth, uint32_t inHeight) override; + void Present(const Runtime::PresentInfo& inPresentInfo) override; + + protected: + void resizeEvent(QResizeEvent* inEvent) override; + bool event(QEvent* inEvent) override; + + private: + void RecreateSwapChain(uint32_t inWidth, uint32_t inHeight); + void RecreateSurfaceAndSwapChain(); + void WaitRenderingIdle() const; + + EditorClient& client; + Common::UniquePtr imageReadySemaphore; + Common::UniquePtr renderFinishedSemaphore; + Common::UniquePtr swapChain; + }; +} diff --git a/Editor/Src/EditorClient.cpp b/Editor/Src/EditorClient.cpp new file mode 100644 index 00000000..d0bf3fe1 --- /dev/null +++ b/Editor/Src/EditorClient.cpp @@ -0,0 +1,38 @@ +// +// Created by johnk on 2026/7/4. +// + +#include +#include + +namespace Editor { + EditorClient::EditorClient() + : world("EditorWorld", this, Runtime::PlayType::editor) + , viewport(nullptr) + { + world.SetSystemGraph(Runtime::SystemGraphPresets::Default3DWorld()); + world.Activate(); + } + + EditorClient::~EditorClient() + { + if (world.Activated()) { + world.Deactivate(); + } + } + + Runtime::World& EditorClient::GetWorld() + { + return world; + } + + Runtime::Viewport* EditorClient::GetViewport() + { + return viewport; + } + + void EditorClient::SetViewport(Runtime::Viewport* inViewport) + { + viewport = inViewport; + } +} diff --git a/Editor/Src/EditorContext.cpp b/Editor/Src/EditorContext.cpp new file mode 100644 index 00000000..b3d326b6 --- /dev/null +++ b/Editor/Src/EditorContext.cpp @@ -0,0 +1,21 @@ +// +// Created by johnk on 2026/7/4. +// + +#include +#include // NOLINT + +namespace Editor { + EditorContext::EditorContext(QObject* inParent) + : QObject(inParent) + , client(Common::MakeUnique()) + { + } + + EditorContext::~EditorContext() = default; + + EditorClient& EditorContext::GetClient() const + { + return *client; + } +} diff --git a/Editor/Src/Widget/Editor.cpp b/Editor/Src/Widget/Editor.cpp index e788102b..3152aa4f 100644 --- a/Editor/Src/Widget/Editor.cpp +++ b/Editor/Src/Widget/Editor.cpp @@ -2,9 +2,64 @@ // Created by Kindem on 2025/3/22. // +#include + +#include #include -#include +#include +#include // NOLINT +#include + +namespace Editor::Internal { + constexpr int tickIntervalMs = 16; + + static QString MakeWindowTitle() + { + const std::string projectName = Core::Paths::HasSetGameRoot() + ? Core::Paths::GameRootDir().DirName() + : "Untitled"; + return QString::fromStdString(std::format("{} - Explosion Editor", projectName)); + } +} namespace Editor { - ExplosionEditor::ExplosionEditor() = default; + ExplosionEditor::ExplosionEditor() + : context(new EditorContext(this)) + , viewport(nullptr) + , tickTimer(new QTimer(this)) + { + SetupWindow(); + StartEngineTick(); + } + + ExplosionEditor::~ExplosionEditor() + { + tickTimer->stop(); + // the viewport must unregister from the client before the context (world/client) goes away with the + // remaining children + delete takeCentralWidget(); + viewport = nullptr; + } + + void ExplosionEditor::SetupWindow() + { + setWindowTitle(Internal::MakeWindowTitle()); + resize(1600, 900); + + viewport = new EditorViewport(context->GetClient(), this); + setCentralWidget(viewport); + } + + void ExplosionEditor::StartEngineTick() + { + frameTimer.start(); + connect(tickTimer, &QTimer::timeout, this, [this]() -> void { TickEngine(); }); + tickTimer->start(Internal::tickIntervalMs); + } + + void ExplosionEditor::TickEngine() + { + const float deltaSeconds = static_cast(frameTimer.restart()) / 1000.0f; + Runtime::EngineHolder::Get().Tick(deltaSeconds); + } } // namespace Editor diff --git a/Editor/Src/Widget/EditorViewport.cpp b/Editor/Src/Widget/EditorViewport.cpp new file mode 100644 index 00000000..e22469da --- /dev/null +++ b/Editor/Src/Widget/EditorViewport.cpp @@ -0,0 +1,139 @@ +// +// Created by johnk on 2026/7/4. +// + +#include + +#include + +#include +#include // NOLINT +#include + +// unity builds can pull windows.h (via httplib/qt) into this translation unit after the RHI headers already ran +// their cleanup, so the win32 semaphore macro would mangle RHI::Device::CreateSemaphore below +#undef CreateSemaphore + +namespace Editor { + EditorViewport::EditorViewport(EditorClient& inClient, QWidget* inParent) + : GraphicsWidget(inParent) + , client(inClient) + , imageReadySemaphore(GetDevice().CreateSemaphore()) + , renderFinishedSemaphore(GetDevice().CreateSemaphore()) + { + setMinimumSize(320, 240); + setFocusPolicy(Qt::StrongFocus); + RecreateSwapChain(GetWidth(), GetHeight()); + client.SetViewport(this); + } + + EditorViewport::~EditorViewport() + { + client.SetViewport(nullptr); + WaitRenderingIdle(); + swapChain.Reset(); + } + + Runtime::Client& EditorViewport::GetClient() + { + return client; + } + + Runtime::PresentInfo EditorViewport::GetNextPresentInfo() + { + const auto backTextureIndex = swapChain->AcquireBackTexture(imageReadySemaphore.Get()); + + Runtime::PresentInfo result; + result.backTexture = swapChain->GetTexture(backTextureIndex); + result.imageReadySemaphore = imageReadySemaphore.Get(); + result.renderFinishedSemaphore = renderFinishedSemaphore.Get(); + return result; + } + + uint32_t EditorViewport::GetWidth() const + { + return static_cast(std::max(width(), 1)); + } + + uint32_t EditorViewport::GetHeight() const + { + return static_cast(std::max(height(), 1)); + } + + void EditorViewport::Resize(uint32_t inWidth, uint32_t inHeight) + { + RecreateSwapChain(inWidth, inHeight); + } + + void EditorViewport::Present(const Runtime::PresentInfo& inPresentInfo) + { + swapChain->Present(inPresentInfo.renderFinishedSemaphore); + } + + void EditorViewport::resizeEvent(QResizeEvent* inEvent) + { + GraphicsWidget::resizeEvent(inEvent); + if (const QSize size = inEvent->size(); + swapChain.Valid() && size.width() > 0 && size.height() > 0) { + RecreateSwapChain(static_cast(size.width()), static_cast(size.height())); + } + } + + bool EditorViewport::event(QEvent* inEvent) + { + // docking/floating the widget can recreate its native window, the surface and swapchain bound to the old + // window handle must be rebuilt on the new one + if (inEvent->type() == QEvent::WinIdChange && swapChain.Valid()) { + RecreateSurfaceAndSwapChain(); + } + return GraphicsWidget::event(inEvent); + } + + void EditorViewport::RecreateSwapChain(uint32_t inWidth, uint32_t inHeight) + { + static std::vector formatQualifiers = { + RHI::PixelFormat::rgba8Unorm, + RHI::PixelFormat::bgra8Unorm + }; + + WaitRenderingIdle(); + if (swapChain.Valid()) { + swapChain.Reset(); + } + + std::optional pixelFormat = {}; + for (const auto format : formatQualifiers) { + if (GetDevice().CheckSwapChainFormatSupport(&GetSurface(), format, RHI::ColorSpace::srgbNonLinear)) { + pixelFormat = format; + break; + } + } + Assert(pixelFormat.has_value()); + + swapChain = GetDevice().CreateSwapChain( + RHI::SwapChainCreateInfo() + .SetPresentQueue(GetDevice().GetQueue(RHI::QueueType::graphics, 0)) + .SetSurface(&GetSurface()) + .SetTextureNum(2) + .SetFormat(pixelFormat.value()) + .SetWidth(inWidth) + .SetHeight(inHeight) + .SetPresentMode(RHI::PresentMode::immediately)); + } + + void EditorViewport::RecreateSurfaceAndSwapChain() + { + WaitRenderingIdle(); + swapChain.Reset(); + surface = device->CreateSurface( + RHI::SurfaceCreateInfo() + .SetWindow(reinterpret_cast(winId()))); // NOLINT + RecreateSwapChain(GetWidth(), GetHeight()); + } + + void EditorViewport::WaitRenderingIdle() const + { + Runtime::EngineHolder::Get().GetRenderModule().GetRenderThread().Flush(); + WaitDeviceIdle(); + } +} diff --git a/Editor/Src/Widget/ProjectHub.cpp b/Editor/Src/Widget/ProjectHub.cpp index 8bfbd390..28f60398 100644 --- a/Editor/Src/Widget/ProjectHub.cpp +++ b/Editor/Src/Widget/ProjectHub.cpp @@ -2,9 +2,12 @@ // Created by johnk on 2025/8/3. // +#include #include +#include #include +#include #include #include @@ -127,10 +130,28 @@ namespace Editor { return Internal::ToJsonValue({ .success = true, .error = {}, .projectPath = projectDir.String() }); } - void ProjectHubBackend::OpenProject(const QString& inProjectPath) // NOLINT + void ProjectHubBackend::OpenProject(const QString& inProjectPath) { - // TODO: launch the editor for the project at inProjectPath - LogInfo(ProjectHub, "open project '{}'", inProjectPath.toStdString()); + const std::string projectPath = inProjectPath.toStdString(); + if (const Common::Path projectDir(projectPath); + !projectDir.Exists() || !projectDir.IsDirectory()) { + LogWarning(ProjectHub, "project '{}' does not exist", projectPath); + return; + } + + std::string projectName = Common::Path(projectPath).DirName(); + if (const auto iter = std::ranges::find_if(recentProjects, [&](const RecentProjectInfo& info) -> bool { return info.path == projectPath; }); + iter != recentProjects.end()) { + projectName = iter->name; + recentProjects.erase(iter); + } + recentProjects.emplace_back(RecentProjectInfo { projectName, projectPath }); + Common::JsonSerializeToFile(recentProjectsFile.String(), recentProjects); + emit RecentProjectsChanged(); + + LogInfo(ProjectHub, "opening project '{}'", projectPath); + QProcess::startDetached(QCoreApplication::applicationFilePath(), { "-project", inProjectPath }); + QCoreApplication::quit(); } QString ProjectHubBackend::BrowseDirectory() const // NOLINT diff --git a/Engine/Source/Runtime/Include/Runtime/Client.h b/Engine/Source/Runtime/Include/Runtime/Client.h index cd05f425..1bda9e1f 100644 --- a/Engine/Source/Runtime/Include/Runtime/Client.h +++ b/Engine/Source/Runtime/Include/Runtime/Client.h @@ -5,11 +5,12 @@ #pragma once #include +#include namespace Runtime { class World; - class Client { + class RUNTIME_API Client { public: virtual ~Client(); diff --git a/Engine/Source/Runtime/Include/Runtime/RenderThreadPtr.h b/Engine/Source/Runtime/Include/Runtime/RenderThreadPtr.h index 0144a9f4..2d70a44c 100644 --- a/Engine/Source/Runtime/Include/Runtime/RenderThreadPtr.h +++ b/Engine/Source/Runtime/Include/Runtime/RenderThreadPtr.h @@ -70,14 +70,16 @@ namespace Runtime { template T* RenderThreadPtr::operator->() const noexcept { - Assert(Core::ThreadContext::IsGameThread()); + // systems are constructed and ticked on game worker threads by the system graph executor, both tags belong + // to the game-side frame timeline + Assert(Core::ThreadContext::IsGameThread() || Core::ThreadContext::IsGameWorkerThread()); return ptr.Get(); } template T& RenderThreadPtr::operator*() const noexcept { - Assert(Core::ThreadContext::IsGameThread()); + Assert(Core::ThreadContext::IsGameThread() || Core::ThreadContext::IsGameWorkerThread()); return *ptr; } @@ -108,7 +110,7 @@ namespace Runtime { template T* RenderThreadPtr::Get() const { - Assert(Core::ThreadContext::IsGameThread()); + Assert(Core::ThreadContext::IsGameThread() || Core::ThreadContext::IsGameWorkerThread()); return ptr.Get(); } diff --git a/Engine/Source/Runtime/Include/Runtime/Viewport.h b/Engine/Source/Runtime/Include/Runtime/Viewport.h index 28f8ecf4..2864741f 100644 --- a/Engine/Source/Runtime/Include/Runtime/Viewport.h +++ b/Engine/Source/Runtime/Include/Runtime/Viewport.h @@ -6,11 +6,12 @@ #include #include +#include namespace Runtime { class Client; - struct PresentInfo { + struct RUNTIME_API PresentInfo { PresentInfo(); RHI::Texture* backTexture; @@ -18,7 +19,7 @@ namespace Runtime { RHI::Semaphore* renderFinishedSemaphore; }; - class Viewport { + class RUNTIME_API Viewport { public: virtual ~Viewport(); diff --git a/Engine/Source/Runtime/Src/System/Render.cpp b/Engine/Source/Runtime/Src/System/Render.cpp index 2d4f5279..8303e198 100644 --- a/Engine/Source/Runtime/Src/System/Render.cpp +++ b/Engine/Source/Runtime/Src/System/Render.cpp @@ -91,12 +91,13 @@ namespace Runtime { result.reserve(playerNum); for (auto i = 0; i < playerNum; i++) { - if (registry.Has(i)) { - result.emplace_back(BuildViewForPlayer(playersInfo.players[i], playerNum, i)); + const auto playerEntity = playersInfo.players[i]; + if (registry.Has(playerEntity)) { + result.emplace_back(BuildViewForPlayer(playerEntity, playerNum, i)); } #if BUILD_EDITOR - if (registry.Has(i)) { - result.emplace_back(BuildViewForPlayer(playersInfo.players[i], playerNum, i)); + if (registry.Has(playerEntity)) { + result.emplace_back(BuildViewForPlayer(playerEntity, playerNum, i)); } #endif } From 43da34c940345f371985d18ed8eb15e4d5206bea Mon Sep 17 00:00:00 2001 From: kindem Date: Sat, 4 Jul 2026 18:11:32 +0800 Subject: [PATCH 11/24] fix: serialize per-frame gpu work and acquire back texture on render thread --- Engine/Source/Runtime/Include/Runtime/Viewport.h | 2 ++ Engine/Source/Runtime/Src/System/Render.cpp | 6 ++++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/Engine/Source/Runtime/Include/Runtime/Viewport.h b/Engine/Source/Runtime/Include/Runtime/Viewport.h index 2864741f..ca1ab0db 100644 --- a/Engine/Source/Runtime/Include/Runtime/Viewport.h +++ b/Engine/Source/Runtime/Include/Runtime/Viewport.h @@ -24,6 +24,8 @@ namespace Runtime { virtual ~Viewport(); virtual Client& GetClient() = 0; + // called on render thread after the previous frame's gpu work finished, acquires the next back texture and + // returns the semaphores guarding it virtual PresentInfo GetNextPresentInfo() = 0; virtual uint32_t GetWidth() const = 0; virtual uint32_t GetHeight() const = 0; diff --git a/Engine/Source/Runtime/Src/System/Render.cpp b/Engine/Source/Runtime/Src/System/Render.cpp index 8303e198..74d8a31f 100644 --- a/Engine/Source/Runtime/Src/System/Render.cpp +++ b/Engine/Source/Runtime/Src/System/Render.cpp @@ -39,13 +39,12 @@ namespace Runtime { views = BuildViews(), scene = registry.GGet().scene.Get(), surfaceExtent = Common::UVec2(clientViewport->GetWidth(), clientViewport->GetHeight()), - presentInfo = clientViewport->GetNextPresentInfo(), viewport = clientViewport, renderModule = &renderModule, inDeltaTimeSeconds ]() -> void { - fence->Wait(); fence->Reset(); + const auto presentInfo = viewport->GetNextPresentInfo(); Render::StandardRenderer::Params rendererParams; rendererParams.device = renderModule->GetDevice(); @@ -60,6 +59,9 @@ namespace Runtime { auto renderer = renderModule->CreateStandardRenderer(rendererParams); renderer.Render(inDeltaTimeSeconds); viewport->Present(presentInfo); + // this frame's gpu work must finish before the renderer (and its recorded command buffers / + // transient resources) goes out of scope, and it keeps the shared semaphores safe to reuse + fence->Wait(); }); } From 0989eb019b84fbfd0f627230b531b37b9f0cdeaf Mon Sep 17 00:00:00 2001 From: kindem Date: Sun, 5 Jul 2026 00:05:16 +0800 Subject: [PATCH 12/24] fix: delete editor viewport directly to avoid native window recreation on teardown --- Editor/Src/Widget/Editor.cpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Editor/Src/Widget/Editor.cpp b/Editor/Src/Widget/Editor.cpp index 3152aa4f..daa2fc85 100644 --- a/Editor/Src/Widget/Editor.cpp +++ b/Editor/Src/Widget/Editor.cpp @@ -35,9 +35,10 @@ namespace Editor { ExplosionEditor::~ExplosionEditor() { tickTimer->stop(); - // the viewport must unregister from the client before the context (world/client) goes away with the - // remaining children - delete takeCentralWidget(); + // deleted directly without takeCentralWidget(): reparenting would recreate the native window and trigger a + // pointless surface rebuild mid-teardown, and the viewport must unregister from the client before the + // context (world/client) goes away with the remaining children + delete viewport; viewport = nullptr; } From 889ece8ddb1cb991428e35c2a16fda03970f2fb2 Mon Sep 17 00:00:00 2001 From: kindem Date: Sun, 5 Jul 2026 10:12:38 +0800 Subject: [PATCH 13/24] feat: implement static mesh unlit render path with vertex factory and primitive scene proxy --- Editor/Include/Editor/EditorClient.h | 6 + Editor/Include/Editor/Widget/EditorViewport.h | 19 +++ Editor/Src/EditorClient.cpp | 140 +++++++++++++++- Editor/Src/Widget/EditorViewport.cpp | 133 +++++++++++++++ Engine/Shader/BasePassPS.esl | 17 ++ Engine/Shader/BasePassVS.esl | 25 +++ .../StaticMesh/VertexFactory.esh | 16 ++ .../Render/Include/Render/MeshRenderData.h | 41 +++++ Engine/Source/Render/Include/Render/Scene.h | 25 ++- .../Include/Render/SceneProxy/Primitive.h | 26 ++- Engine/Source/Render/Include/Render/Shader.h | 1 + .../Render/Include/Render/VertexFactory.h | 22 +++ Engine/Source/Render/Src/MeshRenderData.cpp | 49 ++++++ Engine/Source/Render/Src/Renderer.cpp | 151 +++++++++++++++++- Engine/Source/Render/Src/Shader.cpp | 15 ++ Engine/Source/Render/Src/VertexFactory.cpp | 9 ++ .../Runtime/Include/Runtime/Asset/Material.h | 4 + .../Runtime/Include/Runtime/Asset/Mesh.h | 7 + .../Runtime/Include/Runtime/System/Scene.h | 70 ++++++-- Engine/Source/Runtime/Src/Asset/Material.cpp | 47 +++++- Engine/Source/Runtime/Src/Asset/Mesh.cpp | 25 +++ Engine/Source/Runtime/Src/System/Scene.cpp | 16 +- 22 files changed, 841 insertions(+), 23 deletions(-) create mode 100644 Engine/Shader/VertexFactory/StaticMesh/VertexFactory.esh create mode 100644 Engine/Source/Render/Include/Render/MeshRenderData.h create mode 100644 Engine/Source/Render/Include/Render/VertexFactory.h create mode 100644 Engine/Source/Render/Src/MeshRenderData.cpp create mode 100644 Engine/Source/Render/Src/VertexFactory.cpp diff --git a/Editor/Include/Editor/EditorClient.h b/Editor/Include/Editor/EditorClient.h index 2bece00b..59787810 100644 --- a/Editor/Include/Editor/EditorClient.h +++ b/Editor/Include/Editor/EditorClient.h @@ -4,6 +4,7 @@ #pragma once +#include #include #include @@ -21,8 +22,13 @@ namespace Editor { Runtime::World& GetWorld() override; Runtime::Viewport* GetViewport() override; void SetViewport(Runtime::Viewport* inViewport); + // loads the project's main level when present, otherwise authors the default level content (player start, + // ground and a cube using engine-default unlit assets materialized into the project) and saves it + void OpenProjectLevel(); + void SaveLevel(); private: + Core::Uri levelUri; Runtime::World world; Runtime::Viewport* viewport; }; diff --git a/Editor/Include/Editor/Widget/EditorViewport.h b/Editor/Include/Editor/Widget/EditorViewport.h index 95e9398e..a6c304d3 100644 --- a/Editor/Include/Editor/Widget/EditorViewport.h +++ b/Editor/Include/Editor/Widget/EditorViewport.h @@ -4,6 +4,8 @@ #pragma once +#include + #include #include #include @@ -24,19 +26,36 @@ namespace Editor { uint32_t GetHeight() const override; void Resize(uint32_t inWidth, uint32_t inHeight) override; void Present(const Runtime::PresentInfo& inPresentInfo) override; + // applies wasd/right-mouse-look input to the editor player's camera entity, called on the game thread right + // before the engine tick + void TickEditorCamera(float inDeltaSeconds); protected: void resizeEvent(QResizeEvent* inEvent) override; bool event(QEvent* inEvent) override; + void keyPressEvent(QKeyEvent* inEvent) override; + void keyReleaseEvent(QKeyEvent* inEvent) override; + void mousePressEvent(QMouseEvent* inEvent) override; + void mouseMoveEvent(QMouseEvent* inEvent) override; + void mouseReleaseEvent(QMouseEvent* inEvent) override; + void focusOutEvent(QFocusEvent* inEvent) override; private: void RecreateSwapChain(uint32_t inWidth, uint32_t inHeight); void RecreateSurfaceAndSwapChain(); void WaitRenderingIdle() const; + Common::FVec3 CameraMoveInput() const; EditorClient& client; Common::UniquePtr imageReadySemaphore; Common::UniquePtr renderFinishedSemaphore; Common::UniquePtr swapChain; + + std::unordered_set pressedKeys; + bool cameraLooking; + bool cameraAnglesInitialized; + float cameraYaw; + float cameraPitch; + QPoint lastMousePos; }; } diff --git a/Editor/Src/EditorClient.cpp b/Editor/Src/EditorClient.cpp index d0bf3fe1..640badf3 100644 --- a/Editor/Src/EditorClient.cpp +++ b/Editor/Src/EditorClient.cpp @@ -2,16 +2,131 @@ // Created by johnk on 2026/7/4. // +#include + #include +#include +#include +#include +#include +#include +#include +#include #include +namespace Editor::Internal { + constexpr const char* mainLevelUri = "asset://Game/Maps/Main"; + constexpr const char* defaultUnlitMaterialUri = "asset://Game/Materials/DefaultUnlit"; + constexpr const char* defaultUnlitInstanceUri = "asset://Game/Materials/DefaultUnlitInstance"; + constexpr const char* cubeMeshUri = "asset://Game/Meshes/Cube"; + + static bool AssetExists(const Core::Uri& inUri) + { + return Core::AssetUriParser(inUri).Parse().Exists(); + } + + template + static void SaveAsset(const Runtime::AssetPtr& inAsset) + { + if (const Common::Path parentDir = Core::AssetUriParser(inAsset->Uri()).Parse().Parent(); + !parentDir.Exists()) { + parentDir.MakeDir(); + } + Runtime::AssetManager::Get().Save(inAsset); + } + + static Runtime::StaticMeshVertices BuildCubeVertices() + { + // 24 vertices (4 per face) so each face gets its own uv space, z-up unit cube centered at origin + static constexpr float h = 0.5f; + static constexpr std::array, 6> facePositions = {{ + {{ { -h, -h, h }, { h, -h, h }, { h, h, h }, { -h, h, h } }}, // +z + {{ { -h, h, -h }, { h, h, -h }, { h, -h, -h }, { -h, -h, -h } }}, // -z + {{ { h, -h, -h }, { h, h, -h }, { h, h, h }, { h, -h, h } }}, // +x + {{ { -h, h, -h }, { -h, -h, -h }, { -h, -h, h }, { -h, h, h } }}, // -x + {{ { h, h, -h }, { -h, h, -h }, { -h, h, h }, { h, h, h } }}, // +y + {{ { -h, -h, -h }, { h, -h, -h }, { h, -h, h }, { -h, -h, h } }} // -y + }}; + + Runtime::StaticMeshVertices result; + for (const auto& face : facePositions) { + const auto baseVertex = static_cast(result.positions.size()); + for (size_t corner = 0; corner < 4; corner++) { + result.positions.emplace_back(face[corner]); + result.tangents.emplace_back(0.0f, 0.0f, 1.0f); + result.uv0.emplace_back(corner == 1 || corner == 2 ? 1.0f : 0.0f, corner >= 2 ? 1.0f : 0.0f); + } + for (const uint32_t index : { 0u, 1u, 2u, 0u, 2u, 3u }) { + result.indices.emplace_back(baseVertex + index); + } + } + result.vertexCount = static_cast(result.positions.size()); + result.indexCount = static_cast(result.indices.size()); + return result; + } + + static Runtime::AssetPtr EnsureDefaultCubeMesh() + { + auto& assetManager = Runtime::AssetManager::Get(); + if (AssetExists(cubeMeshUri)) { + return assetManager.SyncLoad(cubeMeshUri, Mirror::Class::Get()); + } + + Runtime::AssetPtr material = new Runtime::Material(defaultUnlitMaterialUri); + material->SetType(Runtime::MaterialType::surface); + material->SetSource("float4 GetBaseColor()\n{\n return baseColor;\n}\n"); + Runtime::MaterialFVec4ParameterField baseColorField; + baseColorField.defaultValue = Common::FVec4(1.0f, 1.0f, 1.0f, 1.0f); + material->EmplaceParameterField("baseColor") = baseColorField; + material->Update(); + SaveAsset(material); + + Runtime::AssetPtr materialInstance = new Runtime::MaterialInstance(defaultUnlitInstanceUri); + materialInstance->SetMaterial(material); + materialInstance->SetParameter("baseColor", Common::FVec4(0.9f, 0.6f, 0.2f, 1.0f)); + SaveAsset(materialInstance); + + Runtime::AssetPtr cubeMesh = new Runtime::StaticMesh(cubeMeshUri); + cubeMesh->SetMaterial(materialInstance); + cubeMesh->EmplaceLOD().vertices = BuildCubeVertices(); + SaveAsset(cubeMesh); + return cubeMesh; + } + + static void AuthorDefaultLevelContent(Runtime::ECRegistry& inRegistry) + { + const Runtime::AssetPtr cubeMesh = EnsureDefaultCubeMesh(); + + const auto ground = inRegistry.Create(); + Runtime::WorldTransform groundTransform; + groundTransform.localToWorld.scale = Common::FVec3(10.0f, 10.0f, 0.2f); + groundTransform.localToWorld.translation = Common::FVec3(0.0f, 0.0f, -0.1f); + inRegistry.Emplace(ground, groundTransform); + auto& groundPrimitive = inRegistry.Emplace(ground); + groundPrimitive.mesh = cubeMesh; + + const auto cube = inRegistry.Create(); + Runtime::WorldTransform cubeTransform; + cubeTransform.localToWorld.translation = Common::FVec3(0.0f, 0.0f, 0.5f); + inRegistry.Emplace(cube, cubeTransform); + auto& cubePrimitive = inRegistry.Emplace(cube); + cubePrimitive.mesh = cubeMesh; + + const auto playerStart = inRegistry.Create(); + Runtime::WorldTransform playerStartTransform; + playerStartTransform.localToWorld = Common::FTransform::LookAt(Common::FVec3(-5.0f, -6.0f, 4.0f), Common::FVec3(0.0f, 0.0f, 0.5f)); + inRegistry.Emplace(playerStart, playerStartTransform); + inRegistry.Emplace(playerStart); + } +} + namespace Editor { EditorClient::EditorClient() - : world("EditorWorld", this, Runtime::PlayType::editor) + : levelUri(Internal::mainLevelUri) + , world("EditorWorld", this, Runtime::PlayType::editor) , viewport(nullptr) { world.SetSystemGraph(Runtime::SystemGraphPresets::Default3DWorld()); - world.Activate(); } EditorClient::~EditorClient() @@ -35,4 +150,25 @@ namespace Editor { { viewport = inViewport; } + + void EditorClient::OpenProjectLevel() + { + Assert(!world.Activated()); + if (Internal::AssetExists(levelUri)) { + const auto level = Runtime::AssetManager::Get().SyncLoad(levelUri, Mirror::Class::Get()); + world.LoadFrom(level); + world.Activate(); + } else { + Internal::AuthorDefaultLevelContent(world.GetRegistry()); + world.Activate(); + SaveLevel(); + } + } + + void EditorClient::SaveLevel() + { + Runtime::AssetPtr level = new Runtime::Level(levelUri); + world.SaveTo(level); + Internal::SaveAsset(level); + } } diff --git a/Editor/Src/Widget/EditorViewport.cpp b/Editor/Src/Widget/EditorViewport.cpp index e22469da..3c720f51 100644 --- a/Editor/Src/Widget/EditorViewport.cpp +++ b/Editor/Src/Widget/EditorViewport.cpp @@ -3,23 +3,48 @@ // #include +#include +#include +#include #include #include #include // NOLINT +#include +#include #include // unity builds can pull windows.h (via httplib/qt) into this translation unit after the RHI headers already ran // their cleanup, so the win32 semaphore macro would mangle RHI::Device::CreateSemaphore below #undef CreateSemaphore +namespace Editor::Internal { + constexpr float cameraMoveSpeed = 5.0f; + constexpr float cameraLookSpeedDegrees = 0.2f; + constexpr float maxCameraPitchDegrees = 89.0f; + constexpr float degToRad = 3.14159265358979323846f / 180.0f; + + static Common::FVec3 DirectionFromYawPitch(float inYawRadians, float inPitchRadians) + { + return { + std::cos(inPitchRadians) * std::cos(inYawRadians), + std::cos(inPitchRadians) * std::sin(inYawRadians), + std::sin(inPitchRadians) + }; + } +} + namespace Editor { EditorViewport::EditorViewport(EditorClient& inClient, QWidget* inParent) : GraphicsWidget(inParent) , client(inClient) , imageReadySemaphore(GetDevice().CreateSemaphore()) , renderFinishedSemaphore(GetDevice().CreateSemaphore()) + , cameraLooking(false) + , cameraAnglesInitialized(false) + , cameraYaw(0.0f) + , cameraPitch(0.0f) { setMinimumSize(320, 240); setFocusPolicy(Qt::StrongFocus); @@ -70,6 +95,45 @@ namespace Editor { swapChain->Present(inPresentInfo.renderFinishedSemaphore); } + void EditorViewport::TickEditorCamera(float inDeltaSeconds) + { + auto& world = client.GetWorld(); + if (!world.Activated()) { + return; + } + auto& registry = world.GetRegistry(); + const auto players = registry.View().All(); + if (players.empty()) { + return; + } + const auto playerEntity = std::get<0>(players[0]); + + if (!cameraAnglesInitialized) { + const auto forward = registry.Get(playerEntity).localToWorld.GetRotationMatrix().Col(2); + cameraYaw = std::atan2(forward.y, forward.x); + cameraPitch = std::asin(std::clamp(forward.z, -1.0f, 1.0f)); + cameraAnglesInitialized = true; + } + + const Common::FVec3 moveInput = CameraMoveInput(); + const bool moving = moveInput.x != 0.0f || moveInput.y != 0.0f || moveInput.z != 0.0f; + if (!moving && !cameraLooking) { + return; + } + + const Common::FVec3 forward = Internal::DirectionFromYawPitch(cameraYaw, cameraPitch); + Common::FVec3 right = Common::FVec3(0.0f, 0.0f, 1.0f).Cross(forward); + right.Normalize(); + + registry.Update(playerEntity, [&](Runtime::WorldTransform& transform) -> void { + const float moveDelta = Internal::cameraMoveSpeed * inDeltaSeconds; + transform.localToWorld.translation += forward * (moveInput.x * moveDelta); + transform.localToWorld.translation += right * (moveInput.y * moveDelta); + transform.localToWorld.translation += Common::FVec3(0.0f, 0.0f, 1.0f) * (moveInput.z * moveDelta); + transform.localToWorld.LookTo(transform.localToWorld.translation + forward); + }); + } + void EditorViewport::resizeEvent(QResizeEvent* inEvent) { GraphicsWidget::resizeEvent(inEvent); @@ -89,6 +153,63 @@ namespace Editor { return GraphicsWidget::event(inEvent); } + void EditorViewport::keyPressEvent(QKeyEvent* inEvent) + { + if (!inEvent->isAutoRepeat()) { + pressedKeys.insert(inEvent->key()); + } + GraphicsWidget::keyPressEvent(inEvent); + } + + void EditorViewport::keyReleaseEvent(QKeyEvent* inEvent) + { + if (!inEvent->isAutoRepeat()) { + pressedKeys.erase(inEvent->key()); + } + GraphicsWidget::keyReleaseEvent(inEvent); + } + + void EditorViewport::mousePressEvent(QMouseEvent* inEvent) + { + if (inEvent->button() == Qt::RightButton) { + cameraLooking = true; + lastMousePos = inEvent->pos(); + setCursor(Qt::BlankCursor); + } + GraphicsWidget::mousePressEvent(inEvent); + } + + void EditorViewport::mouseMoveEvent(QMouseEvent* inEvent) + { + if (cameraLooking) { + const QPoint delta = inEvent->pos() - lastMousePos; + lastMousePos = inEvent->pos(); + + cameraYaw -= static_cast(delta.x()) * Internal::cameraLookSpeedDegrees * Internal::degToRad; + cameraPitch -= static_cast(delta.y()) * Internal::cameraLookSpeedDegrees * Internal::degToRad; + const float maxPitch = Internal::maxCameraPitchDegrees * Internal::degToRad; + cameraPitch = std::clamp(cameraPitch, -maxPitch, maxPitch); + } + GraphicsWidget::mouseMoveEvent(inEvent); + } + + void EditorViewport::mouseReleaseEvent(QMouseEvent* inEvent) + { + if (inEvent->button() == Qt::RightButton) { + cameraLooking = false; + unsetCursor(); + } + GraphicsWidget::mouseReleaseEvent(inEvent); + } + + void EditorViewport::focusOutEvent(QFocusEvent* inEvent) + { + pressedKeys.clear(); + cameraLooking = false; + unsetCursor(); + GraphicsWidget::focusOutEvent(inEvent); + } + void EditorViewport::RecreateSwapChain(uint32_t inWidth, uint32_t inHeight) { static std::vector formatQualifiers = { @@ -136,4 +257,16 @@ namespace Editor { Runtime::EngineHolder::Get().GetRenderModule().GetRenderThread().Flush(); WaitDeviceIdle(); } + + Common::FVec3 EditorViewport::CameraMoveInput() const + { + Common::FVec3 result(0.0f, 0.0f, 0.0f); + if (pressedKeys.contains(Qt::Key_W)) { result.x += 1.0f; } + if (pressedKeys.contains(Qt::Key_S)) { result.x -= 1.0f; } + if (pressedKeys.contains(Qt::Key_D)) { result.y += 1.0f; } + if (pressedKeys.contains(Qt::Key_A)) { result.y -= 1.0f; } + if (pressedKeys.contains(Qt::Key_E)) { result.z += 1.0f; } + if (pressedKeys.contains(Qt::Key_Q)) { result.z -= 1.0f; } + return result; + } } diff --git a/Engine/Shader/BasePassPS.esl b/Engine/Shader/BasePassPS.esl index e69de29b..9945a2a0 100644 --- a/Engine/Shader/BasePassPS.esl +++ b/Engine/Shader/BasePassPS.esl @@ -0,0 +1,17 @@ +#include + +VkBinding(1, 0) cbuffer materialUniform : register(b0) { + float4 baseColor; +}; + +#include + +struct FragmentInput { + float4 position : SV_POSITION; + float2 uv0 : TEXCOORD0; +}; + +float4 PSMain(FragmentInput input) : SV_TARGET +{ + return GetBaseColor(); +} diff --git a/Engine/Shader/BasePassVS.esl b/Engine/Shader/BasePassVS.esl index e69de29b..2224fdff 100644 --- a/Engine/Shader/BasePassVS.esl +++ b/Engine/Shader/BasePassVS.esl @@ -0,0 +1,25 @@ +#include +#include + +VkBinding(0, 0) cbuffer vsUniform : register(b0) { + row_major float4x4 localToWorld; + row_major float4x4 worldToClip; +}; + +struct FragmentInput { + float4 position : SV_POSITION; + float2 uv0 : TEXCOORD0; +}; + +FragmentInput VSMain(VertexFactoryInput vfInput) +{ + const float4 worldPosition = mul(localToWorld, float4(GetLocalPosition(vfInput), 1.0f)); + + FragmentInput output; + output.position = mul(worldToClip, worldPosition); +#if VULKAN + output.position.y = - output.position.y; +#endif + output.uv0 = GetUv0(vfInput); + return output; +} diff --git a/Engine/Shader/VertexFactory/StaticMesh/VertexFactory.esh b/Engine/Shader/VertexFactory/StaticMesh/VertexFactory.esh new file mode 100644 index 00000000..00ecd118 --- /dev/null +++ b/Engine/Shader/VertexFactory/StaticMesh/VertexFactory.esh @@ -0,0 +1,16 @@ +#include + +struct VertexFactoryInput { + VkLocation(0) float3 position : POSITION; + VkLocation(1) float2 uv0 : TEXCOORD0; +}; + +float3 GetLocalPosition(VertexFactoryInput input) +{ + return input.position; +} + +float2 GetUv0(VertexFactoryInput input) +{ + return input.uv0; +} diff --git a/Engine/Source/Render/Include/Render/MeshRenderData.h b/Engine/Source/Render/Include/Render/MeshRenderData.h new file mode 100644 index 00000000..8e9209bd --- /dev/null +++ b/Engine/Source/Render/Include/Render/MeshRenderData.h @@ -0,0 +1,41 @@ +// +// Created by johnk on 2026/7/5. +// + +#pragma once + +#include + +#include +#include +#include +#include + +namespace Render { + // gpu geometry for a single static mesh lod, vertex layout matches StaticMeshVertexFactory (position + uv0 + // interleaved), created and destroyed on the render thread and shared between scene proxies + class MeshRenderData { + public: + struct Vertex { + Common::FVec3 position; + Common::FVec2 uv0; + }; + + static constexpr size_t vertexStride = sizeof(Vertex); + + MeshRenderData(RHI::Device& inDevice, const std::vector& inVertices, const std::vector& inIndices); + ~MeshRenderData(); + + NonCopyable(MeshRenderData) + NonMovable(MeshRenderData) + + RHI::Buffer* GetVertexBuffer() const; + RHI::Buffer* GetIndexBuffer() const; + uint32_t GetIndexCount() const; + + private: + Common::UniquePtr vertexBuffer; + Common::UniquePtr indexBuffer; + uint32_t indexCount; + }; +} diff --git a/Engine/Source/Render/Include/Render/Scene.h b/Engine/Source/Render/Include/Render/Scene.h index f3ba5308..bd14da42 100644 --- a/Engine/Source/Render/Include/Render/Scene.h +++ b/Engine/Source/Render/Include/Render/Scene.h @@ -7,6 +7,7 @@ #include #include #include +#include namespace Render { // Render::Scene is a container of render-thread world data copy. @@ -14,6 +15,7 @@ namespace Render { class Scene final { public: using EntityId = uint32_t; + template using SceneProxyContainer = std::unordered_map; Scene(); ~Scene(); @@ -25,14 +27,14 @@ namespace Render { template SP& Get(EntityId inEntity); template const SP& Get(EntityId inEntity) const; template void Remove(EntityId inEntity); + template const SceneProxyContainer& All() const; private: - template using SceneProxyContainer = std::unordered_map; - template SceneProxyContainer& GetSceneProxyContainer(); template const SceneProxyContainer& GetSceneProxyContainer() const; SceneProxyContainer lightSceneProxies; + SceneProxyContainer primitiveSceneProxies; }; } @@ -65,6 +67,13 @@ namespace Render { GetSceneProxyContainer().erase(inEntity); } + template + const Scene::SceneProxyContainer& Scene::All() const + { + Assert(Core::ThreadContext::IsRenderThread()); + return GetSceneProxyContainer(); + } + template Scene::SceneProxyContainer& Scene::GetSceneProxyContainer() { @@ -92,4 +101,16 @@ namespace Render { { return lightSceneProxies; } + + template <> + inline Scene::SceneProxyContainer& Scene::GetSceneProxyContainer() + { + return primitiveSceneProxies; + } + + template <> + inline const Scene::SceneProxyContainer& Scene::GetSceneProxyContainer() const + { + return primitiveSceneProxies; + } } diff --git a/Engine/Source/Render/Include/Render/SceneProxy/Primitive.h b/Engine/Source/Render/Include/Render/SceneProxy/Primitive.h index 6e3543b4..ca96a356 100644 --- a/Engine/Source/Render/Include/Render/SceneProxy/Primitive.h +++ b/Engine/Source/Render/Include/Render/SceneProxy/Primitive.h @@ -4,12 +4,34 @@ #pragma once -#include +#include +#include +#include +#include namespace Render { + class MaterialShaderType; + class VertexFactoryType; + struct PrimitiveSceneProxy { PrimitiveSceneProxy(); - // TODO + Common::FMat4x4 localToWorld; + Common::SharedPtr mesh; + const VertexFactoryType* vertexFactoryType; + const MaterialShaderType* vertexShaderType; + const MaterialShaderType* pixelShaderType; + Common::FVec4 baseColor; }; } + +namespace Render { + inline PrimitiveSceneProxy::PrimitiveSceneProxy() + : localToWorld(Common::FMat4x4Consts::identity) + , vertexFactoryType(nullptr) + , vertexShaderType(nullptr) + , pixelShaderType(nullptr) + , baseColor(1.0f, 1.0f, 1.0f, 1.0f) + { + } +} diff --git a/Engine/Source/Render/Include/Render/Shader.h b/Engine/Source/Render/Include/Render/Shader.h index 015b5fc3..8400ab66 100644 --- a/Engine/Source/Render/Include/Render/Shader.h +++ b/Engine/Source/Render/Include/Render/Shader.h @@ -370,6 +370,7 @@ namespace Render { ~ShaderMap(); // render thread + bool HasShaderInstance(const ShaderType& inShaderType, const ShaderVariantValueMap& inShaderVariants) const; ShaderInstance GetShaderInstance(const ShaderType& inShaderType, const ShaderVariantValueMap& inShaderVariants); void Invalidate(); diff --git a/Engine/Source/Render/Include/Render/VertexFactory.h b/Engine/Source/Render/Include/Render/VertexFactory.h new file mode 100644 index 00000000..da8d507d --- /dev/null +++ b/Engine/Source/Render/Include/Render/VertexFactory.h @@ -0,0 +1,22 @@ +// +// Created by johnk on 2026/7/5. +// + +#pragma once + +#include +#include + +namespace Render { + class RENDER_API StaticMeshVertexFactory final : public StaticVertexFactoryType { + public: + VertexFactoryTypeInfo(StaticMeshVertexFactory, "Engine/Shader/Explosion/VertexFactory/StaticMesh/VertexFactory.esh") + DeclVertexInput(PositionInput, POSITION, RHI::VertexFormat::float32X3, 0) + DeclVertexInput(Uv0Input, TEXCOORD, RHI::VertexFormat::float32X2, 12) + MakeVertexInputVec(PositionInput, Uv0Input) + EmptyVariantFieldVec + BeginSupportedMaterialTypes + MaterialType::surface, + EndSupportedMaterialTypes + }; +} diff --git a/Engine/Source/Render/Src/MeshRenderData.cpp b/Engine/Source/Render/Src/MeshRenderData.cpp new file mode 100644 index 00000000..3f7f30ba --- /dev/null +++ b/Engine/Source/Render/Src/MeshRenderData.cpp @@ -0,0 +1,49 @@ +// +// Created by johnk on 2026/7/5. +// + +#include + +namespace Render::Internal { + static Common::UniquePtr CreateUploadedBuffer(RHI::Device& inDevice, const void* inData, size_t inSize, RHI::BufferUsageBits inUsage, const std::string& inDebugName) + { + const RHI::BufferCreateInfo createInfo = RHI::BufferCreateInfo() + .SetSize(inSize) + .SetUsages(inUsage | RHI::BufferUsageBits::mapWrite | RHI::BufferUsageBits::copySrc) + .SetInitialState(RHI::BufferState::staging) + .SetDebugName(inDebugName); + + Common::UniquePtr result = inDevice.CreateBuffer(createInfo); + Assert(result.Valid()); + auto* data = result->Map(RHI::MapMode::write, 0, inSize); + memcpy(data, inData, inSize); + result->Unmap(); + return result; + } +} + +namespace Render { + MeshRenderData::MeshRenderData(RHI::Device& inDevice, const std::vector& inVertices, const std::vector& inIndices) + : vertexBuffer(Internal::CreateUploadedBuffer(inDevice, inVertices.data(), inVertices.size() * sizeof(Vertex), RHI::BufferUsageBits::vertex, "meshVertexBuffer")) + , indexBuffer(Internal::CreateUploadedBuffer(inDevice, inIndices.data(), inIndices.size() * sizeof(uint32_t), RHI::BufferUsageBits::index, "meshIndexBuffer")) + , indexCount(static_cast(inIndices.size())) + { + } + + MeshRenderData::~MeshRenderData() = default; + + RHI::Buffer* MeshRenderData::GetVertexBuffer() const + { + return vertexBuffer.Get(); + } + + RHI::Buffer* MeshRenderData::GetIndexBuffer() const + { + return indexBuffer.Get(); + } + + uint32_t MeshRenderData::GetIndexCount() const + { + return indexCount; + } +} diff --git a/Engine/Source/Render/Src/Renderer.cpp b/Engine/Source/Render/Src/Renderer.cpp index 9c3526e3..e8e75ac9 100644 --- a/Engine/Source/Render/Src/Renderer.cpp +++ b/Engine/Source/Render/Src/Renderer.cpp @@ -2,10 +2,43 @@ // Created by johnk on 2022/8/3. // +#include + +#include +#include #include +#include +#include namespace Render::Internal { const Common::LinearColor surfaceClearColor = { 0.1f, 0.1f, 0.12f, 1.0f }; + + struct ALIGN_AS_GPU BasePassVsUniform { + Common::FMat4x4 localToWorld; + Common::FMat4x4 worldToClip; + }; + + struct ALIGN_AS_GPU BasePassPsUniform { + Common::FVec4 baseColor; + }; + + struct BasePassDraw { + size_t viewIndex; + RasterPipeline* pipeline; + RGBindGroupRef bindGroup; + RGBufferViewRef vertexBufferView; + RGBufferViewRef indexBufferView; + uint32_t indexCount; + }; + + static RVertexState BuildVertexState(const VertexFactoryType& inVertexFactoryType) + { + RVertexBufferLayout layout(RHI::VertexStepMode::perVertex, MeshRenderData::vertexStride); + for (const auto& input : inVertexFactoryType.GetVertexInputs()) { + layout.AddAttribute(RVertexAttribute(RVertexBinding(input.name, 0), input.format, input.offset)); + } + return RVertexState().AddVertexBufferLayout(layout); + } } namespace Render { @@ -33,15 +66,127 @@ namespace Render { void StandardRenderer::Render(float inDeltaTimeSeconds) { + const RHI::PixelFormat colorFormat = surface->GetCreateInfo().format; + auto* backTexture = rgBuilder.ImportTexture(surface, RHI::TextureState::present); auto* backTextureView = rgBuilder.CreateTextureView(backTexture, RGTextureViewDesc(RHI::TextureViewType::colorAttachment, RHI::TextureViewDimension::tv2D)); + auto* depthTexture = rgBuilder.CreateTexture( + RGTextureDesc() + .SetDimension(RHI::TextureDimension::t2D) + .SetWidth(surfaceExtent.x) + .SetHeight(surfaceExtent.y) + .SetDepthOrArraySize(1) + .SetFormat(RHI::PixelFormat::d32Float) + .SetUsages(RHI::TextureUsageBits::depthStencilAttachment) + .SetMipLevels(1) + .SetSamples(1) + .SetInitialState(RHI::TextureState::depthStencilWrite) + .SetDebugName("sceneDepth")); + auto* depthTextureView = rgBuilder.CreateTextureView(depthTexture, RGTextureViewDesc(RHI::TextureViewType::depthStencil, RHI::TextureViewDimension::tv2D)); + + std::vector draws; + std::vector passBindGroups; + if (scene != nullptr) { + ShaderMap& shaderMap = ShaderMap::Get(*device); + size_t drawIndex = 0; + + for (const auto& [entity, proxy] : scene->All()) { + if (!proxy.mesh.Valid() || proxy.vertexFactoryType == nullptr || proxy.vertexShaderType == nullptr || proxy.pixelShaderType == nullptr) { + continue; + } + // material shaders compile asynchronously, primitives simply do not draw until artifacts arrive + if (!shaderMap.HasShaderInstance(*proxy.vertexShaderType, {}) || !shaderMap.HasShaderInstance(*proxy.pixelShaderType, {})) { + continue; + } + + const ShaderInstance vertexShader = shaderMap.GetShaderInstance(*proxy.vertexShaderType, {}); + const ShaderInstance pixelShader = shaderMap.GetShaderInstance(*proxy.pixelShaderType, {}); + auto* pipeline = PipelineCache::Get(*device).GetOrCreate( + RasterPipelineStateDesc() + .SetVertexShader(vertexShader) + .SetPixelShader(pixelShader) + .SetVertexState(Internal::BuildVertexState(*proxy.vertexFactoryType)) + .SetPrimitiveState(RPrimitiveState().SetCullMode(RHI::CullMode::none)) + .SetDepthStencilState( + RDepthStencilState() + .SetDepthEnabled(true) + .SetFormat(RHI::PixelFormat::d32Float) + .SetDepthCompareFunc(RHI::CompareFunc::greaterEqual)) + .SetFragmentState(RFragmentState().AddColorTarget(RHI::ColorTargetState(colorFormat, RHI::ColorWriteBits::all, false)))); + + auto* vertexBuffer = rgBuilder.ImportBuffer(proxy.mesh->GetVertexBuffer(), RHI::BufferState::shaderReadOnly); + auto* vertexBufferView = rgBuilder.CreateBufferView( + vertexBuffer, RGBufferViewDesc(RHI::BufferViewType::vertex, vertexBuffer->GetDesc().size, 0, RHI::VertexBufferViewInfo(MeshRenderData::vertexStride))); + auto* indexBuffer = rgBuilder.ImportBuffer(proxy.mesh->GetIndexBuffer(), RHI::BufferState::shaderReadOnly); + auto* indexBufferView = rgBuilder.CreateBufferView( + indexBuffer, RGBufferViewDesc(RHI::BufferViewType::index, indexBuffer->GetDesc().size, 0, RHI::IndexBufferViewInfo(RHI::IndexFormat::uint32))); + + for (size_t viewIndex = 0; viewIndex < views.size(); viewIndex++) { + const View& view = views[viewIndex]; + + Internal::BasePassVsUniform vsUniform {}; + vsUniform.localToWorld = proxy.localToWorld; + vsUniform.worldToClip = view.data.projectionMatrix * view.data.viewMatrix; + + Internal::BasePassPsUniform psUniform {}; + psUniform.baseColor = proxy.baseColor; + + auto* vsUniformBuffer = rgBuilder.CreateBuffer( + RGBufferDesc(sizeof(Internal::BasePassVsUniform), RHI::BufferUsageBits::uniform | RHI::BufferUsageBits::mapWrite, RHI::BufferState::staging, std::format("basePassVsUniform{}", drawIndex))); + auto* vsUniformBufferView = rgBuilder.CreateBufferView(vsUniformBuffer, RGBufferViewDesc(RHI::BufferViewType::uniformBinding, sizeof(Internal::BasePassVsUniform))); + rgBuilder.QueueBufferUpload(vsUniformBuffer, RGBufferUploadInfo(&vsUniform, sizeof(Internal::BasePassVsUniform), 0, 0, true)); + + auto* psUniformBuffer = rgBuilder.CreateBuffer( + RGBufferDesc(sizeof(Internal::BasePassPsUniform), RHI::BufferUsageBits::uniform | RHI::BufferUsageBits::mapWrite, RHI::BufferState::staging, std::format("basePassPsUniform{}", drawIndex))); + auto* psUniformBufferView = rgBuilder.CreateBufferView(psUniformBuffer, RGBufferViewDesc(RHI::BufferViewType::uniformBinding, sizeof(Internal::BasePassPsUniform))); + rgBuilder.QueueBufferUpload(psUniformBuffer, RGBufferUploadInfo(&psUniform, sizeof(Internal::BasePassPsUniform), 0, 0, true)); + + auto* bindGroup = rgBuilder.AllocateBindGroup( + RGBindGroupDesc::Create(pipeline->GetPipelineLayout()->GetBindGroupLayout(0)) + .UniformBuffer("vsUniform", vsUniformBufferView) + .UniformBuffer("materialUniform", psUniformBufferView)); + + Internal::BasePassDraw draw {}; + draw.viewIndex = viewIndex; + draw.pipeline = pipeline; + draw.bindGroup = bindGroup; + draw.vertexBufferView = vertexBufferView; + draw.indexBufferView = indexBufferView; + draw.indexCount = proxy.mesh->GetIndexCount(); + draws.emplace_back(draw); + passBindGroups.emplace_back(bindGroup); + drawIndex++; + } + } + } rgBuilder.AddRasterPass( "BasePass", RGRasterPassDesc() - .AddColorAttachment(RGColorAttachment(backTextureView, RHI::LoadOp::clear, RHI::StoreOp::store, Internal::surfaceClearColor)), - {}, - [](const RGBuilder&, RHI::RasterPassCommandRecorder&) -> void {}, + .AddColorAttachment(RGColorAttachment(backTextureView, RHI::LoadOp::clear, RHI::StoreOp::store, Internal::surfaceClearColor)) + .SetDepthStencilAttachment(RGDepthStencilAttachment(depthTextureView, false, RHI::LoadOp::clear, RHI::StoreOp::discard, 0.0f)), + passBindGroups, + [draws = std::move(draws), views = views](const RGBuilder& rg, RHI::RasterPassCommandRecorder& recorder) -> void { + for (size_t viewIndex = 0; viewIndex < views.size(); viewIndex++) { + const auto& viewport = views[viewIndex].data.viewport; + recorder.SetViewport( + static_cast(viewport.min.x), static_cast(viewport.min.y), + static_cast(viewport.ExtentX()), static_cast(viewport.ExtentY()), 0.0f, 1.0f); + recorder.SetScissor(viewport.min.x, viewport.min.y, viewport.ExtentX(), viewport.ExtentY()); + recorder.SetPrimitiveTopology(RHI::PrimitiveTopology::triangleList); + + for (const auto& draw : draws) { + if (draw.viewIndex != viewIndex) { + continue; + } + recorder.SetPipeline(draw.pipeline->GetRHI()); + recorder.SetBindGroup(0, rg.GetRHI(draw.bindGroup)); + recorder.SetVertexBuffer(0, rg.GetRHI(draw.vertexBufferView)); + recorder.SetIndexBuffer(rg.GetRHI(draw.indexBufferView)); + recorder.DrawIndexed(draw.indexCount, 1, 0, 0, 0); + } + } + }, {}, [backTexture](const RGBuilder& rg, RHI::CommandRecorder& recorder) -> void { recorder.ResourceBarrier(RHI::Barrier::Transition(rg.GetRHI(backTexture), RHI::TextureState::renderTarget, RHI::TextureState::present)); diff --git a/Engine/Source/Render/Src/Shader.cpp b/Engine/Source/Render/Src/Shader.cpp index 73c510e0..89416e10 100644 --- a/Engine/Source/Render/Src/Shader.cpp +++ b/Engine/Source/Render/Src/Shader.cpp @@ -260,6 +260,8 @@ namespace Render { , includeDirectories(inIncludeDirectories) , shaderVariantFields(Common::VectorUtils::Combine(inVertexFactoryType.GetVariantFields(), inShaderVariantFields)) { + // the base pass source includes "VertexFactory.esh" resolved from the vertex factory's own directory + includeDirectories.emplace_back(Common::Path(vertexFactory.GetSourceFile()).Parent().String()); } MaterialShaderType::~MaterialShaderType() = default; @@ -428,6 +430,19 @@ namespace Render { ShaderMap::~ShaderMap() = default; + bool ShaderMap::HasShaderInstance(const ShaderType& inShaderType, const ShaderVariantValueMap& inShaderVariants) const // NOLINT + { + Assert(Core::ThreadContext::IsRenderThread()); + + const ShaderArtifactRegistry& registry = ShaderArtifactRegistry::Get(); + const auto typeIter = registry.typeArtifactsRT.find(inShaderType.GetKey()); + if (typeIter == registry.typeArtifactsRT.end()) { + return false; + } + const ShaderVariantKey variantKey = ShaderUtils::ComputeVariantKey(inShaderType.GetVariantFields(), inShaderVariants); + return typeIter->second.variantArtifacts.contains(variantKey); + } + ShaderInstance ShaderMap::GetShaderInstance(const ShaderType& inShaderType, const ShaderVariantValueMap& inShaderVariants) { Assert(Core::ThreadContext::IsRenderThread()); diff --git a/Engine/Source/Render/Src/VertexFactory.cpp b/Engine/Source/Render/Src/VertexFactory.cpp new file mode 100644 index 00000000..e06489e9 --- /dev/null +++ b/Engine/Source/Render/Src/VertexFactory.cpp @@ -0,0 +1,9 @@ +// +// Created by johnk on 2026/7/5. +// + +#include + +namespace Render { + ImplementStaticVertexFactoryType(StaticMeshVertexFactory) +} diff --git a/Engine/Source/Runtime/Include/Runtime/Asset/Material.h b/Engine/Source/Runtime/Include/Runtime/Asset/Material.h index b0420bdd..e1fcb9f0 100644 --- a/Engine/Source/Runtime/Include/Runtime/Asset/Material.h +++ b/Engine/Source/Runtime/Include/Runtime/Asset/Material.h @@ -179,10 +179,14 @@ namespace Runtime { EFunc() ParameterField& EmplaceParameterField(const std::string& inName); EFunc() void Update(); + void PostLoad() override; + const Render::MaterialShaderType* FindShaderType(Render::VertexFactoryTypeKey inVertexFactoryTypeKey, RHI::ShaderStageBits inStage) const; private: using StageShaderTypeMap = std::unordered_map>; + void CompileShaderTypes() const; + EProperty() MaterialType type; EProperty() std::string source; EProperty() VariantFieldMap variantFields; diff --git a/Engine/Source/Runtime/Include/Runtime/Asset/Mesh.h b/Engine/Source/Runtime/Include/Runtime/Asset/Mesh.h index 7a857842..4215ae51 100644 --- a/Engine/Source/Runtime/Include/Runtime/Asset/Mesh.h +++ b/Engine/Source/Runtime/Include/Runtime/Asset/Mesh.h @@ -19,6 +19,7 @@ namespace Runtime { EProperty() std::vector positions; EProperty() std::vector tangents; EProperty() std::vector uv0; + EProperty() std::vector indices; // optional EProperty() std::vector uv1; EProperty() std::vector colors; @@ -39,6 +40,12 @@ namespace Runtime { explicit StaticMesh(Core::Uri inUri); ~StaticMesh() override; + EFunc() const AssetPtr& GetMaterial() const; + EFunc() void SetMaterial(const AssetPtr& inMaterial); + EFunc() size_t GetLODCount() const; + EFunc() const StaticMeshLOD& GetLOD(size_t inIndex) const; + EFunc() StaticMeshLOD& EmplaceLOD(); + private: EProperty() AssetPtr material; EProperty() std::vector lodVec; diff --git a/Engine/Source/Runtime/Include/Runtime/System/Scene.h b/Engine/Source/Runtime/Include/Runtime/System/Scene.h index 8527705d..7572cdae 100644 --- a/Engine/Source/Runtime/Include/Runtime/System/Scene.h +++ b/Engine/Source/Runtime/Include/Runtime/System/Scene.h @@ -8,12 +8,16 @@ #include #include +#include #include #include +#include +#include #include #include #include #include +#include namespace Runtime { class RUNTIME_API EClass() SceneSystem final : public System { @@ -29,9 +33,9 @@ namespace Runtime { void Tick(float inDeltaTimeSeconds) override; private: - template void QueueCreateSceneProxy(Entity inEntity); + template void QueueCreateSceneProxy(Entity inEntity, bool inWithScale = false); template void QueueUpdateSceneProxyContent(Entity inEntity); - template void QueueUpdateSceneProxyTransform(Entity inEntity); + template void QueueUpdateSceneProxyTransform(Entity inEntity, bool inWithScale = false); template void QueueRemoveSceneProxy(Entity inEntity); Render::RenderModule& renderModule; @@ -39,7 +43,7 @@ namespace Runtime { EventsObserver directionalLightsObserver; EventsObserver pointLightsObserver; EventsObserver spotLightsObserver; - // TODO primitive + EventsObserver staticPrimitivesObserver; }; } @@ -89,21 +93,67 @@ namespace Runtime::Internal { outSceneProxy.intensity = inComponent.intensity; } - // TODO primitive + // runs on the render thread: uploads the mesh geometry and resolves the material's shader types, the asset chain + // is kept alive by the component copy captured into the render thread task + template <> + inline void UpdateSceneProxyContent(Render::PrimitiveSceneProxy& outSceneProxy, const StaticPrimitive& inComponent) + { + outSceneProxy.mesh = nullptr; + outSceneProxy.vertexFactoryType = nullptr; + outSceneProxy.vertexShaderType = nullptr; + outSceneProxy.pixelShaderType = nullptr; + + if (inComponent.mesh.Get() == nullptr || inComponent.mesh->GetLODCount() == 0) { + return; + } + const AssetPtr& materialInstance = inComponent.mesh->GetMaterial(); + if (materialInstance.Get() == nullptr || materialInstance->GetMaterial().Get() == nullptr) { + return; + } + + const StaticMeshVertices& vertices = inComponent.mesh->GetLOD(0).vertices; + if (vertices.positions.empty() || vertices.indices.empty()) { + return; + } + std::vector gpuVertices; + gpuVertices.reserve(vertices.positions.size()); + for (size_t i = 0; i < vertices.positions.size(); i++) { + Render::MeshRenderData::Vertex vertex; + vertex.position = vertices.positions[i]; + vertex.uv0 = i < vertices.uv0.size() ? vertices.uv0[i] : Common::FVec2(); + gpuVertices.emplace_back(vertex); + } + + RHI::Device* device = EngineHolder::Get().GetRenderModule().GetDevice(); + outSceneProxy.mesh = new Render::MeshRenderData(*device, gpuVertices, vertices.indices); + + const Render::VertexFactoryType& vertexFactoryType = Render::StaticMeshVertexFactory::Get(); + const Material* material = materialInstance->GetMaterial().Get(); + outSceneProxy.vertexFactoryType = &vertexFactoryType; + outSceneProxy.vertexShaderType = material->FindShaderType(vertexFactoryType.GetKey(), RHI::ShaderStageBits::sVertex); + outSceneProxy.pixelShaderType = material->FindShaderType(vertexFactoryType.GetKey(), RHI::ShaderStageBits::sPixel); + + if (materialInstance->HasParameter("baseColor")) { + if (const auto baseColorParam = materialInstance->GetParameter("baseColor"); + std::holds_alternative(baseColorParam)) { + outSceneProxy.baseColor = std::get(baseColorParam); + } + } + } } namespace Runtime { template - void SceneSystem::QueueCreateSceneProxy(Entity inEntity) + void SceneSystem::QueueCreateSceneProxy(Entity inEntity, bool inWithScale) { const auto& sceneHolder = registry.GGet(); const auto& component = registry.Get(inEntity); const auto* transform = registry.Find(inEntity); - renderModule.GetRenderThread().EmplaceTask([scene = sceneHolder.scene.Get(), inEntity, component, transform = Internal::GetOptional(transform)]() -> void { + renderModule.GetRenderThread().EmplaceTask([scene = sceneHolder.scene.Get(), inEntity, component, transform = Internal::GetOptional(transform), inWithScale]() -> void { SceneProxy sceneProxy; Internal::UpdateSceneProxyContent(sceneProxy, component); if (transform.has_value()) { - Internal::UpdateSceneProxyWorldTransform(sceneProxy, transform.value(), false); + Internal::UpdateSceneProxyWorldTransform(sceneProxy, transform.value(), inWithScale); } scene->Add(inEntity, std::move(sceneProxy)); }); @@ -121,13 +171,13 @@ namespace Runtime { } template - void SceneSystem::QueueUpdateSceneProxyTransform(Entity inEntity) + void SceneSystem::QueueUpdateSceneProxyTransform(Entity inEntity, bool inWithScale) { const auto& sceneHolder = registry.GGet(); const auto& transform = registry.Get(inEntity); - renderModule.GetRenderThread().EmplaceTask([scene = sceneHolder.scene.Get(), inEntity, transform]() -> void { + renderModule.GetRenderThread().EmplaceTask([scene = sceneHolder.scene.Get(), inEntity, transform, inWithScale]() -> void { auto& sceneProxy = scene->Get(inEntity); - Internal::UpdateSceneProxyWorldTransform(sceneProxy, transform, false); + Internal::UpdateSceneProxyWorldTransform(sceneProxy, transform, inWithScale); }); } diff --git a/Engine/Source/Runtime/Src/Asset/Material.cpp b/Engine/Source/Runtime/Src/Asset/Material.cpp index 509eccec..e6b8d07e 100644 --- a/Engine/Source/Runtime/Src/Asset/Material.cpp +++ b/Engine/Source/Runtime/Src/Asset/Material.cpp @@ -2,7 +2,11 @@ // Created by johnk on 2025/3/21. // +#include + +#include #include +#include namespace Runtime::Internal { static std::string GetVariantFieldMacro(const std::string& inVariantFieldName) @@ -230,8 +234,8 @@ namespace Runtime { RHI::ShaderStageBits::sVertex, RHI::ShaderStageBits::sPixel}; static std::unordered_map stageEntrySourceFileMap = { - {RHI::ShaderStageBits::sVertex, "Engine/Shader/BasePassVS.esl"}, - {RHI::ShaderStageBits::sPixel, "Engine/Shader/BasePassPS.esl"}}; + {RHI::ShaderStageBits::sVertex, "Engine/Shader/Explosion/BasePassVS.esl"}, + {RHI::ShaderStageBits::sPixel, "Engine/Shader/Explosion/BasePassPS.esl"}}; static std::unordered_map stageSimpleNameMap = { {RHI::ShaderStageBits::sVertex, "VS"}, {RHI::ShaderStageBits::sPixel, "PS"}}; @@ -272,10 +276,47 @@ namespace Runtime { stage, stageEntrySourceFileMap.at(stage), stageEntryPointMap.at(stage), - std::vector {materialRootCacheDir.String()}, + std::vector {materialRootCacheDir.String(), std::string("Engine/Shader/Explosion")}, Internal::BuildShaderVariantFieldVec(variantFields))); } } + + CompileShaderTypes(); + } + + void Material::PostLoad() + { + Update(); + } + + const Render::MaterialShaderType* Material::FindShaderType(Render::VertexFactoryTypeKey inVertexFactoryTypeKey, RHI::ShaderStageBits inStage) const + { + const auto stageMapIter = shaderTypes.find(inVertexFactoryTypeKey); + if (stageMapIter == shaderTypes.end()) { + return nullptr; + } + const auto shaderTypeIter = stageMapIter->second.find(inStage); + return shaderTypeIter == stageMapIter->second.end() ? nullptr : shaderTypeIter->second.Get(); + } + + void Material::CompileShaderTypes() const + { + std::vector typesToCompile; + for (const auto& stageShaderTypeMap : shaderTypes | std::views::values) { + for (const auto& shaderType : stageShaderTypeMap | std::views::values) { + typesToCompile.emplace_back(shaderType.Get()); + } + } + if (typesToCompile.empty()) { + return; + } + + const RHI::RHIType rhiType = EngineHolder::Get().GetRenderModule().GetDevice()->GetGpu().GetInstance().GetRHIType(); + + Render::ShaderCompileOptions options; + options.byteCodeType = rhiType == RHI::RHIType::directX12 ? Render::ShaderByteCodeType::dxil : Render::ShaderByteCodeType::spirv; + options.withDebugInfo = static_cast(BUILD_CONFIG_DEBUG); // NOLINT + (void) Render::ShaderTypeCompiler::Get().Compile(typesToCompile, options); } MaterialInstance::MaterialInstance(Core::Uri inUri) diff --git a/Engine/Source/Runtime/Src/Asset/Mesh.cpp b/Engine/Source/Runtime/Src/Asset/Mesh.cpp index f2674c9f..36e66228 100644 --- a/Engine/Source/Runtime/Src/Asset/Mesh.cpp +++ b/Engine/Source/Runtime/Src/Asset/Mesh.cpp @@ -11,4 +11,29 @@ namespace Runtime { } StaticMesh::~StaticMesh() = default; + + const AssetPtr& StaticMesh::GetMaterial() const + { + return material; + } + + void StaticMesh::SetMaterial(const AssetPtr& inMaterial) + { + material = inMaterial; + } + + size_t StaticMesh::GetLODCount() const + { + return lodVec.size(); + } + + const StaticMeshLOD& StaticMesh::GetLOD(size_t inIndex) const + { + return lodVec.at(inIndex); + } + + StaticMeshLOD& StaticMesh::EmplaceLOD() + { + return lodVec.emplace_back(); + } } diff --git a/Engine/Source/Runtime/Src/System/Scene.cpp b/Engine/Source/Runtime/Src/System/Scene.cpp index 9cab2e9c..0541b197 100644 --- a/Engine/Source/Runtime/Src/System/Scene.cpp +++ b/Engine/Source/Runtime/Src/System/Scene.cpp @@ -16,12 +16,20 @@ namespace Runtime { , directionalLightsObserver(inRegistry.EventsObserver()) , pointLightsObserver(inRegistry.EventsObserver()) , spotLightsObserver(inRegistry.EventsObserver()) + , staticPrimitivesObserver(inRegistry.EventsObserver()) { transformUpdatedObserver .ObConstructed() .ObUpdated(); inRegistry.GEmplace(renderModule.NewScene()); + + // components created before the system graph was built (e.g. a loaded level or editor-authored defaults) + // never fire construct events, mirror them into the render scene here + inRegistry.View().Each([this](Entity e, DirectionalLight&) -> void { QueueCreateSceneProxy(e); }); + inRegistry.View().Each([this](Entity e, PointLight&) -> void { QueueCreateSceneProxy(e); }); + inRegistry.View().Each([this](Entity e, SpotLight&) -> void { QueueCreateSceneProxy(e); }); + inRegistry.View().Each([this](Entity e, StaticPrimitive&) -> void { QueueCreateSceneProxy(e, true); }); } SceneSystem::~SceneSystem() // NOLINT @@ -34,23 +42,29 @@ namespace Runtime { directionalLightsObserver.Constructed().Each([this](Entity e) -> void { QueueCreateSceneProxy(e); }); pointLightsObserver.Constructed().Each([this](Entity e) -> void { QueueCreateSceneProxy(e); }); spotLightsObserver.Constructed().Each([this](Entity e) -> void { QueueCreateSceneProxy(e); }); + staticPrimitivesObserver.Constructed().Each([this](Entity e) -> void { QueueCreateSceneProxy(e, true); }); directionalLightsObserver.Updated().Each([this](Entity e) -> void { QueueUpdateSceneProxyContent(e); }); pointLightsObserver.Updated().Each([this](Entity e) -> void { QueueUpdateSceneProxyContent(e); }); spotLightsObserver.Updated().Each([this](Entity e) -> void { QueueUpdateSceneProxyContent(e); }); + staticPrimitivesObserver.Updated().Each([this](Entity e) -> void { QueueUpdateSceneProxyContent(e); }); directionalLightsObserver.Removed().Each([this](Entity e) -> void { QueueRemoveSceneProxy(e); }); pointLightsObserver.Removed().Each([this](Entity e) -> void { QueueRemoveSceneProxy(e); }); spotLightsObserver.Removed().Each([this](Entity e) -> void { QueueRemoveSceneProxy(e); }); - // TODO primitive + staticPrimitivesObserver.Removed().Each([this](Entity e) -> void { QueueRemoveSceneProxy(e); }); transformUpdatedObserver.Each([this](Entity e) -> void { if (registry.Has(e) || registry.Has(e) || registry.Has(e)) { QueueUpdateSceneProxyTransform(e); } + if (registry.Has(e)) { + QueueUpdateSceneProxyTransform(e, true); + } }); transformUpdatedObserver.Clear(); directionalLightsObserver.Clear(); pointLightsObserver.Clear(); spotLightsObserver.Clear(); + staticPrimitivesObserver.Clear(); } } From 4727d9a12f0a7666068b1bd631e61f55bb9d8220 Mon Sep 17 00:00:00 2001 From: kindem Date: Sun, 5 Jul 2026 10:49:49 +0800 Subject: [PATCH 14/24] fix: repair static mesh render path bring-up issues - fix dangling reference to the temporary view result in PlayerSystem::CreatePlayer - pass FTransform (the registered reflected constructor argument) when emplacing WorldTransform/Name - start the module-local render worker threads used by render graph code inlined into Runtime - keep the graph cull from dropping the write-only depth attachment and view it with the depth aspect - skip the vulkan stencil attachment for depth-only formats - use bare hlsl semantics so dxil and spirv reflection keys match - report physical pixels from the editor viewport so render targets match the swapchain extent --- Editor/Src/EditorClient.cpp | 24 +++++++++---------- Editor/Src/Widget/EditorViewport.cpp | 7 +++--- Engine/Shader/BasePassPS.esl | 2 +- Engine/Shader/BasePassVS.esl | 2 +- .../StaticMesh/VertexFactory.esh | 3 ++- .../Source/RHI-Vulkan/Src/CommandRecorder.cpp | 6 ++++- .../Render/Include/Render/RenderModule.h | 2 -- .../Render/Include/Render/VertexFactory.h | 3 +-- .../Source/Render/SharedSrc/RenderModule.cpp | 11 --------- Engine/Source/Render/Src/Renderer.cpp | 6 +++-- .../Runtime/Include/Runtime/System/Player.h | 9 ++++--- Engine/Source/Runtime/Src/Asset/Material.cpp | 3 ++- Engine/Source/Runtime/Src/Engine.cpp | 16 +++++++++++-- 13 files changed, 52 insertions(+), 42 deletions(-) diff --git a/Editor/Src/EditorClient.cpp b/Editor/Src/EditorClient.cpp index 640badf3..8d3867ca 100644 --- a/Editor/Src/EditorClient.cpp +++ b/Editor/Src/EditorClient.cpp @@ -15,10 +15,10 @@ #include namespace Editor::Internal { - constexpr const char* mainLevelUri = "asset://Game/Maps/Main"; - constexpr const char* defaultUnlitMaterialUri = "asset://Game/Materials/DefaultUnlit"; - constexpr const char* defaultUnlitInstanceUri = "asset://Game/Materials/DefaultUnlitInstance"; - constexpr const char* cubeMeshUri = "asset://Game/Meshes/Cube"; + const Core::Uri mainLevelUri("asset://Game/Maps/Main"); + const Core::Uri defaultUnlitMaterialUri("asset://Game/Materials/DefaultUnlit"); + const Core::Uri defaultUnlitInstanceUri("asset://Game/Materials/DefaultUnlitInstance"); + const Core::Uri cubeMeshUri("asset://Game/Meshes/Cube"); static bool AssetExists(const Core::Uri& inUri) { @@ -39,7 +39,7 @@ namespace Editor::Internal { { // 24 vertices (4 per face) so each face gets its own uv space, z-up unit cube centered at origin static constexpr float h = 0.5f; - static constexpr std::array, 6> facePositions = {{ + static const std::array, 6> facePositions = {{ {{ { -h, -h, h }, { h, -h, h }, { h, h, h }, { -h, h, h } }}, // +z {{ { -h, h, -h }, { h, h, -h }, { h, -h, -h }, { -h, -h, -h } }}, // -z {{ { h, -h, -h }, { h, h, -h }, { h, h, h }, { h, -h, h } }}, // +x @@ -97,24 +97,24 @@ namespace Editor::Internal { { const Runtime::AssetPtr cubeMesh = EnsureDefaultCubeMesh(); + // WorldTransform's reflected constructor takes an FTransform, other argument shapes would not match it const auto ground = inRegistry.Create(); - Runtime::WorldTransform groundTransform; - groundTransform.localToWorld.scale = Common::FVec3(10.0f, 10.0f, 0.2f); - groundTransform.localToWorld.translation = Common::FVec3(0.0f, 0.0f, -0.1f); + Common::FTransform groundTransform; + groundTransform.scale = Common::FVec3(10.0f, 10.0f, 0.2f); + groundTransform.translation = Common::FVec3(0.0f, 0.0f, -0.1f); inRegistry.Emplace(ground, groundTransform); auto& groundPrimitive = inRegistry.Emplace(ground); groundPrimitive.mesh = cubeMesh; const auto cube = inRegistry.Create(); - Runtime::WorldTransform cubeTransform; - cubeTransform.localToWorld.translation = Common::FVec3(0.0f, 0.0f, 0.5f); + Common::FTransform cubeTransform; + cubeTransform.translation = Common::FVec3(0.0f, 0.0f, 0.5f); inRegistry.Emplace(cube, cubeTransform); auto& cubePrimitive = inRegistry.Emplace(cube); cubePrimitive.mesh = cubeMesh; const auto playerStart = inRegistry.Create(); - Runtime::WorldTransform playerStartTransform; - playerStartTransform.localToWorld = Common::FTransform::LookAt(Common::FVec3(-5.0f, -6.0f, 4.0f), Common::FVec3(0.0f, 0.0f, 0.5f)); + const Common::FTransform playerStartTransform = Common::FTransform::LookAt(Common::FVec3(-5.0f, -6.0f, 4.0f), Common::FVec3(0.0f, 0.0f, 0.5f)); inRegistry.Emplace(playerStart, playerStartTransform); inRegistry.Emplace(playerStart); } diff --git a/Editor/Src/Widget/EditorViewport.cpp b/Editor/Src/Widget/EditorViewport.cpp index 3c720f51..0a4d6151 100644 --- a/Editor/Src/Widget/EditorViewport.cpp +++ b/Editor/Src/Widget/EditorViewport.cpp @@ -77,12 +77,13 @@ namespace Editor { uint32_t EditorViewport::GetWidth() const { - return static_cast(std::max(width(), 1)); + // physical pixels: the swapchain (and thus the render targets) live in device pixel space + return static_cast(std::max(1.0, width() * devicePixelRatioF())); } uint32_t EditorViewport::GetHeight() const { - return static_cast(std::max(height(), 1)); + return static_cast(std::max(1.0, height() * devicePixelRatioF())); } void EditorViewport::Resize(uint32_t inWidth, uint32_t inHeight) @@ -139,7 +140,7 @@ namespace Editor { GraphicsWidget::resizeEvent(inEvent); if (const QSize size = inEvent->size(); swapChain.Valid() && size.width() > 0 && size.height() > 0) { - RecreateSwapChain(static_cast(size.width()), static_cast(size.height())); + RecreateSwapChain(GetWidth(), GetHeight()); } } diff --git a/Engine/Shader/BasePassPS.esl b/Engine/Shader/BasePassPS.esl index 9945a2a0..1c6a17af 100644 --- a/Engine/Shader/BasePassPS.esl +++ b/Engine/Shader/BasePassPS.esl @@ -8,7 +8,7 @@ VkBinding(1, 0) cbuffer materialUniform : register(b0) { struct FragmentInput { float4 position : SV_POSITION; - float2 uv0 : TEXCOORD0; + float2 uv0 : TEXCOORD; }; float4 PSMain(FragmentInput input) : SV_TARGET diff --git a/Engine/Shader/BasePassVS.esl b/Engine/Shader/BasePassVS.esl index 2224fdff..8189d167 100644 --- a/Engine/Shader/BasePassVS.esl +++ b/Engine/Shader/BasePassVS.esl @@ -8,7 +8,7 @@ VkBinding(0, 0) cbuffer vsUniform : register(b0) { struct FragmentInput { float4 position : SV_POSITION; - float2 uv0 : TEXCOORD0; + float2 uv0 : TEXCOORD; }; FragmentInput VSMain(VertexFactoryInput vfInput) diff --git a/Engine/Shader/VertexFactory/StaticMesh/VertexFactory.esh b/Engine/Shader/VertexFactory/StaticMesh/VertexFactory.esh index 00ecd118..dcadaf34 100644 --- a/Engine/Shader/VertexFactory/StaticMesh/VertexFactory.esh +++ b/Engine/Shader/VertexFactory/StaticMesh/VertexFactory.esh @@ -1,8 +1,9 @@ #include +// bare semantics (implicit index 0) keep the dxil and spirv reflection keys identical struct VertexFactoryInput { VkLocation(0) float3 position : POSITION; - VkLocation(1) float2 uv0 : TEXCOORD0; + VkLocation(1) float2 uv0 : TEXCOORD; }; float3 GetLocalPosition(VertexFactoryInput input) diff --git a/Engine/Source/RHI-Vulkan/Src/CommandRecorder.cpp b/Engine/Source/RHI-Vulkan/Src/CommandRecorder.cpp index 34082ca9..457c3a05 100644 --- a/Engine/Source/RHI-Vulkan/Src/CommandRecorder.cpp +++ b/Engine/Source/RHI-Vulkan/Src/CommandRecorder.cpp @@ -467,7 +467,11 @@ namespace RHI::Vulkan { renderingInfo.pDepthAttachment = &depthAttachmentInfo; - if (!inBeginInfo.depthStencilAttachment->depthReadOnly) { + // depth-only formats must not present a stencil attachment, otherwise validation requires the + // pipeline's (undefined) stencil format to match the view format + const auto depthStencilFormat = depthStencilTextureView->GetTexture().GetCreateInfo().format; + const bool hasStencil = depthStencilFormat == PixelFormat::d32FloatS8Uint || depthStencilFormat == PixelFormat::d24UnormS8Uint; + if (hasStencil && !inBeginInfo.depthStencilAttachment->depthReadOnly) { stencilAttachmentInfo.sType = VK_STRUCTURE_TYPE_RENDERING_ATTACHMENT_INFO; stencilAttachmentInfo.imageView = depthStencilTextureView->GetNative(); stencilAttachmentInfo.imageLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL; diff --git a/Engine/Source/Render/Include/Render/RenderModule.h b/Engine/Source/Render/Include/Render/RenderModule.h index 7507cd2d..ec749f14 100644 --- a/Engine/Source/Render/Include/Render/RenderModule.h +++ b/Engine/Source/Render/Include/Render/RenderModule.h @@ -28,8 +28,6 @@ namespace Render { void Initialize(const RenderModuleInitParams& inParams); void DeInitialize(); - // called on render thread once per frame, reclaims pooled resources/caches not used for several frames - void ForfeitFrameResources() const; RHI::Device* GetDevice() const; Render::RenderThread& GetRenderThread() const; Scene* NewScene() const; diff --git a/Engine/Source/Render/Include/Render/VertexFactory.h b/Engine/Source/Render/Include/Render/VertexFactory.h index da8d507d..72f898ad 100644 --- a/Engine/Source/Render/Include/Render/VertexFactory.h +++ b/Engine/Source/Render/Include/Render/VertexFactory.h @@ -4,11 +4,10 @@ #pragma once -#include #include namespace Render { - class RENDER_API StaticMeshVertexFactory final : public StaticVertexFactoryType { + class StaticMeshVertexFactory final : public StaticVertexFactoryType { public: VertexFactoryTypeInfo(StaticMeshVertexFactory, "Engine/Shader/Explosion/VertexFactory/StaticMesh/VertexFactory.esh") DeclVertexInput(PositionInput, POSITION, RHI::VertexFormat::float32X3, 0) diff --git a/Engine/Source/Render/SharedSrc/RenderModule.cpp b/Engine/Source/Render/SharedSrc/RenderModule.cpp index f08431da..8cbc5475 100644 --- a/Engine/Source/Render/SharedSrc/RenderModule.cpp +++ b/Engine/Source/Render/SharedSrc/RenderModule.cpp @@ -4,8 +4,6 @@ #include #include -#include -#include #include namespace Render { @@ -59,15 +57,6 @@ namespace Render { initialized = false; } - void RenderModule::ForfeitFrameResources() const - { - Assert(Core::ThreadContext::IsRenderThread()); - BufferPool::Get(*rhiDevice).Forfeit(); - TexturePool::Get(*rhiDevice).Forfeit(); - ResourceViewCache::Get(*rhiDevice).Forfeit(); - BindGroupCache::Get(*rhiDevice).Forfeit(); - } - RHI::Device* RenderModule::GetDevice() const { return rhiDevice.Get(); diff --git a/Engine/Source/Render/Src/Renderer.cpp b/Engine/Source/Render/Src/Renderer.cpp index e8e75ac9..d8413f93 100644 --- a/Engine/Source/Render/Src/Renderer.cpp +++ b/Engine/Source/Render/Src/Renderer.cpp @@ -24,7 +24,7 @@ namespace Render::Internal { struct BasePassDraw { size_t viewIndex; - RasterPipeline* pipeline; + RasterPipelineState* pipeline; RGBindGroupRef bindGroup; RGBufferViewRef vertexBufferView; RGBufferViewRef indexBufferView; @@ -82,7 +82,9 @@ namespace Render { .SetSamples(1) .SetInitialState(RHI::TextureState::depthStencilWrite) .SetDebugName("sceneDepth")); - auto* depthTextureView = rgBuilder.CreateTextureView(depthTexture, RGTextureViewDesc(RHI::TextureViewType::depthStencil, RHI::TextureViewDimension::tv2D)); + // written but never sampled, keep the graph cull from dropping the depth attachment + depthTexture->MaskAsUsed(); + auto* depthTextureView = rgBuilder.CreateTextureView(depthTexture, RGTextureViewDesc(RHI::TextureViewType::depthStencil, RHI::TextureViewDimension::tv2D, RHI::TextureAspect::depth)); std::vector draws; std::vector passBindGroups; diff --git a/Engine/Source/Runtime/Include/Runtime/System/Player.h b/Engine/Source/Runtime/Include/Runtime/System/Player.h index 2cc746c4..39e82183 100644 --- a/Engine/Source/Runtime/Include/Runtime/System/Player.h +++ b/Engine/Source/Runtime/Include/Runtime/System/Player.h @@ -38,17 +38,20 @@ namespace Runtime { Entity PlayerSystem::CreatePlayer() { // a game world requires exactly one player start, an editor world tolerates an empty level and falls back to - // spawning the player at the identity transform - const auto& playerStartQuery = registry.View().All(); + // spawning the player at the identity transform; the result is copied because View() returns a temporary the + // reference would dangle into + const auto playerStartQuery = registry.View().All(); Assert(playType == PlayType::editor ? playerStartQuery.size() <= 1 : playerStartQuery.size() == 1); const auto playerEntity = registry.Create(); registry.Emplace(playerEntity); registry.Emplace(playerEntity); + // the registered reflected constructor takes an FTransform, passing a WorldTransform copy would silently + // match the wrong constructor and lose the data if (playerStartQuery.empty()) { registry.Emplace(playerEntity); } else { - registry.Emplace(playerEntity, std::get<2>(playerStartQuery[0])); + registry.Emplace(playerEntity, std::get<2>(playerStartQuery[0]).localToWorld); } auto& player = registry.Emplace(playerEntity); diff --git a/Engine/Source/Runtime/Src/Asset/Material.cpp b/Engine/Source/Runtime/Src/Asset/Material.cpp index e6b8d07e..866f2e42 100644 --- a/Engine/Source/Runtime/Src/Asset/Material.cpp +++ b/Engine/Source/Runtime/Src/Asset/Material.cpp @@ -311,8 +311,9 @@ namespace Runtime { return; } + // the compiler/artifact-registry singletons used here are this module's Render.Static copies, matching the + // ones the renderer executing in this module reads const RHI::RHIType rhiType = EngineHolder::Get().GetRenderModule().GetDevice()->GetGpu().GetInstance().GetRHIType(); - Render::ShaderCompileOptions options; options.byteCodeType = rhiType == RHI::RHIType::directX12 ? Render::ShaderByteCodeType::dxil : Render::ShaderByteCodeType::spirv; options.withDebugInfo = static_cast(BUILD_CONFIG_DEBUG); // NOLINT diff --git a/Engine/Source/Runtime/Src/Engine.cpp b/Engine/Source/Runtime/Src/Engine.cpp index d13e521c..eb3e9eb6 100644 --- a/Engine/Source/Runtime/Src/Engine.cpp +++ b/Engine/Source/Runtime/Src/Engine.cpp @@ -10,6 +10,9 @@ #include #include #include +#include +#include +#include #include #include #include @@ -35,6 +38,7 @@ namespace Runtime { Engine::~Engine() { + Render::RenderWorkerThreads::Get().Stop(); renderModule->DeInitialize(); ::Core::ModuleManager::Get().Unload("Render"); @@ -65,12 +69,17 @@ namespace Runtime { Core::ThreadContext::IncFrameNumber(); + // the shader artifact copy and pool forfeits intentionally run through this module's Render.Static singleton + // copies: the renderer itself executes in this module too, so these are the instances it reads auto& renderThread = renderModule->GetRenderThread(); - renderThread.EmplaceTask([renderModule = renderModule]() -> void { + renderThread.EmplaceTask([device = renderModule->GetDevice()]() -> void { Core::ThreadContext::IncFrameNumber(); Core::Console::Get().PerformRenderThreadSettingsCopy(); Render::ShaderArtifactRegistry::Get().PerformThreadCopy(); - renderModule->ForfeitFrameResources(); + Render::BufferPool::Get(*device).Forfeit(); + Render::TexturePool::Get(*device).Forfeit(); + Render::ResourceViewCache::Get(*device).Forfeit(); + Render::BindGroupCache::Get(*device).Forfeit(); }); for (auto* world : worlds) { @@ -103,6 +112,9 @@ namespace Runtime { Render::RenderModuleInitParams initParams; initParams.rhiType = RHI::GetRHITypeByAbbrString(inRhiTypeStr); renderModule->Initialize(initParams); + // the render module started its own module-local worker threads, render-layer code inlined into this module + // (e.g. render graph buffer uploads) binds to this module's singleton copy which must be started as well + Render::RenderWorkerThreads::Get().Start(); LogInfo(Render, "RHI type: {}", inRhiTypeStr); } From b9cec7feedfc30fac3d479e29dd7d0473c1ce55d Mon Sep 17 00:00:00 2001 From: kindem Date: Sun, 5 Jul 2026 10:49:58 +0800 Subject: [PATCH 15/24] feat: add dock manager with outliner, inspector and log panels - thin DockManager over QMainWindow docking with layout persistence and view menu - Name component and comp metadata tagging for the inspector's component catalog - Outliner/Inspector/Log web panels bridged over QWebChannel with reflection-serialized payloads - structured editor log stream attached to the engine logger - EditorContext selection/mutation funnel shared across panels --- Editor/Include/Editor/EditorContext.h | 17 ++- Editor/Include/Editor/EditorLog.h | 45 +++++++ Editor/Include/Editor/Widget/Dock.h | 32 +++++ Editor/Include/Editor/Widget/Editor.h | 7 + Editor/Include/Editor/Widget/InspectorPanel.h | 43 ++++++ Editor/Include/Editor/Widget/LogPanel.h | 47 +++++++ Editor/Include/Editor/Widget/OutlinerPanel.h | 53 ++++++++ Editor/Src/EditorContext.cpp | 60 +++++++++ Editor/Src/EditorLog.cpp | 64 +++++++++ Editor/Src/Main.cpp | 2 + Editor/Src/Widget/Dock.cpp | 63 +++++++++ Editor/Src/Widget/Editor.cpp | 44 +++++- Editor/Src/Widget/InspectorPanel.cpp | 126 ++++++++++++++++++ Editor/Src/Widget/LogPanel.cpp | 66 +++++++++ Editor/Src/Widget/OutlinerPanel.cpp | 75 +++++++++++ Editor/Web/src/App.tsx | 6 + Editor/Web/src/pages/editor/inspector.tsx | 121 +++++++++++++++++ Editor/Web/src/pages/editor/log.tsx | 71 ++++++++++ Editor/Web/src/pages/editor/outliner.tsx | 78 +++++++++++ .../Include/Runtime/Component/Camera.h | 2 +- .../Runtime/Include/Runtime/Component/Light.h | 6 +- .../Runtime/Include/Runtime/Component/Name.h | 21 +++ .../Include/Runtime/Component/Player.h | 2 +- .../Include/Runtime/Component/Primitive.h | 2 +- .../Include/Runtime/Component/Transform.h | 6 +- Engine/Source/Runtime/Src/Component/Name.cpp | 14 ++ 26 files changed, 1058 insertions(+), 15 deletions(-) create mode 100644 Editor/Include/Editor/EditorLog.h create mode 100644 Editor/Include/Editor/Widget/Dock.h create mode 100644 Editor/Include/Editor/Widget/InspectorPanel.h create mode 100644 Editor/Include/Editor/Widget/LogPanel.h create mode 100644 Editor/Include/Editor/Widget/OutlinerPanel.h create mode 100644 Editor/Src/EditorLog.cpp create mode 100644 Editor/Src/Widget/Dock.cpp create mode 100644 Editor/Src/Widget/InspectorPanel.cpp create mode 100644 Editor/Src/Widget/LogPanel.cpp create mode 100644 Editor/Src/Widget/OutlinerPanel.cpp create mode 100644 Editor/Web/src/pages/editor/inspector.tsx create mode 100644 Editor/Web/src/pages/editor/log.tsx create mode 100644 Editor/Web/src/pages/editor/outliner.tsx create mode 100644 Engine/Source/Runtime/Include/Runtime/Component/Name.h create mode 100644 Engine/Source/Runtime/Src/Component/Name.cpp diff --git a/Editor/Include/Editor/EditorContext.h b/Editor/Include/Editor/EditorContext.h index 6d8a7763..0ddfc418 100644 --- a/Editor/Include/Editor/EditorContext.h +++ b/Editor/Include/Editor/EditorContext.h @@ -10,8 +10,9 @@ #include namespace Editor { - // single source of truth for one open editor session: owns the edited world's client, later phases add the - // selection state and change notifications shared across panels + // single source of truth for one open editor session: owns the edited world's client, the selection state and + // the change notifications shared across panels; all entity-level edits funnel through here so every panel + // observes the same signals class EditorContext final : public QObject { Q_OBJECT @@ -20,8 +21,20 @@ namespace Editor { ~EditorContext() override; EditorClient& GetClient() const; + Runtime::Entity GetSelectedEntity() const; + void SetSelectedEntity(Runtime::Entity inEntity); + Runtime::Entity CreateEntity(const std::string& inName); + void DestroyEntity(Runtime::Entity inEntity); + void RenameEntity(Runtime::Entity inEntity, const std::string& inName); + void NotifyComponentsChanged(Runtime::Entity inEntity); + + Q_SIGNALS: + void SelectionChanged(Runtime::Entity inEntity); + void WorldStructureChanged(); + void ComponentsChanged(Runtime::Entity inEntity); private: Common::UniquePtr client; + Runtime::Entity selectedEntity; }; } diff --git a/Editor/Include/Editor/EditorLog.h b/Editor/Include/Editor/EditorLog.h new file mode 100644 index 00000000..49ec419e --- /dev/null +++ b/Editor/Include/Editor/EditorLog.h @@ -0,0 +1,45 @@ +// +// Created by johnk on 2026/7/5. +// + +#pragma once + +#include +#include +#include +#include +#include + +#include + +namespace Editor { + // log stream attached to the engine logger that keeps a bounded history and forwards structured entries to + // listeners (the log panel backend), log calls arrive from arbitrary threads so everything is mutex-guarded and + // listeners must marshal to their own thread themselves + class EditorLogStream final : public Core::LogStream { + public: + using EntryListener = std::function; + using ListenerHandle = size_t; + + // attaches the singleton stream to the engine logger on first use + static EditorLogStream& Get(); + + void Write(const std::string& inString) override; + void Write(const Core::LogEntry& inEntry) override; + void Flush() override; + + std::vector Snapshot() const; + ListenerHandle AddListener(EntryListener inListener); + void RemoveListener(ListenerHandle inHandle); + + private: + static constexpr size_t maxHistoryEntries = 2000; + + EditorLogStream(); + + mutable std::mutex mutex; + std::deque history; + std::unordered_map listeners; + ListenerHandle nextListenerHandle; + }; +} diff --git a/Editor/Include/Editor/Widget/Dock.h b/Editor/Include/Editor/Widget/Dock.h new file mode 100644 index 00000000..b9cabf5b --- /dev/null +++ b/Editor/Include/Editor/Widget/Dock.h @@ -0,0 +1,32 @@ +// +// Created by johnk on 2026/7/5. +// + +#pragma once + +#include + +#include +#include +#include + +namespace Editor { + // thin layer over QMainWindow docking: registers panels as dock widgets, builds the view menu and persists the + // layout, so the backing dock implementation can be swapped without touching the panels + class DockManager final : public QObject { + Q_OBJECT + + public: + explicit DockManager(QMainWindow& inWindow); + ~DockManager() override; + + QDockWidget* Register(const QString& inId, const QString& inTitle, Qt::DockWidgetArea inArea, QWidget* inContent); + void BuildViewMenu(QMenu& outMenu) const; + void SaveLayout() const; + void RestoreLayout() const; + + private: + QMainWindow& window; + std::vector docks; + }; +} diff --git a/Editor/Include/Editor/Widget/Editor.h b/Editor/Include/Editor/Widget/Editor.h index 8c012fd6..dcac5812 100644 --- a/Editor/Include/Editor/Widget/Editor.h +++ b/Editor/Include/Editor/Widget/Editor.h @@ -9,6 +9,7 @@ #include #include +#include namespace Editor { class EditorViewport; @@ -20,12 +21,18 @@ namespace Editor { ExplosionEditor(); ~ExplosionEditor() override; + protected: + void closeEvent(QCloseEvent* inEvent) override; + private: void SetupWindow(); + void SetupDocks(); + void SetupMenus(); void StartEngineTick(); void TickEngine(); EditorContext* context; + DockManager* dockManager; EditorViewport* viewport; QTimer* tickTimer; QElapsedTimer frameTimer; diff --git a/Editor/Include/Editor/Widget/InspectorPanel.h b/Editor/Include/Editor/Widget/InspectorPanel.h new file mode 100644 index 00000000..f01d188d --- /dev/null +++ b/Editor/Include/Editor/Widget/InspectorPanel.h @@ -0,0 +1,43 @@ +// +// Created by johnk on 2026/7/5. +// + +#pragma once + +#include +#include + +namespace Editor { + class InspectorBackend final : public QObject { + Q_OBJECT + Q_PROPERTY(QJsonValue selectedEntityData READ GetSelectedEntityData NOTIFY SelectedEntityDataChanged) + Q_PROPERTY(QJsonValue addableComponents READ GetAddableComponents CONSTANT) + + public: + explicit InspectorBackend(EditorContext& inContext, QObject* inParent = nullptr); + + QJsonValue GetSelectedEntityData() const; + QJsonValue GetAddableComponents() const; + + public Q_SLOTS: + void UpdateComponent(uint inEntity, const QString& inClassName, const QJsonValue& inContent); + void AddComponent(uint inEntity, const QString& inClassName); + void RemoveComponent(uint inEntity, const QString& inClassName); + + Q_SIGNALS: + void SelectedEntityDataChanged(); + + private: + EditorContext& context; + }; + + class InspectorPanel final : public WebWidget { + Q_OBJECT + + public: + explicit InspectorPanel(EditorContext& inContext, QWidget* inParent = nullptr); + + private: + InspectorBackend* backend; + }; +} diff --git a/Editor/Include/Editor/Widget/LogPanel.h b/Editor/Include/Editor/Widget/LogPanel.h new file mode 100644 index 00000000..212a82e8 --- /dev/null +++ b/Editor/Include/Editor/Widget/LogPanel.h @@ -0,0 +1,47 @@ +// +// Created by johnk on 2026/7/5. +// + +#pragma once + +#include +#include +#include + +namespace Editor { + struct EClass() LogEntryInfo { + EClassBody(LogEntryInfo) + + EProperty() std::string time; + EProperty() std::string tag; + EProperty() std::string level; + EProperty() std::string content; + }; + + class LogBackend final : public QObject { + Q_OBJECT + + public: + explicit LogBackend(QObject* inParent = nullptr); + ~LogBackend() override; + + public Q_SLOTS: + QJsonValue GetHistory() const; + + Q_SIGNALS: + void EntryAppended(QJsonValue inEntry); + + private: + EditorLogStream::ListenerHandle listenerHandle; + }; + + class LogPanel final : public WebWidget { + Q_OBJECT + + public: + explicit LogPanel(QWidget* inParent = nullptr); + + private: + LogBackend* backend; + }; +} diff --git a/Editor/Include/Editor/Widget/OutlinerPanel.h b/Editor/Include/Editor/Widget/OutlinerPanel.h new file mode 100644 index 00000000..cb7af8f3 --- /dev/null +++ b/Editor/Include/Editor/Widget/OutlinerPanel.h @@ -0,0 +1,53 @@ +// +// Created by johnk on 2026/7/5. +// + +#pragma once + +#include +#include +#include + +namespace Editor { + struct EClass() OutlinerEntityInfo { + EClassBody(OutlinerEntityInfo) + + EProperty() uint32_t id; + EProperty() std::string name; + }; + + class OutlinerBackend final : public QObject { + Q_OBJECT + Q_PROPERTY(QJsonValue entities READ GetEntities NOTIFY EntitiesChanged) + Q_PROPERTY(uint selectedEntity READ GetSelectedEntity NOTIFY SelectionChanged) + + public: + explicit OutlinerBackend(EditorContext& inContext, QObject* inParent = nullptr); + + QJsonValue GetEntities() const; + uint GetSelectedEntity() const; + + public Q_SLOTS: + void SelectEntity(uint inEntity); + void CreateEntity(const QString& inName); + void DestroyEntity(uint inEntity); + void RenameEntity(uint inEntity, const QString& inName); + + Q_SIGNALS: + void EntitiesChanged(); + void SelectionChanged(); + + private: + EditorContext& context; + }; + + class OutlinerPanel final : public WebWidget { + Q_OBJECT + + public: + explicit OutlinerPanel(EditorContext& inContext, QWidget* inParent = nullptr); + + private: + OutlinerBackend* backend; + }; +} diff --git a/Editor/Src/EditorContext.cpp b/Editor/Src/EditorContext.cpp index b3d326b6..e2a7987e 100644 --- a/Editor/Src/EditorContext.cpp +++ b/Editor/Src/EditorContext.cpp @@ -4,11 +4,14 @@ #include #include // NOLINT +#include +#include namespace Editor { EditorContext::EditorContext(QObject* inParent) : QObject(inParent) , client(Common::MakeUnique()) + , selectedEntity(Runtime::entityNull) { } @@ -18,4 +21,61 @@ namespace Editor { { return *client; } + + Runtime::Entity EditorContext::GetSelectedEntity() const + { + return selectedEntity; + } + + void EditorContext::SetSelectedEntity(Runtime::Entity inEntity) + { + if (selectedEntity == inEntity) { + return; + } + selectedEntity = inEntity; + emit SelectionChanged(selectedEntity); + } + + Runtime::Entity EditorContext::CreateEntity(const std::string& inName) + { + auto& registry = client->GetWorld().GetRegistry(); + const auto entity = registry.Create(); + // Name's reflected constructor takes a std::string + registry.Emplace(entity, inName); + registry.Emplace(entity); + emit WorldStructureChanged(); + return entity; + } + + void EditorContext::DestroyEntity(Runtime::Entity inEntity) + { + auto& registry = client->GetWorld().GetRegistry(); + if (!registry.Valid(inEntity)) { + return; + } + registry.Destroy(inEntity); + if (selectedEntity == inEntity) { + SetSelectedEntity(Runtime::entityNull); + } + emit WorldStructureChanged(); + } + + void EditorContext::RenameEntity(Runtime::Entity inEntity, const std::string& inName) + { + auto& registry = client->GetWorld().GetRegistry(); + if (!registry.Valid(inEntity)) { + return; + } + if (registry.Has(inEntity)) { + registry.Update(inEntity, [&](Runtime::Name& name) -> void { name.value = inName; }); + } else { + registry.Emplace(inEntity, inName); + } + emit WorldStructureChanged(); + } + + void EditorContext::NotifyComponentsChanged(Runtime::Entity inEntity) + { + emit ComponentsChanged(inEntity); + } } diff --git a/Editor/Src/EditorLog.cpp b/Editor/Src/EditorLog.cpp new file mode 100644 index 00000000..41a0a5cc --- /dev/null +++ b/Editor/Src/EditorLog.cpp @@ -0,0 +1,64 @@ +// +// Created by johnk on 2026/7/5. +// + +#include + +#include + +namespace Editor { + EditorLogStream& EditorLogStream::Get() + { + static EditorLogStream* stream = []() -> EditorLogStream* { + auto* result = new EditorLogStream(); + Core::Logger::Get().Attach(Common::UniquePtr(result)); + return result; + }(); + return *stream; + } + + EditorLogStream::EditorLogStream() + : nextListenerHandle(0) + { + } + + void EditorLogStream::Write(const std::string& inString) + { + } + + void EditorLogStream::Write(const Core::LogEntry& inEntry) + { + std::unique_lock lock(mutex); + history.emplace_back(inEntry); + if (history.size() > maxHistoryEntries) { + history.pop_front(); + } + for (const auto& listener : listeners | std::views::values) { + listener(inEntry); + } + } + + void EditorLogStream::Flush() + { + } + + std::vector EditorLogStream::Snapshot() const + { + std::unique_lock lock(mutex); + return { history.begin(), history.end() }; + } + + EditorLogStream::ListenerHandle EditorLogStream::AddListener(EntryListener inListener) + { + std::unique_lock lock(mutex); + const ListenerHandle handle = nextListenerHandle++; + listeners.emplace(handle, std::move(inListener)); + return handle; + } + + void EditorLogStream::RemoveListener(ListenerHandle inHandle) + { + std::unique_lock lock(mutex); + listeners.erase(inHandle); + } +} diff --git a/Editor/Src/Main.cpp b/Editor/Src/Main.cpp index 4feceea1..15e601da 100644 --- a/Editor/Src/Main.cpp +++ b/Editor/Src/Main.cpp @@ -7,6 +7,7 @@ #include #include +#include #include #include #include @@ -50,6 +51,7 @@ static bool NeedInitCore(EditorApplicationModel inModel) static void InitializePreQtApp(EditorApplicationModel inModel) { + Editor::EditorLogStream::Get(); Editor::WebUIServer::Get().Start(); if (!NeedInitCore(inModel)) { diff --git a/Editor/Src/Widget/Dock.cpp b/Editor/Src/Widget/Dock.cpp new file mode 100644 index 00000000..330ed05b --- /dev/null +++ b/Editor/Src/Widget/Dock.cpp @@ -0,0 +1,63 @@ +// +// Created by johnk on 2026/7/5. +// + +#include + +#include +#include +#include // NOLINT + +namespace Editor::Internal { + static QSettings MakeLayoutSettings() + { + const auto settingsFile = (Core::Paths::HasSetGameRoot() ? Core::Paths::GameCacheDir() : Core::Paths::EngineCacheDir()) / "Editor" / "Layout.ini"; + return { QString::fromStdString(settingsFile.String()), QSettings::IniFormat }; + } +} + +namespace Editor { + DockManager::DockManager(QMainWindow& inWindow) + : QObject(&inWindow) + , window(inWindow) + { + window.setDockNestingEnabled(true); + } + + DockManager::~DockManager() = default; + + QDockWidget* DockManager::Register(const QString& inId, const QString& inTitle, Qt::DockWidgetArea inArea, QWidget* inContent) + { + auto* dock = new QDockWidget(inTitle, &window); + dock->setObjectName(inId); + dock->setWidget(inContent); + window.addDockWidget(inArea, dock); + docks.emplace_back(dock); + return dock; + } + + void DockManager::BuildViewMenu(QMenu& outMenu) const + { + for (auto* dock : docks) { + outMenu.addAction(dock->toggleViewAction()); + } + } + + void DockManager::SaveLayout() const + { + QSettings settings = Internal::MakeLayoutSettings(); + settings.setValue("mainWindowGeometry", window.saveGeometry()); + settings.setValue("mainWindowState", window.saveState()); + } + + void DockManager::RestoreLayout() const + { + QSettings settings = Internal::MakeLayoutSettings(); + if (const auto geometry = settings.value("mainWindowGeometry"); geometry.isValid()) { + window.restoreGeometry(geometry.toByteArray()); + } + if (const auto state = settings.value("mainWindowState"); state.isValid()) { + window.restoreState(state.toByteArray()); + } + } +} diff --git a/Editor/Src/Widget/Editor.cpp b/Editor/Src/Widget/Editor.cpp index daa2fc85..13998e36 100644 --- a/Editor/Src/Widget/Editor.cpp +++ b/Editor/Src/Widget/Editor.cpp @@ -4,9 +4,15 @@ #include +#include +#include + #include #include #include +#include +#include +#include #include // NOLINT #include @@ -25,30 +31,59 @@ namespace Editor::Internal { namespace Editor { ExplosionEditor::ExplosionEditor() : context(new EditorContext(this)) + , dockManager(new DockManager(*this)) , viewport(nullptr) , tickTimer(new QTimer(this)) { SetupWindow(); + SetupDocks(); + SetupMenus(); + dockManager->RestoreLayout(); + context->GetClient().OpenProjectLevel(); StartEngineTick(); } ExplosionEditor::~ExplosionEditor() { tickTimer->stop(); - // deleted directly without takeCentralWidget(): reparenting would recreate the native window and trigger a - // pointless surface rebuild mid-teardown, and the viewport must unregister from the client before the - // context (world/client) goes away with the remaining children + // the viewport must unregister from the client (and drain in-flight gpu work) before the context + // (world/client) goes away with the remaining children delete viewport; viewport = nullptr; } + void ExplosionEditor::closeEvent(QCloseEvent* inEvent) + { + dockManager->SaveLayout(); + QMainWindow::closeEvent(inEvent); + } + void ExplosionEditor::SetupWindow() { setWindowTitle(Internal::MakeWindowTitle()); resize(1600, 900); + } + void ExplosionEditor::SetupDocks() + { viewport = new EditorViewport(context->GetClient(), this); - setCentralWidget(viewport); + + auto* viewportDock = dockManager->Register("viewport", "Viewport", Qt::LeftDockWidgetArea, viewport); + dockManager->Register("outliner", "Outliner", Qt::RightDockWidgetArea, new OutlinerPanel(*context)); + dockManager->Register("inspector", "Inspector", Qt::RightDockWidgetArea, new InspectorPanel(*context)); + dockManager->Register("log", "Log", Qt::BottomDockWidgetArea, new LogPanel()); + resizeDocks({ viewportDock }, { 1100 }, Qt::Horizontal); + } + + void ExplosionEditor::SetupMenus() + { + QMenu* fileMenu = menuBar()->addMenu("&File"); + QAction* saveLevelAction = fileMenu->addAction("&Save Level"); + saveLevelAction->setShortcut(QKeySequence::Save); + connect(saveLevelAction, &QAction::triggered, this, [this]() -> void { context->GetClient().SaveLevel(); }); + + QMenu* viewMenu = menuBar()->addMenu("&View"); + dockManager->BuildViewMenu(*viewMenu); } void ExplosionEditor::StartEngineTick() @@ -61,6 +96,7 @@ namespace Editor { void ExplosionEditor::TickEngine() { const float deltaSeconds = static_cast(frameTimer.restart()) / 1000.0f; + viewport->TickEditorCamera(deltaSeconds); Runtime::EngineHolder::Get().Tick(deltaSeconds); } } // namespace Editor diff --git a/Editor/Src/Widget/InspectorPanel.cpp b/Editor/Src/Widget/InspectorPanel.cpp new file mode 100644 index 00000000..861b0dba --- /dev/null +++ b/Editor/Src/Widget/InspectorPanel.cpp @@ -0,0 +1,126 @@ +// +// Created by johnk on 2026/7/5. +// + +#include +#include + +#include +#include +#include // NOLINT + +namespace Editor::Internal { + // the dynamic (de)serializers of QtJsonSerializer only depend on the runtime Mirror::Class, any meta class works + // as the template instantiation carrier + using DynJsonSerializer = QtJsonSerializer; + + static const Mirror::Class* FindCompClass(const QString& inClassName) + { + return Mirror::Class::Find(Mirror::Id(inClassName.toStdString())); + } +} + +namespace Editor { + InspectorBackend::InspectorBackend(EditorContext& inContext, QObject* inParent) + : QObject(inParent) + , context(inContext) + { + connect(&context, &EditorContext::SelectionChanged, this, [this](Runtime::Entity) -> void { emit SelectedEntityDataChanged(); }); + connect(&context, &EditorContext::ComponentsChanged, this, [this](Runtime::Entity inEntity) -> void { + if (inEntity == context.GetSelectedEntity()) { + emit SelectedEntityDataChanged(); + } + }); + connect(&context, &EditorContext::WorldStructureChanged, this, [this]() -> void { emit SelectedEntityDataChanged(); }); + } + + QJsonValue InspectorBackend::GetSelectedEntityData() const + { + const auto entity = context.GetSelectedEntity(); + auto& registry = context.GetClient().GetWorld().GetRegistry(); + if (entity == Runtime::entityNull || !registry.Valid(entity)) { + return QJsonValue::Null; + } + + // the component payloads are inherently dynamic, only the envelope is built by hand and every component + // content goes through the reflection-driven serializer + QJsonArray comps; + registry.CompEach(entity, [&](Runtime::CompClass compClass) -> void { + const Mirror::Any compRef = registry.GetDyn(compClass, entity); + + QJsonObject content; + Internal::DynJsonSerializer::QtJsonSerializeDyn(content, *compClass, compRef); + + QJsonObject comp; + comp["className"] = QString::fromStdString(compClass->GetName()); + comp["content"] = content; + comps.append(comp); + }); + + QJsonObject result; + result["entity"] = static_cast(entity); + result["comps"] = comps; + return result; + } + + QJsonValue InspectorBackend::GetAddableComponents() const + { + std::vector classNames; + for (const auto* clazz : Mirror::Class::GetAll()) { + if (clazz->HasMeta("comp")) { + classNames.emplace_back(clazz->GetName()); + } + } + std::ranges::sort(classNames); + + QJsonValue result; + QtJsonSerialize(result, classNames); + return result; + } + + void InspectorBackend::UpdateComponent(uint inEntity, const QString& inClassName, const QJsonValue& inContent) + { + auto& registry = context.GetClient().GetWorld().GetRegistry(); + const auto* compClass = Internal::FindCompClass(inClassName); + if (compClass == nullptr || !registry.Valid(inEntity) || !registry.HasDyn(compClass, inEntity)) { + return; + } + + registry.UpdateDyn(compClass, inEntity, [&](const Mirror::Any& compRef) -> void { + Internal::DynJsonSerializer::QtJsonDeserializeDyn(inContent.toObject(), *compClass, compRef); + }); + context.NotifyComponentsChanged(inEntity); + } + + void InspectorBackend::AddComponent(uint inEntity, const QString& inClassName) + { + auto& registry = context.GetClient().GetWorld().GetRegistry(); + const auto* compClass = Internal::FindCompClass(inClassName); + if (compClass == nullptr || !registry.Valid(inEntity) || registry.HasDyn(compClass, inEntity) || !compClass->HasDefaultConstructor()) { + return; + } + + registry.EmplaceDyn(compClass, inEntity, {}); + context.NotifyComponentsChanged(inEntity); + } + + void InspectorBackend::RemoveComponent(uint inEntity, const QString& inClassName) + { + auto& registry = context.GetClient().GetWorld().GetRegistry(); + const auto* compClass = Internal::FindCompClass(inClassName); + if (compClass == nullptr || !registry.Valid(inEntity) || !registry.HasDyn(compClass, inEntity)) { + return; + } + + registry.RemoveDyn(compClass, inEntity); + context.NotifyComponentsChanged(inEntity); + } + + InspectorPanel::InspectorPanel(EditorContext& inContext, QWidget* inParent) + : WebWidget(inParent) + { + Load("/editor/inspector"); + backend = new InspectorBackend(inContext, this); + GetWebChannel()->registerObject("backend", backend); + } +} diff --git a/Editor/Src/Widget/LogPanel.cpp b/Editor/Src/Widget/LogPanel.cpp new file mode 100644 index 00000000..9ddcc0a6 --- /dev/null +++ b/Editor/Src/Widget/LogPanel.cpp @@ -0,0 +1,66 @@ +// +// Created by johnk on 2026/7/5. +// + +#include +#include +#include // NOLINT + +namespace Editor::Internal { + static LogEntryInfo ToLogEntryInfo(const Core::LogEntry& inEntry) + { + static std::unordered_map levelStringMap = { + { Core::LogLevel::verbose, "verbose" }, + { Core::LogLevel::info, "info" }, + { Core::LogLevel::warning, "warning" }, + { Core::LogLevel::error, "error" } + }; + + LogEntryInfo result; + result.time = inEntry.time; + result.tag = inEntry.tag; + result.level = levelStringMap.at(inEntry.level); + result.content = inEntry.content; + return result; + } +} + +namespace Editor { + LogBackend::LogBackend(QObject* inParent) + : QObject(inParent) + { + // entries arrive from arbitrary threads, marshal them onto the widget thread before emitting to the web page + listenerHandle = EditorLogStream::Get().AddListener([this](const Core::LogEntry& inEntry) -> void { + QMetaObject::invokeMethod(this, [this, entry = inEntry]() -> void { + QJsonValue json; + QtJsonSerialize(json, Internal::ToLogEntryInfo(entry)); + emit EntryAppended(json); + }, Qt::QueuedConnection); + }); + } + + LogBackend::~LogBackend() + { + EditorLogStream::Get().RemoveListener(listenerHandle); + } + + QJsonValue LogBackend::GetHistory() const + { + std::vector entries; + for (const auto& entry : EditorLogStream::Get().Snapshot()) { + entries.emplace_back(Internal::ToLogEntryInfo(entry)); + } + + QJsonValue result; + QtJsonSerialize(result, entries); + return result; + } + + LogPanel::LogPanel(QWidget* inParent) + : WebWidget(inParent) + { + Load("/editor/log"); + backend = new LogBackend(this); + GetWebChannel()->registerObject("backend", backend); + } +} diff --git a/Editor/Src/Widget/OutlinerPanel.cpp b/Editor/Src/Widget/OutlinerPanel.cpp new file mode 100644 index 00000000..29f3ff4e --- /dev/null +++ b/Editor/Src/Widget/OutlinerPanel.cpp @@ -0,0 +1,75 @@ +// +// Created by johnk on 2026/7/5. +// + +#include + +#include +#include +#include // NOLINT +#include + +namespace Editor { + OutlinerBackend::OutlinerBackend(EditorContext& inContext, QObject* inParent) + : QObject(inParent) + , context(inContext) + { + connect(&context, &EditorContext::WorldStructureChanged, this, [this]() -> void { emit EntitiesChanged(); }); + connect(&context, &EditorContext::SelectionChanged, this, [this](Runtime::Entity) -> void { emit SelectionChanged(); }); + } + + QJsonValue OutlinerBackend::GetEntities() const + { + const auto& registry = context.GetClient().GetWorld().GetRegistry(); + + std::vector entities; + registry.Each([&](Runtime::Entity entity) -> void { + if (registry.Has(entity)) { + return; + } + OutlinerEntityInfo info; + info.id = entity; + const auto* name = registry.Find(entity); + info.name = name != nullptr && !name->value.empty() ? name->value : std::format("Entity {}", entity); + entities.emplace_back(std::move(info)); + }); + + QJsonValue result; + QtJsonSerialize(result, entities); + return result; + } + + uint OutlinerBackend::GetSelectedEntity() const + { + return context.GetSelectedEntity(); + } + + void OutlinerBackend::SelectEntity(uint inEntity) + { + context.SetSelectedEntity(inEntity); + } + + void OutlinerBackend::CreateEntity(const QString& inName) + { + const auto entity = context.CreateEntity(inName.isEmpty() ? "Entity" : inName.toStdString()); + context.SetSelectedEntity(entity); + } + + void OutlinerBackend::DestroyEntity(uint inEntity) + { + context.DestroyEntity(inEntity); + } + + void OutlinerBackend::RenameEntity(uint inEntity, const QString& inName) + { + context.RenameEntity(inEntity, inName.toStdString()); + } + + OutlinerPanel::OutlinerPanel(EditorContext& inContext, QWidget* inParent) + : WebWidget(inParent) + { + Load("/editor/outliner"); + backend = new OutlinerBackend(inContext, this); + GetWebChannel()->registerObject("backend", backend); + } +} diff --git a/Editor/Web/src/App.tsx b/Editor/Web/src/App.tsx index 6c3d3819..e6f98bef 100644 --- a/Editor/Web/src/App.tsx +++ b/Editor/Web/src/App.tsx @@ -1,12 +1,18 @@ import { Route, Routes } from 'react-router-dom'; import ProjectHubPage from '@/pages/project-hub'; import PrototypePage from '@/pages/prototype'; +import OutlinerPage from '@/pages/editor/outliner'; +import InspectorPage from '@/pages/editor/inspector'; +import LogPage from '@/pages/editor/log'; function App() { return ( } path='/project-hub' /> } path='/prototype' /> + } path='/editor/outliner' /> + } path='/editor/inspector' /> + } path='/editor/log' /> ); } diff --git a/Editor/Web/src/pages/editor/inspector.tsx b/Editor/Web/src/pages/editor/inspector.tsx new file mode 100644 index 00000000..af845514 --- /dev/null +++ b/Editor/Web/src/pages/editor/inspector.tsx @@ -0,0 +1,121 @@ +import { useEffect, useState } from 'react'; +import { Button } from '@heroui/button'; +import { Card, CardBody, CardHeader } from '@heroui/card'; +import { Textarea } from '@heroui/input'; +import { ScrollShadow } from '@heroui/scroll-shadow'; +import { Select, SelectItem, addToast } from '@heroui/react'; +import { QWebChannel } from '@/qwebchannel'; + +interface ComponentData { + className: string; + content: object; +} + +interface SelectedEntityData { + entity: number; + comps: Array; +} + +export default function InspectorPage() { + const [entityData, setEntityData] = useState(null); + const [addableComponents, setAddableComponents] = useState(Array); + const [componentToAdd, setComponentToAdd] = useState(''); + const [editedContents, setEditedContents] = useState>({}); + const [connected, setConnected] = useState(false); + + useEffect(() => { + new QWebChannel(window.qt.webChannelTransport, (channel: QWebChannel): void => { + window.backend = channel.objects.backend; + const refresh = (): void => { + setEntityData(window.backend.selectedEntityData ?? null); + setEditedContents({}); + }; + refresh(); + setAddableComponents(window.backend.addableComponents ?? []); + window.backend.SelectedEntityDataChanged.connect(refresh); + setConnected(true); + }); + }, []); + + function contentText(comp: ComponentData): string { + return editedContents[comp.className] ?? JSON.stringify(comp.content, null, 2); + } + + function onEditContent(className: string, text: string): void { + setEditedContents({ ...editedContents, [className]: text }); + } + + function onApply(comp: ComponentData): void { + if (!entityData) { + return; + } + try { + const parsed = JSON.parse(contentText(comp)); + window.backend.UpdateComponent(entityData.entity, comp.className, parsed); + } catch (e) { + addToast({ title: 'Invalid JSON', description: String(e), color: 'danger' }); + } + } + + function onRemove(comp: ComponentData): void { + if (entityData) { + window.backend.RemoveComponent(entityData.entity, comp.className); + } + } + + function onAddComponent(): void { + if (entityData && componentToAdd) { + window.backend.AddComponent(entityData.entity, componentToAdd); + } + } + + if (!entityData) { + return ( +
+ {connected ? 'No entity selected' : 'Connecting...'} +
+ ); + } + + return ( +
+
+ + +
+ + {(entityData.comps ?? []).map((comp) => ( + + + {comp.className} +
+ + +
+
+ +