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