Standalone renderer layer: FrameData/DrawItem submission, scene and canvas as clients #18
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Goal
Make the renderer its own layer (a small "render engine") that builds and executes a render graph from plain per-frame data. Scene, Canvas and UI become clients of it; the renderer knows nothing about them.
Test for done-ness of the design: a different scene system (or none at all) could be built on top without changing the renderer.
Layers
Renderer vocabulary
RenderViews. Rebuilt every frame.viewProjection, clear color,DrawItems, and typed instance lists (e.g. sprite instances). Scene-levelSceneView(scene + camera entity + target) is turned into one of these by extraction.u64 sortKey,PipelineId, resource handles (texture, mesh), instance offset. Handles only, never pointers; lives one frame.*Renderer): one perPipelineId, owning its shaders, pipeline, descriptor layout, instance format and per-frame buffers; draws already-sorted batches. The pipeline set stays closed; variation (e.g. blend mode) goes in sort-key sub-bits, not new generic state.RenderGraphis the pass representation; don't add a parallelRenderPass/DrawPacketdata struct that duplicates its passes and access declarations.DrawItem: user shaders, compute, post-processing, the ImGui composite. A feature adds passes to the graph each frame. It gets aRenderGraph&only duringaddPasses()and never stores it. Features are ordered by phase (e.g.Scene,PostProcess,Overlay), not registration order.ImGuiPassfeature.Ownership and frame lifecycle
RenderContextandRenderGraph. The graph is a member,reset()at the start of each frame. It keeps its allocations and gives persistent graph state (transient image/buffer pools, cached barrier plans, aliasing) a place to live later. Per-frame semantics are unchanged: rebuilt every frame.vk::CommandBufferappears is the graph'sExecuteFn, which is the recording layer. The loop becomes:ResourceRegistryRef, and asset loading or headless tools shouldn't need a swapchain. Renderer-side, per-handle GPU state (descriptor caches such asSpriteRenderSystem::descriptorSets,ImGuiRenderer::textures) belongs to the features/drawers that use it. Lifetime:Device→ResourceRegistry→Renderer, torn down in reverse afterwaitIdle().ResourceRegistry{device, renderContext.getImageFormat()}is what makes the registry look renderer-owned. Make the format a per-target creation parameter (or engine-config default) so the registry depends only onDevice.Interfaces (sketch)
Renderer::render: acquire the frame (return early, callingframeSkipped(), on resize/minimize); call each feature'sbeginFrame;graph.reset(); import the backbuffer; per view sort items by key, merge neighbours into batches, upload instance data, and add one graph pass per view (dispatching batches to drawers); let features add passes in phase order (UI composite isOverlay, so last); execute; present.Where current code goes
Device,RenderContext(owned byRenderer),Swapchain,RenderGraph(owned byRenderer),ResourceRegistry+ pools (sibling, by reference),SpriteRenderSystem,ModelRenderSystem.Scene,Entity, components,SceneView,SceneViewRegistry,Canvas.SceneRenderer→extractScene(client) + generic sort/batch inRenderer.Application::Impl::rungraph building andrenderContext.beginFrame()/endFrame()→Renderer::render. TheviewTargetsmap becomesFrameContext::importTarget.handles.hppmixes render handles (Texture/Mesh/RenderTarget) with client handles (Scene/Panel/SceneView); split so renderer headers don't declare scene types.ImGuiRenderersplits in two:NewFrame, dock layout,reset_layoutcommand,getTextureId.ImGuiPass : RenderFeature(Overlayphase):ImGui_ImplVulkan_RenderDrawDatainto the backbuffer, per-target descriptor cache. It should declaresample()only for targets whosegetTextureIdwas called this frame, instead of the current "main" pass sampling every view target.Plan
Each step should build and run on its own.
RendererownsRenderContext+RenderGraphand takes over frame acquisition and graph building fromApplication::run. Application callsrenderer.render(frame)with no command buffer. ImGui can stay a hard-coded final pass for this step.DrawItem/RenderView/FrameData/ instance structs.SceneRendererintoextractScene+ sort/batch inRenderer. Renames:SpriteRenderSystem→SpriteRenderer,ModelRenderSystem→ModelRenderer,SpriteDrawItem→DrawItem.CanvastoextractCanvas. As the second client, it validates that the API isn't shaped aroundScene.sophia_rendererCMake target so the boundary is enforced by the build (renderer can't include scene headers); splithandles.hpp. Decouple render target format from the swapchain format soResourceRegistrydepends only onDevice.RenderFeature+FrameContext, with phase ordering. SplitImGuiRendererinto the UI plugin andImGuiPass(first real feature, replaces the hard-coded UI pass).RenderGraph(BufferRef,importBuffer,PassBuilder::readBuffer/writeBuffer, buffer barriers), compute passes that skipbeginRendering, and handle-based graphics/compute pipelines created from user SPIR-V (ideally with reflection for layouts).Watch for
u32 userData, interpreted by the client.NewFrameruns before acquisition, a skipped frame must still close the ImGui frame (ImGui::EndFrame()) or the nextNewFrameasserts. That's whatRenderFeature::frameSkipped()is for.drawCalls) belong insiderender()after acquisition, not inonRender().onRender()only collects CPU-side data; this also lets CPU work for frame N+1 overlap the GPU on frame N.DrawItempipeline set. The closed set covers built-in drawers; custom pipelines come in through features.Related
SpriteRenderSystem/SceneRenderer, which this refactors.