mirror of
https://github.com/SpartanJ/eepp.git
synced 2026-09-27 16:56:33 +03:00
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:
@@ -7,6 +7,8 @@ namespace EE { namespace System {
|
||||
|
||||
class EE_API CPU {
|
||||
public:
|
||||
static bool hasSSSE3();
|
||||
|
||||
static bool hasAVX2();
|
||||
|
||||
static bool hasNEON();
|
||||
|
||||
@@ -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 = []() {
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user