const std = @import("std"); const fs = std.fs; const debug = std.debug; const panic = debug.panic; const process = std.process; 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 = std.heap.c_allocator; var window: *c.GLFWwindow = undefined; fn errorCallback(err: c_int, description: [*c]const u8) callconv(.C) void { panic("Error {}: {s}\n", .{ err, description }); } pub fn main() !void { const progress = std.Progress.start(.{}); defer progress.end(); var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer arena.deinit(); var args = process.args(); _ = args.next(); const filename = args.next() orelse "config.yaml"; var config = try cfg.Config.parse(&arena.allocator(), filename); _ = 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..@as(usize, @intCast(monitor_count))], 0..) |monitor, i| { debug.print( "monitor {}: '{s}'\n", .{ i, @as([*:0]const u8, @ptrCast(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| { 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 config.project_root.openFile(config.fragment, .{}); var last_stat = try shader_file.stat(); try reloadShader(&config, &main_program, 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(), progress, &config, &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(); prev_time = now_time; control.update(); const stat = try shader_file.stat(); if (stat.mtime > last_stat.mtime or control.reload_requested) { try reloadShader(&config, &main_program, config.fragment, stat.size); try cache.refresh(); last_stat = stat; control.reload_requested = false; } fbo.bind(); c.glClear(c.GL_COLOR_BUFFER_BIT); main_program.bind(); constants.normalized_quad.draw(); fbo.unbind(); for (outputs.items, 0..) |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(); } } fn escape_dquote(str: []const u8) []const u8 { const state = struct { var buf: [1024]u8 = undefined; }; const len = std.mem.replacementSize(u8, str, "\"", "\\\""); _ = std.mem.replace(u8, str, "\"", "\\\"", state.buf[0..len]); return state.buf[0..len]; } // given a file and the directory it is in, resolve references fn loadFile(config: *const cfg.Config, writer: anytype, dir: []const u8, filename: []const u8) !void { const file_dir = try fs.path.resolve(c_allocator, &[_][]const u8{ dir, fs.path.dirname(filename) orelse "", }); defer c_allocator.free(file_dir); const file_path = try fs.path.resolve(c_allocator, &[_][]const u8{ dir, filename }); defer c_allocator.free(file_path); var file = try config.project_root.openFile(file_path, .{}); defer file.close(); var reader = file.reader(); var line_number: u32 = 1; var buf: [1024]u8 = undefined; var wrote_line = false; while (try reader.readUntilDelimiterOrEof(&buf, '\n')) |line| { if (std.mem.startsWith(u8, line, "#pragma ")) { var parts = std.mem.splitScalar(u8, 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] != '"') { debug.print("{s}:{}: Invalid #pragma include\n", .{ file_path, line_number }); debug.print("{s}:{}: {s}\n", .{ file_path, line_number, line }); continue; } include_path = include_path[1 .. include_path.len - 1]; loadFile(config, writer, file_dir, include_path) catch unreachable; _ = try writer.print("#line {d} \"{s}\"\n", .{ line_number, escape_dquote(filename) }); continue; } else { debug.print("{s}:{}: Unknown #pragma directive '{s}'\n", .{ file_path, line_number, pragma }); debug.print("{s}:{}: {s}\n", .{ file_path, line_number, line }); } } if (!std.mem.startsWith(u8, line, "#version") and !wrote_line) { _ = try writer.print("#line {d} \"{s}\"\n", .{ line_number - 1, escape_dquote(filename) }); wrote_line = true; } _ = try writer.write(line); _ = try writer.write("\n"); line_number += 1; } } fn reloadShader(config: *const cfg.Config, current: *gl.ShaderProgram, frag_filename: []const u8, size: u64) !void { var buffer = try std.ArrayList(u8).initCapacity(c_allocator, @as(usize, @intCast(size))); errdefer buffer.deinit(); const writer = buffer.writer(); _ = try writer.write( \\#version 330 core \\#extension GL_ARB_shading_language_include : require \\ ); try loadFile(config, writer, "", frag_filename); const frag_source = try 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| { debug.print("Error while reloading shader: {}\n", .{err}); } }