Fixed bug when generating identical fanouts
This commit is contained in:
parent
2bca2a7554
commit
9ce581b5ce
288
examples/13_debug_cellshade/main.cpp
Normal file
288
examples/13_debug_cellshade/main.cpp
Normal file
@ -0,0 +1,288 @@
|
|||||||
|
// Example 13 — Debug Cell-Shading Pipeline with Tiled Debug Canvas
|
||||||
|
//
|
||||||
|
// An improved cell-shading pipeline where every processing node performs
|
||||||
|
// exactly one OpenCV operation. A variadic DebugCanvas<N> node tiles N
|
||||||
|
// cv::Mat inputs into a single debug window, making each pipeline stage
|
||||||
|
// visible side-by-side at runtime.
|
||||||
|
//
|
||||||
|
// Improvements over example 12:
|
||||||
|
// - Bilateral filter before quantisation (edge-preserving smoothing):
|
||||||
|
// flattens colour regions without softening object edges
|
||||||
|
// - Dilated edge mask for bolder black outlines
|
||||||
|
// - 6-level quantisation for richer tonal detail
|
||||||
|
//
|
||||||
|
// Topology (auto-fanouts inserted by make_network):
|
||||||
|
//
|
||||||
|
// [capture]──┬──> [bilateral]──> [quant]──┬──> [comp]──> [debug:0 result]
|
||||||
|
// │ └──> [debug:1 quantised]
|
||||||
|
// ├──> [to_gray]──> [blur]──> [canny]──┬──> [dilate]──> [comp]
|
||||||
|
// │ └──> [debug:2 edges]
|
||||||
|
// └──> [debug:3 original]
|
||||||
|
//
|
||||||
|
// make_network detects the 3-way fan from [capture], the 2-way fan from
|
||||||
|
// [quant], and the 2-way fan from [canny], inserting FanoutNode instances
|
||||||
|
// automatically.
|
||||||
|
|
||||||
|
#include <kpn/kpn.hpp>
|
||||||
|
#include <opencv2/core.hpp>
|
||||||
|
#include <opencv2/imgproc.hpp>
|
||||||
|
#include <opencv2/highgui.hpp>
|
||||||
|
#include <opencv2/videoio.hpp>
|
||||||
|
#include <algorithm>
|
||||||
|
#include <chrono>
|
||||||
|
#include <cmath>
|
||||||
|
#include <iostream>
|
||||||
|
#include <string>
|
||||||
|
#include <thread>
|
||||||
|
#include <type_traits>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
// ── Synthetic source for environments without a webcam ───────────────────────
|
||||||
|
|
||||||
|
static cv::Mat make_gradient(int W, int H) {
|
||||||
|
cv::Mat xr(H, W, CV_8UC1), yg(H, W, CV_8UC1), b(H, W, CV_8UC1, cv::Scalar(128));
|
||||||
|
for (int x = 0; x < W; ++x) xr.col(x).setTo(x * 255 / W);
|
||||||
|
for (int y = 0; y < H; ++y) yg.row(y).setTo(y * 255 / H);
|
||||||
|
cv::Mat channels[3] = {b, yg, xr};
|
||||||
|
cv::Mat grad;
|
||||||
|
cv::merge(channels, 3, grad);
|
||||||
|
return grad;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Pipeline nodes — one cv:: call per function ───────────────────────────────
|
||||||
|
|
||||||
|
static cv::Mat capture() {
|
||||||
|
constexpr int W = 640, H = 480;
|
||||||
|
static cv::VideoCapture cap;
|
||||||
|
static bool opened = false;
|
||||||
|
if (!opened) {
|
||||||
|
opened = true;
|
||||||
|
cap.open(0, cv::CAP_V4L2);
|
||||||
|
if (cap.isOpened()) {
|
||||||
|
cap.set(cv::CAP_PROP_FRAME_WIDTH, W);
|
||||||
|
cap.set(cv::CAP_PROP_FRAME_HEIGHT, H);
|
||||||
|
} else {
|
||||||
|
std::cerr << "[capture] no webcam — using synthetic animated pattern\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cv::Mat frame;
|
||||||
|
if (cap.isOpened()) {
|
||||||
|
auto t0 = std::chrono::steady_clock::now();
|
||||||
|
cap >> frame;
|
||||||
|
auto elapsed = std::chrono::steady_clock::now() - t0;
|
||||||
|
if (elapsed < std::chrono::milliseconds(20))
|
||||||
|
std::this_thread::sleep_for(std::chrono::milliseconds(33) - elapsed);
|
||||||
|
if (frame.empty()) frame = cv::Mat::zeros(H, W, CV_8UC3);
|
||||||
|
} else {
|
||||||
|
static int tick = 0;
|
||||||
|
static cv::Mat grad = make_gradient(W, H);
|
||||||
|
++tick;
|
||||||
|
frame = grad.clone();
|
||||||
|
int r = 150 + (tick % 80) * 4;
|
||||||
|
cv::circle(frame, {W/2, H/2}, r, {255, 200, 0}, -1);
|
||||||
|
cv::circle(frame, {W/2, H/2}, r / 2, { 0, 128, 255}, -1);
|
||||||
|
cv::circle(frame, {W*2/5, H*2/5}, r / 3, {200, 0, 200}, -1);
|
||||||
|
std::this_thread::sleep_for(std::chrono::milliseconds(33));
|
||||||
|
}
|
||||||
|
return frame.clone();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Edge-preserving smooth: flattens colour within regions while keeping sharp
|
||||||
|
// boundaries — much better than Gaussian blur as a pre-quantisation step.
|
||||||
|
static cv::Mat bilateral_filter(cv::Mat bgr) {
|
||||||
|
cv::Mat out;
|
||||||
|
cv::bilateralFilter(bgr, out, 5, 75, 75);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Snap each channel to N discrete tonal levels.
|
||||||
|
static cv::Mat quantise(cv::Mat bgr) {
|
||||||
|
constexpr int levels = 6;
|
||||||
|
constexpr double step = 256.0 / levels;
|
||||||
|
static const cv::Mat lut = []() {
|
||||||
|
cv::Mat l(1, 256, CV_8UC1);
|
||||||
|
for (int i = 0; i < 256; ++i)
|
||||||
|
l.at<uchar>(i) = cv::saturate_cast<uchar>(
|
||||||
|
std::floor(i / step) * step + step / 2.0);
|
||||||
|
return l;
|
||||||
|
}();
|
||||||
|
cv::Mat out;
|
||||||
|
cv::LUT(bgr, lut, out);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
// BGR → greyscale; the edge path works on the original (not bilateral-filtered)
|
||||||
|
// frame so that fine edge detail is preserved.
|
||||||
|
static cv::Mat to_gray(cv::Mat bgr) {
|
||||||
|
cv::Mat gray;
|
||||||
|
cv::cvtColor(bgr, gray, cv::COLOR_BGR2GRAY);
|
||||||
|
return gray;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Suppress high-frequency noise before the Canny detector.
|
||||||
|
static cv::Mat gaussian_blur(cv::Mat gray) {
|
||||||
|
cv::Mat out;
|
||||||
|
cv::GaussianBlur(gray, out, {5, 5}, 0);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Detect strong edges; returns a binary mask (CV_8UC1).
|
||||||
|
static cv::Mat canny_edges(cv::Mat blurred) {
|
||||||
|
cv::Mat out;
|
||||||
|
cv::Canny(blurred, out, 50, 150);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Widen the edge mask for bolder cartoon outlines.
|
||||||
|
static cv::Mat dilate_edges(cv::Mat edges) {
|
||||||
|
static const cv::Mat kernel = cv::getStructuringElement(cv::MORPH_RECT, {3, 3});
|
||||||
|
cv::Mat out;
|
||||||
|
cv::dilate(edges, out, kernel);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Burn black outlines into the quantised colour image.
|
||||||
|
static cv::Mat composite(cv::Mat quantised, cv::Mat thick_edges) {
|
||||||
|
cv::Mat result = quantised.clone();
|
||||||
|
result.setTo(cv::Scalar(0, 0, 0), thick_edges);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── DebugCanvas<N> ────────────────────────────────────────────────────────────
|
||||||
|
//
|
||||||
|
// Variadic MainThreadNode that accepts N cv::Mat inputs (any mix of BGR and
|
||||||
|
// greyscale) and tiles them into one debug window arranged as a
|
||||||
|
// ceil(sqrt(N)) × ceil(N/cols) grid.
|
||||||
|
//
|
||||||
|
// Template trick: MatArg<I> aliases cv::Mat for all I, so the pack expansion
|
||||||
|
// MatArg<0>, MatArg<1>, ..., MatArg<N-1>
|
||||||
|
// produces exactly N cv::Mat arguments — enough to drive the MainThreadNode
|
||||||
|
// base without manually spelling out the type N times.
|
||||||
|
|
||||||
|
template<std::size_t>
|
||||||
|
using MatArg = cv::Mat;
|
||||||
|
|
||||||
|
template<std::size_t N>
|
||||||
|
class DebugCanvas;
|
||||||
|
|
||||||
|
template<std::size_t N, typename Seq = std::make_index_sequence<N>>
|
||||||
|
struct DebugCanvasBase;
|
||||||
|
|
||||||
|
template<std::size_t N, std::size_t... Is>
|
||||||
|
struct DebugCanvasBase<N, std::index_sequence<Is...>> {
|
||||||
|
using type = kpn::MainThreadNode<DebugCanvas<N>, kpn::in<>, MatArg<Is>...>;
|
||||||
|
};
|
||||||
|
|
||||||
|
template<std::size_t N>
|
||||||
|
class DebugCanvas : public DebugCanvasBase<N>::type {
|
||||||
|
using Base = typename DebugCanvasBase<N>::type;
|
||||||
|
public:
|
||||||
|
static constexpr std::string_view label() { return "debug_canvas"; }
|
||||||
|
static constexpr std::size_t unique_tag = 0;
|
||||||
|
|
||||||
|
explicit DebugCanvas(std::vector<std::string> slot_labels = {},
|
||||||
|
std::size_t fifo_cap = 4)
|
||||||
|
: Base(fifo_cap), labels_(std::move(slot_labels))
|
||||||
|
{
|
||||||
|
cv::namedWindow("Debug Canvas", cv::WINDOW_NORMAL);
|
||||||
|
cv::resizeWindow("Debug Canvas", cols() * CW, rows() * CH);
|
||||||
|
}
|
||||||
|
|
||||||
|
~DebugCanvas() { cv::destroyWindow("Debug Canvas"); }
|
||||||
|
|
||||||
|
// Called by MainThreadNode::step() with exactly N cv::Mat arguments.
|
||||||
|
template<typename... Ms>
|
||||||
|
bool operator()(Ms&&... mats) {
|
||||||
|
static_assert(sizeof...(Ms) == N, "DebugCanvas: wrong number of inputs");
|
||||||
|
std::vector<cv::Mat> imgs;
|
||||||
|
imgs.reserve(N);
|
||||||
|
(imgs.push_back(to_bgr(std::forward<Ms>(mats))), ...);
|
||||||
|
|
||||||
|
cv::imshow("Debug Canvas", tile(imgs));
|
||||||
|
int key = cv::waitKey(1);
|
||||||
|
if (key == 'q' || key == 27) return false;
|
||||||
|
try { return cv::getWindowProperty("Debug Canvas", cv::WND_PROP_VISIBLE) >= 1; }
|
||||||
|
catch (const cv::Exception&) { return false; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
static constexpr int CW = 640, CH = 480;
|
||||||
|
|
||||||
|
static int cols() { return std::max(1, (int)std::ceil(std::sqrt((double)N))); }
|
||||||
|
static int rows() { return ((int)N + cols() - 1) / cols(); }
|
||||||
|
|
||||||
|
std::vector<std::string> labels_;
|
||||||
|
|
||||||
|
static cv::Mat to_bgr(const cv::Mat& m) {
|
||||||
|
if (m.channels() == 1) {
|
||||||
|
cv::Mat bgr;
|
||||||
|
cv::cvtColor(m, bgr, cv::COLOR_GRAY2BGR);
|
||||||
|
return bgr;
|
||||||
|
}
|
||||||
|
return m;
|
||||||
|
}
|
||||||
|
|
||||||
|
cv::Mat tile(const std::vector<cv::Mat>& imgs) const {
|
||||||
|
const int c = cols(), r = rows();
|
||||||
|
cv::Mat canvas(r * CH, c * CW, CV_8UC3, cv::Scalar(30, 30, 30));
|
||||||
|
for (int i = 0; i < (int)imgs.size(); ++i) {
|
||||||
|
if (imgs[i].empty()) continue;
|
||||||
|
cv::Mat cell = canvas(cv::Rect((i % c) * CW, (i / c) * CH, CW, CH));
|
||||||
|
cv::Mat resized;
|
||||||
|
cv::resize(imgs[i], resized, {CW, CH});
|
||||||
|
resized.copyTo(cell);
|
||||||
|
if (i < (int)labels_.size() && !labels_[i].empty())
|
||||||
|
cv::putText(cell, labels_[i], {8, 36},
|
||||||
|
cv::FONT_HERSHEY_SIMPLEX, 1.0, {0, 255, 255}, 2,
|
||||||
|
cv::LINE_AA);
|
||||||
|
}
|
||||||
|
return canvas;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── main ──────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
using namespace kpn;
|
||||||
|
|
||||||
|
auto src = make_node<capture, "capture" >(4);
|
||||||
|
auto bilateral= make_node<bilateral_filter, "bilateral" >(4);
|
||||||
|
auto quant = make_node<quantise, "quant" >(4);
|
||||||
|
auto gray = make_node<to_gray, "to_gray" >(4);
|
||||||
|
auto blur = make_node<gaussian_blur, "blur" >(4);
|
||||||
|
auto canny = make_node<canny_edges, "canny" >(4);
|
||||||
|
auto dilate = make_node<dilate_edges, "dilate" >(4);
|
||||||
|
auto comp = make_node<composite, "comp" >(4);
|
||||||
|
|
||||||
|
DebugCanvas<4> debug({"result", "quantised", "edges", "original"});
|
||||||
|
|
||||||
|
// make_network auto-inserts FanoutNode instances wherever a source port
|
||||||
|
// feeds more than one consumer:
|
||||||
|
// capture → 3-way fanout (bilateral, to_gray, debug[3])
|
||||||
|
// quant → 2-way fanout (comp, debug[1])
|
||||||
|
// canny → 2-way fanout (dilate, debug[2])
|
||||||
|
auto net = make_network(
|
||||||
|
edge(src.output<0>(), bilateral.input<0>()), // frame → bilateral
|
||||||
|
edge(src.output<0>(), gray.input<0>()), // frame → to_gray
|
||||||
|
edge(src.output<0>(), debug.input<3>()), // frame → debug[3] original
|
||||||
|
edge(bilateral.output<0>(), quant.input<0>()), // smooth → quant
|
||||||
|
edge(quant.output<0>(), comp.input<0>()), // quant → comp
|
||||||
|
edge(quant.output<0>(), debug.input<1>()), // quant → debug[1] quantised
|
||||||
|
edge(gray.output<0>(), blur.input<0>()), // gray → blur
|
||||||
|
edge(blur.output<0>(), canny.input<0>()), // blurred→ canny
|
||||||
|
edge(canny.output<0>(), dilate.input<0>()), // edges → dilate
|
||||||
|
edge(canny.output<0>(), debug.input<2>()), // edges → debug[2] edges
|
||||||
|
edge(dilate.output<0>(), comp.input<1>()), // thick → comp
|
||||||
|
edge(comp.output<0>(), debug.input<0>()) // result → debug[0] result
|
||||||
|
);
|
||||||
|
|
||||||
|
std::cout << "Debug cell-shading pipeline running — press 'q' to stop.\n";
|
||||||
|
std::cout << "Canvas: [0] result [1] quantised [2] edges [3] original\n";
|
||||||
|
|
||||||
|
net.start();
|
||||||
|
while (debug.step())
|
||||||
|
cv::waitKey(8);
|
||||||
|
net.stop();
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@ -29,6 +29,10 @@ if(OpenCV_FOUND)
|
|||||||
|
|
||||||
add_executable(12_static_cellshade 12_static_cellshade/main.cpp)
|
add_executable(12_static_cellshade 12_static_cellshade/main.cpp)
|
||||||
target_link_libraries(12_static_cellshade PRIVATE kpn ${OpenCV_LIBS})
|
target_link_libraries(12_static_cellshade PRIVATE kpn ${OpenCV_LIBS})
|
||||||
|
|
||||||
|
add_executable(13_debug_cellshade 13_debug_cellshade/main.cpp)
|
||||||
|
target_link_libraries(13_debug_cellshade PRIVATE kpn ${OpenCV_LIBS})
|
||||||
|
|
||||||
if(KPN_WEB_DEBUG)
|
if(KPN_WEB_DEBUG)
|
||||||
kpn_target_enable_web_debug(09_opencv_cellshade)
|
kpn_target_enable_web_debug(09_opencv_cellshade)
|
||||||
endif()
|
endif()
|
||||||
|
|||||||
@ -44,7 +44,7 @@ using repeat_tuple_t = typename repeat_tuple<T, N>::type;
|
|||||||
// .connect("fan", fan.output<0>(), "nodeA", nodeA.input<0>())
|
// .connect("fan", fan.output<0>(), "nodeA", nodeA.input<0>())
|
||||||
// .connect("fan", fan.output<1>(), "nodeB", nodeB.input<0>())
|
// .connect("fan", fan.output<1>(), "nodeB", nodeB.input<0>())
|
||||||
|
|
||||||
template<typename T, std::size_t N>
|
template<typename T, std::size_t N, std::size_t Id = 0>
|
||||||
class FanoutNode : public INode {
|
class FanoutNode : public INode {
|
||||||
public:
|
public:
|
||||||
using args_tuple = std::tuple<T>;
|
using args_tuple = std::tuple<T>;
|
||||||
@ -141,8 +141,10 @@ private:
|
|||||||
auto cpu0 = NodeStats::cpu_now();
|
auto cpu0 = NodeStats::cpu_now();
|
||||||
|
|
||||||
for (std::size_t i = 0; i < N; ++i) {
|
for (std::size_t i = 0; i < N; ++i) {
|
||||||
if (out_channels_[i])
|
if (out_channels_[i]) {
|
||||||
out_channels_[i]->push(val);
|
try { out_channels_[i]->push(val); }
|
||||||
|
catch (const ChannelOverflowError&) {} // drop for this output independently
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
auto cpu1 = NodeStats::cpu_now();
|
auto cpu1 = NodeStats::cpu_now();
|
||||||
@ -150,8 +152,6 @@ private:
|
|||||||
stats_.record_exec(duration_t(t2 - t1), duration_t(t1 - t0), cpu0, cpu1);
|
stats_.record_exec(duration_t(t2 - t1), duration_t(t1 - t0), cpu0, cpu1);
|
||||||
} catch (const ChannelClosedError&) {
|
} catch (const ChannelClosedError&) {
|
||||||
break;
|
break;
|
||||||
} catch (const ChannelOverflowError& e) {
|
|
||||||
std::cerr << "[kpn] fanout overflow: " << e.what() << "\n";
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -169,7 +169,7 @@ private:
|
|||||||
|
|
||||||
template<typename T, std::size_t N>
|
template<typename T, std::size_t N>
|
||||||
FanoutNode<T, N> make_fanout(std::size_t fifo_capacity = 5) {
|
FanoutNode<T, N> make_fanout(std::size_t fifo_capacity = 5) {
|
||||||
return FanoutNode<T, N>(fifo_capacity);
|
return FanoutNode<T, N, 0>(fifo_capacity);
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace kpn
|
} // namespace kpn
|
||||||
|
|||||||
@ -116,7 +116,7 @@ struct FanoutPlaceholder {
|
|||||||
|
|
||||||
namespace kpn {
|
namespace kpn {
|
||||||
// Forward declaration — FanoutNode is defined in fanout.hpp.
|
// Forward declaration — FanoutNode is defined in fanout.hpp.
|
||||||
template<typename T, std::size_t N> class FanoutNode;
|
template<typename T, std::size_t N, std::size_t Id> class FanoutNode;
|
||||||
} // namespace kpn
|
} // namespace kpn
|
||||||
|
|
||||||
namespace kpn::tmp {
|
namespace kpn::tmp {
|
||||||
@ -172,7 +172,7 @@ template<std::size_t FanoutId, typename T, std::size_t N,
|
|||||||
typename DstNodeT, std::size_t DstI, typename... DstTail, std::size_t I>
|
typename DstNodeT, std::size_t DstI, typename... DstTail, std::size_t I>
|
||||||
struct fanout_to_dst_edges<FanoutId, T, N,
|
struct fanout_to_dst_edges<FanoutId, T, N,
|
||||||
TypeList<DstDesc<DstNodeT, DstI>, DstTail...>, I> {
|
TypeList<DstDesc<DstNodeT, DstI>, DstTail...>, I> {
|
||||||
using head_edge = SimpleEdge<kpn::FanoutNode<T, N>, I, DstNodeT, DstI>;
|
using head_edge = SimpleEdge<kpn::FanoutNode<T, N, FanoutId>, I, DstNodeT, DstI>;
|
||||||
using rest = typename fanout_to_dst_edges<FanoutId, T, N,
|
using rest = typename fanout_to_dst_edges<FanoutId, T, N,
|
||||||
TypeList<DstTail...>, I+1>::type;
|
TypeList<DstTail...>, I+1>::type;
|
||||||
using type = append_t<rest, head_edge>;
|
using type = append_t<rest, head_edge>;
|
||||||
@ -282,7 +282,7 @@ struct process_edge {
|
|||||||
using DstDescs = typename collect_dsts<Port, AllEdges>::type;
|
using DstDescs = typename collect_dsts<Port, AllEdges>::type;
|
||||||
static constexpr std::size_t fid = State::next_id;
|
static constexpr std::size_t fid = State::next_id;
|
||||||
using fanout_type = FanoutPlaceholder<T, N, fid>;
|
using fanout_type = FanoutPlaceholder<T, N, fid>;
|
||||||
using src_to_fan = SimpleEdge<SrcNode, Edge::src_idx, FanoutNode<T, N>, 0>;
|
using src_to_fan = SimpleEdge<SrcNode, Edge::src_idx, FanoutNode<T, N, fid>, 0>;
|
||||||
using fan_to_dsts = typename fanout_to_dst_edges<fid, T, N, DstDescs>::type;
|
using fan_to_dsts = typename fanout_to_dst_edges<fid, T, N, DstDescs>::type;
|
||||||
using fanout_edges = concat_t<append_t<typename State::edges, src_to_fan>, fan_to_dsts>;
|
using fanout_edges = concat_t<append_t<typename State::edges, src_to_fan>, fan_to_dsts>;
|
||||||
using new_fanouts = append_t<typename State::fanouts, fanout_type>;
|
using new_fanouts = append_t<typename State::fanouts, fanout_type>;
|
||||||
@ -340,10 +340,9 @@ struct to_fanout_tuple<TypeList<>> { using type = std::tuple<>; };
|
|||||||
template<typename T, std::size_t N, std::size_t Id, typename... Rest>
|
template<typename T, std::size_t N, std::size_t Id, typename... Rest>
|
||||||
struct to_fanout_tuple<TypeList<FanoutPlaceholder<T, N, Id>, Rest...>> {
|
struct to_fanout_tuple<TypeList<FanoutPlaceholder<T, N, Id>, Rest...>> {
|
||||||
using rest = typename to_fanout_tuple<TypeList<Rest...>>::type;
|
using rest = typename to_fanout_tuple<TypeList<Rest...>>::type;
|
||||||
// prepend FanoutNode<T,N>
|
|
||||||
template<typename Tuple> struct prepend;
|
template<typename Tuple> struct prepend;
|
||||||
template<typename... Ts> struct prepend<std::tuple<Ts...>> {
|
template<typename... Ts> struct prepend<std::tuple<Ts...>> {
|
||||||
using type = std::tuple<kpn::FanoutNode<T, N>, Ts...>;
|
using type = std::tuple<kpn::FanoutNode<T, N, Id>, Ts...>;
|
||||||
};
|
};
|
||||||
using type = typename prepend<rest>::type;
|
using type = typename prepend<rest>::type;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -175,6 +175,70 @@ TEST_CASE("static_network: same function distinguished by UniqueTag", "[static_n
|
|||||||
net.stop();
|
net.stop();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST_CASE("static_network: two independent fan-outs of the same element type are wired independently", "[static_network]") {
|
||||||
|
// Both src_a and src_b fan out to two consumers each.
|
||||||
|
// Without the FanoutId fix, both would produce FanoutNode<int,2> — the same
|
||||||
|
// C++ type — and find_node would wire all four consumers to the first instance.
|
||||||
|
auto src_a = make_node<add10, "src_a">(8);
|
||||||
|
auto src_b = make_node<negate_val, "src_b">(8);
|
||||||
|
auto cA = make_node<increment, "cA">(8);
|
||||||
|
auto cB = make_node<multiply2, "cB">(8);
|
||||||
|
auto cC = make_node<multiply3, "cC">(8);
|
||||||
|
auto cD = make_node<square, "cD">(8);
|
||||||
|
|
||||||
|
Channel<int> outA(8), outB(8), outC(8), outD(8);
|
||||||
|
cA.set_output_channel<0>(&outA);
|
||||||
|
cB.set_output_channel<0>(&outB);
|
||||||
|
cC.set_output_channel<0>(&outC);
|
||||||
|
cD.set_output_channel<0>(&outD);
|
||||||
|
|
||||||
|
auto net = make_network(
|
||||||
|
edge(src_a.output<0>(), cA.input<0>()), // src_a → FanoutNode<int,2,0> → cA, cB
|
||||||
|
edge(src_a.output<0>(), cB.input<0>()),
|
||||||
|
edge(src_b.output<0>(), cC.input<0>()), // src_b → FanoutNode<int,2,1> → cC, cD
|
||||||
|
edge(src_b.output<0>(), cD.input<0>())
|
||||||
|
);
|
||||||
|
|
||||||
|
net.start();
|
||||||
|
src_a.input_channel<0>().push(0); // 0 → +10=10 → {+1=11, *2=20}
|
||||||
|
src_b.input_channel<0>().push(5); // 5 → negate=-5 → {*3=-15, ^2=25}
|
||||||
|
|
||||||
|
REQUIRE(outA.pop() == 11);
|
||||||
|
REQUIRE(outB.pop() == 20);
|
||||||
|
REQUIRE(outC.pop() == -15);
|
||||||
|
REQUIRE(outD.pop() == 25);
|
||||||
|
net.stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("static_network: code reuse - same function at corresponding stages of parallel branches", "[static_network]") {
|
||||||
|
// Both branches use increment and multiply2 — code reuse via distinct UniqueTag.
|
||||||
|
// Topology: src → fanout → { increment(tag=1) → multiply2(tag=1) → outA }
|
||||||
|
// → { increment(tag=2) → multiply2(tag=2) → outB }
|
||||||
|
auto src = make_node<add10, "src" >(8);
|
||||||
|
auto incA = make_node<increment, "inc", 1>(8);
|
||||||
|
auto mulA = make_node<multiply2, "mul", 1>(8);
|
||||||
|
auto incB = make_node<increment, "inc", 2>(8);
|
||||||
|
auto mulB = make_node<multiply2, "mul", 2>(8);
|
||||||
|
|
||||||
|
Channel<int> outA(8), outB(8);
|
||||||
|
mulA.set_output_channel<0>(&outA);
|
||||||
|
mulB.set_output_channel<0>(&outB);
|
||||||
|
|
||||||
|
auto net = make_network(
|
||||||
|
edge(src.output<0>(), incA.input<0>()),
|
||||||
|
edge(src.output<0>(), incB.input<0>()),
|
||||||
|
edge(incA.output<0>(), mulA.input<0>()),
|
||||||
|
edge(incB.output<0>(), mulB.input<0>())
|
||||||
|
);
|
||||||
|
|
||||||
|
net.start();
|
||||||
|
src.input_channel<0>().push(0); // 0 → +10=10 → both: +1=11 → *2=22
|
||||||
|
|
||||||
|
REQUIRE(outA.pop() == 22);
|
||||||
|
REQUIRE(outB.pop() == 22);
|
||||||
|
net.stop();
|
||||||
|
}
|
||||||
|
|
||||||
TEST_CASE("static_network: label is accessible as static member", "[static_network]") {
|
TEST_CASE("static_network: label is accessible as static member", "[static_network]") {
|
||||||
using MyNode = decltype(make_node<increment, "my_node">(5));
|
using MyNode = decltype(make_node<increment, "my_node">(5));
|
||||||
REQUIRE(MyNode::label() == "my_node");
|
REQUIRE(MyNode::label() == "my_node");
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user