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();