Skip to content

Commit

Permalink
Metal alpha blending fixes + color handling improvements (#4913)
Browse files Browse the repository at this point in the history
This PR addresses #2125 for the Metal renderer. Both options are
available: "Apple-style" blending where colors are blended in a wide
gamut color space, which reduces but does not eliminate artifacts; and
linear blending where colors are blended in linear RGB.

Because this doesn't add support for linear blending on Linux, I don't
know whether the issue should be closed or not.

### List of changes in no particular order
- We now set the layer's color space in the renderer not in the apprt
- We always set the layer to Display P3 color spaces
- If the user hasn't configured their `window-colorspace` to
`display-p3` then terminal colors are automatically converted from sRGB
to the corresponding Display P3 color in the shader
- Background color is not set with the clear color anymore, instead we
explicitly set all bg cell colors since this is needed for minimum
contrast to not break with dark text on the default bg color (try it
out, it forces it fully white right now), and we just draw the
background as a part of the bg cells shader. Note: We may want to move
the main background color to be the `backgroundColor` property on the
`CAMetalLayer`, because this should fix the flash of transparency during
startup (#4516) and the weirdness at the edge of the window when
resizing. I didn't make that a part of this PR because it requires
further changes and my changes are already pretty significant, but I can
make it a follow-up.
- Added a config option for changing alpha blending between "native"
blending, where colors are just blended directly in sRGB (or Display P3)
and linear blending, where colors are blended in linear space.
- Added a config option for an experimental technique that I think works
pretty well which compensates for the perceptual thinning and thickening
of dark and light text respectively when using linear blending.
- Custom shaders can now be hot reloaded with config reloads.
- Fixed a bug that was revealed when I changed how we handle
backgrounds, page widths weren't being set while cloning the screen.

### Main takeaways
Color blending now matches nearly identically to Apple apps like
Terminal.app and TextEdit, not *quite* identical in worst case
scenarios, off by the tiniest bit, because the default color space is
*slightly* different than Display P3.

Linear alpha blending is now available for mac users who prefer more
accurate color reproduction, and personally I think it looks very nice
with the alpha correction turned on, I will be daily driving that
configuration.

### Future work
- Handle primary background color with `CALayer.backgroundColor` instead
of in shader, to avoid issues around edges when resizing.
- Parse color space info directly from ICC profiles and compute the
color conversion matrix dynamically, and pass it as a uniform to the
shaders.
- Port linear blending option to OpenGL.
- Maybe support wide gamut images (right now all images are assumed to
be sRGB).
  • Loading branch information
mitchellh authored Jan 13, 2025
2 parents c1938d1 + a8b9c5b commit 5081e65
Show file tree
Hide file tree
Showing 11 changed files with 630 additions and 208 deletions.
16 changes: 0 additions & 16 deletions macos/Sources/Features/QuickTerminal/QuickTerminalController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -375,19 +375,6 @@ class QuickTerminalController: BaseTerminalController {
// Some APIs such as window blur have no effect unless the window is visible.
guard window.isVisible else { return }

// Terminals typically operate in sRGB color space and macOS defaults
// to "native" which is typically P3. There is a lot more resources
// covered in this GitHub issue: https://github.com/mitchellh/ghostty/pull/376
// Ghostty defaults to sRGB but this can be overridden.
switch (self.derivedConfig.windowColorspace) {
case "display-p3":
window.colorSpace = .displayP3
case "srgb":
fallthrough
default:
window.colorSpace = .sRGB
}

// If we have window transparency then set it transparent. Otherwise set it opaque.
if (self.derivedConfig.backgroundOpacity < 1) {
window.isOpaque = false
Expand Down Expand Up @@ -457,15 +444,13 @@ class QuickTerminalController: BaseTerminalController {
let quickTerminalAnimationDuration: Double
let quickTerminalAutoHide: Bool
let quickTerminalSpaceBehavior: QuickTerminalSpaceBehavior
let windowColorspace: String
let backgroundOpacity: Double

init() {
self.quickTerminalScreen = .main
self.quickTerminalAnimationDuration = 0.2
self.quickTerminalAutoHide = true
self.quickTerminalSpaceBehavior = .move
self.windowColorspace = ""
self.backgroundOpacity = 1.0
}

Expand All @@ -474,7 +459,6 @@ class QuickTerminalController: BaseTerminalController {
self.quickTerminalAnimationDuration = config.quickTerminalAnimationDuration
self.quickTerminalAutoHide = config.quickTerminalAutoHide
self.quickTerminalSpaceBehavior = config.quickTerminalSpaceBehavior
self.windowColorspace = config.windowColorspace
self.backgroundOpacity = config.backgroundOpacity
}
}
Expand Down
13 changes: 0 additions & 13 deletions macos/Sources/Features/Terminal/TerminalController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -366,19 +366,6 @@ class TerminalController: BaseTerminalController {
// If window decorations are disabled, remove our title
if (!config.windowDecorations) { window.styleMask.remove(.titled) }

// Terminals typically operate in sRGB color space and macOS defaults
// to "native" which is typically P3. There is a lot more resources
// covered in this GitHub issue: https://github.com/mitchellh/ghostty/pull/376
// Ghostty defaults to sRGB but this can be overridden.
switch (config.windowColorspace) {
case "display-p3":
window.colorSpace = .displayP3
case "srgb":
fallthrough
default:
window.colorSpace = .sRGB
}

// If we have only a single surface (no splits) and that surface requested
// an initial size then we set it here now.
if case let .leaf(leaf) = surfaceTree {
Expand Down
9 changes: 0 additions & 9 deletions macos/Sources/Ghostty/Ghostty.Config.swift
Original file line number Diff line number Diff line change
Expand Up @@ -132,15 +132,6 @@ extension Ghostty {
return v
}

var windowColorspace: String {
guard let config = self.config else { return "" }
var v: UnsafePointer<Int8>? = nil
let key = "window-colorspace"
guard ghostty_config_get(config, &v, key, UInt(key.count)) else { return "" }
guard let ptr = v else { return "" }
return String(cString: ptr)
}

var windowSaveState: String {
guard let config = self.config else { return "" }
var v: UnsafePointer<Int8>? = nil
Expand Down
63 changes: 63 additions & 0 deletions pkg/macos/graphics/color_space.zig
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,72 @@ pub const ColorSpace = opaque {
) orelse Allocator.Error.OutOfMemory;
}

pub fn createNamed(name: Name) Allocator.Error!*ColorSpace {
return @as(
?*ColorSpace,
@ptrFromInt(@intFromPtr(c.CGColorSpaceCreateWithName(name.cfstring()))),
) orelse Allocator.Error.OutOfMemory;
}

pub fn release(self: *ColorSpace) void {
c.CGColorSpaceRelease(@ptrCast(self));
}

pub const Name = enum {
/// This color space uses the DCI P3 primaries, a D65 white point, and
/// the sRGB transfer function.
displayP3,
/// The Display P3 color space with a linear transfer function and
/// extended-range values.
extendedLinearDisplayP3,
/// The sRGB colorimetry and non-linear transfer function are specified
/// in IEC 61966-2-1.
sRGB,
/// This color space has the same colorimetry as `sRGB`, but uses a
/// linear transfer function.
linearSRGB,
/// This color space has the same colorimetry as `sRGB`, but you can
/// encode component values below `0.0` and above `1.0`. Negative values
/// are encoded as the signed reflection of the original encoding
/// function, as shown in the formula below:
/// ```
/// extendedTransferFunction(x) = sign(x) * sRGBTransferFunction(abs(x))
/// ```
extendedSRGB,
/// This color space has the same colorimetry as `sRGB`; in addition,
/// you may encode component values below `0.0` and above `1.0`.
extendedLinearSRGB,
/// ...
genericGrayGamma2_2,
/// ...
linearGray,
/// This color space has the same colorimetry as `genericGrayGamma2_2`,
/// but you can encode component values below `0.0` and above `1.0`.
/// Negative values are encoded as the signed reflection of the
/// original encoding function, as shown in the formula below:
/// ```
/// extendedGrayTransferFunction(x) = sign(x) * gamma22Function(abs(x))
/// ```
extendedGray,
/// This color space has the same colorimetry as `linearGray`; in
/// addition, you may encode component values below `0.0` and above `1.0`.
extendedLinearGray,

fn cfstring(self: Name) c.CFStringRef {
return switch (self) {
.displayP3 => c.kCGColorSpaceDisplayP3,
.extendedLinearDisplayP3 => c.kCGColorSpaceExtendedLinearDisplayP3,
.sRGB => c.kCGColorSpaceSRGB,
.extendedSRGB => c.kCGColorSpaceExtendedSRGB,
.linearSRGB => c.kCGColorSpaceLinearSRGB,
.extendedLinearSRGB => c.kCGColorSpaceExtendedLinearSRGB,
.genericGrayGamma2_2 => c.kCGColorSpaceGenericGrayGamma2_2,
.extendedGray => c.kCGColorSpaceExtendedGray,
.linearGray => c.kCGColorSpaceLinearGray,
.extendedLinearGray => c.kCGColorSpaceExtendedLinearGray,
};
}
};
};

test {
Expand Down
40 changes: 40 additions & 0 deletions src/config/Config.zig
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,32 @@ const c = @cImport({
/// This is currently only supported on macOS.
@"font-thicken-strength": u8 = 255,

/// What color space to use when performing alpha blending.
///
/// This affects how text looks for different background/foreground color pairs.
///
/// Valid values:
///
/// * `native` - Perform alpha blending in the native color space for the OS.
/// On macOS this corresponds to Display P3, and on Linux it's sRGB.
///
/// * `linear` - Perform alpha blending in linear space. This will eliminate
/// the darkening artifacts around the edges of text that are very visible
/// when certain color combinations are used (e.g. red / green), but makes
/// dark text look much thinner than normal and light text much thicker.
/// 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,

/// 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%,
/// etc.). In each case, the values represent the amount to change the original
Expand Down Expand Up @@ -5753,6 +5779,20 @@ pub const GraphemeWidthMethod = enum {
unicode,
};

/// See text-blending
pub const TextBlending = enum {
native,
linear,
@"linear-corrected",

pub fn isLinear(self: TextBlending) bool {
return switch (self) {
.native => false,
.linear, .@"linear-corrected" => true,
};
}
};

/// See freetype-load-flag
pub const FreetypeLoadFlags = packed struct {
// The defaults here at the time of writing this match the defaults
Expand Down
7 changes: 3 additions & 4 deletions src/font/face/coretext.zig
Original file line number Diff line number Diff line change
Expand Up @@ -343,13 +343,12 @@ pub const Face = struct {
} = if (!self.isColorGlyph(glyph_index)) .{
.color = false,
.depth = 1,
.space = try macos.graphics.ColorSpace.createDeviceGray(),
.context_opts = @intFromEnum(macos.graphics.BitmapInfo.alpha_mask) &
@intFromEnum(macos.graphics.ImageAlphaInfo.only),
.space = try macos.graphics.ColorSpace.createNamed(.linearGray),
.context_opts = @intFromEnum(macos.graphics.ImageAlphaInfo.only),
} else .{
.color = true,
.depth = 4,
.space = try macos.graphics.ColorSpace.createDeviceRGB(),
.space = try macos.graphics.ColorSpace.createNamed(.displayP3),
.context_opts = @intFromEnum(macos.graphics.BitmapInfo.byte_order_32_little) |
@intFromEnum(macos.graphics.ImageAlphaInfo.premultiplied_first),
};
Expand Down
Loading

0 comments on commit 5081e65

Please sign in to comment.