#pragma once #include "channel.hpp" #include "diagnostics.hpp" #include "fixed_string.hpp" #include "node.hpp" // INode #include "port.hpp" #include #include #include #include #include #include namespace kpn { // ── MainThreadNode ──────────────────────────────────────────────────────────── // // Base class for nodes that must run on the main thread (e.g. OpenCV display // on Wayland/Qt). Registered as a normal INode in the Network so it appears // in diagnostics and the web UI, but spawns no thread. // // Usage: // class MyDisplay : public kpn::MainThreadNode, TypeA, TypeB> { // public: // MyDisplay(...) { /* constructor runs on main thread */ } // bool operator()(TypeA a, TypeB b) { ...; return true; /* false = stop */ } // }; // // MyDisplay disp(...); // net.add("display", disp).connect(...).build(); // net.start(); // while (disp.step()) ; // drives the event loop on the main thread // net.stop(); // // step() behaviour: // - try_pop on every input channel with zero timeout // - if all inputs have data: calls operator(), records stats, returns its result // - if any input is missing: returns true immediately (caller should yield/waitKey) template class MainThreadNode; template class MainThreadNode, Args...> : public INode { public: static constexpr std::size_t input_count = sizeof...(Args); static_assert( sizeof...(InNames) == 0 || sizeof...(InNames) == input_count, "MainThreadNode: name count must match input type count, or provide none" ); using args_tuple = std::tuple; // required by Network::connect type check explicit MainThreadNode(std::size_t fifo_capacity = 8) { init_channels(std::make_index_sequence{}, fifo_capacity); } // ── INode ───────────────────────────────────────────────────────────────── void start() override { enable_channels(std::make_index_sequence{}); running_.store(true, std::memory_order_relaxed); } void stop() override { running_.store(false, std::memory_order_relaxed); disable_channels(std::make_index_sequence{}); } bool running() const override { return running_.load(std::memory_order_relaxed); } void set_name(std::string) override {} const NodeStats& stats() const override { return stats_; } NodeSnapshot node_snapshot(const std::string& name, double elapsed_s) const override { uint64_t frames = stats_.frames_processed.load(std::memory_order_relaxed); double exec_ms = stats_.ema_exec_us.load(std::memory_order_relaxed) / 1000.0; double blocked_ms = stats_.total_blocked_us.load(std::memory_order_relaxed) / 1000.0; double total_ms = exec_ms + blocked_ms; return { name, frames, exec_ms, stats_.max_exec_us.load(std::memory_order_relaxed) / 1000.0, blocked_ms, elapsed_s > 0 ? frames / elapsed_s : 0.0, stats_.total_cpu_us.load(std::memory_order_relaxed) / 1000.0, total_ms > 0 ? 100.0 * exec_ms / total_ms : 0.0, }; } // ── Port access (for Network::connect) ─────────────────────────────────── template Channel>>& input_channel() { return *std::get(channels_); } template InputPort input() { static_assert(I < input_count, "input index out of range"); return {*this}; } template auto input() { constexpr std::size_t idx = index_of(); static_assert(idx != npos, "unknown input port name"); return input(); } // ── Main-thread driver ──────────────────────────────────────────────────── // Call this in a loop on the main thread instead of net.start()'s thread. // Returns false when operator() returns false or all channels are closed. bool step() { if (!running_.load(std::memory_order_relaxed)) return false; auto t0 = clock_t::now(); auto inputs = try_pop_all(std::make_index_sequence{}); auto t1 = clock_t::now(); if (!inputs.has_value()) return true; // not all inputs ready — yield auto cpu0 = NodeStats::cpu_now(); bool cont = std::apply( [this](Args&&... a) { return static_cast(this)->operator()(std::forward(a)...); }, std::move(*inputs)); auto cpu1 = NodeStats::cpu_now(); auto t2 = clock_t::now(); stats_.record_exec(duration_t(t2 - t1), duration_t(t1 - t0), cpu0, cpu1); return cont; } private: template void init_channels(std::index_sequence, std::size_t cap) { ((std::get(channels_) = std::make_unique>>(cap)), ...); } template void enable_channels(std::index_sequence) { (std::get(channels_)->enable(), ...); } template void disable_channels(std::index_sequence) { (std::get(channels_)->disable(), ...); } // Try to pop one item from every channel with zero timeout. // Returns nullopt if any channel has no data ready. template std::optional try_pop_all(std::index_sequence) { args_tuple result; bool all_ready = true; // Use a fold that short-circuits on first missing item ((all_ready = all_ready && std::get(channels_)->try_pop( std::get(result), std::chrono::milliseconds(0))), ...); if (!all_ready) return std::nullopt; return result; } // Build the channel tuple type template static auto make_channel_tuple(std::index_sequence) -> std::tuple>>...>; using channels_t = decltype(make_channel_tuple( std::make_index_sequence{})); channels_t channels_; std::atomic running_{false}; NodeStats stats_; }; } // namespace kpn