fix(vk): mark a clear's targets in the parallel replay's dry pass (the one-frame black scenes) #1639
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!1639
Loading…
Reference in a new issue
No description provided.
Delete branch "fix/parallel-replay-clears"
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?
With
RX_VK_PARALLEL(the multi-threaded replay, off by default) the whole 3D scene goes black for one frame now and then, with the HUD still drawn over it. This fixes the cause I found; the mode stays off by default.What goes wrong
RX_VK_PARALLELrecords a command buffer's segments on several threads, out of order, after a dry pass on the replay thread. A worker that samples a texture nobody has marked as the GPU's uploads what its memory holds, and that upload runs after the earlier segments' work.The dry pass marks the targets of draws and dispatches for that reason (
dry_mark_written), but it left clears out, and a clear opens a pass on the worker just as a draw does. So a target the game clears in one segment and samples in a later one was overwritten with stale memory whenever the later segment was recorded first. The result is the whole 3D scene black for one frame, with the HUD over it.The existing warning names it every time:
#... 853x480 NVN 0x29 written in segment N, uploaded by the later segment N+2, a target made that frame.Changes
NOP_CLEAR_COLORandNOP_CLEAR_DS(RX_VK_NO_DRY_CLEARS=1: as before).dry_mark_writtenskips "the same targets as the draw before" within one dry pass only (dry_marks_begin). Itsfirstis the pass's first draw or dispatch, so a command buffer that began with a dispatch compared its first draw with the last draw of the dry pass before. This one is by reading; I could not make it happen on demand (RX_VK_DRY_LAST_KEPT=1: as before).written by: a pass's colour target).Numbers
Unattended runs (
HOS_HEADLESS, scripted input), Ryzen 9 9950X3D, RTX 4070 SUPER, built from4a7ae1b. Castle Town's market with the camera turning, about 2,450 draws.8 threads and segments of 32, to make the fault frequent; 5,520 presents a run:
The same with
RX_VK_FLASH(a shot every present), 4,200 presents a run:On 3 threads, eight other save points (Kokiri Forest, the Deku Tree, Dodongo's Cavern, Jabu-Jabu, Hyrule Castle, Hyrule Field, the Temple of Time, and one made after meeting Zelda and Impa), about 3,800 presents each with
RX_VK_FLASH: no flash, no warning.The replay's wall time a frame in the market:
Not done
par_threads()was not run again (I have no save there), so the mode stays off by default.CLEAR_TEXdestinations are still not marked in the dry pass. None showed up in these runs' warnings.RX_VK_PARALLEL>=2; this does not touch that.Test
python -I -B tests/test_parallel_dry_marks.py(new, source checks; fails on main).run_alltexture_handle, texture_init, mem_trim pass.🤖 Generated with Claude Code
Looks right to me. A clear on the worker goes through
begin_pass(cmd, targets(s)), which marks every colour target plus depth, so markingtargets(s)in the dry pass is exactly what the worker will do later, just earlier and in stream order. Partial, scissored and layered clears mark the whole texture there, the same as the serial path does throughbegin_pass, so nothing changes in behaviour beyond the ordering. Passingfirst = trueand settingdry_last_validmeans a draw on the same targets right after the clear is still skipped correctly.The
NOP_UPDATE_UBfix is correct too: the worker now counts an update under the sameu.size && u.size <= avail - sizeof utest the dry pass appends by, soub_limitno longer drifts ahead ofub_updates.dry_marks_begin()is thread_local and runs on the same thread as the dry pass, so it's fine.The serial path only gains a
dry &&test per clear, and the dry pass gains onetargets()lookup (cached byrt_serial) per clear, so the cost is negligible. Thewhystring is only formatted when the warning fires.Nits, none blocking:
tests/test_*.pyglob, so it is registered, but it will break on any rewording and doesn't exercise the marking itself. That's acceptable for this path, since there's no GPU in CI.dry_marks_begin()in place, the draw'sfirstargument (dry_work == 0) is now redundant for draws. You could leave it or drop it.COPY_B2T/COPY_T2T/CLEAR_TEXdestinations are still unmarked in the dry pass, so the same kind of order warning could still come from a copy target. A follow-up could mark those too.CI is green. No overlap with #1473 or the other open vk_replay PRs on these lines.
Nit:
clear_color_targetreturns beforebegin_passwhenmask & 0xFis 0 orindexhas no view, so in that case the worker marks nothing, but the dry pass still marks every target here. Over-marking setsgpu_written, and that can stop a later segment from uploading a CPU-written target that the serial replay would have uploaded. It's rare, but gating on the payload's mask (e.g.payload<nvn_p_clear_color>(a, avail).mask & 0xF) would match the worker exactly.Same here:
clear_depth_stencilreturns early when no depth is bound, or when neither aspect is masked in. A depth-only clear with no depth target then marks the colour targets in the dry pass, although the worker never opens a pass for them. Checkings.rt.depth(and the masks) first would keep the two in step.