From ed8b8920fff5a5e514a2fded753c4789c7bb36b9 Mon Sep 17 00:00:00 2001 From: foxnne Date: Thu, 8 Oct 2026 10:02:55 -0500 Subject: [PATCH] windows: a composition spike for the overlay's four questions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `spikes/windows-composition/`: a Windows program on fizzy's own pins of fizzyedit/SDL and zigwin32 that answers step 5 of docs/WINDOWS_LINUX_GLASS_PLAN.md, the spikes NATIVE_WINDOWS_PLAN.md's Phase 2 waits on. A transparent SDL window presents on D3D12 through the fork's topmost DirectComposition target, as fizzy's windows do. Under it, a Windows.UI.Composition target that isn't topmost holds the host backdrop, cut to a moving pill (∪ an orbiting circle) by a rounded-rect clip, a Direct2D path rebuilt each frame, or an AlphaMask effect over a swapchain the app presents. A display-sized overlay tests 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 `IGeometrySource2D` and `IGraphicsEffect` are implemented as COM objects in Zig. Run in the Windows 11 ARM VM (WARP, 60 Hz) with real input, offsets measured from screenshots: 1. The two targets share the HWND, aligned to the pixel. 2. The path clip is antialiased. It leads SDL's VSYNC present by the present queue (2 frames, 35 ms at 60 Hz), and lands exactly (±0.5 px) with a 2-frame clip delay or with a present that doesn't wait. Frames in flight make no difference. 3. AlphaMask takes a presented swapchain as its mask. 4. Only `WS_EX_TRANSPARENT | WS_EX_LAYERED` passes clicks to other processes while the tree still shows. `HTTRANSPARENT` passes only within the thread, though `WindowFromPoint` says otherwise. A drag under the overlay keeps its capture, and `SDL_SetWindowOpacity` (layered) leaves both trees drawing. The VM draws the host backdrop black, so the blur and real-refresh timings need a hardware GPU. Co-Authored-By: Claude Opus 5.5 --- docs/WINDOWS_LINUX_GLASS_PLAN.md | 20 +- spikes/windows-composition/README.md | 145 ++++++ spikes/windows-composition/build.zig | 33 ++ spikes/windows-composition/build.zig.zon | 24 + spikes/windows-composition/src/comp.zig | 131 +++++ spikes/windows-composition/src/effect.zig | 201 +++++++ spikes/windows-composition/src/geometry.zig | 218 ++++++++ spikes/windows-composition/src/main.zig | 549 ++++++++++++++++++++ spikes/windows-composition/src/overlay.zig | 134 +++++ spikes/windows-composition/src/sdl3-c.h | 5 + spikes/windows-composition/src/surface.zig | 124 +++++ spikes/windows-composition/src/user32.zig | 84 +++ spikes/windows-composition/src/winrt.zig | 251 +++++++++ 13 files changed, 1916 insertions(+), 3 deletions(-) create mode 100644 spikes/windows-composition/README.md create mode 100644 spikes/windows-composition/build.zig create mode 100644 spikes/windows-composition/build.zig.zon create mode 100644 spikes/windows-composition/src/comp.zig create mode 100644 spikes/windows-composition/src/effect.zig create mode 100644 spikes/windows-composition/src/geometry.zig create mode 100644 spikes/windows-composition/src/main.zig create mode 100644 spikes/windows-composition/src/overlay.zig create mode 100644 spikes/windows-composition/src/sdl3-c.h create mode 100644 spikes/windows-composition/src/surface.zig create mode 100644 spikes/windows-composition/src/user32.zig create mode 100644 spikes/windows-composition/src/winrt.zig diff --git a/docs/WINDOWS_LINUX_GLASS_PLAN.md b/docs/WINDOWS_LINUX_GLASS_PLAN.md index 01fcd75d5..8285f1490 100644 --- a/docs/WINDOWS_LINUX_GLASS_PLAN.md +++ b/docs/WINDOWS_LINUX_GLASS_PLAN.md @@ -2,7 +2,8 @@ Status: in progress. Research under `docs/NATIVE_WINDOWS_PLAN.md`. Step 0 (the glass programs compiled again) is done (#242). Steps 3 (Windows menus and dialogs as Acrylic popups) and 4 (the -Linux window material) are built in PRs of their own, each behind a flag; nothing else here is. +Linux window material) are built in PRs of their own, each behind a flag, and step 5's spikes are +run (`spikes/windows-composition/`); nothing else here is. An investigation, not yet a design anyone has agreed to. macOS has floats as OS windows by default, and on macOS 26 the OS draws a view drag's glass (`docs/POPOUT_WINDOWS_PLAN.md`, "P6"; @@ -378,9 +379,22 @@ Each step is useful by itself, and the spikes come before anything that depends 3. a host backdrop masked through an effect graph by a swapchain the app presents; 4. the overlay's click-through and z-order with a drag in flight, and `WS_EX_LAYERED` on a window with a composition swapchain and a DWM backdrop. + + *Run on WARP in the Windows 11 ARM VM* (`spikes/windows-composition/`; its README has the + numbers). All four hold: + 1. The two targets share the HWND, aligned to the pixel. + 2. The path clip is antialiased. It **leads** SDL's VSYNC present by its queue (2 frames), and + lands exactly with a 2-frame delay or with a present that doesn't wait. + 3. AlphaMask takes a presented swapchain as its mask. + 4. Only `WS_EX_TRANSPARENT | WS_EX_LAYERED` passes clicks to other processes, the tree still + showing; a drag keeps its capture, and `WS_EX_LAYERED` leaves both trees drawing. + + Still to see on a hardware GPU: the blur itself (the VM draws the host backdrop black) and the + present queue's depth at real refresh rates. 6. **Windows: the overlay** (`viewports.carries`, the `os_backdrop` glass), if spikes 1 and 2 - hold, and with the go-ahead `NATIVE_WINDOWS_PLAN.md` asks for (its Phase 2). If they don't, the - fallback is the overlay with the app's glass and no OS blur under it. + hold (they do on WARP, the clip delayed by the present queue's depth), and with the go-ahead + `NATIVE_WINDOWS_PLAN.md` asks for (its Phase 2). If they don't, the fallback is the overlay + with the app's glass and no OS blur under it. That still carries a view past every window, which is the part users will notice most. 7. **Optional: real refraction on Windows** through capture, behind a flag, if the clear lens over other apps turns out to matter. diff --git a/spikes/windows-composition/README.md b/spikes/windows-composition/README.md new file mode 100644 index 000000000..031bd4642 --- /dev/null +++ b/spikes/windows-composition/README.md @@ -0,0 +1,145 @@ +# Spike: the drag's glass on Windows, through Windows.UI.Composition + +Answers the four questions `docs/WINDOWS_LINUX_GLASS_PLAN.md` ("Order of work", step 5) asks before +fizzy builds the Windows overlay (`NATIVE_WINDOWS_PLAN.md`, Phase 2): + +1. Can a Windows.UI.Composition target that is **not** topmost share an HWND with the topmost + DirectComposition target fizzyedit/SDL presents a transparent window's D3D12 swapchain through? +2. A host backdrop clipped by a path rebuilt every frame: what does its edge look like, and how far + does it sit from the app's own present? +3. Can an effect graph mask the host backdrop with a swapchain the app presents? +4. Does the overlay pass every click through while its composition tree still shows, with a drag in + flight? And does `WS_EX_LAYERED` (`SDL_SetWindowOpacity`, the ghost fade) leave a window with a + composition swapchain and a composition tree alone? + +## Run + +```sh +cd spikes/windows-composition +zig build -Dtarget=x86_64-windows-gnu # or aarch64-windows-gnu +``` + +Then run `zig-out/bin/windows-composition-spike.exe` on Windows 11. It uses fizzy's own pins of +fizzyedit/SDL and zigwin32, and it logs to its console. + +The window is transparent, and SDL presents it on D3D12 through its DirectComposition target, as +fizzy's windows are. Under that, a composition target holds one sprite: the host backdrop (a blur +of what is behind the window), cut to a pill that sweeps side to side, with a circle orbiting it. +SDL draws a red box round the pill, a yellow box round the circle, and a green mark at the pill's +leading end. The glass should sit exactly inside the red box. + +| Key | What it does | +|---|---| +| `1`–`5` | Mode: `full` window backdrop, `rect` rounded-rect clip, `path` Direct2D path clip, `mask` effect-graph mask, `hosted` (the picture as a swapchain in composition's own tree, not SDL's target) | +| `M` | Merge the circle in, or show the pill alone | +| `T` | When the clip is set: `before` the present, set and `pump` the dispatcher, or `after` | +| `D` | Clip delay: give composition the shape from 0–3 frames ago | +| `F` | SDL_GPU frames in flight, 1–3 | +| `B` | Host backdrop, or a flat colour (easier to see, and measurable) | +| `O` | `SDL_SetWindowOpacity` 0.6 / 1 (spike 4b) | +| `V` | Overlay over the whole display, through its variants: `layered`, `layered_alpha`, `transparent`, `hittest`, off | +| `Space` / `Up` / `Down` | Pause, or change the speed | + +Environment variables set the same things at launch: `SPIKE_MODE`, `SPIKE_TIMING`, +`SPIKE_CLIP_DELAY`, `SPIKE_FRAMES_IN_FLIGHT`, `SPIKE_PRESENT` (`vsync`, `mailbox`, `immediate`), +`SPIKE_OVERLAY`, `SPIKE_SPEED` (px/s), `SPIKE_COLOR=1`, `SPIKE_SINGLE=1`, `SPIKE_TRANSLUCENT=1`, and +`SPIKE_SECONDS` to quit after a while. + +## How it was measured + +Everything below was run in the Windows 11 ARM VM (build 26300, D3D12 on WARP, 60 Hz, aarch64 +build), driven through `SendInput`. + +**Offsets:** read from screenshots (`Graphics.CopyFromScreen`, which captures DWM's composed frame). +The analyser takes the row through the red box's middle and compares the blue colour-brush glass +with the box's inner edges. The green mark gives the direction of travel. Six screenshots per run. + +**Clicks:** `EnumWindows` and DWM attributes for the window list, and the spike's own log of the +clicks and drags that reach it. + +The pieces are bound by hand. Every WinRT interface's GUID and vtable slots were read from the OS's +own metadata (`C:\Windows\System32\WinMetadata\*.winmd`, through System.Reflection.Metadata). Each +one is cited in `src/winrt.zig`. + +## Findings + +**1. Two targets on one HWND: yes.** +- `CreateDesktopWindowTarget(hwnd, FALSE)` on SDL's claimed transparent window succeeds, with + IsTopmost false. +- The composition tree draws under SDL's DirectComposition content, and SDL's picture draws over it. +- Static, the glass and SDL's box align to the pixel: composition's units on a desktop target are + the swapchain's pixels. +- So fizzy needs no SDL change to put a backdrop under its picture. + +**2. Path clip rebuilt every frame: works, antialiased, and early by SDL's present queue.** +- A new `CompositionPath` per frame, from a hand-written `IGeometrySource2D`, costs one + `GetGeometry` call per frame (never `TryGetGeometryUsingFactory`). +- The clip's edge is antialiased over 1–2 px (12 → 29 → 175 → 230 across the pill's end), so the + plan's "aliased edge" worry doesn't hold on WARP. +- The clip **leads** the present rather than trailing it, as the plan expected. At 400 px/s with + SDL's default VSYNC, the glass sits about 14 px ahead of the box: ~2 frames, 35 ms. + +| SDL present | Frames in flight | When the clip is set | Clip delay | Glass vs rim | +|---|---|---|---|---| +| VSYNC | 2 | before present / pump | 0 | 2.1–2.3 frames ahead | +| VSYNC | 2 | after present | 0 | 3.3 frames ahead | +| VSYNC | 1 | pump | 0 | 2.2 frames ahead | +| VSYNC | 1 or 2 | pump | 1 | 1.3 frames ahead | +| VSYNC | 2 | pump | **2** | **0.0 ±0.5 px** | +| MAILBOX / IMMEDIATE | 2 | pump | 0 | **0.0 ±0.5 px** | + +- Composition properties land at the next DWM frame. SDL's VSYNC present arrives two frames after + its submit; fewer frames in flight don't shorten that, but presenting without the vsync wait does. +- Pumping the thread's messages between setting the clip and presenting changes nothing; there + is rarely a message waiting. +- For fizzy: + - Either give composition the shape from as many frames back as the present queue is deep + (2 here), measured rather than assumed: `IDXGISwapChain::GetFrameStatistics`, or SDL exposing + its queue depth. + - Or have the fork present with a frame-latency waitable swapchain (max latency 1), so the + present is not queued. + - Either way, check it on real hardware at 60 and 120 Hz before trusting a constant. + +**3. Effect-graph mask: yes.** +- A hand-written `IGraphicsEffect` naming D2D's AlphaMask (no properties) compiles in + `CreateEffectFactory` (LoadStatus `Pending`, then fine). +- A `CompositionEffectSourceParameter` takes a surface brush over + `CreateCompositionSurfaceForSwapChain`: a swapchain the app presents. The glass shows exactly + where the mask is drawn, antialiased by Direct2D. +- The mask here is a D3D11 swapchain presented without waiting, so it leads SDL's VSYNC picture + by the same 2 frames as the clip. A mask presented on fizzy's own device and queue would move + with the picture, which is the point of this route. +- Composite/SourceIn is wired as a fallback, and wasn't needed. + +**4. Overlay: `WS_EX_TRANSPARENT | WS_EX_LAYERED`.** +- Tested on a display-sized, topmost, never-activated window, with no taskbar entry and + `WS_EX_NOREDIRECTIONBITMAP`, holding a host backdrop pill and a hosted swapchain rim that follow + the pointer: + +| Style | Click to a window of the same thread | Click to another process (the desktop) | Tree shows | +|---|---|---|---| +| `TRANSPARENT \| LAYERED` | passes | passes | yes | +| same, `SetLayeredWindowAttributes(255)` | passes | passes | yes | +| `TRANSPARENT` alone | blocked; the overlay even becomes foreground | blocked | yes | +| `WM_NCHITTEST` → `HTTRANSPARENT` | passes | **blocked** | yes | + +- `WindowFromPoint` claims `HTTRANSPARENT` passes to the desktop. The real click doesn't. +- A drag that starts in the window under the overlay and crosses onto the desktop keeps its + capture: 30 of 30 motion events reached it, with the overlay's glass following the pointer. +- `SDL_SetWindowOpacity(0.6)` makes the window `WS_EX_LAYERED`. SDL's DirectComposition picture + and the composition tree both still draw, at 60%, so the ghost fade can stay as it is. + +**`hosted` (the fallback for spike 1, not needed):** a swapchain in composition's own tree, beside +the clip, aligns with it with no delay (0 px in 5 of 6 samples, one sample under a frame). + +## Not answered here (needs a real GPU) + +- **The blur itself.** In the VM the host backdrop draws solid black. Does the glass show a blur of + what is behind the window on a hardware GPU, with transparency effects on, and fall back when + they are off or on battery saver? +- **The timings on real hardware:** the present queue's depth at 60/120/144 Hz, and with a + waitable swapchain in the fork. +- **Windows 11 23H2 / 24H2**: the VM is a 26300 build. + +Run it on a real machine and note, for each of `path` with `B` off: the blur, the edge, and the +offset at clip delay 0 and 2 (`D`). diff --git a/spikes/windows-composition/build.zig b/spikes/windows-composition/build.zig new file mode 100644 index 000000000..3ccc81371 --- /dev/null +++ b/spikes/windows-composition/build.zig @@ -0,0 +1,33 @@ +//! The composition spike: Windows only. `zig build -Dtarget=x86_64-windows-gnu` (or +//! aarch64-windows-gnu) puts `windows-composition-spike.exe` in `zig-out/bin`. See README.md. +const std = @import("std"); + +pub fn build(b: *std.Build) void { + const target = b.standardTargetOptions(.{ .default_target = .{ .os_tag = .windows, .abi = .gnu } }); + const optimize = b.standardOptimizeOption(.{}); + + const sdl = b.dependency("sdl", .{ .target = target, .optimize = optimize }); + const sdl_c = b.addTranslateC(.{ + .root_source_file = b.path("src/sdl3-c.h"), + .target = target, + .optimize = optimize, + }); + sdl_c.addIncludePath(sdl.artifact("SDL3").getEmittedIncludeTree()); + + const exe = b.addExecutable(.{ + .name = "windows-composition-spike", + .root_module = b.createModule(.{ + .root_source_file = b.path("src/main.zig"), + .target = target, + .optimize = optimize, + .link_libc = true, + .imports = &.{ + .{ .name = "sdl3-c", .module = sdl_c.createModule() }, + .{ .name = "win32", .module = b.dependency("zigwin32", .{}).module("win32") }, + }, + }), + }); + exe.root_module.linkLibrary(sdl.artifact("SDL3")); + for ([_][]const u8{ "d3d11", "dxgi", "d2d1", "dwmapi", "user32", "ole32" }) |lib| exe.root_module.linkSystemLibrary(lib, .{}); + b.installArtifact(exe); +} diff --git a/spikes/windows-composition/build.zig.zon b/spikes/windows-composition/build.zig.zon new file mode 100644 index 000000000..381369ca1 --- /dev/null +++ b/spikes/windows-composition/build.zig.zon @@ -0,0 +1,24 @@ +.{ + .name = .windows_composition_spike, + .version = "0.0.0", + .fingerprint = 0x28d1b8a12c4276b6, + .minimum_zig_version = "0.16.0", + .paths = .{ + "build.zig", + "build.zig.zon", + "README.md", + "src", + }, + // The same pins as fizzy's root `build.zig.zon`: fizzyedit/SDL (through sdl_zig), whose D3D12 + // presents a transparent window through a topmost DirectComposition target, and zigwin32. + .dependencies = .{ + .sdl = .{ + .url = "https://github.com/fizzyedit/sdl_zig/archive/58abe388d4200b0f66701c0701acc6d1910bc55f.tar.gz", + .hash = "sdl-1.0.3+3.4.16-i4QD0c0KqgB_riMOO7mEHunTksg6ofuVCDPkOoYd3A4I", + }, + .zigwin32 = .{ + .url = "https://github.com/marlersoft/zigwin32/archive/ec98bb4d9eea532320a8551720a9e3ec6de64994.tar.gz", + .hash = "win32-25.0.28-preview-mX5pFWMt5QPTVIGh3r2-OpPunpcCCjApyRbA6Zn6WALH", + }, + }, +} diff --git a/spikes/windows-composition/src/comp.zig b/spikes/windows-composition/src/comp.zig new file mode 100644 index 000000000..edac6d5cc --- /dev/null +++ b/spikes/windows-composition/src/comp.zig @@ -0,0 +1,131 @@ +//! The compositor and the handful of things the spike makes with it, each a WinRT call by slot +//! (`winrt.slots`). One compositor serves every window on the thread. +const std = @import("std"); +const winrt = @import("winrt.zig"); +const u = @import("user32.zig"); + +const Obj = winrt.Obj; +const s = winrt.slots; + +const log = std.log.scoped(.comp); + +pub const Comp = struct { + c1: Obj, + c3: ?Obj, + c5: ?Obj, + c6: ?Obj, + c7: ?Obj, + desktop: Obj, + interop: Obj, + + pub fn init() !Comp { + const inspectable = try winrt.activate("Windows.UI.Composition.Compositor"); + const c: Comp = .{ + .c1 = try winrt.qi(inspectable, &winrt.IID_ICompositor, "QI ICompositor"), + .c3 = winrt.tryQi(inspectable, &winrt.IID_ICompositor3), + .c5 = winrt.tryQi(inspectable, &winrt.IID_ICompositor5), + .c6 = winrt.tryQi(inspectable, &winrt.IID_ICompositor6), + .c7 = winrt.tryQi(inspectable, &winrt.IID_ICompositor7), + .desktop = try winrt.qi(inspectable, &winrt.IID_ICompositorDesktopInterop, "QI ICompositorDesktopInterop"), + .interop = try winrt.qi(inspectable, &winrt.IID_ICompositorInterop, "QI ICompositorInterop"), + }; + log.info("compositor: ICompositor3 (host backdrop) {s}, ICompositor5 (paths) {s}, ICompositor6 (geometric clip) {s}, ICompositor7 (rounded rect clip) {s}", .{ + have(c.c3), have(c.c5), have(c.c6), have(c.c7), + }); + return c; + } + + fn have(o: ?Obj) []const u8 { + return if (o != null) "yes" else "NO"; + } + + pub const Target = struct { target: Obj, topmost: bool }; + + /// A `DesktopWindowTarget` for `hwnd`, as `ICompositionTarget`. + pub fn desktopTarget(c: *const Comp, hwnd: u.HWND, topmost: bool) !Target { + var dwt: ?*anyopaque = null; + try winrt.check(winrt.method(*const fn (Obj, u.HWND, u.BOOL, *?*anyopaque) callconv(.winapi) winrt.HRESULT, c.desktop, s.ICompositorDesktopInterop.CreateDesktopWindowTarget)(c.desktop, hwnd, @intFromBool(topmost), &dwt), "CreateDesktopWindowTarget"); + var is_top: u8 = 0; + try winrt.check(winrt.method(*const fn (Obj, *u8) callconv(.winapi) winrt.HRESULT, dwt.?, s.IDesktopWindowTarget.get_IsTopmost)(dwt.?, &is_top), "get_IsTopmost"); + return .{ .target = try winrt.qi(dwt.?, &winrt.IID_ICompositionTarget, "QI ICompositionTarget"), .topmost = is_top != 0 }; + } + + pub const Container = struct { visual: Obj, children: Obj }; + + pub fn container(c: *const Comp) !Container { + const cv = try winrt.get(c.c1, s.ICompositor.CreateContainerVisual, "CreateContainerVisual"); + return .{ .visual = try winrt.qi(cv, &winrt.IID_IVisual, "QI IVisual"), .children = try winrt.get(cv, s.IContainerVisual.get_Children, "get_Children") }; + } + + pub const Sprite = struct { sprite: Obj, visual: Obj }; + + pub fn sprite(c: *const Comp) !Sprite { + const sv = try winrt.get(c.c1, s.ICompositor.CreateSpriteVisual, "CreateSpriteVisual"); + return .{ .sprite = sv, .visual = try winrt.qi(sv, &winrt.IID_IVisual, "QI IVisual") }; + } + + /// A blur of what is behind the window, as `ICompositionBrush`. + pub fn hostBackdrop(c: *const Comp) !Obj { + const c3 = c.c3 orelse return error.NoHostBackdrop; + const b = try winrt.get(c3, s.ICompositor3.CreateHostBackdropBrush, "CreateHostBackdropBrush"); + return winrt.qi(b, &winrt.IID_ICompositionBrush, "QI ICompositionBrush (backdrop)"); + } + + pub fn colorBrush(c: *const Comp, color: winrt.Color) !Obj { + var b: ?*anyopaque = null; + try winrt.check(winrt.method(*const fn (Obj, winrt.Color, *?*anyopaque) callconv(.winapi) winrt.HRESULT, c.c1, s.ICompositor.CreateColorBrushWithColor)(c.c1, color, &b), "CreateColorBrush"); + return winrt.qi(b.?, &winrt.IID_ICompositionBrush, "QI ICompositionBrush (colour)"); + } + + /// A brush showing `swapchain` (an `IDXGISwapChain1` made for composition). + pub fn swapchainBrush(c: *const Comp, swapchain: Obj) !Obj { + var surf: ?*anyopaque = null; + try winrt.check(winrt.method(*const fn (Obj, Obj, *?*anyopaque) callconv(.winapi) winrt.HRESULT, c.interop, s.ICompositorInterop.CreateCompositionSurfaceForSwapChain)(c.interop, swapchain, &surf), "CreateCompositionSurfaceForSwapChain"); + var b: ?*anyopaque = null; + try winrt.check(winrt.method(*const fn (Obj, Obj, *?*anyopaque) callconv(.winapi) winrt.HRESULT, c.c1, s.ICompositor.CreateSurfaceBrushWithSurface)(c.c1, surf.?, &b), "CreateSurfaceBrush"); + return winrt.qi(b.?, &winrt.IID_ICompositionBrush, "QI ICompositionBrush (surface)"); + } + + pub const RectClip = struct { rect: Obj, clip: Obj }; + + pub fn rectClip(c: *const Comp) !RectClip { + const c7 = c.c7 orelse return error.NoRectangleClip; + const r = try winrt.get(c7, s.ICompositor7.CreateRectangleClip, "CreateRectangleClip"); + return .{ .rect = r, .clip = try winrt.qi(r, &winrt.IID_ICompositionClip, "QI ICompositionClip (rect)") }; + } + + pub fn setRectClip(_: *const Comp, rect: Obj, l: f32, t: f32, r: f32, b: f32, radius: f32) !void { + const R = s.IRectangleClip; + try winrt.put(f32, rect, R.put_Left, l, "put_Left"); + try winrt.put(f32, rect, R.put_Top, t, "put_Top"); + try winrt.put(f32, rect, R.put_Right, r, "put_Right"); + try winrt.put(f32, rect, R.put_Bottom, b, "put_Bottom"); + const v: winrt.Vec2 = .{ .x = radius, .y = radius }; + inline for (.{ R.put_TopLeftRadius, R.put_TopRightRadius, R.put_BottomRightRadius, R.put_BottomLeftRadius }) |slot| + try winrt.put(winrt.Vec2, rect, slot, v, "put_*Radius"); + } + + pub const PathClip = struct { path_geometry: Obj, clip: Obj }; + + pub fn pathClip(c: *const Comp) !PathClip { + const c5 = c.c5 orelse return error.NoPaths; + const c6 = c.c6 orelse return error.NoGeometricClip; + const pg = try winrt.get(c5, s.ICompositor5.CreatePathGeometry, "CreatePathGeometry"); + const geom = try winrt.qi(pg, &winrt.IID_ICompositionGeometry, "QI ICompositionGeometry"); + var gc: ?*anyopaque = null; + try winrt.check(winrt.method(*const fn (Obj, Obj, *?*anyopaque) callconv(.winapi) winrt.HRESULT, c6, s.ICompositor6.CreateGeometricClipWithGeometry)(c6, geom, &gc), "CreateGeometricClip"); + return .{ .path_geometry = pg, .clip = try winrt.qi(gc.?, &winrt.IID_ICompositionClip, "QI ICompositionClip (geometric)") }; + } + + pub fn setSize(_: *const Comp, visual: Obj, w: f32, h: f32) !void { + try winrt.put(winrt.Vec2, visual, s.IVisual.put_Size, .{ .x = w, .y = h }, "put_Size"); + } + + pub fn setOffset(_: *const Comp, visual: Obj, x: f32, y: f32) !void { + try winrt.put(winrt.Vec3, visual, s.IVisual.put_Offset, .{ .x = x, .y = y, .z = 0 }, "put_Offset"); + } + + pub fn insertTop(_: *const Comp, children: Obj, visual: Obj) !void { + try winrt.put(Obj, children, s.IVisualCollection.InsertAtTop, visual, "InsertAtTop"); + } +}; diff --git a/spikes/windows-composition/src/effect.zig b/spikes/windows-composition/src/effect.zig new file mode 100644 index 000000000..1eb67f3aa --- /dev/null +++ b/spikes/windows-composition/src/effect.zig @@ -0,0 +1,201 @@ +//! An effect description composition can compile: a Direct2D effect by CLSID, its sources and +//! properties, given through `IGraphicsEffectD2D1Interop`, as Win2D's effects give them. Written +//! by hand, for spike 3: the host backdrop masked by a surface the app presents. +const std = @import("std"); +const win32 = @import("win32"); +const winrt = @import("winrt.zig"); +const geometry = @import("geometry.zig"); + +const Guid = winrt.Guid; +const HRESULT = winrt.HRESULT; + +pub const Effect = extern struct { + /// `IGraphicsEffect`, which also answers for `IGraphicsEffectSource` (it adds nothing to + /// IInspectable), IUnknown, IInspectable and IAgileObject. + effect_vtbl: *const EffectVtbl = &effect_vtbl, + /// `IGraphicsEffectD2D1Interop`. + interop_vtbl: *const InteropVtbl = &interop_vtbl, + refs: u32 = 1, + id: Guid, + /// `IGraphicsEffectSource`s, referenced. + sources: [2]?*anyopaque, + source_count: u32, + /// `IPropertyValue`s, referenced, by D2D property index. + properties: [1]?*anyopaque, + property_count: u32, + + /// Owns one reference to each of `sources` and `properties`. + pub fn create(id: Guid, sources: []const winrt.Obj, properties: []const winrt.Obj) !*Effect { + const e = try std.heap.c_allocator.create(Effect); + e.* = .{ .id = id, .sources = .{ null, null }, .source_count = @intCast(sources.len), .properties = .{null}, .property_count = @intCast(properties.len) }; + for (sources, 0..) |s, i| e.sources[i] = s; + for (properties, 0..) |p, i| e.properties[i] = p; + return e; + } + + pub fn object(e: *Effect) winrt.Obj { + return @ptrCast(&e.effect_vtbl); + } + + fn fromEffect(this: *anyopaque) *Effect { + return @ptrCast(@alignCast(this)); + } + + fn fromInterop(this: *anyopaque) *Effect { + const field: **const InteropVtbl = @ptrCast(@alignCast(this)); + return @fieldParentPtr("interop_vtbl", field); + } + + fn query(e: *Effect, riid: *const Guid, out: *?*anyopaque) HRESULT { + if (eql(riid, &winrt.IID_IUnknown) or eql(riid, &winrt.IID_IInspectable) or eql(riid, &winrt.IID_IGraphicsEffect) or + eql(riid, &winrt.IID_IGraphicsEffectSource) or eql(riid, &winrt.IID_IAgileObject)) + { + out.* = @ptrCast(&e.effect_vtbl); + } else if (eql(riid, &winrt.IID_IGraphicsEffectD2D1Interop)) { + out.* = @ptrCast(&e.interop_vtbl); + } else { + out.* = null; + return e_nointerface; + } + _ = @atomicRmw(u32, &e.refs, .Add, 1, .monotonic); + return 0; + } + + fn addRef(e: *Effect) u32 { + return @atomicRmw(u32, &e.refs, .Add, 1, .monotonic) + 1; + } + + fn release(e: *Effect) u32 { + const left = @atomicRmw(u32, &e.refs, .Sub, 1, .acq_rel) - 1; + if (left == 0) { + for (e.sources) |s| if (s) |o| winrt.release(o); + for (e.properties) |p| if (p) |o| winrt.release(o); + std.heap.c_allocator.destroy(e); + } + return left; + } +}; + +const e_nointerface: HRESULT = @bitCast(@as(u32, 0x80004002)); +const e_invalidarg: HRESULT = @bitCast(@as(u32, 0x80070057)); + +fn eql(a: *const Guid, b: *const Guid) bool { + return std.mem.eql(u8, std.mem.asBytes(a), std.mem.asBytes(b)); +} + +const EffectVtbl = extern struct { + QueryInterface: *const fn (*anyopaque, *const Guid, *?*anyopaque) callconv(.winapi) HRESULT, + AddRef: *const fn (*anyopaque) callconv(.winapi) u32, + Release: *const fn (*anyopaque) callconv(.winapi) u32, + GetIids: *const fn (*anyopaque, *u32, *?*Guid) callconv(.winapi) HRESULT, + GetRuntimeClassName: *const fn (*anyopaque, *winrt.HSTRING) callconv(.winapi) HRESULT, + GetTrustLevel: *const fn (*anyopaque, *i32) callconv(.winapi) HRESULT, + get_Name: *const fn (*anyopaque, *winrt.HSTRING) callconv(.winapi) HRESULT, + put_Name: *const fn (*anyopaque, winrt.HSTRING) callconv(.winapi) HRESULT, +}; + +const InteropVtbl = extern struct { + QueryInterface: *const fn (*anyopaque, *const Guid, *?*anyopaque) callconv(.winapi) HRESULT, + AddRef: *const fn (*anyopaque) callconv(.winapi) u32, + Release: *const fn (*anyopaque) callconv(.winapi) u32, + GetEffectId: *const fn (*anyopaque, *Guid) callconv(.winapi) HRESULT, + GetNamedPropertyMapping: *const fn (*anyopaque, ?[*:0]const u16, *u32, *i32) callconv(.winapi) HRESULT, + GetPropertyCount: *const fn (*anyopaque, *u32) callconv(.winapi) HRESULT, + GetProperty: *const fn (*anyopaque, u32, *?*anyopaque) callconv(.winapi) HRESULT, + GetSource: *const fn (*anyopaque, u32, *?*anyopaque) callconv(.winapi) HRESULT, + GetSourceCount: *const fn (*anyopaque, *u32) callconv(.winapi) HRESULT, +}; + +const effect_vtbl: EffectVtbl = .{ + .QueryInterface = struct { + fn f(this: *anyopaque, riid: *const Guid, out: *?*anyopaque) callconv(.winapi) HRESULT { + return Effect.fromEffect(this).query(riid, out); + } + }.f, + .AddRef = struct { + fn f(this: *anyopaque) callconv(.winapi) u32 { + return Effect.fromEffect(this).addRef(); + } + }.f, + .Release = struct { + fn f(this: *anyopaque) callconv(.winapi) u32 { + return Effect.fromEffect(this).release(); + } + }.f, + .GetIids = geometry.inspectableGetIids, + .GetRuntimeClassName = geometry.inspectableGetRuntimeClassName, + .GetTrustLevel = geometry.inspectableGetTrustLevel, + // Unnamed: the brush names its sources' parameters, not the effect. + .get_Name = struct { + fn f(_: *anyopaque, name: *winrt.HSTRING) callconv(.winapi) HRESULT { + name.* = null; + return 0; + } + }.f, + .put_Name = struct { + fn f(_: *anyopaque, _: winrt.HSTRING) callconv(.winapi) HRESULT { + return 0; + } + }.f, +}; + +const interop_vtbl: InteropVtbl = .{ + .QueryInterface = struct { + fn f(this: *anyopaque, riid: *const Guid, out: *?*anyopaque) callconv(.winapi) HRESULT { + return Effect.fromInterop(this).query(riid, out); + } + }.f, + .AddRef = struct { + fn f(this: *anyopaque) callconv(.winapi) u32 { + return Effect.fromInterop(this).addRef(); + } + }.f, + .Release = struct { + fn f(this: *anyopaque) callconv(.winapi) u32 { + return Effect.fromInterop(this).release(); + } + }.f, + .GetEffectId = struct { + fn f(this: *anyopaque, id: *Guid) callconv(.winapi) HRESULT { + id.* = Effect.fromInterop(this).id; + return 0; + } + }.f, + .GetNamedPropertyMapping = struct { + fn f(_: *anyopaque, _: ?[*:0]const u16, _: *u32, _: *i32) callconv(.winapi) HRESULT { + return e_invalidarg; + } + }.f, + .GetPropertyCount = struct { + fn f(this: *anyopaque, count: *u32) callconv(.winapi) HRESULT { + count.* = Effect.fromInterop(this).property_count; + return 0; + } + }.f, + .GetProperty = struct { + fn f(this: *anyopaque, index: u32, out: *?*anyopaque) callconv(.winapi) HRESULT { + const e = Effect.fromInterop(this); + if (index >= e.property_count) return e_invalidarg; + const p = e.properties[index].?; + winrt.addRef(p); + out.* = p; + return 0; + } + }.f, + .GetSource = struct { + fn f(this: *anyopaque, index: u32, out: *?*anyopaque) callconv(.winapi) HRESULT { + const e = Effect.fromInterop(this); + if (index >= e.source_count) return e_invalidarg; + const s = e.sources[index].?; + winrt.addRef(s); + out.* = s; + return 0; + } + }.f, + .GetSourceCount = struct { + fn f(this: *anyopaque, count: *u32) callconv(.winapi) HRESULT { + count.* = Effect.fromInterop(this).source_count; + return 0; + } + }.f, +}; diff --git a/spikes/windows-composition/src/geometry.zig b/spikes/windows-composition/src/geometry.zig new file mode 100644 index 000000000..7b84e8556 --- /dev/null +++ b/spikes/windows-composition/src/geometry.zig @@ -0,0 +1,218 @@ +//! The glass's outline as Direct2D builds it, and the object that hands it to composition. +//! +//! `CompositionPath` takes any `IGeometrySource2D` that also answers `IGeometrySource2DInterop` +//! with a Direct2D geometry (Win2D's `CanvasGeometry` is one). `Source` is that object, written +//! by hand: a fresh one each frame, as fizzy would trace the field's edge each frame. +const std = @import("std"); +const win32 = @import("win32"); +const winrt = @import("winrt.zig"); + +const d2d = win32.graphics.direct2d; +const common = d2d.common; +const Guid = winrt.Guid; +const HRESULT = winrt.HRESULT; + +/// What the spike draws: a pill and, when `merged`, a circle orbiting it, the two as one outline +/// (a union, standing in for LiquidField's smooth union). Pixels of the window. +pub const Shape = struct { + pill: common.D2D_RECT_F, + circle: common.D2D_POINT_2F, + radius: f32, + merged: bool, + + pub fn bounds(s: Shape) common.D2D_RECT_F { + var b = s.pill; + if (s.merged) { + b.left = @min(b.left, s.circle.x - s.radius); + b.top = @min(b.top, s.circle.y - s.radius); + b.right = @max(b.right, s.circle.x + s.radius); + b.bottom = @max(b.bottom, s.circle.y + s.radius); + } + return b; + } +}; + +/// `s` as one geometry of `factory`'s, owned by the caller. +pub fn build(factory: *d2d.ID2D1Factory, s: Shape) !*d2d.ID2D1Geometry { + const h = s.pill.bottom - s.pill.top; + var pill: *d2d.ID2D1RoundedRectangleGeometry = undefined; + try winrt.check(factory.CreateRoundedRectangleGeometry(&.{ .rect = s.pill, .radiusX = h / 2, .radiusY = h / 2 }, &pill), "CreateRoundedRectangleGeometry"); + if (!s.merged) return &pill.ID2D1Geometry; + defer _ = pill.IUnknown.Release(); + var circle: *d2d.ID2D1EllipseGeometry = undefined; + try winrt.check(factory.CreateEllipseGeometry(&.{ .point = s.circle, .radiusX = s.radius, .radiusY = s.radius }, &circle), "CreateEllipseGeometry"); + defer _ = circle.IUnknown.Release(); + var path: *d2d.ID2D1PathGeometry = undefined; + try winrt.check(factory.CreatePathGeometry(&path), "CreatePathGeometry"); + errdefer _ = path.IUnknown.Release(); + var sink: *d2d.ID2D1GeometrySink = undefined; + try winrt.check(path.Open(&sink), "ID2D1PathGeometry.Open"); + defer _ = sink.IUnknown.Release(); + try winrt.check(pill.ID2D1Geometry.CombineWithGeometry(&circle.ID2D1Geometry, d2d.D2D1_COMBINE_MODE_UNION, null, 0.25, &sink.ID2D1SimplifiedGeometrySink), "CombineWithGeometry"); + try winrt.check(sink.ID2D1SimplifiedGeometrySink.Close(), "ID2D1GeometrySink.Close"); + return &path.ID2D1Geometry; +} + +/// How composition asked for the geometry, for the report. +pub var get_geometry_calls: u32 = 0; +pub var try_factory_calls: u32 = 0; + +/// An `IGeometrySource2D` over one Direct2D geometry. Starts with one reference, which the caller +/// gives up once `CompositionPath` has taken its own. +pub const Source = extern struct { + /// `IGeometrySource2D` (and IUnknown, IInspectable, IAgileObject): this pointer. + source_vtbl: *const SourceVtbl = &source_vtbl, + /// `IGeometrySource2DInterop`. + interop_vtbl: *const InteropVtbl = &interop_vtbl, + refs: u32 = 1, + geometry: *d2d.ID2D1Geometry, + factory: *d2d.ID2D1Factory, + + /// Takes over `geometry`'s reference. + pub fn create(geometry: *d2d.ID2D1Geometry, factory: *d2d.ID2D1Factory) !*Source { + const s = try std.heap.c_allocator.create(Source); + s.* = .{ .geometry = geometry, .factory = factory }; + return s; + } + + pub fn object(s: *Source) winrt.Obj { + return @ptrCast(&s.source_vtbl); + } + + fn fromSource(this: *anyopaque) *Source { + return @ptrCast(@alignCast(this)); + } + + fn fromInterop(this: *anyopaque) *Source { + const field: **const InteropVtbl = @ptrCast(@alignCast(this)); + return @fieldParentPtr("interop_vtbl", field); + } + + fn query(s: *Source, riid: *const Guid, out: *?*anyopaque) HRESULT { + if (eql(riid, &winrt.IID_IUnknown) or eql(riid, &winrt.IID_IInspectable) or + eql(riid, &winrt.IID_IGeometrySource2D) or eql(riid, &winrt.IID_IAgileObject)) + { + out.* = @ptrCast(&s.source_vtbl); + } else if (eql(riid, &winrt.IID_IGeometrySource2DInterop)) { + out.* = @ptrCast(&s.interop_vtbl); + } else { + out.* = null; + return e_nointerface; + } + _ = @atomicRmw(u32, &s.refs, .Add, 1, .monotonic); + return 0; + } + + fn addRef(s: *Source) u32 { + return @atomicRmw(u32, &s.refs, .Add, 1, .monotonic) + 1; + } + + fn release(s: *Source) u32 { + const left = @atomicRmw(u32, &s.refs, .Sub, 1, .acq_rel) - 1; + if (left == 0) { + _ = s.geometry.IUnknown.Release(); + std.heap.c_allocator.destroy(s); + } + return left; + } +}; + +const e_nointerface: HRESULT = @bitCast(@as(u32, 0x80004002)); +const e_notimpl: HRESULT = @bitCast(@as(u32, 0x80004001)); + +fn eql(a: *const Guid, b: *const Guid) bool { + return std.mem.eql(u8, std.mem.asBytes(a), std.mem.asBytes(b)); +} + +const SourceVtbl = extern struct { + QueryInterface: *const fn (*anyopaque, *const Guid, *?*anyopaque) callconv(.winapi) HRESULT, + AddRef: *const fn (*anyopaque) callconv(.winapi) u32, + Release: *const fn (*anyopaque) callconv(.winapi) u32, + GetIids: *const fn (*anyopaque, *u32, *?*Guid) callconv(.winapi) HRESULT, + GetRuntimeClassName: *const fn (*anyopaque, *winrt.HSTRING) callconv(.winapi) HRESULT, + GetTrustLevel: *const fn (*anyopaque, *i32) callconv(.winapi) HRESULT, +}; + +const InteropVtbl = extern struct { + QueryInterface: *const fn (*anyopaque, *const Guid, *?*anyopaque) callconv(.winapi) HRESULT, + AddRef: *const fn (*anyopaque) callconv(.winapi) u32, + Release: *const fn (*anyopaque) callconv(.winapi) u32, + GetGeometry: *const fn (*anyopaque, *?*d2d.ID2D1Geometry) callconv(.winapi) HRESULT, + TryGetGeometryUsingFactory: *const fn (*anyopaque, ?*d2d.ID2D1Factory, *?*d2d.ID2D1Geometry) callconv(.winapi) HRESULT, +}; + +const source_vtbl: SourceVtbl = .{ + .QueryInterface = struct { + fn f(this: *anyopaque, riid: *const Guid, out: *?*anyopaque) callconv(.winapi) HRESULT { + return Source.fromSource(this).query(riid, out); + } + }.f, + .AddRef = struct { + fn f(this: *anyopaque) callconv(.winapi) u32 { + return Source.fromSource(this).addRef(); + } + }.f, + .Release = struct { + fn f(this: *anyopaque) callconv(.winapi) u32 { + return Source.fromSource(this).release(); + } + }.f, + .GetIids = inspectableGetIids, + .GetRuntimeClassName = inspectableGetRuntimeClassName, + .GetTrustLevel = inspectableGetTrustLevel, +}; + +const interop_vtbl: InteropVtbl = .{ + .QueryInterface = struct { + fn f(this: *anyopaque, riid: *const Guid, out: *?*anyopaque) callconv(.winapi) HRESULT { + return Source.fromInterop(this).query(riid, out); + } + }.f, + .AddRef = struct { + fn f(this: *anyopaque) callconv(.winapi) u32 { + return Source.fromInterop(this).addRef(); + } + }.f, + .Release = struct { + fn f(this: *anyopaque) callconv(.winapi) u32 { + return Source.fromInterop(this).release(); + } + }.f, + .GetGeometry = struct { + fn f(this: *anyopaque, out: *?*d2d.ID2D1Geometry) callconv(.winapi) HRESULT { + get_geometry_calls += 1; + const s = Source.fromInterop(this); + _ = s.geometry.IUnknown.AddRef(); + out.* = s.geometry; + return 0; + } + }.f, + // As Win2D answers it: the geometry when it is of the factory asked about, else none. + .TryGetGeometryUsingFactory = struct { + fn f(this: *anyopaque, factory: ?*d2d.ID2D1Factory, out: *?*d2d.ID2D1Geometry) callconv(.winapi) HRESULT { + try_factory_calls += 1; + const s = Source.fromInterop(this); + if (factory != null and @intFromPtr(factory.?) == @intFromPtr(s.factory)) { + _ = s.geometry.IUnknown.AddRef(); + out.* = s.geometry; + } else out.* = null; + return 0; + } + }.f, +}; + +pub fn inspectableGetIids(_: *anyopaque, count: *u32, iids: *?*Guid) callconv(.winapi) HRESULT { + count.* = 0; + iids.* = null; + return 0; +} + +pub fn inspectableGetRuntimeClassName(_: *anyopaque, name: *winrt.HSTRING) callconv(.winapi) HRESULT { + name.* = null; + return e_notimpl; +} + +pub fn inspectableGetTrustLevel(_: *anyopaque, level: *i32) callconv(.winapi) HRESULT { + level.* = 0; // BaseTrust + return 0; +} diff --git a/spikes/windows-composition/src/main.zig b/spikes/windows-composition/src/main.zig new file mode 100644 index 000000000..f5d3befcc --- /dev/null +++ b/spikes/windows-composition/src/main.zig @@ -0,0 +1,549 @@ +//! Windows composition spike (docs/WINDOWS_LINUX_GLASS_PLAN.md, "Order of work" step 5). See +//! README.md for what each mode asks and how to read what it shows. +//! +//! An SDL window made transparent and claimed for SDL_GPU on D3D12, so fizzyedit/SDL presents it +//! through a topmost DirectComposition target, as fizzy's windows are. Under that, on the same +//! window, a Windows.UI.Composition target that is not topmost, holding one sprite: the host +//! backdrop (a blur of what is behind the window), cut to a moving shape. The app draws the +//! shape's rim in its own present; the blur is a composition property; how the two line up is the +//! point. +const std = @import("std"); +const builtin = @import("builtin"); +const c = @import("sdl3-c"); +const win32 = @import("win32"); +const winrt = @import("winrt.zig"); +const comp = @import("comp.zig"); +const geometry = @import("geometry.zig"); +const surface = @import("surface.zig"); +const effect = @import("effect.zig"); +const Overlay = @import("overlay.zig"); +const u = @import("user32.zig"); + +comptime { + if (builtin.os.tag != .windows) @compileError("the composition spike is Windows only: -Dtarget=x86_64-windows-gnu or aarch64-windows-gnu"); +} + +pub const std_options: std.Options = .{ .log_level = .info }; +const log = std.log.scoped(.spike); + +const Mode = enum { + /// The backdrop over the whole window: does the tree show under SDL's target at all (spike 1)? + full, + /// A rounded-rect clip, its edges set each frame: the cheap case, a card or tab. + rect, + /// A Direct2D path rebuilt each frame (pill ∪ circle) as a geometric clip (spike 2). + path, + /// The backdrop masked through an effect graph by a swapchain the app presents (spike 3). + mask, + /// As `path`, with the app's picture hosted in this tree instead of SDL's target: spike 1's + /// fallback, should the two targets not share the window. + hosted, +}; + +const Timing = enum { + /// Composition properties set, then the present: as Avalonia does. + before, + /// Set, then the thread's messages dispatched (the DispatcherQueue commits), then the present. + pump, + /// The present, then the properties: one frame behind on purpose, for comparison. + after, +}; + +const State = struct { + mode: Mode = .path, + timing: Timing = .pump, + merged: bool = true, + use_backdrop: bool = true, + translucent: bool = false, + paused: bool = false, + /// Pixels a second the pill travels, at a constant speed between its turns, so that an offset + /// between the rim and the glass divides by the pixels a frame into frames. + speed: f32 = 600, + /// How far it has travelled, and the circle's angle. + travel: f32 = 0, + phase: f32 = 0, + overlay_variant: ?Overlay.Variant = null, + /// SDL_GPU's frames in flight (SDL's default is 2): how far the present trails its submit. + frames_in_flight: u32 = 2, + /// The composition side gets the shape from this many frames ago, to land with the present. + clip_delay: u32 = 0, +}; + +fn envEnum(comptime E: type, name: [*:0]const u8) ?E { + const v = std.c.getenv(name) orelse return null; + return std.meta.stringToEnum(E, std.mem.span(v)); +} + +fn envNumber(name: [*:0]const u8) ?f32 { + const v = std.c.getenv(name) orelse return null; + return std.fmt.parseFloat(f32, std.mem.span(v)) catch null; +} + +pub fn main() !void { + var st: State = .{}; + if (envEnum(Mode, "SPIKE_MODE")) |m| st.mode = m; + if (envEnum(Timing, "SPIKE_TIMING")) |t| st.timing = t; + if (envEnum(Overlay.Variant, "SPIKE_OVERLAY")) |v| st.overlay_variant = v; + if (envNumber("SPIKE_SPEED")) |v| st.speed = v; + if (std.c.getenv("SPIKE_SINGLE") != null) st.merged = false; + if (envNumber("SPIKE_FRAMES_IN_FLIGHT")) |v| st.frames_in_flight = @intFromFloat(v); + if (envNumber("SPIKE_CLIP_DELAY")) |v| st.clip_delay = @intFromFloat(v); + st.translucent = std.c.getenv("SPIKE_TRANSLUCENT") != null; + if (std.c.getenv("SPIKE_COLOR") != null) st.use_backdrop = false; + const run_seconds = envNumber("SPIKE_SECONDS"); + + const queue_controller = try winrt.initThread(); + _ = queue_controller; + + // SDL3 is DPI aware: pixel sizes below (`SDL_GetWindowSizeInPixels`, the swapchain's) are the + // units composition's desktop target uses too. + if (!c.SDL_Init(c.SDL_INIT_VIDEO)) return sdlError("SDL_Init"); + const window = c.SDL_CreateWindow("fizzy composition spike", 1000, 640, c.SDL_WINDOW_TRANSPARENT | c.SDL_WINDOW_HIGH_PIXEL_DENSITY) orelse return sdlError("SDL_CreateWindow"); + const hwnd: u.HWND = @ptrCast(c.SDL_GetPointerProperty(c.SDL_GetWindowProperties(window), c.SDL_PROP_WINDOW_WIN32_HWND_POINTER, null) orelse return error.NoHwnd); + const gpu = c.SDL_CreateGPUDevice(c.SDL_GPU_SHADERFORMAT_DXIL, false, "direct3d12") orelse return sdlError("SDL_CreateGPUDevice"); + if (!c.SDL_ClaimWindowForGPUDevice(gpu, window)) return sdlError("SDL_ClaimWindowForGPUDevice"); + if (!c.SDL_SetGPUAllowedFramesInFlight(gpu, st.frames_in_flight)) return sdlError("SDL_SetGPUAllowedFramesInFlight"); + // SDL's present mode: VSYNC (fizzy's), or MAILBOX / IMMEDIATE where the swapchain supports them. + if (std.c.getenv("SPIKE_PRESENT")) |v| { + const name = std.mem.span(v); + const mode: c.SDL_GPUPresentMode = if (std.mem.eql(u8, name, "mailbox")) c.SDL_GPU_PRESENTMODE_MAILBOX else if (std.mem.eql(u8, name, "immediate")) c.SDL_GPU_PRESENTMODE_IMMEDIATE else c.SDL_GPU_PRESENTMODE_VSYNC; + log.info("present mode {s}: supported {}, set {}", .{ name, c.SDL_WindowSupportsGPUPresentMode(gpu, window, mode), c.SDL_SetGPUSwapchainParameters(gpu, window, c.SDL_GPU_SWAPCHAINCOMPOSITION_SDR, mode) }); + } + if (st.translucent) { + _ = c.SDL_SetWindowOpacity(window, 0.6); + log.info("spike 4b: SDL_SetWindowOpacity(0.6) -> ex style 0x{x}", .{@as(u32, @truncate(@as(usize, @bitCast(u.GetWindowLongPtrW(hwnd, u.GWL_EXSTYLE)))))}); + } + log.info("SDL_GPU driver {s}; window transparent, claimed (fizzyedit/SDL presents it through a topmost DirectComposition target)", .{std.mem.span(c.SDL_GetGPUDeviceDriver(gpu))}); + + // No DWM system backdrop (the host backdrop is the only material), and permission for it. + const none: u32 = u.DWMSBT_NONE; + const yes: u.BOOL = 1; + log.info("DWM: SYSTEMBACKDROP_TYPE none hr=0x{x}, USE_HOSTBACKDROPBRUSH hr=0x{x}", .{ + @as(u32, @bitCast(u.DwmSetWindowAttribute(hwnd, u.DWMWA_SYSTEMBACKDROP_TYPE, &none, 4))), + @as(u32, @bitCast(u.DwmSetWindowAttribute(hwnd, u.DWMWA_USE_HOSTBACKDROPBRUSH, &yes, 4))), + }); + + const red = try solidTexture(gpu, .{ 230, 40, 40, 255 }); + const yellow = try solidTexture(gpu, .{ 240, 200, 40, 255 }); + const green = try solidTexture(gpu, .{ 40, 220, 80, 255 }); + + // ---- the composition tree under SDL's ---- + const cmp = try comp.Comp.init(); + const tgt = try cmp.desktopTarget(hwnd, false); + log.info("spike 1: DesktopWindowTarget made on SDL's window, IsTopmost = {} (SDL's DirectComposition target is the topmost one)", .{tgt.topmost}); + const root = try cmp.container(); + try winrt.put(?*anyopaque, tgt.target, winrt.slots.ICompositionTarget.put_Root, root.visual, "put_Root"); + const glass = try cmp.sprite(); + try cmp.insertTop(root.children, glass.visual); + const backdrop = cmp.hostBackdrop() catch |e| blk: { + log.err("no host backdrop brush ({s}): a colour stands in", .{@errorName(e)}); + break :blk null; + }; + const colour = try cmp.colorBrush(.{ .a = 255, .r = 60, .g = 140, .b = 230 }); + const rect_clip = try cmp.rectClip(); + const path_clip = try cmp.pathClip(); + const path_factory = try winrt.factory("Windows.UI.Composition.CompositionPath", &winrt.IID_ICompositionPathFactory); + + const g2 = try surface.Gpu.init(); + log.info("Direct2D/D3D11 for the presented masks and rims: {s}", .{if (g2.warp) "WARP" else "hardware"}); + + var w_px: c_int = 0; + var h_px: c_int = 0; + _ = c.SDL_GetWindowSizeInPixels(window, &w_px, &h_px); + const W: f32 = @floatFromInt(w_px); + const H: f32 = @floatFromInt(h_px); + try cmp.setSize(glass.visual, W, H); + + // Spike 3 and the hosted picture, made when first asked for. + var mask: ?Mask = null; + var hosted: ?Hosted = null; + var overlay: ?Overlay = null; + defer if (overlay) |*o| o.close(); + + var applied: ?Mode = null; + var applied_brush: ?bool = null; + var last_ns = c.SDL_GetTicksNS(); + const start_ns = last_ns; + var stats: Stats = .{ .since = last_ns }; + var drag_motion: u32 = 0; + var history: [8]geometry.Shape = undefined; + var history_at: usize = 0; + + main: while (true) { + var ev: c.SDL_Event = undefined; + while (c.SDL_PollEvent(&ev)) switch (ev.type) { + c.SDL_EVENT_QUIT => break :main, + c.SDL_EVENT_KEY_DOWN => switch (ev.key.key) { + c.SDLK_ESCAPE => break :main, + c.SDLK_1 => st.mode = .full, + c.SDLK_2 => st.mode = .rect, + c.SDLK_3 => st.mode = .path, + c.SDLK_4 => st.mode = .mask, + c.SDLK_5 => st.mode = .hosted, + c.SDLK_M => st.merged = !st.merged, + c.SDLK_F => { + st.frames_in_flight = st.frames_in_flight % 3 + 1; + _ = c.SDL_SetGPUAllowedFramesInFlight(gpu, st.frames_in_flight); + }, + c.SDLK_D => st.clip_delay = (st.clip_delay + 1) % 4, + c.SDLK_T => st.timing = @enumFromInt((@intFromEnum(st.timing) + 1) % 3), + c.SDLK_B => st.use_backdrop = !st.use_backdrop, + c.SDLK_SPACE => st.paused = !st.paused, + c.SDLK_UP => st.speed *= 1.5, + c.SDLK_DOWN => st.speed /= 1.5, + c.SDLK_O => { + // Spike 4b: SDL makes the window layered for this. + st.translucent = !st.translucent; + _ = c.SDL_SetWindowOpacity(window, if (st.translucent) 0.6 else 1.0); + log.info("spike 4b: SDL_SetWindowOpacity({d}) -> ex style 0x{x}", .{ @as(f32, if (st.translucent) 0.6 else 1.0), @as(u32, @truncate(@as(usize, @bitCast(u.GetWindowLongPtrW(hwnd, u.GWL_EXSTYLE))))) }); + }, + c.SDLK_V => { + if (overlay) |*o| o.close(); + overlay = null; + st.overlay_variant = Overlay.Variant.next(st.overlay_variant); + }, + else => {}, + }, + c.SDL_EVENT_MOUSE_BUTTON_DOWN => log.info("click reached the spike window at {d:.0},{d:.0}", .{ ev.button.x, ev.button.y }), + c.SDL_EVENT_MOUSE_BUTTON_UP => if (drag_motion > 0) { + log.info("drag ended: the spike window saw {d} motion events while held", .{drag_motion}); + drag_motion = 0; + }, + c.SDL_EVENT_MOUSE_MOTION => if (ev.motion.state != 0) { + drag_motion += 1; + }, + else => {}, + }; + if (run_seconds) |secs| if (@as(f32, @floatFromInt(c.SDL_GetTicksNS() - start_ns)) / 1e9 > secs) break :main; + + // The overlay, opened for the variant asked. + if (overlay == null) if (st.overlay_variant) |v| { + overlay = Overlay.open(&cmp, &g2, v) catch |e| blk: { + log.err("overlay {s}: {s}", .{ @tagName(v), @errorName(e) }); + st.overlay_variant = null; + break :blk null; + }; + }; + if (overlay) |*o| if (try o.update(&cmp)) |hit| log.info("overlay {s}: a click at the pointer goes to {s}", .{ @tagName(o.variant), hit }); + + const now = c.SDL_GetTicksNS(); + const dt: f32 = @as(f32, @floatFromInt(now - last_ns)) / 1e9; + last_ns = now; + if (!st.paused) { + st.travel += dt * st.speed; + st.phase += dt * 2.6; + } + const shape = shapeAt(st, W, H); + // The composition side's shape, `clip_delay` frames old. + history[history_at % history.len] = shape; + const clip_shape = history[(history_at + history.len - @min(st.clip_delay, history_at)) % history.len]; + history_at += 1; + + // Brush and clip kind, when the mode or brush changes. + if (applied != st.mode or applied_brush != st.use_backdrop) { + const brush = if (st.use_backdrop) (backdrop orelse colour) else colour; + switch (st.mode) { + .mask => { + if (mask == null) mask = Mask.init(&cmp, &g2, w_px, h_px, brush) catch |e| blk: { + log.err("spike 3: {s}", .{@errorName(e)}); + break :blk null; + }; + if (mask) |*m| { + try m.setSource(brush); + try winrt.put(?*anyopaque, glass.sprite, winrt.slots.ISpriteVisual.put_Brush, m.brush, "put_Brush (effect)"); + } + try winrt.put(?*anyopaque, glass.visual, winrt.slots.IVisual.put_Clip, null, "put_Clip none"); + }, + else => { + try winrt.put(?*anyopaque, glass.sprite, winrt.slots.ISpriteVisual.put_Brush, brush, "put_Brush"); + const clip: ?*anyopaque = switch (st.mode) { + .full => null, + .rect => rect_clip.clip, + .path, .hosted => path_clip.clip, + .mask => unreachable, + }; + try winrt.put(?*anyopaque, glass.visual, winrt.slots.IVisual.put_Clip, clip, "put_Clip"); + }, + } + if (st.mode == .hosted and hosted == null) hosted = try Hosted.init(&cmp, &g2, root.children, w_px, h_px); + if (hosted) |*hs| try winrt.put(u8, hs.visual, winrt.slots.IVisual.put_IsVisible, @intFromBool(st.mode == .hosted), "put_IsVisible"); + log.info("mode {s}, brush {s}", .{ @tagName(st.mode), if (st.use_backdrop and backdrop != null) "host backdrop" else "colour" }); + applied = st.mode; + applied_brush = st.use_backdrop; + } + + // The app's own frame: SDL draws the rim (nothing in `hosted`, where the hosted picture does). + const cmd = c.SDL_AcquireGPUCommandBuffer(gpu) orelse return sdlError("SDL_AcquireGPUCommandBuffer"); + var tex: ?*c.SDL_GPUTexture = null; + var tw: u32 = 0; + var th: u32 = 0; + if (!c.SDL_WaitAndAcquireGPUSwapchainTexture(cmd, window, &tex, &tw, &th)) return sdlError("SDL_WaitAndAcquireGPUSwapchainTexture"); + target_w = @floatFromInt(tw); + target_h = @floatFromInt(th); + if (tex) |t| { + clear(cmd, t); + if (st.mode != .hosted and st.mode != .full) { + box(cmd, t, red, shape.pill, 2); + // Which way the pill is going: a mark inside the box at its leading end, for + // telling a glass that trails the rim from one that leads it. + const lead_x = if (movingRight(st, W)) shape.pill.right - 14 else shape.pill.left + 6; + blit(cmd, t, green, lead_x, (shape.pill.top + shape.pill.bottom) / 2 - 4, 8, 8); + if (st.merged and st.mode != .rect) box(cmd, t, yellow, circleBox(shape), 2); + } + } + + if (st.timing != .after) try applyShape(st, &cmp, clip_shape, shape, rect_clip, path_clip, path_factory, &g2, &mask, &hosted); + if (st.timing == .pump) stats.pumped += u.pump(); + const t0 = c.SDL_GetTicksNS(); + if (!c.SDL_SubmitGPUCommandBuffer(cmd)) return sdlError("SDL_SubmitGPUCommandBuffer"); + stats.submit_ns += c.SDL_GetTicksNS() - t0; + if (st.timing == .after) try applyShape(st, &cmp, clip_shape, shape, rect_clip, path_clip, path_factory, &g2, &mask, &hosted); + + stats.frames += 1; + if (now - stats.since >= std.time.ns_per_s) { + var title: [200]u8 = undefined; + const fps = @as(f32, @floatFromInt(stats.frames)) * 1e9 / @as(f32, @floatFromInt(now - stats.since)); + const text = std.fmt.bufPrintZ(&title, "spike {s} | timing {s} | in flight {d} | clip delay {d} | {d:.0} fps | {d:.0} px/s | opacity {s} | overlay {s}", .{ + @tagName(st.mode), @tagName(st.timing), st.frames_in_flight, st.clip_delay, fps, st.speed, if (st.translucent) "0.6" else "1", if (st.overlay_variant) |v| @tagName(v) else "off", + }) catch "spike"; + _ = c.SDL_SetWindowTitle(window, text.ptr); + log.info("{s} | submit {d:.2} ms avg | {d} messages pumped | geometry: GetGeometry {d}, TryGetGeometryUsingFactory {d}", .{ + text, @as(f32, @floatFromInt(stats.submit_ns)) / 1e6 / @as(f32, @floatFromInt(stats.frames)), stats.pumped, geometry.get_geometry_calls, geometry.try_factory_calls, + }); + stats = .{ .since = now }; + } + } + c.SDL_ReleaseWindowFromGPUDevice(gpu, window); + c.SDL_DestroyGPUDevice(gpu); + c.SDL_DestroyWindow(window); + c.SDL_Quit(); +} + +const Stats = struct { since: u64, frames: u32 = 0, submit_ns: u64 = 0, pumped: u32 = 0 }; + +/// Where the glass is at the state's phase: a pill sweeping side to side, a circle orbiting it. +fn shapeAt(st: State, W: f32, H: f32) geometry.Shape { + const pw: f32 = 300; + const ph: f32 = 96; + // Back and forth across the window at a constant speed. + const span = W - pw - 400; + const along = @mod(st.travel, 2 * span); + const cx = 200 + pw / 2 + (if (along < span) along else 2 * span - along); + const cy = H / 2; + return .{ + .pill = .{ .left = cx - pw / 2, .top = cy - ph / 2, .right = cx + pw / 2, .bottom = cy + ph / 2 }, + .circle = .{ .x = cx + 150 * @cos(st.phase), .y = cy + 110 * @sin(st.phase) }, + .radius = 46, + .merged = st.merged, + }; +} + +fn movingRight(st: State, W: f32) bool { + const span = W - 300 - 400; + return @mod(st.travel, 2 * span) < span; +} + +fn circleBox(s: geometry.Shape) @TypeOf(s.pill) { + return .{ .left = s.circle.x - s.radius, .top = s.circle.y - s.radius, .right = s.circle.x + s.radius, .bottom = s.circle.y + s.radius }; +} + +/// The composition side of a frame: the clip or mask for `shape` (`clip_delay` frames old), and +/// in `hosted` the app's rim for `now`, the frame's own shape. +fn applyShape(st: State, cmp: *const comp.Comp, shape: geometry.Shape, now: geometry.Shape, rect_clip: comp.Comp.RectClip, path_clip: comp.Comp.PathClip, path_factory: winrt.Obj, g2: *const surface.Gpu, mask: *?Mask, hosted: *?Hosted) !void { + switch (st.mode) { + .full => {}, + .rect => { + const p = shape.pill; + try cmp.setRectClip(rect_clip.rect, p.left, p.top, p.right, p.bottom, (p.bottom - p.top) / 2); + }, + .path, .hosted => { + const geom = try geometry.build(g2.d2dFactory(), shape); + const src = try geometry.Source.create(geom, g2.d2dFactory()); + defer winrt.release(src.object()); + var path: ?*anyopaque = null; + try winrt.check(winrt.method(*const fn (winrt.Obj, winrt.Obj, *?*anyopaque) callconv(.winapi) winrt.HRESULT, path_factory, winrt.slots.ICompositionPathFactory.Create)(path_factory, src.object(), &path), "CompositionPath.Create"); + defer winrt.release(path.?); + try winrt.put(winrt.Obj, path_clip.path_geometry, winrt.slots.ICompositionPathGeometry.put_Path, path.?, "put_Path"); + if (st.mode == .hosted) if (hosted.*) |*hs| try hs.draw(g2, now); + }, + .mask => if (mask.*) |*m| { + const geom = try geometry.build(g2.d2dFactory(), shape); + defer _ = geom.IUnknown.Release(); + try m.surface.draw(&.{.{ .fill = geom }}); + }, + } +} + +/// Spike 3: an effect brush, the backdrop masked by a swapchain the app presents. +const Mask = struct { + surface: surface.Surface, + brush: winrt.Obj, + effect_brush: winrt.Obj, + + fn init(cmp: *const comp.Comp, g2: *const surface.Gpu, w: c_int, h: c_int, source: winrt.Obj) !Mask { + const params = try winrt.factory("Windows.UI.Composition.CompositionEffectSourceParameter", &winrt.IID_ICompositionEffectSourceParameterFactory); + const backdrop_param = try sourceParameter(params, try winrt.hstring("backdrop")); + const mask_param = try sourceParameter(params, try winrt.hstring("mask")); + + // D2D AlphaMask (input 0 masked by input 1's alpha); else Composite, SourceIn. Each effect + // takes a reference to its sources. + winrt.addRef(backdrop_param); + winrt.addRef(mask_param); + var made = tryEffect(cmp, win32.graphics.direct2d.CLSID_D2D1AlphaMask, &.{ backdrop_param, mask_param }, &.{}, "AlphaMask"); + if (made == null) { + const statics = try winrt.factory("Windows.Foundation.PropertyValue", &winrt.IID_IPropertyValueStatics); + var boxed: ?*anyopaque = null; + try winrt.check(winrt.method(*const fn (winrt.Obj, u32, *?*anyopaque) callconv(.winapi) winrt.HRESULT, statics, winrt.slots.IPropertyValueStatics.CreateUInt32)(statics, 2, &boxed), "PropertyValue.CreateUInt32"); + const mode = try winrt.qi(boxed.?, &winrt.IID_IPropertyValue, "QI IPropertyValue"); + winrt.addRef(backdrop_param); + winrt.addRef(mask_param); + made = tryEffect(cmp, win32.graphics.direct2d.CLSID_D2D1Composite, &.{ mask_param, backdrop_param }, &.{mode}, "Composite SourceIn"); + } + const factory = made orelse return error.NoEffect; + const effect_brush = try winrt.get(factory, winrt.slots.ICompositionEffectFactory.CreateBrush, "CompositionEffectFactory.CreateBrush"); + + var s = try surface.Surface.init(g2, @intCast(w), @intCast(h)); + const empty = try geometry.build(g2.d2dFactory(), .{ .pill = .{ .left = 0, .top = 0, .right = 1, .bottom = 1 }, .circle = .{ .x = 0, .y = 0 }, .radius = 0, .merged = false }); + defer _ = empty.IUnknown.Release(); + try s.draw(&.{.{ .fill = empty }}); + const mask_brush = try cmp.swapchainBrush(s.unknown()); + try setParam(effect_brush, "mask", mask_brush); + try setParam(effect_brush, "backdrop", source); + log.info("spike 3: effect brush made, the mask a composition surface over a swapchain the app presents", .{}); + return .{ .surface = s, .brush = try winrt.qi(effect_brush, &winrt.IID_ICompositionBrush, "QI ICompositionBrush (effect)"), .effect_brush = effect_brush }; + } + + fn setSource(m: *Mask, source: winrt.Obj) !void { + try setParam(m.effect_brush, "backdrop", source); + } + + fn setParam(effect_brush: winrt.Obj, comptime name: []const u8, brush: winrt.Obj) !void { + try winrt.check(winrt.method(*const fn (winrt.Obj, winrt.HSTRING, winrt.Obj) callconv(.winapi) winrt.HRESULT, effect_brush, winrt.slots.ICompositionEffectBrush.SetSourceParameter)(effect_brush, try winrt.hstring(name), brush), "SetSourceParameter " ++ name); + } + + fn sourceParameter(params: winrt.Obj, name: winrt.HSTRING) !winrt.Obj { + var p: ?*anyopaque = null; + try winrt.check(winrt.method(*const fn (winrt.Obj, winrt.HSTRING, *?*anyopaque) callconv(.winapi) winrt.HRESULT, params, winrt.slots.ICompositionEffectSourceParameterFactory.Create)(params, name, &p), "CompositionEffectSourceParameter.Create"); + return winrt.qi(p.?, &winrt.IID_IGraphicsEffectSource, "QI IGraphicsEffectSource"); + } + + /// An effect factory for the effect, or null with why logged. + fn tryEffect(cmp: *const comp.Comp, id: winrt.Guid, sources: []const winrt.Obj, props: []const winrt.Obj, what: []const u8) ?winrt.Obj { + const e = effect.Effect.create(id, sources, props) catch return null; + defer winrt.release(e.object()); + var factory: ?*anyopaque = null; + const hr = winrt.method(*const fn (winrt.Obj, winrt.Obj, *?*anyopaque) callconv(.winapi) winrt.HRESULT, cmp.c1, winrt.slots.ICompositor.CreateEffectFactory)(cmp.c1, e.object(), &factory); + if (hr < 0) { + log.err("spike 3: CreateEffectFactory({s}) failed, HRESULT 0x{x:0>8}", .{ what, @as(u32, @bitCast(hr)) }); + return null; + } + var status: i32 = 0; + _ = winrt.method(*const fn (winrt.Obj, *i32) callconv(.winapi) winrt.HRESULT, factory.?, winrt.slots.ICompositionEffectFactory.get_LoadStatus)(factory.?, &status); + var ext: i32 = 0; + _ = winrt.method(*const fn (winrt.Obj, *i32) callconv(.winapi) winrt.HRESULT, factory.?, winrt.slots.ICompositionEffectFactory.get_ExtendedError)(factory.?, &ext); + log.info("spike 3: CreateEffectFactory({s}) ok, LoadStatus {d} (0 = success), ExtendedError 0x{x:0>8}", .{ what, status, @as(u32, @bitCast(ext)) }); + return factory; + } +}; + +/// Spike 1's fallback: the app's picture (the rim) a swapchain in this tree, above the glass. +const Hosted = struct { + surface: surface.Surface, + visual: winrt.Obj, + + fn init(cmp: *const comp.Comp, g2: *const surface.Gpu, children: winrt.Obj, w: c_int, h: c_int) !Hosted { + const s = try surface.Surface.init(g2, @intCast(w), @intCast(h)); + const sp = try cmp.sprite(); + try winrt.put(?*anyopaque, sp.sprite, winrt.slots.ISpriteVisual.put_Brush, try cmp.swapchainBrush(s.unknown()), "put_Brush (hosted)"); + try cmp.setSize(sp.visual, @floatFromInt(w), @floatFromInt(h)); + try cmp.insertTop(children, sp.visual); + return .{ .surface = s, .visual = sp.visual }; + } + + fn draw(hs: *Hosted, g2: *const surface.Gpu, shape: geometry.Shape) !void { + const geom = try geometry.build(g2.d2dFactory(), shape); + defer _ = geom.IUnknown.Release(); + try hs.surface.draw(&.{.{ .stroke = .{ .geometry = geom, .width = 3, .color = .{ .r = 0.9, .g = 0.15, .b = 0.15, .a = 1 } } }}); + } +}; + +// ---- SDL_GPU: a clear and solid boxes by blit, no shaders ------------------------------------ + +fn sdlError(what: []const u8) error{Sdl} { + log.err("{s}: {s}", .{ what, c.SDL_GetError() }); + return error.Sdl; +} + +fn solidTexture(gpu: *c.SDL_GPUDevice, rgba: [4]u8) !*c.SDL_GPUTexture { + var info = std.mem.zeroes(c.SDL_GPUTextureCreateInfo); + info.type = c.SDL_GPU_TEXTURETYPE_2D; + info.format = c.SDL_GPU_TEXTUREFORMAT_R8G8B8A8_UNORM; + info.usage = c.SDL_GPU_TEXTUREUSAGE_SAMPLER; + info.width = 1; + info.height = 1; + info.layer_count_or_depth = 1; + info.num_levels = 1; + const tex = c.SDL_CreateGPUTexture(gpu, &info) orelse return sdlError("SDL_CreateGPUTexture"); + var tbi = std.mem.zeroes(c.SDL_GPUTransferBufferCreateInfo); + tbi.usage = c.SDL_GPU_TRANSFERBUFFERUSAGE_UPLOAD; + tbi.size = 4; + const tb = c.SDL_CreateGPUTransferBuffer(gpu, &tbi) orelse return sdlError("SDL_CreateGPUTransferBuffer"); + defer c.SDL_ReleaseGPUTransferBuffer(gpu, tb); + const p: [*]u8 = @ptrCast(c.SDL_MapGPUTransferBuffer(gpu, tb, false) orelse return sdlError("SDL_MapGPUTransferBuffer")); + @memcpy(p[0..4], &rgba); + c.SDL_UnmapGPUTransferBuffer(gpu, tb); + const cmd = c.SDL_AcquireGPUCommandBuffer(gpu) orelse return sdlError("SDL_AcquireGPUCommandBuffer"); + const copy = c.SDL_BeginGPUCopyPass(cmd); + var src = std.mem.zeroes(c.SDL_GPUTextureTransferInfo); + src.transfer_buffer = tb; + var dst = std.mem.zeroes(c.SDL_GPUTextureRegion); + dst.texture = tex; + dst.w = 1; + dst.h = 1; + dst.d = 1; + c.SDL_UploadToGPUTexture(copy, &src, &dst, false); + c.SDL_EndGPUCopyPass(copy); + if (!c.SDL_SubmitGPUCommandBuffer(cmd)) return sdlError("SDL_SubmitGPUCommandBuffer"); + return tex; +} + +fn clear(cmd: *c.SDL_GPUCommandBuffer, target: *c.SDL_GPUTexture) void { + var ct = std.mem.zeroes(c.SDL_GPUColorTargetInfo); + ct.texture = target; + ct.load_op = c.SDL_GPU_LOADOP_CLEAR; + ct.store_op = c.SDL_GPU_STOREOP_STORE; + const pass = c.SDL_BeginGPURenderPass(cmd, &ct, 1, null); + c.SDL_EndGPURenderPass(pass); +} + +/// The swapchain texture's size this frame, which blits stay inside. +var target_w: f32 = 0; +var target_h: f32 = 0; + +fn blit(cmd: *c.SDL_GPUCommandBuffer, target: *c.SDL_GPUTexture, src: *c.SDL_GPUTexture, x: f32, y: f32, w_in: f32, h_in: f32) void { + const w = @min(w_in, target_w - x); + const h = @min(h_in, target_h - y); + if (w <= 0 or h <= 0 or x + w <= 0 or y + h <= 0) return; + var info = std.mem.zeroes(c.SDL_GPUBlitInfo); + info.source.texture = src; + info.source.w = 1; + info.source.h = 1; + const x0 = @max(x, 0); + const y0 = @max(y, 0); + info.destination.texture = target; + info.destination.x = @intFromFloat(@round(x0)); + info.destination.y = @intFromFloat(@round(y0)); + info.destination.w = @intFromFloat(@round(w - (x0 - x))); + info.destination.h = @intFromFloat(@round(h - (y0 - y))); + info.load_op = c.SDL_GPU_LOADOP_LOAD; + info.filter = c.SDL_GPU_FILTER_NEAREST; + c.SDL_BlitGPUTexture(cmd, &info); +} + +/// A rectangle's outline, `t` pixels, just outside it. +fn box(cmd: *c.SDL_GPUCommandBuffer, target: *c.SDL_GPUTexture, src: *c.SDL_GPUTexture, r: anytype, t: f32) void { + blit(cmd, target, src, r.left - t, r.top - t, r.right - r.left + 2 * t, t); + blit(cmd, target, src, r.left - t, r.bottom, r.right - r.left + 2 * t, t); + blit(cmd, target, src, r.left - t, r.top, t, r.bottom - r.top); + blit(cmd, target, src, r.right, r.top, t, r.bottom - r.top); +} diff --git a/spikes/windows-composition/src/overlay.zig b/spikes/windows-composition/src/overlay.zig new file mode 100644 index 000000000..ec29e6fac --- /dev/null +++ b/spikes/windows-composition/src/overlay.zig @@ -0,0 +1,134 @@ +//! Spike 4: the overlay a drag would carry its glass in on Windows (WINDOWS_LINUX_GLASS_PLAN.md, +//! "one overlay per display"). A window over the whole primary display, topmost, never activated, +//! no taskbar entry, no redirection bitmap, holding a host-backdrop pill and its rim that follow +//! the pointer. The question is which styles let every click through to what is under it while +//! the composition tree still shows; `variant` picks one. +const std = @import("std"); +const win32 = @import("win32"); +const winrt = @import("winrt.zig"); +const comp = @import("comp.zig"); +const surface = @import("surface.zig"); +const geometry = @import("geometry.zig"); +const u = @import("user32.zig"); + +const log = std.log.scoped(.overlay); + +pub const Variant = enum { + /// `WS_EX_TRANSPARENT | WS_EX_LAYERED`, no layered attributes set. + layered, + /// The same, with `SetLayeredWindowAttributes(255, LWA_ALPHA)`. + layered_alpha, + /// `WS_EX_TRANSPARENT` alone. + transparent, + /// No click-through style; `WM_NCHITTEST` answers `HTTRANSPARENT`. + hittest, + + pub fn next(v: ?Variant) ?Variant { + const cur = v orelse return .layered; + const i = @intFromEnum(cur) + 1; + return if (i >= @typeInfo(Variant).@"enum".fields.len) null else @enumFromInt(i); + } +}; + +const pill_w = 220; +const pill_h = 72; +const rim = 4; + +hwnd: u.HWND, +variant: Variant, +target: winrt.Obj, +backdrop: winrt.Obj, +rim_visual: winrt.Obj, +rim_surface: surface.Surface, +last_hit: [64]u8 = undefined, +last_hit_len: usize = 0, + +const Self = @This(); + +var class_registered = false; +const class_name = std.unicode.utf8ToUtf16LeStringLiteral("fizzy_spike_overlay"); +var hittest_transparent = false; + +fn wndProc(hwnd: u.HWND, msg: u32, wp: usize, lp: isize) callconv(.winapi) isize { + if (msg == u.WM_NCHITTEST and hittest_transparent) return u.HTTRANSPARENT; + return u.DefWindowProcW(hwnd, msg, wp, lp); +} + +pub fn open(c: *const comp.Comp, gpu: *const surface.Gpu, variant: Variant) !Self { + const instance = u.GetModuleHandleW(null); + if (!class_registered) { + _ = u.RegisterClassExW(&.{ .lpfnWndProc = wndProc, .hInstance = instance, .lpszClassName = class_name }); + class_registered = true; + } + hittest_transparent = variant == .hittest; + var ex: u32 = u.WS_EX_TOPMOST | u.WS_EX_TOOLWINDOW | u.WS_EX_NOACTIVATE | u.WS_EX_NOREDIRECTIONBITMAP; + switch (variant) { + .layered, .layered_alpha => ex |= u.WS_EX_TRANSPARENT | u.WS_EX_LAYERED, + .transparent => ex |= u.WS_EX_TRANSPARENT, + .hittest => {}, + } + const sw = u.GetSystemMetrics(0); + const sh = u.GetSystemMetrics(1); + const hwnd = u.CreateWindowExW(ex, class_name, std.unicode.utf8ToUtf16LeStringLiteral("spike overlay"), u.WS_POPUP, 0, 0, sw, sh, null, null, instance, null) orelse return error.CreateWindow; + if (variant == .layered_alpha) _ = u.SetLayeredWindowAttributes(hwnd, 0, 255, u.LWA_ALPHA); + const yes: u.BOOL = 1; + const hr = u.DwmSetWindowAttribute(hwnd, u.DWMWA_USE_HOSTBACKDROPBRUSH, &yes, @sizeOf(u.BOOL)); + _ = u.ShowWindow(hwnd, u.SW_SHOWNOACTIVATE); + log.info("open {s}: ex style 0x{x} (asked 0x{x}), {d}x{d}, USE_HOSTBACKDROPBRUSH hr=0x{x}", .{ @tagName(variant), @as(u32, @truncate(@as(usize, @bitCast(u.GetWindowLongPtrW(hwnd, u.GWL_EXSTYLE))))), ex, sw, sh, @as(u32, @bitCast(hr)) }); + + const t = try c.desktopTarget(hwnd, false); + const root = try c.container(); + try winrt.put(?*anyopaque, t.target, winrt.slots.ICompositionTarget.put_Root, root.visual, "overlay put_Root"); + + // The rim under the glass: a picture the app presents (Direct2D into a composition swapchain), + // hosted in this tree. + var rim_surface = try surface.Surface.init(gpu, pill_w + 2 * rim, pill_h + 2 * rim); + const outline = try geometry.build(gpu.d2dFactory(), .{ .pill = .{ .left = rim / 2, .top = rim / 2, .right = pill_w + rim * 1.5, .bottom = pill_h + rim * 1.5 }, .circle = .{ .x = 0, .y = 0 }, .radius = 0, .merged = false }); + defer _ = outline.IUnknown.Release(); + try rim_surface.draw(&.{.{ .stroke = .{ .geometry = outline, .width = rim, .color = .{ .r = 1, .g = 0.25, .b = 0.2, .a = 1 } } }}); + const rim_sprite = try c.sprite(); + try winrt.put(?*anyopaque, rim_sprite.sprite, winrt.slots.ISpriteVisual.put_Brush, try c.swapchainBrush(rim_surface.unknown()), "overlay rim put_Brush"); + try c.setSize(rim_sprite.visual, pill_w + 2 * rim, pill_h + 2 * rim); + try c.insertTop(root.children, rim_sprite.visual); + + const glass = try c.sprite(); + try winrt.put(?*anyopaque, glass.sprite, winrt.slots.ISpriteVisual.put_Brush, try c.hostBackdrop(), "overlay glass put_Brush"); + try c.setSize(glass.visual, pill_w, pill_h); + const clip = try c.rectClip(); + try c.setRectClip(clip.rect, 0, 0, pill_w, pill_h, pill_h / 2); + try winrt.put(?*anyopaque, glass.visual, winrt.slots.IVisual.put_Clip, clip.clip, "overlay put_Clip"); + try c.insertTop(root.children, glass.visual); + + return .{ .hwnd = hwnd, .variant = variant, .target = t.target, .backdrop = glass.visual, .rim_visual = rim_sprite.visual, .rim_surface = rim_surface }; +} + +/// The glass where the pointer is, a little below and right of it as a carried view would be. +/// Returns what the desktop says is under the pointer when that changes (`WindowFromPoint`). +pub fn update(o: *Self, c: *const comp.Comp) !?[]const u8 { + var p: u.POINT = undefined; + _ = u.GetCursorPos(&p); + const x: f32 = @floatFromInt(p.x + 16); + const y: f32 = @floatFromInt(p.y + 16); + try c.setOffset(o.backdrop, x, y); + try c.setOffset(o.rim_visual, x - rim, y - rim); + + // Who takes a click at the pointer: the overlay, or the window under it. + const hit = u.WindowFromPoint(p); + var buf: [64]u8 = undefined; + const name: []const u8 = if (hit) |h| blk: { + if (h == o.hwnd) break :blk "the overlay"; + var w: [48]u16 = undefined; + const n = u.GetClassNameW(u.GetAncestor(h, u.GA_ROOT) orelse h, &w, w.len); + const len = std.unicode.utf16LeToUtf8(&buf, w[0..@intCast(@max(n, 0))]) catch 0; + break :blk buf[0..len]; + } else "nothing"; + if (std.mem.eql(u8, name, o.last_hit[0..o.last_hit_len])) return null; + @memcpy(o.last_hit[0..name.len], name); + o.last_hit_len = name.len; + return o.last_hit[0..o.last_hit_len]; +} + +pub fn close(o: *Self) void { + winrt.put(?*anyopaque, o.target, winrt.slots.ICompositionTarget.put_Root, null, "overlay put_Root null") catch {}; + _ = u.DestroyWindow(o.hwnd); +} diff --git a/spikes/windows-composition/src/sdl3-c.h b/spikes/windows-composition/src/sdl3-c.h new file mode 100644 index 000000000..5280d5c1f --- /dev/null +++ b/spikes/windows-composition/src/sdl3-c.h @@ -0,0 +1,5 @@ +// SDL's C API for the spike, as fizzy's native backend translates it (`src/backend/native/sdl3-c.h`). +#define SDL_DISABLE_OLD_NAMES +#include "SDL3/SDL.h" +#define SDL_MAIN_HANDLED +#include "SDL3/SDL_main.h" diff --git a/spikes/windows-composition/src/surface.zig b/spikes/windows-composition/src/surface.zig new file mode 100644 index 000000000..e3102153f --- /dev/null +++ b/spikes/windows-composition/src/surface.zig @@ -0,0 +1,124 @@ +//! A picture the app presents as a swapchain made for composition (D3D11, drawn with Direct2D): +//! spike 3's mask, the overlay's rim, and the app's picture hosted in composition's own tree +//! rather than SDL's DirectComposition target (spike 1's fallback). In fizzy the device would be +//! SDL's D3D12; what is asked here is only whether composition takes such a swapchain where it is +//! put, so D3D11, which Direct2D draws into with no shaders, stands in. +const std = @import("std"); +const win32 = @import("win32"); +const winrt = @import("winrt.zig"); + +const d2d = win32.graphics.direct2d; +const common = d2d.common; +const d3d = win32.graphics.direct3d; +const d3d11 = win32.graphics.direct3d11; +const dxgi = win32.graphics.dxgi; + +/// Direct2D and D3D11, once: the factory also makes every geometry the spike traces. +pub const Gpu = struct { + factory: *d2d.ID2D1Factory1, + device: *d3d11.ID3D11Device, + d2d_device: *d2d.ID2D1Device, + dxgi_factory: *dxgi.IDXGIFactory2, + warp: bool, + + pub fn init() !Gpu { + var factory: *d2d.ID2D1Factory1 = undefined; + try winrt.check(d2d.D2D1CreateFactory(d2d.D2D1_FACTORY_TYPE_SINGLE_THREADED, d2d.IID_ID2D1Factory1, null, @ptrCast(&factory)), "D2D1CreateFactory"); + var device: *d3d11.ID3D11Device = undefined; + var warp = false; + if (d3d11.D3D11CreateDevice(null, d3d.D3D_DRIVER_TYPE_HARDWARE, null, .{ .BGRA_SUPPORT = 1 }, null, 0, d3d11.D3D11_SDK_VERSION, &device, null, null) < 0) { + try winrt.check(d3d11.D3D11CreateDevice(null, d3d.D3D_DRIVER_TYPE_WARP, null, .{ .BGRA_SUPPORT = 1 }, null, 0, d3d11.D3D11_SDK_VERSION, &device, null, null), "D3D11CreateDevice (WARP)"); + warp = true; + } + var dxgi_device: *dxgi.IDXGIDevice = undefined; + try winrt.check(device.IUnknown.QueryInterface(dxgi.IID_IDXGIDevice, @ptrCast(&dxgi_device)), "QI IDXGIDevice"); + defer _ = dxgi_device.IUnknown.Release(); + var d2d_device: *d2d.ID2D1Device = undefined; + try winrt.check(factory.CreateDevice(dxgi_device, &d2d_device), "ID2D1Factory1.CreateDevice"); + var dxgi_factory: *dxgi.IDXGIFactory2 = undefined; + try winrt.check(dxgi.CreateDXGIFactory2(0, dxgi.IID_IDXGIFactory2, @ptrCast(&dxgi_factory)), "CreateDXGIFactory2"); + return .{ .factory = factory, .device = device, .d2d_device = d2d_device, .dxgi_factory = dxgi_factory, .warp = warp }; + } + + pub fn d2dFactory(g: *const Gpu) *d2d.ID2D1Factory { + return &g.factory.ID2D1Factory; + } +}; + +/// One swapchain for composition, premultiplied, and the Direct2D context that draws into it. +pub const Surface = struct { + swapchain: *dxgi.IDXGISwapChain1, + context: *d2d.ID2D1DeviceContext, + brush: *d2d.ID2D1SolidColorBrush, + width: u32, + height: u32, + + pub fn init(gpu: *const Gpu, width: u32, height: u32) !Surface { + const desc: dxgi.DXGI_SWAP_CHAIN_DESC1 = .{ + .Width = width, + .Height = height, + .Format = .B8G8R8A8_UNORM, + .Stereo = 0, + .SampleDesc = .{ .Count = 1, .Quality = 0 }, + .BufferUsage = dxgi.DXGI_USAGE_RENDER_TARGET_OUTPUT, + .BufferCount = 2, + .Scaling = dxgi.DXGI_SCALING_STRETCH, + .SwapEffect = dxgi.DXGI_SWAP_EFFECT_FLIP_SEQUENTIAL, + .AlphaMode = .PREMULTIPLIED, + .Flags = 0, + }; + var swapchain: *dxgi.IDXGISwapChain1 = undefined; + try winrt.check(gpu.dxgi_factory.CreateSwapChainForComposition(&gpu.device.IUnknown, &desc, null, &swapchain), "CreateSwapChainForComposition"); + var context: *d2d.ID2D1DeviceContext = undefined; + try winrt.check(gpu.d2d_device.CreateDeviceContext(.{}, &context), "ID2D1Device.CreateDeviceContext"); + var brush: *d2d.ID2D1SolidColorBrush = undefined; + try winrt.check(context.ID2D1RenderTarget.CreateSolidColorBrush(&.{ .r = 1, .g = 1, .b = 1, .a = 1 }, null, &brush), "CreateSolidColorBrush"); + return .{ .swapchain = swapchain, .context = context, .brush = brush, .width = width, .height = height }; + } + + /// The swapchain as `IUnknown`, for `ICompositorInterop.CreateCompositionSurfaceForSwapChain`. + pub fn unknown(s: *const Surface) winrt.Obj { + return @ptrCast(s.swapchain); + } + + pub const Paint = union(enum) { + /// The geometry filled, opaque white: a mask. + fill: *d2d.ID2D1Geometry, + /// The geometry's outline, `width` pixels, in `color` (premultiplied by Direct2D). + stroke: struct { geometry: *d2d.ID2D1Geometry, width: f32, color: common.D2D_COLOR_F }, + }; + + /// Clear to nothing, paint, present (not waiting for a vblank: the caller's own present does). + pub fn draw(s: *Surface, paints: []const Paint) !void { + var surface: *dxgi.IDXGISurface = undefined; + try winrt.check(s.swapchain.IDXGISwapChain.GetBuffer(0, dxgi.IID_IDXGISurface, @ptrCast(&surface)), "GetBuffer"); + defer _ = surface.IUnknown.Release(); + const props: d2d.D2D1_BITMAP_PROPERTIES1 = .{ + .pixelFormat = .{ .format = .B8G8R8A8_UNORM, .alphaMode = common.D2D1_ALPHA_MODE_PREMULTIPLIED }, + .dpiX = 96, + .dpiY = 96, + .bitmapOptions = .{ .TARGET = 1, .CANNOT_DRAW = 1 }, + .colorContext = null, + }; + var bitmap: *d2d.ID2D1Bitmap1 = undefined; + try winrt.check(s.context.CreateBitmapFromDxgiSurface(surface, &props, &bitmap), "CreateBitmapFromDxgiSurface"); + defer _ = bitmap.IUnknown.Release(); + s.context.SetTarget(&bitmap.ID2D1Image); + const rt = &s.context.ID2D1RenderTarget; + rt.BeginDraw(); + rt.Clear(&.{ .r = 0, .g = 0, .b = 0, .a = 0 }); + for (paints) |p| switch (p) { + .fill => |geometry| { + s.brush.SetColor(&.{ .r = 1, .g = 1, .b = 1, .a = 1 }); + rt.FillGeometry(geometry, &s.brush.ID2D1Brush, null); + }, + .stroke => |st| { + s.brush.SetColor(&st.color); + rt.DrawGeometry(st.geometry, &s.brush.ID2D1Brush, st.width, null); + }, + }; + try winrt.check(rt.EndDraw(null, null), "EndDraw"); + s.context.SetTarget(null); + try winrt.check(s.swapchain.IDXGISwapChain.Present(0, 0), "IDXGISwapChain.Present"); + } +}; diff --git a/spikes/windows-composition/src/user32.zig b/spikes/windows-composition/src/user32.zig new file mode 100644 index 000000000..c24861198 --- /dev/null +++ b/spikes/windows-composition/src/user32.zig @@ -0,0 +1,84 @@ +//! The few user32/dwmapi/kernel32 calls the spike makes, declared with plain integer flags. +pub const HWND = *anyopaque; +pub const BOOL = i32; +pub const POINT = extern struct { x: i32, y: i32 }; +pub const RECT = extern struct { left: i32, top: i32, right: i32, bottom: i32 }; +pub const WNDPROC = *const fn (HWND, u32, usize, isize) callconv(.winapi) isize; + +pub const WNDCLASSEXW = extern struct { + cbSize: u32 = @sizeOf(WNDCLASSEXW), + style: u32 = 0, + lpfnWndProc: WNDPROC, + cbClsExtra: i32 = 0, + cbWndExtra: i32 = 0, + hInstance: ?*anyopaque, + hIcon: ?*anyopaque = null, + hCursor: ?*anyopaque = null, + hbrBackground: ?*anyopaque = null, + lpszMenuName: ?[*:0]const u16 = null, + lpszClassName: [*:0]const u16, + hIconSm: ?*anyopaque = null, +}; + +pub const MSG = extern struct { + hwnd: ?HWND, + message: u32, + wParam: usize, + lParam: isize, + time: u32, + pt: POINT, + lPrivate: u32, +}; + +pub extern "user32" fn RegisterClassExW(*const WNDCLASSEXW) callconv(.winapi) u16; +pub extern "user32" fn CreateWindowExW(ex_style: u32, class: [*:0]const u16, name: [*:0]const u16, style: u32, x: i32, y: i32, w: i32, h: i32, parent: ?HWND, menu: ?*anyopaque, instance: ?*anyopaque, param: ?*anyopaque) callconv(.winapi) ?HWND; +pub extern "user32" fn DestroyWindow(HWND) callconv(.winapi) BOOL; +pub extern "user32" fn DefWindowProcW(HWND, u32, usize, isize) callconv(.winapi) isize; +pub extern "user32" fn ShowWindow(HWND, i32) callconv(.winapi) BOOL; +pub extern "user32" fn SetLayeredWindowAttributes(HWND, color: u32, alpha: u8, flags: u32) callconv(.winapi) BOOL; +pub extern "user32" fn GetCursorPos(*POINT) callconv(.winapi) BOOL; +pub extern "user32" fn WindowFromPoint(POINT) callconv(.winapi) ?HWND; +pub extern "user32" fn GetAncestor(HWND, u32) callconv(.winapi) ?HWND; +pub extern "user32" fn GetClassNameW(HWND, [*]u16, i32) callconv(.winapi) i32; +pub extern "user32" fn GetSystemMetrics(i32) callconv(.winapi) i32; +pub extern "user32" fn GetWindowLongPtrW(HWND, i32) callconv(.winapi) isize; +pub extern "user32" fn GetWindowRect(HWND, *RECT) callconv(.winapi) BOOL; +pub extern "user32" fn PeekMessageW(*MSG, ?HWND, u32, u32, u32) callconv(.winapi) BOOL; +pub extern "user32" fn TranslateMessage(*const MSG) callconv(.winapi) BOOL; +pub extern "user32" fn DispatchMessageW(*const MSG) callconv(.winapi) isize; +pub extern "kernel32" fn GetModuleHandleW(?[*:0]const u16) callconv(.winapi) ?*anyopaque; +pub extern "dwmapi" fn DwmSetWindowAttribute(HWND, attribute: u32, value: *const anyopaque, size: u32) callconv(.winapi) i32; +pub extern "dwmapi" fn DwmFlush() callconv(.winapi) i32; + +pub const WS_POPUP: u32 = 0x80000000; +pub const WS_EX_TOPMOST: u32 = 0x00000008; +pub const WS_EX_TRANSPARENT: u32 = 0x00000020; +pub const WS_EX_TOOLWINDOW: u32 = 0x00000080; +pub const WS_EX_LAYERED: u32 = 0x00080000; +pub const WS_EX_NOREDIRECTIONBITMAP: u32 = 0x00200000; +pub const WS_EX_NOACTIVATE: u32 = 0x08000000; +pub const LWA_ALPHA: u32 = 0x2; +pub const SW_SHOWNOACTIVATE: i32 = 4; +pub const WM_NCHITTEST: u32 = 0x0084; +pub const HTTRANSPARENT: isize = -1; +pub const GA_ROOT: u32 = 2; +pub const GWL_EXSTYLE: i32 = -20; +pub const PM_REMOVE: u32 = 0x1; + +/// `DWMWA_USE_HOSTBACKDROPBRUSH`: the window may be painted with a host backdrop brush. +pub const DWMWA_USE_HOSTBACKDROPBRUSH: u32 = 17; +pub const DWMWA_SYSTEMBACKDROP_TYPE: u32 = 38; +pub const DWMSBT_NONE: u32 = 1; + +/// Dispatch the thread's waiting messages: SDL's (which SDL turns into its events) and the +/// DispatcherQueue's, which is when composition commits what was set since. +pub fn pump() u32 { + var msg: MSG = undefined; + var n: u32 = 0; + while (PeekMessageW(&msg, null, 0, 0, PM_REMOVE) != 0) { + _ = TranslateMessage(&msg); + _ = DispatchMessageW(&msg); + n += 1; + } + return n; +} diff --git a/spikes/windows-composition/src/winrt.zig b/spikes/windows-composition/src/winrt.zig new file mode 100644 index 000000000..670f8fd83 --- /dev/null +++ b/spikes/windows-composition/src/winrt.zig @@ -0,0 +1,251 @@ +//! Just enough of Windows.UI.Composition to answer the spike, called by vtable slot. +//! +//! zig's MinGW headers carry no C++/WinRT and zigwin32 has only the interop interfaces, so every +//! WinRT interface here is a GUID and the slots of the methods called. Both were read from +//! `C:\Windows\System32\WinMetadata\Windows.UI.winmd` (and Windows.Graphics, Windows.Foundation, +//! Windows.System) with System.Reflection.Metadata: slot 0-2 are IUnknown's, 3-5 IInspectable's, +//! and a WinRT interface's own methods follow in metadata order, overloads included. A WinRT +//! method returns HRESULT; its result, if any, is a last out-parameter. +const std = @import("std"); +const win32 = @import("win32"); + +pub const Guid = win32.zig.Guid; +pub const HRESULT = i32; +pub const HSTRING = ?*anyopaque; +pub const Obj = *anyopaque; + +pub const Vec2 = extern struct { x: f32, y: f32 }; +pub const Vec3 = extern struct { x: f32, y: f32, z: f32 }; + +const log = std.log.scoped(.winrt); + +pub fn check(hr: HRESULT, what: []const u8) !void { + if (hr < 0) { + log.err("{s}: HRESULT 0x{x:0>8}", .{ what, @as(u32, @bitCast(hr)) }); + return error.ComFailed; + } +} + +fn slot(obj: Obj, comptime i: usize) usize { + const vtbl: *const [*]const usize = @ptrCast(@alignCast(obj)); + return vtbl.*[i]; +} + +/// Method `i` of `obj`'s vtable, as `F`. +pub fn method(comptime F: type, obj: Obj, comptime i: usize) F { + return @ptrFromInt(slot(obj, i)); +} + +pub fn qi(obj: Obj, iid: *const Guid, what: []const u8) !Obj { + var out: ?*anyopaque = null; + try check(method(*const fn (Obj, *const Guid, *?*anyopaque) callconv(.winapi) HRESULT, obj, 0)(obj, iid, &out), what); + return out.?; +} + +/// `qi`, null where the interface is not there (an older Windows). +pub fn tryQi(obj: Obj, iid: *const Guid) ?Obj { + var out: ?*anyopaque = null; + if (method(*const fn (Obj, *const Guid, *?*anyopaque) callconv(.winapi) HRESULT, obj, 0)(obj, iid, &out) < 0) return null; + return out; +} + +pub fn addRef(obj: Obj) void { + _ = method(*const fn (Obj) callconv(.winapi) u32, obj, 1)(obj); +} + +pub fn release(obj: Obj) void { + _ = method(*const fn (Obj) callconv(.winapi) u32, obj, 2)(obj); +} + +/// A method taking nothing and giving one interface pointer. +pub fn get(obj: Obj, comptime i: usize, what: []const u8) !Obj { + var out: ?*anyopaque = null; + try check(method(*const fn (Obj, *?*anyopaque) callconv(.winapi) HRESULT, obj, i)(obj, &out), what); + return out orelse error.ComFailed; +} + +/// A method taking one value of type `T` and giving nothing (a setter, mostly). +pub fn put(comptime T: type, obj: Obj, comptime i: usize, value: T, what: []const u8) !void { + try check(method(*const fn (Obj, T) callconv(.winapi) HRESULT, obj, i)(obj, value), what); +} + +// ---- HSTRING and activation, from combase at runtime (no import library needed) --------------- + +pub fn hstring(comptime s: []const u8) !HSTRING { + const w = comptime std.unicode.utf8ToUtf16LeStringLiteral(s); + var h: ?win32.system.win_rt.HSTRING = null; + try check(win32.system.win_rt.WindowsCreateString(w, w.len, &h), "WindowsCreateString"); + return @ptrCast(h); +} + +const Ro = struct { + RoInitialize: *const fn (u32) callconv(.winapi) HRESULT, + RoActivateInstance: *const fn (HSTRING, *?*anyopaque) callconv(.winapi) HRESULT, + RoGetActivationFactory: *const fn (HSTRING, *const Guid, *?*anyopaque) callconv(.winapi) HRESULT, +}; +var ro: Ro = undefined; + +const DispatcherQueueOptions = extern struct { + dwSize: u32 = @sizeOf(DispatcherQueueOptions), + threadType: u32, // DQTYPE_THREAD_CURRENT = 2 + apartmentType: u32, // DQTAT_COM_NONE = 0 +}; + +fn sym(comptime T: type, dll: [*:0]const u8, name: [*:0]const u8) !T { + const lib = win32.system.library_loader.LoadLibraryA(dll) orelse return error.NoLibrary; + const p = win32.system.library_loader.GetProcAddress(lib, name) orelse return error.NoSymbol; + return @ptrCast(p); +} + +/// WinRT on this thread, and a DispatcherQueue on it: composition commits on the queue's tick, +/// which runs from this thread's message pump (SDL's). Returns the queue controller. +pub fn initThread() !Obj { + inline for (@typeInfo(Ro).@"struct".fields) |f| + @field(ro, f.name) = try sym(f.type, "combase.dll", f.name ++ ""); + // RO_INIT_SINGLETHREADED: S_FALSE or RPC_E_CHANGED_MODE when SDL got there first, both fine. + _ = ro.RoInitialize(0); + const create = try sym(*const fn (DispatcherQueueOptions, *?*anyopaque) callconv(.winapi) HRESULT, "CoreMessaging.dll", "CreateDispatcherQueueController"); + var controller: ?*anyopaque = null; + try check(create(.{ .threadType = 2, .apartmentType = 0 }, &controller), "CreateDispatcherQueueController"); + return controller.?; +} + +pub fn activate(comptime class: []const u8) !Obj { + var out: ?*anyopaque = null; + try check(ro.RoActivateInstance(try hstring(class), &out), "RoActivateInstance " ++ class); + return out.?; +} + +pub fn factory(comptime class: []const u8, iid: *const Guid) !Obj { + var out: ?*anyopaque = null; + try check(ro.RoGetActivationFactory(try hstring(class), iid, &out), "RoGetActivationFactory " ++ class); + return out.?; +} + +// ---- interfaces ----------------------------------------------------------------------------- + +fn g(comptime s: []const u8) Guid { + return Guid.initString(s); +} + +pub const IID_IUnknown = g("00000000-0000-0000-c000-000000000046"); +pub const IID_IInspectable = g("af86e2e0-b12d-4c6a-9c5a-d7aa65101e90"); +pub const IID_IAgileObject = g("94ea2b94-e9cc-49e0-c0ff-ee64ca8f5b90"); + +pub const IID_ICompositor = g("b403ca50-7f8c-4e83-985f-cc45060036d8"); +pub const IID_ICompositor2 = g("735081dc-5e24-45da-a38f-e32cc349a9a0"); +pub const IID_ICompositor3 = g("c9dd8ef0-6eb1-4e3c-a658-675d9c64d4ab"); +pub const IID_ICompositor5 = g("48ea31ad-7fcd-4076-a79c-90cc4b852c9b"); +pub const IID_ICompositor6 = g("7a38b2bd-cec8-4eeb-830f-d8d07aedebc3"); +pub const IID_ICompositor7 = g("d3483fad-9a12-53ba-bfc8-88b7ff7977c6"); +pub const IID_ICompositorDesktopInterop = g("29e691fa-4567-4dca-b319-d0f207eb6807"); +pub const IID_ICompositorInterop = g("25297d5c-3ad4-4c9c-b5cf-e36a38512330"); + +pub const IID_ICompositionTarget = g("a1bea8ba-d726-4663-8129-6b5e7927ffa6"); +pub const IID_IDesktopWindowTarget = g("6329d6ca-3366-490e-9db3-25312929ac51"); +pub const IID_IVisual = g("117e202d-a859-4c89-873b-c2aa566788e3"); +pub const IID_IContainerVisual = g("02f6bc74-ed20-4773-afe6-d49b4a93db32"); +pub const IID_ISpriteVisual = g("08e05581-1ad1-4f97-9757-402d76e4233b"); +pub const IID_ICompositionBrush = g("ab0d7608-30c0-40e9-b568-b60a6bd1fb46"); +pub const IID_ICompositionClip = g("1ccd2a52-cfc7-4ace-9983-146bb8eb6a3c"); +pub const IID_IRectangleClip = g("b3e7549e-00b4-5b53-8be8-353f6c433101"); +pub const IID_ICompositionGeometricClip = g("c840b581-81c9-4444-a2c1-ccaece3a50e5"); +pub const IID_ICompositionGeometry = g("e985217c-6a17-4207-abd8-5fd3dd612a9d"); +pub const IID_ICompositionPathGeometry = g("0b6a417e-2c77-4c23-af5e-6304c147bb61"); +pub const IID_ICompositionPathFactory = g("9c1e8c6a-0f33-4751-9437-eb3fb9d3ab07"); +pub const IID_ICompositionEffectFactory = g("be5624af-ba7e-4510-9850-41c0b4ff74df"); +pub const IID_ICompositionEffectBrush = g("bf7f795e-83cc-44bf-a447-3e3c071789ec"); +pub const IID_ICompositionEffectSourceParameterFactory = g("b3d9f276-aba3-4724-acf3-d0397464db1c"); +pub const IID_ICompositionSurfaceBrush = g("ad016d79-1e4c-4c0d-9c29-83338c87c162"); + +pub const IID_IGeometrySource2D = g("caff7902-670c-4181-a624-da977203b845"); +pub const IID_IGeometrySource2DInterop = g("0657af73-53fd-47cf-84ff-c8492d2a80a3"); +pub const IID_IGraphicsEffect = g("cb51c0ce-8fe6-4636-b202-861faa07d8f3"); +pub const IID_IGraphicsEffectSource = g("2d8f9ddc-4339-4eb9-9216-f9deb75658a2"); +pub const IID_IGraphicsEffectD2D1Interop = g("2fc57384-a068-44d7-a331-30982fcf7177"); +pub const IID_IPropertyValueStatics = g("629bdbc8-d932-4ff4-96b9-8d96c5c1e858"); +pub const IID_IPropertyValue = g("4bd682dd-7554-40e9-9a9b-82654ede7e62"); + +/// The slots called, by interface (see the file comment for where they come from). +pub const slots = struct { + pub const ICompositor = struct { + pub const CreateColorBrushWithColor = 8; + pub const CreateContainerVisual = 9; + pub const CreateEffectFactory = 11; + pub const CreateSpriteVisual = 22; + pub const CreateSurfaceBrushWithSurface = 24; + }; + pub const ICompositor3 = struct { + pub const CreateHostBackdropBrush = 6; + }; + pub const ICompositor5 = struct { + pub const CreatePathGeometry = 16; + }; + pub const ICompositor6 = struct { + pub const CreateGeometricClipWithGeometry = 7; + }; + pub const ICompositor7 = struct { + pub const CreateRectangleClip = 8; + }; + pub const ICompositorDesktopInterop = struct { + pub const CreateDesktopWindowTarget = 3; // IUnknown-based: (HWND, BOOL isTopmost, **target) + }; + pub const ICompositorInterop = struct { + pub const CreateCompositionSurfaceForSwapChain = 4; // IUnknown-based: (IUnknown*, **surface) + }; + pub const IDesktopWindowTarget = struct { + pub const get_IsTopmost = 6; + }; + pub const ICompositionTarget = struct { + pub const put_Root = 7; + }; + pub const IContainerVisual = struct { + pub const get_Children = 6; + }; + pub const IVisualCollection = struct { + pub const InsertAtTop = 9; + }; + pub const ISpriteVisual = struct { + pub const put_Brush = 7; + }; + pub const IVisual = struct { + pub const put_Clip = 15; + pub const put_IsVisible = 19; + pub const put_Offset = 21; + pub const put_Opacity = 23; + pub const put_Size = 36; + }; + pub const IRectangleClip = struct { + pub const put_Bottom = 7; + pub const put_BottomLeftRadius = 9; + pub const put_BottomRightRadius = 11; + pub const put_Left = 13; + pub const put_Right = 15; + pub const put_Top = 17; + pub const put_TopLeftRadius = 19; + pub const put_TopRightRadius = 21; + }; + pub const ICompositionPathGeometry = struct { + pub const put_Path = 7; + }; + pub const ICompositionPathFactory = struct { + pub const Create = 6; + }; + pub const ICompositionEffectFactory = struct { + pub const CreateBrush = 6; + pub const get_ExtendedError = 7; + pub const get_LoadStatus = 8; + }; + pub const ICompositionEffectBrush = struct { + pub const SetSourceParameter = 7; + }; + pub const ICompositionEffectSourceParameterFactory = struct { + pub const Create = 6; + }; + pub const IPropertyValueStatics = struct { + pub const CreateUInt32 = 11; + }; +}; + +/// `Windows.UI.Color`: a, r, g, b bytes. +pub const Color = extern struct { a: u8, r: u8, g: u8, b: u8 };