Skip to content

wayland: fixes to frame insets (C2b), each with a suite assertion - #4

Merged
foxnne merged 7 commits into
fizzy-3.4from
wayland/frame-insets-fixes
Oct 9, 2026
Merged

foxnne merged 7 commits into
fizzy-3.4from
wayland/frame-insets-fixes

Conversation

@foxnne

@foxnne foxnne commented Oct 9, 2026 •

Copy link
Copy Markdown

Part of fizzyedit/fizzy#280 (step C2b). Run by fizzyedit/sdl_zig#4.

Patch 6 ("Wayland: frame insets", 2d6efde) before it is proposed upstream. A test commit first, so CI can show it failing on fizzy-3.4, then one fix per problem, each described "squash into 6 at the next rebase".

Commits

  1. test: the frame-insets suite checks what patch 6 still gets wrong. New FizzyWayland assertions, read from the WAYLAND_DEBUG trace under headless weston (details below). Test files only.
  2. A window made again keeps its frame insets (C2b 1). SDL_RecreateWindow calls CreateSDLWindow(_this, window, 0), so the insets were gone after an OpenGL renderer was attached to a shown window, or after a compositor reconnect. The insets asked for are now also kept in the window's own properties under internal names (SDL.internal.wayland.frame_inset.*, as the GPU drivers and renderer keep per-window data), which outlive the window data. No public setter: see "Upstream" below.
  3. An opaque window's opaque region is its frame, not its shadow (C2b 2). Was (0, 0, w, h).
  4. Clamp a framed window to the toplevel bounds by its frame (C2b 3). configure_bounds bound the window geometry (the frame), but were compared with the surface. Untested: weston 14 advertises xdg_wm_base v5 but never sends configure_bounds (nothing in libweston/desktop/xdg-shell.c does), so under headless weston the clamp never runs. The floating 0x0 configure that reaches it does happen. Checked by reading.
  5. The input band round a framed window is a creation property (C2b 4). New SDL_PROP_WINDOW_CREATE_WAYLAND_FRAME_INPUT_MARGIN_NUMBER ("SDL.window.create.wayland.frame_input_margin"), default 8, 0 for the frame alone, each side still at most its inset. It is kept on recreate as the insets are. Documented beside the insets in SDL_video.h. Fizzy keeps the default.
  6. Publish a window's frame insets from creation on (C2b 5). Wayland_CreateWindow publishes the insets the window will float with (zero for libdecor). The first configure corrects them if the window comes up maximized, tiled or fullscreen.
  7. A framed window shown again is framed again (found while doing this; not in C2b's list). Hiding destroys the xdg_surface and its geometry. Shown again at the same size, nothing looked changed and set_window_geometry was never sent to the new xdg_surface. Hiding now forgets the applied insets, and ApplyFrameInsets skips a window with no xdg_surface.

What the new assertions check

  • Every floating check (Floating, Maximized/Fullscreen before and after, Recreated, ShownAgain, input-margin tests): the opaque region is the frame, (24, 6, 346, 254) for 400x300. Maximized or fullscreen: the whole surface.
  • wayland_frameInsetsBeforeShow: a window made hidden publishes 24/6/30/40.
  • wayland_frameInsetsRecreated: after SDL_CreateRenderer(window, "opengles2") makes the shown window again (flags go from VULKAN to OPENGL), the new objects' geometry, input region and opaque region are the frame's. The trace is read from a mark set before the recreate, because libwayland reuses the ids of destroyed objects.
  • wayland_frameInsetsShownAgain: hide, show; the new xdg_surface's geometry is the frame. The window is released from the GPU device while hidden and claimed again after showing. A first version kept it claimed, and the run hung with nothing printed.
  • wayland_frameInsetsInputMarginWide (12) and ...None (0): the input region follows the property.

Trace marks now also go to the real stderr, so a test that hangs while tracing shows how far it got.

Verified

  • Green, this branch (011a2dc5) via sdl_zig#4's pin: https://github.com/fizzyedit/sdl_zig/actions/runs/38001031572. In test-fizzy-wayland-insets at scale 1 and 2, and in test-fizzy-linux (wayland) (where libdecor is installed), all 9 FizzyWayland tests pass, 0 asserts fail. The run concludes success. The build (master, …) jobs fail as they do on fizzy itself: Zig master, continue-on-error.
  • Red, the test commit alone (1eb90b75, fizzy-3.4 + suite) on a throwaway sdl_zig branch, since deleted: https://github.com/fizzyedit/sdl_zig/actions/runs/38000927122. At both scales every new assertion fails, and Popup alone passes:
    • opaque region (0, 0, 400, 300) instead of (24, 6, 346, 254) in every floating check (2);
    • created hidden: all four insets -1 (not published) (5);
    • recreated: no set_window_geometry, input region never set, opaque region the whole surface (1);
    • shown again: no set_window_geometry, input and opaque region never set (the hide/show fix);
    • margin 12: input region (16, 0, 362, 268) instead of (12, 0, 370, 272); margin 0: (16, 0, 362, 268) instead of (24, 6, 346, 254) (4).
  • An earlier red run (https://github.com/fizzyedit/sdl_zig/actions/runs/37984303044, on the test commit's first version) showed the same failures for 1, 2 and 5, then hung in wayland_frameInsetsShownAgain with the window still claimed by the GPU device. That is why the test now releases and reclaims the window, and why the jobs have a timeout.
  • 3 is untestable under weston, as above. Checked by reading.
  • Compiles clean with -Wall -Wextra -Wmissing-prototypes -Wmissing-variable-declarations -Wshadow for x86_64-linux and aarch64-macos (suite), and for Linux through sdl_zig (zig build test-fizzy -Dtarget=x86_64-linux-gnu). The macOS runners skip the suite (Wayland only) and pass.

Upstream

Upstream main now has its own version of this feature: libsdl-org/SDL 5d7167cb "wayland: Add opt-in border inset window properties" (2026-09-24), with cleanups in 86b89cff and dff05080. It isn't in release-3.4.x. There an application opts in with SDL_PROP_WINDOW_CREATE_WAYLAND_ENABLE_INSETS_BOOLEAN and then sets SDL_PROP_WINDOW_WAYLAND_BORDER_INSET_{LEFT,TOP,RIGHT,BOTTOM}_NUMBER on the window. SDL reads them each time it configures, so they are their own setter. Upstream does the window geometry, configure sizes, min/max and popup anchoring, as patch 6 does. Where they differ:

  • Upstream has no input region. The whole surface, shadow included, takes input. Patch 6 sets frame plus band.
  • Its opaque region is still the whole surface (C2b 2 applies to it), and its bounds clamp compares with the surface (C2b 3 applies to it too).
  • Tiled windows keep their insets upstream. Patch 6 zeroes them.
  • No "in effect" values are published. The app infers them from the maximized/fullscreen flags.
  • enable_insets comes from the creation properties, so upstream loses it on SDL_RecreateWindow the same way (C2b 1). The inset values themselves survive, since window properties outlive the window data.

So this PR adds no setter. Upstream already has the setter shape, settable window properties, and a fork-only one would be a third API to reconcile at the next rebase. The upstream path for patch 6 is to adopt upstream's API when fizzy rebases onto an SDL release that has it, and to propose what it lacks as fixes to upstream's feature: input region plus band (with this margin property), opaque region, bounds clamp, re-framing after hide/show, and keeping enable_insets across a recreate.

The SDL commit needs a permanent tag before sdl_zig's pin on it merges.

🤖 Generated with Claude Code

foxnne and others added 7 commits October 9, 2026 17:44
New assertions in the FizzyWayland suite (testautomation_fizzy_wayland.c) for
the fixes to patch 6 ("Wayland: frame insets") that fizzyedit/fizzy#280 step
C2b lists, each read from libwayland's request trace under headless weston:

- every floating check now also asserts an opaque window's opaque region is
  the frame, not the shadow round it, and a maximized or fullscreen one's the
  whole surface;
- wayland_frameInsetsBeforeShow: a window made hidden publishes its insets
  (SDL_PROP_WINDOW_WAYLAND_FRAME_INSET_*_NUMBER) before it is shown;
- wayland_frameInsetsRecreated: after an OpenGL ES renderer makes the shown
  window again through SDL_RecreateWindow, the new objects' window geometry,
  input region and opaque region are the frame's;
- wayland_frameInsetsShownAgain: hidden and shown again, the new xdg_surface
  gets the frame as its window geometry;
- wayland_frameInsetsInputMarginWide and ...None: the input band follows
  SDL_PROP_WINDOW_CREATE_WAYLAND_FRAME_INPUT_MARGIN_NUMBER (12, and 0).

The trace is now read between two marks where a window's objects are made
again, as libwayland reuses the ids of destroyed objects. Test files only.

On fizzy-3.4 (fizzy-3.4.16-8) every new assertion fails; the fixes follow as
their own changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
SDL_RecreateWindow destroys the native window and makes it again with
CreateSDLWindow(_this, window, 0): no creation properties. It does so when an
OpenGL renderer is made for a shown window created without SDL_WINDOW_OPENGL,
and when the Wayland driver reconnects to the compositor. The frame insets
were read only from the creation properties, so after that the window had
none: its whole surface, shadow included, was the window to the compositor
and took input.

The insets asked for at creation are now also kept in the window's own
properties, under internal names (SDL.internal.wayland.frame_inset.*, as the
GPU drivers and the renderer keep their per-window data), which outlive the
window data. A window made with no creation properties takes them from there.

No public API: upstream main has since added a settable form of the same idea
(SDL_PROP_WINDOW_WAYLAND_BORDER_INSET_*_NUMBER, libsdl-org/SDL 5d7167c), so a
setter here would be a third shape to reconcile at the next rebase.

Tested by FizzyWayland's wayland_frameInsetsRecreated (fizzyedit/fizzy#280,
C2b). Squash into 6 ("Wayland: frame insets") at the next rebase.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
SetSurfaceOpaqueRegion gave the compositor the whole surface as opaque, so for
a window with frame insets that isn't SDL_WINDOW_TRANSPARENT it said the
shadow round the frame was opaque too. The compositor may skip drawing what
lies under an opaque region, which is wrong under a shadow wherever its
buffer carries alpha. The opaque region is now the frame: the surface less the
insets in effect, so the whole surface while they are zero (maximized, tiled,
fullscreen, or no insets), as before.

Fizzy's window is transparent, so fizzy never hit this.

Tested by every FizzyWayland check of a floating, maximized or fullscreen
window (fizzyedit/fizzy#280, C2b). Squash into 6 ("Wayland: frame insets")
at the next rebase.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
When a compositor leaves a new floating toplevel's size to the client (a 0x0
configure), SDL clamps the window's own size to the bounds the compositor
sent with xdg_toplevel.configure_bounds. Those bound the window geometry,
which with frame insets is the frame, but were compared with the whole
surface, so a framed window that would just fit was made smaller by its
insets. The bounds are now widened by the insets in effect before the clamp.
The libdecor path is unchanged, as insets never apply there.

Not covered by a test: weston 14 advertises xdg_wm_base version 5 but never
sends configure_bounds (libweston/desktop/xdg-shell.c), so under headless
weston the clamp never runs. Checked by reading. In scale_to_display mode the
clamp compares bounds in points with a size in pixels; that predates the
insets and is left as is.

Part of fizzyedit/fizzy#280, C2b. Squash into 6 ("Wayland: frame insets") at
the next rebase.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
While a window has frame insets only the frame and a band round it take
input, so the rest of the shadow passes clicks through; the band is where the
application's hit test answers the edges it resizes from. It was a fixed 8
units. SDL_PROP_WINDOW_CREATE_WAYLAND_FRAME_INPUT_MARGIN_NUMBER now sets it,
8 by default, 0 for the frame alone; each side's band is still at most that
side's inset. An application whose resize edge is wider than 8, or that wants
no band, can say so. The value is kept for a window SDL makes again, as the
insets are.

Public API, one creation property beside the frame insets, documented with
them. Fizzy keeps the default: its resize edge is 6 points.

Tested by FizzyWayland's wayland_frameInsetsInputMarginWide (12) and
wayland_frameInsetsInputMarginNone (0) (fizzyedit/fizzy#280, C2b). Squash
into 6 ("Wayland: frame insets") at the next rebase.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
SDL_PROP_WINDOW_WAYLAND_FRAME_INSET_*_NUMBER were first set by
ApplyFrameInsets, at the compositor's first configure, so before a window was
shown they were missing and an application could not lay itself out (where
its frame and title bar go) before showing it. Wayland_CreateWindow now
publishes the insets the window will float with, those asked for (zero for a
libdecor window), and the first configure corrects them if the window comes
up maximized, tiled or fullscreen. Publishing is split out of
ApplyFrameInsets as PublishFrameInsets for that.

Tested by FizzyWayland's wayland_frameInsetsBeforeShow (fizzyedit/fizzy#280,
C2b). Squash into 6 ("Wayland: frame insets") at the next rebase.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Hiding a window destroys its xdg_surface, and with it the window geometry.
Shown again at the same size, with the same insets, ConfigureWindowGeometry
saw nothing change and never sent set_window_geometry to the new
xdg_surface, so the compositor took the whole surface, shadow included, as
the window. Hiding now forgets the insets applied, so the configure that
shows the window applies them again; and ApplyFrameInsets does nothing for a
window without an xdg_surface, so a resize while hidden can't mark them
applied before there is a surface to frame.

Found by reading while fixing the rest of fizzyedit/fizzy#280's C2b; not in
its list. Tested by FizzyWayland's wayland_frameInsetsShownAgain. Squash into
6 ("Wayland: frame insets") at the next rebase.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@foxnne
foxnne marked this pull request as ready for review October 9, 2026 23:09
@foxnne
foxnne merged commit 011a2dc into fizzy-3.4 Oct 9, 2026
foxnne added a commit to fizzyedit/fizzy that referenced this pull request Oct 10, 2026
…#314)

Part of #280 (step C2b landed on the forks).

Moves fizzy's `.sdl` pin to sdl_zig `fizzy-1.0.3+3.4.16-9` (SDL
`fizzy-3.4.16-9`), which fixes six problems in SDL patch 6 (Wayland
frame insets, the margins fizzy draws its Linux window shadow in):

| Fix | SDL commit | Shown by the suite |
|---|---|---|
| A window SDL makes again (e.g. for an OpenGL renderer) keeps its
insets | `9258288` | yes |
| An opaque window's opaque region is its frame, not its shadow |
`0ca75f2` | yes |
| The compositor's toplevel bounds are widened by the insets before the
clamp | `0426af9` | no: weston never sends bounds |
| The input band is a creation property,
`SDL_PROP_WINDOW_CREATE_WAYLAND_FRAME_INPUT_MARGIN_NUMBER` (default 8) |
`91b7a29` | yes |
| The insets are published from creation, not only after show |
`883f5c8` | yes |
| A window hidden and shown again is framed again | `011a2dc` | yes (new
bug, found while testing) |

Fork PRs: fizzyedit/SDL#4 → `fizzy-3.4.16-9`, fizzyedit/sdl_zig#4 →
`fizzy-1.0.3+3.4.16-9`.
- [Red](https://github.com/fizzyedit/sdl_zig/actions/runs/38000927122):
with only the test commit, every new assertion fails at scale 1 and 2.
-
[Green](https://github.com/fizzyedit/sdl_zig/actions/runs/38001031572):
with the fixes, all 9 Wayland tests pass at both scales.

Fizzy's own use (`linux_titlebar.zig`) is unchanged: it keeps the
default band, and its insets now also survive a recreate.

**For later:** upstream SDL `main` gained its own version of frame
insets on 2026-09-24. It isn't in `release-3.4.x`, and it has none of
these fixes. `docs/DEPENDENCIES.md` now says to move fizzy to upstream's
API at the rebase onto a release that has it, keeping only what upstream
lacks as patches.

**#312 overlaps:** it moves this pin from the root `build.zig.zon` into
`backend/build.zig.zon`. Whichever PR lands second carries the `-9` pin
into the other's location.

## Verified

On macOS (arm64), in a fresh workspace on `main`:

| Gate | Result |
|---|---|
| `zig build` | 43/43 steps |
| `zig build test` | 75/75 steps |
| `zig build test-integration` | 35/35 steps, 354/354 tests |
| `zig build check-web` | 16/16 steps |
| `zig build test-sdk-version` | 12/12 steps |
| `zig build -Dtarget=x86_64-linux-gnu` | 54/54 steps (builds the
changed Wayland code) |

Not verified: the fixes on a real GNOME desktop with fizzy itself. The
evidence is the fork's suite under headless weston. The SDK fingerprint
doesn't move, since plugins never link SDL.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant