Stop compositing a special effect on frames that did not request it - #178
Conversation
R_AddSpecialEffects issues no RB_DrawSpecialEffects command once every effect is off, so the mask and depth captures that command leaves in rbRVSpecialActiveMask and rbRVSpecialBlurPrepared are never refreshed. RB_DisplaySpecialEffects kept honouring them, and went on compositing the last frame's blur on every later frame. Answering the multiplayer join card is the common way in: the game clears its soft focus and the renderer's blur bit, yet the world stays blurred, from a depth mask captured when the card was up, until a vid_restart. Only honour that state on the frame whose command produced it, as the special-frame record at the end of the view already does. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017RU1i6R3gN4cdKRELGiCEc
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
|
Windows and Linux results. Commits: base Correction to the repro above: Distant-region sharpness. Same view within each run, and the same spawn view in base and fix on each platform:
|
themuffinator
left a comment
There was a problem hiding this comment.
This is the right fix, and the diagnosis holds up. Thank you for reworking it — the difference between this and #172 is the difference between describing the bug and guessing at it.
The guard matches the rule the special-frame record at the end of the view already applies (draw_common.cpp:9993), and backEnd.frameCount is assigned from cmd->frameCount in tr_backend.cpp:505, so it is stable for the whole backend frame and the comparison means exactly what you want it to mean.
Reproduced and verified on Windows
I ran your console repro headlessly on Windows x64, OpenGL, at a frozen view (g_stopTime 1) so all three captures show the same frame. Sharpness is the mean absolute luminance gradient over the centre of the frame:
| reference | r_forceSpecialEffects 1 |
back to 0 |
|
|---|---|---|---|
main (71c3230c) |
0.699 | 0.543 | 0.543 — stuck |
| with this PR | 0.645 | 0.505 | 0.645 — recovers exactly |
So it is not macOS-specific, and the fix restores the reference frame precisely rather than approximately. (The two rows start at different absolute values because the spawn view differs between runs, as you noted.)
One extra data point
The same probe on Vulkan recovers correctly on main without this change — 0.656 → 0.541 → 0.656. That corroborates the diagnosis rather than contradicting it: VK_GuiExecutor_PrepareSpecialEffects recomputes its mask from tr.specialEffectsEnabled every frame, so it has no stale state to honour. The bug is specific to the GL backend's file-scope rbRVSpecial* statics, which is exactly where you put the fix.
renderer_classic_special_frame_domain.py, renderer_classic_gui_domain.py, gui_clipping_contract.py, renderer_vulkan_probe_safety.py and renderer_vulkan_gui_residency.py pass on Windows. Builds clean on MSVC.
Merging. I will close #171 once you confirm the join-card path on a build from main.
Fixes #171. Supersedes #172.
After the multiplayer join card is answered, the world can stay blurred until
vid_restart, even though the game has cleared its soft focus and the renderer's blur bit.Cause
R_AddSpecialEffectsissues noRB_DrawSpecialEffectscommand once every effect is off. That command is the only placerbRVSpecialActiveMask,rbRVSpecialBlurPreparedand the depth captures are refreshed, so after the last effect frame they keep their old values.RB_DisplaySpecialEffectsnever checked their age, so it kept compositing the blur every frame, from a depth mask captured while the card was up. The stale mask is why the lingering blur looks different from the card's intended soft focus, and why it can look inconsistent.After the join card was answered on GL (game code at
9889d9e, unmodified), a debugger read showed:tr.specialEffectsEnabled/joinScreenSoftFocusEnabled0/falserbRVSpecialActiveMask/rbRVSpecialBlurPrepared1/truerbRVSpecialCommandFrame1271whilebackEnd.frameCountadvanced1940 → 2093Fix
RB_DisplaySpecialEffectsnow returns unlessrbRVSpecialCommandFrame == backEnd.frameCount, so effect state is only honoured on the frame whose command produced it. The special-frame record at the end of the view (draw_common.cpp,rbRVSpecialCommandFrame == backEnd.frameCount && rbRVSpecialCommandView == viewDef) already applies the same rule. No game-module change is needed: the game's own bookkeeping was already correct.Repro
Console only, on OpenGL:
+set si_gameType DM +set ui_autoJoin 1 +spawnServer mp/q4dm1r_forceSpecialEffects 1, wait a moment, thenr_forceSpecialEffects 0vid_restart.The player-facing path is the join card:
+set ui_autoJoin 0, then click JOIN GAME.What was tested, and what wasn't
macOS arm64 (M1 Max), OpenGL (Apple GL 2.1 compatibility path), fullscreen 3456x2104, 4x MSAA,
openQ4-gameat9889d9e. Each pair of compared frames shows the same view. The join card was answered throughidSessionLocal::MenuEventwith a left click on the button, the same path a real click takes. Sharpness is the mean absolute luminance gradient over the distant region of the frame (higher is sharper):Console repro, same view within each run (distant region):
r_forceSpecialEffects 10The two rows start at different values because the spawn view differs between runs.
r_skipPostProcess 1andg_doubleVision 0all leave the blur in place.vid_restartrestores 12.81.renderer_classic_special_frame_domain.py,renderer_vulkan_world_interaction_compatibility.pyandrenderer_classic_gui_domain.pypass.🤖 Generated with Claude Code
https://claude.ai/code/session_017RU1i6R3gN4cdKRELGiCEc