Skip to content

Re-implement Rounded Corners Using Signed Distance Fields (SDF)#9091

Closed
bea4dev wants to merge 6 commits into
kovidgoyal:masterfrom
bea4dev:new-sdf
Closed

Re-implement Rounded Corners Using Signed Distance Fields (SDF)#9091
bea4dev wants to merge 6 commits into
kovidgoyal:masterfrom
bea4dev:new-sdf

Conversation

@bea4dev

@bea4dev bea4dev commented Oct 10, 2025

Copy link
Copy Markdown
Contributor

Based on the issues you pointed out previously, I’ve reimplemented the feature.
Anti-aliasing is now applied only to the rounded corner segments; straight lines are rendered without AA.
This implementation should align best with the intended behavior.
It also addresses the slight misalignment observed on some low-DPI configurations.

Thank you for your time—your review would be greatly appreciated.

  • before
image
  • after
image

@neurocyte

Copy link
Copy Markdown

This version is an improvement. But, the corners are a little darker than the horizontal/vertical lines though.

image

Still, this is still better than the previous version.

Unfortunately, it seems there is a bug at some font sizes:
image

@bea4dev

bea4dev commented Oct 10, 2025

Copy link
Copy Markdown
Contributor Author

This version is an improvement. But, the corners are a little darker than the horizontal/vertical lines though.
Still, this is still better than the previous version.

Unfortunately, it seems there is a bug at some font sizes: <img alt="image" width="195" height="166"

It probably appears darker than the straight segments because not applying AA to the straight lines creates a mismatch.
I’m considering making that behavior configurable via the settings.

I’d like to investigate the bug. Could you share your OS, scale factor, and font size?

@bea4dev

bea4dev commented Oct 10, 2025

Copy link
Copy Markdown
Contributor Author

@neurocyte

@neurocyte

Copy link
Copy Markdown
kitty 0.43.1 (2224f722cf) created by Kovid Goyal
Linux dent 6.17.1-arch1-1 #1 SMP PREEMPT_DYNAMIC Mon, 06 Oct 2025 18:48:29 +0000 x86_64
Arch Linux 6.17.1-arch1-1 (/dev/tty)
DISTRIB_ID="Arch"
DISTRIB_RELEASE="rolling"
DISTRIB_DESCRIPTION="Arch Linux"
Running under: Wayland (kwin 6.4.5) missing: blur
OpenGL: '3.1.0 NVIDIA 580.95.05' Detected version: 3.1
Frozen: False
Fonts:
  medium: Iosevka: /usr/share/fonts/TTF/Iosevka-Regular.ttc:0
          Features: ('onum', 'ss01', 'ss02', 'ss03', 'ss12', 'ss16')
    bold: Iosevka-Bold: /usr/share/fonts/TTF/Iosevka-Bold.ttc:0
          Features: ('onum', 'ss01', 'ss02', 'ss03', 'ss12', 'ss16')
  italic: Iosevka-Medium-Italic: /usr/share/fonts/TTF/Iosevka-Medium.ttc:7
          Features: ('onum', 'ss01', 'ss02', 'ss03', 'ss12', 'ss16')
      bi: Iosevka-Bold-Italic: /usr/share/fonts/TTF/Iosevka-Bold.ttc:7
          Features: ('onum', 'ss01', 'ss02', 'ss03', 'ss12', 'ss16')

font_size 18.0 to reproduce.

Display scaling is set to 100%.

@neurocyte

Copy link
Copy Markdown

The bug reproduces at sizes 6, 12, 18, 24, 30. So it's something to do with multiples of 6?

@neurocyte

Copy link
Copy Markdown

Different fonts reproduce at difference sizes. With JetBrains its 10, 12, 17, 18, 20, 25. Totally different pattern. Must be something to do with font metrics.

@bea4dev

bea4dev commented Oct 10, 2025

Copy link
Copy Markdown
Contributor Author

@neurocyte I’ve fixed it. Please take a look.

@bea4dev

bea4dev commented Oct 10, 2025

Copy link
Copy Markdown
Contributor Author

@neurocyte

image

I’ve added a configuration option to adjust anti-aliasing.
Please try setting the same value as shown below.

box_drawing_line_aa_strength 0.8
box_drawing_corner_aa_strength 0.8

(Using a value around 0.5 might produce cleaner rendering.)

@neurocyte

Copy link
Copy Markdown

Perfection! (with the 0.8 values)
image

You should make this the default.

@bea4dev

bea4dev commented Oct 10, 2025

Copy link
Copy Markdown
Contributor Author

In most cases, values between 0.5 and 0.8 work well. However, on high-DPI setups there’s also the issue that straight lines can look blurrier than necessary, which makes this tricky.

At the moment, the defaults are set as follows, and AA is not applied to straight lines:

box_drawing_line_aa_strength 0.0
box_drawing_corner_aa_strength 0.8

@kovidgoyal
I’ll leave the final decision up to you.

@alex-huff

alex-huff commented Oct 10, 2025

Copy link
Copy Markdown
Contributor

This does look much better for me as well.

Note: At low dpi having a non-zero box_drawing_line_aa_strength massively dulls out straight the line color.

--override=box_drawing_line_aa_strength=0.8 --override=box_drawing_corner_aa_strength=0.8:
screenshot-1760106331458958721

--override=box_drawing_corner_aa_strength=0.8:
screenshot-1760106359468447176

No args:
screenshot-1760106377536539708

@alex-huff

alex-huff commented Oct 10, 2025

Copy link
Copy Markdown
Contributor

I wonder if there is a way to make the corners brighter (or darker on light themes) instead of the straight lines duller.

@bea4dev

bea4dev commented Oct 10, 2025

Copy link
Copy Markdown
Contributor Author

Around 0.5 might be the sweet spot.

image

@bea4dev

bea4dev commented Oct 10, 2025

Copy link
Copy Markdown
Contributor Author

I wonder if there is a way to make the corners brighter (or darker on light themes) instead of the straight lines duller.

The anti-aliasing code we’re currently using is a simple implementation (the smoothstep function), so there may still be a chance that a different anti-aliasing method could resolve both issues.(It might be a good idea to use Chrome’s rounded-corner implementation as a reference.)

@alex-huff

alex-huff commented Oct 10, 2025

Copy link
Copy Markdown
Contributor

(It might be a good idea to use Chrome’s rounded-corner implementation as a reference.)

I messed with the css of this page to make the corners have a similar radius and width as kitty. Zooming into the topright corner of your comment, it appears that chromium is pretty much just cranking up the width for the curved part.

qutebrowser (chromimum based):
screenshot-1760112418541677012

And here is kitty after simply multiplying the stroke by 1.5:
screenshot-1760112651007427819

Zoomed out:
screenshot-1760112872955160286

But when I increase the box_drawing_scale above 1, it looks far too thick.

@kovidgoyal

kovidgoyal commented Oct 10, 2025

Copy link
Copy Markdown
Owner

So far it basically looks like the algorithm has to be fine tuned depending on two things: 1) line thickness 2) bg/fg contrast. Ideally, especially at low thickness we should not dull the straight lines but instead make the corner strokes a bit thicker. Probably have a matrix of fine tuning that looks like this:

Stroke width (px)  dark   light
1                  1.5    1.5   (only corner AA)
2                  1.2    1.2   (only corner AA)
3                  1.1    1.1   (corner less line AA)
rest               1      1     (corner AA less line AA)

the numbers are stroke width multipliers for corners. The actual numbers I used are just guesses. I guess one has to setup some kind of test harness to generate rendering at various thicknesses and colors to determine the best numbers. Or find out how chromium or some graphics library does it and copy their implementation.

Ideally I hope we can avoid having to AA the straight lines, it makes the colors dull and means they wont match other things like filled shapes or text colors.

The problem for us is we dont know the colors since we are just rendering masks that can be used in any color combination. So just key it off the default bg/fg color pair I guess.

@bea4dev

bea4dev commented Oct 10, 2025

Copy link
Copy Markdown
Contributor Author

For reference, I reviewed Skia’s AAA implementation, and if we were to replicate it, we might need to significantly change the current code.
It seems our options are:

  1. Make the value configurable and leave some tuning to users
  2. Explore other anti-aliasing methods
  3. Make substantial changes to bring our code closer to Skia’s implementation

My view is that option 1 is fine for the time being (though the default values could be improved).
I’ll also keep exploring whether option 2 is feasible.

@alex-huff

alex-huff commented Oct 10, 2025

Copy link
Copy Markdown
Contributor

we might need to significantly change the current code.

It looks like no matter what I set box_drawing_scale to, the width of vlines and hlines is always at least 2px. Could it be as simple as always using a stroke of at least 2 for rounded corners to account for this?

diff --git a/kitty/decorations.c b/kitty/decorations.c
index 6c7e67fa3..37d6f24b7 100644
--- a/kitty/decorations.c
+++ b/kitty/decorations.c
@@ -1321,9 +1321,7 @@ rounded_corner(Canvas *self, uint level, Corner which) {
     // Render a rounded box corner; AA is forced to 0.0 for straight segments and to the user-configured strength for curved pixels.
     const double stroke_x = thickness_as_float(self, level, true);
     const double stroke_y = thickness_as_float(self, level, false);
-    const double stroke = fmax(stroke_x, stroke_y);
-    if (stroke <= 0.0) return;
-
+    const double stroke = fmax(2.0, fmax(stroke_x, stroke_y));
     const double Hx = (double)half_width(self);
     const double Hy = (double)half_height(self);
     const double corner_radius = fmin(Hx, Hy);

Or if that is not enough, it could be enough use a table like Kovid suggested.

@guzman109

guzman109 commented Oct 10, 2025

Copy link
Copy Markdown
Screenshot 2025-10-10 at 12 24 51 PM

Confirming that it looks perfect with

--override=box_drawing_line_aa_strength=0.8 --override=box_drawing_corner_aa_strength=0.8

Using MonoLisa Font

@bea4dev

bea4dev commented Oct 10, 2025

Copy link
Copy Markdown
Contributor Author
image

I may have implemented it perfectly using ceil().
Please take a look.
@alex-huff
@kovidgoyal

@alex-huff

alex-huff commented Oct 10, 2025

Copy link
Copy Markdown
Contributor

Please take a look. @alex-huff @kovidgoyal

This does fix the mixmatch of widths between the straight and rounded segments.

In rounded_corner could you somehow calculate the pixel width of vline/hline and use that as stroke without needing to force everything to be even widths?

Edit: nevermind the non AA straight lines were already always even px width before this change.

This looks pretty good to me at various widths and colorschemes

screenshot-1760124874714130547 screenshot-1760124888325060590 screenshot-1760124903034787054 screenshot-1760124916427214125 screenshot-1760124998305363350 screenshot-1760125082706875420 screenshot-1760125111880904812 screenshot-1760125142981613011

@alex-huff

alex-huff commented Oct 10, 2025

Copy link
Copy Markdown
Contributor

@bea4dev

This breaks Box Drawings Heavy Double Dash Vertical (U+254F) and related characters.

master:
screenshot-1760128780048774890
pr:
screenshot-1760128681201747715

@alex-huff

alex-huff commented Oct 10, 2025

Copy link
Copy Markdown
Contributor

This breaks Box Drawings Heavy Double Dash Vertical (U+254F) and related characters.

I've been playing around with this and found that reverting draw_hline and draw_vline to the way they were before this pr fixes all the problems introduced with characters like Box Drawings Heavy Double Dash Vertical (U+254F) and also keeps line widths consistent with master. Using the old draw_hline and draw_vline also allows for the user to have odd line widths. Also, calculating the stroke for the rounded corners is easy since you can use hline_limits and vline_limits to get the exact pixel width that a connecting hline or vline would be. This approach does remove the possibility of using AA for the straight lines, but I don't think this is needed.

The code probably needs some cleaning but in case you want to use this as inspiration:

diff --git a/kitty/decorations.c b/kitty/decorations.c
index fda690c5a..77564c585 100644
--- a/kitty/decorations.c
+++ b/kitty/decorations.c
@@ -318,69 +318,41 @@ add_vholes(Canvas *self, uint level, uint num) {
 static double clamp_double(double value, double lower, double upper);
 static double smoothstep(double edge0, double edge1, double x);
 
+static Range
+hline_limits(Canvas *self, uint y, uint level) {
+    uint sz = thickness(self, level, false);
+    Range r = {.start=minus(y, sz / 2)};
+    r.end = min(r.start + sz, self->height);
+    return r;
+}
+
 static void
 draw_hline(Canvas *self, uint x1, uint x2, uint y, uint level) {
-    if (x1 >= x2 || y >= self->height) return;
-
-    double stroke = fmax(1.0, thickness_as_float(self, level, false));
-    stroke = ceil(stroke);
-
-    if ((uint)stroke % 2 != 0) {
-        stroke += 1.0;
+    // Draw a horizontal line between [x1, x2) centered at y with the thickness given by level and self->supersample_factor
+    Range r = hline_limits(self, y, level);
+    for (uint y = r.start; y < r.end; y++) {
+        uint8_t *py = self->mask + y * self->width;
+        memset(py + x1, 255, minus(min(x2, self->width), x1));
     }
+}
 
-    const double half_stroke = 0.5 * stroke;
-    const double aa = (double)self->supersample_factor * OPT(box_drawing_line_aa_strength);
-
-    if (x1 > self->width) x1 = self->width;
-    if (x2 > self->width) x2 = self->width;
-
-    for (uint py = 0; py < self->height; py++) {
-        const double pos_y = (double)py - (double)y + 0.5;
-        const double dist = fabs(pos_y);
-        const double outer = half_stroke - dist;
-        const double inner = -half_stroke - dist;
-        const double alpha = smoothstep(-aa, aa, outer) - smoothstep(-aa, aa, inner);
-        if (alpha <= 0.0) continue;
-
-        const uint8_t val = (uint8_t)lrint(clamp_double(alpha, 0.0, 1.0) * 255.0);
-        uint8_t *row = self->mask + (size_t)py * self->width;
-        for (uint px = x1; px < x2; px++) {
-            if (val > row[px]) row[px] = val;
-        }
-    }
+static Range
+vline_limits(Canvas *self, uint x, uint level) {
+    uint sz = thickness(self, level, true);
+    Range r = {.start = minus(x, sz / 2)};
+    r.end = min(r.start + sz, self->width);
+    return r;
 }
 
+
 static void
 draw_vline(Canvas *self, uint y1, uint y2, uint x, uint level) {
-    if (y1 >= y2 || x >= self->width) return;
-
-    double stroke = fmax(1.0, thickness_as_float(self, level, true));
-    stroke = ceil(stroke);
-
-    if ((uint)stroke % 2 != 0) {
-        stroke += 1.0;
-    }
-
-    const double half_stroke = 0.5 * stroke;
-    const double aa = (double)self->supersample_factor * OPT(box_drawing_line_aa_strength);
-
-    if (y1 > self->height) y1 = self->height;
-    if (y2 > self->height) y2 = self->height;
-
-    for (uint px = 0; px < self->width; px++) {
-        const double pos_x = (double)px - (double)x + 0.5;
-        const double dist = fabs(pos_x);
-        const double outer = half_stroke - dist;
-        const double inner = -half_stroke - dist;
-        const double alpha = smoothstep(-aa, aa, outer) - smoothstep(-aa, aa, inner);
-        if (alpha <= 0.0) continue;
-
-        const uint8_t val = (uint8_t)lrint(clamp_double(alpha, 0.0, 1.0) * 255.0);
-        for (uint py = y1; py < y2; py++) {
-            uint8_t *p = &self->mask[(size_t)py * self->width + px];
-            if (val > *p) *p = val;
-        }
+    // Draw a vertical line between [y1, y2) centered at x with the thickness given by level and self->supersample_factor
+    Range r = vline_limits(self, x, level);
+    uint xsz = minus(r.end, r.start);
+    for (uint y = y1; y < min(y2, self->height); y++) {
+        uint8_t *py = self->mask + y * self->width;
+        memset(py + r.start, 255, xsz);
     }
 }
 
@@ -1332,39 +1304,36 @@ fading_vline(Canvas *self, uint level, uint num, Edge fade) {
 static void
 rounded_corner(Canvas *self, uint level, Corner which) {
     // Render a rounded box corner; AA is forced to 0.0 for straight segments and to the user-configured strength for curved pixels.
-    const double stroke_x = thickness_as_float(self, level, true);
-    const double stroke_y = thickness_as_float(self, level, false);
-    double stroke = fmax(stroke_x, stroke_y);
-    if (stroke <= 0.0) return;
-
-    stroke = ceil(stroke);
-
-    if ((uint)stroke % 2 != 0) {
-        stroke += 1.0;
-    }
-
-    const double Hx = (double)half_width(self);
-    const double Hy = (double)half_height(self);
-    const double corner_radius = fmin(Hx, Hy);
-    const double bx = Hx - corner_radius;
-    const double by = Hy - corner_radius;
+    const uint Hx = half_width(self);
+    const uint Hy = half_height(self);
+    const Range hori_line_range = hline_limits(self, Hy, level);
+    const Range vert_line_range = vline_limits(self, Hx, level);
+    const uint hori_line_height = hori_line_range.end - hori_line_range.start;
+    const uint vert_line_width = vert_line_range.end - vert_line_range.start;
+    double adjusted_Hx = (double)Hx;
+    double adjusted_Hy = (double)Hy;
+    if (hori_line_height % 2 != 0) adjusted_Hy += 0.5;
+    if (vert_line_width % 2 != 0) adjusted_Hx += 0.5;
+    const double stroke = (double)max(hori_line_height, vert_line_width);
+    const double corner_radius = fmin(adjusted_Hx, adjusted_Hy);
+    const double bx = adjusted_Hx - corner_radius;
+    const double by = adjusted_Hy - corner_radius;
 
     // Anti-aliasing on corners controlled by configuration
     const double aa_corner = (double)self->supersample_factor * OPT(box_drawing_corner_aa_strength);
-    const double aa_line = (double)self->supersample_factor * OPT(box_drawing_line_aa_strength);
     const double half_stroke = 0.5 * stroke;
 
-    const double x_shift = (which & RIGHT_EDGE) ? Hx : -Hx;
-    const double y_shift = (which & TOP_EDGE) ? -Hy : Hy;
+    const double x_shift = (which & RIGHT_EDGE) ? adjusted_Hx : -adjusted_Hx;
+    const double y_shift = (which & TOP_EDGE) ? -adjusted_Hy : adjusted_Hy;
 
     for (uint y = 0; y < self->height; y++) {
         const double sample_y = (double)y + y_shift + 0.5;
-        const double pos_y = sample_y - Hy;
+        const double pos_y = sample_y - adjusted_Hy;
         const uint row_off = y * self->width;
 
         for (uint x = 0; x < self->width; x++) {
             const double sample_x = (double)x + x_shift + 0.5;
-            const double pos_x = sample_x - Hx;
+            const double pos_x = sample_x - adjusted_Hx;
 
             const double qx = fabs(pos_x) - bx;
             const double qy = fabs(pos_y) - by;
@@ -1372,7 +1341,7 @@ rounded_corner(Canvas *self, uint level, Corner which) {
             const double dy = qy > 0.0 ? qy : 0.0;
             const double dist = hypot(dx, dy) + fmin(fmax(qx, qy), 0.0) - corner_radius;
 
-            const double aa = (qx > 1e-7 && qy > 1e-7) ? aa_corner : aa_line;
+            const double aa = (qx > 1e-7 && qy > 1e-7) ? aa_corner : 0.0;
             const double outer = half_stroke - dist;
             const double inner = -half_stroke - dist;
             const double alpha = smoothstep(-aa, aa, outer) - smoothstep(-aa, aa, inner);

This even works with single pixel lines:
image

@bea4dev

bea4dev commented Oct 11, 2025

Copy link
Copy Markdown
Contributor Author

This breaks Box Drawings Heavy Double Dash Vertical (U+254F) and related characters.

I've been playing around with this and found that reverting draw_hline and draw_vline to the way they were before this pr fixes all the problems introduced with characters like Box Drawings Heavy Double Dash Vertical (U+254F) and also keeps line widths consistent with master. Using the old draw_hline and draw_vline also allows for the user to have odd line widths. Also, calculating the stroke for the rounded corners is easy since you can use hline_limits and vline_limits to get the exact pixel width that a connecting hline or vline would be. This approach does remove the possibility of using AA for the straight lines, but I don't think this is needed.

Thank you for your help.
I’ll investigate and try implementing it based on your proposal.

@bea4dev

bea4dev commented Oct 11, 2025

Copy link
Copy Markdown
Contributor Author

I’ve verified that all lines render well and that the corner joints connect cleanly.
Please take a look.
Many thanks to @alex-huff for the idea.

image image

@alex-huff
@kovidgoyal

@alex-huff

Copy link
Copy Markdown
Contributor

Thanks for working on this @bea4dev, rounded corners looks much better now.

@kovidgoyal

Copy link
Copy Markdown
Owner

Yes, hopefully this issue can be put permanently to bed thanks to your efforts.

@ArtemChandragupta

Copy link
Copy Markdown

Marvelous results, congratulations!

zchee pushed a commit to zchee/kitty that referenced this pull request Jul 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants