renderer: remove all gpu-side glyph constraint logic

Now that it's done at the rasterization stage, we don't need to handle
it on the GPU. This also means that we can switch to nearest neighbor
interpolation in the Metal shader since we're guaranteed to be pixel
perfect. Accidentally, we were already nearest neighbor in the OpenGL
shaders because I used the Rectangle texture mode in the big renderer
rework, which doesn't support interpolation- anyway, that's no longer
problematic since we won't be scaling glyphs on the GPU anymore.
This commit is contained in:
Qwerasd
2025-07-04 15:39:56 -06:00
parent f292132762
commit ec20c455c7
7 changed files with 86 additions and 247 deletions

View File

@ -205,24 +205,6 @@ pub fn isCovering(cp: u21) bool {
};
}
pub const FgMode = enum {
/// Normal non-colored text rendering. The text can leave the cell
/// size if it is larger than the cell to allow for ligatures.
normal,
/// Colored text rendering, specifically Emoji.
color,
/// Similar to normal but the text must be constrained to the cell
/// size. If a glyph is larger than the cell then it must be resized
/// to fit.
constrained,
/// Similar to normal, but the text consists of Powerline glyphs and is
/// optionally exempt from padding color extension and minimum contrast requirements.
powerline,
};
/// Returns the appropriate `constraint_width` for
/// the provided cell when rendering its glyph(s).
pub fn constraintWidth(cell_pin: terminal.Pin) u2 {
@ -277,85 +259,10 @@ pub fn constraintWidth(cell_pin: terminal.Pin) u2 {
return 1;
}
/// Returns the appropriate foreground mode for the given cell. This is
/// meant to be called from the typical updateCell function within a
/// renderer.
pub fn fgMode(
presentation: font.Presentation,
cell_pin: terminal.Pin,
) FgMode {
return switch (presentation) {
// Emoji is always full size and color.
.emoji => .color,
// If it is text it is slightly more complex. If we are a codepoint
// in the private use area and we are at the end or the next cell
// is not empty, we need to constrain rendering.
//
// We do this specifically so that Nerd Fonts can render their
// icons without overlapping with subsequent characters. But if
// the subsequent character is empty, then we allow it to use
// the full glyph size. See #1071.
.text => text: {
const cell = cell_pin.rowAndCell().cell;
const cp = cell.codepoint();
if (!ziglyph.general_category.isPrivateUse(cp) and
!ziglyph.blocks.isDingbats(cp))
{
break :text .normal;
}
// Special-case Powerline glyphs. They exhibit box drawing behavior
// and should not be constrained. They have their own special category
// though because they're used for other logic (i.e. disabling
// min contrast).
if (isPowerline(cp)) {
break :text .powerline;
}
// If we are at the end of the screen its definitely constrained
if (cell_pin.x == cell_pin.node.data.size.cols - 1) break :text .constrained;
// If we have a previous cell and it was PUA then we need to
// also constrain. This is so that multiple PUA glyphs align.
// As an exception, we ignore powerline glyphs since they are
// used for box drawing and we consider them whitespace.
if (cell_pin.x > 0) prev: {
const prev_cp = prev_cp: {
var copy = cell_pin;
copy.x -= 1;
const prev_cell = copy.rowAndCell().cell;
break :prev_cp prev_cell.codepoint();
};
// Powerline is whitespace
if (isPowerline(prev_cp)) break :prev;
if (ziglyph.general_category.isPrivateUse(prev_cp)) {
break :text .constrained;
}
}
// If the next cell is empty, then we allow it to use the
// full glyph size.
const next_cp = next_cp: {
var copy = cell_pin;
copy.x += 1;
const next_cell = copy.rowAndCell().cell;
break :next_cp next_cell.codepoint();
};
if (next_cp == 0 or
isSpace(next_cp) or
isPowerline(next_cp))
{
break :text .normal;
}
// Must be constrained
break :text .constrained;
},
};
/// Whether min contrast should be disabled for a given glyph.
pub fn noMinContrast(cp: u21) bool {
// TODO: We should disable for all box drawing type characters.
return isPowerline(cp);
}
// Some general spaces, others intentionally kept

View File

@ -13,7 +13,7 @@ const math = @import("../math.zig");
const Surface = @import("../Surface.zig");
const link = @import("link.zig");
const cellpkg = @import("cell.zig");
const fgMode = cellpkg.fgMode;
const noMinContrast = cellpkg.noMinContrast;
const constraintWidth = cellpkg.constraintWidth;
const isCovering = cellpkg.isCovering;
const imagepkg = @import("image.zig");
@ -2933,9 +2933,8 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
);
try self.cells.add(self.alloc, .underline, .{
.mode = .fg,
.atlas = .grayscale,
.grid_pos = .{ @intCast(x), @intCast(y) },
.constraint_width = 1,
.color = .{ color.r, color.g, color.b, alpha },
.glyph_pos = .{ render.glyph.atlas_x, render.glyph.atlas_y },
.glyph_size = .{ render.glyph.width, render.glyph.height },
@ -2965,9 +2964,8 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
);
try self.cells.add(self.alloc, .overline, .{
.mode = .fg,
.atlas = .grayscale,
.grid_pos = .{ @intCast(x), @intCast(y) },
.constraint_width = 1,
.color = .{ color.r, color.g, color.b, alpha },
.glyph_pos = .{ render.glyph.atlas_x, render.glyph.atlas_y },
.glyph_size = .{ render.glyph.width, render.glyph.height },
@ -2997,9 +2995,8 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
);
try self.cells.add(self.alloc, .strikethrough, .{
.mode = .fg,
.atlas = .grayscale,
.grid_pos = .{ @intCast(x), @intCast(y) },
.constraint_width = 1,
.color = .{ color.r, color.g, color.b, alpha },
.glyph_pos = .{ render.glyph.atlas_x, render.glyph.atlas_y },
.glyph_size = .{ render.glyph.width, render.glyph.height },
@ -3024,6 +3021,8 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
const rac = cell_pin.rowAndCell();
const cell = rac.cell;
const cp = cell.codepoint();
// Render
const render = try self.font_grid.renderGlyph(
self.alloc,
@ -3034,7 +3033,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
.thicken = self.config.font_thicken,
.thicken_strength = self.config.font_thicken_strength,
.cell_width = cell.gridWidth(),
.constraint = getConstraint(cell.codepoint()),
.constraint = getConstraint(cp),
.constraint_width = constraintWidth(cell_pin),
},
);
@ -3045,27 +3044,13 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
return;
}
// We always use fg mode for sprite glyphs, since we know we never
// need to constrain them, and we don't have any color sprites.
//
// Otherwise we defer to `fgMode`.
const mode: shaderpkg.CellText.Mode =
if (render.glyph.sprite)
.fg
else switch (fgMode(
render.presentation,
cell_pin,
)) {
.normal => .fg,
.color => .fg_color,
.constrained => .fg_constrained,
.powerline => .fg_powerline,
};
try self.cells.add(self.alloc, .text, .{
.mode = mode,
.atlas = switch (render.presentation) {
.emoji => .color,
.text => .grayscale,
},
.bools = .{ .no_min_contrast = noMinContrast(cp) },
.grid_pos = .{ @intCast(x), @intCast(y) },
.constraint_width = cell.gridWidth(),
.color = .{ color.r, color.g, color.b, alpha },
.glyph_pos = .{ render.glyph.atlas_x, render.glyph.atlas_y },
.glyph_size = .{ render.glyph.width, render.glyph.height },
@ -3150,7 +3135,8 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
};
self.cells.setCursor(.{
.mode = .cursor,
.atlas = .grayscale,
.bools = .{ .is_cursor_glyph = true },
.grid_pos = .{ x, screen.cursor.y },
.color = .{ cursor_color.r, cursor_color.g, cursor_color.b, alpha },
.glyph_pos = .{ render.glyph.atlas_x, render.glyph.atlas_y },
@ -3199,7 +3185,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
// Add our text
try self.cells.add(self.alloc, .text, .{
.mode = .fg,
.atlas = .grayscale,
.grid_pos = .{ @intCast(coord.x), @intCast(coord.y) },
.color = .{ fg.r, fg.g, fg.b, 255 },
.glyph_pos = .{ render.glyph.atlas_x, render.glyph.atlas_y },

View File

@ -269,15 +269,16 @@ pub const CellText = extern struct {
bearings: [2]i16 align(4) = .{ 0, 0 },
grid_pos: [2]u16 align(4),
color: [4]u8 align(4),
mode: Mode align(1),
constraint_width: u8 align(1) = 0,
atlas: Atlas align(1),
bools: packed struct(u8) {
no_min_contrast: bool = false,
is_cursor_glyph: bool = false,
_padding: u6 = 0,
} align(1) = .{},
pub const Mode = enum(u8) {
fg = 1,
fg_constrained = 2,
fg_color = 3,
cursor = 4,
fg_powerline = 5,
pub const Atlas = enum(u8) {
grayscale = 0,
color = 1,
};
test {

View File

@ -237,15 +237,16 @@ pub const CellText = extern struct {
bearings: [2]i16 align(4) = .{ 0, 0 },
grid_pos: [2]u16 align(4),
color: [4]u8 align(4),
mode: Mode align(4),
constraint_width: u32 align(4) = 0,
atlas: Atlas align(1),
bools: packed struct(u8) {
no_min_contrast: bool = false,
is_cursor_glyph: bool = false,
_padding: u6 = 0,
} align(1) = .{},
pub const Mode = enum(u32) {
fg = 1,
fg_constrained = 2,
fg_color = 3,
cursor = 4,
fg_powerline = 5,
pub const Atlas = enum(u8) {
grayscale = 0,
color = 1,
};
// test {

View File

@ -4,21 +4,15 @@ layout(binding = 0) uniform sampler2DRect atlas_grayscale;
layout(binding = 1) uniform sampler2DRect atlas_color;
in CellTextVertexOut {
flat uint mode;
flat uint atlas;
flat vec4 color;
flat vec4 bg_color;
vec2 tex_coord;
} in_data;
// These are the possible modes that "mode" can be set to. This is
// used to multiplex multiple render modes into a single shader.
//
// NOTE: this must be kept in sync with the fragment shader
const uint MODE_TEXT = 1u;
const uint MODE_TEXT_CONSTRAINED = 2u;
const uint MODE_TEXT_COLOR = 3u;
const uint MODE_TEXT_CURSOR = 4u;
const uint MODE_TEXT_POWERLINE = 5u;
// Values `atlas` can take.
const uint ATLAS_GRAYSCALE = 0u;
const uint ATLAS_COLOR = 1u;
// Must declare this output for some versions of OpenGL.
layout(location = 0) out vec4 out_FragColor;
@ -27,12 +21,9 @@ void main() {
bool use_linear_blending = (bools & USE_LINEAR_BLENDING) != 0;
bool use_linear_correction = (bools & USE_LINEAR_CORRECTION) != 0;
switch (in_data.mode) {
switch (in_data.atlas) {
default:
case MODE_TEXT_CURSOR:
case MODE_TEXT_CONSTRAINED:
case MODE_TEXT_POWERLINE:
case MODE_TEXT:
case ATLAS_GRAYSCALE:
{
// Our input color is always linear.
vec4 color = in_data.color;
@ -84,7 +75,7 @@ void main() {
return;
}
case MODE_TEXT_COLOR:
case ATLAS_COLOR:
{
// For now, we assume that color glyphs
// are already premultiplied linear colors.

View File

@ -15,22 +15,22 @@ layout(location = 3) in uvec2 grid_pos;
// The color of the rendered text glyph.
layout(location = 4) in uvec4 color;
// The mode for this cell.
layout(location = 5) in uint mode;
// Which atlas this glyph is in.
layout(location = 5) in uint atlas;
// The width to constrain the glyph to, in cells, or 0 for no constraint.
layout(location = 6) in uint constraint_width;
// Misc glyph properties.
layout(location = 6) in uint glyph_bools;
// These are the possible modes that "mode" can be set to. This is
// used to multiplex multiple render modes into a single shader.
const uint MODE_TEXT = 1u;
const uint MODE_TEXT_CONSTRAINED = 2u;
const uint MODE_TEXT_COLOR = 3u;
const uint MODE_TEXT_CURSOR = 4u;
const uint MODE_TEXT_POWERLINE = 5u;
// Values `atlas` can take.
const uint ATLAS_GRAYSCALE = 0u;
const uint ATLAS_COLOR = 1u;
// Masks for the `glyph_bools` attribute
const uint NO_MIN_CONTRAST = 1u;
const uint IS_CURSOR_GLYPH = 2u;
out CellTextVertexOut {
flat uint mode;
flat uint atlas;
flat vec4 color;
flat vec4 bg_color;
vec2 tex_coord;
@ -69,7 +69,7 @@ void main() {
corner.x = float(vid == 1 || vid == 3);
corner.y = float(vid == 2 || vid == 3);
out_data.mode = mode;
out_data.atlas = atlas;
// === Grid Cell ===
// +X
@ -102,25 +102,6 @@ void main() {
offset.y = cell_size.y - offset.y;
// If we're constrained then we need to scale the glyph.
if (mode == MODE_TEXT_CONSTRAINED) {
float max_width = cell_size.x * constraint_width;
// If this glyph is wider than the constraint width,
// fit it to the width and remove its horizontal offset.
if (size.x > max_width) {
float new_y = size.y * (max_width / size.x);
offset.y += (size.y - new_y) / 2.0;
offset.x = 0.0;
size.y = new_y;
size.x = max_width;
} else if (max_width - size.x > offset.x) {
// However, if it does fit in the constraint width, make
// sure the offset is small enough to not push it over the
// right edge of the constraint width.
offset.x = max_width - size.x;
}
}
// Calculate the final position of the cell which uses our glyph size
// and glyph offset to create the correct bounding box for the glyph.
cell_pos = cell_pos + size * corner + offset;
@ -149,11 +130,7 @@ void main() {
// If we have a minimum contrast, we need to check if we need to
// change the color of the text to ensure it has enough contrast
// with the background.
// We only apply this adjustment to "normal" text with MODE_TEXT,
// since we want color glyphs to appear in their original color
// and Powerline glyphs to be unaffected (else parts of the line would
// have different colors as some parts are displayed via background colors).
if (min_contrast > 1.0f && mode == MODE_TEXT) {
if (min_contrast > 1.0f && (glyph_bools & NO_MIN_CONTRAST) == 0) {
// Ensure our minimum contrast
out_data.color = contrasted_color(min_contrast, out_data.color, out_data.bg_color);
}
@ -161,8 +138,9 @@ void main() {
// Check if current position is under cursor (including wide cursor)
bool is_cursor_pos = ((grid_pos.x == cursor_pos.x) || (cursor_wide && (grid_pos.x == (cursor_pos.x + 1)))) && (grid_pos.y == cursor_pos.y);
// If this cell is the cursor cell, then we need to change the color.
if (mode != MODE_TEXT_CURSOR && is_cursor_pos) {
// If this cell is the cursor cell, but we're not processing
// the cursor glyph itself, then we need to change the color.
if ((glyph_bools & IS_CURSOR_GLYPH) == 0 && is_cursor_pos) {
out_data.color = load_color(unpack4u8(cursor_color_packed_4u8), use_linear_blending);
}
}

View File

@ -509,13 +509,17 @@ fragment float4 cell_bg_fragment(
//-------------------------------------------------------------------
#pragma mark - Cell Text Shader
// The possible modes that a cell fg entry can take.
enum CellTextMode : uint8_t {
MODE_TEXT = 1u,
MODE_TEXT_CONSTRAINED = 2u,
MODE_TEXT_COLOR = 3u,
MODE_TEXT_CURSOR = 4u,
MODE_TEXT_POWERLINE = 5u,
enum CellTextAtlas : uint8_t {
ATLAS_GRAYSCALE = 0u,
ATLAS_COLOR = 1u,
};
// We use a packed struct of bools for misc properties of the glyph.
enum CellTextBools : uint8_t {
// Don't apply min contrast to this glyph.
NO_MIN_CONTRAST = 1u,
// This is the cursor glyph.
IS_CURSOR_GLYPH = 2u,
};
struct CellTextVertexIn {
@ -534,16 +538,16 @@ struct CellTextVertexIn {
// The color of the rendered text glyph.
uchar4 color [[attribute(4)]];
// The mode for this cell.
uint8_t mode [[attribute(5)]];
// Which atlas to sample for our glyph.
uint8_t atlas [[attribute(5)]];
// The width to constrain the glyph to, in cells, or 0 for no constraint.
uint8_t constraint_width [[attribute(6)]];
// Misc properties of the glyph.
uint8_t bools [[attribute(6)]];
};
struct CellTextVertexOut {
float4 position [[position]];
uint8_t mode [[flat]];
uint8_t atlas [[flat]];
float4 color [[flat]];
float4 bg_color [[flat]];
float2 tex_coord;
@ -577,7 +581,7 @@ vertex CellTextVertexOut cell_text_vertex(
corner.y = float(vid == 2 || vid == 3);
CellTextVertexOut out;
out.mode = in.mode;
out.atlas = in.atlas;
// === Grid Cell ===
// +X
@ -610,25 +614,6 @@ vertex CellTextVertexOut cell_text_vertex(
offset.y = uniforms.cell_size.y - offset.y;
// If we're constrained then we need to scale the glyph.
if (in.mode == MODE_TEXT_CONSTRAINED) {
float max_width = uniforms.cell_size.x * in.constraint_width;
// If this glyph is wider than the constraint width,
// fit it to the width and remove its horizontal offset.
if (size.x > max_width) {
float new_y = size.y * (max_width / size.x);
offset.y += (size.y - new_y) / 2;
offset.x = 0;
size.y = new_y;
size.x = max_width;
} else if (max_width - size.x > offset.x) {
// However, if it does fit in the constraint width, make
// sure the offset is small enough to not push it over the
// right edge of the constraint width.
offset.x = max_width - size.x;
}
}
// Calculate the final position of the cell which uses our glyph size
// and glyph offset to create the correct bounding box for the glyph.
cell_pos = cell_pos + size * corner + offset;
@ -665,11 +650,7 @@ vertex CellTextVertexOut cell_text_vertex(
// If we have a minimum contrast, we need to check if we need to
// change the color of the text to ensure it has enough contrast
// with the background.
// We only apply this adjustment to "normal" text with MODE_TEXT,
// since we want color glyphs to appear in their original color
// and Powerline glyphs to be unaffected (else parts of the line would
// have different colors as some parts are displayed via background colors).
if (uniforms.min_contrast > 1.0f && in.mode == MODE_TEXT) {
if (uniforms.min_contrast > 1.0f && (in.bools & NO_MIN_CONTRAST) == 0) {
// Ensure our minimum contrast
out.color = contrasted_color(uniforms.min_contrast, out.color, out.bg_color);
}
@ -681,8 +662,9 @@ vertex CellTextVertexOut cell_text_vertex(
in.grid_pos.x == uniforms.cursor_pos.x + 1
) && in.grid_pos.y == uniforms.cursor_pos.y;
// If this cell is the cursor cell, then we need to change the color.
if (in.mode != MODE_TEXT_CURSOR && is_cursor_pos) {
// If this cell is the cursor cell, but we're not processing
// the cursor glyph itself, then we need to change the color.
if ((in.bools & IS_CURSOR_GLYPH) == 0 && is_cursor_pos) {
out.color = load_color(
uniforms.cursor_color,
uniforms.use_display_p3,
@ -702,19 +684,12 @@ fragment float4 cell_text_fragment(
constexpr sampler textureSampler(
coord::pixel,
address::clamp_to_edge,
// TODO(qwerasd): This can be changed back to filter::nearest when
// we move the constraint logic out of the GPU code
// which should once again guarantee pixel perfect
// sizing.
filter::linear
filter::nearest
);
switch (in.mode) {
switch (in.atlas) {
default:
case MODE_TEXT_CURSOR:
case MODE_TEXT_CONSTRAINED:
case MODE_TEXT_POWERLINE:
case MODE_TEXT: {
case ATLAS_GRAYSCALE: {
// Our input color is always linear.
float4 color = in.color;
@ -764,7 +739,7 @@ fragment float4 cell_text_fragment(
return color;
}
case MODE_TEXT_COLOR: {
case ATLAS_COLOR: {
// For now, we assume that color glyphs
// are already premultiplied linear colors.
float4 color = textureColor.sample(textureSampler, in.tex_coord);