mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-14 07:46:12 +03:00
fix(macos): prevent transparency leakage/flash in new/resized surfaces
By using the `CAMetalLayer`'s `backgroundColor` property instead of drawing the background color in our shader, it is always stretched to cover the full surface, even when live-resizing, and it doesn't require us to draw a frame for it to be initialized so there's no transparent flash when a new surface is created (as in a new split/tab). This commit also allows for hot reload of `background-opacity`, `window-vsync`, and `window-colorspace`.
This commit is contained in:
@ -148,6 +148,9 @@ layer: objc.Object, // CAMetalLayer
|
|||||||
/// a display link.
|
/// a display link.
|
||||||
display_link: ?DisplayLink = null,
|
display_link: ?DisplayLink = null,
|
||||||
|
|
||||||
|
/// The `CGColorSpace` that represents our current terminal color space
|
||||||
|
terminal_colorspace: *graphics.ColorSpace,
|
||||||
|
|
||||||
/// Custom shader state. This is only set if we have custom shaders.
|
/// Custom shader state. This is only set if we have custom shaders.
|
||||||
custom_shader_state: ?CustomShaderState = null,
|
custom_shader_state: ?CustomShaderState = null,
|
||||||
|
|
||||||
@ -569,9 +572,20 @@ pub fn init(alloc: Allocator, options: renderer.Options) !Metal {
|
|||||||
// color space using converted colors, which reduces,
|
// color space using converted colors, which reduces,
|
||||||
// but does not fully eliminate blending artifacts.
|
// but does not fully eliminate blending artifacts.
|
||||||
const colorspace = try graphics.ColorSpace.createNamed(.displayP3);
|
const colorspace = try graphics.ColorSpace.createNamed(.displayP3);
|
||||||
errdefer colorspace.release();
|
defer colorspace.release();
|
||||||
layer.setProperty("colorspace", colorspace);
|
layer.setProperty("colorspace", colorspace);
|
||||||
|
|
||||||
|
// Create a colorspace the represents our terminal colors
|
||||||
|
// this will allow us to create e.g. `CGColor`s for things
|
||||||
|
// like the current background color.
|
||||||
|
const terminal_colorspace = try graphics.ColorSpace.createNamed(
|
||||||
|
switch (options.config.colorspace) {
|
||||||
|
.@"display-p3" => .displayP3,
|
||||||
|
.srgb => .sRGB,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
errdefer terminal_colorspace.release();
|
||||||
|
|
||||||
// Make our view layer-backed with our Metal layer. On iOS views are
|
// Make our view layer-backed with our Metal layer. On iOS views are
|
||||||
// always layer backed so we don't need to do this. But on iOS the
|
// always layer backed so we don't need to do this. But on iOS the
|
||||||
// caller MUST be sure to set the layerClass to CAMetalLayer.
|
// caller MUST be sure to set the layerClass to CAMetalLayer.
|
||||||
@ -667,6 +681,7 @@ pub fn init(alloc: Allocator, options: renderer.Options) !Metal {
|
|||||||
// Metal stuff
|
// Metal stuff
|
||||||
.layer = layer,
|
.layer = layer,
|
||||||
.display_link = display_link,
|
.display_link = display_link,
|
||||||
|
.terminal_colorspace = terminal_colorspace,
|
||||||
.custom_shader_state = null,
|
.custom_shader_state = null,
|
||||||
.gpu_state = gpu_state,
|
.gpu_state = gpu_state,
|
||||||
};
|
};
|
||||||
@ -690,6 +705,8 @@ pub fn deinit(self: *Metal) void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.terminal_colorspace.release();
|
||||||
|
|
||||||
self.cells.deinit(self.alloc);
|
self.cells.deinit(self.alloc);
|
||||||
|
|
||||||
self.font_shaper.deinit();
|
self.font_shaper.deinit();
|
||||||
@ -1170,6 +1187,32 @@ pub fn updateFrame(
|
|||||||
@intFromFloat(@round(self.config.background_opacity * 255.0)),
|
@intFromFloat(@round(self.config.background_opacity * 255.0)),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Update the background color on our layer
|
||||||
|
//
|
||||||
|
// TODO: Is this expensive? Should we be checking if our
|
||||||
|
// bg color has changed first before doing this work?
|
||||||
|
{
|
||||||
|
const color = graphics.c.CGColorCreate(
|
||||||
|
@ptrCast(self.terminal_colorspace),
|
||||||
|
&[4]f64{
|
||||||
|
@as(f64, @floatFromInt(critical.bg.r)) / 255.0,
|
||||||
|
@as(f64, @floatFromInt(critical.bg.g)) / 255.0,
|
||||||
|
@as(f64, @floatFromInt(critical.bg.b)) / 255.0,
|
||||||
|
self.config.background_opacity,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
defer graphics.c.CGColorRelease(color);
|
||||||
|
|
||||||
|
// We use a CATransaction so that Core Animation knows that we
|
||||||
|
// updated the background color property. Otherwise it behaves
|
||||||
|
// weird, not updating the color until we resize.
|
||||||
|
const CATransaction = objc.getClass("CATransaction").?;
|
||||||
|
CATransaction.msgSend(void, "begin", .{});
|
||||||
|
defer CATransaction.msgSend(void, "commit", .{});
|
||||||
|
|
||||||
|
self.layer.setProperty("backgroundColor", color);
|
||||||
|
}
|
||||||
|
|
||||||
// Go through our images and see if we need to setup any textures.
|
// Go through our images and see if we need to setup any textures.
|
||||||
{
|
{
|
||||||
var image_it = self.images.iterator();
|
var image_it = self.images.iterator();
|
||||||
@ -2077,6 +2120,32 @@ pub fn changeConfig(self: *Metal, config: *DerivedConfig) !void {
|
|||||||
self.default_cursor_color = if (!config.cursor_invert) config.cursor_color else null;
|
self.default_cursor_color = if (!config.cursor_invert) config.cursor_color else null;
|
||||||
self.cursor_invert = config.cursor_invert;
|
self.cursor_invert = config.cursor_invert;
|
||||||
|
|
||||||
|
// Update our layer's opaqueness and display sync in case they changed.
|
||||||
|
{
|
||||||
|
// We use a CATransaction so that Core Animation knows that we
|
||||||
|
// updated the opaque property. Otherwise it behaves weird, not
|
||||||
|
// properly going from opaque to transparent unless we resize.
|
||||||
|
const CATransaction = objc.getClass("CATransaction").?;
|
||||||
|
CATransaction.msgSend(void, "begin", .{});
|
||||||
|
defer CATransaction.msgSend(void, "commit", .{});
|
||||||
|
|
||||||
|
self.layer.setProperty("opaque", config.background_opacity >= 1);
|
||||||
|
self.layer.setProperty("displaySyncEnabled", config.vsync);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update our terminal colorspace if it changed
|
||||||
|
if (self.config.colorspace != config.colorspace) {
|
||||||
|
const terminal_colorspace = try graphics.ColorSpace.createNamed(
|
||||||
|
switch (config.colorspace) {
|
||||||
|
.@"display-p3" => .displayP3,
|
||||||
|
.srgb => .sRGB,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
errdefer terminal_colorspace.release();
|
||||||
|
self.terminal_colorspace.release();
|
||||||
|
self.terminal_colorspace = terminal_colorspace;
|
||||||
|
}
|
||||||
|
|
||||||
const old_blending = self.config.blending;
|
const old_blending = self.config.blending;
|
||||||
const old_custom_shaders = self.config.custom_shaders;
|
const old_custom_shaders = self.config.custom_shaders;
|
||||||
|
|
||||||
|
@ -280,7 +280,24 @@ fragment float4 cell_bg_fragment(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// We load the color for the cell, converting it appropriately, and return it.
|
// Load the color for the cell.
|
||||||
|
uchar4 cell_color = cells[grid_pos.y * uniforms.grid_size.x + grid_pos.x];
|
||||||
|
|
||||||
|
// We have special case handling for when the cell color matches the bg color.
|
||||||
|
if (all(cell_color == uniforms.bg_color)) {
|
||||||
|
// If we have any background transparency then we render bg-colored cells as
|
||||||
|
// fully transparent, since the background is handled by the layer bg color
|
||||||
|
// and we don't want to double up our bg color, but if our bg color is fully
|
||||||
|
// opaque then our layer is opaque and can't handle transparency, so we need
|
||||||
|
// to return the bg color directly instead.
|
||||||
|
if (uniforms.bg_color.a == 255) {
|
||||||
|
return bg;
|
||||||
|
} else {
|
||||||
|
return float4(0.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert the color and return it.
|
||||||
//
|
//
|
||||||
// TODO: We may want to blend the color with the background
|
// TODO: We may want to blend the color with the background
|
||||||
// color, rather than purely replacing it, this needs
|
// color, rather than purely replacing it, this needs
|
||||||
@ -292,7 +309,7 @@ fragment float4 cell_bg_fragment(
|
|||||||
// fragment of each cell. It's not the most epxensive
|
// fragment of each cell. It's not the most epxensive
|
||||||
// operation, but it is still wasted work.
|
// operation, but it is still wasted work.
|
||||||
return load_color(
|
return load_color(
|
||||||
cells[grid_pos.y * uniforms.grid_size.x + grid_pos.x],
|
cell_color,
|
||||||
uniforms.use_display_p3,
|
uniforms.use_display_p3,
|
||||||
uniforms.use_linear_blending
|
uniforms.use_linear_blending
|
||||||
);
|
);
|
||||||
|
Reference in New Issue
Block a user