diff --git a/shaders/cell.f.glsl b/shaders/cell.f.glsl new file mode 100644 index 000000000..9541056f6 --- /dev/null +++ b/shaders/cell.f.glsl @@ -0,0 +1,8 @@ +#version 330 core + +/// The background color for this cell. +flat in vec4 bg_color; + +void main() { + gl_FragColor = bg_color; +} diff --git a/shaders/cell.v.glsl b/shaders/cell.v.glsl new file mode 100644 index 000000000..5cc0c4c01 --- /dev/null +++ b/shaders/cell.v.glsl @@ -0,0 +1,31 @@ +#version 330 core + +// The grid coordinates (x, y) where x < columns and y < rows +layout (location = 0) in vec2 grid_coord; + +// The background color for this cell in RGBA (0 to 1.0) +layout (location = 1) in vec4 bg_color_in; + +// The background color for this cell in RGBA (0 to 1.0) +flat out vec4 bg_color; + +uniform vec2 cell_dims; +uniform mat4 projection; + +void main() { + // Top-left cell coordinates converted to world space + vec2 cell_pos = cell_dims * grid_coord; + + // Turn the cell position into a vertex point depending on the + // gl_VertexID. Since we use instanced drawing, we have 4 vertices + // for each corner of the cell. We can use gl_VertexID to determine + // which one we're looking at. Using this, we can use 1 or 0 to keep + // or discard the value for the vertex. + vec2 position; + position.x = (gl_VertexID == 0 || gl_VertexID == 1) ? 1. : 0.; + position.y = (gl_VertexID == 0 || gl_VertexID == 3) ? 0. : 1.; + cell_pos = cell_pos + cell_dims * position; + + gl_Position = projection * vec4(cell_pos, 1.0, 1.0); + bg_color = bg_color_in; +} diff --git a/src/App.zig b/src/App.zig index 5bff4d645..4838f1ca8 100644 --- a/src/App.zig +++ b/src/App.zig @@ -8,6 +8,7 @@ const Allocator = std.mem.Allocator; const glfw = @import("glfw"); const gl = @import("opengl.zig"); const TextRenderer = @import("TextRenderer.zig"); +const Grid = @import("Grid.zig"); const log = std.log; @@ -16,6 +17,7 @@ alloc: Allocator, window: glfw.Window, text: TextRenderer, +grid: Grid, /// Initialize the main app instance. This creates the main window, sets /// up the renderer state, compiles the shaders, etc. This is the primary @@ -43,14 +45,21 @@ pub fn init(alloc: Allocator) !App { gl.glad.versionMinor(version), }); - // Blending for text + // Culling, probably not necessary. We have to change the winding + // order since our 0,0 is top-left. gl.c.glEnable(gl.c.GL_CULL_FACE); + gl.c.glFrontFace(gl.c.GL_CW); + + // Blending for text gl.c.glEnable(gl.c.GL_BLEND); gl.c.glBlendFunc(gl.c.GL_SRC_ALPHA, gl.c.GL_ONE_MINUS_SRC_ALPHA); // Setup our text renderer var texter = try TextRenderer.init(alloc); - errdefer texter.deinit(); + errdefer texter.deinit(alloc); + + const grid = try Grid.init(alloc); + try grid.setScreenSize(.{ .width = 3000, .height = 1666 }); window.setSizeCallback((struct { fn callback(_: glfw.Window, width: i32, height: i32) void { @@ -63,6 +72,7 @@ pub fn init(alloc: Allocator) !App { .alloc = alloc, .window = window, .text = texter, + .grid = grid, }; } @@ -78,8 +88,8 @@ pub fn run(self: App) !void { gl.clearColor(0.2, 0.3, 0.3, 1.0); gl.clear(gl.c.GL_COLOR_BUFFER_BIT); - try self.text.render("sh $ /bin/bash -c \"echo hello\"", 25.0, 25.0, .{ 0.5, 0.8, 0.2 }); - //try self.text.render("hi", 25.0, 25.0, .{ 0.5, 0.8, 0.2 }); + try self.grid.render(); + //try self.text.render("sh $ /bin/bash -c \"echo hello\"", 25.0, 25.0, .{ 0.5, 0.8, 0.2 }); try self.window.swapBuffers(); try glfw.waitEvents(); diff --git a/src/Grid.zig b/src/Grid.zig new file mode 100644 index 000000000..e01b6dcb5 --- /dev/null +++ b/src/Grid.zig @@ -0,0 +1,165 @@ +//! Represents a single terminal grid. +const Grid = @This(); + +const std = @import("std"); +const assert = std.debug.assert; +const Allocator = std.mem.Allocator; +const Atlas = @import("Atlas.zig"); +const FontAtlas = @import("FontAtlas.zig"); +const gl = @import("opengl.zig"); +const gb = @import("gb_math.zig"); + +const log = std.log.scoped(.grid); + +/// The dimensions of a single "cell" in the terminal grid. +/// +/// The dimensions are dependent on the current loaded set of font glyphs. +/// We calculate the width based on the widest character and the height based +/// on the height requirement for an underscore (the "lowest" -- visually -- +/// character). +/// +/// The units for the width and height are in world space. They have to +/// be normalized using the screen projection. +/// +/// TODO(mitchellh): we should recalculate cell dimensions when new glyphs +/// are loaded. +const CellDim = struct { + width: f32, + height: f32, +}; + +/// The dimensions of the screen that the grid is rendered to. This is the +/// terminal screen, so it is likely a subset of the window size. The dimensions +/// should be in pixels. +const ScreenDim = struct { + width: i32, + height: i32, +}; + +/// Current cell dimensions for this grid. +cell_dims: CellDim, + +/// Shader program for cell rendering. +program: gl.Program, + +pub fn init(alloc: Allocator) !Grid { + // Initialize our font atlas. We will initially populate the + // font atlas with all the visible ASCII characters since they are common. + var atlas = try Atlas.init(alloc, 512); + errdefer atlas.deinit(alloc); + var font = try FontAtlas.init(atlas); + errdefer font.deinit(alloc); + try font.loadFaceFromMemory(face_ttf, 30); + + // Load all visible ASCII characters and build our cell width based on + // the widest character that we see. + const cell_width: f32 = cell_width: { + var cell_width: f32 = 0; + var i: u8 = 32; + while (i <= 126) : (i += 1) { + const glyph = try font.addGlyph(alloc, i); + if (glyph.advance_x > cell_width) { + cell_width = @ceil(glyph.advance_x); + } + } + + break :cell_width cell_width; + }; + + // The cell height is the vertical height required to render underscore + // '_' which should live at the bottom of a cell. + const cell_height: f32 = cell_height: { + // TODO(render): kitty does a calculation based on other font + // metrics that we probably want to research more. For now, this is + // fine. + assert(font.ft_face != null); + const glyph = font.getGlyph('_').?; + var res: i32 = font.ft_face.*.ascender >> 6; + res -= glyph.offset_y; + res += @intCast(i32, glyph.height); + break :cell_height @intToFloat(f32, res); + }; + log.debug("cell dimensions w={d} h={d}", .{ cell_width, cell_height }); + + // Create our shader + const program = try gl.Program.createVF( + @embedFile("../shaders/cell.v.glsl"), + @embedFile("../shaders/cell.f.glsl"), + ); + + // Set our cell dimensions + const pbind = try program.use(); + defer pbind.unbind(); + try program.setUniform("cell_dims", @Vector(2, f32){ cell_width, cell_height }); + + return Grid{ + .cell_dims = .{ .width = cell_width, .height = cell_height }, + .program = program, + }; +} + +/// Set the screen size for rendering. This will update the projection +/// used for the shader so that the scaling of the grid is correct. +pub fn setScreenSize(self: Grid, dim: ScreenDim) !void { + // Create a 2D orthographic projection matrix with the full width/height. + var projection: gb.gbMat4 = undefined; + gb.gb_mat4_ortho2d( + &projection, + 0, + @intToFloat(f32, dim.width), + @intToFloat(f32, dim.height), + 0, + ); + + // Update the projection uniform within our shader + const bind = try self.program.use(); + defer bind.unbind(); + try self.program.setUniform("projection", projection); + + log.debug("screen size w={d} h={d}", .{ dim.width, dim.height }); +} + +pub fn render(self: Grid) !void { + const pbind = try self.program.use(); + defer pbind.unbind(); + + // Setup our VAO + const vao = try gl.VertexArray.create(); + defer vao.destroy(); + try vao.bind(); + + // Element buffer (EBO) + const ebo = try gl.Buffer.create(); + defer ebo.destroy(); + var ebobinding = try ebo.bind(.ElementArrayBuffer); + defer ebobinding.unbind(); + try ebobinding.setData([6]u32{ + 0, 1, 3, + 1, 2, 3, + }, .StaticDraw); + + // Vertex buffer (VBO) + const vbo = try gl.Buffer.create(); + defer vbo.destroy(); + var binding = try vbo.bind(.ArrayBuffer); + defer binding.unbind(); + try binding.setData([_][6]f32{ + .{ 0, 0, 1, 0, 0, 1 }, + .{ 1, 0, 0, 1, 0, 1 }, + .{ 2, 0, 0, 0, 1, 1 }, + }, .StaticDraw); + try binding.attribute(0, 2, [6]f32, 0); + try binding.attribute(1, 4, [6]f32, 2); + try binding.attributeDivisor(0, 1); + try binding.attributeDivisor(1, 1); + + try gl.drawElementsInstanced( + gl.c.GL_TRIANGLES, + 6, + gl.c.GL_UNSIGNED_INT, + 3, + ); + try gl.VertexArray.unbind(); +} + +const face_ttf = @embedFile("../fonts/FiraCode-Regular.ttf"); diff --git a/src/TextRenderer.zig b/src/TextRenderer.zig index 2d0770cfe..1119403a6 100644 --- a/src/TextRenderer.zig +++ b/src/TextRenderer.zig @@ -99,6 +99,7 @@ pub fn init(alloc: std.mem.Allocator) !TextRenderer { // Update the initialize size so we have some projection. We // expect this will get updated almost immediately. try res.setScreenSize(3000, 1666); + //try res.setScreenSize(1432, 874); return res; } diff --git a/src/opengl/Buffer.zig b/src/opengl/Buffer.zig index afa9274b6..d5fffac62 100644 --- a/src/opengl/Buffer.zig +++ b/src/opengl/Buffer.zig @@ -150,6 +150,12 @@ pub const Binding = struct { try b.enableAttribArray(idx); } + /// VertexAttribDivisor + pub fn attributeDivisor(_: Binding, idx: c.GLuint, divisor: c.GLuint) !void { + c.glVertexAttribDivisor(idx, divisor); + try errors.getError(); + } + pub inline fn attributeAdvanced( _: Binding, idx: c.GLuint, diff --git a/src/opengl/Program.zig b/src/opengl/Program.zig index 4f22f7efc..0e961b4c7 100644 --- a/src/opengl/Program.zig +++ b/src/opengl/Program.zig @@ -82,6 +82,7 @@ pub inline fn setUniform(p: Program, n: [:0]const u8, value: anytype) !void { // Perform the correct call depending on the type of the value. switch (@TypeOf(value)) { + @Vector(2, f32) => c.glUniform2f(loc, value[0], value[1]), @Vector(3, f32) => c.glUniform3f(loc, value[0], value[1], value[2]), @Vector(4, f32) => c.glUniform4f(loc, value[0], value[1], value[2], value[3]), gb.gbMat4 => c.glUniformMatrix4fv( diff --git a/src/opengl/draw.zig b/src/opengl/draw.zig index e66944375..bcb573812 100644 --- a/src/opengl/draw.zig +++ b/src/opengl/draw.zig @@ -20,6 +20,16 @@ pub fn drawElements(mode: c.GLenum, count: c.GLsizei, typ: c.GLenum, offset: usi try errors.getError(); } +pub fn drawElementsInstanced( + mode: c.GLenum, + count: c.GLsizei, + typ: c.GLenum, + primcount: usize, +) !void { + c.glDrawElementsInstanced(mode, count, typ, null, @intCast(c.GLsizei, primcount)); + try errors.getError(); +} + pub fn viewport(x: c.GLint, y: c.GLint, width: c.GLsizei, height: c.GLsizei) !void { c.glViewport(x, y, width, height); }