pt_cb_present tested the composing context's readiness fence with a zero
timeout and dropped any slot whose commands had not finished, guest
frames included. Nothing was swapped, so the screen held the previous
image while emulation ran on: seconds of 1-10 fps on an 8K display while
every counter reported 60. The presenter now makes its GPU queue wait on
a guest frame's fence (glWaitSync; the CPU does not block). Only optional
temporal frames may still be dropped when unready or late.
present_loop counted a dropped image as a present; it now counts swaps
in presents and drops in a new dropped field (debug JSON
present_thread.dropped).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Opt-in under the render thread. Composed frames go to a ring of offscreen
slots at the window's size (binds of framebuffer 0 on the composing context
land on the current slot); a present thread with a second, shared context on
the same window waits on the slot's fence, copies it 1:1 and swaps. The
compositor's wait moves off rendering; the composing side waits only while
every slot is queued or on screen. Falls back to direct swaps when the shared
context or sync objects are unavailable.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>