renderer/opengl: better organization of custom shader state

This commit is contained in:
Mitchell Hashimoto
2023-11-17 16:48:34 -08:00
parent 47971e7663
commit e0afa442c4
3 changed files with 213 additions and 171 deletions

View File

@ -22,7 +22,7 @@ const math = @import("../math.zig");
const Surface = @import("../Surface.zig"); const Surface = @import("../Surface.zig");
const CellProgram = @import("opengl/CellProgram.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); const log = std.log.scoped(.grid);
@ -1427,16 +1427,29 @@ fn drawCustomPrograms(
) !void { ) !void {
_ = self; _ = 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 // Bind our cell program state, buffers
const bind = try program.bind(); const bind = try program.bind();
defer bind.unbind(); defer bind.unbind();
// Sync the uniform data. try gl.drawElementsInstanced(
// TODO: only do this when the data has changed gl.c.GL_TRIANGLES,
try program.syncUniforms(); 6,
gl.c.GL_UNSIGNED_BYTE,
try gl.drawElementsInstanced(gl.c.GL_TRIANGLES, 6, gl.c.GL_UNSIGNED_BYTE, 1); 1,
);
} }
} }
@ -1545,7 +1558,7 @@ const GLState = struct {
cell_program: CellProgram, cell_program: CellProgram,
texture: gl.Texture, texture: gl.Texture,
texture_color: gl.Texture, texture_color: gl.Texture,
custom_programs: []const CustomProgram, custom: ?custom.State,
pub fn init( pub fn init(
alloc: Allocator, alloc: Allocator,
@ -1557,7 +1570,8 @@ const GLState = struct {
const arena_alloc = arena.allocator(); const arena_alloc = arena.allocator();
// Load our custom shaders // Load our custom shaders
const custom_shaders: []const [:0]const u8 = shadertoy.loadFromFiles( const custom_state: ?custom.State = custom: {
const shaders: []const [:0]const u8 = shadertoy.loadFromFiles(
arena_alloc, arena_alloc,
config.custom_shaders.items, config.custom_shaders.items,
.glsl, .glsl,
@ -1565,13 +1579,16 @@ const GLState = struct {
log.warn("error loading custom shaders err={}", .{err}); log.warn("error loading custom shaders err={}", .{err});
break :err &.{}; break :err &.{};
}; };
if (shaders.len == 0) break :custom null;
// Create our custom programs break :custom custom.State.init(
const custom_programs = try CustomProgram.createList(alloc, custom_shaders); alloc,
errdefer { shaders,
for (custom_programs) |p| p.deinit(); ) catch |err| err: {
alloc.free(custom_programs); log.warn("error initializing custom shaders err={}", .{err});
} break :err null;
};
};
// Blending for text. We use GL_ONE here because we should be using // Blending for text. We use GL_ONE here because we should be using
// premultiplied alpha for all our colors in our fragment shaders. // premultiplied alpha for all our colors in our fragment shaders.
@ -1628,15 +1645,14 @@ const GLState = struct {
return .{ return .{
.cell_program = cell_program, .cell_program = cell_program,
.custom_programs = custom_programs,
.texture = tex, .texture = tex,
.texture_color = tex_color, .texture_color = tex_color,
.custom = custom_state,
}; };
} }
pub fn deinit(self: *GLState, alloc: Allocator) void { pub fn deinit(self: *GLState, alloc: Allocator) void {
for (self.custom_programs) |p| p.deinit(); if (self.custom) |v| v.deinit(alloc);
alloc.free(self.custom_programs);
self.texture.destroy(); self.texture.destroy();
self.texture_color.destroy(); self.texture_color.destroy();
self.cell_program.deinit(); self.cell_program.deinit();

View File

@ -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();
}
};

View File

@ -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();
}
};
};