Merge pull request #1717 from mitchellh/metalv2

Metal Improvements (plus temporary, purposeful regression)
This commit is contained in:
Mitchell Hashimoto
2024-04-28 10:10:50 -07:00
committed by GitHub
15 changed files with 1321 additions and 489 deletions

File diff suppressed because it is too large Load Diff

View File

@ -939,8 +939,8 @@ pub fn rebuildCells(
// Determine our x/y range for preedit. We don't want to render anything
// here because we will render the preedit separately.
const preedit_range: ?struct {
y: usize,
x: [2]usize,
y: terminal.size.CellCountInt,
x: [2]terminal.size.CellCountInt,
cp_offset: usize,
} = if (preedit) |preedit_v| preedit: {
const range = preedit_v.range(screen.cursor.x, screen.pages.cols - 1);
@ -958,7 +958,7 @@ pub fn rebuildCells(
// Build each cell
var row_it = screen.pages.rowIterator(.right_down, .{ .viewport = .{} }, null);
var y: usize = 0;
var y: terminal.size.CellCountInt = 0;
while (row_it.next()) |row| {
defer y += 1;

View File

@ -78,9 +78,13 @@ pub const Preedit = struct {
/// Range returns the start and end x position of the preedit text
/// along with any codepoint offset necessary to fit the preedit
/// into the available space.
pub fn range(self: *const Preedit, start: usize, max: usize) struct {
start: usize,
end: usize,
pub fn range(
self: *const Preedit,
start: terminal.size.CellCountInt,
max: terminal.size.CellCountInt,
) struct {
start: terminal.size.CellCountInt,
end: terminal.size.CellCountInt,
cp_offset: usize,
} {
// If our width is greater than the number of cells we have
@ -92,7 +96,7 @@ pub const Preedit = struct {
// Rebuild our width in reverse order. This is because we want
// to offset by the end cells, not the start cells (if we have to).
var w: usize = 0;
var w: terminal.size.CellCountInt = 0;
for (0..self.codepoints.len) |i| {
const reverse_i = self.codepoints.len - i - 1;
const cp = self.codepoints[reverse_i];

View File

@ -50,6 +50,7 @@ pub const MTLIndexType = enum(c_ulong) {
/// https://developer.apple.com/documentation/metal/mtlvertexformat?language=objc
pub const MTLVertexFormat = enum(c_ulong) {
uchar4 = 3,
ushort2 = 13,
float2 = 29,
float4 = 31,
int2 = 33,

View File

@ -49,6 +49,21 @@ pub fn Buffer(comptime T: type) type {
self.buffer.msgSend(void, objc.sel("release"), .{});
}
/// Get the buffer contents as a slice of T. The contents are
/// mutable. The contents may or may not be automatically synced
/// depending on the buffer storage mode. See the Metal docs.
pub fn contents(self: *Self) ![]T {
const len_bytes = self.buffer.getProperty(c_ulong, "length");
assert(@mod(len_bytes, @sizeOf(T)) == 0);
const len = @divExact(len_bytes, @sizeOf(T));
const ptr = self.buffer.msgSend(
?[*]T,
objc.sel("contents"),
.{},
).?;
return ptr[0..len];
}
/// Sync new contents to the buffer.
pub fn sync(self: *Self, device: objc.Object, data: []const T) !void {
// If we need more bytes than our buffer has, we need to reallocate.

417
src/renderer/metal/cell.zig Normal file
View File

@ -0,0 +1,417 @@
const std = @import("std");
const assert = std.debug.assert;
const Allocator = std.mem.Allocator;
const renderer = @import("../../renderer.zig");
const terminal = @import("../../terminal/main.zig");
const mtl_shaders = @import("shaders.zig");
/// The possible cell content keys that exist.
pub const Key = enum {
bg,
text,
underline,
strikethrough,
/// Returns the GPU vertex type for this key.
fn CellType(self: Key) type {
return switch (self) {
.bg => mtl_shaders.CellBg,
.text,
.underline,
.strikethrough,
=> mtl_shaders.CellText,
};
}
};
/// The contents of all the cells in the terminal.
///
/// The goal of this data structure is to make it efficient for two operations:
///
/// 1. Setting the contents of a cell by coordinate. More specifically,
/// we want to be efficient setting cell contents by row since we
/// will be doing row dirty tracking.
///
/// 2. Syncing the contents of the CPU buffers to GPU buffers. This happens
/// every frame and should be as fast as possible.
///
/// To achieve this, the contents are stored in contiguous arrays by
/// GPU vertex type and we have an array of mappings indexed by coordinate
/// that map to the index in the GPU vertex array that the content is at.
pub const Contents = struct {
/// The map contains the mapping of cell content for every cell in the
/// terminal to the index in the cells array that the content is at.
/// This is ALWAYS sized to exactly (rows * cols) so we want to keep
/// this as small as possible.
///
/// Before any operation, this must be initialized by calling resize
/// on the contents.
map: []Map,
/// The grid size of the terminal. This is used to determine the
/// map array index from a coordinate.
cols: usize,
/// The actual GPU data (on the CPU) for all the cells in the terminal.
/// This only contains the cells that have content set. To determine
/// if a cell has content set, we check the map.
///
/// This data is synced to a buffer on every frame.
bgs: std.ArrayListUnmanaged(mtl_shaders.CellBg),
text: std.ArrayListUnmanaged(mtl_shaders.CellText),
/// True when the cursor should be rendered. This is managed by
/// the setCursor method and should not be set directly.
cursor: bool,
/// The amount of text elements we reserve at the beginning for
/// special elements like the cursor.
const text_reserved_len = 1;
pub fn init(alloc: Allocator) !Contents {
const map = try alloc.alloc(Map, 0);
errdefer alloc.free(map);
var result: Contents = .{
.map = map,
.cols = 0,
.bgs = .{},
.text = .{},
.cursor = false,
};
// We preallocate some amount of space for cell contents
// we always have as a prefix. For now the current prefix
// is length 1: the cursor.
try result.text.ensureTotalCapacity(alloc, text_reserved_len);
result.text.items.len = text_reserved_len;
return result;
}
pub fn deinit(self: *Contents, alloc: Allocator) void {
alloc.free(self.map);
self.bgs.deinit(alloc);
self.text.deinit(alloc);
}
/// Resize the cell contents for the given grid size. This will
/// always invalidate the entire cell contents.
pub fn resize(
self: *Contents,
alloc: Allocator,
size: renderer.GridSize,
) !void {
const map = try alloc.alloc(Map, size.rows * size.columns);
errdefer alloc.free(map);
@memset(map, .{});
alloc.free(self.map);
self.map = map;
self.cols = size.columns;
self.bgs.clearAndFree(alloc);
self.text.shrinkAndFree(alloc, text_reserved_len);
}
/// Returns the slice of fg cell contents to sync with the GPU.
pub fn fgCells(self: *const Contents) []const mtl_shaders.CellText {
const start: usize = if (self.cursor) 0 else 1;
return self.text.items[start..];
}
/// Returns the slice of bg cell contents to sync with the GPU.
pub fn bgCells(self: *const Contents) []const mtl_shaders.CellBg {
return self.bgs.items;
}
/// Set the cursor value. If the value is null then the cursor
/// is hidden.
pub fn setCursor(self: *Contents, v: ?mtl_shaders.CellText) void {
const cell = v orelse {
self.cursor = false;
return;
};
self.cursor = true;
self.text.items[0] = cell;
}
/// Get the cell contents for the given type and coordinate.
pub fn get(
self: *const Contents,
comptime key: Key,
coord: terminal.Coordinate,
) ?key.CellType() {
const mapping = self.map[self.index(coord)].array.get(key);
if (!mapping.set) return null;
return switch (key) {
.bg => self.bgs.items[mapping.index],
.text,
.underline,
.strikethrough,
=> self.text.items[mapping.index],
};
}
/// Set the cell contents for a given type of content at a given
/// coordinate (provided by the celll contents).
pub fn set(
self: *Contents,
alloc: Allocator,
comptime key: Key,
cell: key.CellType(),
) !void {
const mapping = self.map[
self.index(.{
.x = cell.grid_pos[0],
.y = cell.grid_pos[1],
})
].array.getPtr(key);
// Get our list of cells based on the key (comptime).
const list = &@field(self, switch (key) {
.bg => "bgs",
.text, .underline, .strikethrough => "text",
});
// If this content type is already set on this cell, we can
// simply update the pre-existing index in the list to the new
// contents.
if (mapping.set) {
list.items[mapping.index] = cell;
return;
}
// Otherwise we need to append the new cell to the list.
const idx: u31 = @intCast(list.items.len);
try list.append(alloc, cell);
mapping.* = .{ .set = true, .index = idx };
}
/// Clear all of the cell contents for a given row.
///
/// Due to the way this works internally, it is best to clear rows
/// from the bottom up. This is because when we clear a row, we
/// swap remove the last element in the list and then update the
/// mapping for the swapped element. If we clear from the top down,
/// then we would have to update the mapping for every element in
/// the list. If we clear from the bottom up, then we only have to
/// update the mapping for the last element in the list.
pub fn clear(self: *Contents, y: terminal.size.CellCountInt) void {
const start_idx = self.index(.{ .x = 0, .y = y });
const end_idx = start_idx + self.cols;
const maps = self.map[start_idx..end_idx];
for (0..self.cols) |x| {
// It is better to clear from the right left due to the same
// reasons noted for bottom-up clearing in the doc comment.
const rev_x = self.cols - x - 1;
const map = &maps[rev_x];
var it = map.array.iterator();
while (it.next()) |entry| {
if (!entry.value.set) continue;
// This value is no longer set
entry.value.set = false;
// Remove the value at index. This does a "swap remove"
// which swaps the last element in to this place. This is
// important because after this we need to update the mapping
// for the swapped element.
const original_index = entry.value.index;
const coord_: ?terminal.Coordinate = switch (entry.key) {
.bg => bg: {
_ = self.bgs.swapRemove(original_index);
if (self.bgs.items.len == original_index) break :bg null;
const new = self.bgs.items[original_index];
break :bg .{ .x = new.grid_pos[0], .y = new.grid_pos[1] };
},
.text,
.underline,
.strikethrough,
=> text: {
_ = self.text.swapRemove(original_index);
if (self.text.items.len == original_index) break :text null;
const new = self.text.items[original_index];
break :text .{ .x = new.grid_pos[0], .y = new.grid_pos[1] };
},
};
// If we have the coordinate of the swapped element, then
// we need to update it to point at its new index, which is
// the index of the element we just removed.
//
// The reason we wouldn't have a coordinate is if we are
// removing the last element in the array, then nothing
// is swapped in and nothing needs to be updated.
if (coord_) |coord| {
const old_index = switch (entry.key) {
.bg => self.bgs.items.len,
.text, .underline, .strikethrough => self.text.items.len,
};
var old_it = self.map[self.index(coord)].array.iterator();
while (old_it.next()) |old_entry| {
if (old_entry.value.set and
old_entry.value.index == old_index)
{
old_entry.value.index = original_index;
break;
}
}
}
}
}
}
fn index(self: *const Contents, coord: terminal.Coordinate) usize {
return coord.y * self.cols + coord.x;
}
/// The mapping of a cell at a specific coordinate to the index in the
/// vertex arrays where the cell content is at, if it is set.
const Map = struct {
/// The set of cell content mappings for a given cell for every
/// possible key. This is used to determine if a cell has a given
/// type of content (i.e. an underlyine styling) and if so what index
/// in the cells array that content is at.
const Array = std.EnumArray(Key, Mapping);
/// The mapping for a given key consists of a bit indicating if the
/// content is set and the index in the cells array that the content
/// is at. We pack this into a 32-bit integer so we only use 4 bytes
/// per possible cell content type.
const Mapping = packed struct(u32) {
set: bool = false,
index: u31 = 0,
};
/// The backing array of mappings.
array: Array = Array.initFill(.{}),
pub fn empty(self: *Map) bool {
var it = self.array.iterator();
while (it.next()) |entry| {
if (entry.value.set) return false;
}
return true;
}
};
};
test Contents {
const testing = std.testing;
const alloc = testing.allocator;
const rows = 10;
const cols = 10;
var c = try Contents.init(alloc);
try c.resize(alloc, .{ .rows = rows, .columns = cols });
defer c.deinit(alloc);
// Assert that get returns null for everything.
for (0..rows) |y| {
for (0..cols) |x| {
try testing.expect(c.get(.bg, .{
.x = @intCast(x),
.y = @intCast(y),
}) == null);
}
}
// Set some contents
const cell: mtl_shaders.CellBg = .{
.mode = .rgb,
.grid_pos = .{ 4, 1 },
.cell_width = 1,
.color = .{ 0, 0, 0, 1 },
};
try c.set(alloc, .bg, cell);
try testing.expectEqual(cell, c.get(.bg, .{ .x = 4, .y = 1 }).?);
// Can clear it
c.clear(1);
for (0..rows) |y| {
for (0..cols) |x| {
try testing.expect(c.get(.bg, .{
.x = @intCast(x),
.y = @intCast(y),
}) == null);
}
}
}
test "Contents clear retains other content" {
const testing = std.testing;
const alloc = testing.allocator;
const rows = 10;
const cols = 10;
var c = try Contents.init(alloc);
try c.resize(alloc, .{ .rows = rows, .columns = cols });
defer c.deinit(alloc);
// Set some contents
const cell1: mtl_shaders.CellBg = .{
.mode = .rgb,
.grid_pos = .{ 4, 1 },
.cell_width = 1,
.color = .{ 0, 0, 0, 1 },
};
const cell2: mtl_shaders.CellBg = .{
.mode = .rgb,
.grid_pos = .{ 4, 2 },
.cell_width = 1,
.color = .{ 0, 0, 0, 1 },
};
try c.set(alloc, .bg, cell1);
try c.set(alloc, .bg, cell2);
c.clear(1);
// Row 2 should still be valid.
try testing.expectEqual(cell2, c.get(.bg, .{ .x = 4, .y = 2 }).?);
}
test "Contents clear last added content" {
const testing = std.testing;
const alloc = testing.allocator;
const rows = 10;
const cols = 10;
var c = try Contents.init(alloc);
try c.resize(alloc, .{ .rows = rows, .columns = cols });
defer c.deinit(alloc);
// Set some contents
const cell1: mtl_shaders.CellBg = .{
.mode = .rgb,
.grid_pos = .{ 4, 1 },
.cell_width = 1,
.color = .{ 0, 0, 0, 1 },
};
const cell2: mtl_shaders.CellBg = .{
.mode = .rgb,
.grid_pos = .{ 4, 2 },
.cell_width = 1,
.color = .{ 0, 0, 0, 1 },
};
try c.set(alloc, .bg, cell1);
try c.set(alloc, .bg, cell2);
c.clear(2);
// Row 2 should still be valid.
try testing.expectEqual(cell1, c.get(.bg, .{ .x = 4, .y = 1 }).?);
}
test "Contents.Map size" {
// We want to be mindful of when this increases because it affects
// renderer memory significantly.
try std.testing.expectEqual(@as(usize, 16), @sizeOf(Contents.Map));
}

View File

@ -16,7 +16,11 @@ pub const Shaders = struct {
/// The cell shader is the shader used to render the terminal cells.
/// It is a single shader that is used for both the background and
/// foreground.
cell_pipeline: objc.Object,
cell_text_pipeline: objc.Object,
/// The cell background shader is the shader used to render the
/// background of terminal cells.
cell_bg_pipeline: objc.Object,
/// The image shader is the shader used to render images for things
/// like the Kitty image protocol.
@ -40,8 +44,11 @@ pub const Shaders = struct {
const library = try initLibrary(device);
errdefer library.msgSend(void, objc.sel("release"), .{});
const cell_pipeline = try initCellPipeline(device, library);
errdefer cell_pipeline.msgSend(void, objc.sel("release"), .{});
const cell_text_pipeline = try initCellTextPipeline(device, library);
errdefer cell_text_pipeline.msgSend(void, objc.sel("release"), .{});
const cell_bg_pipeline = try initCellBgPipeline(device, library);
errdefer cell_bg_pipeline.msgSend(void, objc.sel("release"), .{});
const image_pipeline = try initImagePipeline(device, library);
errdefer image_pipeline.msgSend(void, objc.sel("release"), .{});
@ -65,7 +72,8 @@ pub const Shaders = struct {
return .{
.library = library,
.cell_pipeline = cell_pipeline,
.cell_text_pipeline = cell_text_pipeline,
.cell_bg_pipeline = cell_bg_pipeline,
.image_pipeline = image_pipeline,
.post_pipelines = post_pipelines,
};
@ -73,7 +81,8 @@ pub const Shaders = struct {
pub fn deinit(self: *Shaders, alloc: Allocator) void {
// Release our primary shaders
self.cell_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.image_pipeline.msgSend(void, objc.sel("release"), .{});
self.library.msgSend(void, objc.sel("release"), .{});
@ -87,25 +96,6 @@ pub const Shaders = struct {
}
};
/// This is a single parameter for the terminal cell shader.
pub const Cell = extern struct {
mode: Mode,
grid_pos: [2]f32,
glyph_pos: [2]u32 = .{ 0, 0 },
glyph_size: [2]u32 = .{ 0, 0 },
glyph_offset: [2]i32 = .{ 0, 0 },
color: [4]u8,
bg_color: [4]u8,
cell_width: u8,
pub const Mode = enum(u8) {
bg = 1,
fg = 2,
fg_constrained = 3,
fg_color = 7,
};
};
/// Single parameter for the image shader. See shader for field details.
pub const Image = extern struct {
grid_pos: [2]f32,
@ -126,6 +116,10 @@ pub const Uniforms = extern struct {
/// The minimum contrast ratio for text. The contrast ratio is calculated
/// according to the WCAG 2.0 spec.
min_contrast: f32,
/// The cursor position and color.
cursor_pos: [2]u16,
cursor_color: [4]u8,
};
/// The uniforms used for custom postprocess shaders.
@ -294,12 +288,31 @@ fn initPostPipeline(
return pipeline_state;
}
/// This is a single parameter for the terminal cell shader.
pub const CellText = extern struct {
mode: Mode,
glyph_pos: [2]u32 = .{ 0, 0 },
glyph_size: [2]u32 = .{ 0, 0 },
glyph_offset: [2]i32 = .{ 0, 0 },
color: [4]u8,
bg_color: [4]u8,
grid_pos: [2]u16,
cell_width: u8,
pub const Mode = enum(u8) {
fg = 1,
fg_constrained = 2,
fg_color = 3,
cursor = 4,
};
};
/// Initialize the cell render pipeline for our shader library.
fn initCellPipeline(device: objc.Object, library: objc.Object) !objc.Object {
fn initCellTextPipeline(device: objc.Object, library: objc.Object) !objc.Object {
// Get our vertex and fragment functions
const func_vert = func_vert: {
const str = try macos.foundation.String.createWithBytes(
"uber_vertex",
"cell_text_vertex",
.utf8,
false,
);
@ -310,7 +323,7 @@ fn initCellPipeline(device: objc.Object, library: objc.Object) !objc.Object {
};
const func_frag = func_frag: {
const str = try macos.foundation.String.createWithBytes(
"uber_fragment",
"cell_text_fragment",
.utf8,
false,
);
@ -344,7 +357,7 @@ fn initCellPipeline(device: objc.Object, library: objc.Object) !objc.Object {
);
attr.setProperty("format", @intFromEnum(mtl.MTLVertexFormat.uchar));
attr.setProperty("offset", @as(c_ulong, @offsetOf(Cell, "mode")));
attr.setProperty("offset", @as(c_ulong, @offsetOf(CellText, "mode")));
attr.setProperty("bufferIndex", @as(c_ulong, 0));
}
{
@ -354,8 +367,8 @@ fn initCellPipeline(device: objc.Object, library: objc.Object) !objc.Object {
.{@as(c_ulong, 1)},
);
attr.setProperty("format", @intFromEnum(mtl.MTLVertexFormat.float2));
attr.setProperty("offset", @as(c_ulong, @offsetOf(Cell, "grid_pos")));
attr.setProperty("format", @intFromEnum(mtl.MTLVertexFormat.ushort2));
attr.setProperty("offset", @as(c_ulong, @offsetOf(CellText, "grid_pos")));
attr.setProperty("bufferIndex", @as(c_ulong, 0));
}
{
@ -366,7 +379,7 @@ fn initCellPipeline(device: objc.Object, library: objc.Object) !objc.Object {
);
attr.setProperty("format", @intFromEnum(mtl.MTLVertexFormat.uint2));
attr.setProperty("offset", @as(c_ulong, @offsetOf(Cell, "glyph_pos")));
attr.setProperty("offset", @as(c_ulong, @offsetOf(CellText, "glyph_pos")));
attr.setProperty("bufferIndex", @as(c_ulong, 0));
}
{
@ -377,7 +390,7 @@ fn initCellPipeline(device: objc.Object, library: objc.Object) !objc.Object {
);
attr.setProperty("format", @intFromEnum(mtl.MTLVertexFormat.uint2));
attr.setProperty("offset", @as(c_ulong, @offsetOf(Cell, "glyph_size")));
attr.setProperty("offset", @as(c_ulong, @offsetOf(CellText, "glyph_size")));
attr.setProperty("bufferIndex", @as(c_ulong, 0));
}
{
@ -388,7 +401,7 @@ fn initCellPipeline(device: objc.Object, library: objc.Object) !objc.Object {
);
attr.setProperty("format", @intFromEnum(mtl.MTLVertexFormat.int2));
attr.setProperty("offset", @as(c_ulong, @offsetOf(Cell, "glyph_offset")));
attr.setProperty("offset", @as(c_ulong, @offsetOf(CellText, "glyph_offset")));
attr.setProperty("bufferIndex", @as(c_ulong, 0));
}
{
@ -399,7 +412,7 @@ fn initCellPipeline(device: objc.Object, library: objc.Object) !objc.Object {
);
attr.setProperty("format", @intFromEnum(mtl.MTLVertexFormat.uchar4));
attr.setProperty("offset", @as(c_ulong, @offsetOf(Cell, "color")));
attr.setProperty("offset", @as(c_ulong, @offsetOf(CellText, "color")));
attr.setProperty("bufferIndex", @as(c_ulong, 0));
}
{
@ -410,7 +423,7 @@ fn initCellPipeline(device: objc.Object, library: objc.Object) !objc.Object {
);
attr.setProperty("format", @intFromEnum(mtl.MTLVertexFormat.uchar4));
attr.setProperty("offset", @as(c_ulong, @offsetOf(Cell, "bg_color")));
attr.setProperty("offset", @as(c_ulong, @offsetOf(CellText, "bg_color")));
attr.setProperty("bufferIndex", @as(c_ulong, 0));
}
{
@ -421,7 +434,7 @@ fn initCellPipeline(device: objc.Object, library: objc.Object) !objc.Object {
);
attr.setProperty("format", @intFromEnum(mtl.MTLVertexFormat.uchar));
attr.setProperty("offset", @as(c_ulong, @offsetOf(Cell, "cell_width")));
attr.setProperty("offset", @as(c_ulong, @offsetOf(CellText, "cell_width")));
attr.setProperty("bufferIndex", @as(c_ulong, 0));
}
@ -436,7 +449,174 @@ fn initCellPipeline(device: objc.Object, library: objc.Object) !objc.Object {
// Access each Cell per instance, not per vertex.
layout.setProperty("stepFunction", @intFromEnum(mtl.MTLVertexStepFunction.per_instance));
layout.setProperty("stride", @as(c_ulong, @sizeOf(Cell)));
layout.setProperty("stride", @as(c_ulong, @sizeOf(CellText)));
}
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)},
);
// Value is MTLPixelFormatBGRA8Unorm
attachment.setProperty("pixelFormat", @as(c_ulong, 80));
// 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);
errdefer pipeline_state.msgSend(void, objc.sel("release"), .{});
return pipeline_state;
}
/// This is a single parameter for the cell bg shader.
pub const CellBg = extern struct {
mode: Mode,
grid_pos: [2]u16,
color: [4]u8,
cell_width: u8,
pub const Mode = enum(u8) {
rgb = 1,
};
};
/// Initialize the cell background render pipeline for our shader library.
fn initCellBgPipeline(device: objc.Object, library: objc.Object) !objc.Object {
// Get our vertex and fragment functions
const func_vert = func_vert: {
const str = try macos.foundation.String.createWithBytes(
"cell_bg_vertex",
.utf8,
false,
);
defer str.release();
const ptr = library.msgSend(?*anyopaque, objc.sel("newFunctionWithName:"), .{str});
break :func_vert objc.Object.fromId(ptr.?);
};
defer func_vert.msgSend(void, objc.sel("release"), .{});
const func_frag = func_frag: {
const str = try macos.foundation.String.createWithBytes(
"cell_bg_fragment",
.utf8,
false,
);
defer str.release();
const ptr = library.msgSend(?*anyopaque, objc.sel("newFunctionWithName:"), .{str});
break :func_frag objc.Object.fromId(ptr.?);
};
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
// Cell 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"));
{
const attr = attrs.msgSend(
objc.Object,
objc.sel("objectAtIndexedSubscript:"),
.{@as(c_ulong, 0)},
);
attr.setProperty("format", @intFromEnum(mtl.MTLVertexFormat.uchar));
attr.setProperty("offset", @as(c_ulong, @offsetOf(CellBg, "mode")));
attr.setProperty("bufferIndex", @as(c_ulong, 0));
}
{
const attr = attrs.msgSend(
objc.Object,
objc.sel("objectAtIndexedSubscript:"),
.{@as(c_ulong, 1)},
);
attr.setProperty("format", @intFromEnum(mtl.MTLVertexFormat.ushort2));
attr.setProperty("offset", @as(c_ulong, @offsetOf(CellBg, "grid_pos")));
attr.setProperty("bufferIndex", @as(c_ulong, 0));
}
{
const attr = attrs.msgSend(
objc.Object,
objc.sel("objectAtIndexedSubscript:"),
.{@as(c_ulong, 2)},
);
attr.setProperty("format", @intFromEnum(mtl.MTLVertexFormat.uchar));
attr.setProperty("offset", @as(c_ulong, @offsetOf(CellBg, "cell_width")));
attr.setProperty("bufferIndex", @as(c_ulong, 0));
}
{
const attr = attrs.msgSend(
objc.Object,
objc.sel("objectAtIndexedSubscript:"),
.{@as(c_ulong, 3)},
);
attr.setProperty("format", @intFromEnum(mtl.MTLVertexFormat.uchar4));
attr.setProperty("offset", @as(c_ulong, @offsetOf(CellBg, "color")));
attr.setProperty("bufferIndex", @as(c_ulong, 0));
}
// 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 Cell per instance, not per vertex.
layout.setProperty("stepFunction", @intFromEnum(mtl.MTLVertexStepFunction.per_instance));
layout.setProperty("stride", @as(c_ulong, @sizeOf(CellBg)));
}
break :vertex_desc desc;
@ -664,11 +844,20 @@ fn checkError(err_: ?*anyopaque) !void {
// on macOS 12 or Apple Silicon macOS 13.
//
// To be safe, we put this test in here.
test "Cell offsets" {
test "CellText offsets" {
const testing = std.testing;
const alignment = @alignOf(Cell);
inline for (@typeInfo(Cell).Struct.fields) |field| {
const offset = @offsetOf(Cell, field.name);
const alignment = @alignOf(CellText);
inline for (@typeInfo(CellText).Struct.fields) |field| {
const offset = @offsetOf(CellText, field.name);
try testing.expectEqual(0, @mod(offset, alignment));
}
}
test "CellBg offsets" {
const testing = std.testing;
const alignment = @alignOf(CellBg);
inline for (@typeInfo(CellBg).Struct.fields) |field| {
const offset = @offsetOf(CellBg, field.name);
try testing.expectEqual(0, @mod(offset, alignment));
}
}

View File

@ -1,56 +1,11 @@
using namespace metal;
// The possible modes that a shader can take.
enum Mode : uint8_t {
MODE_BG = 1u,
MODE_FG = 2u,
MODE_FG_CONSTRAINED = 3u,
MODE_FG_COLOR = 7u,
};
struct Uniforms {
float4x4 projection_matrix;
float2 cell_size;
float min_contrast;
};
struct VertexIn {
// The mode for this cell.
uint8_t mode [[attribute(0)]];
// The grid coordinates (x, y) where x < columns and y < rows
float2 grid_pos [[attribute(1)]];
// The width of the cell in cells (i.e. 2 for double-wide).
uint8_t cell_width [[attribute(6)]];
// The color. For BG modes, this is the bg color, for FG modes this is
// the text color. For styles, this is the color of the style.
uchar4 color [[attribute(5)]];
// The fields below are present only when rendering text (fg mode)
// The background color of the cell. This is used to determine if
// we need to render the text with a different color to ensure
// contrast.
uchar4 bg_color [[attribute(7)]];
// The position of the glyph in the texture (x,y)
uint2 glyph_pos [[attribute(2)]];
// The size of the glyph in the texture (w,h)
uint2 glyph_size [[attribute(3)]];
// The left and top bearings for the glyph (x,y)
int2 glyph_offset [[attribute(4)]];
};
struct VertexOut {
float4 position [[position]];
float2 cell_size;
uint8_t mode;
float4 color;
float2 tex_coord;
ushort2 cursor_pos;
uchar4 cursor_color;
};
//-------------------------------------------------------------------
@ -103,15 +58,41 @@ float4 contrasted_color(float min, float4 fg, float4 bg) {
}
//-------------------------------------------------------------------
// Terminal Grid Cell Shader
// Cell Background Shader
//-------------------------------------------------------------------
#pragma mark - Terminal Grid Cell Shader
#pragma mark - Cell BG Shader
vertex VertexOut uber_vertex(unsigned int vid [[vertex_id]],
VertexIn input [[stage_in]],
constant Uniforms& uniforms [[buffer(1)]]) {
// The possible modes that a cell bg entry can take.
enum CellBgMode : uint8_t {
MODE_RGB = 1u,
};
struct CellBgVertexIn {
// The mode for this cell.
uint8_t mode [[attribute(0)]];
// The grid coordinates (x, y) where x < columns and y < rows
ushort2 grid_pos [[attribute(1)]];
// The color. For BG modes, this is the bg color, for FG modes this is
// the text color. For styles, this is the color of the style.
uchar4 color [[attribute(3)]];
// The width of the cell in cells (i.e. 2 for double-wide).
uint8_t cell_width [[attribute(2)]];
};
struct CellBgVertexOut {
float4 position [[position]];
float4 color;
};
vertex CellBgVertexOut cell_bg_vertex(unsigned int vid [[vertex_id]],
CellBgVertexIn input [[stage_in]],
constant Uniforms& uniforms
[[buffer(1)]]) {
// Convert the grid x,y into world space x, y by accounting for cell size
float2 cell_pos = uniforms.cell_size * input.grid_pos;
float2 cell_pos = uniforms.cell_size * float2(input.grid_pos);
// Scaled cell size for the cell width
float2 cell_size_scaled = uniforms.cell_size;
@ -131,82 +112,161 @@ vertex VertexOut uber_vertex(unsigned int vid [[vertex_id]],
position.x = (vid == 0 || vid == 1) ? 1.0f : 0.0f;
position.y = (vid == 0 || vid == 3) ? 0.0f : 1.0f;
VertexOut out;
// Calculate the final position of our cell in world space.
// We have to add our cell size since our vertices are offset
// one cell up and to the left. (Do the math to verify yourself)
cell_pos = cell_pos + cell_size_scaled * position;
CellBgVertexOut out;
out.color = float4(input.color) / 255.0f;
out.position =
uniforms.projection_matrix * float4(cell_pos.x, cell_pos.y, 0.0f, 1.0f);
return out;
}
fragment float4 cell_bg_fragment(CellBgVertexOut in [[stage_in]]) {
return in.color;
}
//-------------------------------------------------------------------
// Cell Text Shader
//-------------------------------------------------------------------
#pragma mark - Cell Text Shader
// The possible modes that a cell fg entry can take.
enum CellTextMode : uint8_t {
MODE_TEXT = 1u,
MODE_TEXT_CONSTRAINED = 2u,
MODE_TEXT_COLOR = 3u,
MODE_TEXT_CURSOR = 4u,
};
struct CellTextVertexIn {
// The mode for this cell.
uint8_t mode [[attribute(0)]];
// The grid coordinates (x, y) where x < columns and y < rows
ushort2 grid_pos [[attribute(1)]];
// The width of the cell in cells (i.e. 2 for double-wide).
uint8_t cell_width [[attribute(6)]];
// The color of the rendered text glyph.
uchar4 color [[attribute(5)]];
// The background color of the cell. This is used to determine if
// we need to render the text with a different color to ensure
// contrast.
uchar4 bg_color [[attribute(7)]];
// The position of the glyph in the texture (x,y)
uint2 glyph_pos [[attribute(2)]];
// The size of the glyph in the texture (w,h)
uint2 glyph_size [[attribute(3)]];
// The left and top bearings for the glyph (x,y)
int2 glyph_offset [[attribute(4)]];
};
struct CellTextVertexOut {
float4 position [[position]];
float2 cell_size;
uint8_t mode;
float4 color;
float2 tex_coord;
};
vertex CellTextVertexOut cell_text_vertex(unsigned int vid [[vertex_id]],
CellTextVertexIn input [[stage_in]],
constant Uniforms& uniforms
[[buffer(1)]]) {
// Convert the grid x,y into world space x, y by accounting for cell size
float2 cell_pos = uniforms.cell_size * float2(input.grid_pos);
// Scaled cell size for the cell width
float2 cell_size_scaled = uniforms.cell_size;
cell_size_scaled.x = cell_size_scaled.x * input.cell_width;
// Turn the cell position into a vertex point depending on the
// vertex ID. Since we use instanced drawing, we have 4 vertices
// for each corner of the cell. We can use vertex ID to determine
// which one we're looking at. Using this, we can use 1 or 0 to keep
// or discard the value for the vertex.
//
// 0 = top-right
// 1 = bot-right
// 2 = bot-left
// 3 = top-left
float2 position;
position.x = (vid == 0 || vid == 1) ? 1.0f : 0.0f;
position.y = (vid == 0 || vid == 3) ? 0.0f : 1.0f;
CellTextVertexOut out;
out.mode = input.mode;
out.cell_size = uniforms.cell_size;
out.color = float4(input.color) / 255.0f;
switch (input.mode) {
case MODE_BG:
// Calculate the final position of our cell in world space.
// We have to add our cell size since our vertices are offset
// one cell up and to the left. (Do the math to verify yourself)
cell_pos = cell_pos + cell_size_scaled * position;
out.position = uniforms.projection_matrix *
float4(cell_pos.x, cell_pos.y, 0.0f, 1.0f);
break;
float2 glyph_size = float2(input.glyph_size);
float2 glyph_offset = float2(input.glyph_offset);
case MODE_FG:
case MODE_FG_CONSTRAINED:
case MODE_FG_COLOR: {
float2 glyph_size = float2(input.glyph_size);
float2 glyph_offset = float2(input.glyph_offset);
// The glyph_offset.y is the y bearing, a y value that when added
// to the baseline is the offset (+y is up). Our grid goes down.
// So we flip it with `cell_size.y - glyph_offset.y`.
glyph_offset.y = cell_size_scaled.y - glyph_offset.y;
// The glyph_offset.y is the y bearing, a y value that when added
// to the baseline is the offset (+y is up). Our grid goes down.
// So we flip it with `cell_size.y - glyph_offset.y`.
glyph_offset.y = cell_size_scaled.y - glyph_offset.y;
// If we're constrained then we need to scale the glyph.
// We also always constrain colored glyphs since we should have
// their scaled cell size exactly correct.
if (input.mode == MODE_FG_CONSTRAINED || input.mode == MODE_FG_COLOR) {
if (glyph_size.x > cell_size_scaled.x) {
float new_y = glyph_size.y * (cell_size_scaled.x / glyph_size.x);
glyph_offset.y += (glyph_size.y - new_y) / 2;
glyph_size.y = new_y;
glyph_size.x = cell_size_scaled.x;
}
}
// Calculate the final position of the cell which uses our glyph size
// and glyph offset to create the correct bounding box for the glyph.
cell_pos = cell_pos + glyph_size * position + glyph_offset;
out.position = uniforms.projection_matrix *
float4(cell_pos.x, cell_pos.y, 0.0f, 1.0f);
// Calculate the texture coordinate in pixels. This is NOT normalized
// (between 0.0 and 1.0) and must be done in the fragment shader.
out.tex_coord =
float2(input.glyph_pos) + float2(input.glyph_size) * position;
// If we have a minimum contrast, we need to check if we need to
// change the color of the text to ensure it has enough contrast
// with the background.
if (uniforms.min_contrast > 1.0f && input.mode == MODE_FG) {
float4 bg_color = float4(input.bg_color) / 255.0f;
out.color =
contrasted_color(uniforms.min_contrast, out.color, bg_color);
}
break;
// If we're constrained then we need to scale the glyph.
// We also always constrain colored glyphs since we should have
// their scaled cell size exactly correct.
if (input.mode == MODE_TEXT_CONSTRAINED || input.mode == MODE_TEXT_COLOR) {
if (glyph_size.x > cell_size_scaled.x) {
float new_y = glyph_size.y * (cell_size_scaled.x / glyph_size.x);
glyph_offset.y += (glyph_size.y - new_y) / 2;
glyph_size.y = new_y;
glyph_size.x = cell_size_scaled.x;
}
}
// Calculate the final position of the cell which uses our glyph size
// and glyph offset to create the correct bounding box for the glyph.
cell_pos = cell_pos + glyph_size * position + glyph_offset;
out.position =
uniforms.projection_matrix * float4(cell_pos.x, cell_pos.y, 0.0f, 1.0f);
// Calculate the texture coordinate in pixels. This is NOT normalized
// (between 0.0 and 1.0) and must be done in the fragment shader.
out.tex_coord = float2(input.glyph_pos) + float2(input.glyph_size) * position;
// If we have a minimum contrast, we need to check if we need to
// change the color of the text to ensure it has enough contrast
// with the background.
if (uniforms.min_contrast > 1.0f && input.mode == MODE_TEXT) {
float4 bg_color = float4(input.bg_color) / 255.0f;
out.color = contrasted_color(uniforms.min_contrast, out.color, bg_color);
}
// If this cell is the cursor cell, then we need to change the color.
if (input.mode != MODE_TEXT_CURSOR &&
input.grid_pos.x == uniforms.cursor_pos.x &&
input.grid_pos.y == uniforms.cursor_pos.y) {
out.color = float4(uniforms.cursor_color) / 255.0f;
}
return out;
}
fragment float4 uber_fragment(VertexOut in [[stage_in]],
texture2d<float> textureGreyscale [[texture(0)]],
texture2d<float> textureColor [[texture(1)]]) {
fragment float4 cell_text_fragment(CellTextVertexOut in [[stage_in]],
texture2d<float> textureGreyscale
[[texture(0)]],
texture2d<float> textureColor
[[texture(1)]]) {
constexpr sampler textureSampler(address::clamp_to_edge, filter::linear);
switch (in.mode) {
case MODE_BG:
return in.color;
case MODE_FG_CONSTRAINED:
case MODE_FG: {
case MODE_TEXT_CURSOR:
case MODE_TEXT_CONSTRAINED:
case MODE_TEXT: {
// Normalize the texture coordinates to [0,1]
float2 size =
float2(textureGreyscale.get_width(), textureGreyscale.get_height());
@ -222,7 +282,7 @@ fragment float4 uber_fragment(VertexOut in [[stage_in]],
return premult;
}
case MODE_FG_COLOR: {
case MODE_TEXT_COLOR: {
// Normalize the texture coordinates to [0,1]
float2 size = float2(textureColor.get_width(), textureColor.get_height());
float2 coord = in.tex_coord / size;
@ -230,7 +290,6 @@ fragment float4 uber_fragment(VertexOut in [[stage_in]],
}
}
}
//-------------------------------------------------------------------
// Image Shader
//-------------------------------------------------------------------

View File

@ -1421,7 +1421,7 @@ fn resizeWithoutReflowGrowCols(
// Keeps track of all our copied rows. Assertions at the end is that
// we copied exactly our page size.
var copied: usize = 0;
var copied: size.CellCountInt = 0;
// This function has an unfortunate side effect in that it causes memory
// fragmentation on rows if the columns are increasing in a way that
@ -2545,7 +2545,7 @@ pub fn cellIterator(
pub const RowIterator = struct {
page_it: PageIterator,
chunk: ?PageIterator.Chunk = null,
offset: usize = 0,
offset: size.CellCountInt = 0,
pub fn next(self: *RowIterator) ?Pin {
const chunk = self.chunk orelse return null;
@ -2767,8 +2767,8 @@ pub const PageIterator = struct {
pub const Chunk = struct {
page: *List.Node,
start: usize,
end: usize,
start: size.CellCountInt,
end: size.CellCountInt,
pub fn rows(self: Chunk) []Row {
const rows_ptr = self.page.data.rows.ptr(self.page.data.memory);
@ -2944,8 +2944,8 @@ fn growRows(self: *PageList, n: usize) !void {
/// should limit the number of active pins as much as possible.
pub const Pin = struct {
page: *List.Node,
y: usize = 0,
x: usize = 0,
y: size.CellCountInt = 0,
x: size.CellCountInt = 0,
pub fn rowAndCell(self: Pin) struct {
row: *pagepkg.Row,
@ -3104,7 +3104,7 @@ pub const Pin = struct {
pub fn left(self: Pin, n: usize) Pin {
assert(n <= self.x);
var result = self;
result.x -= n;
result.x -= std.math.cast(size.CellCountInt, n) orelse result.x;
return result;
}
@ -3112,7 +3112,8 @@ pub const Pin = struct {
pub fn right(self: Pin, n: usize) Pin {
assert(self.x + n < self.page.data.size.cols);
var result = self;
result.x += n;
result.x +|= std.math.cast(size.CellCountInt, n) orelse
std.math.maxInt(size.CellCountInt);
return result;
}
@ -3147,7 +3148,8 @@ pub const Pin = struct {
const rows = self.page.data.size.rows - (self.y + 1);
if (n <= rows) return .{ .offset = .{
.page = self.page,
.y = n + self.y,
.y = std.math.cast(size.CellCountInt, self.y + n) orelse
std.math.maxInt(size.CellCountInt),
.x = self.x,
} };
@ -3165,7 +3167,8 @@ pub const Pin = struct {
} };
if (n_left <= page.data.size.rows) return .{ .offset = .{
.page = page,
.y = n_left - 1,
.y = std.math.cast(size.CellCountInt, n_left - 1) orelse
std.math.maxInt(size.CellCountInt),
.x = self.x,
} };
n_left -= page.data.size.rows;
@ -3184,7 +3187,8 @@ pub const Pin = struct {
// Index fits within this page
if (n <= self.y) return .{ .offset = .{
.page = self.page,
.y = self.y - n,
.y = std.math.cast(size.CellCountInt, self.y - n) orelse
std.math.maxInt(size.CellCountInt),
.x = self.x,
} };
@ -3198,7 +3202,8 @@ pub const Pin = struct {
} };
if (n_left <= page.data.size.rows) return .{ .offset = .{
.page = page,
.y = page.data.size.rows - n_left,
.y = std.math.cast(size.CellCountInt, page.data.size.rows - n_left) orelse
std.math.maxInt(size.CellCountInt),
.x = self.x,
} };
n_left -= page.data.size.rows;
@ -3210,8 +3215,8 @@ const Cell = struct {
page: *List.Node,
row: *pagepkg.Row,
cell: *pagepkg.Cell,
row_idx: usize,
col_idx: usize,
row_idx: size.CellCountInt,
col_idx: size.CellCountInt,
/// Get the cell style.
///
@ -3231,7 +3236,7 @@ const Cell = struct {
/// this file then consider a different approach and ask yourself very
/// carefully if you really need this.
pub fn screenPoint(self: Cell) point.Point {
var y: usize = self.row_idx;
var y: size.CellCountInt = self.row_idx;
var page = self.page;
while (page.prev) |prev| {
y += prev.data.size.rows;
@ -3402,7 +3407,7 @@ test "PageList pointFromPin traverse pages" {
try testing.expectEqual(point.Point{
.screen = .{
.y = expected_y,
.y = @intCast(expected_y),
.x = 2,
},
}, s.pointFromPin(.screen, .{
@ -5629,7 +5634,7 @@ test "PageList resize (no reflow) more rows adds blank rows if cursor at bottom"
// Go through our active, we should get only 3,4,5
for (0..3) |y| {
const get = s.getCell(.{ .active = .{ .y = y } }).?;
const get = s.getCell(.{ .active = .{ .y = @intCast(y) } }).?;
const expected: u21 = @intCast(y + 2);
try testing.expectEqual(expected, get.cell.content.codepoint);
}
@ -6557,7 +6562,7 @@ test "PageList resize reflow less cols no wrapped rows" {
while (it.next()) |offset| {
for (0..4) |x| {
var offset_copy = offset;
offset_copy.x = x;
offset_copy.x = @intCast(x);
const rac = offset_copy.rowAndCell();
const cells = offset.page.data.getCells(rac.row);
try testing.expectEqual(@as(usize, 5), cells.len);
@ -7247,7 +7252,7 @@ test "PageList resize reflow less cols copy style" {
while (it.next()) |offset| {
for (0..s.cols - 1) |x| {
var offset_copy = offset;
offset_copy.x = x;
offset_copy.x = @intCast(x);
const rac = offset_copy.rowAndCell();
const style_id = rac.cell.style_id;
try testing.expect(style_id != 0);

View File

@ -1412,8 +1412,8 @@ pub fn selectionString(self: *Screen, alloc: Allocator, opts: SelectionString) !
if (mapbuilder) |*b| {
for (0..encode_len) |_| try b.append(.{
.page = chunk.page,
.y = y,
.x = x,
.y = @intCast(y),
.x = @intCast(x),
});
}
}
@ -1425,8 +1425,8 @@ pub fn selectionString(self: *Screen, alloc: Allocator, opts: SelectionString) !
if (mapbuilder) |*b| {
for (0..encode_len) |_| try b.append(.{
.page = chunk.page,
.y = y,
.x = x,
.y = @intCast(y),
.x = @intCast(x),
});
}
}
@ -1441,7 +1441,7 @@ pub fn selectionString(self: *Screen, alloc: Allocator, opts: SelectionString) !
try strbuilder.append('\n');
if (mapbuilder) |*b| try b.append(.{
.page = chunk.page,
.y = y,
.y = @intCast(y),
.x = chunk.page.data.size.cols - 1,
});
}
@ -3959,7 +3959,10 @@ test "Screen: resize (no reflow) less rows trims blank lines" {
// Write only a background color into the remaining rows
for (1..s.pages.rows) |y| {
const list_cell = s.pages.getCell(.{ .active = .{ .x = 0, .y = y } }).?;
const list_cell = s.pages.getCell(.{ .active = .{
.x = 0,
.y = @intCast(y),
} }).?;
list_cell.cell.* = .{
.content_tag = .bg_color_rgb,
.content = .{ .color_rgb = .{ .r = 0xFF, .g = 0, .b = 0 } },
@ -3991,7 +3994,10 @@ test "Screen: resize (no reflow) more rows trims blank lines" {
// Write only a background color into the remaining rows
for (1..s.pages.rows) |y| {
const list_cell = s.pages.getCell(.{ .active = .{ .x = 0, .y = y } }).?;
const list_cell = s.pages.getCell(.{ .active = .{
.x = 0,
.y = @intCast(y),
} }).?;
list_cell.cell.* = .{
.content_tag = .bg_color_rgb,
.content = .{ .color_rgb = .{ .r = 0xFF, .g = 0, .b = 0 } },
@ -4118,7 +4124,10 @@ test "Screen: resize (no reflow) more rows with soft wrapping" {
// Every second row should be wrapped
for (0..6) |y| {
const list_cell = s.pages.getCell(.{ .screen = .{ .x = 0, .y = y } }).?;
const list_cell = s.pages.getCell(.{ .screen = .{
.x = 0,
.y = @intCast(y),
} }).?;
const row = list_cell.row;
const wrapped = (y % 2 == 0);
try testing.expectEqual(wrapped, row.wrap);
@ -4135,7 +4144,10 @@ test "Screen: resize (no reflow) more rows with soft wrapping" {
// Every second row should be wrapped
for (0..6) |y| {
const list_cell = s.pages.getCell(.{ .screen = .{ .x = 0, .y = y } }).?;
const list_cell = s.pages.getCell(.{ .screen = .{
.x = 0,
.y = @intCast(y),
} }).?;
const row = list_cell.row;
const wrapped = (y % 2 == 0);
try testing.expectEqual(wrapped, row.wrap);

View File

@ -435,7 +435,7 @@ pub fn adjust(
const cells = next.page.data.getCells(rac.row);
if (page.Cell.hasTextAny(cells)) {
end_pin.* = next;
end_pin.x = cells.len - 1;
end_pin.x = @intCast(cells.len - 1);
break;
}
}

View File

@ -4169,7 +4169,10 @@ test "Terminal: insertLines colors with bg color" {
}
for (0..t.cols) |x| {
const list_cell = t.screen.pages.getCell(.{ .active = .{ .x = x, .y = 1 } }).?;
const list_cell = t.screen.pages.getCell(.{ .active = .{
.x = @intCast(x),
.y = 1,
} }).?;
try testing.expect(list_cell.cell.content_tag == .bg_color_rgb);
try testing.expectEqual(Cell.RGB{
.r = 0xFF,
@ -5297,7 +5300,10 @@ test "Terminal: index bottom of primary screen background sgr" {
defer testing.allocator.free(str);
try testing.expectEqualStrings("\n\n\nA", str);
for (0..5) |x| {
const list_cell = t.screen.pages.getCell(.{ .active = .{ .x = x, .y = 4 } }).?;
const list_cell = t.screen.pages.getCell(.{ .active = .{
.x = @intCast(x),
.y = 4,
} }).?;
try testing.expect(list_cell.cell.content_tag == .bg_color_rgb);
try testing.expectEqual(Cell.RGB{
.r = 0xFF,
@ -5349,7 +5355,10 @@ test "Terminal: index bottom of scroll region with background SGR" {
}
for (0..t.cols) |x| {
const list_cell = t.screen.pages.getCell(.{ .active = .{ .x = x, .y = 2 } }).?;
const list_cell = t.screen.pages.getCell(.{ .active = .{
.x = @intCast(x),
.y = 2,
} }).?;
try testing.expect(list_cell.cell.content_tag == .bg_color_rgb);
try testing.expectEqual(Cell.RGB{
.r = 0xFF,
@ -5961,7 +5970,10 @@ test "Terminal: deleteLines colors with bg color" {
}
for (0..t.cols) |x| {
const list_cell = t.screen.pages.getCell(.{ .active = .{ .x = x, .y = 4 } }).?;
const list_cell = t.screen.pages.getCell(.{ .active = .{
.x = @intCast(x),
.y = 4,
} }).?;
try testing.expect(list_cell.cell.content_tag == .bg_color_rgb);
try testing.expectEqual(Cell.RGB{
.r = 0xFF,
@ -6148,7 +6160,10 @@ test "Terminal: deleteLines resets wrap" {
}
for (0..t.rows) |y| {
const list_cell = t.screen.pages.getCell(.{ .active = .{ .x = 0, .y = y } }).?;
const list_cell = t.screen.pages.getCell(.{ .active = .{
.x = 0,
.y = @intCast(y),
} }).?;
const row = list_cell.row;
try testing.expect(!row.wrap);
}
@ -7183,7 +7198,10 @@ test "Terminal: deleteChars preserves background sgr" {
try testing.expectEqualStrings("AB23", str);
}
for (t.cols - 2..t.cols) |x| {
const list_cell = t.screen.pages.getCell(.{ .active = .{ .x = x, .y = 0 } }).?;
const list_cell = t.screen.pages.getCell(.{ .active = .{
.x = @intCast(x),
.y = 0,
} }).?;
try testing.expect(list_cell.cell.content_tag == .bg_color_rgb);
try testing.expectEqual(Cell.RGB{
.r = 0xFF,
@ -7573,7 +7591,10 @@ test "Terminal: eraseLine right preserves background sgr" {
defer testing.allocator.free(str);
try testing.expectEqualStrings("A", str);
for (1..5) |x| {
const list_cell = t.screen.pages.getCell(.{ .active = .{ .x = x, .y = 0 } }).?;
const list_cell = t.screen.pages.getCell(.{ .active = .{
.x = @intCast(x),
.y = 0,
} }).?;
try testing.expect(list_cell.cell.content_tag == .bg_color_rgb);
try testing.expectEqual(Cell.RGB{
.r = 0xFF,
@ -7727,7 +7748,10 @@ test "Terminal: eraseLine left preserves background sgr" {
defer testing.allocator.free(str);
try testing.expectEqualStrings(" CDE", str);
for (0..2) |x| {
const list_cell = t.screen.pages.getCell(.{ .active = .{ .x = x, .y = 0 } }).?;
const list_cell = t.screen.pages.getCell(.{ .active = .{
.x = @intCast(x),
.y = 0,
} }).?;
try testing.expect(list_cell.cell.content_tag == .bg_color_rgb);
try testing.expectEqual(Cell.RGB{
.r = 0xFF,
@ -7847,7 +7871,10 @@ test "Terminal: eraseLine complete preserves background sgr" {
defer testing.allocator.free(str);
try testing.expectEqualStrings("", str);
for (0..5) |x| {
const list_cell = t.screen.pages.getCell(.{ .active = .{ .x = x, .y = 0 } }).?;
const list_cell = t.screen.pages.getCell(.{ .active = .{
.x = @intCast(x),
.y = 0,
} }).?;
try testing.expect(list_cell.cell.content_tag == .bg_color_rgb);
try testing.expectEqual(Cell.RGB{
.r = 0xFF,
@ -8096,7 +8123,10 @@ test "Terminal: eraseDisplay erase below preserves SGR bg" {
defer testing.allocator.free(str);
try testing.expectEqualStrings("ABC\nD", str);
for (1..5) |x| {
const list_cell = t.screen.pages.getCell(.{ .active = .{ .x = x, .y = 1 } }).?;
const list_cell = t.screen.pages.getCell(.{ .active = .{
.x = @intCast(x),
.y = 1,
} }).?;
try testing.expect(list_cell.cell.content_tag == .bg_color_rgb);
try testing.expectEqual(Cell.RGB{
.r = 0xFF,
@ -8271,7 +8301,10 @@ test "Terminal: eraseDisplay erase above preserves SGR bg" {
defer testing.allocator.free(str);
try testing.expectEqualStrings("\n F\nGHI", str);
for (0..2) |x| {
const list_cell = t.screen.pages.getCell(.{ .active = .{ .x = x, .y = 1 } }).?;
const list_cell = t.screen.pages.getCell(.{ .active = .{
.x = @intCast(x),
.y = 1,
} }).?;
try testing.expect(list_cell.cell.content_tag == .bg_color_rgb);
try testing.expectEqual(Cell.RGB{
.r = 0xFF,

View File

@ -5,6 +5,7 @@ const ArenaAllocator = std.heap.ArenaAllocator;
const terminal = @import("../main.zig");
const point = @import("../point.zig");
const size = @import("../size.zig");
const command = @import("graphics_command.zig");
const PageList = @import("../PageList.zig");
const Screen = @import("../Screen.zig");
@ -265,13 +266,13 @@ pub const ImageStorage = struct {
);
},
.intersect_cell => |v| {
.intersect_cell => |v| intersect_cell: {
self.deleteIntersecting(
alloc,
t,
.{ .active = .{
.x = v.x,
.y = v.y,
.x = std.math.cast(size.CellCountInt, v.x) orelse break :intersect_cell,
.y = std.math.cast(size.CellCountInt, v.y) orelse break :intersect_cell,
} },
v.delete,
{},
@ -279,13 +280,13 @@ pub const ImageStorage = struct {
);
},
.intersect_cell_z => |v| {
.intersect_cell_z => |v| intersect_cell_z: {
self.deleteIntersecting(
alloc,
t,
.{ .active = .{
.x = v.x,
.y = v.y,
.x = std.math.cast(size.CellCountInt, v.x) orelse break :intersect_cell_z,
.y = std.math.cast(size.CellCountInt, v.y) orelse break :intersect_cell_z,
} },
v.delete,
v.z,
@ -317,7 +318,7 @@ pub const ImageStorage = struct {
// v.y is in active coords so we want to convert it to a pin
// so we can compare by page offsets.
const target_pin = t.screen.pages.pin(.{ .active = .{
.y = v.y,
.y = std.math.cast(size.CellCountInt, v.y) orelse break :row,
} }) orelse break :row;
var it = self.placements.iterator();

View File

@ -25,6 +25,7 @@ pub const Charset = charsets.Charset;
pub const CharsetSlot = charsets.Slots;
pub const CharsetActiveSlot = charsets.ActiveSlot;
pub const Cell = page.Cell;
pub const Coordinate = point.Coordinate;
pub const CSI = Parser.Action.CSI;
pub const DCS = Parser.Action.DCS;
pub const MouseShape = @import("mouse_shape.zig").MouseShape;

View File

@ -1,6 +1,7 @@
const std = @import("std");
const Allocator = std.mem.Allocator;
const assert = std.debug.assert;
const size = @import("size.zig");
/// The possible reference locations for a point. When someone says "(42, 80)" in the context of a terminal, that could mean multiple
/// things: it is in the current visible viewport? the current active
@ -65,8 +66,8 @@ pub const Point = union(Tag) {
};
pub const Coordinate = struct {
x: usize = 0,
y: usize = 0,
x: size.CellCountInt = 0,
y: size.CellCountInt = 0,
pub fn eql(self: Coordinate, other: Coordinate) bool {
return self.x == other.x and self.y == other.y;