diff --git a/build.zig b/build.zig index 6146f93f5..cb60aabb7 100644 --- a/build.zig +++ b/build.zig @@ -663,6 +663,7 @@ fn addDeps( .target = step.target, .optimize = step.optimize, }); + const opengl_dep = b.dependency("opengl", .{}); const pixman_dep = b.dependency("pixman", .{ .target = step.target, .optimize = step.optimize, @@ -730,6 +731,7 @@ fn addDeps( step.addModule("spirv_cross", spirv_cross_dep.module("spirv_cross")); step.addModule("harfbuzz", harfbuzz_dep.module("harfbuzz")); step.addModule("xev", libxev_dep.module("xev")); + step.addModule("opengl", opengl_dep.module("opengl")); step.addModule("pixman", pixman_dep.module("pixman")); step.addModule("ziglyph", ziglyph_dep.module("ziglyph")); diff --git a/build.zig.zon b/build.zig.zon index 580c8593e..309df74d1 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -32,6 +32,7 @@ .harfbuzz = .{ .path = "./pkg/harfbuzz" }, .libpng = .{ .path = "./pkg/libpng" }, .macos = .{ .path = "./pkg/macos" }, + .opengl = .{ .path = "./pkg/opengl" }, .pixman = .{ .path = "./pkg/pixman" }, .tracy = .{ .path = "./pkg/tracy" }, .zlib = .{ .path = "./pkg/zlib" }, diff --git a/pkg/opengl/Buffer.zig b/pkg/opengl/Buffer.zig new file mode 100644 index 000000000..b794ca4f0 --- /dev/null +++ b/pkg/opengl/Buffer.zig @@ -0,0 +1,218 @@ +const Buffer = @This(); + +const std = @import("std"); +const c = @import("c.zig"); +const errors = @import("errors.zig"); +const glad = @import("glad.zig"); + +id: c.GLuint, + +/// Enum for possible binding targets. +pub const Target = enum(c_uint) { + ArrayBuffer = c.GL_ARRAY_BUFFER, + ElementArrayBuffer = c.GL_ELEMENT_ARRAY_BUFFER, + _, +}; + +/// Enum for possible buffer usages. +pub const Usage = enum(c_uint) { + StreamDraw = c.GL_STREAM_DRAW, + StreamRead = c.GL_STREAM_READ, + StreamCopy = c.GL_STREAM_COPY, + StaticDraw = c.GL_STATIC_DRAW, + StaticRead = c.GL_STATIC_READ, + StaticCopy = c.GL_STATIC_COPY, + DynamicDraw = c.GL_DYNAMIC_DRAW, + DynamicRead = c.GL_DYNAMIC_READ, + DynamicCopy = c.GL_DYNAMIC_COPY, + _, +}; + +/// Binding is a bound buffer. By using this for functions that operate +/// on bound buffers, you can easily defer unbinding and in safety-enabled +/// modes verify that unbound buffers are never accessed. +pub const Binding = struct { + target: Target, + + /// Sets the data of this bound buffer. The data can be any array-like + /// type. The size of the data is automatically determined based on the type. + pub inline fn setData( + b: Binding, + data: anytype, + usage: Usage, + ) !void { + const info = dataInfo(&data); + glad.context.BufferData.?(@intFromEnum(b.target), info.size, info.ptr, @intFromEnum(usage)); + try errors.getError(); + } + + /// Sets the data of this bound buffer. The data can be any array-like + /// type. The size of the data is automatically determined based on the type. + pub inline fn setSubData( + b: Binding, + offset: usize, + data: anytype, + ) !void { + const info = dataInfo(data); + glad.context.BufferSubData.?(@intFromEnum(b.target), @intCast(offset), info.size, info.ptr); + try errors.getError(); + } + + /// Sets the buffer data with a null buffer that is expected to be + /// filled in the future using subData. This requires the type just so + /// we can setup the data size. + pub inline fn setDataNull( + b: Binding, + comptime T: type, + usage: Usage, + ) !void { + glad.context.BufferData.?(@intFromEnum(b.target), @sizeOf(T), null, @intFromEnum(usage)); + try errors.getError(); + } + + /// Same as setDataNull but lets you manually specify the buffer size. + pub inline fn setDataNullManual( + b: Binding, + size: usize, + usage: Usage, + ) !void { + glad.context.BufferData.?(@intFromEnum(b.target), @intCast(size), null, @intFromEnum(usage)); + try errors.getError(); + } + + fn dataInfo(data: anytype) struct { + size: isize, + ptr: *const anyopaque, + } { + return switch (@typeInfo(@TypeOf(data))) { + .Pointer => |ptr| switch (ptr.size) { + .One => .{ + .size = @sizeOf(ptr.child) * data.len, + .ptr = data, + }, + .Slice => .{ + .size = @intCast(@sizeOf(ptr.child) * data.len), + .ptr = data.ptr, + }, + else => { + std.log.err("invalid buffer data pointer size: {}", .{ptr.size}); + unreachable; + }, + }, + else => { + std.log.err("invalid buffer data type: {s}", .{@tagName(@typeInfo(@TypeOf(data)))}); + unreachable; + }, + }; + } + + pub inline fn enableAttribArray(_: Binding, idx: c.GLuint) !void { + glad.context.EnableVertexAttribArray.?(idx); + } + + /// Shorthand for vertexAttribPointer that is specialized towards the + /// common use case of specifying an array of homogeneous types that + /// don't need normalization. This also enables the attribute at idx. + pub fn attribute( + b: Binding, + idx: c.GLuint, + size: c.GLint, + comptime T: type, + offset: usize, + ) !void { + const info: struct { + // Type of the each component in the array. + typ: c.GLenum, + + // The byte offset between each full set of attributes. + stride: c.GLsizei, + + // The size of each component used in calculating the offset. + offset: usize, + } = switch (@typeInfo(T)) { + .Array => |ary| .{ + .typ = switch (ary.child) { + f32 => c.GL_FLOAT, + else => @compileError("unsupported array child type"), + }, + .offset = @sizeOf(ary.child), + .stride = @sizeOf(T), + }, + else => @compileError("unsupported type"), + }; + + try b.attributeAdvanced( + idx, + size, + info.typ, + false, + info.stride, + offset * info.offset, + ); + try b.enableAttribArray(idx); + } + + /// VertexAttribDivisor + pub fn attributeDivisor(_: Binding, idx: c.GLuint, divisor: c.GLuint) !void { + glad.context.VertexAttribDivisor.?(idx, divisor); + try errors.getError(); + } + + pub inline fn attributeAdvanced( + _: Binding, + idx: c.GLuint, + size: c.GLint, + typ: c.GLenum, + normalized: bool, + stride: c.GLsizei, + offset: usize, + ) !void { + const normalized_c: c.GLboolean = if (normalized) c.GL_TRUE else c.GL_FALSE; + const offsetPtr = if (offset > 0) + @as(*const anyopaque, @ptrFromInt(offset)) + else + null; + + glad.context.VertexAttribPointer.?(idx, size, typ, normalized_c, stride, offsetPtr); + try errors.getError(); + } + + pub inline fn attributeIAdvanced( + _: Binding, + idx: c.GLuint, + size: c.GLint, + typ: c.GLenum, + stride: c.GLsizei, + offset: usize, + ) !void { + const offsetPtr = if (offset > 0) + @as(*const anyopaque, @ptrFromInt(offset)) + else + null; + + glad.context.VertexAttribIPointer.?(idx, size, typ, stride, offsetPtr); + try errors.getError(); + } + + pub inline fn unbind(b: *Binding) void { + glad.context.BindBuffer.?(@intFromEnum(b.target), 0); + b.* = undefined; + } +}; + +/// Create a single buffer. +pub inline fn create() !Buffer { + var vbo: c.GLuint = undefined; + glad.context.GenBuffers.?(1, &vbo); + return Buffer{ .id = vbo }; +} + +/// glBindBuffer +pub inline fn bind(v: Buffer, target: Target) !Binding { + glad.context.BindBuffer.?(@intFromEnum(target), v.id); + return Binding{ .target = target }; +} + +pub inline fn destroy(v: Buffer) void { + glad.context.DeleteBuffers.?(1, &v.id); +} diff --git a/pkg/opengl/Program.zig b/pkg/opengl/Program.zig new file mode 100644 index 000000000..d266bd226 --- /dev/null +++ b/pkg/opengl/Program.zig @@ -0,0 +1,128 @@ +const Program = @This(); + +const std = @import("std"); +const assert = std.debug.assert; +const log = std.log.scoped(.opengl); + +const c = @import("c.zig"); +const Shader = @import("Shader.zig"); +const errors = @import("errors.zig"); +const glad = @import("glad.zig"); + +id: c.GLuint, + +const Binding = struct { + pub inline fn unbind(_: Binding) void { + glad.context.UseProgram.?(0); + } +}; + +pub inline fn create() !Program { + const id = glad.context.CreateProgram.?(); + if (id == 0) try errors.mustError(); + + log.debug("program created id={}", .{id}); + return Program{ .id = id }; +} + +/// Create a program from a vertex and fragment shader source. This will +/// compile and link the vertex and fragment shader. +pub inline fn createVF(vsrc: [:0]const u8, fsrc: [:0]const u8) !Program { + const vs = try Shader.create(c.GL_VERTEX_SHADER); + try vs.setSourceAndCompile(vsrc); + defer vs.destroy(); + + const fs = try Shader.create(c.GL_FRAGMENT_SHADER); + try fs.setSourceAndCompile(fsrc); + defer fs.destroy(); + + const p = try create(); + try p.attachShader(vs); + try p.attachShader(fs); + try p.link(); + + return p; +} + +pub inline fn attachShader(p: Program, s: Shader) !void { + glad.context.AttachShader.?(p.id, s.id); + try errors.getError(); +} + +pub inline fn link(p: Program) !void { + glad.context.LinkProgram.?(p.id); + + // Check if linking succeeded + var success: c_int = undefined; + glad.context.GetProgramiv.?(p.id, c.GL_LINK_STATUS, &success); + if (success == c.GL_TRUE) { + log.debug("program linked id={}", .{p.id}); + return; + } + + log.err("program link failure id={} message={s}", .{ + p.id, + std.mem.sliceTo(&p.getInfoLog(), 0), + }); + return error.CompileFailed; +} + +pub inline fn use(p: Program) !Binding { + glad.context.UseProgram.?(p.id); + try errors.getError(); + return Binding{}; +} + +/// Requires the program is currently in use. +pub inline fn setUniform( + p: Program, + n: [:0]const u8, + value: anytype, +) !void { + const loc = glad.context.GetUniformLocation.?( + p.id, + @ptrCast(n.ptr), + ); + if (loc < 0) { + return error.UniformNameInvalid; + } + try errors.getError(); + + // Perform the correct call depending on the type of the value. + switch (@TypeOf(value)) { + comptime_int => glad.context.Uniform1i.?(loc, value), + f32 => glad.context.Uniform1f.?(loc, value), + @Vector(2, f32) => glad.context.Uniform2f.?(loc, value[0], value[1]), + @Vector(3, f32) => glad.context.Uniform3f.?(loc, value[0], value[1], value[2]), + @Vector(4, f32) => glad.context.Uniform4f.?(loc, value[0], value[1], value[2], value[3]), + [4]@Vector(4, f32) => glad.context.UniformMatrix4fv.?( + loc, + 1, + c.GL_FALSE, + @ptrCast(&value), + ), + else => { + log.warn("unsupported uniform type {}", .{@TypeOf(value)}); + unreachable; + }, + } + try errors.getError(); +} + +/// getInfoLog returns the info log for this program. This attempts to +/// keep the log fully stack allocated and is therefore limited to a max +/// amount of elements. +// +// NOTE(mitchellh): we can add a dynamic version that uses an allocator +// if we ever need it. +pub inline fn getInfoLog(s: Program) [512]u8 { + var msg: [512]u8 = undefined; + glad.context.GetProgramInfoLog.?(s.id, msg.len, null, &msg); + return msg; +} + +pub inline fn destroy(p: Program) void { + assert(p.id != 0); + glad.context.DeleteProgram.?(p.id); + log.debug("program destroyed id={}", .{p.id}); +} diff --git a/pkg/opengl/Shader.zig b/pkg/opengl/Shader.zig new file mode 100644 index 000000000..beaae9e94 --- /dev/null +++ b/pkg/opengl/Shader.zig @@ -0,0 +1,56 @@ +const Shader = @This(); + +const std = @import("std"); +const assert = std.debug.assert; +const log = std.log.scoped(.opengl); + +const c = @import("c.zig"); +const errors = @import("errors.zig"); +const glad = @import("glad.zig"); + +id: c.GLuint, + +pub inline fn create(typ: c.GLenum) errors.Error!Shader { + const id = glad.context.CreateShader.?(typ); + if (id == 0) { + try errors.mustError(); + unreachable; + } + + log.debug("shader created id={}", .{id}); + return Shader{ .id = id }; +} + +/// Set the source and compile a shader. +pub inline fn setSourceAndCompile(s: Shader, source: [:0]const u8) !void { + glad.context.ShaderSource.?(s.id, 1, &@as([*c]const u8, @ptrCast(source)), null); + glad.context.CompileShader.?(s.id); + + // Check if compilation succeeded + var success: c_int = undefined; + glad.context.GetShaderiv.?(s.id, c.GL_COMPILE_STATUS, &success); + if (success == c.GL_TRUE) return; + log.err("shader compilation failure id={} message={s}", .{ + s.id, + std.mem.sliceTo(&s.getInfoLog(), 0), + }); + return error.CompileFailed; +} + +/// getInfoLog returns the info log for this shader. This attempts to +/// keep the log fully stack allocated and is therefore limited to a max +/// amount of elements. +// +// NOTE(mitchellh): we can add a dynamic version that uses an allocator +// if we ever need it. +pub inline fn getInfoLog(s: Shader) [512]u8 { + var msg: [512]u8 = undefined; + glad.context.GetShaderInfoLog.?(s.id, msg.len, null, &msg); + return msg; +} + +pub inline fn destroy(s: Shader) void { + assert(s.id != 0); + glad.context.DeleteShader.?(s.id); + log.debug("shader destroyed id={}", .{s.id}); +} diff --git a/pkg/opengl/Texture.zig b/pkg/opengl/Texture.zig new file mode 100644 index 000000000..91a65b565 --- /dev/null +++ b/pkg/opengl/Texture.zig @@ -0,0 +1,163 @@ +const Texture = @This(); + +const std = @import("std"); +const c = @import("c.zig"); +const errors = @import("errors.zig"); +const glad = @import("glad.zig"); + +id: c.GLuint, + +pub inline fn active(target: c.GLenum) !void { + glad.context.ActiveTexture.?(target); + try errors.getError(); +} + +/// Enun for possible texture binding targets. +pub const Target = enum(c_uint) { + @"1D" = c.GL_TEXTURE_1D, + @"2D" = c.GL_TEXTURE_2D, + @"3D" = c.GL_TEXTURE_3D, + @"1DArray" = c.GL_TEXTURE_1D_ARRAY, + @"2DArray" = c.GL_TEXTURE_2D_ARRAY, + Rectangle = c.GL_TEXTURE_RECTANGLE, + CubeMap = c.GL_TEXTURE_CUBE_MAP, + Buffer = c.GL_TEXTURE_BUFFER, + @"2DMultisample" = c.GL_TEXTURE_2D_MULTISAMPLE, + @"2DMultisampleArray" = c.GL_TEXTURE_2D_MULTISAMPLE_ARRAY, +}; + +/// Enum for possible texture parameters. +pub const Parameter = enum(c_uint) { + BaseLevel = c.GL_TEXTURE_BASE_LEVEL, + CompareFunc = c.GL_TEXTURE_COMPARE_FUNC, + CompareMode = c.GL_TEXTURE_COMPARE_MODE, + LodBias = c.GL_TEXTURE_LOD_BIAS, + MinFilter = c.GL_TEXTURE_MIN_FILTER, + MagFilter = c.GL_TEXTURE_MAG_FILTER, + MinLod = c.GL_TEXTURE_MIN_LOD, + MaxLod = c.GL_TEXTURE_MAX_LOD, + MaxLevel = c.GL_TEXTURE_MAX_LEVEL, + SwizzleR = c.GL_TEXTURE_SWIZZLE_R, + SwizzleG = c.GL_TEXTURE_SWIZZLE_G, + SwizzleB = c.GL_TEXTURE_SWIZZLE_B, + SwizzleA = c.GL_TEXTURE_SWIZZLE_A, + WrapS = c.GL_TEXTURE_WRAP_S, + WrapT = c.GL_TEXTURE_WRAP_T, + WrapR = c.GL_TEXTURE_WRAP_R, +}; + +/// Internal format enum for texture images. +pub const InternalFormat = enum(c_int) { + Red = c.GL_RED, + RGBA = c.GL_RGBA, + + // There are so many more that I haven't filled in. + _, +}; + +/// Format for texture images +pub const Format = enum(c_uint) { + Red = c.GL_RED, + BGRA = c.GL_BGRA, + + // There are so many more that I haven't filled in. + _, +}; + +/// Data type for texture images. +pub const DataType = enum(c_uint) { + UnsignedByte = c.GL_UNSIGNED_BYTE, + + // There are so many more that I haven't filled in. + _, +}; + +pub const Binding = struct { + target: Target, + + pub inline fn unbind(b: *Binding) void { + glad.context.BindTexture.?(@intFromEnum(b.target), 0); + b.* = undefined; + } + + pub fn generateMipmap(b: Binding) void { + glad.context.GenerateMipmap.?(@intFromEnum(b.target)); + } + + pub fn parameter(b: Binding, name: Parameter, value: anytype) !void { + switch (@TypeOf(value)) { + c.GLint => glad.context.TexParameteri.?( + @intFromEnum(b.target), + @intFromEnum(name), + value, + ), + else => unreachable, + } + } + + pub fn image2D( + b: Binding, + level: c.GLint, + internal_format: InternalFormat, + width: c.GLsizei, + height: c.GLsizei, + border: c.GLint, + format: Format, + typ: DataType, + data: ?*const anyopaque, + ) !void { + glad.context.TexImage2D.?( + @intFromEnum(b.target), + level, + @intFromEnum(internal_format), + width, + height, + border, + @intFromEnum(format), + @intFromEnum(typ), + data, + ); + } + + pub fn subImage2D( + b: Binding, + level: c.GLint, + xoffset: c.GLint, + yoffset: c.GLint, + width: c.GLsizei, + height: c.GLsizei, + format: Format, + typ: DataType, + data: ?*const anyopaque, + ) !void { + glad.context.TexSubImage2D.?( + @intFromEnum(b.target), + level, + xoffset, + yoffset, + width, + height, + @intFromEnum(format), + @intFromEnum(typ), + data, + ); + } +}; + +/// Create a single texture. +pub inline fn create() !Texture { + var id: c.GLuint = undefined; + glad.context.GenTextures.?(1, &id); + return Texture{ .id = id }; +} + +/// glBindTexture +pub inline fn bind(v: Texture, target: Target) !Binding { + glad.context.BindTexture.?(@intFromEnum(target), v.id); + try errors.getError(); + return Binding{ .target = target }; +} + +pub inline fn destroy(v: Texture) void { + glad.context.DeleteTextures.?(1, &v.id); +} diff --git a/pkg/opengl/VertexArray.zig b/pkg/opengl/VertexArray.zig new file mode 100644 index 000000000..b86794042 --- /dev/null +++ b/pkg/opengl/VertexArray.zig @@ -0,0 +1,29 @@ +const VertexArray = @This(); + +const c = @import("c.zig"); +const glad = @import("glad.zig"); +const errors = @import("errors.zig"); + +id: c.GLuint, + +/// Create a single vertex array object. +pub inline fn create() !VertexArray { + var vao: c.GLuint = undefined; + glad.context.GenVertexArrays.?(1, &vao); + return VertexArray{ .id = vao }; +} + +// Unbind any active vertex array. +pub inline fn unbind() !void { + glad.context.BindVertexArray.?(0); +} + +/// glBindVertexArray +pub inline fn bind(v: VertexArray) !void { + glad.context.BindVertexArray.?(v.id); + try errors.getError(); +} + +pub inline fn destroy(v: VertexArray) void { + glad.context.DeleteVertexArrays.?(1, &v.id); +} diff --git a/pkg/opengl/build.zig b/pkg/opengl/build.zig new file mode 100644 index 000000000..34e5a8ab1 --- /dev/null +++ b/pkg/opengl/build.zig @@ -0,0 +1,5 @@ +const std = @import("std"); + +pub fn build(b: *std.Build) !void { + _ = b.addModule("opengl", .{ .source_file = .{ .path = "main.zig" } }); +} diff --git a/pkg/opengl/c.zig b/pkg/opengl/c.zig new file mode 100644 index 000000000..8f4a0f22f --- /dev/null +++ b/pkg/opengl/c.zig @@ -0,0 +1,3 @@ +pub usingnamespace @cImport({ + @cInclude("glad/gl.h"); +}); diff --git a/pkg/opengl/draw.zig b/pkg/opengl/draw.zig new file mode 100644 index 000000000..ea6b63103 --- /dev/null +++ b/pkg/opengl/draw.zig @@ -0,0 +1,59 @@ +const c = @import("c.zig"); +const errors = @import("errors.zig"); +const glad = @import("glad.zig"); + +pub fn clearColor(r: f32, g: f32, b: f32, a: f32) void { + glad.context.ClearColor.?(r, g, b, a); +} + +pub fn clear(mask: c.GLbitfield) void { + glad.context.Clear.?(mask); +} + +pub fn drawArrays(mode: c.GLenum, first: c.GLint, count: c.GLsizei) !void { + glad.context.DrawArrays.?(mode, first, count); + try errors.getError(); +} + +pub fn drawElements(mode: c.GLenum, count: c.GLsizei, typ: c.GLenum, offset: usize) !void { + const offsetPtr = if (offset == 0) null else @as(*const anyopaque, @ptrFromInt(offset)); + glad.context.DrawElements.?(mode, count, typ, offsetPtr); + try errors.getError(); +} + +pub fn drawElementsInstanced( + mode: c.GLenum, + count: c.GLsizei, + typ: c.GLenum, + primcount: usize, +) !void { + glad.context.DrawElementsInstanced.?(mode, count, typ, null, @intCast(primcount)); + try errors.getError(); +} + +pub fn enable(cap: c.GLenum) !void { + glad.context.Enable.?(cap); + try errors.getError(); +} + +pub fn frontFace(mode: c.GLenum) !void { + glad.context.FrontFace.?(mode); + try errors.getError(); +} + +pub fn blendFunc(sfactor: c.GLenum, dfactor: c.GLenum) !void { + glad.context.BlendFunc.?(sfactor, dfactor); + try errors.getError(); +} + +pub fn viewport(x: c.GLint, y: c.GLint, width: c.GLsizei, height: c.GLsizei) !void { + glad.context.Viewport.?(x, y, width, height); +} + +pub fn pixelStore(mode: c.GLenum, value: anytype) !void { + switch (@typeInfo(@TypeOf(value))) { + .ComptimeInt, .Int => glad.context.PixelStorei.?(mode, value), + else => unreachable, + } + try errors.getError(); +} diff --git a/pkg/opengl/errors.zig b/pkg/opengl/errors.zig new file mode 100644 index 000000000..86639a53a --- /dev/null +++ b/pkg/opengl/errors.zig @@ -0,0 +1,33 @@ +const std = @import("std"); +const c = @import("c.zig"); +const glad = @import("glad.zig"); + +pub const Error = error{ + InvalidEnum, + InvalidValue, + InvalidOperation, + InvalidFramebufferOperation, + OutOfMemory, + + Unknown, +}; + +/// getError returns the error (if any) from the last OpenGL operation. +pub fn getError() Error!void { + return switch (glad.context.GetError.?()) { + c.GL_NO_ERROR => {}, + c.GL_INVALID_ENUM => Error.InvalidEnum, + c.GL_INVALID_VALUE => Error.InvalidValue, + c.GL_INVALID_OPERATION => Error.InvalidOperation, + c.GL_INVALID_FRAMEBUFFER_OPERATION => Error.InvalidFramebufferOperation, + c.GL_OUT_OF_MEMORY => Error.OutOfMemory, + else => Error.Unknown, + }; +} + +/// mustError just calls getError but always results in an error being returned. +/// If getError has no error, then Unknown is returned. +pub fn mustError() Error!void { + try getError(); + return Error.Unknown; +} diff --git a/pkg/opengl/extensions.zig b/pkg/opengl/extensions.zig new file mode 100644 index 000000000..ca8a4973d --- /dev/null +++ b/pkg/opengl/extensions.zig @@ -0,0 +1,32 @@ +const std = @import("std"); +const c = @import("c.zig"); +const errors = @import("errors.zig"); +const glad = @import("glad.zig"); + +/// Returns the number of extensions. +pub fn len() !u32 { + var n: c.GLint = undefined; + glad.context.GetIntegerv.?(c.GL_NUM_EXTENSIONS, &n); + try errors.getError(); + return @intCast(n); +} + +/// Returns an iterator for the extensions. +pub fn iterator() !Iterator { + return Iterator{ .len = try len() }; +} + +/// Iterator for the available extensions. +pub const Iterator = struct { + /// The total number of extensions. + len: c.GLuint = 0, + i: c.GLuint = 0, + + pub fn next(self: *Iterator) !?[]const u8 { + if (self.i >= self.len) return null; + const res = glad.context.GetStringi.?(c.GL_EXTENSIONS, self.i); + try errors.getError(); + self.i += 1; + return std.mem.sliceTo(res, 0); + } +}; diff --git a/pkg/opengl/glad.zig b/pkg/opengl/glad.zig new file mode 100644 index 000000000..4ee85c549 --- /dev/null +++ b/pkg/opengl/glad.zig @@ -0,0 +1,45 @@ +const std = @import("std"); +const c = @import("c.zig"); + +pub const Context = c.GladGLContext; + +/// This is the current context. Set this var manually prior to calling +/// any of this package's functions. I know its nasty to have a global but +/// this makes it match OpenGL API styles where it also operates on a +/// threadlocal global. +pub threadlocal var context: Context = undefined; + +/// Initialize Glad. This is guaranteed to succeed if no errors are returned. +/// The getProcAddress param is an anytype so that we can accept multiple +/// forms of the function depending on what we're interfacing with. +pub fn load(getProcAddress: anytype) !c_int { + const GlProc = *const fn () callconv(.C) void; + const GlfwFn = *const fn ([*:0]const u8) callconv(.C) ?GlProc; + + const res = switch (@TypeOf(getProcAddress)) { + // glfw + GlfwFn => c.gladLoadGLContext(&context, @ptrCast(getProcAddress)), + + // null proc address means that we are just loading the globally + // pointed gl functions + @TypeOf(null) => c.gladLoaderLoadGLContext(&context), + + // try as-is. If this introduces a compiler error, then add a new case. + else => c.gladLoadGLContext(&context, getProcAddress), + }; + if (res == 0) return error.GLInitFailed; + return res; +} + +pub fn unload() void { + c.gladLoaderUnloadGLContext(&context); + context = undefined; +} + +pub fn versionMajor(res: c_uint) c_uint { + return c.GLAD_VERSION_MAJOR(res); +} + +pub fn versionMinor(res: c_uint) c_uint { + return c.GLAD_VERSION_MINOR(res); +} diff --git a/pkg/opengl/main.zig b/pkg/opengl/main.zig new file mode 100644 index 000000000..79d32acea --- /dev/null +++ b/pkg/opengl/main.zig @@ -0,0 +1,23 @@ +//! OpenGL bindings. +//! +//! These are purpose-built for usage within this program. While they closely +//! align with the OpenGL C APIs, they aren't meant to be general purpose, +//! they aren't meant to have 100% API coverage, and they aren't meant to +//! be hyper-performant. +//! +//! For performance-intensive or unsupported aspects of OpenGL, the C +//! API is exposed via the `c` constant. +//! +//! WARNING: Lots of performance improvements that we can make with Zig +//! comptime help. I'm deferring this until later but have some fun ideas. + +pub const c = @import("c.zig"); +pub const glad = @import("glad.zig"); +pub usingnamespace @import("draw.zig"); + +pub const ext = @import("extensions.zig"); +pub const Buffer = @import("Buffer.zig"); +pub const Program = @import("Program.zig"); +pub const Shader = @import("Shader.zig"); +pub const Texture = @import("Texture.zig"); +pub const VertexArray = @import("VertexArray.zig"); diff --git a/src/renderer/OpenGL.zig b/src/renderer/OpenGL.zig index 32781a7eb..ce442ed48 100644 --- a/src/renderer/OpenGL.zig +++ b/src/renderer/OpenGL.zig @@ -7,6 +7,7 @@ const glfw = @import("glfw"); const assert = std.debug.assert; const testing = std.testing; const Allocator = std.mem.Allocator; +const ArenaAllocator = std.heap.ArenaAllocator; const apprt = @import("../apprt.zig"); const configpkg = @import("../config.zig"); const font = @import("../font/main.zig"); @@ -14,7 +15,7 @@ const imgui = @import("imgui"); const renderer = @import("../renderer.zig"); const terminal = @import("../terminal/main.zig"); const Terminal = terminal.Terminal; -const gl = @import("opengl/main.zig"); +const gl = @import("opengl"); const trace = @import("tracy").trace; const math = @import("../math.zig"); const Surface = @import("../Surface.zig"); @@ -226,8 +227,10 @@ const GPUCellMode = enum(u8) { /// configuration. This must be exported so that we don't need to /// pass around Config pointers which makes memory management a pain. pub const DerivedConfig = struct { + arena: ArenaAllocator, + font_thicken: bool, - font_features: std.ArrayList([]const u8), + font_features: std.ArrayListUnmanaged([]const u8), font_styles: font.Group.StyleStatus, cursor_color: ?terminal.color.RGB, cursor_text: ?terminal.color.RGB, @@ -238,17 +241,21 @@ pub const DerivedConfig = struct { selection_background: ?terminal.color.RGB, selection_foreground: ?terminal.color.RGB, invert_selection_fg_bg: bool, + custom_shaders: std.ArrayListUnmanaged([]const u8), pub fn init( alloc_gpa: Allocator, config: *const configpkg.Config, ) !DerivedConfig { + var arena = ArenaAllocator.init(alloc_gpa); + errdefer arena.deinit(); + const alloc = arena.allocator(); + + // Copy our shaders + const custom_shaders = try config.@"custom-shader".value.list.clone(alloc); + // Copy our font features - var font_features = features: { - var clone = try config.@"font-feature".list.clone(alloc_gpa); - break :features clone.toManaged(alloc_gpa); - }; - errdefer font_features.deinit(); + const font_features = try config.@"font-feature".list.clone(alloc); // Get our font styles var font_styles = font.Group.StyleStatus.initFill(true); @@ -287,11 +294,15 @@ pub const DerivedConfig = struct { bg.toTerminalRGB() else null, + + .custom_shaders = custom_shaders, + + .arena = arena, }; } pub fn deinit(self: *DerivedConfig) void { - self.font_features.deinit(); + self.arena.deinit(); } }; diff --git a/src/renderer/shadertoy.zig b/src/renderer/shadertoy.zig index 45c225d85..92b340e76 100644 --- a/src/renderer/shadertoy.zig +++ b/src/renderer/shadertoy.zig @@ -335,5 +335,22 @@ test "shadertoy to msl" { defer alloc.free(msl); } +test "shadertoy to glsl" { + const testing = std.testing; + const alloc = testing.allocator; + + const src = try testGlslZ(alloc, test_crt); + defer alloc.free(src); + + var spvlist = std.ArrayListAligned(u8, @alignOf(u32)).init(alloc); + defer spvlist.deinit(); + try spirvFromGlsl(spvlist.writer(), null, src); + + const glsl = try glslFromSpv(alloc, spvlist.items); + defer alloc.free(glsl); + + //log.warn("glsl={s}", .{glsl}); +} + const test_crt = @embedFile("shaders/test_shadertoy_crt.glsl"); const test_invalid = @embedFile("shaders/test_shadertoy_invalid.glsl");