fix(vk): no mirror is made inside a parallel replay's segment #1184
No reviewers
Labels
No labels
accessibility
autorelease: pending
autorelease: tagged
bug
dependencies
documentation
duplicate
enhancement
github_actions
good first issue
help wanted
invalid
question
testing wanted
wontfix
No milestone
No assignees
1 participant
Due date
No due date set.
Dependencies
No dependencies set
Reference
MagicBeanDev/Hookshot!1184
Loading…
Reference in a new issue
No description provided.
Delete branch "fix/parallel-mirror-make"
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?
What this fixes
With
RX_VK_PARALLEL, large structures go missing in play: houses, big rocks, bushes, the tree over the Kokiri shop. With small segments, geometry is torn into huge stretched triangles. Serial replay is complete. This is theRX_VK_PARALLELpart of #994.gpu_buffer_bindmakes a pool's VRAM mirror the first time the pool is bound (mirror_make):In a parallel replay that recording is the uploads of the segment whose worker bound the pool first. The workers record out of order, so this need not be the first segment in the queue that reads the pool. An earlier segment, submitted first, reads the mirror before it is filled.
This matches what was seen:
Change
Inside a parallel segment (
par_active), the first binding no longer makes the mirror. The pool is marked wanted, and the mirror is made at the next submission's start, asRX_VK_MIRROR_DEFER=1already does for every pool. Serial replay is unchanged. The cost is that a pool first bound in a segment is read from guest memory for one more submission.tests/test_parallel_mirror_make.pychecks the first-binding condition: it fails on current main and passes with this change.Tested
RX 7900 XTX, RADV (Mesa 26.2.6), Linux. Hookshot v2.0.7 plus #1069 and #1108 plus this change. Scripted runs: the same save in front of the Kokiri shop, 4 camera directions, 2 runs per setting.
RX_VK_PARALLEL=0RX_VK_PARALLEL=4RX_VK_PARALLEL=4,RX_VK_PARALLEL_SEG=40RX_VK_PARALLEL=4,RX_VK_PARALLEL_SEG=1000000(one segment)Ruled out before this was found: #1069 (
RX_VK_RB_REFRESH_EACH=1),RX_VK_NO_DESC_REUSE,RX_VK_NO_GRID_PATCH. #1108 alone was not tested without this change.Not covered
mirror_syncinside a segment (a pool synced by a later segment first) was already handled bysync_segand is unchanged.🤖 Generated with Claude Code
Nice find, and the diagnosis checks out against the code.
Walked through the failure on main: segment k's worker binds a pool first,
mirror_makerecords the whole-pool fill into segment k's upload and setssynced_gen = gen_now,sync_seg = k. Then a worker for an earlier segment j < k binds the same pool, takes the slow path, seessync_seg > texture_segment()and callsmirror_sync, which copies nothing (no page is newer thansynced_gen) and hands backp->mirror. Segment j runs before k's upload in the queue, so it reads an empty mirror. That lines up with what you saw (worse with more segments, fine with one, self-healing once everything is mirrored).What I checked with
!par:MIRROR_WANTEDand every binding of it (both the lock-freetl_poolspath and the locked path, any worker) getsp->buffer, so no segment can end up bound to a mirror that doesn't exist or isn't filled yet. The page table isn't touched either, sinceguest_pages_setonly happens inmirror_make.mirror_wanted.push_backand themstatechange happen underpools_mu, and the mirror is made atmemory_begin_submissionon the replay thread, outsidepar_active.par_activeis only set while workers record segments (the replay thread just waits and submits), and it's set before the jobs are pushed underpar_pool.mu, so the relaxed load in the workers is fine.mirror_makecall sites are this one andmemory_begin_submission, so nothing else makes a mirror inside a segment. Pools already mirrored are still covered bymemory_sync_mirrors_before_segments.The trade-off is that a pool first bound inside a segment is read from guest memory for one submission. That's the case the make-at-first-binding was added for (the shadow line flicker), so it could come back briefly for those pools under
RX_VK_PARALLEL. That's much better than missing geometry and it's documented, so I'm fine with it. If it ever shows up, a follow-up could make those mirrors on the replay thread before the split.Test:
tests/test_parallel_mirror_make.pygets picked up byrun_all.py's glob and doesn't need anARGSentry. Its checks hold on the head source: onemirror_make(*p)line ingpu_buffer_bind, and that line contains!par. It's a source-shape check, which is how the repo's other tests do it. No CI checks have run on this head yet.Overlaps: #1133 and #1150 both edit
gpu_buffer_bindnear this line (the direct-binding branch, and a new read-back-direct early return), and #1127 changes the inherit step inmirror_make. I don't see any conflict in what they do, just nearby hunks that might need a small rebase depending on merge order. The branch is also a bit behind main (theRX_VK_WB_DIFFdefault), but GitHub reports it mergeable.LGTM.
Checked: with
!parthe pool falls straight into theMIRROR_WANTEDbranch below and bindsp->buffer, and the lock-free path above already returns the guest buffer forMIRROR_WANTED. So once one worker marks the pool wanted, every segment binds guest memory consistently for the rest of the submission, which is what we want.