From 9b9eea2343d3779f3f3d5383c5e0e9fea5da65a6 Mon Sep 17 00:00:00 2001 From: Tom Laird-McConnell Date: Fri, 9 Oct 2026 17:25:26 -0700 Subject: [PATCH] Keep at least three screens of kitty images in the registry MaxImageRegistryBytes (32 MB) is now a floor: the registry holds at least three screens' worth of pixels, computed from the terminal's size and cell pixel size. A client that tiles a full-screen picture, as Consolonia does, keeps a few screens of tiles in the terminal by id to show them again without resending; at 32 MB a 2560x1440 screen already could not hold three of them, and a tile the registry had dropped placed as nothing. Zero or less still means no limit, and a configured budget larger than three screens still applies. Co-Authored-By: Claude Fable 5.1 --- .../Graphics/KittyRegistryBudgetTests.cs | 64 +++++++++++++++++++ src/XTerm.NET/InputHandler.KittyGraphics.cs | 29 ++++++++- src/XTerm.NET/Options/TerminalOptions.cs | 7 +- 3 files changed, 96 insertions(+), 4 deletions(-) create mode 100644 src/XTerm.NET.Tests/Graphics/KittyRegistryBudgetTests.cs diff --git a/src/XTerm.NET.Tests/Graphics/KittyRegistryBudgetTests.cs b/src/XTerm.NET.Tests/Graphics/KittyRegistryBudgetTests.cs new file mode 100644 index 00000000..70d24af6 --- /dev/null +++ b/src/XTerm.NET.Tests/Graphics/KittyRegistryBudgetTests.cs @@ -0,0 +1,64 @@ +using XTerm.Options; + +namespace XTerm.Tests.Graphics; + +/// +/// The kitty image registry keeps at least three screens' worth of pixels whatever +/// says: a client that tiles full-screen pictures +/// keeps that many tiles in the terminal to show again, and one it could not hold would place as +/// nothing. +/// +public class KittyRegistryBudgetTests +{ + private const string Esc = "\u001b"; + private const string St = Esc + "\\"; + + // 30x12 cells of 2x3 pixels: one screen is 8,640 bytes, three are 25,920 + private static Terminal Fresh(long registryBytes) => new(new TerminalOptions + { + Cols = 30, + Rows = 12, + CellWidthPixels = 2, + CellHeightPixels = 3, + MaxImageRegistryBytes = registryBytes + }); + + private static string Apc(string control, string payload = "") + => payload.Length == 0 ? $"{Esc}_G{control}{St}" : $"{Esc}_G{control};{payload}{St}"; + + private static string SolidRgba(int width, int height) + => Convert.ToBase64String(new byte[width * height * 4]); + + [Fact] + public void An_image_within_three_screens_is_kept_however_small_the_configured_budget() + { + var terminal = Fresh(registryBytes: 1); + + terminal.Write(Apc("a=t,i=7,f=32,s=4,v=6,q=2", SolidRgba(4, 6))); // 96 bytes + terminal.Write(Apc("a=p,i=7,q=2")); + + Assert.True(ImageAssertions.IsImageAt(terminal, 0, 0), "the image was still in the registry to place"); + } + + [Fact] + public void An_image_beyond_three_screens_is_still_dropped_under_a_small_budget() + { + var terminal = Fresh(registryBytes: 1); + + terminal.Write(Apc("a=t,i=8,f=32,s=100,v=100,q=2", SolidRgba(100, 100))); // 40,000 bytes + terminal.Write(Apc("a=p,i=8,q=2")); + + Assert.False(ImageAssertions.IsImageAt(terminal, 0, 0), "three screens are 25,920 bytes; it did not fit"); + } + + [Fact] + public void A_configured_budget_larger_than_three_screens_still_applies() + { + var terminal = Fresh(registryBytes: 1024 * 1024); + + terminal.Write(Apc("a=t,i=9,f=32,s=100,v=100,q=2", SolidRgba(100, 100))); // 40,000 bytes + terminal.Write(Apc("a=p,i=9,q=2")); + + Assert.True(ImageAssertions.IsImageAt(terminal, 0, 0)); + } +} diff --git a/src/XTerm.NET/InputHandler.KittyGraphics.cs b/src/XTerm.NET/InputHandler.KittyGraphics.cs index 7b5bd76f..59275d62 100644 --- a/src/XTerm.NET/InputHandler.KittyGraphics.cs +++ b/src/XTerm.NET/InputHandler.KittyGraphics.cs @@ -14,6 +14,31 @@ namespace XTerm; /// public partial class InputHandler { + /// + /// What the kitty image registry may hold: , + /// or three screens of pixels when the screen is larger than that allows. + /// + /// + /// A client that tiles a full-screen picture (Consolonia does) keeps a few screens' worth of + /// tiles in the terminal, by id, to show them again without resending. A registry too small for + /// that drops tiles the client still counts on, and placing one again then shows nothing. The + /// option stays the floor, so a configured budget larger than three screens still applies; zero + /// or less still means no limit. + /// + private long KittyRegistryBudget + { + get + { + long configured = _terminal.Options.MaxImageRegistryBytes; + if (configured <= 0) + return configured; + + long screen = 4L * _terminal.Cols * _terminal.Options.CellWidthPixels + * _terminal.Rows * _terminal.Options.CellHeightPixels; + return Math.Max(configured, 3 * screen); + } + } + private void NoteAnimated(Graphics.TerminalImage image) { foreach (var known in AnimatedImages) @@ -407,7 +432,7 @@ private void CompleteKittyTransmission(Graphics.KittyTransmission transmission) // A client that sent only a number gets an id chosen here, and is told what it was. var id = command.ImageId != 0 ? command.ImageId : _kittyImages.NextAssignedId(); - _kittyImages.Store(id, image, _terminal.Options.MaxImageRegistryBytes, command.ImageNumber); + _kittyImages.Store(id, image, KittyRegistryBudget, command.ImageNumber); if (command.Action == Graphics.KittyAction.TransmitAndDisplay) PlaceKittyImage(image, command, id); @@ -482,7 +507,7 @@ private void AddKittyFrame(Graphics.KittyCommand command, byte[] pixels, int wid // Charged to the REGISTRY, not measured against this animation alone: several // animations could otherwise each grow to the whole budget, and the registry's counter // would still hold the size each image was first stored at. - if (!_kittyImages.TryCharge(frameBytes, _terminal.Options.MaxImageRegistryBytes)) + if (!_kittyImages.TryCharge(frameBytes, KittyRegistryBudget)) { ReplyToKitty(command, Graphics.KittyError.TooLarge); return; diff --git a/src/XTerm.NET/Options/TerminalOptions.cs b/src/XTerm.NET/Options/TerminalOptions.cs index d6b4e1cd..87220fe3 100644 --- a/src/XTerm.NET/Options/TerminalOptions.cs +++ b/src/XTerm.NET/Options/TerminalOptions.cs @@ -300,12 +300,15 @@ public int TabStopWidth public bool PointerShapesEnabled { get; set; } = true; /// - /// Budget for images held by client id but not currently on screen, in bytes. + /// Budget for images held by client id but not currently on screen, in bytes. A floor: the + /// terminal always holds at least three screens' worth of pixels, so a program that tiles a + /// full-screen picture can keep a few screens of tiles to show again without resending them. /// /// /// Kitty transmits a picture once and may show it later, so an image can be live with no cell /// referencing it. Cell references cannot account for those, and without a ceiling a program - /// could transmit without ever placing and never be collected. Oldest goes first. + /// could transmit without ever placing and never be collected. Oldest goes first. Zero or less + /// means no limit. /// public long MaxImageRegistryBytes { get; set; } = 32L * 1024 * 1024;