#pragma once #include "channel.hpp" #include "diagnostics.hpp" #include "fixed_string.hpp" #include "inode.hpp" #include "port.hpp" #include "scheduler.hpp" #include "traits.hpp" #include #include #include #include #include #include #include #include #include #include namespace kpn { // ── InterruptNode ───────────────────────────────────────────────────────────── // // A source node (zero inputs) driven by an external event — camera frame ready, // timer tick, socket data, etc. — rather than self-submission. // // Usage: // auto node = make_interrupt_node(pool, out<"frame">{}); // camera_sdk.on_frame_ready(node.get_trigger()); // register with external source // network.add("camera", node).connect(...).build().start(); // // The trigger callable is safe to call from any thread, including signal handlers, // provided the underlying scheduler's submit() is signal-safe. After fire_once() // completes the node is idle until the next trigger fires — it does NOT busy-loop. template, fixed_string Label = "", std::size_t UniqueTag = 0> class InterruptNode; template class InterruptNode, Label, UniqueTag> : public INode { public: using F = decltype(Func); using return_raw = return_t; using return_tuple = normalised_return_t; static_assert(arity_v == 0, "InterruptNode function must take no arguments (it has no input channels)"); static constexpr std::string_view label() { return Label.view(); } static constexpr std::size_t unique_tag = UniqueTag; static constexpr std::size_t input_count = 0; static constexpr std::size_t output_count = std::tuple_size_v; static_assert( sizeof...(OutNames) == 0 || sizeof...(OutNames) == output_count, "make_interrupt_node: number of output names must match return tuple size, or provide none" ); explicit InterruptNode(std::shared_ptr sched, std::size_t fifo_capacity = 5) : scheduler_(std::move(sched)), fifo_capacity_(fifo_capacity) {} ~InterruptNode() override { stop(); } // ── INode ───────────────────────────────────────────────────────────────── void start() override { stop_flag_.store(false, std::memory_order_relaxed); pending_.store(0, std::memory_order_relaxed); // Does NOT self-submit — waits for first external trigger. } void stop() override { stop_flag_.store(true, std::memory_order_seq_cst); // In-flight fire_once() observes stop_flag_ on its next check. } bool running() const override { return !stop_flag_.load(std::memory_order_relaxed); } void set_name(std::string name) override { name_ = std::move(name); } void set_error_handler(NodeErrorHandler h) { error_handler_ = std::move(h); } void set_max_exec_time(std::chrono::milliseconds t) { max_exec_time_ = t; } 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 qwait_ms = stats_.queue_wait_us.load(std::memory_order_relaxed) / 1000.0; double total_ms = exec_ms; // no blocked time for interrupt nodes return { name, frames, exec_ms, stats_.max_exec_us.load(std::memory_order_relaxed) / 1000.0, 0.0, // blocked_ms — not applicable elapsed_s > 0 ? frames / elapsed_s : 0.0, stats_.total_cpu_us.load(std::memory_order_relaxed) / 1000.0, total_ms > 0 ? 100.0 : 0.0, qwait_ms, }; } // ── Port access — by index ──────────────────────────────────────────────── template OutputPort output() { static_assert(I < output_count, "output index out of range"); return {*this}; } template auto output() { constexpr std::size_t idx = index_of(); static_assert(idx != npos, "unknown output port name"); return output(); } template void set_output_channel(Channel>* ch) { std::get(output_channels_) = ch; } // ── Trigger ─────────────────────────────────────────────────────────────── // Returns a callable that fires this node when called. // Pass it to a camera SDK, timer, or any external event source. // Thread-safe; may be called from any thread. std::function get_trigger() { return [this] { trigger(); }; } private: // Each trigger() increments pending_. When going 0→1 a task is submitted. // Each fire_once() handles one pending event and decrements; if more remain // (old value > 1) it resubmits itself. This guarantees every trigger produces // exactly one execution even if triggers arrive faster than fire_once completes. void trigger() { if (stop_flag_.load(std::memory_order_relaxed)) return; if (pending_.fetch_add(1, std::memory_order_acq_rel) == 0) scheduler_->submit([this] { fire_once(); }); } void fire_once() { if (stop_flag_.load(std::memory_order_relaxed)) { pending_.store(0, std::memory_order_release); return; } auto t0 = clock_t::now(); int64_t now_us = std::chrono::duration_cast( t0.time_since_epoch()).count(); stats_.exec_start_us.store(now_us, std::memory_order_relaxed); bool fatal = false; try { auto t1 = clock_t::now(); stats_.record_queue_wait(duration_t(t1 - t0)); auto cpu0 = NodeStats::cpu_now(); if constexpr (std::is_void_v) { Func(); } else { auto result = Func(); push_outputs(normalise(std::move(result)), std::make_index_sequence{}); } auto cpu1 = NodeStats::cpu_now(); auto t2 = clock_t::now(); stats_.record_exec(duration_t(t2 - t1), duration_t::zero(), cpu0, cpu1); } catch (const ChannelOverflowError& e) { std::cerr << "[kpn] interrupt node overflow: " << e.what() << "\n"; } catch (...) { if (!error_handler_ || !error_handler_(name_, std::current_exception())) fatal = true; } stats_.exec_start_us.store(0, std::memory_order_relaxed); if (fatal) { pending_.store(0, std::memory_order_release); stop_flag_.store(true, std::memory_order_relaxed); return; } // Decrement and resubmit only if more triggers are queued. // fetch_sub returns old value; old > 1 means new > 0. if (pending_.fetch_sub(1, std::memory_order_acq_rel) > 1) scheduler_->submit([this] { fire_once(); }); } template static return_tuple normalise(R&& r) { if constexpr (is_tuple_v) return std::move(r); else return std::make_tuple(std::move(r)); } template void push_outputs(return_tuple&& result, std::index_sequence) { (push_one(std::get(std::move(result))), ...); } template void push_one(std::tuple_element_t&& val) { auto* ch = std::get(output_channels_); if (!ch) return; try { ch->push(std::move(val)); } catch (const ChannelOverflowError&) { throw ChannelOverflowError(ch->capacity(), "interrupt node '" + name_ + "' " + output_port_label()); } } template static std::string output_port_label() { if constexpr (sizeof...(OutNames) > 0) { constexpr std::array names{OutNames.view()...}; return std::string("output['") + std::string(names[I]) + "']"; } else { return "output[" + std::to_string(I) + "]"; } } template static auto make_output_channel_tuple(std::index_sequence) -> std::tuple>*...>; using output_channels_t = decltype(make_output_channel_tuple( std::make_index_sequence{})); std::shared_ptr scheduler_; std::string name_; std::size_t fifo_capacity_; output_channels_t output_channels_{}; std::atomic stop_flag_{true}; std::atomic pending_{0}; // triggers awaiting execution NodeStats stats_; NodeErrorHandler error_handler_; std::chrono::milliseconds max_exec_time_{0}; }; // ── make_interrupt_node factory ─────────────────────────────────────────────── template auto make_interrupt_node(std::shared_ptr sched, std::size_t fifo_capacity = 5) { return InterruptNode, Label, UniqueTag>(std::move(sched), fifo_capacity); } template auto make_interrupt_node(std::shared_ptr sched, out, std::size_t fifo_capacity = 5) { return InterruptNode, Label, UniqueTag>( std::move(sched), fifo_capacity); } } // namespace kpn