From c593eed15050bb42e8523e8e0624a79591fa5594 Mon Sep 17 00:00:00 2001 From: Bram Oosterhuis Date: Mon, 20 Oct 2025 22:10:49 +0200 Subject: [PATCH 01/22] CompositorClient: return nullptr of no remote connection can be made --- .../src/Mesa/Implementation.cpp | 21 ++++++++++++------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/Source/compositorclient/src/Mesa/Implementation.cpp b/Source/compositorclient/src/Mesa/Implementation.cpp index 08490384..1dd12de1 100644 --- a/Source/compositorclient/src/Mesa/Implementation.cpp +++ b/Source/compositorclient/src/Mesa/Implementation.cpp @@ -575,13 +575,8 @@ namespace Linux { return result; } - const Exchange::IComposition::IDisplay* RemoteDisplay() const - { - return _remoteDisplay; - } - Exchange::IComposition::IDisplay* RemoteDisplay() - { - return _remoteDisplay; + bool IsValid() const { + return _remoteDisplay != nullptr; } private: @@ -906,6 +901,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 From 664eab7790c96db69cdfb8fec862e600b5c44e18 Mon Sep 17 00:00:00 2001 From: Bram Oosterhuis Date: Mon, 20 Oct 2025 22:11:32 +0200 Subject: [PATCH 02/22] CompositorClient: add initial rendering test app and plugin --- .../Mesa/test/client-renderer/CMakeLists.txt | 23 ++ .../test/client-renderer/app/CMakeLists.txt | 45 +++ .../Mesa/test/client-renderer/app/Main.cpp | 156 ++++++++ .../Mesa/test/client-renderer/app/Module.cpp | 23 ++ .../Mesa/test/client-renderer/app/Module.h | 37 ++ .../client-renderer/common/CMakeLists.txt | 66 ++++ .../test/client-renderer/common/Fonts/Arial.h | 137 +++++++ .../client-renderer/common/Fonts/Arial.png | Bin 0 -> 41030 bytes .../common/Fonts/CMakeLists.txt | 12 + .../test/client-renderer/common/Fonts/Font.h | 53 +++ .../Mesa/test/client-renderer/common/IModel.h | 31 ++ .../test/client-renderer/common/Module.cpp | 23 ++ .../Mesa/test/client-renderer/common/Module.h | 37 ++ .../test/client-renderer/common/Renderer.cpp | 318 +++++++++++++++ .../test/client-renderer/common/Renderer.h | 163 ++++++++ .../client-renderer/common/TerminalInput.h | 100 +++++ .../client-renderer/common/TextRender.cpp | 344 ++++++++++++++++ .../test/client-renderer/common/TextRender.h | 109 ++++++ .../client-renderer/common/TextureBounce.cpp | 366 ++++++++++++++++++ .../client-renderer/common/TextureBounce.h | 118 ++++++ .../client-renderer/common/TextureLoader.cpp | 94 +++++ .../client-renderer/common/TextureLoader.h | 41 ++ .../common/ml-tv-color-small.png | Bin 0 -> 65893 bytes .../client-renderer/plugin/CMakeLists.txt | 62 +++ .../plugin/ClientCompositorRender.conf.in | 12 + .../plugin/ClientCompositorRender.cpp | 160 ++++++++ .../plugin/ClientCompositorRender.h | 110 ++++++ .../ClientCompositorRenderImplementation.cpp | 261 +++++++++++++ .../test/client-renderer/plugin/Module.cpp | 5 + .../Mesa/test/client-renderer/plugin/Module.h | 19 + 30 files changed, 2925 insertions(+) create mode 100644 Source/compositorclient/src/Mesa/test/client-renderer/CMakeLists.txt create mode 100644 Source/compositorclient/src/Mesa/test/client-renderer/app/CMakeLists.txt create mode 100644 Source/compositorclient/src/Mesa/test/client-renderer/app/Main.cpp create mode 100644 Source/compositorclient/src/Mesa/test/client-renderer/app/Module.cpp create mode 100644 Source/compositorclient/src/Mesa/test/client-renderer/app/Module.h create mode 100644 Source/compositorclient/src/Mesa/test/client-renderer/common/CMakeLists.txt create mode 100644 Source/compositorclient/src/Mesa/test/client-renderer/common/Fonts/Arial.h create mode 100644 Source/compositorclient/src/Mesa/test/client-renderer/common/Fonts/Arial.png create mode 100644 Source/compositorclient/src/Mesa/test/client-renderer/common/Fonts/CMakeLists.txt create mode 100644 Source/compositorclient/src/Mesa/test/client-renderer/common/Fonts/Font.h create mode 100644 Source/compositorclient/src/Mesa/test/client-renderer/common/IModel.h create mode 100644 Source/compositorclient/src/Mesa/test/client-renderer/common/Module.cpp create mode 100644 Source/compositorclient/src/Mesa/test/client-renderer/common/Module.h create mode 100644 Source/compositorclient/src/Mesa/test/client-renderer/common/Renderer.cpp create mode 100644 Source/compositorclient/src/Mesa/test/client-renderer/common/Renderer.h create mode 100644 Source/compositorclient/src/Mesa/test/client-renderer/common/TerminalInput.h create mode 100644 Source/compositorclient/src/Mesa/test/client-renderer/common/TextRender.cpp create mode 100644 Source/compositorclient/src/Mesa/test/client-renderer/common/TextRender.h create mode 100644 Source/compositorclient/src/Mesa/test/client-renderer/common/TextureBounce.cpp create mode 100644 Source/compositorclient/src/Mesa/test/client-renderer/common/TextureBounce.h create mode 100644 Source/compositorclient/src/Mesa/test/client-renderer/common/TextureLoader.cpp create mode 100644 Source/compositorclient/src/Mesa/test/client-renderer/common/TextureLoader.h create mode 100644 Source/compositorclient/src/Mesa/test/client-renderer/common/ml-tv-color-small.png create mode 100644 Source/compositorclient/src/Mesa/test/client-renderer/plugin/CMakeLists.txt create mode 100644 Source/compositorclient/src/Mesa/test/client-renderer/plugin/ClientCompositorRender.conf.in create mode 100644 Source/compositorclient/src/Mesa/test/client-renderer/plugin/ClientCompositorRender.cpp create mode 100644 Source/compositorclient/src/Mesa/test/client-renderer/plugin/ClientCompositorRender.h create mode 100644 Source/compositorclient/src/Mesa/test/client-renderer/plugin/ClientCompositorRenderImplementation.cpp create mode 100644 Source/compositorclient/src/Mesa/test/client-renderer/plugin/Module.cpp create mode 100644 Source/compositorclient/src/Mesa/test/client-renderer/plugin/Module.h diff --git a/Source/compositorclient/src/Mesa/test/client-renderer/CMakeLists.txt b/Source/compositorclient/src/Mesa/test/client-renderer/CMakeLists.txt new file mode 100644 index 00000000..f6577d41 --- /dev/null +++ b/Source/compositorclient/src/Mesa/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/src/Mesa/test/client-renderer/app/CMakeLists.txt b/Source/compositorclient/src/Mesa/test/client-renderer/app/CMakeLists.txt new file mode 100644 index 00000000..468e632c --- /dev/null +++ b/Source/compositorclient/src/Mesa/test/client-renderer/app/CMakeLists.txt @@ -0,0 +1,45 @@ +# 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(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 + 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/src/Mesa/test/client-renderer/app/Main.cpp b/Source/compositorclient/src/Mesa/test/client-renderer/app/Main.cpp new file mode 100644 index 00000000..725fb242 --- /dev/null +++ b/Source/compositorclient/src/Mesa/test/client-renderer/app/Main.cpp @@ -0,0 +1,156 @@ +/* + * 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 + +using namespace Thunder; + +static 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); + } + } +}; + +int main(int argc, char* argv[]) +{ + const char* executableName(Thunder::Core::FileNameOnly(argv[0])); + 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) { + fprintf(stderr, "Failed to initialize renderer\n"); + Core::Singleton::Dispose(); + return 1; + } + + if (!renderer.Register(&model, configStr)) { + fprintf(stderr, "Failed to initialize model\n"); + Core::Singleton::Dispose(); + return 1; + } + + Compositor::TerminalInput keyboard; + ASSERT(keyboard.IsValid() == true); + + renderer.Start(); + + 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': + renderer.ToggleFPS(); + break; + case 'Q': + quitApp = true; + break; + case 'H': + TRACE_GLOBAL(Trace::Information, ("Available commands:")); + TRACE_GLOBAL(Trace::Information, (" S - Start/Stop the rendering")); + TRACE_GLOBAL(Trace::Information, (" F - Show current FPS")); + TRACE_GLOBAL(Trace::Information, (" Q - Quit the application")); + TRACE_GLOBAL(Trace::Information, (" H - Show this help message")); + 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 0; +} diff --git a/Source/compositorclient/src/Mesa/test/client-renderer/app/Module.cpp b/Source/compositorclient/src/Mesa/test/client-renderer/app/Module.cpp new file mode 100644 index 00000000..160593e6 --- /dev/null +++ b/Source/compositorclient/src/Mesa/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/src/Mesa/test/client-renderer/app/Module.h b/Source/compositorclient/src/Mesa/test/client-renderer/app/Module.h new file mode 100644 index 00000000..12df3601 --- /dev/null +++ b/Source/compositorclient/src/Mesa/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/src/Mesa/test/client-renderer/common/CMakeLists.txt b/Source/compositorclient/src/Mesa/test/client-renderer/common/CMakeLists.txt new file mode 100644 index 00000000..2d172512 --- /dev/null +++ b/Source/compositorclient/src/Mesa/test/client-renderer/common/CMakeLists.txt @@ -0,0 +1,66 @@ +# 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}Messaging CONFIG REQUIRED) +find_package(ClientCompositor REQUIRED) +find_package(CompileSettingsDebug CONFIG REQUIRED) + +add_subdirectory(Fonts) + +add_library(ClientCompositorRenderCommon STATIC + Module.cpp + TextureBounce.cpp + TextureLoader.cpp + Renderer.cpp + TextRender.cpp +) + +target_link_libraries(ClientCompositorRenderCommon + PUBLIC + ${NAMESPACE}Core::${NAMESPACE}Core + ${NAMESPACE}Messaging::${NAMESPACE}Messaging + ClientCompositor::ClientCompositor + 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/src/Mesa/test/client-renderer/common/Fonts/Arial.h b/Source/compositorclient/src/Mesa/test/client-renderer/common/Fonts/Arial.h new file mode 100644 index 00000000..be35aa88 --- /dev/null +++ b/Source/compositorclient/src/Mesa/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/src/Mesa/test/client-renderer/common/Fonts/Arial.png b/Source/compositorclient/src/Mesa/test/client-renderer/common/Fonts/Arial.png new file mode 100644 index 0000000000000000000000000000000000000000..db85ae49d1fc547c8ae97bda579f492c819287e9 GIT binary patch literal 41030 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 cz&jB5A7=VpvYO*) + +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/src/Mesa/test/client-renderer/common/Fonts/Font.h b/Source/compositorclient/src/Mesa/test/client-renderer/common/Fonts/Font.h new file mode 100644 index 00000000..1923dfff --- /dev/null +++ b/Source/compositorclient/src/Mesa/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/src/Mesa/test/client-renderer/common/IModel.h b/Source/compositorclient/src/Mesa/test/client-renderer/common/IModel.h new file mode 100644 index 00000000..9dec3b56 --- /dev/null +++ b/Source/compositorclient/src/Mesa/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/src/Mesa/test/client-renderer/common/Module.cpp b/Source/compositorclient/src/Mesa/test/client-renderer/common/Module.cpp new file mode 100644 index 00000000..5b25d568 --- /dev/null +++ b/Source/compositorclient/src/Mesa/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/src/Mesa/test/client-renderer/common/Module.h b/Source/compositorclient/src/Mesa/test/client-renderer/common/Module.h new file mode 100644 index 00000000..af2e36e7 --- /dev/null +++ b/Source/compositorclient/src/Mesa/test/client-renderer/common/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 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 + diff --git a/Source/compositorclient/src/Mesa/test/client-renderer/common/Renderer.cpp b/Source/compositorclient/src/Mesa/test/client-renderer/common/Renderer.cpp new file mode 100644 index 00000000..eab936a2 --- /dev/null +++ b/Source/compositorclient/src/Mesa/test/client-renderer/common/Renderer.cpp @@ -0,0 +1,318 @@ +/* + * 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) + , _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()) { + fprintf(stderr, "Failed to initialize EGL\n"); + 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)) { + fprintf(stderr, "Failed to initialize FPS counter\n"); + return false; + } + + // Release EGL context for render thread + if (!eglMakeCurrent(_eglDisplay, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT)) { + EGLint error = eglGetError(); + fprintf(stderr, "Failed to release EGL context: 0x%x\n", error); + return false; + } + + return true; + } else { + fprintf(stderr, "Failed to initialize display\n"); + 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 { + fprintf(stderr, "Failed to initialize model during registration\n"); + } + + 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)) { + EGLint error = eglGetError(); + fprintf(stderr, "InitializeModel: eglMakeCurrent failed: 0x%x\n", error); + 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)) { + EGLint error = eglGetError(); + fprintf(stderr, "InitializeModel: release failed: 0x%x\n", error); + } + + 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) { + fprintf(stderr, "eglGetDisplay failed\n"); + return false; + } + + if (!eglInitialize(_eglDisplay, nullptr, nullptr)) { + fprintf(stderr, "eglInitialize failed\n"); + 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)) { + fprintf(stderr, "eglChooseConfig failed\n"); + 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) { + fprintf(stderr, "eglCreateContext failed\n"); + return false; + } + + _eglSurface = eglCreateWindowSurface(_eglDisplay, eglConfig, window, nullptr); + if (_eglSurface == EGL_NO_SURFACE) { + fprintf(stderr, "eglCreateWindowSurface failed\n"); + return false; + } + + if (!eglMakeCurrent(_eglDisplay, _eglSurface, _eglSurface, _eglContext)) { + EGLint error = eglGetError(); + fprintf(stderr, "eglMakeCurrent failed: 0x%x\n", error); + 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() + { + while (_running.load() && !ShouldExit()) { + if (_models.empty() || _selectedModel >= _models.size()) { + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + continue; + } + + // Make context current for this render thread + if (!eglMakeCurrent(_eglDisplay, _eglSurface, _eglSurface, _eglContext)) { + EGLint error = eglGetError(); + fprintf(stderr, "Draw: eglMakeCurrent failed: 0x%x\n", error); + break; + } + + 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 + eglSwapBuffers(_eglDisplay, _eglSurface); + + _surface->RequestRender(); + + if (WaitForRendered(1000) == Core::ERROR_TIMEDOUT) { + TRACE(Trace::Warning, ("Timed out waiting for rendered callback")); + } + } else { + TRACE(Trace::Warning, ("Model draw failed")); + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + } + } + + // 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/src/Mesa/test/client-renderer/common/Renderer.h b/Source/compositorclient/src/Mesa/test/client-renderer/common/Renderer.h new file mode 100644 index 00000000..044f4fd5 --- /dev/null +++ b/Source/compositorclient/src/Mesa/test/client-renderer/common/Renderer.h @@ -0,0 +1,163 @@ +/* + * 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 + { + 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() + { + bool expected = true; + if (_running.compare_exchange_strong(expected, false)) { + TRACE(Trace::Information, ("Stopping Render")); + _render.join(); + _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(); + } + void ToggleFPS() + { + _showFps = !_showFps; + } + + // 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; + + 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/src/Mesa/test/client-renderer/common/TerminalInput.h b/Source/compositorclient/src/Mesa/test/client-renderer/common/TerminalInput.h new file mode 100644 index 00000000..c0879737 --- /dev/null +++ b/Source/compositorclient/src/Mesa/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/src/Mesa/test/client-renderer/common/TextRender.cpp b/Source/compositorclient/src/Mesa/test/client-renderer/common/TextRender.cpp new file mode 100644 index 00000000..b69e167e --- /dev/null +++ b/Source/compositorclient/src/Mesa/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/src/Mesa/test/client-renderer/common/TextRender.h b/Source/compositorclient/src/Mesa/test/client-renderer/common/TextRender.h new file mode 100644 index 00000000..1fc67fb1 --- /dev/null +++ b/Source/compositorclient/src/Mesa/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/src/Mesa/test/client-renderer/common/TextureBounce.cpp b/Source/compositorclient/src/Mesa/test/client-renderer/common/TextureBounce.cpp new file mode 100644 index 00000000..6bbf8193 --- /dev/null +++ b/Source/compositorclient/src/Mesa/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/src/Mesa/test/client-renderer/common/TextureBounce.h b/Source/compositorclient/src/Mesa/test/client-renderer/common/TextureBounce.h new file mode 100644 index 00000000..e8643b91 --- /dev/null +++ b/Source/compositorclient/src/Mesa/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/src/Mesa/test/client-renderer/common/TextureLoader.cpp b/Source/compositorclient/src/Mesa/test/client-renderer/common/TextureLoader.cpp new file mode 100644 index 00000000..c75f5c4b --- /dev/null +++ b/Source/compositorclient/src/Mesa/test/client-renderer/common/TextureLoader.cpp @@ -0,0 +1,94 @@ +/* + * 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, {}}; + + 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); + + // Force RGBA8 + if (bit_depth == 16) png_set_strip_16(png); + if (color_type == PNG_COLOR_TYPE_PALETTE) png_set_palette_to_rgb(png); + if (color_type == PNG_COLOR_TYPE_GRAY && bit_depth < 8) png_set_expand_gray_1_2_4_to_8(png); + if (png_get_valid(png, info, PNG_INFO_tRNS)) png_set_tRNS_to_alpha(png); + 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); + 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/src/Mesa/test/client-renderer/common/TextureLoader.h b/Source/compositorclient/src/Mesa/test/client-renderer/common/TextureLoader.h new file mode 100644 index 00000000..ad2b77ad --- /dev/null +++ b/Source/compositorclient/src/Mesa/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/src/Mesa/test/client-renderer/common/ml-tv-color-small.png b/Source/compositorclient/src/Mesa/test/client-renderer/common/ml-tv-color-small.png new file mode 100644 index 0000000000000000000000000000000000000000..da3f5a48de0eace33b1364a51bc2b52adc892c13 GIT binary patch literal 65893 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 sYyUa|H?CZ metadata( + // Version + 1, 0, 0, + // Preconditions + { subsystem::GRAPHICS }, + // Terminations + {}, + // 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(); + } + } + } 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/src/Mesa/test/client-renderer/plugin/ClientCompositorRender.h b/Source/compositorclient/src/Mesa/test/client-renderer/plugin/ClientCompositorRender.h new file mode 100644 index 00000000..9901fa39 --- /dev/null +++ b/Source/compositorclient/src/Mesa/test/client-renderer/plugin/ClientCompositorRender.h @@ -0,0 +1,110 @@ +/* + * 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: + 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/src/Mesa/test/client-renderer/plugin/ClientCompositorRenderImplementation.cpp b/Source/compositorclient/src/Mesa/test/client-renderer/plugin/ClientCompositorRenderImplementation.cpp new file mode 100644 index 00000000..8c790c80 --- /dev/null +++ b/Source/compositorclient/src/Mesa/test/client-renderer/plugin/ClientCompositorRenderImplementation.cpp @@ -0,0 +1,261 @@ +/* + * 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 + +namespace Thunder { +namespace Plugin { + class ClientCompositorRenderImplementation : public PluginHost::IStateControl { + private: + class Config : public Core::JSON::Container { + public: + Config(const Config&) = delete; + Config& operator=(const Config&) = delete; + + Config() + : Core::JSON::Container() + , Width(Compositor::Render::DefaultWidth) + , Height(Compositor::Render::DefaultHeight) + , TextAtlas("Arial.png") + , Image("ml-tv-color-small.png") + , ImageCount(40) + { + Add(_T("width"), &Width); + Add(_T("heigth"), &Height); + Add(_T("textatlas"), &TextAtlas); + Add(_T("image"), &Image); + Add(_T("imagecount"), &ImageCount); + } + ~Config() override = default; + + public: + Core::JSON::DecUInt16 Width; + Core::JSON::DecUInt16 Height; + Core::JSON::String TextAtlas; + Core::JSON::String Image; + Core::JSON::DecUInt32 ImageCount; + }; + + 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); + Config config; + config.FromString(service->ConfigLine()); + + if (_renderer.Configure(config.Width.Value(), config.Height.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/src/Mesa/test/client-renderer/plugin/Module.cpp b/Source/compositorclient/src/Mesa/test/client-renderer/plugin/Module.cpp new file mode 100644 index 00000000..d7836109 --- /dev/null +++ b/Source/compositorclient/src/Mesa/test/client-renderer/plugin/Module.cpp @@ -0,0 +1,5 @@ + +#include "Module.h" + +MODULE_NAME_DECLARATION(BUILD_REFERENCE) + diff --git a/Source/compositorclient/src/Mesa/test/client-renderer/plugin/Module.h b/Source/compositorclient/src/Mesa/test/client-renderer/plugin/Module.h new file mode 100644 index 00000000..d2cfe787 --- /dev/null +++ b/Source/compositorclient/src/Mesa/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 + From 746c9d6ca7e380893dd226de3743ff26349f01f6 Mon Sep 17 00:00:00 2001 From: Bram Oosterhuis Date: Mon, 20 Oct 2025 22:12:34 +0200 Subject: [PATCH 03/22] CompositorClient: move GBM surface util --- .../src/Mesa/test/CMakeLists.txt | 34 +++++--------- .../Mesa/test/gbm_buffer_test/CMakeLists.txt | 44 +++++++++++++++++++ .../{ => gbm_buffer_test}/gbm_buffer_test.cpp | 0 3 files changed, 54 insertions(+), 24 deletions(-) create mode 100644 Source/compositorclient/src/Mesa/test/gbm_buffer_test/CMakeLists.txt rename Source/compositorclient/src/Mesa/test/{ => gbm_buffer_test}/gbm_buffer_test.cpp (100%) diff --git a/Source/compositorclient/src/Mesa/test/CMakeLists.txt b/Source/compositorclient/src/Mesa/test/CMakeLists.txt index 841481d5..48a323af 100644 --- a/Source/compositorclient/src/Mesa/test/CMakeLists.txt +++ b/Source/compositorclient/src/Mesa/test/CMakeLists.txt @@ -1,7 +1,8 @@ + # 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,28 +16,13 @@ # See the License for the specific language governing permissions and # limitations under the License. -find_package(CompileSettingsDebug CONFIG REQUIRED) -find_package(gbm REQUIRED) -find_package(libdrm REQUIRED) -find_package(EGL REQUIRED) - -add_executable(gbm_buffer_test gbm_buffer_test.cpp) - -# Set target properties -set_target_properties(gbm_buffer_test PROPERTIES - CXX_STANDARD 11 - CXX_STANDARD_REQUIRED ON - CXX_EXTENSIONS OFF - OUTPUT_NAME gbm_buffer_test -) - -target_link_libraries(gbm_buffer_test - PRIVATE - CompileSettingsDebug::CompileSettingsDebug - libdrm::libdrm - gbm::gbm - EGL::EGL -) +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) -install(TARGETS gbm_buffer_test DESTINATION ${CMAKE_INSTALL_BINDIR}/${NAMESPACE}Tests COMPONENT ${NAMESPACE}_Test) +if(BUILD_CLIENT_COMPOSITOR_GBM_UTIL) +add_subdirectory(gbm_buffer_test) +endif() +if(BUILD_CLIENT_COMPOSITOR_RENDER_TEST) +add_subdirectory(client-renderer) +endif() \ No newline at end of file diff --git a/Source/compositorclient/src/Mesa/test/gbm_buffer_test/CMakeLists.txt b/Source/compositorclient/src/Mesa/test/gbm_buffer_test/CMakeLists.txt new file mode 100644 index 00000000..e4252e83 --- /dev/null +++ b/Source/compositorclient/src/Mesa/test/gbm_buffer_test/CMakeLists.txt @@ -0,0 +1,44 @@ +# 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. + +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) +find_package(EGL REQUIRED) + +add_executable(gbm_buffer_test gbm_buffer_test.cpp) + +# Set target properties +set_target_properties(gbm_buffer_test PROPERTIES + CXX_STANDARD 11 + CXX_STANDARD_REQUIRED ON + CXX_EXTENSIONS OFF + OUTPUT_NAME gbm_buffer_test +) + +target_link_libraries(gbm_buffer_test + PRIVATE + CompileSettingsDebug::CompileSettingsDebug + libdrm::libdrm + 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/src/Mesa/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/src/Mesa/test/gbm_buffer_test/gbm_buffer_test.cpp From 9d2e3f50af6805ef1602a8c67c06d68e0b625b13 Mon Sep 17 00:00:00 2001 From: Bram Oosterhuis Date: Wed, 22 Oct 2025 14:27:21 +0200 Subject: [PATCH 04/22] CompositorClient: move mesa tests to root test dir --- .../compositorclient/src/Mesa/CMakeLists.txt | 2 - .../test/client-renderer/CMakeLists.txt | 0 .../test/client-renderer/app/CMakeLists.txt | 0 .../test/client-renderer/app/Main.cpp | 0 .../test/client-renderer/app/Module.cpp | 0 .../test/client-renderer/app/Module.h | 0 .../client-renderer/common/CMakeLists.txt | 2 +- .../test/client-renderer/common/Fonts/Arial.h | 0 .../client-renderer/common/Fonts/Arial.png | Bin .../common/Fonts/CMakeLists.txt | 0 .../test/client-renderer/common/Fonts/Font.h | 0 .../test/client-renderer/common/IModel.h | 0 .../test/client-renderer/common/Module.cpp | 0 .../test/client-renderer/common/Module.h | 0 .../test/client-renderer/common/Renderer.cpp | 9 +++ .../test/client-renderer/common/Renderer.h | 4 +- .../client-renderer/common/TerminalInput.h | 0 .../client-renderer/common/TextRender.cpp | 0 .../test/client-renderer/common/TextRender.h | 0 .../client-renderer/common/TextureBounce.cpp | 0 .../client-renderer/common/TextureBounce.h | 0 .../client-renderer/common/TextureLoader.cpp | 0 .../client-renderer/common/TextureLoader.h | 0 .../common/ml-tv-color-small.png | Bin .../client-renderer/plugin/CMakeLists.txt | 51 ++++++++++-- .../plugin/ClientCompositorRender.conf.in | 8 ++ .../plugin/ClientCompositorRender.cpp | 24 +++++- .../plugin/ClientCompositorRender.h | 76 ++++++++++++++++-- .../ClientCompositorRenderImplementation.cpp | 36 +-------- .../test/client-renderer/plugin/Module.cpp | 0 .../test/client-renderer/plugin/Module.h | 0 .../test/gbm_buffer_test/CMakeLists.txt | 0 .../test/gbm_buffer_test/gbm_buffer_test.cpp | 0 33 files changed, 158 insertions(+), 54 deletions(-) rename Source/compositorclient/{src/Mesa => }/test/client-renderer/CMakeLists.txt (100%) rename Source/compositorclient/{src/Mesa => }/test/client-renderer/app/CMakeLists.txt (100%) rename Source/compositorclient/{src/Mesa => }/test/client-renderer/app/Main.cpp (100%) rename Source/compositorclient/{src/Mesa => }/test/client-renderer/app/Module.cpp (100%) rename Source/compositorclient/{src/Mesa => }/test/client-renderer/app/Module.h (100%) rename Source/compositorclient/{src/Mesa => }/test/client-renderer/common/CMakeLists.txt (97%) rename Source/compositorclient/{src/Mesa => }/test/client-renderer/common/Fonts/Arial.h (100%) rename Source/compositorclient/{src/Mesa => }/test/client-renderer/common/Fonts/Arial.png (100%) rename Source/compositorclient/{src/Mesa => }/test/client-renderer/common/Fonts/CMakeLists.txt (100%) rename Source/compositorclient/{src/Mesa => }/test/client-renderer/common/Fonts/Font.h (100%) rename Source/compositorclient/{src/Mesa => }/test/client-renderer/common/IModel.h (100%) rename Source/compositorclient/{src/Mesa => }/test/client-renderer/common/Module.cpp (100%) rename Source/compositorclient/{src/Mesa => }/test/client-renderer/common/Module.h (100%) rename Source/compositorclient/{src/Mesa => }/test/client-renderer/common/Renderer.cpp (95%) rename Source/compositorclient/{src/Mesa => }/test/client-renderer/common/Renderer.h (100%) rename Source/compositorclient/{src/Mesa => }/test/client-renderer/common/TerminalInput.h (100%) rename Source/compositorclient/{src/Mesa => }/test/client-renderer/common/TextRender.cpp (100%) rename Source/compositorclient/{src/Mesa => }/test/client-renderer/common/TextRender.h (100%) rename Source/compositorclient/{src/Mesa => }/test/client-renderer/common/TextureBounce.cpp (100%) rename Source/compositorclient/{src/Mesa => }/test/client-renderer/common/TextureBounce.h (100%) rename Source/compositorclient/{src/Mesa => }/test/client-renderer/common/TextureLoader.cpp (100%) rename Source/compositorclient/{src/Mesa => }/test/client-renderer/common/TextureLoader.h (100%) rename Source/compositorclient/{src/Mesa => }/test/client-renderer/common/ml-tv-color-small.png (100%) rename Source/compositorclient/{src/Mesa => }/test/client-renderer/plugin/CMakeLists.txt (54%) rename Source/compositorclient/{src/Mesa => }/test/client-renderer/plugin/ClientCompositorRender.conf.in (52%) rename Source/compositorclient/{src/Mesa => }/test/client-renderer/plugin/ClientCompositorRender.cpp (83%) rename Source/compositorclient/{src/Mesa => }/test/client-renderer/plugin/ClientCompositorRender.h (60%) rename Source/compositorclient/{src/Mesa => }/test/client-renderer/plugin/ClientCompositorRenderImplementation.cpp (87%) rename Source/compositorclient/{src/Mesa => }/test/client-renderer/plugin/Module.cpp (100%) rename Source/compositorclient/{src/Mesa => }/test/client-renderer/plugin/Module.h (100%) rename Source/compositorclient/{src/Mesa => }/test/gbm_buffer_test/CMakeLists.txt (100%) rename Source/compositorclient/{src/Mesa => }/test/gbm_buffer_test/gbm_buffer_test.cpp (100%) 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/test/client-renderer/CMakeLists.txt b/Source/compositorclient/test/client-renderer/CMakeLists.txt similarity index 100% rename from Source/compositorclient/src/Mesa/test/client-renderer/CMakeLists.txt rename to Source/compositorclient/test/client-renderer/CMakeLists.txt diff --git a/Source/compositorclient/src/Mesa/test/client-renderer/app/CMakeLists.txt b/Source/compositorclient/test/client-renderer/app/CMakeLists.txt similarity index 100% rename from Source/compositorclient/src/Mesa/test/client-renderer/app/CMakeLists.txt rename to Source/compositorclient/test/client-renderer/app/CMakeLists.txt diff --git a/Source/compositorclient/src/Mesa/test/client-renderer/app/Main.cpp b/Source/compositorclient/test/client-renderer/app/Main.cpp similarity index 100% rename from Source/compositorclient/src/Mesa/test/client-renderer/app/Main.cpp rename to Source/compositorclient/test/client-renderer/app/Main.cpp diff --git a/Source/compositorclient/src/Mesa/test/client-renderer/app/Module.cpp b/Source/compositorclient/test/client-renderer/app/Module.cpp similarity index 100% rename from Source/compositorclient/src/Mesa/test/client-renderer/app/Module.cpp rename to Source/compositorclient/test/client-renderer/app/Module.cpp diff --git a/Source/compositorclient/src/Mesa/test/client-renderer/app/Module.h b/Source/compositorclient/test/client-renderer/app/Module.h similarity index 100% rename from Source/compositorclient/src/Mesa/test/client-renderer/app/Module.h rename to Source/compositorclient/test/client-renderer/app/Module.h diff --git a/Source/compositorclient/src/Mesa/test/client-renderer/common/CMakeLists.txt b/Source/compositorclient/test/client-renderer/common/CMakeLists.txt similarity index 97% rename from Source/compositorclient/src/Mesa/test/client-renderer/common/CMakeLists.txt rename to Source/compositorclient/test/client-renderer/common/CMakeLists.txt index 2d172512..8efa53c3 100644 --- a/Source/compositorclient/src/Mesa/test/client-renderer/common/CMakeLists.txt +++ b/Source/compositorclient/test/client-renderer/common/CMakeLists.txt @@ -19,8 +19,8 @@ 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(ClientCompositor REQUIRED) find_package(CompileSettingsDebug CONFIG REQUIRED) add_subdirectory(Fonts) diff --git a/Source/compositorclient/src/Mesa/test/client-renderer/common/Fonts/Arial.h b/Source/compositorclient/test/client-renderer/common/Fonts/Arial.h similarity index 100% rename from Source/compositorclient/src/Mesa/test/client-renderer/common/Fonts/Arial.h rename to Source/compositorclient/test/client-renderer/common/Fonts/Arial.h diff --git a/Source/compositorclient/src/Mesa/test/client-renderer/common/Fonts/Arial.png b/Source/compositorclient/test/client-renderer/common/Fonts/Arial.png similarity index 100% rename from Source/compositorclient/src/Mesa/test/client-renderer/common/Fonts/Arial.png rename to Source/compositorclient/test/client-renderer/common/Fonts/Arial.png diff --git a/Source/compositorclient/src/Mesa/test/client-renderer/common/Fonts/CMakeLists.txt b/Source/compositorclient/test/client-renderer/common/Fonts/CMakeLists.txt similarity index 100% rename from Source/compositorclient/src/Mesa/test/client-renderer/common/Fonts/CMakeLists.txt rename to Source/compositorclient/test/client-renderer/common/Fonts/CMakeLists.txt diff --git a/Source/compositorclient/src/Mesa/test/client-renderer/common/Fonts/Font.h b/Source/compositorclient/test/client-renderer/common/Fonts/Font.h similarity index 100% rename from Source/compositorclient/src/Mesa/test/client-renderer/common/Fonts/Font.h rename to Source/compositorclient/test/client-renderer/common/Fonts/Font.h diff --git a/Source/compositorclient/src/Mesa/test/client-renderer/common/IModel.h b/Source/compositorclient/test/client-renderer/common/IModel.h similarity index 100% rename from Source/compositorclient/src/Mesa/test/client-renderer/common/IModel.h rename to Source/compositorclient/test/client-renderer/common/IModel.h diff --git a/Source/compositorclient/src/Mesa/test/client-renderer/common/Module.cpp b/Source/compositorclient/test/client-renderer/common/Module.cpp similarity index 100% rename from Source/compositorclient/src/Mesa/test/client-renderer/common/Module.cpp rename to Source/compositorclient/test/client-renderer/common/Module.cpp diff --git a/Source/compositorclient/src/Mesa/test/client-renderer/common/Module.h b/Source/compositorclient/test/client-renderer/common/Module.h similarity index 100% rename from Source/compositorclient/src/Mesa/test/client-renderer/common/Module.h rename to Source/compositorclient/test/client-renderer/common/Module.h diff --git a/Source/compositorclient/src/Mesa/test/client-renderer/common/Renderer.cpp b/Source/compositorclient/test/client-renderer/common/Renderer.cpp similarity index 95% rename from Source/compositorclient/src/Mesa/test/client-renderer/common/Renderer.cpp rename to Source/compositorclient/test/client-renderer/common/Renderer.cpp index eab936a2..85d62fb3 100644 --- a/Source/compositorclient/src/Mesa/test/client-renderer/common/Renderer.cpp +++ b/Source/compositorclient/test/client-renderer/common/Renderer.cpp @@ -281,9 +281,18 @@ namespace Compositor { _textRender.Draw(ss.str(), 10, 10); } + // Insert fence to track GPU completion + EGLSyncKHR sync = eglCreateSync(_eglDisplay, EGL_SYNC_FENCE_KHR, nullptr); + // Swap buffers eglSwapBuffers(_eglDisplay, _eglSurface); + // Wait for GPU to actually finish rendering + if (sync != EGL_NO_SYNC_KHR) { + eglClientWaitSync(_eglDisplay, sync, EGL_SYNC_FLUSH_COMMANDS_BIT_KHR, 16000000); // 16ms timeout + eglDestroySync(_eglDisplay, sync); + } + _surface->RequestRender(); if (WaitForRendered(1000) == Core::ERROR_TIMEDOUT) { diff --git a/Source/compositorclient/src/Mesa/test/client-renderer/common/Renderer.h b/Source/compositorclient/test/client-renderer/common/Renderer.h similarity index 100% rename from Source/compositorclient/src/Mesa/test/client-renderer/common/Renderer.h rename to Source/compositorclient/test/client-renderer/common/Renderer.h index 044f4fd5..bb8de292 100644 --- a/Source/compositorclient/src/Mesa/test/client-renderer/common/Renderer.h +++ b/Source/compositorclient/test/client-renderer/common/Renderer.h @@ -63,7 +63,7 @@ namespace Compositor { EGLSurface GetEGLSurface() const { return _eglSurface; } bool Configure(const uint16_t width = DefaultWidth, const uint16_t height = DefaultHeight); - + void Start() { if (_models.empty()) { @@ -109,7 +109,7 @@ namespace Compositor { { _showFps = !_showFps; } - + // ICallback void Rendered(Thunder::Compositor::IDisplay::ISurface*) override; void Published(Thunder::Compositor::IDisplay::ISurface*) override; diff --git a/Source/compositorclient/src/Mesa/test/client-renderer/common/TerminalInput.h b/Source/compositorclient/test/client-renderer/common/TerminalInput.h similarity index 100% rename from Source/compositorclient/src/Mesa/test/client-renderer/common/TerminalInput.h rename to Source/compositorclient/test/client-renderer/common/TerminalInput.h diff --git a/Source/compositorclient/src/Mesa/test/client-renderer/common/TextRender.cpp b/Source/compositorclient/test/client-renderer/common/TextRender.cpp similarity index 100% rename from Source/compositorclient/src/Mesa/test/client-renderer/common/TextRender.cpp rename to Source/compositorclient/test/client-renderer/common/TextRender.cpp diff --git a/Source/compositorclient/src/Mesa/test/client-renderer/common/TextRender.h b/Source/compositorclient/test/client-renderer/common/TextRender.h similarity index 100% rename from Source/compositorclient/src/Mesa/test/client-renderer/common/TextRender.h rename to Source/compositorclient/test/client-renderer/common/TextRender.h diff --git a/Source/compositorclient/src/Mesa/test/client-renderer/common/TextureBounce.cpp b/Source/compositorclient/test/client-renderer/common/TextureBounce.cpp similarity index 100% rename from Source/compositorclient/src/Mesa/test/client-renderer/common/TextureBounce.cpp rename to Source/compositorclient/test/client-renderer/common/TextureBounce.cpp diff --git a/Source/compositorclient/src/Mesa/test/client-renderer/common/TextureBounce.h b/Source/compositorclient/test/client-renderer/common/TextureBounce.h similarity index 100% rename from Source/compositorclient/src/Mesa/test/client-renderer/common/TextureBounce.h rename to Source/compositorclient/test/client-renderer/common/TextureBounce.h diff --git a/Source/compositorclient/src/Mesa/test/client-renderer/common/TextureLoader.cpp b/Source/compositorclient/test/client-renderer/common/TextureLoader.cpp similarity index 100% rename from Source/compositorclient/src/Mesa/test/client-renderer/common/TextureLoader.cpp rename to Source/compositorclient/test/client-renderer/common/TextureLoader.cpp diff --git a/Source/compositorclient/src/Mesa/test/client-renderer/common/TextureLoader.h b/Source/compositorclient/test/client-renderer/common/TextureLoader.h similarity index 100% rename from Source/compositorclient/src/Mesa/test/client-renderer/common/TextureLoader.h rename to Source/compositorclient/test/client-renderer/common/TextureLoader.h diff --git a/Source/compositorclient/src/Mesa/test/client-renderer/common/ml-tv-color-small.png b/Source/compositorclient/test/client-renderer/common/ml-tv-color-small.png similarity index 100% rename from Source/compositorclient/src/Mesa/test/client-renderer/common/ml-tv-color-small.png rename to Source/compositorclient/test/client-renderer/common/ml-tv-color-small.png diff --git a/Source/compositorclient/src/Mesa/test/client-renderer/plugin/CMakeLists.txt b/Source/compositorclient/test/client-renderer/plugin/CMakeLists.txt similarity index 54% rename from Source/compositorclient/src/Mesa/test/client-renderer/plugin/CMakeLists.txt rename to Source/compositorclient/test/client-renderer/plugin/CMakeLists.txt index e21f0dff..e5d42aaf 100644 --- a/Source/compositorclient/src/Mesa/test/client-renderer/plugin/CMakeLists.txt +++ b/Source/compositorclient/test/client-renderer/plugin/CMakeLists.txt @@ -51,12 +51,47 @@ if(INSTALL_CLIENT_COMPOSITOR_RENDER_TEST_PLUGIN) install(TARGETS ClientCompositorRender DESTINATION ${CMAKE_INSTALL_LIBDIR}/${STORAGE_DIRECTORY}/plugins COMPONENT ${NAMESPACE}_Runtime) -foreach(Index RANGE 1 ${PLUGIN_COMPOSITORCLIENT_PLUGIN_INSTANCES}) - write_config( - PLUGINS ClientCompositorRender - CLASSNAME ClientCompositorRender - LOCATOR libClientCompositorRender${CMAKE_SHARED_LIBRARY_SUFFIX} - INSTALL_NAME ClientCompositorRender${Index}.json - ) -endforeach() + # 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/src/Mesa/test/client-renderer/plugin/ClientCompositorRender.conf.in b/Source/compositorclient/test/client-renderer/plugin/ClientCompositorRender.conf.in similarity index 52% rename from Source/compositorclient/src/Mesa/test/client-renderer/plugin/ClientCompositorRender.conf.in rename to Source/compositorclient/test/client-renderer/plugin/ClientCompositorRender.conf.in index c35b2577..1652d6f3 100644 --- a/Source/compositorclient/src/Mesa/test/client-renderer/plugin/ClientCompositorRender.conf.in +++ b/Source/compositorclient/test/client-renderer/plugin/ClientCompositorRender.conf.in @@ -10,3 +10,11 @@ if "@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/src/Mesa/test/client-renderer/plugin/ClientCompositorRender.cpp b/Source/compositorclient/test/client-renderer/plugin/ClientCompositorRender.cpp similarity index 83% rename from Source/compositorclient/src/Mesa/test/client-renderer/plugin/ClientCompositorRender.cpp rename to Source/compositorclient/test/client-renderer/plugin/ClientCompositorRender.cpp index 86a17d46..e05a5580 100644 --- a/Source/compositorclient/src/Mesa/test/client-renderer/plugin/ClientCompositorRender.cpp +++ b/Source/compositorclient/test/client-renderer/plugin/ClientCompositorRender.cpp @@ -20,7 +20,6 @@ #include "ClientCompositorRender.h" namespace Thunder { - namespace ClientCompositorRender { extern Exchange::IMemory* MemoryObserver(const RPC::IRemoteConnection* connection); } @@ -73,6 +72,29 @@ namespace Plugin { 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."); } diff --git a/Source/compositorclient/src/Mesa/test/client-renderer/plugin/ClientCompositorRender.h b/Source/compositorclient/test/client-renderer/plugin/ClientCompositorRender.h similarity index 60% rename from Source/compositorclient/src/Mesa/test/client-renderer/plugin/ClientCompositorRender.h rename to Source/compositorclient/test/client-renderer/plugin/ClientCompositorRender.h index 9901fa39..0b9ebf5f 100644 --- a/Source/compositorclient/src/Mesa/test/client-renderer/plugin/ClientCompositorRender.h +++ b/Source/compositorclient/test/client-renderer/plugin/ClientCompositorRender.h @@ -16,7 +16,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - + #pragma once #include "Module.h" @@ -25,7 +25,7 @@ namespace Thunder { namespace Plugin { - class ClientCompositorRender : public PluginHost::IPlugin{ + class ClientCompositorRender : public PluginHost::IPlugin { private: class Notification : public RPC::IRemoteConnection::INotification, public PluginHost::IStateControl::INotification { public: @@ -53,24 +53,83 @@ namespace Plugin { { } - - void StateChange(const PluginHost::IStateControl::state state) 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) + 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) + PUSH_WARNING(DISABLE_WARNING_THIS_IN_MEMBER_INITIALIZER_LIST) ClientCompositorRender() : _connectionId(0) , _service(nullptr) @@ -79,7 +138,7 @@ PUSH_WARNING(DISABLE_WARNING_THIS_IN_MEMBER_INITIALIZER_LIST) , _notification(this) { } -POP_WARNING() + POP_WARNING() ~ClientCompositorRender() override = default; BEGIN_INTERFACE_MAP(ClientCompositorRender) @@ -89,12 +148,13 @@ POP_WARNING() END_INTERFACE_MAP public: - // IPlugin + // 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); diff --git a/Source/compositorclient/src/Mesa/test/client-renderer/plugin/ClientCompositorRenderImplementation.cpp b/Source/compositorclient/test/client-renderer/plugin/ClientCompositorRenderImplementation.cpp similarity index 87% rename from Source/compositorclient/src/Mesa/test/client-renderer/plugin/ClientCompositorRenderImplementation.cpp rename to Source/compositorclient/test/client-renderer/plugin/ClientCompositorRenderImplementation.cpp index 8c790c80..1a21c0cd 100644 --- a/Source/compositorclient/src/Mesa/test/client-renderer/plugin/ClientCompositorRenderImplementation.cpp +++ b/Source/compositorclient/test/client-renderer/plugin/ClientCompositorRenderImplementation.cpp @@ -24,39 +24,11 @@ #include #include +#include "ClientCompositorRender.h" + namespace Thunder { namespace Plugin { class ClientCompositorRenderImplementation : public PluginHost::IStateControl { - private: - class Config : public Core::JSON::Container { - public: - Config(const Config&) = delete; - Config& operator=(const Config&) = delete; - - Config() - : Core::JSON::Container() - , Width(Compositor::Render::DefaultWidth) - , Height(Compositor::Render::DefaultHeight) - , TextAtlas("Arial.png") - , Image("ml-tv-color-small.png") - , ImageCount(40) - { - Add(_T("width"), &Width); - Add(_T("heigth"), &Height); - Add(_T("textatlas"), &TextAtlas); - Add(_T("image"), &Image); - Add(_T("imagecount"), &ImageCount); - } - ~Config() override = default; - - public: - Core::JSON::DecUInt16 Width; - Core::JSON::DecUInt16 Height; - Core::JSON::String TextAtlas; - Core::JSON::String Image; - Core::JSON::DecUInt32 ImageCount; - }; - public: ClientCompositorRenderImplementation(const ClientCompositorRenderImplementation&) = delete; ClientCompositorRenderImplementation& operator=(const ClientCompositorRenderImplementation&) = delete; @@ -81,10 +53,10 @@ namespace Plugin { uint32_t result(Core::ERROR_NONE); ASSERT(service != nullptr); - Config config; + ClientCompositorRender::Config config; config.FromString(service->ConfigLine()); - if (_renderer.Configure(config.Width.Value(), config.Height.Value())) { + 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(); diff --git a/Source/compositorclient/src/Mesa/test/client-renderer/plugin/Module.cpp b/Source/compositorclient/test/client-renderer/plugin/Module.cpp similarity index 100% rename from Source/compositorclient/src/Mesa/test/client-renderer/plugin/Module.cpp rename to Source/compositorclient/test/client-renderer/plugin/Module.cpp diff --git a/Source/compositorclient/src/Mesa/test/client-renderer/plugin/Module.h b/Source/compositorclient/test/client-renderer/plugin/Module.h similarity index 100% rename from Source/compositorclient/src/Mesa/test/client-renderer/plugin/Module.h rename to Source/compositorclient/test/client-renderer/plugin/Module.h diff --git a/Source/compositorclient/src/Mesa/test/gbm_buffer_test/CMakeLists.txt b/Source/compositorclient/test/gbm_buffer_test/CMakeLists.txt similarity index 100% rename from Source/compositorclient/src/Mesa/test/gbm_buffer_test/CMakeLists.txt rename to Source/compositorclient/test/gbm_buffer_test/CMakeLists.txt diff --git a/Source/compositorclient/src/Mesa/test/gbm_buffer_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/gbm_buffer_test.cpp rename to Source/compositorclient/test/gbm_buffer_test/gbm_buffer_test.cpp From 6a5a199f5e29f013ea21f22665c666d076d7881d Mon Sep 17 00:00:00 2001 From: Bram Oosterhuis Date: Wed, 22 Oct 2025 14:28:18 +0200 Subject: [PATCH 05/22] CompositorClient: move existing test to subdir --- Source/compositorclient/CMakeLists.txt | 4 -- .../src/Mesa/test/CMakeLists.txt | 28 -------------- Source/compositorclient/test/CMakeLists.txt | 34 ++++++++--------- .../test/legacy-test/CMakeLists.txt | 38 +++++++++++++++++++ .../test/{ => legacy-test}/main.cpp | 6 +-- 5 files changed, 56 insertions(+), 54 deletions(-) delete mode 100644 Source/compositorclient/src/Mesa/test/CMakeLists.txt create mode 100644 Source/compositorclient/test/legacy-test/CMakeLists.txt rename Source/compositorclient/test/{ => legacy-test}/main.cpp (100%) diff --git a/Source/compositorclient/CMakeLists.txt b/Source/compositorclient/CMakeLists.txt index 22b5580e..3c95d3a3 100644 --- a/Source/compositorclient/CMakeLists.txt +++ b/Source/compositorclient/CMakeLists.txt @@ -27,8 +27,4 @@ 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() diff --git a/Source/compositorclient/src/Mesa/test/CMakeLists.txt b/Source/compositorclient/src/Mesa/test/CMakeLists.txt deleted file mode 100644 index 48a323af..00000000 --- a/Source/compositorclient/src/Mesa/test/CMakeLists.txt +++ /dev/null @@ -1,28 +0,0 @@ - -# 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(BUILD_CLIENT_COMPOSITOR_GBM_UTIL "Build the GBM basic test" ON) -option(BUILD_CLIENT_COMPOSITOR_RENDER_TEST "Build the renderer compositor client test" ON) - -if(BUILD_CLIENT_COMPOSITOR_GBM_UTIL) -add_subdirectory(gbm_buffer_test) -endif() - -if(BUILD_CLIENT_COMPOSITOR_RENDER_TEST) -add_subdirectory(client-renderer) -endif() \ No newline at end of file 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/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; From 5a0f8f5c163a2aabc14a6ba771a9a00711649a2c Mon Sep 17 00:00:00 2001 From: Bram Oosterhuis Date: Wed, 22 Oct 2025 14:33:46 +0200 Subject: [PATCH 06/22] CompositorClient: Simplify project definition --- Source/compositorclient/CMakeLists.txt | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/Source/compositorclient/CMakeLists.txt b/Source/compositorclient/CMakeLists.txt index 3c95d3a3..404b9e88 100644 --- a/Source/compositorclient/CMakeLists.txt +++ b/Source/compositorclient/CMakeLists.txt @@ -19,12 +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) - add_subdirectory(test) +add_subdirectory(test) From 6d1cac90203a8d6d865049f82aef8b33ae8842fc Mon Sep 17 00:00:00 2001 From: Bram Oosterhuis Date: Wed, 22 Oct 2025 15:42:36 +0200 Subject: [PATCH 07/22] CompositorClient: Fix link error --- .../compositorclient/test/client-renderer/common/CMakeLists.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/Source/compositorclient/test/client-renderer/common/CMakeLists.txt b/Source/compositorclient/test/client-renderer/common/CMakeLists.txt index 8efa53c3..40f29770 100644 --- a/Source/compositorclient/test/client-renderer/common/CMakeLists.txt +++ b/Source/compositorclient/test/client-renderer/common/CMakeLists.txt @@ -38,6 +38,7 @@ target_link_libraries(ClientCompositorRenderCommon ${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 From 390bb267669b3713852d7fa632f85f15efd7688a Mon Sep 17 00:00:00 2001 From: Bram Oosterhuis Date: Fri, 24 Oct 2025 15:59:38 +0200 Subject: [PATCH 08/22] CompositorClient: Remove lock and rely on GBM's internal locking --- .../src/Mesa/Implementation.cpp | 26 +------------------ 1 file changed, 1 insertion(+), 25 deletions(-) diff --git a/Source/compositorclient/src/Mesa/Implementation.cpp b/Source/compositorclient/src/Mesa/Implementation.cpp index 1dd12de1..e09f9e26 100644 --- a/Source/compositorclient/src/Mesa/Implementation.cpp +++ b/Source/compositorclient/src/Mesa/Implementation.cpp @@ -219,7 +219,6 @@ namespace Linux { , _id(_remoteClient->Native()) , _width(width) , _height(height) - , _gbmBufferLock() , _name(name) , _keyboard(nullptr) , _wheel(nullptr) @@ -273,36 +272,14 @@ namespace Linux { private: gbm_bo* Lock(const uint16_t ms) { - ASSERT((_gbmSurface != nullptr) && "Failed to lock a framebuffer, surface is null"); - - gbm_bo* frameBuffer = nullptr; - - if (_gbmBufferLock.try_lock_for(std::chrono::milliseconds(ms))) { - frameBuffer = gbm_surface_lock_front_buffer(_gbmSurface); - - 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)); - } - } else { - TRACE(Trace::Error, (_T("Failed to lock front buffer within %d ms"), ms)); - } - - return frameBuffer; + return (_gbmSurface != nullptr) ? gbm_surface_lock_front_buffer(_gbmSurface) : nullptr; } void Unlock(gbm_bo* frameBuffer) { - ASSERT((_gbmSurface != nullptr) && "Failed to release framebuffer, surface is null"); - if ((_gbmSurface != nullptr) && (frameBuffer != nullptr)) { gbm_surface_release_buffer(_gbmSurface, frameBuffer); - TRACE(Trace::Information, (_T("Released framebuffer[%p]"), frameBuffer)); } - - _gbmBufferLock.unlock(); } public: @@ -460,7 +437,6 @@ 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; From 62589f17b736002b363cc110a1ddf46832b47ff6 Mon Sep 17 00:00:00 2001 From: Bram Oosterhuis Date: Thu, 30 Oct 2025 10:40:23 +0100 Subject: [PATCH 09/22] CompositorClient: shutdown the plugin if Graphics subsystem is going down --- .../test/client-renderer/plugin/ClientCompositorRender.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/compositorclient/test/client-renderer/plugin/ClientCompositorRender.cpp b/Source/compositorclient/test/client-renderer/plugin/ClientCompositorRender.cpp index e05a5580..c14a1cc9 100644 --- a/Source/compositorclient/test/client-renderer/plugin/ClientCompositorRender.cpp +++ b/Source/compositorclient/test/client-renderer/plugin/ClientCompositorRender.cpp @@ -32,7 +32,7 @@ namespace Plugin { // Preconditions { subsystem::GRAPHICS }, // Terminations - {}, + { subsystem::NOT_GRAPHICS }, // Controls {}); } From 7fa9950e147c635ed43e211d3e359337a432f0e3 Mon Sep 17 00:00:00 2001 From: Bram Oosterhuis Date: Thu, 30 Oct 2025 10:46:32 +0100 Subject: [PATCH 10/22] CompositorClient: allow to skip parts of the render chain --- .../test/client-renderer/app/CMakeLists.txt | 2 + .../test/client-renderer/app/Main.cpp | 41 ++++++++++++- .../test/client-renderer/common/Renderer.cpp | 61 ++++++++++--------- .../test/client-renderer/common/Renderer.h | 28 ++++++++- 4 files changed, 97 insertions(+), 35 deletions(-) diff --git a/Source/compositorclient/test/client-renderer/app/CMakeLists.txt b/Source/compositorclient/test/client-renderer/app/CMakeLists.txt index 468e632c..5e018063 100644 --- a/Source/compositorclient/test/client-renderer/app/CMakeLists.txt +++ b/Source/compositorclient/test/client-renderer/app/CMakeLists.txt @@ -18,6 +18,7 @@ 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 @@ -29,6 +30,7 @@ target_link_libraries(client-compositor-render PRIVATE ${NAMESPACE}Core::${NAMESPACE}Core ${NAMESPACE}Messaging::${NAMESPACE}Messaging + ${NAMESPACE}LocalTracer::${NAMESPACE}LocalTracer CompileSettingsDebug::CompileSettingsDebug ClientCompositorRenderCommon ) diff --git a/Source/compositorclient/test/client-renderer/app/Main.cpp b/Source/compositorclient/test/client-renderer/app/Main.cpp index 725fb242..f83dcc0e 100644 --- a/Source/compositorclient/test/client-renderer/app/Main.cpp +++ b/Source/compositorclient/test/client-renderer/app/Main.cpp @@ -17,7 +17,7 @@ * limitations under the License. */ - #include "Module.h" +#include "Module.h" #include "TextureBounce.h" #include "IModel.h" @@ -26,12 +26,16 @@ #include +#include + #include #include using namespace Thunder; -static const char Namespace[] = EXPAND_AND_QUOTE(NAMESPACE); +namespace { +const char Namespace[] = EXPAND_AND_QUOTE(NAMESPACE); +} class ConsoleOptions : public Thunder::Core::Options { public: @@ -75,6 +79,23 @@ class ConsoleOptions : public Thunder::Core::Options { int main(int argc, char* argv[]) { + Messaging::LocalTracer& tracer = Messaging::LocalTracer::Open(); + + const std::map> modules = { + { "App_CompositionClientRender", { "" } }, + { "Common_CompositionClientRender", { "" } }, + { "CompositorBuffer", { "Error", "Information" } }, + { "CompositorBackend", { "Error" } }, + { "CompositorRenderer", { "Error", "Warning", "Information" } }, + { "DRMCommon", { "Error", "Warning", "Information" } } + }; + + for (const auto& module_entry : modules) { + for (const auto& category : module_entry.second) { + tracer.EnableMessage(module_entry.first, category, true); + } + } + const char* executableName(Thunder::Core::FileNameOnly(argv[0])); ConsoleOptions options(argc, argv); bool quitApp(false); @@ -115,6 +136,8 @@ int main(int argc, char* argv[]) renderer.Start(); + bool result; + if (keyboard.IsValid() == true) { while (!renderer.ShouldExit() && !quitApp) { switch (toupper(keyboard.Read())) { @@ -124,7 +147,19 @@ int main(int argc, char* argv[]) } break; case 'F': - renderer.ToggleFPS(); + result = renderer.ToggleFPS(); + printf("%d FPS: %s\n", __LINE__, result ? "off" : "on"); + break; + case 'Z': + result = renderer.ToggleRequestRender(); + printf("%d RequestRender: %s\n", __LINE__, result ? "off" : "on"); + break; + case 'R': + renderer.TriggerRender(); + break; + case 'M': + result = renderer.ToggleModelRender(); + printf("%d Model Render: %s\n", __LINE__, result ? "off" : "on"); break; case 'Q': quitApp = true; diff --git a/Source/compositorclient/test/client-renderer/common/Renderer.cpp b/Source/compositorclient/test/client-renderer/common/Renderer.cpp index 85d62fb3..4c192582 100644 --- a/Source/compositorclient/test/client-renderer/common/Renderer.cpp +++ b/Source/compositorclient/test/client-renderer/common/Renderer.cpp @@ -46,6 +46,8 @@ namespace Compositor { , _rendering() , _renderSync() , _showFps(true) + , _skipRender(false) + , _skipModel(false) , _models() , _selectedModel(~0) , _rng(static_cast(std::chrono::steady_clock::now().time_since_epoch().count())) @@ -258,49 +260,48 @@ namespace Compositor { void Render::Draw() { + // Make context current for this render thread + if (!eglMakeCurrent(_eglDisplay, _eglSurface, _eglSurface, _eglContext)) { + EGLint error = eglGetError(); + fprintf(stderr, "Draw: eglMakeCurrent failed: 0x%x\n", error); + return; + } + while (_running.load() && !ShouldExit()) { if (_models.empty() || _selectedModel >= _models.size()) { std::this_thread::sleep_for(std::chrono::milliseconds(100)); continue; } - // Make context current for this render thread - if (!eglMakeCurrent(_eglDisplay, _eglSurface, _eglSurface, _eglContext)) { - EGLint error = eglGetError(); - fprintf(stderr, "Draw: eglMakeCurrent failed: 0x%x\n", error); - break; - } - - 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); - } - - // Insert fence to track GPU completion - EGLSyncKHR sync = eglCreateSync(_eglDisplay, EGL_SYNC_FENCE_KHR, nullptr); - - // Swap buffers - eglSwapBuffers(_eglDisplay, _eglSurface); - - // Wait for GPU to actually finish rendering - if (sync != EGL_NO_SYNC_KHR) { - eglClientWaitSync(_eglDisplay, sync, EGL_SYNC_FLUSH_COMMANDS_BIT_KHR, 16000000); // 16ms timeout - eglDestroySync(_eglDisplay, sync); + 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 + eglSwapBuffers(_eglDisplay, _eglSurface); + } else { + TRACE(Trace::Warning, ("Model draw failed")); + std::this_thread::sleep_for(std::chrono::milliseconds(4)); } + } + if (_skipRender == false) { _surface->RequestRender(); - if (WaitForRendered(1000) == Core::ERROR_TIMEDOUT) { + // allow for for 2 25FPS frame delay + if (WaitForRendered(80) == Core::ERROR_TIMEDOUT) { + printf("%d @[%" PRIu64 "] BRAM Render Timeout\n", __LINE__, Core::Time::Now().Ticks()); TRACE(Trace::Warning, ("Timed out waiting for rendered callback")); } } else { - TRACE(Trace::Warning, ("Model draw failed")); - std::this_thread::sleep_for(std::chrono::milliseconds(100)); + std::this_thread::sleep_for(std::chrono::milliseconds(16)); } } diff --git a/Source/compositorclient/test/client-renderer/common/Renderer.h b/Source/compositorclient/test/client-renderer/common/Renderer.h index bb8de292..636f1c51 100644 --- a/Source/compositorclient/test/client-renderer/common/Renderer.h +++ b/Source/compositorclient/test/client-renderer/common/Renderer.h @@ -105,11 +105,33 @@ namespace Compositor { { return _exitRequested.load(); } - void ToggleFPS() + 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; @@ -147,6 +169,8 @@ namespace Compositor { std::condition_variable _renderSync; bool _showFps; + bool _skipRender; + bool _skipModel; std::vector _models; std::atomic _selectedModel; From 244444fcac29d261942d7a0128d45ffb20523204 Mon Sep 17 00:00:00 2001 From: Bram Oosterhuis Date: Thu, 30 Oct 2025 10:47:37 +0100 Subject: [PATCH 11/22] CompositorClient: Enhance ContentBuffer handling with atomic operations and timeout handling --- .../src/Mesa/Implementation.cpp | 264 +++++++++++++++--- 1 file changed, 227 insertions(+), 37 deletions(-) diff --git a/Source/compositorclient/src/Mesa/Implementation.cpp b/Source/compositorclient/src/Mesa/Implementation.cpp index e09f9e26..b439d844 100644 --- a/Source/compositorclient/src/Mesa/Implementation.cpp +++ b/Source/compositorclient/src/Mesa/Implementation.cpp @@ -93,9 +93,7 @@ namespace Linux { const char* backendName = GetGbmBackendName(gbmDevice); return (backendName != nullptr) && (strcmp(backendName, name) == 0); } - } - class Display : public Compositor::IDisplay { public: Display() = delete; @@ -121,6 +119,8 @@ namespace Linux { class SurfaceImplementation : public Compositor::IDisplay::ISurface { private: + static constexpr size_t MaxContentBuffers = 4; + // 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>; @@ -136,12 +136,13 @@ namespace Linux { : 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) + , _requestedAt(0) { ASSERT(_bo != nullptr); if (_bo != nullptr) { // for now only single plane buffers are supported - ASSERT(gbm_bo_get_plane_count(_bo) == 1); + ASSERT(gbm_bo_get_plane_count(_bo) == 1); Add(gbm_bo_get_fd_for_plane(_bo, 0), gbm_bo_get_stride_for_plane(_bo, 0), @@ -155,7 +156,7 @@ 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) { @@ -172,16 +173,17 @@ 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)); + TRACE_GLOBAL(Trace::Information, (_T("ContentBuffer[%p] Destroyed callback from GBM"), 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)); @@ -191,8 +193,21 @@ namespace Linux { protected: void Rendered() override { - _parent.Unlock(_bo); - _parent.Rendered(); + // Try to release - atomic get-and-clear + uint64_t requested = _requestedAt.exchange(0, std::memory_order_acq_rel); + + if (requested > 0) { + // We cleared it, so we do the release + // uint64_t age = Core::Time::Now().Ticks() - requested; + + // printf("%d @[%" PRIu64 "] BRAM Rendered on ContentBuffer=%p, frameBuffer=%p (age: %" PRIu64 " µs)\n", + // __LINE__, Core::Time::Now().Ticks(), (void*)this, (void*)_bo, age); + + _parent.Rendered(_bo); + } else { + // Already released (timeout beat us, or double-call bug) + TRACE(Trace::Warning, (_T("ContentBuffer %p already released (was %" PRIu64 ")"), this, requested)); + } } void Published() override @@ -200,9 +215,74 @@ namespace Linux { _parent.Published(); } + public: + bool RequestRender() + { + uint64_t expected = 0; + uint64_t desired = Core::Time::Now().Ticks(); + + // Atomic compare-and-swap: only set if currently 0 + if (_requestedAt.compare_exchange_strong(expected, desired, std::memory_order_acq_rel)) { + // Success - buffer was unlocked, now locked with timestamp + // printf("%d @[%" PRIu64 "] BRAM Request Render on ContentBuffer=%p, frameBuffer=%p\n", + // __LINE__, Core::Time::Now().Ticks(), (void*)this, (void*)_bo); + + BaseClass::RequestRender(); + return true; + } else { + // Failed - buffer still locked + uint64_t now = Core::Time::Now().Ticks(); + uint64_t age = (expected > 0 && now > expected) ? (now - expected) : 0; + + TRACE(Trace::Error, (_T("ContentBuffer %p still locked for %" PRIu64 " µs (requested at %" PRIu64 ")"), this, age, expected)); + + return false; + } + } + + bool IsStuck(uint64_t now, uint64_t timeoutUs) const + { + uint64_t requested = _requestedAt.load(std::memory_order_acquire); + + if (requested > 0) { + return (now - requested) > timeoutUs; + } + + return false; + } + + bool ForceRelease() + { + // Atomic check-and-clear + uint64_t requested = _requestedAt.exchange(0, std::memory_order_acq_rel); + + if (requested > 0) { + uint64_t age = Core::Time::Now().Ticks() - requested; + + TRACE(Trace::Error, (_T("ContentBuffer %p TIMEOUT %" PRIu64 " µs, force releasing!"), this, age)); + + _parent.Rendered(_bo); // Force release + return true; + } + + return false; // Already released + } + + gbm_bo* Bo() const { return _bo; } + + uint64_t Age() const + { + uint64_t requested = _requestedAt.load(std::memory_order_acquire); + if (requested > 0) { + return Core::Time::Now().Ticks() - requested; + } + return 0; + } + private: SurfaceImplementation& _parent; gbm_bo* _bo; + std::atomic _requestedAt; }; public: @@ -225,7 +305,10 @@ namespace Linux { , _pointer(nullptr) , _touchpanel(nullptr) , _callback(callback) + , _contentBuffers() + , _bufferLock() { + _contentBuffers.fill(nullptr); _display.AddRef(); ASSERT(_remoteClient != nullptr); @@ -255,33 +338,76 @@ namespace Linux { _touchpanel->Release(); } + // Prevent new RequestRender() calls from allocating new buffers. + gbm_surface* surface = _gbmSurface; + _gbmSurface = nullptr; + + { + Core::SafeSyncType lock(_bufferLock); + + 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); + + // Explicitly delete the ContentBuffer + delete _contentBuffers[i]; + _contentBuffers[i] = nullptr; + } + } + + TRACE(Trace::Information, (_T("Cleaned up all ContentBuffers for surface %s"), _name.c_str())); + } + // Cleanup the remote client buffers if (_remoteClient != nullptr) { _remoteClient->Release(); } - // 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; + if (surface != nullptr) { + gbm_surface_destroy(surface); } _display.Release(); } private: - gbm_bo* Lock(const uint16_t ms) + void CheckStuckBuffers() { - return (_gbmSurface != nullptr) ? gbm_surface_lock_front_buffer(_gbmSurface) : nullptr; - } + const uint64_t TIMEOUT_TICKS = 200000; // 200ms + uint64_t now = Core::Time::Now().Ticks(); - void Unlock(gbm_bo* frameBuffer) - { - if ((_gbmSurface != nullptr) && (frameBuffer != nullptr)) { - gbm_surface_release_buffer(_gbmSurface, frameBuffer); + for (size_t i = 0; i < MaxContentBuffers; i++) { + ContentBuffer* buffer = _contentBuffers[i]; + + if (buffer != nullptr && buffer->IsStuck(now, TIMEOUT_TICKS)) { + // Buffer is stuck - force release it + buffer->ForceRelease(); + } } } + // void PrintBufferStatus() + // { + // printf("Buffer Status:\n"); + + // for (size_t i = 0; i < MaxContentBuffers; i++) { + // ContentBuffer* buffer = _contentBuffers[i]; + + // if (buffer != nullptr) { + // uint64_t age = buffer->Age(); + + // if (age > 0) { + // printf(" Slot %zu: LOCKED for %llu µs\n", i, age); + // } else { + // printf(" Slot %zu: FREE\n", i); + // } + // } else { + // printf(" Slot %zu: EMPTY\n", i); + // } + // } + // } + public: EGLNativeWindowType Native() const override { @@ -390,8 +516,12 @@ namespace Linux { } } - void Rendered() + void Rendered(gbm_bo* frameBuffer = nullptr) { + if ((_gbmSurface != nullptr) && (frameBuffer != nullptr)) { + gbm_surface_release_buffer(_gbmSurface, frameBuffer); + } + if (_callback != nullptr) { _callback->Rendered(this); } @@ -404,25 +534,82 @@ namespace Linux { } } - void RequestRender() override + void RemoveContentBuffer(ContentBuffer* buffer) { - gbm_bo* frameBuffer = Lock(1000); + Core::SafeSyncType lock(_bufferLock); + for (size_t i = 0; i < MaxContentBuffers; i++) { + if (_contentBuffers[i] == buffer) { + _contentBuffers[i] = nullptr; + TRACE(Trace::Information, (_T("Removed ContentBuffer[%p] from slot %zu for surface %s"), buffer, i, _name.c_str())); + break; + } + } + } + + void RequestRender() + { + CheckStuckBuffers(); + + if (_gbmSurface == nullptr) { + Rendered(); + return; + } + + gbm_bo* frameBuffer = gbm_surface_lock_front_buffer(_gbmSurface); + + // bail-out if we cannot lock the front buffer 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)); + Rendered(); + return; + } + + // Fast path: Buffer exists + ContentBuffer* buffer = static_cast(gbm_bo_get_user_data(frameBuffer)); - if (buffer == nullptr) { - buffer = new ContentBuffer(*this, frameBuffer); - gbm_bo_set_user_data(frameBuffer, buffer, &ContentBuffer::Destroyed); + if (buffer == nullptr) { + Core::SafeSyncType lock(_bufferLock); + + // Double-check pattern: another thread might have created it + buffer = static_cast(gbm_bo_get_user_data(frameBuffer)); + if (buffer != nullptr) { + // printf("%d @[%" PRIu64 "] BRAM Request Render on ContentBuffer=%p, frameBuffer=%p\n", __LINE__, Core::Time::Now().Ticks(), (void*)buffer, (void*)frameBuffer); + buffer->RequestRender(); + return; } - ASSERT(buffer != nullptr); - buffer->RequestRender(); + // Find empty slot in array + size_t slot = MaxContentBuffers; + for (size_t i = 0; i < MaxContentBuffers; i++) { + if (_contentBuffers[i] == nullptr) { + slot = i; + break; + } + } + + if (slot == MaxContentBuffers) { + // Pool exhausted - this should never happen with MaxContentBuffers=4 + TRACE(Trace::Error, (_T("ContentBuffer pool exhausted for surface %s! All %zu slots full."), _name.c_str(), MaxContentBuffers)); + TRACE(Trace::Error, (_T("This driver may use more buffers than expected. Consider increasing MaxContentBuffers."))); + + // Graceful degradation: still call Rendered to keep render loop going + Rendered(frameBuffer); + return; + } + + // Create new ContentBuffer + buffer = new ContentBuffer(*this, frameBuffer); + _contentBuffers[slot] = buffer; + + // Set as user data so GBM can call our Destroyed callback + gbm_bo_set_user_data(frameBuffer, buffer, &ContentBuffer::Destroyed); + + TRACE(Trace::Information, (_T("Created ContentBuffer[%p] for surface %s in slot %zu"), buffer, _name.c_str(), slot)); } + + ASSERT(buffer != nullptr); + // printf("%d @[%" PRIu64 "] BRAM Request Render on ContentBuffer=%p, frameBuffer=%p\n", __LINE__, Core::Time::Now().Ticks(), (void*)buffer, (void*)frameBuffer); + buffer->RequestRender(); } uint32_t Process() @@ -443,6 +630,8 @@ namespace Linux { IPointer* _pointer; ITouchPanel* _touchpanel; ISurface::ICallback* _callback; + std::array _contentBuffers; + Core::CriticalSection _bufferLock; static uint32_t _surfaceIndex; }; // class SurfaceImplementation @@ -551,7 +740,8 @@ namespace Linux { return result; } - bool IsValid() const { + bool IsValid() const + { return _remoteDisplay != nullptr; } @@ -669,7 +859,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); @@ -877,13 +1067,13 @@ namespace Linux { Compositor::IDisplay* Compositor::IDisplay::Instance(const string& displayName) { - Compositor::IDisplay* result(nullptr); + Compositor::IDisplay* result(nullptr); Linux::Display& display = Linux::Display::Instance(displayName); - if (display.IsValid() == false){ + if (display.IsValid() == false) { display.Release(); - } else{ + } else { result = &(display); } From 7e94583798d737d7a410ed88cb7eba4daa5e8f89 Mon Sep 17 00:00:00 2001 From: Bram Oosterhuis Date: Thu, 13 Nov 2025 17:22:13 +0100 Subject: [PATCH 12/22] CompositorClient: Remove unused RenderAPI header and clean up includes --- .../src/Mesa/Implementation.cpp | 4 - Source/compositorclient/src/Mesa/RenderAPI.h | 356 ------------------ 2 files changed, 360 deletions(-) delete mode 100644 Source/compositorclient/src/Mesa/RenderAPI.h diff --git a/Source/compositorclient/src/Mesa/Implementation.cpp b/Source/compositorclient/src/Mesa/Implementation.cpp index b439d844..c6a33ef2 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,7 +36,6 @@ extern "C" { #include #include -#include "RenderAPI.h" #include #include 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 From 368812c48f8184d56abacd7d052e4cf1f287b01d Mon Sep 17 00:00:00 2001 From: Bram Oosterhuis Date: Fri, 14 Nov 2025 15:17:32 +0100 Subject: [PATCH 13/22] CompositorClient: Add GPU fence synchronization with timeout handling in rendering --- .../test/client-renderer/common/Renderer.cpp | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/Source/compositorclient/test/client-renderer/common/Renderer.cpp b/Source/compositorclient/test/client-renderer/common/Renderer.cpp index 4c192582..52e3ac28 100644 --- a/Source/compositorclient/test/client-renderer/common/Renderer.cpp +++ b/Source/compositorclient/test/client-renderer/common/Renderer.cpp @@ -286,6 +286,17 @@ namespace Compositor { // Swap buffers eglSwapBuffers(_eglDisplay, _eglSurface); + + 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)); From a446c08ffdaea3554a35682fc0a2ab17d9ed9da1 Mon Sep 17 00:00:00 2001 From: Bram Oosterhuis Date: Thu, 11 Dec 2025 20:46:18 +0100 Subject: [PATCH 14/22] GraphicsBuffer: Handle client disconnection gracefully --- .../include/graphicsbuffer/GraphicsBufferType.h | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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) { From e093d3ddfc5a444af4d7a42a3bccaabacfd033df Mon Sep 17 00:00:00 2001 From: Bram Oosterhuis Date: Fri, 12 Dec 2025 15:28:20 +0100 Subject: [PATCH 15/22] CompositorClient: Small improvements --- .../src/Mesa/Implementation.cpp | 40 +++++++++---------- .../client-renderer/common/CMakeLists.txt | 2 + 2 files changed, 22 insertions(+), 20 deletions(-) diff --git a/Source/compositorclient/src/Mesa/Implementation.cpp b/Source/compositorclient/src/Mesa/Implementation.cpp index c6a33ef2..56d9dae0 100644 --- a/Source/compositorclient/src/Mesa/Implementation.cpp +++ b/Source/compositorclient/src/Mesa/Implementation.cpp @@ -39,6 +39,8 @@ extern "C" { #include #include +#include +#include namespace Thunder { namespace Linux { @@ -73,21 +75,20 @@ 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); } } class Display : public Compositor::IDisplay { @@ -310,7 +311,7 @@ namespace Linux { 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); } @@ -351,8 +352,6 @@ namespace Linux { _contentBuffers[i] = nullptr; } } - - TRACE(Trace::Information, (_T("Cleaned up all ContentBuffers for surface %s"), _name.c_str())); } // Cleanup the remote client buffers @@ -868,19 +867,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; @@ -944,7 +944,7 @@ namespace Linux { _gbmDevice = nullptr; } - if (_gpuId > 0) { + if (_gpuId >= 0) { ::close(_gpuId); _gpuId = -1; } diff --git a/Source/compositorclient/test/client-renderer/common/CMakeLists.txt b/Source/compositorclient/test/client-renderer/common/CMakeLists.txt index 40f29770..338b31da 100644 --- a/Source/compositorclient/test/client-renderer/common/CMakeLists.txt +++ b/Source/compositorclient/test/client-renderer/common/CMakeLists.txt @@ -33,6 +33,8 @@ add_library(ClientCompositorRenderCommon STATIC TextRender.cpp ) +set_target_properties(ClientCompositorRenderCommon PROPERTIES POSITION_INDEPENDENT_CODE ON) + target_link_libraries(ClientCompositorRenderCommon PUBLIC ${NAMESPACE}Core::${NAMESPACE}Core From 828451cd78d5ba2f43957fc92413ba7e607432e5 Mon Sep 17 00:00:00 2001 From: Bram Oosterhuis Date: Tue, 23 Dec 2025 17:30:21 +0100 Subject: [PATCH 16/22] CompositorClient: Adding a triple buffer setup to prevent gbm deadlocks. --- .../src/Mesa/Implementation.cpp | 480 +++++++++++------- 1 file changed, 286 insertions(+), 194 deletions(-) diff --git a/Source/compositorclient/src/Mesa/Implementation.cpp b/Source/compositorclient/src/Mesa/Implementation.cpp index 56d9dae0..80e0308d 100644 --- a/Source/compositorclient/src/Mesa/Implementation.cpp +++ b/Source/compositorclient/src/Mesa/Implementation.cpp @@ -118,32 +118,57 @@ namespace Linux { 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) - , _requestedAt(0) + , _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(gbm_bo_get_fd_for_plane(_bo, 0), - gbm_bo_get_stride_for_plane(_bo, 0), - gbm_bo_get_offset(_bo, 0)); + Add(fd, gbm_bo_get_stride_for_plane(_bo, i), gbm_bo_get_offset(_bo, i)); + + if (fd >= 0) { + ::close(fd); // safe, since Add() dup()'d it + } + } std::array descriptors; descriptors.fill(-1); @@ -156,10 +181,10 @@ namespace Linux { 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"))); } } } @@ -174,112 +199,106 @@ namespace Linux { static void Destroyed(gbm_bo* bo, void* data) { - ASSERT(data != nullptr); ContentBuffer* buffer = static_cast(data); - - TRACE_GLOBAL(Trace::Information, (_T("ContentBuffer[%p] Destroyed callback from GBM"), 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)); } } - protected: - void Rendered() override - { - // Try to release - atomic get-and-clear - uint64_t requested = _requestedAt.exchange(0, std::memory_order_acq_rel); - - if (requested > 0) { - // We cleared it, so we do the release - // uint64_t age = Core::Time::Now().Ticks() - requested; - - // printf("%d @[%" PRIu64 "] BRAM Rendered on ContentBuffer=%p, frameBuffer=%p (age: %" PRIu64 " µs)\n", - // __LINE__, Core::Time::Now().Ticks(), (void*)this, (void*)_bo, age); + gbm_bo* Bo() const { return _bo; } - _parent.Rendered(_bo); - } else { - // Already released (timeout beat us, or double-call bug) - TRACE(Trace::Warning, (_T("ContentBuffer %p already released (was %" PRIu64 ")"), this, requested)); - } + BufferState State() const + { + return _state.load(std::memory_order_acquire); } - void Published() override + // FREE → STAGED (after client locks front buffer) + bool Stage() { - _parent.Published(); + 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; } - public: - bool RequestRender() + // STAGED → PENDING (submit to compositor) + bool Submit() { - uint64_t expected = 0; - uint64_t desired = Core::Time::Now().Ticks(); - - // Atomic compare-and-swap: only set if currently 0 - if (_requestedAt.compare_exchange_strong(expected, desired, std::memory_order_acq_rel)) { - // Success - buffer was unlocked, now locked with timestamp - // printf("%d @[%" PRIu64 "] BRAM Request Render on ContentBuffer=%p, frameBuffer=%p\n", - // __LINE__, Core::Time::Now().Ticks(), (void*)this, (void*)_bo); - + BufferState expected = BufferState::STAGED; + if (_state.compare_exchange_strong(expected, BufferState::PENDING, + std::memory_order_acq_rel)) { BaseClass::RequestRender(); return true; - } else { - // Failed - buffer still locked - uint64_t now = Core::Time::Now().Ticks(); - uint64_t age = (expected > 0 && now > expected) ? (now - expected) : 0; - - TRACE(Trace::Error, (_T("ContentBuffer %p still locked for %" PRIu64 " µs (requested at %" PRIu64 ")"), this, age, expected)); - - return false; } + TRACE(Trace::Error, + (_T("Buffer %p: Submit failed (expected STAGED, got %s)"), + _bo, StateToString(expected))); + return false; } - bool IsStuck(uint64_t now, uint64_t timeoutUs) const + // PENDING → ACTIVE (compositor GPU done) + bool Activate() { - uint64_t requested = _requestedAt.load(std::memory_order_acquire); - - if (requested > 0) { - return (now - requested) > timeoutUs; + 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; } - bool ForceRelease() + // ACTIVE → RETIRED (new buffer became active) + bool Retire() { - // Atomic check-and-clear - uint64_t requested = _requestedAt.exchange(0, std::memory_order_acq_rel); - - if (requested > 0) { - uint64_t age = Core::Time::Now().Ticks() - requested; - - TRACE(Trace::Error, (_T("ContentBuffer %p TIMEOUT %" PRIu64 " µs, force releasing!"), this, age)); - - _parent.Rendered(_bo); // Force release + 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; + } - return false; // Already released + // 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; } - gbm_bo* Bo() const { return _bo; } + protected: + void Rendered() override + { + _parent.OnBufferRendered(this); + } - uint64_t Age() const + void Published() override { - uint64_t requested = _requestedAt.load(std::memory_order_acquire); - if (requested > 0) { - return Core::Time::Now().Ticks() - requested; - } - return 0; + _parent.OnBufferPublished(this); } private: SurfaceImplementation& _parent; gbm_bo* _bo; - std::atomic _requestedAt; + std::atomic _state; }; public: @@ -289,7 +308,9 @@ 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)) @@ -304,6 +325,8 @@ namespace Linux { , _callback(callback) , _contentBuffers() , _bufferLock() + , _activeBuffer(nullptr) + , _retiredBuffer(nullptr) { _contentBuffers.fill(nullptr); _display.AddRef(); @@ -366,43 +389,6 @@ namespace Linux { _display.Release(); } - private: - void CheckStuckBuffers() - { - const uint64_t TIMEOUT_TICKS = 200000; // 200ms - uint64_t now = Core::Time::Now().Ticks(); - - for (size_t i = 0; i < MaxContentBuffers; i++) { - ContentBuffer* buffer = _contentBuffers[i]; - - if (buffer != nullptr && buffer->IsStuck(now, TIMEOUT_TICKS)) { - // Buffer is stuck - force release it - buffer->ForceRelease(); - } - } - } - - // void PrintBufferStatus() - // { - // printf("Buffer Status:\n"); - - // for (size_t i = 0; i < MaxContentBuffers; i++) { - // ContentBuffer* buffer = _contentBuffers[i]; - - // if (buffer != nullptr) { - // uint64_t age = buffer->Age(); - - // if (age > 0) { - // printf(" Slot %zu: LOCKED for %llu µs\n", i, age); - // } else { - // printf(" Slot %zu: FREE\n", i); - // } - // } else { - // printf(" Slot %zu: EMPTY\n", i); - // } - // } - // } - public: EGLNativeWindowType Native() const override { @@ -511,22 +497,96 @@ namespace Linux { } } - void Rendered(gbm_bo* frameBuffer = nullptr) + uint32_t Process() + { + return Core::ERROR_NONE; + } + + // ───────────────────────────────────────────────────────────────────────── + // Called after eglSwapBuffers + // ───────────────────────────────────────────────────────────────────────── + void RequestRender() { - if ((_gbmSurface != nullptr) && (frameBuffer != nullptr)) { + 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(Trace::Information, (_T("Surface[%d]: lock_front_buffer took %" PRIu64 " µs, returned %p"), _id, (after - before), static_cast(frameBuffer))); + + if (frameBuffer == nullptr) { + TRACE(Trace::Error, (_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; } - if (_callback != nullptr) { - _callback->Rendered(this); + // 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(Trace::Warning, + (_T("Surface %s: orphaned retired buffer %p"), + _name.c_str(), oldRetired->Bo())); + ReleaseToGbm(oldRetired); + } + } + } + + NotifyRendered(); + } + + // ───────────────────────────────────────────────────────────────────────── + // Called when compositor signals Published (VSync done) + // ───────────────────────────────────────────────────────────────────────── + void OnBufferPublished(ContentBuffer* buffer VARIABLE_IS_NOT_USED) + { + // Release retired buffer (RETIRED → FREE) + ContentBuffer* retired = _retiredBuffer.exchange(nullptr, std::memory_order_acq_rel); + + if (retired != nullptr) { + ReleaseToGbm(retired); } + + NotifyPublished(); } void RemoveContentBuffer(ContentBuffer* buffer) @@ -536,80 +596,83 @@ namespace Linux { for (size_t i = 0; i < MaxContentBuffers; i++) { if (_contentBuffers[i] == buffer) { _contentBuffers[i] = nullptr; - TRACE(Trace::Information, (_T("Removed ContentBuffer[%p] from slot %zu for surface %s"), buffer, i, _name.c_str())); 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); } - void RequestRender() + private: + void ReleaseToGbm(ContentBuffer* buffer) { - CheckStuckBuffers(); - - if (_gbmSurface == nullptr) { - Rendered(); - return; + if (buffer != nullptr && buffer->Release() && _gbmSurface != nullptr) { + gbm_surface_release_buffer(_gbmSurface, buffer->Bo()); + TRACE(Trace::Information, + (_T("Surface %s: buffer %p released to GBM"), + _name.c_str(), buffer->Bo())); } + } - gbm_bo* frameBuffer = gbm_surface_lock_front_buffer(_gbmSurface); + ContentBuffer* GetOrCreateContentBuffer(gbm_bo* frameBuffer) + { + ContentBuffer* buffer = static_cast( + gbm_bo_get_user_data(frameBuffer)); - // bail-out if we cannot lock the front buffer - if (frameBuffer == nullptr) { - Rendered(); - return; + if (buffer != nullptr) { + return buffer; } - // Fast path: Buffer exists - ContentBuffer* buffer = static_cast(gbm_bo_get_user_data(frameBuffer)); - - if (buffer == nullptr) { - Core::SafeSyncType lock(_bufferLock); + Core::SafeSyncType lock(_bufferLock); - // Double-check pattern: another thread might have created it - buffer = static_cast(gbm_bo_get_user_data(frameBuffer)); - if (buffer != nullptr) { - // printf("%d @[%" PRIu64 "] BRAM Request Render on ContentBuffer=%p, frameBuffer=%p\n", __LINE__, Core::Time::Now().Ticks(), (void*)buffer, (void*)frameBuffer); - buffer->RequestRender(); - return; - } + // Double-check after lock + buffer = static_cast(gbm_bo_get_user_data(frameBuffer)); + if (buffer != nullptr) { + return buffer; + } - // Find empty slot in array - size_t slot = MaxContentBuffers; - for (size_t i = 0; i < MaxContentBuffers; i++) { - if (_contentBuffers[i] == nullptr) { - slot = i; - break; - } + // 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) { - // Pool exhausted - this should never happen with MaxContentBuffers=4 - TRACE(Trace::Error, (_T("ContentBuffer pool exhausted for surface %s! All %zu slots full."), _name.c_str(), MaxContentBuffers)); - TRACE(Trace::Error, (_T("This driver may use more buffers than expected. Consider increasing MaxContentBuffers."))); + if (slot == MaxContentBuffers) { + TRACE(Trace::Error, + (_T("Surface %s: buffer pool exhausted"), _name.c_str())); + return nullptr; + } - // Graceful degradation: still call Rendered to keep render loop going - Rendered(frameBuffer); - return; - } + buffer = new ContentBuffer(*this, frameBuffer); + _contentBuffers[slot] = buffer; + gbm_bo_set_user_data(frameBuffer, buffer, &ContentBuffer::Destroyed); - // Create new ContentBuffer - buffer = new ContentBuffer(*this, frameBuffer); - _contentBuffers[slot] = buffer; + TRACE(Trace::Information, + (_T("Surface %s: created ContentBuffer %p in slot %zu"), + _name.c_str(), buffer, slot)); - // Set as user data so GBM can call our Destroyed callback - gbm_bo_set_user_data(frameBuffer, buffer, &ContentBuffer::Destroyed); + return buffer; + } - TRACE(Trace::Information, (_T("Created ContentBuffer[%p] for surface %s in slot %zu"), buffer, _name.c_str(), slot)); + void NotifyRendered() + { + if (_callback != nullptr) { + _callback->Rendered(this); } - - ASSERT(buffer != nullptr); - // printf("%d @[%" PRIu64 "] BRAM Request Render on ContentBuffer=%p, frameBuffer=%p\n", __LINE__, Core::Time::Now().Ticks(), (void*)buffer, (void*)frameBuffer); - buffer->RequestRender(); } - uint32_t Process() + void NotifyPublished() { - return Core::ERROR_NONE; + if (_callback != nullptr) { + _callback->Published(this); + } } private: @@ -628,6 +691,10 @@ namespace Linux { 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 @@ -768,6 +835,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())); @@ -918,20 +1024,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__)); } From a2f65a274ce717371c27327cbc61357c912440eb Mon Sep 17 00:00:00 2001 From: Bram Oosterhuis Date: Tue, 23 Dec 2025 17:31:11 +0100 Subject: [PATCH 17/22] CompositorClient: Add more Tracing to the test app --- .../test/client-renderer/app/Main.cpp | 387 ++++++++++++++---- .../test/client-renderer/common/Module.h | 40 ++ .../test/client-renderer/common/Renderer.cpp | 13 +- 3 files changed, 360 insertions(+), 80 deletions(-) diff --git a/Source/compositorclient/test/client-renderer/app/Main.cpp b/Source/compositorclient/test/client-renderer/app/Main.cpp index f83dcc0e..43cc3475 100644 --- a/Source/compositorclient/test/client-renderer/app/Main.cpp +++ b/Source/compositorclient/test/client-renderer/app/Main.cpp @@ -30,6 +30,7 @@ #include #include +#include using namespace Thunder; @@ -77,115 +78,345 @@ class ConsoleOptions : public Thunder::Core::Options { } }; -int main(int argc, char* argv[]) -{ - Messaging::LocalTracer& tracer = Messaging::LocalTracer::Open(); - - const std::map> modules = { - { "App_CompositionClientRender", { "" } }, - { "Common_CompositionClientRender", { "" } }, - { "CompositorBuffer", { "Error", "Information" } }, - { "CompositorBackend", { "Error" } }, - { "CompositorRenderer", { "Error", "Warning", "Information" } }, - { "DRMCommon", { "Error", "Warning", "Information" } } +class Tracer { +public: + struct ModuleConfig { + std::string Module; + std::set EnabledCategories; }; - for (const auto& module_entry : modules) { - for (const auto& category : module_entry.second) { - tracer.EnableMessage(module_entry.first, category, true); + 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); } - const char* executableName(Thunder::Core::FileNameOnly(argv[0])); - ConsoleOptions options(argc, argv); - bool quitApp(false); + // Bulk configure + void Configure(const std::string& module, const std::vector& categories) + { + for (const auto& category : categories) { + Set(module, category, true); + } + } - TRACE_GLOBAL(Trace::Information, ("%s - build: %s", executableName, __TIMESTAMP__)); + // 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); + } + } - std::string texturePath = options.Texture; + // 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; + } - if (texturePath.empty()) { - texturePath = "/usr/share/" + std::string(Namespace) + "/ClientCompositorRender/ml-tv-color-small.png"; + // 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; } - Compositor::TextureBounce::Config config; - config.Image = texturePath; - config.ImageCount = options.TextureNumber; + // 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; + } - std::string configStr; - config.ToString(configStr); + 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" }); + + 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) { - fprintf(stderr, "Failed to initialize renderer\n"); - Core::Singleton::Dispose(); - return 1; + TRACE_GLOBAL(Trace::Error, ("Failed to initialize renderer")); + exitCode = 1; } if (!renderer.Register(&model, configStr)) { - fprintf(stderr, "Failed to initialize model\n"); - Core::Singleton::Dispose(); - return 1; + TRACE_GLOBAL(Trace::Error, ("Failed to initialize model")); + exitCode = 2; } - Compositor::TerminalInput keyboard; - ASSERT(keyboard.IsValid() == true); + if (exitCode == 0) { + Compositor::TerminalInput keyboard; + ASSERT(keyboard.IsValid() == true); - renderer.Start(); + renderer.Start(); - bool result; + 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(); + 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; } - break; - case 'F': - result = renderer.ToggleFPS(); - printf("%d FPS: %s\n", __LINE__, result ? "off" : "on"); - break; - case 'Z': - result = renderer.ToggleRequestRender(); - printf("%d RequestRender: %s\n", __LINE__, result ? "off" : "on"); - break; - case 'R': - renderer.TriggerRender(); - break; - case 'M': - result = renderer.ToggleModelRender(); - printf("%d Model Render: %s\n", __LINE__, result ? "off" : "on"); - break; - case 'Q': - quitApp = true; - break; - case 'H': - TRACE_GLOBAL(Trace::Information, ("Available commands:")); - TRACE_GLOBAL(Trace::Information, (" S - Start/Stop the rendering")); - TRACE_GLOBAL(Trace::Information, (" F - Show current FPS")); - TRACE_GLOBAL(Trace::Information, (" Q - Quit the application")); - TRACE_GLOBAL(Trace::Information, (" H - Show this help message")); - break; - default: - break; - } - std::this_thread::sleep_for(std::chrono::milliseconds(100)); + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + } + } else { + TRACE_GLOBAL(Thunder::Trace::Error, ("Failed to initialize keyboard input")); } - } else { - TRACE_GLOBAL(Thunder::Trace::Error, ("Failed to initialize keyboard input")); - } - renderer.Stop(); - TRACE_GLOBAL(Thunder::Trace::Information, ("Exiting %s.... ", executableName)); + renderer.Stop(); + TRACE_GLOBAL(Thunder::Trace::Information, ("Exiting %s.... ", executableName)); + } } Core::Singleton::Dispose(); - return 0; + return exitCode; } diff --git a/Source/compositorclient/test/client-renderer/common/Module.h b/Source/compositorclient/test/client-renderer/common/Module.h index af2e36e7..7b3f0d20 100644 --- a/Source/compositorclient/test/client-renderer/common/Module.h +++ b/Source/compositorclient/test/client-renderer/common/Module.h @@ -35,3 +35,43 @@ #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 index 52e3ac28..bb482ef5 100644 --- a/Source/compositorclient/test/client-renderer/common/Renderer.cpp +++ b/Source/compositorclient/test/client-renderer/common/Renderer.cpp @@ -260,6 +260,11 @@ namespace Compositor { 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(); @@ -285,12 +290,14 @@ namespace Compositor { } // 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); + 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")); } @@ -305,15 +312,17 @@ namespace Compositor { if (_skipRender == false) { _surface->RequestRender(); + afterRequest = Core::Time::Now().Ticks(); // allow for for 2 25FPS frame delay if (WaitForRendered(80) == Core::ERROR_TIMEDOUT) { - printf("%d @[%" PRIu64 "] BRAM Render Timeout\n", __LINE__, Core::Time::Now().Ticks()); 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 From ca6ec5528ff4cfba009c6de0effc2f13165f4ad1 Mon Sep 17 00:00:00 2001 From: Bram Oosterhuis Date: Tue, 30 Dec 2025 20:26:56 +0100 Subject: [PATCH 18/22] CompositorClient: Improve cleanup process in test Render destructor and Stop method --- .../test/client-renderer/common/Renderer.h | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/Source/compositorclient/test/client-renderer/common/Renderer.h b/Source/compositorclient/test/client-renderer/common/Renderer.h index 636f1c51..d985bc8c 100644 --- a/Source/compositorclient/test/client-renderer/common/Renderer.h +++ b/Source/compositorclient/test/client-renderer/common/Renderer.h @@ -46,6 +46,12 @@ namespace Compositor { virtual ~Render() override { + Stop(); + + for (auto* model : _models) { + Unregister(model); + } + CleanupEGL(); if (_surface) { @@ -83,12 +89,16 @@ namespace Compositor { } void Stop() { - bool expected = true; - if (_running.compare_exchange_strong(expected, false)) { - TRACE(Trace::Information, ("Stopping Render")); + _running.store(false); + _renderSync.notify_all(); + + if (_render.joinable()) { + TRACE(Trace::Information, ("Stopping Render thread")); _render.join(); - _selectedModel = ~0; + TRACE(Trace::Information, ("Render thread stopped")); } + + _selectedModel = ~0; } bool IsRunning() const { From 4fb920faa0fa6a85f21758839a8f8af9c3fdb8ba Mon Sep 17 00:00:00 2001 From: Bram Oosterhuis Date: Wed, 31 Dec 2025 16:07:53 +0100 Subject: [PATCH 19/22] CompositorClient: Replace stderr logging with TRACE for EGL initialization errors --- .../test/client-renderer/common/Renderer.cpp | 33 +++++++++---------- 1 file changed, 15 insertions(+), 18 deletions(-) diff --git a/Source/compositorclient/test/client-renderer/common/Renderer.cpp b/Source/compositorclient/test/client-renderer/common/Renderer.cpp index bb482ef5..3a2b665f 100644 --- a/Source/compositorclient/test/client-renderer/common/Renderer.cpp +++ b/Source/compositorclient/test/client-renderer/common/Renderer.cpp @@ -81,7 +81,7 @@ namespace Compositor { ASSERT(_surface != nullptr); if (!InitializeEGL()) { - fprintf(stderr, "Failed to initialize EGL\n"); + TRACE(Trace::Error, ("Failed to initialize EGL")); return false; } @@ -97,20 +97,20 @@ namespace Compositor { config.ToString(configStr); if (!_textRender.Initialize(width, height, configStr)) { - fprintf(stderr, "Failed to initialize FPS counter\n"); + 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(); - fprintf(stderr, "Failed to release EGL context: 0x%x\n", error); + TRACE(Trace::Error, ("Configure: eglMakeCurrent release failed: 0x%x", error)); return false; } return true; } else { - fprintf(stderr, "Failed to initialize display\n"); + TRACE(Trace::Error, ("Failed to create Compositor Display")); return false; } } @@ -143,7 +143,7 @@ namespace Compositor { if (result == true) { _models.push_back(model); } else { - fprintf(stderr, "Failed to initialize model during registration\n"); + TRACE(Trace::Error, ("Failed to initialize model during registration")); } return result; @@ -165,8 +165,7 @@ namespace Compositor { // Make context current if (!eglMakeCurrent(_eglDisplay, _eglSurface, _eglSurface, _eglContext)) { - EGLint error = eglGetError(); - fprintf(stderr, "InitializeModel: eglMakeCurrent failed: 0x%x\n", error); + TRACE(Trace::Error, ("InitializeModel: eglMakeCurrent failed: 0x%x", eglGetError())); return false; } @@ -175,8 +174,7 @@ namespace Compositor { // Release context if (!eglMakeCurrent(_eglDisplay, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT)) { - EGLint error = eglGetError(); - fprintf(stderr, "InitializeModel: release failed: 0x%x\n", error); + TRACE(Trace::Error, ("InitializeModel: eglMakeCurrent release failed: 0x%x", eglGetError())); } return result; @@ -189,12 +187,12 @@ namespace Compositor { _eglDisplay = eglGetDisplay(display); if (_eglDisplay == EGL_NO_DISPLAY) { - fprintf(stderr, "eglGetDisplay failed\n"); + TRACE(Trace::Error, ("eglGetDisplay failed: 0x%x", eglGetError())); return false; } if (!eglInitialize(_eglDisplay, nullptr, nullptr)) { - fprintf(stderr, "eglInitialize failed\n"); + TRACE(Trace::Error, ("eglInitialize failed: 0x%x", eglGetError())); return false; } @@ -211,7 +209,7 @@ namespace Compositor { EGLConfig eglConfig; EGLint numConfigs; if (!eglChooseConfig(_eglDisplay, configAttribs, &eglConfig, 1, &numConfigs)) { - fprintf(stderr, "eglChooseConfig failed\n"); + TRACE(Trace::Error, ("eglChooseConfig failed: 0x%x", eglGetError())); return false; } @@ -222,19 +220,18 @@ namespace Compositor { _eglContext = eglCreateContext(_eglDisplay, eglConfig, EGL_NO_CONTEXT, contextAttribs); if (_eglContext == EGL_NO_CONTEXT) { - fprintf(stderr, "eglCreateContext failed\n"); + TRACE(Trace::Error, ("Failed to create EGL context: 0x%x", eglGetError())); return false; } _eglSurface = eglCreateWindowSurface(_eglDisplay, eglConfig, window, nullptr); if (_eglSurface == EGL_NO_SURFACE) { - fprintf(stderr, "eglCreateWindowSurface failed\n"); + TRACE(Trace::Error, ("Failed to create EGL surface: 0x%x", eglGetError())); return false; } if (!eglMakeCurrent(_eglDisplay, _eglSurface, _eglSurface, _eglContext)) { - EGLint error = eglGetError(); - fprintf(stderr, "eglMakeCurrent failed: 0x%x\n", error); + TRACE(Trace::Error, ("eglMakeCurrent failed: 0x%x", eglGetError())); return false; } @@ -268,7 +265,7 @@ namespace Compositor { // Make context current for this render thread if (!eglMakeCurrent(_eglDisplay, _eglSurface, _eglSurface, _eglContext)) { EGLint error = eglGetError(); - fprintf(stderr, "Draw: eglMakeCurrent failed: 0x%x\n", error); + TRACE(Trace::Error, ("Draw: eglMakeCurrent failed: 0x%x", error)); return; } @@ -322,7 +319,7 @@ namespace Compositor { 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))); + 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 From 3418a67ea98662e4fe274ed19a7600a8b3e5dc05 Mon Sep 17 00:00:00 2001 From: Bram Oosterhuis Date: Mon, 5 Jan 2026 14:43:15 +0100 Subject: [PATCH 20/22] CompositorClient: Move buffer traces to its own category --- .../src/Mesa/Implementation.cpp | 23 ++++++++----------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/Source/compositorclient/src/Mesa/Implementation.cpp b/Source/compositorclient/src/Mesa/Implementation.cpp index 80e0308d..2f6da611 100644 --- a/Source/compositorclient/src/Mesa/Implementation.cpp +++ b/Source/compositorclient/src/Mesa/Implementation.cpp @@ -91,6 +91,10 @@ namespace Linux { 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; @@ -516,10 +520,10 @@ namespace Linux { gbm_bo* frameBuffer = gbm_surface_lock_front_buffer(_gbmSurface); uint64_t after = Core::Time::Now().Ticks(); - TRACE(Trace::Information, (_T("Surface[%d]: lock_front_buffer took %" PRIu64 " µs, returned %p"), _id, (after - before), static_cast(frameBuffer))); + TRACE(BufferInfo, (_T("Surface[%d]: lock_front_buffer took %" PRIu64 " µs, returned %p"), _id, (after - before), static_cast(frameBuffer))); if (frameBuffer == nullptr) { - TRACE(Trace::Error, (_T("Surface %s: lock_front_buffer failed"), _name.c_str())); + TRACE(BufferError, (_T("Surface %s: lock_front_buffer failed"), _name.c_str())); NotifyRendered(); return; } @@ -563,9 +567,7 @@ namespace Linux { // Handle orphaned retired buffer (shouldn't happen normally) if (oldRetired != nullptr) { - TRACE(Trace::Warning, - (_T("Surface %s: orphaned retired buffer %p"), - _name.c_str(), oldRetired->Bo())); + TRACE(BufferError, (_T("Surface %s: orphaned retired buffer %p"), _name.c_str(), oldRetired->Bo())); ReleaseToGbm(oldRetired); } } @@ -612,9 +614,7 @@ namespace Linux { { if (buffer != nullptr && buffer->Release() && _gbmSurface != nullptr) { gbm_surface_release_buffer(_gbmSurface, buffer->Bo()); - TRACE(Trace::Information, - (_T("Surface %s: buffer %p released to GBM"), - _name.c_str(), buffer->Bo())); + TRACE(BufferInfo, (_T("Surface %s: buffer %p released to GBM"), _name.c_str(), buffer->Bo())); } } @@ -645,8 +645,7 @@ namespace Linux { } if (slot == MaxContentBuffers) { - TRACE(Trace::Error, - (_T("Surface %s: buffer pool exhausted"), _name.c_str())); + TRACE(Trace::Error, (_T("Surface %s: buffer pool exhausted"), _name.c_str())); return nullptr; } @@ -654,9 +653,7 @@ namespace Linux { _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)); + TRACE(Trace::Information, (_T("Surface %s: created ContentBuffer %p in slot %zu"), _name.c_str(), buffer, slot)); return buffer; } From 133b6592f5a64a3b2b6205d1b3292edf1e031c50 Mon Sep 17 00:00:00 2001 From: Bram Oosterhuis Date: Mon, 5 Jan 2026 14:43:36 +0100 Subject: [PATCH 21/22] CompositorClient: Add additional tracer configuration for common composition client render --- Source/compositorclient/test/client-renderer/app/Main.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/Source/compositorclient/test/client-renderer/app/Main.cpp b/Source/compositorclient/test/client-renderer/app/Main.cpp index 43cc3475..5a1f15bc 100644 --- a/Source/compositorclient/test/client-renderer/app/Main.cpp +++ b/Source/compositorclient/test/client-renderer/app/Main.cpp @@ -320,6 +320,7 @@ int main(int argc, char* argv[]) { Tracer tracer; tracer.Configure("App_CompositionClientRender", { "Information", "Error", "Warning" }); + tracer.Configure("Common_CompositionClientRender", { "Error", "Warning" }); ConsoleOptions options(argc, argv); bool quitApp(false); From b934c14960cbc545ede6029c1f95e832c30a8b69 Mon Sep 17 00:00:00 2001 From: Bram Oosterhuis Date: Tue, 6 Jan 2026 09:56:36 +0100 Subject: [PATCH 22/22] CompositorClient: Add libpng transformation documentation for license compliance --- .../client-renderer/common/TextureLoader.cpp | 23 ++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/Source/compositorclient/test/client-renderer/common/TextureLoader.cpp b/Source/compositorclient/test/client-renderer/common/TextureLoader.cpp index c75f5c4b..7a852725 100644 --- a/Source/compositorclient/test/client-renderer/common/TextureLoader.cpp +++ b/Source/compositorclient/test/client-renderer/common/TextureLoader.cpp @@ -36,6 +36,11 @@ static void PngWarnFn(png_structp, png_const_charp 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; @@ -59,13 +64,29 @@ PixelData LoadPNG(const std::string& filename) { int color_type = png_get_color_type(png, info); int bit_depth = png_get_bit_depth(png, info); - // Force RGBA8 + // 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);