renderer(opengl): implement blinking cells, make render modes a bitfield

The render modes in the vertex shader has always been... strange. It
kind of functioned like an enum and a bitfield at the same time. To
comfortably accommodate adding blink information on cells, I've
refactored it to make it into a true bitfield -- this made the logic
on the Zig side much simpler, with the tradeoff being that logic is
slightly harder to read in the shader code as GLSL does not support
implicitly casting integers after bitmasking to booleans.

The blink is currently synchronized to the cursor blinking (which is
what most other terminal emulators do), though we should be able to
have a separate cell blink timer in the future should the need appear.
This commit is contained in:
Leah Amelia Chen
2024-08-31 01:05:21 +02:00
parent 12bf107bcb
commit 1d04c52bb2
4 changed files with 73 additions and 91 deletions

View File

@ -108,6 +108,9 @@ cursor_color: ?terminal.color.RGB,
/// foreground color as the cursor color. /// foreground color as the cursor color.
cursor_invert: bool, cursor_invert: bool,
/// Whether blinking cells are currently visible. Synchronized with cursor blinking.
blink_visible: bool = true,
/// Padding options /// Padding options
padding: renderer.Options.Padding, padding: renderer.Options.Padding,
@ -701,7 +704,7 @@ pub fn updateFrame(
self: *OpenGL, self: *OpenGL,
surface: *apprt.Surface, surface: *apprt.Surface,
state: *renderer.State, state: *renderer.State,
cursor_blink_visible: bool, blink_visible: bool,
) !void { ) !void {
_ = surface; _ = surface;
@ -770,7 +773,7 @@ pub fn updateFrame(
const cursor_style = renderer.cursorStyle( const cursor_style = renderer.cursorStyle(
state, state,
self.focused, self.focused,
cursor_blink_visible, blink_visible,
); );
// Get our preedit state // Get our preedit state
@ -854,6 +857,9 @@ pub fn updateFrame(
.color_palette = state.terminal.color_palette.colors, .color_palette = state.terminal.color_palette.colors,
}; };
}; };
self.blink_visible = blink_visible;
defer { defer {
critical.screen.deinit(); critical.screen.deinit();
if (critical.preedit) |p| p.deinit(self.alloc); if (critical.preedit) |p| p.deinit(self.alloc);
@ -1279,7 +1285,7 @@ pub fn rebuildCells(
const screen_cell = row.cells(.all)[screen.cursor.x]; const screen_cell = row.cells(.all)[screen.cursor.x];
const x = screen.cursor.x - @intFromBool(screen_cell.wide == .spacer_tail); const x = screen.cursor.x - @intFromBool(screen_cell.wide == .spacer_tail);
for (self.cells.items[start_i..]) |cell| { for (self.cells.items[start_i..]) |cell| {
if (cell.grid_col == x and cell.mode.isFg()) { if (cell.grid_col == x and cell.mode.fg) {
cursor_cell = cell; cursor_cell = cell;
break; break;
} }
@ -1416,7 +1422,7 @@ pub fn rebuildCells(
_ = try self.addCursor(screen, cursor_style, cursor_color); _ = try self.addCursor(screen, cursor_style, cursor_color);
if (cursor_cell) |*cell| { if (cursor_cell) |*cell| {
if (cell.mode.isFg() and cell.mode != .fg_color) { if (cell.mode.fg and !cell.mode.fg_color) {
const cell_color = if (self.cursor_invert) blk: { const cell_color = if (self.cursor_invert) blk: {
const sty = screen.cursor.page_pin.style(screen.cursor.page_cell); const sty = screen.cursor.page_pin.style(screen.cursor.page_cell);
break :blk sty.bg(screen.cursor.page_cell, color_palette) orelse self.background_color; break :blk sty.bg(screen.cursor.page_cell, color_palette) orelse self.background_color;
@ -1436,8 +1442,8 @@ pub fn rebuildCells(
// Some debug mode safety checks // Some debug mode safety checks
if (std.debug.runtime_safety) { if (std.debug.runtime_safety) {
for (self.cells_bg.items) |cell| assert(cell.mode == .bg); for (self.cells_bg.items) |cell| assert(!cell.mode.fg);
for (self.cells.items) |cell| assert(cell.mode != .bg); for (self.cells.items) |cell| assert(cell.mode.fg);
} }
} }
@ -1469,7 +1475,7 @@ fn addPreeditCell(
// Add our opaque background cell // Add our opaque background cell
try self.cells_bg.append(self.alloc, .{ try self.cells_bg.append(self.alloc, .{
.mode = .bg, .mode = .{ .fg = false },
.grid_col = @intCast(x), .grid_col = @intCast(x),
.grid_row = @intCast(y), .grid_row = @intCast(y),
.grid_width = if (cp.wide) 2 else 1, .grid_width = if (cp.wide) 2 else 1,
@ -1491,7 +1497,7 @@ fn addPreeditCell(
// Add our text // Add our text
try self.cells.append(self.alloc, .{ try self.cells.append(self.alloc, .{
.mode = .fg, .mode = .{ .fg = true },
.grid_col = @intCast(x), .grid_col = @intCast(x),
.grid_row = @intCast(y), .grid_row = @intCast(y),
.grid_width = if (cp.wide) 2 else 1, .grid_width = if (cp.wide) 2 else 1,
@ -1558,7 +1564,7 @@ fn addCursor(
}; };
try self.cells.append(self.alloc, .{ try self.cells.append(self.alloc, .{
.mode = .fg, .mode = .{ .fg = true },
.grid_col = @intCast(x), .grid_col = @intCast(x),
.grid_row = @intCast(screen.cursor.y), .grid_row = @intCast(screen.cursor.y),
.grid_width = if (wide) 2 else 1, .grid_width = if (wide) 2 else 1,
@ -1702,7 +1708,7 @@ fn updateCell(
}; };
try self.cells_bg.append(self.alloc, .{ try self.cells_bg.append(self.alloc, .{
.mode = .bg, .mode = .{ .fg = false },
.grid_col = @intCast(x), .grid_col = @intCast(x),
.grid_row = @intCast(y), .grid_row = @intCast(y),
.grid_width = cell.gridWidth(), .grid_width = cell.gridWidth(),
@ -1743,16 +1749,20 @@ fn updateCell(
}, },
); );
var mode: CellProgram.CellMode = .{ .fg = true };
// If we're rendering a color font, we use the color atlas // If we're rendering a color font, we use the color atlas
const mode: CellProgram.CellMode = switch (try fgMode( switch (try fgMode(
render.presentation, render.presentation,
cell_pin, cell_pin,
)) { )) {
.normal => .fg, .normal => {},
.color => .fg_color, .color => mode.fg_color = true,
.constrained => .fg_constrained, .constrained => mode.fg_constrained = true,
.powerline => .fg_powerline, .powerline => mode.fg_powerline = true,
}; }
mode.fg_blink = style.flags.blink;
try self.cells.append(self.alloc, .{ try self.cells.append(self.alloc, .{
.mode = mode, .mode = mode,
@ -1799,7 +1809,7 @@ fn updateCell(
const color = style.underlineColor(palette) orelse colors.fg; const color = style.underlineColor(palette) orelse colors.fg;
try self.cells.append(self.alloc, .{ try self.cells.append(self.alloc, .{
.mode = .fg, .mode = .{ .fg = true },
.grid_col = @intCast(x), .grid_col = @intCast(x),
.grid_row = @intCast(y), .grid_row = @intCast(y),
.grid_width = cell.gridWidth(), .grid_width = cell.gridWidth(),
@ -1832,7 +1842,7 @@ fn updateCell(
); );
try self.cells.append(self.alloc, .{ try self.cells.append(self.alloc, .{
.mode = .fg, .mode = .{ .fg = true },
.grid_col = @intCast(x), .grid_col = @intCast(x),
.grid_row = @intCast(y), .grid_row = @intCast(y),
.grid_width = cell.gridWidth(), .grid_width = cell.gridWidth(),
@ -2156,6 +2166,10 @@ fn drawCellProgram(
"padding_vertical_bottom", "padding_vertical_bottom",
self.padding_extend_bottom, self.padding_extend_bottom,
); );
try program.program.setUniform(
"blink_visible",
self.blink_visible,
);
} }
// Draw background images first // Draw background images first

View File

@ -48,31 +48,14 @@ pub const Cell = extern struct {
grid_width: u8, grid_width: u8,
}; };
pub const CellMode = enum(u8) { pub const CellMode = packed struct(u8) {
bg = 1, fg: bool,
fg = 2, fg_constrained: bool = false,
fg_constrained = 3, fg_color: bool = false,
fg_color = 7, fg_powerline: bool = false,
fg_powerline = 15, fg_blink: bool = false,
// Non-exhaustive because masks change it _padding: u3 = 0,
_,
/// Apply a mask to the mode.
pub fn mask(self: CellMode, m: CellMode) CellMode {
return @enumFromInt(@intFromEnum(self) | @intFromEnum(m));
}
pub fn isFg(self: CellMode) bool {
// Since we use bit tricks below, we want to ensure the enum
// doesn't change without us looking at this logic again.
comptime {
const info = @typeInfo(CellMode).Enum;
std.debug.assert(info.fields.len == 5);
}
return @intFromEnum(self) & @intFromEnum(@as(CellMode, .fg)) != 0;
}
}; };
pub fn init() !CellProgram { pub fn init() !CellProgram {

View File

@ -22,32 +22,29 @@ uniform sampler2D text_color;
// Dimensions of the cell // Dimensions of the cell
uniform vec2 cell_size; uniform vec2 cell_size;
uniform bool blink_visible;
// See vertex shader // See vertex shader
const uint MODE_BG = 1u; const uint MODE_FG = 1u;
const uint MODE_FG = 2u; const uint MODE_FG_CONSTRAINED = 2u;
const uint MODE_FG_CONSTRAINED = 3u; const uint MODE_FG_COLOR = 4u;
const uint MODE_FG_COLOR = 7u; const uint MODE_FG_POWERLINE = 8u;
const uint MODE_FG_POWERLINE = 15u; const uint MODE_FG_BLINK = 16u;
void main() { void main() {
float a; if ((mode & MODE_FG) == 0u) {
// Background
switch (mode) { out_FragColor = color;
case MODE_BG:
out_FragColor = color;
break;
case MODE_FG:
case MODE_FG_CONSTRAINED:
case MODE_FG_POWERLINE:
a = texture(text, glyph_tex_coords).r;
vec3 premult = color.rgb * color.a;
out_FragColor = vec4(premult.rgb*a, a);
break;
case MODE_FG_COLOR:
out_FragColor = texture(text_color, glyph_tex_coords);
break;
} }
if ((mode & MODE_FG_BLINK) != 0u && !blink_visible) {
discard;
}
if ((mode & MODE_FG_COLOR) != 0u) {
out_FragColor = texture(text_color, glyph_tex_coords);
return;
}
float a = texture(text, glyph_tex_coords).r;
vec3 premult = color.rgb * color.a;
out_FragColor = vec4(premult.rgb*a, a);
} }

View File

@ -4,11 +4,11 @@
// used to multiplex multiple render modes into a single shader. // used to multiplex multiple render modes into a single shader.
// //
// NOTE: this must be kept in sync with the fragment shader // NOTE: this must be kept in sync with the fragment shader
const uint MODE_BG = 1u; const uint MODE_FG = 1u;
const uint MODE_FG = 2u; const uint MODE_FG_CONSTRAINED = 2u;
const uint MODE_FG_CONSTRAINED = 3u; const uint MODE_FG_COLOR = 4u;
const uint MODE_FG_COLOR = 7u; const uint MODE_FG_POWERLINE = 8u;
const uint MODE_FG_POWERLINE = 15u; const uint MODE_FG_BLINK = 16u;
// The grid coordinates (x, y) where x < columns and y < rows // The grid coordinates (x, y) where x < columns and y < rows
layout (location = 0) in vec2 grid_coord; layout (location = 0) in vec2 grid_coord;
@ -170,8 +170,8 @@ void main() {
vec2 cell_size_scaled = cell_size; vec2 cell_size_scaled = cell_size;
cell_size_scaled.x = cell_size_scaled.x * grid_width; cell_size_scaled.x = cell_size_scaled.x * grid_width;
switch (mode) { if ((mode & MODE_FG) == 0u) {
case MODE_BG: // Draw background
// If we're at the edge of the grid, we add our padding to the background // If we're at the edge of the grid, we add our padding to the background
// to extend it. Note: grid_padding is top/right/bottom/left. // to extend it. Note: grid_padding is top/right/bottom/left.
if (grid_coord.y == 0 && padding_vertical_top) { if (grid_coord.y == 0 && padding_vertical_top) {
@ -194,12 +194,7 @@ void main() {
gl_Position = projection * vec4(cell_pos, cell_z, 1.0); gl_Position = projection * vec4(cell_pos, cell_z, 1.0);
color = color_in / 255.0; color = color_in / 255.0;
break; } else {
case MODE_FG:
case MODE_FG_CONSTRAINED:
case MODE_FG_COLOR:
case MODE_FG_POWERLINE:
vec2 glyph_offset_calc = glyph_offset; vec2 glyph_offset_calc = glyph_offset;
// The glyph_offset.y is the y bearing, a y value that when added // The glyph_offset.y is the y bearing, a y value that when added
@ -211,7 +206,7 @@ void main() {
// We also always constrain colored glyphs since we should have // We also always constrain colored glyphs since we should have
// their scaled cell size exactly correct. // their scaled cell size exactly correct.
vec2 glyph_size_calc = glyph_size; vec2 glyph_size_calc = glyph_size;
if (mode == MODE_FG_CONSTRAINED || mode == MODE_FG_COLOR) { if ((mode & (MODE_FG_CONSTRAINED | MODE_FG_COLOR)) != 0u) {
if (glyph_size.x > cell_size_scaled.x) { if (glyph_size.x > cell_size_scaled.x) {
float new_y = glyph_size.y * (cell_size_scaled.x / glyph_size.x); float new_y = glyph_size.y * (cell_size_scaled.x / glyph_size.x);
glyph_offset_calc.y = glyph_offset_calc.y + ((glyph_size.y - new_y) / 2); glyph_offset_calc.y = glyph_offset_calc.y + ((glyph_size.y - new_y) / 2);
@ -227,16 +222,10 @@ void main() {
// We need to convert our texture position and size to normalized // We need to convert our texture position and size to normalized
// device coordinates (0 to 1.0) by dividing by the size of the texture. // device coordinates (0 to 1.0) by dividing by the size of the texture.
ivec2 text_size; ivec2 text_size;
switch(mode) { if ((mode & MODE_FG_COLOR) != 0u) {
case MODE_FG_CONSTRAINED: text_size = textureSize(text_color, 0);
case MODE_FG_POWERLINE: } else {
case MODE_FG: text_size = textureSize(text, 0);
text_size = textureSize(text, 0);
break;
case MODE_FG_COLOR:
text_size = textureSize(text_color, 0);
break;
} }
vec2 glyph_tex_pos = glyph_pos / text_size; vec2 glyph_tex_pos = glyph_pos / text_size;
vec2 glyph_tex_size = glyph_size / text_size; vec2 glyph_tex_size = glyph_size / text_size;
@ -250,11 +239,10 @@ void main() {
// and Powerline glyphs to be unaffected (else parts of the line would // and Powerline glyphs to be unaffected (else parts of the line would
// have different colors as some parts are displayed via background colors). // have different colors as some parts are displayed via background colors).
vec4 color_final = color_in / 255.0; vec4 color_final = color_in / 255.0;
if (min_contrast > 1.0 && mode == MODE_FG) { if (min_contrast > 1.0 && (mode & ~(MODE_FG | MODE_FG_BLINK)) != 0u) {
vec4 bg_color = bg_color_in / 255.0; vec4 bg_color = bg_color_in / 255.0;
color_final = contrasted_color(min_contrast, color_final, bg_color); color_final = contrasted_color(min_contrast, color_final, bg_color);
} }
color = color_final; color = color_final;
break;
} }
} }