dotfiles/scripts/wlbg/src/main.zig

473 lines
13 KiB
Zig

const std = @import("std");
const xev = @import("xev");
const wayland = @import("wayland");
const c = @import("ffi.zig").c;
const options = @import("options.zig");
const DrawTimerHandler = @import("DrawTimerHandler.zig");
const Gfx = @import("Gfx.zig");
const Globals = @import("Globals.zig");
const OutputInfo = @import("OutputInfo.zig");
const OutputWindow = @import("OutputWindow.zig");
const PointerState = @import("PointerState.zig");
const wl = wayland.client.wl;
const zwlr = wayland.client.zwlr;
const zxdg = wayland.client.zxdg;
pub const std_options = std.Options{
.log_level = .debug,
.logFn = @import("common").logFn,
};
pub fn main() !void {
std.log.info("initializing event loop", .{});
var loop = try xev.Loop.init(.{});
defer loop.deinit();
std.log.info("connecting to wayland display", .{});
const dpy = try wl.Display.connect(null);
defer dpy.disconnect();
const globs = try Globals.collect(dpy);
defer globs.outputs.deinit();
const output_info = try std.heap.c_allocator.alloc(OutputInfo, globs.outputs.items.len);
defer std.heap.c_allocator.free(output_info);
@memset(output_info, .{});
for (globs.outputs.items, 0..) |output, i| {
const xdg_output = try globs.xdg_output_manager.getXdgOutput(output);
xdg_output.setListener(*OutputInfo, xdgOutputListener, &output_info[i]);
}
if (dpy.roundtrip() != .SUCCESS) return error.RoundtipFail;
if (c.eglBindAPI(c.EGL_OPENGL_API) == 0) return error.EGLError;
const egl_dpy = c.eglGetDisplay(@ptrCast(dpy)) orelse return error.EGLError;
if (c.eglInitialize(egl_dpy, null, null) != c.EGL_TRUE) return error.EGLError;
defer _ = c.eglTerminate(egl_dpy);
const config = egl_conf: {
var config: c.EGLConfig = undefined;
var n_config: i32 = 0;
if (c.eglChooseConfig(
egl_dpy,
&[_]i32{
c.EGL_SURFACE_TYPE, c.EGL_WINDOW_BIT,
c.EGL_RENDERABLE_TYPE, c.EGL_OPENGL_BIT,
c.EGL_RED_SIZE, 8,
c.EGL_GREEN_SIZE, 8,
c.EGL_BLUE_SIZE, 8,
c.EGL_NONE,
},
&config,
1,
&n_config,
) != c.EGL_TRUE) return error.EGLError;
break :egl_conf config;
};
std.log.info("creating EGL context", .{});
const egl_ctx = c.eglCreateContext(
egl_dpy,
config,
c.EGL_NO_CONTEXT,
&[_]i32{
c.EGL_CONTEXT_MAJOR_VERSION, 4,
c.EGL_CONTEXT_MINOR_VERSION, 3,
c.EGL_CONTEXT_OPENGL_DEBUG, c.EGL_TRUE,
c.EGL_CONTEXT_OPENGL_PROFILE_MASK, c.EGL_CONTEXT_OPENGL_CORE_PROFILE_BIT,
c.EGL_NONE,
},
) orelse return error.EGLError;
defer _ = c.eglDestroyContext(egl_dpy, egl_ctx);
const output_windows = try std.heap.c_allocator.alloc(OutputWindow, globs.outputs.items.len);
defer std.heap.c_allocator.free(output_windows);
for (output_windows, 0..) |*output_window, i| {
std.log.info("creating EGL surface #{}", .{i});
const surface = try globs.compositor.createSurface();
const lsurf = try globs.layer_shell.getLayerSurface(
surface,
globs.outputs.items[i],
.background,
"wlbg",
);
var winsize: ?[2]c_int = null;
lsurf.setListener(*?[2]c_int, layerSurfaceListener, &winsize);
lsurf.setAnchor(.{
.top = true,
.right = true,
.bottom = true,
.left = true,
});
lsurf.setExclusiveZone(-1);
surface.commit();
if (dpy.dispatch() != .SUCCESS) return error.DispatchFail;
const egl_win = win: {
std.log.info("creating EGL window #{}", .{i});
const size = winsize orelse return error.DidNotGetWindowSize;
break :win try wl.EglWindow.create(surface, size[0], size[1]);
};
errdefer egl_win.destroy();
const egl_surface = c.eglCreateWindowSurface(
egl_dpy,
config,
@ptrCast(egl_win),
null,
) orelse return error.EGLError;
errdefer _ = c.eglDestroySurface(egl_dpy, egl_surface);
output_window.* = .{
.egl_win = egl_win,
.egl_surface = egl_surface,
.surface = surface,
};
}
defer for (output_windows) |output| output.deinit(egl_ctx);
var r_timer_completion: xev.Completion = undefined;
var r_timer = try xev.Timer.init();
defer r_timer.deinit();
var dth = DrawTimerHandler{
.should_redraw = try std.heap.c_allocator.alloc(bool, output_info.len),
.completion = &r_timer_completion,
.loop = &loop,
};
defer std.heap.c_allocator.free(dth.should_redraw);
@memset(dth.should_redraw, true);
var pointer_state = PointerState{
.active_surface_idx = null,
.surface_positions = try std.heap.c_allocator.alloc([2]c_int, output_info.len),
};
defer std.heap.c_allocator.free(pointer_state.surface_positions);
@memset(pointer_state.surface_positions, .{ 0, 0 });
var pointer_listener_data = PointerListenerData{
.pstate = &pointer_state,
.outputs = output_windows,
.dth = &dth,
};
const pointer = try globs.seat.getPointer();
defer pointer.destroy();
pointer.setListener(*PointerListenerData, pointerListener, &pointer_listener_data);
const base_offset: [2]i32 = off: {
if (comptime options.multihead_mode == .individual) break :off .{ 0, 0 };
var total_width: i32 = 0;
var total_height: i32 = 0;
for (output_info) |inf| {
const xmax = inf.x;
const ymax = inf.y;
if (xmax > total_width)
total_width = xmax;
if (ymax > total_height)
total_height = ymax;
}
break :off .{
@divTrunc(total_width, 2),
@divTrunc(total_height, 2),
};
};
if (c.eglMakeCurrent(
egl_dpy,
output_windows[0].egl_surface,
output_windows[0].egl_surface,
egl_ctx,
) != c.EGL_TRUE) return error.EGLError;
c.glDebugMessageCallback(&glDebugCb, null);
c.glEnable(c.GL_DEBUG_OUTPUT);
std.log.info("initialized OpenGL {s}", .{c.glGetString(c.GL_VERSION)});
var gfx = try Gfx.init(dpy, egl_dpy, output_info);
defer gfx.deinit();
var rdata = RenderData{
.gfx = &gfx,
.egl_dpy = egl_dpy,
.egl_ctx = egl_ctx,
.outputs = output_windows,
.output_info = output_info,
.last_time = loop.now(),
.base_offset = base_offset,
.pointer_state = &pointer_state,
.dth = &dth,
};
var rbg_timer_completion: xev.Completion = undefined;
var rbg_timer = try xev.Timer.init();
defer rbg_timer.deinit();
rbg_timer.run(
&loop,
&rbg_timer_completion,
0,
RenderData,
&rdata,
renderBackgroundCb,
);
r_timer.run(
&loop,
&r_timer_completion,
0,
RenderData,
&rdata,
renderCb,
);
var wl_poll_completion = xev.Completion{
.op = .{ .poll = .{ .fd = dpy.getFd() } },
.userdata = dpy,
.callback = wlPollCb,
};
loop.add(&wl_poll_completion);
if (dpy.dispatchPending() != .SUCCESS) return error.RoundtipFail;
std.debug.assert(dpy.prepareRead());
std.log.info("running event loop", .{});
try loop.run(.until_done);
}
const RenderData = struct {
gfx: *Gfx,
egl_dpy: c.EGLDisplay,
egl_ctx: c.EGLContext,
outputs: []const OutputWindow,
output_info: []const OutputInfo,
last_time: isize,
base_offset: [2]c_int,
pointer_state: *PointerState,
dth: *DrawTimerHandler,
};
fn renderCb(
data: ?*RenderData,
loop: *xev.Loop,
completion: *xev.Completion,
result: xev.Timer.RunError!void,
) xev.CallbackAction {
_ = completion;
result catch unreachable;
const now = loop.now();
const delta_time = now - data.?.last_time;
data.?.last_time = now;
data.?.dth.resetTimer();
data.?.gfx.preDraw(
delta_time,
data.?.pointer_state,
data.?.output_info,
data.?.dth,
) catch |e| {
std.log.err("running preDraw: {}", .{e});
loop.stop();
return .disarm;
};
for (data.?.outputs, 0..) |output, i| {
if (!data.?.dth.should_redraw[i])
continue;
if (c.eglMakeCurrent(
data.?.egl_dpy,
output.egl_surface,
output.egl_surface,
data.?.egl_ctx,
) != c.EGL_TRUE) {
std.log.err("failed to set EGL context", .{});
loop.stop();
return .disarm;
}
data.?.gfx.draw(
delta_time,
data.?.pointer_state,
i,
data.?.outputs,
data.?.output_info,
data.?.dth,
) catch |e| {
std.log.err("drawing: {}", .{e});
loop.stop();
return .disarm;
};
}
return data.?.dth.nextAction();
}
fn renderBackgroundCb(
data: ?*RenderData,
loop: *xev.Loop,
completion: *xev.Completion,
result: xev.Timer.RunError!void,
) xev.CallbackAction {
result catch unreachable;
resetXevTimerCompletion(completion, loop.now(), options.refresh_time);
var rand: f32 = if (options.multihead_mode == .combined) std.crypto.random.float(f32) else 0.0;
for (data.?.output_info, 0..) |info, i| {
if (options.multihead_mode == .individual) rand = std.crypto.random.float(f32);
data.?.gfx.drawBackground(
info,
i,
data.?.base_offset,
rand,
) catch |e| {
std.log.err("drawing background: {}", .{e});
loop.stop();
return .disarm;
};
}
data.?.dth.damageAll();
return .rearm;
}
fn wlPollCb(
userdata: ?*anyopaque,
loop: *xev.Loop,
_: *xev.Completion,
result: xev.Result,
) xev.CallbackAction {
result.poll catch |e| {
std.log.err("unable to poll wayland FD: {}", .{e});
loop.stop();
return .disarm;
};
const dpy: *wl.Display = @ptrCast(@alignCast(userdata));
if (dpy.readEvents() != .SUCCESS) {
std.log.err("error reading wayland events", .{});
loop.stop();
return .disarm;
}
while (!dpy.prepareRead()) {
if (dpy.dispatchPending() != .SUCCESS or dpy.flush() != .SUCCESS) {
std.log.err("error processing wayland events", .{});
loop.stop();
return .disarm;
}
}
return .rearm;
}
fn layerSurfaceListener(lsurf: *zwlr.LayerSurfaceV1, ev: zwlr.LayerSurfaceV1.Event, winsize: *?[2]c_int) void {
switch (ev) {
.configure => |configure| {
winsize.* = .{ @intCast(configure.width), @intCast(configure.height) };
lsurf.setSize(configure.width, configure.height);
lsurf.ackConfigure(configure.serial);
},
else => {},
}
}
fn xdgOutputListener(_: *zxdg.OutputV1, ev: zxdg.OutputV1.Event, info: *OutputInfo) void {
switch (ev) {
.logical_position => |pos| {
info.x = pos.x;
info.y = pos.y;
},
.logical_size => |size| {
info.width = size.width;
info.height = size.height;
},
else => {},
}
}
const PointerListenerData = struct {
pstate: *PointerState,
outputs: []const OutputWindow,
dth: *DrawTimerHandler,
};
fn pointerListener(_: *wl.Pointer, ev: wl.Pointer.Event, d: *PointerListenerData) void {
switch (ev) {
.motion => |motion| {
if (d.pstate.active_surface_idx) |i| {
d.pstate.surface_positions[i] = .{
motion.surface_x.toInt(),
motion.surface_y.toInt(),
};
d.dth.damage(i);
}
},
.enter => |enter| {
for (d.outputs, 0..) |out, i| {
if (out.surface == enter.surface) {
d.dth.damage(i);
d.pstate.active_surface_idx = i;
break;
}
}
},
.leave => {
if (d.pstate.active_surface_idx) |i| {
d.dth.damage(i);
d.pstate.active_surface_idx = null;
}
},
else => {},
}
}
fn resetXevTimerCompletion(completion: *xev.Completion, now: i64, in: i64) void {
const next_time = now + in;
completion.op.timer.reset = .{
.tv_sec = @divTrunc(next_time, std.time.ms_per_s),
.tv_nsec = @mod(next_time, std.time.ms_per_s) * std.time.ns_per_ms,
};
}
fn glDebugCb(
source: c.GLenum,
@"type": c.GLenum,
id: c.GLuint,
severity: c.GLenum,
len: c.GLsizei,
msgp: ?[*:0]const u8,
udata: ?*const anyopaque,
) callconv(.C) void {
_ = source;
_ = @"type";
_ = id;
_ = udata;
const log = std.log.scoped(.gl);
// Mesa likes to include trailing newlines sometimes
const msg = std.mem.trim(u8, msgp.?[0..@intCast(len)], &std.ascii.whitespace);
switch (severity) {
c.GL_DEBUG_SEVERITY_HIGH => log.err("{s}", .{msg}),
c.GL_DEBUG_SEVERITY_MEDIUM, c.GL_DEBUG_SEVERITY_LOW => log.warn("{s}", .{msg}),
c.GL_DEBUG_SEVERITY_NOTIFICATION => log.info("{s}", .{msg}),
else => unreachable,
}
}