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.