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