FreeRDP
Loading...
Searching...
No Matches
sdl_rail_window.cpp
1
19#include <algorithm>
20#include <string>
21#include <utility>
22
23#include <freerdp/codec/color.h>
24#include <freerdp/log.h>
25#include <freerdp/window.h>
26
27#include "sdl_rail_platform.hpp"
28#include "sdl_rail_window.hpp"
29#include "sdl_rail_window_style.hpp"
30#include "sdl_wayland.hpp"
31#include "sdl_window.hpp"
32#include "sdl_x11.hpp"
33
34#define TAG CLIENT_TAG("sdl.rail.window")
35
36/* Timeout bounding stale anchored frame while awaiting server surface update. */
37static constexpr uint64_t kAwaitServerFrameMs = 1000;
38
39/* Maximum timeout waiting for server restore geometry order. */
40static constexpr uint64_t kAwaitRestoreRectMs = 500;
41
42/* Check for real per-pixel alpha in region. */
43static bool regionHasAlpha(const uint8_t* base, uint32_t stride, const SDL_Rect& r)
44{
45 for (int y = r.y; y < r.y + r.h; y++)
46 {
47 const uint8_t* row = base + static_cast<size_t>(y) * stride;
48 for (int x = r.x; x < r.x + r.w; x++)
49 if (row[static_cast<size_t>(x) * 4 + 3] != 0)
50 return true;
51 }
52 return false;
53}
54
55static bool regionIsBlank(const uint8_t* base, uint32_t stride, const SDL_Rect& r)
56{
57 for (int y = r.y; y < r.y + r.h; y++)
58 {
59 const auto* row = reinterpret_cast<const uint32_t*>(base + static_cast<size_t>(y) * stride);
60 for (int x = r.x; x < r.x + r.w; x++)
61 if (row[x] != 0)
62 return false;
63 }
64 return true;
65}
66
67SdlRailWindow::SdlRailWindow(uint64_t id, const SDL_Rect& rect) : _id(id), _windowRect(rect)
68{
69}
70
71/* Out of line: SdlWindow is only forward-declared in the header. */
72SdlRailWindow::~SdlRailWindow() = default;
73
74uint64_t SdlRailWindow::id() const
75{
76 return _id;
77}
78
79SDL_WindowID SdlRailWindow::sdlId() const
80{
81 return _win ? _win->id() : 0;
82}
83
84SDL_Window* SdlRailWindow::window() const
85{
86 return _win ? _win->window() : nullptr;
87}
88
89SDL_Renderer* SdlRailWindow::renderer() const
90{
91 return _win ? _win->renderer() : nullptr;
92}
93
94void SdlRailWindow::updateWindowRect(const SDL_Rect& rect)
95{
96 std::unique_lock lock(_gfxLock);
97 _awaitRestoreUntil = 0; /* Server geometry received. */
98 if (_localMoveActive)
99 {
100 /* Adopt server size change during local move. */
101 if (!_localMoveIsResize && ((rect.w != _windowRect.w) || (rect.h != _windowRect.h)))
102 {
103 WLog_DBG(TAG, "size adopted mid-move id=0x%08" PRIx32 " %dx%d -> %dx%d at %d,%d",
104 static_cast<uint32_t>(_id), _windowRect.w, _windowRect.h, rect.w, rect.h,
105 rect.x, rect.y);
106 _windowRect.w = rect.w;
107 _windowRect.h = rect.h;
108 /* Record server re-anchor position. */
109 _localMoveServerPos = { rect.x, rect.y };
110 _localMoveSizeChanged = true;
111 _painted = false;
112 }
113 return;
114 }
115 /* Accept post-move geometry echoes. */
116 if (SDL_RectsEqual(&rect, &_windowRect))
117 return;
118 /* A resize recreates the render target, so the content needs a full re-copy. */
119 if ((rect.w != _windowRect.w) || (rect.h != _windowRect.h))
120 _painted = false;
121 _windowRect = rect;
122 /* Mark dirty even if frozen/maximized so pending restore rect applies upon unfreeze. */
123 _geometryDirty = true;
124}
125
126void SdlRailWindow::setLocalMoveActive(bool active)
127{
128 std::unique_lock lock(_gfxLock);
129 _localMoveActive = active;
130 if (active)
131 {
132 _localMoveIsResize = false; /* default to move; setResizeAnchor marks a resize */
133 _localMoveSizeChanged = false;
134 }
135}
136
137void SdlRailWindow::setResizeAnchor(bool right, bool bottom)
138{
139 std::unique_lock lock(_gfxLock);
140 _resizeAnchorRight = right;
141 _resizeAnchorBottom = bottom;
142 _localMoveIsResize = true;
143}
144
145bool SdlRailWindow::localMoveActive() const
146{
147 std::unique_lock lock(_gfxLock);
148 return _localMoveActive;
149}
150
151bool SdlRailWindow::localMoveSizeChanged() const
152{
153 std::unique_lock lock(_gfxLock);
154 return _localMoveSizeChanged;
155}
156
157SDL_Point SdlRailWindow::localMoveServerPos() const
158{
159 std::unique_lock lock(_gfxLock);
160 return _localMoveServerPos;
161}
162
163void SdlRailWindow::adoptLocalGeometry(const SDL_Rect& rect)
164{
165 /* Adopt local geometry so the server's echoing WINDOW_ORDER is a no-op (not a size snap). */
166 std::unique_lock lock(_gfxLock);
167 /* A completed local move settles geometry via the completion path, not the reporter. */
168 _geomApplyPending = false;
169 /* Translate visible offset by move delta. */
170 if (_visOffsetSet)
171 {
172 _visOffset.x += rect.x - _windowRect.x;
173 _visOffset.y += rect.y - _windowRect.y;
174 }
175 _windowRect = rect;
176 _geometryDirty = false;
177 /* A resize drag ends here; the server surface still has the pre-drag size. */
178 _awaitingFrameUntil =
179 (_localMoveActive && _localMoveIsResize) ? (SDL_GetTicks() + kAwaitServerFrameMs) : 0;
180 _localMoveActive = false;
181 _needsFullBlit = true;
182 /* Repaint real content (clear placeholder). */
183 if (_hasGfx)
184 _gfxDamage.assign(1, SDL_Rect{ 0, 0, static_cast<int>(_gfxW), static_cast<int>(_gfxH) });
185}
186
187SDL_Rect SdlRailWindow::windowRect() const
188{
189 std::unique_lock lock(_gfxLock);
190 return _windowRect;
191}
192
193void SdlRailWindow::markDeleted()
194{
195 std::unique_lock lock(_gfxLock);
196 _deleted = true;
197}
198
199bool SdlRailWindow::isDeleted() const
200{
201 std::unique_lock lock(_gfxLock);
202 return _deleted;
203}
204
205bool SdlRailWindow::geomApplyPending() const
206{
207 std::unique_lock lock(_gfxLock);
208 return _geomApplyPending;
209}
210
211void SdlRailWindow::clearGeomApplyPending()
212{
213 std::unique_lock lock(_gfxLock);
214 _geomApplyPending = false;
215}
216
217void SdlRailWindow::armLoopEnd()
218{
219 std::unique_lock lock(_gfxLock);
220 _loopEnd.pending = true;
221}
222
223bool SdlRailWindow::loopEndPending() const
224{
225 std::unique_lock lock(_gfxLock);
226 return _loopEnd.pending;
227}
228
229void SdlRailWindow::deferMaximize()
230{
231 std::unique_lock lock(_gfxLock);
232 _loopEnd.maximize = true;
233}
234
235void SdlRailWindow::deferSnap(const SDL_Rect& serverRect)
236{
237 std::unique_lock lock(_gfxLock);
238 _loopEnd.snap = true;
239 _loopEnd.snapRect = serverRect;
240}
241
242void SdlRailWindow::clearLoopEnd()
243{
244 std::unique_lock lock(_gfxLock);
245 _loopEnd = {};
246}
247
248SdlRailWindow::LoopEndActions SdlRailWindow::takeLoopEnd()
249{
250 std::unique_lock lock(_gfxLock);
251 const LoopEndActions actions{ _loopEnd.maximize, _loopEnd.snap, _loopEnd.snapRect };
252 _loopEnd = {};
253 return actions;
254}
255
256bool SdlRailWindow::takeWmOverride(SDL_Rect& outer)
257{
258 std::unique_lock lock(_gfxLock);
259 if (!_wmRefused)
260 return false;
261 outer = _wmRefusedOuter;
262 _wmRefused = false;
263 return true;
264}
265
266void SdlRailWindow::setVisibilityRects(std::vector<SDL_Rect> rects)
267{
268 std::unique_lock lock(_gfxLock);
269 _visRects = std::move(rects);
270 _visDirty = true;
271}
272
273void SdlRailWindow::setVisibleOffset(SDL_Point offset)
274{
275 std::unique_lock lock(_gfxLock);
276 /* Freeze visible offset during local move. */
277 if (_localMoveActive)
278 return;
279 if (_visOffsetSet && (offset.x == _visOffset.x) && (offset.y == _visOffset.y))
280 return;
281 _visOffset = offset;
282 _visOffsetSet = true;
283 _visDirty = true;
284}
285
286void SdlRailWindow::setMinMaxSize(SDL_Point minSize, SDL_Point maxSize)
287{
288 std::unique_lock lock(_gfxLock);
289 _minSize = { std::max(0, minSize.x), std::max(0, minSize.y) };
290 _maxSize = { std::max(0, maxSize.x), std::max(0, maxSize.y) };
291 _minMaxDirty = true;
292 /* Re-apply geometry on min/max change. */
293 _geometryDirty = true;
294}
295
296void SdlRailWindow::setResizeMargins(int left, int top, int right, int bottom)
297{
298 std::unique_lock lock(_gfxLock);
299 const SDL_Rect m = { left, top, right, bottom };
300 if (SDL_RectsEqual(&m, &_resizeMargins))
301 return;
302 _resizeMargins = m;
303 /* Margins usually arrive after the first frame; the window must regrow to cover them. */
304 if (!railMaximized())
305 _geometryDirty = true;
306}
307
308SDL_Rect SdlRailWindow::resizeMargins() const
309{
310 std::unique_lock lock(_gfxLock);
311 return _resizeMargins;
312}
313
314void SdlRailWindow::setFrameMargins(const SDL_Rect& m)
315{
316 std::unique_lock lock(_gfxLock);
317 _frameMargins = m;
318}
319
320SDL_Rect SdlRailWindow::frameMargins() const
321{
322 std::unique_lock lock(_gfxLock);
323 return _frameMargins;
324}
325
326void SdlRailWindow::setOwner(uint64_t ownerId)
327{
328 std::unique_lock lock(_gfxLock);
329 _ownerId = ownerId;
330}
331
332uint64_t SdlRailWindow::owner() const
333{
334 std::unique_lock lock(_gfxLock);
335 return _ownerId;
336}
337
338bool SdlRailWindow::isPopup() const
339{
340 std::unique_lock lock(_gfxLock);
341 return _isPopup;
342}
343
344bool SdlRailWindow::isFullscreen() const
345{
346 std::unique_lock lock(_gfxLock);
347 return _fullscreen;
348}
349
350bool SdlRailWindow::isLayered() const
351{
352 std::unique_lock lock(_gfxLock);
353 return _layered;
354}
355
356void SdlRailWindow::setShadowAnchored(bool anchored)
357{
358 std::unique_lock lock(_gfxLock);
359 _shadowAnchored = anchored;
360}
361
362void SdlRailWindow::setFrame(bool frame)
363{
364 std::unique_lock lock(_gfxLock);
365 _frame = frame;
366}
367
368bool SdlRailWindow::isFrame() const
369{
370 std::unique_lock lock(_gfxLock);
371 return _frame;
372}
373
374bool SdlRailWindow::styleResizable() const
375{
376 /* Once resizable, keep band eligibility to avoid oscillation. */
377 return _everResizable || ((_style & (WS_THICKFRAME | WS_MAXIMIZEBOX)) != 0);
378}
379
380/* Minimum grabbable width for each outside-band resize edge. */
381static constexpr int kMinGrip = 4;
382
383SDL_Rect SdlRailWindow::bandMargins() const
384{
385 if (!_visible || _isPopup || _layered || !styleResizable() || effectivelyMaximized())
386 return { 0, 0, 0, 0 };
387 /* Enforce minimum grip margins for resizable windows. */
388 const SDL_Rect& m = _resizeMargins;
389 return { std::max(m.x, kMinGrip), std::max(m.y, kMinGrip), std::max(m.w, kMinGrip),
390 std::max(m.h, kMinGrip) };
391}
392
393SDL_Rect SdlRailWindow::bandInsets() const
394{
395 /* X11-only transparent resize ring. */
396 const auto& caps = railPlatformCaps();
397 if (!caps.positionsReadable || !caps.supportsTransparentWindows)
398 return { 0, 0, 0, 0 };
399 return bandMargins();
400}
401
402/* Add edge insets (x=L y=T w=R h=B) to a server rect to get its outer (band-inclusive) rect. */
403static SDL_Rect addInsets(const SDL_Rect& r, const SDL_Rect& i)
404{
405 return { r.x - i.x, r.y - i.y, r.w + i.x + i.w, r.h + i.y + i.h };
406}
407
408/* Inverse of addInsets: strip the insets back off an outer rect to recover the server rect.
409 */
410static SDL_Rect stripInsets(const SDL_Rect& r, const SDL_Rect& i)
411{
412 return { r.x + i.x, r.y + i.y, r.w - i.x - i.w, r.h - i.y - i.h };
413}
414
415/* Caller holds _gfxLock. */
416bool SdlRailWindow::isFullDisplaySize() const
417{
418 SDL_Rect disp{};
419 return SDL_GetDisplayBounds(SDL_GetPrimaryDisplay(), &disp) && (_windowRect.w >= disp.w) &&
420 (_windowRect.h >= disp.h);
421}
422
423/* Server rect inflated by the FRESH band insets (the target the window should become); caller
424 * holds _gfxLock. Used by reconcile/create to size the window. */
425SDL_Rect SdlRailWindow::targetOuterRect() const
426{
427 return addInsets(_windowRect, bandInsets());
428}
429
430/* The insets baked into the window ON SCREEN (not the freshly recomputed target). */
431SDL_Rect SdlRailWindow::insets() const
432{
433 std::unique_lock lock(_gfxLock);
434 return _appliedInsets;
435}
436
437/* On-screen outer geometry: _windowRect inflated by the applied insets. */
438SDL_Rect SdlRailWindow::outerRect() const
439{
440 std::unique_lock lock(_gfxLock);
441 return addInsets(_windowRect, _appliedInsets);
442}
443
444SDL_Rect SdlRailWindow::serverRect(const SDL_Rect& outer) const
445{
446 return stripInsets(outer, insets());
447}
448
449/* Surface blit offset within the local window (content anchor when maximized). */
450SDL_Point SdlRailWindow::blitOffset() const
451{
452 if (!effectivelyMaximized())
453 return { _appliedInsets.x, _appliedInsets.y };
454 /* Use frame margins as origin only when surface matches frame-inclusive geometry. */
455 const bool frameInSurface =
456 ((_frameMargins.x > 0) || (_frameMargins.y > 0)) &&
457 (static_cast<int>(_gfxW) == _windowRect.w + _frameMargins.x + _frameMargins.w) &&
458 (static_cast<int>(_gfxH) == _windowRect.h + _frameMargins.y + _frameMargins.h);
459 SDL_Point content = { 0, 0 };
460 if (frameInSurface)
461 content = { _frameMargins.x, _frameMargins.y };
462 else if (_visOffsetSet)
463 content = { _visOffset.x - _windowRect.x, _visOffset.y - _windowRect.y };
464 return { -std::clamp(content.x, 0, static_cast<int>(_gfxW)),
465 -std::clamp(content.y, 0, static_cast<int>(_gfxH)) };
466}
467
468SDL_Point SdlRailWindow::serverOrigin() const
469{
470 std::unique_lock lock(_gfxLock);
471 /* Derive server coordinate from blit offset to match on-screen pixels. */
472 const SDL_Point dst = blitOffset();
473 return { _windowRect.x - dst.x, _windowRect.y - dst.y };
474}
475
476void SdlRailWindow::setStyle(uint32_t style, uint32_t exStyle)
477{
478 std::unique_lock lock(_gfxLock);
479 const bool wasResizable = styleResizable();
480 const bool wasTopmost = _topmost;
481 _style = style;
482 _exStyle = exStyle;
483 _topmost = (exStyle & WS_EX_TOPMOST) != 0;
484 if (_topmost != wasTopmost)
485 _topmostDirty = true;
486 if ((style & (WS_THICKFRAME | WS_MAXIMIZEBOX)) != 0)
487 _everResizable = true;
488 /* Update resizability on style change. */
489 if (styleResizable() != wasResizable)
490 _styleDirty = true;
491 /* Classify popup and layered window types once on creation. */
492 if (!_popupClassified)
493 {
494 const auto classification = classifySdlRailWindowStyle(style, exStyle, _ownerId != 0);
495 _isPopup = classification.popup;
496 _layered = classification.layered;
497 _clickThrough = classification.clickThrough;
498 _overlay = classification.overlay;
499 _popupClassified = true;
500 }
501}
502
503void SdlRailWindow::setTitle(const std::string& title)
504{
505 std::unique_lock lock(_gfxLock);
506 _title = title;
507 _titleDirty = true;
508}
509
510void SdlRailWindow::setVisible(bool visible)
511{
512 std::unique_lock lock(_gfxLock);
513 _visible = visible;
514}
515
516void SdlRailWindow::setIcon(const SdlRailIcon& icon)
517{
518 std::unique_lock lock(_gfxLock);
519 _icon = icon;
520 _iconDirty = true;
521}
522/* Caller holds _gfxLock. */
523bool SdlRailWindow::create(SDL_Window* parent, const SDL_Rect& parentRect)
524{
525 if (_win)
526 return true;
527
528 const RailPlatformCaps& caps = railPlatformCaps();
529
530 /* Local window = server rect + band insets (the outside resize band is part of our window). */
531 const SDL_Rect vis = targetOuterRect();
532 _appliedInsets = bandInsets(); /* the insets baked into the window we are about to create */
533 /* Realize full-display popups as fullscreen toplevels on Wayland. */
534 const bool fullscreen = _isPopup && !caps.positionsReadable && isFullDisplaySize();
535 if (_isPopup && parent && !fullscreen)
536 {
537 /* SDL popups position parent-relative (works on Wayland too, via xdg_popup). */
538 const SDL_Rect rel = { vis.x - parentRect.x, vis.y - parentRect.y, vis.w, vis.h };
539 _win = std::make_unique<SdlWindow>(
540 SdlWindow::createPopup(parent, rel, caps.supportsTransparentWindows, _overlay));
541 }
542 else if (_isPopup && !caps.positionsReadable && !fullscreen)
543 {
544 /* No owner yet: a Wayland popup needs a parent; retry once an app window exists. */
545 WLog_VRB(TAG, "popup create deferred id=0x%08" PRIx32 ": no parent yet",
546 static_cast<uint32_t>(_id));
547 return false;
548 }
549 else
550 {
551 /* Transparent so the band ring outside the content stays invisible. */
552 Uint32 flags = SDL_WINDOW_BORDERLESS | SDL_WINDOW_HIDDEN;
553 if (caps.supportsTransparentWindows)
554 flags |= SDL_WINDOW_TRANSPARENT;
555 _win = std::make_unique<SdlWindow>(
556 SdlWindow::create(SDL_GetPrimaryDisplay(), _title, flags, vis));
557 }
558 if (!_win || !_win->window() || !_win->renderer())
559 {
560 _win.reset();
561 WLog_WARN(TAG, "create failed id=0x%08" PRIx32 " %s", static_cast<uint32_t>(_id), role());
562 return false;
563 }
564 _win->resizeable(styleResizable());
565 if (fullscreen)
566 {
567 /* Cover the whole output (panel included) - the Wayland-correct way to reach 0,0. */
568 _fullscreen = true;
569 SDL_SetWindowFullscreen(_win->window(), true);
570 WLog_DBG(TAG, "fullscreen id=0x%08" PRIx32 " %dx%d (WS_POPUP spans display)",
571 static_cast<uint32_t>(_id), _windowRect.w, _windowRect.h);
572 }
573
574 /* Bind seat/pointer on Wayland. */
575 if (!_isPopup && !caps.positionsReadable)
576 sdl_wayland_move_prepare(_win->window());
577
578 WLog_DBG(TAG,
579 "create id=0x%08" PRIx32 " sdl=%" PRIu32 " %s vis=%dx%d+%d+%d margins=L%d,T%d,R%d,B%d "
580 "transparent=%d",
581 static_cast<uint32_t>(_id), static_cast<uint32_t>(_win->id()), role(), vis.w, vis.h,
582 vis.x, vis.y, _resizeMargins.x, _resizeMargins.y, _resizeMargins.w, _resizeMargins.h,
583 caps.supportsTransparentWindows ? 1 : 0);
584 return true;
585}
586
587bool SdlRailWindow::reconcile(SDL_Window* parent, const SDL_Rect& parentRect)
588{
589 std::unique_lock lock(_gfxLock);
590
591 /* Hidden windows get no local SDL window. Layered windows (e.g. docks) need GFX content and
592 * vis-rects to show; this hides empty DWM snap overlays. */
593 const bool popupReady = !_isPopup || _hasGfx;
594 /* Suppress server shadow frames and unparented full-display overlays. */
595 const bool unplaceable =
596 _frame || (_overlay && !railPlatformCaps().positionsReadable && isFullDisplaySize());
597 /* Layered window is shown only when anchored to an adjoining visible popup or overlay. */
598 const bool drawable =
599 _visible && (_windowRect.w > 0) && (_windowRect.h > 0) && popupReady && !unplaceable &&
600 (!_layered || (_hasGfx && !_visRects.empty() && (_shadowAnchored || _overlay)));
601
602 if (!drawable)
603 {
604 /* Log standalone layered windows suppressed by popup shadow heuristic. */
605 if (_layered && _hasGfx && !_visRects.empty() && !_shadowAnchored && !_overlay &&
606 !_shadowSuppressLogged)
607 {
608 _shadowSuppressLogged = true;
609 WLog_DBG(TAG,
610 "shadow-rule suppressed id=0x%08" PRIx32 " %dx%d style=0x%08" PRIx32
611 " ex=0x%08" PRIx32,
612 static_cast<uint32_t>(_id), _windowRect.w, _windowRect.h, _style, _exStyle);
613 }
614 if (_win)
615 {
616 if ((SDL_GetWindowFlags(_win->window()) & SDL_WINDOW_HIDDEN) == 0)
617 {
618 WLog_DBG(TAG, "hide id=0x%08" PRIx32 " %s", static_cast<uint32_t>(_id), role());
619 SDL_HideWindow(_win->window());
620 }
621 _mapped = false;
622 }
623 if (_isPopup && !_visible)
624 {
625 _gfxPresented = false;
626 _hasGfx = false;
627 }
628 return false;
629 }
630
631 if (!_win)
632 {
633 if (!create(parent, parentRect))
634 return false;
635 _geometryDirty = false;
636 /* Shown + raised in paint() after the first frame (created hidden). */
637 }
638
639 /* create() returned false on failure, so _win is non-null from here down. */
640 if (_topmostDirty)
641 {
642 _topmostDirty = false;
643 /* X11 only; Wayland has no protocol for this, the call still returns true. */
644 if (!_isPopup && !_layered)
645 std::ignore = SDL_SetWindowAlwaysOnTop(_win->window(), _topmost);
646 }
647
648 /* Resizability first: SDL refuses to maximize a non-resizable window. */
649 if (_styleDirty)
650 {
651 _win->resizeable(styleResizable());
652 _styleDirty = false;
653 /* Insets refresh on geometry updates to avoid flicker. */
654 }
655
656 /* State first: maximized/minimized gates the geometry apply below. */
657 /* Hold geometry sync until server sends restored rect to prevent re-maximizing. */
658 if (applyServerState(_maxState, "maximize", SDL_MaximizeWindow))
659 _awaitRestoreUntil = SDL_GetTicks() + kAwaitRestoreRectMs;
660 std::ignore = applyServerState(_minState, "minimize", SDL_MinimizeWindow);
661 const bool restorePending = restoreRectPending();
662
663 /* Update insets across maximize transitions outside active drag grab. */
664 const bool maxed = effectivelyMaximized();
665 if ((maxed != _wasMaximized) && !_localMoveActive && !restorePending)
666 {
667 _appliedInsets = bandInsets();
668 if (!maxed)
669 _geometryDirty = true;
670 _wasMaximized = maxed;
671 }
672
673 /* _GTK_FRAME_EXTENTS: tell the WM the band ring is frame, not content (snap/tile geometry). */
674 const SDL_Rect ext = bandInsets();
675 if (!SDL_RectsEqual(&ext, &_extentsApplied) &&
676 sdl_x11_set_frame_extents(_win->window(), ext.x, ext.w, ext.y, ext.h))
677 _extentsApplied = ext;
678
679 /* Clamp min size hints to target bounds so stale hints do not block programmatic resize.
680 * Defer size hints and programmatic resize during active WM move/resize grab. */
681 const bool wmOwnsGeometry =
682 _localMoveActive || geometryFrozen() || _fullscreen || restorePending;
683 if ((_minMaxDirty || _geometryDirty) && !wmOwnsGeometry)
684 {
685 const SDL_Rect vis = targetOuterRect();
686 SDL_SetWindowMinimumSize(_win->window(), std::clamp(_minSize.x, 0, vis.w),
687 std::clamp(_minSize.y, 0, vis.h));
688 int maxW = (_maxSize.x > 0) ? std::max(1, _maxSize.x) : 0;
689 int maxH = (_maxSize.y > 0) ? std::max(1, _maxSize.y) : 0;
690 /* Wayland: cap to the usable area - an oversized window cannot be dragged into reach. */
691 SDL_Rect usable{};
692 if (!railPlatformCaps().positionsReadable &&
693 SDL_GetDisplayUsableBounds(SDL_GetPrimaryDisplay(), &usable))
694 {
695 /* The local window is the outer frame: cap to the usable area plus the insets. */
696 const SDL_Rect bi = bandInsets();
697 const int capW = usable.w + bi.x + bi.w;
698 const int capH = usable.h + bi.y + bi.h;
699 if ((maxW == 0) || (maxW > capW))
700 maxW = capW;
701 if ((maxH == 0) || (maxH > capH))
702 maxH = capH;
703 }
704 SDL_SetWindowMaximumSize(_win->window(), maxW, maxH);
705 _minMaxDirty = false;
706 }
707
708 /* Skip geometry apply when maximized, minimized or WM-dragged. */
709 if (_geometryDirty && !wmOwnsGeometry)
710 {
711 const SDL_Rect vis = targetOuterRect();
712 /* The window is (or becomes) vis = _windowRect + fresh insets: record those as the insets
713 * now baked in, so the round-trip back to server coords strips exactly this. */
714 _appliedInsets = bandInsets();
715 int cw = 0;
716 int ch = 0;
717 SDL_GetWindowSize(_win->window(), &cw, &ch);
718 bool applied = false;
719 /* Apply position and size if changed. */
720 if (_isPopup && parent)
721 {
722 if (!SDL_SetWindowPosition(_win->window(), vis.x - parentRect.x, vis.y - parentRect.y))
723 WLog_VRB(TAG, "popup reposition unsupported id=0x%08" PRIx32 ": %s",
724 static_cast<uint32_t>(_id), SDL_GetError());
725 else if (!railPlatformCaps().positionsReadable)
726 _needsFullBlit = true; /* Wayland applies move on next surface commit */
727 applied = true;
728 }
729 else if (railPlatformCaps().positionsReadable &&
730 !(_isPopup && SDL_GetWindowParent(_win->window())))
731 {
732 int cx = 0;
733 int cy = 0;
734 SDL_GetWindowPosition(_win->window(), &cx, &cy);
735 if ((cx != vis.x) || (cy != vis.y))
736 {
737 SDL_SetWindowPosition(_win->window(), vis.x, vis.y);
738 applied = true;
739 }
740 }
741 if ((cw != vis.w) || (ch != vis.h))
742 {
743 std::ignore = _win->resize({ vis.w, vis.h });
744 applied = true;
745 /* Settle window resize via SDL_SyncWindow before reading back geometry to avoid
746 * adopting transient dimensions while configure events are in flight. */
747 if (railPlatformCaps().positionsReadable && !_isPopup && !_localMoveActive &&
748 !_loopEnd.pending)
749 {
750 const bool settled = SDL_SyncWindow(_win->window());
751 int aw = 0;
752 int ah = 0;
753 SDL_GetWindowSize(_win->window(), &aw, &ah);
754 if (settled && ((aw != vis.w) || (ah != vis.h)))
755 {
756 /* Adopt WM size while preserving server origin. */
757 _wmRefusedOuter = { vis.x, vis.y, aw, ah };
758 _wmRefused = true;
759 applied = false; /* Geometry refused by WM; do not await echo. */
760 }
761 }
762 }
763 _geometryDirty = false;
764 /* Echo filtering is only tracked for top-level app windows. */
765 _geomApplyPending = applied && !_isPopup;
766 }
767 /* Make dialog transient for owner. */
768 if (!_isPopup && parent && !_parentApplied)
769 {
770 if (SDL_SetWindowParent(_win->window(), parent))
771 _parentApplied = true;
772 }
773 if (_titleDirty)
774 {
775 if (!_isPopup)
776 SDL_SetWindowTitle(_win->window(), _title.c_str());
777 _titleDirty = false;
778 }
779 if (_iconDirty)
780 {
781 if (!_isPopup && !_icon.bgra.empty())
782 {
783 SDL_Surface* s = SDL_CreateSurfaceFrom(
784 static_cast<int>(_icon.w), static_cast<int>(_icon.h), SDL_PIXELFORMAT_BGRA32,
785 _icon.bgra.data(), static_cast<int>(_icon.w * 4));
786 if (s)
787 {
788 if (!SDL_SetWindowIcon(_win->window(), s))
789 WLog_WARN(TAG, "SDL_SetWindowIcon failed for window 0x%08" PRIx32 ": %s",
790 static_cast<uint32_t>(_id), SDL_GetError());
791 SDL_DestroySurface(s);
792 }
793 }
794 _iconDirty = false;
795 }
796
797 /* Defer show until first frame; this runs every reconcile, so skip the call once visible. */
798 if (!_minState.rail && _gfxPresented &&
799 (SDL_GetWindowFlags(_win->window()) & SDL_WINDOW_HIDDEN))
800 {
801 /* If transient-for owner was closed or hidden, show dialog standalone. */
802 SDL_Window* owner = _isPopup ? nullptr : SDL_GetWindowParent(_win->window());
803 if (owner && ((SDL_GetWindowFlags(owner) & SDL_WINDOW_HIDDEN) != 0))
804 std::ignore = SDL_SetWindowParent(_win->window(), nullptr);
805 SDL_ShowWindow(_win->window());
806 }
807 return true;
808}
809
810bool SdlRailWindow::takeSurfaceChange(uint32_t surfaceId)
811{
812 std::unique_lock lock(_gfxLock);
813 if (_surfaceId == surfaceId)
814 return false;
815 _surfaceId = surfaceId;
816 return true;
817}
818
819void SdlRailWindow::updateGfxSurface(const void* data, uint32_t stride, uint32_t width,
820 uint32_t height, const RECTANGLE_16* damage, uint32_t nbDamage,
821 WINPR_ATTR_UNUSED uint32_t format)
822{
823 std::unique_lock lock(_gfxLock);
824 /* The copies below are byte-for-byte; gdi_CreateSurface yields nothing else. */
825 WINPR_ASSERT((format == PIXEL_FORMAT_BGRA32) || (format == PIXEL_FORMAT_BGRX32));
826 /* Deep-copy GDI pixels to prevent UAF. */
827 const size_t bytes = static_cast<size_t>(stride) * height;
828 if (!data || (bytes == 0) || (width == 0) || (height == 0))
829 {
830 if (_hasGfx)
831 WLog_DBG(TAG, "gfx cleared id=0x%08" PRIx32 " (surface unmapped)",
832 static_cast<uint32_t>(_id));
833 _gfxBuffer.clear();
834 _hasGfx = false;
835 _gfxDamage.clear();
836 _gfxStride = stride;
837 _gfxW = width;
838 _gfxH = height;
839 _gfxVisOrigin = { 0, 0 };
840 return;
841 }
842
843 const auto* src = static_cast<const uint8_t*>(data);
844 /* Geometry/stride change, first frame, or no damage rects: full copy + repaint. */
845 const bool full = !_hasGfx || (_gfxBuffer.size() != bytes) || (_gfxStride != stride) ||
846 (_gfxW != width) || (_gfxH != height) || (nbDamage == 0);
847 if (full)
848 {
849 const SDL_Rect bounds{ 0, 0, static_cast<int>(width), static_cast<int>(height) };
850 /* Check _hasGfx first to avoid redundant full-surface blank checks once content exists. */
851 if (!_hasGfx && regionIsBlank(src, stride, bounds))
852 return;
853 _gfxBuffer.assign(src, src + bytes);
854 _gfxDamage.assign(1, SDL_Rect{ 0, 0, static_cast<int>(width), static_cast<int>(height) });
855 _needsFullBlit = true;
856 /* Blend real alpha, force opaque otherwise. */
857 _gfxHasAlpha = honorsAlpha() &&
858 regionHasAlpha(_gfxBuffer.data(), stride,
859 { 0, 0, static_cast<int>(width), static_cast<int>(height) });
860 }
861 else
862 {
863 const SDL_Rect bounds{ 0, 0, static_cast<int>(width), static_cast<int>(height) };
864 for (uint32_t i = 0; i < nbDamage; i++)
865 {
866 const SDL_Rect r{ damage[i].left, damage[i].top, damage[i].right - damage[i].left,
867 damage[i].bottom - damage[i].top };
868 SDL_Rect clip{};
869 if (!SDL_GetRectIntersection(&r, &bounds, &clip))
870 continue;
871 std::ignore = freerdp_image_copy_no_overlap(
872 _gfxBuffer.data(), PIXEL_FORMAT_BGRA32, stride, static_cast<UINT32>(clip.x),
873 static_cast<UINT32>(clip.y), static_cast<UINT32>(clip.w),
874 static_cast<UINT32>(clip.h), src, PIXEL_FORMAT_BGRA32, stride,
875 static_cast<UINT32>(clip.x), static_cast<UINT32>(clip.y), nullptr,
876 FREERDP_FLIP_NONE);
877 _gfxDamage.push_back(clip);
878 /* Alpha may first appear in a later incremental frame (e.g. a menu fading in). */
879 if (honorsAlpha() && !_gfxHasAlpha && regionHasAlpha(_gfxBuffer.data(), stride, clip))
880 _gfxHasAlpha = true;
881 }
882 /* A hidden window accumulates rects without ever painting; collapse to one full repaint. */
883 if (_gfxDamage.size() > 32)
884 _gfxDamage.assign(1,
885 SDL_Rect{ 0, 0, static_cast<int>(width), static_cast<int>(height) });
886 }
887 if ((_gfxW != width) || (_gfxH != height))
888 {
889 _resizeAnchorRight = false;
890 _resizeAnchorBottom = false;
891 _needsFullBlit = true;
892 /* Invalidate surface origin until visibility rect matching new dimensions arrives. */
893 _gfxVisOrigin = { 0, 0 };
894 }
895 _hasGfx = true;
896 _gfxStride = stride;
897 _gfxW = width;
898 _gfxH = height;
899 WLog_VRB(TAG, "gfx id=0x%08" PRIx32 " %ux%u full=%d nDamage=%u", static_cast<uint32_t>(_id),
900 width, height, full ? 1 : 0, nbDamage);
901}
902
903void SdlRailWindow::invalidateAll()
904{
905 std::unique_lock lock(_gfxLock);
906 _needsFullBlit = true;
907 if (_hasGfx)
908 _gfxDamage.assign(1, SDL_Rect{ 0, 0, static_cast<int>(_gfxW), static_cast<int>(_gfxH) });
909}
910
911void SdlRailWindow::setServerState(StateSync& s, bool m)
912{
913 std::unique_lock lock(_gfxLock);
914 if (m != s.server)
915 {
916 s.server = m;
917 s.dirty = true;
918 }
919}
920
921void SdlRailWindow::setServerMaximized(bool m)
922{
923 setServerState(_maxState, m);
924}
925
926void SdlRailWindow::setServerMinimized(bool m)
927{
928 setServerState(_minState, m);
929}
930
931bool SdlRailWindow::applyServerState(StateSync& s, const char* what, bool (*enter)(SDL_Window*))
932{
933 if (!s.dirty)
934 return false;
935 bool restored = false;
936 if (s.server && !s.rail)
937 {
938 /* Apply server maximize/minimize state. */
939 s.rail = true;
940 WLog_DBG(TAG, "%s id=0x%08" PRIx32 "", what, static_cast<uint32_t>(_id));
941 enter(_win->window());
942 }
943 else if (!s.server && s.rail)
944 {
945 restored = true;
946 s.rail = false;
947 WLog_DBG(TAG, "restore id=0x%08" PRIx32 " (%s)", static_cast<uint32_t>(_id), what);
948 SDL_RestoreWindow(_win->window());
949 }
950 else
951 {
952 s.dirty = false;
953 return false;
954 }
955 /* Settle WM state before reading window properties. */
956 (void)SDL_SyncWindow(_win->window());
957 s.dirty = false;
958 return restored;
959}
960
961/* Caller holds _gfxLock. */
962bool SdlRailWindow::restoreRectPending() const
963{
964 return (_awaitRestoreUntil != 0) && (SDL_GetTicks() < _awaitRestoreUntil);
965}
966
967bool SdlRailWindow::effectivelyMaximized() const
968{
969 return _maxState.rail ||
970 (_win && (SDL_GetWindowFlags(_win->window()) & SDL_WINDOW_MAXIMIZED) != 0);
971}
972
973bool SdlRailWindow::paint(SDL_Surface* primary, SDL_PixelFormat fallbackFormat,
974 const std::vector<SDL_Rect>& damage, SDL_Window* parent,
975 const SDL_Rect& parentRect)
976{
977 if (!reconcile(parent, parentRect))
978 return false;
979
980 bool ok = false;
981 {
982 std::unique_lock lock(_gfxLock);
983 if (_hasGfx)
984 {
985 ok = paintGfx(fallbackFormat);
986 }
987 else
988 {
989 lock.unlock();
990 ok = paintLegacy(primary, damage);
991 }
992 }
993
994 /* Map in the same pass as the first frame, else the window defers a frame (menu lag). */
995 if (_gfxPresented && !_mapped && !_minState.rail)
996 {
997 _mapped = true;
998 WLog_DBG(TAG, "map id=0x%08" PRIx32 " %s", static_cast<uint32_t>(_id), role());
999 SDL_ShowWindow(_win->window());
1000 /* Reassert target position after show to override stale events. */
1001 if (_isPopup && parent)
1002 {
1003 std::unique_lock lock(_gfxLock);
1004 const SDL_Rect vis = targetOuterRect();
1005 SDL_SetWindowPosition(_win->window(), vis.x - parentRect.x, vis.y - parentRect.y);
1006 if (!railPlatformCaps().positionsReadable)
1007 _needsFullBlit = true; /* Wayland applies move on next surface commit */
1008 }
1009 if ((!_isPopup && !_layered) || _fullscreen)
1010 _win->raise(); /* Bring app and fullscreen windows to front. */
1011 }
1012 return ok;
1013}
1014
1015/* Caller holds _gfxLock. Blits the window-mapped GFX surface via the shared SdlWindow path. */
1016bool SdlRailWindow::paintGfx(SDL_PixelFormat format)
1017{
1018 /* Blit using applied insets to preserve resize borders. */
1019 const SDL_Rect bi = _appliedInsets;
1020 int ww = 0;
1021 int wh = 0;
1022 SDL_GetWindowSizeInPixels(_win->window(), &ww, &wh);
1023 const int cw = ww - bi.x - bi.w; /* content area inside the insets */
1024 const int ch = wh - bi.y - bi.h;
1025 const int gw =
1026 static_cast<int>(_gfxW); /* GFX surface size as int (used across the blit paths) */
1027 const int gh = static_cast<int>(_gfxH);
1028 /* Only adopt visibility origin rects that match current surface dimensions. */
1029 if ((_visRects.size() == 1) && (_visRects.at(0).w == gw) && (_visRects.at(0).h == gh))
1030 _gfxVisOrigin = { _visRects.at(0).x, _visRects.at(0).y };
1031 /* Server surface caught up with the content area: the drag is fully settled. */
1032 if (!_localMoveActive && (cw == gw) && (ch == gh))
1033 {
1034 _resizeAnchorRight = false;
1035 _resizeAnchorBottom = false;
1036 _awaitingFrameUntil = 0;
1037 }
1038 /* Timeout safety net for server resize frame anchoring. */
1039 if ((_awaitingFrameUntil != 0) && (SDL_GetTicks() > _awaitingFrameUntil))
1040 {
1041 WLog_DBG(TAG, "resize frame timeout id=0x%08" PRIx32 " win=%dx%d gfx=%dx%d",
1042 static_cast<uint32_t>(_id), cw, ch, gw, gh);
1043 _awaitingFrameUntil = 0;
1044 /* Adopt server surface dimensions if resize was clamped. */
1045 if ((cw != gw) || (ch != gh))
1046 {
1047 if (_resizeAnchorRight)
1048 _windowRect.x += _windowRect.w - gw;
1049 if (_resizeAnchorBottom)
1050 _windowRect.y += _windowRect.h - gh;
1051 _windowRect.w = gw;
1052 _windowRect.h = gh;
1053 _resizeAnchorRight = false;
1054 _resizeAnchorBottom = false;
1055 _geometryDirty = true;
1056 _needsFullBlit = true;
1057 }
1058 }
1059
1060 /* Keep stale frame anchored while awaiting server GFX surface. */
1061 const bool awaitingServerResize = (_awaitingFrameUntil != 0) && ((cw != gw) || (ch != gh));
1062 const bool localResize = awaitingServerResize ||
1063 (_localMoveActive && (_localMoveIsResize || (cw != gw) || (ch != gh)));
1064
1065 /* Window or surface size changed outside a drag: repaint once even without damage. */
1066 const bool winResized = (ww != _lastWinW) || (wh != _lastWinH);
1067 const bool gfxResized = (gw != _lastGfxW) || (gh != _lastGfxH);
1068 const bool serverResize =
1069 !_localMoveActive && !awaitingServerResize && (winResized || gfxResized || _needsFullBlit);
1070
1071 /* Skip undamaged frames. */
1072 if (!localResize && !serverResize && _gfxDamage.empty() && !(_layered && _visDirty))
1073 {
1074 WLog_VRB(TAG, "paintGfx skip id=0x%08" PRIx32 " no-damage no-resize",
1075 static_cast<uint32_t>(_id));
1076 return true;
1077 }
1078
1079 /* Gate popup mapping until initial content is presented to prevent black flash. */
1080 if (_isPopup && !_gfxPresented && (_gfxW > 0) && (_gfxH > 0))
1081 {
1082 const uint32_t first = *reinterpret_cast<const uint32_t*>(_gfxBuffer.data());
1083 /* BGRA32 as a host word: alpha on top, so mask it off and test the colour alone. */
1084 bool uniform = (first & 0x00FFFFFFu) == 0;
1085 for (uint32_t y = 0; uniform && (y < _gfxH); y++)
1086 {
1087 const auto* row = reinterpret_cast<const uint32_t*>(
1088 _gfxBuffer.data() + static_cast<size_t>(y) * _gfxStride);
1089 for (uint32_t x = 0; x < _gfxW; x++)
1090 if (row[x] != first)
1091 {
1092 uniform = false;
1093 break;
1094 }
1095 }
1096 if (uniform)
1097 {
1098 /* Disarm repaint triggers while popup surface remains blank. */
1099 _gfxDamage.clear();
1100 _needsFullBlit = false;
1101 _lastWinW = ww;
1102 _lastWinH = wh;
1103 _lastGfxW = gw;
1104 _lastGfxH = gh;
1105 return true;
1106 }
1107 }
1108
1109 /* RAIL content is mostly opaque (ignore alpha). Exception: layered app windows. */
1110 SDL_PixelFormat contentFormat = format;
1111 if (format == SDL_PIXELFORMAT_BGRA32)
1112 {
1113 const bool blend = railPlatformCaps().supportsTransparentWindows;
1114 if (!blend || (_isPopup && !_layered && !_gfxHasAlpha))
1115 contentFormat = SDL_PIXELFORMAT_BGRX32;
1116 }
1117
1118 SDL_Surface* s = SDL_CreateSurfaceFrom(gw, gh, contentFormat, _gfxBuffer.data(),
1119 static_cast<int>(_gfxStride));
1120 if (!s)
1121 {
1122 WLog_WARN(TAG, "paintGfx id=0x%08" PRIx32 " SDL_CreateSurfaceFrom failed: %s",
1123 static_cast<uint32_t>(_id), SDL_GetError());
1124 return false;
1125 }
1126
1127 /* Content blits at the inset offset; the ring outside it is the transparent resize band. */
1128 if (localResize)
1129 {
1130 /* Crop frame rows during mid-drag restore until new surface arrives. */
1131 const SDL_Point crop = { std::clamp(_gfxVisOrigin.x, 0, gw),
1132 std::clamp(_gfxVisOrigin.y, 0, gh) };
1133 /* Anchor the stale frame to the fixed corner. */
1134 const SDL_Point off = { (_resizeAnchorRight ? (ww - bi.w - gw) : bi.x) - crop.x,
1135 (_resizeAnchorBottom ? (wh - bi.h - gh) : bi.y) - crop.y };
1136 /* Show dashes only during active drag; on release, keep the clean anchored frame. */
1137 const bool showDashes = _localMoveActive && _localMoveIsResize;
1138 /* Only fill revealed area during resize, not move drag. */
1139 const bool fillRevealed = showDashes || awaitingServerResize;
1140 std::ignore =
1141 _win->paintResizeFrame(s, off, !_gfxDamage.empty(), bi, fillRevealed, showDashes);
1142 }
1143 else
1144 {
1145 /* Render accumulated damage or re-blit full surface on bare resize. */
1146 const SDL_Rect full = { 0, 0, gw, gh };
1147 const SDL_Point dst = blitOffset();
1148 const bool maxed = effectivelyMaximized();
1149 if (_layered && !_visRects.empty() && !maxed)
1150 {
1151 /* Layered shadow cutout: draw only inside visibility rects. */
1152 const bool logClip = _visDirty;
1153 /* Consume full-blit request on resize. */
1154 if (_visDirty || serverResize)
1155 {
1156 /* Wipe so newly-excluded regions don't keep stale pixels. */
1157 std::ignore = _win->fill(static_cast<Uint8>(0), 0, 0, 0);
1158 _gfxDamage.assign(1, full);
1159 _visDirty = false;
1160 _needsFullBlit = false;
1161 }
1162 const SDL_Point off = { _visOffsetSet ? (_visOffset.x - _windowRect.x) : 0,
1163 _visOffsetSet ? (_visOffset.y - _windowRect.y) : 0 };
1164 if (logClip)
1165 WLog_VRB(TAG,
1166 "clip id=0x%08" PRIx32 " off=%d,%d nVis=%zu vis0=%dx%d+%d+%d win=%dx%d "
1167 "gfx=%ux%u",
1168 static_cast<uint32_t>(_id), off.x, off.y, _visRects.size(),
1169 _visRects.at(0).w, _visRects.at(0).h, _visRects.at(0).x, _visRects.at(0).y,
1170 ww, wh, _gfxW, _gfxH);
1171 const std::vector<SDL_Rect>& damageRects = _gfxDamage;
1172 std::vector<SDL_Rect> draw;
1173 draw.reserve(damageRects.size() * _visRects.size());
1174 for (const auto& d : damageRects)
1175 {
1176 for (auto v : _visRects)
1177 {
1178 v.x += off.x;
1179 v.y += off.y;
1180 SDL_Rect part{};
1181 if (SDL_GetRectIntersection(&d, &v, &part))
1182 draw.push_back(part);
1183 }
1184 }
1185 if (!draw.empty()) /* empty vector would mean "draw everything" to drawRects */
1186 std::ignore = _win->drawRects(s, dst, draw);
1187 }
1188 else if (_gfxDamage.empty() || serverResize)
1189 {
1190 /* Clear target and redraw surface on size change. */
1191 if (serverResize)
1192 {
1193 const bool transparentWin =
1194 (SDL_GetWindowFlags(_win->window()) & SDL_WINDOW_TRANSPARENT) != 0;
1195 /* Wipe surface to avoid black border artifacts on opaque windows. */
1196 std::ignore = _win->fill(static_cast<Uint8>(0), 0, 0,
1197 static_cast<Uint8>(transparentWin ? 0x00 : 0xFF));
1198 _needsFullBlit = false;
1199 }
1200 std::ignore = _win->drawRects(s, dst, { full });
1201 }
1202 else
1203 {
1204 std::ignore = _win->drawRects(s, dst, _gfxDamage);
1205 }
1206
1207 _win->updateSurface();
1208 _gfxPresented = true; /* First frame rendered; paint() may map the window. */
1209 }
1210 SDL_DestroySurface(s);
1211 WLog_VRB(TAG, "paintGfx id=0x%08" PRIx32 " mode=%s win=%dx%d dmg=%zu",
1212 static_cast<uint32_t>(_id),
1213 localResize ? "resize" : (serverResize ? "server-resize" : "gfx"), ww, wh,
1214 _gfxDamage.size());
1215 _gfxDamage.clear();
1216 /* Preserve size baseline during local resize. */
1217 if (!localResize)
1218 {
1219 _lastWinW = ww;
1220 _lastWinH = wh;
1221 _lastGfxW = gw;
1222 _lastGfxH = gh;
1223 }
1224 return true;
1225}
1226
1227bool SdlRailWindow::paintLegacy(SDL_Surface* primary, const std::vector<SDL_Rect>& damage)
1228{
1229 SDL_Rect rect{};
1230 SDL_Rect bi{};
1231 std::vector<SDL_Rect> vis;
1232 bool full = false;
1233 {
1234 std::unique_lock lock(_gfxLock);
1235 /* Layered decorations have no meaningful content in the shared primary. */
1236 if (!primary || _layered || (_windowRect.w <= 0) || (_windowRect.h <= 0))
1237 return true;
1238
1239 /* Damage-driven: re-copy only server-updated regions, keep the last frame elsewhere. */
1240 full = !_painted;
1241 if (!full && damage.empty())
1242 {
1243 WLog_VRB(TAG, "paintLegacy skip id=0x%08" PRIx32 " no-damage",
1244 static_cast<uint32_t>(_id));
1245 return true;
1246 }
1247
1248 rect = _windowRect;
1249 bi = bandInsets();
1250 vis = _visRects;
1251 }
1252
1253 if (vis.empty())
1254 vis.push_back({ 0, 0, rect.w, rect.h });
1255
1256 /* Paint only the visible sub-rects (stacked windows don't bleed through), clamped and 1:1. */
1257 const SDL_Rect bounds = { 0, 0, primary->w, primary->h };
1258 bool blitted = false;
1259 for (const auto& v : vis)
1260 {
1261 SDL_Rect src = { rect.x + v.x, rect.y + v.y, v.w, v.h };
1262 SDL_Rect clipped{};
1263 if (!SDL_GetRectIntersection(&src, &bounds, &clipped))
1264 continue;
1265
1266 if (full)
1267 {
1268 SDL_Rect dst = { clipped.x - rect.x + bi.x, clipped.y - rect.y + bi.y, clipped.w,
1269 clipped.h };
1270 if (_win->blit(primary, clipped, dst))
1271 blitted = true;
1272 continue;
1273 }
1274 for (const auto& d : damage)
1275 {
1276 SDL_Rect part{};
1277 if (!SDL_GetRectIntersection(&clipped, &d, &part))
1278 continue;
1279 SDL_Rect dst = { part.x - rect.x + bi.x, part.y - rect.y + bi.y, part.w, part.h };
1280 if (_win->blit(primary, part, dst))
1281 blitted = true;
1282 }
1283 }
1284 if (blitted)
1285 {
1286 {
1287 std::unique_lock lock(_gfxLock);
1288 _painted = true;
1289 }
1290 _win->updateSurface();
1291 WLog_VRB(TAG, "paintLegacy id=0x%08" PRIx32 " full=%d visRects=%zu",
1292 static_cast<uint32_t>(_id), full ? 1 : 0, vis.size());
1293 /* Defer mapping until GFX frame arrives to avoid desktop flash. */
1294 }
1295 return true;
1296}