terminal: release Kitty placement pins on eviction (#13656)

Image eviction removed associated placements from storage without
deinitializing them. Pin-backed placements therefore remained registered
with the screen after eviction, allowing graphics-heavy output to
accumulate stale tracked pins.

Pass the owning screen through image insertion and eviction, and
deinitialize each placement before removing it. Cover both the released
pin and a retained image's live pin in the eviction regression test.
This commit is contained in:
Mitchell Hashimoto
2026-08-05 16:31:23 -07:00
committed by GitHub
6 changed files with 149 additions and 89 deletions

View File

@@ -990,7 +990,7 @@ test "kitty renderer ignores pending payloads and retains native placements" {
defer state.deinit(alloc);
const storage = &t.screens.active.kitty_images;
const pending = try storage.addPendingImage(io, alloc, .{
const pending = try storage.addPendingImage(io, alloc, t.screens.active, .{
.id = 1,
.width = 1,
.height = 1,
@@ -1022,7 +1022,7 @@ test "kitty renderer ignores pending payloads and retains native placements" {
// A newer pending replacement clears the renderer placement and marks
// the copied texture data for unload without deleting the native pin.
_ = try storage.addPendingImage(io, alloc, .{
_ = try storage.addPendingImage(io, alloc, t.screens.active, .{
.id = 1,
.width = 1,
.height = 1,

View File

@@ -1010,7 +1010,7 @@ test "image_get exposes pending metadata without a data pointer" {
));
const alloc = lib.alloc.default(&lib.alloc.test_allocator);
const pending = try graphics.addPendingImage(testing.io, alloc, .{
const pending = try graphics.addPendingImage(testing.io, alloc, terminal_c.zigTerminal(t).?.screens.active, .{
.id = 42,
.number = 7,
.width = 1,

View File

@@ -377,7 +377,7 @@ fn loadAndAddImage(
// Validate and store our image
var img = try loading.complete(alloc);
errdefer img.deinit(alloc);
try storage.addImage(io, alloc, img);
try storage.addImage(io, alloc, terminal.screens.active, img);
// Get our display settings
const display_ = loading.display;

View File

@@ -220,7 +220,7 @@ pub const ImageStorage = struct {
if (limit < self.total_bytes) {
const req_bytes = self.total_bytes - limit;
log.info("evicting images to lower limit, evicting={}", .{req_bytes});
if (!self.evictImage(io, alloc, req_bytes)) {
if (!self.evictImage(io, alloc, s, req_bytes)) {
log.warn("failed to evict enough images for required bytes", .{});
}
}
@@ -231,7 +231,13 @@ pub const ImageStorage = struct {
/// Add an image to the storage. This will automatically free any existing
/// image with the same ID. Prefer addPendingImage for pending data so the
/// caller receives a completion token.
pub fn addImage(self: *ImageStorage, io: std.Io, alloc: Allocator, img: Image) Allocator.Error!void {
pub fn addImage(
self: *ImageStorage,
io: std.Io,
alloc: Allocator,
s: *terminal.Screen,
img: Image,
) Allocator.Error!void {
const new_len = img.data.len();
// If the image itself is over the limit, then error immediately
@@ -258,7 +264,7 @@ pub const ImageStorage = struct {
if (total_bytes > self.total_limit) {
const req_bytes = total_bytes - self.total_limit;
log.info("evicting images to make space for {} bytes", .{req_bytes});
if (!self.evictImageExcept(io, alloc, req_bytes, img.id)) {
if (!self.evictImageExcept(io, alloc, s, req_bytes, img.id)) {
log.warn("failed to evict enough images for required bytes", .{});
return error.OutOfMemory;
}
@@ -294,10 +300,11 @@ pub const ImageStorage = struct {
self: *ImageStorage,
io: std.Io,
alloc: Allocator,
s: *terminal.Screen,
img: Image,
) Allocator.Error!PendingImage {
assert(img.data == .pending);
try self.addImage(io, alloc, img);
try self.addImage(io, alloc, s, img);
const stored = self.images.get(img.id).?;
return .{ .id = stored.id, .generation = stored.generation };
}
@@ -713,8 +720,14 @@ pub const ImageStorage = struct {
///
/// This will evict as many images as necessary to make space for
/// req bytes.
fn evictImage(self: *ImageStorage, io: std.Io, alloc: Allocator, req: usize) bool {
return self.evictImageExcept(io, alloc, req, null);
fn evictImage(
self: *ImageStorage,
io: std.Io,
alloc: Allocator,
s: *terminal.Screen,
req: usize,
) bool {
return self.evictImageExcept(io, alloc, s, req, null);
}
/// Evict images while preserving `exclude_id`, used when replacing an
@@ -723,6 +736,7 @@ pub const ImageStorage = struct {
self: *ImageStorage,
io: std.Io,
alloc: Allocator,
s: *terminal.Screen,
req: usize,
exclude_id: ?u32,
) bool {
@@ -780,6 +794,7 @@ pub const ImageStorage = struct {
var p_it = self.placements.iterator();
while (p_it.next()) |entry| {
if (entry.key_ptr.image_id == c.id) {
entry.value_ptr.deinit(s);
self.removePlacementByPtr(entry.key_ptr);
}
}
@@ -1032,8 +1047,8 @@ test "storage: add placement with zero placement id" {
var s: ImageStorage = .{};
defer s.deinit(alloc, t.screens.active);
try s.addImage(io, alloc, .{ .id = 1, .width = 50, .height = 50 });
try s.addImage(io, alloc, .{ .id = 2, .width = 25, .height = 25 });
try s.addImage(io, alloc, t.screens.active, .{ .id = 1, .width = 50, .height = 50 });
try s.addImage(io, alloc, t.screens.active, .{ .id = 2, .width = 25, .height = 25 });
try s.addPlacement(io, alloc, t.screens.active, 1, 0, .{ .location = .{ .pin = try trackPin(&t, .{ .x = 25, .y = 25 }) } });
try s.addPlacement(io, alloc, t.screens.active, 1, 0, .{ .location = .{ .pin = try trackPin(&t, .{ .x = 25, .y = 25 }) } });
@@ -1060,7 +1075,7 @@ test "storage: replacing placement releases tracked pin" {
var s: ImageStorage = .{};
defer s.deinit(alloc, t.screens.active);
try s.addImage(io, alloc, .{ .id = 1 });
try s.addImage(io, alloc, t.screens.active, .{ .id = 1 });
const tracked = t.screens.active.pages.countTrackedPins();
try s.addPlacement(io, alloc, t.screens.active, 1, 1, .{
@@ -1098,7 +1113,7 @@ test "storage: adding placement reclaims garbage placements" {
var s: ImageStorage = .{};
defer s.deinit(alloc, t.screens.active);
try s.addImage(io, alloc, .{ .id = 1 });
try s.addImage(io, alloc, t.screens.active, .{ .id = 1 });
const tracked = t.screens.active.pages.countTrackedPins();
const old_pin = try trackPin(&t, .{ .x = 0, .y = 0 });
@@ -1135,7 +1150,7 @@ test "storage: placement count limit permits replacement" {
var s: ImageStorage = .{};
defer s.deinit(alloc, t.screens.active);
try s.addImage(io, alloc, .{ .id = 1 });
try s.addImage(io, alloc, t.screens.active, .{ .id = 1 });
try s.addPlacement(io, alloc, t.screens.active, 1, 1, .{ .location = .{ .virtual = {} } });
const img = s.images.getPtr(1).?;
@@ -1158,9 +1173,9 @@ test "storage: delete all placements and images" {
var s: ImageStorage = .{};
defer s.deinit(alloc, t.screens.active);
try s.addImage(io, alloc, .{ .id = 1 });
try s.addImage(io, alloc, .{ .id = 2 });
try s.addImage(io, alloc, .{ .id = 3 });
try s.addImage(io, alloc, t.screens.active, .{ .id = 1 });
try s.addImage(io, alloc, t.screens.active, .{ .id = 2 });
try s.addImage(io, alloc, t.screens.active, .{ .id = 3 });
try s.addPlacement(io, alloc, t.screens.active, 1, 1, .{ .location = .{ .pin = try trackPin(&t, .{ .x = 1, .y = 1 }) } });
try s.addPlacement(io, alloc, t.screens.active, 2, 1, .{ .location = .{ .pin = try trackPin(&t, .{ .x = 1, .y = 1 }) } });
@@ -1183,9 +1198,9 @@ test "storage: delete all placements and images preserves limit" {
var s: ImageStorage = .{};
defer s.deinit(alloc, t.screens.active);
s.total_limit = 5000;
try s.addImage(io, alloc, .{ .id = 1 });
try s.addImage(io, alloc, .{ .id = 2 });
try s.addImage(io, alloc, .{ .id = 3 });
try s.addImage(io, alloc, t.screens.active, .{ .id = 1 });
try s.addImage(io, alloc, t.screens.active, .{ .id = 2 });
try s.addImage(io, alloc, t.screens.active, .{ .id = 3 });
try s.addPlacement(io, alloc, t.screens.active, 1, 1, .{ .location = .{ .pin = try trackPin(&t, .{ .x = 1, .y = 1 }) } });
try s.addPlacement(io, alloc, t.screens.active, 2, 1, .{ .location = .{ .pin = try trackPin(&t, .{ .x = 1, .y = 1 }) } });
@@ -1208,9 +1223,9 @@ test "storage: delete all placements" {
var s: ImageStorage = .{};
defer s.deinit(alloc, t.screens.active);
try s.addImage(io, alloc, .{ .id = 1 });
try s.addImage(io, alloc, .{ .id = 2 });
try s.addImage(io, alloc, .{ .id = 3 });
try s.addImage(io, alloc, t.screens.active, .{ .id = 1 });
try s.addImage(io, alloc, t.screens.active, .{ .id = 2 });
try s.addImage(io, alloc, t.screens.active, .{ .id = 3 });
try s.addPlacement(io, alloc, t.screens.active, 1, 1, .{ .location = .{ .pin = try trackPin(&t, .{ .x = 1, .y = 1 }) } });
try s.addPlacement(io, alloc, t.screens.active, 2, 1, .{ .location = .{ .pin = try trackPin(&t, .{ .x = 1, .y = 1 }) } });
@@ -1232,9 +1247,9 @@ test "storage: delete all placements by image id" {
var s: ImageStorage = .{};
defer s.deinit(alloc, t.screens.active);
try s.addImage(io, alloc, .{ .id = 1 });
try s.addImage(io, alloc, .{ .id = 2 });
try s.addImage(io, alloc, .{ .id = 3 });
try s.addImage(io, alloc, t.screens.active, .{ .id = 1 });
try s.addImage(io, alloc, t.screens.active, .{ .id = 2 });
try s.addImage(io, alloc, t.screens.active, .{ .id = 3 });
try s.addPlacement(io, alloc, t.screens.active, 1, 1, .{ .location = .{ .pin = try trackPin(&t, .{ .x = 1, .y = 1 }) } });
try s.addPlacement(io, alloc, t.screens.active, 2, 1, .{ .location = .{ .pin = try trackPin(&t, .{ .x = 1, .y = 1 }) } });
@@ -1256,9 +1271,9 @@ test "storage: delete all placements by image id and unused images" {
var s: ImageStorage = .{};
defer s.deinit(alloc, t.screens.active);
try s.addImage(io, alloc, .{ .id = 1 });
try s.addImage(io, alloc, .{ .id = 2 });
try s.addImage(io, alloc, .{ .id = 3 });
try s.addImage(io, alloc, t.screens.active, .{ .id = 1 });
try s.addImage(io, alloc, t.screens.active, .{ .id = 2 });
try s.addImage(io, alloc, t.screens.active, .{ .id = 3 });
try s.addPlacement(io, alloc, t.screens.active, 1, 1, .{ .location = .{ .pin = try trackPin(&t, .{ .x = 1, .y = 1 }) } });
try s.addPlacement(io, alloc, t.screens.active, 2, 1, .{ .location = .{ .pin = try trackPin(&t, .{ .x = 1, .y = 1 }) } });
@@ -1280,9 +1295,9 @@ test "storage: delete placement by specific id" {
var s: ImageStorage = .{};
defer s.deinit(alloc, t.screens.active);
try s.addImage(io, alloc, .{ .id = 1 });
try s.addImage(io, alloc, .{ .id = 2 });
try s.addImage(io, alloc, .{ .id = 3 });
try s.addImage(io, alloc, t.screens.active, .{ .id = 1 });
try s.addImage(io, alloc, t.screens.active, .{ .id = 2 });
try s.addImage(io, alloc, t.screens.active, .{ .id = 3 });
try s.addPlacement(io, alloc, t.screens.active, 1, 1, .{ .location = .{ .pin = try trackPin(&t, .{ .x = 1, .y = 1 }) } });
try s.addPlacement(io, alloc, t.screens.active, 1, 2, .{ .location = .{ .pin = try trackPin(&t, .{ .x = 1, .y = 1 }) } });
try s.addPlacement(io, alloc, t.screens.active, 2, 1, .{ .location = .{ .pin = try trackPin(&t, .{ .x = 1, .y = 1 }) } });
@@ -1311,8 +1326,8 @@ test "storage: delete intersecting cursor" {
var s: ImageStorage = .{};
defer s.deinit(alloc, t.screens.active);
try s.addImage(io, alloc, .{ .id = 1, .width = 50, .height = 50 });
try s.addImage(io, alloc, .{ .id = 2, .width = 25, .height = 25 });
try s.addImage(io, alloc, t.screens.active, .{ .id = 1, .width = 50, .height = 50 });
try s.addImage(io, alloc, t.screens.active, .{ .id = 2, .width = 25, .height = 25 });
try s.addPlacement(io, alloc, t.screens.active, 1, 1, .{ .location = .{ .pin = try trackPin(&t, .{ .x = 0, .y = 0 }) } });
try s.addPlacement(io, alloc, t.screens.active, 1, 2, .{ .location = .{ .pin = try trackPin(&t, .{ .x = 25, .y = 25 }) } });
@@ -1343,7 +1358,7 @@ test "storage: delete intersecting cursor checks interior row column" {
var s: ImageStorage = .{};
defer s.deinit(alloc, t.screens.active);
try s.addImage(io, alloc, .{ .id = 1, .width = 10, .height = 10 });
try s.addImage(io, alloc, t.screens.active, .{ .id = 1, .width = 10, .height = 10 });
try s.addPlacement(io, alloc, t.screens.active, 1, 1, .{ .location = .{ .pin = try trackPin(&t, .{ .x = 0, .y = 0 }) } });
try s.addPlacement(io, alloc, t.screens.active, 1, 2, .{ .location = .{ .pin = try trackPin(&t, .{ .x = 20, .y = 0 }) } });
@@ -1370,7 +1385,7 @@ test "storage: delete intersecting cell checks interior row column" {
var s: ImageStorage = .{};
defer s.deinit(alloc, t.screens.active);
try s.addImage(io, alloc, .{ .id = 1, .width = 10, .height = 10 });
try s.addImage(io, alloc, t.screens.active, .{ .id = 1, .width = 10, .height = 10 });
try s.addPlacement(io, alloc, t.screens.active, 1, 1, .{ .location = .{ .pin = try trackPin(&t, .{ .x = 0, .y = 0 }) } });
try s.addPlacement(io, alloc, t.screens.active, 1, 2, .{ .location = .{ .pin = try trackPin(&t, .{ .x = 20, .y = 0 }) } });
@@ -1399,8 +1414,8 @@ test "storage: delete intersecting cursor plus unused" {
var s: ImageStorage = .{};
defer s.deinit(alloc, t.screens.active);
try s.addImage(io, alloc, .{ .id = 1, .width = 50, .height = 50 });
try s.addImage(io, alloc, .{ .id = 2, .width = 25, .height = 25 });
try s.addImage(io, alloc, t.screens.active, .{ .id = 1, .width = 50, .height = 50 });
try s.addImage(io, alloc, t.screens.active, .{ .id = 2, .width = 25, .height = 25 });
try s.addPlacement(io, alloc, t.screens.active, 1, 1, .{ .location = .{ .pin = try trackPin(&t, .{ .x = 0, .y = 0 }) } });
try s.addPlacement(io, alloc, t.screens.active, 1, 2, .{ .location = .{ .pin = try trackPin(&t, .{ .x = 25, .y = 25 }) } });
@@ -1432,8 +1447,8 @@ test "storage: delete intersecting cursor hits multiple" {
var s: ImageStorage = .{};
defer s.deinit(alloc, t.screens.active);
try s.addImage(io, alloc, .{ .id = 1, .width = 50, .height = 50 });
try s.addImage(io, alloc, .{ .id = 2, .width = 25, .height = 25 });
try s.addImage(io, alloc, t.screens.active, .{ .id = 1, .width = 50, .height = 50 });
try s.addImage(io, alloc, t.screens.active, .{ .id = 2, .width = 25, .height = 25 });
try s.addPlacement(io, alloc, t.screens.active, 1, 1, .{ .location = .{ .pin = try trackPin(&t, .{ .x = 0, .y = 0 }) } });
try s.addPlacement(io, alloc, t.screens.active, 1, 2, .{ .location = .{ .pin = try trackPin(&t, .{ .x = 25, .y = 25 }) } });
@@ -1459,8 +1474,8 @@ test "storage: delete by column" {
var s: ImageStorage = .{};
defer s.deinit(alloc, t.screens.active);
try s.addImage(io, alloc, .{ .id = 1, .width = 50, .height = 50 });
try s.addImage(io, alloc, .{ .id = 2, .width = 25, .height = 25 });
try s.addImage(io, alloc, t.screens.active, .{ .id = 1, .width = 50, .height = 50 });
try s.addImage(io, alloc, t.screens.active, .{ .id = 2, .width = 25, .height = 25 });
try s.addPlacement(io, alloc, t.screens.active, 1, 1, .{ .location = .{ .pin = try trackPin(&t, .{ .x = 0, .y = 0 }) } });
try s.addPlacement(io, alloc, t.screens.active, 1, 2, .{ .location = .{ .pin = try trackPin(&t, .{ .x = 25, .y = 25 }) } });
@@ -1492,7 +1507,7 @@ test "storage: delete by column 1x1" {
var s: ImageStorage = .{};
defer s.deinit(alloc, t.screens.active);
try s.addImage(io, alloc, .{ .id = 1, .width = 1, .height = 1 });
try s.addImage(io, alloc, t.screens.active, .{ .id = 1, .width = 1, .height = 1 });
try s.addPlacement(io, alloc, t.screens.active, 1, 1, .{ .location = .{ .pin = try trackPin(&t, .{ .x = 0, .y = 0 }) } });
try s.addPlacement(io, alloc, t.screens.active, 1, 2, .{ .location = .{ .pin = try trackPin(&t, .{ .x = 1, .y = 0 }) } });
try s.addPlacement(io, alloc, t.screens.active, 1, 3, .{ .location = .{ .pin = try trackPin(&t, .{ .x = 2, .y = 0 }) } });
@@ -1527,8 +1542,8 @@ test "storage: delete by row" {
var s: ImageStorage = .{};
defer s.deinit(alloc, t.screens.active);
try s.addImage(io, alloc, .{ .id = 1, .width = 50, .height = 50 });
try s.addImage(io, alloc, .{ .id = 2, .width = 25, .height = 25 });
try s.addImage(io, alloc, t.screens.active, .{ .id = 1, .width = 50, .height = 50 });
try s.addImage(io, alloc, t.screens.active, .{ .id = 2, .width = 25, .height = 25 });
try s.addPlacement(io, alloc, t.screens.active, 1, 1, .{ .location = .{ .pin = try trackPin(&t, .{ .x = 0, .y = 0 }) } });
try s.addPlacement(io, alloc, t.screens.active, 1, 2, .{ .location = .{ .pin = try trackPin(&t, .{ .x = 25, .y = 25 }) } });
@@ -1560,7 +1575,7 @@ test "storage: delete by row 1x1" {
var s: ImageStorage = .{};
defer s.deinit(alloc, t.screens.active);
try s.addImage(io, alloc, .{ .id = 1, .width = 1, .height = 1 });
try s.addImage(io, alloc, t.screens.active, .{ .id = 1, .width = 1, .height = 1 });
try s.addPlacement(io, alloc, t.screens.active, 1, 1, .{ .location = .{ .pin = try trackPin(&t, .{ .y = 0 }) } });
try s.addPlacement(io, alloc, t.screens.active, 1, 2, .{ .location = .{ .pin = try trackPin(&t, .{ .y = 1 }) } });
try s.addPlacement(io, alloc, t.screens.active, 1, 3, .{ .location = .{ .pin = try trackPin(&t, .{ .y = 2 }) } });
@@ -1593,9 +1608,9 @@ test "storage: delete images by range 1" {
var s: ImageStorage = .{};
defer s.deinit(alloc, t.screens.active);
try s.addImage(io, alloc, .{ .id = 1 });
try s.addImage(io, alloc, .{ .id = 2 });
try s.addImage(io, alloc, .{ .id = 3 });
try s.addImage(io, alloc, t.screens.active, .{ .id = 1 });
try s.addImage(io, alloc, t.screens.active, .{ .id = 2 });
try s.addImage(io, alloc, t.screens.active, .{ .id = 3 });
try s.addPlacement(io, alloc, t.screens.active, 1, 1, .{ .location = .{ .pin = try trackPin(&t, .{ .x = 1, .y = 1 }) } });
try s.addPlacement(io, alloc, t.screens.active, 2, 1, .{ .location = .{ .pin = try trackPin(&t, .{ .x = 1, .y = 1 }) } });
try testing.expectEqual(@as(usize, 3), s.images.count());
@@ -1619,9 +1634,9 @@ test "storage: delete images by range 2" {
var s: ImageStorage = .{};
defer s.deinit(alloc, t.screens.active);
try s.addImage(io, alloc, .{ .id = 1 });
try s.addImage(io, alloc, .{ .id = 2 });
try s.addImage(io, alloc, .{ .id = 3 });
try s.addImage(io, alloc, t.screens.active, .{ .id = 1 });
try s.addImage(io, alloc, t.screens.active, .{ .id = 2 });
try s.addImage(io, alloc, t.screens.active, .{ .id = 3 });
try s.addPlacement(io, alloc, t.screens.active, 1, 1, .{ .location = .{ .pin = try trackPin(&t, .{ .x = 1, .y = 1 }) } });
try s.addPlacement(io, alloc, t.screens.active, 2, 1, .{ .location = .{ .pin = try trackPin(&t, .{ .x = 1, .y = 1 }) } });
try testing.expectEqual(@as(usize, 3), s.images.count());
@@ -1645,9 +1660,9 @@ test "storage: delete images by range 3" {
var s: ImageStorage = .{};
defer s.deinit(alloc, t.screens.active);
try s.addImage(io, alloc, .{ .id = 1 });
try s.addImage(io, alloc, .{ .id = 2 });
try s.addImage(io, alloc, .{ .id = 3 });
try s.addImage(io, alloc, t.screens.active, .{ .id = 1 });
try s.addImage(io, alloc, t.screens.active, .{ .id = 2 });
try s.addImage(io, alloc, t.screens.active, .{ .id = 3 });
try s.addPlacement(io, alloc, t.screens.active, 1, 1, .{ .location = .{ .pin = try trackPin(&t, .{ .x = 1, .y = 1 }) } });
try s.addPlacement(io, alloc, t.screens.active, 2, 1, .{ .location = .{ .pin = try trackPin(&t, .{ .x = 1, .y = 1 }) } });
try s.addPlacement(io, alloc, t.screens.active, 3, 1, .{ .location = .{ .pin = try trackPin(&t, .{ .x = 1, .y = 1 }) } });
@@ -1680,9 +1695,9 @@ test "storage: delete images by range 4" {
var s: ImageStorage = .{};
defer s.deinit(alloc, t.screens.active);
try s.addImage(io, alloc, .{ .id = 1 });
try s.addImage(io, alloc, .{ .id = 2 });
try s.addImage(io, alloc, .{ .id = 3 });
try s.addImage(io, alloc, t.screens.active, .{ .id = 1 });
try s.addImage(io, alloc, t.screens.active, .{ .id = 2 });
try s.addImage(io, alloc, t.screens.active, .{ .id = 3 });
try s.addPlacement(io, alloc, t.screens.active, 1, 1, .{ .location = .{ .pin = try trackPin(&t, .{ .x = 1, .y = 1 }) } });
try s.addPlacement(io, alloc, t.screens.active, 2, 1, .{ .location = .{ .pin = try trackPin(&t, .{ .x = 1, .y = 1 }) } });
try s.addPlacement(io, alloc, t.screens.active, 3, 1, .{ .location = .{ .pin = try trackPin(&t, .{ .x = 1, .y = 1 }) } });
@@ -1872,7 +1887,7 @@ test "storage: generation stamps on image add and replace" {
// Fresh storage has generation zero (never mutated).
try testing.expectEqual(@as(u64, 0), s.generation);
try s.addImage(io, alloc, .{ .id = 1, .width = 1, .height = 1 });
try s.addImage(io, alloc, t.screens.active, .{ .id = 1, .width = 1, .height = 1 });
const gen1 = s.generation;
try testing.expect(gen1 > 0);
@@ -1880,7 +1895,7 @@ test "storage: generation stamps on image add and replace" {
try testing.expectEqual(gen1, img1.generation);
// A second image gets a strictly greater stamp.
try s.addImage(io, alloc, .{ .id = 2, .width = 1, .height = 1 });
try s.addImage(io, alloc, t.screens.active, .{ .id = 2, .width = 1, .height = 1 });
const gen2 = s.generation;
try testing.expect(gen2 > gen1);
try testing.expectEqual(gen2, s.imageById(2).?.generation);
@@ -1888,7 +1903,7 @@ test "storage: generation stamps on image add and replace" {
// Retransmitting the same image ID (identical dimensions) gets a
// fresh stamp: this is what makes same-sized retransmissions
// detectable by renderers.
try s.addImage(io, alloc, .{ .id = 1, .width = 1, .height = 1 });
try s.addImage(io, alloc, t.screens.active, .{ .id = 1, .width = 1, .height = 1 });
const gen3 = s.generation;
try testing.expect(gen3 > gen2);
try testing.expectEqual(gen3, s.imageById(1).?.generation);
@@ -1906,7 +1921,7 @@ test "storage: generation bumps on placement and delete" {
var s: ImageStorage = .{};
defer s.deinit(alloc, t.screens.active);
try s.addImage(io, alloc, .{ .id = 1 });
try s.addImage(io, alloc, t.screens.active, .{ .id = 1 });
const gen_add = s.generation;
try s.addPlacement(io, alloc, t.screens.active, 1, 1, .{ .location = .{ .pin = try trackPin(&t, .{ .x = 1, .y = 1 }) } });
@@ -1933,7 +1948,7 @@ test "storage: generation bumps when setLimit evicts or disables" {
defer s.deinit(alloc, t.screens.active);
const data = try alloc.dupe(u8, "1234");
try s.addImage(io, alloc, .{
try s.addImage(io, alloc, t.screens.active, .{
.id = 1,
.width = 1,
.height = 1,
@@ -1968,12 +1983,12 @@ test "storage: imageByNumber returns most recently transmitted" {
// Two images sharing a number: the newest transmission wins,
// regardless of insertion order or clock resolution.
try s.addImage(io, alloc, .{ .id = 1, .number = 7 });
try s.addImage(io, alloc, .{ .id = 2, .number = 7 });
try s.addImage(io, alloc, t.screens.active, .{ .id = 1, .number = 7 });
try s.addImage(io, alloc, t.screens.active, .{ .id = 2, .number = 7 });
try testing.expectEqual(@as(u32, 2), s.imageByNumber(7).?.id);
// Retransmit the first: it becomes the newest.
try s.addImage(io, alloc, .{ .id = 1, .number = 7 });
try s.addImage(io, alloc, t.screens.active, .{ .id = 1, .number = 7 });
try testing.expectEqual(@as(u32, 1), s.imageByNumber(7).?.id);
}
@@ -2002,7 +2017,7 @@ test "storage: no-op delete does not mark a mutation" {
try testing.expectEqual(@as(u64, 0), s.generation);
// Same for a delete that matches nothing.
try s.addImage(io, alloc, .{ .id = 1 });
try s.addImage(io, alloc, t.screens.active, .{ .id = 1 });
try s.addPlacement(io, alloc, t.screens.active, 1, 1, .{ .location = .{ .pin = try trackPin(&t, .{ .x = 1, .y = 1 }) } });
const gen = s.generation;
s.dirty = false;
@@ -2026,17 +2041,17 @@ test "storage: evicts images in priority order" {
var s: ImageStorage = .{ .total_limit = 192 };
defer s.deinit(alloc, t.screens.active);
try s.addImage(io, alloc, .{
try s.addImage(io, alloc, t.screens.active, .{
.id = 1,
.data = .{ .complete = try alloc.dupe(u8, "*" ** 64) },
.metadata = .{ .transient = false },
});
try s.addImage(io, alloc, .{
try s.addImage(io, alloc, t.screens.active, .{
.id = 2,
.data = .{ .complete = try alloc.dupe(u8, "*" ** 64) },
.metadata = .{ .transient = true },
});
try s.addImage(io, alloc, .{
try s.addImage(io, alloc, t.screens.active, .{
.id = 3,
.data = .{ .complete = try alloc.dupe(u8, "*" ** 64) },
.metadata = .{ .transient = true },
@@ -2051,7 +2066,7 @@ test "storage: evicts images in priority order" {
);
const gen = s.generation;
const result = s.evictImage(io, alloc, 96);
const result = s.evictImage(io, alloc, t.screens.active, 96);
try testing.expect(s.dirty);
try testing.expect(s.generation > gen);
try testing.expectEqual(true, result);
@@ -2061,6 +2076,50 @@ test "storage: evicts images in priority order" {
try testing.expect(!s.images.contains(3));
}
test "storage: eviction releases placement pins" {
const testing = std.testing;
const io = testing.io;
const alloc = testing.allocator;
var t = try terminal.Terminal.init(io, alloc, .{ .rows = 3, .cols = 3 });
defer t.deinit(alloc);
var s: ImageStorage = .{ .total_limit = 128 };
defer s.deinit(alloc, t.screens.active);
const tracked = t.screens.active.pages.countTrackedPins();
try s.addImage(io, alloc, t.screens.active, .{
.id = 1,
.data = .{ .complete = try alloc.dupe(u8, "*" ** 64) },
});
try s.addPlacement(io, alloc, t.screens.active, 1, 1, .{
.location = .{ .pin = try trackPin(&t, .{ .x = 0, .y = 0 }) },
});
try s.addImage(io, alloc, t.screens.active, .{
.id = 2,
.data = .{ .complete = try alloc.dupe(u8, "*" ** 64) },
});
try s.addPlacement(io, alloc, t.screens.active, 2, 1, .{
.location = .{ .pin = try trackPin(&t, .{ .x = 1, .y = 1 }) },
});
try testing.expectEqual(tracked + 2, t.screens.active.pages.countTrackedPins());
// Adding a third image evicts the oldest equally-ranked image and its
// placement. The newer image's placement and tracked pin remain intact.
try s.addImage(io, alloc, t.screens.active, .{
.id = 3,
.data = .{ .complete = try alloc.dupe(u8, "*" ** 64) },
});
try testing.expect(!s.images.contains(1));
try testing.expect(s.images.contains(2));
try testing.expect(s.images.contains(3));
try testing.expectEqual(@as(usize, 1), s.placements.count());
try testing.expect(s.placements.contains(.{
.image_id = 2,
.placement_id = .{ .tag = .external, .id = 1 },
}));
try testing.expectEqual(tracked + 1, t.screens.active.pages.countTrackedPins());
}
test "storage: pending image completes once and preserves age" {
const testing = std.testing;
const io = testing.io;
@@ -2071,7 +2130,7 @@ test "storage: pending image completes once and preserves age" {
var s: ImageStorage = .{ .total_limit = 16 };
defer s.deinit(alloc, t.screens.active);
const pending = try s.addPendingImage(io, alloc, .{
const pending = try s.addPendingImage(io, alloc, t.screens.active, .{
.id = 1,
.number = 7,
.width = 2,
@@ -2119,7 +2178,7 @@ test "storage: stale pending completion loses to delete replacement and eviction
var s: ImageStorage = .{ .total_limit = 8 };
defer s.deinit(alloc, t.screens.active);
const deleted = try s.addPendingImage(io, alloc, .{
const deleted = try s.addPendingImage(io, alloc, t.screens.active, .{
.id = 1,
.data = .{ .pending = 4 },
});
@@ -2129,11 +2188,11 @@ test "storage: stale pending completion loses to delete replacement and eviction
try testing.expect(!deleted_completed);
defer alloc.free(deleted_data);
const replaced = try s.addPendingImage(io, alloc, .{
const replaced = try s.addPendingImage(io, alloc, t.screens.active, .{
.id = 2,
.data = .{ .pending = 4 },
});
try s.addImage(io, alloc, .{
try s.addImage(io, alloc, t.screens.active, .{
.id = 2,
.data = .{ .complete = try alloc.dupe(u8, "live") },
});
@@ -2143,11 +2202,11 @@ test "storage: stale pending completion loses to delete replacement and eviction
defer alloc.free(replaced_data);
try testing.expectEqualStrings("live", s.imageById(2).?.data.bytes().?);
const evicted = try s.addPendingImage(io, alloc, .{
const evicted = try s.addPendingImage(io, alloc, t.screens.active, .{
.id = 3,
.data = .{ .pending = 4 },
});
try s.addImage(io, alloc, .{
try s.addImage(io, alloc, t.screens.active, .{
.id = 4,
.data = .{ .complete = try alloc.dupe(u8, "12345678") },
});
@@ -2168,7 +2227,7 @@ test "storage: replacement reuses pending reservation and preserves placements"
var s: ImageStorage = .{ .total_limit = 12 };
defer s.deinit(alloc, t.screens.active);
const pending = try s.addPendingImage(io, alloc, .{
const pending = try s.addPendingImage(io, alloc, t.screens.active, .{
.id = 1,
.width = 2,
.height = 1,
@@ -2178,12 +2237,12 @@ test "storage: replacement reuses pending reservation and preserves placements"
try s.addPlacement(io, alloc, t.screens.active, 1, 1, .{
.location = .{ .pin = try trackPin(&t, .{ .x = 1, .y = 1 }) },
});
try s.addImage(io, alloc, .{
try s.addImage(io, alloc, t.screens.active, .{
.id = 2,
.data = .{ .complete = try alloc.dupe(u8, "keep") },
});
try s.addImage(io, alloc, .{
try s.addImage(io, alloc, t.screens.active, .{
.id = 1,
.width = 2,
.height = 1,
@@ -2202,7 +2261,7 @@ test "storage: replacement reuses pending reservation and preserves placements"
// Growing the replacement requires eviction, but the replacement ID and
// its placement are excluded. The other image supplies the needed bytes.
try s.addImage(io, alloc, .{
try s.addImage(io, alloc, t.screens.active, .{
.id = 1,
.data = .{ .complete = try alloc.dupe(u8, "1234567890") },
});
@@ -2227,17 +2286,17 @@ test "storage: pending images share exact eviction ordering" {
var s: ImageStorage = .{ .total_limit = 192 };
defer s.deinit(alloc, t.screens.active);
_ = try s.addPendingImage(io, alloc, .{
_ = try s.addPendingImage(io, alloc, t.screens.active, .{
.id = 1,
.data = .{ .pending = 64 },
.metadata = .{ .transient = false },
});
_ = try s.addPendingImage(io, alloc, .{
_ = try s.addPendingImage(io, alloc, t.screens.active, .{
.id = 2,
.data = .{ .pending = 64 },
.metadata = .{ .transient = true },
});
try s.addImage(io, alloc, .{
try s.addImage(io, alloc, t.screens.active, .{
.id = 3,
.data = .{ .complete = try alloc.dupe(u8, "*" ** 64) },
.metadata = .{ .transient = true },
@@ -2247,7 +2306,7 @@ test "storage: pending images share exact eviction ordering" {
});
// ID 3 is transient and unused, so one exact-size eviction is enough.
try testing.expect(s.evictImage(io, alloc, 64));
try testing.expect(s.evictImage(io, alloc, t.screens.active, 64));
try testing.expect(s.images.contains(1));
try testing.expect(s.images.contains(2));
try testing.expect(!s.images.contains(3));

View File

@@ -1195,7 +1195,7 @@ test "unicode render placement: dog 4x2" {
defer s.deinit(alloc, t.screens.active);
const image: Image = .{ .id = 1, .width = 500, .height = 306 };
try s.addImage(io, alloc, image);
try s.addImage(io, alloc, t.screens.active, image);
try s.addPlacement(io, alloc, t.screens.active, 1, 0, .{
.location = .{ .virtual = {} },
.columns = 4,
@@ -1263,7 +1263,7 @@ test "unicode render placement: dog 2x2 with blank cells" {
defer s.deinit(alloc, t.screens.active);
const image: Image = .{ .id = 1, .width = 500, .height = 306 };
try s.addImage(io, alloc, image);
try s.addImage(io, alloc, t.screens.active, image);
try s.addPlacement(io, alloc, t.screens.active, 1, 0, .{
.location = .{ .virtual = {} },
.columns = 2,
@@ -1330,7 +1330,7 @@ test "unicode render placement: dog 1x1" {
defer s.deinit(alloc, t.screens.active);
const image: Image = .{ .id = 1, .width = 500, .height = 306 };
try s.addImage(io, alloc, image);
try s.addImage(io, alloc, t.screens.active, image);
try s.addPlacement(io, alloc, t.screens.active, 1, 0, .{
.location = .{ .virtual = {} },
.columns = 1,

View File

@@ -1226,6 +1226,7 @@ test "complete snapshot preserves Kitty virtual placeholders" {
try t.screens.active.kitty_images.addImage(
testing.io,
testing.allocator,
t.screens.active,
.{ .id = 1 },
);
try t.screens.active.kitty_images.addPlacement(