mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-08-02 14:57:31 +03:00
metal impl
This commit is contained in:

committed by
rohitb

parent
320dc35d0a
commit
1934f7daa3
@ -7,6 +7,7 @@ pub const Metal = @This();
|
|||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const builtin = @import("builtin");
|
const builtin = @import("builtin");
|
||||||
const glfw = @import("glfw");
|
const glfw = @import("glfw");
|
||||||
|
const wuffs = @import("wuffs");
|
||||||
const objc = @import("objc");
|
const objc = @import("objc");
|
||||||
const macos = @import("macos");
|
const macos = @import("macos");
|
||||||
const imgui = @import("imgui");
|
const imgui = @import("imgui");
|
||||||
@ -59,6 +60,9 @@ const glfwNative = glfw.Native(.{
|
|||||||
|
|
||||||
const log = std.log.scoped(.metal);
|
const log = std.log.scoped(.metal);
|
||||||
|
|
||||||
|
/// The maximum size of a background image.
|
||||||
|
const max_image_size = 400 * 1024 * 1024; // 400MB
|
||||||
|
|
||||||
/// Allocator that can be used
|
/// Allocator that can be used
|
||||||
alloc: std.mem.Allocator,
|
alloc: std.mem.Allocator,
|
||||||
|
|
||||||
@ -130,6 +134,16 @@ font_grid: *font.SharedGrid,
|
|||||||
font_shaper: font.Shaper,
|
font_shaper: font.Shaper,
|
||||||
font_shaper_cache: font.ShaperCache,
|
font_shaper_cache: font.ShaperCache,
|
||||||
|
|
||||||
|
/// The background image(s) to draw. Currently, we always draw the last image.
|
||||||
|
background_image: configpkg.SinglePath,
|
||||||
|
|
||||||
|
/// The background image mode to use.
|
||||||
|
background_image_mode: configpkg.BackgroundImageMode,
|
||||||
|
|
||||||
|
/// The current background image to draw. If it is null, then we will not
|
||||||
|
/// draw any background image.
|
||||||
|
current_background_image: ?Image = null,
|
||||||
|
|
||||||
/// The images that we may render.
|
/// The images that we may render.
|
||||||
images: ImageMap = .{},
|
images: ImageMap = .{},
|
||||||
image_placements: ImagePlacementList = .{},
|
image_placements: ImagePlacementList = .{},
|
||||||
@ -429,6 +443,9 @@ pub const DerivedConfig = struct {
|
|||||||
cursor_text: ?terminal.color.RGB,
|
cursor_text: ?terminal.color.RGB,
|
||||||
background: terminal.color.RGB,
|
background: terminal.color.RGB,
|
||||||
background_opacity: f64,
|
background_opacity: f64,
|
||||||
|
background_image: configpkg.SinglePath,
|
||||||
|
background_image_opacity: f32,
|
||||||
|
background_image_mode: configpkg.BackgroundImageMode,
|
||||||
foreground: terminal.color.RGB,
|
foreground: terminal.color.RGB,
|
||||||
selection_background: ?terminal.color.RGB,
|
selection_background: ?terminal.color.RGB,
|
||||||
selection_foreground: ?terminal.color.RGB,
|
selection_foreground: ?terminal.color.RGB,
|
||||||
@ -453,6 +470,9 @@ pub const DerivedConfig = struct {
|
|||||||
// Copy our shaders
|
// Copy our shaders
|
||||||
const custom_shaders = try config.@"custom-shader".clone(alloc);
|
const custom_shaders = try config.@"custom-shader".clone(alloc);
|
||||||
|
|
||||||
|
// Copy our background image
|
||||||
|
const background_image = try config.@"background-image".clone(alloc);
|
||||||
|
|
||||||
// Copy our font features
|
// Copy our font features
|
||||||
const font_features = try config.@"font-feature".clone(alloc);
|
const font_features = try config.@"font-feature".clone(alloc);
|
||||||
|
|
||||||
@ -493,6 +513,11 @@ pub const DerivedConfig = struct {
|
|||||||
|
|
||||||
.background = config.background.toTerminalRGB(),
|
.background = config.background.toTerminalRGB(),
|
||||||
.foreground = config.foreground.toTerminalRGB(),
|
.foreground = config.foreground.toTerminalRGB(),
|
||||||
|
|
||||||
|
.background_image = background_image,
|
||||||
|
.background_image_opacity = config.@"background-image-opacity",
|
||||||
|
.background_image_mode = config.@"background-image-mode",
|
||||||
|
|
||||||
.invert_selection_fg_bg = config.@"selection-invert-fg-bg",
|
.invert_selection_fg_bg = config.@"selection-invert-fg-bg",
|
||||||
.bold_is_bright = config.@"bold-is-bright",
|
.bold_is_bright = config.@"bold-is-bright",
|
||||||
.min_contrast = @floatCast(config.@"minimum-contrast"),
|
.min_contrast = @floatCast(config.@"minimum-contrast"),
|
||||||
@ -692,6 +717,8 @@ pub fn init(alloc: Allocator, options: renderer.Options) !Metal {
|
|||||||
.default_foreground_color = options.config.foreground,
|
.default_foreground_color = options.config.foreground,
|
||||||
.background_color = null,
|
.background_color = null,
|
||||||
.default_background_color = options.config.background,
|
.default_background_color = options.config.background,
|
||||||
|
.background_image = options.config.background_image,
|
||||||
|
.background_image_mode = options.config.background_image_mode,
|
||||||
.cursor_color = null,
|
.cursor_color = null,
|
||||||
.default_cursor_color = options.config.cursor_color,
|
.default_cursor_color = options.config.cursor_color,
|
||||||
.cursor_invert = options.config.cursor_invert,
|
.cursor_invert = options.config.cursor_invert,
|
||||||
@ -717,6 +744,9 @@ pub fn init(alloc: Allocator, options: renderer.Options) !Metal {
|
|||||||
.use_display_p3 = options.config.colorspace == .@"display-p3",
|
.use_display_p3 = options.config.colorspace == .@"display-p3",
|
||||||
.use_linear_blending = options.config.blending.isLinear(),
|
.use_linear_blending = options.config.blending.isLinear(),
|
||||||
.use_linear_correction = options.config.blending == .@"linear-corrected",
|
.use_linear_correction = options.config.blending == .@"linear-corrected",
|
||||||
|
.use_experimental_linear_correction = options.config.blending == .@"linear-corrected",
|
||||||
|
.has_bg_image = (options.config.background_image.value != null),
|
||||||
|
.bg_image_opacity = options.config.background_image_opacity,
|
||||||
},
|
},
|
||||||
|
|
||||||
// Fonts
|
// Fonts
|
||||||
@ -1143,6 +1173,17 @@ pub fn updateFrame(
|
|||||||
try self.prepKittyGraphics(state.terminal);
|
try self.prepKittyGraphics(state.terminal);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (self.current_background_image == null and
|
||||||
|
self.background_image.value != null)
|
||||||
|
{
|
||||||
|
self.prepBackgroundImage() catch |err| switch (err) {
|
||||||
|
error.InvalidData => {
|
||||||
|
log.warn("invalid image data, skipping", .{});
|
||||||
|
},
|
||||||
|
else => return err,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// If we have any terminal dirty flags set then we need to rebuild
|
// If we have any terminal dirty flags set then we need to rebuild
|
||||||
// the entire screen. This can be optimized in the future.
|
// the entire screen. This can be optimized in the future.
|
||||||
const full_rebuild: bool = rebuild: {
|
const full_rebuild: bool = rebuild: {
|
||||||
@ -1232,6 +1273,7 @@ pub fn updateFrame(
|
|||||||
// TODO: Is this expensive? Should we be checking if our
|
// TODO: Is this expensive? Should we be checking if our
|
||||||
// bg color has changed first before doing this work?
|
// bg color has changed first before doing this work?
|
||||||
{
|
{
|
||||||
|
std.log.info("Updating background color to {}", .{critical.bg});
|
||||||
const color = graphics.c.CGColorCreate(
|
const color = graphics.c.CGColorCreate(
|
||||||
@ptrCast(self.terminal_colorspace),
|
@ptrCast(self.terminal_colorspace),
|
||||||
&[4]f64{
|
&[4]f64{
|
||||||
@ -1284,6 +1326,31 @@ pub fn updateFrame(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if we need to update our current background image
|
||||||
|
if (self.current_background_image) |current_background_image| {
|
||||||
|
switch (current_background_image) {
|
||||||
|
.ready => {},
|
||||||
|
|
||||||
|
.pending_gray,
|
||||||
|
.pending_gray_alpha,
|
||||||
|
.pending_rgb,
|
||||||
|
.pending_rgba,
|
||||||
|
.replace_gray,
|
||||||
|
.replace_gray_alpha,
|
||||||
|
.replace_rgb,
|
||||||
|
.replace_rgba,
|
||||||
|
=> try self.current_background_image.?.upload(self.alloc, self.gpu_state.device),
|
||||||
|
|
||||||
|
.unload_pending,
|
||||||
|
.unload_replace,
|
||||||
|
.unload_ready,
|
||||||
|
=> {
|
||||||
|
self.current_background_image.?.deinit(self.alloc);
|
||||||
|
self.current_background_image = null;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Draw the frame to the screen.
|
/// Draw the frame to the screen.
|
||||||
@ -1405,7 +1472,10 @@ pub fn drawFrame(self: *Metal, surface: *apprt.Surface) !void {
|
|||||||
);
|
);
|
||||||
defer encoder.msgSend(void, objc.sel("endEncoding"), .{});
|
defer encoder.msgSend(void, objc.sel("endEncoding"), .{});
|
||||||
|
|
||||||
// Draw background images first
|
// Draw background image set by the user first
|
||||||
|
try self.drawBackgroundImage(encoder, frame);
|
||||||
|
|
||||||
|
// Then draw background images
|
||||||
try self.drawImagePlacements(encoder, frame, 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
|
// Then draw background cells
|
||||||
@ -1608,6 +1678,92 @@ fn drawPostShader(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn drawBackgroundImage(
|
||||||
|
self: *Metal,
|
||||||
|
encoder: objc.Object,
|
||||||
|
frame: *const FrameState,
|
||||||
|
) !void {
|
||||||
|
// If we don't have a background image, just return
|
||||||
|
const current_background_image = self.current_background_image orelse return;
|
||||||
|
|
||||||
|
// Use our background image shader pipeline
|
||||||
|
encoder.msgSend(
|
||||||
|
void,
|
||||||
|
objc.sel("setRenderPipelineState:"),
|
||||||
|
.{self.shaders.bg_image_pipeline.value},
|
||||||
|
);
|
||||||
|
|
||||||
|
// Set our uniforms
|
||||||
|
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:"),
|
||||||
|
.{ frame.uniforms.buffer.value, @as(c_ulong, 0), @as(c_ulong, 1) },
|
||||||
|
);
|
||||||
|
|
||||||
|
// Get the texture
|
||||||
|
const texture = switch (current_background_image) {
|
||||||
|
.ready => |t| t,
|
||||||
|
else => {
|
||||||
|
return;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create our vertex buffer, which is always exactly one item.
|
||||||
|
const Buffer = mtl_buffer.Buffer(mtl_shaders.BgImage);
|
||||||
|
var buf = try Buffer.initFill(self.gpu_state.device, &.{.{
|
||||||
|
.terminal_size = .{
|
||||||
|
@as(f32, @floatFromInt(self.size.terminal().width)),
|
||||||
|
@as(f32, @floatFromInt(self.size.terminal().height)),
|
||||||
|
},
|
||||||
|
.mode = self.background_image_mode,
|
||||||
|
}});
|
||||||
|
defer buf.deinit();
|
||||||
|
|
||||||
|
// Set our buffer
|
||||||
|
encoder.msgSend(
|
||||||
|
void,
|
||||||
|
objc.sel("setVertexBuffer:offset:atIndex:"),
|
||||||
|
.{ buf.buffer.value, @as(c_ulong, 0), @as(c_ulong, 0) },
|
||||||
|
);
|
||||||
|
|
||||||
|
// Set our texture
|
||||||
|
encoder.msgSend(
|
||||||
|
void,
|
||||||
|
objc.sel("setVertexTexture:atIndex:"),
|
||||||
|
.{
|
||||||
|
texture.value,
|
||||||
|
@as(c_ulong, 0),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
encoder.msgSend(
|
||||||
|
void,
|
||||||
|
objc.sel("setFragmentTexture:atIndex:"),
|
||||||
|
.{
|
||||||
|
texture.value,
|
||||||
|
@as(c_ulong, 0),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// Draw!
|
||||||
|
encoder.msgSend(
|
||||||
|
void,
|
||||||
|
objc.sel("drawIndexedPrimitives:indexCount:indexType:indexBuffer:indexBufferOffset:instanceCount:"),
|
||||||
|
.{
|
||||||
|
@intFromEnum(mtl.MTLPrimitiveType.triangle),
|
||||||
|
@as(c_ulong, 6),
|
||||||
|
@intFromEnum(mtl.MTLIndexType.uint16),
|
||||||
|
self.gpu_state.instance.buffer.value,
|
||||||
|
@as(c_ulong, 0),
|
||||||
|
@as(c_ulong, 1),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
fn drawImagePlacements(
|
fn drawImagePlacements(
|
||||||
self: *Metal,
|
self: *Metal,
|
||||||
encoder: objc.Object,
|
encoder: objc.Object,
|
||||||
@ -2118,6 +2274,82 @@ fn prepKittyImage(
|
|||||||
gop.value_ptr.transmit_time = image.transmit_time;
|
gop.value_ptr.transmit_time = image.transmit_time;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Prepares the current background image for upload
|
||||||
|
pub fn prepBackgroundImage(self: *Metal) !void {
|
||||||
|
// If the user doesn't have a background image, do nothing...
|
||||||
|
const path = self.background_image.value orelse return;
|
||||||
|
|
||||||
|
// Read the file content
|
||||||
|
const file_content = try self.readImageContent(path);
|
||||||
|
defer self.alloc.free(file_content);
|
||||||
|
|
||||||
|
// Decode the image
|
||||||
|
const decoded_image: wuffs.ImageData = blk: {
|
||||||
|
// Extract the file extension
|
||||||
|
const ext = std.fs.path.extension(path);
|
||||||
|
const ext_lower = try std.ascii.allocLowerString(self.alloc, ext);
|
||||||
|
defer self.alloc.free(ext_lower);
|
||||||
|
|
||||||
|
// Match based on extension
|
||||||
|
if (std.mem.eql(u8, ext_lower, ".png")) {
|
||||||
|
break :blk try wuffs.png.decode(self.alloc, file_content);
|
||||||
|
} else if (std.mem.eql(u8, ext_lower, ".jpg") or std.mem.eql(u8, ext_lower, ".jpeg")) {
|
||||||
|
break :blk try wuffs.jpeg.decode(self.alloc, file_content);
|
||||||
|
} else {
|
||||||
|
log.warn("unsupported image format: {s}", .{ext});
|
||||||
|
return error.InvalidData;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
defer self.alloc.free(decoded_image.data);
|
||||||
|
|
||||||
|
// Copy the data into the pending state
|
||||||
|
const data = try self.alloc.dupe(u8, decoded_image.data);
|
||||||
|
errdefer self.alloc.free(data);
|
||||||
|
const pending: Image.Pending = .{
|
||||||
|
.width = decoded_image.width,
|
||||||
|
.height = decoded_image.height,
|
||||||
|
.data = data.ptr,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Store the image
|
||||||
|
self.current_background_image = .{ .pending_rgba = pending };
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reads the content of the given image path and returns it
|
||||||
|
pub fn readImageContent(self: *Metal, path: []const u8) ![]u8 {
|
||||||
|
assert(std.fs.path.isAbsolute(path));
|
||||||
|
// Open the file
|
||||||
|
var file = std.fs.openFileAbsolute(path, .{}) catch |err| {
|
||||||
|
log.warn("failed to open file: {}", .{err});
|
||||||
|
return error.InvalidData;
|
||||||
|
};
|
||||||
|
defer file.close();
|
||||||
|
|
||||||
|
// File must be a regular file
|
||||||
|
if (file.stat()) |stat| {
|
||||||
|
if (stat.kind != .file) {
|
||||||
|
log.warn("file is not a regular file kind={}", .{stat.kind});
|
||||||
|
return error.InvalidData;
|
||||||
|
}
|
||||||
|
} else |err| {
|
||||||
|
log.warn("failed to stat file: {}", .{err});
|
||||||
|
return error.InvalidData;
|
||||||
|
}
|
||||||
|
|
||||||
|
var buf_reader = std.io.bufferedReader(file.reader());
|
||||||
|
const reader = buf_reader.reader();
|
||||||
|
|
||||||
|
// Read the file
|
||||||
|
var managed = std.ArrayList(u8).init(self.alloc);
|
||||||
|
errdefer managed.deinit();
|
||||||
|
reader.readAllArrayList(&managed, max_image_size) catch |err| {
|
||||||
|
log.warn("failed to read file: {}", .{err});
|
||||||
|
return error.InvalidData;
|
||||||
|
};
|
||||||
|
|
||||||
|
return managed.toOwnedSlice();
|
||||||
|
}
|
||||||
|
|
||||||
/// Update the configuration.
|
/// Update the configuration.
|
||||||
pub fn changeConfig(self: *Metal, config: *DerivedConfig) !void {
|
pub fn changeConfig(self: *Metal, config: *DerivedConfig) !void {
|
||||||
// We always redo the font shaper in case font features changed. We
|
// We always redo the font shaper in case font features changed. We
|
||||||
@ -2152,6 +2384,15 @@ 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;
|
||||||
|
|
||||||
|
// Reset current background image
|
||||||
|
self.background_image = config.background_image;
|
||||||
|
self.uniforms.has_bg_image = (config.background_image.value != null);
|
||||||
|
self.uniforms.bg_image_opacity = config.background_image_opacity;
|
||||||
|
self.background_image_mode = config.background_image_mode;
|
||||||
|
if (self.current_background_image) |*img| {
|
||||||
|
img.markForUnload();
|
||||||
|
}
|
||||||
|
|
||||||
// Update our layer's opaqueness and display sync in case they changed.
|
// Update our layer's opaqueness and display sync in case they changed.
|
||||||
{
|
{
|
||||||
// We use a CATransaction so that Core Animation knows that we
|
// We use a CATransaction so that Core Animation knows that we
|
||||||
@ -2288,6 +2529,9 @@ pub fn setScreenSize(
|
|||||||
.use_display_p3 = old.use_display_p3,
|
.use_display_p3 = old.use_display_p3,
|
||||||
.use_linear_blending = old.use_linear_blending,
|
.use_linear_blending = old.use_linear_blending,
|
||||||
.use_linear_correction = old.use_linear_correction,
|
.use_linear_correction = old.use_linear_correction,
|
||||||
|
.use_experimental_linear_correction = old.use_experimental_linear_correction,
|
||||||
|
.has_bg_image = old.has_bg_image,
|
||||||
|
.bg_image_opacity = old.bg_image_opacity,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Reset our cell contents if our grid size has changed.
|
// Reset our cell contents if our grid size has changed.
|
||||||
@ -2709,7 +2953,7 @@ fn rebuildCells(
|
|||||||
const bg_alpha: u8 = bg_alpha: {
|
const bg_alpha: u8 = bg_alpha: {
|
||||||
const default: u8 = 255;
|
const default: u8 = 255;
|
||||||
|
|
||||||
if (self.config.background_opacity >= 1) break :bg_alpha default;
|
if (self.current_background_image == null and self.config.background_opacity >= 1) break :bg_alpha default;
|
||||||
|
|
||||||
// Cells that are selected should be fully opaque.
|
// Cells that are selected should be fully opaque.
|
||||||
if (selected) break :bg_alpha default;
|
if (selected) break :bg_alpha default;
|
||||||
@ -2722,6 +2966,11 @@ fn rebuildCells(
|
|||||||
break :bg_alpha default;
|
break :bg_alpha default;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If we have a background image, use the configured background image opacity.
|
||||||
|
if (self.current_background_image != null) {
|
||||||
|
break :bg_alpha @intFromFloat(@round((1 - self.config.background_image_opacity) * 255.0));
|
||||||
|
}
|
||||||
|
|
||||||
// Otherwise, we use the configured background opacity.
|
// Otherwise, we use the configured background opacity.
|
||||||
break :bg_alpha @intFromFloat(@round(self.config.background_opacity * 255.0));
|
break :bg_alpha @intFromFloat(@round(self.config.background_opacity * 255.0));
|
||||||
};
|
};
|
||||||
|
@ -4,6 +4,7 @@ const assert = std.debug.assert;
|
|||||||
const macos = @import("macos");
|
const macos = @import("macos");
|
||||||
const objc = @import("objc");
|
const objc = @import("objc");
|
||||||
const math = @import("../../math.zig");
|
const math = @import("../../math.zig");
|
||||||
|
const configpkg = @import("../../config.zig");
|
||||||
|
|
||||||
const mtl = @import("api.zig");
|
const mtl = @import("api.zig");
|
||||||
|
|
||||||
@ -24,6 +25,9 @@ pub const Shaders = struct {
|
|||||||
/// like the Kitty image protocol.
|
/// like the Kitty image protocol.
|
||||||
image_pipeline: objc.Object,
|
image_pipeline: objc.Object,
|
||||||
|
|
||||||
|
/// Background image shader for images set by the user
|
||||||
|
bg_image_pipeline: objc.Object,
|
||||||
|
|
||||||
/// Custom shaders to run against the final drawable texture. This
|
/// Custom shaders to run against the final drawable texture. This
|
||||||
/// can be used to apply a lot of effects. Each shader is run in sequence
|
/// can be used to apply a lot of effects. Each shader is run in sequence
|
||||||
/// against the output of the previous shader.
|
/// against the output of the previous shader.
|
||||||
@ -52,6 +56,9 @@ pub const Shaders = struct {
|
|||||||
const image_pipeline = try initImagePipeline(device, library, pixel_format);
|
const image_pipeline = try initImagePipeline(device, library, pixel_format);
|
||||||
errdefer image_pipeline.msgSend(void, objc.sel("release"), .{});
|
errdefer image_pipeline.msgSend(void, objc.sel("release"), .{});
|
||||||
|
|
||||||
|
const bg_image_pipeline = try initBgImagePipeline(device, library, pixel_format);
|
||||||
|
errdefer bg_image_pipeline.msgSend(void, objc.sel("release"), .{});
|
||||||
|
|
||||||
const post_pipelines: []const objc.Object = initPostPipelines(
|
const post_pipelines: []const objc.Object = initPostPipelines(
|
||||||
alloc,
|
alloc,
|
||||||
device,
|
device,
|
||||||
@ -75,6 +82,7 @@ pub const Shaders = struct {
|
|||||||
.cell_text_pipeline = cell_text_pipeline,
|
.cell_text_pipeline = cell_text_pipeline,
|
||||||
.cell_bg_pipeline = cell_bg_pipeline,
|
.cell_bg_pipeline = cell_bg_pipeline,
|
||||||
.image_pipeline = image_pipeline,
|
.image_pipeline = image_pipeline,
|
||||||
|
.bg_image_pipeline = bg_image_pipeline,
|
||||||
.post_pipelines = post_pipelines,
|
.post_pipelines = post_pipelines,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -84,6 +92,7 @@ pub const Shaders = struct {
|
|||||||
self.cell_text_pipeline.msgSend(void, objc.sel("release"), .{});
|
self.cell_text_pipeline.msgSend(void, objc.sel("release"), .{});
|
||||||
self.cell_bg_pipeline.msgSend(void, objc.sel("release"), .{});
|
self.cell_bg_pipeline.msgSend(void, objc.sel("release"), .{});
|
||||||
self.image_pipeline.msgSend(void, objc.sel("release"), .{});
|
self.image_pipeline.msgSend(void, objc.sel("release"), .{});
|
||||||
|
self.bg_image_pipeline.msgSend(void, objc.sel("release"), .{});
|
||||||
self.library.msgSend(void, objc.sel("release"), .{});
|
self.library.msgSend(void, objc.sel("release"), .{});
|
||||||
|
|
||||||
// Release our postprocess shaders
|
// Release our postprocess shaders
|
||||||
@ -104,6 +113,12 @@ pub const Image = extern struct {
|
|||||||
dest_size: [2]f32,
|
dest_size: [2]f32,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// Single parameter for the background image shader. See shader for field details.
|
||||||
|
pub const BgImage = extern struct {
|
||||||
|
terminal_size: [2]f32,
|
||||||
|
mode: configpkg.BackgroundImageMode,
|
||||||
|
};
|
||||||
|
|
||||||
/// The uniforms that are passed to the terminal cell shader.
|
/// The uniforms that are passed to the terminal cell shader.
|
||||||
pub const Uniforms = extern struct {
|
pub const Uniforms = extern struct {
|
||||||
// Note: all of the explicit aligmnments are copied from the
|
// Note: all of the explicit aligmnments are copied from the
|
||||||
@ -160,6 +175,12 @@ pub const Uniforms = extern struct {
|
|||||||
/// (thickness) to gamma-incorrect blending.
|
/// (thickness) to gamma-incorrect blending.
|
||||||
use_linear_correction: bool align(1) = false,
|
use_linear_correction: bool align(1) = false,
|
||||||
|
|
||||||
|
/// Indicates if the user has set a background image.
|
||||||
|
has_bg_image: bool align(1),
|
||||||
|
|
||||||
|
/// The opacity of the background image.
|
||||||
|
bg_image_opacity: f32 align(4),
|
||||||
|
|
||||||
const PaddingExtend = packed struct(u8) {
|
const PaddingExtend = packed struct(u8) {
|
||||||
left: bool = false,
|
left: bool = false,
|
||||||
right: bool = false,
|
right: bool = false,
|
||||||
@ -680,6 +701,119 @@ fn initImagePipeline(
|
|||||||
return pipeline_state;
|
return pipeline_state;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn initBgImagePipeline(
|
||||||
|
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(
|
||||||
|
"bg_image_vertex",
|
||||||
|
.utf8,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
defer str.release();
|
||||||
|
|
||||||
|
const ptr = library.msgSend(?*anyopaque, objc.sel("newFunctionWithName:"), .{str});
|
||||||
|
break :func_vert objc.Object.fromId(ptr.?);
|
||||||
|
};
|
||||||
|
const func_frag = func_frag: {
|
||||||
|
const str = try macos.foundation.String.createWithBytes(
|
||||||
|
"bg_image_fragment",
|
||||||
|
.utf8,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
defer str.release();
|
||||||
|
|
||||||
|
const ptr = library.msgSend(?*anyopaque, objc.sel("newFunctionWithName:"), .{str});
|
||||||
|
break :func_frag objc.Object.fromId(ptr.?);
|
||||||
|
};
|
||||||
|
defer func_vert.msgSend(void, objc.sel("release"), .{});
|
||||||
|
defer func_frag.msgSend(void, objc.sel("release"), .{});
|
||||||
|
|
||||||
|
// Create the vertex descriptor. The vertex descriptor describes the
|
||||||
|
// data layout of the vertex inputs. We use indexed (or "instanced")
|
||||||
|
// rendering, so this makes it so that each instance gets a single
|
||||||
|
// Image as input.
|
||||||
|
const vertex_desc = vertex_desc: {
|
||||||
|
const desc = init: {
|
||||||
|
const Class = objc.getClass("MTLVertexDescriptor").?;
|
||||||
|
const id_alloc = Class.msgSend(objc.Object, objc.sel("alloc"), .{});
|
||||||
|
const id_init = id_alloc.msgSend(objc.Object, objc.sel("init"), .{});
|
||||||
|
break :init id_init;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Our attributes are the fields of the input
|
||||||
|
const attrs = objc.Object.fromId(desc.getProperty(?*anyopaque, "attributes"));
|
||||||
|
autoAttribute(BgImage, attrs);
|
||||||
|
|
||||||
|
// The layout describes how and when we fetch the next vertex input.
|
||||||
|
const layouts = objc.Object.fromId(desc.getProperty(?*anyopaque, "layouts"));
|
||||||
|
{
|
||||||
|
const layout = layouts.msgSend(
|
||||||
|
objc.Object,
|
||||||
|
objc.sel("objectAtIndexedSubscript:"),
|
||||||
|
.{@as(c_ulong, 0)},
|
||||||
|
);
|
||||||
|
|
||||||
|
// Access each Image per instance, not per vertex.
|
||||||
|
layout.setProperty("stepFunction", @intFromEnum(mtl.MTLVertexStepFunction.per_instance));
|
||||||
|
layout.setProperty("stride", @as(c_ulong, @sizeOf(BgImage)));
|
||||||
|
}
|
||||||
|
|
||||||
|
break :vertex_desc desc;
|
||||||
|
};
|
||||||
|
defer vertex_desc.msgSend(void, objc.sel("release"), .{});
|
||||||
|
|
||||||
|
// Create our descriptor
|
||||||
|
const desc = init: {
|
||||||
|
const Class = objc.getClass("MTLRenderPipelineDescriptor").?;
|
||||||
|
const id_alloc = Class.msgSend(objc.Object, objc.sel("alloc"), .{});
|
||||||
|
const id_init = id_alloc.msgSend(objc.Object, objc.sel("init"), .{});
|
||||||
|
break :init id_init;
|
||||||
|
};
|
||||||
|
defer desc.msgSend(void, objc.sel("release"), .{});
|
||||||
|
|
||||||
|
// Set our properties
|
||||||
|
desc.setProperty("vertexFunction", func_vert);
|
||||||
|
desc.setProperty("fragmentFunction", func_frag);
|
||||||
|
desc.setProperty("vertexDescriptor", vertex_desc);
|
||||||
|
|
||||||
|
// Set our color attachment
|
||||||
|
const attachments = objc.Object.fromId(desc.getProperty(?*anyopaque, "colorAttachments"));
|
||||||
|
{
|
||||||
|
const attachment = attachments.msgSend(
|
||||||
|
objc.Object,
|
||||||
|
objc.sel("objectAtIndexedSubscript:"),
|
||||||
|
.{@as(c_ulong, 0)},
|
||||||
|
);
|
||||||
|
|
||||||
|
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.
|
||||||
|
attachment.setProperty("blendingEnabled", true);
|
||||||
|
attachment.setProperty("rgbBlendOperation", @intFromEnum(mtl.MTLBlendOperation.add));
|
||||||
|
attachment.setProperty("alphaBlendOperation", @intFromEnum(mtl.MTLBlendOperation.add));
|
||||||
|
attachment.setProperty("sourceRGBBlendFactor", @intFromEnum(mtl.MTLBlendFactor.one));
|
||||||
|
attachment.setProperty("sourceAlphaBlendFactor", @intFromEnum(mtl.MTLBlendFactor.one));
|
||||||
|
attachment.setProperty("destinationRGBBlendFactor", @intFromEnum(mtl.MTLBlendFactor.one_minus_source_alpha));
|
||||||
|
attachment.setProperty("destinationAlphaBlendFactor", @intFromEnum(mtl.MTLBlendFactor.one_minus_source_alpha));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make our state
|
||||||
|
var err: ?*anyopaque = null;
|
||||||
|
const pipeline_state = device.msgSend(
|
||||||
|
objc.Object,
|
||||||
|
objc.sel("newRenderPipelineStateWithDescriptor:error:"),
|
||||||
|
.{ desc, &err },
|
||||||
|
);
|
||||||
|
try checkError(err);
|
||||||
|
|
||||||
|
return pipeline_state;
|
||||||
|
}
|
||||||
|
|
||||||
fn autoAttribute(T: type, attrs: objc.Object) void {
|
fn autoAttribute(T: type, attrs: objc.Object) void {
|
||||||
inline for (@typeInfo(T).@"struct".fields, 0..) |field, i| {
|
inline for (@typeInfo(T).@"struct".fields, 0..) |field, i| {
|
||||||
const offset = @offsetOf(T, field.name);
|
const offset = @offsetOf(T, field.name);
|
||||||
|
@ -23,6 +23,9 @@ struct Uniforms {
|
|||||||
bool use_display_p3;
|
bool use_display_p3;
|
||||||
bool use_linear_blending;
|
bool use_linear_blending;
|
||||||
bool use_linear_correction;
|
bool use_linear_correction;
|
||||||
|
bool use_experimental_linear_correction;
|
||||||
|
bool has_bg_image;
|
||||||
|
float bg_image_opacity;
|
||||||
};
|
};
|
||||||
|
|
||||||
//-------------------------------------------------------------------
|
//-------------------------------------------------------------------
|
||||||
@ -237,9 +240,14 @@ vertex CellBgVertexOut cell_bg_vertex(
|
|||||||
position.zw = 1.0;
|
position.zw = 1.0;
|
||||||
out.position = position;
|
out.position = position;
|
||||||
|
|
||||||
|
uchar4 bg_color = uniforms.bg_color;
|
||||||
|
if (uniforms.has_bg_image) {
|
||||||
|
bg_color.a = uchar((1.0f - uniforms.bg_image_opacity) * 255);
|
||||||
|
}
|
||||||
|
|
||||||
// Convert the background color to Display P3
|
// Convert the background color to Display P3
|
||||||
out.bg_color = load_color(
|
out.bg_color = load_color(
|
||||||
uniforms.bg_color,
|
bg_color,
|
||||||
uniforms.use_display_p3,
|
uniforms.use_display_p3,
|
||||||
uniforms.use_linear_blending
|
uniforms.use_linear_blending
|
||||||
);
|
);
|
||||||
@ -677,3 +685,122 @@ fragment float4 image_fragment(
|
|||||||
return rgba;
|
return rgba;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//-------------------------------------------------------------------
|
||||||
|
// Background Image Shader
|
||||||
|
//-------------------------------------------------------------------
|
||||||
|
#pragma mark - BG Image Shader
|
||||||
|
|
||||||
|
enum BgImageMode : uint8_t {
|
||||||
|
MODE_ZOOMED = 0u,
|
||||||
|
MODE_STRETCHED = 1u,
|
||||||
|
MODE_TILED = 2u,
|
||||||
|
MODE_CENTERED = 3u,
|
||||||
|
MODE_UPPER_LEFT = 4u,
|
||||||
|
MODE_UPPER_RIGHT = 5u,
|
||||||
|
MODE_LOWER_LEFT = 6u,
|
||||||
|
MODE_LOWER_RIGHT = 7u,
|
||||||
|
};
|
||||||
|
|
||||||
|
struct BgImageVertexIn {
|
||||||
|
float2 terminal_size [[attribute(0)]];
|
||||||
|
uint8_t mode [[attribute(1)]];
|
||||||
|
};
|
||||||
|
|
||||||
|
struct BgImageVertexOut {
|
||||||
|
float4 position [[position]];
|
||||||
|
float2 tex_coord;
|
||||||
|
};
|
||||||
|
|
||||||
|
vertex BgImageVertexOut bg_image_vertex(
|
||||||
|
uint vid [[vertex_id]],
|
||||||
|
BgImageVertexIn in [[stage_in]],
|
||||||
|
texture2d<uint> image [[texture(0)]],
|
||||||
|
constant Uniforms& uniforms [[buffer(1)]]
|
||||||
|
) {
|
||||||
|
BgImageVertexOut out;
|
||||||
|
|
||||||
|
// Calculate the position of the image
|
||||||
|
float2 position;
|
||||||
|
position.x = (vid == 0 || vid == 1) ? 1.0 : 0.0;
|
||||||
|
position.y = (vid == 0 || vid == 3) ? 0.0 : 1.0;
|
||||||
|
|
||||||
|
// Get the size of the image
|
||||||
|
float2 image_size = float2(image.get_width(), image.get_height());
|
||||||
|
|
||||||
|
// Handles the scale of the image relative to the terminal size
|
||||||
|
float2 scale = float2(1.0, 1.0);
|
||||||
|
|
||||||
|
switch (in.mode) {
|
||||||
|
case MODE_ZOOMED: {
|
||||||
|
// Scale to fit the terminal size
|
||||||
|
float2 aspect_ratio = float2(
|
||||||
|
in.terminal_size.x / in.terminal_size.y,
|
||||||
|
image_size.x / image_size.y
|
||||||
|
);
|
||||||
|
if (aspect_ratio.x > aspect_ratio.y) {
|
||||||
|
scale.x = aspect_ratio.y / aspect_ratio.x;
|
||||||
|
} else {
|
||||||
|
scale.y = aspect_ratio.x / aspect_ratio.y;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case MODE_CENTERED:
|
||||||
|
case MODE_UPPER_LEFT:
|
||||||
|
case MODE_UPPER_RIGHT:
|
||||||
|
case MODE_LOWER_LEFT:
|
||||||
|
case MODE_LOWER_RIGHT: {
|
||||||
|
// Scale to match the actual size of the image
|
||||||
|
scale.x = image_size.x / in.terminal_size.x;
|
||||||
|
scale.y = image_size.y / in.terminal_size.y;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case MODE_STRETCHED:
|
||||||
|
case MODE_TILED:
|
||||||
|
// No adjustments needed
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
float2 final_image_size = in.terminal_size * position * scale;
|
||||||
|
|
||||||
|
float2 offset = float2(0.0, 0.0);
|
||||||
|
switch (in.mode) {
|
||||||
|
case MODE_ZOOMED:
|
||||||
|
case MODE_STRETCHED:
|
||||||
|
case MODE_TILED:
|
||||||
|
case MODE_CENTERED:
|
||||||
|
offset = (in.terminal_size * (1.0 - scale)) / 2.0;
|
||||||
|
break;
|
||||||
|
case MODE_UPPER_LEFT:
|
||||||
|
offset = float2(0.0, 0.0);
|
||||||
|
break;
|
||||||
|
case MODE_UPPER_RIGHT:
|
||||||
|
offset = float2(in.terminal_size.x - image_size.x, 0.0);
|
||||||
|
break;
|
||||||
|
case MODE_LOWER_LEFT:
|
||||||
|
offset = float2(0.0, in.terminal_size.y - image_size.y);
|
||||||
|
break;
|
||||||
|
case MODE_LOWER_RIGHT:
|
||||||
|
offset = float2(in.terminal_size.x - image_size.x, in.terminal_size.y - image_size.y);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
out.position = uniforms.projection_matrix * float4(final_image_size + offset, 0.0, 1.0);
|
||||||
|
out.tex_coord = position;
|
||||||
|
if (in.mode == MODE_TILED) {
|
||||||
|
out.tex_coord = position * in.terminal_size / image_size;
|
||||||
|
}
|
||||||
|
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
fragment float4 bg_image_fragment(
|
||||||
|
BgImageVertexOut in [[stage_in]],
|
||||||
|
texture2d<uint> image [[texture(0)]],
|
||||||
|
constant Uniforms& uniforms [[buffer(1)]]
|
||||||
|
) {
|
||||||
|
constexpr sampler textureSampler(address::repeat, filter::linear);
|
||||||
|
float2 norm_coord = fract(in.tex_coord);
|
||||||
|
float4 color = float4(image.sample(textureSampler, norm_coord)) / 255.0f;
|
||||||
|
|
||||||
|
return float4(color.rgb * color.a * uniforms.bg_image_opacity, color.a * uniforms.bg_image_opacity);
|
||||||
|
}
|
||||||
|
Reference in New Issue
Block a user