Skip to content

docs: native windows and glass on Windows and Linux, the research - #236

Merged
foxnne merged 2 commits into
mainfrom
docs/windows-linux-glass
Oct 8, 2026
Merged

foxnne merged 2 commits into
mainfrom
docs/windows-linux-glass

Conversation

@foxnne

@foxnne foxnne commented Oct 8, 2026

Copy link
Copy Markdown
Collaborator

Part of #226 (docs/NATIVE_WINDOWS_PLAN.md)

What changes

Adds docs/WINDOWS_LINUX_GLASS_PLAN.md, the research behind the plan's Windows Phase 2 and its Linux blur region. It takes apart what the macOS pop-out and Liquid Glass work rest on, piece by piece per OS, and how Windows and Linux can supply each piece, with sources.

The findings:

The plan's Windows decisions stand: the slider along DWM's backdrop types, one activation group, an Acrylic carry window, and the drop zones in the app's glass. Per-shape glass through Windows.UI.Composition is still the plan's "Phase 2, only with a go-ahead".

Steps 3 (Windows menus as Acrylic popups) and 4 (the Linux blur) are built in the two PRs stacked on this one.

This is a cloud session's research. Its claims marked (spike) were not run.

SDK impact

  • None

Verified

Docs only.

Follow-ups

The two step PRs stacked on this one.

🤖 Generated with Claude Code

foxnne and others added 2 commits October 8, 2026 09:48
`Popout.envSwitch` cached its answer in a struct declared inside the function, and the struct did
not name the variable. Zig gives a struct like that one type for every comptime argument, so every
switch shared one cache: whichever was read first in a run answered for all of them. In practice
`FIZZY_POPOUT` is read first (`beginFrame`), so `FIZZY_NATIVE_GLASS=0`, `FIZZY_NATIVE_MENUS` and
`FIZZY_NATIVE_DIALOGS` were ignored and took `FIZZY_POPOUT`'s value instead (unset: the settings).
The struct now holds the name, which makes it a type of its own per switch.

Seen while testing a new switch in the Ubuntu VM: it never turned on. Reproduced outside fizzy
with the same function in a 15-line program on Zig 0.16.0 (`ENV_B=1` read as null, `ENV_A=1`
made B true), fixed there the same way.
What the macOS pop-out and Liquid Glass work rests on, taken apart into the pieces each OS has to
supply, and how Windows and Linux can supply them, with sources. No OS but macOS 26 merges or
refracts glass (Microsoft has said Windows is not getting it), so elsewhere the shape stays fizzy's
own (LiquidField's smooth union) and the OS supplies only the blur under it: on Windows the visual
layer's host backdrop clipped to the field's outline (a backdrop cannot be a mask brush's source),
on Wayland ext-background-effect-v1, which GNOME 51 and Plasma 6.7 now ship, its region applied with
the surface commit. Carried views on Windows go into one overlay per display rather than a window
moved under the pointer. X11 is deprioritised as its sessions are removed. Step 0 for both: the
SPIR-V and DXIL glass programs predate #227 and still draw the earlier glass.

It works under docs/NATIVE_WINDOWS_PLAN.md (#226), whose Windows decisions stand: the slider along
DWM's backdrop types, one activation group, an Acrylic carry window, the drop zones in the app's
glass. This file is the research for that plan's Phase 2 (per-shape glass through
Windows.UI.Composition, which waits on a go-ahead) and for its Linux blur region.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@foxnne
foxnne marked this pull request as ready for review October 8, 2026 14:54
Base automatically changed from popout/env-switches to main October 8, 2026 15:08
@foxnne
foxnne merged commit 3c8119d into main Oct 8, 2026
5 checks passed
@foxnne
foxnne deleted the docs/windows-linux-glass branch October 8, 2026 15:08
foxnne pushed a commit that referenced this pull request Oct 8, 2026
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Euk6iCavQdW7MG5FN2vvFw
foxnne added a commit that referenced this pull request Oct 8, 2026
Part of #226

## What changes

Two pieces pixi's dropper magnifier (fizzyedit/pixi#3) needs to be a
clear liquid orb over its zoom, with no frost and strong refraction:
native Liquid Glass on macOS, the app's glass everywhere else. And the
SDK release that ships them, so the orb can be tried against `main` as
soon as this merges.

**`core.native_glass.offered`**
- The app publishes each frame whether the OS draws glass declared
outside a view drag (`publishOffered`, from `Popout.beginFrame`'s
`nativeGlass()`).
- A plugin that sees it declares its glass with `native_glass.add`.
Fizzy's overlay of Liquid Glass already stays up while any glass is
declared.
- Pixi declares a clear piece (`frost = 0`, the lens alone) over a zoom
it draws in the window, so the OS's lens refracts the window beneath.
- Off macOS 26, or with native glass off, `offered()` is false.

**`LiquidField.drawPicture` / `pictureMargin` / `lens`, and
`glass_look.forLens`**
- The app's glass as a lens over a picture the plugin drew. The middle
is the picture as it is, with no frost, colour or lift; the rim bends
and lights it.
- It reads no capture, so the middle stays pixel-exact.
- On the web, where no look is published, it is the earlier glass.
`pictureMargin` says how far past the shapes the picture should reach
for that glass's rim, at the strongest shape lens.

**Tried and dropped:** pixi tried the drop zones' frosted glass with its
zoom laid over it. On a Mac it read as a thick grey border or a flat
rim.

**Merged with main** (#234–#236, #239, #240).

## SDK impact

- [ ] None
- [ ] Core-only or additive: reaches plugins at the next SDK release
- [ ] Fingerprint moved: recorded in `sdk/src/version.zig`,
`sdk_version` untouched, PR labelled `sdk`
- [x] SDK release: bumps `sdk_version`, lists the `sdk` PRs since the
last `sdk-v*` tag

`sdk_version` goes 0.2.17 → 0.2.18 (ef95301). Since `sdk-v0.2.17` this
release carries:
- this PR's core additions;
- #223, #224 and #227's core (the one-slider glass, `glass_look`,
`core.native_glass`, `core.screens`'s menus and dialogs).

The fingerprint has not moved, so installed plugins keep loading.
Merging tags `sdk-v0.2.18` and, per #240, asks the store plugins to
repin.

## Verified

- `zig test core/gfx/glass_look.zig`: 13 pass.
- `zig fmt --check` and `zig ast-check` on the changed files.
- `tests/integration.zig` compiles `drawPicture` and checks
`pictureMargin`.
- CI: Linux, macOS, Windows, integration and the Windows cross-build on
this head.
- [x] macOS: pixi's orb tried locally against this branch by the
maintainer, who approved it.
- [ ] Windows:
- [ ] Linux:
- [ ] Web:

## Follow-ups

- pixi#3 repinned to `sdk-v0.2.18` once it's tagged.
- The orb pinching off pixi's sample button and merging with it (the
plan's next step for this consumer).

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

https://claude.ai/code/session_01Euk6iCavQdW7MG5FN2vvFw

---------

Co-authored-by: Claude <noreply@anthropic.com>
foxnne added a commit that referenced this pull request Oct 8, 2026
)

Part of #236 (`docs/WINDOWS_LINUX_GLASS_PLAN.md`, step 0; no plan issue)

## What changes

On Linux (Vulkan) and Windows (D3D12) the in-window glass — dialogs,
menus, a drag's drops — now draws Apple's lens and follows the one
window-opacity slider (`glass_look`), as it already does on macOS. The
SPIR-V and DXIL embedded in `LiquidField` were last compiled in #206;
the HLSL changed in #227, so those two platforms kept drawing the
earlier glass whatever `publishLook` said.

- **The HLSL needed nothing.** Apart from each language's declarations
it is `liquid_glass.glsl` and `liquid_glass.metal` line for line, with
three deliberate, documented differences (the loop form, samples at
level 0, the dither taking `uv`). `LiquidField.sample` agrees with them
on the field.
- **Compiled with the commands at the top of the HLSL**, by
SDL_shadercross with the DXC it vendors
(libsdl-org/DirectXShaderCompiler at 2c84a1c5) — the toolchain #206
used: it compiles #206's HLSL to #206's SPIR-V and DXIL byte for byte.
The DXIL is signed (validator 1.9, as before) and passes `dxv`; the
SPIR-V passes `spirv-val`; the interface is unchanged (two samplers, one
uniform buffer, colour at location 0, uv at 1). That DXC wants clang on
Linux: built with GCC 13 it corrupts its heap compiling DXIL.
- `publishLook`'s comment says every form of the program draws the lens
now, and that the web publishes none (`Editor.zig` passes null on wasm).
- Step 0 of `docs/WINDOWS_LINUX_GLASS_PLAN.md` and the "still to do" in
`docs/NATIVE_WINDOWS_PLAN.md`'s materials library are marked done.

## SDK impact

- [ ] None
- [x] Core-only or additive: reaches plugins at the next SDK release —
the programs are embedded in `core`. 0.2.18 (#228,
`LiquidField.drawPicture` for pixi's dropper magnifier) shipped without
them, so the magnifier's lens shows on Vulkan/D3D12 from the next
release.
- [ ] Fingerprint moved: recorded in `sdk/src/version.zig`,
`sdk_version` untouched, PR labelled `sdk`
- [ ] SDK release: bumps `sdk_version`, lists the `sdk` PRs since the
last `sdk-v*` tag

## Verified

Not yet seen in the app: the cloud session this was built in could not
build fizzy (its network policy denies gitlab.freedesktop.org and
sndio.org, which sdl_zig fetches from). What was checked instead (and CI
on fb422e9 builds the app and passes its tests on macOS, Windows and
Linux, the headless integration tests on Linux, and the Windows
cross-build of fizzy's backend — that shows the new programs embed and
link, not how they draw):

- [ ] macOS: unchanged (Metal source untouched).
- [ ] Windows: `core`, with `LiquidField` and the new DXIL embedded,
compiles for x86_64-windows-gnu; the DXIL validates (`dxv`). **Not run
on D3D12** — wants a Windows machine (WARP is enough).
- [ ] Linux: the new SPIR-V drawn through SDL_GPU on lavapipe, as
`GpuRenderer` draws a program (frost at sampler 0, sharp at 1, the punch
and add passes), with `pack` + `applyLook` uniforms and the look from
`glass_look.inApp` at window opacity 0, 0.35, 0.7, 1 — a dialog
(`forText`) and three drops beside a carried card (`forDrops`), over an
opaque and a translucent window. Within 1/255 of `liquid_glass.glsl`
(built with glslang) at every pixel of every case; with no look
published, identical to the old SPIR-V; with one published, the old
SPIR-V was off by up to 204/255. `core` compiles and its check runs on
x86_64-linux. **Not seen in the app.**
- [ ] Web: unchanged (GLSL untouched; the web publishes no look).

## Follow-ups

- See it in the app on Linux and Windows (a dialog, and a drag's drops,
at a few window opacities).
- The next SDK release carries these to plugins that build `core` in.

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

https://claude.ai/code/session_01Vci2bBhv2EH5QxygzWxmx8

Co-authored-by: Claude <noreply@anthropic.com>
foxnne added a commit that referenced this pull request Oct 8, 2026
…=1 (#237)

Part of #226 (`docs/NATIVE_WINDOWS_PLAN.md`, "Linux — the compositor's
blur"); step 4 of `docs/WINDOWS_LINUX_GLASS_PLAN.md` (#236).

## What changes

With `FIZZY_BLUR_BEHIND=1`, on a Wayland compositor that blurs, the
window's base goes translucent at the window opacity over the desktop's
blur, as it already does over macOS's vibrancy and Windows' Acrylic.
Elsewhere, and without the switch, nothing changes: the window stays
opaque.

- **`platform/wayland_blur.zig`:** binds `ext-background-effect-v1`
(GNOME 51, Plasma 6.7, niri) or KDE's `org_kde_kwin_blur`.
- It uses SDL's own libwayland-client (`RTLD_NOLOAD`), with both
protocols declared by hand, on an event queue of its own.
- The region is sent only when it changes, and it lands with the commit
the frame's present makes.
- "No blur" is an empty region on the ext protocol, and the blur object
unset and released on KDE's protocol (KWin blurs the whole window behind
an empty one).
  - The first probe logs what it found.
- **`platform/blur_region.zig`** (std-only, unit tested): steps the
rounded frame into rectangles.
- **`linux_titlebar.blurBehind` and `Editor.tick`:** the blur covers the
frame inside the shadow's margin while the window is translucent, and is
square when maximized.

Off by default until someone sees it on a desktop that blurs.

## SDK impact

- [x] None

## Verified

- [x] Linux, Ubuntu 26.04 / GNOME 50 (aarch64 UTM VM, native Wayland,
`FIZZY_BLUR_BEHIND=1 WAYLAND_DEBUG=client`). GNOME 50 has neither
protocol, so this covers the "absent" path only:
  - one `get_registry` on fizzy's own unnamed queue;
- the log line `ext-background-effect-v1 absent, org_kde_kwin_blur
absent`;
  - no `wl_display.error`;
  - fizzy alive after 12 s, window opaque.
- [x] Protocol details:
- opcodes checked against wayland.app (`ext-background-effect-v1`) and
plasma-wayland-protocols' `blur.xml`;
- KWin's empty-region-means-whole-window rule read in
`src/plugins/blur/blur.cpp` on `Plasma/6.6`. Master only takes a
non-empty region.
- [x] A stub compositor speaking both protocols, in the cloud session
that wrote this: the region applied on commit, unchanged frames sent
nothing, cleared when opaque, KDE's unset, nothing asked when
capabilities = 0.
- [x] `fizzy-blur-region-tests` (5 tests).
- [x] Gates: `zig build`, `test`, `test-integration`, `check-web`,
`test-sdk-version`, and the Windows and Linux cross-builds.
- [ ] Not seen where the blur would actually show: GNOME 51, Plasma
6.7+, or Plasma ≤6.6 (KDE's protocol and the "off" path). Please try one
of those before this goes default-on:
  - the blur sits inside the frame, not in the shadow margin;
  - the corners line up;
  - maximized is square and full;
  - opacity 1 gives no blur.
- [ ] macOS / Windows / Web: no change there (the code is
`comptime`-gated to Linux; it builds on all of them).

Relies on the `envSwitch` fix (#235, merged): before it,
`FIZZY_BLUR_BEHIND` read as `FIZZY_POPOUT`.

## Follow-ups

- Default-on once seen on GNOME 51 or Plasma.
- Wayland popups (menus and dialogs as `xdg_popup`s) are the other half
of step 4, not built.

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

Co-authored-by: Claude <noreply@anthropic.com>
foxnne added a commit that referenced this pull request Oct 8, 2026
Part of #226 (`docs/NATIVE_WINDOWS_PLAN.md`, Phase 2); step 5 of
`docs/WINDOWS_LINUX_GLASS_PLAN.md` (#236).

## What changes

Adds `spikes/windows-composition/`, a standalone Windows program
answering the four questions the plan's Phase 2 (the drag's glass as an
overlay over the desktop's blur) waits on. Nothing in fizzy changes. The
research doc's step 5 records the answers.

How it works:
- **It uses fizzy's own pins** of fizzyedit/SDL and zigwin32.
- **The window is fizzy's kind:** a transparent SDL window presenting on
D3D12 through the fork's topmost DirectComposition target.
- **Under it:** a Windows.UI.Composition target that isn't topmost,
holding the host backdrop, cut to a pill (∪ an orbiting circle) that
sweeps across the window. Five modes:
  - no clip;
  - a rounded-rect clip;
  - a Direct2D path rebuilt every frame;
  - an AlphaMask effect over a swapchain the app presents;
- the app's picture hosted in composition's own tree (the fallback for
question 1).
- **An overlay over the whole display** tries four click-through styles.
- **WinRT is bound by hand.** Every GUID and vtable slot was read from
the OS's own WinMetadata with System.Reflection.Metadata, and each is
cited in `src/winrt.zig`. `IGeometrySource2D` and `IGraphicsEffect` are
COM objects written in Zig.

At about 1,700 lines this is over CONTRIBUTING's 800-line guide. It's
throwaway code in `spikes/`, built by nothing else.

## SDK impact

- [x] None

## Verified

Windows 11 ARM VM (`fizzy-win11`, build 26300, D3D12 on WARP, 60 Hz),
aarch64-windows-gnu, driven by `SendInput`. Offsets were measured from
screenshots: the clip's glass against the red box SDL presents round the
pill, six samples per run, with a direction mark telling lead from lag.

1. **Two targets on one HWND: yes.** `CreateDesktopWindowTarget(hwnd,
FALSE)` on SDL's claimed window succeeds and draws under SDL's picture.
Static, the two align to the pixel.
2. **Path clip rebuilt every frame: works and is antialiased** (1–2 px).
Composition calls `GetGeometry` once per frame. With SDL's VSYNC present
the clip **leads** the picture by the present queue:

   | SDL present | Clip delay | Glass vs SDL's rim |
   |---|---|---|
   | VSYNC (frames in flight 1 or 2) | 0 | ~2.2 frames ahead (35 ms) |
   | VSYNC | 1 | 1.3 frames ahead |
   | VSYNC | **2** | **0 ±0.5 px** |
   | MAILBOX / IMMEDIATE | 0 | **0 ±0.5 px** |

So fizzy either gives composition the shape from as many frames back as
the present queue is deep (measured, not assumed), or has the fork
present through a frame-latency waitable swapchain.
3. **Effect-graph mask: yes.** D2D's AlphaMask with a
`CompositionEffectSourceParameter` over
`CreateCompositionSurfaceForSwapChain` shows the glass exactly where the
presented mask is.
4. **Overlay:**

   | Style | Same-thread click | Another process | Tree shows |
   |---|---|---|---|
   | `TRANSPARENT \| LAYERED` | passes | passes | yes |
   | `TRANSPARENT \| LAYERED`, alpha 255 | passes | passes | yes |
   | `TRANSPARENT` alone | blocked | blocked | yes |
| `HTTRANSPARENT` | passes | **blocked** (though `WindowFromPoint` says
it passes) | yes |

A drag under the layered overlay keeps its capture (30/30 motion
events). `SDL_SetWindowOpacity(0.6)` (layered) leaves both SDL's picture
and the composition tree drawing.

- [ ] **The blur on a hardware GPU.** The VM draws the host backdrop
black. Run it, press `3` (path) with the backdrop on, and check the blur
and the edge, at clip delay 0 and 2 (`D`).
- [ ] **The present queue's depth at 60/120/144 Hz** on hardware, and
Windows 11 23H2/24H2.
- [ ] macOS / Linux / Web: n/a (Windows only; not part of fizzy's
build).

## Follow-ups

- If the blur holds on hardware: the overlay (step 6), with the clip
delayed by the measured present-queue depth, or a waitable swapchain in
the SDL fork.

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

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
foxnne added a commit that referenced this pull request Oct 8, 2026
…#238)

Part of #226 (`docs/NATIVE_WINDOWS_PLAN.md`); step 3 of
`docs/WINDOWS_LINUX_GLASS_PLAN.md` (#236).

## What changes

With floats as windows (`FIZZY_POPOUT=1` on Windows), menus and dialogs
now leave the main window as they do on macOS (`viewports.menus`). Each
one is a borderless window owned by the window it opens from, never
activated, and dressed by DWM (`win32_titlebar.viewportMenuChrome`):
- Acrylic (`DWMSBT_TRANSIENTWINDOW`, the material of Windows 11's own
menus);
- DWM's rounded corners and shadow;
- no border, no system menu, no DWM transitions.

DWM draws a never-activated window's backdrop as its solid fallback, so
the menu's window is kept looking active (`WM_NCACTIVATE`). An owned
window isn't carried when its owner moves, so a menu's window rides on
nothing and is placed each frame.

## SDK impact

- [x] None

## Verified

Windows 11 ARM VM (`fizzy-win11`, WARP, aarch64-windows-gnu Debug),
driven with real input (`SendInput`) and checked against the window list
(`EnumWindows` plus DWM attributes) and screenshots:
- [x] The File and Help menus and the "Unsaved changes" dialog each open
in a window of their own:
  - owned by the main window and directly above it in the z-order;
  - `WS_EX_NOACTIVATE`, no `WS_SYSMENU`;
  - backdrop type Acrylic (3), corner preference round;
  - the main window stays the foreground window.
- [x] The first click on an item acts: New File opens `untitled-1`, and
Open Files opens the Open dialog. A temporary log showed the press
arriving on the menu's window and mapping into the menu's dvui
subwindow, not what lies under it.
- [x] The dialog's window hides when the main window is minimized and
comes back on restore.
- [x] Gates: `zig build`, `test`, `test-integration`, `check-web`,
`test-sdk-version`, and the Windows and Linux cross-builds.
- [ ] **The look on a hardware GPU.** The VM's DWM draws every backdrop
as its fallback, so these need a real machine:
  - Acrylic rather than a solid colour;
  - rounded corners and a shadow;
  - whether the `WM_NCACTIVATE` trick keeps the Acrylic.
- [ ] A menu opened over a float's window. Not exercised; creating a
float needs a view drag.
- [ ] macOS / Linux / Web: unchanged (`viewports.menus` was already on
for macOS, and this is the Windows branch).

Seen while testing, not caused by this change:
- A menu closes when fizzy is deactivated. My harness's own process
start deactivated it, which made a later click fall through to the main
window.
- A dialog drifts a few pixels left over each minimize/restore. It does
the same drawn in-window (`FIZZY_NATIVE_DIALOGS=0`).

## Follow-ups

- NATIVE_WINDOWS_PLAN's Phase 1 on Windows: the slider along DWM's
backdrop types, one activation group, and a policy probe.
- Caption buttons in a Windows float's header (in
`app/layout/Floats.zig`).

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

Co-authored-by: Claude <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.

2 participants