From e45ff6af007a8cb637ec23f46a83330d62b4fd6d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Lucas=20Golini?= Date: Thu, 3 Sep 2026 20:18:30 -0300 Subject: [PATCH] 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. --- include/eepp/system/cpu.hpp | 2 + src/eepp/system/cpu.cpp | 22 +++++++ .../eterm/terminal/kittygraphicsprotocol.hpp | 4 ++ .../eterm/terminal/kittygraphicsprotocol.cpp | 38 +++++++++++-- src/tests/unit_tests/eterm_tests.cpp | 57 +++++++++++++++++++ 5 files changed, 119 insertions(+), 4 deletions(-) diff --git a/include/eepp/system/cpu.hpp b/include/eepp/system/cpu.hpp index 5f5856832..194dacf82 100644 --- a/include/eepp/system/cpu.hpp +++ b/include/eepp/system/cpu.hpp @@ -7,6 +7,8 @@ namespace EE { namespace System { class EE_API CPU { public: + static bool hasSSSE3(); + static bool hasAVX2(); static bool hasNEON(); diff --git a/src/eepp/system/cpu.cpp b/src/eepp/system/cpu.cpp index 04b2a9a19..756aa0a1a 100644 --- a/src/eepp/system/cpu.cpp +++ b/src/eepp/system/cpu.cpp @@ -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 = []() { diff --git a/src/modules/eterm/include/eterm/terminal/kittygraphicsprotocol.hpp b/src/modules/eterm/include/eterm/terminal/kittygraphicsprotocol.hpp index 16dc562f4..c137975fb 100644 --- a/src/modules/eterm/include/eterm/terminal/kittygraphicsprotocol.hpp +++ b/src/modules/eterm/include/eterm/terminal/kittygraphicsprotocol.hpp @@ -221,6 +221,10 @@ class KittyGraphicsProtocol { std::vector mPlaceholderCells; std::vector mUpdates; PendingTransfer mPending; + std::vector mDecodedScratch; + std::vector mPixelScratch; + std::vector mEncodedScratch; + std::vector mComposeSourceScratch; size_t mStorageBytes{ 0 }; size_t mFrameStorageBytes{ 0 }; Uint64 mCreationSerial{ 0 }; diff --git a/src/modules/eterm/src/eterm/terminal/kittygraphicsprotocol.cpp b/src/modules/eterm/src/eterm/terminal/kittygraphicsprotocol.cpp index 5f372a90c..b38b11901 100644 --- a/src/modules/eterm/src/eterm/terminal/kittygraphicsprotocol.cpp +++ b/src/modules/eterm/src/eterm/terminal/kittygraphicsprotocol.cpp @@ -62,6 +62,12 @@ bool checkedPixelBytes( Uint32 width, Uint32 height, size_t channels, size_t& re return true; } +void recycleBuffer( std::vector& buffer, std::vector& cache ) { + buffer.clear(); + if ( buffer.capacity() > cache.capacity() ) + buffer.swap( cache ); +} + bool decodeBase64( std::string_view input, bool finalChunk, std::vector& 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 pixels; if ( format == 100 ) { - std::vector encoded; + std::vector 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 rgba; + std::vector rgba = std::move( mPixelScratch ); rgba.resize( static_cast( 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> 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::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::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 sourceCopy( static_cast( width ) * height * 4 ); + std::vector sourceCopy = std::move( mComposeSourceScratch ); + sourceCopy.resize( static_cast( width ) * height * 4 ); std::vector result( sourceCopy.size() ); const size_t imageStride = static_cast( imageWidth ) * 4; const size_t rowBytes = static_cast( 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; diff --git a/src/tests/unit_tests/eterm_tests.cpp b/src/tests/unit_tests/eterm_tests.cpp index 7936bd146..d05946171 100644 --- a/src/tests/unit_tests/eterm_tests.cpp +++ b/src/tests/unit_tests/eterm_tests.cpp @@ -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 boundarySource( length ); + for ( size_t i = 0; i < boundarySource.size(); ++i ) + boundarySource[i] = static_cast( ( i * 197 + length ) & 0xFF ); + std::string boundaryEncoded; + ASSERT_TRUE( Base64::encode( + std::string_view( reinterpret_cast( boundarySource.data() ), + boundarySource.size() ), + boundaryEncoded ) ); + std::vector 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 source( 65537 ); + for ( size_t i = 0; i < source.size(); ++i ) + source[i] = static_cast( ( i * 131 + i / 7 ) & 0xFF ); + std::string encoded; + ASSERT_TRUE( Base64::encode( + std::string_view( reinterpret_cast( source.data() ), source.size() ), + encoded ) ); + std::vector 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( -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( 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 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.