Skip to content

fix(gfx): send fog CLUT payload inline to end sky flicker#4343

Open
semeaj wants to merge 1 commit into
open-goal:masterfrom
semeaj:fix/fog-clut-race
Open

fix(gfx): send fog CLUT payload inline to end sky flicker#4343
semeaj wants to merge 1 commit into
open-goal:masterfrom
semeaj:fix/fog-clut-race

Conversation

@semeaj

@semeaj semeaj commented Jul 17, 2026

Copy link
Copy Markdown

Problem

Jak 2 and Jak 3 exhibit a random one-frame sky/fog flicker, most visible on the jak3 title screen: the sky's warm haze layer pops or briefly disappears for exactly one frame at random intervals (typically every 1 to 3 seconds), independent of framerate. Framerate only changes how visible each bad frame is.

Root cause

The fog texture animation (real-fog-texture-anim-func in texture-anim-funcs.gc) rewrites its 16x16 CLUT in EE memory every frame: a base color fill of all 256 entries followed by the per-entry alpha computation. The PC-port upload record (upload-clut-16-16) only carried the CLUT's EE address. The render thread memcpy's that memory whenever the tex bucket renders, racing the game thread which is concurrently writing the next frame's values. A torn read renders the fog layer (which tints both sky and terrain) with wrong colors for exactly one frame, then self-heals on the next upload.

This was isolated with per-frame instrumentation: with the sky bucket's DMA, the generated cloud texture (all mip levels), texture pool bindings, and GL state all hashed bit-identical on flicker frames, fully serializing the game and render threads eliminated the flicker (2 events vs 104-182 per 2.5 min baseline), which narrowed it to a cross-thread read of live EE memory; the fog CLUT is the only such per-frame-rewritten payload in the sky path.

Fix

Carry the CLUT payload inline in the double-buffered DMA buffer, immediately after the upload record (qwc 65 = 1 record + 64 quadwords of CLUT). The buffer is per-frame double-buffered, so the renderer always sees a coherent copy with no synchronization needed.

The C++ handler accepts both forms based on transfer size, so the other upload-clut-16-16 callers (bigmap, blit-displays), whose data is stable while a frame renders, keep the address form unchanged.

Test plan

  • jak3 title screen at fps 60, vsync off: sky no longer flickers (verified over 3+ minute telemetry captures: large-magnitude one-frame events eliminated; also verified by eye)
  • jak2 compiles and boots (shares the fog anim code; same fix applied)
  • bigmap and progress menu still render correctly (legacy address path)

I work off a self-hosted forge, so this GitHub account is quiet; the full investigation telemetry (per-frame DMA and texture content hashes, framebuffer pixel-patch captures, before/after event-rate runs) is available if anyone wants the raw data.

(AI-assisted)

The fog texture animation rewrites its 16x16 CLUT in EE memory every
frame (base fill plus the per-entry alpha loop), but the PC texture
upload record only carried the CLUT's address. The render thread then
memcpy'd it whenever the tex bucket rendered, racing the game thread
which was already writing the next frame's values. A torn read draws
the fog layer (which tints both sky and terrain) with wrong colors for
exactly one frame, producing a random one-frame sky flicker, most
visible on the jak3 title screen.

Carry the CLUT payload inline in the double-buffered dma buffer right
after the upload record (qwc 65) so the renderer always sees a
coherent copy. The C++ handler accepts both forms; callers with
frame-stable data (bigmap, blit-displays) keep the address form.

Applies to jak2 and jak3, which share the fog anim code.

(AI-assisted)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

1 participant