12b4f1c8
Bind wl_output globals into ScaleTracker
a73x 2026-04-09 10:15
Binds wl_output v4 in the registry listener and attaches a per-output listener that routes scale/done events into the Connection's ScaleTracker. Also handles global_remove for hotplug cleanup. Connection is now heap-allocated so the pointer passed to registry.setListener remains stable across the init call boundary — with wl_output hotplug support the listener can fire after init returns, so a stack-local Connection would dangle. Verified manually on dual-monitor sway: two outputs bound, Apple Studio Display at scale=2 and Dell AW3225QF at scale=1. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
diff --git a/build.zig b/build.zig index 42d5c65..8e236e9 100644 --- a/build.zig +++ b/build.zig @@ -27,6 +27,7 @@ pub fn build(b: *std.Build) void { scanner.generate("wl_compositor", 6); scanner.generate("wl_seat", 9); scanner.generate("wl_data_device_manager", 3); scanner.generate("wl_output", 4); scanner.generate("xdg_wm_base", 6); // wayland module — generated bindings + our Connection wrapper diff --git a/src/main.zig b/src/main.zig index d7e6927..959be9f 100644 --- a/src/main.zig +++ b/src/main.zig @@ -85,7 +85,7 @@ fn runTerminal(alloc: std.mem.Allocator) !void { const initial_h: u32 = @as(u32, rows) * cell_h; // === wayland === var conn = try wayland_client.Connection.init(); const conn = try wayland_client.Connection.init(alloc); defer conn.deinit(); const window = try conn.createWindow(alloc, "waystty"); @@ -1757,7 +1757,7 @@ fn makeTestInstances( } fn runTextCoverageCompare(alloc: std.mem.Allocator) !void { var conn = try wayland_client.Connection.init(); const conn = try wayland_client.Connection.init(alloc); defer conn.deinit(); const window = try conn.createWindow(alloc, "waystty-text-compare"); @@ -1847,7 +1847,7 @@ fn runTextCoverageCompare(alloc: std.mem.Allocator) !void { } fn runDrawSmokeTest(alloc: std.mem.Allocator) !void { var conn = try wayland_client.Connection.init(); const conn = try wayland_client.Connection.init(alloc); defer conn.deinit(); std.debug.print("wayland connected\n", .{}); @@ -2179,7 +2179,7 @@ test "buildTextCoverageCompareScene repeats the same specimen in four panels" { } fn runRenderSmokeTest(alloc: std.mem.Allocator) !void { var conn = try wayland_client.Connection.init(); const conn = try wayland_client.Connection.init(alloc); defer conn.deinit(); std.debug.print("wayland connected\n", .{}); @@ -2210,7 +2210,7 @@ fn runRenderSmokeTest(alloc: std.mem.Allocator) !void { } fn runVulkanSmokeTest(alloc: std.mem.Allocator) !void { var conn = try wayland_client.Connection.init(); const conn = try wayland_client.Connection.init(alloc); defer conn.deinit(); std.debug.print("wayland connected\n", .{}); @@ -2237,7 +2237,7 @@ fn runVulkanSmokeTest(alloc: std.mem.Allocator) !void { } fn runWaylandSmokeTest(alloc: std.mem.Allocator) !void { var conn = try wayland_client.Connection.init(); const conn = try wayland_client.Connection.init(alloc); defer conn.deinit(); std.debug.print("connected\n", .{}); diff --git a/src/wayland.zig b/src/wayland.zig index 5443aba..f19aa7a 100644 --- a/src/wayland.zig +++ b/src/wayland.zig @@ -3,6 +3,7 @@ const posix = std.posix; const wayland = @import("wayland"); const wl = wayland.client.wl; const xdg = wayland.client.xdg; const ScaleTracker = @import("scale_tracker").ScaleTracker; const c = @cImport({ @cInclude("xkbcommon/xkbcommon.h"); @@ -10,6 +11,13 @@ const c = @cImport({ @cInclude("unistd.h"); }); pub const Output = struct { wl_output: *wl.Output, name: u32, tracker: *ScaleTracker, pending_scale: i32 = 1, }; pub const KeyboardEvent = struct { keysym: u32, modifiers: Modifiers, @@ -254,32 +262,65 @@ pub const Connection = struct { display: *wl.Display, registry: *wl.Registry, globals: Globals, pub fn init() !Connection { alloc: std.mem.Allocator, scale_tracker: ScaleTracker, outputs: std.ArrayListUnmanaged(*Output), // Heap-allocated so the pointer passed to `registry.setListener` remains stable // across the init call boundary. With wl_output hotplug support the listener // can fire after init returns, so we need a stable address — a stack-local // Connection would dangle. pub fn init(alloc: std.mem.Allocator) !*Connection { const display = try wl.Display.connect(null); errdefer display.disconnect(); const registry = try display.getRegistry(); errdefer registry.destroy(); var globals = Globals{}; registry.setListener(*Globals, registryListener, &globals); if (display.roundtrip() != .SUCCESS) return error.RoundtripFailed; if (globals.compositor == null) return error.NoCompositor; if (globals.wm_base == null) return error.NoXdgWmBase; if (globals.seat == null) return error.NoSeat; const conn = try alloc.create(Connection); errdefer alloc.destroy(conn); return .{ conn.* = .{ .display = display, .registry = registry, .globals = globals, .globals = Globals{}, .alloc = alloc, .scale_tracker = ScaleTracker.init(alloc), .outputs = .empty, }; errdefer { for (conn.outputs.items) |out| { out.wl_output.release(); alloc.destroy(out); } conn.outputs.deinit(alloc); conn.scale_tracker.deinit(); } registry.setListener(*Connection, registryListener, conn); if (display.roundtrip() != .SUCCESS) return error.RoundtripFailed; // Second roundtrip so each wl_output's initial scale/done events are // received after the output's own listener is attached in the first pass. if (display.roundtrip() != .SUCCESS) return error.RoundtripFailed; if (conn.globals.compositor == null) return error.NoCompositor; if (conn.globals.wm_base == null) return error.NoXdgWmBase; if (conn.globals.seat == null) return error.NoSeat; return conn; } pub fn deinit(self: *Connection) void { const alloc = self.alloc; for (self.outputs.items) |out| { out.wl_output.release(); alloc.destroy(out); } self.outputs.deinit(alloc); self.scale_tracker.deinit(); self.display.disconnect(); alloc.destroy(self); } pub fn createWindow(self: *Connection, alloc: std.mem.Allocator, title: [*:0]const u8) !*Window { @@ -493,22 +534,68 @@ fn xdgToplevelListener(_: *xdg.Toplevel, event: xdg.Toplevel.Event, window: *Win fn registryListener( registry: *wl.Registry, event: wl.Registry.Event, globals: *Globals, conn: *Connection, ) void { switch (event) { .global => |g| { const iface = std.mem.span(g.interface); if (std.mem.eql(u8, iface, std.mem.span(wl.Compositor.interface.name))) { globals.compositor = registry.bind(g.name, wl.Compositor, 6) catch return; conn.globals.compositor = registry.bind(g.name, wl.Compositor, 6) catch return; } else if (std.mem.eql(u8, iface, std.mem.span(wl.DataDeviceManager.interface.name))) { globals.data_device_manager = registry.bind(g.name, wl.DataDeviceManager, 3) catch return; conn.globals.data_device_manager = registry.bind(g.name, wl.DataDeviceManager, 3) catch return; } else if (std.mem.eql(u8, iface, std.mem.span(xdg.WmBase.interface.name))) { globals.wm_base = registry.bind(g.name, xdg.WmBase, 5) catch return; conn.globals.wm_base = registry.bind(g.name, xdg.WmBase, 5) catch return; } else if (std.mem.eql(u8, iface, std.mem.span(wl.Seat.interface.name))) { globals.seat = registry.bind(g.name, wl.Seat, 9) catch return; conn.globals.seat = registry.bind(g.name, wl.Seat, 9) catch return; } else if (std.mem.eql(u8, iface, std.mem.span(wl.Output.interface.name))) { const wl_out = registry.bind(g.name, wl.Output, 4) catch return; const out = conn.alloc.create(Output) catch { wl_out.release(); return; }; out.* = .{ .wl_output = wl_out, .name = g.name, .tracker = &conn.scale_tracker, }; conn.outputs.append(conn.alloc, out) catch { wl_out.release(); conn.alloc.destroy(out); return; }; conn.scale_tracker.addOutput(g.name) catch {}; wl_out.setListener(*Output, outputListener, out); } }, .global_remove => |g| { var i: usize = 0; while (i < conn.outputs.items.len) : (i += 1) { const out = conn.outputs.items[i]; if (out.name == g.name) { conn.scale_tracker.removeOutput(out.name); out.wl_output.release(); conn.alloc.destroy(out); _ = conn.outputs.swapRemove(i); return; } } }, .global_remove => {}, } } fn outputListener( _: *wl.Output, event: wl.Output.Event, out: *Output, ) void { switch (event) { .scale => |s| { out.pending_scale = s.factor; }, .done => { out.tracker.setOutputScale(out.name, out.pending_scale); }, .geometry, .mode, .name, .description => {}, } }