renderer/opengl: implement min contrast

This commit is contained in:
Mitchell Hashimoto
2023-12-01 21:51:12 -08:00
parent e3eba92c0e
commit ec8f3d036e
3 changed files with 147 additions and 9 deletions

View File

@ -101,6 +101,7 @@ surface_mailbox: apprt.surface.Mailbox,
/// simple we apply all OpenGL context changes in the render() call.
deferred_screen_size: ?SetScreenSize = null,
deferred_font_size: ?SetFontSize = null,
deferred_config: ?SetConfig = null,
/// If we're drawing with single threaded operations
draw_mutex: DrawMutex = drawMutexZero,
@ -206,6 +207,20 @@ const SetFontSize = struct {
}
};
const SetConfig = struct {
fn apply(self: SetConfig, r: *const OpenGL) !void {
_ = self;
const gl_state = r.gl_state orelse return error.OpenGLUninitialized;
const bind = try gl_state.cell_program.program.use();
defer bind.unbind();
try gl_state.cell_program.program.setUniform(
"min_contrast",
r.config.min_contrast,
);
}
};
/// The configuration for this renderer that is derived from the main
/// configuration. This must be exported so that we don't need to
/// pass around Config pointers which makes memory management a pain.
@ -224,6 +239,7 @@ pub const DerivedConfig = struct {
selection_background: ?terminal.color.RGB,
selection_foreground: ?terminal.color.RGB,
invert_selection_fg_bg: bool,
min_contrast: f32,
custom_shaders: std.ArrayListUnmanaged([]const u8),
custom_shader_animation: bool,
links: link.Set,
@ -275,6 +291,7 @@ pub const DerivedConfig = struct {
.background = config.background.toTerminalRGB(),
.foreground = config.foreground.toTerminalRGB(),
.invert_selection_fg_bg = config.@"selection-invert-fg-bg",
.min_contrast = @floatCast(config.@"minimum-contrast"),
.selection_background = if (config.@"selection-background") |bg|
bg.toTerminalRGB()
@ -336,6 +353,7 @@ pub fn init(alloc: Allocator, options: renderer.Options) !OpenGL {
.padding = options.padding,
.surface_mailbox = options.surface_mailbox,
.deferred_font_size = .{ .metrics = metrics },
.deferred_config = .{},
};
}
@ -462,6 +480,7 @@ pub fn displayRealize(self: *OpenGL) !void {
self.deferred_screen_size = .{ .size = size };
}
self.deferred_font_size = .{ .metrics = metrics };
self.deferred_config = .{};
}
/// Callback called by renderer.Thread when it begins.
@ -1148,6 +1167,10 @@ fn addPreeditCell(
.g = bg.g,
.b = bg.b,
.a = 255,
.bg_r = 0,
.bg_g = 0,
.bg_b = 0,
.bg_a = 0,
});
// Add our text
@ -1166,6 +1189,10 @@ fn addPreeditCell(
.g = fg.g,
.b = fg.b,
.a = 255,
.bg_r = bg.r,
.bg_g = bg.g,
.bg_b = bg.b,
.bg_a = 255,
});
}
@ -1227,6 +1254,10 @@ fn addCursor(
.g = color.g,
.b = color.b,
.a = alpha,
.bg_r = 0,
.bg_g = 0,
.bg_b = 0,
.bg_a = 0,
.glyph_x = glyph.atlas_x,
.glyph_y = glyph.atlas_y,
.glyph_width = glyph.width,
@ -1334,7 +1365,7 @@ pub fn updateCell(
const alpha: u8 = if (cell.attrs.faint) 175 else 255;
// If the cell has a background, we always draw it.
if (colors.bg) |rgb| {
const bg: [4]u8 = if (colors.bg) |rgb| bg: {
// Determine our background alpha. If we have transparency configured
// then this is dynamic depending on some situations. This is all
// in an attempt to make transparency look the best for various
@ -1378,8 +1409,19 @@ pub fn updateCell(
.g = rgb.g,
.b = rgb.b,
.a = bg_alpha,
.bg_r = 0,
.bg_g = 0,
.bg_b = 0,
.bg_a = 0,
});
}
break :bg .{ rgb.r, rgb.g, rgb.b, bg_alpha };
} else .{
self.draw_background.r,
self.draw_background.g,
self.draw_background.b,
@intFromFloat(@max(0, @min(255, @round(self.config.background_opacity * 255)))),
};
// If the cell has a character, draw it
if (cell.char > 0) {
@ -1416,6 +1458,10 @@ pub fn updateCell(
.g = colors.fg.g,
.b = colors.fg.b,
.a = alpha,
.bg_r = bg[0],
.bg_g = bg[1],
.bg_b = bg[2],
.bg_a = bg[3],
});
}
@ -1453,6 +1499,10 @@ pub fn updateCell(
.g = color.g,
.b = color.b,
.a = alpha,
.bg_r = bg[0],
.bg_g = bg[1],
.bg_b = bg[2],
.bg_a = bg[3],
});
}
@ -1472,6 +1522,10 @@ pub fn updateCell(
.g = colors.fg.g,
.b = colors.fg.b,
.a = alpha,
.bg_r = bg[0],
.bg_g = bg[1],
.bg_b = bg[2],
.bg_a = bg[3],
});
}
@ -1511,6 +1565,9 @@ pub fn changeConfig(self: *OpenGL, config: *DerivedConfig) !void {
self.font_shaper = font_shaper;
}
// Update our uniforms
self.deferred_config = .{};
self.config.deinit();
self.config = config.*;
}
@ -1728,6 +1785,10 @@ fn drawCellProgram(
try v.apply(self);
self.deferred_font_size = null;
}
if (self.deferred_config) |v| {
try v.apply(self);
self.deferred_config = null;
}
// Draw background images first
try self.drawImages(

View File

@ -29,12 +29,18 @@ pub const Cell = extern struct {
glyph_offset_x: i32 = 0,
glyph_offset_y: i32 = 0,
/// vec4 fg_color_in
/// vec4 color_in
r: u8,
g: u8,
b: u8,
a: u8,
/// vec4 bg_color_in
bg_r: u8,
bg_g: u8,
bg_b: u8,
bg_a: u8,
/// uint mode
mode: CellMode,
@ -105,9 +111,11 @@ pub fn init() !CellProgram {
offset += 2 * @sizeOf(i32);
try vbobind.attributeAdvanced(4, 4, gl.c.GL_UNSIGNED_BYTE, false, @sizeOf(Cell), offset);
offset += 4 * @sizeOf(u8);
try vbobind.attributeIAdvanced(5, 1, gl.c.GL_UNSIGNED_BYTE, @sizeOf(Cell), offset);
offset += 1 * @sizeOf(u8);
try vbobind.attributeAdvanced(5, 4, gl.c.GL_UNSIGNED_BYTE, false, @sizeOf(Cell), offset);
offset += 4 * @sizeOf(u8);
try vbobind.attributeIAdvanced(6, 1, gl.c.GL_UNSIGNED_BYTE, @sizeOf(Cell), offset);
offset += 1 * @sizeOf(u8);
try vbobind.attributeIAdvanced(7, 1, gl.c.GL_UNSIGNED_BYTE, @sizeOf(Cell), offset);
try vbobind.enableAttribArray(0);
try vbobind.enableAttribArray(1);
try vbobind.enableAttribArray(2);
@ -115,6 +123,7 @@ pub fn init() !CellProgram {
try vbobind.enableAttribArray(4);
try vbobind.enableAttribArray(5);
try vbobind.enableAttribArray(6);
try vbobind.enableAttribArray(7);
try vbobind.attributeDivisor(0, 1);
try vbobind.attributeDivisor(1, 1);
try vbobind.attributeDivisor(2, 1);
@ -122,6 +131,7 @@ pub fn init() !CellProgram {
try vbobind.attributeDivisor(4, 1);
try vbobind.attributeDivisor(5, 1);
try vbobind.attributeDivisor(6, 1);
try vbobind.attributeDivisor(7, 1);
return .{
.program = program,

View File

@ -25,14 +25,18 @@ layout (location = 3) in vec2 glyph_offset;
// depends on mode.
layout (location = 4) in vec4 color_in;
// Only set for MODE_FG, this is the background color of the FG text.
// This is used to detect minimal contrast for the text.
layout (location = 5) in vec4 bg_color_in;
// The mode of this shader. The mode determines what fields are used,
// what the output will be, etc. This shader is capable of executing in
// multiple "modes" so that we can share some logic and so that we can draw
// the entire terminal grid in a single GPU pass.
layout (location = 5) in uint mode_in;
layout (location = 6) in uint mode_in;
// The width in cells of this item.
layout (location = 6) in uint grid_width;
layout (location = 7) in uint grid_width;
// The background or foreground color for the fragment, depending on
// whether this is a background or foreground pass.
@ -54,6 +58,7 @@ uniform vec2 cell_size;
uniform mat4 projection;
uniform float strikethrough_position;
uniform float strikethrough_thickness;
uniform float min_contrast;
/********************************************************************
* Modes
@ -75,6 +80,61 @@ uniform float strikethrough_thickness;
*
*/
//-------------------------------------------------------------------
// Color Functions
//-------------------------------------------------------------------
// https://www.w3.org/TR/2008/REC-WCAG20-20081211/#relativeluminancedef
float luminance_component(float c) {
if (c <= 0.03928) {
return c / 12.92;
} else {
return pow((c + 0.055) / 1.055, 2.4);
}
}
float relative_luminance(vec3 color) {
vec3 color_adjusted = vec3(
luminance_component(color.r),
luminance_component(color.g),
luminance_component(color.b)
);
vec3 weights = vec3(0.2126, 0.7152, 0.0722);
return dot(color_adjusted, weights);
}
// https://www.w3.org/TR/2008/REC-WCAG20-20081211/#contrast-ratiodef
float contrast_ratio(vec3 color1, vec3 color2) {
float luminance1 = relative_luminance(color1) + 0.05;
float luminance2 = relative_luminance(color2) + 0.05;
return max(luminance1, luminance2) / min(luminance1, luminance2);
}
// Return the fg if the contrast ratio is greater than min, otherwise
// return a color that satisfies the contrast ratio. Currently, the color
// is always white or black, whichever has the highest contrast ratio.
vec4 contrasted_color(float min_ratio, vec4 fg, vec4 bg) {
vec3 fg_premult = fg.rgb * fg.a;
vec3 bg_premult = bg.rgb * bg.a;
float ratio = contrast_ratio(fg_premult, bg_premult);
if (ratio < min_ratio) {
float white_ratio = contrast_ratio(vec3(1.0, 1.0, 1.0), bg_premult);
float black_ratio = contrast_ratio(vec3(0.0, 0.0, 0.0), bg_premult);
if (white_ratio > black_ratio) {
return vec4(1.0, 1.0, 1.0, fg.a);
} else {
return vec4(0.0, 0.0, 0.0, fg.a);
}
}
return fg;
}
//-------------------------------------------------------------------
// Main
//-------------------------------------------------------------------
void main() {
// We always forward our mode unmasked because the fragment
// shader doesn't use any of the masks.
@ -147,8 +207,15 @@ void main() {
vec2 glyph_tex_size = glyph_size / text_size;
glyph_tex_coords = glyph_tex_pos + glyph_tex_size * position;
// Set our foreground color output
color = color_in / 255.;
// 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.
vec4 color_final = color_in / 255.0;
if (min_contrast > 1.0 && mode == MODE_FG) {
vec4 bg_color = bg_color_in / 255.0;
color_final = contrasted_color(min_contrast, color_final, bg_color);
}
color = color_final;
break;
case MODE_STRIKETHROUGH: