Skip to content

build: fizzy builds on dvui's SDL3 backend again, and CI keeps it - #328

Merged
foxnne merged 4 commits into
mainfrom
build/sdl3-mode
Oct 10, 2026
Merged

foxnne merged 4 commits into
mainfrom
build/sdl3-mode

Conversation

@foxnne

@foxnne foxnne commented Oct 10, 2026 •

Copy link
Copy Markdown
Collaborator

What changes

zig build -Dnative-backend=sdl3 (fizzy on dvui's own SDL3 backend) compiles and runs again, and CI now builds it on macOS so it can't break unnoticed again.

On main it stopped compiling. The backend package's platform module reached into things only fizzy's own backend (backend/src/SDLBackend.zig) has:

  • win.backend.impl.viewports, in platform/window.zig: restyling and reskinning each float's own window.
  • win.backend.impl.window_skin, in platform/window.zig.
  • back.present_hook, in platform/macos_monitor.zig.
  • fizzy_native_transaction_begin / fizzy_native_viewport_transact, in platform/macos_monitor.zig. These are defined in the backend's macos_monitor.m, so the mode failed at link time once the first three compiled.

All of these now sit behind one comptime flag, platform.own_backend, which is true only when @import("backend") is the package's own backend: that is, the one that declares platform. On dvui's backend those parts do nothing. SDLBackend asserts the flag at comptime, so fizzy's own build fails rather than silently losing them.

Why one flag instead of a @hasField per field: #324 renames viewports to viewport_slots. With a per-field @hasField("viewports"), that rename would turn the guard false on fizzy's own backend, and floats would quietly stop being reskinned. With the flag, a renamed field still breaks fizzy's build.

The transaction calls aren't just a link fix. macosAppPreBeginSync opens a Core Animation transaction that only the present hook closes. Without the guard, dvui's backend would have left that transaction open through a full-screen transition.

Full screen on dvui's backend: AppKit's own, with fizzy's chrome

Full screen was broken on this mode. The macOS monitor (platform/macos_monitor.zig + macos/window_monitor.m) takes full screen and zooms over from AppKit. It animates the window itself and draws each step from the backend's begin and present hooks inside a Core Animation transaction, which only fizzy's backend has. On dvui's backend those steps were drawn out of time with the window.

macos_monitor.install now leaves that out on dvui's backend. The window goes in and out as AppKit and SDL take it, and every query (in_fullscreen_space, zoomed, titlebar inset) answers from AppKit's own state.

One piece of the monitor is about fizzy's chrome, not its backend: the setStyleMask: guard. SDL puts its own style on the window at will-enter and did-exit, and styleTitled puts fizzy's back. Without the guard, AppKit keeps the content size across each change. Measured with the monitor left out entirely, the window came back from each round trip 40 pt shorter: 800 → 760 → 720. fizzy_macos_window_keep_full_size_content installs that guard alone, and on dvui's backend it is all install does.

What stays on dvui's backend: the frosted or Liquid Glass window, the transparent title-less titlebar and the titlebar hit test. They are plain AppKit calls that work on any SDL window. What goes: floats as OS windows, the window skin on float windows, custom GPU programs, fizzy's own full-screen and zoom animation, live-resize sync and the health counters.

Option considered: drop the mode

The other way to fix this was to remove -Dnative-backend=sdl3 entirely: NativeBackend, the build option, and the supported/@hasDecl fallbacks. Fizzy's own app depends on its backend's viewports, glass and window skin, and nothing shipped on the sdl3 mode. I built that version and it passed every gate.

The decision went to keeping the mode. A dvui app that uses fizzy's backend package on the stock dvui backend is in the same position as fizzy in this mode (example-app's coming run-dvui). Building fizzy this way is the cheapest proof that the platform pieces still work for such an app. It costs one guard per backend-only reach, plus one CI step.

CI

The macOS leg of the test matrix gets two steps: fetch with -Dnative-backend=sdl3, then zig build -Dnative-backend=sdl3 -Dno-emit. It runs on macOS only because all four breakages were in macOS-only branches, which a Linux build never analyses. Cost: dvui's own SDL is compiled on that leg. It isn't in the leg's saved Zig cache until the next build.zig.zon change re-keys it.

Coordination

#324 landed first, carrying the stand-in viewports for dvui's backend (viewports_none). Main is merged into this branch (1dca6885): the two own_backend-guarded loops in platform/window.zig now read impl.viewport_slots, and zig build, -Dnative-backend=sdl3 and the gates pass on the merge.

SDK impact

  • None. The backend package and platform aren't part of the plugin boundary, and test-sdk-version passes.

Verified

  • macOS (arm64, macOS 26, notched display):
    • Gates pass: zig build, zig build test (514/515, 1 skipped), zig build test-integration (44/44 steps, 358/358), zig build check-web, zig build test-sdk-version.

    • zig build -Dnative-backend=sdl3 builds, links and runs.

    • Full screen, driven through the agent plugin (fizzyedit/agent's fizzy-mcp, in an isolated --profile). fizzy.toggleFullScreen ran four times; after each toggle, fizzy's layout (fizzy_screen) and AppKit's window frames (CGWindowList) were sampled for 3.5 s. The start frame is 300,204 1200×800 in every run:

      build after each exit full screen
      sdl3, before this PR's full-screen change (monitor installed) back to 1200×800, fizzy's own animation drawn out of time 0,39 1800×1130
      sdl3, monitor left out entirely 800 → 760 → 720, moving down 40 pt each time 0,39 1800×1130
      sdl3, this PR back to 300,204 1200×800 every time, AppKit's animation 0,39 1800×1130
      fizzy's own backend, this PR back to 300,204 1200×800 every time, its own animation (layout steps through 3140 → 3600 px) 0,39 1800×1130

      The 39 pt at the top in full screen is the area below the notch, the same on both backends. Fizzy's layout follows each way: full width with the titlebar strip collapsed in full screen, the strip back after.

    • Screenshots after a round trip on the sdl3 build show the frosted window with its transparent titlebar and traffic lights.

    • The own_backend assert works: pointing the flag at a declaration that doesn't exist fails fizzy's own build.

  • Windows: zig build -Dtarget=x86_64-windows-gnu -Dno-emit cross-compiles. It compiles; whether it runs wasn't tested.
  • Linux: CI. Only linux_titlebar.zig is relevant there, and it is unchanged.
  • Web: check-web.

Follow-ups

  • Fizzy on dvui's backend on Windows and Linux isn't built by CI. The platform's reach into the backend there is only impl.window, which dvui's backend also has.
  • FIZZY_VERDICT writes nothing on dvui's backend: verdicts read fizzy's backend's health counters, which dvui's backend doesn't have. The soak tape itself plays there (found by the "Floating glass windows" session).
  • A window-only capture of the sdl3 build in full screen shows the titlebar strip with the traffic lights at the top. AppKit normally reveals that strip on hover, and a window capture may draw it regardless; not checked on screen.
  • Zoom (double-clicking the titlebar) on dvui's backend is also AppKit's own now, with the same guard. It hasn't been measured.

🤖 Generated with Claude Code

`zig build -Dnative-backend=sdl3` had stopped compiling: the platform module reached into fields
and symbols only fizzy's own backend has (the floats' window slots, `window_skin`,
`present_hook`, and the Core Animation transaction calls in `macos_monitor.m`). They are now
behind one flag, `platform.own_backend`, and on dvui's backend those parts do nothing. A macOS
CI step compiles the mode.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The monitor takes full screen and zooms over from AppKit and draws their steps itself, which needs
fizzy's backend's frame hooks; on dvui's backend its steps were drawn out of time with the window.
There it now installs only the guard that keeps the window's content under the title bar through
SDL's styles (`fizzy_macos_window_keep_full_size_content`): fizzy's chrome stays, and without that
guard the window came back from each full-screen round trip a title bar shorter.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
foxnne added a commit that referenced this pull request Oct 10, 2026
…324)

Part of #281: step 1 of "Popout's generic half moves into the backend".

A dvui app on fizzy's backend can now open OS windows through
`@import("backend").viewports`, the same API fizzy's Popout uses. Before
this PR it lived in fizzy's `src/backend/backend_native.zig`, out of
reach of any other app. That is what `run-dvui` in example-app will
build on.

## What moved

- **`backend/src/viewports.zig`** is the `viewports` namespace from
`backend_native.zig`, moved as it was. The `if (comptime !supported)
return …` guards are gone: this backend always supports viewports, so
`supported` is just `true`. Each function still calls
`dvui.currentWindow().backend.impl.viewport*`.
- **`WindowGlassLook`** moves into `platform.window`, beside the
`WindowGlass` it converts to. Both the main window's glass and a
viewport's take it.
- **`SDLBackend.viewports`** exports the API. The backend's slot array,
also named `viewports`, is renamed `viewport_slots` to free that name (a
mechanical rename, 23 sites).
- **Fizzy** re-exports it: `pub const viewports =
@import("backend").viewports;`. Popout and Editor are unchanged.

## A stand-in for backends without OS windows

`backend/src/viewports_none.zig` has every name `viewports` has, and
each one reports that there are no OS windows: `available()` is false
and `open` returns null. An app's floats, menus and dialogs then stay in
the main window. It is the web's former stand-in, moved into the
package, so these all share it:
- fizzy on the web, on dvui's testing backend (integration tests) and on
dvui's own SDL3 backend;
- any dvui app built on either kind of backend.

That app imports it beside the backend (`viewportsNoneModule` in
`backend/build.zig`, which needs only dvui) and picks the backend's own
where it has one:

```zig
const viewports = if (@hasDecl(backend, "viewports")) backend.viewports else @import("viewports_none");
```

`backend_native.zig` does exactly that. `backend_web.zig` uses the
stand-in, and its `WindowGlassLook` is the stand-in's. The stand-in's
`windowGlass` takes any look: on dvui's SDL3 backend fizzy still dresses
its main window with `platform`'s.

**With #328** (`-Dnative-backend=sdl3` builds again, with a CI step):
whichever lands second renames the two `impl.viewports` loops that #328
guards in `platform/window.zig` to `impl.viewport_slots`. I trial-merged
the two locally with that rename. Both `zig build` and `zig build
-Dnative-backend=sdl3` succeed, and the SDL3 build starts and plays the
soak tape (no verdict there, since verdicts read fizzy's backend's
counters).

## Verification

On macOS, on this branch: `zig build`, `test` (514/515, 1 skipped),
`test-integration` (358/358), `check-web`, `test-sdk-version`, the
backend's own tests, and the Windows and Linux cross-builds all pass,
with and without the stand-in commit. No behaviour changes on fizzy's
backend: the same functions run, reached by a different path.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
The two loops own_backend guards in platform/window.zig read viewport_slots, as #324 renamed them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@foxnne
foxnne marked this pull request as ready for review October 10, 2026 13:34
@foxnne
foxnne enabled auto-merge (squash) October 10, 2026 13:36
@foxnne
foxnne merged commit a8affbe into main Oct 10, 2026
8 checks passed
@foxnne
foxnne deleted the build/sdl3-mode branch October 10, 2026 13:49
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