Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- ● I now have the full picture of both regressions. Let me write the analysis.
- ---
- Regression 1: 5f032cff — "window drag to ~4fps"
- Commit: SwapChain: Remove unused event
- What it did
- Removed DXGI_SWAP_CHAIN_FLAG_FRAME_LATENCY_WAITABLE_OBJECT from both CreateSwapChainForComposition and ResizeBuffers, then removed the waitable event handle (swapChainEvent) and its accessor (getEvent()).
- The removal was logically justified: the SwapChainThread (which used the waitable event to signal vblank-readiness) had been deleted two commits earlier in 22ae5baa42. No code was consuming getEvent() any more, so the event was
- genuinely "unused" from JUCE's perspective.
- Why it breaks drag
- The DXGI_SWAP_CHAIN_FLAG_FRAME_LATENCY_WAITABLE_OBJECT flag has two independent roles:
- 1. Enables GetFrameLatencyWaitableObject() — the handle that SwapChainThread was waiting on. No longer needed. ✓
- 2. Makes IDXGISwapChain2::SetMaximumFrameLatency() operate on the per-swap-chain latency rather than the device-global latency.
- MSDN (emphasis mine): "This method is valid only for swap chains created with the DXGI_SWAP_CHAIN_FLAG_FRAME_LATENCY_WAITABLE_OBJECT flag."
- Without the flag, the call chain2->SetMaximumFrameLatency(1) silently does nothing. The effective max frame latency reverts to the DXGI global default: 3 frames.
- With latency = 3 instead of 1, during live window drag:
- - WM_PAINT fires rapidly (every few ms, as Windows generates them during move/resize)
- - Each WM_PAINT → handlePaintMessage() → handleDirect2DPaint() → Present1(syncInterval=1)
- - Present1 can accept frames 1, 2, 3 into its queue without blocking — so the first three Presents return quickly
- - Frame 4 blocks until frame 1 clears the DWM compositor (~one vblank per queued frame)
- - For 16 instances, the UI thread serialises 16 × ~3 × Present1 stalls per burst → collapses to 4fps
- The fix is straightforward: keep the flag (swapChainDescription.Flags = DXGI_SWAP_CHAIN_FLAG_FRAME_LATENCY_WAITABLE_OBJECT) and the SetMaximumFrameLatency(1) call, and simply stop acquiring the waitable event handle (the
- GetFrameLatencyWaitableObject() call). Both changes can be separated.
- ---
- Regression 2: bf41bcc9 — "too many repaints, whole UI laggy"
- Commit: HWNDComponentPeer: Rethink the component painting mechanism
- The commit chain that led here
- Understanding this requires the history:
- ┌──────────────────────┬─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┬─────────────────────────────────────────────────────────────────────────────────────────────┐
- │ Commit │ Change │ Side effect │
- ├──────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────────┤
- │ cf3dbc8d (8.0.12 │ repaint() → D2D context directly; onVBlank() → handleDirect2DPaint() every tick │ Clean, but external WM_PAINT events (from DWM/Windows exposing windows) not handled │
- │ baseline) │ │ │
- ├──────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────────┤
- │ 258203706 │ repaint() → local deferredRepaints; onVBlank() → InvalidateRect; WM_PAINT → immediate handleDirect2DPaint() │ Introduced resize sluggishness: rapid WM_PAINT during drag → rapid Present1(syncInterval=1) │
- │ │ │ → each blocks for a vblank → stall │
- ├──────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────────┤
- │ 7685a84c81 │ Add isSizing() gate: during resize, batch via vblank; otherwise, WM_PAINT → immediate paint │ Fixed top-level resize. Plugin editor windows (embedded in host) don't set isSizing() │
- │ │ │ correctly, so the bug persisted for plugins │
- ├──────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────────┤
- │ bf41bcc9 │ Extend vblank-deferral to ALL windows, not just isSizing(). repaint() → InvalidateRect() immediately; WM_PAINT │ Regression │
- │ │ → schedulePaintOnVblank = true; vblank → one paint │ │
- └──────────────────────┴─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┴─────────────────────────────────────────────────────────────────────────────────────────────┘
- Why bf41bcc9 breaks multi-instance performance
- The intent was correct — defer all paints to vblank to prevent stalls. But the implementation of repaint() changed from a silent list accumulation to an immediate Windows message:
- Old repaint() (all versions before bf41bcc9):
- deferredRepaints.add(area); // local RectangleList, no Windows message
- onVBlank() called InvalidateRect for the entire list once per vblank, then cleared it. ONE WM_PAINT per HWND per vblank, regardless of how many repaint() calls were made.
- New repaint() (bf41bcc9):
- auto r = D2DUtilities::toRECT(area);
- InvalidateRect(peer.getHWND(), &r, FALSE); // immediate Windows message
- InvalidateRect coalesces at the HWND level — multiple calls within one message pump cycle produce one WM_PAINT. But the message pump runs many times between vblanks. Each time it processes other events (timer callbacks, mouse moves,
- input), JUCE's animated screens call repaint() → InvalidateRect. If the pump cycles between these calls, they produce separate WM_PAINT events.
- For the 16-instance scenario, Editor::Tick() fires at ~20fps per screen. Between vblanks (~16ms), multiple Tick() cycles may run:
- - Tick #1: repaint(screenA) + repaint(screenB) → InvalidateRect × 2 on HWND_1
- - Message pump processes something → WM_PAINT on HWND_1 → addDeferredRepaint(A ∪ B) + schedulePaintOnVblank = true
- - Tick #2 (before vblank): repaint(screenA) + repaint(screenB) → InvalidateRect × 2 again
- - Message pump → another WM_PAINT on HWND_1 → addDeferredRepaint(A ∪ B) again — the D2D context now has the same region twice
- addDeferredRepaint calls RectangleList::add() which appends without guaranteed deduplication. The D2D context's deferredRepaints accumulates across multiple WM_PAINT events within a vblank interval. At vblank, handleDirect2DPaint()
- paints with the inflated rect list — potentially painting the same screen regions N times, where N is the number of message pump cycles between vblanks.
- In old code, the deferredRepaints in the peer was a RectangleList that accumulated all repaint() calls, then was converted to a single InvalidateRect batch at vblank, cleared, and produced exactly ONE WM_PAINT → ONE D2D paint. The new
- code bypasses this batching gate.
- The underlying design tension
- The commit chain is trying to solve two conflicting goals:
- - Goal A: Make WM_PAINT from external sources (DWM compositing, host repaints) trigger a paint — requires going through the WM_PAINT path
- - Goal B: Batch JUCE-internal repaint() calls and only present once per vblank — requires NOT going through Windows messages for every repaint()
- 258203706 achieved Goal A but broke Goal B (resize stall). 7685a84c81 partially fixed Goal B via isSizing(). bf41bcc9 tried to fully generalise Goal B's fix but broke Goal A/B separation: it routes JUCE-internal repaint() through
- Windows messages, making them subject to pump timing rather than vblank timing.
- The version that had both working was 7685a84c81 for top-level windows (external WM_PAINT handled via handlePaintMessage() → immediate D2D; internal repaint() → local list → InvalidateRect at vblank → D2D). The unsolved part was: same
- for plugin editors where isSizing() isn't set.
- A targeted fix for bf41bcc9's stated goal (plugin editor resize) would be: restore repaint() → deferredRepaints.add() (not InvalidateRect), and in onVBlank() flush deferredRepaints directly to the D2D context (not via InvalidateRect)
- before calling handleDirect2DPaint(). This eliminates the WM_PAINT round-trip for internal repaints entirely, which avoids both the stall and the "too many repaints" problem, while still painting at vblank for both internal and
- external repaint requests.
- ---
- Summary table for your JUCE forum post
- ┌───────────────┬──────────┬──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┬────────────────────────────────────────────────────────────────────────────────────┐
- │ Regression │ Commit │ Root cause │ Minimal fix │
- ├───────────────┼──────────┼──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼────────────────────────────────────────────────────────────────────────────────────┤
- │ Drag → 4fps │ 5f032cff │ Removing DXGI_SWAP_CHAIN_FLAG_FRAME_LATENCY_WAITABLE_OBJECT makes SetMaximumFrameLatency(1) a no-op; latency reverts │ Keep the flag; remove only the GetFrameLatencyWaitableObject() call │
- │ │ │ to 3 frames │ │
- ├───────────────┼──────────┼──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼────────────────────────────────────────────────────────────────────────────────────┤
- │ Too many │ bf41bcc9 │ repaint() → InvalidateRect() immediately bypasses vblank batching; multiple pump cycles between vblanks produce │ Restore repaint() → deferredRepaints.add(); in onVBlank(), flush deferredRepaints │
- │ repaints │ │ multiple WM_PAINT events → multiple addDeferredRepaint() calls per vblank │ to D2D context directly rather than via InvalidateRect │
- └───────────────┴──────────┴──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┴────────────────────────────────────────────────────────────────────────────────────┘
- ✻ Cogitated for 11m 50s
Advertisement
Add Comment
Please, Sign In to add comment