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