diff --git a/Source/compositorclient/CMakeLists.txt b/Source/compositorclient/CMakeLists.txt index 22b5580e..404b9e88 100644 --- a/Source/compositorclient/CMakeLists.txt +++ b/Source/compositorclient/CMakeLists.txt @@ -19,16 +19,14 @@ cmake_minimum_required(VERSION 3.15) find_package(Thunder) -project(Compositor) - -project_version(1.0.0) +project(Compositor + VERSION 1.0.0 + LANGUAGES CXX + DESCRIPTION "A graphical/input abstraction. This library is included in microservices that require user interaction (Graphics/input). The implementation hides all the compositor-specific details and allows for relaying keys to plugins." +) list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_LIST_DIR}/cmake") add_subdirectory(include) add_subdirectory(src) - -option(BUILD_COMPOSITORCLIENT_TEST "Build Compositor Client test" OFF) -if (BUILD_COMPOSITORCLIENT_TEST) - add_subdirectory(test) -endif() +add_subdirectory(test) diff --git a/Source/compositorclient/src/Mesa/CMakeLists.txt b/Source/compositorclient/src/Mesa/CMakeLists.txt index 9f81d010..605435cc 100644 --- a/Source/compositorclient/src/Mesa/CMakeLists.txt +++ b/Source/compositorclient/src/Mesa/CMakeLists.txt @@ -53,5 +53,3 @@ target_compile_definitions(${PLUGIN_COMPOSITOR_IMPLEMENTATION} PUBLIC EGL_NO_X11 ) - -add_subdirectory(test) \ No newline at end of file diff --git a/Source/compositorclient/src/Mesa/Implementation.cpp b/Source/compositorclient/src/Mesa/Implementation.cpp index 08490384..2f6da611 100644 --- a/Source/compositorclient/src/Mesa/Implementation.cpp +++ b/Source/compositorclient/src/Mesa/Implementation.cpp @@ -21,12 +21,9 @@ extern "C" { #include - #include - #include #include - #include } @@ -39,10 +36,11 @@ extern "C" { #include #include -#include "RenderAPI.h" #include #include +#include +#include namespace Thunder { namespace Linux { @@ -77,25 +75,26 @@ namespace Linux { const char* GetGbmBackendName(gbm_device* gbmDevice) { - static gbm_device* cachedDevice = nullptr; - static const char* cachedName = nullptr; - - if (gbmDevice != nullptr && gbmDevice != cachedDevice) { - cachedName = gbm_device_get_backend_name(gbmDevice); - cachedDevice = gbmDevice; - TRACE_GLOBAL(Trace::Information, (_T("GBM Backend: %s"), cachedName)); + if (gbmDevice == nullptr) { + return nullptr; + } + const char* name = gbm_device_get_backend_name(gbmDevice); + if (name != nullptr) { + TRACE_GLOBAL(Trace::Information, (_T("GBM Backend: %s"), name)); } - return cachedName; + return name; } bool IsGbmBackend(gbm_device* gbmDevice, const char* name) { const char* backendName = GetGbmBackendName(gbmDevice); - return (backendName != nullptr) && (strcmp(backendName, name) == 0); + return (backendName != nullptr) && (std::strcmp(backendName, name) == 0); } - } - + + DEFINE_MESSAGING_CATEGORY(Core::Messaging::BaseCategoryType, BufferInfo) + DEFINE_MESSAGING_CATEGORY(Core::Messaging::BaseCategoryType, BufferError) + class Display : public Compositor::IDisplay { public: Display() = delete; @@ -121,31 +120,59 @@ namespace Linux { class SurfaceImplementation : public Compositor::IDisplay::ISurface { private: + static constexpr size_t MaxContentBuffers = 4; + + enum class BufferState : uint8_t { + FREE, // In GBM pool + STAGED, // Locked, render complete, ready to submit + PENDING, // Submitted, waiting for Rendered + ACTIVE, // On screen + RETIRED // Previous frame, waiting for Published + }; + + static const char* StateToString(BufferState state) + { + switch (state) { + case BufferState::FREE: + return "FREE"; + case BufferState::STAGED: + return "STAGED"; + case BufferState::PENDING: + return "PENDING"; + case BufferState::ACTIVE: + return "ACTIVE"; + case BufferState::RETIRED: + return "RETIRED"; + default: + return "UNKNOWN"; + } + } + // for now only single plane buffers are supported, e.g. GBM_FORMAT_ARGB8888, GBM_FORMAT_XRGB8888, etc. class ContentBuffer : public Graphics::ClientBufferType<1> { using BaseClass = Graphics::ClientBufferType<1>; public: - ContentBuffer() = delete; - ContentBuffer(ContentBuffer&&) = delete; - ContentBuffer(const ContentBuffer&) = delete; - ContentBuffer& operator=(ContentBuffer&&) = delete; - ContentBuffer& operator=(const ContentBuffer&) = delete; - ContentBuffer(SurfaceImplementation& parent, gbm_bo* frameBuffer) : BaseClass(gbm_bo_get_width(frameBuffer), gbm_bo_get_height(frameBuffer), gbm_bo_get_format(frameBuffer), gbm_bo_get_modifier(frameBuffer), Exchange::IGraphicsBuffer::TYPE_DMA) , _parent(parent) , _bo(frameBuffer) + , _state(BufferState::FREE) { ASSERT(_bo != nullptr); if (_bo != nullptr) { - // for now only single plane buffers are supported - ASSERT(gbm_bo_get_plane_count(_bo) == 1); + const uint8_t nPlanes = gbm_bo_get_plane_count(_bo); + + for (uint8_t i = 0; i < nPlanes; ++i) { + int fd = gbm_bo_get_fd_for_plane(_bo, i); + + Add(fd, gbm_bo_get_stride_for_plane(_bo, i), gbm_bo_get_offset(_bo, i)); - Add(gbm_bo_get_fd_for_plane(_bo, 0), - gbm_bo_get_stride_for_plane(_bo, 0), - gbm_bo_get_offset(_bo, 0)); + if (fd >= 0) { + ::close(fd); // safe, since Add() dup()'d it + } + } std::array descriptors; descriptors.fill(-1); @@ -155,13 +182,13 @@ namespace Linux { if (nDescriptors > 0) { Core::PrivilegedRequest::Container container(descriptors.begin(), descriptors.begin() + nDescriptors); Core::PrivilegedRequest request; - + const string connector = ConnectorPath() + _T("descriptors"); - if (request.Offer(100, connector, parent.Id(), container) == Core::ERROR_NONE) { - TRACE(Trace::Information, (_T("Offered buffer to compositor server"))); + if (request.Offer(100, connector, _parent.Id(), container) == Core::ERROR_NONE) { + TRACE(Trace::Information, (_T("Offered buffer to compositor"))); } else { - TRACE(Trace::Error, (_T("Failed to offer buffer to compositor server"))); + TRACE(Trace::Error, (_T("Failed to offer buffer to compositor"))); } } } @@ -172,37 +199,110 @@ namespace Linux { virtual ~ContentBuffer() { Core::ResourceMonitor::Instance().Unregister(*this); - }; + } static void Destroyed(gbm_bo* bo, void* data) { - ASSERT(data != nullptr); ContentBuffer* buffer = static_cast(data); - - TRACE_GLOBAL(Trace::Information, (_T("ContentBuffer[%p] Destroyed signaled"), buffer)); - if ((buffer != nullptr) && (bo == buffer->_bo)) { + buffer->_parent.RemoveContentBuffer(buffer); delete buffer; - } else { - TRACE_GLOBAL(Trace::Error, (_T("ContentBuffer[%p] Destroyed signaled with mismatched gbm_bo[%p]"), buffer, bo)); } } + gbm_bo* Bo() const { return _bo; } + + BufferState State() const + { + return _state.load(std::memory_order_acquire); + } + + // FREE → STAGED (after client locks front buffer) + bool Stage() + { + BufferState expected = BufferState::FREE; + if (_state.compare_exchange_strong(expected, BufferState::STAGED, + std::memory_order_acq_rel)) { + return true; + } + TRACE(Trace::Error, + (_T("Buffer %p: Stage failed (expected FREE, got %s)"), + _bo, StateToString(expected))); + return false; + } + + // STAGED → PENDING (submit to compositor) + bool Submit() + { + BufferState expected = BufferState::STAGED; + if (_state.compare_exchange_strong(expected, BufferState::PENDING, + std::memory_order_acq_rel)) { + BaseClass::RequestRender(); + return true; + } + TRACE(Trace::Error, + (_T("Buffer %p: Submit failed (expected STAGED, got %s)"), + _bo, StateToString(expected))); + return false; + } + + // PENDING → ACTIVE (compositor GPU done) + bool Activate() + { + BufferState expected = BufferState::PENDING; + if (_state.compare_exchange_strong(expected, BufferState::ACTIVE, + std::memory_order_acq_rel)) { + return true; + } + TRACE(Trace::Error, + (_T("Buffer %p: Activate failed (expected PENDING, got %s)"), + _bo, StateToString(expected))); + return false; + } + + // ACTIVE → RETIRED (new buffer became active) + bool Retire() + { + BufferState expected = BufferState::ACTIVE; + if (_state.compare_exchange_strong(expected, BufferState::RETIRED, + std::memory_order_acq_rel)) { + return true; + } + TRACE(Trace::Error, + (_T("Buffer %p: Retire failed (expected ACTIVE, got %s)"), + _bo, StateToString(expected))); + return false; + } + + // RETIRED → FREE (released back to GBM) + bool Release() + { + BufferState expected = BufferState::RETIRED; + if (_state.compare_exchange_strong(expected, BufferState::FREE, + std::memory_order_acq_rel)) { + return true; + } + TRACE(Trace::Error, + (_T("Buffer %p: Release failed (expected RETIRED, got %s)"), + _bo, StateToString(expected))); + return false; + } + protected: void Rendered() override { - _parent.Unlock(_bo); - _parent.Rendered(); + _parent.OnBufferRendered(this); } void Published() override { - _parent.Published(); + _parent.OnBufferPublished(this); } private: SurfaceImplementation& _parent; gbm_bo* _bo; + std::atomic _state; }; public: @@ -212,27 +312,33 @@ namespace Linux { SurfaceImplementation& operator=(SurfaceImplementation&&) = delete; SurfaceImplementation& operator=(const SurfaceImplementation&) = delete; - SurfaceImplementation(Display& display, const std::string& name, const uint32_t width, const uint32_t height, ICallback* callback) + SurfaceImplementation(Display& display, const std::string& name, + const uint32_t width, const uint32_t height, + ICallback* callback) : _display(display) , _gbmSurface(display.CreateGbmSurface(width, height)) , _remoteClient(display.CreateRemoteSurface(name, width, height)) , _id(_remoteClient->Native()) , _width(width) , _height(height) - , _gbmBufferLock() , _name(name) , _keyboard(nullptr) , _wheel(nullptr) , _pointer(nullptr) , _touchpanel(nullptr) , _callback(callback) + , _contentBuffers() + , _bufferLock() + , _activeBuffer(nullptr) + , _retiredBuffer(nullptr) { + _contentBuffers.fill(nullptr); _display.AddRef(); ASSERT(_remoteClient != nullptr); ASSERT(_gbmSurface != nullptr); - TRACE(Trace::Information, (_T("Construct surface[%d] %s %dx%d (hxb)"), _id, name.c_str(), height, width)); + TRACE(Trace::Information, (_T("Surface[%d] %s %dx%d constructed"), _id, name.c_str(), width, height)); _display.Register(this); } @@ -256,53 +362,35 @@ namespace Linux { _touchpanel->Release(); } - // Cleanup the remote client buffers - if (_remoteClient != nullptr) { - _remoteClient->Release(); - } + // Prevent new RequestRender() calls from allocating new buffers. + gbm_surface* surface = _gbmSurface; + _gbmSurface = nullptr; - // lets hope DRM cleans up the gbm buffer objects for us, if so drm should call ContentBuffer::Destroyed()... - if (_gbmSurface != nullptr) { - gbm_surface_destroy(_gbmSurface); - _gbmSurface = nullptr; - } - - _display.Release(); - } - - private: - gbm_bo* Lock(const uint16_t ms) - { - ASSERT((_gbmSurface != nullptr) && "Failed to lock a framebuffer, surface is null"); - - gbm_bo* frameBuffer = nullptr; + { + Core::SafeSyncType lock(_bufferLock); - if (_gbmBufferLock.try_lock_for(std::chrono::milliseconds(ms))) { - frameBuffer = gbm_surface_lock_front_buffer(_gbmSurface); + for (size_t i = 0; i < MaxContentBuffers; i++) { + if (_contentBuffers[i] != nullptr) { + // Clear user data to prevent GBM from calling our Destroyed callback + gbm_bo_set_user_data(_contentBuffers[i]->Bo(), nullptr, nullptr); - if (frameBuffer == nullptr) { - _gbmBufferLock.unlock(); // Unlock the mutex to prevent deadlock - TRACE(Trace::Error, (_T("Failed to lock front buffer, surface %p"), _gbmSurface)); - } else { - TRACE(Trace::Information, (_T("Acquired framebuffer[%p]"), frameBuffer)); + // Explicitly delete the ContentBuffer + delete _contentBuffers[i]; + _contentBuffers[i] = nullptr; + } } - } else { - TRACE(Trace::Error, (_T("Failed to lock front buffer within %d ms"), ms)); } - return frameBuffer; - } - - void Unlock(gbm_bo* frameBuffer) - { - ASSERT((_gbmSurface != nullptr) && "Failed to release framebuffer, surface is null"); + // Cleanup the remote client buffers + if (_remoteClient != nullptr) { + _remoteClient->Release(); + } - if ((_gbmSurface != nullptr) && (frameBuffer != nullptr)) { - gbm_surface_release_buffer(_gbmSurface, frameBuffer); - TRACE(Trace::Information, (_T("Released framebuffer[%p]"), frameBuffer)); + if (surface != nullptr) { + gbm_surface_destroy(surface); } - _gbmBufferLock.unlock(); + _display.Release(); } public: @@ -413,44 +501,175 @@ namespace Linux { } } - void Rendered() + uint32_t Process() { - if (_callback != nullptr) { - _callback->Rendered(this); + return Core::ERROR_NONE; + } + + // ───────────────────────────────────────────────────────────────────────── + // Called after eglSwapBuffers + // ───────────────────────────────────────────────────────────────────────── + void RequestRender() + { + if (_gbmSurface == nullptr) { + NotifyRendered(); + return; + } + + uint64_t before = Core::Time::Now().Ticks(); + gbm_bo* frameBuffer = gbm_surface_lock_front_buffer(_gbmSurface); + uint64_t after = Core::Time::Now().Ticks(); + + TRACE(BufferInfo, (_T("Surface[%d]: lock_front_buffer took %" PRIu64 " µs, returned %p"), _id, (after - before), static_cast(frameBuffer))); + + if (frameBuffer == nullptr) { + TRACE(BufferError, (_T("Surface %s: lock_front_buffer failed"), _name.c_str())); + NotifyRendered(); + return; } + + ContentBuffer* buffer = GetOrCreateContentBuffer(frameBuffer); + + if (buffer == nullptr) { + gbm_surface_release_buffer(_gbmSurface, frameBuffer); + NotifyRendered(); + return; + } + + // FREE → STAGED → PENDING + if (buffer->Stage() && buffer->Submit()) { + // Success - wait for Rendered callback + return; + } + + // Failed - release buffer and notify + gbm_surface_release_buffer(_gbmSurface, frameBuffer); + NotifyRendered(); } - void Published() + // ───────────────────────────────────────────────────────────────────────── + // Called when compositor signals Rendered (GPU done) + // ───────────────────────────────────────────────────────────────────────── + void OnBufferRendered(ContentBuffer* buffer) { - if (_callback != nullptr) { - _callback->Published(this); + // PENDING → ACTIVE + if (!buffer->Activate()) { + return; } + + // Retire previous active buffer (ACTIVE → RETIRED) + ContentBuffer* oldActive = _activeBuffer.exchange(buffer, std::memory_order_acq_rel); + + if (oldActive != nullptr && oldActive != buffer) { + if (oldActive->Retire()) { + // Store for release on Published + ContentBuffer* oldRetired = _retiredBuffer.exchange(oldActive, std::memory_order_acq_rel); + + // Handle orphaned retired buffer (shouldn't happen normally) + if (oldRetired != nullptr) { + TRACE(BufferError, (_T("Surface %s: orphaned retired buffer %p"), _name.c_str(), oldRetired->Bo())); + ReleaseToGbm(oldRetired); + } + } + } + + NotifyRendered(); } - void RequestRender() override + // ───────────────────────────────────────────────────────────────────────── + // Called when compositor signals Published (VSync done) + // ───────────────────────────────────────────────────────────────────────── + void OnBufferPublished(ContentBuffer* buffer VARIABLE_IS_NOT_USED) { - gbm_bo* frameBuffer = Lock(1000); + // Release retired buffer (RETIRED → FREE) + ContentBuffer* retired = _retiredBuffer.exchange(nullptr, std::memory_order_acq_rel); - if (frameBuffer == nullptr) { - // bail-out if we cannot lock the front buffer - std::this_thread::sleep_for(std::chrono::milliseconds(5)); - Rendered(); // we still need call Rendered to request a new frame buffer - } else { - ContentBuffer* buffer = static_cast(gbm_bo_get_user_data(frameBuffer)); + if (retired != nullptr) { + ReleaseToGbm(retired); + } + + NotifyPublished(); + } + + void RemoveContentBuffer(ContentBuffer* buffer) + { + Core::SafeSyncType lock(_bufferLock); - if (buffer == nullptr) { - buffer = new ContentBuffer(*this, frameBuffer); - gbm_bo_set_user_data(frameBuffer, buffer, &ContentBuffer::Destroyed); + for (size_t i = 0; i < MaxContentBuffers; i++) { + if (_contentBuffers[i] == buffer) { + _contentBuffers[i] = nullptr; + break; } + } + + // Clear atomic pointers if they reference this buffer + ContentBuffer* expected = buffer; + _activeBuffer.compare_exchange_strong(expected, nullptr, std::memory_order_acq_rel); + expected = buffer; + _retiredBuffer.compare_exchange_strong(expected, nullptr, std::memory_order_acq_rel); + } - ASSERT(buffer != nullptr); - buffer->RequestRender(); + private: + void ReleaseToGbm(ContentBuffer* buffer) + { + if (buffer != nullptr && buffer->Release() && _gbmSurface != nullptr) { + gbm_surface_release_buffer(_gbmSurface, buffer->Bo()); + TRACE(BufferInfo, (_T("Surface %s: buffer %p released to GBM"), _name.c_str(), buffer->Bo())); } } - uint32_t Process() + ContentBuffer* GetOrCreateContentBuffer(gbm_bo* frameBuffer) { - return Core::ERROR_NONE; + ContentBuffer* buffer = static_cast( + gbm_bo_get_user_data(frameBuffer)); + + if (buffer != nullptr) { + return buffer; + } + + Core::SafeSyncType lock(_bufferLock); + + // Double-check after lock + buffer = static_cast(gbm_bo_get_user_data(frameBuffer)); + if (buffer != nullptr) { + return buffer; + } + + // Find empty slot + size_t slot = MaxContentBuffers; + for (size_t i = 0; i < MaxContentBuffers; i++) { + if (_contentBuffers[i] == nullptr) { + slot = i; + break; + } + } + + if (slot == MaxContentBuffers) { + TRACE(Trace::Error, (_T("Surface %s: buffer pool exhausted"), _name.c_str())); + return nullptr; + } + + buffer = new ContentBuffer(*this, frameBuffer); + _contentBuffers[slot] = buffer; + gbm_bo_set_user_data(frameBuffer, buffer, &ContentBuffer::Destroyed); + + TRACE(Trace::Information, (_T("Surface %s: created ContentBuffer %p in slot %zu"), _name.c_str(), buffer, slot)); + + return buffer; + } + + void NotifyRendered() + { + if (_callback != nullptr) { + _callback->Rendered(this); + } + } + + void NotifyPublished() + { + if (_callback != nullptr) { + _callback->Published(this); + } } private: @@ -460,13 +679,18 @@ namespace Linux { const uint8_t _id; const int32_t _width; // real pixels allocated in the gpu! const int32_t _height; // real pixels allocated in the gpu - std::timed_mutex _gbmBufferLock; const string _name; IKeyboard* _keyboard; IWheel* _wheel; IPointer* _pointer; ITouchPanel* _touchpanel; ISurface::ICallback* _callback; + std::array _contentBuffers; + Core::CriticalSection _bufferLock; + + // Buffer state tracking - lock-free + std::atomic _activeBuffer; // Currently on screen + std::atomic _retiredBuffer; // Waiting for release static uint32_t _surfaceIndex; }; // class SurfaceImplementation @@ -575,13 +799,9 @@ namespace Linux { return result; } - const Exchange::IComposition::IDisplay* RemoteDisplay() const + bool IsValid() const { - return _remoteDisplay; - } - Exchange::IComposition::IDisplay* RemoteDisplay() - { - return _remoteDisplay; + return _remoteDisplay != nullptr; } private: @@ -612,6 +832,45 @@ namespace Linux { if (_remoteDisplay == nullptr) { TRACE(Trace::Error, (_T ( "Could not create remote display for Display %s!" ), Name().c_str())); + } else { + // Get render node path from remote display + std::string renderNode; + + if (_remoteDisplay != nullptr) { + renderNode = _remoteDisplay->Port(); + } + + if (renderNode.empty()) { + TRACE(Trace::Error, (_T("Remote display did not provide a render node for Display %s"), Name().c_str())); + return; + } + + // Open the DRM render node + _gpuId = ::open(renderNode.c_str(), O_RDWR | O_CLOEXEC); + + if (_gpuId < 0) { + TRACE(Trace::Error, (_T("Failed to open render node %s, errno=%d"), renderNode.c_str(), errno)); + return; + } + + // Create GBM device + _gbmDevice = gbm_create_device(_gpuId); + + if (_gbmDevice == nullptr) { + TRACE(Trace::Error, (_T("Failed to create GBM device for %s"), renderNode.c_str())); + ::close(_gpuId); + _gpuId = -1; + return; + } + + // Get the resolved device name (may be null) + const char* resolvedName = drmGetRenderDeviceNameFromFd(_gpuId); + + if (resolvedName == nullptr) { + resolvedName = renderNode.c_str(); // fallback + } + + TRACE(Trace::Information, (_T("Opened GBM[%p] device on fd=%d, RenderNode=%s"), _gbmDevice, _gpuId, resolvedName)); } } else { TRACE(Trace::Error, (_T("Could not open connection to Compositor with node %s. Error: %s"), _compositorServerRPCConnection->Source().RemoteId().c_str(), Core::NumberType(result).Text().c_str())); @@ -698,7 +957,7 @@ namespace Linux { { return (_remoteDisplay != nullptr ? _remoteDisplay->CreateClient(name, width, height) : nullptr); } - + gbm_surface* CreateGbmSurface(const uint32_t width, const uint32_t height) const { gbm_surface* surface(nullptr); @@ -711,19 +970,20 @@ namespace Linux { for (uint32_t format : FormatPriority) { char* formatName = drmGetFormatName(format); - - ASSERT(formatName != nullptr); // Should always be valid for known DRM formats + if (formatName == nullptr) { + TRACE(Trace::Warning, ("Unknown DRM format %#x - skipping", format)); + continue; + } surface = gbm_surface_create(_gbmDevice, width, height, format, usage); if (surface != nullptr) { - TRACE(Trace::Information, ("Successfully created surface with format: %s", formatName ? formatName : "Unknown")); + TRACE(Trace::Information, ("Successfully created surface with format: %s", formatName)); + free(formatName); break; } - TRACE(Trace::Warning, ("Failed to create GBM surface with format: %s, trying next...", formatName ? formatName : "Unknown")); - if (formatName != nullptr) { - free(formatName); - } + TRACE(Trace::Warning, ("Failed to create GBM surface with format: %s, trying next...", formatName)); + free(formatName); } return surface; @@ -761,20 +1021,6 @@ namespace Linux { , _gpuId(-1) , _gbmDevice(nullptr) { - Core::PrivilegedRequest::Container descriptors; - Core::PrivilegedRequest request; - - const string connector = ConnectorPath() + _T("descriptors"); - - if (request.Request(1000, connector, DisplayId, descriptors) == Core::ERROR_NONE) { - ASSERT(descriptors.size() == 1); - _gpuId = descriptors[0].Move(); - _gbmDevice = gbm_create_device(_gpuId); - TRACE(Trace::Information, (_T ( "Opened GBM[%p] device on fd: %d, RenderDevice: %s"), _gbmDevice, _gpuId, drmGetRenderDeviceNameFromFd(_gpuId))); - } else { - TRACE(Trace::Error, (_T ( "Failed to get display file descriptor from compositor server"))); - } - TRACE(Trace::Information, (_T("Display[%p] Constructed build @ %s"), this, __TIMESTAMP__)); } @@ -787,7 +1033,7 @@ namespace Linux { _gbmDevice = nullptr; } - if (_gpuId > 0) { + if (_gpuId >= 0) { ::close(_gpuId); _gpuId = -1; } @@ -906,6 +1152,16 @@ namespace Linux { Compositor::IDisplay* Compositor::IDisplay::Instance(const string& displayName) { - return (&(Linux::Display::Instance(displayName))); + Compositor::IDisplay* result(nullptr); + + Linux::Display& display = Linux::Display::Instance(displayName); + + if (display.IsValid() == false) { + display.Release(); + } else { + result = &(display); + } + + return result; } } // namespace Thunder diff --git a/Source/compositorclient/src/Mesa/RenderAPI.h b/Source/compositorclient/src/Mesa/RenderAPI.h deleted file mode 100644 index 04668ca3..00000000 --- a/Source/compositorclient/src/Mesa/RenderAPI.h +++ /dev/null @@ -1,356 +0,0 @@ -/* - * If not stated otherwise in this file or this component's LICENSE file the - * following copyright and licenses apply: - * - * Copyright 2022 Metrological B.V. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#pragma once - -#ifndef MODULE_NAME -#define MODULE_NAME CompositorRenderEGL -#endif - -#include -#include - -#include -#include - -#include - -namespace Thunder { -namespace Compositor { - -#ifndef EGL_VERSION_1_5 - using PFNEGLCREATEIMAGEPROC = PFNEGLCREATEIMAGEKHRPROC; - using PFNEGLDESTROYIMAGEPROC = PFNEGLDESTROYIMAGEKHRPROC; - using PFNEGLCREATESYNCPROC = PFNEGLCREATESYNCKHRPROC; - using PFNEGLDESTROYSYNCPROC = PFNEGLDESTROYSYNCKHRPROC; - using PFNEGLWAITSYNCPROC = PFNEGLWAITSYNCKHRPROC; - using PFNEGLCLIENTWAITSYNCPROC = PFNEGLCLIENTWAITSYNCKHRPROC; - - constexpr char eglCreateImageProc[] = "eglCreateImageKHR"; - constexpr char eglDestroyImageProc[] = "eglDestroyImageKHR"; - constexpr char eglCreateSyncProc[] = "eglCreateSyncKHR"; - constexpr char eglDestroySyncProc[] = "eglDestroySyncKHR"; - constexpr char eglWaitSyncProc[] = "eglWaitSyncKHR"; - constexpr char eglClientWaitSyncProc[] = "eglClientWaitSyncKHR"; -#else - constexpr char eglCreateImageProc[] = "eglCreateImage"; - constexpr char eglDestroyImageProc[] = "eglDestroyImage"; - constexpr char eglCreateSyncProc[] = "eglCreateSync"; - constexpr char eglDestroySyncProc[] = "eglDestroySync"; - constexpr char eglWaitSyncProc[] = "eglWaitSync"; - constexpr char eglClientWaitSyncProc[] = "eglClientWaitSync"; -#endif - - namespace API { - template - class Attributes { - protected: - std::vector attributes; - typename std::vector::iterator pos; - - public: - Attributes(TYPE terminator = EGL_NONE) - { - attributes.push_back(terminator); - } - - inline void Append(TYPE param) - { - attributes.insert(attributes.end() - 1, param); - } - - inline void Append(TYPE name, TYPE value) - { - Append(name); - Append(value); - } - - inline operator TYPE*(void) - { - return &attributes[0]; - } - - inline operator const TYPE*(void) const - { - return &attributes[0]; - } - - inline const TYPE& operator[](size_t idx) const - { - return attributes[idx]; - } - - inline size_t Size() const - { - return attributes.size(); - } - }; - - static inline bool HasExtension(const std::string& extensions, const std::string& extention) - { - return ((extention.size() > 0) && (extensions.find(extention) != std::string::npos)); - } - - class GL { - public: -/* simple stringification operator to make errorcodes human readable */ -#define CASE_TO_STRING(value) \ - case value: \ - return #value; - - static const char* ErrorString(GLenum code) - { - switch (code) { - CASE_TO_STRING(GL_NO_ERROR) - CASE_TO_STRING(GL_INVALID_ENUM) - CASE_TO_STRING(GL_INVALID_VALUE) - CASE_TO_STRING(GL_INVALID_OPERATION) - CASE_TO_STRING(GL_INVALID_FRAMEBUFFER_OPERATION) - CASE_TO_STRING(GL_OUT_OF_MEMORY) - CASE_TO_STRING(GL_STACK_UNDERFLOW_KHR) - CASE_TO_STRING(GL_STACK_OVERFLOW_KHR) - default: - return "Unknown"; - } - } - - static const char* SourceString(GLenum code) - { - switch (code) { - CASE_TO_STRING(GL_DEBUG_SOURCE_API_KHR) - CASE_TO_STRING(GL_DEBUG_SOURCE_WINDOW_SYSTEM_KHR) - CASE_TO_STRING(GL_DEBUG_SOURCE_SHADER_COMPILER_KHR) - CASE_TO_STRING(GL_DEBUG_SOURCE_THIRD_PARTY_KHR) - CASE_TO_STRING(GL_DEBUG_SOURCE_APPLICATION_KHR) - CASE_TO_STRING(GL_DEBUG_SOURCE_OTHER_KHR) - default: - return "Unknown"; - } - } - - static const char* TypeString(GLenum code) - { - switch (code) { - CASE_TO_STRING(GL_DEBUG_TYPE_ERROR_KHR) - CASE_TO_STRING(GL_DEBUG_TYPE_DEPRECATED_BEHAVIOR_KHR) - CASE_TO_STRING(GL_DEBUG_TYPE_UNDEFINED_BEHAVIOR_KHR) - CASE_TO_STRING(GL_DEBUG_TYPE_PORTABILITY_KHR) - CASE_TO_STRING(GL_DEBUG_TYPE_PERFORMANCE_KHR) - CASE_TO_STRING(GL_DEBUG_TYPE_OTHER_KHR) - CASE_TO_STRING(GL_DEBUG_TYPE_MARKER_KHR) - CASE_TO_STRING(GL_DEBUG_TYPE_PUSH_GROUP_KHR) - CASE_TO_STRING(GL_DEBUG_TYPE_POP_GROUP_KHR) - default: - return "Unknown"; - } - } - - static const char* ResetStatusString(GLenum status) - { - switch (status) { - CASE_TO_STRING(GL_GUILTY_CONTEXT_RESET_KHR) - CASE_TO_STRING(GL_INNOCENT_CONTEXT_RESET_KHR) - CASE_TO_STRING(GL_UNKNOWN_CONTEXT_RESET_KHR) - default: - return "Invalid"; - } - } -#undef CASE_TO_STRING - - static inline std::string ShaderInfoLog(GLuint handle) - { - - std::string log; - - if (glIsShader(handle) == true) { - GLint length; - - glGetShaderiv(handle, GL_INFO_LOG_LENGTH, &length); - - if (length > 1) { - log.resize(length); - glGetShaderInfoLog(handle, length, NULL, &log[0]); - } - } else { - log = ("Handle is a invalid shader handle"); - } - - return log; - } - - static inline std::string ProgramInfoLog(GLuint handle) - { - std::string log; - if (glIsProgram(handle)) { - GLint length(0); - - glGetProgramiv(handle, GL_INFO_LOG_LENGTH, &length); - - if (length > 1) { - log.resize(length); - glGetProgramInfoLog(handle, length, NULL, &log[0]); - } - } else { - log = ("Handle is a invalid program handle"); - } - - return log; - } - - static inline bool HasRenderer(const std::string& name) - { - static const std::string renderer(reinterpret_cast(glGetString(GL_RENDERER))); - return ((name.size() > 0) && (renderer.find(name) != std::string::npos)); - } - - GL(const GL&) = delete; - GL& operator=(const GL&) = delete; - - GL() - : glEGLImageTargetTexture2DOES(nullptr) - , glEGLImageTargetRenderbufferStorageOES(nullptr) - , glDebugMessageCallbackKHR(nullptr) - , glDebugMessageControlKHR(nullptr) - , glPopDebugGroupKHR(nullptr) - , glPushDebugGroupKHR(nullptr) - , glGetGraphicsResetStatusKHR(nullptr) - { - glEGLImageTargetTexture2DOES = reinterpret_cast(eglGetProcAddress("glEGLImageTargetTexture2DOES")); - glEGLImageTargetRenderbufferStorageOES = reinterpret_cast(eglGetProcAddress("glEGLImageTargetRenderbufferStorageOES")); - glDebugMessageCallbackKHR = reinterpret_cast(eglGetProcAddress("glDebugMessageCallbackKHR")); - glDebugMessageControlKHR = reinterpret_cast(eglGetProcAddress("glDebugMessageControlKHR")); - glPopDebugGroupKHR = reinterpret_cast(eglGetProcAddress("glPopDebugGroupKHR")); - glPushDebugGroupKHR = reinterpret_cast(eglGetProcAddress("glPushDebugGroupKHR")); - glGetGraphicsResetStatusKHR = reinterpret_cast(eglGetProcAddress("glGetGraphicsResetStatusKHR")); - } - - PFNGLEGLIMAGETARGETTEXTURE2DOESPROC glEGLImageTargetTexture2DOES; - PFNGLEGLIMAGETARGETRENDERBUFFERSTORAGEOESPROC glEGLImageTargetRenderbufferStorageOES; - PFNGLDEBUGMESSAGECALLBACKKHRPROC glDebugMessageCallbackKHR; - PFNGLDEBUGMESSAGECONTROLKHRPROC glDebugMessageControlKHR; - PFNGLPOPDEBUGGROUPKHRPROC glPopDebugGroupKHR; - PFNGLPUSHDEBUGGROUPKHRPROC glPushDebugGroupKHR; - PFNGLGETGRAPHICSRESETSTATUSKHRPROC glGetGraphicsResetStatusKHR; - }; // class GL - - class EGL { - public: -/* simple stringification operator to make errorcodes human readable */ -#define CASE_TO_STRING(value) \ - case value: \ - return #value; - - static const char* ErrorString(EGLint code) - { - switch (code) { - CASE_TO_STRING(EGL_SUCCESS) - CASE_TO_STRING(EGL_NOT_INITIALIZED) - CASE_TO_STRING(EGL_BAD_ACCESS) - CASE_TO_STRING(EGL_BAD_ALLOC) - CASE_TO_STRING(EGL_BAD_ATTRIBUTE) - CASE_TO_STRING(EGL_BAD_CONTEXT) - CASE_TO_STRING(EGL_BAD_CONFIG) - CASE_TO_STRING(EGL_BAD_CURRENT_SURFACE) - CASE_TO_STRING(EGL_BAD_DISPLAY) - CASE_TO_STRING(EGL_BAD_SURFACE) - CASE_TO_STRING(EGL_BAD_MATCH) - CASE_TO_STRING(EGL_BAD_PARAMETER) - CASE_TO_STRING(EGL_BAD_NATIVE_PIXMAP) - CASE_TO_STRING(EGL_BAD_NATIVE_WINDOW) - CASE_TO_STRING(EGL_CONTEXT_LOST) - default: - return "Unknown"; - } - } -#undef CASE_TO_STRING - static inline bool HasClientAPI(const std::string& api, EGLDisplay dpy = eglGetCurrentDisplay()) - { - static const std::string apis(eglQueryString(dpy, EGL_CLIENT_APIS)); - return ((api.size() > 0) && (apis.find(api) != std::string::npos)); - } - - EGL(const EGL&) = delete; - EGL& operator=(const EGL&) = delete; - - EGL() - : eglGetPlatformDisplayEXT(nullptr) - , eglQueryDmaBufFormatsEXT(nullptr) - , eglQueryDmaBufModifiersEXT(nullptr) - , eglQueryDisplayAttribEXT(nullptr) - , eglQueryDeviceStringEXT(nullptr) - , eglQueryDevicesEXT(nullptr) - , eglDebugMessageControl(nullptr) - , eglQueryDebug(nullptr) - , eglLabelObject(nullptr) - , eglCreateImage(nullptr) - , eglDestroyImage(nullptr) - , eglCreateSync(nullptr) - , eglDestroySync(nullptr) - , eglWaitSync(nullptr) - , eglClientWaitSync(nullptr) - , eglExportDmaBufImageQueryMesa(nullptr) - , eglExportDmaBufImageMesa(nullptr) - { - eglGetPlatformDisplayEXT = reinterpret_cast(eglGetProcAddress("eglGetPlatformDisplayEXT")); - eglQueryDmaBufFormatsEXT = reinterpret_cast(eglGetProcAddress("eglQueryDmaBufFormatsEXT")); - eglQueryDmaBufModifiersEXT = reinterpret_cast(eglGetProcAddress("eglQueryDmaBufModifiersEXT")); - eglQueryDisplayAttribEXT = reinterpret_cast(eglGetProcAddress("eglQueryDisplayAttribEXT")); - eglQueryDeviceStringEXT = reinterpret_cast(eglGetProcAddress("eglQueryDeviceStringEXT")); - eglQueryDevicesEXT = reinterpret_cast(eglGetProcAddress("eglQueryDevicesEXT")); - - eglDebugMessageControl = reinterpret_cast(eglGetProcAddress("eglDebugMessageControlKHR")); - eglQueryDebug = reinterpret_cast(eglGetProcAddress("eglQueryDebugKHR")); - eglLabelObject = reinterpret_cast(eglGetProcAddress("eglLabelObjectKHR")); - eglCreateImage = reinterpret_cast(eglGetProcAddress(eglCreateImageProc)); - eglDestroyImage = reinterpret_cast(eglGetProcAddress(eglDestroyImageProc)); - eglCreateSync = reinterpret_cast(eglGetProcAddress(eglCreateSyncProc)); - eglDestroySync = reinterpret_cast(eglGetProcAddress(eglDestroySyncProc)); - eglWaitSync = reinterpret_cast(eglGetProcAddress(eglWaitSyncProc)); - eglClientWaitSync = reinterpret_cast(eglGetProcAddress(eglClientWaitSyncProc)); - - eglExportDmaBufImageQueryMesa = reinterpret_cast(eglGetProcAddress("eglExportDMABUFImageQueryMESA")); - eglExportDmaBufImageMesa = reinterpret_cast(eglGetProcAddress("eglExportDMABUFImageMESA")); - } - - public: - PFNEGLGETPLATFORMDISPLAYEXTPROC eglGetPlatformDisplayEXT; - - PFNEGLQUERYDMABUFFORMATSEXTPROC eglQueryDmaBufFormatsEXT; - PFNEGLQUERYDMABUFMODIFIERSEXTPROC eglQueryDmaBufModifiersEXT; - PFNEGLQUERYDISPLAYATTRIBEXTPROC eglQueryDisplayAttribEXT; - PFNEGLQUERYDEVICESTRINGEXTPROC eglQueryDeviceStringEXT; - PFNEGLQUERYDEVICESEXTPROC eglQueryDevicesEXT; - - PFNEGLDEBUGMESSAGECONTROLKHRPROC eglDebugMessageControl; - PFNEGLQUERYDEBUGKHRPROC eglQueryDebug; - PFNEGLLABELOBJECTKHRPROC eglLabelObject; - PFNEGLCREATEIMAGEPROC eglCreateImage; - PFNEGLDESTROYIMAGEPROC eglDestroyImage; - PFNEGLCREATESYNCPROC eglCreateSync; - PFNEGLDESTROYSYNCPROC eglDestroySync; - PFNEGLWAITSYNCPROC eglWaitSync; - PFNEGLCLIENTWAITSYNCPROC eglClientWaitSync; - - PFNEGLEXPORTDMABUFIMAGEQUERYMESAPROC eglExportDmaBufImageQueryMesa; - PFNEGLEXPORTDMABUFIMAGEMESAPROC eglExportDmaBufImageMesa; - }; // class EGL - - } // namespace API -} // namespace Compositor -} // namespace Thunder diff --git a/Source/compositorclient/test/CMakeLists.txt b/Source/compositorclient/test/CMakeLists.txt index 7ad7b107..67817c57 100644 --- a/Source/compositorclient/test/CMakeLists.txt +++ b/Source/compositorclient/test/CMakeLists.txt @@ -1,7 +1,8 @@ -# If not stated otherwise in this file or this component's LICENSE file the + +# If not stated otherwise in this file or this component's license file the # following copyright and licenses apply: # -# Copyright 2023 Metrological +# Copyright 2025 Metrological # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -15,24 +16,19 @@ # See the License for the specific language governing permissions and # limitations under the License. +option(BUILD_CLIENT_COMPOSITOR_GBM_UTIL "Build the GBM basic test" ON) +option(BUILD_CLIENT_COMPOSITOR_RENDER_TEST "Build the renderer compositor client test" ON) +option(BUILD_COMPOSITORCLIENT_TEST "Build Compositor Client legacy test" OFF) -find_package(CompileSettingsDebug CONFIG REQUIRED) -find_package(${NAMESPACE}Core CONFIG REQUIRED) -find_package(${NAMESPACE}Messaging CONFIG REQUIRED) -find_package(${NAMESPACE}LocalTracer CONFIG REQUIRED) - -add_executable(compositorclient_test main.cpp) -target_link_libraries(compositorclient_test - PRIVATE - ${NAMESPACE}Core::${NAMESPACE}Core - ${NAMESPACE}Messaging::${NAMESPACE}Messaging - ${NAMESPACE}LocalTracer::${NAMESPACE}LocalTracer - ClientVirtualInput::ClientVirtualInput - CompileSettingsDebug::CompileSettingsDebug - ClientCompositor::ClientCompositor -) +if(BUILD_CLIENT_COMPOSITOR_GBM_UTIL) +add_subdirectory(gbm_buffer_test) +endif() -if(INSTALL_TESTS) - install(TARGETS compositorclient_test DESTINATION ${CMAKE_INSTALL_BINDIR} COMPONENT ${NAMESPACE}_Test) +if(BUILD_CLIENT_COMPOSITOR_RENDER_TEST) +add_subdirectory(client-renderer) endif() + +if(BUILD_COMPOSITORCLIENT_TEST) +add_subdirectory(legacy-test) +endif() \ No newline at end of file diff --git a/Source/compositorclient/test/client-renderer/CMakeLists.txt b/Source/compositorclient/test/client-renderer/CMakeLists.txt new file mode 100644 index 00000000..f6577d41 --- /dev/null +++ b/Source/compositorclient/test/client-renderer/CMakeLists.txt @@ -0,0 +1,23 @@ +# If not stated otherwise in this file or this component's license file the +# following copyright and licenses apply: +# +# Copyright 2025 Metrological +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License + +option(INSTALL_CLIENT_COMPOSITOR_RENDER_TEST_APP "Install the client texture renderer test application" ON) +option(INSTALL_CLIENT_COMPOSITOR_RENDER_TEST_PLUGIN "Install the client texture renderer test plugin" ON) + +add_subdirectory(common) +add_subdirectory(app) +add_subdirectory(plugin) \ No newline at end of file diff --git a/Source/compositorclient/test/client-renderer/app/CMakeLists.txt b/Source/compositorclient/test/client-renderer/app/CMakeLists.txt new file mode 100644 index 00000000..5e018063 --- /dev/null +++ b/Source/compositorclient/test/client-renderer/app/CMakeLists.txt @@ -0,0 +1,47 @@ +# If not stated otherwise in this file or this component's license file the +# following copyright and licenses apply: +# +# Copyright 2025 Metrological +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License + +find_package(Thunder REQUIRED) +find_package(${NAMESPACE}Core CONFIG REQUIRED) +find_package(${NAMESPACE}Messaging CONFIG REQUIRED) +find_package(${NAMESPACE}LocalTracer CONFIG REQUIRED) +find_package(CompileSettingsDebug CONFIG REQUIRED) + +add_executable(client-compositor-render + Module.cpp + Main.cpp +) + +target_link_libraries(client-compositor-render + PRIVATE + ${NAMESPACE}Core::${NAMESPACE}Core + ${NAMESPACE}Messaging::${NAMESPACE}Messaging + ${NAMESPACE}LocalTracer::${NAMESPACE}LocalTracer + CompileSettingsDebug::CompileSettingsDebug + ClientCompositorRenderCommon +) + +set_target_properties(client-compositor-render PROPERTIES + CXX_STANDARD 11 + CXX_STANDARD_REQUIRED YES +) + +if(INSTALL_CLIENT_COMPOSITOR_RENDER_TEST_APP) +install(TARGETS client-compositor-render + DESTINATION ${CMAKE_INSTALL_BINDIR} + COMPONENT ${NAMESPACE}_Test) +endif() \ No newline at end of file diff --git a/Source/compositorclient/test/client-renderer/app/Main.cpp b/Source/compositorclient/test/client-renderer/app/Main.cpp new file mode 100644 index 00000000..5a1f15bc --- /dev/null +++ b/Source/compositorclient/test/client-renderer/app/Main.cpp @@ -0,0 +1,423 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2025 Metrological + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "Module.h" + +#include "TextureBounce.h" +#include "IModel.h" +#include "TerminalInput.h" +#include "Renderer.h" + +#include + +#include + +#include +#include +#include + +using namespace Thunder; + +namespace { +const char Namespace[] = EXPAND_AND_QUOTE(NAMESPACE); +} + +class ConsoleOptions : public Thunder::Core::Options { +public: + ConsoleOptions(int argc, TCHAR* argv[]) + : Thunder::Core::Options(argc, argv, _T("t:n:W:H:h")) + , Texture("/usr/share/" + std::string(Namespace) + "/ClientCompositorRender/ml-tv-color-small.png") + , Width(1920) + , Height(1080) + { + Parse(); + } + + std::string Texture; + uint8_t TextureNumber; + uint16_t Width; + uint16_t Height; + +private: + void Option(const TCHAR option, const TCHAR* argument) override + { + switch (option) { + case 't': + Texture = argument; + break; + case 'n': + TextureNumber = static_cast(std::stoi(argument)); + break; + case 'W': + Width = static_cast(std::stoi(argument)); + break; + case 'H': + Height = static_cast(std::stoi(argument)); + break; + case 'h': + default: + fprintf(stderr, "Usage: " EXPAND_AND_QUOTE(APPLICATION_NAME) " [-t ] [-n 40] [-W 1280] [-H 720]\n"); + exit(EXIT_FAILURE); + } + } +}; + +class Tracer { +public: + struct ModuleConfig { + std::string Module; + std::set EnabledCategories; + }; + + Tracer() + : _tracer(Messaging::LocalTracer::Open()) + , _printer(true) + , _modules() + { + _tracer.Callback(&_printer); + } + + ~Tracer() + { + _tracer.Close(); + } + + // Simple enable/disable + void Set(const std::string& module, const std::string& category, bool enable) + { + if (enable) { + _modules[module].EnabledCategories.insert(category); + } else { + _modules[module].EnabledCategories.erase(category); + } + + _tracer.EnableMessage(module, category, enable); + } + + // Bulk configure + void Configure(const std::string& module, const std::vector& categories) + { + for (const auto& category : categories) { + Set(module, category, true); + } + } + + // Enable ALL discovered categories for a module + void EnableAll(const std::string& module) + { + auto categories = DiscoverCategories(module); + for (const auto& category : categories) { + Set(module, category, true); + } + } + + // Auto-discover available modules + std::vector DiscoverModules() + { + std::vector modules; + + class ModuleHandler : public Core::Messaging::IControl::IHandler { + public: + ModuleHandler(std::vector& modules) : _modules(modules) {} + + void Handle(Core::Messaging::IControl* control) override { + const string& module = control->Metadata().Module(); + if (std::find(_modules.begin(), _modules.end(), module) == _modules.end()) { + _modules.push_back(module); + } + } + private: + std::vector& _modules; + } handler(modules); + + Core::Messaging::IControl::Iterate(handler); + + return modules; + } + + // Auto-discover categories for a module + std::vector DiscoverCategories(const std::string& module) + { + std::vector categories; + + class CategoryHandler : public Core::Messaging::IControl::IHandler { + public: + CategoryHandler(const std::string& module, std::vector& categories) + : _module(module), _categories(categories) {} + + void Handle(Core::Messaging::IControl* control) override { + if (control->Metadata().Module() == _module) { + const string& category = control->Metadata().Category(); + if (!category.empty()) { + _categories.push_back(category); + } + } + } + private: + const std::string& _module; + std::vector& _categories; + } handler(module, categories); + + Core::Messaging::IControl::Iterate(handler); + + return categories; + } + + // Fully dynamic menu + void Menu(Compositor::TerminalInput& keyboard, uint32_t timeoutSeconds = 30) + { + auto availableModules = DiscoverModules(); + + printf("\n"); + printf("=== Trace Configuration Menu ===\n"); + printf("Discovered %zu modules\n", availableModules.size()); + printf("Timeout: %us\n", timeoutSeconds); + printf("\n"); + printf("Commands:\n"); + printf(" [1-9] - Select module\n"); + printf(" D - Refresh module list\n"); + printf(" Q - Exit menu\n"); + printf("\n"); + + DisplayModuleList(availableModules); + + std::string selectedModule; + std::vector selectedCategories; + auto lastActivity = std::chrono::steady_clock::now(); + + while (true) { + auto now = std::chrono::steady_clock::now(); + auto elapsed = std::chrono::duration_cast(now - lastActivity).count(); + + if (elapsed >= timeoutSeconds) { + printf("Menu timeout\n"); + break; + } + + char key = keyboard.Read(); + if (key != 0) { + lastActivity = std::chrono::steady_clock::now(); + char upper = toupper(key); + + if (upper == 'Q') { + break; + } + + if (upper == 'D') { + availableModules = DiscoverModules(); + DisplayModuleList(availableModules); + selectedModule.clear(); + continue; + } + + if (selectedModule.empty()) { + // Module selection + if (key >= '1' && key <= '9') { + size_t idx = static_cast(key - '1'); + if (idx < availableModules.size()) { + selectedModule = availableModules[idx]; + selectedCategories = DiscoverCategories(selectedModule); + + printf("\n"); + printf("Module: %s\n", selectedModule.c_str()); + printf("\n"); + printf("Commands:\n"); + printf(" [1-9] - Toggle category\n"); + printf(" A - Enable all\n"); + printf(" O - Disable all\n"); + printf(" B - Back to modules\n"); + printf("\n"); + + DisplayCategoryList(selectedModule, selectedCategories); + } + } + } else { + // Category toggle mode + if (upper == 'B') { + selectedModule.clear(); + DisplayModuleList(availableModules); + } else if (upper == 'A') { + for (const auto& cat : selectedCategories) { + Set(selectedModule, cat, true); + } + DisplayCategoryList(selectedModule, selectedCategories); + } else if (upper == 'O') { + for (const auto& cat : selectedCategories) { + Set(selectedModule, cat, false); + } + DisplayCategoryList(selectedModule, selectedCategories); + } else if (key >= '1' && key <= '9') { + size_t idx = static_cast(key - '1'); + if (idx < selectedCategories.size()) { + const auto& category = selectedCategories[idx]; + bool currentlyEnabled = IsEnabled(selectedModule, category); + Set(selectedModule, category, !currentlyEnabled); + DisplayCategoryList(selectedModule, selectedCategories); + } + } + } + } + + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + } + } + +private: + bool IsEnabled(const std::string& module, const std::string& category) const + { + auto it = _modules.find(module); + if (it != _modules.end()) { + return it->second.EnabledCategories.count(category) > 0; + } + return false; + } + + void DisplayModuleList(const std::vector& modules) const + { + printf("Available modules:\n"); + for (size_t i = 0; i < modules.size() && i < 9; ++i) { + bool configured = (_modules.find(modules[i]) != _modules.end()); + printf(" [%zu] %s%s\n", i + 1, modules[i].c_str(), configured ? " *" : ""); + } + } + + void DisplayCategoryList(const std::string& module, + const std::vector& categories) const + { + printf("Categories for %s:\n", module.c_str()); + for (size_t i = 0; i < categories.size() && i < 9; ++i) { + bool enabled = IsEnabled(module, categories[i]); + printf(" [%zu] %s: %s\n", i + 1, categories[i].c_str(), enabled ? "ON" : "OFF"); + } + } + + Messaging::LocalTracer& _tracer; + Messaging::ConsolePrinter _printer; + std::map _modules; +}; + +int main(int argc, char* argv[]) +{ + const char* executableName(Thunder::Core::FileNameOnly(argv[0])); + int exitCode(0); + + { + Tracer tracer; + tracer.Configure("App_CompositionClientRender", { "Information", "Error", "Warning" }); + tracer.Configure("Common_CompositionClientRender", { "Error", "Warning" }); + + ConsoleOptions options(argc, argv); + bool quitApp(false); + + TRACE_GLOBAL(Trace::Information, ("%s - build: %s", executableName, __TIMESTAMP__)); + + std::string texturePath = options.Texture; + + if (texturePath.empty()) { + texturePath = "/usr/share/" + std::string(Namespace) + "/ClientCompositorRender/ml-tv-color-small.png"; + } + + Compositor::TextureBounce::Config config; + config.Image = texturePath; + config.ImageCount = options.TextureNumber; + + std::string configStr; + config.ToString(configStr); + + Compositor::Render renderer; + Compositor::TextureBounce model; + + if (renderer.Configure(options.Width, options.Height) == false) { + TRACE_GLOBAL(Trace::Error, ("Failed to initialize renderer")); + exitCode = 1; + } + + if (!renderer.Register(&model, configStr)) { + TRACE_GLOBAL(Trace::Error, ("Failed to initialize model")); + exitCode = 2; + } + + if (exitCode == 0) { + Compositor::TerminalInput keyboard; + ASSERT(keyboard.IsValid() == true); + + renderer.Start(); + + bool result; + + if (keyboard.IsValid() == true) { + while (!renderer.ShouldExit() && !quitApp) { + switch (toupper(keyboard.Read())) { + case 'S': + if (renderer.ShouldExit() == false) { + (renderer.IsRunning() == false) ? renderer.Start() : renderer.Stop(); + } + break; + case 'F': + result = renderer.ToggleFPS(); + TRACE_GLOBAL(Trace::Information, ("FPS: %s", result ? "on" : "off")); + break; + case 'Z': + result = renderer.ToggleRequestRender(); + TRACE_GLOBAL(Trace::Information, ("RequestRender: %s", result ? "off" : "on")); + break; + case 'R': + renderer.TriggerRender(); + break; + case 'M': + result = renderer.ToggleModelRender(); + TRACE_GLOBAL(Trace::Information, ("Model Render: %s", result ? "off" : "on")); + break; + case 'T': + tracer.Menu(keyboard, 30); // 30 second timeout + TRACE_GLOBAL(Trace::Information, ("Returning to main menu")); + break; + case 'Q': + quitApp = true; + break; + case 'H': + TRACE_GLOBAL(Trace::Information, ("Available commands:")); + TRACE_GLOBAL(Trace::Information, (" S - Start/Stop rendering loop")); + TRACE_GLOBAL(Trace::Information, (" F - Toggle FPS display overlay")); + TRACE_GLOBAL(Trace::Information, (" Z - Toggle surface RequestRender calls")); + TRACE_GLOBAL(Trace::Information, (" R - Trigger single render request")); + TRACE_GLOBAL(Trace::Information, (" M - Toggle model Draw calls")); + TRACE_GLOBAL(Trace::Information, (" T - Trace configuration menu")); + TRACE_GLOBAL(Trace::Information, (" Q - Quit application")); + TRACE_GLOBAL(Trace::Information, (" H - Show this help")); + break; + default: + break; + } + + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + } + } else { + TRACE_GLOBAL(Thunder::Trace::Error, ("Failed to initialize keyboard input")); + } + + renderer.Stop(); + TRACE_GLOBAL(Thunder::Trace::Information, ("Exiting %s.... ", executableName)); + } + } + + Core::Singleton::Dispose(); + + return exitCode; +} diff --git a/Source/compositorclient/test/client-renderer/app/Module.cpp b/Source/compositorclient/test/client-renderer/app/Module.cpp new file mode 100644 index 00000000..160593e6 --- /dev/null +++ b/Source/compositorclient/test/client-renderer/app/Module.cpp @@ -0,0 +1,23 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2025 Metrological + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "Module.h" + +MODULE_NAME_DECLARATION(BUILD_REFERENCE) + diff --git a/Source/compositorclient/test/client-renderer/app/Module.h b/Source/compositorclient/test/client-renderer/app/Module.h new file mode 100644 index 00000000..12df3601 --- /dev/null +++ b/Source/compositorclient/test/client-renderer/app/Module.h @@ -0,0 +1,37 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2025 Metrological + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#ifndef MODULE_NAME +#define MODULE_NAME App_CompositionClientRender +#endif + +#include +#include + +#if defined(__WINDOWS__) +#if defined(COMPOSITORCLIENT_EXPORTS) +#undef EXTERNAL +#define EXTERNAL EXTERNAL_EXPORT +#else +#pragma comment(lib, "compositorclient.lib") +#endif +#endif + diff --git a/Source/compositorclient/test/client-renderer/common/CMakeLists.txt b/Source/compositorclient/test/client-renderer/common/CMakeLists.txt new file mode 100644 index 00000000..338b31da --- /dev/null +++ b/Source/compositorclient/test/client-renderer/common/CMakeLists.txt @@ -0,0 +1,69 @@ +# If not stated otherwise in this file or this component's license file the +# following copyright and licenses apply: +# +# Copyright 2025 Metrological +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License + +find_package(PNG REQUIRED) +find_package(EGL REQUIRED) +find_package(GLESv2 REQUIRED) +find_package(Thunder REQUIRED) +find_package(${NAMESPACE}Core CONFIG REQUIRED) +find_package(${NAMESPACE}Messaging CONFIG REQUIRED) +find_package(CompileSettingsDebug CONFIG REQUIRED) + +add_subdirectory(Fonts) + +add_library(ClientCompositorRenderCommon STATIC + Module.cpp + TextureBounce.cpp + TextureLoader.cpp + Renderer.cpp + TextRender.cpp +) + +set_target_properties(ClientCompositorRenderCommon PROPERTIES POSITION_INDEPENDENT_CODE ON) + +target_link_libraries(ClientCompositorRenderCommon + PUBLIC + ${NAMESPACE}Core::${NAMESPACE}Core + ${NAMESPACE}Messaging::${NAMESPACE}Messaging + ClientCompositor::ClientCompositor + ClientVirtualInput::ClientVirtualInput # this hack is needed because the test app cannot look up the library in the staging dir yet... + CompileSettingsDebug::CompileSettingsDebug + EGL::EGL + GLESv2::GLESv2 + PNG::PNG + ArialFont +) + +target_include_directories(ClientCompositorRenderCommon PUBLIC + $) + + +set_target_properties(ClientCompositorRenderCommon PROPERTIES + CXX_STANDARD 11 + CXX_STANDARD_REQUIRED YES +) + +target_compile_definitions(ClientCompositorRenderCommon PUBLIC + NAMESPACE=${NAMESPACE} +) + +if(INSTALL_CLIENT_COMPOSITOR_RENDER_TEST_APP OR INSTALL_CLIENT_COMPOSITOR_RENDER_TEST_PLUGIN) +install(FILES + ${CMAKE_CURRENT_SOURCE_DIR}/ml-tv-color-small.png + DESTINATION ${CMAKE_INSTALL_DATADIR}/${NAMESPACE}/ClientCompositorRender + COMPONENT ${NAMESPACE}_Test) +endif() \ No newline at end of file diff --git a/Source/compositorclient/test/client-renderer/common/Fonts/Arial.h b/Source/compositorclient/test/client-renderer/common/Fonts/Arial.h new file mode 100644 index 00000000..be35aa88 --- /dev/null +++ b/Source/compositorclient/test/client-renderer/common/Fonts/Arial.h @@ -0,0 +1,137 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2025 Metrological + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include "Font.h" + +namespace Thunder { +namespace Compositor { + + static const Character characters_Arial[] = { + { ' ', 411, 146, 12, 12, 6, 6 }, + { '!', 164, 112, 15, 34, 3, 28 }, + { '"', 262, 146, 21, 19, 5, 28 }, + { '#', 283, 77, 30, 34, 6, 28 }, + { '$', 230, 0, 29, 38, 6, 30 }, + { '%', 434, 0, 38, 35, 5, 29 }, + { '&', 33, 42, 32, 35, 5, 28 }, + { '\'', 283, 146, 15, 19, 4, 28 }, + { '(', 44, 0, 21, 42, 4, 29 }, + { ')', 65, 0, 21, 42, 6, 29 }, + { '*', 195, 146, 23, 23, 5, 28 }, + { '+', 140, 146, 28, 28, 4, 25 }, + { ',', 246, 146, 16, 20, 3, 9 }, + { '-', 345, 146, 20, 15, 5, 16 }, + { '.', 365, 146, 15, 15, 3, 9 }, + { '/', 392, 0, 21, 36, 6, 29 }, + { '0', 159, 42, 28, 35, 5, 29 }, + { '1', 112, 112, 26, 34, 4, 28 }, + { '2', 243, 42, 27, 35, 4, 29 }, + { '3', 270, 42, 27, 35, 5, 29 }, + { '4', 29, 112, 28, 34, 5, 28 }, + { '5', 297, 42, 27, 35, 5, 28 }, + { '6', 324, 42, 27, 35, 4, 29 }, + { '7', 85, 112, 27, 34, 4, 28 }, + { '8', 351, 42, 27, 35, 5, 29 }, + { '9', 378, 42, 27, 35, 4, 29 }, + { ':', 125, 146, 15, 29, 3, 23 }, + { ';', 215, 112, 16, 33, 3, 23 }, + { '<', 437, 112, 28, 29, 4, 25 }, + { '=', 218, 146, 28, 22, 4, 22 }, + { '>', 465, 112, 28, 29, 4, 25 }, + { '?', 187, 42, 28, 35, 5, 29 }, + { '@', 191, 0, 39, 40, 3, 29 }, + { 'A', 91, 77, 33, 34, 6, 28 }, + { 'B', 463, 77, 29, 34, 3, 28 }, + { 'C', 65, 42, 32, 35, 4, 29 }, + { 'D', 221, 77, 31, 34, 3, 28 }, + { 'E', 492, 77, 29, 34, 3, 28 }, + { 'F', 57, 112, 28, 34, 3, 28 }, + { 'G', 0, 42, 33, 35, 4, 29 }, + { 'H', 313, 77, 30, 34, 3, 28 }, + { 'I', 179, 112, 15, 34, 3, 28 }, + { 'J', 457, 42, 25, 35, 5, 28 }, + { 'K', 343, 77, 30, 34, 3, 28 }, + { 'L', 138, 112, 26, 34, 3, 28 }, + { 'M', 57, 77, 34, 34, 3, 28 }, + { 'N', 373, 77, 30, 34, 3, 28 }, + { 'O', 472, 0, 34, 35, 4, 29 }, + { 'P', 0, 112, 29, 34, 3, 28 }, + { 'Q', 157, 0, 34, 41, 4, 29 }, + { 'R', 252, 77, 31, 34, 3, 28 }, + { 'S', 97, 42, 31, 35, 5, 29 }, + { 'T', 403, 77, 30, 34, 5, 28 }, + { 'U', 128, 42, 31, 35, 4, 28 }, + { 'V', 124, 77, 33, 34, 6, 28 }, + { 'W', 15, 77, 42, 34, 6, 28 }, + { 'X', 157, 77, 32, 34, 5, 28 }, + { 'Y', 189, 77, 32, 34, 5, 28 }, + { 'Z', 433, 77, 30, 34, 5, 28 }, + { '[', 86, 0, 19, 42, 4, 29 }, + { '\\', 413, 0, 21, 36, 6, 29 }, + { ']', 105, 0, 19, 42, 6, 29 }, + { '^', 168, 146, 27, 24, 6, 28 }, + { '_', 380, 146, 31, 14, 6, 2 }, + { '`', 298, 146, 19, 17, 4, 30 }, + { 'a', 231, 112, 29, 30, 5, 23 }, + { 'b', 340, 0, 26, 36, 4, 29 }, + { 'c', 314, 112, 26, 30, 5, 23 }, + { 'd', 259, 0, 27, 36, 5, 29 }, + { 'e', 260, 112, 27, 30, 5, 23 }, + { 'f', 482, 42, 21, 35, 6, 29 }, + { 'g', 286, 0, 27, 36, 5, 23 }, + { 'h', 405, 42, 26, 35, 4, 29 }, + { 'i', 503, 42, 15, 35, 4, 29 }, + { 'j', 124, 0, 18, 42, 7, 29 }, + { 'k', 431, 42, 26, 35, 4, 29 }, + { 'l', 0, 77, 15, 35, 4, 29 }, + { 'm', 402, 112, 35, 29, 4, 23 }, + { 'n', 28, 146, 26, 29, 4, 23 }, + { 'o', 287, 112, 27, 30, 5, 23 }, + { 'p', 366, 0, 26, 36, 4, 23 }, + { 'q', 313, 0, 27, 36, 5, 23 }, + { 'r', 105, 146, 20, 29, 4, 23 }, + { 's', 340, 112, 26, 30, 5, 23 }, + { 't', 194, 112, 21, 33, 6, 27 }, + { 'u', 54, 146, 26, 29, 4, 23 }, + { 'v', 493, 112, 28, 29, 6, 23 }, + { 'w', 366, 112, 36, 29, 6, 23 }, + { 'x', 0, 146, 28, 29, 6, 23 }, + { 'y', 215, 42, 28, 35, 6, 23 }, + { 'z', 80, 146, 25, 29, 5, 23 }, + { '{', 0, 0, 22, 42, 5, 29 }, + { '|', 142, 0, 15, 42, 3, 29 }, + { '}', 22, 0, 22, 42, 5, 29 }, + { '~', 317, 146, 28, 16, 5, 19 }, + }; + + static const Font Arial( + "Arial", + "Arial.png", + 32, + 0, + 0, + 521, + 175, + 95, + characters_Arial + ); +} // namespace Compositor +} // namespace Thunder \ No newline at end of file diff --git a/Source/compositorclient/test/client-renderer/common/Fonts/Arial.png b/Source/compositorclient/test/client-renderer/common/Fonts/Arial.png new file mode 100644 index 00000000..db85ae49 Binary files /dev/null and b/Source/compositorclient/test/client-renderer/common/Fonts/Arial.png differ diff --git a/Source/compositorclient/test/client-renderer/common/Fonts/CMakeLists.txt b/Source/compositorclient/test/client-renderer/common/Fonts/CMakeLists.txt new file mode 100644 index 00000000..34e00368 --- /dev/null +++ b/Source/compositorclient/test/client-renderer/common/Fonts/CMakeLists.txt @@ -0,0 +1,12 @@ + +add_library(ArialFont INTERFACE) + +target_include_directories(ArialFont INTERFACE + $) + +if(INSTALL_CLIENT_COMPOSITOR_RENDER_TEST_APP OR INSTALL_CLIENT_COMPOSITOR_RENDER_TEST_PLUGIN) +install(FILES + ${CMAKE_CURRENT_SOURCE_DIR}/Arial.png + DESTINATION ${CMAKE_INSTALL_DATADIR}/${NAMESPACE}/ClientCompositorRender + COMPONENT ${NAMESPACE}_Test) +endif() \ No newline at end of file diff --git a/Source/compositorclient/test/client-renderer/common/Fonts/Font.h b/Source/compositorclient/test/client-renderer/common/Fonts/Font.h new file mode 100644 index 00000000..1923dfff --- /dev/null +++ b/Source/compositorclient/test/client-renderer/common/Fonts/Font.h @@ -0,0 +1,53 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2025 Metrological + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +namespace Thunder { +namespace Compositor { + struct Character { + int codePoint; + int x; + int y; + int width; + int height; + int originX; + int originY; + }; + + struct Font { + const char* name; + const char* file; + int size; + int bold; + int italic; + int width; + int height; + int characterCount; + const Character* characters; + + Font(const char* n, const char* f, int s, int b, int i, + int w, int h, int count, const Character* chars) + : name(n), file(f), size(s), bold(b), italic(i) + , width(w), height(h), characterCount(count), characters(chars) + { + } + }; +} // namespace Compositor +} // namespace Thunder \ No newline at end of file diff --git a/Source/compositorclient/test/client-renderer/common/IModel.h b/Source/compositorclient/test/client-renderer/common/IModel.h new file mode 100644 index 00000000..9dec3b56 --- /dev/null +++ b/Source/compositorclient/test/client-renderer/common/IModel.h @@ -0,0 +1,31 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2025 Metrological + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +namespace Thunder { +namespace Compositor { + struct IModel { + virtual ~IModel() = default; + + virtual bool Initialize(const uint16_t width, const uint16_t height, const std::string& config) = 0; + virtual bool Draw() = 0; + }; +} // namespace Compositor +} // namespace Thunder diff --git a/Source/compositorclient/test/client-renderer/common/Module.cpp b/Source/compositorclient/test/client-renderer/common/Module.cpp new file mode 100644 index 00000000..5b25d568 --- /dev/null +++ b/Source/compositorclient/test/client-renderer/common/Module.cpp @@ -0,0 +1,23 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2025 Metrological + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "Module.h" + +MODULE_NAME_ARCHIVE_DECLARATION + diff --git a/Source/compositorclient/test/client-renderer/common/Module.h b/Source/compositorclient/test/client-renderer/common/Module.h new file mode 100644 index 00000000..7b3f0d20 --- /dev/null +++ b/Source/compositorclient/test/client-renderer/common/Module.h @@ -0,0 +1,77 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2025 Metrological + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#ifndef MODULE_NAME +#define MODULE_NAME Common_CompositionClientRender +#endif + +#include +#include + +#if defined(__WINDOWS__) +#if defined(COMPOSITORCLIENT_EXPORTS) +#undef EXTERNAL +#define EXTERNAL EXTERNAL_EXPORT +#else +#pragma comment(lib, "compositorclient.lib") +#endif +#endif + + +namespace Thunder { +namespace Trace { +class Timing { +public: + ~Timing() = default; + Timing() = delete; + Timing(const Timing&) = delete; + Timing& operator=(const Timing&) = delete; + Timing(const TCHAR formatter[], ...) + { + va_list ap; + va_start(ap, formatter); + Thunder::Core::Format(_text, formatter, ap); + va_end(ap); + } + explicit Timing(const string& text) + : _text(Thunder::Core::ToString(text)) + { + } + +public: + const char* Data() const + { + return (_text.c_str()); + } + uint16_t Length() const + { + return (static_cast(_text.length())); + } + +private: + std::string _text; +}; // class Timing + +} // namespace Trace +} // namespace Thunder + + + diff --git a/Source/compositorclient/test/client-renderer/common/Renderer.cpp b/Source/compositorclient/test/client-renderer/common/Renderer.cpp new file mode 100644 index 00000000..3a2b665f --- /dev/null +++ b/Source/compositorclient/test/client-renderer/common/Renderer.cpp @@ -0,0 +1,345 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2025 Metrological + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "Renderer.h" + +#include +#include +#include +#include "Fonts/Arial.h" + +namespace Thunder { +namespace Compositor { + + static const char Namespace[] = EXPAND_AND_QUOTE(NAMESPACE); + + Render::Render() + : _display(nullptr) + , _surface(nullptr) + , _displayName() + , _canvasWidth(0) + , _canvasHeight(0) + , _eglDisplay(EGL_NO_DISPLAY) + , _eglContext(EGL_NO_CONTEXT) + , _eglSurface(EGL_NO_SURFACE) + , _render() + , _exitMutex() + , _exitSignal() + , _exitRequested(false) + , _running(false) + , _rendering() + , _renderSync() + , _showFps(true) + , _skipRender(false) + , _skipModel(false) + , _models() + , _selectedModel(~0) + , _rng(static_cast(std::chrono::steady_clock::now().time_since_epoch().count())) + , _textRender(&Arial) + , _lastFPSUpdate(0) + , _frameCount(0) + , _currentFPS(0.0f) + { + } + + bool Render::Configure(const uint16_t width, const uint16_t height) + { + _canvasWidth = width; + _canvasHeight = height; + + _lastFPSUpdate = Core::Time::Now().Ticks(); + + _displayName = Compositor::IDisplay::SuggestedName(); + + if (_displayName.empty()) { + pid_t pid = getpid(); + char uniqueName[64]; + snprintf(uniqueName, sizeof(uniqueName), "CompositorClient-%d-%" PRIu64, pid, _lastFPSUpdate); + _displayName = uniqueName; + } + + _display = Compositor::IDisplay::Instance(_displayName); + + if (_display != nullptr) { + _surface = _display->Create(_displayName, width, height, this); + ASSERT(_surface != nullptr); + + if (!InitializeEGL()) { + TRACE(Trace::Error, ("Failed to initialize EGL")); + return false; + } + + TextRender::Config config; + config.FontAtlas = "/usr/share/" + std::string(Namespace) + "/ClientCompositorRender/Arial.png"; + config.Scale = 1.0f; + config.Red = 0.0f; // Green text + config.Green = 1.0f; + config.Blue = 0.0f; + config.Alpha = 1.0f; + + std::string configStr; + config.ToString(configStr); + + if (!_textRender.Initialize(width, height, configStr)) { + TRACE(Trace::Error, ("Failed to initialize FPS counter")); + return false; + } + + // Release EGL context for render thread + if (!eglMakeCurrent(_eglDisplay, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT)) { + EGLint error = eglGetError(); + TRACE(Trace::Error, ("Configure: eglMakeCurrent release failed: 0x%x", error)); + return false; + } + + return true; + } else { + TRACE(Trace::Error, ("Failed to create Compositor Display")); + return false; + } + } + + // ICallback + void Render::Rendered(Thunder::Compositor::IDisplay::ISurface*) + { + _renderSync.notify_all(); + } + + void Render::Published(Thunder::Compositor::IDisplay::ISurface*) + { + _frameCount++; + + uint64_t now = Core::Time::Now().Ticks(); + uint64_t elapsed = now - _lastFPSUpdate; + + // Update FPS every second (1000000 microseconds) + if (elapsed >= 1000000) { + _currentFPS = (_frameCount * 1000000.0f) / elapsed; + _frameCount = 0; + _lastFPSUpdate = now; + } + } + + bool Render::Register(IModel* model, const std::string& config) + { + bool result = InitializeModel(model, config); + + if (result == true) { + _models.push_back(model); + } else { + TRACE(Trace::Error, ("Failed to initialize model during registration")); + } + + return result; + } + + void Render::Unregister(IModel* model) + { + auto it = std::find(_models.begin(), _models.end(), model); + if (it != _models.end()) { + _models.erase(it); + } + } + + bool Render::InitializeModel(IModel* model, const std::string& config) + { + if (model == nullptr) { + return false; + } + + // Make context current + if (!eglMakeCurrent(_eglDisplay, _eglSurface, _eglSurface, _eglContext)) { + TRACE(Trace::Error, ("InitializeModel: eglMakeCurrent failed: 0x%x", eglGetError())); + return false; + } + + // Initialize model with active context + bool result = model->Initialize(_canvasWidth, _canvasHeight, config); + + // Release context + if (!eglMakeCurrent(_eglDisplay, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT)) { + TRACE(Trace::Error, ("InitializeModel: eglMakeCurrent release failed: 0x%x", eglGetError())); + } + + return result; + } + + bool Render::InitializeEGL() + { + EGLNativeDisplayType display = (_display != nullptr) ? _display->Native() : EGL_DEFAULT_DISPLAY; + EGLNativeWindowType window = (_surface != nullptr) ? _surface->Native() : 0; + + _eglDisplay = eglGetDisplay(display); + if (_eglDisplay == EGL_NO_DISPLAY) { + TRACE(Trace::Error, ("eglGetDisplay failed: 0x%x", eglGetError())); + return false; + } + + if (!eglInitialize(_eglDisplay, nullptr, nullptr)) { + TRACE(Trace::Error, ("eglInitialize failed: 0x%x", eglGetError())); + return false; + } + + static const EGLint configAttribs[] = { + EGL_SURFACE_TYPE, EGL_WINDOW_BIT, + EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT, + EGL_RED_SIZE, 8, + EGL_GREEN_SIZE, 8, + EGL_BLUE_SIZE, 8, + EGL_ALPHA_SIZE, 8, + EGL_NONE + }; + + EGLConfig eglConfig; + EGLint numConfigs; + if (!eglChooseConfig(_eglDisplay, configAttribs, &eglConfig, 1, &numConfigs)) { + TRACE(Trace::Error, ("eglChooseConfig failed: 0x%x", eglGetError())); + return false; + } + + static const EGLint contextAttribs[] = { + EGL_CONTEXT_CLIENT_VERSION, 2, + EGL_NONE + }; + + _eglContext = eglCreateContext(_eglDisplay, eglConfig, EGL_NO_CONTEXT, contextAttribs); + if (_eglContext == EGL_NO_CONTEXT) { + TRACE(Trace::Error, ("Failed to create EGL context: 0x%x", eglGetError())); + return false; + } + + _eglSurface = eglCreateWindowSurface(_eglDisplay, eglConfig, window, nullptr); + if (_eglSurface == EGL_NO_SURFACE) { + TRACE(Trace::Error, ("Failed to create EGL surface: 0x%x", eglGetError())); + return false; + } + + if (!eglMakeCurrent(_eglDisplay, _eglSurface, _eglSurface, _eglContext)) { + TRACE(Trace::Error, ("eglMakeCurrent failed: 0x%x", eglGetError())); + return false; + } + + return true; + } + + void Render::CleanupEGL() + { + if (_eglDisplay != EGL_NO_DISPLAY) { + eglMakeCurrent(_eglDisplay, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT); + if (_eglSurface != EGL_NO_SURFACE) { + eglDestroySurface(_eglDisplay, _eglSurface); + _eglSurface = EGL_NO_SURFACE; + } + if (_eglContext != EGL_NO_CONTEXT) { + eglDestroyContext(_eglDisplay, _eglContext); + _eglContext = EGL_NO_CONTEXT; + } + eglTerminate(_eglDisplay); + _eglDisplay = EGL_NO_DISPLAY; + } + } + + void Render::Draw() + { + uint64_t loopStart(Core::Time::Now().Ticks()); + uint64_t beforeSwap(loopStart); + uint64_t afterSwap(loopStart); + uint64_t afterRequest(loopStart); + + // Make context current for this render thread + if (!eglMakeCurrent(_eglDisplay, _eglSurface, _eglSurface, _eglContext)) { + EGLint error = eglGetError(); + TRACE(Trace::Error, ("Draw: eglMakeCurrent failed: 0x%x", error)); + return; + } + + while (_running.load() && !ShouldExit()) { + if (_models.empty() || _selectedModel >= _models.size()) { + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + continue; + } + + if (_skipModel == false) { + if (_models[_selectedModel.load()]->Draw() == true) { + + // Draw FPS if enabled + if (_showFps) { + _textRender.Draw(_displayName, 10, _canvasHeight - 40); + std::ostringstream ss; + ss << "FPS: " << std::fixed << std::setprecision(2) << _currentFPS; + _textRender.Draw(ss.str(), 10, 10); + } + + // Swap buffers + beforeSwap = Core::Time::Now().Ticks(); + eglSwapBuffers(_eglDisplay, _eglSurface); + afterSwap = Core::Time::Now().Ticks(); + + EGLSync fence = eglCreateSync(_eglDisplay, EGL_SYNC_FENCE, nullptr); + if (fence != EGL_NO_SYNC) { + // Wait for GPU to finish (100ms timeout) + EGLint result = eglClientWaitSync(_eglDisplay, fence, EGL_SYNC_FLUSH_COMMANDS_BIT, 100000000); + if (result == EGL_TIMEOUT_EXPIRED) { + TRACE(Trace::Error, ("Client GPU fence timeout after 100ms")); + } + eglDestroySync(_eglDisplay, fence); + } + + } else { + TRACE(Trace::Warning, ("Model draw failed")); + std::this_thread::sleep_for(std::chrono::milliseconds(4)); + } + } + + if (_skipRender == false) { + _surface->RequestRender(); + afterRequest = Core::Time::Now().Ticks(); + + // allow for for 2 25FPS frame delay + if (WaitForRendered(80) == Core::ERROR_TIMEDOUT) { + TRACE(Trace::Warning, ("Timed out waiting for rendered callback")); + } + } else { + std::this_thread::sleep_for(std::chrono::milliseconds(16)); + } + + TRACE(Trace::Timing, (_T("Surface[%s]: draw=%" PRIu64 " us, swap=%" PRIu64 " us, request=%" PRIu64 " us, total=%" PRIu64 " us"), _displayName.c_str(), (beforeSwap - loopStart), (afterSwap - beforeSwap), (afterRequest - afterSwap), (afterRequest - loopStart))); + } + + // Release context when done + eglMakeCurrent(_eglDisplay, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT); + } + + uint32_t Render::WaitForRendered(uint32_t timeoutMs) + { + uint32_t ret = Core::ERROR_NONE; + + std::unique_lock lock(_rendering); + if (timeoutMs == Core::infinite) { + _renderSync.wait(lock); + } else { + if (_renderSync.wait_for(lock, std::chrono::milliseconds(timeoutMs)) == std::cv_status::timeout) { + ret = Core::ERROR_TIMEDOUT; + } + } + + return ret; + } +} // namespace Compositor +} // namespace Thunder \ No newline at end of file diff --git a/Source/compositorclient/test/client-renderer/common/Renderer.h b/Source/compositorclient/test/client-renderer/common/Renderer.h new file mode 100644 index 00000000..d985bc8c --- /dev/null +++ b/Source/compositorclient/test/client-renderer/common/Renderer.h @@ -0,0 +1,197 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2025 Metrological + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once +#include "Module.h" +#include "IModel.h" +#include "TextRender.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Thunder { +namespace Compositor { + class Render : public Thunder::Compositor::IDisplay::ISurface::ICallback { + public: + static constexpr uint16_t DefaultWidth = 1920; + static constexpr uint16_t DefaultHeight = 1080; + + Render(const Render&) = delete; + Render& operator=(const Render&) = delete; + Render& operator=(Render&&) = delete; + + Render(); + + virtual ~Render() override + { + Stop(); + + for (auto* model : _models) { + Unregister(model); + } + + CleanupEGL(); + + if (_surface) { + _surface->Release(); + _surface = nullptr; + } + if (_display) { + _display->Release(); + _display = nullptr; + } + } + + EGLDisplay GetEGLDisplay() const { return _eglDisplay; } + EGLContext GetEGLContext() const { return _eglContext; } + EGLSurface GetEGLSurface() const { return _eglSurface; } + + bool Configure(const uint16_t width = DefaultWidth, const uint16_t height = DefaultHeight); + + void Start() + { + if (_models.empty()) { + TRACE(Trace::Warning, ("No models registered, cannot start rendering")); + return; + } + + bool expected = false; + + std::uniform_int_distribution modelDist(0, static_cast(_models.size() - 1)); + + if (_running.compare_exchange_strong(expected, true)) { + TRACE(Trace::Information, ("Starting Render")); + _selectedModel = modelDist(_rng); + _render = std::thread(&Render::Draw, this); + } + } + void Stop() + { + _running.store(false); + _renderSync.notify_all(); + + if (_render.joinable()) { + TRACE(Trace::Information, ("Stopping Render thread")); + _render.join(); + TRACE(Trace::Information, ("Render thread stopped")); + } + + _selectedModel = ~0; + } + bool IsRunning() const + { + return _running.load(); + } + void RequestExit() + { + TRACE(Trace::Information, ("Exit requested via output termination")); + std::lock_guard lock(_exitMutex); + _exitRequested = true; + _exitSignal.notify_one(); + } + bool ShouldExit() const + { + return _exitRequested.load(); + } + bool ToggleFPS() + { + _showFps = !_showFps; + return _showFps; + } + + bool ToggleRequestRender() + { + _skipRender = !_skipRender; + return _skipRender; + } + + bool ToggleModelRender() + { + _skipModel = !_skipModel; + return _skipModel; + } + + void TriggerRender() + { + _surface->RequestRender(); + + if (WaitForRendered(1000) == Core::ERROR_TIMEDOUT) { + TRACE(Trace::Warning, ("Timed out waiting for rendered callback")); + } + } + + // ICallback + void Rendered(Thunder::Compositor::IDisplay::ISurface*) override; + void Published(Thunder::Compositor::IDisplay::ISurface*) override; + + bool Register(IModel* model, const std::string& config); + void Unregister(IModel* model); + + private: + bool InitializeModel(IModel* model, const std::string& config); + bool InitializeEGL(); + void CleanupEGL(); + void Draw(); + uint32_t WaitForRendered(uint32_t timeoutMs); + + private: + Thunder::Compositor::IDisplay* _display; + Thunder::Compositor::IDisplay::ISurface* _surface; + + std::string _displayName; + uint16_t _canvasWidth; + uint16_t _canvasHeight; + + EGLDisplay _eglDisplay; + EGLContext _eglContext; + EGLSurface _eglSurface; + + std::thread _render; + + std::mutex _exitMutex; + std::condition_variable _exitSignal; + std::atomic _exitRequested; + + std::atomic _running; + std::mutex _rendering; + std::condition_variable _renderSync; + + bool _showFps; + bool _skipRender; + bool _skipModel; + + std::vector _models; + std::atomic _selectedModel; + + std::mt19937 _rng; + + TextRender _textRender; + uint64_t _lastFPSUpdate; + uint32_t _frameCount; + + float _currentFPS; + }; +} // namespace Compositor +} // namespace Thunder \ No newline at end of file diff --git a/Source/compositorclient/test/client-renderer/common/TerminalInput.h b/Source/compositorclient/test/client-renderer/common/TerminalInput.h new file mode 100644 index 00000000..c0879737 --- /dev/null +++ b/Source/compositorclient/test/client-renderer/common/TerminalInput.h @@ -0,0 +1,100 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2025 Metrological + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include + +namespace Thunder { +namespace Compositor { + class TerminalInput { + public: + typedef void (*InputHandler)(char); + + TerminalInput() + : _original() + , _originalFlags(0) + , _valid(SetupTerminal()) + { + } + + ~TerminalInput() + { + if (_valid) { + RestoreTerminal(); + } + } + + TerminalInput(const TerminalInput&) = delete; + TerminalInput& operator=(const TerminalInput&) = delete; + TerminalInput(TerminalInput&&) = delete; + TerminalInput& operator=(TerminalInput&&) = delete; + + bool IsValid() const { return _valid; } + + char Read() + { + char c = 0; + read(STDIN_FILENO, &c, 1); + return c; + } + + private: + bool SetupTerminal() + { + if (tcgetattr(STDIN_FILENO, &_original) != 0) { + return false; + } + + _originalFlags = fcntl(STDIN_FILENO, F_GETFL, 0); + if (_originalFlags == -1) { + return false; + } + + struct termios raw = _original; + + raw.c_lflag &= ~(ICANON | ECHO); // No line buffering, no echo + raw.c_cc[VMIN] = 0; // Non-blocking read + raw.c_cc[VTIME] = 0; // No timeout + + if (tcsetattr(STDIN_FILENO, TCSANOW, &raw) != 0) { + return false; + } + + if (fcntl(STDIN_FILENO, F_SETFL, _originalFlags | O_NONBLOCK) == -1) { + tcsetattr(STDIN_FILENO, TCSAFLUSH, &_original); + return false; + } + + return true; + } + + void RestoreTerminal() + { + tcsetattr(STDIN_FILENO, TCSAFLUSH, &_original); + fcntl(STDIN_FILENO, F_SETFL, _originalFlags); + } + + private: + struct termios _original; + int _originalFlags; + bool _valid; + }; +} // namespace Compositor +} // namespace Thunder \ No newline at end of file diff --git a/Source/compositorclient/test/client-renderer/common/TextRender.cpp b/Source/compositorclient/test/client-renderer/common/TextRender.cpp new file mode 100644 index 00000000..b69e167e --- /dev/null +++ b/Source/compositorclient/test/client-renderer/common/TextRender.cpp @@ -0,0 +1,344 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2025 Metrological + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "TextRender.h" +#include "TextureLoader.h" +#include +#include + +namespace Thunder { +namespace Compositor { + static const char Namespace[] = EXPAND_AND_QUOTE(NAMESPACE); + + TextRender::TextRender(const Font* font) + : _program(0) + , _fontTexture(0) + , _vbo(0) + , _canvasWidth(0) + , _canvasHeight(0) + , _scale(1.0f) + , _colorR(1.0f) + , _colorG(1.0f) + , _colorB(1.0f) + , _colorA(1.0f) + , _font(font) + { + } + + TextRender::~TextRender() + { + Cleanup(); + } + + bool TextRender::Initialize(const uint16_t width, const uint16_t height, const std::string& config) + { + Config cfg; + Core::OptionalType error; + cfg.FromString(config); + + if (error.IsSet()) { + TRACE(Trace::Error, ("Failed to parse TextRender config: %s", error.Value().Message().c_str())); + return false; + } + + _canvasWidth = width; + _canvasHeight = height; + + if (!CreateShaderProgram()) { + return false; + } + + std::string fontPath = cfg.FontAtlas.Value(); + if (fontPath.empty()) { + fontPath = "/usr/share/" + std::string(Namespace) + "/ClientCompositorRender/Arial.png"; + } + + if (!LoadFontAtlas(fontPath)) { + return false; + } + + CreateQuadBuffer(); + + _scale = cfg.Scale.Value(); + _colorR = cfg.Red.Value(); + _colorG = cfg.Green.Value(); + _colorB = cfg.Blue.Value(); + _colorA = cfg.Alpha.Value(); + + return true; + } + + bool TextRender::LoadFontAtlas(const std::string& path) + { + auto pixelData = Thunder::Compositor::Texture::LoadPNG(path); + if (pixelData.data.empty()) { + fprintf(stderr, "Failed to load font atlas: %s\n", path.c_str()); + return false; + } + + glGenTextures(1, &_fontTexture); + glBindTexture(GL_TEXTURE_2D, _fontTexture); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, pixelData.width, pixelData.height, 0, + GL_RGBA, GL_UNSIGNED_BYTE, pixelData.data.data()); + glBindTexture(GL_TEXTURE_2D, 0); + + return true; + } + + bool TextRender::CreateShaderProgram() + { + const char* vsrc = R"( + attribute vec2 aPos; + attribute vec2 aTex; + varying vec2 vTex; + uniform vec2 uResolution; + void main() { + vec2 clipSpace = (aPos / uResolution) * 2.0 - 1.0; + gl_Position = vec4(clipSpace * vec2(1, -1), 0.0, 1.0); + vTex = aTex; + } + )"; + + const char* fsrc = nullptr; + + if (HasExtension("GL_OES_standard_derivatives")) { + fsrc = R"( + #extension GL_OES_standard_derivatives : enable + + precision highp float; + uniform sampler2D uTexture; + uniform vec4 uColor; + varying vec2 vTex; + + void main() { + float sample = texture2D(uTexture, vTex).r; + float scale = 1.0 / fwidth(sample); + float signedDistance = (sample - 0.5) * scale; + float alpha = clamp(signedDistance + 0.5, 0.0, 1.0); + gl_FragColor = vec4(uColor.rgb, alpha * uColor.a); + } + )"; + } else { + fsrc = R"( + precision highp float; + uniform sampler2D uTexture; + uniform vec4 uColor; + varying vec2 vTex; + + void main() { + float sample = texture2D(uTexture, vTex).r; + float signedDistance = (sample - 0.5) * 5.0; + float alpha = clamp(signedDistance + 0.5, 0.0, 1.0); + gl_FragColor = vec4(uColor.rgb, alpha * uColor.a); + } + )"; + } + + GLuint vs = glCreateShader(GL_VERTEX_SHADER); + glShaderSource(vs, 1, &vsrc, nullptr); + glCompileShader(vs); + if (!CheckShaderCompile(vs, "TextRender vertex")) { + return false; + } + + GLuint fs = glCreateShader(GL_FRAGMENT_SHADER); + glShaderSource(fs, 1, &fsrc, nullptr); + glCompileShader(fs); + if (!CheckShaderCompile(fs, "TextRender fragment")) { + glDeleteShader(vs); + return false; + } + + _program = glCreateProgram(); + glAttachShader(_program, vs); + glAttachShader(_program, fs); + glLinkProgram(_program); + + GLint linked = 0; + glGetProgramiv(_program, GL_LINK_STATUS, &linked); + if (!linked) { + char log[512]; + glGetProgramInfoLog(_program, sizeof(log), nullptr, log); + fprintf(stderr, "TextRender shader link error: %s\n", log); + glDeleteShader(vs); + glDeleteShader(fs); + return false; + } + + glDeleteShader(vs); + glDeleteShader(fs); + return true; + } + + bool TextRender::CheckShaderCompile(GLuint shader, const char* label) + { + GLint compiled; + glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled); + if (!compiled) { + char log[512]; + glGetShaderInfoLog(shader, sizeof(log), nullptr, log); + fprintf(stderr, "%s shader compile error: %s\n", label, log); + return false; + } + return true; + } + + bool TextRender::HasExtension(const char* extension) const + { + const char* extensions = reinterpret_cast(glGetString(GL_EXTENSIONS)); + if (extensions == nullptr) { + return false; + } + return strstr(extensions, extension) != nullptr; + } + + void TextRender::CreateQuadBuffer() + { + const GLfloat vertices[] = { + 0.0f, 0.0f, 0.0f, 0.0f, + 1.0f, 0.0f, 1.0f, 0.0f, + 0.0f, 1.0f, 0.0f, 1.0f, + 1.0f, 1.0f, 1.0f, 1.0f + }; + glGenBuffers(1, &_vbo); + glBindBuffer(GL_ARRAY_BUFFER, _vbo); + glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); + glBindBuffer(GL_ARRAY_BUFFER, 0); + } + + const Character* TextRender::FindCharacter(int codePoint) const + { + if (_font == nullptr) { + return nullptr; + } + + for (int i = 0; i < _font->characterCount; ++i) { + if (_font->characters[i].codePoint == codePoint) { + return &_font->characters[i]; + } + } + return nullptr; + } + + void TextRender::Draw(const std::string& text, float x, float y) + { + if (_font == nullptr || text.empty()) { + return; + } + + glUseProgram(_program); + glEnable(GL_BLEND); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + + glActiveTexture(GL_TEXTURE0); + glBindTexture(GL_TEXTURE_2D, _fontTexture); + + GLint uResolution = glGetUniformLocation(_program, "uResolution"); + GLint uTexture = glGetUniformLocation(_program, "uTexture"); + GLint uColor = glGetUniformLocation(_program, "uColor"); + GLint aPos = glGetAttribLocation(_program, "aPos"); + GLint aTex = glGetAttribLocation(_program, "aTex"); + + glUniform2f(uResolution, static_cast(_canvasWidth), static_cast(_canvasHeight)); + glUniform1i(uTexture, 0); + glUniform4f(uColor, _colorR, _colorG, _colorB, _colorA); + + glBindBuffer(GL_ARRAY_BUFFER, _vbo); + glEnableVertexAttribArray(aPos); + glVertexAttribPointer(aPos, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(GLfloat), (void*)0); + glEnableVertexAttribArray(aTex); + glVertexAttribPointer(aTex, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(GLfloat), (void*)(2 * sizeof(GLfloat))); + + float cursorX = x; + float cursorY = _canvasHeight - y; + + const float atlasWidth = static_cast(_font->width); + const float atlasHeight = static_cast(_font->height); + + for (char c : text) { + const Character* ch = FindCharacter(static_cast(c)); + if (ch == nullptr) { + continue; + } + + float xpos = cursorX + ch->originX * _scale; + float ypos = cursorY - ch->originY * _scale; + float w = ch->width * _scale; + float h = ch->height * _scale; + + float u0 = ch->x / atlasWidth; + float v0 = ch->y / atlasHeight; + float u1 = (ch->x + ch->width) / atlasWidth; + float v1 = (ch->y + ch->height) / atlasHeight; + + GLfloat vertices[] = { + xpos, ypos, u0, v0, + xpos + w, ypos, u1, v0, + xpos, ypos + h, u0, v1, + xpos + w, ypos + h, u1, v1 + }; + + glBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(vertices), vertices); + glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); + + cursorX += (ch->width + 1) * _scale; + } + + glDisableVertexAttribArray(aPos); + glDisableVertexAttribArray(aTex); + glBindBuffer(GL_ARRAY_BUFFER, 0); + glBindTexture(GL_TEXTURE_2D, 0); + glDisable(GL_BLEND); + } + + void TextRender::SetColor(float r, float g, float b, float a) + { + _colorR = r; + _colorG = g; + _colorB = b; + _colorA = a; + } + + void TextRender::SetScale(float scale) + { + _scale = scale; + } + + void TextRender::Cleanup() + { + if (_fontTexture) { + glDeleteTextures(1, &_fontTexture); + _fontTexture = 0; + } + if (_vbo) { + glDeleteBuffers(1, &_vbo); + _vbo = 0; + } + if (_program) { + glDeleteProgram(_program); + _program = 0; + } + } + +} // namespace Compositor +} // namespace Thunder \ No newline at end of file diff --git a/Source/compositorclient/test/client-renderer/common/TextRender.h b/Source/compositorclient/test/client-renderer/common/TextRender.h new file mode 100644 index 00000000..1fc67fb1 --- /dev/null +++ b/Source/compositorclient/test/client-renderer/common/TextRender.h @@ -0,0 +1,109 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2025 Metrological + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include "Module.h" +#include "IModel.h" +#include "Fonts/Font.h" + +#include +#include +#include + +using namespace Thunder; +namespace Thunder { +namespace Compositor { + class TextRender { + public: + class Config : public Core::JSON::Container { + public: + Config(const Config&) = delete; + Config& operator=(const Config&) = delete; + Config() + : Core::JSON::Container() + , FontAtlas() + , Scale(1.0f) + , Red(1.0f) + , Green(1.0f) + , Blue(1.0f) + , Alpha(1.0f) + { + Add(_T("fontatlas"), &FontAtlas); + Add(_T("scale"), &Scale); + Add(_T("red"), &Red); + Add(_T("green"), &Green); + Add(_T("blue"), &Blue); + Add(_T("alpha"), &Alpha); + } + ~Config() = default; + + public: + Core::JSON::String FontAtlas; + Core::JSON::DecUInt32 X; + Core::JSON::DecUInt32 Y; + Core::JSON::Float Scale; + Core::JSON::Float Red; + Core::JSON::Float Green; + Core::JSON::Float Blue; + Core::JSON::Float Alpha; + }; + + TextRender(const Font* font); + + TextRender() = delete; + TextRender(const TextRender&) = delete; + TextRender(TextRender&&) = delete; + TextRender& operator=(const TextRender&) = delete; + TextRender& operator=(TextRender&&) = delete; + ~TextRender(); + + bool Initialize(const uint16_t width, const uint16_t height, const std::string& config); + void Draw(const std::string& text, float x, float y); + void SetColor(float r, float g, float b, float a); + void SetScale(float scale); + + private: + bool HasExtension(const char* extension) const; + bool LoadFontAtlas(const std::string& path); + bool CreateShaderProgram(); + bool CheckShaderCompile(GLuint shader, const char* label); + void CreateQuadBuffer(); + const Character* FindCharacter(int codePoint) const; + void Cleanup(); + + private: + GLuint _program; + GLuint _fontTexture; + GLuint _vbo; + + int _canvasWidth; + int _canvasHeight; + + float _scale; + float _colorR; + float _colorG; + float _colorB; + float _colorA; + + const Font* _font; + }; + +} // namespace Compositor +} // namespace Thunder \ No newline at end of file diff --git a/Source/compositorclient/test/client-renderer/common/TextureBounce.cpp b/Source/compositorclient/test/client-renderer/common/TextureBounce.cpp new file mode 100644 index 00000000..6bbf8193 --- /dev/null +++ b/Source/compositorclient/test/client-renderer/common/TextureBounce.cpp @@ -0,0 +1,366 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2025 Metrological + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "TextureBounce.h" + +#include +#include +#include + +namespace Thunder { +namespace Compositor { + TextureBounce::TextureBounce() + : _program(0) + , _textureId(0) + , _vbo(0) + , _canvasHeight(0) + , _canvasWidth(0) + , _textureWidth(0) + , _textureHeight(0) + , _sprites() + , _scale(1.0f) + , _lastFrameTime(Core::Time::Now().Ticks()) + { + } + + TextureBounce::~TextureBounce() + { + Cleanup(); + } + + bool TextureBounce::Initialize(const uint16_t width, const uint16_t height, const std::string& config) + { + Config cfg; + Core::OptionalType error; + + cfg.FromString(config); + + if (error.IsSet() == true) { + TRACE(Trace::Error, ("Failed to parse config: %s", error.Value().Message().c_str())); + return false; + } + + _canvasWidth = width; + _canvasHeight = height; + + if (!CreateShaderProgram()) { + return false; + } + if (!LoadTexture(cfg.Image.Value())) { + return false; + } + CreateVertexBuffer(); + + // Initialize sprites + std::mt19937 rng(static_cast(std::chrono::steady_clock::now().time_since_epoch().count())); + std::uniform_real_distribution xDist(0.0f, DefaultWidth - _textureWidth * _scale); + std::uniform_real_distribution yDist(0.0f, DefaultHeight - _textureHeight * _scale); + std::uniform_real_distribution vDist(-300.0f, 300.0f); + std::uniform_real_distribution scaleDist(0.3f, 1.0f); + + _sprites.clear(); + for (int i = 0; i < MaxSprites; ++i) { + Sprite s; + s.x = xDist(rng); + s.y = yDist(rng); + s.vx = vDist(rng); + s.vy = vDist(rng); + float spriteScale = _scale * scaleDist(rng); + s.width = _textureWidth * spriteScale; + s.height = _textureHeight * spriteScale; + s.mass = s.width * s.height; // massa proportioneel aan oppervlakte + _sprites.push_back(s); + } + + return true; + } + + bool TextureBounce::Draw() + { + uint64_t now = Core::Time::Now().Ticks(); + float dt = (now - _lastFrameTime) / 1000000.0f; + _lastFrameTime = now; + + UpdateSprites(dt); + return RenderFrame(); + } + + bool TextureBounce::LoadTexture(const std::string& path) + { + auto pixelData = Thunder::Compositor::Texture::LoadPNG(path); + if (pixelData.data.empty()) { + fprintf(stderr, "Failed to load texture: %s\n", path.c_str()); + return false; + } + + glGenTextures(1, &_textureId); + glBindTexture(GL_TEXTURE_2D, _textureId); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, pixelData.width, pixelData.height, 0, + GL_RGBA, GL_UNSIGNED_BYTE, pixelData.data.data()); + glBindTexture(GL_TEXTURE_2D, 0); + + _textureWidth = pixelData.width; + _textureHeight = pixelData.height; + return true; + } + + bool TextureBounce::CreateShaderProgram() + { + const char* vsrc = R"( + attribute vec2 aPos; + attribute vec2 aTex; + varying vec2 vTex; + uniform vec2 uResolution; + uniform vec2 uPos; + uniform vec2 uSize; + void main() { + vec2 pos = aPos * uSize + uPos; + vec2 zeroToOne = pos / uResolution; + vec2 clipSpace = zeroToOne * 2.0 - 1.0; + gl_Position = vec4(clipSpace * vec2(1, -1), 0.0, 1.0); + vTex = aTex; + } + )"; + + const char* fsrc = R"( + precision mediump float; + varying vec2 vTex; + uniform sampler2D uTexture; + void main() { + gl_FragColor = texture2D(uTexture, vTex); + } + )"; + + GLuint vs = glCreateShader(GL_VERTEX_SHADER); + glShaderSource(vs, 1, &vsrc, nullptr); + glCompileShader(vs); + if (!CheckShaderCompile(vs, "vertex")) + return false; + + GLuint fs = glCreateShader(GL_FRAGMENT_SHADER); + glShaderSource(fs, 1, &fsrc, nullptr); + glCompileShader(fs); + if (!CheckShaderCompile(fs, "fragment")) + return false; + + _program = glCreateProgram(); + glAttachShader(_program, vs); + glAttachShader(_program, fs); + glLinkProgram(_program); + + GLint linked = 0; + glGetProgramiv(_program, GL_LINK_STATUS, &linked); + if (!linked) { + char log[512]; + glGetProgramInfoLog(_program, sizeof(log), nullptr, log); + fprintf(stderr, "Shader link error: %s\n", log); + return false; + } + + glDeleteShader(vs); + glDeleteShader(fs); + return true; + } + + bool TextureBounce::CheckShaderCompile(GLuint shader, const char* label) + { + GLint compiled; + glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled); + if (!compiled) { + char log[512]; + glGetShaderInfoLog(shader, sizeof(log), nullptr, log); + fprintf(stderr, "%s shader compile error: %s\n", label, log); + return false; + } + return true; + } + + void TextureBounce::CreateVertexBuffer() + { + const GLfloat vertices[] = { + 0.0f, 0.0f, 0.0f, 0.0f, + 1.0f, 0.0f, 1.0f, 0.0f, + 0.0f, 1.0f, 0.0f, 1.0f, + 1.0f, 1.0f, 1.0f, 1.0f + }; + glGenBuffers(1, &_vbo); + glBindBuffer(GL_ARRAY_BUFFER, _vbo); + glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); + glBindBuffer(GL_ARRAY_BUFFER, 0); + } + + void TextureBounce::UpdateSprites(float dt) + { + static constexpr float MaxSpeed = 600.0f; + static constexpr float Friction = 0.9998f; + static constexpr float BounceFactor = 1.3f; + static constexpr float BaseMinSpeed = 10.0f; // voor een "standaard" sprite van massa 1 + + for (auto& s : _sprites) { + s.x += s.vx * dt; + s.y += s.vy * dt; + + // Randbotsing + if (s.x <= 0) { + s.x = 0; + s.vx = -s.vx * BounceFactor; + } else if (s.x + s.width >= _canvasWidth) { + s.x = _canvasWidth - s.width; + s.vx = -s.vx * BounceFactor; + } + + if (s.y <= 0) { + s.y = 0; + s.vy = -s.vy * BounceFactor; + } else if (s.y + s.height >= _canvasHeight) { + s.y = _canvasHeight - s.height; + s.vy = -s.vy * BounceFactor; + } + + // Lichte damping + s.vx *= pow(Friction, dt); + s.vy *= pow(Friction, dt); + + // Massa-afhankelijke minimale snelheid + float minSpeed = BaseMinSpeed / std::sqrt(s.mass); // grotere massa → lagere minSpeed + + if (std::abs(s.vx) < minSpeed) + s.vx = (s.vx >= 0) ? minSpeed : -minSpeed; + if (std::abs(s.vy) < minSpeed) + s.vy = (s.vy >= 0) ? minSpeed : -minSpeed; + + // Max snelheid + s.vx = Clamp(s.vx, -MaxSpeed, MaxSpeed); + s.vy = Clamp(s.vy, -MaxSpeed, MaxSpeed); + } + + HandleCollisions(); + } + + void TextureBounce::HandleCollisions() + { + for (size_t i = 0; i < _sprites.size(); ++i) { + for (size_t j = i + 1; j < _sprites.size(); ++j) { + auto& a = _sprites[i]; + auto& b = _sprites[j]; + + // Check overlap + if (a.x < b.x + b.width && a.x + a.width > b.x && a.y < b.y + b.height && a.y + a.height > b.y) { + // Normaal vector + float nx = (b.x + b.width / 2.0f) - (a.x + a.width / 2.0f); + float ny = (b.y + b.height / 2.0f) - (a.y + a.height / 2.0f); + float dist = std::sqrt(nx * nx + ny * ny); + if (dist == 0) { + nx = 0; + ny = 1; + dist = 1; + } + nx /= dist; + ny /= dist; + + // Tangent + float tx = -ny, ty = nx; + + // Projecteer snelheden + float va_n = a.vx * nx + a.vy * ny; + float vb_n = b.vx * nx + b.vy * ny; + float va_t = a.vx * tx + a.vy * ty; + float vb_t = b.vx * tx + b.vy * ty; + + // Elastische botsing (massa-gecorrigeerd) + float va_n_new = (va_n * (a.mass - b.mass) + 2.0f * b.mass * vb_n) / (a.mass + b.mass); + float vb_n_new = (vb_n * (b.mass - a.mass) + 2.0f * a.mass * va_n) / (a.mass + b.mass); + + // Terug naar xy + a.vx = va_n_new * nx + va_t * tx; + a.vy = va_n_new * ny + va_t * ty; + b.vx = vb_n_new * nx + vb_t * tx; + b.vy = vb_n_new * ny + vb_t * ty; + + // Position correction met massa-verdeling + float overlapX = (a.width + b.width) / 2.0f - std::abs((a.x + a.width / 2.0f) - (b.x + b.width / 2.0f)); + float overlapY = (a.height + b.height) / 2.0f - std::abs((a.y + a.height / 2.0f) - (b.y + b.height / 2.0f)); + float totalMass = a.mass + b.mass; + + a.x -= nx * overlapX * (b.mass / totalMass); + a.y -= ny * overlapY * (b.mass / totalMass); + b.x += nx * overlapX * (a.mass / totalMass); + b.y += ny * overlapY * (a.mass / totalMass); + } + } + } + } + + bool TextureBounce::RenderFrame() + { + glViewport(0, 0, _canvasWidth, _canvasHeight); + glClearColor(0.25f, 0.25f, 0.25f, 1.0f); + glClear(GL_COLOR_BUFFER_BIT); + + glEnable(GL_BLEND); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + + glUseProgram(_program); + glBindBuffer(GL_ARRAY_BUFFER, _vbo); + + GLint aPos = glGetAttribLocation(_program, "aPos"); + GLint aTex = glGetAttribLocation(_program, "aTex"); + GLint uRes = glGetUniformLocation(_program, "uResolution"); + GLint uPos = glGetUniformLocation(_program, "uPos"); + GLint uSize = glGetUniformLocation(_program, "uSize"); + + glEnableVertexAttribArray(aPos); + glVertexAttribPointer(aPos, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(GLfloat), (void*)0); + glEnableVertexAttribArray(aTex); + glVertexAttribPointer(aTex, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(GLfloat), (void*)(2 * sizeof(GLfloat))); + + glUniform2f(uRes, (float)_canvasWidth, (float)_canvasHeight); + glActiveTexture(GL_TEXTURE0); + glBindTexture(GL_TEXTURE_2D, _textureId); + glUniform1i(glGetUniformLocation(_program, "uTexture"), 0); + + for (auto& s : _sprites) { + glUniform2f(uPos, s.x, s.y); + glUniform2f(uSize, s.width, s.height); + glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); + } + + glDisableVertexAttribArray(aPos); + glDisableVertexAttribArray(aTex); + glBindBuffer(GL_ARRAY_BUFFER, 0); + + return true; + } + + void TextureBounce::Cleanup() + { + + if (_textureId) + glDeleteTextures(1, &_textureId); + if (_vbo) + glDeleteBuffers(1, &_vbo); + if (_program) + glDeleteProgram(_program); + } +} // namespace Compositor +} // namespace Thunder \ No newline at end of file diff --git a/Source/compositorclient/test/client-renderer/common/TextureBounce.h b/Source/compositorclient/test/client-renderer/common/TextureBounce.h new file mode 100644 index 00000000..e8643b91 --- /dev/null +++ b/Source/compositorclient/test/client-renderer/common/TextureBounce.h @@ -0,0 +1,118 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2025 Metrological + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include "Module.h" +#include "IModel.h" + +#include + +#include +#include +#include +#include +#include +#include +#include "TextureLoader.h" + +namespace Thunder { +namespace Compositor { + class TextureBounce : public IModel { + public: + class Config : public Core::JSON::Container { + public: + Config(const Config&) = delete; + Config& operator=(const Config&) = delete; + Config() + : Core::JSON::Container() + , Image() + , ImageCount(1) + { + Add(_T("image"), &Image); + Add(_T("imagecount"), &ImageCount); + } + ~Config() = default; + + public: + Core::JSON::String Image; + Core::JSON::DecUInt32 ImageCount; + }; // class Config + + struct Sprite { + float x, y; + float vx, vy; + float width, height; + float mass; + }; + + static constexpr uint16_t DefaultWidth = 1920; + static constexpr uint16_t DefaultHeight = 1080; + static constexpr uint8_t MaxSprites = 40; + + TextureBounce(); // = delete; + TextureBounce(const TextureBounce&) = delete; + TextureBounce(TextureBounce&&) = delete; + TextureBounce& operator=(const TextureBounce&) = delete; + TextureBounce& operator=(TextureBounce&&) = delete; + + // TextureBounce(const uint16_t width = DefaultWidth, const uint16_t height = DefaultHeight); + ~TextureBounce(); + + bool Initialize(const uint16_t width, const uint16_t height, const std::string& config) override; + bool Draw() override; + + private: + bool LoadTexture(const std::string& path); + bool CreateShaderProgram(); + bool CheckShaderCompile(GLuint shader, const char* label); + void CreateVertexBuffer(); + bool RenderFrame(); + void UpdateSprites(float dt); + void HandleCollisions(); + void Cleanup(); + + template + inline const T& Clamp(const T& v, const T& lo, const T& hi) + { + return (v < lo) ? lo : (v > hi) ? hi + : v; + } + + private: + // GL + GLuint _program; + GLuint _textureId; + GLuint _vbo; + + int _canvasHeight; + int _canvasWidth; + + uint32_t _textureWidth; + uint32_t _textureHeight; + + // Sprites + std::vector _sprites; + float _scale; + + // Timing + uint64_t _lastFrameTime; + }; +} // namespace Compositor +} // namespace Thunder \ No newline at end of file diff --git a/Source/compositorclient/test/client-renderer/common/TextureLoader.cpp b/Source/compositorclient/test/client-renderer/common/TextureLoader.cpp new file mode 100644 index 00000000..7a852725 --- /dev/null +++ b/Source/compositorclient/test/client-renderer/common/TextureLoader.cpp @@ -0,0 +1,115 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2025 Metrological + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "TextureLoader.h" +#include +#include +#include + +namespace Thunder { +namespace Compositor { +namespace Texture { + +static void PngErrorFn(png_structp, png_const_charp msg) { + fprintf(stderr, "[TextureLoader] PNG error: %s\n", msg); +} +static void PngWarnFn(png_structp, png_const_charp msg) { + fprintf(stderr, "[TextureLoader] PNG warning: %s\n", msg); +} + +PixelData LoadPNG(const std::string& filename) { + PixelData result = {0, 0, 4, {}}; + + // PNG loading implementation follows the transformation pipeline described in + // the official libpng manual: http://www.libpng.org/pub/png/libpng-manual.txt + // Section III.2 "Reading PNG files" - we normalize all input formats to RGBA8 + // for consistent texture handling across the compositor system. + + FILE* fp = fopen(filename.c_str(), "rb"); + if (!fp) return result; + + png_structp png = png_create_read_struct(PNG_LIBPNG_VER_STRING, nullptr, PngErrorFn, PngWarnFn); + if (!png) { fclose(fp); return result; } + + png_infop info = png_create_info_struct(png); + if (!info) { png_destroy_read_struct(&png, nullptr, nullptr); fclose(fp); return result; } + + if (setjmp(png_jmpbuf(png))) { + png_destroy_read_struct(&png, &info, nullptr); + fclose(fp); + return result; + } + + png_init_io(png, fp); + png_read_info(png, info); + + result.width = png_get_image_width(png, info); + result.height = png_get_image_height(png, info); + int color_type = png_get_color_type(png, info); + int bit_depth = png_get_bit_depth(png, info); + + // Apply libpng transformations to normalize any input PNG format to RGBA8 output. + // This transformation chain handles: 16-bit depths, palette images, grayscale formats, + // transparency chunks, and ensures consistent 4-byte-per-pixel RGBA layout. + // Each transformation is applied conditionally based on the input format detected above. + // See libpng-manual.txt Section III.2 for the standard transformation sequence. + + // Reduce 16-bit samples to 8-bit for embedded device memory efficiency + if (bit_depth == 16) png_set_strip_16(png); + + // Expand palette-based images to full RGB for uniform processing + if (color_type == PNG_COLOR_TYPE_PALETTE) png_set_palette_to_rgb(png); + + // Normalize low-bit-depth grayscale (1,2,4-bit) to 8-bit + if (color_type == PNG_COLOR_TYPE_GRAY && bit_depth < 8) png_set_expand_gray_1_2_4_to_8(png); + + // Convert transparency chunks (tRNS) to full alpha channel + if (png_get_valid(png, info, PNG_INFO_tRNS)) png_set_tRNS_to_alpha(png); + + // Add opaque alpha channel (0xFF) to RGB/Gray/Palette images without transparency + if (color_type == PNG_COLOR_TYPE_RGB || color_type == PNG_COLOR_TYPE_GRAY || color_type == PNG_COLOR_TYPE_PALETTE) + png_set_filler(png, 0xFF, PNG_FILLER_AFTER); + + // Convert grayscale to RGB (grayscale and grayscale+alpha both handled) + if (color_type == PNG_COLOR_TYPE_GRAY || color_type == PNG_COLOR_TYPE_GRAY_ALPHA) + png_set_gray_to_rgb(png); + + png_read_update_info(png, info); + + result.data.resize(result.width * result.height * 4); + result.bytes_per_pixel = 4; + + // Row pointers point direct naar buffer (rechtop!) + png_bytep* row_pointers = new png_bytep[result.height]; + for (unsigned int y = 0; y < result.height; ++y) { + row_pointers[y] = result.data.data() + y * result.width * 4; + } + + png_read_image(png, row_pointers); + delete[] row_pointers; + + png_destroy_read_struct(&png, &info, nullptr); + fclose(fp); + + return result; +} + +} // namespace Texture +} // namespace Compositor +} // namespace Thunder diff --git a/Source/compositorclient/test/client-renderer/common/TextureLoader.h b/Source/compositorclient/test/client-renderer/common/TextureLoader.h new file mode 100644 index 00000000..ad2b77ad --- /dev/null +++ b/Source/compositorclient/test/client-renderer/common/TextureLoader.h @@ -0,0 +1,41 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2025 Metrological + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include +#include + +namespace Thunder { +namespace Compositor { +namespace Texture { + +struct PixelData { + uint32_t width; + uint32_t height; + uint32_t bytes_per_pixel; + std::vector data; +}; + +PixelData LoadPNG(const std::string& filename); + +} // namespace Texture +} // namespace Compositor +} // namespace Thunder \ No newline at end of file diff --git a/Source/compositorclient/test/client-renderer/common/ml-tv-color-small.png b/Source/compositorclient/test/client-renderer/common/ml-tv-color-small.png new file mode 100644 index 00000000..da3f5a48 Binary files /dev/null and b/Source/compositorclient/test/client-renderer/common/ml-tv-color-small.png differ diff --git a/Source/compositorclient/test/client-renderer/plugin/CMakeLists.txt b/Source/compositorclient/test/client-renderer/plugin/CMakeLists.txt new file mode 100644 index 00000000..e5d42aaf --- /dev/null +++ b/Source/compositorclient/test/client-renderer/plugin/CMakeLists.txt @@ -0,0 +1,97 @@ +# If not stated otherwise in this file or this component's license file the +# following copyright and licenses apply: +# +# Copyright 2025 Metrological +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License + +find_package(Thunder REQUIRED) +find_package(${NAMESPACE}Core CONFIG REQUIRED) +find_package(${NAMESPACE}Messaging CONFIG REQUIRED) +find_package(${NAMESPACE}Plugins CONFIG REQUIRED) +find_package(CompileSettingsDebug CONFIG REQUIRED) + +set(PLUGIN_COMPOSITORCLIENT_PLUGIN_STARTMODE "Activated" CACHE STRING "Automatically start ClientCompositorRender plugin") +set(PLUGIN_COMPOSITORCLIENT_PLUGIN_RESUMED "true" CACHE STRING "Set ClientCompositorRender plugin resume state") +set(PLUGIN_COMPOSITORCLIENT_PLUGIN_MODE "Local" CACHE STRING "Controls if the plugin should run in its own process, in process or remote.") +set(PLUGIN_COMPOSITORCLIENT_PLUGIN_INSTANCES 1 CACHE STRING "Controls how many instances will be installed") + +add_library(ClientCompositorRender + Module.cpp + ClientCompositorRender.cpp + ClientCompositorRenderImplementation.cpp +) + +set_target_properties(ClientCompositorRender PROPERTIES + CXX_STANDARD 11 + CXX_STANDARD_REQUIRED YES +) + +target_link_libraries(ClientCompositorRender + PRIVATE + ${NAMESPACE}Core::${NAMESPACE}Core + ${NAMESPACE}Messaging::${NAMESPACE}Messaging + ${NAMESPACE}Plugins::${NAMESPACE}Plugins + CompileSettingsDebug::CompileSettingsDebug + ClientCompositorRenderCommon +) + + +if(INSTALL_CLIENT_COMPOSITOR_RENDER_TEST_PLUGIN) +install(TARGETS ClientCompositorRender + DESTINATION ${CMAKE_INSTALL_LIBDIR}/${STORAGE_DIRECTORY}/plugins COMPONENT ${NAMESPACE}_Runtime) + + # Cap instances at 16 and warn if exceeded + if(PLUGIN_COMPOSITORCLIENT_PLUGIN_INSTANCES GREATER 16) + message(WARNING "PLUGIN_COMPOSITORCLIENT_PLUGIN_INSTANCES is ${PLUGIN_COMPOSITORCLIENT_PLUGIN_INSTANCES}, capping at 16") + set(PLUGIN_COMPOSITORCLIENT_PLUGIN_INSTANCES 16) + endif() + + # Determine grid layout based on instance count + if(PLUGIN_COMPOSITORCLIENT_PLUGIN_INSTANCES EQUAL 1) + set(GRID_COLS 1) + set(GRID_ROWS 1) + set(CELL_WIDTH 100) + set(CELL_HEIGHT 100) + elseif(PLUGIN_COMPOSITORCLIENT_PLUGIN_INSTANCES LESS_EQUAL 4) + set(GRID_COLS 2) + set(GRID_ROWS 2) + set(CELL_WIDTH 50) + set(CELL_HEIGHT 50) + else() + set(GRID_COLS 4) + set(GRID_ROWS 4) + set(CELL_WIDTH 25) + set(CELL_HEIGHT 25) + endif() + + foreach(Index RANGE 1 ${PLUGIN_COMPOSITORCLIENT_PLUGIN_INSTANCES}) + # Calculate grid position (0-based index for math) + math(EXPR ZeroIndex "${Index} - 1") + math(EXPR Col "${ZeroIndex} % ${GRID_COLS}") + math(EXPR Row "${ZeroIndex} / ${GRID_COLS}") + + # Calculate percentage positions + math(EXPR PLUGIN_COMPOSITORCLIENT_PLUGIN_INSTANCE_X "${Col} * ${CELL_WIDTH}") + math(EXPR PLUGIN_COMPOSITORCLIENT_PLUGIN_INSTANCE_Y "${Row} * ${CELL_HEIGHT}") + set(PLUGIN_COMPOSITORCLIENT_PLUGIN_INSTANCE_WIDTH ${CELL_WIDTH}) + set(PLUGIN_COMPOSITORCLIENT_PLUGIN_INSTANCE_HEIGHT ${CELL_HEIGHT}) + + write_config( + PLUGINS ClientCompositorRender + CLASSNAME ClientCompositorRender + LOCATOR libClientCompositorRender${CMAKE_SHARED_LIBRARY_SUFFIX} + INSTALL_NAME ClientCompositorRender${Index}.json + ) + endforeach() +endif() \ No newline at end of file diff --git a/Source/compositorclient/test/client-renderer/plugin/ClientCompositorRender.conf.in b/Source/compositorclient/test/client-renderer/plugin/ClientCompositorRender.conf.in new file mode 100644 index 00000000..1652d6f3 --- /dev/null +++ b/Source/compositorclient/test/client-renderer/plugin/ClientCompositorRender.conf.in @@ -0,0 +1,20 @@ +startmode = "@PLUGIN_COMPOSITORCLIENT_PLUGIN_STARTMODE@" +resumed = "@PLUGIN_COMPOSITORCLIENT_PLUGIN_RESUMED@" + +configuration = JSON() + +root = JSON() +root.add("mode", "@PLUGIN_COMPOSITORCLIENT_PLUGIN_MODE@") +if "@PLUGIN_COMPOSITORCLIENT_PLUGIN_USER@": + root.add("user", "@PLUGIN_COMPOSITORCLIENT_PLUGIN_USER@") +if "@PLUGIN_COMPOSITORCLIENT_PLUGIN_GROUP@": + root.add("group", "@PLUGIN_COMPOSITORCLIENT_PLUGIN_GROUP@") +configuration.add("root", root) + +if @PLUGIN_COMPOSITORCLIENT_PLUGIN_INSTANCES@ > 1: + _geometry = JSON() + _geometry.add("x", @PLUGIN_COMPOSITORCLIENT_PLUGIN_INSTANCE_X@) + _geometry.add("y", @PLUGIN_COMPOSITORCLIENT_PLUGIN_INSTANCE_Y@) + _geometry.add("width", @PLUGIN_COMPOSITORCLIENT_PLUGIN_INSTANCE_WIDTH@) + _geometry.add("height", @PLUGIN_COMPOSITORCLIENT_PLUGIN_INSTANCE_HEIGHT@) + configuration.add("relative-geometry", _geometry) \ No newline at end of file diff --git a/Source/compositorclient/test/client-renderer/plugin/ClientCompositorRender.cpp b/Source/compositorclient/test/client-renderer/plugin/ClientCompositorRender.cpp new file mode 100644 index 00000000..c14a1cc9 --- /dev/null +++ b/Source/compositorclient/test/client-renderer/plugin/ClientCompositorRender.cpp @@ -0,0 +1,182 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2025 Metrological + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "ClientCompositorRender.h" + +namespace Thunder { +namespace ClientCompositorRender { + extern Exchange::IMemory* MemoryObserver(const RPC::IRemoteConnection* connection); +} + +namespace Plugin { + namespace { + static Metadata metadata( + // Version + 1, 0, 0, + // Preconditions + { subsystem::GRAPHICS }, + // Terminations + { subsystem::NOT_GRAPHICS }, + // Controls + {}); + } + + /* virtual */ const string ClientCompositorRender::Initialize(PluginHost::IShell* service) + { + string message; + ASSERT(_memory == nullptr); + ASSERT(_statecontrol == nullptr); + ASSERT(_service == nullptr); + ASSERT(service != nullptr); + ASSERT(_connectionId == 0); + + // Setup skip URL for right offset. + _service = service; + _service->AddRef(); + _skipURL = static_cast(_service->WebPrefix().length()); + + // Register the Process::Notification stuff. The Remote process might die before we get a + // change to "register" the sink for these events !!! So do it ahead of instantiation. + _service->Register(&_notification); + + _statecontrol = _service->Root(_connectionId, 2000, _T("ClientCompositorRenderImplementation")); + + if (_statecontrol != nullptr) { + _statecontrol->Register(&_notification); + + uint32_t result = _statecontrol->Configure(_service); + + if (result != Core::ERROR_NONE) { + message = _T("ClientCompositorRender could not be configured."); + } else { + RPC::IRemoteConnection* connection = _service->RemoteConnection(_connectionId); + if (connection != nullptr) { + _memory = Thunder::ClientCompositorRender::MemoryObserver(connection); + ASSERT(_memory != nullptr); + connection->Release(); + } + } + + Config config; + config.FromString(service->ConfigLine()); + + if (config.RelativeGeometry.IsSet()) { + Geometry params; + + params.X = (config.CanvasWidth.Value() * config.RelativeGeometry.X.Value()) / 100; + params.Y = (config.CanvasHeight.Value() * config.RelativeGeometry.Y.Value()) / 100; + params.Width = (config.CanvasWidth.Value() * config.RelativeGeometry.Width.Value()) / 100; + params.Height = (config.CanvasHeight.Value() * config.RelativeGeometry.Height.Value()) / 100; + + string paramsString; + params.ToString(paramsString); + + PluginHost::IDispatcher* dispatch = service->QueryInterfaceByCallsign("Controller"); + + string output; + Core::hresult result = dispatch->Invoke(0, 42, "", "Compositor.1.geometry@" + service->Callsign(), paramsString, output); + + std::cout << "Requested resize: Result=" << result << " response " << output << std::endl; + } + + } else { + message = _T("ClientCompositorRender could not be instantiated."); + } + + return message; + } + + /* virtual */ void ClientCompositorRender::Deinitialize(PluginHost::IShell* service VARIABLE_IS_NOT_USED) + { + if (_service != nullptr) { + ASSERT(_service == _service); + + _service->Unregister(&_notification); + + if (_statecontrol != nullptr) { + _statecontrol->Unregister(&_notification); + _statecontrol->Release(); + _statecontrol = nullptr; + } + + if (_memory != nullptr) { + _memory->Release(); + _memory = nullptr; + } + + RPC::IRemoteConnection* connection(_service->RemoteConnection(_connectionId)); + + // If this was running in a (container) process... + if (connection != nullptr) { + // Lets trigger a cleanup sequence for + // out-of-process code. Which will guard + // that unwilling processes, get shot if + // not stopped friendly :~) + connection->Terminate(); + connection->Release(); + } + + _service->Release(); + _service = nullptr; + _connectionId = 0; + } + } + + /* virtual */ string ClientCompositorRender::Information() const + { + // No additional info to report. + return (string()); + } + + void ClientCompositorRender::Deactivated(RPC::IRemoteConnection* connection) + { + if (_connectionId == connection->Id()) { + ASSERT(_service != nullptr); + Core::IWorkerPool::Instance().Submit(PluginHost::IShell::Job::Create(_service, PluginHost::IShell::DEACTIVATED, PluginHost::IShell::FAILURE)); + } + } + + void ClientCompositorRender::StateChange(const PluginHost::IStateControl::state state) + { + switch (state) { + case PluginHost::IStateControl::RESUMED: + TRACE(Trace::Information, + (string(_T("StateChange: { \"suspend\":false }")))); + _service->Notify("{ \"suspended\":false }"); + break; + case PluginHost::IStateControl::SUSPENDED: + TRACE(Trace::Information, + (string(_T("StateChange: { \"suspend\":true }")))); + _service->Notify("{ \"suspended\":true }"); + break; + case PluginHost::IStateControl::EXITED: + Core::IWorkerPool::Instance().Submit( + PluginHost::IShell::Job::Create(_service, + PluginHost::IShell::DEACTIVATED, + PluginHost::IShell::REQUESTED)); + break; + case PluginHost::IStateControl::UNINITIALIZED: + break; + default: + ASSERT(false); + break; + } + } +} +} diff --git a/Source/compositorclient/test/client-renderer/plugin/ClientCompositorRender.h b/Source/compositorclient/test/client-renderer/plugin/ClientCompositorRender.h new file mode 100644 index 00000000..0b9ebf5f --- /dev/null +++ b/Source/compositorclient/test/client-renderer/plugin/ClientCompositorRender.h @@ -0,0 +1,170 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2025 Metrological + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include "Module.h" + +#include + +namespace Thunder { +namespace Plugin { + class ClientCompositorRender : public PluginHost::IPlugin { + private: + class Notification : public RPC::IRemoteConnection::INotification, public PluginHost::IStateControl::INotification { + public: + Notification() = delete; + Notification(const Notification&) = delete; + explicit Notification(ClientCompositorRender* parent) + : _parent(*parent) + { + ASSERT(parent != nullptr); + } + ~Notification() override + { + TRACE(Trace::Information, (_T("ClientCompositorRender::Notification destructed. Line: %d"), __LINE__)); + } + + public: + void Activated(RPC::IRemoteConnection* /* connection */) override + { + } + void Deactivated(RPC::IRemoteConnection* connectionId) override + { + _parent.Deactivated(connectionId); + } + void Terminated(RPC::IRemoteConnection* /* connection */) override + { + } + + void StateChange(const PluginHost::IStateControl::state state) override + { + _parent.StateChange(state); + } + + BEGIN_INTERFACE_MAP(Notification) + INTERFACE_ENTRY(RPC::IRemoteConnection::INotification) + INTERFACE_ENTRY(PluginHost::IStateControl::INotification) + END_INTERFACE_MAP + + private: + ClientCompositorRender& _parent; + }; + + public: + static constexpr uint16_t DefaultWidth = 1920; + static constexpr uint16_t DefaultHeight = 1080; + + class Geometry : public Core::JSON::Container { + public: + Geometry() + : Core::JSON::Container() + , X(0) + , Y(0) + , Width(100) + , Height(100) + { + Add(_T("x"), &X); + Add(_T("y"), &Y); + Add(_T("width"), &Width); + Add(_T("height"), &Height); + } + ~Geometry() override = default; + + public: + Core::JSON::DecUInt16 X; + Core::JSON::DecUInt16 Y; + Core::JSON::DecUInt16 Width; + Core::JSON::DecUInt16 Height; + }; + + class Config : public Core::JSON::Container { + public: + Config(const Config&) = delete; + Config& operator=(const Config&) = delete; + + Config() + : Core::JSON::Container() + , CanvasWidth(DefaultWidth) + , CanvasHeight(DefaultHeight) + , TextAtlas("Arial.png") + , Image("ml-tv-color-small.png") + , ImageCount(40) + , RelativeGeometry() + { + Add(_T("canvas-width"), &CanvasWidth); + Add(_T("canvas-heigth"), &CanvasHeight); + Add(_T("textatlas"), &TextAtlas); + Add(_T("image"), &Image); + Add(_T("imagecount"), &ImageCount); + Add(_T("relative-geometry"), &RelativeGeometry); + } + ~Config() override = default; + + public: + Core::JSON::DecUInt16 CanvasWidth; + Core::JSON::DecUInt16 CanvasHeight; + Core::JSON::String TextAtlas; + Core::JSON::String Image; + Core::JSON::DecUInt32 ImageCount; + Geometry RelativeGeometry; + }; + + public: + ClientCompositorRender(const ClientCompositorRender&) = delete; + ClientCompositorRender& operator=(const ClientCompositorRender&) = delete; + PUSH_WARNING(DISABLE_WARNING_THIS_IN_MEMBER_INITIALIZER_LIST) + ClientCompositorRender() + : _connectionId(0) + , _service(nullptr) + , _memory(nullptr) + , _statecontrol(nullptr) + , _notification(this) + { + } + POP_WARNING() + ~ClientCompositorRender() override = default; + + BEGIN_INTERFACE_MAP(ClientCompositorRender) + INTERFACE_ENTRY(IPlugin) + INTERFACE_AGGREGATE(Exchange::IMemory, _memory) + INTERFACE_AGGREGATE(PluginHost::IStateControl, _statecontrol) + END_INTERFACE_MAP + + public: + // IPlugin + const string Initialize(PluginHost::IShell* service) override; + void Deinitialize(PluginHost::IShell* service) override; + string Information() const override; + + void StateChange(PluginHost::IStateControl::state); + + private: + void Deactivated(RPC::IRemoteConnection* connection); + + private: + uint32_t _skipURL; + uint32_t _connectionId; + PluginHost::IShell* _service; + Exchange::IMemory* _memory; + PluginHost::IStateControl* _statecontrol; + Core::SinkType _notification; + }; +} +} diff --git a/Source/compositorclient/test/client-renderer/plugin/ClientCompositorRenderImplementation.cpp b/Source/compositorclient/test/client-renderer/plugin/ClientCompositorRenderImplementation.cpp new file mode 100644 index 00000000..1a21c0cd --- /dev/null +++ b/Source/compositorclient/test/client-renderer/plugin/ClientCompositorRenderImplementation.cpp @@ -0,0 +1,233 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2025 Metrological + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "Module.h" +#include + +#include +#include +#include + +#include "ClientCompositorRender.h" + +namespace Thunder { +namespace Plugin { + class ClientCompositorRenderImplementation : public PluginHost::IStateControl { + public: + ClientCompositorRenderImplementation(const ClientCompositorRenderImplementation&) = delete; + ClientCompositorRenderImplementation& operator=(const ClientCompositorRenderImplementation&) = delete; + + ClientCompositorRenderImplementation() + : _adminLock() + , _observers() + , _state(PluginHost::IStateControl::UNINITIALIZED) + , _requestedCommand(PluginHost::IStateControl::SUSPEND) + , _job(*this) + , _renderer() + , _model() + { + } + + ~ClientCompositorRenderImplementation() override = default; + + friend Core::ThreadPool::JobType; + + uint32_t Configure(PluginHost::IShell* service) override + { + uint32_t result(Core::ERROR_NONE); + + ASSERT(service != nullptr); + ClientCompositorRender::Config config; + config.FromString(service->ConfigLine()); + + if (_renderer.Configure(config.CanvasWidth.Value(), config.CanvasHeight.Value())) { + Compositor::TextureBounce::Config model_config; + model_config.Image = service->DataPath() + config.Image.Value(); + model_config.ImageCount = config.ImageCount.Value(); + + std::string configStr; + model_config.ToString(configStr); + + if (!_renderer.Register(&_model, configStr)) { + fprintf(stderr, "Failed to initialize model\n"); + result = Core::ERROR_OPENING_FAILED; + } + + } else { + result = Core::ERROR_ILLEGAL_STATE; + } + + return result; + } + + PluginHost::IStateControl::state State() const override + { + PluginHost::IStateControl::state state; + _adminLock.Lock(); + state = _state; + _adminLock.Unlock(); + return state; + } + + void Dispatch() + { + bool stateChanged = false; + + _adminLock.Lock(); + if (_requestedCommand == PluginHost::IStateControl::RESUME) { + if ((_state == PluginHost::IStateControl::UNINITIALIZED || _state == PluginHost::IStateControl::SUSPENDED)) { + _renderer.Start(); + stateChanged = true; + _state = PluginHost::IStateControl::RESUMED; + } + } else { + if (_state == PluginHost::IStateControl::RESUMED || _state == PluginHost::IStateControl::UNINITIALIZED) { + _renderer.Stop(); + stateChanged = true; + _state = PluginHost::IStateControl::SUSPENDED; + } + } + if (stateChanged) { + for (auto& observer : _observers) { + observer->StateChange(_state); + } + } + _adminLock.Unlock(); + } + + uint32_t Request(const PluginHost::IStateControl::command command) override + { + _adminLock.Lock(); + _requestedCommand = command; + _job.Submit(); + _adminLock.Unlock(); + return (Core::ERROR_NONE); + } + + void Register(PluginHost::IStateControl::INotification* notification) override + { + ASSERT(notification != nullptr); + + _adminLock.Lock(); + + // Only subscribe an interface once. + std::list::iterator index(std::find(_observers.begin(), _observers.end(), notification)); + ASSERT(index == _observers.end()); + + if (index == _observers.end()) { + // We will keep a reference to this observer, reference it.. + notification->AddRef(); + _observers.push_back(notification); + } + _adminLock.Unlock(); + } + + void Unregister(PluginHost::IStateControl::INotification* notification) override + { + ASSERT(notification != nullptr); + + _adminLock.Lock(); + // Only subscribe an interface once. + std::list::iterator index(std::find(_observers.begin(), _observers.end(), notification)); + + // Unregister only once :-) + ASSERT(index != _observers.end()); + + if (index != _observers.end()) { + + // We will keep a reference to this observer, reference it.. + (*index)->Release(); + _observers.erase(index); + } + _adminLock.Unlock(); + } + + BEGIN_INTERFACE_MAP(ClientCompositorRenderImplementation) + INTERFACE_ENTRY(PluginHost::IStateControl) + END_INTERFACE_MAP + + private: + mutable Core::CriticalSection _adminLock; + std::list _observers; + PluginHost::IStateControl::state _state; + PluginHost::IStateControl::command _requestedCommand; + Core::WorkerPool::JobType _job; + + Compositor::Render _renderer; + Compositor::TextureBounce _model; + }; + + SERVICE_REGISTRATION(ClientCompositorRenderImplementation, 1, 0) + +} /* namespace Plugin */ + +namespace ClientCompositorRender { + class MemoryObserverImpl : public Exchange::IMemory { + private: + MemoryObserverImpl(); + MemoryObserverImpl(const MemoryObserverImpl&); + MemoryObserverImpl& operator=(const MemoryObserverImpl&); + + public: + MemoryObserverImpl(const RPC::IRemoteConnection* connection) + : _main(connection == nullptr ? Core::ProcessInfo().Id() : connection->RemoteId()) + { + } + ~MemoryObserverImpl() + { + } + + public: + uint64_t Resident() const override + { + return _main.Resident(); + } + uint64_t Allocated() const override + { + return _main.Allocated(); + } + uint64_t Shared() const override + { + return _main.Shared(); + } + uint8_t Processes() const override + { + return (IsOperational() ? 1 : 0); + } + bool IsOperational() const override + { + return _main.IsActive(); + } + + BEGIN_INTERFACE_MAP(MemoryObserverImpl) + INTERFACE_ENTRY(Exchange::IMemory) + END_INTERFACE_MAP + + private: + Core::ProcessInfo _main; + }; + + Exchange::IMemory* MemoryObserver(const RPC::IRemoteConnection* connection) + { + ASSERT(connection != nullptr); + Exchange::IMemory* result = Core::ServiceType::Create(connection); + return (result); + } +} +} // namespace ClientCompositorRender diff --git a/Source/compositorclient/test/client-renderer/plugin/Module.cpp b/Source/compositorclient/test/client-renderer/plugin/Module.cpp new file mode 100644 index 00000000..d7836109 --- /dev/null +++ b/Source/compositorclient/test/client-renderer/plugin/Module.cpp @@ -0,0 +1,5 @@ + +#include "Module.h" + +MODULE_NAME_DECLARATION(BUILD_REFERENCE) + diff --git a/Source/compositorclient/test/client-renderer/plugin/Module.h b/Source/compositorclient/test/client-renderer/plugin/Module.h new file mode 100644 index 00000000..d2cfe787 --- /dev/null +++ b/Source/compositorclient/test/client-renderer/plugin/Module.h @@ -0,0 +1,19 @@ +#pragma once + +#ifndef MODULE_NAME +#define MODULE_NAME Plugin_CompositionClientRender +#endif + +#include +#include +#include + +#if defined(__WINDOWS__) +#if defined(COMPOSITORCLIENT_EXPORTS) +#undef EXTERNAL +#define EXTERNAL EXTERNAL_EXPORT +#else +#pragma comment(lib, "compositorclient.lib") +#endif +#endif + diff --git a/Source/compositorclient/src/Mesa/test/CMakeLists.txt b/Source/compositorclient/test/gbm_buffer_test/CMakeLists.txt similarity index 89% rename from Source/compositorclient/src/Mesa/test/CMakeLists.txt rename to Source/compositorclient/test/gbm_buffer_test/CMakeLists.txt index 841481d5..e4252e83 100644 --- a/Source/compositorclient/src/Mesa/test/CMakeLists.txt +++ b/Source/compositorclient/test/gbm_buffer_test/CMakeLists.txt @@ -15,6 +15,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +option(INSTALL_CLIENT_COMPOSITOR_GBM_TEST_APP "Install the GBM compositor client test application" OFF) + find_package(CompileSettingsDebug CONFIG REQUIRED) find_package(gbm REQUIRED) find_package(libdrm REQUIRED) @@ -37,6 +39,6 @@ target_link_libraries(gbm_buffer_test gbm::gbm EGL::EGL ) - +if(INSTALL_CLIENT_COMPOSITOR_GBM_TEST_APP) install(TARGETS gbm_buffer_test DESTINATION ${CMAKE_INSTALL_BINDIR}/${NAMESPACE}Tests COMPONENT ${NAMESPACE}_Test) - +endif() diff --git a/Source/compositorclient/src/Mesa/test/gbm_buffer_test.cpp b/Source/compositorclient/test/gbm_buffer_test/gbm_buffer_test.cpp similarity index 100% rename from Source/compositorclient/src/Mesa/test/gbm_buffer_test.cpp rename to Source/compositorclient/test/gbm_buffer_test/gbm_buffer_test.cpp diff --git a/Source/compositorclient/test/legacy-test/CMakeLists.txt b/Source/compositorclient/test/legacy-test/CMakeLists.txt new file mode 100644 index 00000000..7ad7b107 --- /dev/null +++ b/Source/compositorclient/test/legacy-test/CMakeLists.txt @@ -0,0 +1,38 @@ +# If not stated otherwise in this file or this component's LICENSE file the +# following copyright and licenses apply: +# +# Copyright 2023 Metrological +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +find_package(CompileSettingsDebug CONFIG REQUIRED) +find_package(${NAMESPACE}Core CONFIG REQUIRED) +find_package(${NAMESPACE}Messaging CONFIG REQUIRED) +find_package(${NAMESPACE}LocalTracer CONFIG REQUIRED) + +add_executable(compositorclient_test main.cpp) + +target_link_libraries(compositorclient_test + PRIVATE + ${NAMESPACE}Core::${NAMESPACE}Core + ${NAMESPACE}Messaging::${NAMESPACE}Messaging + ${NAMESPACE}LocalTracer::${NAMESPACE}LocalTracer + ClientVirtualInput::ClientVirtualInput + CompileSettingsDebug::CompileSettingsDebug + ClientCompositor::ClientCompositor +) + +if(INSTALL_TESTS) + install(TARGETS compositorclient_test DESTINATION ${CMAKE_INSTALL_BINDIR} COMPONENT ${NAMESPACE}_Test) +endif() diff --git a/Source/compositorclient/test/main.cpp b/Source/compositorclient/test/legacy-test/main.cpp similarity index 100% rename from Source/compositorclient/test/main.cpp rename to Source/compositorclient/test/legacy-test/main.cpp index 761747dc..82eae8c8 100644 --- a/Source/compositorclient/test/main.cpp +++ b/Source/compositorclient/test/legacy-test/main.cpp @@ -76,12 +76,12 @@ int main(VARIABLE_IS_NOT_USED int argc, VARIABLE_IS_NOT_USED const char* argv[]) ASSERT(display != nullptr); if (surface != nullptr) { - surface = display->Create("TestClient", 1280, 720); - TRACE_GLOBAL(Thunder::Trace::Information, ("Create %s surface", surface->Name().c_str())); - } else { TRACE_GLOBAL(Thunder::Trace::Information, ("Release %s surface", surface->Name().c_str())); surface->Release(); surface = nullptr; + } else { + surface = display->Create("TestClient", 1280, 720); + TRACE_GLOBAL(Thunder::Trace::Information, ("Create %s surface", surface->Name().c_str())); } break; diff --git a/Source/graphicsbuffer/include/graphicsbuffer/GraphicsBufferType.h b/Source/graphicsbuffer/include/graphicsbuffer/GraphicsBufferType.h index cc5bba1c..e253bb8e 100644 --- a/Source/graphicsbuffer/include/graphicsbuffer/GraphicsBufferType.h +++ b/Source/graphicsbuffer/include/graphicsbuffer/GraphicsBufferType.h @@ -223,7 +223,7 @@ namespace Graphics { // Do not initialize members for now, this constructor is called after a mmap in the // placement new operator above. Initializing them now will reset the original values // of the buffer metadata. - SharedStorageType() {}; + SharedStorageType() { }; void* operator new(size_t stAllocateBlock, int fd) { @@ -910,7 +910,7 @@ namespace Graphics { // Now the request *MUST* succeed! requested = SharedBufferType::Rendered(); - ASSERT(requested == true); + ASSERT((requested == true) || (SharedBufferType::IsDestroyed() == true)); } if (requested == true) { @@ -934,7 +934,7 @@ namespace Graphics { // Now the request *MUST* succeed! requested = SharedBufferType::Published(); - ASSERT(requested == true); + ASSERT((requested == true) || (SharedBufferType::IsDestroyed() == true)); } if (requested == true) {