Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(macos): prevent transparency leakage/flash in new/resized surfaces #5083

Merged
merged 1 commit into from
Jan 15, 2025
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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`.
  • Loading branch information
qwerasd205 committed Jan 15, 2025
commit 34abe2ceba9c42bed6737143a051701501ccd515
71 changes: 70 additions & 1 deletion src/renderer/Metal.zig
Original file line number Diff line number Diff line change
@@ -148,6 +148,9 @@ layer: objc.Object, // CAMetalLayer
/// a display link.
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: ?CustomShaderState = null,

@@ -569,9 +572,20 @@ pub fn init(alloc: Allocator, options: renderer.Options) !Metal {
// color space using converted colors, which reduces,
// but does not fully eliminate blending artifacts.
const colorspace = try graphics.ColorSpace.createNamed(.displayP3);
errdefer colorspace.release();
defer colorspace.release();
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
// 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.
@@ -667,6 +681,7 @@ pub fn init(alloc: Allocator, options: renderer.Options) !Metal {
// Metal stuff
.layer = layer,
.display_link = display_link,
.terminal_colorspace = terminal_colorspace,
.custom_shader_state = null,
.gpu_state = gpu_state,
};
@@ -690,6 +705,8 @@ pub fn deinit(self: *Metal) void {
}
}

self.terminal_colorspace.release();

self.cells.deinit(self.alloc);

self.font_shaper.deinit();
@@ -1170,6 +1187,32 @@ pub fn updateFrame(
@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.
{
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.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_custom_shaders = self.config.custom_shaders;

21 changes: 19 additions & 2 deletions src/renderer/shaders/cell.metal
Original file line number Diff line number Diff line change
@@ -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
// 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
// operation, but it is still wasted work.
return load_color(
cells[grid_pos.y * uniforms.grid_size.x + grid_pos.x],
cell_color,
uniforms.use_display_p3,
uniforms.use_linear_blending
);