Context
SkySystem registers multiple sky backends (SkySpherePass, SkyAtmospherePass, HDRIBackdropPass) into the render graph. Only one backend is active per scene. Switching between modes at runtime requires enabling/disabling passes, but the current RenderGraph::Compile() uses an idempotent guard:
pass.Callback->Compile(device, scene, builder, inspector, &pass.Handle);
// Inside every Compile(): if (output_pass && !(*output_pass)) { ... }
A pass compiled on the first Compile() call is never recompiled. A pass that was disabled during the first Compile() has pass.Handle == nullptr and is silently skipped in Execute().
Current workaround: all sky backends are compiled on startup (registered as enabled). Only one is active per Execute() frame. This wastes pipeline creation time for unused backends.
Problem
When mode switches at runtime (e.g. from SkySphere to Atmosphere), the new pass's Compile() has already run but its resource declarations (Setup()) may not match the current graph topology if transient resources changed. AllocateFramebuffers() also needs to re-run to allocate the new pass's framebuffer.
Additionally, the render graph topology (BuildTopology, BuildLifetimes, BuildBarriers, AllocateTransientResources) is computed once at Compile() time. Adding or removing active passes can invalidate the topology.
Design options
Option A — Selective recompile: When SkySystem::ApplyMode detects a mode change, call RenderGraph::Compile() again. Since Compile() is idempotent for already-compiled passes (handle guard), only new passes get compiled. Framebuffers are re-allocated via AllocateFramebuffers(). Old framebuffers are deferred-freed via the existing old_fb_count path.
Option B — Full reset + recompile: Clear pass.Handle for all sky passes before calling Compile(). Forces a full re-bake of the active backend's pipeline. More expensive but correct when topology changes.
Option C — Lazy compilation: Compile each pass on first Execute() when enabled rather than eagerly at startup. Requires a "needs_compile" flag per pass and moving compilation into the execute loop (with a one-frame stall on first use).
Recommended: Option A first. Option C if startup cost becomes measurable.
Acceptance criteria
- Switching sky mode mid-session (e.g. via Settings panel or scene reload) produces no visual glitch, no Vulkan validation error, and no frame stall longer than one Compile() call.
- The sky backend that was active before the switch disposes its framebuffer via the deferred-free path.
References
- sky-rendering.md §7: "Mode changes at runtime require RenderGraph::Compile()"
- SkySystem::ApplyMode() in ZEngine/Rendering/Renderers/Sky/SkySystem.cpp
- RenderGraph::Compile() in ZEngine/Rendering/Renderers/RenderGraph.cpp
Context
SkySystem registers multiple sky backends (SkySpherePass, SkyAtmospherePass, HDRIBackdropPass) into the render graph. Only one backend is active per scene. Switching between modes at runtime requires enabling/disabling passes, but the current RenderGraph::Compile() uses an idempotent guard:
A pass compiled on the first Compile() call is never recompiled. A pass that was disabled during the first Compile() has pass.Handle == nullptr and is silently skipped in Execute().
Current workaround: all sky backends are compiled on startup (registered as enabled). Only one is active per Execute() frame. This wastes pipeline creation time for unused backends.
Problem
When mode switches at runtime (e.g. from SkySphere to Atmosphere), the new pass's Compile() has already run but its resource declarations (Setup()) may not match the current graph topology if transient resources changed. AllocateFramebuffers() also needs to re-run to allocate the new pass's framebuffer.
Additionally, the render graph topology (BuildTopology, BuildLifetimes, BuildBarriers, AllocateTransientResources) is computed once at Compile() time. Adding or removing active passes can invalidate the topology.
Design options
Option A — Selective recompile: When SkySystem::ApplyMode detects a mode change, call RenderGraph::Compile() again. Since Compile() is idempotent for already-compiled passes (handle guard), only new passes get compiled. Framebuffers are re-allocated via AllocateFramebuffers(). Old framebuffers are deferred-freed via the existing old_fb_count path.
Option B — Full reset + recompile: Clear pass.Handle for all sky passes before calling Compile(). Forces a full re-bake of the active backend's pipeline. More expensive but correct when topology changes.
Option C — Lazy compilation: Compile each pass on first Execute() when enabled rather than eagerly at startup. Requires a "needs_compile" flag per pass and moving compilation into the execute loop (with a one-frame stall on first use).
Recommended: Option A first. Option C if startup cost becomes measurable.
Acceptance criteria
References