From 5c8f984ea157bd40da631a17ddafcbf07a5b04db Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Mon, 27 Jan 2025 19:15:18 -0500 Subject: [PATCH] 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. --- src/config/Config.zig | 7 ++- src/renderer/Metal.zig | 6 +-- src/renderer/metal/shaders.zig | 2 +- src/renderer/shaders/cell.metal | 89 +++++++++++++++++++++------------ 4 files changed, 63 insertions(+), 41 deletions(-) diff --git a/src/config/Config.zig b/src/config/Config.zig index 839656169..c7aeebb01 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -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 diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig index 52a5437c6..dccccef17 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.zig @@ -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. diff --git a/src/renderer/metal/shaders.zig b/src/renderer/metal/shaders.zig index 62d363173..b297de809 100644 --- a/src/renderer/metal/shaders.zig +++ b/src/renderer/metal/shaders.zig @@ -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, diff --git a/src/renderer/shaders/cell.metal b/src/renderer/shaders/cell.metal index 1a6d17ea3..3ca0f9149 100644 --- a/src/renderer/shaders/cell.metal +++ b/src/renderer/shaders/cell.metal @@ -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.