diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig index 0afc363a8..73c515239 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.zig @@ -29,6 +29,7 @@ const Health = renderer.Health; const mtl = @import("metal/api.zig"); const mtl_buffer = @import("metal/buffer.zig"); +const mtl_cell = @import("metal/cell.zig"); const mtl_image = @import("metal/image.zig"); const mtl_sampler = @import("metal/sampler.zig"); const mtl_shaders = @import("metal/shaders.zig"); @@ -36,7 +37,6 @@ const Image = mtl_image.Image; const ImageMap = mtl_image.ImageMap; const Shaders = mtl_shaders.Shaders; -const CellBuffer = mtl_buffer.Buffer(mtl_shaders.Cell); const ImageBuffer = mtl_buffer.Buffer(mtl_shaders.Image); const InstanceBuffer = mtl_buffer.Buffer(u16); @@ -90,8 +90,7 @@ current_background_color: terminal.color.RGB, /// The current set of cells to render. This is rebuilt on every frame /// but we keep this around so that we don't reallocate. Each set of /// cells goes into a separate shader. -cells_bg: std.ArrayListUnmanaged(mtl_shaders.Cell), -cells: std.ArrayListUnmanaged(mtl_shaders.Cell), +cells: mtl_cell.Contents, /// The current GPU uniform values. uniforms: mtl_shaders.Uniforms, @@ -108,18 +107,9 @@ image_text_end: u32 = 0, /// Metal state shaders: Shaders, // Compiled shaders -buf_cells: CellBuffer, // Vertex buffer for cells -buf_cells_bg: CellBuffer, // Vertex buffer for background cells -buf_instance: InstanceBuffer, // MTLBuffer /// Metal objects -device: objc.Object, // MTLDevice -queue: objc.Object, // MTLCommandQueue layer: objc.Object, // CAMetalLayer -texture_greyscale: objc.Object, // MTLTexture -texture_color: objc.Object, // MTLTexture -texture_greyscale_modified: usize = 0, -texture_color_modified: usize = 0, /// Custom shader state. This is only set if we have custom shaders. custom_shader_state: ?CustomShaderState = null, @@ -128,10 +118,151 @@ custom_shader_state: ?CustomShaderState = null, /// this will have to be part of the frame state. health: std.atomic.Value(Health) = .{ .raw = .healthy }, -/// Sempahore blocking our in-flight buffer updates. For now this is just -/// one but in the future if we implement double/triple-buffering this -/// will be incremented. -inflight: std.Thread.Semaphore = .{ .permits = 1 }, +/// Our GPU state +gpu_state: GPUState, + +/// State we need for the GPU that is shared between all frames. +pub const GPUState = struct { + // The count of buffers we use for double/triple buffering. If + // this is one then we don't do any double+ buffering at all. This + // is comptime because there isn't a good reason to change this at + // runtime and there is a lot of complexity to support it. For comptime, + // this is useful for debugging. + // TODO(mitchellh): enable triple-buffering when we improve our frame times + const BufferCount = 1; + + /// The frame data, the current frame index, and the semaphore protecting + /// the frame data. This is used to implement double/triple/etc. buffering. + frames: [BufferCount]FrameState, + frame_index: std.math.IntFittingRange(0, BufferCount) = 0, + frame_sema: std.Thread.Semaphore = .{ .permits = BufferCount }, + + device: objc.Object, // MTLDevice + queue: objc.Object, // MTLCommandQueue + + /// This buffer is written exactly once so we can use it globally. + instance: InstanceBuffer, // MTLBuffer + + pub fn init() !GPUState { + const device = objc.Object.fromId(mtl.MTLCreateSystemDefaultDevice()); + const queue = device.msgSend(objc.Object, objc.sel("newCommandQueue"), .{}); + errdefer queue.msgSend(void, objc.sel("release"), .{}); + + var instance = try InstanceBuffer.initFill(device, &.{ + 0, 1, 3, // Top-left triangle + 1, 2, 3, // Bottom-right triangle + }); + errdefer instance.deinit(); + + var result: GPUState = .{ + .device = device, + .queue = queue, + .instance = instance, + .frames = undefined, + }; + + // Initialize all of our frame state. + for (&result.frames) |*frame| { + frame.* = try FrameState.init(result.device); + } + + return result; + } + + pub fn deinit(self: *GPUState) void { + // Wait for all of our inflight draws to complete so that + // we can cleanly deinit our GPU state. + for (0..BufferCount) |_| self.frame_sema.wait(); + for (&self.frames) |*frame| frame.deinit(); + self.instance.deinit(); + self.queue.msgSend(void, objc.sel("release"), .{}); + } + + /// Get the next frame state to draw to. This will wait on the + /// semaphore to ensure that the frame is available. This must + /// always be paired with a call to releaseFrame. + pub fn nextFrame(self: *GPUState) *FrameState { + self.frame_sema.wait(); + errdefer self.frame_sema.post(); + self.frame_index = (self.frame_index + 1) % BufferCount; + return &self.frames[self.frame_index]; + } + + /// This should be called when the frame has completed drawing. + pub fn releaseFrame(self: *GPUState) void { + self.frame_sema.post(); + } +}; + +/// State we need duplicated for every frame. Any state that could be +/// in a data race between the GPU and CPU while a frame is being +/// drawn should be in this struct. +/// +/// While a draw is in-process, we "lock" the state (via a semaphore) +/// and prevent the CPU from updating the state until Metal reports +/// that the frame is complete. +/// +/// This is used to implement double/triple buffering. +pub const FrameState = struct { + uniforms: UniformBuffer, + cells: CellTextBuffer, + cells_bg: CellBgBuffer, + + greyscale: objc.Object, // MTLTexture + greyscale_modified: usize = 0, + color: objc.Object, // MTLTexture + color_modified: usize = 0, + + /// A buffer containing the uniform data. + const UniformBuffer = mtl_buffer.Buffer(mtl_shaders.Uniforms); + const CellBgBuffer = mtl_buffer.Buffer(mtl_shaders.CellBg); + const CellTextBuffer = mtl_buffer.Buffer(mtl_shaders.CellText); + + pub fn init(device: objc.Object) !FrameState { + // Uniform buffer contains exactly 1 uniform struct. The + // uniform data will be undefined so this must be set before + // a frame is drawn. + var uniforms = try UniformBuffer.init(device, 1); + errdefer uniforms.deinit(); + + // Create the buffers for our vertex data. The preallocation size + // is likely too small but our first frame update will resize it. + var cells = try CellTextBuffer.init(device, 10 * 10); + errdefer cells.deinit(); + var cells_bg = try CellBgBuffer.init(device, 10 * 10); + errdefer cells_bg.deinit(); + + // Initialize our textures for our font atlas. + const greyscale = try initAtlasTexture(device, &.{ + .data = undefined, + .size = 8, + .format = .greyscale, + }); + errdefer deinitMTLResource(greyscale); + const color = try initAtlasTexture(device, &.{ + .data = undefined, + .size = 8, + .format = .rgba, + }); + errdefer deinitMTLResource(color); + + return .{ + .uniforms = uniforms, + .cells = cells, + .cells_bg = cells_bg, + .greyscale = greyscale, + .color = color, + }; + } + + pub fn deinit(self: *FrameState) void { + self.uniforms.deinit(); + self.cells.deinit(); + self.cells_bg.deinit(); + deinitMTLResource(self.greyscale); + deinitMTLResource(self.color); + } +}; pub const CustomShaderState = struct { /// The screen texture that we render the terminal to. If we don't have @@ -309,8 +440,8 @@ pub fn init(alloc: Allocator, options: renderer.Options) !Metal { }; // Initialize our metal stuff - const device = objc.Object.fromId(mtl.MTLCreateSystemDefaultDevice()); - const queue = device.msgSend(objc.Object, objc.sel("newCommandQueue"), .{}); + var gpu_state = try GPUState.init(); + errdefer gpu_state.deinit(); // Get our CAMetalLayer const layer = switch (builtin.os.tag) { @@ -324,7 +455,7 @@ pub fn init(alloc: Allocator, options: renderer.Options) !Metal { else => @compileError("unsupported target for Metal"), }; - layer.setProperty("device", device.value); + layer.setProperty("device", gpu_state.device.value); layer.setProperty("opaque", options.config.background_opacity >= 1); layer.setProperty("displaySyncEnabled", false); // disable v-sync @@ -353,17 +484,6 @@ pub fn init(alloc: Allocator, options: renderer.Options) !Metal { }); errdefer font_shaper.deinit(); - // Vertex buffers - var buf_cells = try CellBuffer.init(device, 160 * 160); - errdefer buf_cells.deinit(); - var buf_cells_bg = try CellBuffer.init(device, 160 * 160); - errdefer buf_cells_bg.deinit(); - var buf_instance = try InstanceBuffer.initFill(device, &.{ - 0, 1, 3, // Top-left triangle - 1, 2, 3, // Bottom-right triangle - }); - errdefer buf_instance.deinit(); - // Load our custom shaders const custom_shaders: []const [:0]const u8 = shadertoy.loadFromFiles( arena_alloc, @@ -379,7 +499,7 @@ pub fn init(alloc: Allocator, options: renderer.Options) !Metal { if (custom_shaders.len == 0) break :state null; // Build our sampler for our texture - var sampler = try mtl_sampler.Sampler.init(device); + var sampler = try mtl_sampler.Sampler.init(gpu_state.device); errdefer sampler.deinit(); break :state .{ @@ -407,32 +527,24 @@ pub fn init(alloc: Allocator, options: renderer.Options) !Metal { errdefer if (custom_shader_state) |*state| state.deinit(); // Initialize our shaders - var shaders = try Shaders.init(alloc, device, custom_shaders); + var shaders = try Shaders.init(alloc, gpu_state.device, custom_shaders); errdefer shaders.deinit(alloc); // Initialize all the data that requires a critical font section. const font_critical: struct { metrics: font.Metrics, - texture_greyscale: objc.Object, - texture_color: objc.Object, } = font_critical: { const grid = options.font_grid; grid.lock.lockShared(); defer grid.lock.unlockShared(); - - // Font atlas textures - const greyscale = try initAtlasTexture(device, &grid.atlas_greyscale); - errdefer deinitMTLResource(greyscale); - const color = try initAtlasTexture(device, &grid.atlas_color); - errdefer deinitMTLResource(color); - break :font_critical .{ .metrics = grid.metrics, - .texture_greyscale = greyscale, - .texture_color = color, }; }; + const cells = try mtl_cell.Contents.init(alloc); + errdefer cells.deinit(alloc); + return Metal{ .alloc = alloc, .config = options.config, @@ -447,12 +559,13 @@ pub fn init(alloc: Allocator, options: renderer.Options) !Metal { .current_background_color = options.config.background, // Render state - .cells_bg = .{}, - .cells = .{}, + .cells = cells, .uniforms = .{ .projection_matrix = undefined, .cell_size = undefined, .min_contrast = options.config.min_contrast, + .cursor_pos = .{ std.math.maxInt(u16), std.math.maxInt(u16) }, + .cursor_color = undefined, }, // Fonts @@ -461,29 +574,18 @@ pub fn init(alloc: Allocator, options: renderer.Options) !Metal { // Shaders .shaders = shaders, - .buf_cells = buf_cells, - .buf_cells_bg = buf_cells_bg, - .buf_instance = buf_instance, // Metal stuff - .device = device, - .queue = queue, .layer = layer, - .texture_greyscale = font_critical.texture_greyscale, - .texture_color = font_critical.texture_color, .custom_shader_state = custom_shader_state, + .gpu_state = gpu_state, }; } pub fn deinit(self: *Metal) void { - // If we have inflight buffers, wait for completion. This ensures that - // any pending GPU operations are completed before we start deallocating - // everything. This is important because our completion callbacks access - // "self" - self.inflight.wait(); + self.gpu_state.deinit(); self.cells.deinit(self.alloc); - self.cells_bg.deinit(self.alloc); self.font_shaper.deinit(); @@ -496,13 +598,6 @@ pub fn deinit(self: *Metal) void { } self.image_placements.deinit(self.alloc); - self.buf_cells_bg.deinit(); - self.buf_cells.deinit(); - self.buf_instance.deinit(); - deinitMTLResource(self.texture_greyscale); - deinitMTLResource(self.texture_color); - self.queue.msgSend(void, objc.sel("release"), .{}); - if (self.custom_shader_state) |*state| state.deinit(); self.shaders.deinit(self.alloc); @@ -567,8 +662,14 @@ pub fn setFocus(self: *Metal, focus: bool) !void { pub fn setFontGrid(self: *Metal, grid: *font.SharedGrid) void { // Update our grid self.font_grid = grid; - self.texture_greyscale_modified = 0; - self.texture_color_modified = 0; + + // Update all our textures so that they sync on the next frame. + // We can modify this without a lock because the GPU does not + // touch this data. + for (&self.gpu_state.frames) |*frame| { + frame.greyscale_modified = 0; + frame.color_modified = 0; + } // Get our metrics from the grid. This doesn't require a lock because // the metrics are never recalculated. @@ -583,6 +684,8 @@ pub fn setFontGrid(self: *Metal, grid: *font.SharedGrid) void { @floatFromInt(metrics.cell_height), }, .min_contrast = self.uniforms.min_contrast, + .cursor_pos = self.uniforms.cursor_pos, + .cursor_color = self.uniforms.cursor_color, }; } @@ -699,7 +802,7 @@ pub fn updateFrame( .replace_grey_alpha, .replace_rgb, .replace_rgba, - => try kv.value_ptr.image.upload(self.alloc, self.device), + => try kv.value_ptr.image.upload(self.alloc, self.gpu_state.device), .unload_pending, .unload_replace, @@ -717,9 +820,17 @@ pub fn updateFrame( pub fn drawFrame(self: *Metal, surface: *apprt.Surface) !void { _ = surface; - // Wait for a buffer to be available. - self.inflight.wait(); - errdefer self.inflight.post(); + // Wait for a frame to be available. + const frame = self.gpu_state.nextFrame(); + errdefer self.gpu_state.releaseFrame(); + // log.debug("drawing frame index={}", .{self.gpu_state.frame_index}); + + // Setup our frame data + const cells_bg = self.cells.bgCells(); + const cells_fg = self.cells.fgCells(); + try frame.uniforms.sync(self.gpu_state.device, &.{self.uniforms}); + try frame.cells_bg.sync(self.gpu_state.device, cells_bg); + try frame.cells.sync(self.gpu_state.device, cells_fg); // If we have custom shaders, update the animation time. if (self.custom_shader_state) |*state| { @@ -750,23 +861,23 @@ pub fn drawFrame(self: *Metal, surface: *apprt.Surface) !void { // If our font atlas changed, sync the texture data texture: { const modified = self.font_grid.atlas_greyscale.modified.load(.monotonic); - if (modified <= self.texture_greyscale_modified) break :texture; + if (modified <= frame.greyscale_modified) break :texture; self.font_grid.lock.lockShared(); defer self.font_grid.lock.unlockShared(); - self.texture_greyscale_modified = self.font_grid.atlas_greyscale.modified.load(.monotonic); - try syncAtlasTexture(self.device, &self.font_grid.atlas_greyscale, &self.texture_greyscale); + frame.greyscale_modified = self.font_grid.atlas_greyscale.modified.load(.monotonic); + try syncAtlasTexture(self.gpu_state.device, &self.font_grid.atlas_greyscale, &frame.greyscale); } texture: { const modified = self.font_grid.atlas_color.modified.load(.monotonic); - if (modified <= self.texture_color_modified) break :texture; + if (modified <= frame.color_modified) break :texture; self.font_grid.lock.lockShared(); defer self.font_grid.lock.unlockShared(); - self.texture_color_modified = self.font_grid.atlas_color.modified.load(.monotonic); - try syncAtlasTexture(self.device, &self.font_grid.atlas_color, &self.texture_color); + frame.color_modified = self.font_grid.atlas_color.modified.load(.monotonic); + try syncAtlasTexture(self.gpu_state.device, &self.font_grid.atlas_color, &frame.color); } // Command buffer (MTLCommandBuffer) - const buffer = self.queue.msgSend(objc.Object, objc.sel("commandBuffer"), .{}); + const buffer = self.gpu_state.queue.msgSend(objc.Object, objc.sel("commandBuffer"), .{}); { // MTLRenderPassDescriptor @@ -818,13 +929,13 @@ pub fn drawFrame(self: *Metal, surface: *apprt.Surface) !void { try self.drawImagePlacements(encoder, self.image_placements.items[0..self.image_bg_end]); // Then draw background cells - try self.drawCells(encoder, &self.buf_cells_bg, self.cells_bg); + try self.drawCellBgs(encoder, frame, cells_bg.len); // Then draw images under text try self.drawImagePlacements(encoder, self.image_placements.items[self.image_bg_end..self.image_text_end]); // Then draw fg cells - try self.drawCells(encoder, &self.buf_cells, self.cells); + try self.drawCellFgs(encoder, frame, cells_fg.len); // Then draw remaining images try self.drawImagePlacements(encoder, self.image_placements.items[self.image_text_end..]); @@ -935,7 +1046,7 @@ fn bufferCompleted( } // Always release our semaphore - self.inflight.post(); + self.gpu_state.releaseFrame(); } fn drawPostShader( @@ -1046,7 +1157,7 @@ fn drawImagePlacement( // Create our vertex buffer, which is always exactly one item. // future(mitchellh): we can group rendering multiple instances of a single image const Buffer = mtl_buffer.Buffer(mtl_shaders.Image); - var buf = try Buffer.initFill(self.device, &.{.{ + var buf = try Buffer.initFill(self.gpu_state.device, &.{.{ .grid_pos = .{ @as(f32, @floatFromInt(p.x)), @as(f32, @floatFromInt(p.y)), @@ -1104,7 +1215,7 @@ fn drawImagePlacement( @intFromEnum(mtl.MTLPrimitiveType.triangle), @as(c_ulong, 6), @intFromEnum(mtl.MTLIndexType.uint16), - self.buf_instance.buffer.value, + self.gpu_state.instance.buffer.value, @as(c_ulong, 0), @as(c_ulong, 1), }, @@ -1113,58 +1224,34 @@ fn drawImagePlacement( // log.debug("drawImagePlacement: {}", .{p}); } -/// Loads some set of cell data into our buffer and issues a draw call. -/// This expects all the Metal command encoder state to be setup. -/// -/// Future: when we move to multiple shaders, this will go away and -/// we'll have a draw call per-shader. -fn drawCells( +/// Draw the cell backgrounds. +fn drawCellBgs( self: *Metal, encoder: objc.Object, - buf: *CellBuffer, - cells: std.ArrayListUnmanaged(mtl_shaders.Cell), + frame: *const FrameState, + len: usize, ) !void { - if (cells.items.len == 0) return; - - try buf.sync(self.device, cells.items); + // This triggers an assertion in the Metal API if we try to draw + // with an instance count of 0 so just bail. + if (len == 0) return; // Use our shader pipeline encoder.msgSend( void, objc.sel("setRenderPipelineState:"), - .{self.shaders.cell_pipeline.value}, + .{self.shaders.cell_bg_pipeline.value}, ); // Set our buffers encoder.msgSend( void, - objc.sel("setVertexBytes:length:atIndex:"), - .{ - @as(*const anyopaque, @ptrCast(&self.uniforms)), - @as(c_ulong, @sizeOf(@TypeOf(self.uniforms))), - @as(c_ulong, 1), - }, - ); - encoder.msgSend( - void, - objc.sel("setFragmentTexture:atIndex:"), - .{ - self.texture_greyscale.value, - @as(c_ulong, 0), - }, - ); - encoder.msgSend( - void, - objc.sel("setFragmentTexture:atIndex:"), - .{ - self.texture_color.value, - @as(c_ulong, 1), - }, + objc.sel("setVertexBuffer:offset:atIndex:"), + .{ frame.cells_bg.buffer.value, @as(c_ulong, 0), @as(c_ulong, 0) }, ); encoder.msgSend( void, objc.sel("setVertexBuffer:offset:atIndex:"), - .{ buf.buffer.value, @as(c_ulong, 0), @as(c_ulong, 0) }, + .{ frame.uniforms.buffer.value, @as(c_ulong, 0), @as(c_ulong, 1) }, ); encoder.msgSend( @@ -1174,9 +1261,69 @@ fn drawCells( @intFromEnum(mtl.MTLPrimitiveType.triangle), @as(c_ulong, 6), @intFromEnum(mtl.MTLIndexType.uint16), - self.buf_instance.buffer.value, + self.gpu_state.instance.buffer.value, @as(c_ulong, 0), - @as(c_ulong, cells.items.len), + @as(c_ulong, len), + }, + ); +} + +/// Draw the cell foregrounds using the text shader. +fn drawCellFgs( + self: *Metal, + encoder: objc.Object, + frame: *const FrameState, + len: usize, +) !void { + // This triggers an assertion in the Metal API if we try to draw + // with an instance count of 0 so just bail. + if (len == 0) return; + + // Use our shader pipeline + encoder.msgSend( + void, + objc.sel("setRenderPipelineState:"), + .{self.shaders.cell_text_pipeline.value}, + ); + + // Set our buffers + encoder.msgSend( + void, + objc.sel("setVertexBuffer:offset:atIndex:"), + .{ frame.cells.buffer.value, @as(c_ulong, 0), @as(c_ulong, 0) }, + ); + 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("setFragmentTexture:atIndex:"), + .{ + frame.greyscale.value, + @as(c_ulong, 0), + }, + ); + encoder.msgSend( + void, + objc.sel("setFragmentTexture:atIndex:"), + .{ + frame.color.value, + @as(c_ulong, 1), + }, + ); + + 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, len), }, ); } @@ -1433,13 +1580,12 @@ pub fn setScreenSize( @floatFromInt(self.grid_metrics.cell_height), }, .min_contrast = old.min_contrast, + .cursor_pos = old.cursor_pos, + .cursor_color = old.cursor_color, }; - // Reset our buffer sizes so that we free memory when the screen shrinks. - // This could be made more clever by only doing this when the screen - // shrinks but the performance cost really isn't that much. - self.cells.clearAndFree(self.alloc); - self.cells_bg.clearAndFree(self.alloc); + // Reset our cell contents. + try self.cells.resize(self.alloc, grid_size); // If we have custom shaders then we update the state if (self.custom_shader_state) |*state| { @@ -1476,7 +1622,7 @@ pub fn setScreenSize( // If we fail to create the texture, then we just don't have a screen // texture and our custom shaders won't run. - const id = self.device.msgSend( + const id = self.gpu_state.device.msgSend( ?*anyopaque, objc.sel("newTextureWithDescriptor:"), .{desc}, @@ -1489,9 +1635,9 @@ pub fn setScreenSize( log.debug("screen size screen={} grid={}, cell_width={} cell_height={}", .{ dim, grid_size, self.grid_metrics.cell_width, self.grid_metrics.cell_height }); } -/// Sync all the CPU cells with the GPU state (but still on the CPU here). -/// This builds all our "GPUCells" on this struct, but doesn't send them -/// down to the GPU yet. +/// Convert the terminal state to GPU cells stored in CPU memory. These +/// are then synced to the GPU in the next frame. This only updates CPU +/// memory and doesn't touch the GPU. fn rebuildCells( self: *Metal, screen: *terminal.Screen, @@ -1500,26 +1646,6 @@ fn rebuildCells( cursor_style_: ?renderer.CursorStyle, color_palette: *const terminal.color.Palette, ) !void { - const rows_usize: usize = @intCast(screen.pages.rows); - const cols_usize: usize = @intCast(screen.pages.cols); - - // Bg cells at most will need space for the visible screen size - self.cells_bg.clearRetainingCapacity(); - try self.cells_bg.ensureTotalCapacity( - self.alloc, - rows_usize * cols_usize, - ); - - // Over-allocate just to ensure we don't allocate again during loops. - self.cells.clearRetainingCapacity(); - try self.cells.ensureTotalCapacity( - self.alloc, - - // * 3 for glyph + underline + strikethrough for each cell - // + 1 for cursor - (rows_usize * cols_usize * 3) + 1, - ); - // Create an arena for all our temporary allocations while rebuilding var arena = ArenaAllocator.init(self.alloc); defer arena.deinit(); @@ -1536,8 +1662,8 @@ 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); @@ -1548,61 +1674,22 @@ fn rebuildCells( }; } else null; - // This is the cell that has [mode == .fg] and is underneath our cursor. - // We keep track of it so that we can invert the colors so the character - // remains visible. - var cursor_cell: ?mtl_shaders.Cell = null; - - // Build each cell - var row_it = screen.pages.rowIterator(.right_down, .{ .viewport = .{} }, null); - var y: usize = 0; + // Go row-by-row to build the cells. We go row by row because we do + // font shaping by row. In the future, we will also do dirty tracking + // by row. + var row_it = screen.pages.rowIterator(.left_up, .{ .viewport = .{} }, null); + var y: terminal.size.CellCountInt = screen.pages.rows; while (row_it.next()) |row| { - defer y += 1; + y = y - 1; - // True if this is the row with our cursor. There are a lot of conditions - // here because the reasons we need to know this are primarily to invert. - // - // - If we aren't drawing the cursor then we don't need to change our rendering. - // - If the cursor is not visible, then we don't need to change rendering. - // - If the cursor style is not a box, then we don't need to change - // rendering because it'll never fully overlap a glyph. - // - If the viewport is not at the bottom, then we don't need to - // change rendering because the cursor is not visible. - // (NOTE: this may not be fully correct, we may be scrolled - // slightly up and the cursor may be visible) - // - If this y doesn't match our cursor y then we don't need to - // change rendering. - // - const cursor_row = if (cursor_style_) |cursor_style| - cursor_style == .block and - screen.viewportIsBottom() and - y == screen.cursor.y - else - false; + // If we're rebuilding a row, then we always clear the cells + self.cells.clear(y); // True if we want to do font shaping around the cursor. We want to // do font shaping as long as the cursor is enabled. const shape_cursor = screen.viewportIsBottom() and y == screen.cursor.y; - // If this is the row with our cursor, then we may have to modify - // the cell with the cursor. - const start_i: usize = self.cells.items.len; - defer if (cursor_row) { - // If we're on a wide spacer tail, then we want to look for - // the previous cell. - const screen_cell = row.cells(.all)[screen.cursor.x]; - const x = screen.cursor.x - @intFromBool(screen_cell.wide == .spacer_tail); - for (self.cells.items[start_i..]) |cell| { - if (cell.grid_pos[0] == @as(f32, @floatFromInt(x)) and - (cell.mode == .fg or cell.mode == .fg_color)) - { - cursor_cell = cell; - break; - } - } - }; - // We need to get this row's selection if there is one for proper // run splitting. const row_selection = sel: { @@ -1622,13 +1709,17 @@ fn rebuildCells( ); while (try iter.next(self.alloc)) |run| { for (try self.font_shaper.shape(run)) |shaper_cell| { - // If this cell falls within our preedit range then we skip it. - // We do this so we don't have conflicting data on the same - // cell. + const coord: terminal.Coordinate = .{ + .x = shaper_cell.x, + .y = y, + }; + + // If this cell falls within our preedit range then we + // skip this because preedits are setup separately. if (preedit_range) |range| { - if (range.y == y and - shaper_cell.x >= range.x[0] and - shaper_cell.x <= range.x[1]) + if (range.y == coord.y and + coord.x >= range.x[0] and + coord.x <= range.x[1]) { continue; } @@ -1638,7 +1729,7 @@ fn rebuildCells( // underline it. const cell: terminal.Pin = cell: { var copy = row; - copy.x = shaper_cell.x; + copy.x = coord.x; break :cell copy; }; @@ -1652,14 +1743,13 @@ fn rebuildCells( color_palette, shaper_cell, run, - shaper_cell.x, - y, + coord, )) |update| { assert(update); } else |err| { log.warn("error building cell, will be invalid x={} y={}, err={}", .{ - shaper_cell.x, - y, + coord.x, + coord.y, err, }); } @@ -1667,48 +1757,57 @@ fn rebuildCells( } } - // Add the cursor at the end so that it overlays everything. If we have - // a cursor cell then we invert the colors on that and add it in so - // that we can always see it. - if (cursor_style_) |cursor_style| cursor_style: { - // If we have a preedit, we try to render the preedit text on top - // of the cursor. - if (preedit) |preedit_v| { - const range = preedit_range.?; - var x = range.x[0]; - for (preedit_v.codepoints[range.cp_offset..]) |cp| { - self.addPreeditCell(cp, x, range.y) catch |err| { - log.warn("error building preedit cell, will be invalid x={} y={}, err={}", .{ - x, - range.y, - err, - }); - }; + // Setup our cursor rendering information. + cursor: { + // By default, we don't handle cursor inversion on the shader. + self.cells.setCursor(null); + self.uniforms.cursor_pos = .{ + std.math.maxInt(u16), + std.math.maxInt(u16), + }; - x += if (cp.wide) 2 else 1; - } + // If we have preedit text, we don't setup a cursor + if (preedit != null) break :cursor; - // Preedit hides the cursor - break :cursor_style; - } + // Prepare the cursor cell contents. + const style = cursor_style_ orelse break :cursor; + self.addCursor(screen, style); - _ = self.addCursor(screen, cursor_style); - if (cursor_cell) |*cell| { - if (cell.mode == .fg) { - cell.color = if (self.config.cursor_text) |txt| - .{ txt.r, txt.g, txt.b, 255 } - else - .{ self.background_color.r, self.background_color.g, self.background_color.b, 255 }; - } - - self.cells.appendAssumeCapacity(cell.*); + // If the cursor is visible then we set our uniforms. + if (style == .block and screen.viewportIsBottom()) { + self.uniforms.cursor_pos = .{ + screen.cursor.x, + screen.cursor.y, + }; + self.uniforms.cursor_color = if (self.config.cursor_text) |txt| .{ + txt.r, + txt.g, + txt.b, + 255, + } else .{ + self.background_color.r, + self.background_color.g, + self.background_color.b, + 255, + }; } } - // Some debug mode safety checks - if (std.debug.runtime_safety) { - for (self.cells_bg.items) |cell| assert(cell.mode == .bg); - for (self.cells.items) |cell| assert(cell.mode != .bg); + // Setup our preedit text. + if (preedit) |preedit_v| { + const range = preedit_range.?; + var x = range.x[0]; + for (preedit_v.codepoints[range.cp_offset..]) |cp| { + self.addPreeditCell(cp, .{ .x = x, .y = range.y }) catch |err| { + log.warn("error building preedit cell, will be invalid x={} y={}, err={}", .{ + x, + range.y, + err, + }); + }; + + x += if (cp.wide) 2 else 1; + } } } @@ -1720,8 +1819,7 @@ fn updateCell( palette: *const terminal.color.Palette, shaper_cell: font.shape.Cell, shaper_run: font.shape.TextRun, - x: usize, - y: usize, + coord: terminal.Coordinate, ) !bool { const BgFg = struct { /// Background is optional because in un-inverted mode @@ -1820,12 +1918,11 @@ fn updateCell( break :bg_alpha @intFromFloat(bg_alpha); }; - self.cells_bg.appendAssumeCapacity(.{ - .mode = .bg, - .grid_pos = .{ @as(f32, @floatFromInt(x)), @as(f32, @floatFromInt(y)) }, + try self.cells.set(self.alloc, .bg, .{ + .mode = .rgb, + .grid_pos = .{ @intCast(coord.x), @intCast(coord.y) }, .cell_width = cell.gridWidth(), .color = .{ rgb.r, rgb.g, rgb.b, bg_alpha }, - .bg_color = .{ 0, 0, 0, 0 }, }); break :bg .{ rgb.r, rgb.g, rgb.b, bg_alpha }; @@ -1849,7 +1946,7 @@ fn updateCell( }, ); - const mode: mtl_shaders.Cell.Mode = switch (try fgMode( + const mode: mtl_shaders.CellText.Mode = switch (try fgMode( render.presentation, cell_pin, )) { @@ -1858,9 +1955,9 @@ fn updateCell( .constrained => .fg_constrained, }; - self.cells.appendAssumeCapacity(.{ + try self.cells.set(self.alloc, .text, .{ .mode = mode, - .grid_pos = .{ @as(f32, @floatFromInt(x)), @as(f32, @floatFromInt(y)) }, + .grid_pos = .{ @intCast(coord.x), @intCast(coord.y) }, .cell_width = cell.gridWidth(), .color = .{ colors.fg.r, colors.fg.g, colors.fg.b, alpha }, .bg_color = bg, @@ -1895,9 +1992,9 @@ fn updateCell( const color = style.underlineColor(palette) orelse colors.fg; - self.cells.appendAssumeCapacity(.{ + try self.cells.set(self.alloc, .underline, .{ .mode = .fg, - .grid_pos = .{ @as(f32, @floatFromInt(x)), @as(f32, @floatFromInt(y)) }, + .grid_pos = .{ @intCast(coord.x), @intCast(coord.y) }, .cell_width = cell.gridWidth(), .color = .{ color.r, color.g, color.b, alpha }, .bg_color = bg, @@ -1918,9 +2015,9 @@ fn updateCell( }, ); - self.cells.appendAssumeCapacity(.{ + try self.cells.set(self.alloc, .strikethrough, .{ .mode = .fg, - .grid_pos = .{ @as(f32, @floatFromInt(x)), @as(f32, @floatFromInt(y)) }, + .grid_pos = .{ @intCast(coord.x), @intCast(coord.y) }, .cell_width = cell.gridWidth(), .color = .{ colors.fg.r, colors.fg.g, colors.fg.b, alpha }, .bg_color = bg, @@ -1937,7 +2034,7 @@ fn addCursor( self: *Metal, screen: *terminal.Screen, cursor_style: renderer.CursorStyle, -) ?*const mtl_shaders.Cell { +) void { // Add the cursor. We render the cursor over the wide character if // we're on the wide characer tail. const wide, const x = cell: { @@ -1975,15 +2072,12 @@ fn addCursor( }, ) catch |err| { log.warn("error rendering cursor glyph err={}", .{err}); - return null; + return; }; - self.cells.appendAssumeCapacity(.{ - .mode = .fg, - .grid_pos = .{ - @as(f32, @floatFromInt(x)), - @as(f32, @floatFromInt(screen.cursor.y)), - }, + self.cells.setCursor(.{ + .mode = .cursor, + .grid_pos = .{ x, screen.cursor.y }, .cell_width = if (wide) 2 else 1, .color = .{ color.r, color.g, color.b, alpha }, .bg_color = .{ 0, 0, 0, 0 }, @@ -1991,15 +2085,12 @@ fn addCursor( .glyph_size = .{ render.glyph.width, render.glyph.height }, .glyph_offset = .{ render.glyph.offset_x, render.glyph.offset_y }, }); - - return &self.cells.items[self.cells.items.len - 1]; } fn addPreeditCell( self: *Metal, cp: renderer.State.Preedit.Codepoint, - x: usize, - y: usize, + coord: terminal.Coordinate, ) !void { // Preedit is rendered inverted const bg = self.foreground_color; @@ -2022,18 +2113,17 @@ fn addPreeditCell( }; // Add our opaque background cell - self.cells_bg.appendAssumeCapacity(.{ - .mode = .bg, - .grid_pos = .{ @as(f32, @floatFromInt(x)), @as(f32, @floatFromInt(y)) }, + try self.cells.set(self.alloc, .bg, .{ + .mode = .rgb, + .grid_pos = .{ @intCast(coord.x), @intCast(coord.y) }, .cell_width = if (cp.wide) 2 else 1, .color = .{ bg.r, bg.g, bg.b, 255 }, - .bg_color = .{ bg.r, bg.g, bg.b, 255 }, }); // Add our text - self.cells.appendAssumeCapacity(.{ + try self.cells.set(self.alloc, .text, .{ .mode = .fg, - .grid_pos = .{ @as(f32, @floatFromInt(x)), @as(f32, @floatFromInt(y)) }, + .grid_pos = .{ @intCast(coord.x), @intCast(coord.y) }, .cell_width = if (cp.wide) 2 else 1, .color = .{ fg.r, fg.g, fg.b, 255 }, .bg_color = .{ bg.r, bg.g, bg.b, 255 }, @@ -2122,3 +2212,7 @@ fn initAtlasTexture(device: objc.Object, atlas: *const font.Atlas) !objc.Object fn deinitMTLResource(obj: objc.Object) void { obj.msgSend(void, objc.sel("release"), .{}); } + +test { + _ = mtl_cell; +} diff --git a/src/renderer/OpenGL.zig b/src/renderer/OpenGL.zig index 7e7ab7dbe..041947f32 100644 --- a/src/renderer/OpenGL.zig +++ b/src/renderer/OpenGL.zig @@ -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; diff --git a/src/renderer/State.zig b/src/renderer/State.zig index 6bff5b219..8a11a7403 100644 --- a/src/renderer/State.zig +++ b/src/renderer/State.zig @@ -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]; diff --git a/src/renderer/metal/api.zig b/src/renderer/metal/api.zig index 0421a34a2..3ec04e367 100644 --- a/src/renderer/metal/api.zig +++ b/src/renderer/metal/api.zig @@ -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, diff --git a/src/renderer/metal/buffer.zig b/src/renderer/metal/buffer.zig index eb5c1d193..b11861817 100644 --- a/src/renderer/metal/buffer.zig +++ b/src/renderer/metal/buffer.zig @@ -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. diff --git a/src/renderer/metal/cell.zig b/src/renderer/metal/cell.zig new file mode 100644 index 000000000..bafb1556e --- /dev/null +++ b/src/renderer/metal/cell.zig @@ -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)); +} diff --git a/src/renderer/metal/shaders.zig b/src/renderer/metal/shaders.zig index d5a6baccb..a6303c78d 100644 --- a/src/renderer/metal/shaders.zig +++ b/src/renderer/metal/shaders.zig @@ -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)); } } diff --git a/src/renderer/shaders/cell.metal b/src/renderer/shaders/cell.metal index 10e5804cc..f486d4ecf 100644 --- a/src/renderer/shaders/cell.metal +++ b/src/renderer/shaders/cell.metal @@ -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 textureGreyscale [[texture(0)]], - texture2d textureColor [[texture(1)]]) { +fragment float4 cell_text_fragment(CellTextVertexOut in [[stage_in]], + texture2d textureGreyscale + [[texture(0)]], + texture2d 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 //------------------------------------------------------------------- diff --git a/src/terminal/PageList.zig b/src/terminal/PageList.zig index 1946a84aa..95df3e911 100644 --- a/src/terminal/PageList.zig +++ b/src/terminal/PageList.zig @@ -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); diff --git a/src/terminal/Screen.zig b/src/terminal/Screen.zig index 98c3daa21..58e4e681b 100644 --- a/src/terminal/Screen.zig +++ b/src/terminal/Screen.zig @@ -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); diff --git a/src/terminal/Selection.zig b/src/terminal/Selection.zig index 9da0f134e..d1bd4accb 100644 --- a/src/terminal/Selection.zig +++ b/src/terminal/Selection.zig @@ -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; } } diff --git a/src/terminal/Terminal.zig b/src/terminal/Terminal.zig index 94d32e1a8..9ddec218e 100644 --- a/src/terminal/Terminal.zig +++ b/src/terminal/Terminal.zig @@ -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, diff --git a/src/terminal/kitty/graphics_storage.zig b/src/terminal/kitty/graphics_storage.zig index 1071f065a..547f78b97 100644 --- a/src/terminal/kitty/graphics_storage.zig +++ b/src/terminal/kitty/graphics_storage.zig @@ -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(); diff --git a/src/terminal/main.zig b/src/terminal/main.zig index be60aa477..857dd79f3 100644 --- a/src/terminal/main.zig +++ b/src/terminal/main.zig @@ -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; diff --git a/src/terminal/point.zig b/src/terminal/point.zig index 41b7a3558..45aa28dea 100644 --- a/src/terminal/point.zig +++ b/src/terminal/point.zig @@ -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;