macOS: pixel-exact live-resize support#49
Conversation
Resolve the 16 issues raised in the cubic-dev-ai review of the pixel-exact live-resize support. font: remove the MAX_FONT_SIZE cap on the HiDPI-scaled physical size so large fonts render at full size on Retina; scale the 6x13 bitmap fallback by the nearest-integer HiDPI factor so it matches the scaled core metrics. window-internal: limit the resize band clear/expose optimization to the top-left-anchored gravities it models (NorthWest/Static); other bit gravities fall back to a full-window clear/expose. events/pointer: share scaleSdlPointToPixels via events.h so XQueryPointer and the event delivery path agree on rounding and scale selection. tests: enforce the exported libx11Compat* shim ABI in symbol coverage via tests/shim-symbols.txt (no-op where the shim is not built). harness patches: - 007: only rebind xftdraw when it was actually bound (renderer-neutral). - 008: resolve the live-resize reflow hook with dlsym(RTLD_DEFAULT, ...) instead of a weak undefined symbol, portable across GNU ld and Mach-O. - 012: extract the duplicated Expose clamp arithmetic into a shared e_x_copy_backbuf_clamped() helper.
|
Thanks for the review. All 16 points are addressed in
|
Yes, please go ahead.
Preserving changes in xwpe would be better.
The current patches are admittedly a bit hacky, as I am still new to XWPE. Please feel free to rework them as needed. |
Rebased onto main and squashed from fix/hidpi-pixel-exact-resize. - HiDPI: size the initial cell grid from the mapped window's pixel size (SDL_GetWindowSizeInPixels) and promote the window texture to pixels, so the first xwpe dialog matches the physical Retina backing instead of the logical 1x size. - Live resize: a CFRunLoop observer (src/mac-live-resize.c) drives a reflow hook each tick of a macOS modal resize drag, so the client repaints real content into the grown area instead of a stretched guess. - XCopyArea stays pixel-exact: clamp a Pixmap source rect to the source texture (clampCopyAreaSrcRect) so a copy that runs past the pixmap leaves the trailing row/column untouched rather than scaling or painting the zero-padded readback. - Adopt the upstream xwpe that now carries the shim-integration changes (bump XWPE_REVISION) and drop the harness patches (005-012) now integrated in xwpe. - Headless-present robustness, clang-format-20, and a resize UI replay/assertion. Conflicts with main (which consolidated the differential tests) were resolved by keeping main's clipCopyAreaRects structure and re-applying the pixel-exact clamp.
Add compat/xmms-patches/0003-fix-teardown-volume-race.patch. XMMS's cleanup_plugins() frees ip_data and op_data, but the plugin-cleanup loops pump the GLib main loop, which dispatches the still-armed volume idle. That tick runs read_volume -> input_get_volume -> get_input_playing / output_get_volume, which read ip_data->playing and op_data->current_output_plugin after the structs are freed, so a late tick dereferences freed memory and XMMS dies with its "You've probably found a bug in XMMS" SIGSEGV handler (exit 1) during close. Null the globals as they are freed and let the reads short-circuit on NULL, so a volume tick that fires mid-teardown becomes a clean no-op instead of a freed-pointer dereference. This closed a flaky failure of tests/ui/replays/xmms-window.replay's assert-exit 0: with the patch out, the close-time crash reproduced 6/80 under real SDL2; with it in, 0/200.
9272d4f to
93aeaac
Compare
Thanks for the heads up! This PR should now be ready for review! It took several days of testing to get this right, but I am starting to get confident that xwpe could be released in macos now with libx11-compat. I will perform some dogfooding, but will you consider making a initial release soon? It will be very nice for the ecosystem where xwpe has been updated recently. |
… hygiene Fixes for the Linux-verifiable findings from the latest review pass: - Runtime-gate SDL_GetWindowSizeInPixels (display.c/.h, window.c, sdl-wrapper.c): the >=2.26 call was selected at compile time only, so a build against >=2.26 headers running on an older SDL resolved a NULL dlsym and aborted at the first XOpenDisplay. Add compatSdlHasWindowSizeInPixels(), which checks the loaded library via SDL_GetVersion, and keep the renderer-output-size fallback always compiled. LIBX11_COMPAT_NO_SIZE_IN_PIXELS forces the fallback so the older-SDL path is exercisable on a new host. - Snap: preserve the ICCCM base size (events.c/.h). A drag ending in [base, base+inc) now rounds DOWN to base (i == 0) instead of being bumped up a whole increment; only a genuine zero-base zero result is floored to one increment. Update the unit test and doc accordingly, and document inc <= 0 as a defined no-op. - Base-size hint cache (display.c): when PBaseSize is absent but PMinSize is set, fall back to the minimum as the snap grid origin (ICCCM), matching missing.c, instead of defaulting the base to 0. - Present wake (drawing.c): clear presentWakePending before rescheduling in the coalesce path so schedulePresentWake can arm the next timer; otherwise a repaint burst that outlives its first wake could be stranded past the safety cap. - Test hygiene (check.c): drain the deferred close of the throwaway victim display on the early-return failure paths so it is not leaked.
…t retry Fixes for the macOS HiDPI / live-resize findings from the latest review pass. These paths are inert at scale 1.0 (Linux/CI), so they are validated on a macOS Retina session; the shared/Linux paths are covered by the existing smoke. - HiDPI client resize (window-internal.c): convert requested/returned SDL sizes through the cached per-axis scale in configureWindow, so a 2x-Retina XResizeWindow no longer requests a doubled native window and records the logical size as physical. - HiDPI remap growth (window.c): demote a top-level's promoted physical geometry back to logical points (and reset the cached scale) in unrealizeTopLevelWindow, so an unmap/map cycle no longer grows the window by the HiDPI factor each time. - Display-scale change (events.c): handle SDL_WINDOWEVENT_DISPLAY_CHANGED and re-derive the per-window scale from the live pixel/point ratio, so moving a window between monitors of different DPI does not keep multiplying by the old scale. Event-driven only; the resize fast path (and its test fakes) is untouched. SDL2 backend only for now (the SDL3 backend uses a different window-event model). - Drag-end snap for hook-less clients (events.c): run snapTopLevelToResizeIncrements for every mapped top-level when forceReflow is set, before the reflow branch, so clients without a reflow hook also settle on a whole-cell size. - Per-window coalesce state (window.h/window-internal.c/events.c): move the live-resize last-seen size and settle counter onto WindowStruct so two windows resizing at once no longer overwrite each other's state and never settle. - Stale origin on top/left resize (events.c): persist the queried logical position on an accepted RESIZED, mirroring the MOVED branch. - Present retry on upload failure (drawing.c): check SDL_UpdateTexture and return False on error so the frame stays dirty for retry instead of leaving stale pixels marked as presented. - Stale live-resize disarm (mac-live-resize.c): ignore a late NSWindowDidEndLiveResize when the captured window is still in live resize (or the observer is already gone), so a delayed end notification cannot kill a new drag's reflow. The new HiDPI-probe fallback and the DISPLAY_CHANGED handler are gated to the SDL2 backend (SDL3 always provides SDL_GetWindowSizeInPixels and routes window events differently), keeping both SDL_BACKEND builds compiling.
596d4fd to
3d2f9f0
Compare
Dragging an xwpe window between monitors with different backing scales (e.g. a Retina 2x display and a 1x display on macOS) left the window with stale physical-pixel geometry and a stale font scale on the SDL3 backend: the DISPLAY_CHANGED handler was compiled out under SDL3, and the client had no way to learn the new backing scale to re-derive its fixed point size. Fonts and layout came out over- or under-sized on the destination monitor. Enable the DISPLAY_CHANGED path on SDL3 and add a client-visible channel for the backing scale so a client that renders at a fixed point size can track the destination monitor: - events.c: let SDL_WINDOWEVENT_DISPLAY_CHANGED through the event filter on the SDL3 backend (only for a known top-level), and enable the convertEvent handler for SDL3. SDL2 follows the move with a SIZE_CHANGED that reflows geometry, but SDL3 delivers no geometry event for a bare move, so re-derive the per-window scale, refresh the global font scale, republish the scale property, and re-promote the window to the new monitor's physical size (reflowing the client) here. - display.c/display.h: publish the probed backing scale on the root window as the _LIBX11_COMPAT_HIDPI_SCALE CARDINAL property (scale * 1000 fixed point) at XOpenDisplay and again on each qualifying monitor move. Format-32 properties use the documented long client convention. - mk/xwpe.mk: bump XWPE_REVISION to the vejeta/xwpe commit that reads the property and reloads its Xft font at the new density, so make xwpe reproduces the end-to-end fix. On a real X server the property is simply absent and clients fall back to 1.0, so behavior there is unchanged. Faked test resizes are unaffected; the DISPLAY_CHANGED work is event-driven and never runs on the resize fast path.
PR vejeta/xwpe#1 (Reload the font on a mixed-DPI monitor move) is merged to main as b850185. Re-point XWPE_REVISION from the feature-branch commit 9192e0c to the immutable merge commit so make xwpe builds against the merged mainline.
hiDpiScaledMetric returns a short, so a large advance width scaled by a high HiDPI factor could exceed SHRT_MAX and wrap negative before the cast. Clamp to [SHRT_MIN, SHRT_MAX] before casting, mirroring the INT_MAX guard already in hiDpiScaledPixelSize. Addresses review feedback on PR sysprog21#49 (font.c:564).
The accelerated present read each dirty rect one scanline at a time, issuing an SDL_RenderReadPixels plus an SDL_UpdateTexture per row - about 1600 round trips for a tall full-window present. Size the CPU scratch to the largest rect this frame (w*h*4, grown once up front) and read/upload each rect in a single call pair instead. Addresses review feedback on PR sysprog21#49 (drawing.c:494).
XSetWMSizeHints caches the resize increments (used by the live-resize drag-end snap) straight from the XSizeHints struct, but a client that writes WM_NORMAL_HINTS through a raw XChangeProperty bypassed that path and left hasResizeInc clear. Add cacheResizeIncrementsFromNormalHints- Property to decode the stored property bytes, and call it from the XChangeProperty WM_NORMAL_HINTS hook so both write paths agree. Addresses review feedback on PR sysprog21#49 (display.c:879).
The observer callback cleared liveResizeObserverRunning before the idle-disarm branch, so the disarmLiveResizeObserver() call there took the synchronous CFRunLoopRemoveObserver path from inside the callback frame - exactly the re-entrant removal the pending-stop latch exists to avoid. Keep liveResizeObserverRunning set until after every disarm decision so any disarm reached from within the callback defers through the latch, then clear it and honour the latch once the callback is about to return. Addresses review feedback on PR sysprog21#49 (mac-live-resize.c:109).
topLeftGravityString cached the result of +stringWithUTF8String:, which returns an autoreleased instance. Under manual retain/release (no ARC) the surrounding autorelease pool drains every run-loop tick, leaving the process-lifetime static dangling and risking a use-after-free on later frames. Build it with +alloc/-initWithUTF8String: instead, which returns a +1 retained instance that is never autoreleased. Addresses review feedback on PR sysprog21#49 (mac-live-resize.c:433).
snapTopLevelToResizeIncrements runs inside the live-resize present frame, where SDL_PumpEvents would re-enter the event path the present must not be nested under. The pump is also unnecessary: the RESIZED echo that SDL_SetWindowSize queues is neutralized at delivery - suppressSdlResizeEcho drops it in the callback, and the RESIZED sizeChanged short-circuit skips the destructive path when it does arrive - so there is nothing to flush synchronously. Remove the pump. Addresses review feedback on PR sysprog21#49 (events.c:768).
The drag-end increment snap ran for every mapped top-level regardless of whether a reflow hook was registered. For a hook-less client the snap resized the SDL window and suppressed the RESIZED echo, but nothing updated ws->w/h or reflowed the client to the snapped size, so the client kept drawing at the pre-snap size and its content was clipped by up to a cell. Move the snap inside the reflow branch, where the reflow that follows reads the snapped size and repaints the client to match, keeping the snapped geometry and the client content consistent. Addresses review feedback on PR sysprog21#49 (events.c:813).
The DISPLAY_CHANGED handler grows the top-level via postSyntheticWindowResize on a mixed-DPI monitor move but did not invalidate the cached (0,0,w,h) visible region, so on a scale-up move the newly added physical columns/rows stayed clipped and black. Invalidate it after the resize exactly as the RESIZED ConfigureNotify path does. Addresses review feedback on PR sysprog21#49 (events.c:2893).
|
@jserv thanks for the thorough review — all 7 points addressed, one commit each (pushed on top of the branch):
|
The resize-increment cache read base_width/base_height (indices 15-16) whenever PBaseSize was set, but those fields exist only in the 18-element ICCCM-v1 payload. A legacy 15-element (pre-ICCCM) write reaches the decoder with dataLength == OldNumPropSizeElements, so dereferencing sizeHints->baseWidth/baseHeight read past the stored property. Only treat PBaseSize as present when dataLength >= NumPropSizeElements, matching the ICCCM layout; otherwise fall back to min size (or 0) as the grid origin, as the code already does when PBaseSize is unset. Addresses cubic-dev-ai review (out-of-bounds read, P1).
The drag-end increment snap calls SDL_SetWindowSize from inside the live-resize present frame and cannot pump SDL to flush the echo, so the RESIZED/SIZE_CHANGED it queues was delivered later and converted into a second, redundant ConfigureNotify (the snap already posts one). Record the exact logical size the snap wrote (snapEchoW/H) and have the event filter drop resize echoes carrying that size, clearing the key on the matching RESIZED so a later genuine resize back to the same size is kept. configureWindow keeps its synchronous-pump suppression flag for the client XResizeWindow path; the size key covers only the snap path that cannot pump. Addresses cubic-dev-ai review (duplicate ConfigureNotify, P2).
The accelerated present grew a per-window CPU readback buffer to the largest dirty rect and retained it between frames, so several 4K/8K windows live-resizing at once could hold multiple full backing-sized buffers indefinitely. Bound the retained buffer at 8 MiB (LIBX11_COMPAT_PRESENT_READBACK_CAP, floored to one scanline of the widest rect) and read any rect larger than the cap in successive horizontal bands that each fit. The common case still transfers a whole rect in one read/upload call pair, so the per-rect fast path jserv asked for is preserved; only the pathological oversized present is banded. Addresses cubic-dev-ai review (retained scratch memory pressure, P2).
…-resize # Conflicts: # src/window.c
Two HiDPI live-resize fixes plus the matching xwpe revision bump. xft: drawUtf8String composited each glyph on the CPU. To alpha-blend it read the destination pixmap back with XGetImage (SDL_RenderReadPixels), blended per pixel, then XPutImage - which re-dirtied the pixmap readback cache, forcing a full-pixmap GPU->CPU readback for every character typed. That readback dominated typing latency (~78% of main-thread time under a sampling profile). Rewrite the draw path to composite on the GPU, mirroring the core-font path: upload the TTF_RenderUTF8_Blended surface to a BLEND texture, apply the colour alpha via SDL_SetTextureAlphaMod, intersect the Xft clip rects with the base renderer clip, and SDL_RenderCopy onto the drawable's render target. Remove the now-dead blendPixel and xftClipContains, and rename the file-local clampToInt to xftClampToInt to avoid colliding with drawing.h. After the change the readback path is gone from the profile (0 samples even under sustained typing). events: on SDL_WINDOWEVENT_DISPLAY_CHANGED, rescale the cached PResizeInc (widthInc/heightInc/baseWidth/baseHeight/minWidth/minHeight) by newScale/oldScale. Those increments live in physical-pixel space, so a monitor move that changes the backing scale left them describing the previous monitor's cell, and the drag-end snap quantised to the wrong grid - leaving a permanent sub-cell remainder band on the new display. Rescaling keeps the snap grid correct even for clients that do not re-publish WM_NORMAL_HINTS after a DPI change. mk/xwpe.mk: bump XWPE_REVISION to 88290d5 (xwpe re-publishes WM_NORMAL_HINTS after its DPI font refit, the client-side half of the remainder-band fix).
The SDL2 leg links libX11-compat against the in-tree dlopen wrapper instead of libSDL2 directly, so every SDL symbol the tree calls needs a SDL_WRAP thunk. The new GPU-composite glyph path in xft.c calls SDL_SetTextureAlphaMod, which had no thunk, leaving an undefined reference in libX11-compat.so on the SDL2 backend (CI) and cascading a link failure into every downstream job. The SDL3 leg links libSDL3 directly, so local SDL3 builds were unaffected. Add the missing thunk. SDL_SetTextureAlphaMod predates SDL 2.0.0, so no version guard is needed.
The previous xwpe capture predated the GPU-composite Xft path and the pixel-exact live-resize work in this PR, and looked dated on HiDPI. Replace assets/xwpe.png with a current capture showing crisp GPU-composited glyphs, syntax highlighting, and clangd-driven inlay hints and semantic colours on a Retina display, and update the caption and alt text to match.
0a96de7 to
cda58c9
Compare
There was a problem hiding this comment.
4 issues found across 35 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="src/mac-live-resize.c">
<violation number="1" location="src/mac-live-resize.c:106">
P1: Destroying the resized window from a reflow callback can leave `liveResizeWindow` dangling and crash on this `inLiveResize` message after the present returns. Keep an owned NSWindow reference and release it during disarm, or clear/disarm it as part of native-window teardown.</violation>
</file>
<file name="src/window-internal.c">
<violation number="1" location="src/window-internal.c:1593">
P2: Resizing a window using NorthEastGravity, CenterGravity, or another non-top-left bit gravity leaves retained pixels at the top-left, not the requested anchor. Translate the copied source/destination rect for each gravity, or consistently fall back to discarding contents for unsupported gravities.</violation>
</file>
<file name="src/drawing.c">
<violation number="1" location="src/drawing.c:301">
P2: A `clock_gettime` failure can permanently suppress presents: `beginCoalesceClientRepaint` creates a deadline from zero, but `coalesceActive` can never reach it while the clock still returns zero. Treat an unavailable clock as an inactive/expired coalesce gate.</violation>
</file>
<file name="src/font.c">
<violation number="1" location="src/font.c:1369">
P1: Core-font text becomes the wrong physical size after moving a window between differently scaled displays: the font face is opened once at a global scale, but display changes alter per-window backing scale without rebuilding that face. Track scale per drawable or recreate/invalidate affected `CompatFont` faces and text-cache entries when the target window's scale changes; do not use a stale process-global scale for this decision.</violation>
</file>
Tip: cubic can generate docs of your entire codebase and keep them up to date. Try it here.
Re-trigger cubic
| return None; | ||
| } | ||
| int fontSize = requestedFontSize(name); | ||
| int fontSize = hiDpiScaledPixelSize(requestedFontSize(name)); |
There was a problem hiding this comment.
P1: Core-font text becomes the wrong physical size after moving a window between differently scaled displays: the font face is opened once at a global scale, but display changes alter per-window backing scale without rebuilding that face. Track scale per drawable or recreate/invalidate affected CompatFont faces and text-cache entries when the target window's scale changes; do not use a stale process-global scale for this decision.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/font.c, line 1369:
<comment>Core-font text becomes the wrong physical size after moving a window between differently scaled displays: the font face is opened once at a global scale, but display changes alter per-window backing scale without rebuilding that face. Track scale per drawable or recreate/invalidate affected `CompatFont` faces and text-cache entries when the target window's scale changes; do not use a stale process-global scale for this decision.</comment>
<file context>
@@ -1261,7 +1366,7 @@ Font XLoadFont(Display *display, _Xconst char *name)
return None;
}
- int fontSize = requestedFontSize(name);
+ int fontSize = hiDpiScaledPixelSize(requestedFontSize(name));
resource->ttf = TTF_OpenFont(fontEntry->filePath, fontSize);
if (!resource->ttf) {
</file context>
There was a problem hiding this comment.
Deferring this to a follow-up. Per-drawable font-scale tracking is a real
enhancement, but it is a larger architectural change than the rest of this PR:
CompatFont faces and the text cache are keyed globally today, so doing this
correctly means rekeying both by target scale and invalidating/rebuilding faces
on every DISPLAY_CHANGED, plus deciding a policy for shared faces used across
windows on different displays.
In practice the multi-display core-font mis-size is also mitigated by the xwpe
side of this work (font re-derive on DPI change) and by the global-scale reset
this PR adds. I would rather land the per-drawable face tracking as its own
change with dedicated tests than fold a partial version into this PR. Filing as
a follow-up.
There was a problem hiding this comment.
Agreed — the per-drawable face tracking is a separate change, and this PR’s global-scale reset plus xwpe font re-derive covers the immediate issue. I’ll keep the full rekey/invalidation work as a follow-up with dedicated tests.
| * (stays YES) yet flips to NO the moment the loop exits, so we disarm | ||
| * exactly then instead of waiting for a possibly-delayed DidEnd | ||
| * notification. */ | ||
| int stillResizing = liveResizeWindowInLiveResize(); |
There was a problem hiding this comment.
P1: Destroying the resized window from a reflow callback can leave liveResizeWindow dangling and crash on this inLiveResize message after the present returns. Keep an owned NSWindow reference and release it during disarm, or clear/disarm it as part of native-window teardown.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/mac-live-resize.c, line 106:
<comment>Destroying the resized window from a reflow callback can leave `liveResizeWindow` dangling and crash on this `inLiveResize` message after the present returns. Keep an owned NSWindow reference and release it during disarm, or clear/disarm it as part of native-window teardown.</comment>
<file context>
@@ -0,0 +1,506 @@
+ * (stays YES) yet flips to NO the moment the loop exits, so we disarm
+ * exactly then instead of waiting for a possibly-delayed DidEnd
+ * notification. */
+ int stillResizing = liveResizeWindowInLiveResize();
+ if (!stillResizing) {
+ /* Modal loop has ended (DidEnd may not have posted yet). Disarm and
</file context>
There was a problem hiding this comment.
Deferring this to a follow-up. The concrete re-entrant use-after-free that was
reachable here (destroying the display/window from inside the reflow callback)
is already handled in this PR: XCloseDisplay defers the actual close while a
present is in flight and drains it afterward, and the observer disarm path keeps
the running latch set until after the disarm decision so both paths route
through the pending-stop handling.
Holding an owned (retained) NSWindow reference and releasing it during disarm is
a stronger invariant that also covers a client destroying the native window by
some path other than XCloseDisplay. That is worth doing, but it changes the
observer's ownership model and wants its own review; I would rather not widen
this PR's Objective-C lifetime handling further here. Filing as a follow-up.
There was a problem hiding this comment.
The reachable re-entrant close is already handled in this PR by the deferred XCloseDisplay path and the pending-stop latch, so that part of the parent comment is too broad here. The retained NSWindow ownership change is a separate, stronger invariant and should go in the follow-up you mentioned.
| int copyH = oldH < (int) windowStruct->h ? oldH : (int) windowStruct->h; | ||
| if (copyW > 0 && copyH > 0) { | ||
| SDL_Rect overlap = {0, 0, copyW, copyH}; | ||
| SDL_RenderCopy(windowRenderer, oldTexture, &overlap, &overlap); |
There was a problem hiding this comment.
P2: Resizing a window using NorthEastGravity, CenterGravity, or another non-top-left bit gravity leaves retained pixels at the top-left, not the requested anchor. Translate the copied source/destination rect for each gravity, or consistently fall back to discarding contents for unsupported gravities.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/window-internal.c, line 1593:
<comment>Resizing a window using NorthEastGravity, CenterGravity, or another non-top-left bit gravity leaves retained pixels at the top-left, not the requested anchor. Translate the copied source/destination rect for each gravity, or consistently fall back to discarding contents for unsupported gravities.</comment>
<file context>
@@ -1484,6 +1575,24 @@ void resizeWindowTexture(Window window)
+ int copyH = oldH < (int) windowStruct->h ? oldH : (int) windowStruct->h;
+ if (copyW > 0 && copyH > 0) {
+ SDL_Rect overlap = {0, 0, copyW, copyH};
+ SDL_RenderCopy(windowRenderer, oldTexture, &overlap, &overlap);
+ }
+ }
</file context>
There was a problem hiding this comment.
Deferring this to a follow-up. The retained-pixel copy here is a best-effort
optimization on top of a full Expose that repaints the window anyway, so a
non-top-left bit gravity does not lose content — it just briefly retains the
wrong pixels in the anchor region until the client redraws. The only consumer
in this stack (xwpe) uses top-left gravity, so the general case is untested in
practice.
Doing this properly means translating the source/destination overlap rect per
gravity anchor (NorthEast, Center, South, ...) and testing each, or explicitly
discarding contents for the gravities we do not model. Both are reasonable; I
would rather add the full gravity matrix with tests as a separate change than
ship a partial anchor implementation here. Filing as a follow-up.
There was a problem hiding this comment.
The parent comment is too broad for this PR. The retained copy is just a temporary optimization while the full Expose repaints, and this stack only uses top-left gravity, so the non-top-left anchor case is out of scope here. A full gravity matrix can land separately with tests.
| uint64_t deadline = coalesceDeadlineGet(); | ||
| if (deadline == 0) | ||
| return False; | ||
| if (monotonicNowNs() >= deadline) { |
There was a problem hiding this comment.
P2: A clock_gettime failure can permanently suppress presents: beginCoalesceClientRepaint creates a deadline from zero, but coalesceActive can never reach it while the clock still returns zero. Treat an unavailable clock as an inactive/expired coalesce gate.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/drawing.c, line 301:
<comment>A `clock_gettime` failure can permanently suppress presents: `beginCoalesceClientRepaint` creates a deadline from zero, but `coalesceActive` can never reach it while the clock still returns zero. Treat an unavailable clock as an inactive/expired coalesce gate.</comment>
<file context>
@@ -223,12 +277,364 @@ static void schedulePresentWake(void)
+ uint64_t deadline = coalesceDeadlineGet();
+ if (deadline == 0)
+ return False;
+ if (monotonicNowNs() >= deadline) {
+ coalesceDeadlineSet(0);
+ return False;
</file context>
| if (monotonicNowNs() >= deadline) { | |
| uint64_t now = monotonicNowNs(); | |
| if (now == 0 || now >= deadline) { |
There was a problem hiding this comment.
Deferring this to a follow-up, but noting it is very low risk in practice.
monotonicNowNs() is backed by SDL_GetPerformanceCounter (with a clock_gettime
fallback), which does not fail on the platforms this shim targets; a genuine
zero return would require the monotonic clock to be unavailable, at which point
coalescing is the least of the problems.
That said, treating an unavailable/zero clock as an expired (inactive) coalesce
gate is the correct defensive behavior and the suggested one-line guard is
harmless. I am grouping it with the other hardening items rather than adding it
to this already-large review batch, so it lands with a focused test for the
zero-clock path. Filing as a follow-up.
There was a problem hiding this comment.
The clock-failure concern is valid, but I’m deferring it to a follow-up with a focused zero-clock test. The parent comment can stay as a hardening item, but it’s out of this batch.
…ape/XTest robustness events.c: drop the SDL3-only guard on the SDL_WINDOWEVENT_DISPLAY_CHANGED filter so mixed-DPI scale refreshes reach the handler on default SDL2 builds too (the subevent enum exists since SDL 2.0.18). display.c: reset globalHiDpiScale to 1.0 at the top of probeGlobalHiDpiScale so a later 1x session cannot inherit a prior Retina scale (the probe only overwrites it on a pixel!=point mismatch, and the probe-failure path returns early). Also remove the redundant inline resize-increment cache in XSetWMSizeHints: the XChangeProperty success path already refreshes the cache from the stored property, so the inline decode both duplicated that work and ran even when the property write failed. drawing.c: check the SDL_RenderCopy return in the accelerated present path and bail (leaving the frame dirty for retry) on failure, mirroring the adjacent readback/upload error handling instead of silently marking a dropped frame presented. xft.c: clip the glyph copy to the drawable's sibling visible region the same way the core-font path does, so Xft text into a child obscured by a higher-stacked sibling can no longer paint over that sibling; and only present when the post-draw shape composite succeeded, so a failed composite recomposes from a fresh baseline instead of flashing masked-out pixels. xtest.c/events.c: tag synthetic XTest motion and button events with which == SDL_TOUCH_MOUSEID (as the wheel path already does) and skip scaleSdlPointToPixels for those in convertEvent. XTest coordinates are already in X11 physical pixels, so the extra scale double-counted them on Retina; real hardware events still carry SDL points and keep the scale.
What
Pixel-exact live-resize support for macOS (HiDPI/Retina), the fix for the
artifacts it exposed, and a fix for an XMMS close-time crash the differential
tests were flaking on. Rebased onto
main(now including #50). Verifiedend-to-end with the xwpe harness.
src/mac-live-resize.c/.h): a CFRunLoopObserverdrives a reflow callback on each tick of Cocoa's modal resize loop, so the
client keeps repainting while its own event loop is blocked in the drag.
Opt-in via
libx11CompatRegisterLiveResizeReflow().src/drawing.c,src/events.c): present during the modalloop, coalescing the client's repaint burst so only the final frame reaches
the screen; re-entrancy guards prevent a nested resize loop. The observer
skips its per-tick full-window readback when a tick produced no new content
(no reflow, window not moving), so residual ticks after mouse-up drain
instantly instead of queueing ~40ms redundant presents.
src/events.c,src/window.*): SDLRESIZED/SIZE_CHANGED -> ConfigureNotify for host-driven drags, while dropping
the SDL echo of a client
XResizeWindow(suppressSdlResizeEcho).src/display.c,src/events.c/.h,src/window.*):replay the ICCCM
size = base + i * incquantization a real WM performs. Aclient that publishes
WM_NORMAL_HINTSPResizeInc(e.g. xwpe, whose grid iswhole font cells) has its increments cached at
XSetWMSizeHints; at drag endsnapTopLevelToResizeIncrementsrounds the window DOWN to a whole increment sono sub-cell remainder band survives. Gated on
PResizeInc, so clients thatnever set it are unaffected. The pure axis math is factored into
libx11CompatSnapAxisToIncrementfor driver-independent unit testing.src/display.*,src/font.c,src/window.*,src/pointer.c): promote top-levels to physical pixels,scale font metrics, translate pointer coords. No-op at scale 1. The initial
promotion prefers
SDL_GetWindowSizeInPixelsso the first mapped dialogadopts the correct cell grid instead of a logical (1x) size.
src/drawing.c):clampCopyAreaSrcRect()keeps a 1:1blit inside the source texture bounds. Root cause of the stale function-key
bar after resize (an Expose carrying the full sub-cell window size vs a
back buffer snapped to a whole-cell grid).
src/display.*,src/events.c; xwpeWeXterm.c):when a window is dragged to a display with a different backing scale, the shim
re-derives the per-window HiDPI scale and rescales the cached
PResizeIncincrements (which live in physical-pixel space) by the new/old scale ratio, so
the drag-end snap quantises to the new monitor's cell. Handled on both the SDL2
(
SDL_WINDOWEVENT_DISPLAY_CHANGED) and SDL3 backends; xwpe reloads its Xft fontat the new size and re-advertises
WM_NORMAL_HINTSso the cell grid stayscorrect.
src/xft.c):drawUtf8Stringused toalpha-blend each glyph on the CPU (
XGetImage-> blend ->XPutImage), whichre-dirtied the pixmap readback cache and forced a full GPU->CPU readback per
character (~78% of main-thread time while typing). It now uploads the blended
glyph to a BLEND texture, applies the colour alpha via
SDL_SetTextureAlphaMod,and
SDL_RenderCopys onto the drawable, mirroring the core-font path -- theper-character readback is gone from the profile.
compat/xmms-patches/0003-fix-teardown-volume-race.patch):cleanup_plugins()freesip_data/op_databut the plugin-cleanup loopspump the GLib main loop, which dispatches the still-armed volume idle;
read_volumethen reads those freed structs and XMMS dies with its SIGSEGVhandler (exit 1) during close. Null the globals as they are freed and let the
volume reads short-circuit on NULL, so a late tick is a clean no-op. Closed a
flaky
xmms-window.replayassert-exit 0failure: the crash reproduced 6/80under real SDL2 without the patch, 0/200 with it.
snap_axis_to_increment)unit tests, a UI replay that resizes xwpe and asserts real content in the
grown area, and the exported
libx11Compat*shim ABI manifest(
tests/shim-symbols.txt).Upstream integration
The genuine xwpe fixes now live upstream, so this PR bumps
XWPE_REVISIONtothe xwpe commit that carries them and drops all
compat/xwpe-patches/(thedirectory is gone;
XWPE_PATCHESglobs an empty set).As clarified before, now:
XWPE_REVISIONand drop the harness patches once the fixes landupstream -> done.
upstream rather than kept as harness patches here -> done via the bumped
revision.
compat/xwpe-patches/layout -> reworked away entirely; nothing left torestructure.