fix(comp_bg2d): stop mutating the slot-16 backdrop under an in-flight DP compose (#1120) - #1123
Open
dfattal wants to merge 1 commit into
Open
fix(comp_bg2d): stop mutating the slot-16 backdrop under an in-flight DP compose (#1120)#1123dfattal wants to merge 1 commit into
dfattal wants to merge 1 commit into
Conversation
… DP compose (#1120) Slot 16 hands the display processor a *borrowed* VkImageView. Since L11 (leia #176) the DP's compose pass is GPU-synced rather than CPU-waited, so when control returns to the runtime, frame N's compose can still be sampling `st->image` — and the runtime then mutates it, with no synchronisation of its own: * `comp_bg2d_teardown` destroys the image/view/memory with no wait at all, and is reachable per frame (`BG2D_MAP_DROP`, and any change of the upload dims). * `bg2d_build` re-uploads into the same image behind a barrier whose `srcStageMask` is `TOP_OF_PIPE_BIT` — that orders nothing against the previous submission's fragment reads. Two mutation kinds, two fixes: * RE-UPLOAD into a live image: name the compose's stage in the transition barrier (`FRAGMENT_SHADER_BIT` once `uploaded_once`). A barrier's first synchronisation scope covers everything submitted earlier on the same queue, and the compose submits on the same `vk->main_queue->queue` this upload does, so the write-after-read hazard is closed at zero CPU cost. `srcAccessMask` stays 0: WAR needs an execution dependency, not a memory one. * DESTROY: a barrier cannot protect a freed object, so drain `vk->main_queue->queue` first — the exact field `compose_pre_weave` reads at submit time, both under the compositor lock, so it is the same queue by construction. Gated on `uploaded_once`, and nothing here mutates unless the capture geometry actually changed, so the steady state pays nothing. A graveyard/deferred-destroy was rejected: the runtime does not own the DP's compose fence, so any deferred drain would be a frame-count heuristic rather than a proof — and the argument that usually favours it (avoid a steady-state CPU wait) does not apply, because bg2d only mutates on change. Two knobs, both inert by default: * `debug.dxr.bg2d.sync` / `DXR_BG2D_SYNC` = 0 restores the pre-#1120 ordering on the SAME build, so the A/B is one binary. * `debug.dxr.bg2d.jiggle` / `DXR_BG2D_JIGGLE` = HZ perturbs a local copy of the canvas rect (+16 x, -16 w) at HZ so the re-crop, teardown and re-upload path runs continuously — the race is only reachable when the canvas moves, and in the steady state it never does. Measured on NP02J (avatar in-process, T2 capture, jiggle 10 Hz): ~9 live-image teardowns/s sustained, COMPOSE_COST 0.164-0.276 ms/frame with gpu_sync=1 in BOTH arms — i.e. the fix is free even under continuous churn. IMPORTANT — this does not claim to cure the #1120 flash frames. Under that same forced churn the *unfixed* arm produced ~1 flash frame in 2585 frames, which does not reproduce the reported ~63/231-class alternation. The reason is in the vendor DP: `alpha_gate_run` ends every composed frame with `vkQueueWaitIdle(vk->main_queue->queue)`, which drains compose N before frame N+1's teardown can run. The defect is therefore real but currently *masked* by an invariant the runtime neither owns nor asserts — and that drain is the last CPU wait left after L11, i.e. the obvious next thing to remove. This lands as hardening so that removing it stays a local decision. Refs #1120 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N3kzh1GTPNrm62TbEhFmqP
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.
What
Slot 16 hands the display processor a borrowed
VkImageView. Since L11 (leia #176) the DP's compose pass is GPU-synced rather than CPU-waited, so when control returns to the runtime, frame N's compose can still be samplingst->image— and the runtime then mutates it with no synchronisation of its own:comp_bg2d_teardowndestroys the image/view/memory with no wait at all, reachable per frame (theBG2D_MAP_DROPbranch, and any change of the upload dims).bg2d_buildre-uploads into the same image behind a barrier whosesrcStageMaskisTOP_OF_PIPE_BIT— that orders nothing against the previous submission's fragment reads.Fix — two mutation kinds, two mechanisms
FRAGMENT_SHADER_BITonceuploaded_once). A barrier's first sync scope covers everything submitted earlier on the same queue, and the compose submits on the samevk->main_queue->queuethis upload does.srcAccessMaskstays 0 — WAR needs an execution dependency, not a memory one.vk->main_queue->queuefirst. That is the exact fieldcompose_pre_weavereads at submit time, both under the compositor lock, so it is the same queue by construction. Gated onuploaded_once.Rejected: a graveyard / deferred destroy. The runtime does not own the DP's
cmp_fence, so any deferred drain would be a frame-count heuristic rather than a proof. The argument that usually favours it (avoid a steady-state CPU wait) does not apply here, becausecomp_bg2donly mutates on change.Kill switches (both inert by default)
debug.dxr.bg2d=off— the whole path, untouched.debug.dxr.bg2d.sync/DXR_BG2D_SYNC= 0 — restores the pre-Android T2 compose-under: runtime alternates between the held bg2d backdrop and the live scene, ~9 Hz whole-window flicker #1120 ordering on the same build, so the A/B is one binary.debug.dxr.bg2d.jiggle/DXR_BG2D_JIGGLE= HZ — perturbs a local copy of the canvas rect (+16 x, −16 w) at HZ so the re-crop → teardown → re-upload path runs continuously. This exists because the race is only reachable when the canvas moves, and in the steady state it never does.Dynamic evidence (NP02J, avatar in-process, T2 capture,
bg2d=capture)Churn proven live, not assumed — the new counter reports ~9 live-image teardowns/s sustained at
jiggle=10(200+ per run, both arms).sync=0(unfixed) + jiggle 10sync=1(fixed) + jiggle 10COMPOSE_COST: 0.164–0.276 ms/frame CPU, gpu_sync=1in both arms — the L11 ≤ 0.3 ms target holds even under continuous churn, i.e. the fix is free. NoDEVICE_LOST, no VK errors in either arm.What this does NOT claim
It does not cure the #1120 flash frames. Under forced churn the unfixed arm produced ~1 flash frame in 2585 frames — nowhere near the reported ~63/231-class alternation, and not attributable.
The reason is mechanical and worth recording: the vendor DP's
alpha_gate_runends every composed frame withvkQueueWaitIdle(vk->main_queue->queue)(leia_display_processor_cnsdk.cpp:2039), which drains compose N before frame N+1's teardown can run. So the defect is real but currently masked — by an invariant the runtime neither owns nor asserts. That drain is also the last CPU wait left after L11, i.e. the obvious next thing someone removes for perf.This therefore lands as hardening, not as a cure: it makes the runtime self-sufficient for its own image lifetimes so that removing the vendor-side drain stays a local decision.
NOT tested
fix_*runs are contaminated: the app PID changed and one recording truncated to 178 frames). Two clean fixed-arm runs are reported above; the full matrix has not been re-run.bg2d=offregression check — not re-measured on this build (the path is untouched by this diff).comp_bg2d.cbuilds on every VK platform but only the Android leg was exercised; CI covers the compile.comp_multileg — sharescomp_bg2dverbatim and is fixed by the same change, but was not exercised.Refs #1120
🤖 Generated with Claude Code
Session: https://claude.ai/code/session_01N3kzh1GTPNrm62TbEhFmqP