eterm: reuse Kitty graphics transfer buffers

Reduce allocation pressure in the Kitty graphics receive path by caching and
reusing decoded, pixel-conversion, encoded-image, and frame-composition
buffers.

Reuse uniquely owned pixel storage when replacing an existing image while
preserving immutable update data still referenced by the renderer.

Add runtime SSSE3 capability detection to CPU for future optimized paths and
extend Kitty graphics tests to cover buffer reuse and strict Base64 decoding
across boundary and large payload sizes.
This commit is contained in:
Martín Lucas Golini
2026-09-03 20:18:30 -03:00
parent 0fa1f65d04
commit e45ff6af00
5 changed files with 119 additions and 4 deletions

View File

@@ -7,6 +7,8 @@ namespace EE { namespace System {
class EE_API CPU {
public:
static bool hasSSSE3();
static bool hasAVX2();
static bool hasNEON();

View File

@@ -12,6 +12,28 @@
namespace EE { namespace System {
bool CPU::hasSSSE3() {
#ifdef EE_ARCH_X86_64
static bool isSSSE3 = []() {
#if defined( COMPILER_MSVC )
int cpuInfo[4];
__cpuid( cpuInfo, 0 );
if ( cpuInfo[0] < 1 )
return false;
__cpuid( cpuInfo, 1 );
return ( cpuInfo[2] & ( 1 << 9 ) ) != 0;
#elif defined( COMPILER_GCC_CLANG )
return __builtin_cpu_supports( "ssse3" );
#else
return false;
#endif
}();
return isSSSE3;
#else
return false;
#endif
}
bool CPU::hasAVX2() {
#ifdef EE_ARCH_X86_64
static bool isAVX2 = []() {

View File

@@ -221,6 +221,10 @@ class KittyGraphicsProtocol {
std::vector<TerminalGraphicsPlaceholderCell> mPlaceholderCells;
std::vector<TerminalGraphicsUpdate> mUpdates;
PendingTransfer mPending;
std::vector<Uint8> mDecodedScratch;
std::vector<Uint8> mPixelScratch;
std::vector<Uint8> mEncodedScratch;
std::vector<Uint8> mComposeSourceScratch;
size_t mStorageBytes{ 0 };
size_t mFrameStorageBytes{ 0 };
Uint64 mCreationSerial{ 0 };

View File

@@ -62,6 +62,12 @@ bool checkedPixelBytes( Uint32 width, Uint32 height, size_t channels, size_t& re
return true;
}
void recycleBuffer( std::vector<Uint8>& buffer, std::vector<Uint8>& cache ) {
buffer.clear();
if ( buffer.capacity() > cache.capacity() )
buffer.swap( cache );
}
bool decodeBase64( std::string_view input, bool finalChunk, std::vector<Uint8>& output ) {
if ( !finalChunk && input.size() % 4 != 0 )
return false;
@@ -442,6 +448,8 @@ KittyGraphicsProtocol::handleTransmit( const KittyGraphicsCommandData& data, boo
transfer.query = query;
transfer.frame = frame;
transfer.active = true;
transfer.decodedData = std::move( mDecodedScratch );
transfer.decodedData.clear();
const Uint32 format = data.format.value_or( 32 );
if ( format == 24 || format == 32 ) {
size_t expectedBytes = 0;
@@ -473,7 +481,8 @@ KittyGraphicsHandleResult KittyGraphicsProtocol::finishTransfer( PendingTransfer
Uint32 height = data.height.value_or( 0 );
std::vector<Uint8> pixels;
if ( format == 100 ) {
std::vector<Uint8> encoded;
std::vector<Uint8> encoded = std::move( mEncodedScratch );
encoded.clear();
if ( data.compression == 'z' ) {
if ( !data.dataSize || *data.dataSize == 0 || *data.dataSize > 64 * 1024 * 1024 )
return { response( data, KittyGraphicsError::TooLarge ),
@@ -488,6 +497,8 @@ KittyGraphicsHandleResult KittyGraphicsProtocol::finishTransfer( PendingTransfer
KittyGraphicsError::DecodeFailed, false };
}
} else if ( data.compression == 0 ) {
mEncodedScratch = std::move( encoded );
mEncodedScratch.clear();
encoded = std::move( transfer.decodedData );
} else {
return { response( data, KittyGraphicsError::Unsupported ),
@@ -516,6 +527,7 @@ KittyGraphicsHandleResult KittyGraphicsProtocol::finishTransfer( PendingTransfer
return { response( data, KittyGraphicsError::DecodeFailed ),
KittyGraphicsError::DecodeFailed, false };
pixels.assign( decoded.getPixelsPtr(), decoded.getPixelsPtr() + rgbaBytes );
recycleBuffer( encoded, mEncodedScratch );
} else {
if ( width == 0 || height == 0 )
return { response( data, KittyGraphicsError::InvalidArgument ),
@@ -548,7 +560,7 @@ KittyGraphicsHandleResult KittyGraphicsProtocol::finishTransfer( PendingTransfer
}
if ( format == 24 ) {
std::vector<Uint8> rgba;
std::vector<Uint8> rgba = std::move( mPixelScratch );
rgba.resize( static_cast<size_t>( width ) * height * 4 );
for ( size_t sourceOffset = 0, destinationOffset = 0; sourceOffset < pixels.size();
sourceOffset += 3, destinationOffset += 4 ) {
@@ -557,6 +569,7 @@ KittyGraphicsHandleResult KittyGraphicsProtocol::finishTransfer( PendingTransfer
rgba[destinationOffset + 2] = pixels[sourceOffset + 2];
rgba[destinationOffset + 3] = 255;
}
recycleBuffer( pixels, mDecodedScratch );
pixels = std::move( rgba );
}
if ( transfer.frame ) {
@@ -684,6 +697,10 @@ KittyGraphicsHandleResult KittyGraphicsProtocol::finishTransfer( PendingTransfer
mUpdates.emplace_back( std::move( update ) );
++mStats.rectangleUpdates;
mPresentationDirty = true;
if ( format == 24 )
recycleBuffer( pixels, mPixelScratch );
else if ( format == 32 )
recycleBuffer( pixels, mDecodedScratch );
return { response( data, KittyGraphicsError::None, imageId ), KittyGraphicsError::None,
true };
}
@@ -731,13 +748,24 @@ KittyGraphicsHandleResult KittyGraphicsProtocol::finishTransfer( PendingTransfer
} ),
mPrimaryPlacements.end() );
}
std::shared_ptr<std::vector<Uint8>> pixelStorage;
if ( existing != mImages.end() && existing->second.rgba.unique() ) {
pixelStorage = existing->second.rgba;
pixelStorage->assign( pixels.begin(), pixels.end() );
if ( format == 24 )
recycleBuffer( pixels, mPixelScratch );
else if ( format == 32 )
recycleBuffer( pixels, mDecodedScratch );
} else {
pixelStorage = std::make_shared<std::vector<Uint8>>( std::move( pixels ) );
}
Image image;
image.size = Sizei( width, height );
image.imageNumber = data.imageNumber.value_or( 0 );
image.usageHint = data.usageHint.value_or( 0 );
image.anonymous = anonymous;
image.creationSerial = ++mCreationSerial;
image.rgba = std::make_shared<std::vector<Uint8>>( std::move( pixels ) );
image.rgba = std::move( pixelStorage );
mStorageBytes = mStorageBytes - oldBytes + image.rgba->size();
const bool replaced = existing != mImages.end();
auto inserted = mImages.insert_or_assign( imageId, std::move( image ) ).first;
@@ -1244,7 +1272,8 @@ KittyGraphicsProtocol::composeFrames( const KittyGraphicsCommandData& data ) {
return { response( data, KittyGraphicsError::InvalidArgument ),
KittyGraphicsError::InvalidArgument, false };
std::vector<Uint8> sourceCopy( static_cast<size_t>( width ) * height * 4 );
std::vector<Uint8> sourceCopy = std::move( mComposeSourceScratch );
sourceCopy.resize( static_cast<size_t>( width ) * height * 4 );
std::vector<Uint8> result( sourceCopy.size() );
const size_t imageStride = static_cast<size_t>( imageWidth ) * 4;
const size_t rowBytes = static_cast<size_t>( width ) * 4;
@@ -1281,6 +1310,7 @@ KittyGraphicsProtocol::composeFrames( const KittyGraphicsCommandData& data ) {
std::memcpy( published + offset, target + offset, 4 );
}
}
recycleBuffer( sourceCopy, mComposeSourceScratch );
TerminalGraphicsUpdate update;
update.type = destinationFrame == 1 ? TerminalGraphicsUpdateType::UpdateRegion
: TerminalGraphicsUpdateType::UpdateFrameRegion;

View File

@@ -978,6 +978,63 @@ UTEST( eterm, kitty_graphics_strict_base64_rejects_invalid_payload_bytes ) {
EXPECT_EQ( KittyGraphicsError::InvalidData, protocol.handle( "a=t,f=32,s=1,v=1;AB==" ).error );
}
UTEST( eterm, kitty_graphics_strict_base64_decodes_boundaries_and_large_payloads ) {
for ( size_t length = 1; length <= 257; ++length ) {
std::vector<Uint8> boundarySource( length );
for ( size_t i = 0; i < boundarySource.size(); ++i )
boundarySource[i] = static_cast<Uint8>( ( i * 197 + length ) & 0xFF );
std::string boundaryEncoded;
ASSERT_TRUE( Base64::encode(
std::string_view( reinterpret_cast<const char*>( boundarySource.data() ),
boundarySource.size() ),
boundaryEncoded ) );
std::vector<Uint8> boundaryDecoded( Base64::decodeSafeOutLen( boundaryEncoded.size() ) );
const size_t boundaryDecodedSize =
Base64::decode( boundaryEncoded.size(), boundaryEncoded.data(), boundaryDecoded.size(),
boundaryDecoded.data(), Base64::DecodeMode::NoWhitespaceStrict );
ASSERT_EQ( boundarySource.size(), boundaryDecodedSize );
boundaryDecoded.resize( boundaryDecodedSize );
EXPECT_TRUE( boundarySource == boundaryDecoded );
}
std::vector<Uint8> source( 65537 );
for ( size_t i = 0; i < source.size(); ++i )
source[i] = static_cast<Uint8>( ( i * 131 + i / 7 ) & 0xFF );
std::string encoded;
ASSERT_TRUE( Base64::encode(
std::string_view( reinterpret_cast<const char*>( source.data() ), source.size() ),
encoded ) );
std::vector<Uint8> decoded( Base64::decodeSafeOutLen( encoded.size() ) );
const size_t decodedSize =
Base64::decode( encoded.size(), encoded.data(), decoded.size(), decoded.data(),
Base64::DecodeMode::NoWhitespaceStrict );
ASSERT_EQ( source.size(), decodedSize );
decoded.resize( decodedSize );
EXPECT_TRUE( source == decoded );
encoded[encoded.size() / 2] = '!';
EXPECT_EQ( static_cast<size_t>( -1 ),
Base64::decode( encoded.size(), encoded.data(), decoded.size(), decoded.data(),
Base64::DecodeMode::NoWhitespaceStrict ) );
}
UTEST( eterm, kitty_graphics_reuses_unreferenced_replacement_pixel_storage ) {
KittyGraphicsProtocol protocol;
ASSERT_EQ( KittyGraphicsError::None,
protocol.handle( "a=t,f=24,s=2,v=1,i=91,q=2;AQIDBAUG" ).error );
auto updates = protocol.takeUpdates();
ASSERT_EQ( static_cast<size_t>( 1 ), updates.size() );
updates.clear();
const auto* storage = protocol.imagePixels( 91 );
ASSERT_TRUE( storage != nullptr );
ASSERT_EQ( KittyGraphicsError::None,
protocol.handle( "a=t,f=24,s=2,v=1,i=91,q=2;BwgJCgsM" ).error );
EXPECT_TRUE( storage == protocol.imagePixels( 91 ) );
const std::vector<Uint8> expected{ 7, 8, 9, 255, 10, 11, 12, 255 };
EXPECT_TRUE( expected == *protocol.imagePixels( 91 ) );
}
#if EE_PLATFORM != EE_PLATFORM_WIN && EE_PLATFORM != EE_PLATFORM_EMSCRIPTEN
UTEST( eterm, kitty_graphics_reads_and_unlinks_posix_shared_memory ) {
// mpv uses the Linux-compatible form without the optional leading slash.