diff --git a/src/renderer/OpenGL.zig b/src/renderer/OpenGL.zig index c71508597..076798630 100644 --- a/src/renderer/OpenGL.zig +++ b/src/renderer/OpenGL.zig @@ -22,7 +22,7 @@ const math = @import("../math.zig"); const Surface = @import("../Surface.zig"); const CellProgram = @import("opengl/CellProgram.zig"); -const CustomProgram = @import("opengl/CustomProgram.zig"); +const custom = @import("opengl/custom.zig"); const log = std.log.scoped(.grid); @@ -1427,16 +1427,29 @@ fn drawCustomPrograms( ) !void { _ = self; - for (gl_state.custom_programs) |program| { + // If we have no custom shaders then we do nothing. + const custom_state = gl_state.custom orelse return; + + // Bind our state that is global to all custom shaders + const custom_bind = try custom_state.bind(); + defer custom_bind.unbind(); + + // Sync the uniform data. + // TODO: only do this when the data has changed + try custom_state.syncUniforms(); + + // Go through each custom shader and draw it. + for (custom_state.programs) |program| { // Bind our cell program state, buffers const bind = try program.bind(); defer bind.unbind(); - // Sync the uniform data. - // TODO: only do this when the data has changed - try program.syncUniforms(); - - try gl.drawElementsInstanced(gl.c.GL_TRIANGLES, 6, gl.c.GL_UNSIGNED_BYTE, 1); + try gl.drawElementsInstanced( + gl.c.GL_TRIANGLES, + 6, + gl.c.GL_UNSIGNED_BYTE, + 1, + ); } } @@ -1545,7 +1558,7 @@ const GLState = struct { cell_program: CellProgram, texture: gl.Texture, texture_color: gl.Texture, - custom_programs: []const CustomProgram, + custom: ?custom.State, pub fn init( alloc: Allocator, @@ -1557,21 +1570,25 @@ const GLState = struct { const arena_alloc = arena.allocator(); // Load our custom shaders - const custom_shaders: []const [:0]const u8 = shadertoy.loadFromFiles( - arena_alloc, - config.custom_shaders.items, - .glsl, - ) catch |err| err: { - log.warn("error loading custom shaders err={}", .{err}); - break :err &.{}; - }; + const custom_state: ?custom.State = custom: { + const shaders: []const [:0]const u8 = shadertoy.loadFromFiles( + arena_alloc, + config.custom_shaders.items, + .glsl, + ) catch |err| err: { + log.warn("error loading custom shaders err={}", .{err}); + break :err &.{}; + }; + if (shaders.len == 0) break :custom null; - // Create our custom programs - const custom_programs = try CustomProgram.createList(alloc, custom_shaders); - errdefer { - for (custom_programs) |p| p.deinit(); - alloc.free(custom_programs); - } + break :custom custom.State.init( + alloc, + shaders, + ) catch |err| err: { + log.warn("error initializing custom shaders err={}", .{err}); + break :err null; + }; + }; // Blending for text. We use GL_ONE here because we should be using // premultiplied alpha for all our colors in our fragment shaders. @@ -1628,15 +1645,14 @@ const GLState = struct { return .{ .cell_program = cell_program, - .custom_programs = custom_programs, .texture = tex, .texture_color = tex_color, + .custom = custom_state, }; } pub fn deinit(self: *GLState, alloc: Allocator) void { - for (self.custom_programs) |p| p.deinit(); - alloc.free(self.custom_programs); + if (self.custom) |v| v.deinit(alloc); self.texture.destroy(); self.texture_color.destroy(); self.cell_program.deinit(); diff --git a/src/renderer/opengl/CustomProgram.zig b/src/renderer/opengl/CustomProgram.zig deleted file mode 100644 index 2013e44fd..000000000 --- a/src/renderer/opengl/CustomProgram.zig +++ /dev/null @@ -1,146 +0,0 @@ -/// The OpenGL program for custom shaders. -const CustomProgram = @This(); - -const std = @import("std"); -const Allocator = std.mem.Allocator; -const gl = @import("opengl"); - -/// The "INDEX" is the index into the global GL state and the -/// "BINDING" is the binding location in the shader. -const UNIFORM_INDEX: gl.c.GLuint = 0; -const UNIFORM_BINDING: gl.c.GLuint = 0; - -/// The uniform state. Whenever this is modified this should be -/// synced to the buffer. The draw/bind calls don't automatically -/// sync this so this should be done whenever the state is modified. -uniforms: Uniforms = .{}, - -/// The actual shader program. -program: gl.Program, - -/// The uniform buffer that is updated with our uniform data. -ubo: gl.Buffer, - -/// This VAO is used for all custom shaders. It contains a single quad -/// by using an EBO. The vertex ID (gl_VertexID) can be used to determine the -/// position of the vertex. -vao: gl.VertexArray, -ebo: gl.Buffer, - -pub const Uniforms = extern struct { - resolution: [3]f32 align(16) = .{ 0, 0, 0 }, - time: f32 align(4) = 1, - time_delta: f32 align(4) = 1, - frame_rate: f32 align(4) = 1, - frame: i32 align(4) = 1, - channel_time: [4][4]f32 align(16) = [1][4]f32{.{ 0, 0, 0, 0 }} ** 4, - channel_resolution: [4][4]f32 align(16) = [1][4]f32{.{ 0, 0, 0, 0 }} ** 4, - mouse: [4]f32 align(16) = .{ 0, 0, 0, 0 }, - date: [4]f32 align(16) = .{ 0, 0, 0, 0 }, - sample_rate: f32 align(4) = 1, -}; - -pub fn createList(alloc: Allocator, srcs: []const [:0]const u8) ![]const CustomProgram { - var programs = std.ArrayList(CustomProgram).init(alloc); - defer programs.deinit(); - errdefer for (programs.items) |program| program.deinit(); - - for (srcs) |src| { - try programs.append(try CustomProgram.init(src)); - } - - return try programs.toOwnedSlice(); -} - -pub fn init(src: [:0]const u8) !CustomProgram { - const program = try gl.Program.createVF( - @embedFile("../shaders/custom.v.glsl"), - src, - //@embedFile("../shaders/temp.f.glsl"), - ); - errdefer program.destroy(); - - // Map our uniform buffer to the global GL state - try program.uniformBlockBinding(UNIFORM_INDEX, UNIFORM_BINDING); - - // Create our uniform buffer that is shared across all custom shaders - const ubo = try gl.Buffer.create(); - errdefer ubo.destroy(); - { - var ubobind = try ubo.bind(.uniform); - defer ubobind.unbind(); - try ubobind.setDataNull(Uniforms, .static_draw); - } - - // Setup our VAO for the custom shader. - const vao = try gl.VertexArray.create(); - errdefer vao.destroy(); - const vaobind = try vao.bind(); - defer vaobind.unbind(); - - // Element buffer (EBO) - const ebo = try gl.Buffer.create(); - errdefer ebo.destroy(); - var ebobind = try ebo.bind(.element_array); - defer ebobind.unbind(); - try ebobind.setData([6]u8{ - 0, 1, 3, // Top-left triangle - 1, 2, 3, // Bottom-right triangle - }, .static_draw); - - return .{ - .program = program, - .ubo = ubo, - .vao = vao, - .ebo = ebo, - }; -} - -pub fn deinit(self: CustomProgram) void { - self.ebo.destroy(); - self.vao.destroy(); - self.program.destroy(); -} - -pub fn syncUniforms(self: CustomProgram) !void { - var ubobind = try self.ubo.bind(.uniform); - defer ubobind.unbind(); - try ubobind.setData(self.uniforms, .static_draw); -} - -pub fn bind(self: CustomProgram) !Binding { - // Move our uniform buffer into proper global index. Note that - // in theory we can do this globally once and never worry about - // it again. I don't think we're high-performance enough at all - // to worry about that and this makes it so you can just move - // around CustomProgram usage without worrying about clobbering - // the global state. - try self.ubo.bindBase(.uniform, UNIFORM_INDEX); - - const program = try self.program.use(); - errdefer program.unbind(); - - const vao = try self.vao.bind(); - errdefer vao.unbind(); - - const ebo = try self.ebo.bind(.element_array); - errdefer ebo.unbind(); - - return .{ - .program = program, - .vao = vao, - .ebo = ebo, - }; -} - -pub const Binding = struct { - program: gl.Program.Binding, - vao: gl.VertexArray.Binding, - ebo: gl.Buffer.Binding, - - pub fn unbind(self: Binding) void { - self.ebo.unbind(); - self.vao.unbind(); - self.program.unbind(); - } -}; diff --git a/src/renderer/opengl/custom.zig b/src/renderer/opengl/custom.zig new file mode 100644 index 000000000..7f1deb5b2 --- /dev/null +++ b/src/renderer/opengl/custom.zig @@ -0,0 +1,172 @@ +const std = @import("std"); +const Allocator = std.mem.Allocator; +const gl = @import("opengl"); + +/// The "INDEX" is the index into the global GL state and the +/// "BINDING" is the binding location in the shader. +const UNIFORM_INDEX: gl.c.GLuint = 0; +const UNIFORM_BINDING: gl.c.GLuint = 0; + +pub const Uniforms = extern struct { + resolution: [3]f32 align(16) = .{ 0, 0, 0 }, + time: f32 align(4) = 1, + time_delta: f32 align(4) = 1, + frame_rate: f32 align(4) = 1, + frame: i32 align(4) = 1, + channel_time: [4][4]f32 align(16) = [1][4]f32{.{ 0, 0, 0, 0 }} ** 4, + channel_resolution: [4][4]f32 align(16) = [1][4]f32{.{ 0, 0, 0, 0 }} ** 4, + mouse: [4]f32 align(16) = .{ 0, 0, 0, 0 }, + date: [4]f32 align(16) = .{ 0, 0, 0, 0 }, + sample_rate: f32 align(4) = 1, +}; + +/// The state associated with custom shaders. +pub const State = struct { + /// The uniform data + uniforms: Uniforms, + + /// The OpenGL buffers + ubo: gl.Buffer, + vao: gl.VertexArray, + ebo: gl.Buffer, + + /// The set of programs for the custom shaders. + programs: []const Program, + + /// The last time the frame was drawn. This is used to update + /// the time uniform. + last_frame_time: std.time.Instant, + + pub fn init( + alloc: Allocator, + srcs: []const [:0]const u8, + ) !State { + // Create our programs + var programs = std.ArrayList(Program).init(alloc); + defer programs.deinit(); + errdefer for (programs.items) |p| p.deinit(); + for (srcs) |src| { + try programs.append(try Program.init(src)); + } + + // Create our uniform buffer that is shared across all + // custom shaders + const ubo = try gl.Buffer.create(); + errdefer ubo.destroy(); + { + var ubobind = try ubo.bind(.uniform); + defer ubobind.unbind(); + try ubobind.setDataNull(Uniforms, .static_draw); + } + + // Setup our VAO for the custom shader. + const vao = try gl.VertexArray.create(); + errdefer vao.destroy(); + const vaobind = try vao.bind(); + defer vaobind.unbind(); + + // Element buffer (EBO) + const ebo = try gl.Buffer.create(); + errdefer ebo.destroy(); + var ebobind = try ebo.bind(.element_array); + defer ebobind.unbind(); + try ebobind.setData([6]u8{ + 0, 1, 3, // Top-left triangle + 1, 2, 3, // Bottom-right triangle + }, .static_draw); + + return .{ + .programs = try programs.toOwnedSlice(), + .uniforms = .{}, + .ubo = ubo, + .vao = vao, + .ebo = ebo, + .last_frame_time = try std.time.Instant.now(), + }; + } + + pub fn deinit(self: *const State, alloc: Allocator) void { + for (self.programs) |p| p.deinit(); + alloc.free(self.programs); + self.ubo.destroy(); + self.ebo.destroy(); + self.vao.destroy(); + } + + pub fn syncUniforms(self: *const State) !void { + var ubobind = try self.ubo.bind(.uniform); + defer ubobind.unbind(); + try ubobind.setData(self.uniforms, .static_draw); + } + + pub fn bind(self: *const State) !Binding { + // Move our uniform buffer into proper global index. Note that + // in theory we can do this globally once and never worry about + // it again. I don't think we're high-performance enough at all + // to worry about that and this makes it so you can just move + // around CustomProgram usage without worrying about clobbering + // the global state. + try self.ubo.bindBase(.uniform, UNIFORM_INDEX); + + const vao = try self.vao.bind(); + errdefer vao.unbind(); + + const ebo = try self.ebo.bind(.element_array); + errdefer ebo.unbind(); + + return .{ + .vao = vao, + .ebo = ebo, + }; + } + + pub const Binding = struct { + vao: gl.VertexArray.Binding, + ebo: gl.Buffer.Binding, + + pub fn unbind(self: Binding) void { + self.ebo.unbind(); + self.vao.unbind(); + } + }; +}; + +/// A single OpenGL program (combined shaders) for custom shaders. +pub const Program = struct { + program: gl.Program, + + pub fn init(src: [:0]const u8) !Program { + const program = try gl.Program.createVF( + @embedFile("../shaders/custom.v.glsl"), + src, + //@embedFile("../shaders/temp.f.glsl"), + ); + errdefer program.destroy(); + + // Map our uniform buffer to the global GL state + try program.uniformBlockBinding(UNIFORM_INDEX, UNIFORM_BINDING); + + return .{ .program = program }; + } + + pub fn deinit(self: *const Program) void { + self.program.destroy(); + } + + pub fn bind(self: *const Program) !Binding { + const program = try self.program.use(); + errdefer program.unbind(); + + return .{ + .program = program, + }; + } + + pub const Binding = struct { + program: gl.Program.Binding, + + pub fn unbind(self: Binding) void { + self.program.unbind(); + } + }; +};