renderer/Metal: improve linear blending correction

More mathematically sound approach, does a much better job of matching
the appearance of non-linear blending. Removed `experimental` from name
because it's not really an experiment anymore.
This commit is contained in:
Qwerasd
2025-01-27 19:15:18 -05:00
parent ac568900a5
commit 5c8f984ea1
4 changed files with 63 additions and 41 deletions

View File

@ -273,10 +273,9 @@ 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.
/// * `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.
///
/// 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

View File

@ -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.
@ -480,6 +488,12 @@ vertex CellTextVertexOut cell_text_vertex(
);
}
// 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;
}
@ -518,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.