mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-13 23:36:09 +03:00
renderer/Metal: improve linear blending correction (#5401)
While I actually do personally prefer the previous style's appearance, I have a feeling this version will be much more popular, since it essentially replicates the appearance of non-linear blending but without any fringing artifacts. Details are explained in the comments and commit messages. <details> <summary> <h3>Screenshots for comparison</h3> </summary> *(open images in separate tabs and make sure they're at 100% scale for proper comparison)* |blending|screenshot| |-|-| |`native`|| |`linear`|| |`linear-corrected` (old)|| |`linear-corrected` (new)|| </details>
This commit is contained in:
@ -259,7 +259,8 @@ pub const renamed = std.StaticStringMap([]const u8).initComptime(&.{
|
||||
|
||||
/// What color space to use when performing alpha blending.
|
||||
///
|
||||
/// This affects how text looks for different background/foreground color pairs.
|
||||
/// This affects the appearance of text and of any images with transparency.
|
||||
/// Additionally, custom shaders will receive colors in the configured space.
|
||||
///
|
||||
/// Valid values:
|
||||
///
|
||||
@ -273,15 +274,10 @@ pub const renamed = std.StaticStringMap([]const u8).initComptime(&.{
|
||||
/// This is also sometimes known as "gamma correction".
|
||||
/// (Currently only supported on macOS. Has no effect on Linux.)
|
||||
///
|
||||
/// * `linear-corrected` - Corrects the thinning/thickening effect of linear
|
||||
/// by applying a correction curve to the text alpha depending on its
|
||||
/// brightness. This compensates for the thinning and makes the weight of
|
||||
/// most text appear very similar to when it's blended non-linearly.
|
||||
///
|
||||
/// Note: This setting affects more than just text, images will also be blended
|
||||
/// in the selected color space, and custom shaders will receive colors in that
|
||||
/// color space as well.
|
||||
@"text-blending": TextBlending = .native,
|
||||
/// * `linear-corrected` - Same as `linear`, but with a correction step applied
|
||||
/// for text that makes it look nearly or completely identical to `native`,
|
||||
/// but without any of the darkening artifacts.
|
||||
@"alpha-blending": AlphaBlending = .native,
|
||||
|
||||
/// All of the configurations behavior adjust various metrics determined by the
|
||||
/// font. The values can be integers (1, -1, etc.) or a percentage (20%, -15%,
|
||||
@ -1221,12 +1217,16 @@ keybind: Keybinds = .{},
|
||||
/// This is currently only supported on macOS and Linux.
|
||||
@"window-theme": WindowTheme = .auto,
|
||||
|
||||
/// The colorspace to use for the terminal window. The default is `srgb` but
|
||||
/// this can also be set to `display-p3` to use the Display P3 colorspace.
|
||||
/// The color space to use when interpreting terminal colors. "Terminal colors"
|
||||
/// refers to colors specified in your configuration and colors produced by
|
||||
/// direct-color SGR sequences.
|
||||
///
|
||||
/// Changing this value at runtime will only affect new windows.
|
||||
/// Valid values:
|
||||
///
|
||||
/// This setting is only supported on macOS.
|
||||
/// * `srgb` - Interpret colors in the sRGB color space. This is the default.
|
||||
/// * `display-p3` - Interpret colors in the Display P3 color space.
|
||||
///
|
||||
/// This setting is currently only supported on macOS.
|
||||
@"window-colorspace": WindowColorspace = .srgb,
|
||||
|
||||
/// The initial window size. This size is in terminal grid cells by default.
|
||||
@ -5826,13 +5826,13 @@ pub const GraphemeWidthMethod = enum {
|
||||
unicode,
|
||||
};
|
||||
|
||||
/// See text-blending
|
||||
pub const TextBlending = enum {
|
||||
/// See alpha-blending
|
||||
pub const AlphaBlending = enum {
|
||||
native,
|
||||
linear,
|
||||
@"linear-corrected",
|
||||
|
||||
pub fn isLinear(self: TextBlending) bool {
|
||||
pub fn isLinear(self: AlphaBlending) bool {
|
||||
return switch (self) {
|
||||
.native => false,
|
||||
.linear, .@"linear-corrected" => true,
|
||||
|
@ -391,7 +391,7 @@ pub const DerivedConfig = struct {
|
||||
links: link.Set,
|
||||
vsync: bool,
|
||||
colorspace: configpkg.Config.WindowColorspace,
|
||||
blending: configpkg.Config.TextBlending,
|
||||
blending: configpkg.Config.AlphaBlending,
|
||||
|
||||
pub fn init(
|
||||
alloc_gpa: Allocator,
|
||||
@ -463,7 +463,7 @@ pub const DerivedConfig = struct {
|
||||
.links = links,
|
||||
.vsync = config.@"window-vsync",
|
||||
.colorspace = config.@"window-colorspace",
|
||||
.blending = config.@"text-blending",
|
||||
.blending = config.@"alpha-blending",
|
||||
.arena = arena,
|
||||
};
|
||||
}
|
||||
@ -667,7 +667,7 @@ pub fn init(alloc: Allocator, options: renderer.Options) !Metal {
|
||||
.cursor_wide = false,
|
||||
.use_display_p3 = options.config.colorspace == .@"display-p3",
|
||||
.use_linear_blending = options.config.blending.isLinear(),
|
||||
.use_experimental_linear_correction = options.config.blending == .@"linear-corrected",
|
||||
.use_linear_correction = options.config.blending == .@"linear-corrected",
|
||||
},
|
||||
|
||||
// Fonts
|
||||
@ -2099,7 +2099,7 @@ pub fn changeConfig(self: *Metal, config: *DerivedConfig) !void {
|
||||
// Set our new color space and blending
|
||||
self.uniforms.use_display_p3 = config.colorspace == .@"display-p3";
|
||||
self.uniforms.use_linear_blending = config.blending.isLinear();
|
||||
self.uniforms.use_experimental_linear_correction = config.blending == .@"linear-corrected";
|
||||
self.uniforms.use_linear_correction = config.blending == .@"linear-corrected";
|
||||
|
||||
// Set our new colors
|
||||
self.default_background_color = config.background;
|
||||
@ -2242,7 +2242,7 @@ pub fn setScreenSize(
|
||||
.cursor_wide = old.cursor_wide,
|
||||
.use_display_p3 = old.use_display_p3,
|
||||
.use_linear_blending = old.use_linear_blending,
|
||||
.use_experimental_linear_correction = old.use_experimental_linear_correction,
|
||||
.use_linear_correction = old.use_linear_correction,
|
||||
};
|
||||
|
||||
// Reset our cell contents if our grid size has changed.
|
||||
|
@ -158,7 +158,7 @@ pub const Uniforms = extern struct {
|
||||
/// Enables a weight correction step that makes text rendered
|
||||
/// with linear alpha blending have a similar apparent weight
|
||||
/// (thickness) to gamma-incorrect blending.
|
||||
use_experimental_linear_correction: bool align(1) = false,
|
||||
use_linear_correction: bool align(1) = false,
|
||||
|
||||
const PaddingExtend = packed struct(u8) {
|
||||
left: bool = false,
|
||||
|
@ -22,7 +22,7 @@ struct Uniforms {
|
||||
bool cursor_wide;
|
||||
bool use_display_p3;
|
||||
bool use_linear_blending;
|
||||
bool use_experimental_linear_correction;
|
||||
bool use_linear_correction;
|
||||
};
|
||||
|
||||
//-------------------------------------------------------------------
|
||||
@ -59,22 +59,28 @@ float3 srgb_to_display_p3(float3 srgb) {
|
||||
|
||||
// Converts a color from sRGB gamma encoding to linear.
|
||||
float4 linearize(float4 srgb) {
|
||||
bool3 cutoff = srgb.rgb <= 0.04045;
|
||||
float3 lower = srgb.rgb / 12.92;
|
||||
float3 higher = pow((srgb.rgb + 0.055) / 1.055, 2.4);
|
||||
srgb.rgb = mix(higher, lower, float3(cutoff));
|
||||
bool3 cutoff = srgb.rgb <= 0.04045;
|
||||
float3 lower = srgb.rgb / 12.92;
|
||||
float3 higher = pow((srgb.rgb + 0.055) / 1.055, 2.4);
|
||||
srgb.rgb = mix(higher, lower, float3(cutoff));
|
||||
|
||||
return srgb;
|
||||
return srgb;
|
||||
}
|
||||
float linearize(float v) {
|
||||
return v <= 0.04045 ? v / 12.92 : pow((v + 0.055) / 1.055, 2.4);
|
||||
}
|
||||
|
||||
// Converts a color from linear to sRGB gamma encoding.
|
||||
float4 unlinearize(float4 linear) {
|
||||
bool3 cutoff = linear.rgb <= 0.0031308;
|
||||
float3 lower = linear.rgb * 12.92;
|
||||
float3 higher = pow(linear.rgb, 1.0 / 2.4) * 1.055 - 0.055;
|
||||
linear.rgb = mix(higher, lower, float3(cutoff));
|
||||
bool3 cutoff = linear.rgb <= 0.0031308;
|
||||
float3 lower = linear.rgb * 12.92;
|
||||
float3 higher = pow(linear.rgb, 1.0 / 2.4) * 1.055 - 0.055;
|
||||
linear.rgb = mix(higher, lower, float3(cutoff));
|
||||
|
||||
return linear;
|
||||
return linear;
|
||||
}
|
||||
float unlinearize(float v) {
|
||||
return v <= 0.0031308 ? v * 12.92 : pow(v, 1.0 / 2.4) * 1.055 - 0.055;
|
||||
}
|
||||
|
||||
// Compute the luminance of the provided color.
|
||||
@ -353,8 +359,9 @@ struct CellTextVertexIn {
|
||||
|
||||
struct CellTextVertexOut {
|
||||
float4 position [[position]];
|
||||
uint8_t mode;
|
||||
float4 color;
|
||||
uint8_t mode [[flat]];
|
||||
float4 color [[flat]];
|
||||
float4 bg_color [[flat]];
|
||||
float2 tex_coord;
|
||||
};
|
||||
|
||||
@ -445,6 +452,13 @@ vertex CellTextVertexOut cell_text_vertex(
|
||||
true
|
||||
);
|
||||
|
||||
// Get the BG color
|
||||
out.bg_color = load_color(
|
||||
bg_colors[in.grid_pos.y * uniforms.grid_size.x + in.grid_pos.x],
|
||||
uniforms.use_display_p3,
|
||||
true
|
||||
);
|
||||
|
||||
// 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.
|
||||
@ -453,14 +467,8 @@ vertex CellTextVertexOut cell_text_vertex(
|
||||
// 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) {
|
||||
// Get the BG color
|
||||
float4 bg_color = load_color(
|
||||
bg_colors[in.grid_pos.y * uniforms.grid_size.x + in.grid_pos.x],
|
||||
uniforms.use_display_p3,
|
||||
true
|
||||
);
|
||||
// Ensure our minimum contrast
|
||||
out.color = contrasted_color(uniforms.min_contrast, out.color, bg_color);
|
||||
out.color = contrasted_color(uniforms.min_contrast, out.color, out.bg_color);
|
||||
}
|
||||
|
||||
// If this cell is the cursor cell, then we need to change the color.
|
||||
@ -473,7 +481,17 @@ vertex CellTextVertexOut cell_text_vertex(
|
||||
) &&
|
||||
in.grid_pos.y == uniforms.cursor_pos.y
|
||||
) {
|
||||
out.color = float4(uniforms.cursor_color) / 255.0f;
|
||||
out.color = load_color(
|
||||
uniforms.cursor_color,
|
||||
uniforms.use_display_p3,
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
// Don't bother rendering if the bg and fg colors are identical, just return
|
||||
// the same point which will be culled because it makes the quad zero sized.
|
||||
if (all(out.color == out.bg_color)) {
|
||||
out.position = float4(0.0);
|
||||
}
|
||||
|
||||
return out;
|
||||
@ -514,19 +532,28 @@ fragment float4 cell_text_fragment(
|
||||
// Fetch our alpha mask for this pixel.
|
||||
float a = textureGrayscale.sample(textureSampler, in.tex_coord).r;
|
||||
|
||||
// Experimental linear blending weight correction.
|
||||
if (uniforms.use_experimental_linear_correction) {
|
||||
float l = luminance(color.rgb);
|
||||
|
||||
// TODO: This is a dynamic dilation term that biases
|
||||
// the alpha adjustment for small font sizes;
|
||||
// it should be computed by dividing the font
|
||||
// size in `pt`s by `13.0` and using that if
|
||||
// it's less than `1.0`, but for now it's
|
||||
// hard coded at 1.0, which has no effect.
|
||||
float d = 13.0 / 13.0;
|
||||
|
||||
a += pow(a, d + d * l) - pow(a, d + 1.0 - d * l);
|
||||
// Linear blending weight correction corrects the alpha value to
|
||||
// produce blending results which match gamma-incorrect blending.
|
||||
if (uniforms.use_linear_correction) {
|
||||
// Short explanation of how this works:
|
||||
//
|
||||
// We get the luminances of the foreground and background colors,
|
||||
// and then unlinearize them and perform blending on them. This
|
||||
// gives us our desired luminance, which we derive our new alpha
|
||||
// value from by mapping the range [bg_l, fg_l] to [0, 1], since
|
||||
// our final blend will be a linear interpolation from bg to fg.
|
||||
//
|
||||
// This yields virtually identical results for grayscale blending,
|
||||
// and very similar but non-identical results for color blending.
|
||||
float4 bg = in.bg_color;
|
||||
float fg_l = luminance(color.rgb);
|
||||
float bg_l = luminance(bg.rgb);
|
||||
// To avoid numbers going haywire, we don't apply correction
|
||||
// when the bg and fg luminances are within 0.001 of each other.
|
||||
if (abs(fg_l - bg_l) > 0.001) {
|
||||
float blend_l = linearize(unlinearize(fg_l) * a + unlinearize(bg_l) * (1.0 - a));
|
||||
a = clamp((blend_l - bg_l) / (fg_l - bg_l), 0.0, 1.0);
|
||||
}
|
||||
}
|
||||
|
||||
// Multiply our whole color by the alpha mask.
|
||||
|
Reference in New Issue
Block a user