Guest User

Untitled

a guest
May 8th, 2026
159
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 16.45 KB | None | 0 0
  1. ● I now have the full picture of both regressions. Let me write the analysis.
  2.  
  3. ---
  4. Regression 1: 5f032cff — "window drag to ~4fps"
  5.  
  6. Commit: SwapChain: Remove unused event
  7.  
  8. What it did
  9.  
  10. 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()).
  11.  
  12. 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
  13. genuinely "unused" from JUCE's perspective.
  14.  
  15. Why it breaks drag
  16.  
  17. The DXGI_SWAP_CHAIN_FLAG_FRAME_LATENCY_WAITABLE_OBJECT flag has two independent roles:
  18.  
  19. 1. Enables GetFrameLatencyWaitableObject() — the handle that SwapChainThread was waiting on. No longer needed. ✓
  20. 2. Makes IDXGISwapChain2::SetMaximumFrameLatency() operate on the per-swap-chain latency rather than the device-global latency.
  21.  
  22. MSDN (emphasis mine): "This method is valid only for swap chains created with the DXGI_SWAP_CHAIN_FLAG_FRAME_LATENCY_WAITABLE_OBJECT flag."
  23.  
  24. Without the flag, the call chain2->SetMaximumFrameLatency(1) silently does nothing. The effective max frame latency reverts to the DXGI global default: 3 frames.
  25.  
  26. With latency = 3 instead of 1, during live window drag:
  27. - WM_PAINT fires rapidly (every few ms, as Windows generates them during move/resize)
  28. - Each WM_PAINT → handlePaintMessage() → handleDirect2DPaint() → Present1(syncInterval=1)
  29. - Present1 can accept frames 1, 2, 3 into its queue without blocking — so the first three Presents return quickly
  30. - Frame 4 blocks until frame 1 clears the DWM compositor (~one vblank per queued frame)
  31. - For 16 instances, the UI thread serialises 16 × ~3 × Present1 stalls per burst → collapses to 4fps
  32.  
  33. 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
  34. GetFrameLatencyWaitableObject() call). Both changes can be separated.
  35.  
  36. ---
  37. Regression 2: bf41bcc9 — "too many repaints, whole UI laggy"
  38.  
  39. Commit: HWNDComponentPeer: Rethink the component painting mechanism
  40.  
  41. The commit chain that led here
  42.  
  43. Understanding this requires the history:
  44.  
  45. ┌──────────────────────┬─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┬─────────────────────────────────────────────────────────────────────────────────────────────┐
  46. │ Commit │ Change │ Side effect │
  47. ├──────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────────┤
  48. │ 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 │
  49. │ baseline) │ │ │
  50. ├──────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────────┤
  51. │ 258203706 │ repaint() → local deferredRepaints; onVBlank() → InvalidateRect; WM_PAINT → immediate handleDirect2DPaint() │ Introduced resize sluggishness: rapid WM_PAINT during drag → rapid Present1(syncInterval=1) │
  52. │ │ │ → each blocks for a vblank → stall │
  53. ├──────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────────┤
  54. │ 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() │
  55. │ │ │ correctly, so the bug persisted for plugins │
  56. ├──────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────────┤
  57. │ bf41bcc9 │ Extend vblank-deferral to ALL windows, not just isSizing(). repaint() → InvalidateRect() immediately; WM_PAINT │ Regression │
  58. │ │ → schedulePaintOnVblank = true; vblank → one paint │ │
  59. └──────────────────────┴─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┴─────────────────────────────────────────────────────────────────────────────────────────────┘
  60.  
  61. Why bf41bcc9 breaks multi-instance performance
  62.  
  63. 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:
  64.  
  65. Old repaint() (all versions before bf41bcc9):
  66. deferredRepaints.add(area); // local RectangleList, no Windows message
  67. 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.
  68.  
  69. New repaint() (bf41bcc9):
  70. auto r = D2DUtilities::toRECT(area);
  71. InvalidateRect(peer.getHWND(), &r, FALSE); // immediate Windows message
  72.  
  73. 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,
  74. input), JUCE's animated screens call repaint() → InvalidateRect. If the pump cycles between these calls, they produce separate WM_PAINT events.
  75.  
  76. For the 16-instance scenario, Editor::Tick() fires at ~20fps per screen. Between vblanks (~16ms), multiple Tick() cycles may run:
  77. - Tick #1: repaint(screenA) + repaint(screenB) → InvalidateRect × 2 on HWND_1
  78. - Message pump processes something → WM_PAINT on HWND_1 → addDeferredRepaint(A ∪ B) + schedulePaintOnVblank = true
  79. - Tick #2 (before vblank): repaint(screenA) + repaint(screenB) → InvalidateRect × 2 again
  80. - Message pump → another WM_PAINT on HWND_1 → addDeferredRepaint(A ∪ B) again — the D2D context now has the same region twice
  81.  
  82. 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()
  83. 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.
  84.  
  85. 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
  86. code bypasses this batching gate.
  87.  
  88. The underlying design tension
  89.  
  90. The commit chain is trying to solve two conflicting goals:
  91. - Goal A: Make WM_PAINT from external sources (DWM compositing, host repaints) trigger a paint — requires going through the WM_PAINT path
  92. - Goal B: Batch JUCE-internal repaint() calls and only present once per vblank — requires NOT going through Windows messages for every repaint()
  93.  
  94. 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
  95. Windows messages, making them subject to pump timing rather than vblank timing.
  96.  
  97. 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
  98. for plugin editors where isSizing() isn't set.
  99.  
  100. 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)
  101. 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
  102. external repaint requests.
  103.  
  104. ---
  105. Summary table for your JUCE forum post
  106.  
  107. ┌───────────────┬──────────┬──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┬────────────────────────────────────────────────────────────────────────────────────┐
  108. │ Regression │ Commit │ Root cause │ Minimal fix │
  109. ├───────────────┼──────────┼──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼────────────────────────────────────────────────────────────────────────────────────┤
  110. │ 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 │
  111. │ │ │ to 3 frames │ │
  112. ├───────────────┼──────────┼──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼────────────────────────────────────────────────────────────────────────────────────┤
  113. │ Too many │ bf41bcc9 │ repaint() → InvalidateRect() immediately bypasses vblank batching; multiple pump cycles between vblanks produce │ Restore repaint() → deferredRepaints.add(); in onVBlank(), flush deferredRepaints │
  114. │ repaints │ │ multiple WM_PAINT events → multiple addDeferredRepaint() calls per vblank │ to D2D context directly rather than via InvalidateRect │
  115. └───────────────┴──────────┴──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┴────────────────────────────────────────────────────────────────────────────────────┘
  116.  
  117. ✻ Cogitated for 11m 50s
  118.  
Advertisement
Add Comment
Please, Sign In to add comment