From 94c3b7ab9910e554ede62f49009b2d715d457b6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Lucas=20Golini?= Date: Sat, 5 Sep 2026 02:54:27 -0300 Subject: [PATCH] window: add headless and Kitty terminal runtimes Add process-level native, headless, and terminal runtime modes while keeping the SDL2/SDL3 window backends responsible for creating the graphics context. Use SDL's offscreen video driver with an eepp-owned logical root FBO for headless rendering. Add caller-owned RGB24/RGBA32 framebuffer readback through the renderer abstraction without leaking GL calls into the Window namespace. Introduce a generic FramePresenter boundary and a Kitty presenter that streams chunked direct RGB24 frames from a bounded worker queue. Optimize Base64 encoding for this streaming path. Add POSIX and Windows terminal lifecycle support, Kitty keyboard input, SGR-Pixels mouse input, focus handling, and terminal pixel-size resize events. Inject translated events through a thread-safe Input queue so applications continue using the normal input path. Make UIApplication independent of physical displays in offscreen modes, preserve native runtime behavior, and report the selected runtime and presentation mode during initialization. Add Sys::setEnv() for portable process environment configuration. --- include/eepp/graphics/framebuffer.hpp | 4 + include/eepp/graphics/renderer/renderer.hpp | 18 + include/eepp/system/sys.hpp | 5 + include/eepp/window.hpp | 1 + include/eepp/window/framepresenter.hpp | 32 ++ include/eepp/window/input.hpp | 14 + include/eepp/window/runtime.hpp | 30 ++ .../window/terminal/kittyframepresenter.hpp | 45 ++ .../eepp/window/terminal/terminalruntime.hpp | 74 +++ include/eepp/window/window.hpp | 42 ++ src/eepp/graphics/framebufferfbo.cpp | 24 +- src/eepp/graphics/framebufferfbo.hpp | 7 +- src/eepp/graphics/renderer/renderer.cpp | 32 +- src/eepp/system/base64.cpp | 47 +- src/eepp/system/sys.cpp | 8 + src/eepp/ui/uiapplication.cpp | 36 +- src/eepp/window/backend/SDL2/inputsdl2.cpp | 2 + src/eepp/window/backend/SDL2/windowsdl2.cpp | 17 + src/eepp/window/backend/SDL3/inputsdl3.cpp | 2 + src/eepp/window/backend/SDL3/windowsdl3.cpp | 17 + src/eepp/window/engine.cpp | 36 ++ src/eepp/window/input.cpp | 25 + src/eepp/window/kittyframepresenter.cpp | 130 ++++++ src/eepp/window/runtime.cpp | 44 ++ src/eepp/window/terminalruntime.cpp | 436 ++++++++++++++++++ src/eepp/window/window.cpp | 116 +++-- 26 files changed, 1163 insertions(+), 81 deletions(-) create mode 100644 include/eepp/window/framepresenter.hpp create mode 100644 include/eepp/window/runtime.hpp create mode 100644 include/eepp/window/terminal/kittyframepresenter.hpp create mode 100644 include/eepp/window/terminal/terminalruntime.hpp create mode 100644 src/eepp/window/kittyframepresenter.cpp create mode 100644 src/eepp/window/runtime.cpp create mode 100644 src/eepp/window/terminalruntime.cpp diff --git a/include/eepp/graphics/framebuffer.hpp b/include/eepp/graphics/framebuffer.hpp index 7bf2b66ca..7591fcb67 100644 --- a/include/eepp/graphics/framebuffer.hpp +++ b/include/eepp/graphics/framebuffer.hpp @@ -43,6 +43,10 @@ class EE_API FrameBuffer { *position 0,0 means 0,0 point in the frame buffer, not the screen. */ virtual void bind() = 0; + /** Selects this framebuffer as the readback source without changing the current view or + * clipping state. */ + virtual void bindAsReadTarget() = 0; + /** @brief Disables the off-screen rendering. ** Anything rendered after this will be rendered to the back-buffer. */ virtual void unbind() = 0; diff --git a/include/eepp/graphics/renderer/renderer.hpp b/include/eepp/graphics/renderer/renderer.hpp index 6960e4452..57659a600 100644 --- a/include/eepp/graphics/renderer/renderer.hpp +++ b/include/eepp/graphics/renderer/renderer.hpp @@ -27,6 +27,8 @@ class RendererGLES2; */ class EE_API Renderer { public: + /** Pixel layouts supported by framebuffer readback operations. */ + enum class PixelFormat : Uint8 { RGB24, RGBA32 }; /** @return The graphic library renderer version from a string. */ static GraphicsLibraryVersion glVersionFromString( std::string glVersion ); @@ -74,6 +76,9 @@ class EE_API Renderer { bool pointSpriteSupported(); + /** Configures point-sprite coordinate replacement when required by the active graphics API. */ + void configurePointSprite(); + bool shadersSupported(); void clear( unsigned int mask ); @@ -360,6 +365,19 @@ class EE_API Renderer { void readPixels( int x, int y, unsigned int width, unsigned int height, void* pixels ); + /** Reads a framebuffer rectangle into caller-owned storage. + * @param x Left coordinate of the rectangle. + * @param y Bottom coordinate of the rectangle. + * @param width Rectangle width in pixels. + * @param height Rectangle height in pixels. + * @param format Destination pixel format. + * @param pixels Destination storage. + * @param stride Destination row stride in bytes. + * @return True if the arguments are valid and the pixels were read. + */ + bool readPixels( int x, int y, unsigned int width, unsigned int height, PixelFormat format, + void* pixels, size_t stride ); + Color readPixel( int x, int y ); void waitForIdle(); diff --git a/include/eepp/system/sys.hpp b/include/eepp/system/sys.hpp index 6f240453e..6e5aaa479 100644 --- a/include/eepp/system/sys.hpp +++ b/include/eepp/system/sys.hpp @@ -112,6 +112,11 @@ class EE_API Sys { /** @return An environment variable */ static std::string getEnv( const std::string& name ); + /** Sets a null-terminated environment variable name and value for the current process. + * @return True if the environment was updated successfully. + */ + static bool setEnv( const char* name, const char* value ); + /** @return A split environment variable */ static std::vector getEnvSplit( const std::string& name ); diff --git a/include/eepp/window.hpp b/include/eepp/window.hpp index 13fddef3b..f96f68b00 100644 --- a/include/eepp/window.hpp +++ b/include/eepp/window.hpp @@ -17,6 +17,7 @@ #include #include #include +#include #include #include #include diff --git a/include/eepp/window/framepresenter.hpp b/include/eepp/window/framepresenter.hpp new file mode 100644 index 000000000..3b2e89d00 --- /dev/null +++ b/include/eepp/window/framepresenter.hpp @@ -0,0 +1,32 @@ +#ifndef EE_WINDOW_FRAMEPRESENTER_HPP +#define EE_WINDOW_FRAMEPRESENTER_HPP + +#include + +namespace EE { namespace Window { + +class Window; + +/** Presentation boundary used to export completed Window frames to an external transport. */ +class FramePresenter { + public: + virtual ~FramePresenter() = default; + + /** Initializes the presenter for a window. + * @return True when presentation can begin. + */ + virtual bool initialize( Window& ) = 0; + + /** Queues or presents the window's completed frame. */ + virtual void present( Window& ) = 0; + + /** Notifies the presenter that the logical framebuffer size changed. */ + virtual void resized( Window&, const Math::Sizei& ) {} + + /** Releases resources associated with the window. */ + virtual void shutdown( Window& ) {} +}; + +}} // namespace EE::Window + +#endif diff --git a/include/eepp/window/input.hpp b/include/eepp/window/input.hpp index d37d665cf..96aa722d0 100644 --- a/include/eepp/window/input.hpp +++ b/include/eepp/window/input.hpp @@ -8,6 +8,9 @@ #include #include +#include +#include + using namespace EE::Graphics; namespace EE { namespace Window { @@ -248,6 +251,12 @@ class EE_API Input { /** Process an input event. Called by the input update. */ void processEvent( InputEvent* Event ); + /** Queues an event from any producer thread for processing during the next normal update cycle. + * If the bounded queue is full, an older mouse-motion event can be discarded to make room. + * @return True if the event was queued. + */ + bool enqueueEvent( InputEvent event ); + /** @return An id of the current event update processed ( */ const Uint64& getEventsSentId() const; @@ -301,10 +310,15 @@ class EE_API Input { Clock mLastMouseEvent; std::map mCallbacks; + std::mutex mInjectedEventsMutex; + std::deque mInjectedEvents; InputFinger* getFingerId( const Int64& fingerId ); void resetFingerWasDown(); + + /** Processes all events injected by producer threads. Called by backend update cycles. */ + void drainQueuedEvents(); }; }} // namespace EE::Window diff --git a/include/eepp/window/runtime.hpp b/include/eepp/window/runtime.hpp new file mode 100644 index 000000000..55295a5f7 --- /dev/null +++ b/include/eepp/window/runtime.hpp @@ -0,0 +1,30 @@ +#ifndef EE_WINDOW_RUNTIME_HPP +#define EE_WINDOW_RUNTIME_HPP + +#include + +namespace EE { namespace Window { + +/** Process-level execution mode, independent of the selected WindowBackend. */ +enum class RuntimeMode : Uint8 { + Native, //!< Normal platform window and display presentation. + Headless, //!< Offscreen rendering without presentation. + Terminal //!< Offscreen rendering presented through a terminal transport. +}; + +/** Process-level runtime selection, resolved once from EEPP_RUNTIME. */ +class EE_API Runtime { + public: + /** @return The process runtime mode resolved from EEPP_RUNTIME on first use. */ + static RuntimeMode mode(); + + /** @return True when the runtime requires an offscreen video driver and logical framebuffer. */ + static bool isOffscreen(); + + /** @return A stable lowercase name for the resolved runtime mode. */ + static const char* modeName(); +}; + +}} // namespace EE::Window + +#endif diff --git a/include/eepp/window/terminal/kittyframepresenter.hpp b/include/eepp/window/terminal/kittyframepresenter.hpp new file mode 100644 index 000000000..e95a56aab --- /dev/null +++ b/include/eepp/window/terminal/kittyframepresenter.hpp @@ -0,0 +1,45 @@ +#ifndef EE_WINDOW_TERMINAL_KITTYFRAMEPRESENTER_HPP +#define EE_WINDOW_TERMINAL_KITTYFRAMEPRESENTER_HPP + +#include +#include +#include +#include +#include + +namespace EE { namespace Window { +/** Presents completed window frames using the Kitty graphics protocol. */ +class KittyFramePresenter final : public FramePresenter { + public: + ~KittyFramePresenter(); + + /** Initializes the terminal transport and sender worker. */ + bool initialize( Window& ) override; + + /** Captures and queues the newest complete RGB24 frame. */ + void present( Window& ) override; + + /** Stops the sender worker and restores the terminal transport. */ + void shutdown( Window& ) override; + + private: + struct Frame { + std::vector pixels; + Math::Sizei size; + }; + std::mutex mMutex; + std::condition_variable mCondition; + std::thread mWorker; + Frame mPending; + Frame mRecycle; + bool mRunning{ false }; + bool mHasPending{ false }; + /** 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& ); +}; + +}} // namespace EE::Window + +#endif diff --git a/include/eepp/window/terminal/terminalruntime.hpp b/include/eepp/window/terminal/terminalruntime.hpp new file mode 100644 index 000000000..53b06d2ee --- /dev/null +++ b/include/eepp/window/terminal/terminalruntime.hpp @@ -0,0 +1,74 @@ +#ifndef EE_WINDOW_TERMINAL_TERMINALRUNTIME_HPP +#define EE_WINDOW_TERMINAL_TERMINALRUNTIME_HPP + +#include +#include +#include +#include +#include + +namespace EE { namespace Window { +class Window; + +/** Process-level terminal transport and input runtime. + * + * Owns terminal mode negotiation, input parsing, synchronized protocol output, and attachment to + * the single top-level Window currently supported by terminal mode. + */ +class EE_API TerminalRuntime { + public: + /** @return The process terminal runtime instance. */ + static TerminalRuntime& instance(); + + ~TerminalRuntime(); + + /** Opens the controlling terminal, enables terminal protocols, and starts input processing. + * @return True if the terminal transport was initialized or was already initialized. + */ + bool initialize(); + + /** Stops input processing and restores the terminal state. */ + void shutdown(); + + /** Writes a complete protocol fragment to the terminal without interleaving other writers. + * @return True if all bytes were written. + */ + bool write( const char* data, size_t size ); + + /** @return The terminal viewport size in pixels, or an empty size when unavailable. */ + Math::Sizei pixelSize() const; + + /** Attaches the top-level window that receives decoded terminal input and resize events. */ + void attach( Window& window ); + + /** Detaches the currently attached window. */ + void detach(); + + /** @return Whether the host terminal currently reports input focus. */ + bool isFocused() const { return mFocused.load( std::memory_order_relaxed ); } + + private: + TerminalRuntime() = default; + + TerminalRuntime( const TerminalRuntime& ) = delete; + + TerminalRuntime& operator=( const TerminalRuntime& ) = delete; + + struct State; + State* mState{ nullptr }; + std::mutex mWriteMutex; + std::thread mInputThread; + std::atomic mReading{ false }; + std::atomic mFocused{ true }; + Window* mWindow{ nullptr }; +#if defined( EE_PLATFORM_POSIX ) + int mFd{ -1 }; +#endif + + /** Reads terminal protocol input and enqueues translated neutral InputEvents. */ + void readInput(); +}; + +}} // namespace EE::Window + +#endif diff --git a/include/eepp/window/window.hpp b/include/eepp/window/window.hpp index 82322acce..5b1c286fd 100644 --- a/include/eepp/window/window.hpp +++ b/include/eepp/window/window.hpp @@ -1,10 +1,12 @@ #ifndef EE_WINDOWCWINDOW_HPP #define EE_WINDOWCWINDOW_HPP +#include #include #include #include #include +#include #include #include @@ -43,6 +45,28 @@ enum class WindowFlashOperation { enum class WindowBackend : Uint32 { SDL2, SDL3, Default }; +/** Pixel layout requested from Window::readFrameBuffer(). */ +enum class FramePixelFormat : Uint8 { RGB24, RGBA32 }; + +/** Row orientation of framebuffer readback storage. */ +enum class FrameOrigin : Uint8 { BottomLeft, TopLeft }; + +/** Description of caller-owned storage for a framebuffer rectangle readback. */ +struct FrameReadback { + /** Destination pixel storage. */ + Uint8* pixels{ nullptr }; + /** Rectangle size in pixels. */ + Sizei size; + /** Destination row stride in bytes. */ + size_t stride{ 0 }; + /** Bottom-left framebuffer coordinate of the rectangle. */ + Vector2i position; + /** Destination pixel layout. */ + FramePixelFormat format{ FramePixelFormat::RGB24 }; + /** Requested destination row orientation. */ + FrameOrigin origin{ FrameOrigin::BottomLeft }; +}; + #ifndef EE_SCREEN_KEYBOARD_ENABLED #define EE_SCREEN_KEYBOARD_ENABLED false #endif @@ -376,6 +400,13 @@ class EE_API Window { */ Image getFrontBufferImage(); + /** Reads a framebuffer rectangle into caller-owned storage without allocating an Image. + * The destination must hold at least `stride * size.y` bytes. TopLeft origin is written + * directly in that orientation without a separate full-frame flip allocation. + * @return True if the readback description is valid and the pixels were read. + */ + bool readFrameBuffer( FrameReadback& readback ); + /** @return The pointer to the Window Info ( read only ) */ const WindowInfo* getWindowInfo() const; @@ -549,6 +580,8 @@ class EE_API Window { Sizei mLastWindowedSize; InputMethod mIME; std::function mMainLoop; + FrameBufferUniquePtr mLogicalFrameBuffer; + std::unique_ptr mFramePresenter; class FrameData { public: @@ -596,6 +629,15 @@ class EE_API Window { void createView(); + /** Creates the logical root framebuffer and presenter required by the active runtime. */ + bool initializeRuntimeRenderTarget(); + + /** Resizes the logical root framebuffer and notifies its presenter. */ + void resizeRuntimeRenderTarget( Uint32 width, Uint32 height ); + + /** Releases runtime presentation and logical framebuffer resources. */ + void shutdownRuntimeRenderTarget(); + void calculateFps(); void limitFps(); diff --git a/src/eepp/graphics/framebufferfbo.cpp b/src/eepp/graphics/framebufferfbo.cpp index dd8a7923f..e5d27daaf 100644 --- a/src/eepp/graphics/framebufferfbo.cpp +++ b/src/eepp/graphics/framebufferfbo.cpp @@ -21,7 +21,8 @@ FrameBufferFBO::FrameBufferFBO( EE::Window::Window* window ) : mLastFB( 0 ), mLastCB( 0 ), mLastDB( 0 ), - mLastSB( 0 ) {} + mLastSB( 0 ), + mCreated( false ) {} FrameBufferFBO::FrameBufferFBO( const Uint32& Width, const Uint32& Height, bool StencilBuffer, bool DepthBuffer, bool useColorBuffer, const Uint32& channels, @@ -34,8 +35,9 @@ FrameBufferFBO::FrameBufferFBO( const Uint32& Width, const Uint32& Height, bool mLastFB( 0 ), mLastCB( 0 ), mLastDB( 0 ), - mLastSB( 0 ) { - create( Width, Height, StencilBuffer, DepthBuffer, useColorBuffer, channels ); + mLastSB( 0 ), + mCreated( false ) { + mCreated = create( Width, Height, StencilBuffer, DepthBuffer, useColorBuffer, channels ); } FrameBufferFBO::~FrameBufferFBO() { @@ -70,7 +72,8 @@ FrameBufferFBO::~FrameBufferFBO() { } bool FrameBufferFBO::create( const Uint32& Width, const Uint32& Height ) { - return create( Width, Height, true, false, false, 4 ); + mCreated = create( Width, Height, true, false, false, 4 ); + return mCreated; } bool FrameBufferFBO::create( const Uint32& Width, const Uint32& Height, bool StencilBuffer, @@ -200,6 +203,13 @@ void FrameBufferFBO::bind() { } } +void FrameBufferFBO::bindAsReadTarget() { + if ( mFrameBuffer ) { + GlobalBatchRenderer::instance()->draw(); + GLi->bindFramebuffer( GL_FRAMEBUFFER, mFrameBuffer ); + } +} + void FrameBufferFBO::unbind() { if ( mFrameBuffer ) { recoverView(); @@ -215,6 +225,9 @@ void FrameBufferFBO::resize( const Uint32& Width, const Uint32& Height ) { mSize.x = Width; mSize.y = Height; + // Resizing is not a bind-stack operation. Preserve the target to which unbind() must eventually + // return, especially for the logical root FBO which remains bound across frames. + const Int32 lastFB = mLastFB; bindFrameBuffer(); if ( mHasDepthBuffer ) { @@ -255,6 +268,7 @@ void FrameBufferFBO::resize( const Uint32& Width, const Uint32& Height ) { } GLi->bindFramebuffer( GL_FRAMEBUFFER, mLastFB ); + mLastFB = lastFB; } void FrameBufferFBO::draw( const Vector2f& position, const Sizef& size ) { @@ -289,7 +303,7 @@ void FrameBufferFBO::draw( Rect src, Rect dst ) { } bool FrameBufferFBO::created() const { - return mFrameBuffer != 0; + return mCreated; } const Int32& FrameBufferFBO::getFrameBufferId() const { diff --git a/src/eepp/graphics/framebufferfbo.hpp b/src/eepp/graphics/framebufferfbo.hpp index 641de94d0..76ca10b8d 100644 --- a/src/eepp/graphics/framebufferfbo.hpp +++ b/src/eepp/graphics/framebufferfbo.hpp @@ -19,6 +19,8 @@ class EE_API FrameBufferFBO : public FrameBuffer { void bind(); + void bindAsReadTarget(); + void unbind(); void resize( const Uint32& Width, const Uint32& Height ); @@ -42,11 +44,12 @@ class EE_API FrameBufferFBO : public FrameBuffer { Int32 mLastCB; Int32 mLastDB; Int32 mLastSB; + bool mCreated; bool create( const Uint32& Width, const Uint32& Height ); - bool create( const Uint32& Width, const Uint32& Height, bool StencilBuffer, bool useColorBuffer, - bool DepthBuffer, const Uint32& channels ); + bool create( const Uint32& Width, const Uint32& Height, bool StencilBuffer, bool DepthBuffer, + bool useColorBuffer, const Uint32& channels ); void bindFrameBuffer(); diff --git a/src/eepp/graphics/renderer/renderer.cpp b/src/eepp/graphics/renderer/renderer.cpp index 4881d5e3b..203ef4004 100644 --- a/src/eepp/graphics/renderer/renderer.cpp +++ b/src/eepp/graphics/renderer/renderer.cpp @@ -397,6 +397,14 @@ bool Renderer::pointSpriteSupported() { #endif } +void Renderer::configurePointSprite() { + if ( GLv_3CP != version() && GLv_3 != version() && GLv_ES2 != version() ) { +#if !defined( EE_GLES2 ) || defined( EE_GLES_BOTH ) + glTexEnvi( GL_POINT_SPRITE, GL_COORD_REPLACE, GL_TRUE ); +#endif + } +} + bool Renderer::shadersSupported() { #ifdef EE_GLES return ( GLv_ES2 == version() || GLv_3 == version() || GLv_3CP == version() ); @@ -863,6 +871,29 @@ void Renderer::readPixels( int x, int y, unsigned int width, unsigned int height glReadPixels( x, y, width, height, GL_RGBA, GL_UNSIGNED_BYTE, pixels ); } +bool Renderer::readPixels( int x, int y, unsigned int width, unsigned int height, + PixelFormat pixelFormat, void* pixels, size_t stride ) { + const unsigned int channels = pixelFormat == PixelFormat::RGB24 ? 3 : 4; + const size_t rowBytes = static_cast( width ) * channels; + if ( nullptr == pixels || stride < rowBytes ) + return false; + GLint packAlignment; + glGetIntegerv( GL_PACK_ALIGNMENT, &packAlignment ); + if ( packAlignment != 1 ) + pixelStorei( GL_PACK_ALIGNMENT, 1 ); + const GLenum format = pixelFormat == PixelFormat::RGB24 ? GL_RGB : GL_RGBA; + if ( stride == rowBytes ) { + glReadPixels( x, y, width, height, format, GL_UNSIGNED_BYTE, pixels ); + } else { + Uint8* output = static_cast( pixels ); + for ( unsigned int row = 0; row < height; ++row ) + glReadPixels( x, y + row, width, 1, format, GL_UNSIGNED_BYTE, output + row * stride ); + } + if ( packAlignment != 1 ) + pixelStorei( GL_PACK_ALIGNMENT, packAlignment ); + return true; +} + Color Renderer::readPixel( int x, int y ) { Uint8 pixel[4]; readPixels( x, y, 1, 1, pixel ); @@ -1256,7 +1287,6 @@ void Renderer::genVertexArrays( int n, unsigned int* arrays ) { #endif } - void Renderer::waitForIdle() { glFinish(); } diff --git a/src/eepp/system/base64.cpp b/src/eepp/system/base64.cpp index d41235e0e..767cf3b29 100644 --- a/src/eepp/system/base64.cpp +++ b/src/eepp/system/base64.cpp @@ -174,40 +174,31 @@ size_t Base64::decode( size_t in_len, const char* in, size_t out_len, unsigned c size_t Base64::encode( size_t in_len, const unsigned char* in, size_t out_len, char* out ) { static const Uint8 base64enc_tab[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + const size_t encodedLen = ( ( in_len + 2 ) / 3 ) * 4; + if ( out_len <= encodedLen ) + return static_cast( -1 ); - size_t ii, io; - Uint32 v; - unsigned rem; - - for ( io = 0, ii = 0, v = 0, rem = 0; ii < in_len; ii++ ) { - unsigned char ch; - ch = in[ii]; - v = ( v << 8 ) | ch; - rem += 8; - while ( rem >= 6 ) { - rem -= 6; - if ( io >= out_len ) - return static_cast( -1 ); /* truncation is failure */ - out[io++] = base64enc_tab[( v >> rem ) & 63]; - } + size_t ii = 0; + size_t io = 0; + const size_t bulkEnd = in_len - in_len % 3; + for ( ; ii < bulkEnd; ii += 3 ) { + const Uint32 value = ( static_cast( in[ii] ) << 16 ) | + ( static_cast( in[ii + 1] ) << 8 ) | in[ii + 2]; + out[io++] = base64enc_tab[value >> 18]; + out[io++] = base64enc_tab[( value >> 12 ) & 63]; + out[io++] = base64enc_tab[( value >> 6 ) & 63]; + out[io++] = base64enc_tab[value & 63]; } - if ( rem ) { - v <<= ( 6 - rem ); - if ( io >= out_len ) - return static_cast( -1 ); /* truncation is failure */ - out[io++] = base64enc_tab[v & 63]; - } - - while ( io & 3 ) { - if ( io >= out_len ) - return static_cast( -1 ); /* truncation is failure */ + if ( ii < in_len ) { + const Uint32 value = static_cast( in[ii] ) << 16 | + ( ii + 1 < in_len ? static_cast( in[ii + 1] ) << 8 : 0 ); + out[io++] = base64enc_tab[value >> 18]; + out[io++] = base64enc_tab[( value >> 12 ) & 63]; + out[io++] = ii + 1 < in_len ? base64enc_tab[( value >> 6 ) & 63] : '='; out[io++] = '='; } - if ( io >= out_len ) - return static_cast( -1 ); /* no room for null terminator */ - out[io] = 0; return io; } diff --git a/src/eepp/system/sys.cpp b/src/eepp/system/sys.cpp index da88b5fd7..26321ede6 100644 --- a/src/eepp/system/sys.cpp +++ b/src/eepp/system/sys.cpp @@ -1228,6 +1228,14 @@ std::string Sys::getEnv( const std::string& name ) { #endif } +bool Sys::setEnv( const char* name, const char* value ) { +#if EE_PLATFORM == EE_PLATFORM_WIN + return 0 == _putenv_s( name, value ); +#else + return 0 == setenv( name, value, 1 ); +#endif +} + std::vector Sys::getEnvSplit( const std::string& name ) { return String::split( getEnv( name.c_str() ), PATH_SEP_CHAR ); } diff --git a/src/eepp/ui/uiapplication.cpp b/src/eepp/ui/uiapplication.cpp index 17a0f1e90..6712406a8 100644 --- a/src/eepp/ui/uiapplication.cpp +++ b/src/eepp/ui/uiapplication.cpp @@ -12,6 +12,7 @@ #include #include #include +#include #include #include @@ -48,30 +49,37 @@ UIApplication::UIApplication( const WindowSettings& windowSettings, const Settin } DisplayManager* displayManager = Engine::instance()->getDisplayManager(); - displayManager->enableScreenSaver(); - displayManager->enableMouseFocusClickThrough(); - displayManager->disableBypassCompositor(); + const bool offscreen = Runtime::isOffscreen(); + if ( !offscreen ) { + displayManager->enableScreenSaver(); + displayManager->enableMouseFocusClickThrough(); + displayManager->disableBypassCompositor(); + } - if ( displayManager->getDisplayIndex( 0 ) == nullptr ) { + if ( !offscreen && displayManager->getDisplayIndex( 0 ) == nullptr ) { std::cerr << "Display not found, exiting" << std::endl; return; } mWindow = Engine::instance()->createWindow( windowSettings, contextSettings ); - if ( !mWindow->isOpen() ) { + if ( nullptr == mWindow || !mWindow->isOpen() ) { std::cerr << "Could not create window, exiting" << std::endl; return; } mDidRun = true; - PixelDensity::setPixelDensity( - appSettings.pixelDensity && *appSettings.pixelDensity > 0 - ? *appSettings.pixelDensity - : eemax( mWindow->getScale(), - displayManager->getDisplayIndex( mWindow->getCurrentDisplayIndex() ) - ->getPixelDensity() ) ); + if ( appSettings.pixelDensity && *appSettings.pixelDensity > 0 ) { + PixelDensity::setPixelDensity( *appSettings.pixelDensity ); + } else if ( offscreen ) { + PixelDensity::setPixelDensity( 1.f ); + } else { + PixelDensity::setPixelDensity( + eemax( mWindow->getScale(), + displayManager->getDisplayIndex( mWindow->getCurrentDisplayIndex() ) + ->getPixelDensity() ) ); + } if ( !appSettings.basePath || appSettings.basePath->empty() ) { FileSystem::changeWorkingDirectory( Sys::getProcessPath() ); @@ -154,10 +162,14 @@ UISceneNode* UIApplication::getUI() const { } int UIApplication::run() { + // Offscreen SDL windows do not receive an initial expose event. Ensure the first logical + // framebuffer is rendered even when the scene was fully laid out before entering the loop. + if ( Runtime::isOffscreen() ) + mUISceneNode->invalidate( nullptr ); + mWindow->runMainLoop( [this]() { mWindow->getInput()->update(); SceneManager::instance()->update(); - if ( mUISceneNode->invalidated() ) { mWindow->clear(); diff --git a/src/eepp/window/backend/SDL2/inputsdl2.cpp b/src/eepp/window/backend/SDL2/inputsdl2.cpp index 45c1b18de..5c36dd189 100644 --- a/src/eepp/window/backend/SDL2/inputsdl2.cpp +++ b/src/eepp/window/backend/SDL2/inputsdl2.cpp @@ -28,6 +28,8 @@ void InputSDL::update() { if ( mEventsSentId == std::numeric_limits::max() ) mEventsSentId = 0; + drainQueuedEvents(); + if ( !mQueuedEvents.empty() ) { for ( const auto& prevEvent : mQueuedEvents ) sendEvent( prevEvent ); diff --git a/src/eepp/window/backend/SDL2/windowsdl2.cpp b/src/eepp/window/backend/SDL2/windowsdl2.cpp index 9f55840f1..1a9b62bda 100644 --- a/src/eepp/window/backend/SDL2/windowsdl2.cpp +++ b/src/eepp/window/backend/SDL2/windowsdl2.cpp @@ -15,6 +15,8 @@ #include #include #include +#include +#include #include @@ -45,6 +47,7 @@ WindowSDL::WindowSDL( WindowSettings Settings, ContextSettings Context ) : } WindowSDL::~WindowSDL() { + shutdownRuntimeRenderTarget(); destroySDLResources(); } @@ -275,6 +278,9 @@ bool WindowSDL::create( WindowSettings Settings, ContextSettings Context ) { setup2D( false ); + if ( !initializeRuntimeRenderTarget() ) + return false; + mWindow.Created = true; if ( "" != mWindow.WindowConfig.Icon ) { @@ -411,25 +417,35 @@ void WindowSDL::setTitle( const std::string& title ) { } bool WindowSDL::isActive() const { + if ( Runtime::mode() == RuntimeMode::Terminal ) + return TerminalRuntime::instance().isFocused(); Uint32 flags = SDL_GetWindowFlags( mSDLWindow ); return 0 != ( ( flags & SDL_WINDOW_INPUT_FOCUS ) && ( flags & SDL_WINDOW_MOUSE_FOCUS ) ); } bool WindowSDL::isVisible() const { + if ( Runtime::mode() == RuntimeMode::Terminal ) + return true; Uint32 flags = SDL_GetWindowFlags( mSDLWindow ); return 0 != ( ( flags & SDL_WINDOW_SHOWN ) && !( flags & SDL_WINDOW_MINIMIZED ) ); } bool WindowSDL::hasFocus() const { + if ( Runtime::mode() == RuntimeMode::Terminal ) + return TerminalRuntime::instance().isFocused(); return 0 != ( SDL_GetWindowFlags( mSDLWindow ) & ( SDL_WINDOW_INPUT_FOCUS | SDL_WINDOW_MOUSE_FOCUS ) ); } bool WindowSDL::hasInputFocus() const { + if ( Runtime::mode() == RuntimeMode::Terminal ) + return TerminalRuntime::instance().isFocused(); return 0 != ( SDL_GetWindowFlags( mSDLWindow ) & ( SDL_WINDOW_INPUT_FOCUS ) ); } bool WindowSDL::hasMouseFocus() const { + if ( Runtime::mode() == RuntimeMode::Terminal ) + return TerminalRuntime::instance().isFocused(); return 0 != ( SDL_GetWindowFlags( mSDLWindow ) & ( SDL_WINDOW_MOUSE_FOCUS ) ); } @@ -447,6 +463,7 @@ void WindowSDL::onWindowResize( Uint32 width, Uint32 height ) { mLastWindowedSize = Sizei( width, height ); mDefaultView.reset( Rectf( 0, 0, mWindow.WindowConfig.Width, mWindow.WindowConfig.Height ) ); + resizeRuntimeRenderTarget( width, height ); setup2D( false ); diff --git a/src/eepp/window/backend/SDL3/inputsdl3.cpp b/src/eepp/window/backend/SDL3/inputsdl3.cpp index 6bfd95df3..e8742770b 100644 --- a/src/eepp/window/backend/SDL3/inputsdl3.cpp +++ b/src/eepp/window/backend/SDL3/inputsdl3.cpp @@ -24,6 +24,8 @@ void InputSDL::update() { if ( mEventsSentId == std::numeric_limits::max() ) mEventsSentId = 0; + drainQueuedEvents(); + if ( !mQueuedEvents.empty() ) { for ( const auto& prevEvent : mQueuedEvents ) sendEvent( prevEvent ); diff --git a/src/eepp/window/backend/SDL3/windowsdl3.cpp b/src/eepp/window/backend/SDL3/windowsdl3.cpp index 48783d520..b08e42831 100644 --- a/src/eepp/window/backend/SDL3/windowsdl3.cpp +++ b/src/eepp/window/backend/SDL3/windowsdl3.cpp @@ -16,6 +16,8 @@ #include #include #include +#include +#include #if EE_PLATFORM == EE_PLATFORM_WIN #include @@ -36,6 +38,7 @@ WindowSDL::WindowSDL( WindowSettings Settings, ContextSettings Context ) : } WindowSDL::~WindowSDL() { + shutdownRuntimeRenderTarget(); destroySDLResources(); } @@ -252,6 +255,9 @@ bool WindowSDL::create( WindowSettings Settings, ContextSettings Context ) { setup2D( false ); + if ( !initializeRuntimeRenderTarget() ) + return false; + mWindow.Created = true; if ( "" != mWindow.WindowConfig.Icon ) { @@ -382,26 +388,36 @@ void WindowSDL::setTitle( const std::string& title ) { } bool WindowSDL::isActive() const { + if ( Runtime::mode() == RuntimeMode::Terminal ) + return TerminalRuntime::instance().isFocused(); Uint64 flags = SDL_GetWindowFlags( mSDLWindow ); return 0 != ( ( flags & SDL_WINDOW_INPUT_FOCUS ) && ( flags & SDL_WINDOW_MOUSE_FOCUS ) ); } bool WindowSDL::isVisible() const { + if ( Runtime::mode() == RuntimeMode::Terminal ) + return true; Uint64 flags = SDL_GetWindowFlags( mSDLWindow ); return 0 != ( !( flags & SDL_WINDOW_HIDDEN ) && !( flags & SDL_WINDOW_MINIMIZED ) ); } bool WindowSDL::hasFocus() const { + if ( Runtime::mode() == RuntimeMode::Terminal ) + return TerminalRuntime::instance().isFocused(); Uint64 flags = SDL_GetWindowFlags( mSDLWindow ); return 0 != ( flags & ( SDL_WINDOW_INPUT_FOCUS | SDL_WINDOW_MOUSE_FOCUS ) ); } bool WindowSDL::hasInputFocus() const { + if ( Runtime::mode() == RuntimeMode::Terminal ) + return TerminalRuntime::instance().isFocused(); Uint64 flags = SDL_GetWindowFlags( mSDLWindow ); return 0 != ( flags & SDL_WINDOW_INPUT_FOCUS ); } bool WindowSDL::hasMouseFocus() const { + if ( Runtime::mode() == RuntimeMode::Terminal ) + return TerminalRuntime::instance().isFocused(); Uint64 flags = SDL_GetWindowFlags( mSDLWindow ); return 0 != ( flags & SDL_WINDOW_MOUSE_FOCUS ); } @@ -420,6 +436,7 @@ void WindowSDL::onWindowResize( Uint32 width, Uint32 height ) { mLastWindowedSize = Sizei( width, height ); mDefaultView.reset( Rectf( 0, 0, mWindow.WindowConfig.Width, mWindow.WindowConfig.Height ) ); + resizeRuntimeRenderTarget( width, height ); setup2D( false ); diff --git a/src/eepp/window/engine.cpp b/src/eepp/window/engine.cpp index 4acab273f..836aee783 100644 --- a/src/eepp/window/engine.cpp +++ b/src/eepp/window/engine.cpp @@ -30,6 +30,8 @@ #include #endif #include +#include +#include #if EE_PLATFORM == EE_PLATFORM_ANDROID #include @@ -52,6 +54,17 @@ using namespace EE::Graphics; namespace EE { namespace Window { +namespace { + +void configureRuntimeVideoDriver() { + if ( !Runtime::isOffscreen() ) + return; + Sys::setEnv( "SDL_VIDEODRIVER", "offscreen" ); + Sys::setEnv( "SDL_VIDEO_DRIVER", "offscreen" ); +} + +} // namespace + static UintPtr sMainThreadId{ 0 }; SINGLETON_DECLARE_IMPLEMENTATION( Engine ) @@ -65,6 +78,7 @@ Engine::Engine() : mDisplayManager( NULL ), mGlobalResourceCatalog( ResourceCatalog::New() ), mDefaultResourceScope( ResourceScope::New() ) { + configureRuntimeVideoDriver(); mDefaultResourceScope->importCatalog( mGlobalResourceCatalog ); #if EE_PLATFORM == EE_PLATFORM_ANDROID mZip = Zip::New(); @@ -104,6 +118,9 @@ Engine::~Engine() { Doc::SyntaxDefinitionManager::destroySingleton(); + for ( auto& window : mWindows ) + window.second->shutdownRuntimeRenderTarget(); + Graphics::Private::FrameBufferRegistry::destroySingleton(); Graphics::Private::VertexBufferRegistry::destroySingleton(); @@ -228,6 +245,20 @@ EE::Window::Window* Engine::createDefaultWindow( const WindowSettings& Settings, } EE::Window::Window* Engine::createWindow( WindowSettings Settings, ContextSettings Context ) { + if ( Runtime::mode() == RuntimeMode::Terminal && !mWindows.empty() ) { + Log::error( "Terminal runtime currently supports one top-level Window" ); + return nullptr; + } + if ( Runtime::mode() == RuntimeMode::Terminal ) { + TerminalRuntime& terminal = TerminalRuntime::instance(); + if ( !terminal.initialize() ) + return nullptr; + const Sizei terminalSize = terminal.pixelSize(); + if ( terminalSize.x > 0 && terminalSize.y > 0 ) { + Settings.Width = terminalSize.x; + Settings.Height = terminalSize.y; + } + } EE::Window::Window* window = NULL; if ( NULL != mWindow ) { @@ -246,6 +277,11 @@ EE::Window::Window* Engine::createWindow( WindowSettings Settings, ContextSettin if ( NULL == window ) { window = createDefaultWindow( Settings, Context ); } + if ( NULL == window ) { + if ( Runtime::mode() == RuntimeMode::Terminal ) + TerminalRuntime::instance().shutdown(); + return nullptr; + } setCurrentWindow( window ); diff --git a/src/eepp/window/input.cpp b/src/eepp/window/input.cpp index f8de3679d..def164368 100644 --- a/src/eepp/window/input.cpp +++ b/src/eepp/window/input.cpp @@ -229,6 +229,31 @@ void Input::processEvent( InputEvent* Event ) { sendEvent( Event ); } +bool Input::enqueueEvent( InputEvent event ) { + static constexpr size_t MaxInjectedEvents = 4096; + std::lock_guard lock( mInjectedEventsMutex ); + if ( mInjectedEvents.size() >= MaxInjectedEvents ) { + auto motion = std::find_if( + mInjectedEvents.begin(), mInjectedEvents.end(), + []( const InputEvent& queued ) { return queued.Type == InputEvent::MouseMotion; } ); + if ( motion == mInjectedEvents.end() ) + return false; + mInjectedEvents.erase( motion ); + } + mInjectedEvents.emplace_back( std::move( event ) ); + return true; +} + +void Input::drainQueuedEvents() { + std::deque events; + { + std::lock_guard lock( mInjectedEventsMutex ); + events.swap( mInjectedEvents ); + } + for ( InputEvent& event : events ) + processEvent( &event ); +} + InputFinger* Input::getFingerId( const Int64& fingerId ) { Uint32 i; diff --git a/src/eepp/window/kittyframepresenter.cpp b/src/eepp/window/kittyframepresenter.cpp new file mode 100644 index 000000000..c9d9485d1 --- /dev/null +++ b/src/eepp/window/kittyframepresenter.cpp @@ -0,0 +1,130 @@ +#include +#include +#include +#include + +#include +#include +#include + +using namespace EE::System; + +namespace EE { namespace Window { + +KittyFramePresenter::~KittyFramePresenter() { + { + std::lock_guard lock( mMutex ); + mRunning = false; + } + mCondition.notify_one(); + if ( mWorker.joinable() ) + mWorker.join(); +} + +bool KittyFramePresenter::initialize( Window& window ) { + if ( !TerminalRuntime::instance().initialize() ) + return false; + TerminalRuntime::instance().attach( window ); + mRunning = true; + mWorker = std::thread( &KittyFramePresenter::run, this ); + return true; +} + +void KittyFramePresenter::present( Window& window ) { + Frame frame; + { + std::lock_guard lock( mMutex ); + frame = std::move( mRecycle ); + } + frame.size = window.getSize(); + frame.pixels.resize( static_cast( frame.size.x ) * frame.size.y * 3 ); + FrameReadback readback{ frame.pixels.data(), frame.size, + static_cast( frame.size.x ) * 3 }; + if ( !window.readFrameBuffer( readback ) ) + return; + + { + std::lock_guard lock( mMutex ); + if ( mHasPending ) + mRecycle = std::move( mPending ); + mPending = std::move( frame ); + mHasPending = true; + } + mCondition.notify_one(); +} + +void KittyFramePresenter::shutdown( Window& ) { + { + std::lock_guard lock( mMutex ); + mRunning = false; + } + mCondition.notify_one(); + if ( mWorker.joinable() ) + mWorker.join(); + TerminalRuntime::instance().detach(); + TerminalRuntime::instance().shutdown(); +} + +void KittyFramePresenter::run() { + for ( ;; ) { + Frame frame; + { + std::unique_lock lock( mMutex ); + mCondition.wait( lock, [this] { return !mRunning || mHasPending; } ); + if ( !mRunning && !mHasPending ) + return; + frame = std::move( mPending ); + mHasPending = false; + } + sendFrame( frame ); + std::lock_guard lock( mMutex ); + mRecycle = std::move( frame ); + } +} + +void KittyFramePresenter::sendFrame( const Frame& frame ) { + std::array binary; + 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; + 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; + } + } + 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 ); + if ( encodedSize == static_cast( -1 ) || headerSize <= 0 || + static_cast( headerSize ) >= sizeof( header ) ) + return; + 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; + first = false; + } +} +}} // namespace EE::Window diff --git a/src/eepp/window/runtime.cpp b/src/eepp/window/runtime.cpp new file mode 100644 index 000000000..09b503372 --- /dev/null +++ b/src/eepp/window/runtime.cpp @@ -0,0 +1,44 @@ +#include + +#include +#include + +namespace EE { namespace Window { + +namespace { + +RuntimeMode resolveRuntimeMode() { + const char* value = std::getenv( "EEPP_RUNTIME" ); + if ( nullptr == value ) + return RuntimeMode::Native; + if ( 0 == std::strcmp( value, "headless" ) ) + return RuntimeMode::Headless; + if ( 0 == std::strcmp( value, "terminal" ) ) + return RuntimeMode::Terminal; + return RuntimeMode::Native; +} + +} // namespace + +RuntimeMode Runtime::mode() { + static const RuntimeMode runtimeMode = resolveRuntimeMode(); + return runtimeMode; +} + +bool Runtime::isOffscreen() { + return mode() != RuntimeMode::Native; +} + +const char* Runtime::modeName() { + switch ( mode() ) { + case RuntimeMode::Headless: + return "headless"; + case RuntimeMode::Terminal: + return "terminal"; + case RuntimeMode::Native: + default: + return "native"; + } +} + +}} // namespace EE::Window diff --git a/src/eepp/window/terminalruntime.cpp b/src/eepp/window/terminalruntime.cpp new file mode 100644 index 000000000..8fa3eba59 --- /dev/null +++ b/src/eepp/window/terminalruntime.cpp @@ -0,0 +1,436 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +#if defined( EE_PLATFORM_POSIX ) +#include +#include +#include +#include +#include +#elif EE_PLATFORM == EE_PLATFORM_WIN +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#include +#endif + +namespace EE { namespace Window { + +struct TerminalRuntime::State { +#if defined( EE_PLATFORM_POSIX ) + termios termiosState{}; + bool hasTermios{ false }; +#elif EE_PLATFORM == EE_PLATFORM_WIN + HANDLE input{ INVALID_HANDLE_VALUE }; + HANDLE output{ INVALID_HANDLE_VALUE }; + DWORD inputMode{ 0 }; + DWORD outputMode{ 0 }; + bool hasInputMode{ false }; + bool hasOutputMode{ false }; +#endif +}; + +TerminalRuntime& TerminalRuntime::instance() { + static TerminalRuntime runtime; + return runtime; +} + +TerminalRuntime::~TerminalRuntime() { + shutdown(); +} + +bool TerminalRuntime::initialize() { + if ( mState ) + return true; +#if defined( EE_PLATFORM_POSIX ) + mFd = ::open( "/dev/tty", O_RDWR | O_CLOEXEC ); + if ( mFd < 0 ) { + Log::error( "Terminal runtime could not open /dev/tty" ); + return false; + } + mState = new State; + mState->hasTermios = 0 == tcgetattr( mFd, &mState->termiosState ); + if ( mState->hasTermios ) { + termios raw = mState->termiosState; + cfmakeraw( &raw ); + tcsetattr( mFd, TCSAFLUSH, &raw ); + } + static constexpr char enter[] = "\033[?1049h\033[?25l\033[>31u\033[?1003h\033[?1006h" + "\033[?1016h\033[?1004h\033[H"; + if ( write( enter, sizeof( enter ) - 1 ) ) + return true; + if ( mState->hasTermios ) + tcsetattr( mFd, TCSAFLUSH, &mState->termiosState ); + ::close( mFd ); + mFd = -1; + delete mState; + mState = nullptr; + return false; +#elif EE_PLATFORM == EE_PLATFORM_WIN + mState = new State; + mState->input = GetStdHandle( STD_INPUT_HANDLE ); + mState->output = GetStdHandle( STD_OUTPUT_HANDLE ); + if ( !mState->input || mState->input == INVALID_HANDLE_VALUE || !mState->output || + mState->output == INVALID_HANDLE_VALUE ) { + Log::error( "Terminal runtime could not open the Windows standard handles" ); + delete mState; + mState = nullptr; + return false; + } + mState->hasInputMode = GetConsoleMode( mState->input, &mState->inputMode ); + mState->hasOutputMode = GetConsoleMode( mState->output, &mState->outputMode ); + if ( mState->hasInputMode ) { + const DWORD mode = ( mState->inputMode | ENABLE_VIRTUAL_TERMINAL_INPUT ) & + ~( ENABLE_ECHO_INPUT | ENABLE_LINE_INPUT | ENABLE_PROCESSED_INPUT ); + SetConsoleMode( mState->input, mode ); + } + if ( mState->hasOutputMode ) { + const DWORD mode = + mState->outputMode | ENABLE_VIRTUAL_TERMINAL_PROCESSING | DISABLE_NEWLINE_AUTO_RETURN; + SetConsoleMode( mState->output, mode ); + } + static constexpr char enter[] = "\033[?1049h\033[?25l\033[>31u\033[?1003h\033[?1006h" + "\033[?1016h\033[?1004h\033[H"; + if ( write( enter, sizeof( enter ) - 1 ) ) + return true; + if ( mState->hasInputMode ) + SetConsoleMode( mState->input, mState->inputMode ); + if ( mState->hasOutputMode ) + SetConsoleMode( mState->output, mState->outputMode ); + delete mState; + mState = nullptr; + return false; +#else + Log::error( "Terminal runtime is not implemented on this platform" ); + return false; +#endif +} + +void TerminalRuntime::shutdown() { + detach(); +#if defined( EE_PLATFORM_POSIX ) + if ( mFd < 0 ) + return; + static constexpr char leave[] = "\033[?1004l\033[?1016l\033[?1006l\033[?1003l\033[hasTermios ) + tcsetattr( mFd, TCSAFLUSH, &mState->termiosState ); + ::close( mFd ); + mFd = -1; + delete mState; + mState = nullptr; +#elif EE_PLATFORM == EE_PLATFORM_WIN + if ( !mState ) + return; + static constexpr char leave[] = "\033[?1004l\033[?1016l\033[?1006l\033[?1003l\033[hasInputMode ) + SetConsoleMode( mState->input, mState->inputMode ); + if ( mState->hasOutputMode ) + SetConsoleMode( mState->output, mState->outputMode ); + delete mState; + mState = nullptr; +#endif +} + +bool TerminalRuntime::write( const char* data, size_t size ) { +#if defined( EE_PLATFORM_POSIX ) + std::lock_guard lock( mWriteMutex ); + while ( size ) { + const ssize_t written = ::write( mFd, data, size ); + if ( written < 0 && errno == EINTR ) + continue; + if ( written <= 0 ) + return false; + data += written; + size -= static_cast( written ); + } + return true; +#elif EE_PLATFORM == EE_PLATFORM_WIN + std::lock_guard lock( mWriteMutex ); + while ( size ) { + DWORD written = 0; + const DWORD chunk = static_cast( eemin( size, MAXDWORD ) ); + if ( !WriteFile( mState->output, data, chunk, &written, nullptr ) || written == 0 ) + return false; + data += written; + size -= written; + } + return true; +#else + return false; +#endif +} + +Math::Sizei TerminalRuntime::pixelSize() const { +#if defined( EE_PLATFORM_POSIX ) + winsize size{}; + if ( mFd >= 0 && 0 == ioctl( mFd, TIOCGWINSZ, &size ) && size.ws_xpixel && size.ws_ypixel ) + return { size.ws_xpixel, size.ws_ypixel }; +#elif EE_PLATFORM == EE_PLATFORM_WIN + if ( mState ) { + CONSOLE_SCREEN_BUFFER_INFO info{}; + CONSOLE_FONT_INFO font{}; + if ( GetConsoleScreenBufferInfo( mState->output, &info ) && + GetCurrentConsoleFont( mState->output, FALSE, &font ) ) { + const Int32 columns = info.srWindow.Right - info.srWindow.Left + 1; + const Int32 rows = info.srWindow.Bottom - info.srWindow.Top + 1; + return { columns * font.dwFontSize.X, rows * font.dwFontSize.Y }; + } + } +#endif + return {}; +} + +void TerminalRuntime::attach( Window& window ) { + if ( mReading.exchange( true ) ) + return; + mWindow = &window; + mInputThread = std::thread( &TerminalRuntime::readInput, this ); +} + +void TerminalRuntime::detach() { + if ( !mReading.exchange( false ) ) + return; + if ( mInputThread.joinable() ) + mInputThread.join(); + mWindow = nullptr; +} + +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 ); +} + +Scancode decodeScancode( Uint32 code ) { + if ( code >= 'a' && code <= 'z' ) + return static_cast( SCANCODE_A + code - 'a' ); + if ( code >= 'A' && code <= 'Z' ) + return static_cast( SCANCODE_A + code - 'A' ); + if ( code >= '1' && code <= '9' ) + return static_cast( SCANCODE_1 + code - '1' ); + if ( code == '0' ) + return SCANCODE_0; + if ( code == static_cast( KEY_UP ) ) + return SCANCODE_UP; + if ( code == static_cast( KEY_DOWN ) ) + return SCANCODE_DOWN; + if ( code == static_cast( KEY_LEFT ) ) + return SCANCODE_LEFT; + if ( code == static_cast( KEY_RIGHT ) ) + return SCANCODE_RIGHT; + if ( code == static_cast( KEY_HOME ) ) + return SCANCODE_HOME; + if ( code == static_cast( KEY_END ) ) + return SCANCODE_END; + if ( code == static_cast( KEY_PAGEUP ) ) + return SCANCODE_PAGEUP; + if ( code == static_cast( KEY_PAGEDOWN ) ) + return SCANCODE_PAGEDOWN; + if ( code == static_cast( KEY_INSERT ) ) + return SCANCODE_INSERT; + if ( code == static_cast( KEY_DELETE ) ) + return SCANCODE_DELETE; + if ( code >= static_cast( KEY_F1 ) && code <= static_cast( KEY_F12 ) ) + return static_cast( SCANCODE_F1 + code - static_cast( KEY_F1 ) ); + switch ( code ) { + case 9: + return SCANCODE_TAB; + case 13: + return SCANCODE_RETURN; + case 27: + return SCANCODE_ESCAPE; + case 32: + return SCANCODE_SPACE; + case 127: + return SCANCODE_BACKSPACE; + default: + return SCANCODE_UNKNOWN; + } +} + +void enqueueKey( Window& window, Uint32 code, Uint32 mods, Uint32 kind, Uint32 text ) { + 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 } }; + window.getInput()->enqueueEvent( event ); + if ( text && kind != 3 ) { + InputEvent input( InputEvent::TextInput ); + input.text = { 0, static_cast( text ) }; + window.getInput()->enqueueEvent( input ); + } +} + +Uint32 navigationCode( char final, unsigned int parameter ) { + if ( parameter == 1 && final >= 'P' && final <= 'S' ) + return KEY_F1 + final - 'P'; + if ( final == 'A' ) + return KEY_UP; + if ( final == 'B' ) + return KEY_DOWN; + if ( final == 'C' ) + return KEY_RIGHT; + if ( final == 'D' ) + return KEY_LEFT; + if ( final == 'H' ) + return KEY_HOME; + if ( final == 'F' ) + return KEY_END; + if ( final == '~' ) { + if ( parameter == 2 ) + return KEY_INSERT; + if ( parameter == 3 ) + return KEY_DELETE; + if ( parameter == 5 ) + return KEY_PAGEUP; + if ( parameter == 6 ) + return KEY_PAGEDOWN; + switch ( parameter ) { + case 15: + return KEY_F5; + case 17: + return KEY_F6; + case 18: + return KEY_F7; + case 19: + return KEY_F8; + case 20: + return KEY_F9; + case 21: + return KEY_F10; + case 23: + return KEY_F11; + case 24: + return KEY_F12; + } + } + return KEY_UNKNOWN; +} +} // namespace + +void TerminalRuntime::readInput() { + std::string pending; + pending.reserve( 256 ); + Math::Sizei lastSize = pixelSize(); + Vector2i lastMouse; + while ( mReading.load( std::memory_order_relaxed ) ) { +#if defined( EE_PLATFORM_POSIX ) + pollfd descriptor{ mFd, POLLIN, 0 }; + if ( ::poll( &descriptor, 1, 50 ) > 0 && descriptor.revents & POLLIN ) { + char data[256]; + const ssize_t count = ::read( mFd, data, sizeof( data ) ); + if ( count > 0 ) + pending.append( data, static_cast( count ) ); + } +#elif EE_PLATFORM == EE_PLATFORM_WIN + if ( WAIT_OBJECT_0 == WaitForSingleObject( mState->input, 50 ) ) { + char data[256]; + DWORD count = 0; + if ( ReadFile( mState->input, data, sizeof( data ), &count, nullptr ) && count > 0 ) + pending.append( data, count ); + } +#endif + while ( pending.size() >= 3 && pending[0] == '\033' && pending[1] == '[' ) { + size_t end = 2; + while ( end < pending.size() && !( pending[end] >= '@' && pending[end] <= '~' ) ) + ++end; + if ( end == pending.size() ) + break; + const char final = pending[end]; + const std::string params = pending.substr( 2, end - 2 ); + if ( params.empty() && ( final == 'I' || final == 'O' ) ) { + mFocused.store( final == 'I', std::memory_order_relaxed ); + InputEvent event( InputEvent::Window ); + event.window = { static_cast( final == 'I' ), + static_cast( final == 'I' + ? InputEvent::WindowKeyboardFocusGain + : InputEvent::WindowKeyboardFocusLost ) }; + mWindow->getInput()->enqueueEvent( event ); + } else if ( !params.empty() && params[0] == '<' && ( final == 'M' || final == 'm' ) ) { + unsigned int button, x, y; + if ( 3 == std::sscanf( params.c_str(), "<%u;%u;%u", &button, &x, &y ) ) { + InputEvent event; + if ( button & 64 ) { + const unsigned int wheelCode = button & 3; + const Uint8 wheelButton = wheelCode == 0 ? EE_BUTTON_WHEELUP + : wheelCode == 1 ? EE_BUTTON_WHEELDOWN + : wheelCode == 2 ? EE_BUTTON_WHEELLEFT + : EE_BUTTON_WHEELRIGHT; + event.Type = InputEvent::MouseButtonDown; + event.button = { 0, wheelButton, 1, static_cast( x - 1 ), + static_cast( y - 1 ) }; + mWindow->getInput()->enqueueEvent( event ); + event.Type = InputEvent::MouseButtonUp; + event.button.state = 0; + mWindow->getInput()->enqueueEvent( event ); + event.Type = InputEvent::MouseWheel; + event.wheel = { 0, + wheelCode == 2 ? -1.f + : wheelCode == 3 ? 1.f + : 0.f, + wheelCode == 0 ? 1.f + : wheelCode == 1 ? -1.f + : 0.f, + InputEvent::WheelEvent::Normal }; + } else if ( button & 32 ) { + event.Type = InputEvent::MouseMotion; + event.motion = { 0, + 0, + static_cast( x - 1 ), + static_cast( y - 1 ), + static_cast( x - 1 - lastMouse.x ), + static_cast( y - 1 - lastMouse.y ) }; + lastMouse = { static_cast( x - 1 ), static_cast( y - 1 ) }; + } else { + event.Type = + final == 'm' ? InputEvent::MouseButtonUp : InputEvent::MouseButtonDown; + event.button = { 0, static_cast( ( button & 3 ) + 1 ), + static_cast( final != 'm' ), + static_cast( x - 1 ), static_cast( y - 1 ) }; + } + mWindow->getInput()->enqueueEvent( event ); + } + } else if ( final == 'u' ) { + unsigned int code = 0, mods = 1, kind = 1, text = 0; + const size_t separator = params.find( ';' ); + 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 ); + } + } else if ( final == 'A' || final == 'B' || final == 'C' || final == 'D' || + final == 'H' || final == 'F' || final == 'P' || final == 'Q' || + final == 'R' || final == 'S' || final == '~' ) { + unsigned int parameter = 1, mods = 1, kind = 1; + std::sscanf( params.c_str(), "%u;%u:%u", ¶meter, &mods, &kind ); + const Uint32 code = navigationCode( final, parameter ); + if ( code != KEY_UNKNOWN ) + enqueueKey( *mWindow, code, decodeModifiers( mods ), kind, 0 ); + } + pending.erase( 0, end + 1 ); + } + if ( !pending.empty() && pending[0] != '\033' ) + pending.erase( 0, 1 ); + const Math::Sizei size = pixelSize(); + if ( size.x > 0 && size.y > 0 && size != lastSize ) { + lastSize = size; + InputEvent event( InputEvent::VideoResize ); + event.resize = { size.x, size.y }; + mWindow->getInput()->enqueueEvent( event ); + } + } +} + +}} // namespace EE::Window diff --git a/src/eepp/window/window.cpp b/src/eepp/window/window.cpp index 5fb91abad..529a6f9af 100644 --- a/src/eepp/window/window.cpp +++ b/src/eepp/window/window.cpp @@ -1,4 +1,5 @@ #include +#include #include #include #include @@ -9,23 +10,10 @@ #include #include #include +#include +#include #include -#ifdef EE_GLES1_LATE_INCLUDE -#if EE_PLATFORM == EE_PLATFORM_IOS -#include -#include -#else -#include - -#ifndef GL_GLEXT_PROTOTYPES -#define GL_GLEXT_PROTOTYPES -#endif - -#include -#endif -#endif - #if EE_PLATFORM == EE_PLATFORM_EMSCRIPTEN #include #endif @@ -221,11 +209,7 @@ void Window::setup2D( const bool& KeepView ) { BlendMode::setMode( BlendMode::Alpha(), true ); - if ( GLv_3CP != GLi->version() && GLv_3 != GLi->version() && GLv_ES2 != GLi->version() ) { -#if !defined( EE_GLES2 ) || defined( EE_GLES_BOTH ) - glTexEnvi( GL_POINT_SPRITE, GL_COORD_REPLACE, GL_TRUE ); -#endif - } + GLi->configurePointSprite(); if ( GLv_2 == GLi->version() || GLv_ES1 == GLi->version() ) { GLi->enableClientState( GL_VERTEX_ARRAY ); @@ -307,17 +291,11 @@ Image Window::getFrontBufferImage() { GlobalBatchRenderer::instance()->draw(); Image fb( mWindow.WindowConfig.Width, mWindow.WindowConfig.Height, 3 ); - GLint pack_alignment; - glGetIntegerv( GL_PACK_ALIGNMENT, &pack_alignment ); - if ( 1 != pack_alignment ) - glPixelStorei( GL_PACK_ALIGNMENT, 1 ); - Uint8* pixelData = fb.getPixels(); - glReadPixels( 0, 0, mWindow.WindowConfig.Width, mWindow.WindowConfig.Height, GL_RGB, - GL_UNSIGNED_BYTE, pixelData ); - - if ( 1 != pack_alignment ) - glPixelStorei( GL_PACK_ALIGNMENT, pack_alignment ); + if ( !GLi->readPixels( 0, 0, mWindow.WindowConfig.Width, mWindow.WindowConfig.Height, + Graphics::Renderer::PixelFormat::RGB24, pixelData, + static_cast( mWindow.WindowConfig.Width ) * 3 ) ) + return {}; for ( unsigned int j = 0; j * 2 < fb.getHeight(); ++j ) { int index1 = j * fb.getWidth() * fb.getChannels(); @@ -334,6 +312,72 @@ Image Window::getFrontBufferImage() { return fb; } +bool Window::readFrameBuffer( FrameReadback& readback ) { + const Uint32 channels = readback.format == FramePixelFormat::RGB24 ? 3 : 4; + const size_t rowBytes = static_cast( readback.size.getWidth() ) * channels; + if ( nullptr == readback.pixels || readback.size.getWidth() <= 0 || + readback.size.getHeight() <= 0 || readback.stride < rowBytes || readback.position.x < 0 || + readback.position.y < 0 || + static_cast( readback.position.x + readback.size.getWidth() ) > getWidth() || + static_cast( readback.position.y + readback.size.getHeight() ) > getHeight() ) + return false; + const auto format = readback.format == FramePixelFormat::RGB24 + ? Graphics::Renderer::PixelFormat::RGB24 + : Graphics::Renderer::PixelFormat::RGBA32; + if ( mLogicalFrameBuffer ) + mLogicalFrameBuffer->bindAsReadTarget(); + if ( !GLi->readPixels( readback.position.x, readback.position.y, readback.size.getWidth(), + readback.size.getHeight(), format, readback.pixels, readback.stride ) ) + return false; + readback.origin = FrameOrigin::BottomLeft; + return true; +} + +bool Window::initializeRuntimeRenderTarget() { + if ( !Runtime::isOffscreen() ) + return true; + + if ( mLogicalFrameBuffer ) + return true; + + mLogicalFrameBuffer = + Graphics::FrameBuffer::New( getWidth(), getHeight(), true, false, true, 4, this ); + + if ( !mLogicalFrameBuffer || !mLogicalFrameBuffer->created() ) { + Log::error( "Unable to create the %s runtime logical framebuffer", Runtime::modeName() ); + mLogicalFrameBuffer.reset(); + return false; + } + + mLogicalFrameBuffer->bind(); + + setup2D( false ); + + if ( Runtime::mode() == RuntimeMode::Terminal ) { + mFramePresenter = std::make_unique(); + if ( !mFramePresenter->initialize( *this ) ) + return false; + } + + return true; +} + +void Window::resizeRuntimeRenderTarget( Uint32 width, Uint32 height ) { + if ( mLogicalFrameBuffer ) { + mLogicalFrameBuffer->resize( width, height ); + mLogicalFrameBuffer->bind(); + } + if ( mFramePresenter ) + mFramePresenter->resized( *this, Sizei( width, height ) ); +} + +void Window::shutdownRuntimeRenderTarget() { + if ( mFramePresenter ) + mFramePresenter->shutdown( *this ); + mFramePresenter.reset(); + mLogicalFrameBuffer.reset(); +} + bool Window::isRunning() const { return mWindow.Created; } @@ -461,6 +505,8 @@ void Window::clear() { void Window::display( bool clear ) { GlobalBatchRenderer::instance()->draw(); + if ( mFramePresenter ) + mFramePresenter->present( *this ); if ( TextureFactory* textureFactory = TextureFactory::existsSingleton() ) textureFactory->collectReleasedTextures(); mIME.updateLocation(); @@ -526,17 +572,21 @@ void Window::sendVideoResizeCb() { } void Window::logSuccessfulInit( const std::string& BackendName ) { + const char* presenter = + Runtime::mode() == RuntimeMode::Terminal ? "Kitty graphics protocol" : "None"; std::string msg( String::format( "Engine Initialized Successfully.\n\tVersion: %s (codename: \"%s\")\n\tBuild time: " "%s\n\tPlatform: %s\n\tOS: %s\n\tArch: %s\n\tCPU Cores: %d\n\tProcess Path: %s\n\tCurrent " - "Working Directory: %s\n\tHome Directory: %s\n\tDisk Free Space: %s\n\tWindow/Input " - "Backend: %s\n\tGL Backend: %s\n\tGL Vendor: %s\n\tGL Renderer: %s\n\tGL Version: " + "Working Directory: %s\n\tHome Directory: %s\n\tDisk Free Space: %s\n\tRuntime Mode: " + "%s\n\tHeadless: %s\n\tFrame Presenter: %s\n\tWindow/Input Backend: %s\n\tGL Backend: " + "%s\n\tGL Vendor: %s\n\tGL Renderer: %s\n\tGL Version: " "%s\n\tGL Shading Language Version: %s\n\tResolution: %dx%d\n\tWindow scale: %.2f", Version::getVersionName(), Version::getCodename(), Version::getBuildTime(), Sys::getPlatform(), Sys::getOSName( true ), Sys::getOSArchitecture(), Sys::getCPUCount(), Sys::getProcessPath(), FileSystem::getCurrentWorkingDirectory(), Sys::getUserDirectory(), FileSystem::sizeToString( FileSystem::getDiskFreeSpace( Sys::getProcessPath() ) ), - BackendName, GLi->versionStr(), GLi->getVendor(), GLi->getRenderer(), GLi->getVersion(), + Runtime::modeName(), Runtime::isOffscreen() ? "Yes" : "No", presenter, BackendName, + GLi->versionStr(), GLi->getVendor(), GLi->getRenderer(), GLi->getVersion(), GLi->getShadingLanguageVersion(), getWidth(), getHeight(), getScale() ) ); #ifndef EE_SILENT