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`|![native](https://github.com/user-attachments/assets/295bbab3-60a7-4915-93d9-a938082fa309)|

|`linear`|![linear](https://github.com/user-attachments/assets/a9a5a5ea-cf57-4730-8ac6-3ce1dc29c5f8)|
|`linear-corrected`
(old)|![linear-corrected_old](https://github.com/user-attachments/assets/4fd9510d-b798-4900-8c31-00e3f67fd806)|
|`linear-corrected`
(new)|![linear-corrected_new](https://github.com/user-attachments/assets/a379f307-db8a-4335-aa11-3ac71d01470e)|
</details>
This commit is contained in:
Mitchell Hashimoto
2025-01-28 09:59:56 -05:00
committed by GitHub
4 changed files with 84 additions and 57 deletions

View File

@ -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,

View File

@ -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.

View File

@ -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,

View File

@ -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.