mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-14 07:46:12 +03:00
Metal alpha blending fixes + color handling improvements (#4913)
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).
This commit is contained in:
@ -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
|
||||
@ -457,7 +444,6 @@ class QuickTerminalController: BaseTerminalController {
|
||||
let quickTerminalAnimationDuration: Double
|
||||
let quickTerminalAutoHide: Bool
|
||||
let quickTerminalSpaceBehavior: QuickTerminalSpaceBehavior
|
||||
let windowColorspace: String
|
||||
let backgroundOpacity: Double
|
||||
|
||||
init() {
|
||||
@ -465,7 +451,6 @@ class QuickTerminalController: BaseTerminalController {
|
||||
self.quickTerminalAnimationDuration = 0.2
|
||||
self.quickTerminalAutoHide = true
|
||||
self.quickTerminalSpaceBehavior = .move
|
||||
self.windowColorspace = ""
|
||||
self.backgroundOpacity = 1.0
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
@ -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
|
||||
|
@ -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),
|
||||
};
|
||||
|
@ -21,6 +21,7 @@ const renderer = @import("../renderer.zig");
|
||||
const math = @import("../math.zig");
|
||||
const Surface = @import("../Surface.zig");
|
||||
const link = @import("link.zig");
|
||||
const graphics = macos.graphics;
|
||||
const fgMode = @import("cell.zig").fgMode;
|
||||
const isCovering = @import("cell.zig").isCovering;
|
||||
const shadertoy = @import("shadertoy.zig");
|
||||
@ -105,10 +106,6 @@ default_cursor_color: ?terminal.color.RGB,
|
||||
/// foreground color as the cursor color.
|
||||
cursor_invert: bool,
|
||||
|
||||
/// The current frame background color. This is only updated during
|
||||
/// the updateFrame method.
|
||||
current_background_color: terminal.color.RGB,
|
||||
|
||||
/// The current set of cells to render. This is rebuilt on every frame
|
||||
/// but we keep this around so that we don't reallocate. Each set of
|
||||
/// cells goes into a separate shader.
|
||||
@ -390,6 +387,8 @@ pub const DerivedConfig = struct {
|
||||
custom_shaders: configpkg.RepeatablePath,
|
||||
links: link.Set,
|
||||
vsync: bool,
|
||||
colorspace: configpkg.Config.WindowColorspace,
|
||||
blending: configpkg.Config.TextBlending,
|
||||
|
||||
pub fn init(
|
||||
alloc_gpa: Allocator,
|
||||
@ -460,7 +459,8 @@ pub const DerivedConfig = struct {
|
||||
.custom_shaders = custom_shaders,
|
||||
.links = links,
|
||||
.vsync = config.@"window-vsync",
|
||||
|
||||
.colorspace = config.@"window-colorspace",
|
||||
.blending = config.@"text-blending",
|
||||
.arena = arena,
|
||||
};
|
||||
}
|
||||
@ -490,10 +490,6 @@ pub fn surfaceInit(surface: *apprt.Surface) !void {
|
||||
}
|
||||
|
||||
pub fn init(alloc: Allocator, options: renderer.Options) !Metal {
|
||||
var arena = ArenaAllocator.init(alloc);
|
||||
defer arena.deinit();
|
||||
const arena_alloc = arena.allocator();
|
||||
|
||||
const ViewInfo = struct {
|
||||
view: objc.Object,
|
||||
scaleFactor: f64,
|
||||
@ -512,7 +508,7 @@ pub fn init(alloc: Allocator, options: renderer.Options) !Metal {
|
||||
nswindow.getProperty(?*anyopaque, "contentView").?,
|
||||
);
|
||||
const scaleFactor = nswindow.getProperty(
|
||||
macos.graphics.c.CGFloat,
|
||||
graphics.c.CGFloat,
|
||||
"backingScaleFactor",
|
||||
);
|
||||
|
||||
@ -553,6 +549,29 @@ pub fn init(alloc: Allocator, options: renderer.Options) !Metal {
|
||||
layer.setProperty("opaque", options.config.background_opacity >= 1);
|
||||
layer.setProperty("displaySyncEnabled", options.config.vsync);
|
||||
|
||||
// Set our layer's pixel format appropriately.
|
||||
layer.setProperty(
|
||||
"pixelFormat",
|
||||
// Using an `*_srgb` pixel format makes Metal gamma encode
|
||||
// the pixels written to it *after* blending, which means
|
||||
// we get linear alpha blending rather than gamma-incorrect
|
||||
// blending.
|
||||
if (options.config.blending.isLinear())
|
||||
@intFromEnum(mtl.MTLPixelFormat.bgra8unorm_srgb)
|
||||
else
|
||||
@intFromEnum(mtl.MTLPixelFormat.bgra8unorm),
|
||||
);
|
||||
|
||||
// Set our layer's color space to Display P3.
|
||||
// This allows us to have "Apple-style" alpha blending,
|
||||
// since it seems to be the case that Apple apps like
|
||||
// Terminal and TextEdit render text in the display's
|
||||
// color space using converted colors, which reduces,
|
||||
// but does not fully eliminate blending artifacts.
|
||||
const colorspace = try graphics.ColorSpace.createNamed(.displayP3);
|
||||
errdefer colorspace.release();
|
||||
layer.setProperty("colorspace", colorspace);
|
||||
|
||||
// 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.
|
||||
@ -578,54 +597,6 @@ pub fn init(alloc: Allocator, options: renderer.Options) !Metal {
|
||||
});
|
||||
errdefer font_shaper.deinit();
|
||||
|
||||
// Load our custom shaders
|
||||
const custom_shaders: []const [:0]const u8 = shadertoy.loadFromFiles(
|
||||
arena_alloc,
|
||||
options.config.custom_shaders,
|
||||
.msl,
|
||||
) catch |err| err: {
|
||||
log.warn("error loading custom shaders err={}", .{err});
|
||||
break :err &.{};
|
||||
};
|
||||
|
||||
// If we have custom shaders then setup our state
|
||||
var custom_shader_state: ?CustomShaderState = state: {
|
||||
if (custom_shaders.len == 0) break :state null;
|
||||
|
||||
// Build our sampler for our texture
|
||||
var sampler = try mtl_sampler.Sampler.init(gpu_state.device);
|
||||
errdefer sampler.deinit();
|
||||
|
||||
break :state .{
|
||||
// Resolution and screen textures will be fixed up by first
|
||||
// call to setScreenSize. Draw calls will bail out early if
|
||||
// the screen size hasn't been set yet, so it won't error.
|
||||
.front_texture = undefined,
|
||||
.back_texture = undefined,
|
||||
.sampler = sampler,
|
||||
.uniforms = .{
|
||||
.resolution = .{ 0, 0, 1 },
|
||||
.time = 1,
|
||||
.time_delta = 1,
|
||||
.frame_rate = 1,
|
||||
.frame = 1,
|
||||
.channel_time = [1][4]f32{.{ 0, 0, 0, 0 }} ** 4,
|
||||
.channel_resolution = [1][4]f32{.{ 0, 0, 0, 0 }} ** 4,
|
||||
.mouse = .{ 0, 0, 0, 0 },
|
||||
.date = .{ 0, 0, 0, 0 },
|
||||
.sample_rate = 1,
|
||||
},
|
||||
|
||||
.first_frame_time = try std.time.Instant.now(),
|
||||
.last_frame_time = try std.time.Instant.now(),
|
||||
};
|
||||
};
|
||||
errdefer if (custom_shader_state) |*state| state.deinit();
|
||||
|
||||
// Initialize our shaders
|
||||
var shaders = try Shaders.init(alloc, gpu_state.device, custom_shaders);
|
||||
errdefer shaders.deinit(alloc);
|
||||
|
||||
// Initialize all the data that requires a critical font section.
|
||||
const font_critical: struct {
|
||||
metrics: font.Metrics,
|
||||
@ -661,7 +632,6 @@ pub fn init(alloc: Allocator, options: renderer.Options) !Metal {
|
||||
.cursor_color = null,
|
||||
.default_cursor_color = options.config.cursor_color,
|
||||
.cursor_invert = options.config.cursor_invert,
|
||||
.current_background_color = options.config.background,
|
||||
|
||||
// Render state
|
||||
.cells = .{},
|
||||
@ -674,7 +644,16 @@ pub fn init(alloc: Allocator, options: renderer.Options) !Metal {
|
||||
.min_contrast = options.config.min_contrast,
|
||||
.cursor_pos = .{ std.math.maxInt(u16), std.math.maxInt(u16) },
|
||||
.cursor_color = undefined,
|
||||
.bg_color = .{
|
||||
options.config.background.r,
|
||||
options.config.background.g,
|
||||
options.config.background.b,
|
||||
@intFromFloat(@round(options.config.background_opacity * 255.0)),
|
||||
},
|
||||
.cursor_wide = false,
|
||||
.use_display_p3 = options.config.colorspace == .@"display-p3",
|
||||
.use_linear_blending = options.config.blending.isLinear(),
|
||||
.use_experimental_linear_correction = options.config.blending == .@"linear-corrected",
|
||||
},
|
||||
|
||||
// Fonts
|
||||
@ -682,16 +661,18 @@ pub fn init(alloc: Allocator, options: renderer.Options) !Metal {
|
||||
.font_shaper = font_shaper,
|
||||
.font_shaper_cache = font.ShaperCache.init(),
|
||||
|
||||
// Shaders
|
||||
.shaders = shaders,
|
||||
// Shaders (initialized below)
|
||||
.shaders = undefined,
|
||||
|
||||
// Metal stuff
|
||||
.layer = layer,
|
||||
.display_link = display_link,
|
||||
.custom_shader_state = custom_shader_state,
|
||||
.custom_shader_state = null,
|
||||
.gpu_state = gpu_state,
|
||||
};
|
||||
|
||||
try result.initShaders();
|
||||
|
||||
// Do an initialize screen size setup to ensure our undefined values
|
||||
// above are initialized.
|
||||
try result.setScreenSize(result.size);
|
||||
@ -723,11 +704,82 @@ pub fn deinit(self: *Metal) void {
|
||||
}
|
||||
self.image_placements.deinit(self.alloc);
|
||||
|
||||
self.deinitShaders();
|
||||
|
||||
self.* = undefined;
|
||||
}
|
||||
|
||||
fn deinitShaders(self: *Metal) void {
|
||||
if (self.custom_shader_state) |*state| state.deinit();
|
||||
|
||||
self.shaders.deinit(self.alloc);
|
||||
}
|
||||
|
||||
self.* = undefined;
|
||||
fn initShaders(self: *Metal) !void {
|
||||
var arena = ArenaAllocator.init(self.alloc);
|
||||
defer arena.deinit();
|
||||
const arena_alloc = arena.allocator();
|
||||
|
||||
// Load our custom shaders
|
||||
const custom_shaders: []const [:0]const u8 = shadertoy.loadFromFiles(
|
||||
arena_alloc,
|
||||
self.config.custom_shaders,
|
||||
.msl,
|
||||
) catch |err| err: {
|
||||
log.warn("error loading custom shaders err={}", .{err});
|
||||
break :err &.{};
|
||||
};
|
||||
|
||||
var custom_shader_state: ?CustomShaderState = state: {
|
||||
if (custom_shaders.len == 0) break :state null;
|
||||
|
||||
// Build our sampler for our texture
|
||||
var sampler = try mtl_sampler.Sampler.init(self.gpu_state.device);
|
||||
errdefer sampler.deinit();
|
||||
|
||||
break :state .{
|
||||
// Resolution and screen textures will be fixed up by first
|
||||
// call to setScreenSize. Draw calls will bail out early if
|
||||
// the screen size hasn't been set yet, so it won't error.
|
||||
.front_texture = undefined,
|
||||
.back_texture = undefined,
|
||||
.sampler = sampler,
|
||||
.uniforms = .{
|
||||
.resolution = .{ 0, 0, 1 },
|
||||
.time = 1,
|
||||
.time_delta = 1,
|
||||
.frame_rate = 1,
|
||||
.frame = 1,
|
||||
.channel_time = [1][4]f32{.{ 0, 0, 0, 0 }} ** 4,
|
||||
.channel_resolution = [1][4]f32{.{ 0, 0, 0, 0 }} ** 4,
|
||||
.mouse = .{ 0, 0, 0, 0 },
|
||||
.date = .{ 0, 0, 0, 0 },
|
||||
.sample_rate = 1,
|
||||
},
|
||||
|
||||
.first_frame_time = try std.time.Instant.now(),
|
||||
.last_frame_time = try std.time.Instant.now(),
|
||||
};
|
||||
};
|
||||
errdefer if (custom_shader_state) |*state| state.deinit();
|
||||
|
||||
var shaders = try Shaders.init(
|
||||
self.alloc,
|
||||
self.gpu_state.device,
|
||||
custom_shaders,
|
||||
// Using an `*_srgb` pixel format makes Metal gamma encode
|
||||
// the pixels written to it *after* blending, which means
|
||||
// we get linear alpha blending rather than gamma-incorrect
|
||||
// blending.
|
||||
if (self.config.blending.isLinear())
|
||||
mtl.MTLPixelFormat.bgra8unorm_srgb
|
||||
else
|
||||
mtl.MTLPixelFormat.bgra8unorm,
|
||||
);
|
||||
errdefer shaders.deinit(self.alloc);
|
||||
|
||||
self.shaders = shaders;
|
||||
self.custom_shader_state = custom_shader_state;
|
||||
}
|
||||
|
||||
/// This is called just prior to spinning up the renderer thread for
|
||||
@ -1111,7 +1163,12 @@ pub fn updateFrame(
|
||||
self.cells_viewport = critical.viewport_pin;
|
||||
|
||||
// Update our background color
|
||||
self.current_background_color = critical.bg;
|
||||
self.uniforms.bg_color = .{
|
||||
critical.bg.r,
|
||||
critical.bg.g,
|
||||
critical.bg.b,
|
||||
@intFromFloat(@round(self.config.background_opacity * 255.0)),
|
||||
};
|
||||
|
||||
// Go through our images and see if we need to setup any textures.
|
||||
{
|
||||
@ -1233,10 +1290,10 @@ pub fn drawFrame(self: *Metal, surface: *apprt.Surface) !void {
|
||||
attachment.setProperty("storeAction", @intFromEnum(mtl.MTLStoreAction.store));
|
||||
attachment.setProperty("texture", screen_texture.value);
|
||||
attachment.setProperty("clearColor", mtl.MTLClearColor{
|
||||
.red = @as(f32, @floatFromInt(self.current_background_color.r)) / 255 * self.config.background_opacity,
|
||||
.green = @as(f32, @floatFromInt(self.current_background_color.g)) / 255 * self.config.background_opacity,
|
||||
.blue = @as(f32, @floatFromInt(self.current_background_color.b)) / 255 * self.config.background_opacity,
|
||||
.alpha = self.config.background_opacity,
|
||||
.red = 0.0,
|
||||
.green = 0.0,
|
||||
.blue = 0.0,
|
||||
.alpha = 0.0,
|
||||
});
|
||||
}
|
||||
|
||||
@ -1252,19 +1309,19 @@ pub fn drawFrame(self: *Metal, surface: *apprt.Surface) !void {
|
||||
defer encoder.msgSend(void, objc.sel("endEncoding"), .{});
|
||||
|
||||
// Draw background images first
|
||||
try self.drawImagePlacements(encoder, self.image_placements.items[0..self.image_bg_end]);
|
||||
try self.drawImagePlacements(encoder, frame, self.image_placements.items[0..self.image_bg_end]);
|
||||
|
||||
// Then draw background cells
|
||||
try self.drawCellBgs(encoder, frame);
|
||||
|
||||
// Then draw images under text
|
||||
try self.drawImagePlacements(encoder, self.image_placements.items[self.image_bg_end..self.image_text_end]);
|
||||
try self.drawImagePlacements(encoder, frame, self.image_placements.items[self.image_bg_end..self.image_text_end]);
|
||||
|
||||
// Then draw fg cells
|
||||
try self.drawCellFgs(encoder, frame, fg_count);
|
||||
|
||||
// Then draw remaining images
|
||||
try self.drawImagePlacements(encoder, self.image_placements.items[self.image_text_end..]);
|
||||
try self.drawImagePlacements(encoder, frame, self.image_placements.items[self.image_text_end..]);
|
||||
}
|
||||
|
||||
// If we have custom shaders, then we render them.
|
||||
@ -1457,6 +1514,7 @@ fn drawPostShader(
|
||||
fn drawImagePlacements(
|
||||
self: *Metal,
|
||||
encoder: objc.Object,
|
||||
frame: *const FrameState,
|
||||
placements: []const mtl_image.Placement,
|
||||
) !void {
|
||||
if (placements.len == 0) return;
|
||||
@ -1468,15 +1526,16 @@ fn drawImagePlacements(
|
||||
.{self.shaders.image_pipeline.value},
|
||||
);
|
||||
|
||||
// Set our uniform, which is the only shared buffer
|
||||
// Set our uniforms
|
||||
encoder.msgSend(
|
||||
void,
|
||||
objc.sel("setVertexBytes:length:atIndex:"),
|
||||
.{
|
||||
@as(*const anyopaque, @ptrCast(&self.uniforms)),
|
||||
@as(c_ulong, @sizeOf(@TypeOf(self.uniforms))),
|
||||
@as(c_ulong, 1),
|
||||
},
|
||||
objc.sel("setVertexBuffer:offset:atIndex:"),
|
||||
.{ frame.uniforms.buffer.value, @as(c_ulong, 0), @as(c_ulong, 1) },
|
||||
);
|
||||
encoder.msgSend(
|
||||
void,
|
||||
objc.sel("setFragmentBuffer:offset:atIndex:"),
|
||||
.{ frame.uniforms.buffer.value, @as(c_ulong, 0), @as(c_ulong, 1) },
|
||||
);
|
||||
|
||||
for (placements) |placement| {
|
||||
@ -1588,6 +1647,11 @@ fn drawCellBgs(
|
||||
);
|
||||
|
||||
// Set our buffers
|
||||
encoder.msgSend(
|
||||
void,
|
||||
objc.sel("setVertexBuffer:offset:atIndex:"),
|
||||
.{ frame.uniforms.buffer.value, @as(c_ulong, 0), @as(c_ulong, 1) },
|
||||
);
|
||||
encoder.msgSend(
|
||||
void,
|
||||
objc.sel("setFragmentBuffer:offset:atIndex:"),
|
||||
@ -1647,18 +1711,17 @@ fn drawCellFgs(
|
||||
encoder.msgSend(
|
||||
void,
|
||||
objc.sel("setFragmentTexture:atIndex:"),
|
||||
.{
|
||||
frame.grayscale.value,
|
||||
@as(c_ulong, 0),
|
||||
},
|
||||
.{ frame.grayscale.value, @as(c_ulong, 0) },
|
||||
);
|
||||
encoder.msgSend(
|
||||
void,
|
||||
objc.sel("setFragmentTexture:atIndex:"),
|
||||
.{
|
||||
frame.color.value,
|
||||
@as(c_ulong, 1),
|
||||
},
|
||||
.{ frame.color.value, @as(c_ulong, 1) },
|
||||
);
|
||||
encoder.msgSend(
|
||||
void,
|
||||
objc.sel("setFragmentBuffer:offset:atIndex:"),
|
||||
.{ frame.uniforms.buffer.value, @as(c_ulong, 0), @as(c_ulong, 2) },
|
||||
);
|
||||
|
||||
encoder.msgSend(
|
||||
@ -2003,17 +2066,47 @@ pub fn changeConfig(self: *Metal, config: *DerivedConfig) !void {
|
||||
// Set our new minimum contrast
|
||||
self.uniforms.min_contrast = config.min_contrast;
|
||||
|
||||
// Set our new color space and blending
|
||||
self.uniforms.use_display_p3 = config.colorspace == .@"display-p3";
|
||||
self.uniforms.use_linear_blending = config.blending.isLinear();
|
||||
self.uniforms.use_experimental_linear_correction = config.blending == .@"linear-corrected";
|
||||
|
||||
// Set our new colors
|
||||
self.default_background_color = config.background;
|
||||
self.default_foreground_color = config.foreground;
|
||||
self.default_cursor_color = if (!config.cursor_invert) config.cursor_color else null;
|
||||
self.cursor_invert = config.cursor_invert;
|
||||
|
||||
const old_blending = self.config.blending;
|
||||
const old_custom_shaders = self.config.custom_shaders;
|
||||
|
||||
self.config.deinit();
|
||||
self.config = config.*;
|
||||
|
||||
// Reset our viewport to force a rebuild, in case of a font change.
|
||||
self.cells_viewport = null;
|
||||
|
||||
// We reinitialize our shaders if our
|
||||
// blending or custom shaders changed.
|
||||
if (old_blending != config.blending or
|
||||
!old_custom_shaders.equal(config.custom_shaders))
|
||||
{
|
||||
self.deinitShaders();
|
||||
try self.initShaders();
|
||||
// We call setScreenSize to reinitialize
|
||||
// the textures used for custom shaders.
|
||||
if (self.custom_shader_state != null) {
|
||||
try self.setScreenSize(self.size);
|
||||
}
|
||||
// And we update our layer's pixel format appropriately.
|
||||
self.layer.setProperty(
|
||||
"pixelFormat",
|
||||
if (config.blending.isLinear())
|
||||
@intFromEnum(mtl.MTLPixelFormat.bgra8unorm_srgb)
|
||||
else
|
||||
@intFromEnum(mtl.MTLPixelFormat.bgra8unorm),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Resize the screen.
|
||||
@ -2057,7 +2150,7 @@ pub fn setScreenSize(
|
||||
}
|
||||
|
||||
// Set the size of the drawable surface to the bounds
|
||||
self.layer.setProperty("drawableSize", macos.graphics.Size{
|
||||
self.layer.setProperty("drawableSize", graphics.Size{
|
||||
.width = @floatFromInt(size.screen.width),
|
||||
.height = @floatFromInt(size.screen.height),
|
||||
});
|
||||
@ -2089,7 +2182,11 @@ pub fn setScreenSize(
|
||||
.min_contrast = old.min_contrast,
|
||||
.cursor_pos = old.cursor_pos,
|
||||
.cursor_color = old.cursor_color,
|
||||
.bg_color = old.bg_color,
|
||||
.cursor_wide = old.cursor_wide,
|
||||
.use_display_p3 = old.use_display_p3,
|
||||
.use_linear_blending = old.use_linear_blending,
|
||||
.use_experimental_linear_correction = old.use_experimental_linear_correction,
|
||||
};
|
||||
|
||||
// Reset our cell contents if our grid size has changed.
|
||||
@ -2124,7 +2221,17 @@ pub fn setScreenSize(
|
||||
const id_init = id_alloc.msgSend(objc.Object, objc.sel("init"), .{});
|
||||
break :init id_init;
|
||||
};
|
||||
desc.setProperty("pixelFormat", @intFromEnum(mtl.MTLPixelFormat.bgra8unorm));
|
||||
desc.setProperty(
|
||||
"pixelFormat",
|
||||
// Using an `*_srgb` pixel format makes Metal gamma encode
|
||||
// the pixels written to it *after* blending, which means
|
||||
// we get linear alpha blending rather than gamma-incorrect
|
||||
// blending.
|
||||
if (self.config.blending.isLinear())
|
||||
@intFromEnum(mtl.MTLPixelFormat.bgra8unorm_srgb)
|
||||
else
|
||||
@intFromEnum(mtl.MTLPixelFormat.bgra8unorm),
|
||||
);
|
||||
desc.setProperty("width", @as(c_ulong, @intCast(size.screen.width)));
|
||||
desc.setProperty("height", @as(c_ulong, @intCast(size.screen.height)));
|
||||
desc.setProperty(
|
||||
@ -2154,7 +2261,17 @@ pub fn setScreenSize(
|
||||
const id_init = id_alloc.msgSend(objc.Object, objc.sel("init"), .{});
|
||||
break :init id_init;
|
||||
};
|
||||
desc.setProperty("pixelFormat", @intFromEnum(mtl.MTLPixelFormat.bgra8unorm));
|
||||
desc.setProperty(
|
||||
"pixelFormat",
|
||||
// Using an `*_srgb` pixel format makes Metal gamma encode
|
||||
// the pixels written to it *after* blending, which means
|
||||
// we get linear alpha blending rather than gamma-incorrect
|
||||
// blending.
|
||||
if (self.config.blending.isLinear())
|
||||
@intFromEnum(mtl.MTLPixelFormat.bgra8unorm_srgb)
|
||||
else
|
||||
@intFromEnum(mtl.MTLPixelFormat.bgra8unorm),
|
||||
);
|
||||
desc.setProperty("width", @as(c_ulong, @intCast(size.screen.width)));
|
||||
desc.setProperty("height", @as(c_ulong, @intCast(size.screen.height)));
|
||||
desc.setProperty(
|
||||
@ -2466,8 +2583,10 @@ fn rebuildCells(
|
||||
// Foreground alpha for this cell.
|
||||
const alpha: u8 = if (style.flags.faint) 175 else 255;
|
||||
|
||||
// If the cell has a background color, set it.
|
||||
if (bg) |rgb| {
|
||||
// Set the cell's background color.
|
||||
{
|
||||
const rgb = bg orelse self.background_color orelse self.default_background_color;
|
||||
|
||||
// Determine our background alpha. If we have transparency configured
|
||||
// then this is dynamic depending on some situations. This is all
|
||||
// in an attempt to make transparency look the best for various
|
||||
@ -2477,23 +2596,20 @@ fn rebuildCells(
|
||||
|
||||
if (self.config.background_opacity >= 1) break :bg_alpha default;
|
||||
|
||||
// If we're selected, we do not apply background opacity
|
||||
// Cells that are selected should be fully opaque.
|
||||
if (selected) break :bg_alpha default;
|
||||
|
||||
// If we're reversed, do not apply background opacity
|
||||
// Cells that are reversed should be fully opaque.
|
||||
if (style.flags.inverse) break :bg_alpha default;
|
||||
|
||||
// If we have a background and its not the default background
|
||||
// then we apply background opacity
|
||||
if (style.bg(cell, color_palette) != null and !rgb.eql(self.background_color orelse self.default_background_color)) {
|
||||
// Cells that have an explicit bg color, which does not
|
||||
// match the current surface bg, should be fully opaque.
|
||||
if (bg != null and !rgb.eql(self.background_color orelse self.default_background_color)) {
|
||||
break :bg_alpha default;
|
||||
}
|
||||
|
||||
// We apply background opacity.
|
||||
var bg_alpha: f64 = @floatFromInt(default);
|
||||
bg_alpha *= self.config.background_opacity;
|
||||
bg_alpha = @ceil(bg_alpha);
|
||||
break :bg_alpha @intFromFloat(bg_alpha);
|
||||
// Otherwise, we use the configured background opacity.
|
||||
break :bg_alpha @intFromFloat(@round(self.config.background_opacity * 255.0));
|
||||
};
|
||||
|
||||
self.cells.bgCell(y, x).* = .{
|
||||
|
@ -74,6 +74,7 @@ pub const MTLPixelFormat = enum(c_ulong) {
|
||||
rgba8unorm = 70,
|
||||
rgba8uint = 73,
|
||||
bgra8unorm = 80,
|
||||
bgra8unorm_srgb = 81,
|
||||
};
|
||||
|
||||
/// https://developer.apple.com/documentation/metal/mtlpurgeablestate?language=objc
|
||||
|
@ -13,9 +13,7 @@ const log = std.log.scoped(.metal);
|
||||
pub const Shaders = struct {
|
||||
library: objc.Object,
|
||||
|
||||
/// The cell shader is the shader used to render the terminal cells.
|
||||
/// It is a single shader that is used for both the background and
|
||||
/// foreground.
|
||||
/// Renders cell foreground elements (text, decorations).
|
||||
cell_text_pipeline: objc.Object,
|
||||
|
||||
/// The cell background shader is the shader used to render the
|
||||
@ -40,17 +38,18 @@ pub const Shaders = struct {
|
||||
alloc: Allocator,
|
||||
device: objc.Object,
|
||||
post_shaders: []const [:0]const u8,
|
||||
pixel_format: mtl.MTLPixelFormat,
|
||||
) !Shaders {
|
||||
const library = try initLibrary(device);
|
||||
errdefer library.msgSend(void, objc.sel("release"), .{});
|
||||
|
||||
const cell_text_pipeline = try initCellTextPipeline(device, library);
|
||||
const cell_text_pipeline = try initCellTextPipeline(device, library, pixel_format);
|
||||
errdefer cell_text_pipeline.msgSend(void, objc.sel("release"), .{});
|
||||
|
||||
const cell_bg_pipeline = try initCellBgPipeline(device, library);
|
||||
const cell_bg_pipeline = try initCellBgPipeline(device, library, pixel_format);
|
||||
errdefer cell_bg_pipeline.msgSend(void, objc.sel("release"), .{});
|
||||
|
||||
const image_pipeline = try initImagePipeline(device, library);
|
||||
const image_pipeline = try initImagePipeline(device, library, pixel_format);
|
||||
errdefer image_pipeline.msgSend(void, objc.sel("release"), .{});
|
||||
|
||||
const post_pipelines: []const objc.Object = initPostPipelines(
|
||||
@ -58,6 +57,7 @@ pub const Shaders = struct {
|
||||
device,
|
||||
library,
|
||||
post_shaders,
|
||||
pixel_format,
|
||||
) catch |err| err: {
|
||||
// If an error happens while building postprocess shaders we
|
||||
// want to just not use any postprocess shaders since we don't
|
||||
@ -137,9 +137,29 @@ pub const Uniforms = extern struct {
|
||||
cursor_pos: [2]u16 align(4),
|
||||
cursor_color: [4]u8 align(4),
|
||||
|
||||
// Whether the cursor is 2 cells wide.
|
||||
/// The background color for the whole surface.
|
||||
bg_color: [4]u8 align(4),
|
||||
|
||||
/// Whether the cursor is 2 cells wide.
|
||||
cursor_wide: bool align(1),
|
||||
|
||||
/// Indicates that colors provided to the shader are already in
|
||||
/// the P3 color space, so they don't need to be converted from
|
||||
/// sRGB.
|
||||
use_display_p3: bool align(1),
|
||||
|
||||
/// Indicates that the color attachments for the shaders have
|
||||
/// an `*_srgb` pixel format, which means the shaders need to
|
||||
/// output linear RGB colors rather than gamma encoded colors,
|
||||
/// since blending will be performed in linear space and then
|
||||
/// Metal itself will re-encode the colors for storage.
|
||||
use_linear_blending: bool align(1),
|
||||
|
||||
/// Enables a weight correction step that makes text rendered
|
||||
/// with linear alpha blending have a similar apparent weight
|
||||
/// (thickness) to gamma-incorrect blending.
|
||||
use_experimental_linear_correction: bool align(1) = false,
|
||||
|
||||
const PaddingExtend = packed struct(u8) {
|
||||
left: bool = false,
|
||||
right: bool = false,
|
||||
@ -201,6 +221,7 @@ fn initPostPipelines(
|
||||
device: objc.Object,
|
||||
library: objc.Object,
|
||||
shaders: []const [:0]const u8,
|
||||
pixel_format: mtl.MTLPixelFormat,
|
||||
) ![]const objc.Object {
|
||||
// If we have no shaders, do nothing.
|
||||
if (shaders.len == 0) return &.{};
|
||||
@ -220,7 +241,12 @@ fn initPostPipelines(
|
||||
// Build each shader. Note we don't use "0.." to build our index
|
||||
// because we need to keep track of our length to clean up above.
|
||||
for (shaders) |source| {
|
||||
pipelines[i] = try initPostPipeline(device, library, source);
|
||||
pipelines[i] = try initPostPipeline(
|
||||
device,
|
||||
library,
|
||||
source,
|
||||
pixel_format,
|
||||
);
|
||||
i += 1;
|
||||
}
|
||||
|
||||
@ -232,6 +258,7 @@ fn initPostPipeline(
|
||||
device: objc.Object,
|
||||
library: objc.Object,
|
||||
data: [:0]const u8,
|
||||
pixel_format: mtl.MTLPixelFormat,
|
||||
) !objc.Object {
|
||||
// Create our library which has the shader source
|
||||
const post_library = library: {
|
||||
@ -301,8 +328,7 @@ fn initPostPipeline(
|
||||
.{@as(c_ulong, 0)},
|
||||
);
|
||||
|
||||
// Value is MTLPixelFormatBGRA8Unorm
|
||||
attachment.setProperty("pixelFormat", @as(c_ulong, 80));
|
||||
attachment.setProperty("pixelFormat", @intFromEnum(pixel_format));
|
||||
}
|
||||
|
||||
// Make our state
|
||||
@ -343,7 +369,11 @@ pub const CellText = extern struct {
|
||||
};
|
||||
|
||||
/// Initialize the cell render pipeline for our shader library.
|
||||
fn initCellTextPipeline(device: objc.Object, library: objc.Object) !objc.Object {
|
||||
fn initCellTextPipeline(
|
||||
device: objc.Object,
|
||||
library: objc.Object,
|
||||
pixel_format: mtl.MTLPixelFormat,
|
||||
) !objc.Object {
|
||||
// Get our vertex and fragment functions
|
||||
const func_vert = func_vert: {
|
||||
const str = try macos.foundation.String.createWithBytes(
|
||||
@ -427,8 +457,7 @@ fn initCellTextPipeline(device: objc.Object, library: objc.Object) !objc.Object
|
||||
.{@as(c_ulong, 0)},
|
||||
);
|
||||
|
||||
// Value is MTLPixelFormatBGRA8Unorm
|
||||
attachment.setProperty("pixelFormat", @as(c_ulong, 80));
|
||||
attachment.setProperty("pixelFormat", @intFromEnum(pixel_format));
|
||||
|
||||
// Blending. This is required so that our text we render on top
|
||||
// of our drawable properly blends into the bg.
|
||||
@ -458,11 +487,15 @@ fn initCellTextPipeline(device: objc.Object, library: objc.Object) !objc.Object
|
||||
pub const CellBg = [4]u8;
|
||||
|
||||
/// Initialize the cell background render pipeline for our shader library.
|
||||
fn initCellBgPipeline(device: objc.Object, library: objc.Object) !objc.Object {
|
||||
fn initCellBgPipeline(
|
||||
device: objc.Object,
|
||||
library: objc.Object,
|
||||
pixel_format: mtl.MTLPixelFormat,
|
||||
) !objc.Object {
|
||||
// Get our vertex and fragment functions
|
||||
const func_vert = func_vert: {
|
||||
const str = try macos.foundation.String.createWithBytes(
|
||||
"full_screen_vertex",
|
||||
"cell_bg_vertex",
|
||||
.utf8,
|
||||
false,
|
||||
);
|
||||
@ -507,8 +540,7 @@ fn initCellBgPipeline(device: objc.Object, library: objc.Object) !objc.Object {
|
||||
.{@as(c_ulong, 0)},
|
||||
);
|
||||
|
||||
// Value is MTLPixelFormatBGRA8Unorm
|
||||
attachment.setProperty("pixelFormat", @as(c_ulong, 80));
|
||||
attachment.setProperty("pixelFormat", @intFromEnum(pixel_format));
|
||||
|
||||
// Blending. This is required so that our text we render on top
|
||||
// of our drawable properly blends into the bg.
|
||||
@ -535,7 +567,11 @@ fn initCellBgPipeline(device: objc.Object, library: objc.Object) !objc.Object {
|
||||
}
|
||||
|
||||
/// Initialize the image render pipeline for our shader library.
|
||||
fn initImagePipeline(device: objc.Object, library: objc.Object) !objc.Object {
|
||||
fn initImagePipeline(
|
||||
device: objc.Object,
|
||||
library: objc.Object,
|
||||
pixel_format: mtl.MTLPixelFormat,
|
||||
) !objc.Object {
|
||||
// Get our vertex and fragment functions
|
||||
const func_vert = func_vert: {
|
||||
const str = try macos.foundation.String.createWithBytes(
|
||||
@ -619,8 +655,7 @@ fn initImagePipeline(device: objc.Object, library: objc.Object) !objc.Object {
|
||||
.{@as(c_ulong, 0)},
|
||||
);
|
||||
|
||||
// Value is MTLPixelFormatBGRA8Unorm
|
||||
attachment.setProperty("pixelFormat", @as(c_ulong, 80));
|
||||
attachment.setProperty("pixelFormat", @intFromEnum(pixel_format));
|
||||
|
||||
// Blending. This is required so that our text we render on top
|
||||
// of our drawable properly blends into the bg.
|
||||
|
@ -18,7 +18,11 @@ struct Uniforms {
|
||||
float min_contrast;
|
||||
ushort2 cursor_pos;
|
||||
uchar4 cursor_color;
|
||||
uchar4 bg_color;
|
||||
bool cursor_wide;
|
||||
bool use_display_p3;
|
||||
bool use_linear_blending;
|
||||
bool use_experimental_linear_correction;
|
||||
};
|
||||
|
||||
//-------------------------------------------------------------------
|
||||
@ -26,40 +30,82 @@ struct Uniforms {
|
||||
//-------------------------------------------------------------------
|
||||
#pragma mark - Colors
|
||||
|
||||
// https://www.w3.org/TR/2008/REC-WCAG20-20081211/#relativeluminancedef
|
||||
float luminance_component(float c) {
|
||||
if (c <= 0.03928f) {
|
||||
return c / 12.92f;
|
||||
} else {
|
||||
return pow((c + 0.055f) / 1.055f, 2.4f);
|
||||
}
|
||||
// D50-adapted sRGB to XYZ conversion matrix.
|
||||
// http://www.brucelindbloom.com/Eqn_RGB_XYZ_Matrix.html
|
||||
constant float3x3 sRGB_XYZ = transpose(float3x3(
|
||||
0.4360747, 0.3850649, 0.1430804,
|
||||
0.2225045, 0.7168786, 0.0606169,
|
||||
0.0139322, 0.0971045, 0.7141733
|
||||
));
|
||||
// XYZ to Display P3 conversion matrix.
|
||||
// http://endavid.com/index.php?entry=79
|
||||
constant float3x3 XYZ_DP3 = transpose(float3x3(
|
||||
2.40414768,-0.99010704,-0.39759019,
|
||||
-0.84239098, 1.79905954, 0.01597023,
|
||||
0.04838763,-0.09752546, 1.27393636
|
||||
));
|
||||
// By composing the two above matrices we get
|
||||
// our sRGB to Display P3 conversion matrix.
|
||||
constant float3x3 sRGB_DP3 = XYZ_DP3 * sRGB_XYZ;
|
||||
|
||||
// Converts a color in linear sRGB to linear Display P3
|
||||
//
|
||||
// TODO: The color matrix should probably be computed
|
||||
// dynamically and passed as a uniform, rather
|
||||
// than being hard coded above.
|
||||
float3 srgb_to_display_p3(float3 srgb) {
|
||||
return sRGB_DP3 * srgb;
|
||||
}
|
||||
|
||||
float relative_luminance(float3 color) {
|
||||
color.r = luminance_component(color.r);
|
||||
color.g = luminance_component(color.g);
|
||||
color.b = luminance_component(color.b);
|
||||
float3 weights = float3(0.2126f, 0.7152f, 0.0722f);
|
||||
return dot(color, weights);
|
||||
// Converts a color from sRGB gamma encoding to linear.
|
||||
float4 linearize(float4 srgb) {
|
||||
bool3 cutoff = srgb.rgb <= 0.04045;
|
||||
float3 lower = srgb.rgb / 12.92;
|
||||
float3 higher = pow((srgb.rgb + 0.055) / 1.055, 2.4);
|
||||
srgb.rgb = mix(higher, lower, float3(cutoff));
|
||||
|
||||
return srgb;
|
||||
}
|
||||
|
||||
// Converts a color from linear to sRGB gamma encoding.
|
||||
float4 unlinearize(float4 linear) {
|
||||
bool3 cutoff = linear.rgb <= 0.0031308;
|
||||
float3 lower = linear.rgb * 12.92;
|
||||
float3 higher = pow(linear.rgb, 1.0 / 2.4) * 1.055 - 0.055;
|
||||
linear.rgb = mix(higher, lower, float3(cutoff));
|
||||
|
||||
return linear;
|
||||
}
|
||||
|
||||
// Compute the luminance of the provided color.
|
||||
//
|
||||
// Takes colors in linear RGB space. If your colors are gamma
|
||||
// encoded, linearize them before using them with this function.
|
||||
float luminance(float3 color) {
|
||||
return dot(color, float3(0.2126f, 0.7152f, 0.0722f));
|
||||
}
|
||||
|
||||
// https://www.w3.org/TR/2008/REC-WCAG20-20081211/#contrast-ratiodef
|
||||
//
|
||||
// Takes colors in linear RGB space. If your colors are gamma
|
||||
// encoded, linearize them before using them with this function.
|
||||
float contrast_ratio(float3 color1, float3 color2) {
|
||||
float l1 = relative_luminance(color1);
|
||||
float l2 = relative_luminance(color2);
|
||||
float l1 = luminance(color1);
|
||||
float l2 = luminance(color2);
|
||||
return (max(l1, l2) + 0.05f) / (min(l1, l2) + 0.05f);
|
||||
}
|
||||
|
||||
// Return the fg if the contrast ratio is greater than min, otherwise
|
||||
// return a color that satisfies the contrast ratio. Currently, the color
|
||||
// is always white or black, whichever has the highest contrast ratio.
|
||||
//
|
||||
// Takes colors in linear RGB space. If your colors are gamma
|
||||
// encoded, linearize them before using them with this function.
|
||||
float4 contrasted_color(float min, float4 fg, float4 bg) {
|
||||
float3 fg_premult = fg.rgb * fg.a;
|
||||
float3 bg_premult = bg.rgb * bg.a;
|
||||
float ratio = contrast_ratio(fg_premult, bg_premult);
|
||||
float ratio = contrast_ratio(fg.rgb, bg.rgb);
|
||||
if (ratio < min) {
|
||||
float white_ratio = contrast_ratio(float3(1.0f), bg_premult);
|
||||
float black_ratio = contrast_ratio(float3(0.0f), bg_premult);
|
||||
float white_ratio = contrast_ratio(float3(1.0f), bg.rgb);
|
||||
float black_ratio = contrast_ratio(float3(0.0f), bg.rgb);
|
||||
if (white_ratio > black_ratio) {
|
||||
return float4(1.0f);
|
||||
} else {
|
||||
@ -70,6 +116,62 @@ float4 contrasted_color(float min, float4 fg, float4 bg) {
|
||||
return fg;
|
||||
}
|
||||
|
||||
// Load a 4 byte RGBA non-premultiplied color and linearize
|
||||
// and convert it as necessary depending on the provided info.
|
||||
//
|
||||
// Returns a color in the Display P3 color space.
|
||||
//
|
||||
// If `display_p3` is true, then the provided color is assumed to
|
||||
// already be in the Display P3 color space, otherwise it's treated
|
||||
// as an sRGB color and is appropriately converted to Display P3.
|
||||
//
|
||||
// `linear` controls whether the returned color is linear or gamma encoded.
|
||||
float4 load_color(
|
||||
uchar4 in_color,
|
||||
bool display_p3,
|
||||
bool linear
|
||||
) {
|
||||
// 0 .. 255 -> 0.0 .. 1.0
|
||||
float4 color = float4(in_color) / 255.0f;
|
||||
|
||||
// If our color is already in Display P3 and
|
||||
// we aren't doing linear blending, then we
|
||||
// already have the correct color here and
|
||||
// can premultiply and return it.
|
||||
if (display_p3 && !linear) {
|
||||
color *= color.a;
|
||||
return color;
|
||||
}
|
||||
|
||||
// The color is in either the sRGB or Display P3 color space,
|
||||
// so in either case, it's a color space which uses the sRGB
|
||||
// transfer function, so we can use one function in order to
|
||||
// linearize it in either case.
|
||||
//
|
||||
// Even if we aren't doing linear blending, the color
|
||||
// needs to be in linear space to convert color spaces.
|
||||
color = linearize(color);
|
||||
|
||||
// If we're *NOT* using display P3 colors, then we're dealing
|
||||
// with an sRGB color, in which case we need to convert it in
|
||||
// to the Display P3 color space, since our output is always
|
||||
// Display P3.
|
||||
if (!display_p3) {
|
||||
color.rgb = srgb_to_display_p3(color.rgb);
|
||||
}
|
||||
|
||||
// If we're not doing linear blending, then we need to
|
||||
// unlinearize after doing the color space conversion.
|
||||
if (!linear) {
|
||||
color = unlinearize(color);
|
||||
}
|
||||
|
||||
// Premultiply our color by its alpha.
|
||||
color *= color.a;
|
||||
|
||||
return color;
|
||||
}
|
||||
|
||||
//-------------------------------------------------------------------
|
||||
// Full Screen Vertex Shader
|
||||
//-------------------------------------------------------------------
|
||||
@ -112,25 +214,54 @@ vertex FullScreenVertexOut full_screen_vertex(
|
||||
//-------------------------------------------------------------------
|
||||
#pragma mark - Cell BG Shader
|
||||
|
||||
struct CellBgVertexOut {
|
||||
float4 position [[position]];
|
||||
float4 bg_color;
|
||||
};
|
||||
|
||||
vertex CellBgVertexOut cell_bg_vertex(
|
||||
uint vid [[vertex_id]],
|
||||
constant Uniforms& uniforms [[buffer(1)]]
|
||||
) {
|
||||
CellBgVertexOut out;
|
||||
|
||||
float4 position;
|
||||
position.x = (vid == 2) ? 3.0 : -1.0;
|
||||
position.y = (vid == 0) ? -3.0 : 1.0;
|
||||
position.zw = 1.0;
|
||||
out.position = position;
|
||||
|
||||
// Convert the background color to Display P3
|
||||
out.bg_color = load_color(
|
||||
uniforms.bg_color,
|
||||
uniforms.use_display_p3,
|
||||
uniforms.use_linear_blending
|
||||
);
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
fragment float4 cell_bg_fragment(
|
||||
FullScreenVertexOut in [[stage_in]],
|
||||
CellBgVertexOut in [[stage_in]],
|
||||
constant uchar4 *cells [[buffer(0)]],
|
||||
constant Uniforms& uniforms [[buffer(1)]]
|
||||
) {
|
||||
int2 grid_pos = int2(floor((in.position.xy - uniforms.grid_padding.wx) / uniforms.cell_size));
|
||||
|
||||
float4 bg = in.bg_color;
|
||||
|
||||
// Clamp x position, extends edge bg colors in to padding on sides.
|
||||
if (grid_pos.x < 0) {
|
||||
if (uniforms.padding_extend & EXTEND_LEFT) {
|
||||
grid_pos.x = 0;
|
||||
} else {
|
||||
return float4(0.0);
|
||||
return bg;
|
||||
}
|
||||
} else if (grid_pos.x > uniforms.grid_size.x - 1) {
|
||||
if (uniforms.padding_extend & EXTEND_RIGHT) {
|
||||
grid_pos.x = uniforms.grid_size.x - 1;
|
||||
} else {
|
||||
return float4(0.0);
|
||||
return bg;
|
||||
}
|
||||
}
|
||||
|
||||
@ -139,18 +270,32 @@ fragment float4 cell_bg_fragment(
|
||||
if (uniforms.padding_extend & EXTEND_UP) {
|
||||
grid_pos.y = 0;
|
||||
} else {
|
||||
return float4(0.0);
|
||||
return bg;
|
||||
}
|
||||
} else if (grid_pos.y > uniforms.grid_size.y - 1) {
|
||||
if (uniforms.padding_extend & EXTEND_DOWN) {
|
||||
grid_pos.y = uniforms.grid_size.y - 1;
|
||||
} else {
|
||||
return float4(0.0);
|
||||
return bg;
|
||||
}
|
||||
}
|
||||
|
||||
// Retrieve color for cell and return it.
|
||||
return float4(cells[grid_pos.y * uniforms.grid_size.x + grid_pos.x]) / 255.0;
|
||||
// We load the color for the cell, converting it appropriately, and return it.
|
||||
//
|
||||
// TODO: We may want to blend the color with the background
|
||||
// color, rather than purely replacing it, this needs
|
||||
// some consideration about config options though.
|
||||
//
|
||||
// TODO: It might be a good idea to do a pass before this
|
||||
// to convert all of the bg colors, so we don't waste
|
||||
// a bunch of work converting the cell color in every
|
||||
// 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],
|
||||
uniforms.use_display_p3,
|
||||
uniforms.use_linear_blending
|
||||
);
|
||||
}
|
||||
|
||||
//-------------------------------------------------------------------
|
||||
@ -222,7 +367,6 @@ vertex CellTextVertexOut cell_text_vertex(
|
||||
|
||||
CellTextVertexOut out;
|
||||
out.mode = in.mode;
|
||||
out.color = float4(in.color) / 255.0f;
|
||||
|
||||
// === Grid Cell ===
|
||||
// +X
|
||||
@ -277,6 +421,14 @@ vertex CellTextVertexOut cell_text_vertex(
|
||||
// be sampled with pixel coordinate mode.
|
||||
out.tex_coord = float2(in.glyph_pos) + float2(in.glyph_size) * corner;
|
||||
|
||||
// Get our color. We always fetch a linearized version to
|
||||
// make it easier to handle minimum contrast calculations.
|
||||
out.color = load_color(
|
||||
in.color,
|
||||
uniforms.use_display_p3,
|
||||
true
|
||||
);
|
||||
|
||||
// If we have a minimum contrast, we need to check if we need to
|
||||
// change the color of the text to ensure it has enough contrast
|
||||
// with the background.
|
||||
@ -285,7 +437,13 @@ vertex CellTextVertexOut cell_text_vertex(
|
||||
// and Powerline glyphs to be unaffected (else parts of the line would
|
||||
// have different colors as some parts are displayed via background colors).
|
||||
if (uniforms.min_contrast > 1.0f && in.mode == MODE_TEXT) {
|
||||
float4 bg_color = float4(bg_colors[in.grid_pos.y * uniforms.grid_size.x + in.grid_pos.x]) / 255.0f;
|
||||
// Get the BG color
|
||||
float4 bg_color = load_color(
|
||||
bg_colors[in.grid_pos.y * uniforms.grid_size.x + in.grid_pos.x],
|
||||
uniforms.use_display_p3,
|
||||
true
|
||||
);
|
||||
// Ensure our minimum contrast
|
||||
out.color = contrasted_color(uniforms.min_contrast, out.color, bg_color);
|
||||
}
|
||||
|
||||
@ -308,7 +466,8 @@ vertex CellTextVertexOut cell_text_vertex(
|
||||
fragment float4 cell_text_fragment(
|
||||
CellTextVertexOut in [[stage_in]],
|
||||
texture2d<float> textureGrayscale [[texture(0)]],
|
||||
texture2d<float> textureColor [[texture(1)]]
|
||||
texture2d<float> textureColor [[texture(1)]],
|
||||
constant Uniforms& uniforms [[buffer(2)]]
|
||||
) {
|
||||
constexpr sampler textureSampler(
|
||||
coord::pixel,
|
||||
@ -322,20 +481,63 @@ fragment float4 cell_text_fragment(
|
||||
case MODE_TEXT_CONSTRAINED:
|
||||
case MODE_TEXT_POWERLINE:
|
||||
case MODE_TEXT: {
|
||||
// We premult the alpha to our whole color since our blend function
|
||||
// uses One/OneMinusSourceAlpha to avoid blurry edges.
|
||||
// We first premult our given color.
|
||||
float4 premult = float4(in.color.rgb * in.color.a, in.color.a);
|
||||
// Our input color is always linear.
|
||||
float4 color = in.color;
|
||||
|
||||
// Then premult the texture color
|
||||
// If we're not doing linear blending, then we need to
|
||||
// re-apply the gamma encoding to our color manually.
|
||||
//
|
||||
// We do it BEFORE premultiplying the alpha because
|
||||
// we want to produce the effect of not linearizing
|
||||
// it in the first place in order to match the look
|
||||
// of software that never does this.
|
||||
if (!uniforms.use_linear_blending) {
|
||||
color = unlinearize(color);
|
||||
}
|
||||
|
||||
// Fetch our alpha mask for this pixel.
|
||||
float a = textureGrayscale.sample(textureSampler, in.tex_coord).r;
|
||||
premult = premult * a;
|
||||
|
||||
return premult;
|
||||
// Experimental linear blending weight correction.
|
||||
if (uniforms.use_experimental_linear_correction) {
|
||||
float l = luminance(color.rgb);
|
||||
|
||||
// TODO: This is a dynamic dilation term that biases
|
||||
// the alpha adjustment for small font sizes;
|
||||
// it should be computed by dividing the font
|
||||
// size in `pt`s by `13.0` and using that if
|
||||
// it's less than `1.0`, but for now it's
|
||||
// hard coded at 1.0, which has no effect.
|
||||
float d = 13.0 / 13.0;
|
||||
|
||||
a += pow(a, d + d * l) - pow(a, d + 1.0 - d * l);
|
||||
}
|
||||
|
||||
// Multiply our whole color by the alpha mask.
|
||||
// Since we use premultiplied alpha, this is
|
||||
// the correct way to apply the mask.
|
||||
color *= a;
|
||||
|
||||
return color;
|
||||
}
|
||||
|
||||
case MODE_TEXT_COLOR: {
|
||||
return textureColor.sample(textureSampler, in.tex_coord);
|
||||
// For now, we assume that color glyphs are
|
||||
// already premultiplied Display P3 colors.
|
||||
float4 color = textureColor.sample(textureSampler, in.tex_coord);
|
||||
|
||||
// If we aren't doing linear blending, we can return this right away.
|
||||
if (!uniforms.use_linear_blending) {
|
||||
return color;
|
||||
}
|
||||
|
||||
// Otherwise we need to linearize the color. Since the alpha is
|
||||
// premultiplied, we need to divide it out before linearizing.
|
||||
color.rgb /= color.a;
|
||||
color = linearize(color);
|
||||
color.rgb *= color.a;
|
||||
|
||||
return color;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -409,7 +611,8 @@ vertex ImageVertexOut image_vertex(
|
||||
|
||||
fragment float4 image_fragment(
|
||||
ImageVertexOut in [[stage_in]],
|
||||
texture2d<uint> image [[texture(0)]]
|
||||
texture2d<uint> image [[texture(0)]],
|
||||
constant Uniforms& uniforms [[buffer(1)]]
|
||||
) {
|
||||
constexpr sampler textureSampler(address::clamp_to_edge, filter::linear);
|
||||
|
||||
@ -418,10 +621,12 @@ fragment float4 image_fragment(
|
||||
// our texture to BGRA8Unorm.
|
||||
uint4 rgba = image.sample(textureSampler, in.tex_coord);
|
||||
|
||||
// Convert to float4 and premultiply the alpha. We should also probably
|
||||
// premultiply the alpha in the texture.
|
||||
float4 result = float4(rgba) / 255.0f;
|
||||
result.rgb *= result.a;
|
||||
return result;
|
||||
return load_color(
|
||||
uchar4(rgba),
|
||||
// We assume all images are sRGB regardless of the configured colorspace
|
||||
// TODO: Maybe support wide gamut images?
|
||||
false,
|
||||
uniforms.use_linear_blending
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -520,6 +520,7 @@ pub fn clone(
|
||||
assert(node.data.capacity.rows >= chunk.end - chunk.start);
|
||||
defer node.data.assertIntegrity();
|
||||
node.data.size.rows = chunk.end - chunk.start;
|
||||
node.data.size.cols = chunk.node.data.size.cols;
|
||||
try node.data.cloneFrom(
|
||||
&chunk.node.data,
|
||||
chunk.start,
|
||||
|
Reference in New Issue
Block a user