metal impl

This commit is contained in:
Rohit-Bevinahally
2025-01-20 01:25:32 +05:30
committed by rohitb
parent 320dc35d0a
commit 1934f7daa3
3 changed files with 513 additions and 3 deletions

View File

@ -7,6 +7,7 @@ pub const Metal = @This();
const std = @import("std");
const builtin = @import("builtin");
const glfw = @import("glfw");
const wuffs = @import("wuffs");
const objc = @import("objc");
const macos = @import("macos");
const imgui = @import("imgui");
@ -59,6 +60,9 @@ const glfwNative = glfw.Native(.{
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
alloc: std.mem.Allocator,
@ -130,6 +134,16 @@ font_grid: *font.SharedGrid,
font_shaper: font.Shaper,
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.
images: ImageMap = .{},
image_placements: ImagePlacementList = .{},
@ -429,6 +443,9 @@ pub const DerivedConfig = struct {
cursor_text: ?terminal.color.RGB,
background: terminal.color.RGB,
background_opacity: f64,
background_image: configpkg.SinglePath,
background_image_opacity: f32,
background_image_mode: configpkg.BackgroundImageMode,
foreground: terminal.color.RGB,
selection_background: ?terminal.color.RGB,
selection_foreground: ?terminal.color.RGB,
@ -453,6 +470,9 @@ pub const DerivedConfig = struct {
// Copy our shaders
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
const font_features = try config.@"font-feature".clone(alloc);
@ -493,6 +513,11 @@ pub const DerivedConfig = struct {
.background = config.background.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",
.bold_is_bright = config.@"bold-is-bright",
.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,
.background_color = null,
.default_background_color = options.config.background,
.background_image = options.config.background_image,
.background_image_mode = options.config.background_image_mode,
.cursor_color = null,
.default_cursor_color = options.config.cursor_color,
.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_linear_blending = options.config.blending.isLinear(),
.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
@ -1143,6 +1173,17 @@ pub fn updateFrame(
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
// the entire screen. This can be optimized in the future.
const full_rebuild: bool = rebuild: {
@ -1232,6 +1273,7 @@ pub fn updateFrame(
// TODO: Is this expensive? Should we be checking if our
// bg color has changed first before doing this work?
{
std.log.info("Updating background color to {}", .{critical.bg});
const color = graphics.c.CGColorCreate(
@ptrCast(self.terminal_colorspace),
&[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.
@ -1405,7 +1472,10 @@ pub fn drawFrame(self: *Metal, surface: *apprt.Surface) !void {
);
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]);
// 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(
self: *Metal,
encoder: objc.Object,
@ -2118,6 +2274,82 @@ fn prepKittyImage(
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.
pub fn changeConfig(self: *Metal, config: *DerivedConfig) !void {
// 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.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.
{
// 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_linear_blending = old.use_linear_blending,
.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.
@ -2709,7 +2953,7 @@ fn rebuildCells(
const bg_alpha: u8 = bg_alpha: {
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.
if (selected) break :bg_alpha default;
@ -2722,6 +2966,11 @@ fn rebuildCells(
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.
break :bg_alpha @intFromFloat(@round(self.config.background_opacity * 255.0));
};

View File

@ -4,6 +4,7 @@ const assert = std.debug.assert;
const macos = @import("macos");
const objc = @import("objc");
const math = @import("../../math.zig");
const configpkg = @import("../../config.zig");
const mtl = @import("api.zig");
@ -24,6 +25,9 @@ pub const Shaders = struct {
/// like the Kitty image protocol.
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
/// can be used to apply a lot of effects. Each shader is run in sequence
/// against the output of the previous shader.
@ -52,6 +56,9 @@ pub const Shaders = struct {
const image_pipeline = try initImagePipeline(device, library, pixel_format);
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(
alloc,
device,
@ -75,6 +82,7 @@ pub const Shaders = struct {
.cell_text_pipeline = cell_text_pipeline,
.cell_bg_pipeline = cell_bg_pipeline,
.image_pipeline = image_pipeline,
.bg_image_pipeline = bg_image_pipeline,
.post_pipelines = post_pipelines,
};
}
@ -84,6 +92,7 @@ pub const Shaders = struct {
self.cell_text_pipeline.msgSend(void, objc.sel("release"), .{});
self.cell_bg_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"), .{});
// Release our postprocess shaders
@ -104,6 +113,12 @@ pub const Image = extern struct {
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.
pub const Uniforms = extern struct {
// Note: all of the explicit aligmnments are copied from the
@ -160,6 +175,12 @@ pub const Uniforms = extern struct {
/// (thickness) to gamma-incorrect blending.
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) {
left: bool = false,
right: bool = false,
@ -680,6 +701,119 @@ fn initImagePipeline(
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 {
inline for (@typeInfo(T).@"struct".fields, 0..) |field, i| {
const offset = @offsetOf(T, field.name);

View File

@ -23,6 +23,9 @@ struct Uniforms {
bool use_display_p3;
bool use_linear_blending;
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;
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
out.bg_color = load_color(
uniforms.bg_color,
bg_color,
uniforms.use_display_p3,
uniforms.use_linear_blending
);
@ -677,3 +685,122 @@ fragment float4 image_fragment(
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);
}