Framegraph#653
Draft
dpwiz wants to merge 58 commits into
Draft
Conversation
Window controls: arrow keys orbit the camera and -/+ dolly, driven from a program-lifetime Orbit state polled via GLFW each frame (updateOrbit). The camera eye is now a parameter threaded through addScenePasses / viewProjFor / pushCamera / pushResolve; headless keeps the fixed cameraEye. Adds the GLFW-b dep. Shadow fix: geomancy's Vulkan projection is Y-down, but shadowFaces are the GL cube-face vectors (Y-up NDC), so each cube face rendered vertically flipped versus the hardware cube sampler -- shadows landed offset from their casters. lightShadowViewProjs now negates clip-space Y (verified (u,v)==(s,t) on the +X/-Y/+Z faces; occluder cull is NONE so the inverted winding is harmless). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Tweak barriers, give each orb its own culled set.
Port to fragr's typed-handles-flags API: the adapters set Flags ManagedImage/ManagedBuffer = Usage and the whole Word64 codec (usageFlags/flagsUsage, marker bits, cross-feed guards) disappears — declaring an access with the wrong resource's flags is a type error now. The driver decodes the schedule's existential accesses by eqTypeRep, over-synchronizing for adapters it does not know. Host work becomes a graph pass on a designated host QueueId: submitGraphQueued defers those passes at record time and runs them after the device submits — schedule waits as vkWaitSemaphoresSafe, the pass's signal as vkSignalSemaphore — so readbacks join the dependency graph (headless's host.readback pass). The adapters implement preRelease (producer-side handoff with the consumer's flags, retiring Scene's two manual pass-tail transitions), the Recorder gains a host mode that tracks state without recording, and Buffer.Usage gains HostRead/HostWrite. All four drivers validate clean, including under sync validation. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
fragr keeps dropped waits' covers on the kept wait now, so the driver's acquire-widening workaround (and its anti-edge caveat) retires; the derived waitDstStageMask spans every consumer behind the watermark by construction. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A queue's passes are cut into segments wherever a pass carries a cross-queue wait the current segment's front does not already imply (same producer, value not above the front's); each segment is its own one-time buffer and submit, front-waiting its hoisted values. Front waits therefore only reference passes registered before the segment's first pass, which makes the segment graph acyclic by construction — the hoistCyclic rejection is gone, and mid-stream dependencies (device ping-pong, device→host→device) schedule instead of deadlocking. The plan is derived from FG.snapshot before recording, so buffers are allocated up front and passes route by passId. Headless demonstrates: the readback copy now rides the graphics queue, sandwiching the async compute chain (geometry / compute passes / copy / host peek — four submits plus the host step on async hardware). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The tonemap exposure moves from a push constant into a metering SSBO. Headless adds a host.meter pass between luminance and tonemap — reading this frame's own mean luminance and writing the exposure — so the frame tonemaps at its own metered value and the old meter-then-re-render double run is gone. The device→host→device sandwich rides the segmented submits (compute waits the host timeline mid-stream). Windowed keeps its lagged EMA (an in-graph meter would stall the loop mid-frame): it pre-writes the metering buffer instead of pushing, and the luminance pass keeps its manual host-visibility barrier only on that polling path — with a meter pass the hand-off is the schedule's. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
submitGraphQueued takes a SubmitConfig; its register sink receives every queue's completion values before anything records or submits. resize wires it straight to fGPUWork: the recycler waits the whole graph with no hand-rolled sync, and a submit failing midway now degrades to the recycler's wait timeout instead of reclaiming under in-flight work. The config also carries deferHost — hand the ordered host-pass tail to a runner (e.g. the frame's recycle thread) instead of blocking the calling thread on the GPU. Consumers follow. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Frame gains fDeferredWork: host work the recycle thread runs before the fGPUWork wait — it blocks on its own timeline values and signals values that wait (and in-flight submits) depend on. Windowed rides submitGraphQueued now: the orb uploads move in-graph as a side-effecting first pass, completions register into fGPUWork through the driver, and the exposure meter becomes a host pass deferred to the recycle thread — same-frame metering with no readback lag and no render-thread stall (only the tonemap's segment waits, on the GPU). The EMA is race-free by construction: frame N+1's meter unblocks only after its own first segment, which the queue orders behind frame N's tonemap, which waited frame N's meter. Scene's meter parameter generalizes to a luminance→exposure function so headless (plain target) and windowed (adaptation state) share the pass. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Cross-pollination while vulkan-utils is unreleased: allocateCommandPool / allocatePrimary move into Vulkan.Utils.Frame (mkRecycledResources now uses the former), SubmitExtras/noExtras move down with them (they are plain Vulkan, nothing framegraph about them), frameSubmitExtras names the canonical windowed shape queueSubmitFrame hard-codes, and waitTwice is exported. The graph driver gains frameSubmitConfig, wiring a frame in one call: canonical extras on the default queue, completions registered into fGPUWork pre-submit, host tail deferred to the recycle thread. The deferral earned a hard constraint, found the loud way (black screen, wedged present): a deferred host pass must not gate a presented image — at vkQueuePresentKHR time every signal the present wait depends on must already be submitted (VUID 03268), and the deferred vkSignalSemaphore is not. Windowed metering therefore returns to the lagged pre-write EMA; the same-frame meter sandwich stays headless-only, and deferHost remains for host work outside the swapchain chain. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
RecycledResources carried one pool, for the graphics family, so anything multi-queue had nowhere to take a recycled pool from: resize built a fresh VkCommandPool every dirty frame and headless one per graph run. Pools are expensive to create and cheap to reset — the inverse of command buffers — so they belong on the frame timeline. rrCommandPools is now a Queues of pools, one per distinct family (roles sharing a family share the handle), reset when the frame retires; RELEASE_RESOURCES is gone from that reset, since returning the arena to the driver defeats the point of recycling. resize takes its compute buffers from the frame's compute pool, and headless builds its queue table once for every run. Only command buffers stay per-run. resize now creates 4 pools for the whole process (2 frames in flight x 2 families) where it previously created one per dirty frame; headless 2 for its 7 graph runs. All drivers clean, 0 syncval hazards. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
recordOrbUploads was a graph pass whose body still hand-rolled both its barriers: a leading one making the previous frame's reads (WAR) and its own uploads (WAW) retire, and a trailing one whose dst stages had to cover every in-graph reader of the three tables — coverage maintained by hand, in a comment. The lights / view-projection / object tables are ManagedBuffers now, imported per frame. orbs.upload declares three TransferDst writes and every consumer declares its read (cull and shade at COMPUTE, geometry and shadow.orbs at VERTEX, ssao.normals at COMPUTE), so the tracker places the transfers — including the cross-frame write-after-read, since the buffers' tracked state persists across graphs exactly like the image layouts do. Both hand-written barriers are gone, and adding a reader can no longer under-synchronize. The pass moved into addScenePasses, so headless refreshes the tables too (identical values at t=0). 0 syncval hazards on the 25s windowed soak that originally caught the WAW; headless metrics unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The adapters emit no ownership transfer, so an EXCLUSIVE resource read on another queue family has undefined contents — silent garbage, no validation message. Every wrapper now records whether its allocation is CONCURRENT (sharedAcrossQueues, a modifier in the describedAs idiom), and crossing queues without it is fatal, naming the resource. Modelling the accessor properly was the substance: a resource that has never been touched on a device queue owns nothing yet (queueRef is Maybe now, not a QueueId 0 default), and the host is not a queue family — its accesses order through the schedule's timeline and the producer's release barrier. Both were false crossings before. It immediately found a real one: headless's readback copy reads display on the graphics queue while the gamma pass writes it on compute, and display was allocated EXCLUSIVE — undefined by spec, working only by RADV's grace. It is CONCURRENT now. resize's offscreen is marked exactly when its topology is async, which is when it crosses. Also moves the completion registration to after recording (still before any submit), so a failure while recording cannot leave the recycler waiting on timelines the unwinding frame scope destroys. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
EXCLUSIVE resources now cross queues properly: the adapters' release and acquire hooks emit the matching barrier pair (fragr hands them the peer queue), naming both families from the driver's QueueSlots. CONCURRENT resources own nothing and keep riding the semaphore alone; the host is not a family and owns nothing either. Getting the pair to match took two things. The release performs the layout transition, so it advances the tracked state — otherwise the next same-queue reader re-emits a transition from a layout that already moved (caught as VUID-VkImageMemoryBarrier-oldLayout-01197 on vis, which ssao.normals reads between geometry and shade). The acquire therefore remembers the state the release saw (releasedRef) and builds its half from that, and marks the node chained so the pass's own declared access does not stack a second barrier on top. The guard from bc1550c narrows accordingly: a cross-family *read* the graph never handed over is still fatal, but a *write* is fine — it acquires by discarding (UNDEFINED old layout), which is what a queue picking up a resource it does not need the contents of is entitled to do. That is what lets gamma re-write display on compute each run after graphics owned it. Demonstrated by making display EXCLUSIVE again: gamma writes it on the compute queue, the headless readback copy reads it on graphics, and the graph hands it over — 10 real family-1-to-family-0 barriers per run by api-dump, everything else QUEUE_FAMILY_IGNORED. 0 validation messages, 0 syncval hazards, headless metrics unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
TODO-next/TODO-misc/TODO-scene-lights/FIXME/NOTE-depth-effects were
scattered across the repo root and half of their entries had been fixed
without anyone striking them out. Consolidated into .mgmt/{TODO,FIXME,NOTE}.md
with every entry re-checked against the tree, and .mgmt/stale.md records what
was retired and why.
Notable: the orb-upload barrier and most of the auto-exposure list are done
(the readback lag went away entirely via the in-graph host meter), the
multi-queue FIXME's text now actively misdescribes the code (it predates
typed Flags), and the scene-lights bench numbers predate three rescales. The
log-average meter, the histogram fix and the depth-pyramid note are still
live and unchanged.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
.mgmt/ holds the working TODO/FIXME/NOTE ledgers, which have always been local scratch (the root notes they came from were never tracked). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Submits go through vkQueueSubmit2, so a wait carries its value and its stage in one SemaphoreSubmitInfo -- no TimelineSemaphoreSubmitInfo chain, and the v1 stage decoder (resourceStage/accessStage) collapses into the sync2 one (accessScopes) it duplicated. Drop the VkEvent split-barrier path. It only ever bought overlap between a producer and its consumer, while each access already places a self-sufficient barrier -- and it was the one primitive a portability-subset implementation may not have at all (VUID-vkCreateEvent-events-04468). The recorder's per-pass "already synchronized" set survives as the ownership acquire's mark, renamed off the events it no longer comes from. Fences follow: the one-shot setup submits wait a timeline instead. Binary semaphores stay only where WSI mandates them -- vkAcquireNextImageKHR signals one and vkQueuePresentKHR waits on one, with no timeline accepted -- so they are confined to SubmitExtras. Requiring synchronization2 moves into frameDeviceRequirements and HeadlessBoot, next to the helpers that now submit with it. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Timeline semaphores and synchronization2 are what everything here synchronizes with, so vulkan-utils asks for them rather than each example remembering to: syncDeviceRequirements, which frameDeviceRequirements is now a swapchain plus. Both boots merge it in, so an example only lists what is actually its own -- visibility-buffer drops to multiview, cube arrays and multi-draw-indirect. Copy-paste and tweak, or stay on older utils; we are committed to 1.3 in this release train and these are not going away. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Driver stopped building CommandBufferBeginInfo/CommandPoolCreateInfo when pool and buffer creation moved to Vulkan.Utils.Frame, and resize's atomicModifyIORef' went when the driver's register hook took over appending to fGPUWork. GHC had been naming all three; the tree now builds warning-free. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Plan-time aliasing needs to know whether two entries' lifetimes can overlap. EntryInfo.live gives positions in execution order, but positions order the executing passes globally while passes on different queues run concurrently -- so disjoint positions do not imply disjoint lifetimes, and packing on positions alone would corrupt silently. Aliasing rides on the schedule's happens-before instead: a vector clock per pass, folded from its queue predecessor and the passes its waits name, so entry a may hand memory to b only when a's last pass is genuinely ordered before b's first -- same queue, or a wait chain that reaches it. Pure and tested first, before any VMA code exists: 13 cases, including the one a positions-only planner gets wrong (disjoint ranges, concurrent queues, no wait -- must not share). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The hand-picked cases only test the cases I thought of, which is the wrong property for code that corrupts silently. So enumerate the space instead: every schedule of 5 passes over 3 queues with every combination of waits -- 248832 of them -- and check happensBefore against an oracle derived the other way round, by explicit edges and transitive closure rather than vector clocks. An oracle sharing the implementation would prove nothing. Also asserts the invariant the feature rests on, over the same space: whatever planAliases puts in one block is pairwise ordered, so two tenancies can never overlap. 0.74s for the suite. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Macos does not retain swapchain images.
Add json viewer dump to vb.
dpwiz
force-pushed
the
framegraph
branch
2 times, most recently
from
July 25, 2026 11:39
794165c to
964c302
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.