From b49b659fc957aee7c27fefdc8fda71e4ded69cd7 Mon Sep 17 00:00:00 2001 From: Michal Date: Fri, 19 Jun 2026 22:33:04 +0200 Subject: [PATCH 1/5] refactor: resolve clang-tidy warnings --- .clang-tidy | 2 +- include/Runtime.hpp | 5 +++ include/data_structures/Context.hpp | 2 +- include/datawriter/CSVFormatStrategy.hpp | 5 +++ include/datawriter/DataWriter.hpp | 4 +-- include/datawriter/IDataFormatStrategy.hpp | 2 +- include/parser/Parser.hpp | 2 +- include/renderer/Renderer.hpp | 5 +++ include/scene/components/BlinkComponent.hpp | 2 +- include/scene/components/Component.hpp | 3 +- .../scene/components/ComponentRegistry.hpp | 5 ++- src/datawriter/CSVFormatStrategy.cpp | 2 +- src/datawriter/DataWriter.cpp | 2 +- src/main.cpp | 2 ++ src/parser/Parser.cpp | 2 +- src/renderer/Renderer.cpp | 4 +-- src/scene/components/BlinkComponent.cpp | 2 ++ src/scene/components/ComponentRegistry.cpp | 8 ++--- tests/unit_tests/RendererTest.cpp | 32 ++++++++++--------- 19 files changed, 58 insertions(+), 33 deletions(-) diff --git a/.clang-tidy b/.clang-tidy index 85b5c39..19cb257 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -1,5 +1,5 @@ Checks: 'bugprone-*,cppcoreguidelines-*,performance-*,readability-*,-cppcoreguidelines-avoid-magic-numbers,-cppcoreguidelines-pro-bounds-array-to-pointer-decay' -WarningsAsErrors: '' +WarningsAsErrors: '*' HeaderFilterRegex: '.*' ExcludeHeaderFilterRegex: '(_deps|\.pb\.h|\.pb\.cc|/usr/)' FormatStyle: 'file' \ No newline at end of file diff --git a/include/Runtime.hpp b/include/Runtime.hpp index e3ee994..bb01728 100644 --- a/include/Runtime.hpp +++ b/include/Runtime.hpp @@ -7,6 +7,11 @@ class Runtime { public: Runtime() = default; ~Runtime() = default; + + Runtime(const Runtime&) = delete; + Runtime& operator=(const Runtime&) = delete; + Runtime(Runtime&&) = delete; + Runtime& operator=(Runtime&&) = delete; static void start(); }; diff --git a/include/data_structures/Context.hpp b/include/data_structures/Context.hpp index 40f90d0..53b7d9b 100644 --- a/include/data_structures/Context.hpp +++ b/include/data_structures/Context.hpp @@ -6,7 +6,7 @@ struct Context { double timestamp = 0.0; - std::vector& markers; + std::vector* markers = nullptr; }; #endif // CONTEXT_HPP \ No newline at end of file diff --git a/include/datawriter/CSVFormatStrategy.hpp b/include/datawriter/CSVFormatStrategy.hpp index 6043c49..1a58bbb 100644 --- a/include/datawriter/CSVFormatStrategy.hpp +++ b/include/datawriter/CSVFormatStrategy.hpp @@ -10,6 +10,11 @@ class CSVFormatStrategy : public IDataFormatStrategy { CSVFormatStrategy() = default; ~CSVFormatStrategy() override; + CSVFormatStrategy(const CSVFormatStrategy&) = delete; + CSVFormatStrategy& operator=(const CSVFormatStrategy&) = delete; + CSVFormatStrategy(CSVFormatStrategy&&) = delete; + CSVFormatStrategy& operator=(CSVFormatStrategy&&) = delete; + void open(const std::string& filepath) override; void close() override; diff --git a/include/datawriter/DataWriter.hpp b/include/datawriter/DataWriter.hpp index 59782c2..ab5a2a1 100644 --- a/include/datawriter/DataWriter.hpp +++ b/include/datawriter/DataWriter.hpp @@ -5,8 +5,8 @@ #include -class EEGData; -class Marker; +struct EEGData; +struct Marker; #include #include diff --git a/include/datawriter/IDataFormatStrategy.hpp b/include/datawriter/IDataFormatStrategy.hpp index cbed789..c178e43 100644 --- a/include/datawriter/IDataFormatStrategy.hpp +++ b/include/datawriter/IDataFormatStrategy.hpp @@ -3,7 +3,7 @@ #include -class EEGData; +struct EEGData; struct Marker; class IDataFormatStrategy { diff --git a/include/parser/Parser.hpp b/include/parser/Parser.hpp index 26bf0dd..09c2e1f 100644 --- a/include/parser/Parser.hpp +++ b/include/parser/Parser.hpp @@ -18,7 +18,7 @@ class Parser { public: Parser() = default; - std::shared_ptr parse(const std::string& filePath); + static std::shared_ptr parse(const std::string& filePath); private: static std::shared_ptr buildSceneObject(const NeuronIDE::SceneObject& protoObj); diff --git a/include/renderer/Renderer.hpp b/include/renderer/Renderer.hpp index 267c810..7044cd5 100644 --- a/include/renderer/Renderer.hpp +++ b/include/renderer/Renderer.hpp @@ -17,6 +17,11 @@ class Renderer { std::shared_ptr> markerQueue); ~Renderer() = default; + Renderer(const Renderer&) = delete; + Renderer& operator=(const Renderer&) = delete; + Renderer(Renderer&&) = delete; + Renderer& operator=(Renderer&&) = delete; + void render(const std::stop_token& stoken); private: diff --git a/include/scene/components/BlinkComponent.hpp b/include/scene/components/BlinkComponent.hpp index b4814e9..dd68a4a 100644 --- a/include/scene/components/BlinkComponent.hpp +++ b/include/scene/components/BlinkComponent.hpp @@ -11,7 +11,7 @@ class Component; class BlinkComponent : public Component { public: - BlinkComponent(std::shared_ptr owner, double freq) + BlinkComponent(const std::shared_ptr& owner, double freq) : Component(owner), blinkFrequencyHz(freq) {} void setFrequency(double freq); diff --git a/include/scene/components/Component.hpp b/include/scene/components/Component.hpp index 4000717..4e8d467 100644 --- a/include/scene/components/Component.hpp +++ b/include/scene/components/Component.hpp @@ -10,7 +10,7 @@ struct Context; class Component { public: Component() = delete; - Component(std::shared_ptr owner) : owner(owner) {} + Component(const std::shared_ptr& owner) : owner(owner) {} virtual ~Component() = default; Component(const Component&) = delete; @@ -22,6 +22,7 @@ class Component { virtual void render(SDL_Renderer* renderer) = 0; protected: + // NOLINTNEXTLINE(cppcoreguidelines-non-private-member-variables-in-classes) std::weak_ptr owner; }; diff --git a/include/scene/components/ComponentRegistry.hpp b/include/scene/components/ComponentRegistry.hpp index 668dc4a..1ef388e 100644 --- a/include/scene/components/ComponentRegistry.hpp +++ b/include/scene/components/ComponentRegistry.hpp @@ -21,6 +21,7 @@ class ComponentRegistry { ComponentRegistry(ComponentRegistry&&) = delete; ComponentRegistry& operator=(ComponentRegistry&&) = delete; + ~ComponentRegistry() = default; static ComponentRegistry& instance() { static ComponentRegistry instance; @@ -39,8 +40,10 @@ class ComponentRegistry { }; #define COMPONENT_REGISTRATION_CONCAT_IMPL(x, y) x##y +// NOLINTNEXTLINE(cppcoreguidelines-macro-usage) #define COMPONENT_REGISTRATION_CONCAT(x, y) COMPONENT_REGISTRATION_CONCAT_IMPL(x, y) +// NOLINTNEXTLINE(cppcoreguidelines-macro-usage) #define REGISTER_COMPONENT(typeId, creatorFunc) \ namespace { \ struct COMPONENT_REGISTRATION_CONCAT(ComponentRegistrar_, __LINE__) { \ @@ -48,7 +51,7 @@ class ComponentRegistry { ComponentRegistry::instance().registerCreator(static_cast(typeId), creatorFunc); \ } \ }; \ - static COMPONENT_REGISTRATION_CONCAT(ComponentRegistrar_, __LINE__) \ + static const COMPONENT_REGISTRATION_CONCAT(ComponentRegistrar_, __LINE__) \ COMPONENT_REGISTRATION_CONCAT(global_registrar_, __LINE__); \ } diff --git a/src/datawriter/CSVFormatStrategy.cpp b/src/datawriter/CSVFormatStrategy.cpp index e70e4a6..69e9be1 100644 --- a/src/datawriter/CSVFormatStrategy.cpp +++ b/src/datawriter/CSVFormatStrategy.cpp @@ -3,7 +3,7 @@ #include #include -CSVFormatStrategy::~CSVFormatStrategy() { close(); } +CSVFormatStrategy::~CSVFormatStrategy() { CSVFormatStrategy::close(); } void CSVFormatStrategy::open(const std::string& filepath) { outputFile.open(filepath, std::ios::out | std::ios::trunc); diff --git a/src/datawriter/DataWriter.cpp b/src/datawriter/DataWriter.cpp index 5175cb5..cfd0880 100644 --- a/src/datawriter/DataWriter.cpp +++ b/src/datawriter/DataWriter.cpp @@ -9,7 +9,7 @@ constexpr auto kWriteLoopSleep = std::chrono::milliseconds(10); template -bool drainQueue(const std::shared_ptr& queue, WriteFn&& writeFn) { +bool drainQueue(const std::shared_ptr& queue, WriteFn writeFn) { bool wroteData = false; if (queue) { diff --git a/src/main.cpp b/src/main.cpp index 1b85885..824bdc3 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,6 +1,8 @@ #include int main(int argc, char* argv[]) { + (void)argc; + (void)argv; Runtime::start(); return 0; } \ No newline at end of file diff --git a/src/parser/Parser.cpp b/src/parser/Parser.cpp index 235dba1..f11a383 100644 --- a/src/parser/Parser.cpp +++ b/src/parser/Parser.cpp @@ -46,7 +46,7 @@ std::shared_ptr Parser::buildSceneObject(const NeuronIDE::SceneObje for (const auto& protoComp : protoObj.components()) { int typeId = static_cast(protoComp.component_type_case()); - if (seenComponentTypes.find(typeId) != seenComponentTypes.end()) { + if (seenComponentTypes.contains(typeId)) { throw std::runtime_error("Parser: duplicate component type in object '" + protoObj.name() + "'."); } diff --git a/src/renderer/Renderer.cpp b/src/renderer/Renderer.cpp index f57b239..142e969 100644 --- a/src/renderer/Renderer.cpp +++ b/src/renderer/Renderer.cpp @@ -6,7 +6,7 @@ #include "scene/Scene.hpp" void Renderer::SDLWindowDeleter::operator()(SDL_Window* window) const { - if (window) { + if (window != nullptr) { SDL_DestroyWindow(window); } } @@ -28,7 +28,7 @@ void Renderer::render(const std::stop_token& stoken) { lastTime = currentTime; currentFrameMarkers.clear(); - Context ctx{deltaTime, currentFrameMarkers}; + Context ctx{deltaTime, ¤tFrameMarkers}; SDL_Event event; while (SDL_PollEvent(&event) == 1) { diff --git a/src/scene/components/BlinkComponent.cpp b/src/scene/components/BlinkComponent.cpp index dbd9503..7911f0f 100644 --- a/src/scene/components/BlinkComponent.cpp +++ b/src/scene/components/BlinkComponent.cpp @@ -11,10 +11,12 @@ std::unique_ptr BlinkComponent::createBlinker( } void BlinkComponent::update(const Context& context) { + (void)context; // TODO: implement blinking logic based on blinkFrequencyHz and context.timestamp } void BlinkComponent::render(SDL_Renderer* renderer) { + (void)renderer; // This component does not render anything itself, it only controls visibility of the owner // object. } diff --git a/src/scene/components/ComponentRegistry.cpp b/src/scene/components/ComponentRegistry.cpp index b3419fc..f8c67c8 100644 --- a/src/scene/components/ComponentRegistry.cpp +++ b/src/scene/components/ComponentRegistry.cpp @@ -7,7 +7,7 @@ #include "scene/components/Component.hpp" void ComponentRegistry::registerCreator(int typeId, ComponentCreatorFunc creator) { - if (creators.find(typeId) != creators.end()) { + if (creators.contains(typeId)) { throw std::runtime_error("Creator for this typeId is already registered."); } creators[typeId] = std::move(creator); @@ -19,9 +19,9 @@ std::unique_ptr ComponentRegistry::build(const NeuronIDE::Component& int typeId = static_cast(activeCase); - auto it = creators.find(typeId); - if (it != creators.end()) { - return it->second(protoComp, owner); + auto iter = creators.find(typeId); + if (iter != creators.end()) { + return iter->second(protoComp, owner); } return nullptr; diff --git a/tests/unit_tests/RendererTest.cpp b/tests/unit_tests/RendererTest.cpp index fa00645..178b5e1 100644 --- a/tests/unit_tests/RendererTest.cpp +++ b/tests/unit_tests/RendererTest.cpp @@ -19,10 +19,10 @@ constexpr uint32_t kDummySurfaceFlags = 0; class CustomComponent : public Component { public: - CustomComponent(std::shared_ptr owner, std::shared_ptr> updates, + CustomComponent(const std::shared_ptr& owner, std::shared_ptr> updates, std::shared_ptr> renders, std::shared_ptr stopSource) - : Component(std::move(owner)), + : Component(owner), updates(std::move(updates)), renders(std::move(renders)), stopSource(std::move(stopSource)) {} @@ -47,12 +47,14 @@ class CustomComponent : public Component { class MarkerComponent : public Component { public: - MarkerComponent(std::shared_ptr owner, + MarkerComponent(const std::shared_ptr& owner, std::shared_ptr stopSource) - : Component(std::move(owner)), stopSource(std::move(stopSource)) {} + : Component(owner), stopSource(std::move(stopSource)) {} void update(const Context& context) override { - context.markers.push_back("test_marker"); + if (context.markers != nullptr) { + context.markers->push_back("test_marker"); + } stopSource->request_stop(); } @@ -81,9 +83,9 @@ TEST(RendererTest, RenderLoop_WhenComponentAdded_CallsUpdateExactlyOnceBeforeSto SDL_CreateRGBSurfaceWithFormat(kDummySurfaceFlags, kDummySurfaceWidth, kDummySurfaceHeight, kDummySurfaceDepth, SDL_PIXELFORMAT_RGBA32); SDL_Renderer* sdlRenderer = SDL_CreateSoftwareRenderer(surface); - auto sharedRenderer = std::shared_ptr(sdlRenderer, [surface](SDL_Renderer* r) { - if (r) { - SDL_DestroyRenderer(r); + auto sharedRenderer = std::shared_ptr(sdlRenderer, [surface](SDL_Renderer* renderer) { + if (renderer) { + SDL_DestroyRenderer(renderer); } if (surface) { SDL_FreeSurface(surface); @@ -115,9 +117,9 @@ TEST(RendererTest, RenderLoop_QueuesMarkersFromComponents) { SDL_CreateRGBSurfaceWithFormat(kDummySurfaceFlags, kDummySurfaceWidth, kDummySurfaceHeight, kDummySurfaceDepth, SDL_PIXELFORMAT_RGBA32); SDL_Renderer* sdlRenderer = SDL_CreateSoftwareRenderer(surface); - auto sharedRenderer = std::shared_ptr(sdlRenderer, [surface](SDL_Renderer* r) { - if (r) { - SDL_DestroyRenderer(r); + auto sharedRenderer = std::shared_ptr(sdlRenderer, [surface](SDL_Renderer* renderer) { + if (renderer) { + SDL_DestroyRenderer(renderer); } if (surface) { SDL_FreeSurface(surface); @@ -129,12 +131,12 @@ TEST(RendererTest, RenderLoop_QueuesMarkersFromComponents) { Renderer renderer(scene, sharedRenderer, markerQueue); renderer.render(stop_source->get_token()); - Marker m; - bool dequeued = markerQueue->try_dequeue(m); + Marker marker; + bool dequeued = markerQueue->try_dequeue(marker); EXPECT_TRUE(dequeued); if (dequeued) { - EXPECT_EQ(m.eventName, "test_marker"); - EXPECT_FALSE(markerQueue->try_dequeue(m)); + EXPECT_EQ(marker.eventName, "test_marker"); + EXPECT_FALSE(markerQueue->try_dequeue(marker)); } SDL_Quit(); From 654c2af4cb4f8b262e714ed1fecb30cad1364c8b Mon Sep 17 00:00:00 2001 From: Michal Date: Fri, 19 Jun 2026 22:36:40 +0200 Subject: [PATCH 2/5] fix: fix code formatting --- include/Runtime.hpp | 6 ++-- include/data_structures/Context.hpp | 2 +- include/datawriter/CSVFormatStrategy.hpp | 6 ++-- include/renderer/Renderer.hpp | 6 ++-- tests/unit_tests/RendererTest.cpp | 43 +++++++++++++----------- 5 files changed, 33 insertions(+), 30 deletions(-) diff --git a/include/Runtime.hpp b/include/Runtime.hpp index bb01728..838b4a6 100644 --- a/include/Runtime.hpp +++ b/include/Runtime.hpp @@ -8,10 +8,10 @@ class Runtime { Runtime() = default; ~Runtime() = default; - Runtime(const Runtime&) = delete; + Runtime(const Runtime&) = delete; Runtime& operator=(const Runtime&) = delete; - Runtime(Runtime&&) = delete; - Runtime& operator=(Runtime&&) = delete; + Runtime(Runtime&&) = delete; + Runtime& operator=(Runtime&&) = delete; static void start(); }; diff --git a/include/data_structures/Context.hpp b/include/data_structures/Context.hpp index 53b7d9b..74c5a04 100644 --- a/include/data_structures/Context.hpp +++ b/include/data_structures/Context.hpp @@ -6,7 +6,7 @@ struct Context { double timestamp = 0.0; - std::vector* markers = nullptr; + std::vector* markers = nullptr; }; #endif // CONTEXT_HPP \ No newline at end of file diff --git a/include/datawriter/CSVFormatStrategy.hpp b/include/datawriter/CSVFormatStrategy.hpp index 1a58bbb..fb76dbe 100644 --- a/include/datawriter/CSVFormatStrategy.hpp +++ b/include/datawriter/CSVFormatStrategy.hpp @@ -10,10 +10,10 @@ class CSVFormatStrategy : public IDataFormatStrategy { CSVFormatStrategy() = default; ~CSVFormatStrategy() override; - CSVFormatStrategy(const CSVFormatStrategy&) = delete; + CSVFormatStrategy(const CSVFormatStrategy&) = delete; CSVFormatStrategy& operator=(const CSVFormatStrategy&) = delete; - CSVFormatStrategy(CSVFormatStrategy&&) = delete; - CSVFormatStrategy& operator=(CSVFormatStrategy&&) = delete; + CSVFormatStrategy(CSVFormatStrategy&&) = delete; + CSVFormatStrategy& operator=(CSVFormatStrategy&&) = delete; void open(const std::string& filepath) override; void close() override; diff --git a/include/renderer/Renderer.hpp b/include/renderer/Renderer.hpp index 7044cd5..3f551b0 100644 --- a/include/renderer/Renderer.hpp +++ b/include/renderer/Renderer.hpp @@ -17,10 +17,10 @@ class Renderer { std::shared_ptr> markerQueue); ~Renderer() = default; - Renderer(const Renderer&) = delete; + Renderer(const Renderer&) = delete; Renderer& operator=(const Renderer&) = delete; - Renderer(Renderer&&) = delete; - Renderer& operator=(Renderer&&) = delete; + Renderer(Renderer&&) = delete; + Renderer& operator=(Renderer&&) = delete; void render(const std::stop_token& stoken); diff --git a/tests/unit_tests/RendererTest.cpp b/tests/unit_tests/RendererTest.cpp index 178b5e1..88da249 100644 --- a/tests/unit_tests/RendererTest.cpp +++ b/tests/unit_tests/RendererTest.cpp @@ -19,9 +19,10 @@ constexpr uint32_t kDummySurfaceFlags = 0; class CustomComponent : public Component { public: - CustomComponent(const std::shared_ptr& owner, std::shared_ptr> updates, - std::shared_ptr> renders, - std::shared_ptr stopSource) + CustomComponent(const std::shared_ptr& owner, + std::shared_ptr> updates, + std::shared_ptr> renders, + std::shared_ptr stopSource) : Component(owner), updates(std::move(updates)), renders(std::move(renders)), @@ -48,7 +49,7 @@ class CustomComponent : public Component { class MarkerComponent : public Component { public: MarkerComponent(const std::shared_ptr& owner, - std::shared_ptr stopSource) + std::shared_ptr stopSource) : Component(owner), stopSource(std::move(stopSource)) {} void update(const Context& context) override { @@ -83,14 +84,15 @@ TEST(RendererTest, RenderLoop_WhenComponentAdded_CallsUpdateExactlyOnceBeforeSto SDL_CreateRGBSurfaceWithFormat(kDummySurfaceFlags, kDummySurfaceWidth, kDummySurfaceHeight, kDummySurfaceDepth, SDL_PIXELFORMAT_RGBA32); SDL_Renderer* sdlRenderer = SDL_CreateSoftwareRenderer(surface); - auto sharedRenderer = std::shared_ptr(sdlRenderer, [surface](SDL_Renderer* renderer) { - if (renderer) { - SDL_DestroyRenderer(renderer); - } - if (surface) { - SDL_FreeSurface(surface); - } - }); + auto sharedRenderer = + std::shared_ptr(sdlRenderer, [surface](SDL_Renderer* renderer) { + if (renderer) { + SDL_DestroyRenderer(renderer); + } + if (surface) { + SDL_FreeSurface(surface); + } + }); auto markerQueue = std::make_shared>(); @@ -117,14 +119,15 @@ TEST(RendererTest, RenderLoop_QueuesMarkersFromComponents) { SDL_CreateRGBSurfaceWithFormat(kDummySurfaceFlags, kDummySurfaceWidth, kDummySurfaceHeight, kDummySurfaceDepth, SDL_PIXELFORMAT_RGBA32); SDL_Renderer* sdlRenderer = SDL_CreateSoftwareRenderer(surface); - auto sharedRenderer = std::shared_ptr(sdlRenderer, [surface](SDL_Renderer* renderer) { - if (renderer) { - SDL_DestroyRenderer(renderer); - } - if (surface) { - SDL_FreeSurface(surface); - } - }); + auto sharedRenderer = + std::shared_ptr(sdlRenderer, [surface](SDL_Renderer* renderer) { + if (renderer) { + SDL_DestroyRenderer(renderer); + } + if (surface) { + SDL_FreeSurface(surface); + } + }); auto markerQueue = std::make_shared>(); From e479f6787b7ba115ea9b6590d7a803725618fc30 Mon Sep 17 00:00:00 2001 From: Michal Date: Fri, 19 Jun 2026 22:52:21 +0200 Subject: [PATCH 3/5] docs: update README --- README.md | 36 +++++++++++++++++++++++++++++++++--- 1 file changed, 33 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 1c3dc88..dd43762 100644 --- a/README.md +++ b/README.md @@ -2,16 +2,16 @@ ## Setup and Build Instructions -This project uses CMake and requires a modern C++17 compiler. +This project uses CMake (minimum version 3.25) and requires a modern C++20 compiler. ### 1. Clone repo and install required dependencies -This project uses GTest, LSL, SDL2, Clang-format, Clang-tidy, protobuf-compiler and cmake, but you don't need to install GTest and LSL because cmake will install it for you. Below is minimal linux setup. +This project uses GTest, LSL, SDL2, Clang-format, Clang-tidy-20, protobuf-compiler, gcovr and cmake, but you don't need to install GTest and LSL because cmake will install it for you. Below is minimal linux setup. ```bash git clone git@github.com:KN-Neuron/Neuron-IDE-runtime.git sudo apt update -sudo apt install cmake clang-format clang-tidy libsdl2-dev protobuf-compiler +sudo apt install cmake clang-format clang-tidy-20 libsdl2-dev protobuf-compiler gcovr ``` ### 2. Build the Project @@ -66,4 +66,34 @@ In case you want to create an example .pb file for testing or some other purpose ```bash protoc --encode=NeuronIDE.Scene protoFiles/neuronide.proto < protoFiles/tests/test_scene.pbtxt > protoFiles/tests/test_scene.pb +``` + +### 6. Code Coverage + +This project supports generating code coverage reports using `gcovr` to ensure that our tests thoroughly exercise the codebase. + +**To build with coverage enabled:** +You need to pass the `NEURON_IDE_ENABLE_COVERAGE` flag during the CMake configuration step. + +```bash +# Generate the build system with coverage instrumentation enabled +cmake -B build -DNEURON_IDE_ENABLE_COVERAGE=ON + +# Compile the project +cmake --build build +``` + +**To run coverage and generate reports:** +Once built with the coverage flag, you can run the `coverage` target. This will automatically execute the test suite and then invoke `gcovr` to generate both a terminal summary and detailed HTML reports. + +```bash +cmake --build build --target coverage +``` + +**To check the coverage reports:** +After running the coverage target, the generated HTML files will be located in the `build/coverage` directory. You can open `index.html` in your favorite web browser to explore detailed line-by-line coverage for each file. + +```bash +# Open the main coverage report in your default web browser +xdg-open build/coverage/index.html ``` \ No newline at end of file From 0d45fc93fee990cc83ae60ac86023b0d7c1645e9 Mon Sep 17 00:00:00 2001 From: Michal Date: Fri, 26 Jun 2026 16:28:15 +0200 Subject: [PATCH 4/5] docs: update readme --- README.md | 328 ++++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 271 insertions(+), 57 deletions(-) diff --git a/README.md b/README.md index dd43762..149838c 100644 --- a/README.md +++ b/README.md @@ -1,99 +1,313 @@ -# Neuron-IDE-runtime +# NeuronIDE Runtime -## Setup and Build Instructions +> Single source of project context. This document is intentionally written to be +> self-contained so that a person *or a language model* can understand the goal, +> the architecture, the current implementation status, and how to build/test the +> project without reading the source first. -This project uses CMake (minimum version 3.25) and requires a modern C++20 compiler. +## 1. What this project is -### 1. Clone repo and install required dependencies -This project uses GTest, LSL, SDL2, Clang-format, Clang-tidy-20, protobuf-compiler, gcovr and cmake, but you don't need to install GTest and LSL because cmake will install it for you. Below is minimal linux setup. +**NeuronIDE** is a tool for designing and running **EEG experiments**. It has two parts: -```bash -git clone git@github.com:KN-Neuron/Neuron-IDE-runtime.git +- an **editor** (out of scope for this repository) that lets a researcher visually + design an experiment and serializes it to a file, and +- a **runtime** (this repository, `Neuron-IDE-runtime`) that reads that file, + reconstructs the experiment as a scene, runs it, and records the data. -sudo apt update -sudo apt install cmake clang-format clang-tidy-20 libsdl2-dev protobuf-compiler gcovr -``` +Running an experiment means: -### 2. Build the Project -To configure and compile the project, open your terminal in the root directory and run: +1. **Reading EEG data** from the participant's headset (the "cap") in real time. +2. **Rendering a graphical interface** that drives the participant — e.g. telling + them what to focus on, or, for **SSVEP** experiments, flickering on-screen + objects at controlled frequencies. +3. **Emitting time markers** for experiment events (e.g. "stimulus shown"), + stamped at the exact moment the corresponding frame hits the screen. +4. **Persisting** the EEG samples and the markers, with timestamps, to an output + file for later offline analysis. -```bash -# Generate the build system in the "build" directory -cmake -B build +The hard requirement that shapes the whole design is **temporal alignment**: the +EEG samples and the stimulus markers must share a **common clock domain** so that +"this brain response happened N ms after that stimulus" is meaningful. See +[§4 Clock synchronization](#4-clock-synchronization-critical). -# Compile the project -cmake --build build +## 2. Tech stack + +| Concern | Choice | +| ------------------------ | ---------------------------------------------------------------------- | +| Language | C++20 | +| Build system | CMake (≥ 3.25), Ninja-friendly | +| Experiment file format | Protocol Buffers (proto3) — see `protoFiles/neuronide.proto` | +| EEG acquisition | [LSL — Lab Streaming Layer](https://github.com/sccn/liblsl) (`liblsl`) | +| Rendering / windowing | SDL2 (+ SDL2_image), with vsync | +| Inter-thread queues | [moodycamel ConcurrentQueue](https://github.com/cameron314/concurrentqueue) (lock-free) | +| Python scripting (planned)| pybind11 (`ScriptComponent`) | +| Testing | GoogleTest + CTest | +| Tooling | clang-format, clang-tidy, gcovr (coverage) | + +`liblsl`, `concurrentqueue`, and `googletest` are fetched automatically by CMake +(`FetchContent`). SDL2 and Protobuf are expected to be installed on the system. + +## 3. Architecture + +The runtime starts by parsing the experiment file into a **Scene**, then runs the +experiment across cooperating threads coordinated by a top-level `Runtime` object. +The design follows a **game-engine-like component model**: a `Scene` owns +`SceneObject`s, and each `SceneObject` owns `Component`s that implement behavior. + +### Data flow + +```mermaid +flowchart LR + File[Experiment file
protobuf] --> Parser + Parser -->|builds| Scene + + subgraph Threads + Renderer + LSLReader + DataWriter + end + + Scene --> Renderer + Cap[EEG headset
LSL stream] --> LSLReader + + Renderer -->|Marker| MQ[(markerQueue)] + LSLReader -->|EEGData| EQ[(eegQueue)] + + EQ --> DataWriter + MQ --> DataWriter + DataWriter --> Out[Output file
CSV] ``` -Once built, the main executable will be available inside the `build` directory. +- **Parser** reads the protobuf experiment file and produces a `Scene` + (objects + components). Stateless entry point: `Parser::parse(path)`. +- **Renderer** owns the SDL window, runs the update→render loop, and uses **vsync** + so it can timestamp markers at the *actual* moment of screen refresh. Components + emit marker names during `update`; the Renderer stamps them with + `lsl::local_clock()` right after `SDL_RenderPresent` and pushes `Marker`s onto + `markerQueue`. +- **LSLReader** resolves and subscribes to the EEG LSL stream and continuously + pushes `EEGData` samples onto `eegQueue`. +- **DataWriter** drains both queues and writes them to disk via a pluggable + formatting strategy (currently CSV). -### 3. Running Tests -This project uses Google Test (GTest) and CTest for testing. The tests are categorized by labels so you can run them selectively. +The two queues (`eegQueue`, `markerQueue`) are the only shared state between +threads and are **lock-free** (`moodycamel::ConcurrentQueue`). Producers +(Renderer, LSLReader) never block on consumers (DataWriter). -**To run all tests:** -```bash -cd build -ctest --output-on-failure +### Component model + +```mermaid +classDiagram + class Scene { + -string experimentName + -vector~shared_ptr~SceneObject~~ objects + +update(Context&) void + +render(SDL_Renderer*) void + } + class SceneObject { + +string name + +bool isVisible + +Transform transform + -vector~unique_ptr~Component~~ components + +update(Context&) void + +render(SDL_Renderer*) void + } + class Component { + <> + #weak_ptr~SceneObject~ owner + +update(Context&)* void + +render(SDL_Renderer*)* void + } + class BlinkComponent + class SpriteRenderer + class TextRenderer + class ScriptComponent + + Scene *-- SceneObject + SceneObject *-- Component + Component <|-- BlinkComponent + Component <|-- SpriteRenderer + Component <|-- TextRenderer + Component <|-- ScriptComponent ``` -**To run only unit tests:** -```bash -cd build -ctest -L unit --output-on-failure +Components are built from protobuf messages by a **`ComponentRegistry`** (a +singleton mapping a proto component type id → factory function). New component +types self-register via the `REGISTER_COMPONENT(typeId, creatorFunc)` macro, so +the parser does not need to know about concrete component classes. + +### Core data structures + +```cpp +struct EEGData { // one EEG sample + double timestamp; // in the local_clock() domain (see §4) + std::vector channels; +}; + +struct Marker { // one experiment event + std::string eventName; + double timestamp; // local_clock() at vsync +}; + +struct Context { // passed to Component::update each frame + double timestamp; // per-frame delta time + std::vector* markers; // sink: components append marker names here +}; ``` -**To run only component tests:** -```bash -cd build -ctest -L component --output-on-failure +## 4. Clock synchronization (critical) + +EEG analysis depends on EEG samples and stimulus markers being **comparable in +time**. The Renderer stamps markers with `lsl::local_clock()` (this machine's +clock). LSL `pull_sample()`, however, returns timestamps in the **sender's** clock +domain. If left unconverted, samples and markers would live in different clocks +and could not be aligned. + +`LSLReader` therefore enables LSL inlet **post-processing** +(`post_clocksync | post_dejitter | post_monotonize`) so the timestamps it stores +are already mapped into the local `lsl::local_clock()` domain — the same clock the +Renderer uses. This is the single most important correctness property of the data +path. + +## 5. Thread lifecycle conventions + +Threads use C++20 `std::jthread` + `std::stop_token` for cooperative cancellation. +Two ownership patterns are in use: + +- **Self-owned thread** (`LSLReader`, `DataWriter`): the class owns its `jthread`. + `start(...)` spawns the worker (and first calls `stop()` so it is restartable); + `stop()` requests stop, joins, and releases resources; the destructor calls + `stop()`. The worker loop body takes the `stop_token`. +- **Caller-owned thread** (`Renderer`): the class exposes `render(stop_token)` and + the *caller* owns the thread driving it. This makes the loop trivially testable + by passing an injected `std::stop_source`. + +`LSLReader` additionally resolves its stream **lazily on the worker thread** (so +`start()` returns immediately instead of blocking the runtime while waiting for the +cap), uses a **blocking pull with a finite timeout** (no busy-wait, low latency, +periodic stop-token checks), and catches `lsl::lost_error` to re-resolve a dropped +stream rather than letting an exception terminate the process. + +## 6. Implementation status + +| Area / class | Status | Notes | +| --------------------------- | ------------- | ------------------------------------------------------------ | +| `Parser` | Implemented | protobuf → `Scene` | +| `Scene` / `SceneObject` | Implemented | component containers | +| `Component` (base) | Implemented | abstract `update` / `render` | +| `ComponentRegistry` | Implemented | proto-type → factory, macro-based self-registration | +| `BlinkComponent` | Implemented | SSVEP flicker | +| `SpriteRenderer` / `TextRenderer` / `ScriptComponent` | **Planned** | defined in `neuronide.proto`, not yet implemented in C++ | +| `Renderer` | Implemented | SDL + vsync, marker timestamping | +| `LSLReader` | Implemented | LSL inlet → `eegQueue`, clock-synced (see §4) | +| `DataWriter` | Implemented | strategy-based; `CSVFormatStrategy` | +| `Runtime` orchestration | **Stub** | `Runtime::start()` currently only prints; wiring of Parser + the three threads is the next integration step | + +The class diagram in older docs is partly aspirational; the table above reflects +the actual code. + +## 7. Repository layout + +``` +neuronide/ +├── README.md # this file (project context) +├── requirements.md # tooling notes, conventions (PL) +├── ROADMAP.md # per-component milestones (PL) +└── Neuron-IDE-runtime/ # the C++ runtime (git repo) + ├── CMakeLists.txt # top-level: deps, warnings, static analysis, coverage + ├── cmake/ # Dependencies / CompilerWarnings / StaticAnalysis / Coverage + ├── protoFiles/ + │ ├── neuronide.proto # experiment file schema + │ └── tests/ # .pbtxt fixtures + compiled .pb + ├── include/ # public headers, mirrored by src/ + │ ├── data_structures/ # EEGData, Marker, Context + │ ├── parser/ # Parser + │ ├── scene/ # Scene, SceneObject, components/ + │ ├── renderer/ # Renderer + │ ├── lslreader/ # LSLReader + │ ├── datawriter/ # DataWriter, IDataFormatStrategy, CSVFormatStrategy + │ └── Runtime.hpp + ├── src/ # one CMake subdirectory (static lib) per module + └── tests/ + ├── unit_tests/ # CTest label: "unit" + └── component_tests/ # CTest label: "component" ``` -### 4. Code Formatting +Each `src//` builds a static library; `runtime_core` links them together +and the `NeuronIDE` executable links `runtime_core`. -This project includes a custom CMake target to automatically format all C++ source files using `clang-format`. +## 8. Build, test, and tooling + +All commands are run from the `Neuron-IDE-runtime/` directory. + +### Dependencies (Linux) -**To format your code:** ```bash -# Assuming you have already configured the build directory -cmake --build build --target format +sudo apt update +sudo apt install cmake clang-format clang-tidy libsdl2-dev protobuf-compiler gcovr +# GTest, LSL and concurrentqueue are fetched automatically by CMake. ``` -This will run `clang-format -i` over all trackable source and header files, applying the style configuration specified in the project. +### Configure & build -### 5. Working with protobuf files +```bash +cmake -B build +cmake --build build +./build/src/NeuronIDE # run the (currently stub) executable +``` -In case you want to create an example .pb file for testing or some other purpose, you have to first create a .pbtxt file (check out protoFiles/tests/test_scene.pbtxt for reference) which you then compile with this command: +### Tests ```bash -protoc --encode=NeuronIDE.Scene protoFiles/neuronide.proto < protoFiles/tests/test_scene.pbtxt > protoFiles/tests/test_scene.pb +cd build +ctest --output-on-failure # all tests +ctest -L unit --output-on-failure # unit tests only +ctest -L component --output-on-failure ``` -### 6. Code Coverage +> Note: `LSLReader` unit tests open a local LSL stream and exercise a real +> outlet→inlet round-trip over loopback; they need loopback multicast to be +> available. + +### Formatting & static analysis -This project supports generating code coverage reports using `gcovr` to ensure that our tests thoroughly exercise the codebase. +- Style: `.clang-format` (Google base, 4-space indent, 100 col, aligned + declarations/assignments). Checks: `.clang-tidy` + (`bugprone-*`, `cppcoreguidelines-*`, `performance-*`, `readability-*`). +- Auto-format every tracked source/header: + ```bash + cmake --build build --target format + ``` -**To build with coverage enabled:** -You need to pass the `NEURON_IDE_ENABLE_COVERAGE` flag during the CMake configuration step. +### Coverage ```bash -# Generate the build system with coverage instrumentation enabled cmake -B build -DNEURON_IDE_ENABLE_COVERAGE=ON - -# Compile the project cmake --build build +cmake --build build --target coverage # runs tests + gcovr +xdg-open build/coverage/index.html ``` -**To run coverage and generate reports:** -Once built with the coverage flag, you can run the `coverage` target. This will automatically execute the test suite and then invoke `gcovr` to generate both a terminal summary and detailed HTML reports. +### Working with protobuf fixtures + +Author a `.pbtxt` (see `protoFiles/tests/test_scene.pbtxt`), then encode it: ```bash -cmake --build build --target coverage +protoc --encode=NeuronIDE.Scene protoFiles/neuronide.proto \ + < protoFiles/tests/test_scene.pbtxt > protoFiles/tests/test_scene.pb ``` -**To check the coverage reports:** -After running the coverage target, the generated HTML files will be located in the `build/coverage` directory. You can open `index.html` in your favorite web browser to explore detailed line-by-line coverage for each file. +## 9. Contribution conventions -```bash -# Open the main coverage report in your default web browser -xdg-open build/coverage/index.html -``` \ No newline at end of file +- **Branches:** `/`, e.g. `feat/setup-project`. +- **Commits:** `(optional scope): description`, e.g. + `feat(parser): create Parser class`. +- **Types:** `feat`, `fix`, `style` (clang config), `test`, `ci` (`.github`). + +## 10. Roadmap (next steps) + +1. Implement `Runtime` orchestration: `Parser → Scene`, then run `Renderer`, + `LSLReader`, and `DataWriter` concurrently and shut them down cleanly. +2. Implement the remaining components: `SpriteRenderer`, `TextRenderer`, + `ScriptComponent` (pybind11), each self-registering with `ComponentRegistry`. +3. Validate `LSLReader` end-to-end against a real EEG headset. From 7deafaef79ad130c8b4d1e300c361eeaaefcdaa3 Mon Sep 17 00:00:00 2001 From: Michal Date: Fri, 26 Jun 2026 16:35:14 +0200 Subject: [PATCH 5/5] docs: fix readme errors --- README.md | 44 ++++++++++++++++++++------------------------ 1 file changed, 20 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index 149838c..88c9546 100644 --- a/README.md +++ b/README.md @@ -196,8 +196,7 @@ stream rather than letting an exception terminate the process. | `Scene` / `SceneObject` | Implemented | component containers | | `Component` (base) | Implemented | abstract `update` / `render` | | `ComponentRegistry` | Implemented | proto-type → factory, macro-based self-registration | -| `BlinkComponent` | Implemented | SSVEP flicker | -| `SpriteRenderer` / `TextRenderer` / `ScriptComponent` | **Planned** | defined in `neuronide.proto`, not yet implemented in C++ | +| `specifiic components` | **Planned** | defined in `neuronide.proto`, not yet implemented in C++ | | `Renderer` | Implemented | SDL + vsync, marker timestamping | | `LSLReader` | Implemented | LSL inlet → `eegQueue`, clock-synced (see §4) | | `DataWriter` | Implemented | strategy-based; `CSVFormatStrategy` | @@ -209,28 +208,25 @@ the actual code. ## 7. Repository layout ``` -neuronide/ -├── README.md # this file (project context) -├── requirements.md # tooling notes, conventions (PL) -├── ROADMAP.md # per-component milestones (PL) -└── Neuron-IDE-runtime/ # the C++ runtime (git repo) - ├── CMakeLists.txt # top-level: deps, warnings, static analysis, coverage - ├── cmake/ # Dependencies / CompilerWarnings / StaticAnalysis / Coverage - ├── protoFiles/ - │ ├── neuronide.proto # experiment file schema - │ └── tests/ # .pbtxt fixtures + compiled .pb - ├── include/ # public headers, mirrored by src/ - │ ├── data_structures/ # EEGData, Marker, Context - │ ├── parser/ # Parser - │ ├── scene/ # Scene, SceneObject, components/ - │ ├── renderer/ # Renderer - │ ├── lslreader/ # LSLReader - │ ├── datawriter/ # DataWriter, IDataFormatStrategy, CSVFormatStrategy - │ └── Runtime.hpp - ├── src/ # one CMake subdirectory (static lib) per module - └── tests/ - ├── unit_tests/ # CTest label: "unit" - └── component_tests/ # CTest label: "component" +Neuron-IDE-runtime/ # the C++ runtime (git repo) + ├── README.md # this file, code context + ├── CMakeLists.txt # top-level: deps, warnings, static analysis, coverage + ├── cmake/ # Dependencies / CompilerWarnings / StaticAnalysis / Coverage + ├── protoFiles/ + │ ├── neuronide.proto # experiment file schema + │ └── tests/ # .pbtxt fixtures + compiled .pb + ├── include/ # public headers, mirrored by src/ + │ ├── data_structures/ # EEGData, Marker, Context + │ ├── parser/ # Parser + │ ├── scene/ # Scene, SceneObject, components/ + │ ├── renderer/ # Renderer + │ ├── lslreader/ # LSLReader + │ ├── datawriter/ # DataWriter, IDataFormatStrategy, CSVFormatStrategy + │ └── Runtime.hpp + ├── src/ # one CMake subdirectory (static lib) per module + └── tests/ + ├── unit_tests/ # CTest label: "unit" + └── component_tests/ # CTest label: "component" ``` Each `src//` builds a static library; `runtime_core` links them together