const std = @import("std"); const c = @import("c.zig").c; const build_config = @import("build_config"); const cfg = @import("config.zig"); const gl = @import("gl.zig"); pub const Output = struct { update_fn: *const fn (output: *Output, fbo: gl.FramebufferObject, fresh: bool) anyerror!?bool, destroy_fn: *const fn (output: *Output, allocator: std.mem.Allocator) void, pub fn create( allocator: std.mem.Allocator, config: *const cfg.OutputConfig, constants: *gl.Constants, ) !*Output { switch (config.*) { inline else => |*con| { const ConfigType = @TypeOf(con.*); if (ConfigType == void) return error.supportDisabled; return con.create(allocator, constants); }, } } pub fn update(self: *Output, fbo: gl.FramebufferObject, fresh: bool) anyerror!?bool { return self.update_fn(self, fbo, fresh); } pub fn destroy(self: *Output, allocator: std.mem.Allocator) void { return self.destroy_fn(self, allocator); } }; pub const WindowOutput = struct { pub const Config = struct { const FilterMode = enum(c.GLuint) { nearest = c.GL_NEAREST, linear = c.GL_LINEAR, }; const CropMode = enum(u32) { contain, cover, center, natural, pub const last: CropMode = .natural; }; width: i32, height: i32, filter: FilterMode, crop: CropMode, monitor: []const u8, fullscreen: bool, pub const default: Config = .{ .width = 800, .height = 600, .filter = .linear, .crop = .contain, .monitor = "", .fullscreen = false, }; pub fn create( config: *const Config, allocator: std.mem.Allocator, constants: *gl.Constants, ) *Output { const self = allocator.create(WindowOutput) catch unreachable; c.glfwDefaultWindowHints(); c.glfwWindowHint(c.GLFW_FOCUSED, c.GLFW_FALSE); c.glfwWindowHint(c.GLFW_FLOATING, c.GLFW_TRUE); c.glfwWindowHint(c.GLFW_TRANSPARENT_FRAMEBUFFER, c.GL_TRUE); self.* = .{ .constants = constants, .output = .{ .update_fn = update, .destroy_fn = destroy, }, .window = c.glfwCreateWindow( config.width, config.height, "glsl-view output", null, constants.main_window, ) orelse { std.debug.panic("unable to create output window\n", .{}); }, .crop = config.crop, }; c.glfwSetWindowUserPointer(self.window, @as(*anyopaque, @ptrCast(self))); _ = c.glfwSetKeyCallback(self.window, keyCallback); _ = c.glfwSetFramebufferSizeCallback(self.window, sizeCallback); c.glfwMakeContextCurrent(self.window); constants.normalized_quad.bind(0); return &self.output; } }; output: Output, window: *c.GLFWwindow, constants: *gl.Constants, crop: Config.CropMode, resized: bool = false, fn update(output: *Output, fbo: gl.FramebufferObject, _: bool) !?bool { const self: *WindowOutput = @fieldParentPtr("output", output); if (c.glfwWindowShouldClose(self.window) == c.GL_TRUE) return true; c.glfwMakeContextCurrent(self.window); c.glClearColor(0, 0, 0, 1); c.glClear(c.GL_COLOR_BUFFER_BIT); c.glfwWindowHint(c.GLFW_RESIZABLE, c.GLFW_FALSE); c.glfwWindowHint(c.GLFW_DECORATED, c.GLFW_FALSE); if (self.resized) { var wwidth: c_int = undefined; var wheight: c_int = undefined; c.glfwGetFramebufferSize(self.window, &wwidth, &wheight); const window_width: f32 = @floatFromInt(wwidth); const window_height: f32 = @floatFromInt(wheight); const width: f32 = @floatFromInt(self.constants.config.width); const height: f32 = @floatFromInt(self.constants.config.height); const width_scale = window_width / width; const height_scale = window_height / height; const scale: f32 = switch (self.crop) { .contain => @min(width_scale, height_scale), .cover => @max(width_scale, height_scale), .center => 1.0, .natural => 1.0 / @ceil(1.0 / @min(width_scale, height_scale)), }; c.glViewport( @intFromFloat((window_width - width * scale) / 2.0), @intFromFloat((window_height - height * scale) / 2.0), @intFromFloat(width * scale), @intFromFloat(height * scale), ); } c.glBindTexture(c.GL_TEXTURE_2D, fbo.texture_id); c.glDrawArrays(c.GL_TRIANGLE_STRIP, 0, 4); self.constants.texture_shader.bind(); self.constants.normalized_quad.draw(); c.glfwSwapBuffers(self.window); return false; } fn destroy(output: *Output, allocator: std.mem.Allocator) void { const self: *WindowOutput = @fieldParentPtr("output", output); c.glfwDestroyWindow(self.window); allocator.destroy(self); } fn keyCallback( win: ?*c.GLFWwindow, key: c_int, scancode: c_int, action: c_int, mods: c_int, ) callconv(.c) void { if (action != c.GLFW_PRESS) return; const userptr = c.glfwGetWindowUserPointer(win).?; const self = @as(*WindowOutput, @ptrCast(@alignCast(userptr))); _ = scancode; _ = mods; switch (key) { // c.GLFW_KEY_F => // toggle fullscreen // c.GLFW_KEY_LEFT => // cycle through monitors // c.GLFW_KEY_RIGHT => // cycle through monitors c.GLFW_KEY_C => { self.resized = true; self.crop = switch (self.crop) { .contain => .cover, .cover => .center, .center => .natural, .natural => .contain, }; std.debug.print("crop mode: {}\n", .{self.crop}); }, else => {}, } } fn sizeCallback(win: ?*c.GLFWwindow, width: c_int, height: c_int) callconv(.c) void { const userptr = c.glfwGetWindowUserPointer(win).?; const self = @as(*WindowOutput, @ptrCast(@alignCast(userptr))); self.resized = true; _ = width; _ = height; } }; pub const StdoutOutput = struct { pub const Config = struct { pub const default: Config = .{}; pub fn create( _: *const Config, allocator: std.mem.Allocator, constants: *gl.Constants, ) !*Output { const self = allocator.create(StdoutOutput) catch unreachable; const buffer = try allocator.alloc(u8, @intCast(constants.config.width * constants.config.height * 4)); errdefer allocator.free(buffer); const stdout = std.fs.File.stdout(); self.* = .{ .output = .{ .update_fn = update, .destroy_fn = destroy, }, .config = constants.config, .buffer = buffer, .file = stdout, }; return &self.output; } }; output: Output, config: *cfg.Config, buffer: []u8, file: std.fs.File, fn update(output: *Output, fbo: gl.FramebufferObject, fresh: bool) !?bool { const self: *StdoutOutput = @fieldParentPtr("output", output); if (fresh) { fbo.bind(); defer fbo.unbind(); fbo.read(self.config.width, self.config.height, self.buffer); _ = try self.file.write(self.buffer); } return null; } fn destroy(output: *Output, allocator: std.mem.Allocator) void { const self: *StdoutOutput = @fieldParentPtr("output", output); allocator.free(self.buffer); allocator.destroy(self); } };