Stage D (shadow part): each in-flight slot's FrameGraph now owns both the
shadow and G-buffer passes as declarative, dependency-free passes in one
layer. The CSM array is imported per cascade via its layer view with
arrayLayers=3 so barriers cover the whole array; every cascade ends in
DepthStencilReadOnlyOptimal for the engine-side forward pass to sample.
- recordPendingShadowPasses / recordPendingGBufferPass are gone, merged
into recordDeferredFrameGraph(): graph record + submit before the
swapchain pass, with a once-per-slot-per-frame guard so repeated
render3D() calls in one script frame never re-record the same command
buffers (resetting in-flight CBs hung the driver).
- buildDeferredFrameGraphs() rebuilds one graph per slot from the current
shadow array + G-buffer targets; shadows can be enabled without a
G-buffer pass and vice versa (graphs are built on demand).
- JobSystem fix (PR #134 scheduler bug): completeJob now decrements
outstanding/outstandingFrame only after fireCompletion +
releaseDependents have finished using the job. Previously a waiter
(beginFrame/endFrame) could observe outstandingFrame == 0 and reset the
per-frame arena while a worker still touched the job (use-after-free:
lost forked job -> hang, or crash). Two stress tests
(render_graph.jobSystemGroupStress, render_graph.jobSystemEngineFramePattern)
cover the engine's exact beginFrame/parallel_for/task-group frame
pattern.
- Recording stays serial (graph->record()) for now: the JobSystem
help-execution path still has a residual timing-sensitive race that can
leave a worker stuck in a pass callback. The parallel executor
(jobSystemPassExecutor) is ready and CPU-tested; enable it once that
race is root-caused upstream (documented in FrameGraphJobs.h).
Verified locally (win32-debug, full clean rebuild): render_graph.*,
thread.*, Shadow3D.*, RenderSystem3D.*, Lighting3D.*, Mesh.* and
AO/AA/Outline/GI — 77 cases pass.
PR #134's secondary-command-buffer recording violated the Vulkan state
machine (secondaries began their own render pass instances without
pInheritanceInfo; the spec-compliant fix 1291df8 was reverted). Replace
that path with the groundwork for the vkb::FrameGraph migration:
- Drop the illegal secondary-CB deferred recording and restore the
branch base's legal inline recording for shadow/GBuffer passes.
- Fix frame-prep culling regressions from PR #134:
* per-item camera frustum culling (item.culled) instead of always
testing against the main camera;
* default-camera culling for the G-buffer pass (item.culledMain);
* restore per-cascade CSM caster masks (item.cascadeMask).
Camera view-proj matrices are snapshotted on the main thread before
the parallel job forks; workers only read immutable data. The light
job's CSM output is joined before the entity job forks so cascade
culling never races with the shadow-upload write.
- Add FrameGraphJobs.h: vkb::PassRecordExecutor backed by the JobSystem
(layer-internal fork/join via frame task groups, layers sequential),
the exact executor the render backend will hand to FrameGraph::record().
- Add device-free tests (test/render_graph.cpp): the engine's target
pass topology (3 CSM cascades + GBuffer in one parallel layer ->
forward -> present) plans with attachment->sampled barrier folding
(zero explicit barriers on forward/present), and the JobSystem
executor records every pass exactly once, in parallel per layer.
Verified locally (win32-debug): render_graph.*, thread.*, Shadow3D.*,
RenderSystem3D.*, Lighting3D.* and Mesh.* all pass (58/58). Bundle-mode
SEGFAULTs are the known pre-existing zeroerr single-process issue, not
this change. See docs/dev/superpowers/specs/2026-08-20-framegraph-mt-render.md
for the staged migration plan (GBuffer into the graph next, then the
dedicated render thread).