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 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));
|
||||
};
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
}
|
||||
|
Reference in New Issue
Block a user