diff --git a/include/data_structures/Context.hpp b/include/data_structures/Context.hpp new file mode 100644 index 0000000..da103ff --- /dev/null +++ b/include/data_structures/Context.hpp @@ -0,0 +1,12 @@ +#ifndef CONTEXT_HPP +#define CONTEXT_HPP + +#include +#include + +struct Context { + double timestamp; + std::vector& markers; +}; + +#endif // CONTEXT_HPP \ No newline at end of file diff --git a/include/data_structures/EEGData.hpp b/include/data_structures/EEGData.hpp new file mode 100644 index 0000000..58eb0a6 --- /dev/null +++ b/include/data_structures/EEGData.hpp @@ -0,0 +1,11 @@ +#ifndef EEGDATA_HPP +#define EEGDATA_HPP + +#include + +struct EEGData { + double timestamp; + std::vector channelValues; +}; + +#endif // EEGDATA_HPP \ No newline at end of file diff --git a/include/data_structures/Marker.hpp b/include/data_structures/Marker.hpp new file mode 100644 index 0000000..6933e2f --- /dev/null +++ b/include/data_structures/Marker.hpp @@ -0,0 +1,11 @@ +#ifndef MARKER_HPP +#define MARKER_HPP + +#include + +struct Marker { + std::string name; + double timestamp; +}; + +#endif // MARKER_HPP \ No newline at end of file diff --git a/include/renderer/Renderer.hpp b/include/renderer/Renderer.hpp new file mode 100644 index 0000000..267c810 --- /dev/null +++ b/include/renderer/Renderer.hpp @@ -0,0 +1,33 @@ +#ifndef RENDERER_HPP +#define RENDERER_HPP + +#include +#include + +#include +#include + +class Scene; +struct Marker; + +class Renderer { + public: + Renderer() = delete; + Renderer(const std::shared_ptr& scene, std::shared_ptr sdlRenderer, + std::shared_ptr> markerQueue); + ~Renderer() = default; + + void render(const std::stop_token& stoken); + + private: + struct SDLWindowDeleter { + void operator()(SDL_Window* window) const; + }; + std::unique_ptr window; + std::shared_ptr sdlRenderer; + std::weak_ptr currentScene; + std::shared_ptr> markerQueue; + std::jthread renderThread; +}; + +#endif // RENDERER_HPP \ No newline at end of file diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 62cd34d..2d94a45 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -7,6 +7,7 @@ add_library(runtime_core OBJECT scene/components/BlinkComponent.cpp scene/SceneObject.cpp scene/Scene.cpp + renderer/Renderer.cpp ${PROTO_SRCS} ${PROTO_HDRS} ) diff --git a/src/renderer/Renderer.cpp b/src/renderer/Renderer.cpp new file mode 100644 index 0000000..f57b239 --- /dev/null +++ b/src/renderer/Renderer.cpp @@ -0,0 +1,58 @@ +#include "renderer/Renderer.hpp" + +#include "data_structures/Context.hpp" +#include "data_structures/Marker.hpp" +#include "lsl_cpp.h" +#include "scene/Scene.hpp" + +void Renderer::SDLWindowDeleter::operator()(SDL_Window* window) const { + if (window) { + SDL_DestroyWindow(window); + } +} + +Renderer::Renderer(const std::shared_ptr& scene, std::shared_ptr sdlRenderer, + std::shared_ptr> markerQueue) + : window(nullptr, SDLWindowDeleter{}), + sdlRenderer(std::move(sdlRenderer)), + currentScene(scene), + markerQueue(std::move(markerQueue)) {} + +void Renderer::render(const std::stop_token& stoken) { + std::vector currentFrameMarkers; + auto lastTime = std::chrono::high_resolution_clock::now(); + + while (!stoken.stop_requested()) { + auto currentTime = std::chrono::high_resolution_clock::now(); + double deltaTime = std::chrono::duration(currentTime - lastTime).count(); + lastTime = currentTime; + + currentFrameMarkers.clear(); + Context ctx{deltaTime, currentFrameMarkers}; + + SDL_Event event; + while (SDL_PollEvent(&event) == 1) { + if (event.type == SDL_QUIT) { + return; + } + } + + if (auto scene = currentScene.lock()) { + scene->update(ctx); + } + + SDL_RenderClear(sdlRenderer.get()); + + if (auto scene = currentScene.lock()) { + scene->render(sdlRenderer.get()); + } + + SDL_RenderPresent(sdlRenderer.get()); + + double exactTime = lsl::local_clock(); + + for (const auto& markerName : currentFrameMarkers) { + markerQueue->enqueue(Marker{markerName, exactTime}); + } + } +} diff --git a/tests/unit_tests/RendererTest.cpp b/tests/unit_tests/RendererTest.cpp new file mode 100644 index 0000000..25b6711 --- /dev/null +++ b/tests/unit_tests/RendererTest.cpp @@ -0,0 +1,141 @@ +#include + +#include +#include +#include + +#include "data_structures/Context.hpp" +#include "data_structures/Marker.hpp" +#include "renderer/Renderer.hpp" +#include "scene/Scene.hpp" +#include "scene/SceneObject.hpp" +#include "scene/components/Component.hpp" + +namespace { +constexpr int kDummySurfaceWidth = 10; +constexpr int kDummySurfaceHeight = 10; +constexpr int kDummySurfaceDepth = 32; +constexpr uint32_t kDummySurfaceFlags = 0; + +class CustomComponent : public Component { + public: + CustomComponent(std::shared_ptr owner, std::shared_ptr> updates, + std::shared_ptr> renders, + std::shared_ptr stopSource) + : Component(std::move(owner)), + updates(std::move(updates)), + renders(std::move(renders)), + stopSource(std::move(stopSource)) {} + + void update(const Context& context) override { + (void)context; + (*updates)++; + + stopSource->request_stop(); + } + + void render(SDL_Renderer* renderer) override { + (void)renderer; + (*renders)++; + } + + private: + std::shared_ptr> updates; + std::shared_ptr> renders; + std::shared_ptr stopSource; +}; + +class MarkerComponent : public Component { + public: + MarkerComponent(std::shared_ptr owner, + std::shared_ptr stopSource) + : Component(std::move(owner)), stopSource(std::move(stopSource)) {} + + void update(const Context& context) override { + context.markers.push_back("test_marker"); + stopSource->request_stop(); + } + + void render(SDL_Renderer* renderer) override { (void)renderer; } + + private: + std::shared_ptr stopSource; +}; + +} // namespace + +TEST(RendererTest, RenderLoop_WhenComponentAdded_CallsUpdateExactlyOnceBeforeStop) { + ASSERT_EQ(SDL_Init(SDL_INIT_EVENTS), 0); + + auto scene = std::make_shared(); + auto obj = std::make_shared("obj"); + + auto updates = std::make_shared>(0); + auto renders = std::make_shared>(0); + auto stop_source = std::make_shared(); + + obj->addComponent(std::make_unique(obj, updates, renders, stop_source)); + scene->addObject(obj); + + SDL_Surface* surface = + 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); + } + if (surface) { + SDL_FreeSurface(surface); + } + }); + + auto markerQueue = std::make_shared>(); + + Renderer renderer(scene, sharedRenderer, markerQueue); + renderer.render(stop_source->get_token()); + + EXPECT_EQ(*updates, 1); + EXPECT_EQ(*renders, 1); + + SDL_Quit(); +} + +TEST(RendererTest, RenderLoop_QueuesMarkersFromComponents) { + ASSERT_EQ(SDL_Init(SDL_INIT_EVENTS), 0); + + auto scene = std::make_shared(); + auto obj = std::make_shared("obj"); + auto stop_source = std::make_shared(); + + obj->addComponent(std::make_unique(obj, stop_source)); + scene->addObject(obj); + + SDL_Surface* surface = + 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); + } + if (surface) { + SDL_FreeSurface(surface); + } + }); + + auto markerQueue = std::make_shared>(); + + Renderer renderer(scene, sharedRenderer, markerQueue); + renderer.render(stop_source->get_token()); + + Marker m; + bool dequeued = markerQueue->try_dequeue(m); + EXPECT_TRUE(dequeued); + if (dequeued) { + EXPECT_EQ(m.name, "test_marker"); + EXPECT_FALSE(markerQueue->try_dequeue(m)); + } + + SDL_Quit(); +} \ No newline at end of file