From bd9360cf3e9ad6a27ddf4d6cbf2ee008b0997cc9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Lucas=20Golini?= Date: Sat, 5 Sep 2026 17:17:09 -0300 Subject: [PATCH] optimize Kitty streaming and complete terminal input Add persistent Kitty image presentation with RGB24 framebuffer updates, tile-based damage detection, adaptive zlib compression, chunked Base64 transmission, and unchanged-frame suppression. Fall back to complete updates when damage exceeds the configured threshold. Expose environment controls for damage updates, persistent presentation, and zlib compression level. Resolve presentation policy once during initialization to keep environment parsing outside the frame path. Repeat a=f on animation-frame continuation chunks as required by the Kitty protocol, and suppress graphics acknowledgements from the terminal input stream. Complete Kitty keyboard translation for punctuation, function, keypad, modifier, lock, and navigation keys. Preserve associated multi-codepoint text and disambiguate Backspace from Delete using their protocol context. Fix IOStreamMemory::write() to report bytes written so multi-chunk compression works correctly, avoid stdout logging in the offscreen examples, and add regression coverage for compressed streams and chunked frame updates. --- .../window/terminal/kittyframepresenter.hpp | 29 ++- src/eepp/system/iostreammemory.cpp | 3 +- src/eepp/window/kittyframepresenter.cpp | 229 +++++++++++++++--- src/eepp/window/terminalruntime.cpp | 125 +++++++++- src/examples/ui_html/ui_html.cpp | 2 +- src/tests/test_all/test.cpp | 2 +- src/tests/unit_tests/eterm_tests.cpp | 31 +++ src/tools/uieditor/uieditor.cpp | 2 +- 8 files changed, 377 insertions(+), 46 deletions(-) diff --git a/include/eepp/window/terminal/kittyframepresenter.hpp b/include/eepp/window/terminal/kittyframepresenter.hpp index e95a56aab..8ea269df7 100644 --- a/include/eepp/window/terminal/kittyframepresenter.hpp +++ b/include/eepp/window/terminal/kittyframepresenter.hpp @@ -8,7 +8,14 @@ #include namespace EE { namespace Window { -/** Presents completed window frames using the Kitty graphics protocol. */ +/** Presents completed window frames using the Kitty graphics protocol. + * + * Presentation policy can be configured before initialization with + * `EEPP_TERMINAL_DAMAGE_UPDATES`, `EEPP_TERMINAL_PERSISTENT_UPDATES`, and + * `EEPP_TERMINAL_ZLIB_LEVEL`. Damage and persistent updates default to enabled. The zlib level + * defaults to 1, accepts levels 1 through 9, and can be set to 0 to disable compression. Disabling + * persistent updates also disables damage updates because rectangles require a persistent image. + */ class KittyFramePresenter final : public FramePresenter { public: ~KittyFramePresenter(); @@ -27,17 +34,35 @@ class KittyFramePresenter final : public FramePresenter { std::vector pixels; Math::Sizei size; }; + struct DamageRectangle { + Int32 x; + Int32 y; + Int32 width; + Int32 height; + }; std::mutex mMutex; std::condition_variable mCondition; std::thread mWorker; Frame mPending; Frame mRecycle; + Frame mPresented; + std::vector mDamageRectangles; + std::vector mTransferPixels; + std::vector mCompressedPixels; + int mZlibCompressionLevel{ 1 }; bool mRunning{ false }; bool mHasPending{ false }; + bool mDamageUpdatesEnabled{ true }; + bool mPersistentUpdatesEnabled{ true }; /** Consumes the bounded newest-frame queue until shutdown. */ void run(); /** Encodes and writes one RGB24 frame using chunked Kitty direct transmission. */ - void sendFrame( const Frame& ); + bool sendFrame( const Frame& ); + /** Sends an initial image or root-image rectangle update, using compression when beneficial. */ + bool sendTransfer( const std::vector& pixels, const DamageRectangle& rectangle, + bool initial ); + /** Packs a bottom-up framebuffer rectangle into top-down Kitty row order. */ + void extractRectangle( const Frame& frame, const DamageRectangle& rectangle ); }; }} // namespace EE::Window diff --git a/src/eepp/system/iostreammemory.cpp b/src/eepp/system/iostreammemory.cpp index 1dde230ca..82f67b738 100644 --- a/src/eepp/system/iostreammemory.cpp +++ b/src/eepp/system/iostreammemory.cpp @@ -37,9 +37,10 @@ ios_size IOStreamMemory::write( const char* data, ios_size size ) { memcpy( mWritePtr + mPos, data, size ); mPos += size; + return size; } - return mPos; + return 0; } ios_size IOStreamMemory::seek( ios_size position ) { diff --git a/src/eepp/window/kittyframepresenter.cpp b/src/eepp/window/kittyframepresenter.cpp index c9d9485d1..1c09e9eef 100644 --- a/src/eepp/window/kittyframepresenter.cpp +++ b/src/eepp/window/kittyframepresenter.cpp @@ -1,16 +1,120 @@ +#include #include +#include +#include #include #include #include #include #include +#include #include using namespace EE::System; namespace EE { namespace Window { +namespace { + +constexpr Int32 TileSize = 32; +constexpr size_t CompressionThreshold = 4096; +constexpr Uint32 StreamImageId = 0x45455050; +constexpr Uint32 StreamPlacementId = 1; +enum class DamageResult : Uint8 { None, Rectangle, Full }; + +bool environmentFlag( const char* name, bool defaultValue ) { + const char* value = std::getenv( name ); + if ( !value || !value[0] ) + return defaultValue; + if ( 0 == std::strcmp( value, "0" ) || String::iequals( value, "false" ) || + String::iequals( value, "off" ) || String::iequals( value, "no" ) ) + return false; + if ( 0 == std::strcmp( value, "1" ) || String::iequals( value, "true" ) || + String::iequals( value, "on" ) || String::iequals( value, "yes" ) ) + return true; + return defaultValue; +} + +int environmentCompressionLevel() { + const char* value = std::getenv( "EEPP_TERMINAL_ZLIB_LEVEL" ); + if ( !value || !value[0] ) + return 1; + char* end = nullptr; + const long level = std::strtol( value, &end, 10 ); + return end != value && *end == '\0' && level >= 0 && level <= 9 ? static_cast( level ) : 1; +} + +template +DamageResult findDamage( const std::vector& current, const std::vector& previous, + const Math::Sizei& size, std::vector& rectangles ) { + rectangles.clear(); + const Int32 tilesX = ( size.x + TileSize - 1 ) / TileSize; + const Int32 tilesY = ( size.y + TileSize - 1 ) / TileSize; + const size_t rowBytes = static_cast( size.x ) * 3; + const size_t totalPixels = static_cast( size.x ) * size.y; + size_t changedPixels = 0; + Int32 left = size.x; + Int32 top = size.y; + Int32 right = 0; + Int32 bottom = 0; + + for ( Int32 tileY = 0; tileY < tilesY; ++tileY ) { + const Int32 y = tileY * TileSize; + const Int32 height = eemin( TileSize, size.y - y ); + for ( Int32 tileX = 0; tileX < tilesX; ++tileX ) { + bool changed = false; + const Int32 x = tileX * TileSize; + const Int32 width = eemin( TileSize, size.x - x ); + for ( Int32 row = 0; row < height && !changed; ++row ) { + const Int32 sourceRow = size.y - 1 - ( y + row ); + const size_t offset = + static_cast( sourceRow ) * rowBytes + static_cast( x ) * 3; + changed = 0 != std::memcmp( current.data() + offset, previous.data() + offset, + static_cast( width ) * 3 ); + } + if ( changed ) { + changedPixels += static_cast( width ) * height; + left = eemin( left, x ); + top = eemin( top, y ); + right = eemax( right, x + width ); + bottom = eemax( bottom, y + height ); + if ( changedPixels * 5 >= totalPixels * 3 ) + return DamageResult::Full; + } + } + } + + if ( left < right && top < bottom ) { + rectangles.push_back( { left, top, right - left, bottom - top } ); + return DamageResult::Rectangle; + } + return DamageResult::None; +} + +bool compressPixels( const std::vector& input, std::vector& output, int level ) { + if ( level == 0 || input.size() < CompressionThreshold ) + return false; + const int maxSize = Compression::getMaxCompressedBufferSize( input.size() ); + if ( maxSize <= 0 ) + return false; + output.resize( static_cast( maxSize ) ); + IOStreamMemory source( reinterpret_cast( input.data() ), input.size() ); + IOStreamMemory destination( reinterpret_cast( output.data() ), output.size() ); + Compression::Config config; + config.zlib.level = level; + if ( Compression::compress( destination, source, Compression::MODE_DEFLATE, config ) != + Compression::OK ) + return false; + const size_t compressedSize = static_cast( destination.tell() ); + if ( compressedSize + 64 >= input.size() * 9 / 10 ) + return false; + output.resize( compressedSize ); + return true; +} + +} // namespace + KittyFramePresenter::~KittyFramePresenter() { { std::lock_guard lock( mMutex ); @@ -24,6 +128,10 @@ KittyFramePresenter::~KittyFramePresenter() { bool KittyFramePresenter::initialize( Window& window ) { if ( !TerminalRuntime::instance().initialize() ) return false; + mPersistentUpdatesEnabled = environmentFlag( "EEPP_TERMINAL_PERSISTENT_UPDATES", true ); + mDamageUpdatesEnabled = + mPersistentUpdatesEnabled && environmentFlag( "EEPP_TERMINAL_DAMAGE_UPDATES", true ); + mZlibCompressionLevel = environmentCompressionLevel(); TerminalRuntime::instance().attach( window ); mRunning = true; mWorker = std::thread( &KittyFramePresenter::run, this ); @@ -76,55 +184,112 @@ void KittyFramePresenter::run() { frame = std::move( mPending ); mHasPending = false; } - sendFrame( frame ); + if ( sendFrame( frame ) ) + std::swap( mPresented, frame ); std::lock_guard lock( mMutex ); mRecycle = std::move( frame ); } } -void KittyFramePresenter::sendFrame( const Frame& frame ) { - std::array binary; +bool KittyFramePresenter::sendFrame( const Frame& frame ) { + if ( frame.size.x <= 0 || frame.size.y <= 0 ) + return false; + const DamageRectangle full{ 0, 0, frame.size.x, frame.size.y }; + if ( !mDamageUpdatesEnabled ) { + if ( mPresented.size == frame.size && mPresented.pixels.size() == frame.pixels.size() && + 0 == + std::memcmp( mPresented.pixels.data(), frame.pixels.data(), frame.pixels.size() ) ) + return true; + extractRectangle( frame, full ); + const bool initial = !mPersistentUpdatesEnabled || mPresented.size != frame.size || + mPresented.pixels.size() != frame.pixels.size(); + return sendTransfer( mTransferPixels, full, initial ); + } + if ( mPresented.size != frame.size || mPresented.pixels.size() != frame.pixels.size() ) { + extractRectangle( frame, full ); + return sendTransfer( mTransferPixels, full, true ); + } + + const DamageResult damage = + findDamage( frame.pixels, mPresented.pixels, frame.size, mDamageRectangles ); + if ( damage == DamageResult::Full ) { + extractRectangle( frame, full ); + return sendTransfer( mTransferPixels, full, false ); + } + if ( damage == DamageResult::None ) + return true; + for ( const DamageRectangle& rectangle : mDamageRectangles ) { + extractRectangle( frame, rectangle ); + if ( !sendTransfer( mTransferPixels, rectangle, false ) ) + return false; + } + return true; +} + +bool KittyFramePresenter::sendTransfer( const std::vector& pixels, + const DamageRectangle& rectangle, bool initial ) { + const bool useCompression = compressPixels( pixels, mCompressedPixels, mZlibCompressionLevel ); + const std::vector& payload = useCompression ? mCompressedPixels : pixels; std::array encoded; - std::array packet; - const size_t rowBytes = static_cast( frame.size.x ) * 3; - // glReadPixels returns bottom-left-origin rows while Kitty consumes top-left-origin pixels. - // Reverse row traversal during streaming so no full-frame flip buffer is needed. - Int32 row = frame.size.y - 1; - size_t column = 0; + std::array packet; + size_t offset = 0; bool first = true; - while ( row >= 0 ) { - size_t count = 0; - while ( count < binary.size() && row >= 0 ) { - const size_t copy = eemin( binary.size() - count, rowBytes - column ); - std::memcpy( binary.data() + count, - frame.pixels.data() + static_cast( row ) * rowBytes + column, - copy ); - count += copy; - column += copy; - if ( column == rowBytes ) { - column = 0; - --row; - } - } + while ( offset < payload.size() ) { + const size_t count = eemin( static_cast( 3072 ), payload.size() - offset ); const size_t encodedSize = - Base64::encode( count, binary.data(), encoded.size(), encoded.data() ); - char header[128]; - const int headerSize = - first - ? std::snprintf( header, sizeof( header ), "\033_Gf=24,t=d,a=T,C=1,s=%d,v=%d,m=%d;", - frame.size.x, frame.size.y, row >= 0 ? 1 : 0 ) - : std::snprintf( header, sizeof( header ), "\033_Gm=%d;", row >= 0 ? 1 : 0 ); + Base64::encode( count, payload.data() + offset, encoded.size(), encoded.data() ); + offset += count; + char header[256]; + int headerSize; + if ( first && initial ) { + if ( mDamageUpdatesEnabled || mPersistentUpdatesEnabled ) { + headerSize = std::snprintf( + header, sizeof( header ), + "\033_Gf=24,t=d,a=T,C=1,q=2,i=%u,p=%u,s=%d,v=%d%s,m=%d;", StreamImageId, + StreamPlacementId, rectangle.width, rectangle.height, + useCompression ? ",o=z" : "", offset < payload.size() ? 1 : 0 ); + } else { + headerSize = std::snprintf( + header, sizeof( header ), "\033_Gf=24,t=d,a=T,C=1,q=2,s=%d,v=%d%s,m=%d;", + rectangle.width, rectangle.height, useCompression ? ",o=z" : "", + offset < payload.size() ? 1 : 0 ); + } + } else if ( first ) { + headerSize = std::snprintf( + header, sizeof( header ), + "\033_Gf=24,t=d,a=f,q=2,i=%u,r=1,x=%d,y=%d,s=%d,v=%d,X=1%s,m=%d;", StreamImageId, + rectangle.x, rectangle.y, rectangle.width, rectangle.height, + useCompression ? ",o=z" : "", offset < payload.size() ? 1 : 0 ); + } else { + headerSize = std::snprintf( header, sizeof( header ), + initial ? "\033_Gm=%d;" : "\033_Ga=f,m=%d;", + offset < payload.size() ? 1 : 0 ); + } if ( encodedSize == static_cast( -1 ) || headerSize <= 0 || static_cast( headerSize ) >= sizeof( header ) ) - return; + return false; std::memcpy( packet.data(), header, static_cast( headerSize ) ); std::memcpy( packet.data() + headerSize, encoded.data(), encodedSize ); packet[static_cast( headerSize ) + encodedSize] = '\033'; packet[static_cast( headerSize ) + encodedSize + 1] = '\\'; if ( !TerminalRuntime::instance().write( packet.data(), static_cast( headerSize ) + encodedSize + 2 ) ) - return; + return false; first = false; } + return true; +} + +void KittyFramePresenter::extractRectangle( const Frame& frame, const DamageRectangle& rectangle ) { + mTransferPixels.resize( static_cast( rectangle.width ) * rectangle.height * 3 ); + const size_t frameStride = static_cast( frame.size.x ) * 3; + const size_t rectangleStride = static_cast( rectangle.width ) * 3; + for ( Int32 row = 0; row < rectangle.height; ++row ) { + const Int32 sourceRow = frame.size.y - 1 - ( rectangle.y + row ); + std::memcpy( mTransferPixels.data() + static_cast( row ) * rectangleStride, + frame.pixels.data() + static_cast( sourceRow ) * frameStride + + static_cast( rectangle.x ) * 3, + rectangleStride ); + } } }} // namespace EE::Window diff --git a/src/eepp/window/terminalruntime.cpp b/src/eepp/window/terminalruntime.cpp index 8fa3eba59..f5a2755d1 100644 --- a/src/eepp/window/terminalruntime.cpp +++ b/src/eepp/window/terminalruntime.cpp @@ -208,11 +208,14 @@ void TerminalRuntime::detach() { namespace { Uint32 decodeModifiers( unsigned int value ) { const unsigned int bits = value ? value - 1 : 0; - return ( bits & 1 ? KEYMOD_SHIFT : 0 ) | ( bits & 2 ? KEYMOD_ALT : 0 ) | - ( bits & 4 ? KEYMOD_CTRL : 0 ) | ( bits & 8 ? KEYMOD_META : 0 ); + return ( bits & 1 ? KEYMOD_SHIFT : 0 ) | ( bits & 2 ? KEYMOD_LALT : 0 ) | + ( bits & 4 ? KEYMOD_CTRL : 0 ) | ( bits & 8 ? KEYMOD_META : 0 ) | + ( bits & 64 ? KEYMOD_CAPS : 0 ) | ( bits & 128 ? KEYMOD_NUM : 0 ); } -Scancode decodeScancode( Uint32 code ) { +Scancode decodeScancode( Uint32 code, bool kittyBackspace ) { + if ( kittyBackspace && code == 127 ) + return SCANCODE_BACKSPACE; if ( code >= 'a' && code <= 'z' ) return static_cast( SCANCODE_A + code - 'a' ); if ( code >= 'A' && code <= 'Z' ) @@ -243,6 +246,10 @@ Scancode decodeScancode( Uint32 code ) { return SCANCODE_DELETE; if ( code >= static_cast( KEY_F1 ) && code <= static_cast( KEY_F12 ) ) return static_cast( SCANCODE_F1 + code - static_cast( KEY_F1 ) ); + if ( code >= 57376 && code <= 57387 ) + return static_cast( SCANCODE_F13 + code - 57376 ); + if ( code >= 57400 && code <= 57408 ) + return static_cast( SCANCODE_KP_1 + code - 57400 ); switch ( code ) { case 9: return SCANCODE_TAB; @@ -252,19 +259,97 @@ Scancode decodeScancode( Uint32 code ) { return SCANCODE_ESCAPE; case 32: return SCANCODE_SPACE; - case 127: - return SCANCODE_BACKSPACE; + case '-': + return SCANCODE_MINUS; + case '=': + return SCANCODE_EQUALS; + case '[': + return SCANCODE_LEFTBRACKET; + case ']': + return SCANCODE_RIGHTBRACKET; + case '\\': + return SCANCODE_BACKSLASH; + case ';': + return SCANCODE_SEMICOLON; + case '\'': + return SCANCODE_APOSTROPHE; + case '`': + return SCANCODE_GRAVE; + case ',': + return SCANCODE_COMMA; + case '.': + return SCANCODE_PERIOD; + case '/': + return SCANCODE_SLASH; + case 57358: + return SCANCODE_CAPSLOCK; + case 57359: + return SCANCODE_SCROLLLOCK; + case 57360: + return SCANCODE_NUMLOCKCLEAR; + case 57361: + return SCANCODE_PRINTSCREEN; + case 57362: + return SCANCODE_PAUSE; + case 57363: + return SCANCODE_MENU; + case 57399: + return SCANCODE_KP_0; + case 57409: + return SCANCODE_KP_PERIOD; + case 57410: + return SCANCODE_KP_DIVIDE; + case 57411: + return SCANCODE_KP_MULTIPLY; + case 57412: + return SCANCODE_KP_MINUS; + case 57413: + return SCANCODE_KP_PLUS; + case 57414: + return SCANCODE_KP_ENTER; + case 57415: + return SCANCODE_KP_EQUALS; + case 57416: + return SCANCODE_KP_COMMA; + case 57441: + return SCANCODE_LSHIFT; + case 57442: + return SCANCODE_LCTRL; + case 57443: + return SCANCODE_LALT; + case 57444: + return SCANCODE_LGUI; + case 57447: + return SCANCODE_RSHIFT; + case 57448: + return SCANCODE_RCTRL; + case 57449: + return SCANCODE_RALT; + case 57450: + return SCANCODE_RGUI; default: return SCANCODE_UNKNOWN; } } -void enqueueKey( Window& window, Uint32 code, Uint32 mods, Uint32 kind, Uint32 text ) { +Keycode decodeKeycode( Uint32 code, Scancode scancode, bool kittyBackspace ) { + // Kitty identifies Backspace with its terminal encoding (DEL), while eepp represents the + // logical key as BS. Keep protocol encoding out of the neutral InputEvent keycode. + if ( kittyBackspace && code == 127 ) + return KEY_BACKSPACE; + return code >= 57344 && scancode != SCANCODE_UNKNOWN + ? static_cast( SCANCODE_TO_KEYCODE( scancode ) ) + : static_cast( code ); +} + +void enqueueKey( Window& window, Uint32 code, Uint32 mods, Uint32 kind, Uint32 text, + bool kittyBackspace = false ) { + const Scancode scancode = decodeScancode( code, kittyBackspace ); InputEvent event( kind == 3 ? InputEvent::KeyUp : InputEvent::KeyDown ); event.key = { 0, static_cast( kind == 3 ? 0 : 1 ), static_cast( kind == 2 ), - { decodeScancode( code ), static_cast( code ), mods, text } }; + { scancode, decodeKeycode( code, scancode, kittyBackspace ), mods, text } }; window.getInput()->enqueueEvent( event ); if ( text && kind != 3 ) { InputEvent input( InputEvent::TextInput ); @@ -342,6 +427,15 @@ void TerminalRuntime::readInput() { pending.append( data, count ); } #endif + // Kitty graphics acknowledgements are APC strings. They are not input events, and leaving + // an unrecognized ESC _ prefix at the front would block all subsequent keyboard and mouse + // input. + while ( pending.size() >= 2 && pending[0] == '\033' && pending[1] == '_' ) { + const size_t end = pending.find( "\033\\", 2 ); + if ( end == std::string::npos ) + break; + pending.erase( 0, end + 2 ); + } while ( pending.size() >= 3 && pending[0] == '\033' && pending[1] == '[' ) { size_t end = 2; while ( end < pending.size() && !( pending[end] >= '@' && pending[end] <= '~' ) ) @@ -408,7 +502,22 @@ void TerminalRuntime::readInput() { if ( separator != std::string::npos ) { code = static_cast( std::strtoul( params.c_str(), nullptr, 10 ) ); std::sscanf( params.c_str() + separator + 1, "%u:%u;%u", &mods, &kind, &text ); - enqueueKey( *mWindow, code, decodeModifiers( mods ), kind, text ); + enqueueKey( *mWindow, code, decodeModifiers( mods ), kind, text, code == 127 ); + const size_t textSeparator = params.find( ';', separator + 1 ); + if ( kind != 3 && textSeparator != std::string::npos ) { + size_t next = params.find( ':', textSeparator + 1 ); + while ( next != std::string::npos ) { + const Uint32 codepoint = static_cast( + std::strtoul( params.c_str() + next + 1, nullptr, 10 ) ); + if ( codepoint ) { + InputEvent input( InputEvent::TextInput ); + input.text = { 0, + static_cast( codepoint ) }; + mWindow->getInput()->enqueueEvent( input ); + } + next = params.find( ':', next + 1 ); + } + } } } else if ( final == 'A' || final == 'B' || final == 'C' || final == 'D' || final == 'H' || final == 'F' || final == 'P' || final == 'Q' || diff --git a/src/examples/ui_html/ui_html.cpp b/src/examples/ui_html/ui_html.cpp index 6f753163a..859e1937b 100644 --- a/src/examples/ui_html/ui_html.cpp +++ b/src/examples/ui_html/ui_html.cpp @@ -71,7 +71,7 @@ EE_MAIN_FUNC int main( int argc, char** argv ) { 4 ) ); Log::instance()->setLogLevelThreshold( LogLevel::Debug ); - Log::instance()->setLogToStdOut( true ); + Log::instance()->setLogToStdOut( !Runtime::isOffscreen() ); Log::instance()->setLiveWrite( true ); Http::setDefaultUserAgent( "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like " diff --git a/src/tests/test_all/test.cpp b/src/tests/test_all/test.cpp index ce507e547..d12f18103 100644 --- a/src/tests/test_all/test.cpp +++ b/src/tests/test_all/test.cpp @@ -86,7 +86,7 @@ void EETest::init() { EE = Engine::instance(); Log::instance()->setLiveWrite( true ); - Log::instance()->setLogToStdOut( true ); + Log::instance()->setLogToStdOut( !Runtime::isOffscreen() ); mTranslator.loadFromString( "" " eepp" diff --git a/src/tests/unit_tests/eterm_tests.cpp b/src/tests/unit_tests/eterm_tests.cpp index b8d975730..e4926bb05 100644 --- a/src/tests/unit_tests/eterm_tests.cpp +++ b/src/tests/unit_tests/eterm_tests.cpp @@ -544,6 +544,20 @@ UTEST( eterm, kitty_graphics_chunk_continuations_reject_metadata_and_wrong_actio EXPECT_EQ( KittyGraphicsError::InvalidArgument, protocol.handle( "m=0;BA==" ).error ); } +UTEST( eterm, kitty_graphics_frame_chunks_repeat_frame_action ) { + KittyGraphicsProtocol protocol; + ASSERT_EQ( KittyGraphicsError::None, protocol.handle( "a=t,f=32,s=1,v=1,i=8;AQIDBA==" ).error ); + protocol.takeUpdates(); + EXPECT_EQ( KittyGraphicsError::None, + protocol.handle( "a=f,i=8,r=1,f=32,s=1,v=1,X=1,m=1;BQYH" ).error ); + const auto final = protocol.handle( "a=f,m=0;CA==" ); + EXPECT_EQ( KittyGraphicsError::None, final.error ); + EXPECT_TRUE( final.changed ); + const std::vector expected{ 5, 6, 7, 8 }; + ASSERT_TRUE( protocol.imagePixels( 8 ) != nullptr ); + EXPECT_TRUE( expected == *protocol.imagePixels( 8 ) ); +} + UTEST( eterm, kitty_graphics_image_number_allocates_id_and_echoes_number ) { KittyGraphicsProtocol protocol; auto created = protocol.handle( "a=t,f=32,s=1,v=1,I=77;AQIDBA==" ); @@ -579,6 +593,23 @@ UTEST( eterm, kitty_graphics_rgb_and_zlib_preserve_rgb24 ) { EXPECT_TRUE( updates.front().pixels && expected == *updates.front().pixels ); } +UTEST( eterm, memory_stream_supports_multichunk_compression_output ) { + std::vector sourcePixels( 128 * 1024 ); + Uint32 state = 0x12345678; + for ( Uint8& pixel : sourcePixels ) { + state ^= state << 13; + state ^= state >> 17; + state ^= state << 5; + pixel = static_cast( state ); + } + std::vector compressed( Compression::getMaxCompressedBufferSize( sourcePixels.size() ) ); + IOStreamMemory source( reinterpret_cast( sourcePixels.data() ), + sourcePixels.size() ); + IOStreamMemory destination( reinterpret_cast( compressed.data() ), compressed.size() ); + EXPECT_EQ( Compression::OK, Compression::compress( destination, source ) ); + EXPECT_TRUE( destination.tell() > 16 * 1024 ); +} + UTEST( eterm, kitty_graphics_png_decodes_to_rgba ) { KittyGraphicsProtocol protocol; const auto result = protocol.handle( "a=t,f=100,i=10;" diff --git a/src/tools/uieditor/uieditor.cpp b/src/tools/uieditor/uieditor.cpp index fc904e943..7c4aaff63 100644 --- a/src/tools/uieditor/uieditor.cpp +++ b/src/tools/uieditor/uieditor.cpp @@ -1234,7 +1234,7 @@ void App::init( const Float& pixelDensityConf, const bool& useAppTheme, const st pixelDensity = pixelDensityConf; Log::instance()->setLiveWrite( true ); - Log::instance()->setLogToStdOut( true ); + Log::instance()->setLogToStdOut( !Runtime::isOffscreen() ); mResPath = Sys::getProcessPath();