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.
cursor_invert: bool,
/// Whether blinking cells are currently visible. Synchronized with cursor blinking.
blink_visible: bool = true,
/// Padding options
padding: renderer.Options.Padding,
@ -701,7 +704,7 @@ pub fn updateFrame(
self: *OpenGL,
surface: *apprt.Surface,
state: *renderer.State,
cursor_blink_visible: bool,
blink_visible: bool,
) !void {
_ = surface;
@ -770,7 +773,7 @@ pub fn updateFrame(
const cursor_style = renderer.cursorStyle(
state,
self.focused,
cursor_blink_visible,
blink_visible,
);
// Get our preedit state
@ -854,6 +857,9 @@ pub fn updateFrame(
.color_palette = state.terminal.color_palette.colors,
};
};
self.blink_visible = blink_visible;
defer {
critical.screen.deinit();
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 x = screen.cursor.x - @intFromBool(screen_cell.wide == .spacer_tail);
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;
break;
}
@ -1416,7 +1422,7 @@ pub fn rebuildCells(
_ = try self.addCursor(screen, cursor_style, cursor_color);
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 sty = screen.cursor.page_pin.style(screen.cursor.page_cell);
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
if (std.debug.runtime_safety) {
for (self.cells_bg.items) |cell| assert(cell.mode == .bg);
for (self.cells.items) |cell| assert(cell.mode != .bg);
for (self.cells_bg.items) |cell| assert(!cell.mode.fg);
for (self.cells.items) |cell| assert(cell.mode.fg);
}
}
@ -1469,7 +1475,7 @@ fn addPreeditCell(
// Add our opaque background cell
try self.cells_bg.append(self.alloc, .{
.mode = .bg,
.mode = .{ .fg = false },
.grid_col = @intCast(x),
.grid_row = @intCast(y),
.grid_width = if (cp.wide) 2 else 1,
@ -1491,7 +1497,7 @@ fn addPreeditCell(
// Add our text
try self.cells.append(self.alloc, .{
.mode = .fg,
.mode = .{ .fg = true },
.grid_col = @intCast(x),
.grid_row = @intCast(y),
.grid_width = if (cp.wide) 2 else 1,
@ -1558,7 +1564,7 @@ fn addCursor(
};
try self.cells.append(self.alloc, .{
.mode = .fg,
.mode = .{ .fg = true },
.grid_col = @intCast(x),
.grid_row = @intCast(screen.cursor.y),
.grid_width = if (wide) 2 else 1,
@ -1702,7 +1708,7 @@ fn updateCell(
};
try self.cells_bg.append(self.alloc, .{
.mode = .bg,
.mode = .{ .fg = false },
.grid_col = @intCast(x),
.grid_row = @intCast(y),
.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
const mode: CellProgram.CellMode = switch (try fgMode(
switch (try fgMode(
render.presentation,
cell_pin,
)) {
.normal => .fg,
.color => .fg_color,
.constrained => .fg_constrained,
.powerline => .fg_powerline,
};
.normal => {},
.color => mode.fg_color = true,
.constrained => mode.fg_constrained = true,
.powerline => mode.fg_powerline = true,
}
mode.fg_blink = style.flags.blink;
try self.cells.append(self.alloc, .{
.mode = mode,
@ -1799,7 +1809,7 @@ fn updateCell(
const color = style.underlineColor(palette) orelse colors.fg;
try self.cells.append(self.alloc, .{
.mode = .fg,
.mode = .{ .fg = true },
.grid_col = @intCast(x),
.grid_row = @intCast(y),
.grid_width = cell.gridWidth(),
@ -1832,7 +1842,7 @@ fn updateCell(
);
try self.cells.append(self.alloc, .{
.mode = .fg,
.mode = .{ .fg = true },
.grid_col = @intCast(x),
.grid_row = @intCast(y),
.grid_width = cell.gridWidth(),
@ -2156,6 +2166,10 @@ fn drawCellProgram(
"padding_vertical_bottom",
self.padding_extend_bottom,
);
try program.program.setUniform(
"blink_visible",
self.blink_visible,
);
}
// Draw background images first

View File

@ -48,31 +48,14 @@ pub const Cell = extern struct {
grid_width: u8,
};
pub const CellMode = enum(u8) {
bg = 1,
fg = 2,
fg_constrained = 3,
fg_color = 7,
fg_powerline = 15,
pub const CellMode = packed struct(u8) {
fg: bool,
fg_constrained: bool = false,
fg_color: bool = false,
fg_powerline: bool = false,
fg_blink: bool = false,
// Non-exhaustive because masks change it
_,
/// 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;
}
_padding: u3 = 0,
};
pub fn init() !CellProgram {

View File

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

View File

@ -4,11 +4,11 @@
// used to multiplex multiple render modes into a single shader.
//
// NOTE: this must be kept in sync with the fragment shader
const uint MODE_BG = 1u;
const uint MODE_FG = 2u;
const uint MODE_FG_CONSTRAINED = 3u;
const uint MODE_FG_COLOR = 7u;
const uint MODE_FG_POWERLINE = 15u;
const uint MODE_FG = 1u;
const uint MODE_FG_CONSTRAINED = 2u;
const uint MODE_FG_COLOR = 4u;
const uint MODE_FG_POWERLINE = 8u;
const uint MODE_FG_BLINK = 16u;
// The grid coordinates (x, y) where x < columns and y < rows
layout (location = 0) in vec2 grid_coord;
@ -170,8 +170,8 @@ void main() {
vec2 cell_size_scaled = cell_size;
cell_size_scaled.x = cell_size_scaled.x * grid_width;
switch (mode) {
case MODE_BG:
if ((mode & MODE_FG) == 0u) {
// Draw 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.
if (grid_coord.y == 0 && padding_vertical_top) {
@ -194,12 +194,7 @@ void main() {
gl_Position = projection * vec4(cell_pos, cell_z, 1.0);
color = color_in / 255.0;
break;
case MODE_FG:
case MODE_FG_CONSTRAINED:
case MODE_FG_COLOR:
case MODE_FG_POWERLINE:
} else {
vec2 glyph_offset_calc = glyph_offset;
// 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
// their scaled cell size exactly correct.
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) {
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);
@ -227,16 +222,10 @@ void main() {
// 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.
ivec2 text_size;
switch(mode) {
case MODE_FG_CONSTRAINED:
case MODE_FG_POWERLINE:
case MODE_FG:
text_size = textureSize(text, 0);
break;
case MODE_FG_COLOR:
if ((mode & MODE_FG_COLOR) != 0u) {
text_size = textureSize(text_color, 0);
break;
} else {
text_size = textureSize(text, 0);
}
vec2 glyph_tex_pos = glyph_pos / 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
// have different colors as some parts are displayed via background colors).
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;
color_final = contrasted_color(min_contrast, color_final, bg_color);
}
color = color_final;
break;
}
}