Skip to content

Commit 7e51f7f

Browse files
metal impl
1 parent 35471cd commit 7e51f7f

File tree

3 files changed

+510
-3
lines changed

3 files changed

+510
-3
lines changed

src/renderer/Metal.zig

Lines changed: 249 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ pub const Metal = @This();
77
const std = @import("std");
88
const builtin = @import("builtin");
99
const glfw = @import("glfw");
10+
const wuffs = @import("wuffs");
1011
const objc = @import("objc");
1112
const macos = @import("macos");
1213
const imgui = @import("imgui");
@@ -59,6 +60,9 @@ const glfwNative = glfw.Native(.{
5960

6061
const log = std.log.scoped(.metal);
6162

63+
/// The maximum size of a background image.
64+
const max_image_size = 400 * 1024 * 1024; // 400MB
65+
6266
/// Allocator that can be used
6367
alloc: std.mem.Allocator,
6468

@@ -130,6 +134,16 @@ font_grid: *font.SharedGrid,
130134
font_shaper: font.Shaper,
131135
font_shaper_cache: font.ShaperCache,
132136

137+
/// The background image(s) to draw. Currently, we always draw the last image.
138+
background_image: configpkg.SinglePath,
139+
140+
/// The background image mode to use.
141+
background_image_mode: configpkg.BackgroundImageMode,
142+
143+
/// The current background image to draw. If it is null, then we will not
144+
/// draw any background image.
145+
current_background_image: ?Image = null,
146+
133147
/// The images that we may render.
134148
images: ImageMap = .{},
135149
image_placements: ImagePlacementList = .{},
@@ -380,6 +394,9 @@ pub const DerivedConfig = struct {
380394
cursor_text: ?terminal.color.RGB,
381395
background: terminal.color.RGB,
382396
background_opacity: f64,
397+
background_image: configpkg.SinglePath,
398+
background_image_opacity: f32,
399+
background_image_mode: configpkg.BackgroundImageMode,
383400
foreground: terminal.color.RGB,
384401
selection_background: ?terminal.color.RGB,
385402
selection_foreground: ?terminal.color.RGB,
@@ -404,6 +421,9 @@ pub const DerivedConfig = struct {
404421
// Copy our shaders
405422
const custom_shaders = try config.@"custom-shader".clone(alloc);
406423

424+
// Copy our background image
425+
const background_image = try config.@"background-image".clone(alloc);
426+
407427
// Copy our font features
408428
const font_features = try config.@"font-feature".clone(alloc);
409429

@@ -444,6 +464,11 @@ pub const DerivedConfig = struct {
444464

445465
.background = config.background.toTerminalRGB(),
446466
.foreground = config.foreground.toTerminalRGB(),
467+
468+
.background_image = background_image,
469+
.background_image_opacity = config.@"background-image-opacity",
470+
.background_image_mode = config.@"background-image-mode",
471+
447472
.invert_selection_fg_bg = config.@"selection-invert-fg-bg",
448473
.bold_is_bright = config.@"bold-is-bright",
449474
.min_contrast = @floatCast(config.@"minimum-contrast"),
@@ -643,6 +668,8 @@ pub fn init(alloc: Allocator, options: renderer.Options) !Metal {
643668
.default_foreground_color = options.config.foreground,
644669
.background_color = null,
645670
.default_background_color = options.config.background,
671+
.background_image = options.config.background_image,
672+
.background_image_mode = options.config.background_image_mode,
646673
.cursor_color = null,
647674
.default_cursor_color = options.config.cursor_color,
648675
.cursor_invert = options.config.cursor_invert,
@@ -668,6 +695,8 @@ pub fn init(alloc: Allocator, options: renderer.Options) !Metal {
668695
.use_display_p3 = options.config.colorspace == .@"display-p3",
669696
.use_linear_blending = options.config.blending.isLinear(),
670697
.use_experimental_linear_correction = options.config.blending == .@"linear-corrected",
698+
.has_bg_image = (options.config.background_image.value != null),
699+
.bg_image_opacity = options.config.background_image_opacity,
671700
},
672701

673702
// Fonts
@@ -1103,6 +1132,17 @@ pub fn updateFrame(
11031132
try self.prepKittyGraphics(state.terminal);
11041133
}
11051134

1135+
if (self.current_background_image == null and
1136+
self.background_image.value != null)
1137+
{
1138+
self.prepBackgroundImage() catch |err| switch (err) {
1139+
error.InvalidData => {
1140+
log.warn("invalid image data, skipping", .{});
1141+
},
1142+
else => return err,
1143+
};
1144+
}
1145+
11061146
// If we have any terminal dirty flags set then we need to rebuild
11071147
// the entire screen. This can be optimized in the future.
11081148
const full_rebuild: bool = rebuild: {
@@ -1192,6 +1232,7 @@ pub fn updateFrame(
11921232
// TODO: Is this expensive? Should we be checking if our
11931233
// bg color has changed first before doing this work?
11941234
{
1235+
std.log.info("Updating background color to {}", .{critical.bg});
11951236
const color = graphics.c.CGColorCreate(
11961237
@ptrCast(self.terminal_colorspace),
11971238
&[4]f64{
@@ -1240,6 +1281,31 @@ pub fn updateFrame(
12401281
}
12411282
}
12421283
}
1284+
1285+
// Check if we need to update our current background image
1286+
if (self.current_background_image) |current_background_image| {
1287+
switch (current_background_image) {
1288+
.ready => {},
1289+
1290+
.pending_gray,
1291+
.pending_gray_alpha,
1292+
.pending_rgb,
1293+
.pending_rgba,
1294+
.replace_gray,
1295+
.replace_gray_alpha,
1296+
.replace_rgb,
1297+
.replace_rgba,
1298+
=> try self.current_background_image.?.upload(self.alloc, self.gpu_state.device),
1299+
1300+
.unload_pending,
1301+
.unload_replace,
1302+
.unload_ready,
1303+
=> {
1304+
self.current_background_image.?.deinit(self.alloc);
1305+
self.current_background_image = null;
1306+
},
1307+
}
1308+
}
12431309
}
12441310

12451311
/// Draw the frame to the screen.
@@ -1351,7 +1417,10 @@ pub fn drawFrame(self: *Metal, surface: *apprt.Surface) !void {
13511417
);
13521418
defer encoder.msgSend(void, objc.sel("endEncoding"), .{});
13531419

1354-
// Draw background images first
1420+
// Draw background image set by the user first
1421+
try self.drawBackgroundImage(encoder, frame);
1422+
1423+
// Then draw background images
13551424
try self.drawImagePlacements(encoder, frame, self.image_placements.items[0..self.image_bg_end]);
13561425

13571426
// Then draw background cells
@@ -1554,6 +1623,92 @@ fn drawPostShader(
15541623
);
15551624
}
15561625

1626+
fn drawBackgroundImage(
1627+
self: *Metal,
1628+
encoder: objc.Object,
1629+
frame: *const FrameState,
1630+
) !void {
1631+
// If we don't have a background image, just return
1632+
const current_background_image = self.current_background_image orelse return;
1633+
1634+
// Use our background image shader pipeline
1635+
encoder.msgSend(
1636+
void,
1637+
objc.sel("setRenderPipelineState:"),
1638+
.{self.shaders.bg_image_pipeline.value},
1639+
);
1640+
1641+
// Set our uniforms
1642+
encoder.msgSend(
1643+
void,
1644+
objc.sel("setVertexBuffer:offset:atIndex:"),
1645+
.{ frame.uniforms.buffer.value, @as(c_ulong, 0), @as(c_ulong, 1) },
1646+
);
1647+
encoder.msgSend(
1648+
void,
1649+
objc.sel("setFragmentBuffer:offset:atIndex:"),
1650+
.{ frame.uniforms.buffer.value, @as(c_ulong, 0), @as(c_ulong, 1) },
1651+
);
1652+
1653+
// Get the texture
1654+
const texture = switch (current_background_image) {
1655+
.ready => |t| t,
1656+
else => {
1657+
return;
1658+
},
1659+
};
1660+
1661+
// Create our vertex buffer, which is always exactly one item.
1662+
const Buffer = mtl_buffer.Buffer(mtl_shaders.BgImage);
1663+
var buf = try Buffer.initFill(self.gpu_state.device, &.{.{
1664+
.terminal_size = .{
1665+
@as(f32, @floatFromInt(self.size.terminal().width)),
1666+
@as(f32, @floatFromInt(self.size.terminal().height)),
1667+
},
1668+
.mode = self.background_image_mode,
1669+
}});
1670+
defer buf.deinit();
1671+
1672+
// Set our buffer
1673+
encoder.msgSend(
1674+
void,
1675+
objc.sel("setVertexBuffer:offset:atIndex:"),
1676+
.{ buf.buffer.value, @as(c_ulong, 0), @as(c_ulong, 0) },
1677+
);
1678+
1679+
// Set our texture
1680+
encoder.msgSend(
1681+
void,
1682+
objc.sel("setVertexTexture:atIndex:"),
1683+
.{
1684+
texture.value,
1685+
@as(c_ulong, 0),
1686+
},
1687+
);
1688+
encoder.msgSend(
1689+
void,
1690+
objc.sel("setFragmentTexture:atIndex:"),
1691+
.{
1692+
texture.value,
1693+
@as(c_ulong, 0),
1694+
},
1695+
);
1696+
1697+
// Draw!
1698+
encoder.msgSend(
1699+
void,
1700+
objc.sel("drawIndexedPrimitives:indexCount:indexType:indexBuffer:indexBufferOffset:instanceCount:"),
1701+
.{
1702+
@intFromEnum(mtl.MTLPrimitiveType.triangle),
1703+
@as(c_ulong, 6),
1704+
@intFromEnum(mtl.MTLIndexType.uint16),
1705+
self.gpu_state.instance.buffer.value,
1706+
@as(c_ulong, 0),
1707+
@as(c_ulong, 1),
1708+
},
1709+
);
1710+
}
1711+
15571712
fn drawImagePlacements(
15581713
self: *Metal,
15591714
encoder: objc.Object,
@@ -2086,6 +2241,82 @@ fn prepKittyImage(
20862241
gop.value_ptr.transmit_time = image.transmit_time;
20872242
}
20882243

2244+
/// Prepares the current background image for upload
2245+
pub fn prepBackgroundImage(self: *Metal) !void {
2246+
// If the user doesn't have a background image, do nothing...
2247+
const path = self.background_image.value orelse return;
2248+
2249+
// Read the file content
2250+
const file_content = try self.readImageContent(path);
2251+
defer self.alloc.free(file_content);
2252+
2253+
// Decode the image
2254+
const decoded_image: wuffs.ImageData = blk: {
2255+
// Extract the file extension
2256+
const ext = std.fs.path.extension(path);
2257+
const ext_lower = try std.ascii.allocLowerString(self.alloc, ext);
2258+
defer self.alloc.free(ext_lower);
2259+
2260+
// Match based on extension
2261+
if (std.mem.eql(u8, ext_lower, ".png")) {
2262+
break :blk try wuffs.png.decode(self.alloc, file_content);
2263+
} else if (std.mem.eql(u8, ext_lower, ".jpg") or std.mem.eql(u8, ext_lower, ".jpeg")) {
2264+
break :blk try wuffs.jpeg.decode(self.alloc, file_content);
2265+
} else {
2266+
log.warn("unsupported image format: {s}", .{ext});
2267+
return error.InvalidData;
2268+
}
2269+
};
2270+
defer self.alloc.free(decoded_image.data);
2271+
2272+
// Copy the data into the pending state
2273+
const data = try self.alloc.dupe(u8, decoded_image.data);
2274+
errdefer self.alloc.free(data);
2275+
const pending: Image.Pending = .{
2276+
.width = decoded_image.width,
2277+
.height = decoded_image.height,
2278+
.data = data.ptr,
2279+
};
2280+
2281+
// Store the image
2282+
self.current_background_image = .{ .pending_rgba = pending };
2283+
}
2284+
2285+
/// Reads the content of the given image path and returns it
2286+
pub fn readImageContent(self: *Metal, path: []const u8) ![]u8 {
2287+
assert(std.fs.path.isAbsolute(path));
2288+
// Open the file
2289+
var file = std.fs.openFileAbsolute(path, .{}) catch |err| {
2290+
log.warn("failed to open file: {}", .{err});
2291+
return error.InvalidData;
2292+
};
2293+
defer file.close();
2294+
2295+
// File must be a regular file
2296+
if (file.stat()) |stat| {
2297+
if (stat.kind != .file) {
2298+
log.warn("file is not a regular file kind={}", .{stat.kind});
2299+
return error.InvalidData;
2300+
}
2301+
} else |err| {
2302+
log.warn("failed to stat file: {}", .{err});
2303+
return error.InvalidData;
2304+
}
2305+
2306+
var buf_reader = std.io.bufferedReader(file.reader());
2307+
const reader = buf_reader.reader();
2308+
2309+
// Read the file
2310+
var managed = std.ArrayList(u8).init(self.alloc);
2311+
errdefer managed.deinit();
2312+
reader.readAllArrayList(&managed, max_image_size) catch |err| {
2313+
log.warn("failed to read file: {}", .{err});
2314+
return error.InvalidData;
2315+
};
2316+
2317+
return managed.toOwnedSlice();
2318+
}
2319+
20892320
/// Update the configuration.
20902321
pub fn changeConfig(self: *Metal, config: *DerivedConfig) !void {
20912322
// We always redo the font shaper in case font features changed. We
@@ -2120,6 +2351,15 @@ pub fn changeConfig(self: *Metal, config: *DerivedConfig) !void {
21202351
self.default_cursor_color = if (!config.cursor_invert) config.cursor_color else null;
21212352
self.cursor_invert = config.cursor_invert;
21222353

2354+
// Reset current background image
2355+
self.background_image = config.background_image;
2356+
self.uniforms.has_bg_image = (config.background_image.value != null);
2357+
self.uniforms.bg_image_opacity = config.background_image_opacity;
2358+
self.background_image_mode = config.background_image_mode;
2359+
if (self.current_background_image) |*img| {
2360+
img.markForUnload();
2361+
}
2362+
21232363
// Update our layer's opaqueness and display sync in case they changed.
21242364
{
21252365
// We use a CATransaction so that Core Animation knows that we
@@ -2256,6 +2496,8 @@ pub fn setScreenSize(
22562496
.use_display_p3 = old.use_display_p3,
22572497
.use_linear_blending = old.use_linear_blending,
22582498
.use_experimental_linear_correction = old.use_experimental_linear_correction,
2499+
.has_bg_image = old.has_bg_image,
2500+
.bg_image_opacity = old.bg_image_opacity,
22592501
};
22602502

22612503
// Reset our cell contents if our grid size has changed.
@@ -2663,7 +2905,7 @@ fn rebuildCells(
26632905
const bg_alpha: u8 = bg_alpha: {
26642906
const default: u8 = 255;
26652907

2666-
if (self.config.background_opacity >= 1) break :bg_alpha default;
2908+
if (self.current_background_image == null and self.config.background_opacity >= 1) break :bg_alpha default;
26672909

26682910
// Cells that are selected should be fully opaque.
26692911
if (selected) break :bg_alpha default;
@@ -2677,6 +2919,11 @@ fn rebuildCells(
26772919
break :bg_alpha default;
26782920
}
26792921

2922+
// If we have a background image, use the configured background image opacity.
2923+
if (self.current_background_image != null) {
2924+
break :bg_alpha @intFromFloat(@round((1 - self.config.background_image_opacity) * 255.0));
2925+
}
2926+
26802927
// Otherwise, we use the configured background opacity.
26812928
break :bg_alpha @intFromFloat(@round(self.config.background_opacity * 255.0));
26822929
};

0 commit comments

Comments
 (0)