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 };