mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-07-31 12:49:03 +00:00
kitty images: add support for transient usage hints
Kitty 0.48 added support for usage hints in the image protocol, specifically for marking images as "transient", meaning that they should be prioritized for eviction if there is memory pressure. https://sw.kovidgoyal.net/kitty/graphics-protocol/#image-usage-hints Also changed the eviction algorithm to use an allocated array for organizing the images to be evicted rather than using an ArrayList to minimize the number of allocations made (no real memory savings though).
This commit is contained in:
@@ -422,6 +422,7 @@ pub const Transmission = struct {
|
||||
placement_id: u32 = 0, // p
|
||||
compression: Compression = .none, // o
|
||||
more_chunks: bool = false, // m
|
||||
usage: Usage = .default, // N
|
||||
|
||||
pub const Format = lib.Enum(lib.target, &.{
|
||||
"rgb", // 24
|
||||
@@ -446,6 +447,19 @@ pub const Transmission = struct {
|
||||
"zlib_deflate", // z
|
||||
});
|
||||
|
||||
/// Usage hints allow for optimising resource consumption strategies.
|
||||
///
|
||||
/// https://sw.kovidgoyal.net/kitty/graphics-protocol/#usage-hints
|
||||
pub const Usage = packed struct(u32) {
|
||||
/// Image with this usage hint is assumed to be used for only a short
|
||||
/// time, so may be evicted before other images if memory pressure is
|
||||
/// encountered.
|
||||
transient: bool = false,
|
||||
_padding: u31 = 0,
|
||||
|
||||
pub const default: Usage = .{};
|
||||
};
|
||||
|
||||
pub fn formatBpp(format: Format) u8 {
|
||||
return switch (format) {
|
||||
.gray => 1,
|
||||
@@ -529,6 +543,10 @@ pub const Transmission = struct {
|
||||
}
|
||||
}
|
||||
|
||||
if (kv.get('N')) |v| {
|
||||
result.usage = @bitCast(v);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
};
|
||||
@@ -1005,6 +1023,26 @@ test "transmission command" {
|
||||
try testing.expectEqual(Transmission.Format.rgb, v.format);
|
||||
try testing.expectEqual(@as(u32, 10), v.width);
|
||||
try testing.expectEqual(@as(u32, 20), v.height);
|
||||
try testing.expectEqual(false, v.usage.transient);
|
||||
}
|
||||
|
||||
test "transmission command with transient hint" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
var p = Parser.init(alloc, 1024 * 1024);
|
||||
defer p.deinit();
|
||||
|
||||
const input = "f=24,s=10,v=20,N=1";
|
||||
for (input) |c| try p.feed(c);
|
||||
const command = try p.complete(alloc);
|
||||
defer command.deinit(alloc);
|
||||
|
||||
try testing.expect(command.control == .transmit);
|
||||
const v = command.control.transmit;
|
||||
try testing.expectEqual(Transmission.Format.rgb, v.format);
|
||||
try testing.expectEqual(@as(u32, 10), v.width);
|
||||
try testing.expectEqual(@as(u32, 20), v.height);
|
||||
try testing.expectEqual(true, v.usage.transient);
|
||||
}
|
||||
|
||||
test "feedSlice matches per-byte feed" {
|
||||
|
||||
@@ -88,6 +88,7 @@ pub const LoadingImage = struct {
|
||||
.height = t.height,
|
||||
.compression = t.compression,
|
||||
.format = t.format,
|
||||
.usage = t.usage,
|
||||
},
|
||||
|
||||
.display = cmd.display(),
|
||||
@@ -512,6 +513,7 @@ pub const Image = struct {
|
||||
format: command.Transmission.Format = .rgb,
|
||||
compression: command.Transmission.Compression = .none,
|
||||
data: []const u8 = "",
|
||||
usage: command.Transmission.Usage = .default,
|
||||
|
||||
/// Unique, monotonically increasing stamp assigned each time an
|
||||
/// image is added to (or replaced in) an ImageStorage. A changed
|
||||
|
||||
@@ -594,8 +594,8 @@ pub const ImageStorage = struct {
|
||||
}
|
||||
|
||||
/// Evict image to make space. This will evict the oldest image,
|
||||
/// prioritizing unused images first, as recommended by the published
|
||||
/// Kitty spec.
|
||||
/// prioritizing transient images and unused images as recommended by the
|
||||
/// published Kitty spec.
|
||||
///
|
||||
/// This will evict as many images as necessary to make space for
|
||||
/// req bytes.
|
||||
@@ -608,14 +608,20 @@ pub const ImageStorage = struct {
|
||||
const Candidate = struct {
|
||||
id: u32,
|
||||
generation: u64,
|
||||
used: bool,
|
||||
// Map images into four distinct blocks:
|
||||
// 0: transient, unused
|
||||
// 1: not transient, unused
|
||||
// 2: transient, used
|
||||
// 3: not transient, used
|
||||
block: u2,
|
||||
};
|
||||
|
||||
var candidates: std.ArrayList(Candidate) = .empty;
|
||||
defer candidates.deinit(alloc);
|
||||
const candidates = try alloc.alloc(Candidate, self.images.count());
|
||||
defer alloc.free(candidates);
|
||||
|
||||
var it = self.images.iterator();
|
||||
while (it.next()) |kv| {
|
||||
var i: usize = 0;
|
||||
while (it.next()) |kv| : (i += 1) {
|
||||
const img = kv.value_ptr;
|
||||
|
||||
// This is a huge waste. See comment above about redesigning
|
||||
@@ -633,17 +639,24 @@ pub const ImageStorage = struct {
|
||||
break :used false;
|
||||
};
|
||||
|
||||
try candidates.append(alloc, .{
|
||||
const transient = img.usage.transient;
|
||||
|
||||
candidates[i] = .{
|
||||
.id = img.id,
|
||||
.generation = img.generation,
|
||||
.used = used,
|
||||
});
|
||||
// Map images into four distinct blocks:
|
||||
// 0: transient, unused
|
||||
// 1: not transient, unused
|
||||
// 2: transient, used
|
||||
// 3: not transient, used
|
||||
.block = (if (transient) @as(u2, 0) else @as(u2, 1)) + (if (used) @as(u2, 2) else @as(u2, 0)),
|
||||
};
|
||||
}
|
||||
|
||||
// Sort
|
||||
std.mem.sortUnstable(
|
||||
Candidate,
|
||||
candidates.items,
|
||||
candidates,
|
||||
{},
|
||||
struct {
|
||||
fn lessThan(
|
||||
@@ -653,17 +666,20 @@ pub const ImageStorage = struct {
|
||||
) bool {
|
||||
_ = ctx;
|
||||
|
||||
// If their usage matches, then it's based on the
|
||||
// generation stamp, which orders by transmit time.
|
||||
// (Stamps are unique but tie-break by ID anyway to
|
||||
// stay deterministic for hand-built test images.)
|
||||
if (lhs.used == rhs.used) return if (lhs.generation == rhs.generation)
|
||||
// If images mapped into different blocks, prioritize lower
|
||||
// numbered blocks.
|
||||
if (lhs.block < rhs.block) return true;
|
||||
if (lhs.block > rhs.block) return false;
|
||||
|
||||
// If images mapped to the same block, compare generations.
|
||||
return if (lhs.generation == rhs.generation)
|
||||
// If the generation is the same, use the ID to
|
||||
// prioritize evicting "earlier" images.
|
||||
lhs.id < rhs.id
|
||||
else
|
||||
// If the generation is different, prioritize evicting
|
||||
// images from earlied generations.
|
||||
lhs.generation < rhs.generation;
|
||||
|
||||
// If not used, then its a better candidate
|
||||
return !lhs.used;
|
||||
}
|
||||
}.lessThan,
|
||||
);
|
||||
@@ -675,7 +691,7 @@ pub const ImageStorage = struct {
|
||||
|
||||
// They're in order of best to evict.
|
||||
var evicted: usize = 0;
|
||||
for (candidates.items) |c| {
|
||||
for (candidates) |c| {
|
||||
// Delete all the placements for this image and the image.
|
||||
var p_it = self.placements.iterator();
|
||||
while (p_it.next()) |entry| {
|
||||
@@ -1599,3 +1615,45 @@ test "storage: no-op delete does not mark a mutation" {
|
||||
try testing.expect(s.dirty);
|
||||
try testing.expect(s.generation > gen);
|
||||
}
|
||||
|
||||
test "storage: evict unused transient image" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
var t = try terminal.Terminal.init(alloc, .{ .rows = 3, .cols = 3 });
|
||||
defer t.deinit(alloc);
|
||||
|
||||
var s: ImageStorage = .{ .total_limit = 192 };
|
||||
defer s.deinit(alloc, t.screens.active);
|
||||
|
||||
try s.addImage(alloc, .{
|
||||
.id = 1,
|
||||
.data = try alloc.dupe(u8, "*" ** 64),
|
||||
.usage = .{ .transient = false },
|
||||
});
|
||||
try s.addImage(alloc, .{
|
||||
.id = 2,
|
||||
.data = try alloc.dupe(u8, "*" ** 64),
|
||||
.usage = .{ .transient = true },
|
||||
});
|
||||
try s.addImage(alloc, .{
|
||||
.id = 3,
|
||||
.data = try alloc.dupe(u8, "*" ** 64),
|
||||
.usage = .{ .transient = true },
|
||||
});
|
||||
try s.addPlacement(
|
||||
alloc,
|
||||
2,
|
||||
1,
|
||||
.{ .location = .{ .pin = try trackPin(&t, .{ .x = 1, .y = 1 }) } },
|
||||
);
|
||||
|
||||
const gen = s.generation;
|
||||
const result = try s.evictImage(alloc, 32);
|
||||
try testing.expect(s.dirty);
|
||||
try testing.expect(s.generation > gen);
|
||||
try testing.expectEqual(true, result);
|
||||
try testing.expectEqual(2, s.images.count());
|
||||
try testing.expect(s.images.contains(1));
|
||||
try testing.expect(s.images.contains(2));
|
||||
try testing.expect(!s.images.contains(3));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user