From fff16bff6928e14a6e5f342f5578f73b0b998733 Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Sat, 5 Jul 2025 20:36:35 -0600 Subject: [PATCH 1/4] font/coretext: fix bitmap size calculation, prevent clipping Previously, many glyphs were having their top and right row/column of pixels clipped off due to not accounting for the slight bearing in the width and height calculation here. --- src/font/face/coretext.zig | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/font/face/coretext.zig b/src/font/face/coretext.zig index 35f094848..89d771d95 100644 --- a/src/font/face/coretext.zig +++ b/src/font/face/coretext.zig @@ -356,8 +356,14 @@ pub const Face = struct { const x = glyph_size.x; const y = glyph_size.y; - const px_width: u32 = @intFromFloat(@ceil(width)); - const px_height: u32 = @intFromFloat(@ceil(height)); + // We have to include the fractional pixels that we won't be offsetting + // in our width and height calculations, that is, we offset by the floor + // of the bearings when we render the glyph, meaning there's still a bit + // of extra width to the area that's drawn in beyond just the width of + // the glyph itself, so we include that extra fraction of a pixel when + // calculating the width and height here. + const px_width: u32 = @intFromFloat(@ceil(width + rect.origin.x - @floor(rect.origin.x))); + const px_height: u32 = @intFromFloat(@ceil(height + rect.origin.y - @floor(rect.origin.y))); // Settings that are specific to if we are rendering text or emoji. const color: struct { From 02d82720d26b222ad0c768d95395d7f4bea864c6 Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Sat, 5 Jul 2025 20:40:12 -0600 Subject: [PATCH 2/4] font/freetype: fix negated force-autohint flag The behavior of this flag was the opposite of its description in the docs- luckily, at the same time, the default (true) was the opposite from what the default actually is in freetype, so users who haven't explicitly set this flag won't see a behavior difference from this. --- src/config/Config.zig | 4 ++-- src/font/face/freetype.zig | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/config/Config.zig b/src/config/Config.zig index 653ce4178..2910372f3 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -435,7 +435,7 @@ pub const compatibility = std.StaticStringMap( /// * `hinting` - Enable or disable hinting. Enabled by default. /// /// * `force-autohint` - Always use the freetype auto-hinter instead of -/// the font's native hinter. Enabled by default. +/// the font's native hinter. Disabled by default. /// /// * `monochrome` - Instructs renderer to use 1-bit monochrome rendering. /// This will disable anti-aliasing, and probably not look very good unless @@ -7084,7 +7084,7 @@ pub const FreetypeLoadFlags = packed struct { // for Freetype itself. Ghostty hasn't made any opinionated changes // to these defaults. hinting: bool = true, - @"force-autohint": bool = true, + @"force-autohint": bool = false, monochrome: bool = false, autohint: bool = true, }; diff --git a/src/font/face/freetype.zig b/src/font/face/freetype.zig index c23ede04a..585d21c5b 100644 --- a/src/font/face/freetype.zig +++ b/src/font/face/freetype.zig @@ -348,7 +348,7 @@ pub const Face = struct { // use options from config .no_hinting = !do_hinting, - .force_autohint = !self.load_flags.@"force-autohint", + .force_autohint = self.load_flags.@"force-autohint", .no_autohint = !self.load_flags.autohint, // NO_SVG set to true because we don't currently support rendering From 87f35bd1c12dd8b50221163f9fc10d17d09eb32a Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Sat, 5 Jul 2025 20:58:15 -0600 Subject: [PATCH 3/4] renderer/opengl: explicit texture options This sets up for a couple improvments (see TODO comments) and also sets the glyph atlas textures to nearest neighbor sampling since we can do that now that we never scale glyphs. --- pkg/opengl/Texture.zig | 24 ++++++++++++++++++++++++ src/renderer/OpenGL.zig | 18 ++++++++++++++++++ src/renderer/opengl/Texture.zig | 12 ++++++++---- 3 files changed, 50 insertions(+), 4 deletions(-) diff --git a/pkg/opengl/Texture.zig b/pkg/opengl/Texture.zig index 2c8e05eff..03e794855 100644 --- a/pkg/opengl/Texture.zig +++ b/pkg/opengl/Texture.zig @@ -92,6 +92,30 @@ pub const Format = enum(c_uint) { _, }; +/// Minification filter for textures. +pub const MinFilter = enum(c_int) { + nearest = c.GL_NEAREST, + linear = c.GL_LINEAR, + nearest_mipmap_nearest = c.GL_NEAREST_MIPMAP_NEAREST, + linear_mipmap_nearest = c.GL_LINEAR_MIPMAP_NEAREST, + nearest_mipmap_linear = c.GL_NEAREST_MIPMAP_LINEAR, + linear_mipmap_linear = c.GL_LINEAR_MIPMAP_LINEAR, +}; + +/// Magnification filter for textures. +pub const MagFilter = enum(c_int) { + nearest = c.GL_NEAREST, + linear = c.GL_LINEAR, +}; + +/// Texture coordinate wrapping mode. +pub const Wrap = enum(c_int) { + clamp_to_edge = c.GL_CLAMP_TO_EDGE, + clamp_to_border = c.GL_CLAMP_TO_BORDER, + mirrored_repeat = c.GL_MIRRORED_REPEAT, + repeat = c.GL_REPEAT, +}; + /// Data type for texture images. pub const DataType = enum(c_uint) { UnsignedByte = c.GL_UNSIGNED_BYTE, diff --git a/src/renderer/OpenGL.zig b/src/renderer/OpenGL.zig index 00df8e273..882d6fc03 100644 --- a/src/renderer/OpenGL.zig +++ b/src/renderer/OpenGL.zig @@ -356,6 +356,10 @@ pub inline fn textureOptions(self: OpenGL) Texture.Options { .format = .rgba, .internal_format = .srgba, .target = .@"2D", + .min_filter = .linear, + .mag_filter = .linear, + .wrap_s = .clamp_to_edge, + .wrap_t = .clamp_to_edge, }; } @@ -388,6 +392,16 @@ pub inline fn imageTextureOptions( .format = format.toPixelFormat(), .internal_format = if (srgb) .srgba else .rgba, .target = .@"2D", + // TODO: Generate mipmaps for image textures and use + // linear_mipmap_linear filtering so that they + // look good even when scaled way down. + .min_filter = .linear, + .mag_filter = .linear, + // TODO: Separate out background image options, use + // repeating coordinate modes so we don't have + // to do the modulus in the shader. + .wrap_s = .clamp_to_edge, + .wrap_t = .clamp_to_edge, }; } @@ -409,6 +423,10 @@ pub fn initAtlasTexture( .format = format, .internal_format = internal_format, .target = .Rectangle, + .min_filter = .nearest, + .mag_filter = .nearest, + .wrap_s = .clamp_to_edge, + .wrap_t = .clamp_to_edge, }, atlas.size, atlas.size, diff --git a/src/renderer/opengl/Texture.zig b/src/renderer/opengl/Texture.zig index 9be2b7078..2f3e7f46a 100644 --- a/src/renderer/opengl/Texture.zig +++ b/src/renderer/opengl/Texture.zig @@ -16,6 +16,10 @@ pub const Options = struct { format: gl.Texture.Format, internal_format: gl.Texture.InternalFormat, target: gl.Texture.Target, + min_filter: gl.Texture.MinFilter, + mag_filter: gl.Texture.MagFilter, + wrap_s: gl.Texture.Wrap, + wrap_t: gl.Texture.Wrap, }; texture: gl.Texture, @@ -48,10 +52,10 @@ pub fn init( { const texbind = tex.bind(opts.target) catch return error.OpenGLFailed; defer texbind.unbind(); - texbind.parameter(.WrapS, gl.c.GL_CLAMP_TO_EDGE) catch return error.OpenGLFailed; - texbind.parameter(.WrapT, gl.c.GL_CLAMP_TO_EDGE) catch return error.OpenGLFailed; - texbind.parameter(.MinFilter, gl.c.GL_LINEAR) catch return error.OpenGLFailed; - texbind.parameter(.MagFilter, gl.c.GL_LINEAR) catch return error.OpenGLFailed; + texbind.parameter(.WrapS, @intFromEnum(opts.wrap_s)) catch return error.OpenGLFailed; + texbind.parameter(.WrapT, @intFromEnum(opts.wrap_t)) catch return error.OpenGLFailed; + texbind.parameter(.MinFilter, @intFromEnum(opts.min_filter)) catch return error.OpenGLFailed; + texbind.parameter(.MagFilter, @intFromEnum(opts.mag_filter)) catch return error.OpenGLFailed; texbind.image2D( 0, opts.internal_format, From 8f50c7f2699bb09ec85a43776c8fb6f59e8abe1e Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Sat, 5 Jul 2025 22:13:26 -0600 Subject: [PATCH 4/4] font/sprite: no more margin in atlas region We no longer need a margin in the atlas because we always sample with nearest neighbor and our glyphs are always pixel perfect, no worry about interpolation between adjacent glyphs anymore! --- src/font/sprite/canvas.zig | 19 +------------------ 1 file changed, 1 insertion(+), 18 deletions(-) diff --git a/src/font/sprite/canvas.zig b/src/font/sprite/canvas.zig index b981449bc..a77b90a56 100644 --- a/src/font/sprite/canvas.zig +++ b/src/font/sprite/canvas.zig @@ -140,24 +140,7 @@ pub const Canvas = struct { const region_height = sfc_height -| self.clip_top -| self.clip_bottom; // Allocate our texture atlas region - const region = region: { - // Reserve a region with a 1px margin on the bottom and right edges - // so that we can avoid interpolation between adjacent glyphs during - // texture sampling. - var region = try atlas.reserve( - alloc, - region_width + 1, - region_height + 1, - ); - - // Modify the region to remove the margin so that we write to the - // non-zero location. The data in an Altlas is always initialized - // to zero (Atlas.clear) so we don't need to worry about zero-ing - // that. - region.width -= 1; - region.height -= 1; - break :region region; - }; + const region = try atlas.reserve(alloc, region_width, region_height); if (region.width > 0 and region.height > 0) { const buffer: []u8 = @ptrCast(self.sfc.image_surface_alpha8.buf);