diff --git a/.config/hypr/hyprland.conf.cgt b/.config/hypr/hyprland.conf.cgt index b5f97f6..57cd255 100644 --- a/.config/hypr/hyprland.conf.cgt +++ b/.config/hypr/hyprland.conf.cgt @@ -82,7 +82,7 @@ env = WLR_NO_HARDWARE_CURSORS, 1 exec-once = waybar -exec-once = swww-daemon +exec-once = wlbg exec-once = sleep 1 && randomwallpaper exec-once = hyprctl setcursor <% opt.cursor.theme %> <% opt.cursor.size %> diff --git a/scripts/wlbg/src/Gfx.zig b/scripts/wlbg/src/Gfx.zig index a99093f..f6b2f38 100644 --- a/scripts/wlbg/src/Gfx.zig +++ b/scripts/wlbg/src/Gfx.zig @@ -4,54 +4,237 @@ const c = @import("ffi.zig").c; const glutil = @import("glutil.zig"); const OutputInfo = @import("OutputInfo.zig"); +const OutputWindow = @import("OutputWindow.zig"); +const PointerState = @import("PointerState.zig"); egl_dpy: c.EGLDisplay, bg_shader_program: c_uint, +main_shader_program: c_uint, +bg_bufs: std.MultiArrayList(BgBuf), +time: i64, +cursor_positions: [][2]c_int, const Gfx = @This(); -pub fn init(egl_dpy: c.EGLDisplay) !Gfx { - const vert_shader = try glutil.createShader(c.GL_VERTEX_SHADER, @embedFile("bg_vert.glsl")); - defer c.glDeleteShader(vert_shader); - const frag_shader = try glutil.createShader(c.GL_FRAGMENT_SHADER, @embedFile("bg_frag.glsl")); - defer c.glDeleteShader(frag_shader); +const BgBuf = struct { + texture: c_uint, + framebuffer: c_uint, + zbuffer: c_uint, +}; - const program = c.glCreateProgram(); - errdefer c.glDeleteProgram(program); - c.glAttachShader(program, vert_shader); - c.glAttachShader(program, frag_shader); - c.glLinkProgram(program); +pub fn init(egl_dpy: c.EGLDisplay, output_info: []const OutputInfo) !Gfx { + const bg_program = shader: { + const vert_shader = try glutil.createShader(c.GL_VERTEX_SHADER, @embedFile("bg_vert.glsl")); + defer c.glDeleteShader(vert_shader); + const frag_shader = try glutil.createShader(c.GL_FRAGMENT_SHADER, @embedFile("bg_frag.glsl")); + defer c.glDeleteShader(frag_shader); - var success: c_int = 0; - c.glGetProgramiv(program, c.GL_LINK_STATUS, &success); - if (success != 1) - return error.ShaderLinkFail; + const program = c.glCreateProgram(); + errdefer c.glDeleteProgram(program); + c.glAttachShader(program, vert_shader); + c.glAttachShader(program, frag_shader); + c.glLinkProgram(program); + + var success: c_int = 0; + c.glGetProgramiv(program, c.GL_LINK_STATUS, &success); + if (success != 1) + return error.ShaderLinkFail; + + break :shader program; + }; + + const main_program = shader: { + const vert_shader = try glutil.createShader(c.GL_VERTEX_SHADER, @embedFile("main_vert.glsl")); + defer c.glDeleteShader(vert_shader); + const frag_shader = try glutil.createShader(c.GL_FRAGMENT_SHADER, @embedFile("main_frag.glsl")); + defer c.glDeleteShader(frag_shader); + + const program = c.glCreateProgram(); + errdefer c.glDeleteProgram(program); + c.glAttachShader(program, vert_shader); + c.glAttachShader(program, frag_shader); + c.glLinkProgram(program); + + var success: c_int = 0; + c.glGetProgramiv(program, c.GL_LINK_STATUS, &success); + if (success != 1) + return error.ShaderLinkFail; + + break :shader program; + }; + + const cursor_positions = try std.heap.c_allocator.alloc([2]c_int, output_info.len); + errdefer std.heap.c_allocator.free(cursor_positions); + @memset(cursor_positions, .{ 0, 0 }); + + var bg_bufs = std.MultiArrayList(BgBuf){}; + errdefer bg_bufs.deinit(std.heap.c_allocator); + + try bg_bufs.resize(std.heap.c_allocator, output_info.len); + + const bg_slice = bg_bufs.slice(); + + // @intCast safety: user is somewhat unlikely to have 2^32 - 1 monitors. + c.glGenTextures(@intCast(output_info.len), bg_slice.items(.texture).ptr); + errdefer c.glDeleteTextures(@intCast(bg_bufs.len), bg_slice.items(.texture).ptr); + c.glGenFramebuffers(@intCast(output_info.len), bg_slice.items(.framebuffer).ptr); + errdefer c.glDeleteFramebuffers(@intCast(bg_bufs.len), bg_slice.items(.framebuffer).ptr); + c.glGenRenderbuffers(@intCast(output_info.len), bg_slice.items(.zbuffer).ptr); + errdefer c.glDeleteRenderbuffers(@intCast(output_info.len), bg_slice.items(.zbuffer).ptr); + + for ( + output_info, + bg_slice.items(.texture), + bg_slice.items(.framebuffer), + bg_slice.items(.zbuffer), + ) |inf, tex, fb, zb| { + c.glBindFramebuffer(c.GL_FRAMEBUFFER, fb); + c.glBindTexture(c.GL_TEXTURE_2D, tex); + c.glBindRenderbuffer(c.GL_RENDERBUFFER, zb); + + c.glTexImage2D( + c.GL_TEXTURE_2D, + 0, + c.GL_RGBA, + inf.width, + inf.height, + 0, + c.GL_RGBA, + c.GL_UNSIGNED_BYTE, + null, + ); + c.glTexParameteri(c.GL_TEXTURE_2D, c.GL_TEXTURE_MAG_FILTER, c.GL_NEAREST); + c.glTexParameteri(c.GL_TEXTURE_2D, c.GL_TEXTURE_MIN_FILTER, c.GL_NEAREST); + + c.glFramebufferTexture2D( + c.GL_FRAMEBUFFER, + c.GL_COLOR_ATTACHMENT0, + c.GL_TEXTURE_2D, + tex, + 0, + ); + + c.glRenderbufferStorage(c.GL_RENDERBUFFER, c.GL_DEPTH_COMPONENT16, inf.width, inf.height); + c.glFramebufferRenderbuffer(c.GL_FRAMEBUFFER, c.GL_DEPTH_ATTACHMENT, c.GL_RENDERBUFFER, zb); + + if (c.glCheckFramebufferStatus(c.GL_FRAMEBUFFER) != c.GL_FRAMEBUFFER_COMPLETE) + return error.FramebufferIncomplete; + } + + c.glBindFramebuffer(c.GL_FRAMEBUFFER, 0); + c.glBindTexture(c.GL_TEXTURE_2D, 0); + c.glBindRenderbuffer(c.GL_FRAMEBUFFER, 0); return .{ .egl_dpy = egl_dpy, - .bg_shader_program = program, + .bg_shader_program = bg_program, + .main_shader_program = main_program, + .bg_bufs = bg_bufs, + .time = 0, + .cursor_positions = cursor_positions, }; } pub fn deinit(self: *Gfx) void { + const bg_slice = self.bg_bufs.slice(); + c.glDeleteTextures(@intCast(bg_slice.len), bg_slice.items(.texture).ptr); + c.glDeleteFramebuffers(@intCast(bg_slice.len), bg_slice.items(.framebuffer).ptr); + c.glDeleteRenderbuffers(@intCast(bg_slice.len), bg_slice.items(.zbuffer).ptr); + self.bg_bufs.deinit(std.heap.c_allocator); + c.glDeleteProgram(self.bg_shader_program); self.* = undefined; } -pub fn drawBackground( +pub fn draw( self: *Gfx, dt: i64, - egl_surface: c.EGLSurface, - info: OutputInfo, - base_xoff: i32, - base_yoff: i32, + pointer_state: *PointerState, + output_idx: usize, + outputs: []const OutputWindow, + infos: []const OutputInfo, ) !void { - _ = dt; + self.time += dt; + c.glBindFramebuffer(c.GL_FRAMEBUFFER, 0); // use default framebuffer + c.glUseProgram(self.main_shader_program); + for (self.cursor_positions, infos, outputs) |*pos, inf, outp| { + const target = if (pointer_state.surface == outp.surface) + .{ pointer_state.x, pointer_state.y } + else + .{ @divTrunc(inf.width, 2), @divTrunc(inf.height, 2) }; + + pos[0] = @intFromFloat(std.math.lerp( + @as(f32, @floatFromInt(pos[0])), + @as(f32, @floatFromInt(target[0])), + std.math.clamp(@as(f32, @floatFromInt(dt)) / 1000.0, 0.0, 1.0), + )); + pos[1] = @intFromFloat(std.math.lerp( + @as(f32, @floatFromInt(pos[1])), + @as(f32, @floatFromInt(target[1])), + std.math.clamp(@as(f32, @floatFromInt(dt)) / 1000.0, 0.0, 1.0), + )); + } + + const vertices = [_]f32{ + -1.0, -1.0, 0.0, 0.0, 0.0, + 1.0, -1.0, 0.0, 1.0, 0.0, + 1.0, 1.0, 0.0, 1.0, 1.0, + + -1.0, -1.0, 0.0, 0.0, 0.0, + 1.0, 1.0, 0.0, 1.0, 1.0, + -1.0, 1.0, 0.0, 0.0, 1.0, + }; + + c.glVertexAttribPointer(0, 3, c.GL_FLOAT, c.GL_FALSE, @sizeOf(f32) * 5, &vertices); + c.glEnableVertexAttribArray(0); + c.glVertexAttribPointer( + 1, + 2, + c.GL_FLOAT, + c.GL_FALSE, + @sizeOf(f32) * 5, + @ptrFromInt(@intFromPtr(&vertices) + @sizeOf(f32) * 3), + ); + c.glEnableVertexAttribArray(1); + + c.glClearColor(1.0, 0.0, 0.0, 1.0); + c.glClear(c.GL_COLOR_BUFFER_BIT); + + c.glBindTexture(c.GL_TEXTURE_2D, self.bg_bufs.get(output_idx).texture); + + c.glUniform2f( + c.glGetUniformLocation(self.main_shader_program, "cursorPos"), + @as(f32, @floatFromInt(self.cursor_positions[output_idx][0])) / @as(f32, @floatFromInt(infos[output_idx].width)), + 1.0 - @as(f32, @floatFromInt(self.cursor_positions[output_idx][1])) / @as(f32, @floatFromInt(infos[output_idx].height)), + ); + c.glUniform1f( + c.glGetUniformLocation(self.main_shader_program, "hasCursor"), + if (outputs[output_idx].surface == pointer_state.surface) 1.0 else 0.0, + ); + //c.glUniform1f( + // c.glGetUniformLocation(self.main_shader_program, "time"), + // @as(f32, @floatFromInt(self.time)) / 1000.0, + //); + + c.glDrawArrays(c.GL_TRIANGLES, 0, vertices.len / 3); + + if (c.eglSwapInterval(self.egl_dpy, 0) != c.EGL_TRUE or + c.eglSwapBuffers(self.egl_dpy, outputs[output_idx].egl_surface) != c.EGL_TRUE) return error.EGLError; +} + +pub fn drawBackground( + self: *Gfx, + info: OutputInfo, + output_idx: usize, + base_off: [2]i32, + rand: f32, +) !void { + std.log.info("draw bg", .{}); // There's just about a 0% chance this works properly when monitors have different resolutions, // but I can't even begin thinking about that. - const xoff = @as(f32, @floatFromInt(info.x - base_xoff)) / @as(f32, @floatFromInt(info.width)); - const yoff = @as(f32, @floatFromInt(info.y - base_yoff)) / @as(f32, @floatFromInt(info.height)); + const xoff = @as(f32, @floatFromInt(info.x - base_off[0])) / @as(f32, @floatFromInt(info.width)); + const yoff = @as(f32, @floatFromInt(info.y - base_off[1])) / @as(f32, @floatFromInt(info.height)); const vertices = [_]f32{ -1.0, -1.0, 0.0, xoff, yoff, @@ -63,6 +246,8 @@ pub fn drawBackground( -1.0, 1.0, 0.0, xoff, yoff, }; + c.glBindFramebuffer(c.GL_FRAMEBUFFER, self.bg_bufs.get(output_idx).framebuffer); + c.glClearColor(1.0, 0.0, 0.0, 1.0); c.glClear(c.GL_COLOR_BUFFER_BIT); @@ -74,11 +259,7 @@ pub fn drawBackground( c.glVertexAttribPointer(1, 2, c.GL_FLOAT, c.GL_FALSE, @sizeOf(f32) * 5, @ptrFromInt(@intFromPtr(&vertices) + @sizeOf(f32) * 3)); c.glEnableVertexAttribArray(1); - const rand = std.crypto.random.float(f32); c.glUniform1f(c.glGetUniformLocation(self.bg_shader_program, "time"), rand * 2000.0 - 1000.0); c.glDrawArrays(c.GL_TRIANGLES, 0, vertices.len / 3); - - if (c.eglSwapInterval(self.egl_dpy, 0) != c.EGL_TRUE or - c.eglSwapBuffers(self.egl_dpy, egl_surface) != c.EGL_TRUE) return error.EGLError; } diff --git a/scripts/wlbg/src/Globals.zig b/scripts/wlbg/src/Globals.zig index 2381884..6425025 100644 --- a/scripts/wlbg/src/Globals.zig +++ b/scripts/wlbg/src/Globals.zig @@ -7,6 +7,7 @@ const zwlr = wayland.client.zwlr; const log = std.log.scoped(.globals); +seat: *wl.Seat, compositor: *wl.Compositor, layer_shell: *zwlr.LayerShellV1, xdg_output_manager: *zxdg.OutputManagerV1, diff --git a/scripts/wlbg/src/OutputWindow.zig b/scripts/wlbg/src/OutputWindow.zig new file mode 100644 index 0000000..6c605ff --- /dev/null +++ b/scripts/wlbg/src/OutputWindow.zig @@ -0,0 +1,14 @@ +const wl = @import("wayland").client.wl; +const c = @import("ffi.zig").c; + +egl_win: *wl.EglWindow, +egl_surface: c.EGLSurface, +surface: *wl.Surface, + +const OutputWindow = @This(); + +pub fn deinit(self: OutputWindow, egl_dpy: c.EGLDisplay) void { + self.egl_win.destroy(); + self.surface.destroy(); + _ = c.eglDestroySurface(egl_dpy, self.egl_surface); +} diff --git a/scripts/wlbg/src/PointerState.zig b/scripts/wlbg/src/PointerState.zig new file mode 100644 index 0000000..bd765ac --- /dev/null +++ b/scripts/wlbg/src/PointerState.zig @@ -0,0 +1,5 @@ +const wl = @import("wayland").client.wl; + +surface: ?*wl.Surface, +x: c_int, +y: c_int, diff --git a/scripts/wlbg/src/main.zig b/scripts/wlbg/src/main.zig index c6d6049..63c3af2 100644 --- a/scripts/wlbg/src/main.zig +++ b/scripts/wlbg/src/main.zig @@ -7,11 +7,19 @@ const c = @import("ffi.zig").c; 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 = struct { + pub const log_level = .debug; +}; + +const fps = 15; + pub fn main() !void { std.log.info("initializing event loop", .{}); var loop = try xev.Loop.init(.{}); @@ -118,10 +126,21 @@ pub fn main() !void { output_window.* = .{ .egl_win = egl_win, .egl_surface = egl_surface, + .surface = surface, }; } defer for (output_windows) |output| output.deinit(egl_ctx); + var pointer_state = PointerState{ + .surface = null, + .x = 0, + .y = 0, + }; + + const pointer = try globs.seat.getPointer(); + defer pointer.destroy(); + pointer.setListener(*PointerState, pointerListener, &pointer_state); + var total_width: i32 = 0; var total_height: i32 = 0; @@ -145,10 +164,10 @@ pub fn main() !void { egl_ctx, ) != c.EGL_TRUE) return error.EGLError; - var gfx = try Gfx.init(egl_dpy); + var gfx = try Gfx.init(egl_dpy, output_info); defer gfx.deinit(); - var rbgdata = RenderBackgroundData{ + var rdata = RenderData{ .gfx = &gfx, .egl_dpy = egl_dpy, .egl_ctx = egl_ctx, @@ -156,6 +175,7 @@ pub fn main() !void { .output_info = output_info, .last_time = loop.now(), .base_offset = .{ base_xoff, base_yoff }, + .pointer_state = &pointer_state, }; var rbg_timer_completion: xev.Completion = undefined; @@ -166,11 +186,24 @@ pub fn main() !void { &loop, &rbg_timer_completion, 0, - RenderBackgroundData, - &rbgdata, + RenderData, + &rdata, renderBackgroundCb, ); + var r_timer_completion: xev.Completion = undefined; + var r_timer = try xev.Timer.init(); + defer r_timer.deinit(); + + r_timer.run( + &loop, + &r_timer_completion, + 0, + RenderData, + &rdata, + renderCb, + ); + var wl_poll_completion = xev.Completion{ .op = .{ .poll = .{ .fd = dpy.getFd() } }, .userdata = dpy, @@ -182,7 +215,7 @@ pub fn main() !void { try loop.run(.until_done); } -const RenderBackgroundData = struct { +const RenderData = struct { gfx: *Gfx, egl_dpy: c.EGLDisplay, egl_ctx: c.EGLContext, @@ -190,10 +223,11 @@ const RenderBackgroundData = struct { output_info: []const OutputInfo, last_time: isize, base_offset: [2]c_int, + pointer_state: *PointerState, }; -fn renderBackgroundCb( - data: ?*RenderBackgroundData, +fn renderCb( + data: ?*RenderData, loop: *xev.Loop, completion: *xev.Completion, result: xev.Timer.RunError!void, @@ -201,16 +235,12 @@ fn renderBackgroundCb( result catch unreachable; const now = loop.now(); - const delta_time = now - data.?.last_time; data.?.last_time = now; - const next_time = now + std.time.ms_per_min; - 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, - }; - for (data.?.outputs, data.?.output_info) |output, info| { + resetXevTimerCompletion(completion, now, 1000 / fps); + + for (data.?.outputs, 0..) |output, i| { if (c.eglMakeCurrent( data.?.egl_dpy, output.egl_surface, @@ -222,12 +252,12 @@ fn renderBackgroundCb( return .disarm; } - data.?.gfx.drawBackground( + data.?.gfx.draw( delta_time, - output.egl_surface, - info, - data.?.base_offset[0], - data.?.base_offset[1], + data.?.pointer_state, + i, + data.?.outputs, + data.?.output_info, ) catch |e| { std.log.err("drawing: {}", .{e}); loop.stop(); @@ -238,6 +268,34 @@ fn renderBackgroundCb( return .rearm; } +fn renderBackgroundCb( + data: ?*RenderData, + loop: *xev.Loop, + completion: *xev.Completion, + result: xev.Timer.RunError!void, +) xev.CallbackAction { + result catch unreachable; + + resetXevTimerCompletion(completion, loop.now(), std.time.ms_per_min); + + const rand = std.crypto.random.float(f32); + for (data.?.outputs, data.?.output_info, 0..) |output, info, i| { + _ = output; + data.?.gfx.drawBackground( + info, + i, + data.?.base_offset, + rand, + ) catch |e| { + std.log.err("drawing background: {}", .{e}); + loop.stop(); + return .disarm; + }; + } + + return .rearm; +} + fn wlPollCb( userdata: ?*anyopaque, loop: *xev.Loop, @@ -260,16 +318,6 @@ fn wlPollCb( return .rearm; } -const OutputWindow = struct { - egl_win: *wl.EglWindow, - egl_surface: c.EGLSurface, - - fn deinit(self: OutputWindow, egl_dpy: c.EGLDisplay) void { - self.egl_win.destroy(); - _ = c.eglDestroySurface(egl_dpy, self.egl_surface); - } -}; - fn layerSurfaceListener(lsurf: *zwlr.LayerSurfaceV1, ev: zwlr.LayerSurfaceV1.Event, winsize: *?[2]c_int) void { switch (ev) { .configure => |configure| { @@ -294,3 +342,23 @@ fn xdgOutputListener(_: *zxdg.OutputV1, ev: zxdg.OutputV1.Event, info: *OutputIn else => {}, } } + +fn pointerListener(_: *wl.Pointer, ev: wl.Pointer.Event, state: *PointerState) void { + switch (ev) { + .motion => |motion| { + state.x = motion.surface_x.toInt(); + state.y = motion.surface_y.toInt(); + }, + .enter => |enter| state.surface = enter.surface, + .leave => state.surface = 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, + }; +} diff --git a/scripts/wlbg/src/main_frag.glsl b/scripts/wlbg/src/main_frag.glsl new file mode 100644 index 0000000..8ec2e89 --- /dev/null +++ b/scripts/wlbg/src/main_frag.glsl @@ -0,0 +1,20 @@ +#version 300 es + +precision highp float; + +uniform sampler2D bg; + +uniform vec2 cursorPos; +uniform float hasCursor; + +in vec2 fragCoord; +in vec2 fragUv; + +void main() { + vec2 diff = cursorPos - fragUv; + float light = clamp(.1 - (diff.x * diff.x + diff.y * diff.y), 0.0, 1.0); + + vec2 zoomedUv = fragUv * .9 + (.1 / 2.0); + gl_FragColor = mix(texture(bg, zoomedUv + (cursorPos - .5) / 10.0), vec4(1.0), light * hasCursor); +} + diff --git a/scripts/wlbg/src/main_vert.glsl b/scripts/wlbg/src/main_vert.glsl new file mode 100644 index 0000000..0b5b066 --- /dev/null +++ b/scripts/wlbg/src/main_vert.glsl @@ -0,0 +1,15 @@ +#version 300 es + +precision mediump float; + +attribute vec4 vPos; +attribute vec2 uv; + +out vec2 fragCoord; +out vec2 fragUv; + +void main() { + gl_Position = vPos; + fragCoord = vPos.xy; + fragUv = uv; +}