From 1eb90b75e7e28161dbf212b6de550c7c875f5e55 Mon Sep 17 00:00:00 2001 From: foxnne Date: Fri, 9 Oct 2026 15:01:33 -0500 Subject: [PATCH 1/7] test: the frame-insets suite checks what patch 6 still gets wrong 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 --- test/testautomation_fizzy_wayland.c | 453 ++++++++++++++++++++++++++-- 1 file changed, 421 insertions(+), 32 deletions(-) diff --git a/test/testautomation_fizzy_wayland.c b/test/testautomation_fizzy_wayland.c index a16d567c59f59..c02bcb9cd738d 100644 --- a/test/testautomation_fizzy_wayland.c +++ b/test/testautomation_fizzy_wayland.c @@ -5,10 +5,13 @@ * decorations": SDL_PROP_WINDOW_CREATE_WAYLAND_FRAME_INSET_*_NUMBER give the * margins of an xdg-shell toplevel's surface outside its frame. While the * window floats, the window geometry is the frame, the input region is the - * frame and a band of at most 8 units round it, the min/max sizes are the - * frame's and popups anchor to the frame; maximized or fullscreen the insets - * are zero. The insets in effect are published as - * SDL_PROP_WINDOW_WAYLAND_FRAME_INSET_*_NUMBER. + * frame and a band round it (8 units, or + * SDL_PROP_WINDOW_CREATE_WAYLAND_FRAME_INPUT_MARGIN_NUMBER), an opaque + * window's opaque region is the frame, the min/max sizes are the frame's and + * popups anchor to the frame; maximized or fullscreen the insets are zero. The + * insets in effect are published as SDL_PROP_WINDOW_WAYLAND_FRAME_INSET_*_NUMBER + * from creation on. A window SDL makes again (SDL_RecreateWindow) or shows + * again keeps all of it. * * What SDL asks of the compositor is read from libwayland's own trace of the * requests it sends (WAYLAND_DEBUG=client), not from SDL's state: each test @@ -17,7 +20,9 @@ * objects (found by id through wl_proxy_get_id). * * The tests run only on the Wayland video driver, and skip elsewhere. They need - * a GPU driver to present frames with (lavapipe will do), so the windows map. + * a GPU driver to present frames with (lavapipe will do), so the windows map, + * and wayland_frameInsetsRecreated needs OpenGL ES through EGL (Mesa's + * llvmpipe will do). */ #include #include @@ -45,6 +50,9 @@ #define SDL_PROP_WINDOW_WAYLAND_FRAME_INSET_RIGHT_NUMBER "SDL.window.wayland.frame_inset.right" #define SDL_PROP_WINDOW_WAYLAND_FRAME_INSET_BOTTOM_NUMBER "SDL.window.wayland.frame_inset.bottom" #endif +#ifndef SDL_PROP_WINDOW_CREATE_WAYLAND_FRAME_INPUT_MARGIN_NUMBER +#define SDL_PROP_WINDOW_CREATE_WAYLAND_FRAME_INPUT_MARGIN_NUMBER "SDL.window.create.wayland.frame_input_margin" +#endif #ifdef FIZZY_WAYLAND_TRACE @@ -83,6 +91,12 @@ typedef struct bool input_subtracted; TraceRect input; /* the first rectangle added */ + bool opaque_set; + bool opaque_none; /* wl_surface.set_opaque_region(nil) */ + int opaque_rects; + bool opaque_subtracted; + TraceRect opaque; + bool min_set, max_set; int min_w, min_h, max_w, max_h; /* xdg_toplevel.set_{min,max}_size */ @@ -241,11 +255,19 @@ static int TraceStart(void) return TEST_COMPLETED; } -/* A line in the trace, which a test's checkpoint names. */ +/* A line in the trace, which a test's checkpoint names, and on the real stderr, + * so a test that hangs while the trace is on shows how far it got. */ static void TraceMark(const char *mark) { + char line[160]; + int length; + fprintf(stderr, "\nfizzy-mark: %s\n", mark); fflush(stderr); + length = SDL_snprintf(line, sizeof(line), "fizzy-mark: %s\n", mark); + if (saved_stderr >= 0 && length > 0) { + (void)!write(saved_stderr, line, (size_t)SDL_min(length, (int)sizeof(line) - 1)); + } } static const char *TraceFindMark(const char *mark) @@ -374,18 +396,20 @@ static TraceRegion *FindRegion(TraceRegion *regions, int *count, Uint32 id, bool /** * Reads the requests sent (and the toplevel configure events received) on a - * window's objects from the start of the trace up to `mark`, keeping the last - * of each. + * window's objects from the mark `from` (the start of the trace if NULL) up + * to `mark`, keeping the last of each. A window whose objects were made again + * (shown again, or recreated) is read from a mark set before that, as + * libwayland reuses the ids of destroyed objects. */ -static bool ParseTrace(const char *mark, const TraceIds *ids, TraceState *state) +static bool ParseTrace(const char *from, const char *mark, const TraceIds *ids, TraceState *state) { TraceRegion regions[MAX_REGIONS]; int region_count = 0; const char *end = TraceFindMark(mark); - const char *p = trace_text; + const char *p = from ? TraceFindMark(from) : trace_text; SDL_zerop(state); - if (!end) { + if (!end || !p || p > end) { return false; } @@ -492,6 +516,18 @@ static bool ParseTrace(const char *mark, const TraceIds *ids, TraceState *state) state->input_subtracted = region->subtracted; state->input = region->first; } + } else if (id == ids->surface && SDL_strcmp(iface, "wl_surface") == 0 && SDL_strcmp(name, "set_opaque_region") == 0) { + const Uint32 region_id = ParseObjectArg(args); + state->opaque_set = true; + state->opaque_none = region_id == 0; + state->opaque_rects = 0; + state->opaque_subtracted = false; + if (region_id) { + const TraceRegion *region = FindRegion(regions, ®ion_count, region_id, false); + state->opaque_rects = region->rects; + state->opaque_subtracted = region->subtracted; + state->opaque = region->first; + } } else if (id == ids->xdg_surface && SDL_strcmp(iface, "xdg_surface") == 0 && SDL_strcmp(name, "set_window_geometry") == 0) { int v[4]; if (ParseInts(args, v, 4) == 4) { @@ -559,7 +595,7 @@ static void LogTrace(const TraceIds *ids) if (ids->positioner) { SDL_snprintf(objects[count++], sizeof(objects[0]), "xdg_positioner#%" SDL_PRIu32 ".", ids->positioner); } - SDL_snprintf(objects[count++], sizeof(objects[0]), "wl_surface#%" SDL_PRIu32 ".set_input_region", ids->surface); + SDL_snprintf(objects[count++], sizeof(objects[0]), "wl_surface#%" SDL_PRIu32 ".set_", ids->surface); while (*p) { const char *eol = SDL_strchr(p, '\n'); @@ -656,16 +692,12 @@ typedef struct bool claimed; } TestWindow; -/* Makes a resizable, borderless window with the insets, as fizzy's Linux - * backend does, shows it and presents into it. Nothing is asserted here, as - * stderr is the trace: the caller checks after TraceStop. */ -static void CreateTestWindow(TestWindow *tw) +/* The properties of a resizable, borderless window with the insets, as fizzy's + * Linux backend makes, and an input margin unless it is negative. */ +static SDL_PropertiesID TestWindowProperties(int input_margin) { SDL_PropertiesID props = SDL_CreateProperties(); - SDL_zerop(tw); - tw->device = SDL_CreateGPUDevice(SDL_GPU_SHADERFORMAT_SPIRV | SDL_GPU_SHADERFORMAT_MSL | SDL_GPU_SHADERFORMAT_DXIL, false, NULL); - SDL_SetStringProperty(props, SDL_PROP_WINDOW_CREATE_TITLE_STRING, "testautomation_fizzy_wayland"); SDL_SetNumberProperty(props, SDL_PROP_WINDOW_CREATE_WIDTH_NUMBER, SURFACE_W); SDL_SetNumberProperty(props, SDL_PROP_WINDOW_CREATE_HEIGHT_NUMBER, SURFACE_H); @@ -675,6 +707,21 @@ static void CreateTestWindow(TestWindow *tw) SDL_SetNumberProperty(props, SDL_PROP_WINDOW_CREATE_WAYLAND_FRAME_INSET_TOP_NUMBER, INSET_TOP); SDL_SetNumberProperty(props, SDL_PROP_WINDOW_CREATE_WAYLAND_FRAME_INSET_RIGHT_NUMBER, INSET_RIGHT); SDL_SetNumberProperty(props, SDL_PROP_WINDOW_CREATE_WAYLAND_FRAME_INSET_BOTTOM_NUMBER, INSET_BOTTOM); + if (input_margin >= 0) { + SDL_SetNumberProperty(props, SDL_PROP_WINDOW_CREATE_WAYLAND_FRAME_INPUT_MARGIN_NUMBER, input_margin); + } + return props; +} + +/* Makes the test window (with an input margin unless it is negative), shows + * it and presents into it. Nothing is asserted here, as stderr is the trace: + * the caller checks after TraceStop. */ +static void CreateTestWindowWithMargin(TestWindow *tw, int input_margin) +{ + SDL_PropertiesID props = TestWindowProperties(input_margin); + + SDL_zerop(tw); + tw->device = SDL_CreateGPUDevice(SDL_GPU_SHADERFORMAT_SPIRV | SDL_GPU_SHADERFORMAT_MSL | SDL_GPU_SHADERFORMAT_DXIL, false, NULL); tw->window = SDL_CreateWindowWithProperties(props); SDL_DestroyProperties(props); @@ -686,17 +733,31 @@ static void CreateTestWindow(TestWindow *tw) } } -static void DestroyTestWindow(TestWindow *tw) +static void CreateTestWindow(TestWindow *tw) +{ + CreateTestWindowWithMargin(tw, -1); +} + +/* Releases the window from the GPU device and destroys the device, keeping the + * window. */ +static void ReleaseTestWindowDevice(TestWindow *tw) { if (tw->claimed) { SDL_WaitForGPUIdle(tw->device); SDL_ReleaseWindowFromGPUDevice(tw->device, tw->window); - } - if (tw->window) { - SDL_DestroyWindow(tw->window); + tw->claimed = false; } if (tw->device) { SDL_DestroyGPUDevice(tw->device); + tw->device = NULL; + } +} + +static void DestroyTestWindow(TestWindow *tw) +{ + ReleaseTestWindowDevice(tw); + if (tw->window) { + SDL_DestroyWindow(tw->window); } SDL_zerop(tw); } @@ -725,16 +786,31 @@ static void CheckPublishedInsets(const Checkpoint *checkpoint, const int expecte } } -/* While floating: the window geometry is the frame, inside the insets, and the - * input region the frame and a band of at most 8 units round it. */ -static void CheckFloatingFrame(const Checkpoint *checkpoint, const TraceState *state) +/* An opaque window's opaque region: one rectangle, the one given. */ +static void CheckOpaqueRegion(const Checkpoint *checkpoint, const TraceState *state, int x, int y, int w, int h, const char *what) +{ + SDLTest_AssertCheck(state->opaque_set && !state->opaque_none && state->opaque_rects == 1 && !state->opaque_subtracted, + "%s: validate that the opaque region set is one rectangle, got: %s, %d rectangles%s", + checkpoint->mark, !state->opaque_set ? "never set" : state->opaque_none ? "none (nil)" : "a region", + state->opaque_rects, state->opaque_subtracted ? ", with subtractions" : ""); + if (state->opaque_set && state->opaque_rects == 1) { + SDLTest_AssertCheck(state->opaque.x == x && state->opaque.y == y && state->opaque.w == w && state->opaque.h == h, + "%s: validate the opaque region is %s, expected: (%d, %d, %d, %d), got: (%d, %d, %d, %d)", + checkpoint->mark, what, x, y, w, h, state->opaque.x, state->opaque.y, state->opaque.w, state->opaque.h); + } +} + +/* While floating: the window geometry is the frame, inside the insets, the + * input region the frame and a band of at most `band` units round it, and the + * opaque region (the test window is opaque) the frame, not its shadow. */ +static void CheckFloatingFrameWithBand(const Checkpoint *checkpoint, const TraceState *state, int band) { const int frame_w = checkpoint->w - INSET_LEFT - INSET_RIGHT; const int frame_h = checkpoint->h - INSET_TOP - INSET_BOTTOM; - const int band_l = SDL_min(INSET_LEFT, INPUT_BAND); - const int band_t = SDL_min(INSET_TOP, INPUT_BAND); - const int band_r = SDL_min(INSET_RIGHT, INPUT_BAND); - const int band_b = SDL_min(INSET_BOTTOM, INPUT_BAND); + const int band_l = SDL_min(INSET_LEFT, band); + const int band_t = SDL_min(INSET_TOP, band); + const int band_r = SDL_min(INSET_RIGHT, band); + const int band_b = SDL_min(INSET_BOTTOM, band); CheckPublishedInsets(checkpoint, insets_asked); @@ -755,8 +831,15 @@ static void CheckFloatingFrame(const Checkpoint *checkpoint, const TraceState *s const int w = frame_w + band_l + band_r, h = frame_h + band_t + band_b; SDLTest_AssertCheck(state->input.x == x && state->input.y == y && state->input.w == w && state->input.h == h, "%s: validate the input region is the frame and a band of at most %d, expected: (%d, %d, %d, %d), got: (%d, %d, %d, %d)", - checkpoint->mark, INPUT_BAND, x, y, w, h, state->input.x, state->input.y, state->input.w, state->input.h); + checkpoint->mark, band, x, y, w, h, state->input.x, state->input.y, state->input.w, state->input.h); } + + CheckOpaqueRegion(checkpoint, state, INSET_LEFT, INSET_TOP, frame_w, frame_h, "the frame, without the shadow"); +} + +static void CheckFloatingFrame(const Checkpoint *checkpoint, const TraceState *state) +{ + CheckFloatingFrameWithBand(checkpoint, state, INPUT_BAND); } /* Maximized or fullscreen: no insets, the window geometry the whole surface @@ -781,11 +864,19 @@ static void CheckFilledFrame(const Checkpoint *checkpoint, const TraceState *sta SDLTest_AssertCheck(state->input_set && state->input_infinite, "%s: validate that the input region is the whole surface (nil), got: %s", checkpoint->mark, !state->input_set ? "never set" : state->input_infinite ? "nil" : "a region"); + CheckOpaqueRegion(checkpoint, state, 0, 0, checkpoint->w, checkpoint->h, "the whole surface"); } static bool ParseAt(const Checkpoint *checkpoint, const TraceIds *ids, TraceState *state) { - return SDLTest_AssertCheck(ParseTrace(checkpoint->mark, ids, state), "Validate that the trace holds the mark '%s'", checkpoint->mark); + return SDLTest_AssertCheck(ParseTrace(NULL, checkpoint->mark, ids, state), "Validate that the trace holds the mark '%s'", checkpoint->mark); +} + +/* Reads the trace from the mark `from`: for a window whose objects were made + * again after it. */ +static bool ParseSince(const char *from, const Checkpoint *checkpoint, const TraceIds *ids, TraceState *state) +{ + return SDLTest_AssertCheck(ParseTrace(from, checkpoint->mark, ids, state), "Validate that the trace holds the marks '%s' and then '%s'", from, checkpoint->mark); } /* Maximizes (or fullscreens) a floating window and restores it. */ @@ -857,6 +948,44 @@ static int frameInsetsFillAndRestore(bool fullscreen) return TEST_COMPLETED; } +/* A floating window made with the input margin given: the input region is the + * frame and a band of min(inset, margin) on each side. */ +static int frameInsetsInputMargin(int margin) +{ + TestWindow tw; + TraceIds ids; + Checkpoint shown = { 0 }; + TraceState state; + bool made, device, claimed; + int result = TraceStart(); + + if (result != TEST_COMPLETED) { + return result < 0 ? TEST_COMPLETED : result; + } + SDL_zero(state); + + CreateTestWindowWithMargin(&tw, margin); + made = tw.window != NULL; + device = tw.device != NULL; + claimed = tw.claimed; + SDL_zero(ids); + if (claimed) { + GetIds(tw.window, &ids); + TakeCheckpoint(tw.window, "shown", &shown); + } + DestroyTestWindow(&tw); + TraceStop(); + + if (CheckTestWindow(made, device, claimed, &ids)) { + if (ParseAt(&shown, &ids, &state)) { + CheckFloatingFrameWithBand(&shown, &state, margin); + } + } + LogTrace(&ids); + TraceFree(); + return TEST_COMPLETED; +} + #endif /* FIZZY_WAYLAND_TRACE */ /* Test case functions */ @@ -1044,6 +1173,241 @@ static int SDLCALL wayland_frameInsetsPopup(void *arg) #endif } +/** + * The insets are published from creation on: a window made hidden has them + * before it is ever shown, the ones it will float with. + */ +static int SDLCALL wayland_frameInsetsBeforeShow(void *arg) +{ +#ifdef FIZZY_WAYLAND_TRACE + SDL_PropertiesID props; + SDL_Window *window; + Checkpoint created = { 0 }; + int result = TraceStart(); + + if (result != TEST_COMPLETED) { + return result < 0 ? TEST_COMPLETED : result; + } + + props = TestWindowProperties(-1); + SDL_SetBooleanProperty(props, SDL_PROP_WINDOW_CREATE_HIDDEN_BOOLEAN, true); + window = SDL_CreateWindowWithProperties(props); + SDL_DestroyProperties(props); + if (window) { + TakeCheckpoint(window, "created hidden", &created); + SDL_DestroyWindow(window); + } + TraceStop(); + + if (SDLTest_AssertCheck(window != NULL, "Validate that the window was made")) { + SDLTest_AssertCheck((created.flags & SDL_WINDOW_HIDDEN) != 0, "%s: validate that the window is hidden", created.mark); + CheckPublishedInsets(&created, insets_asked); + } + TraceFree(); + return TEST_COMPLETED; +#else + SDLTest_Log("Wayland only, skipping test"); + return TEST_SKIPPED; +#endif +} + +/** + * A window SDL makes again keeps its insets. SDL_RecreateWindow destroys the + * native window and makes it again with no creation properties; attaching an + * OpenGL renderer to a shown window made without SDL_WINDOW_OPENGL does so. + * After that the new objects' window geometry, input region and opaque region + * are the frame's again. + */ +static int SDLCALL wayland_frameInsetsRecreated(void *arg) +{ +#ifdef FIZZY_WAYLAND_TRACE + TestWindow tw; + SDL_Renderer *renderer = NULL; + TraceIds ids, recreated_ids; + Checkpoint shown = { 0 }, recreated = { 0 }; + TraceState state; + bool made, device, claimed; + char renderer_error[256] = "none"; + int result = TraceStart(); + + if (result != TEST_COMPLETED) { + return result < 0 ? TEST_COMPLETED : result; + } + SDL_zero(state); + + CreateTestWindow(&tw); + made = tw.window != NULL; + device = tw.device != NULL; + claimed = tw.claimed; + SDL_zero(ids); + SDL_zero(recreated_ids); + if (claimed) { + int i; + + GetIds(tw.window, &ids); + TakeCheckpoint(tw.window, "shown", &shown); + + ReleaseTestWindowDevice(&tw); + TraceMark("recreating"); + renderer = SDL_CreateRenderer(tw.window, "opengles2"); + if (!renderer) { + SDL_strlcpy(renderer_error, SDL_GetError(), sizeof(renderer_error)); + } + if (renderer) { + for (i = 0; i < 10; ++i) { + SDL_Event event; + while (SDL_PollEvent(&event)) { + } + SDL_SetRenderDrawColor(renderer, 51, 76, 102, 255); + SDL_RenderClear(renderer); + SDL_RenderPresent(renderer); + SDL_Delay(5); + } + SDL_SyncWindow(tw.window); + GetIds(tw.window, &recreated_ids); + TakeCheckpoint(tw.window, "recreated", &recreated); + SDL_DestroyRenderer(renderer); + } + } + DestroyTestWindow(&tw); + TraceStop(); + + if (CheckTestWindow(made, device, claimed, &ids)) { + if (ParseAt(&shown, &ids, &state)) { + CheckFloatingFrame(&shown, &state); + } + + SDLTest_AssertCheck(renderer != NULL, "Validate that an OpenGL ES renderer was made for the window, got error: %s", renderer_error); + if (renderer) { + SDLTest_AssertCheck((recreated.flags & SDL_WINDOW_OPENGL) && !(recreated.flags & SDL_WINDOW_VULKAN), + "%s: validate that the window was made again for OpenGL, got flags: 0x%" SDL_PRIx64, + recreated.mark, (Uint64)recreated.flags); + SDLTest_AssertCheck(recreated_ids.xdg_surface != 0 && recreated_ids.toplevel != 0, + "%s: validate that the window made again is an xdg-shell toplevel, got xdg_surface %" SDL_PRIu32 ", xdg_toplevel %" SDL_PRIu32, + recreated.mark, recreated_ids.xdg_surface, recreated_ids.toplevel); + SDLTest_AssertCheck(recreated.w == SURFACE_W && recreated.h == SURFACE_H, + "%s: validate the surface is the size made, expected: %dx%d, got: %dx%d", + recreated.mark, SURFACE_W, SURFACE_H, recreated.w, recreated.h); + if (ParseSince("recreating", &recreated, &recreated_ids, &state)) { + CheckFloatingFrame(&recreated, &state); + } + } + } + LogTrace(&recreated_ids); + TraceFree(); + return TEST_COMPLETED; +#else + SDLTest_Log("Wayland only, skipping test"); + return TEST_SKIPPED; +#endif +} + +/** + * A window hidden and shown again gets a new xdg_surface, whose window + * geometry must be the frame again although the size and the insets have not + * changed. + */ +static int SDLCALL wayland_frameInsetsShownAgain(void *arg) +{ +#ifdef FIZZY_WAYLAND_TRACE + TestWindow tw; + TraceIds ids, again_ids; + Checkpoint shown = { 0 }, again = { 0 }; + TraceState state; + bool made, device, claimed, reclaimed = false; + int result = TraceStart(); + + if (result != TEST_COMPLETED) { + return result < 0 ? TEST_COMPLETED : result; + } + SDL_zero(state); + + CreateTestWindow(&tw); + made = tw.window != NULL; + device = tw.device != NULL; + claimed = tw.claimed; + SDL_zero(ids); + SDL_zero(again_ids); + if (claimed) { + SDL_Event event; + + GetIds(tw.window, &ids); + TakeCheckpoint(tw.window, "shown", &shown); + + /* Out of the GPU device while hidden: a swapchain waiting on frames of an unmapped + * surface can block for good. */ + SDL_WaitForGPUIdle(tw.device); + SDL_ReleaseWindowFromGPUDevice(tw.device, tw.window); + tw.claimed = false; + + SDL_HideWindow(tw.window); + SDL_SyncWindow(tw.window); + while (SDL_PollEvent(&event)) { + } + TraceMark("hidden"); + + SDL_ShowWindow(tw.window); + TraceMark("showing again"); + tw.claimed = SDL_ClaimWindowForGPUDevice(tw.device, tw.window); + reclaimed = tw.claimed; + if (reclaimed) { + Settle(tw.device, tw.window); + } + GetIds(tw.window, &again_ids); + TakeCheckpoint(tw.window, "shown again", &again); + } + DestroyTestWindow(&tw); + TraceStop(); + + if (CheckTestWindow(made, device, claimed, &ids)) { + if (ParseAt(&shown, &ids, &state)) { + CheckFloatingFrame(&shown, &state); + } + SDLTest_AssertCheck(reclaimed, "%s: validate that the window was claimed for the GPU device again", again.mark); + SDLTest_AssertCheck((again.flags & SDL_WINDOW_HIDDEN) == 0, "%s: validate that the window is shown", again.mark); + SDLTest_AssertCheck(again_ids.xdg_surface != 0 && again_ids.toplevel != 0, + "%s: validate that the window is an xdg-shell toplevel again, got xdg_surface %" SDL_PRIu32 ", xdg_toplevel %" SDL_PRIu32, + again.mark, again_ids.xdg_surface, again_ids.toplevel); + if (again_ids.xdg_surface && ParseSince("hidden", &again, &again_ids, &state)) { + CheckFloatingFrame(&again, &state); + } + } + LogTrace(&again_ids); + TraceFree(); + return TEST_COMPLETED; +#else + SDLTest_Log("Wayland only, skipping test"); + return TEST_SKIPPED; +#endif +} + +/** + * SDL_PROP_WINDOW_CREATE_WAYLAND_FRAME_INPUT_MARGIN_NUMBER widens the input + * band past the default 8, up to each inset. + */ +static int SDLCALL wayland_frameInsetsInputMarginWide(void *arg) +{ +#ifdef FIZZY_WAYLAND_TRACE + return frameInsetsInputMargin(12); +#else + SDLTest_Log("Wayland only, skipping test"); + return TEST_SKIPPED; +#endif +} + +/** + * An input margin of 0: only the frame takes input. + */ +static int SDLCALL wayland_frameInsetsInputMarginNone(void *arg) +{ +#ifdef FIZZY_WAYLAND_TRACE + return frameInsetsInputMargin(0); +#else + SDLTest_Log("Wayland only, skipping test"); + return TEST_SKIPPED; +#endif +} + /* ================= Test References ================== */ static const SDLTest_TestCaseReference waylandTestFrameInsetsFloating = { @@ -1062,11 +1426,36 @@ static const SDLTest_TestCaseReference waylandTestFrameInsetsPopup = { wayland_frameInsetsPopup, "wayland_frameInsetsPopup", "A popup is anchored to its parent's frame", TEST_ENABLED }; +static const SDLTest_TestCaseReference waylandTestFrameInsetsBeforeShow = { + wayland_frameInsetsBeforeShow, "wayland_frameInsetsBeforeShow", "A window's insets are published before it is shown", TEST_ENABLED +}; + +static const SDLTest_TestCaseReference waylandTestFrameInsetsRecreated = { + wayland_frameInsetsRecreated, "wayland_frameInsetsRecreated", "A window SDL makes again (SDL_RecreateWindow) keeps its insets", TEST_ENABLED +}; + +static const SDLTest_TestCaseReference waylandTestFrameInsetsShownAgain = { + wayland_frameInsetsShownAgain, "wayland_frameInsetsShownAgain", "A window hidden and shown again is framed again", TEST_ENABLED +}; + +static const SDLTest_TestCaseReference waylandTestFrameInsetsInputMarginWide = { + wayland_frameInsetsInputMarginWide, "wayland_frameInsetsInputMarginWide", "The input band follows a wider input margin", TEST_ENABLED +}; + +static const SDLTest_TestCaseReference waylandTestFrameInsetsInputMarginNone = { + wayland_frameInsetsInputMarginNone, "wayland_frameInsetsInputMarginNone", "With an input margin of 0 only the frame takes input", TEST_ENABLED +}; + static const SDLTest_TestCaseReference *waylandTests[] = { &waylandTestFrameInsetsFloating, &waylandTestFrameInsetsMaximized, &waylandTestFrameInsetsFullscreen, &waylandTestFrameInsetsPopup, + &waylandTestFrameInsetsBeforeShow, + &waylandTestFrameInsetsRecreated, + &waylandTestFrameInsetsShownAgain, + &waylandTestFrameInsetsInputMarginWide, + &waylandTestFrameInsetsInputMarginNone, NULL }; From 925828871c3fb2f0496423b618b243db4310105a Mon Sep 17 00:00:00 2001 From: foxnne Date: Fri, 9 Oct 2026 15:01:43 -0500 Subject: [PATCH 2/7] Wayland: a window made again keeps its frame insets 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 5d7167cb), 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 --- include/SDL3/SDL_video.h | 3 ++- src/video/wayland/SDL_waylandwindow.c | 31 ++++++++++++++++++++++----- 2 files changed, 28 insertions(+), 6 deletions(-) diff --git a/include/SDL3/SDL_video.h b/include/SDL3/SDL_video.h index 76877b9bd0362..cffe25a4c7639 100644 --- a/include/SDL3/SDL_video.h +++ b/include/SDL3/SDL_video.h @@ -1366,7 +1366,8 @@ extern SDL_DECLSPEC SDL_Window * SDLCALL SDL_CreatePopupWindow(SDL_Window *paren * the rest of the shadow passes clicks through. Maximized, tiled or * fullscreen, the margins are zero (see * `SDL_PROP_WINDOW_WAYLAND_FRAME_INSET_LEFT_NUMBER`). xdg-shell windows - * only, not libdecor's. + * only, not libdecor's. The window keeps them if SDL has to make it again + * (as for an OpenGL renderer on a window made without `SDL_WINDOW_OPENGL`). * * These are additional supported properties on Windows: * diff --git a/src/video/wayland/SDL_waylandwindow.c b/src/video/wayland/SDL_waylandwindow.c index 4eabc97a7e541..7179bcdc31938 100644 --- a/src/video/wayland/SDL_waylandwindow.c +++ b/src/video/wayland/SDL_waylandwindow.c @@ -169,6 +169,24 @@ static bool HasFrameInsets(SDL_WindowData *wind) return wind->frame_insets.left || wind->frame_insets.top || wind->frame_insets.right || wind->frame_insets.bottom; } +/* The frame insets asked for at creation are kept in the window's properties as well, which + * outlive the window data: SDL_RecreateWindow makes the window again with no creation + * properties, and the window keeps its insets. + */ +#define WAYLAND_KEPT_FRAME_INSET_LEFT "SDL.internal.wayland.frame_inset.left" +#define WAYLAND_KEPT_FRAME_INSET_TOP "SDL.internal.wayland.frame_inset.top" +#define WAYLAND_KEPT_FRAME_INSET_RIGHT "SDL.internal.wayland.frame_inset.right" +#define WAYLAND_KEPT_FRAME_INSET_BOTTOM "SDL.internal.wayland.frame_inset.bottom" + +static int GetKeptCreateNumber(SDL_PropertiesID create_props, SDL_PropertiesID window_props, const char *create_name, const char *kept_name) +{ + const Sint64 kept = SDL_GetNumberProperty(window_props, kept_name, 0); + const int value = SDL_max((int)SDL_GetNumberProperty(create_props, create_name, kept), 0); + + SDL_SetNumberProperty(window_props, kept_name, value); + return value; +} + /* Tells the compositor the frame is the window, takes input on the frame and a band around it * only, and publishes the insets in effect. Called whenever the size or the insets change. */ @@ -2846,12 +2864,15 @@ bool Wayland_CreateWindow(SDL_VideoDevice *_this, SDL_Window *window, SDL_Proper // Default to all capabilities data->wm_caps = WAYLAND_WM_CAPS_ALL; - // Margins outside the frame for a shadow the application draws; toplevels only. + /* Margins outside the frame for a shadow the application draws; toplevels only. A window + * made again by SDL_RecreateWindow has no creation properties, and keeps those it had. + */ if (!SDL_WINDOW_IS_POPUP(window) && !custom_surface_role) { - data->frame_insets.left = SDL_max((int)SDL_GetNumberProperty(create_props, SDL_PROP_WINDOW_CREATE_WAYLAND_FRAME_INSET_LEFT_NUMBER, 0), 0); - data->frame_insets.top = SDL_max((int)SDL_GetNumberProperty(create_props, SDL_PROP_WINDOW_CREATE_WAYLAND_FRAME_INSET_TOP_NUMBER, 0), 0); - data->frame_insets.right = SDL_max((int)SDL_GetNumberProperty(create_props, SDL_PROP_WINDOW_CREATE_WAYLAND_FRAME_INSET_RIGHT_NUMBER, 0), 0); - data->frame_insets.bottom = SDL_max((int)SDL_GetNumberProperty(create_props, SDL_PROP_WINDOW_CREATE_WAYLAND_FRAME_INSET_BOTTOM_NUMBER, 0), 0); + const SDL_PropertiesID window_props = SDL_GetWindowProperties(window); + data->frame_insets.left = GetKeptCreateNumber(create_props, window_props, SDL_PROP_WINDOW_CREATE_WAYLAND_FRAME_INSET_LEFT_NUMBER, WAYLAND_KEPT_FRAME_INSET_LEFT); + data->frame_insets.top = GetKeptCreateNumber(create_props, window_props, SDL_PROP_WINDOW_CREATE_WAYLAND_FRAME_INSET_TOP_NUMBER, WAYLAND_KEPT_FRAME_INSET_TOP); + data->frame_insets.right = GetKeptCreateNumber(create_props, window_props, SDL_PROP_WINDOW_CREATE_WAYLAND_FRAME_INSET_RIGHT_NUMBER, WAYLAND_KEPT_FRAME_INSET_RIGHT); + data->frame_insets.bottom = GetKeptCreateNumber(create_props, window_props, SDL_PROP_WINDOW_CREATE_WAYLAND_FRAME_INSET_BOTTOM_NUMBER, WAYLAND_KEPT_FRAME_INSET_BOTTOM); } data->scale_factor = 1.0; From 0ca75f2e463c2e241d3d948acf90feabdc77a442 Mon Sep 17 00:00:00 2001 From: foxnne Date: Fri, 9 Oct 2026 15:03:45 -0500 Subject: [PATCH 3/7] Wayland: an opaque window's opaque region is its frame, not its shadow 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 --- include/SDL3/SDL_video.h | 3 ++- src/video/wayland/SDL_waylandwindow.c | 8 ++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/include/SDL3/SDL_video.h b/include/SDL3/SDL_video.h index cffe25a4c7639..bf92fa37ddddd 100644 --- a/include/SDL3/SDL_video.h +++ b/include/SDL3/SDL_video.h @@ -1363,7 +1363,8 @@ extern SDL_DECLSPEC SDL_Window * SDLCALL SDL_CreatePopupWindow(SDL_Window *paren * (`xdg_surface.set_window_geometry`), so it places, snaps and tiles the * frame; the surface grows by the margins around the size it configures; * and only the frame and a band of up to 8 units around it take input, so - * the rest of the shadow passes clicks through. Maximized, tiled or + * the rest of the shadow passes clicks through; the opaque region of a + * window that isn't `SDL_WINDOW_TRANSPARENT` is the frame. Maximized, tiled or * fullscreen, the margins are zero (see * `SDL_PROP_WINDOW_WAYLAND_FRAME_INSET_LEFT_NUMBER`). xdg-shell windows * only, not libdecor's. The window keeps them if SDL has to make it again diff --git a/src/video/wayland/SDL_waylandwindow.c b/src/video/wayland/SDL_waylandwindow.c index 7179bcdc31938..d01e5017e2b81 100644 --- a/src/video/wayland/SDL_waylandwindow.c +++ b/src/video/wayland/SDL_waylandwindow.c @@ -400,9 +400,13 @@ static void SetSurfaceOpaqueRegion(SDL_WindowData *wind, bool is_opaque) SDL_VideoData *viddata = wind->waylandData; if (is_opaque) { + // Only the frame: any frame insets around it hold a shadow, which is never opaque. + const int left = wind->applied_frame_insets.left; + const int top = wind->applied_frame_insets.top; + const int width = SDL_max(wind->current.logical_width - left - wind->applied_frame_insets.right, 0); + const int height = SDL_max(wind->current.logical_height - top - wind->applied_frame_insets.bottom, 0); struct wl_region *region = wl_compositor_create_region(viddata->compositor); - wl_region_add(region, 0, 0, - wind->current.logical_width, wind->current.logical_height); + wl_region_add(region, left, top, width, height); wl_surface_set_opaque_region(wind->surface, region); wl_region_destroy(region); } else { From 0426af903b1c5f061399712ad9997dab8ff0c2da Mon Sep 17 00:00:00 2001 From: foxnne Date: Fri, 9 Oct 2026 15:04:07 -0500 Subject: [PATCH 4/7] Wayland: clamp a framed window to the toplevel bounds by its frame 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 --- src/video/wayland/SDL_waylandwindow.c | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/video/wayland/SDL_waylandwindow.c b/src/video/wayland/SDL_waylandwindow.c index d01e5017e2b81..fadb5132dfcb4 100644 --- a/src/video/wayland/SDL_waylandwindow.c +++ b/src/video/wayland/SDL_waylandwindow.c @@ -992,11 +992,15 @@ static void handle_xdg_toplevel_configure(void *data, width = window->floating.w; height = window->floating.h; - // Clamp the window to the toplevel bounds, if any are set. + /* Clamp the window to the toplevel bounds, if any are set. They bound the + * frame, so the surface may be larger by its insets. + */ if (wind->shell_surface_status == WAYLAND_SHELL_SURFACE_STATUS_WAITING_FOR_CONFIGURE && wind->toplevel_bounds.width && wind->toplevel_bounds.height) { - width = SDL_min(wind->toplevel_bounds.width, width); - height = SDL_min(wind->toplevel_bounds.height, height); + int il, it, ir, ib; + GetFrameInsets(wind, floating, &il, &it, &ir, &ib); + width = SDL_min(wind->toplevel_bounds.width + il + ir, width); + height = SDL_min(wind->toplevel_bounds.height + it + ib, height); } } else { width = window->windowed.w; From 91b7a295f8e6f61a6a81843617786904bcb09e46 Mon Sep 17 00:00:00 2001 From: foxnne Date: Fri, 9 Oct 2026 15:04:44 -0500 Subject: [PATCH 5/7] Wayland: the input band round a framed window is a creation property 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 --- include/SDL3/SDL_video.h | 14 ++++++++--- src/video/wayland/SDL_waylandwindow.c | 35 +++++++++++++++------------ src/video/wayland/SDL_waylandwindow.h | 2 ++ 3 files changed, 32 insertions(+), 19 deletions(-) diff --git a/include/SDL3/SDL_video.h b/include/SDL3/SDL_video.h index bf92fa37ddddd..72713e89e7d71 100644 --- a/include/SDL3/SDL_video.h +++ b/include/SDL3/SDL_video.h @@ -1362,13 +1362,20 @@ extern SDL_DECLSPEC SDL_Window * SDLCALL SDL_CreatePopupWindow(SDL_Window *paren * the compositor is told the frame is the window * (`xdg_surface.set_window_geometry`), so it places, snaps and tiles the * frame; the surface grows by the margins around the size it configures; - * and only the frame and a band of up to 8 units around it take input, so - * the rest of the shadow passes clicks through; the opaque region of a - * window that isn't `SDL_WINDOW_TRANSPARENT` is the frame. Maximized, tiled or + * only the frame and a band around it take input (see + * `SDL_PROP_WINDOW_CREATE_WAYLAND_FRAME_INPUT_MARGIN_NUMBER`), so the rest + * of the shadow passes clicks through; and the opaque region of a window + * that isn't `SDL_WINDOW_TRANSPARENT` is the frame. Maximized, tiled or * fullscreen, the margins are zero (see * `SDL_PROP_WINDOW_WAYLAND_FRAME_INSET_LEFT_NUMBER`). xdg-shell windows * only, not libdecor's. The window keeps them if SDL has to make it again * (as for an OpenGL renderer on a window made without `SDL_WINDOW_OPENGL`). + * - `SDL_PROP_WINDOW_CREATE_WAYLAND_FRAME_INPUT_MARGIN_NUMBER` - the width, in + * window coordinates, of the band around the frame that still takes input + * while the window has frame insets, where the application's hit test (see + * SDL_SetWindowHitTest()) can answer the edges it resizes from. Each side's + * band is at most that side's inset. Defaults to 8; 0 leaves only the + * frame taking input. * * These are additional supported properties on Windows: * @@ -1462,6 +1469,7 @@ extern SDL_DECLSPEC SDL_Window * SDLCALL SDL_CreateWindowWithProperties(SDL_Prop #define SDL_PROP_WINDOW_CREATE_WAYLAND_FRAME_INSET_TOP_NUMBER "SDL.window.create.wayland.frame_inset.top" #define SDL_PROP_WINDOW_CREATE_WAYLAND_FRAME_INSET_RIGHT_NUMBER "SDL.window.create.wayland.frame_inset.right" #define SDL_PROP_WINDOW_CREATE_WAYLAND_FRAME_INSET_BOTTOM_NUMBER "SDL.window.create.wayland.frame_inset.bottom" +#define SDL_PROP_WINDOW_CREATE_WAYLAND_FRAME_INPUT_MARGIN_NUMBER "SDL.window.create.wayland.frame_input_margin" #define SDL_PROP_WINDOW_CREATE_WIN32_HWND_POINTER "SDL.window.create.win32.hwnd" #define SDL_PROP_WINDOW_CREATE_WIN32_PIXEL_FORMAT_HWND_POINTER "SDL.window.create.win32.pixel_format_hwnd" #define SDL_PROP_WINDOW_CREATE_X11_WINDOW_NUMBER "SDL.window.create.x11.window" diff --git a/src/video/wayland/SDL_waylandwindow.c b/src/video/wayland/SDL_waylandwindow.c index fadb5132dfcb4..f2b777a7a0a34 100644 --- a/src/video/wayland/SDL_waylandwindow.c +++ b/src/video/wayland/SDL_waylandwindow.c @@ -145,9 +145,10 @@ static void GetBufferSize(SDL_Window *window, int *width, int *height) } } -/* The band around a frame with insets that still takes input, in window coordinates: where an - * application's hit test answers the edges it resizes from, as a decorated window's border - * would, while the rest of its shadow passes clicks through. +/* The band around a frame with insets that still takes input by default, in window coordinates: + * where an application's hit test answers the edges it resizes from, as a decorated window's + * border would, while the rest of its shadow passes clicks through + * (SDL_PROP_WINDOW_CREATE_WAYLAND_FRAME_INPUT_MARGIN_NUMBER). */ #define WAYLAND_FRAME_INPUT_MARGIN 8 @@ -169,18 +170,19 @@ static bool HasFrameInsets(SDL_WindowData *wind) return wind->frame_insets.left || wind->frame_insets.top || wind->frame_insets.right || wind->frame_insets.bottom; } -/* The frame insets asked for at creation are kept in the window's properties as well, which - * outlive the window data: SDL_RecreateWindow makes the window again with no creation - * properties, and the window keeps its insets. +/* The frame insets and input margin asked for at creation are kept in the window's properties + * as well, which outlive the window data: SDL_RecreateWindow makes the window again with no + * creation properties, and the window keeps its insets. */ #define WAYLAND_KEPT_FRAME_INSET_LEFT "SDL.internal.wayland.frame_inset.left" #define WAYLAND_KEPT_FRAME_INSET_TOP "SDL.internal.wayland.frame_inset.top" #define WAYLAND_KEPT_FRAME_INSET_RIGHT "SDL.internal.wayland.frame_inset.right" #define WAYLAND_KEPT_FRAME_INSET_BOTTOM "SDL.internal.wayland.frame_inset.bottom" +#define WAYLAND_KEPT_FRAME_INPUT_MARGIN "SDL.internal.wayland.frame_input_margin" -static int GetKeptCreateNumber(SDL_PropertiesID create_props, SDL_PropertiesID window_props, const char *create_name, const char *kept_name) +static int GetKeptCreateNumber(SDL_PropertiesID create_props, SDL_PropertiesID window_props, const char *create_name, const char *kept_name, int default_value) { - const Sint64 kept = SDL_GetNumberProperty(window_props, kept_name, 0); + const Sint64 kept = SDL_GetNumberProperty(window_props, kept_name, default_value); const int value = SDL_max((int)SDL_GetNumberProperty(create_props, create_name, kept), 0); SDL_SetNumberProperty(window_props, kept_name, value); @@ -201,10 +203,10 @@ static void ApplyFrameInsets(SDL_Window *window, int left, int top, int right, i } if (left || top || right || bottom) { - const int ml = SDL_min(left, WAYLAND_FRAME_INPUT_MARGIN); - const int mt = SDL_min(top, WAYLAND_FRAME_INPUT_MARGIN); - const int mr = SDL_min(right, WAYLAND_FRAME_INPUT_MARGIN); - const int mb = SDL_min(bottom, WAYLAND_FRAME_INPUT_MARGIN); + const int ml = SDL_min(left, wind->frame_input_margin); + const int mt = SDL_min(top, wind->frame_input_margin); + const int mr = SDL_min(right, wind->frame_input_margin); + const int mb = SDL_min(bottom, wind->frame_input_margin); struct wl_region *region = wl_compositor_create_region(wind->waylandData->compositor); wl_region_add(region, left - ml, top - mt, frame_w + ml + mr, frame_h + mt + mb); wl_surface_set_input_region(wind->surface, region); @@ -2877,10 +2879,11 @@ bool Wayland_CreateWindow(SDL_VideoDevice *_this, SDL_Window *window, SDL_Proper */ if (!SDL_WINDOW_IS_POPUP(window) && !custom_surface_role) { const SDL_PropertiesID window_props = SDL_GetWindowProperties(window); - data->frame_insets.left = GetKeptCreateNumber(create_props, window_props, SDL_PROP_WINDOW_CREATE_WAYLAND_FRAME_INSET_LEFT_NUMBER, WAYLAND_KEPT_FRAME_INSET_LEFT); - data->frame_insets.top = GetKeptCreateNumber(create_props, window_props, SDL_PROP_WINDOW_CREATE_WAYLAND_FRAME_INSET_TOP_NUMBER, WAYLAND_KEPT_FRAME_INSET_TOP); - data->frame_insets.right = GetKeptCreateNumber(create_props, window_props, SDL_PROP_WINDOW_CREATE_WAYLAND_FRAME_INSET_RIGHT_NUMBER, WAYLAND_KEPT_FRAME_INSET_RIGHT); - data->frame_insets.bottom = GetKeptCreateNumber(create_props, window_props, SDL_PROP_WINDOW_CREATE_WAYLAND_FRAME_INSET_BOTTOM_NUMBER, WAYLAND_KEPT_FRAME_INSET_BOTTOM); + data->frame_insets.left = GetKeptCreateNumber(create_props, window_props, SDL_PROP_WINDOW_CREATE_WAYLAND_FRAME_INSET_LEFT_NUMBER, WAYLAND_KEPT_FRAME_INSET_LEFT, 0); + data->frame_insets.top = GetKeptCreateNumber(create_props, window_props, SDL_PROP_WINDOW_CREATE_WAYLAND_FRAME_INSET_TOP_NUMBER, WAYLAND_KEPT_FRAME_INSET_TOP, 0); + data->frame_insets.right = GetKeptCreateNumber(create_props, window_props, SDL_PROP_WINDOW_CREATE_WAYLAND_FRAME_INSET_RIGHT_NUMBER, WAYLAND_KEPT_FRAME_INSET_RIGHT, 0); + data->frame_insets.bottom = GetKeptCreateNumber(create_props, window_props, SDL_PROP_WINDOW_CREATE_WAYLAND_FRAME_INSET_BOTTOM_NUMBER, WAYLAND_KEPT_FRAME_INSET_BOTTOM, 0); + data->frame_input_margin = GetKeptCreateNumber(create_props, window_props, SDL_PROP_WINDOW_CREATE_WAYLAND_FRAME_INPUT_MARGIN_NUMBER, WAYLAND_KEPT_FRAME_INPUT_MARGIN, WAYLAND_FRAME_INPUT_MARGIN); } data->scale_factor = 1.0; diff --git a/src/video/wayland/SDL_waylandwindow.h b/src/video/wayland/SDL_waylandwindow.h index 025122e1e2a0a..e788be36055a4 100644 --- a/src/video/wayland/SDL_waylandwindow.h +++ b/src/video/wayland/SDL_waylandwindow.h @@ -209,6 +209,8 @@ struct SDL_WindowData { int left, top, right, bottom; } frame_insets, applied_frame_insets; + // The band round the frame that takes input (SDL_PROP_WINDOW_CREATE_WAYLAND_FRAME_INPUT_MARGIN_NUMBER). + int frame_input_margin; bool floating; bool suspended; From 883f5c83224d6f15f3dd5ec0e463926b68398567 Mon Sep 17 00:00:00 2001 From: foxnne Date: Fri, 9 Oct 2026 15:05:38 -0500 Subject: [PATCH 6/7] Wayland: publish a window's frame insets from creation on 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 --- include/SDL3/SDL_video.h | 6 ++++-- src/video/wayland/SDL_waylandwindow.c | 25 ++++++++++++++++++++----- 2 files changed, 24 insertions(+), 7 deletions(-) diff --git a/include/SDL3/SDL_video.h b/include/SDL3/SDL_video.h index 72713e89e7d71..143ac14aee914 100644 --- a/include/SDL3/SDL_video.h +++ b/include/SDL3/SDL_video.h @@ -1634,8 +1634,10 @@ extern SDL_DECLSPEC SDL_Window * SDLCALL SDL_GetWindowParent(SDL_Window *window) * `SDL_PROP_WINDOW_WAYLAND_FRAME_INSET_RIGHT_NUMBER` and * `SDL_PROP_WINDOW_WAYLAND_FRAME_INSET_BOTTOM_NUMBER`: the frame insets in * effect now, in window coordinates — those asked for at creation while the - * window floats, zero while it is maximized, tiled or fullscreen. They - * change with the window's size; read them on `SDL_EVENT_WINDOW_RESIZED`. + * window floats, zero while it is maximized, tiled or fullscreen. Set from + * creation on: until the window is first shown, the ones it will float + * with. They change with the window's size; read them on + * `SDL_EVENT_WINDOW_RESIZED`. * * On X11: * diff --git a/src/video/wayland/SDL_waylandwindow.c b/src/video/wayland/SDL_waylandwindow.c index f2b777a7a0a34..be69b7e60988e 100644 --- a/src/video/wayland/SDL_waylandwindow.c +++ b/src/video/wayland/SDL_waylandwindow.c @@ -189,6 +189,16 @@ static int GetKeptCreateNumber(SDL_PropertiesID create_props, SDL_PropertiesID w return value; } +// Publishes the frame insets in effect (SDL_PROP_WINDOW_WAYLAND_FRAME_INSET_*). +static void PublishFrameInsets(SDL_Window *window, int left, int top, int right, int bottom) +{ + const SDL_PropertiesID props = SDL_GetWindowProperties(window); + SDL_SetNumberProperty(props, SDL_PROP_WINDOW_WAYLAND_FRAME_INSET_LEFT_NUMBER, left); + SDL_SetNumberProperty(props, SDL_PROP_WINDOW_WAYLAND_FRAME_INSET_TOP_NUMBER, top); + SDL_SetNumberProperty(props, SDL_PROP_WINDOW_WAYLAND_FRAME_INSET_RIGHT_NUMBER, right); + SDL_SetNumberProperty(props, SDL_PROP_WINDOW_WAYLAND_FRAME_INSET_BOTTOM_NUMBER, bottom); +} + /* Tells the compositor the frame is the window, takes input on the frame and a band around it * only, and publishes the insets in effect. Called whenever the size or the insets change. */ @@ -220,11 +230,7 @@ static void ApplyFrameInsets(SDL_Window *window, int left, int top, int right, i wind->applied_frame_insets.right = right; wind->applied_frame_insets.bottom = bottom; - const SDL_PropertiesID props = SDL_GetWindowProperties(window); - SDL_SetNumberProperty(props, SDL_PROP_WINDOW_WAYLAND_FRAME_INSET_LEFT_NUMBER, left); - SDL_SetNumberProperty(props, SDL_PROP_WINDOW_WAYLAND_FRAME_INSET_TOP_NUMBER, top); - SDL_SetNumberProperty(props, SDL_PROP_WINDOW_WAYLAND_FRAME_INSET_RIGHT_NUMBER, right); - SDL_SetNumberProperty(props, SDL_PROP_WINDOW_WAYLAND_FRAME_INSET_BOTTOM_NUMBER, bottom); + PublishFrameInsets(window, left, top, right, bottom); } static void SetMinMaxDimensions(SDL_Window *window) @@ -3039,6 +3045,15 @@ bool Wayland_CreateWindow(SDL_VideoDevice *_this, SDL_Window *window, SDL_Proper SDL_SetPointerProperty(props, SDL_PROP_WINDOW_WAYLAND_VIEWPORT_POINTER, data->viewport); SDL_SetPointerProperty(props, SDL_PROP_WINDOW_WAYLAND_EGL_WINDOW_POINTER, data->egl_window); + /* Until the compositor first configures the window, the insets in effect are those it will + * float with, so an application can lay itself out before showing it. + */ + if (HasFrameInsets(data)) { + int il, it, ir, ib; + GetFrameInsets(data, true, &il, &it, &ir, &ib); + PublishFrameInsets(window, il, it, ir, ib); + } + data->hit_test_result = SDL_HITTEST_NORMAL; return true; From 011a2dc5a28e9191426138f6f76b07559862be9d Mon Sep 17 00:00:00 2001 From: foxnne Date: Fri, 9 Oct 2026 15:06:08 -0500 Subject: [PATCH 7/7] Wayland: a framed window shown again is framed again 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 --- src/video/wayland/SDL_waylandwindow.c | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/video/wayland/SDL_waylandwindow.c b/src/video/wayland/SDL_waylandwindow.c index be69b7e60988e..e0a7313b18912 100644 --- a/src/video/wayland/SDL_waylandwindow.c +++ b/src/video/wayland/SDL_waylandwindow.c @@ -200,7 +200,8 @@ static void PublishFrameInsets(SDL_Window *window, int left, int top, int right, } /* Tells the compositor the frame is the window, takes input on the frame and a band around it - * only, and publishes the insets in effect. Called whenever the size or the insets change. + * only, and publishes the insets in effect. Called whenever the size or the insets change. A + * hidden window has no xdg_surface to frame; the configure that shows it applies them. */ static void ApplyFrameInsets(SDL_Window *window, int left, int top, int right, int bottom) { @@ -208,9 +209,10 @@ static void ApplyFrameInsets(SDL_Window *window, int left, int top, int right, i const int frame_w = SDL_max(wind->current.logical_width - left - right, 1); const int frame_h = SDL_max(wind->current.logical_height - top - bottom, 1); - if (wind->shell_surface.xdg.surface) { - xdg_surface_set_window_geometry(wind->shell_surface.xdg.surface, left, top, frame_w, frame_h); + if (!wind->shell_surface.xdg.surface) { + return; } + xdg_surface_set_window_geometry(wind->shell_surface.xdg.surface, left, top, frame_w, frame_h); if (left || top || right || bottom) { const int ml = SDL_min(left, wind->frame_input_margin); @@ -2430,6 +2432,11 @@ void Wayland_HideWindow(SDL_VideoDevice *_this, SDL_Window *window) wl_surface_attach(wind->surface, NULL, 0, 0); wl_surface_commit(wind->surface); + /* The window geometry went with the xdg_surface: forget the insets applied, so the next + * configure frames the window again even at the same size. + */ + SDL_zero(wind->applied_frame_insets); + SDL_zero(wind->shell_surface); wind->show_hide_sync_required = true; struct wl_callback *cb = wl_display_sync(_this->internal->display);