Optimize eterm Kitty graphics rendering

- bulk-ingest Kitty APC payloads without per-byte UTF-8 processing
  - preserve APC buffer capacity between graphics commands
  - update existing GPU textures when replacing video frames
  - validate Base64 while decoding directly into transfer storage
  - reserve known image sizes and share immutable pixel buffers
  - coalesce superseded anonymous video-frame updates
  - support POSIX shared-memory transmission with stable snapshots
  - fix large unchunked images and anonymous placement replacement
  - add fragmentation, malformed input, video, and shared-memory tests
  - update Kitty graphics protocol documentation
This commit is contained in:
Martín Lucas Golini
2026-09-03 19:39:47 -03:00
parent 57754feb31
commit 0fa1f65d04
8 changed files with 319 additions and 51 deletions
+45 -3
View File
@@ -1,5 +1,5 @@
#include <eepp/system/base64.hpp>
#include <array>
#include <eepp/system/base64.hpp>
namespace EE { namespace System {
@@ -106,6 +106,46 @@ size_t decodeBase64( size_t in_len, const char* in, size_t out_len, unsigned cha
return io;
}
size_t decodeBase64Strict( size_t inLen, const char* input, size_t outLen, unsigned char* output ) {
size_t outputOffset = 0;
Uint32 value = 0;
unsigned bits = 0;
size_t padding = 0;
for ( size_t offset = 0; offset < inLen; ++offset ) {
const Uint8 decoded = base64dec_tab[static_cast<unsigned char>( input[offset] )];
if ( decoded == BASE64_PADDING ) {
padding = inLen - offset;
if ( padding > 2 || inLen % 4 != 0 )
return static_cast<size_t>( -1 );
for ( size_t remainder = offset; remainder < inLen; ++remainder )
if ( input[remainder] != '=' )
return static_cast<size_t>( -1 );
break;
}
if ( decoded > 63 || padding != 0 )
return static_cast<size_t>( -1 );
value = ( value << 6 ) | decoded;
bits += 6;
if ( bits >= 8 ) {
bits -= 8;
if ( outputOffset >= outLen )
return static_cast<size_t>( -1 );
output[outputOffset++] = static_cast<unsigned char>( ( value >> bits ) & 0xFF );
if ( bits == 0 )
value = 0;
}
}
const size_t dataLength = inLen - padding;
if ( dataLength % 4 == 1 || ( padding == 1 && dataLength % 4 != 3 ) ||
( padding == 2 && dataLength % 4 != 2 ) )
return static_cast<size_t>( -1 );
// Reject non-canonical encodings whose unused bits are non-zero. Besides being strict, this
// avoids accepting multiple byte strings for the same payload at protocol boundaries.
if ( bits != 0 && ( value & ( ( 1u << bits ) - 1u ) ) != 0 )
return static_cast<size_t>( -1 );
return outputOffset;
}
} // namespace
size_t Base64::decode( size_t in_len, const char* in, size_t out_len, unsigned char* out ) {
@@ -114,6 +154,8 @@ size_t Base64::decode( size_t in_len, const char* in, size_t out_len, unsigned c
size_t Base64::decode( size_t in_len, const char* in, size_t out_len, unsigned char* out,
DecodeMode mode ) {
if ( mode == DecodeMode::NoWhitespaceStrict )
return decodeBase64Strict( in_len, in, out_len, out );
return mode == DecodeMode::NoWhitespace ? decodeBase64<false>( in_len, in, out_len, out )
: decodeBase64<true>( in_len, in, out_len, out );
}
@@ -165,8 +207,8 @@ bool Base64::encode( std::string_view in, std::string& out ) {
if ( out.size() < b64len )
out.resize( b64len );
const size_t len =
encode( in.size(), reinterpret_cast<const unsigned char*>( in.data() ), out.size(), out.data() );
const size_t len = encode( in.size(), reinterpret_cast<const unsigned char*>( in.data() ),
out.size(), out.data() );
if ( len != static_cast<size_t>( -1 ) && len != out.size() )
out.resize( len );
@@ -35,6 +35,7 @@ struct KittyGraphicsCommandData {
std::string_view payload;
std::optional<Uint32> format;
std::optional<Uint32> dataSize;
std::optional<Uint32> dataOffset;
std::optional<Uint32> more;
std::optional<Uint32> imageId;
std::optional<Uint32> imageNumber;
@@ -167,7 +168,7 @@ class KittyGraphicsProtocol {
Int32 gapMs{ 40 };
Uint32 usageHint{ 0 };
};
std::vector<Uint8> rgba;
std::shared_ptr<std::vector<Uint8>> rgba;
std::unordered_map<Uint32, Frame> frames;
Sizei size;
Uint32 imageNumber{ 0 };
@@ -9,8 +9,16 @@
#include <charconv>
#include <cstring>
#include <limits>
#include <thread>
#include <type_traits>
#if EE_PLATFORM != EE_PLATFORM_WIN && EE_PLATFORM != EE_PLATFORM_EMSCRIPTEN
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <unistd.h>
#endif
using namespace EE::System;
namespace eterm { namespace Terminal {
@@ -54,40 +62,96 @@ bool checkedPixelBytes( Uint32 width, Uint32 height, size_t channels, size_t& re
return true;
}
bool validBase64( std::string_view input, bool finalChunk ) {
if ( input.size() % 4 == 1 )
bool decodeBase64( std::string_view input, bool finalChunk, std::vector<Uint8>& output ) {
if ( !finalChunk && input.size() % 4 != 0 )
return false;
const size_t oldSize = output.size();
const size_t capacity = Base64::decodeSafeOutLen( input.size() );
constexpr size_t MaxTransferBytes = 64 * 1024 * 1024;
if ( oldSize > MaxTransferBytes || capacity > MaxTransferBytes - oldSize )
return false;
output.resize( oldSize + capacity );
const size_t decodedSize =
Base64::decode( input.size(), input.data(), capacity, output.data() + oldSize,
Base64::DecodeMode::NoWhitespaceStrict );
if ( decodedSize == static_cast<size_t>( -1 ) ) {
output.resize( oldSize );
return false;
size_t padding = 0;
for ( size_t i = 0; i < input.size(); ++i ) {
const unsigned char character = input[i];
const bool alphabet =
( character >= 'A' && character <= 'Z' ) || ( character >= 'a' && character <= 'z' ) ||
( character >= '0' && character <= '9' ) || character == '+' || character == '/';
if ( character == '=' ) {
++padding;
if ( !finalChunk || padding > 2 )
return false;
} else if ( !alphabet || padding != 0 ) {
return false;
}
}
return finalChunk || input.size() % 4 == 0;
output.resize( oldSize + decodedSize );
return true;
}
bool decodeBase64( std::string_view input, bool finalChunk, std::vector<Uint8>& output ) {
if ( !validBase64( input, finalChunk ) )
bool readSharedMemory( const KittyGraphicsCommandData& data, std::vector<Uint8>& output ) {
#if EE_PLATFORM == EE_PLATFORM_WIN || EE_PLATFORM == EE_PLATFORM_EMSCRIPTEN
(void)data;
(void)output;
return false;
#else
std::vector<Uint8> decodedName;
if ( !decodeBase64( data.payload, true, decodedName ) || decodedName.empty() ||
decodedName.size() > 255 ||
std::find( decodedName.begin() + 1, decodedName.end(), '/' ) != decodedName.end() ||
std::find( decodedName.begin(), decodedName.end(), 0 ) != decodedName.end() )
return false;
std::vector<Uint8> decoded( Base64::decodeSafeOutLen( input.size() ) );
const size_t decodedSize = Base64::decode( input.size(), input.data(), decoded.size(),
decoded.data(), Base64::DecodeMode::NoWhitespace );
if ( decodedSize == static_cast<size_t>( -1 ) )
std::string name( decodedName.begin(), decodedName.end() );
const int descriptor = shm_open( name.c_str(), O_RDONLY, 0 );
if ( descriptor == -1 )
return false;
decoded.resize( decodedSize );
constexpr size_t MaxTransferBytes = 64 * 1024 * 1024;
if ( output.size() > MaxTransferBytes || decoded.size() > MaxTransferBytes - output.size() )
return false;
output.insert( output.end(), decoded.begin(), decoded.end() );
return true;
// POSIX Kitty transfers are single-use. Unlink immediately after opening so all error paths
// still retire the client-owned object while the descriptor keeps its contents alive.
shm_unlink( name.c_str() );
struct stat status{};
const size_t offset = data.dataOffset.value_or( 0 );
bool valid = fstat( descriptor, &status ) == 0 && status.st_size >= 0 &&
static_cast<Uint64>( status.st_size ) >= offset;
size_t bytes = 0;
if ( valid ) {
const size_t available = static_cast<size_t>( status.st_size ) - offset;
bytes = data.dataSize.value_or( static_cast<Uint32>(
std::min<size_t>( available, std::numeric_limits<Uint32>::max() ) ) );
valid = bytes <= available && bytes <= 128 * 1024 * 1024;
}
if ( valid && bytes != 0 ) {
const size_t mappingBytes = offset + bytes;
valid = mappingBytes >= bytes && mappingBytes <= 128 * 1024 * 1024;
void* mapping = valid ? mmap( nullptr, mappingBytes, PROT_READ, MAP_SHARED, descriptor, 0 )
: MAP_FAILED;
if ( mapping == MAP_FAILED ) {
valid = false;
} else {
const auto* source = static_cast<const Uint8*>( mapping ) + offset;
// mpv reuses the same shm name for every frame. If it reopened the object before
// we unlinked it above, its next memcpy can overlap this read. Require consecutive
// identical observations after yielding to the writer; otherwise retain the previous
// displayed frame instead of publishing visibly torn rows.
std::vector<Uint8> snapshot( source, source + bytes );
bool stable = false;
unsigned stableObservations = 0;
constexpr unsigned RequiredStableObservations = 2;
constexpr unsigned MaxSnapshotAttempts = 6;
for ( unsigned attempt = 0; attempt < MaxSnapshotAttempts; ++attempt ) {
std::this_thread::yield();
if ( std::memcmp( snapshot.data(), source, bytes ) == 0 ) {
if ( ++stableObservations == RequiredStableObservations ) {
stable = true;
break;
}
} else {
stableObservations = 0;
snapshot.assign( source, source + bytes );
}
}
if ( stable )
output = std::move( snapshot );
else
valid = false;
munmap( mapping, mappingBytes );
}
}
close( descriptor );
return valid && bytes != 0;
#endif
}
bool placementContains( const TerminalVisiblePlacement& placement, Vector2i cell ) {
@@ -147,6 +211,9 @@ KittyGraphicsParseResult KittyGraphicsProtocol::parse( std::string_view command
case 'S':
PARSE_UINT_FIELD( dataSize );
break;
case 'O':
PARSE_UINT_FIELD( dataOffset );
break;
case 'm':
PARSE_UINT_FIELD( more );
valid = valid && unsignedValue <= 1;
@@ -327,6 +394,20 @@ KittyGraphicsHandleResult
KittyGraphicsProtocol::handleTransmit( const KittyGraphicsCommandData& data, bool display,
bool query, bool frame, Vector2i cursor ) {
const bool more = data.more.value_or( 0 ) != 0;
if ( data.transmission == 's' ) {
if ( mPending.active )
mPending = {};
PendingTransfer transfer;
transfer.data = data;
transfer.data.payload = {};
transfer.display = display;
transfer.query = query;
transfer.frame = frame;
if ( !readSharedMemory( data, transfer.decodedData ) )
return { response( data, KittyGraphicsError::DecodeFailed ),
KittyGraphicsError::DecodeFailed, false };
return finishTransfer( std::move( transfer ), cursor );
}
if ( mPending.active ) {
if ( frame != mPending.frame || data.format || data.dataSize || data.imageId ||
data.imageNumber || data.usageHint || data.placementId || data.width || data.height ||
@@ -361,6 +442,14 @@ KittyGraphicsProtocol::handleTransmit( const KittyGraphicsCommandData& data, boo
transfer.query = query;
transfer.frame = frame;
transfer.active = true;
const Uint32 format = data.format.value_or( 32 );
if ( format == 24 || format == 32 ) {
size_t expectedBytes = 0;
if ( data.width && data.height &&
checkedPixelBytes( *data.width, *data.height, format == 24 ? 3 : 4, expectedBytes ) &&
expectedBytes <= 64 * 1024 * 1024 )
transfer.decodedData.reserve( expectedBytes );
}
if ( !decodeBase64( data.payload, !more, transfer.decodedData ) )
return { response( data, KittyGraphicsError::InvalidData ), KittyGraphicsError::InvalidData,
false };
@@ -495,7 +584,9 @@ KittyGraphicsHandleResult KittyGraphicsProtocol::finishTransfer( PendingTransfer
std::vector<Uint8>* destinationPixels = nullptr;
bool createdFrame = false;
if ( frameNumber == 1 ) {
destinationPixels = &image->second.rgba;
if ( !image->second.rgba.unique() )
image->second.rgba = std::make_shared<std::vector<Uint8>>( *image->second.rgba );
destinationPixels = image->second.rgba.get();
} else {
auto frame = image->second.frames.find( frameNumber );
if ( frame == image->second.frames.end() ) {
@@ -505,7 +596,7 @@ KittyGraphicsHandleResult KittyGraphicsProtocol::finishTransfer( PendingTransfer
if ( data.columns ) {
const Uint32 baseFrame = *data.columns;
if ( baseFrame == 1 )
newFrame.rgba = image->second.rgba;
newFrame.rgba = *image->second.rgba;
else {
auto base = image->second.frames.find( baseFrame );
if ( base == image->second.frames.end() )
@@ -620,7 +711,7 @@ KittyGraphicsHandleResult KittyGraphicsProtocol::finishTransfer( PendingTransfer
false };
auto existing = mImages.find( imageId );
const size_t oldBytes = existing == mImages.end() ? 0 : existing->second.rgba.size();
const size_t oldBytes = existing == mImages.end() ? 0 : existing->second.rgba->size();
if ( !ensureCapacity( pixels.size(), imageId, existing == mImages.end() ) )
return { response( data, KittyGraphicsError::NoSpace ), KittyGraphicsError::NoSpace,
false };
@@ -646,8 +737,8 @@ KittyGraphicsHandleResult KittyGraphicsProtocol::finishTransfer( PendingTransfer
image.usageHint = data.usageHint.value_or( 0 );
image.anonymous = anonymous;
image.creationSerial = ++mCreationSerial;
image.rgba = std::move( pixels );
mStorageBytes = mStorageBytes - oldBytes + image.rgba.size();
image.rgba = std::make_shared<std::vector<Uint8>>( std::move( pixels ) );
mStorageBytes = mStorageBytes - oldBytes + image.rgba->size();
const bool replaced = existing != mImages.end();
auto inserted = mImages.insert_or_assign( imageId, std::move( image ) ).first;
@@ -658,7 +749,7 @@ KittyGraphicsHandleResult KittyGraphicsProtocol::finishTransfer( PendingTransfer
update.imageSize = inserted->second.size;
update.region =
Rect( 0, 0, inserted->second.size.getWidth(), inserted->second.size.getHeight() );
update.rgba = std::make_shared<const std::vector<Uint8>>( inserted->second.rgba );
update.rgba = inserted->second.rgba;
mUpdates.emplace_back( std::move( update ) );
++mStats.fullImageUpdates;
++mPresentationGeneration;
@@ -1122,7 +1213,7 @@ KittyGraphicsProtocol::composeFrames( const KittyGraphicsCommandData& data ) {
false };
auto pixelsFor = [&]( Uint32 frameNumber ) -> std::vector<Uint8>* {
if ( frameNumber == 1 )
return &image->second.rgba;
return image->second.rgba.get();
auto frame = image->second.frames.find( frameNumber );
return frame == image->second.frames.end() ? nullptr : &frame->second.rgba;
};
@@ -1162,6 +1253,10 @@ KittyGraphicsProtocol::composeFrames( const KittyGraphicsCommandData& data ) {
source->data() + static_cast<size_t>( sourceY + row ) * imageStride +
static_cast<size_t>( sourceX ) * 4,
rowBytes );
if ( destinationFrame == 1 && !image->second.rgba.unique() ) {
image->second.rgba = std::make_shared<std::vector<Uint8>>( *image->second.rgba );
destination = image->second.rgba.get();
}
const bool replace = data.cursorMovement.value_or( 0 ) == 1;
for ( Uint32 row = 0; row < height; ++row ) {
Uint8* target = destination->data() +
@@ -1239,7 +1334,7 @@ void KittyGraphicsProtocol::eraseImage( KittyImageId imageId ) {
auto image = mImages.find( imageId );
if ( image == mImages.end() )
return;
mStorageBytes -= image->second.rgba.size();
mStorageBytes -= image->second.rgba->size();
for ( const auto& frame : image->second.frames )
mFrameStorageBytes -= frame.second.rgba.size();
mImages.erase( image );
@@ -1252,7 +1347,7 @@ void KittyGraphicsProtocol::eraseImage( KittyImageId imageId ) {
bool KittyGraphicsProtocol::ensureCapacity( size_t bytes, KittyImageId replacingId,
bool addingImage ) {
auto replaced = mImages.find( replacingId );
const size_t replacedBytes = replaced == mImages.end() ? 0 : replaced->second.rgba.size();
const size_t replacedBytes = replaced == mImages.end() ? 0 : replaced->second.rgba->size();
auto hasCapacity = [&] {
return bytes <= mMaxStorageBytes &&
mStorageBytes - replacedBytes <= mMaxStorageBytes - bytes &&
@@ -1401,7 +1496,7 @@ std::shared_ptr<TerminalGraphicsPresentation> KittyGraphicsProtocol::takePresent
const std::vector<Uint8>* KittyGraphicsProtocol::imagePixels( KittyImageId imageId ) const {
auto image = mImages.find( imageId );
return image == mImages.end() ? nullptr : &image->second.rgba;
return image == mImages.end() ? nullptr : image->second.rgba.get();
}
bool KittyGraphicsProtocol::hasVirtualPlacements() const {
@@ -1539,7 +1634,7 @@ void KittyGraphicsProtocol::resync() {
create.imageId = image.first;
create.imageSize = image.second.size;
create.region = Rect( 0, 0, image.second.size.getWidth(), image.second.size.getHeight() );
create.rgba = std::make_shared<const std::vector<Uint8>>( image.second.rgba );
create.rgba = image.second.rgba;
mUpdates.emplace_back( std::move( create ) );
for ( const auto& frame : image.second.frames ) {
TerminalGraphicsUpdate createFrame;
@@ -26,8 +26,22 @@ bool KittyGraphicsRenderer::applyUpdates( std::vector<TerminalGraphicsUpdate>&&
return false;
switch ( update.type ) {
case TerminalGraphicsUpdateType::CreateImage:
case TerminalGraphicsUpdateType::ReplaceImage: {
if ( !update.rgba || update.imageSize.getWidth() <= 0 ||
update.imageSize.getHeight() <= 0 )
return false;
auto existing = mImages.find( update.imageId );
if ( existing != mImages.end() && existing->second.texture &&
existing->second.size == update.imageSize ) {
existing->second.texture->update( update.rgba->data(),
update.imageSize.getWidth(),
update.imageSize.getHeight(), 0, 0 );
existing->second.frames.clear();
break;
}
[[fallthrough]];
}
case TerminalGraphicsUpdateType::CreateImage: {
if ( !update.rgba || update.imageSize.getWidth() <= 0 ||
update.imageSize.getHeight() <= 0 )
return false;
@@ -2664,10 +2664,16 @@ void TerminalEmulator::strdump( void ) {
}
void TerminalEmulator::strreset( void ) {
auto old = mStrescseq.buf;
char* buffer = mStrescseq.buf;
size_t capacity = mStrescseq.siz;
constexpr size_t MaxRetainedStringCapacity = 128 * 1024;
if ( !buffer || capacity > MaxRetainedStringCapacity ) {
buffer = (char*)xrealloc( buffer, STR_BUF_SIZ );
capacity = STR_BUF_SIZ;
}
mStrescseq = STREscape{};
mStrescseq.buf = (char*)xrealloc( old, STR_BUF_SIZ );
mStrescseq.siz = STR_BUF_SIZ;
mStrescseq.buf = buffer;
mStrescseq.siz = capacity;
}
void TerminalEmulator::sendbreak( const TerminalArg* ) {
@@ -3230,6 +3236,45 @@ int TerminalEmulator::twrite( const char* buf, int buflen, int show_ctrl ) {
int n;
for ( n = 0; n < buflen; n += charsize ) {
/* Kitty control data and payload are ASCII transport bytes. Once ESC _ G has been
* recognized, append ordinary bytes in bulk instead of routing every Base64 byte through
* UTF-8 decoding and the terminal character state machine. Control bytes remain on the
* normal path so fragmented ESC \\ termination and malformed strings retain their exact
* behavior. */
if ( !show_ctrl && ( mTerm.esc & ESC_STR ) && mStrescseq.type == '_' &&
mStrescseq.len > 0 && mStrescseq.buf[0] == 'G' ) {
int end = n;
while ( end < buflen ) {
const unsigned char byte = static_cast<unsigned char>( buf[end] );
if ( byte == '\a' || byte == 030 || byte == 032 || byte == 033 ||
( byte >= 0x80 && byte <= 0x9F ) )
break;
++end;
}
const size_t bytes = static_cast<size_t>( end - n );
if ( bytes != 0 ) {
if ( !mStrescseq.discarded ) {
if ( mStrescseq.len > MAX_KITTY_GRAPHICS_APC_SIZE ||
bytes > MAX_KITTY_GRAPHICS_APC_SIZE - mStrescseq.len ) {
mStrescseq.discarded = true;
} else {
const size_t required = mStrescseq.len + bytes + 1;
if ( required > mStrescseq.siz ) {
size_t capacity = mStrescseq.siz;
while ( capacity < required )
capacity = eemin( capacity * 2, MAX_KITTY_GRAPHICS_APC_SIZE + 1 );
mStrescseq.buf = (char*)xrealloc( mStrescseq.buf, capacity );
mStrescseq.siz = capacity;
}
std::memcpy( mStrescseq.buf + mStrescseq.len, buf + n, bytes );
mStrescseq.len += bytes;
}
}
n = end;
if ( n == buflen )
return buflen;
}
}
if ( IS_SET( MODE_UTF8 ) ) {
/* process a complete utf8 char */
charsize = utf8decode( buf + n, &u, buflen - n );
+70 -1
View File
@@ -1,6 +1,7 @@
#include "utest.hpp"
#include <atomic>
#include <chrono>
#include <deque>
#include <eepp/system/base64.hpp>
#include <eepp/system/compression.hpp>
#include <eepp/system/iostreammemory.hpp>
@@ -13,6 +14,12 @@
#include <limits>
#include <thread>
#if EE_PLATFORM != EE_PLATFORM_WIN && EE_PLATFORM != EE_PLATFORM_EMSCRIPTEN
#include <fcntl.h>
#include <sys/mman.h>
#include <unistd.h>
#endif
using namespace eterm::Terminal;
using namespace eterm::System;
using namespace EE::System;
@@ -23,6 +30,7 @@ class MockPty : public IPseudoTerminal {
std::string mWrites;
bool mLoopWrites{ true };
size_t mMaxRead{ std::numeric_limits<size_t>::max() };
std::deque<size_t> mReadSizes;
size_t mReadOffset{ 0 };
std::atomic<size_t> mBytesRead{ 0 };
int mCols = 80;
@@ -48,7 +56,12 @@ class MockPty : public IPseudoTerminal {
int read( char* buf, size_t n, bool ) override {
if ( mReadOffset == mBuffer.size() )
return 0;
size_t toRead = std::min( { n, mBuffer.size() - mReadOffset, mMaxRead } );
size_t readLimit = mMaxRead;
if ( !mReadSizes.empty() ) {
readLimit = mReadSizes.front();
mReadSizes.pop_front();
}
size_t toRead = std::min( { n, mBuffer.size() - mReadOffset, readLimit } );
memcpy( buf, mBuffer.data() + mReadOffset, toRead );
mReadOffset += toRead;
mBytesRead.fetch_add( toRead, std::memory_order_relaxed );
@@ -935,6 +948,62 @@ UTEST( eterm, kitty_graphics_apc_is_fragmentation_safe_and_not_terminal_text ) {
EXPECT_EQ( static_cast<Rune>( 'K' ), display->mSecondGlyph.u );
}
UTEST( eterm, kitty_graphics_bulk_apc_accepts_every_input_split_boundary ) {
const std::string stream = "\033_Ga=T,f=32,s=1,v=1,q=2;AQIDBA==\033\\";
for ( size_t split = 1; split < stream.size(); ++split ) {
auto pty = std::make_unique<MockPty>();
pty->mBuffer = stream;
pty->mLoopWrites = false;
pty->mReadSizes = { split, stream.size() - split };
auto process = std::make_unique<MockProcess>();
auto display = std::make_shared<MockDisplay>();
auto term =
TerminalEmulator::create( std::move( pty ), std::move( process ), display, 100 );
term->update();
while ( !term->update() ) {
}
ASSERT_TRUE( display->mGraphics != nullptr );
ASSERT_EQ( static_cast<size_t>( 1 ), display->mGraphics->placements.size() );
}
}
UTEST( eterm, kitty_graphics_strict_base64_rejects_invalid_payload_bytes ) {
KittyGraphicsProtocol protocol;
EXPECT_EQ( KittyGraphicsError::InvalidData,
protocol.handle( "a=t,f=32,s=1,v=1;AQI BA==" ).error );
EXPECT_EQ( KittyGraphicsError::InvalidData,
protocol.handle( "a=t,f=32,s=1,v=1;AQIDBA=$" ).error );
EXPECT_EQ( KittyGraphicsError::InvalidData,
protocol.handle( "a=t,f=32,s=1,v=1;AQ=DBA==" ).error );
EXPECT_EQ( KittyGraphicsError::InvalidData, protocol.handle( "a=t,f=32,s=1,v=1;AB==" ).error );
}
#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.
const std::string name = "eterm-kitty-unit-" + std::to_string( getpid() );
shm_unlink( name.c_str() );
const int descriptor = shm_open( name.c_str(), O_CREAT | O_EXCL | O_RDWR, 0600 );
ASSERT_TRUE( descriptor >= 0 );
const Uint8 stored[] = { 99, 98, 1, 2, 3 };
ASSERT_EQ( static_cast<ssize_t>( sizeof( stored ) ),
write( descriptor, stored, sizeof( stored ) ) );
close( descriptor );
std::string encodedName;
ASSERT_TRUE( Base64::encode( name, encodedName ) );
KittyGraphicsProtocol protocol;
EXPECT_EQ( KittyGraphicsError::None,
protocol.handle( "a=T,t=s,f=24,s=1,v=1,O=2,S=3,q=2,m=1;" + encodedName ).error );
auto updates = protocol.takeUpdates();
ASSERT_EQ( static_cast<size_t>( 1 ), updates.size() );
ASSERT_TRUE( updates[0].rgba != nullptr );
const std::vector<Uint8> expected{ 1, 2, 3, 255 };
EXPECT_TRUE( expected == *updates[0].rgba );
EXPECT_EQ( -1, shm_open( name.c_str(), O_RDONLY, 0 ) );
}
#endif
UTEST( eterm, kitty_graphics_accepts_unchunked_direct_image_larger_than_eight_kibibytes ) {
std::vector<Uint8> rgb( 64 * 64 * 3 );
for ( size_t offset = 0; offset < rgb.size(); offset += 3 )