diff --git a/engine/include/XCEngine/Rendering/Execution/CameraFramePlan.h b/engine/include/XCEngine/Rendering/Execution/CameraFramePlan.h index 95a0a5b2..3bdc62a3 100644 --- a/engine/include/XCEngine/Rendering/Execution/CameraFramePlan.h +++ b/engine/include/XCEngine/Rendering/Execution/CameraFramePlan.h @@ -177,20 +177,5 @@ struct CameraFramePlan { } }; -// Compatibility adapter for callers that still require CameraRenderRequest snapshots. -inline CameraRenderRequest BuildCompatibilityCameraRenderRequest( - const CameraFramePlan& plan) { - CameraRenderRequest request = plan.request; - request.shadowCaster = plan.shadowCaster; - request.directionalShadow = plan.directionalShadow; - request.postProcess = plan.postProcess; - request.finalOutput = plan.finalOutput; - request.finalColorPolicy = plan.finalColorPolicy; - request.preScenePasses = plan.preScenePasses; - request.postScenePasses = plan.postScenePasses; - request.overlayPasses = plan.overlayPasses; - return request; -} - } // namespace Rendering } // namespace XCEngine diff --git a/engine/include/XCEngine/Rendering/Execution/CameraRenderer.h b/engine/include/XCEngine/Rendering/Execution/CameraRenderer.h index 02788081..93f59ea4 100644 --- a/engine/include/XCEngine/Rendering/Execution/CameraRenderer.h +++ b/engine/include/XCEngine/Rendering/Execution/CameraRenderer.h @@ -2,7 +2,6 @@ #include #include -#include #include #include @@ -50,7 +49,6 @@ public: RenderPass* GetDepthOnlyPass() const { return m_depthOnlyPass.get(); } RenderPass* GetShadowCasterPass() const { return m_shadowCasterPass.get(); } - bool Render(const CameraRenderRequest& request); bool Render(const CameraFramePlan& plan); private: diff --git a/engine/include/XCEngine/Rendering/Graph/RenderGraph.h b/engine/include/XCEngine/Rendering/Graph/RenderGraph.h new file mode 100644 index 00000000..809bb23c --- /dev/null +++ b/engine/include/XCEngine/Rendering/Graph/RenderGraph.h @@ -0,0 +1,110 @@ +#pragma once + +#include +#include + +#include +#include + +namespace XCEngine { +namespace Rendering { + +class RenderGraphPassBuilder; +class RenderGraphBuilder; +class RenderGraphCompiler; +class RenderGraphExecutor; + +class RenderGraph { +public: + void Reset(); + + size_t GetTextureCount() const { + return m_textures.size(); + } + + size_t GetPassCount() const { + return m_passes.size(); + } + +private: + struct TextureResource { + Containers::String name; + RenderGraphTextureDesc desc = {}; + RenderGraphTextureKind kind = RenderGraphTextureKind::Transient; + RHI::RHIResourceView* importedView = nullptr; + RenderGraphImportedTextureOptions importedOptions = {}; + }; + + struct TextureAccess { + RenderGraphTextureHandle texture = {}; + RenderGraphAccessMode mode = RenderGraphAccessMode::Read; + }; + + struct PassNode { + Containers::String name; + RenderGraphPassType type = RenderGraphPassType::Raster; + std::vector accesses; + RenderGraphExecuteCallback executeCallback = {}; + }; + + std::vector m_textures; + std::vector m_passes; + + friend class RenderGraphBuilder; + friend class RenderGraphPassBuilder; + friend class RenderGraphCompiler; + friend class RenderGraphExecutor; +}; + +class RenderGraphPassBuilder { +public: + void ReadTexture(RenderGraphTextureHandle texture); + void WriteTexture(RenderGraphTextureHandle texture); + void SetExecuteCallback(RenderGraphExecuteCallback callback); + +private: + RenderGraphPassBuilder(RenderGraph* graph, RenderGraphPassHandle passHandle); + + RenderGraph* m_graph = nullptr; + RenderGraphPassHandle m_passHandle = {}; + + friend class RenderGraphBuilder; +}; + +class RenderGraphBuilder { +public: + explicit RenderGraphBuilder(RenderGraph& graph) + : m_graph(graph) { + } + + void Reset(); + + RenderGraphTextureHandle ImportTexture( + const Containers::String& name, + const RenderGraphTextureDesc& desc, + RHI::RHIResourceView* importedView = nullptr, + const RenderGraphImportedTextureOptions& importedOptions = {}); + + RenderGraphTextureHandle CreateTransientTexture( + const Containers::String& name, + const RenderGraphTextureDesc& desc); + + RenderGraphPassHandle AddRasterPass( + const Containers::String& name, + const std::function& setup); + + RenderGraphPassHandle AddComputePass( + const Containers::String& name, + const std::function& setup); + +private: + RenderGraphPassHandle AddPass( + const Containers::String& name, + RenderGraphPassType type, + const std::function& setup); + + RenderGraph& m_graph; +}; + +} // namespace Rendering +} // namespace XCEngine diff --git a/engine/include/XCEngine/Rendering/Graph/RenderGraphCompiler.h b/engine/include/XCEngine/Rendering/Graph/RenderGraphCompiler.h new file mode 100644 index 00000000..f07494f4 --- /dev/null +++ b/engine/include/XCEngine/Rendering/Graph/RenderGraphCompiler.h @@ -0,0 +1,68 @@ +#pragma once + +#include + +#include + +namespace XCEngine { +namespace Rendering { + +class CompiledRenderGraph { +public: + void Reset(); + + size_t GetPassCount() const { + return m_passes.size(); + } + + const Containers::String& GetPassName(size_t index) const; + RenderGraphPassType GetPassType(size_t index) const; + bool TryGetTextureLifetime( + RenderGraphTextureHandle handle, + RenderGraphTextureLifetime& outLifetime) const; + bool TryGetImportedTextureOptions( + RenderGraphTextureHandle handle, + RenderGraphImportedTextureOptions& outOptions) const; + +private: + struct CompiledTextureAccess { + RenderGraphTextureHandle texture = {}; + RenderGraphAccessMode mode = RenderGraphAccessMode::Read; + RHI::ResourceStates requiredState = RHI::ResourceStates::Common; + }; + + struct CompiledPass { + Containers::String name; + RenderGraphPassType type = RenderGraphPassType::Raster; + Core::uint32 originalPassIndex = kInvalidRenderGraphHandle; + std::vector accesses; + RenderGraphExecuteCallback executeCallback = {}; + }; + + struct CompiledTexture { + Containers::String name; + RenderGraphTextureDesc desc = {}; + RenderGraphTextureKind kind = RenderGraphTextureKind::Transient; + RHI::RHIResourceView* importedView = nullptr; + RenderGraphImportedTextureOptions importedOptions = {}; + }; + + std::vector m_passes; + std::vector m_textures; + std::vector m_textureLifetimes; + + friend class RenderGraphCompiler; + friend class RenderGraphExecutor; + friend class RenderGraphRuntimeResources; +}; + +class RenderGraphCompiler { +public: + static bool Compile( + const RenderGraph& graph, + CompiledRenderGraph& outCompiledGraph, + Containers::String* outErrorMessage = nullptr); +}; + +} // namespace Rendering +} // namespace XCEngine diff --git a/engine/include/XCEngine/Rendering/Graph/RenderGraphExecutor.h b/engine/include/XCEngine/Rendering/Graph/RenderGraphExecutor.h new file mode 100644 index 00000000..aec47bbe --- /dev/null +++ b/engine/include/XCEngine/Rendering/Graph/RenderGraphExecutor.h @@ -0,0 +1,17 @@ +#pragma once + +#include + +namespace XCEngine { +namespace Rendering { + +class RenderGraphExecutor { +public: + static bool Execute( + const CompiledRenderGraph& graph, + const RenderContext& renderContext, + Containers::String* outErrorMessage = nullptr); +}; + +} // namespace Rendering +} // namespace XCEngine diff --git a/engine/include/XCEngine/Rendering/Graph/RenderGraphTypes.h b/engine/include/XCEngine/Rendering/Graph/RenderGraphTypes.h new file mode 100644 index 00000000..53052d7c --- /dev/null +++ b/engine/include/XCEngine/Rendering/Graph/RenderGraphTypes.h @@ -0,0 +1,104 @@ +#pragma once + +#include +#include +#include +#include + +#include +#include + +namespace XCEngine { +namespace RHI { +class RHIResourceView; +class RHITexture; +} // namespace RHI + +namespace Rendering { + +class RenderGraphRuntimeResources; + +constexpr Core::uint32 kInvalidRenderGraphHandle = std::numeric_limits::max(); + +enum class RenderGraphPassType : Core::uint8 { + Raster = 0, + Compute = 1 +}; + +enum class RenderGraphTextureKind : Core::uint8 { + Imported = 0, + Transient = 1 +}; + +enum class RenderGraphAccessMode : Core::uint8 { + Read = 0, + Write = 1 +}; + +enum class RenderGraphTextureViewType : Core::uint8 { + RenderTarget = 0, + ShaderResource = 1 +}; + +struct RenderGraphTextureHandle { + Core::uint32 index = kInvalidRenderGraphHandle; + + bool IsValid() const { + return index != kInvalidRenderGraphHandle; + } +}; + +struct RenderGraphPassHandle { + Core::uint32 index = kInvalidRenderGraphHandle; + + bool IsValid() const { + return index != kInvalidRenderGraphHandle; + } +}; + +struct RenderGraphTextureDesc { + Core::uint32 width = 0u; + Core::uint32 height = 0u; + Core::uint32 format = static_cast(RHI::Format::Unknown); + Core::uint32 textureType = static_cast(RHI::TextureType::Texture2D); + Core::uint32 sampleCount = 1u; + Core::uint32 sampleQuality = 0u; + + bool IsValid() const { + return width > 0u && + height > 0u && + format != static_cast(RHI::Format::Unknown) && + sampleCount > 0u; + } +}; + +struct RenderGraphTextureLifetime { + RenderGraphTextureKind kind = RenderGraphTextureKind::Transient; + bool used = false; + Core::uint32 firstPassIndex = kInvalidRenderGraphHandle; + Core::uint32 lastPassIndex = kInvalidRenderGraphHandle; +}; + +struct RenderGraphImportedTextureOptions { + RHI::ResourceStates initialState = RHI::ResourceStates::Common; + RHI::ResourceStates finalState = RHI::ResourceStates::Common; + bool graphOwnsTransitions = false; +}; + +struct RenderGraphExecutionContext { + const RenderContext& renderContext; + const RenderGraphRuntimeResources* runtimeResources = nullptr; + + RHI::RHIResourceView* ResolveTextureView( + RenderGraphTextureHandle handle, + RenderGraphTextureViewType viewType) const; + bool TryGetTextureDesc( + RenderGraphTextureHandle handle, + RenderGraphTextureDesc& outDesc) const; + bool IsTransientTexture(RenderGraphTextureHandle handle) const; +}; + +using RenderGraphExecuteCallback = std::function; + +} // namespace Rendering +} // namespace XCEngine diff --git a/engine/src/Rendering/Execution/CameraRenderer.cpp b/engine/src/Rendering/Execution/CameraRenderer.cpp index 06b2aff0..824f88f1 100644 --- a/engine/src/Rendering/Execution/CameraRenderer.cpp +++ b/engine/src/Rendering/Execution/CameraRenderer.cpp @@ -3,6 +3,9 @@ #include "Components/CameraComponent.h" #include "Rendering/Caches/FullscreenPassSurfaceCache.h" #include "Rendering/Execution/DirectionalShadowExecutionState.h" +#include "Rendering/Graph/RenderGraph.h" +#include "Rendering/Graph/RenderGraphCompiler.h" +#include "Rendering/Graph/RenderGraphExecutor.h" #include "Rendering/Passes/BuiltinDepthOnlyPass.h" #include "Rendering/Passes/BuiltinObjectIdPass.h" #include "Rendering/Passes/BuiltinShadowCasterPass.h" @@ -14,12 +17,17 @@ #include "Scene/Scene.h" #include +#include +#include namespace XCEngine { namespace Rendering { namespace { +constexpr RHI::Format kRenderGraphImportedColorFormat = RHI::Format::R8G8B8A8_UNorm; +constexpr RHI::Format kRenderGraphImportedDepthFormat = RHI::Format::D24_UNorm_S8_UInt; + std::shared_ptr CreateDefaultPipelineAsset() { static const std::shared_ptr s_defaultPipelineAsset = std::make_shared(); @@ -46,6 +54,229 @@ std::unique_ptr CreatePipelineFromAsset( return std::make_unique(); } +struct RenderGraphImportedSurface { + std::vector colorTextures = {}; + RenderGraphTextureHandle depthTexture = {}; +}; + +using RenderGraphImportedTextureRegistry = + std::unordered_map; + +enum class RenderGraphSurfaceImportUsage { + Source = 0, + Output = 1 +}; + +Containers::String BuildRenderGraphResourceName( + const Containers::String& surfaceName, + const char* slotName, + size_t index = 0u, + bool indexed = false) { + std::string name = surfaceName.CStr(); + name += '.'; + name += slotName; + if (indexed) { + name += std::to_string(index); + } + + return Containers::String(name.c_str()); +} + +RenderGraphTextureDesc BuildImportedTextureDesc( + const RenderSurface& surface, + RHI::Format format) { + RenderGraphTextureDesc desc = {}; + desc.width = surface.GetWidth() > 0u ? surface.GetWidth() : surface.GetRenderAreaWidth(); + desc.height = surface.GetHeight() > 0u ? surface.GetHeight() : surface.GetRenderAreaHeight(); + desc.format = static_cast(format); + desc.textureType = static_cast(RHI::TextureType::Texture2D); + desc.sampleCount = std::max(surface.GetSampleCount(), 1u); + desc.sampleQuality = surface.GetSampleQuality(); + return desc; +} + +RenderGraphImportedTextureOptions BuildImportedTextureOptions( + const RenderSurface& surface, + bool isDepth, + RenderGraphSurfaceImportUsage usage) { + const RHI::ResourceStates beforeState = + isDepth ? surface.GetDepthStateBefore() : surface.GetColorStateBefore(); + const RHI::ResourceStates afterState = + isDepth ? surface.GetDepthStateAfter() : surface.GetColorStateAfter(); + + RenderGraphImportedTextureOptions options = {}; + options.initialState = + usage == RenderGraphSurfaceImportUsage::Output + ? beforeState + : afterState; + options.finalState = + usage == RenderGraphSurfaceImportUsage::Output + ? afterState + : options.initialState; + options.graphOwnsTransitions = false; + return options; +} + +RenderGraphTextureHandle ImportRenderGraphTexture( + RenderGraphBuilder& builder, + RenderGraphImportedTextureRegistry& registry, + const Containers::String& name, + const RenderGraphTextureDesc& desc, + RHI::RHIResourceView* view, + const RenderGraphImportedTextureOptions& importedOptions) { + if (view == nullptr) { + return {}; + } + + const auto existing = registry.find(view); + if (existing != registry.end()) { + return existing->second; + } + + const RenderGraphTextureHandle handle = + builder.ImportTexture(name, desc, view, importedOptions); + registry.emplace(view, handle); + return handle; +} + +RenderGraphImportedSurface ImportRenderGraphSurface( + RenderGraphBuilder& builder, + RenderGraphImportedTextureRegistry& registry, + const Containers::String& surfaceName, + const RenderSurface* surface, + RenderGraphSurfaceImportUsage usage) { + RenderGraphImportedSurface importedSurface = {}; + if (surface == nullptr) { + return importedSurface; + } + + const RenderGraphTextureDesc colorDesc = + BuildImportedTextureDesc(*surface, kRenderGraphImportedColorFormat); + const std::vector& colorAttachments = surface->GetColorAttachments(); + importedSurface.colorTextures.reserve(colorAttachments.size()); + for (size_t colorIndex = 0; colorIndex < colorAttachments.size(); ++colorIndex) { + RHI::RHIResourceView* colorAttachment = colorAttachments[colorIndex]; + if (colorAttachment == nullptr) { + continue; + } + + importedSurface.colorTextures.push_back( + ImportRenderGraphTexture( + builder, + registry, + BuildRenderGraphResourceName(surfaceName, "Color", colorIndex, true), + colorDesc, + colorAttachment, + BuildImportedTextureOptions( + *surface, + false, + usage))); + } + + if (RHI::RHIResourceView* depthAttachment = surface->GetDepthAttachment(); + depthAttachment != nullptr) { + importedSurface.depthTexture = + ImportRenderGraphTexture( + builder, + registry, + BuildRenderGraphResourceName(surfaceName, "Depth"), + BuildImportedTextureDesc(*surface, kRenderGraphImportedDepthFormat), + depthAttachment, + BuildImportedTextureOptions( + *surface, + true, + usage)); + } + + return importedSurface; +} + +RenderGraphTextureHandle GetPrimaryColorTexture( + const RenderGraphImportedSurface& surface) { + for (RenderGraphTextureHandle texture : surface.colorTextures) { + if (texture.IsValid()) { + return texture; + } + } + + return {}; +} + +void ReadRenderGraphSurface( + RenderGraphPassBuilder& passBuilder, + const RenderGraphImportedSurface& surface) { + for (RenderGraphTextureHandle texture : surface.colorTextures) { + if (texture.IsValid()) { + passBuilder.ReadTexture(texture); + } + } + + if (surface.depthTexture.IsValid()) { + passBuilder.ReadTexture(surface.depthTexture); + } +} + +void ReadRenderGraphColorSurface( + RenderGraphPassBuilder& passBuilder, + const RenderGraphImportedSurface& surface) { + for (RenderGraphTextureHandle texture : surface.colorTextures) { + if (texture.IsValid()) { + passBuilder.ReadTexture(texture); + } + } +} + +void WriteRenderGraphSurface( + RenderGraphPassBuilder& passBuilder, + const RenderGraphImportedSurface& surface) { + for (RenderGraphTextureHandle texture : surface.colorTextures) { + if (texture.IsValid()) { + passBuilder.WriteTexture(texture); + } + } + + if (surface.depthTexture.IsValid()) { + passBuilder.WriteTexture(surface.depthTexture); + } +} + +void WriteRenderGraphColorSurface( + RenderGraphPassBuilder& passBuilder, + const RenderGraphImportedSurface& surface) { + for (RenderGraphTextureHandle texture : surface.colorTextures) { + if (texture.IsValid()) { + passBuilder.WriteTexture(texture); + } + } +} + +bool IsFullscreenSequenceStage( + CameraFrameStage stage) { + return stage == CameraFrameStage::PostProcess || + stage == CameraFrameStage::FinalOutput; +} + +Containers::String BuildRenderGraphSequencePassName( + const Containers::String& stageName, + size_t passIndex) { + std::string name = stageName.CStr(); + name += ".Pass"; + name += std::to_string(passIndex); + return Containers::String(name.c_str()); +} + +RenderGraphTextureDesc BuildFullscreenTransientTextureDesc( + const RenderSurface& surface) { + RenderGraphTextureDesc desc = {}; + desc.width = surface.GetWidth() > 0u ? surface.GetWidth() : surface.GetRenderAreaWidth(); + desc.height = surface.GetHeight() > 0u ? surface.GetHeight() : surface.GetRenderAreaHeight(); + desc.format = static_cast(kRenderGraphImportedColorFormat); + desc.textureType = static_cast(RHI::TextureType::Texture2D); + desc.sampleCount = 1u; + desc.sampleQuality = 0u; + return desc; +} + Resources::ShaderKeywordSet BuildSceneGlobalShaderKeywords( const RenderSceneData& sceneData) { Resources::ShaderKeywordSet keywords = {}; @@ -174,6 +405,17 @@ private: bool m_failed = false; }; +bool EnsureInitializedPassSequence( + std::unique_ptr& activeSequence, + RenderPassSequence* sequence, + const RenderContext& context) { + if (activeSequence == nullptr) { + activeSequence = std::make_unique(sequence, context); + } + + return activeSequence->IsReady(); +} + struct CameraFrameExecutionState { RenderPipeline* pipeline = nullptr; RenderPass* objectIdPass = nullptr; @@ -222,8 +464,13 @@ bool ExecuteFullscreenPassSequenceStage( return false; } - if (sequence == nullptr || sequence->GetPassCount() <= 1u) { - return activeSequence->Execute(passContext); + if (sequence == nullptr) { + return true; + } + + if (sequence->GetPassCount() <= 1u) { + return sequence->GetPassCount() == 0u || + sequence->ExecutePass(0u, passContext); } if (surfaceCache == nullptr || @@ -288,6 +535,44 @@ bool ExecuteFullscreenPassSequenceStage( return true; } +bool TryBuildRenderGraphTransientSurface( + const RenderSurface& templateSurface, + const RenderGraphExecutionContext& graphContext, + RenderGraphTextureHandle textureHandle, + RenderSurface& outSurface) { + if (!textureHandle.IsValid() || + !graphContext.IsTransientTexture(textureHandle)) { + return false; + } + + RenderGraphTextureDesc textureDesc = {}; + RHI::RHIResourceView* renderTargetView = + graphContext.ResolveTextureView( + textureHandle, + RenderGraphTextureViewType::RenderTarget); + if (renderTargetView == nullptr || + !graphContext.TryGetTextureDesc(textureHandle, textureDesc)) { + return false; + } + + outSurface = templateSurface; + CopyIntermediateSurfaceLayout(templateSurface, outSurface); + outSurface.SetColorAttachment(renderTargetView); + outSurface.SetAutoTransitionEnabled(false); + outSurface.SetSampleDesc(textureDesc.sampleCount, textureDesc.sampleQuality); + outSurface.SetColorStateBefore(RHI::ResourceStates::RenderTarget); + outSurface.SetColorStateAfter(RHI::ResourceStates::PixelShaderResource); + return true; +} + +bool ExecuteFullscreenPassSequencePass( + RenderPassSequence* sequence, + size_t passIndex, + const RenderPassContext& passContext, + const RenderGraphExecutionContext& graphContext, + RenderGraphTextureHandle sourceColorHandle, + RenderGraphTextureHandle outputColorHandle); + RenderPassContext BuildFrameStagePassContext( CameraFrameStage stage, const CameraFramePlan& plan, @@ -378,6 +663,170 @@ bool ExecuteFrameStage( } } +bool ExecuteRenderGraphPlan( + const CameraFramePlan& plan, + const DirectionalShadowExecutionState& shadowState, + const RenderSceneData& sceneData, + CameraFrameExecutionState& executionState) { + RenderGraph graph = {}; + RenderGraphBuilder graphBuilder(graph); + RenderGraphImportedTextureRegistry importedTextures = {}; + + bool stageExecutionSucceeded = true; + for (const CameraFrameStageInfo& stageInfo : kOrderedCameraFrameStages) { + if (!plan.HasFrameStage(stageInfo.stage) && + stageInfo.stage != CameraFrameStage::MainScene) { + continue; + } + + const CameraFrameStage stage = stageInfo.stage; + const Containers::String stageName(GetCameraFrameStageName(stageInfo.stage)); + RenderPassSequence* const stageSequence = plan.GetPassSequence(stage); + + if (IsFullscreenSequenceStage(stage) && + stageSequence != nullptr && + stageSequence->GetPassCount() > 1u) { + const RenderPassContext stagePassContext = + BuildFrameStagePassContext(stage, plan, sceneData); + const RenderGraphImportedSurface sourceSurface = + ImportRenderGraphSurface( + graphBuilder, + importedTextures, + stageName + ".Source", + stagePassContext.sourceSurface, + RenderGraphSurfaceImportUsage::Source); + const RenderGraphImportedSurface outputSurface = + ImportRenderGraphSurface( + graphBuilder, + importedTextures, + stageName + ".Output", + &stagePassContext.surface, + RenderGraphSurfaceImportUsage::Output); + RenderGraphTextureHandle currentSourceColor = GetPrimaryColorTexture(sourceSurface); + const RenderGraphTextureHandle finalOutputColor = + GetPrimaryColorTexture(outputSurface); + const RenderGraphTextureDesc transientDesc = + BuildFullscreenTransientTextureDesc(stagePassContext.surface); + + for (size_t passIndex = 0; passIndex < stageSequence->GetPassCount(); ++passIndex) { + const bool isLastPass = (passIndex + 1u) == stageSequence->GetPassCount(); + const size_t sequencePassIndex = passIndex; + const RenderGraphTextureHandle passSourceColor = currentSourceColor; + const RenderGraphTextureHandle passOutputColor = + isLastPass + ? finalOutputColor + : graphBuilder.CreateTransientTexture( + BuildRenderGraphSequencePassName(stageName, sequencePassIndex) + ".Color", + transientDesc); + + graphBuilder.AddRasterPass( + BuildRenderGraphSequencePassName(stageName, sequencePassIndex), + [&, stage, sequencePassIndex, passSourceColor, passOutputColor]( + RenderGraphPassBuilder& passBuilder) { + if (passSourceColor.IsValid()) { + passBuilder.ReadTexture(passSourceColor); + } + if (passOutputColor.IsValid()) { + passBuilder.WriteTexture(passOutputColor); + } + passBuilder.SetExecuteCallback( + [&, stage, sequencePassIndex, passSourceColor, passOutputColor]( + const RenderGraphExecutionContext& executionContext) { + if (!stageExecutionSucceeded) { + return; + } + + std::unique_ptr& activeSequence = + stage == CameraFrameStage::PostProcess + ? executionState.postProcessPasses + : executionState.finalOutputPasses; + if (!EnsureInitializedPassSequence( + activeSequence, + plan.GetPassSequence(stage), + plan.request.context)) { + stageExecutionSucceeded = false; + return; + } + + stageExecutionSucceeded = + ExecuteFullscreenPassSequencePass( + plan.GetPassSequence(stage), + sequencePassIndex, + BuildFrameStagePassContext(stage, plan, sceneData), + executionContext, + passSourceColor, + passOutputColor); + }); + }); + + currentSourceColor = passOutputColor; + } + + continue; + } + + const RenderGraphImportedSurface sourceSurface = + ImportRenderGraphSurface( + graphBuilder, + importedTextures, + stageName + ".Source", + plan.GetSourceSurface(stageInfo.stage), + RenderGraphSurfaceImportUsage::Source); + const RenderGraphImportedSurface outputSurface = + ImportRenderGraphSurface( + graphBuilder, + importedTextures, + stageName + ".Output", + plan.GetOutputSurface(stageInfo.stage), + RenderGraphSurfaceImportUsage::Output); + + graphBuilder.AddRasterPass( + stageName, + [&, sourceSurface, outputSurface, stage](RenderGraphPassBuilder& passBuilder) { + if (IsFullscreenSequenceStage(stage)) { + ReadRenderGraphColorSurface(passBuilder, sourceSurface); + WriteRenderGraphColorSurface(passBuilder, outputSurface); + } else { + ReadRenderGraphSurface(passBuilder, sourceSurface); + WriteRenderGraphSurface(passBuilder, outputSurface); + } + passBuilder.SetExecuteCallback( + [&, stage](const RenderGraphExecutionContext&) { + if (!stageExecutionSucceeded) { + return; + } + + stageExecutionSucceeded = ExecuteFrameStage( + stage, + plan, + shadowState, + sceneData, + executionState); + }); + }); + } + + CompiledRenderGraph compiledGraph = {}; + Containers::String errorMessage; + if (!RenderGraphCompiler::Compile(graph, compiledGraph, &errorMessage)) { + Debug::Logger::Get().Error( + Debug::LogCategory::Rendering, + Containers::String("CameraRenderer::Render failed: RenderGraph compile failed: ") + + errorMessage); + return false; + } + + if (!RenderGraphExecutor::Execute(compiledGraph, plan.request.context, &errorMessage)) { + Debug::Logger::Get().Error( + Debug::LogCategory::Rendering, + Containers::String("CameraRenderer::Render failed: RenderGraph execute failed: ") + + errorMessage); + return false; + } + + return stageExecutionSucceeded; +} + RenderEnvironmentData BuildEnvironmentData(const CameraFramePlan& plan) { RenderEnvironmentData environment = {}; const RenderSurface& mainSceneSurface = plan.GetMainSceneSurface(); @@ -547,6 +996,76 @@ bool CameraRenderer::BuildSceneDataForPlan( return true; } +namespace { + +bool ExecuteFullscreenPassSequencePass( + RenderPassSequence* sequence, + size_t passIndex, + const RenderPassContext& passContext, + const RenderGraphExecutionContext& graphContext, + RenderGraphTextureHandle sourceColorHandle, + RenderGraphTextureHandle outputColorHandle) { + if (sequence == nullptr || passIndex >= sequence->GetPassCount()) { + return false; + } + + if (sequence->GetPassCount() <= 1u) { + return sequence->ExecutePass(passIndex, passContext); + } + + const RenderSurface* currentSourceSurface = passContext.sourceSurface; + RHI::RHIResourceView* currentSourceColorView = passContext.sourceColorView; + RHI::ResourceStates currentSourceColorState = passContext.sourceColorState; + RenderSurface transientSourceSurface = {}; + if (sourceColorHandle.IsValid() && + graphContext.IsTransientTexture(sourceColorHandle)) { + if (!TryBuildRenderGraphTransientSurface( + passContext.surface, + graphContext, + sourceColorHandle, + transientSourceSurface)) { + return false; + } + + currentSourceSurface = &transientSourceSurface; + currentSourceColorView = + graphContext.ResolveTextureView( + sourceColorHandle, + RenderGraphTextureViewType::ShaderResource); + if (currentSourceColorView == nullptr) { + return false; + } + currentSourceColorState = RHI::ResourceStates::PixelShaderResource; + } + + const RenderSurface* outputSurface = &passContext.surface; + RenderSurface transientOutputSurface = {}; + if (outputColorHandle.IsValid() && + graphContext.IsTransientTexture(outputColorHandle)) { + if (!TryBuildRenderGraphTransientSurface( + passContext.surface, + graphContext, + outputColorHandle, + transientOutputSurface)) { + return false; + } + + outputSurface = &transientOutputSurface; + } + + const RenderPassContext chainedContext = { + passContext.renderContext, + *outputSurface, + passContext.sceneData, + currentSourceSurface, + currentSourceColorView, + currentSourceColorState + }; + return sequence->ExecutePass(passIndex, chainedContext); +} + +} // namespace + bool CameraRenderer::ExecuteRenderPlan( const CameraFramePlan& plan, const DirectionalShadowExecutionState& shadowState, @@ -558,29 +1077,7 @@ bool CameraRenderer::ExecuteRenderPlan( executionState.shadowCasterPass = m_shadowCasterPass.get(); executionState.postProcessSurfaceCache = m_postProcessSurfaceCache.get(); executionState.finalOutputSurfaceCache = m_finalOutputSurfaceCache.get(); - - for (const CameraFrameStageInfo& stageInfo : kOrderedCameraFrameStages) { - if (!plan.HasFrameStage(stageInfo.stage) && - stageInfo.stage != CameraFrameStage::MainScene) { - continue; - } - - if (!ExecuteFrameStage( - stageInfo.stage, - plan, - shadowState, - sceneData, - executionState)) { - return false; - } - } - - return true; -} - -bool CameraRenderer::Render( - const CameraRenderRequest& request) { - return Render(CameraFramePlan::FromRequest(request)); + return ExecuteRenderGraphPlan(plan, shadowState, sceneData, executionState); } bool CameraRenderer::Render( diff --git a/engine/src/Rendering/Execution/SceneRenderer.cpp b/engine/src/Rendering/Execution/SceneRenderer.cpp index 225d58ab..f41c8bfb 100644 --- a/engine/src/Rendering/Execution/SceneRenderer.cpp +++ b/engine/src/Rendering/Execution/SceneRenderer.cpp @@ -44,21 +44,6 @@ void SceneRenderer::SetPipelineAsset(std::shared_ptr m_cameraRenderer.SetPipelineAsset(std::move(pipelineAsset)); } -std::vector SceneRenderer::BuildRenderRequests( - const Components::Scene& scene, - Components::CameraComponent* overrideCamera, - const RenderContext& context, - const RenderSurface& surface) { - const std::vector plans = - BuildFramePlans(scene, overrideCamera, context, surface); - std::vector requests = {}; - requests.reserve(plans.size()); - for (const CameraFramePlan& plan : plans) { - requests.push_back(BuildCompatibilityCameraRenderRequest(plan)); - } - return requests; -} - std::vector SceneRenderer::BuildFramePlans( const Components::Scene& scene, Components::CameraComponent* overrideCamera, @@ -71,15 +56,6 @@ std::vector SceneRenderer::BuildFramePlans( : std::vector(); } -bool SceneRenderer::Render(const CameraRenderRequest& request) { - return Render(CameraFramePlan::FromRequest(request)); -} - -bool SceneRenderer::Render(const std::vector& requests) { - std::vector plans = CreateFramePlansFromLegacyRequests(requests); - return Render(plans); -} - bool SceneRenderer::Render(const CameraFramePlan& plan) { return m_cameraRenderer.Render(plan); } @@ -130,16 +106,5 @@ bool SceneRenderer::Render( return Render(BuildFramePlans(scene, overrideCamera, context, surface)); } -std::vector SceneRenderer::CreateFramePlansFromLegacyRequests( - const std::vector& requests) const { - std::vector plans = {}; - plans.reserve(requests.size()); - for (const CameraRenderRequest& request : requests) { - plans.push_back(CameraFramePlan::FromRequest(request)); - } - - return plans; -} - } // namespace Rendering } // namespace XCEngine diff --git a/engine/src/Rendering/Graph/RenderGraph.cpp b/engine/src/Rendering/Graph/RenderGraph.cpp new file mode 100644 index 00000000..6be68113 --- /dev/null +++ b/engine/src/Rendering/Graph/RenderGraph.cpp @@ -0,0 +1,120 @@ +#include + +#include + +namespace XCEngine { +namespace Rendering { + +void RenderGraph::Reset() { + m_textures.clear(); + m_passes.clear(); +} + +RenderGraphPassBuilder::RenderGraphPassBuilder( + RenderGraph* graph, + RenderGraphPassHandle passHandle) + : m_graph(graph) + , m_passHandle(passHandle) { +} + +void RenderGraphPassBuilder::ReadTexture(RenderGraphTextureHandle texture) { + if (m_graph == nullptr || !m_passHandle.IsValid()) { + return; + } + + RenderGraph::TextureAccess access = {}; + access.texture = texture; + access.mode = RenderGraphAccessMode::Read; + m_graph->m_passes[m_passHandle.index].accesses.push_back(access); +} + +void RenderGraphPassBuilder::WriteTexture(RenderGraphTextureHandle texture) { + if (m_graph == nullptr || !m_passHandle.IsValid()) { + return; + } + + RenderGraph::TextureAccess access = {}; + access.texture = texture; + access.mode = RenderGraphAccessMode::Write; + m_graph->m_passes[m_passHandle.index].accesses.push_back(access); +} + +void RenderGraphPassBuilder::SetExecuteCallback(RenderGraphExecuteCallback callback) { + if (m_graph == nullptr || !m_passHandle.IsValid()) { + return; + } + + m_graph->m_passes[m_passHandle.index].executeCallback = std::move(callback); +} + +void RenderGraphBuilder::Reset() { + m_graph.Reset(); +} + +RenderGraphTextureHandle RenderGraphBuilder::ImportTexture( + const Containers::String& name, + const RenderGraphTextureDesc& desc, + RHI::RHIResourceView* importedView, + const RenderGraphImportedTextureOptions& importedOptions) { + RenderGraph::TextureResource resource = {}; + resource.name = name; + resource.desc = desc; + resource.kind = RenderGraphTextureKind::Imported; + resource.importedView = importedView; + resource.importedOptions = importedOptions; + m_graph.m_textures.push_back(resource); + + RenderGraphTextureHandle handle = {}; + handle.index = static_cast(m_graph.m_textures.size() - 1u); + return handle; +} + +RenderGraphTextureHandle RenderGraphBuilder::CreateTransientTexture( + const Containers::String& name, + const RenderGraphTextureDesc& desc) { + RenderGraph::TextureResource resource = {}; + resource.name = name; + resource.desc = desc; + resource.kind = RenderGraphTextureKind::Transient; + resource.importedView = nullptr; + m_graph.m_textures.push_back(resource); + + RenderGraphTextureHandle handle = {}; + handle.index = static_cast(m_graph.m_textures.size() - 1u); + return handle; +} + +RenderGraphPassHandle RenderGraphBuilder::AddRasterPass( + const Containers::String& name, + const std::function& setup) { + return AddPass(name, RenderGraphPassType::Raster, setup); +} + +RenderGraphPassHandle RenderGraphBuilder::AddComputePass( + const Containers::String& name, + const std::function& setup) { + return AddPass(name, RenderGraphPassType::Compute, setup); +} + +RenderGraphPassHandle RenderGraphBuilder::AddPass( + const Containers::String& name, + RenderGraphPassType type, + const std::function& setup) { + RenderGraph::PassNode pass = {}; + pass.name = name; + pass.type = type; + m_graph.m_passes.push_back(pass); + + RenderGraphPassHandle handle = {}; + handle.index = static_cast(m_graph.m_passes.size() - 1u); + + if (setup) { + RenderGraphPassBuilder passBuilder(&m_graph, handle); + setup(passBuilder); + } + + return handle; +} + +} // namespace Rendering +} // namespace XCEngine diff --git a/engine/src/Rendering/Graph/RenderGraphCompiler.cpp b/engine/src/Rendering/Graph/RenderGraphCompiler.cpp new file mode 100644 index 00000000..cc7c6af0 --- /dev/null +++ b/engine/src/Rendering/Graph/RenderGraphCompiler.cpp @@ -0,0 +1,253 @@ +#include + +#include +#include +#include + +namespace XCEngine { +namespace Rendering { +namespace { + +RHI::ResourceStates ResolveRequiredState( + RenderGraphPassType passType, + RenderGraphAccessMode accessMode) { + if (accessMode == RenderGraphAccessMode::Write) { + return passType == RenderGraphPassType::Compute + ? RHI::ResourceStates::UnorderedAccess + : RHI::ResourceStates::RenderTarget; + } + + return passType == RenderGraphPassType::Compute + ? RHI::ResourceStates::GenericRead + : RHI::ResourceStates::PixelShaderResource; +} + +void WriteError( + const Containers::String& message, + Containers::String* outErrorMessage) { + if (outErrorMessage != nullptr) { + *outErrorMessage = message; + } +} + +} // namespace + +void CompiledRenderGraph::Reset() { + m_passes.clear(); + m_textures.clear(); + m_textureLifetimes.clear(); +} + +const Containers::String& CompiledRenderGraph::GetPassName(size_t index) const { + static const Containers::String kEmptyString; + return index < m_passes.size() ? m_passes[index].name : kEmptyString; +} + +RenderGraphPassType CompiledRenderGraph::GetPassType(size_t index) const { + return index < m_passes.size() + ? m_passes[index].type + : RenderGraphPassType::Raster; +} + +bool CompiledRenderGraph::TryGetTextureLifetime( + RenderGraphTextureHandle handle, + RenderGraphTextureLifetime& outLifetime) const { + if (!handle.IsValid() || handle.index >= m_textureLifetimes.size()) { + return false; + } + + outLifetime = m_textureLifetimes[handle.index]; + return true; +} + +bool CompiledRenderGraph::TryGetImportedTextureOptions( + RenderGraphTextureHandle handle, + RenderGraphImportedTextureOptions& outOptions) const { + if (!handle.IsValid() || handle.index >= m_textures.size()) { + return false; + } + + const CompiledRenderGraph::CompiledTexture& texture = m_textures[handle.index]; + if (texture.kind != RenderGraphTextureKind::Imported) { + return false; + } + + outOptions = texture.importedOptions; + return true; +} + +bool RenderGraphCompiler::Compile( + const RenderGraph& graph, + CompiledRenderGraph& outCompiledGraph, + Containers::String* outErrorMessage) { + outCompiledGraph.Reset(); + + for (const RenderGraph::TextureResource& texture : graph.m_textures) { + if (!texture.desc.IsValid()) { + WriteError( + Containers::String("RenderGraph texture desc is invalid: ") + texture.name, + outErrorMessage); + return false; + } + } + + const size_t passCount = graph.m_passes.size(); + const size_t textureCount = graph.m_textures.size(); + + std::vector> outgoingEdges(passCount); + std::vector incomingEdgeCount(passCount, 0u); + std::unordered_set edgeKeys; + std::vector lastWriter(textureCount, kInvalidRenderGraphHandle); + std::vector> lastReaders(textureCount); + + auto addUniqueReader = []( + std::vector& readers, + Core::uint32 passIndex) { + if (std::find(readers.begin(), readers.end(), passIndex) == readers.end()) { + readers.push_back(passIndex); + } + }; + + auto addEdge = [&]( + Core::uint32 from, + Core::uint32 to) { + if (from == kInvalidRenderGraphHandle || to == kInvalidRenderGraphHandle || from == to) { + return; + } + + const Core::uint64 key = + (static_cast(from) << 32u) | static_cast(to); + if (!edgeKeys.insert(key).second) { + return; + } + + outgoingEdges[from].push_back(to); + ++incomingEdgeCount[to]; + }; + + for (Core::uint32 passIndex = 0u; passIndex < static_cast(passCount); ++passIndex) { + const RenderGraph::PassNode& pass = graph.m_passes[passIndex]; + for (const RenderGraph::TextureAccess& access : pass.accesses) { + if (!access.texture.IsValid() || access.texture.index >= textureCount) { + WriteError( + Containers::String("RenderGraph pass '") + pass.name + + "' references an invalid texture handle", + outErrorMessage); + return false; + } + + const RenderGraph::TextureResource& texture = graph.m_textures[access.texture.index]; + std::vector& readers = lastReaders[access.texture.index]; + Core::uint32& writer = lastWriter[access.texture.index]; + + if (access.mode == RenderGraphAccessMode::Read) { + if (texture.kind == RenderGraphTextureKind::Transient && + writer == kInvalidRenderGraphHandle) { + WriteError( + Containers::String("RenderGraph transient texture '") + texture.name + + "' is read before any pass writes it", + outErrorMessage); + return false; + } + + addEdge(writer, passIndex); + addUniqueReader(readers, passIndex); + continue; + } + + addEdge(writer, passIndex); + for (Core::uint32 readerPassIndex : readers) { + addEdge(readerPassIndex, passIndex); + } + readers.clear(); + writer = passIndex; + } + } + + std::vector executionOrder; + executionOrder.reserve(passCount); + std::vector emitted(passCount, false); + + while (executionOrder.size() < passCount) { + bool progressed = false; + for (Core::uint32 passIndex = 0u; passIndex < static_cast(passCount); ++passIndex) { + if (emitted[passIndex] || incomingEdgeCount[passIndex] != 0u) { + continue; + } + + emitted[passIndex] = true; + executionOrder.push_back(passIndex); + for (Core::uint32 dependentPassIndex : outgoingEdges[passIndex]) { + if (incomingEdgeCount[dependentPassIndex] > 0u) { + --incomingEdgeCount[dependentPassIndex]; + } + } + + progressed = true; + break; + } + + if (!progressed) { + WriteError( + "RenderGraph failed to compile because pass dependencies contain a cycle", + outErrorMessage); + outCompiledGraph.Reset(); + return false; + } + } + + outCompiledGraph.m_textureLifetimes.resize(textureCount); + for (size_t textureIndex = 0u; textureIndex < textureCount; ++textureIndex) { + CompiledRenderGraph::CompiledTexture compiledTexture = {}; + compiledTexture.name = graph.m_textures[textureIndex].name; + compiledTexture.desc = graph.m_textures[textureIndex].desc; + compiledTexture.kind = graph.m_textures[textureIndex].kind; + compiledTexture.importedView = graph.m_textures[textureIndex].importedView; + compiledTexture.importedOptions = graph.m_textures[textureIndex].importedOptions; + outCompiledGraph.m_textures.push_back(std::move(compiledTexture)); + + outCompiledGraph.m_textureLifetimes[textureIndex].kind = graph.m_textures[textureIndex].kind; + } + + outCompiledGraph.m_passes.reserve(passCount); + for (Core::uint32 compiledPassIndex = 0u; + compiledPassIndex < static_cast(executionOrder.size()); + ++compiledPassIndex) { + const Core::uint32 originalPassIndex = executionOrder[compiledPassIndex]; + const RenderGraph::PassNode& sourcePass = graph.m_passes[originalPassIndex]; + + CompiledRenderGraph::CompiledPass compiledPass = {}; + compiledPass.name = sourcePass.name; + compiledPass.type = sourcePass.type; + compiledPass.originalPassIndex = originalPassIndex; + compiledPass.accesses.reserve(sourcePass.accesses.size()); + for (const RenderGraph::TextureAccess& access : sourcePass.accesses) { + CompiledRenderGraph::CompiledTextureAccess compiledAccess = {}; + compiledAccess.texture = access.texture; + compiledAccess.mode = access.mode; + compiledAccess.requiredState = + ResolveRequiredState(sourcePass.type, access.mode); + compiledPass.accesses.push_back(compiledAccess); + } + compiledPass.executeCallback = sourcePass.executeCallback; + outCompiledGraph.m_passes.push_back(std::move(compiledPass)); + + for (const RenderGraph::TextureAccess& access : sourcePass.accesses) { + RenderGraphTextureLifetime& lifetime = + outCompiledGraph.m_textureLifetimes[access.texture.index]; + if (!lifetime.used) { + lifetime.used = true; + lifetime.firstPassIndex = compiledPassIndex; + } + lifetime.lastPassIndex = compiledPassIndex; + } + } + + if (outErrorMessage != nullptr) { + outErrorMessage->Clear(); + } + return true; +} + +} // namespace Rendering +} // namespace XCEngine diff --git a/engine/src/Rendering/Graph/RenderGraphExecutor.cpp b/engine/src/Rendering/Graph/RenderGraphExecutor.cpp new file mode 100644 index 00000000..9e09159f --- /dev/null +++ b/engine/src/Rendering/Graph/RenderGraphExecutor.cpp @@ -0,0 +1,293 @@ +#include + +#include +#include +#include + +namespace XCEngine { +namespace Rendering { +namespace { + +bool IsGraphManagedTransientState(RHI::ResourceStates state) { + return state == RHI::ResourceStates::RenderTarget || + state == RHI::ResourceStates::PixelShaderResource || + state == RHI::ResourceStates::GenericRead; +} + +RenderGraphTextureViewType ResolveBarrierViewType(RHI::ResourceStates state) { + return state == RHI::ResourceStates::RenderTarget + ? RenderGraphTextureViewType::RenderTarget + : RenderGraphTextureViewType::ShaderResource; +} + +} // namespace + +class RenderGraphRuntimeResources { +public: + explicit RenderGraphRuntimeResources(const CompiledRenderGraph& graph) + : m_graph(graph) + , m_textureAllocations(graph.m_textures.size()) + , m_textureStates( + graph.m_textures.size(), + RHI::ResourceStates::Common) { + } + + ~RenderGraphRuntimeResources() { + Reset(); + } + + bool Initialize( + const RenderContext& renderContext, + Containers::String* outErrorMessage) { + for (size_t textureIndex = 0u; textureIndex < m_graph.m_textures.size(); ++textureIndex) { + const CompiledRenderGraph::CompiledTexture& texture = m_graph.m_textures[textureIndex]; + const RenderGraphTextureLifetime& lifetime = m_graph.m_textureLifetimes[textureIndex]; + if (texture.kind != RenderGraphTextureKind::Transient || !lifetime.used) { + continue; + } + + if (renderContext.device == nullptr) { + if (outErrorMessage != nullptr) { + *outErrorMessage = + Containers::String("RenderGraph cannot allocate transient texture without a valid device: ") + + texture.name; + } + Reset(); + return false; + } + + if (!CreateTransientTexture(renderContext, texture, m_textureAllocations[textureIndex])) { + if (outErrorMessage != nullptr) { + *outErrorMessage = + Containers::String("RenderGraph failed to allocate transient texture: ") + + texture.name; + } + Reset(); + return false; + } + } + + return true; + } + + RHI::RHIResourceView* ResolveTextureView( + RenderGraphTextureHandle handle, + RenderGraphTextureViewType viewType) const { + if (!handle.IsValid() || handle.index >= m_graph.m_textures.size()) { + return nullptr; + } + + const CompiledRenderGraph::CompiledTexture& texture = m_graph.m_textures[handle.index]; + if (texture.kind == RenderGraphTextureKind::Imported) { + return texture.importedView; + } + + const TextureAllocation& allocation = m_textureAllocations[handle.index]; + switch (viewType) { + case RenderGraphTextureViewType::RenderTarget: + return allocation.renderTargetView; + case RenderGraphTextureViewType::ShaderResource: + return allocation.shaderResourceView; + default: + return nullptr; + } + } + + bool TryGetTextureDesc( + RenderGraphTextureHandle handle, + RenderGraphTextureDesc& outDesc) const { + if (!handle.IsValid() || handle.index >= m_graph.m_textures.size()) { + return false; + } + + outDesc = m_graph.m_textures[handle.index].desc; + return true; + } + + bool IsTransientTexture(RenderGraphTextureHandle handle) const { + return handle.IsValid() && + handle.index < m_graph.m_textures.size() && + m_graph.m_textures[handle.index].kind == RenderGraphTextureKind::Transient; + } + + bool TransitionPassResources( + const CompiledRenderGraph::CompiledPass& pass, + const RenderContext& renderContext, + Containers::String* outErrorMessage) { + for (const CompiledRenderGraph::CompiledTextureAccess& access : pass.accesses) { + if (!access.texture.IsValid() || + access.texture.index >= m_graph.m_textures.size() || + !IsTransientTexture(access.texture) || + !IsGraphManagedTransientState(access.requiredState)) { + continue; + } + + if (renderContext.commandList == nullptr) { + if (outErrorMessage != nullptr) { + *outErrorMessage = + Containers::String("RenderGraph cannot transition transient texture without a valid command list: ") + + m_graph.m_textures[access.texture.index].name; + } + return false; + } + + RHI::ResourceStates& currentState = m_textureStates[access.texture.index]; + if (currentState == access.requiredState) { + continue; + } + + RHI::RHIResourceView* resourceView = + ResolveTextureView(access.texture, ResolveBarrierViewType(access.requiredState)); + if (resourceView == nullptr) { + if (outErrorMessage != nullptr) { + *outErrorMessage = + Containers::String("RenderGraph cannot resolve transient texture view for state transition: ") + + m_graph.m_textures[access.texture.index].name; + } + return false; + } + + renderContext.commandList->TransitionBarrier( + resourceView, + currentState, + access.requiredState); + currentState = access.requiredState; + } + + return true; + } + +private: + struct TextureAllocation { + RHI::RHITexture* texture = nullptr; + RHI::RHIResourceView* renderTargetView = nullptr; + RHI::RHIResourceView* shaderResourceView = nullptr; + }; + + static void DestroyTextureAllocation(TextureAllocation& allocation) { + if (allocation.renderTargetView != nullptr) { + allocation.renderTargetView->Shutdown(); + delete allocation.renderTargetView; + allocation.renderTargetView = nullptr; + } + + if (allocation.shaderResourceView != nullptr) { + allocation.shaderResourceView->Shutdown(); + delete allocation.shaderResourceView; + allocation.shaderResourceView = nullptr; + } + + if (allocation.texture != nullptr) { + allocation.texture->Shutdown(); + delete allocation.texture; + allocation.texture = nullptr; + } + } + + bool CreateTransientTexture( + const RenderContext& renderContext, + const CompiledRenderGraph::CompiledTexture& texture, + TextureAllocation& allocation) { + RHI::TextureDesc textureDesc = {}; + textureDesc.width = texture.desc.width; + textureDesc.height = texture.desc.height; + textureDesc.depth = 1u; + textureDesc.mipLevels = 1u; + textureDesc.arraySize = 1u; + textureDesc.format = texture.desc.format; + textureDesc.textureType = texture.desc.textureType; + textureDesc.sampleCount = texture.desc.sampleCount; + textureDesc.sampleQuality = texture.desc.sampleQuality; + textureDesc.flags = 0u; + + allocation.texture = renderContext.device->CreateTexture(textureDesc); + if (allocation.texture == nullptr) { + DestroyTextureAllocation(allocation); + return false; + } + + RHI::ResourceViewDesc viewDesc = {}; + viewDesc.format = texture.desc.format; + viewDesc.dimension = RHI::ResourceViewDimension::Texture2D; + viewDesc.mipLevel = 0u; + + allocation.renderTargetView = + renderContext.device->CreateRenderTargetView(allocation.texture, viewDesc); + if (allocation.renderTargetView == nullptr) { + DestroyTextureAllocation(allocation); + return false; + } + + allocation.shaderResourceView = + renderContext.device->CreateShaderResourceView(allocation.texture, viewDesc); + if (allocation.shaderResourceView == nullptr) { + DestroyTextureAllocation(allocation); + return false; + } + + return true; + } + + void Reset() { + for (TextureAllocation& allocation : m_textureAllocations) { + DestroyTextureAllocation(allocation); + } + } + + const CompiledRenderGraph& m_graph; + std::vector m_textureAllocations; + std::vector m_textureStates; +}; + +RHI::RHIResourceView* RenderGraphExecutionContext::ResolveTextureView( + RenderGraphTextureHandle handle, + RenderGraphTextureViewType viewType) const { + return runtimeResources != nullptr + ? runtimeResources->ResolveTextureView(handle, viewType) + : nullptr; +} + +bool RenderGraphExecutionContext::TryGetTextureDesc( + RenderGraphTextureHandle handle, + RenderGraphTextureDesc& outDesc) const { + return runtimeResources != nullptr && + runtimeResources->TryGetTextureDesc(handle, outDesc); +} + +bool RenderGraphExecutionContext::IsTransientTexture(RenderGraphTextureHandle handle) const { + return runtimeResources != nullptr && + runtimeResources->IsTransientTexture(handle); +} + +bool RenderGraphExecutor::Execute( + const CompiledRenderGraph& graph, + const RenderContext& renderContext, + Containers::String* outErrorMessage) { + if (outErrorMessage != nullptr) { + outErrorMessage->Clear(); + } + + RenderGraphRuntimeResources runtimeResources(graph); + if (!runtimeResources.Initialize(renderContext, outErrorMessage)) { + return false; + } + + RenderGraphExecutionContext executionContext = { + renderContext, + &runtimeResources + }; + for (const CompiledRenderGraph::CompiledPass& pass : graph.m_passes) { + if (!runtimeResources.TransitionPassResources(pass, renderContext, outErrorMessage)) { + return false; + } + + if (pass.executeCallback) { + pass.executeCallback(executionContext); + } + } + + return true; +} + +} // namespace Rendering +} // namespace XCEngine diff --git a/engine/src/Rendering/Planning/CameraFramePlanBuilder.cpp b/engine/src/Rendering/Planning/CameraFramePlanBuilder.cpp index d8d89ee5..5453bbe6 100644 --- a/engine/src/Rendering/Planning/CameraFramePlanBuilder.cpp +++ b/engine/src/Rendering/Planning/CameraFramePlanBuilder.cpp @@ -42,7 +42,7 @@ RenderSurface ConfigureFullscreenStageSurface( std::vector CameraFramePlanBuilder::BuildPlans( const std::vector& requests, const RenderPipelineAsset* pipelineAsset) { - std::vector plans = CreatePlansFromCompatibilityRequests(requests); + std::vector plans = CreatePlansFromRequests(requests); ResolveCameraFinalColorPolicies(plans, pipelineAsset); AttachFullscreenStageRequests(plans); return plans; @@ -75,7 +75,7 @@ void CameraFramePlanBuilder::UpdateTrackedSurfaceState(const RenderSurface* surf } } -std::vector CameraFramePlanBuilder::CreatePlansFromCompatibilityRequests( +std::vector CameraFramePlanBuilder::CreatePlansFromRequests( const std::vector& requests) const { std::vector plans = {}; plans.reserve(requests.size()); diff --git a/engine/src/Rendering/Planning/CameraFramePlanBuilder.h b/engine/src/Rendering/Planning/CameraFramePlanBuilder.h index ac7d62b5..a1ffa081 100644 --- a/engine/src/Rendering/Planning/CameraFramePlanBuilder.h +++ b/engine/src/Rendering/Planning/CameraFramePlanBuilder.h @@ -24,7 +24,7 @@ public: void UpdateTrackedSurfaceState(const RenderSurface* surface); private: - std::vector CreatePlansFromCompatibilityRequests( + std::vector CreatePlansFromRequests( const std::vector& requests) const; void PrepareOwnedFullscreenStageState(size_t planCount); void ResolveCameraFinalColorPolicies( diff --git a/tests/Rendering/unit/test_camera_scene_renderer.cpp b/tests/Rendering/unit/test_camera_scene_renderer.cpp index d289bcd7..014a456d 100644 --- a/tests/Rendering/unit/test_camera_scene_renderer.cpp +++ b/tests/Rendering/unit/test_camera_scene_renderer.cpp @@ -285,6 +285,98 @@ private: XCEngine::RHI::RHIDeviceInfo m_deviceInfo = {}; }; +class MockNoopCommandList final : public XCEngine::RHI::RHICommandList { +public: + void Shutdown() override {} + void Reset() override {} + void Close() override {} + + void TransitionBarrier( + XCEngine::RHI::RHIResourceView*, + XCEngine::RHI::ResourceStates, + XCEngine::RHI::ResourceStates) override { + } + + void BeginRenderPass( + XCEngine::RHI::RHIRenderPass*, + XCEngine::RHI::RHIFramebuffer*, + const XCEngine::RHI::Rect&, + uint32_t, + const XCEngine::RHI::ClearValue*) override { + } + + void EndRenderPass() override {} + void SetShader(XCEngine::RHI::RHIShader*) override {} + void SetPipelineState(XCEngine::RHI::RHIPipelineState*) override {} + void SetGraphicsDescriptorSets( + uint32_t, + uint32_t, + XCEngine::RHI::RHIDescriptorSet**, + XCEngine::RHI::RHIPipelineLayout*) override { + } + void SetComputeDescriptorSets( + uint32_t, + uint32_t, + XCEngine::RHI::RHIDescriptorSet**, + XCEngine::RHI::RHIPipelineLayout*) override { + } + void SetPrimitiveTopology(XCEngine::RHI::PrimitiveTopology) override {} + void SetViewport(const XCEngine::RHI::Viewport&) override {} + void SetViewports(uint32_t, const XCEngine::RHI::Viewport*) override {} + void SetScissorRect(const XCEngine::RHI::Rect&) override {} + void SetScissorRects(uint32_t, const XCEngine::RHI::Rect*) override {} + void SetRenderTargets( + uint32_t, + XCEngine::RHI::RHIResourceView**, + XCEngine::RHI::RHIResourceView*) override { + } + void SetStencilRef(uint8_t) override {} + void SetBlendFactor(const float[4]) override {} + void SetVertexBuffers( + uint32_t, + uint32_t, + XCEngine::RHI::RHIResourceView**, + const uint64_t*, + const uint32_t*) override { + } + void SetIndexBuffer(XCEngine::RHI::RHIResourceView*, uint64_t) override {} + void Draw(uint32_t, uint32_t, uint32_t, uint32_t) override {} + void DrawIndexed(uint32_t, uint32_t, uint32_t, int32_t, uint32_t) override {} + void Clear(float, float, float, float, uint32_t) override {} + void ClearRenderTarget( + XCEngine::RHI::RHIResourceView*, + const float[4], + uint32_t, + const XCEngine::RHI::Rect*) override { + } + void ClearDepthStencil( + XCEngine::RHI::RHIResourceView*, + float, + uint8_t, + uint32_t, + const XCEngine::RHI::Rect*) override { + } + void CopyResource(XCEngine::RHI::RHIResourceView*, XCEngine::RHI::RHIResourceView*) override {} + void Dispatch(uint32_t, uint32_t, uint32_t) override {} +}; + +class MockNoopCommandQueue final : public XCEngine::RHI::RHICommandQueue { +public: + void Shutdown() override {} + void ExecuteCommandLists(uint32_t, void**) override {} + void Signal(XCEngine::RHI::RHIFence*, uint64_t) override {} + void Wait(XCEngine::RHI::RHIFence*, uint64_t) override {} + uint64_t GetCompletedValue() override { return 0u; } + void WaitForIdle() override {} + XCEngine::RHI::CommandQueueType GetType() const override { + return XCEngine::RHI::CommandQueueType::Direct; + } + uint64_t GetTimestampFrequency() const override { return 0u; } + void* GetNativeHandle() override { return nullptr; } + void WaitForPreviousFrame() override {} + uint64_t GetCurrentFrame() const override { return 0u; } +}; + struct MockPipelineAssetState { int createCalls = 0; std::shared_ptr lastCreatedPipelineState; @@ -516,10 +608,13 @@ public: }; RenderContext CreateValidContext() { + static MockNoopCommandList s_commandList; + static MockNoopCommandQueue s_commandQueue; + RenderContext context; context.device = reinterpret_cast(1); - context.commandList = reinterpret_cast(1); - context.commandQueue = reinterpret_cast(1); + context.commandList = &s_commandList; + context.commandQueue = &s_commandQueue; return context; } @@ -688,7 +783,7 @@ TEST(CameraRenderer_Test, UsesOverrideCameraAndSurfaceSizeWhenSubmittingScene) { request.cameraDepth = overrideCamera->GetDepth(); request.clearFlags = RenderClearFlags::None; - ASSERT_TRUE(renderer.Render(request)); + ASSERT_TRUE(renderer.Render(CameraFramePlan::FromRequest(request))); EXPECT_EQ(state->renderCalls, 1); EXPECT_EQ(state->lastSurfaceWidth, 640u); EXPECT_EQ(state->lastSurfaceHeight, 480u); @@ -728,7 +823,7 @@ TEST(CameraRenderer_Test, AppliesRequestClearColorOverrideToSceneData) { request.hasClearColorOverride = true; request.clearColorOverride = XCEngine::Math::Color(0.27f, 0.27f, 0.27f, 1.0f); - ASSERT_TRUE(renderer.Render(request)); + ASSERT_TRUE(renderer.Render(CameraFramePlan::FromRequest(request))); EXPECT_FLOAT_EQ(state->lastClearColor.r, 0.27f); EXPECT_FLOAT_EQ(state->lastClearColor.g, 0.27f); EXPECT_FLOAT_EQ(state->lastClearColor.b, 0.27f); @@ -756,7 +851,7 @@ TEST(CameraRenderer_Test, PromotesSkyboxMaterialIntoEnvironmentFrameData) { request.surface = RenderSurface(320, 200); request.surface.SetDepthAttachment(reinterpret_cast(1)); - ASSERT_TRUE(renderer.Render(request)); + ASSERT_TRUE(renderer.Render(CameraFramePlan::FromRequest(request))); EXPECT_TRUE(state->lastHasSkybox); EXPECT_EQ(state->lastEnvironmentMode, RenderEnvironmentMode::MaterialSkybox); EXPECT_EQ(state->lastSkyboxMaterial, &skyboxMaterial); @@ -788,7 +883,7 @@ TEST(CameraRenderer_Test, ExecutesInjectedPreAndPostPassSequencesAroundPipelineR request.preScenePasses = &prePasses; request.postScenePasses = &postPasses; - ASSERT_TRUE(renderer.Render(request)); + ASSERT_TRUE(renderer.Render(CameraFramePlan::FromRequest(request))); EXPECT_EQ( state->eventLog, (std::vector{ @@ -830,7 +925,7 @@ TEST(CameraRenderer_Test, ExecutesObjectIdPassBetweenPipelineAndPostPassesWhenRe request.objectId.surface.SetColorAttachment(reinterpret_cast(1)); request.objectId.surface.SetDepthAttachment(reinterpret_cast(2)); - ASSERT_TRUE(renderer.Render(request)); + ASSERT_TRUE(renderer.Render(CameraFramePlan::FromRequest(request))); EXPECT_EQ( state->eventLog, (std::vector{ @@ -883,7 +978,7 @@ TEST(CameraRenderer_Test, RoutesSceneColorThroughPostProcessAndFinalOutputStages request.finalOutput.destinationSurface = request.surface; request.finalOutput.passes = &finalOutputPasses; - ASSERT_TRUE(renderer.Render(request)); + ASSERT_TRUE(renderer.Render(CameraFramePlan::FromRequest(request))); EXPECT_EQ(state->lastSurfaceWidth, 256u); EXPECT_EQ(state->lastSurfaceHeight, 128u); EXPECT_EQ(state->lastCameraViewportWidth, 256u); @@ -970,7 +1065,7 @@ TEST(CameraRenderer_Test, ChainsMultiPassPostProcessThroughIntermediateSurface) request.postProcess.destinationSurface.SetColorAttachment(destinationColorAttachment); request.postProcess.passes = &postProcessPasses; - ASSERT_TRUE(renderer.Render(request)); + ASSERT_TRUE(renderer.Render(CameraFramePlan::FromRequest(request))); ASSERT_NE(firstPassRaw, nullptr); EXPECT_TRUE(firstPassRaw->lastHasSourceSurface); @@ -1082,7 +1177,7 @@ TEST(CameraRenderer_Test, KeepsPostProcessAndFinalOutputScratchSurfacesIndepende request.finalOutput.destinationSurface.SetColorAttachment(finalOutputDestination); request.finalOutput.passes = &finalOutputPasses; - ASSERT_TRUE(renderer.Render(request)); + ASSERT_TRUE(renderer.Render(CameraFramePlan::FromRequest(request))); ASSERT_NE(postSecondPassRaw, nullptr); EXPECT_EQ(postSecondPassRaw->lastSourceSurfaceWidth, 512u); @@ -1100,9 +1195,12 @@ TEST(CameraRenderer_Test, KeepsPostProcessAndFinalOutputScratchSurfacesIndepende EXPECT_EQ(allocationState->createTextureCalls, 2); EXPECT_EQ(allocationState->createRenderTargetViewCalls, 2); EXPECT_EQ(allocationState->createShaderViewCalls, 2); - EXPECT_EQ(allocationState->shutdownTextureCalls, 0); - EXPECT_EQ(allocationState->shutdownRenderTargetViewCalls, 0); - EXPECT_EQ(allocationState->shutdownShaderViewCalls, 0); + EXPECT_EQ(allocationState->shutdownTextureCalls, 2); + EXPECT_EQ(allocationState->shutdownRenderTargetViewCalls, 2); + EXPECT_EQ(allocationState->shutdownShaderViewCalls, 2); + EXPECT_EQ(allocationState->destroyTextureCalls, 2); + EXPECT_EQ(allocationState->destroyRenderTargetViewCalls, 2); + EXPECT_EQ(allocationState->destroyShaderViewCalls, 2); EXPECT_EQ( pipelineState->eventLog, (std::vector{ @@ -1185,7 +1283,7 @@ TEST(CameraRenderer_Test, ExecutesFormalFrameStagesInDocumentedOrder) { request.objectId.surface.SetColorAttachment(reinterpret_cast(6)); request.objectId.surface.SetDepthAttachment(reinterpret_cast(7)); - ASSERT_TRUE(renderer.Render(request)); + ASSERT_TRUE(renderer.Render(CameraFramePlan::FromRequest(request))); EXPECT_EQ( state->eventLog, (std::vector{ @@ -1245,7 +1343,7 @@ TEST(CameraRenderer_Test, ExecutesShadowCasterAndDepthOnlyRequestsBeforeMainPipe request.depthOnly.hasClearColorOverride = true; request.depthOnly.clearColorOverride = XCEngine::Math::Color(0.3f, 0.2f, 0.1f, 1.0f); - ASSERT_TRUE(renderer.Render(request)); + ASSERT_TRUE(renderer.Render(CameraFramePlan::FromRequest(request))); EXPECT_EQ( state->eventLog, (std::vector{ @@ -1309,7 +1407,7 @@ TEST(CameraRenderer_Test, AutoAllocatesDirectionalShadowSurfaceFromShadowPlan) { request.directionalShadow.cameraData.viewProjection = XCEngine::Math::Matrix4x4::Translation(XCEngine::Math::Vector3(11.0f, 12.0f, 13.0f)); - ASSERT_TRUE(renderer.Render(request)); + ASSERT_TRUE(renderer.Render(CameraFramePlan::FromRequest(request))); EXPECT_EQ( pipelineState->eventLog, (std::vector{ @@ -1391,7 +1489,7 @@ TEST(CameraRenderer_Test, ReusesDirectionalShadowSurfaceWhenPlanMatches) { request.directionalShadow.cameraData.viewportHeight = 128; request.directionalShadow.cameraData.clearFlags = RenderClearFlags::Depth; - ASSERT_TRUE(renderer.Render(request)); + ASSERT_TRUE(renderer.Render(CameraFramePlan::FromRequest(request))); XCEngine::RHI::RHIResourceView* firstShadowMap = pipelineState->lastShadowMap; ASSERT_NE(firstShadowMap, nullptr); EXPECT_EQ(allocationState->createTextureCalls, 1); @@ -1400,7 +1498,7 @@ TEST(CameraRenderer_Test, ReusesDirectionalShadowSurfaceWhenPlanMatches) { EXPECT_EQ(allocationState->shutdownDepthViewCalls, 0); EXPECT_EQ(allocationState->destroyTextureCalls, 0); - ASSERT_TRUE(renderer.Render(request)); + ASSERT_TRUE(renderer.Render(CameraFramePlan::FromRequest(request))); EXPECT_EQ(allocationState->createTextureCalls, 1); EXPECT_EQ(allocationState->createDepthViewCalls, 1); EXPECT_EQ(allocationState->createShaderViewCalls, 1); @@ -1449,7 +1547,7 @@ TEST(CameraRenderer_Test, RecreatesDirectionalShadowSurfaceWhenPlanSizeChanges) request.directionalShadow.cameraData.viewportHeight = 128; request.directionalShadow.cameraData.clearFlags = RenderClearFlags::Depth; - ASSERT_TRUE(renderer.Render(request)); + ASSERT_TRUE(renderer.Render(CameraFramePlan::FromRequest(request))); XCEngine::RHI::RHIResourceView* firstShadowMap = pipelineState->lastShadowMap; ASSERT_NE(firstShadowMap, nullptr); @@ -1458,7 +1556,7 @@ TEST(CameraRenderer_Test, RecreatesDirectionalShadowSurfaceWhenPlanSizeChanges) request.directionalShadow.cameraData.viewportWidth = 512; request.directionalShadow.cameraData.viewportHeight = 256; - ASSERT_TRUE(renderer.Render(request)); + ASSERT_TRUE(renderer.Render(CameraFramePlan::FromRequest(request))); EXPECT_EQ(allocationState->createTextureCalls, 2); EXPECT_EQ(allocationState->createDepthViewCalls, 2); EXPECT_EQ(allocationState->createShaderViewCalls, 2); @@ -1512,7 +1610,7 @@ TEST(CameraRenderer_Test, EnablesDirectionalShadowSurfaceAfterInitialFrameWithou request.surface = RenderSurface(320, 180); request.cameraDepth = camera->GetDepth(); - ASSERT_TRUE(renderer.Render(request)); + ASSERT_TRUE(renderer.Render(CameraFramePlan::FromRequest(request))); EXPECT_EQ(pipelineState->eventLog, (std::vector{ "pipeline" })); EXPECT_FALSE(pipelineState->lastHasMainDirectionalShadow); EXPECT_EQ(pipelineState->lastShadowMap, nullptr); @@ -1529,7 +1627,7 @@ TEST(CameraRenderer_Test, EnablesDirectionalShadowSurfaceAfterInitialFrameWithou request.directionalShadow.cameraData.viewportHeight = 128; request.directionalShadow.cameraData.clearFlags = RenderClearFlags::Depth; - ASSERT_TRUE(renderer.Render(request)); + ASSERT_TRUE(renderer.Render(CameraFramePlan::FromRequest(request))); EXPECT_EQ( pipelineState->eventLog, (std::vector{ @@ -1572,7 +1670,7 @@ TEST(CameraRenderer_Test, StopsRenderingWhenShadowCasterRequestIsInvalid) { request.shadowCaster.surface = RenderSurface(64, 64); request.shadowCaster.surface.SetColorAttachment(reinterpret_cast(1)); - EXPECT_FALSE(renderer.Render(request)); + EXPECT_FALSE(renderer.Render(CameraFramePlan::FromRequest(request))); EXPECT_TRUE(state->eventLog.empty()); } @@ -1599,7 +1697,7 @@ TEST(CameraRenderer_Test, StopsRenderingWhenPostProcessRequestIsInvalid) { request.postProcess.destinationSurface = RenderSurface(320, 180); request.postProcess.destinationSurface.SetColorAttachment(reinterpret_cast(1)); - EXPECT_FALSE(renderer.Render(request)); + EXPECT_FALSE(renderer.Render(CameraFramePlan::FromRequest(request))); EXPECT_TRUE(state->eventLog.empty()); } @@ -1626,7 +1724,7 @@ TEST(CameraRenderer_Test, ShutsDownInitializedPassesWhenPipelineRenderFails) { request.cameraDepth = camera->GetDepth(); request.preScenePasses = &prePasses; - EXPECT_FALSE(renderer.Render(request)); + EXPECT_FALSE(renderer.Render(CameraFramePlan::FromRequest(request))); EXPECT_EQ( state->eventLog, (std::vector{ "init:pre", "pre", "pipeline" })); @@ -1663,7 +1761,7 @@ TEST(CameraRenderer_Test, StopsRenderingWhenObjectIdPassFails) { request.objectId.surface.SetColorAttachment(reinterpret_cast(1)); request.objectId.surface.SetDepthAttachment(reinterpret_cast(2)); - EXPECT_FALSE(renderer.Render(request)); + EXPECT_FALSE(renderer.Render(CameraFramePlan::FromRequest(request))); EXPECT_EQ( state->eventLog, (std::vector{ "init:pre", "pre", "pipeline", "objectId" })); @@ -1695,7 +1793,7 @@ TEST(CameraRenderer_Test, ShutsDownSequencesWhenPostPassInitializationFails) { request.preScenePasses = &prePasses; request.postScenePasses = &postPasses; - EXPECT_FALSE(renderer.Render(request)); + EXPECT_FALSE(renderer.Render(CameraFramePlan::FromRequest(request))); EXPECT_EQ( state->eventLog, (std::vector{ @@ -2460,7 +2558,7 @@ TEST(CameraRenderer_Test, UsesResolvedRenderAreaForCameraViewportDimensions) { request.surface = RenderSurface(640, 480); request.surface.SetRenderArea(XCEngine::Math::RectInt(80, 120, 320, 240)); - ASSERT_TRUE(renderer.Render(request)); + ASSERT_TRUE(renderer.Render(CameraFramePlan::FromRequest(request))); EXPECT_EQ(state->lastRenderAreaX, 80); EXPECT_EQ(state->lastRenderAreaY, 120); EXPECT_EQ(state->lastRenderAreaWidth, 320); @@ -2574,8 +2672,11 @@ TEST(SceneRenderer_Test, SortsManualCameraRequestsByDepthBeforeRendering) { nearRequest.cameraStackOrder = 0; nearRequest.clearFlags = RenderClearFlags::Depth; - const std::vector requests = { farRequest, nearRequest }; - ASSERT_TRUE(renderer.Render(requests)); + const std::vector plans = { + CameraFramePlan::FromRequest(farRequest), + CameraFramePlan::FromRequest(nearRequest) + }; + ASSERT_TRUE(renderer.Render(plans)); ASSERT_EQ(state->renderedCameras.size(), 2u); ASSERT_EQ(state->renderedClearFlags.size(), 2u); EXPECT_EQ(state->renderedCameras[0], nearCamera); @@ -2612,8 +2713,11 @@ TEST(SceneRenderer_Test, PreservesManualSubmissionOrderForEqualPriorityRequests) CameraRenderRequest secondRequest = firstRequest; secondRequest.camera = secondCamera; - const std::vector requests = { secondRequest, firstRequest }; - ASSERT_TRUE(renderer.Render(requests)); + const std::vector plans = { + CameraFramePlan::FromRequest(secondRequest), + CameraFramePlan::FromRequest(firstRequest) + }; + ASSERT_TRUE(renderer.Render(plans)); ASSERT_EQ(state->renderedCameras.size(), 2u); EXPECT_EQ(state->renderedCameras[0], secondCamera); EXPECT_EQ(state->renderedCameras[1], firstCamera); diff --git a/tests/Rendering/unit/test_render_graph.cpp b/tests/Rendering/unit/test_render_graph.cpp new file mode 100644 index 00000000..13b1fe92 --- /dev/null +++ b/tests/Rendering/unit/test_render_graph.cpp @@ -0,0 +1,535 @@ +#include + +#include +#include +#include +#include +#include + +#include +#include +#include + +using namespace XCEngine::Rendering; +using namespace XCEngine::RHI; + +namespace { + +struct MockTransientAllocationState { + int createTextureCalls = 0; + int shutdownTextureCalls = 0; + int destroyTextureCalls = 0; + int createRenderTargetViewCalls = 0; + int shutdownRenderTargetViewCalls = 0; + int destroyRenderTargetViewCalls = 0; + int createShaderViewCalls = 0; + int shutdownShaderViewCalls = 0; + int destroyShaderViewCalls = 0; +}; + +class MockTransientTexture final : public RHITexture { +public: + MockTransientTexture( + std::shared_ptr state, + uint32_t width, + uint32_t height, + Format format) + : m_state(std::move(state)) + , m_width(width) + , m_height(height) + , m_format(format) { + } + + ~MockTransientTexture() override { + ++m_state->destroyTextureCalls; + } + + uint32_t GetWidth() const override { return m_width; } + uint32_t GetHeight() const override { return m_height; } + uint32_t GetDepth() const override { return 1u; } + uint32_t GetMipLevels() const override { return 1u; } + Format GetFormat() const override { return m_format; } + TextureType GetTextureType() const override { return TextureType::Texture2D; } + ResourceStates GetState() const override { return m_stateValue; } + void SetState(ResourceStates state) override { m_stateValue = state; } + void* GetNativeHandle() override { return nullptr; } + const std::string& GetName() const override { return m_name; } + void SetName(const std::string& name) override { m_name = name; } + + void Shutdown() override { + ++m_state->shutdownTextureCalls; + } + +private: + std::shared_ptr m_state; + uint32_t m_width = 0u; + uint32_t m_height = 0u; + Format m_format = Format::Unknown; + ResourceStates m_stateValue = ResourceStates::Common; + std::string m_name; +}; + +class MockTransientView final : public RHIResourceView { +public: + MockTransientView( + std::shared_ptr state, + ResourceViewType viewType, + Format format) + : m_state(std::move(state)) + , m_viewType(viewType) + , m_format(format) { + } + + ~MockTransientView() override { + if (m_viewType == ResourceViewType::RenderTarget) { + ++m_state->destroyRenderTargetViewCalls; + } else { + ++m_state->destroyShaderViewCalls; + } + } + + void Shutdown() override { + if (m_viewType == ResourceViewType::RenderTarget) { + ++m_state->shutdownRenderTargetViewCalls; + } else { + ++m_state->shutdownShaderViewCalls; + } + } + + void* GetNativeHandle() override { return nullptr; } + bool IsValid() const override { return true; } + ResourceViewType GetViewType() const override { return m_viewType; } + ResourceViewDimension GetDimension() const override { return ResourceViewDimension::Texture2D; } + Format GetFormat() const override { return m_format; } + +private: + std::shared_ptr m_state; + ResourceViewType m_viewType = ResourceViewType::ShaderResource; + Format m_format = Format::Unknown; +}; + +class MockTransientDevice final : public RHIDevice { +public: + explicit MockTransientDevice(std::shared_ptr state) + : m_state(std::move(state)) { + } + + bool Initialize(const RHIDeviceDesc&) override { return true; } + void Shutdown() override {} + + RHIBuffer* CreateBuffer(const BufferDesc&) override { return nullptr; } + + RHITexture* CreateTexture(const TextureDesc& desc) override { + ++m_state->createTextureCalls; + return new MockTransientTexture( + m_state, + desc.width, + desc.height, + static_cast(desc.format)); + } + + RHITexture* CreateTexture( + const TextureDesc& desc, + const void*, + size_t, + uint32_t) override { + return CreateTexture(desc); + } + + RHISwapChain* CreateSwapChain(const SwapChainDesc&, RHICommandQueue*) override { return nullptr; } + RHICommandList* CreateCommandList(const CommandListDesc&) override { return nullptr; } + RHICommandQueue* CreateCommandQueue(const CommandQueueDesc&) override { return nullptr; } + RHIShader* CreateShader(const ShaderCompileDesc&) override { return nullptr; } + RHIPipelineState* CreatePipelineState(const GraphicsPipelineDesc&) override { return nullptr; } + RHIPipelineLayout* CreatePipelineLayout(const RHIPipelineLayoutDesc&) override { return nullptr; } + RHIFence* CreateFence(const FenceDesc&) override { return nullptr; } + RHISampler* CreateSampler(const SamplerDesc&) override { return nullptr; } + RHIRenderPass* CreateRenderPass(uint32_t, const AttachmentDesc*, const AttachmentDesc*) override { + return nullptr; + } + RHIFramebuffer* CreateFramebuffer( + RHIRenderPass*, + uint32_t, + uint32_t, + uint32_t, + RHIResourceView**, + RHIResourceView*) override { + return nullptr; + } + RHIDescriptorPool* CreateDescriptorPool(const DescriptorPoolDesc&) override { return nullptr; } + RHIDescriptorSet* CreateDescriptorSet(RHIDescriptorPool*, const DescriptorSetLayoutDesc&) override { + return nullptr; + } + RHIResourceView* CreateVertexBufferView(RHIBuffer*, const ResourceViewDesc&) override { return nullptr; } + RHIResourceView* CreateIndexBufferView(RHIBuffer*, const ResourceViewDesc&) override { return nullptr; } + + RHIResourceView* CreateRenderTargetView( + RHITexture*, + const ResourceViewDesc& desc) override { + ++m_state->createRenderTargetViewCalls; + return new MockTransientView( + m_state, + ResourceViewType::RenderTarget, + static_cast(desc.format)); + } + + RHIResourceView* CreateDepthStencilView(RHITexture*, const ResourceViewDesc&) override { return nullptr; } + RHIResourceView* CreateShaderResourceView(RHIBuffer*, const ResourceViewDesc&) override { return nullptr; } + + RHIResourceView* CreateShaderResourceView( + RHITexture*, + const ResourceViewDesc& desc) override { + ++m_state->createShaderViewCalls; + return new MockTransientView( + m_state, + ResourceViewType::ShaderResource, + static_cast(desc.format)); + } + + RHIResourceView* CreateUnorderedAccessView(RHIBuffer*, const ResourceViewDesc&) override { return nullptr; } + RHIResourceView* CreateUnorderedAccessView(RHITexture*, const ResourceViewDesc&) override { + return nullptr; + } + + const RHICapabilities& GetCapabilities() const override { return m_capabilities; } + const RHIDeviceInfo& GetDeviceInfo() const override { return m_deviceInfo; } + void* GetNativeDevice() override { return nullptr; } + +private: + std::shared_ptr m_state; + RHICapabilities m_capabilities = {}; + RHIDeviceInfo m_deviceInfo = {}; +}; + +struct MockTransitionBarrierCall { + RHIResourceView* resource = nullptr; + ResourceStates before = ResourceStates::Common; + ResourceStates after = ResourceStates::Common; +}; + +class MockTransientCommandList final : public RHICommandList { +public: + void Shutdown() override {} + void Reset() override {} + void Close() override {} + + void TransitionBarrier( + RHIResourceView* resource, + ResourceStates stateBefore, + ResourceStates stateAfter) override { + MockTransitionBarrierCall call = {}; + call.resource = resource; + call.before = stateBefore; + call.after = stateAfter; + transitionCalls.push_back(call); + } + + void BeginRenderPass( + RHIRenderPass*, + RHIFramebuffer*, + const Rect&, + uint32_t, + const ClearValue*) override { + } + + void EndRenderPass() override {} + void SetShader(RHIShader*) override {} + void SetPipelineState(RHIPipelineState*) override {} + void SetGraphicsDescriptorSets(uint32_t, uint32_t, RHIDescriptorSet**, RHIPipelineLayout*) override {} + void SetComputeDescriptorSets(uint32_t, uint32_t, RHIDescriptorSet**, RHIPipelineLayout*) override {} + void SetPrimitiveTopology(PrimitiveTopology) override {} + void SetViewport(const Viewport&) override {} + void SetViewports(uint32_t, const Viewport*) override {} + void SetScissorRect(const Rect&) override {} + void SetScissorRects(uint32_t, const Rect*) override {} + void SetRenderTargets(uint32_t, RHIResourceView**, RHIResourceView*) override {} + void SetStencilRef(uint8_t) override {} + void SetBlendFactor(const float[4]) override {} + void SetVertexBuffers(uint32_t, uint32_t, RHIResourceView**, const uint64_t*, const uint32_t*) override {} + void SetIndexBuffer(RHIResourceView*, uint64_t) override {} + void Draw(uint32_t, uint32_t, uint32_t, uint32_t) override {} + void DrawIndexed(uint32_t, uint32_t, uint32_t, int32_t, uint32_t) override {} + void Clear(float, float, float, float, uint32_t) override {} + void ClearRenderTarget(RHIResourceView*, const float[4], uint32_t, const Rect*) override {} + void ClearDepthStencil(RHIResourceView*, float, uint8_t, uint32_t, const Rect*) override {} + void CopyResource(RHIResourceView*, RHIResourceView*) override {} + void Dispatch(uint32_t, uint32_t, uint32_t) override {} + + std::vector transitionCalls = {}; +}; + +RenderGraphTextureDesc BuildTestTextureDesc() { + RenderGraphTextureDesc desc = {}; + desc.width = 1280u; + desc.height = 720u; + desc.format = static_cast(Format::R8G8B8A8_UNorm); + desc.textureType = static_cast(TextureType::Texture2D); + desc.sampleCount = 1u; + desc.sampleQuality = 0u; + return desc; +} + +} // namespace + +TEST(RenderGraph_Test, CompilesDeclaredPassOrderAndTracksTextureLifetimes) { + RenderGraph graph; + RenderGraphBuilder builder(graph); + + const RenderGraphTextureDesc desc = BuildTestTextureDesc(); + const RenderGraphTextureHandle sceneColor = builder.CreateTransientTexture("SceneColor", desc); + const RenderGraphTextureHandle bloomColor = builder.CreateTransientTexture("BloomColor", desc); + const RenderGraphTextureHandle backBuffer = builder.ImportTexture("BackBuffer", desc); + + builder.AddRasterPass( + "Opaque", + [&](RenderGraphPassBuilder& pass) { + pass.WriteTexture(sceneColor); + }); + + builder.AddComputePass( + "Bloom", + [&](RenderGraphPassBuilder& pass) { + pass.ReadTexture(sceneColor); + pass.WriteTexture(bloomColor); + }); + + builder.AddRasterPass( + "Final", + [&](RenderGraphPassBuilder& pass) { + pass.ReadTexture(bloomColor); + pass.WriteTexture(backBuffer); + }); + + CompiledRenderGraph compiledGraph; + XCEngine::Containers::String errorMessage; + ASSERT_TRUE(RenderGraphCompiler::Compile(graph, compiledGraph, &errorMessage)) + << errorMessage.CStr(); + + ASSERT_EQ(compiledGraph.GetPassCount(), 3u); + EXPECT_EQ(compiledGraph.GetPassName(0u), "Opaque"); + EXPECT_EQ(compiledGraph.GetPassName(1u), "Bloom"); + EXPECT_EQ(compiledGraph.GetPassName(2u), "Final"); + EXPECT_EQ(compiledGraph.GetPassType(0u), RenderGraphPassType::Raster); + EXPECT_EQ(compiledGraph.GetPassType(1u), RenderGraphPassType::Compute); + + RenderGraphTextureLifetime lifetime = {}; + ASSERT_TRUE(compiledGraph.TryGetTextureLifetime(sceneColor, lifetime)); + EXPECT_TRUE(lifetime.used); + EXPECT_EQ(lifetime.kind, RenderGraphTextureKind::Transient); + EXPECT_EQ(lifetime.firstPassIndex, 0u); + EXPECT_EQ(lifetime.lastPassIndex, 1u); + + ASSERT_TRUE(compiledGraph.TryGetTextureLifetime(bloomColor, lifetime)); + EXPECT_TRUE(lifetime.used); + EXPECT_EQ(lifetime.firstPassIndex, 1u); + EXPECT_EQ(lifetime.lastPassIndex, 2u); + + ASSERT_TRUE(compiledGraph.TryGetTextureLifetime(backBuffer, lifetime)); + EXPECT_TRUE(lifetime.used); + EXPECT_EQ(lifetime.kind, RenderGraphTextureKind::Imported); + EXPECT_EQ(lifetime.firstPassIndex, 2u); + EXPECT_EQ(lifetime.lastPassIndex, 2u); +} + +TEST(RenderGraph_Test, OrdersImportedTextureHazardsAcrossFullscreenStyleChain) { + RenderGraph graph; + RenderGraphBuilder builder(graph); + + const RenderGraphTextureDesc desc = BuildTestTextureDesc(); + const RenderGraphTextureHandle sceneColor = builder.ImportTexture( + "SceneColor", + desc, + reinterpret_cast(1)); + const RenderGraphTextureHandle postColor = builder.ImportTexture( + "PostColor", + desc, + reinterpret_cast(2)); + + builder.AddRasterPass( + "MainScene", + [&](RenderGraphPassBuilder& pass) { + pass.WriteTexture(sceneColor); + }); + + builder.AddRasterPass( + "PostProcess", + [&](RenderGraphPassBuilder& pass) { + pass.ReadTexture(sceneColor); + pass.WriteTexture(postColor); + }); + + builder.AddRasterPass( + "FinalOutput", + [&](RenderGraphPassBuilder& pass) { + pass.ReadTexture(postColor); + pass.WriteTexture(sceneColor); + }); + + CompiledRenderGraph compiledGraph; + XCEngine::Containers::String errorMessage; + ASSERT_TRUE(RenderGraphCompiler::Compile(graph, compiledGraph, &errorMessage)) + << errorMessage.CStr(); + + ASSERT_EQ(compiledGraph.GetPassCount(), 3u); + EXPECT_EQ(compiledGraph.GetPassName(0u), "MainScene"); + EXPECT_EQ(compiledGraph.GetPassName(1u), "PostProcess"); + EXPECT_EQ(compiledGraph.GetPassName(2u), "FinalOutput"); + + RenderGraphTextureLifetime lifetime = {}; + ASSERT_TRUE(compiledGraph.TryGetTextureLifetime(sceneColor, lifetime)); + EXPECT_EQ(lifetime.kind, RenderGraphTextureKind::Imported); + EXPECT_EQ(lifetime.firstPassIndex, 0u); + EXPECT_EQ(lifetime.lastPassIndex, 2u); + + ASSERT_TRUE(compiledGraph.TryGetTextureLifetime(postColor, lifetime)); + EXPECT_EQ(lifetime.kind, RenderGraphTextureKind::Imported); + EXPECT_EQ(lifetime.firstPassIndex, 1u); + EXPECT_EQ(lifetime.lastPassIndex, 2u); +} + +TEST(RenderGraph_Test, PreservesImportedTextureStateContractAcrossCompile) { + RenderGraph graph; + RenderGraphBuilder builder(graph); + + const RenderGraphTextureDesc desc = BuildTestTextureDesc(); + RenderGraphImportedTextureOptions importedOptions = {}; + importedOptions.initialState = ResourceStates::Present; + importedOptions.finalState = ResourceStates::PixelShaderResource; + importedOptions.graphOwnsTransitions = true; + + const RenderGraphTextureHandle importedTexture = builder.ImportTexture( + "ImportedColor", + desc, + reinterpret_cast(7), + importedOptions); + + builder.AddRasterPass( + "SampleImported", + [&](RenderGraphPassBuilder& pass) { + pass.ReadTexture(importedTexture); + }); + + CompiledRenderGraph compiledGraph; + XCEngine::Containers::String errorMessage; + ASSERT_TRUE(RenderGraphCompiler::Compile(graph, compiledGraph, &errorMessage)) + << errorMessage.CStr(); + + RenderGraphImportedTextureOptions resolvedOptions = {}; + ASSERT_TRUE(compiledGraph.TryGetImportedTextureOptions(importedTexture, resolvedOptions)); + EXPECT_EQ(resolvedOptions.initialState, ResourceStates::Present); + EXPECT_EQ(resolvedOptions.finalState, ResourceStates::PixelShaderResource); + EXPECT_TRUE(resolvedOptions.graphOwnsTransitions); +} + +TEST(RenderGraph_Test, RejectsTransientTextureReadBeforeWrite) { + RenderGraph graph; + RenderGraphBuilder builder(graph); + + const RenderGraphTextureDesc desc = BuildTestTextureDesc(); + const RenderGraphTextureHandle sceneColor = builder.CreateTransientTexture("SceneColor", desc); + + builder.AddRasterPass( + "InvalidRead", + [&](RenderGraphPassBuilder& pass) { + pass.ReadTexture(sceneColor); + }); + + CompiledRenderGraph compiledGraph; + XCEngine::Containers::String errorMessage; + EXPECT_FALSE(RenderGraphCompiler::Compile(graph, compiledGraph, &errorMessage)); + EXPECT_FALSE(errorMessage.Empty()); +} + +TEST(RenderGraph_Test, ExecutesCompiledPassCallbacksInCompiledOrder) { + RenderGraph graph; + RenderGraphBuilder builder(graph); + + const RenderGraphTextureDesc desc = BuildTestTextureDesc(); + const RenderGraphTextureHandle sceneColor = builder.CreateTransientTexture("SceneColor", desc); + const RenderGraphTextureHandle backBuffer = builder.ImportTexture( + "BackBuffer", + desc, + reinterpret_cast(1)); + + std::vector eventLog; + auto allocationState = std::make_shared(); + MockTransientDevice device(allocationState); + MockTransientCommandList commandList; + RHIResourceView* opaqueTargetView = nullptr; + RHIResourceView* finalSourceView = nullptr; + + builder.AddRasterPass( + "Opaque", + [&](RenderGraphPassBuilder& pass) { + pass.WriteTexture(sceneColor); + pass.SetExecuteCallback( + [&](const RenderGraphExecutionContext& executionContext) { + EXPECT_TRUE(executionContext.IsTransientTexture(sceneColor)); + EXPECT_NE( + executionContext.ResolveTextureView( + sceneColor, + RenderGraphTextureViewType::RenderTarget), + nullptr); + opaqueTargetView = + executionContext.ResolveTextureView( + sceneColor, + RenderGraphTextureViewType::RenderTarget); + eventLog.push_back("Opaque"); + }); + }); + + builder.AddRasterPass( + "Final", + [&](RenderGraphPassBuilder& pass) { + pass.ReadTexture(sceneColor); + pass.WriteTexture(backBuffer); + pass.SetExecuteCallback( + [&](const RenderGraphExecutionContext& executionContext) { + RenderGraphTextureDesc resolvedDesc = {}; + EXPECT_TRUE(executionContext.TryGetTextureDesc(sceneColor, resolvedDesc)); + EXPECT_EQ(resolvedDesc.width, desc.width); + EXPECT_NE( + executionContext.ResolveTextureView( + sceneColor, + RenderGraphTextureViewType::ShaderResource), + nullptr); + finalSourceView = + executionContext.ResolveTextureView( + sceneColor, + RenderGraphTextureViewType::ShaderResource); + eventLog.push_back("Final"); + }); + }); + + CompiledRenderGraph compiledGraph; + XCEngine::Containers::String errorMessage; + ASSERT_TRUE(RenderGraphCompiler::Compile(graph, compiledGraph, &errorMessage)) + << errorMessage.CStr(); + + RenderContext renderContext = {}; + renderContext.device = &device; + renderContext.commandList = &commandList; + renderContext.commandQueue = reinterpret_cast(1); + ASSERT_TRUE(RenderGraphExecutor::Execute(compiledGraph, renderContext, &errorMessage)) + << errorMessage.CStr(); + + ASSERT_EQ(eventLog.size(), 2u); + EXPECT_EQ(eventLog[0], "Opaque"); + EXPECT_EQ(eventLog[1], "Final"); + ASSERT_EQ(commandList.transitionCalls.size(), 2u); + EXPECT_EQ(commandList.transitionCalls[0].resource, opaqueTargetView); + EXPECT_EQ(commandList.transitionCalls[0].before, ResourceStates::Common); + EXPECT_EQ(commandList.transitionCalls[0].after, ResourceStates::RenderTarget); + EXPECT_EQ(commandList.transitionCalls[1].resource, finalSourceView); + EXPECT_EQ(commandList.transitionCalls[1].before, ResourceStates::RenderTarget); + EXPECT_EQ(commandList.transitionCalls[1].after, ResourceStates::PixelShaderResource); + EXPECT_EQ(allocationState->createTextureCalls, 1); + EXPECT_EQ(allocationState->createRenderTargetViewCalls, 1); + EXPECT_EQ(allocationState->createShaderViewCalls, 1); + EXPECT_EQ(allocationState->shutdownTextureCalls, 1); + EXPECT_EQ(allocationState->shutdownRenderTargetViewCalls, 1); + EXPECT_EQ(allocationState->shutdownShaderViewCalls, 1); + EXPECT_EQ(allocationState->destroyTextureCalls, 1); + EXPECT_EQ(allocationState->destroyRenderTargetViewCalls, 1); + EXPECT_EQ(allocationState->destroyShaderViewCalls, 1); +}