const std = @import("std"); const fs = @import("std").fs; const panic = std.debug.panic; const c = @import("c.zig"); const debug_gl = @import("debug_gl.zig"); const cfg = @import("config.zig"); const out = @import("output.zig"); const gl = @import("gl.zig"); const ctrl = @import("control.zig"); const c_allocator = @import("std").heap.c_allocator; var window: *c.GLFWwindow = undefined; fn errorCallback(err: c_int, description: [*c]const u8) callconv(.C) void { panic("Error: {}\n", .{description}); } pub fn main() !void { var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer arena.deinit(); var config = try cfg.Config.parse(&arena.allocator, "config.yaml"); _ = c.glfwSetErrorCallback(errorCallback); if (c.glfwInit() == c.GL_FALSE) { panic("GLFW init failure\n", .{}); } defer c.glfwTerminate(); var monitor_count: c_int = 0; const monitors = c.glfwGetMonitors(&monitor_count); for (monitors[0..@intCast(usize, monitor_count)]) |monitor, i| { std.debug.warn( "monitor {}: '{s}'\n", .{ i, @ptrCast([*:0]const u8, c.glfwGetMonitorName(monitor)) }, ); } c.glfwWindowHint(c.GLFW_CONTEXT_VERSION_MAJOR, 4); c.glfwWindowHint(c.GLFW_CONTEXT_VERSION_MINOR, 2); c.glfwWindowHint(c.GLFW_OPENGL_FORWARD_COMPAT, c.GL_TRUE); c.glfwWindowHint(c.GLFW_OPENGL_DEBUG_CONTEXT, debug_gl.is_on); c.glfwWindowHint(c.GLFW_OPENGL_PROFILE, c.GLFW_OPENGL_CORE_PROFILE); c.glfwWindowHint(c.GLFW_DEPTH_BITS, 0); c.glfwWindowHint(c.GLFW_STENCIL_BITS, 0); c.glfwWindowHint(c.GLFW_VISIBLE, c.GLFW_FALSE); window = c.glfwCreateWindow(config.width, config.height, "glsl-view", null, null) orelse { panic("unable to create window\n", .{}); }; defer c.glfwDestroyWindow(window); c.glfwMakeContextCurrent(window); c.glfwSwapInterval(1); var constants = try gl.Constants.create(window, &config); defer constants.destroy(); var outputs = try std.ArrayList(*out.Output).initCapacity(&arena.allocator, config.outputs.len); defer outputs.deinit(); for (config.outputs) |output_config, i| { try outputs.append(out.Output.create(&arena.allocator, output_config, &constants)); } defer for (outputs.items) |output| { output.destroy(output); }; c.glfwMakeContextCurrent(window); c.glClearColor(0.0, 0.0, 0.0, 1.0); debug_gl.assertNoError(); debug_gl.init(); const start_time = c.glfwGetTime(); var prev_time = start_time; constants.normalized_quad.bind(0); var main_program = try gl.ShaderProgram.create( \\#version 330 core \\ \\layout(location = 0) in vec2 position; \\out vec2 uv; \\ \\void main() { \\ uv = position / 2.0 + 0.5; \\ gl_Position = vec4(position, 0, 1); \\} , \\#version 330 core \\ \\in vec2 uv; \\out vec4 color; \\ \\void main() { \\ color = vec4(0,0,0,1); \\} ); defer main_program.destroy(); var fbo = try gl.FramebufferObject.create(config.width, config.height); defer fbo.destroy(); const shader_file = try fs.cwd().openFile(config.fragment, .{}); var last_stat = try shader_file.stat(); try reloadShader(&main_program, shader_file, config.fragment, last_stat.size); var cache = gl.UniformCache.init(std.heap.c_allocator, &main_program); defer cache.deinit(); const control = try ctrl.ControlServer.init(&arena.allocator, config.osc, &cache); defer control.destroy(); while (c.glfwWindowShouldClose(window) == c.GL_FALSE) { c.glfwMakeContextCurrent(window); c.glClear(c.GL_COLOR_BUFFER_BIT); const now_time = c.glfwGetTime(); const elapsed = now_time - prev_time; prev_time = now_time; const stat = try shader_file.stat(); if (stat.mtime > last_stat.mtime) { try reloadShader(&main_program, shader_file, config.fragment, stat.size); try cache.refresh(); last_stat = stat; } control.update(); fbo.bind(); c.glClear(c.GL_COLOR_BUFFER_BIT); main_program.bind(); constants.normalized_quad.draw(); fbo.unbind(); for (outputs.items) |output, i| { const close = output.update(output, fbo.texture_id); if (close) { const removed = outputs.swapRemove(i); removed.destroy(removed); break; } } if (outputs.items.len == 0) break; c.glfwPollEvents(); } } // given a file and the directory it is in, resolve references fn loadFile(writer: anytype, dir: []const u8, filename: []const u8) !void { const file_dir = try std.fs.path.resolve(c_allocator, &[_][]const u8{ dir, std.fs.path.dirname(filename) orelse "", }); defer c_allocator.free(file_dir); const file_path = try std.fs.path.resolve(c_allocator, &[_][]const u8{ dir, filename }); defer c_allocator.free(file_path); var file = try fs.openFileAbsolute(file_path, .{}); defer file.close(); var reader = file.reader(); var buf: [1024]u8 = undefined; while (try reader.readUntilDelimiterOrEof(&buf, '\n')) |line| { if (std.mem.startsWith(u8, line, "#pragma ")) { var parts = std.mem.split(line, " "); _ = parts.next(); const pragma = parts.next().?; if (std.mem.eql(u8, pragma, "include")) { var include_path = parts.next().?; if (include_path[0] != '"' or include_path[include_path.len - 1] != '"') { std.debug.warn("Invalid #pragma directive '{}'\n", .{line}); continue; } include_path = include_path[1 .. include_path.len - 1]; loadFile(writer, file_dir, include_path) catch unreachable; continue; } else { std.debug.warn("Unknown #pragma directive '{}'\n", .{pragma}); } } _ = try writer.write(line); _ = try writer.write("\n"); } } fn reloadShader(current: *gl.ShaderProgram, frag_file: fs.File, frag_filename: []const u8, size: u64) !void { var buffer = try std.ArrayList(u8).initCapacity(c_allocator, @intCast(usize, size)); var writer = buffer.writer(); errdefer buffer.deinit(); try loadFile(buffer.writer(), "", frag_filename); const frag_source = buffer.toOwnedSlice(); defer c_allocator.free(frag_source); const vert_source = \\#version 330 core \\ \\layout(location = 0) in vec2 position; \\out vec2 uv; \\ \\void main() { \\ uv = position / 2.0 + 0.5; \\ gl_Position = vec4(position, 0, 1); \\} ; if (gl.ShaderProgram.create(vert_source, frag_source)) |new_program| { current.destroy(); current.* = new_program; } else |err| { std.debug.warn("Error while reloading shader:{}\n", .{err}); } }