From e0fcd426b9ed9d4f80f7ef1873c6137b3f321423 Mon Sep 17 00:00:00 2001 From: Joe Rivera Date: Fri, 2 Oct 2026 09:48:15 -0500 Subject: [PATCH 1/2] =?UTF-8?q?examples:=20ariadne=5Fstream=20=E2=80=94=20?= =?UTF-8?q?the=20Phase-3=20streaming=20"holy=20grail"=20demo?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit One live video stream shown TWO ways from a single .ari document: as the MAIN SCENE (a `type: stream` node — a quad textured with the live frames, filling the viewport) AND in a draggable/resizable ImGui window (a `type: stream_view` widget the SAME stream is routed to; it also shows a static image when no stream is live). The source is hardware-free by default (synthetic moving test pattern), so the demo runs and verifies headless; --camera feeds it from a real SDL3 camera (cvc::gl::capture) when present. The host (ariadne_stream.cpp) opens one stream at the app root, registers the two sinks (register_stream_node_type + register_stream_view_widget), loads stream.ari, realizes the scene, and renders — a navigable window or a one-frame offscreen PNG. Everything else is the stock cvcGL Ariadne runner (cf. ariadne_hello). The stream pool is sized for BOTH sinks (expected_subscribers=3): the pool is fixed-size and refuses rather than under-provisions, so a second sink on the same token gets no frames unless budgeted. Verified: `ariadne_stream stream.ari --offscreen --png` renders the live synthetic stream textured onto the scene quad (headless EGL). The stream_view window renders in an INTERACTIVE session; cvcGL's offscreen path does not composite the ImGui overlay (ariadne_hello hides it in captures for the same reason), and the widget's pull logic is covered hardware-free by cvcgl_stream_view with its draw reusing the Kind::Image path. --- src/cvcGL/examples/CMakeLists.txt | 8 + src/cvcGL/examples/ariadne_stream.cpp | 209 ++++++++++++++++++++++++++ src/cvcGL/examples/stream.ari | 43 ++++++ 3 files changed, 260 insertions(+) create mode 100644 src/cvcGL/examples/ariadne_stream.cpp create mode 100644 src/cvcGL/examples/stream.ari diff --git a/src/cvcGL/examples/CMakeLists.txt b/src/cvcGL/examples/CMakeLists.txt index e3b75aa7..17f2edb3 100644 --- a/src/cvcGL/examples/CMakeLists.txt +++ b/src/cvcGL/examples/CMakeLists.txt @@ -19,6 +19,14 @@ add_executable(ariadne_hello ariadne_hello.cpp) target_link_libraries(ariadne_hello PRIVATE cvcGL ${VTK_LIBRARIES}) vtk_module_autoinit(TARGETS ariadne_hello MODULES ${VTK_LIBRARIES}) +# ariadne_stream — the Phase-3 streaming "holy grail": one live video stream shown both as the main +# scene (a `type: stream` quad filling the viewport) AND in a draggable ImGui `type: stream_view` +# widget window, wired from stream.ari. Synthetic test pattern by default (headless-verifiable), +# or a real SDL3 camera with --camera. +add_executable(ariadne_stream ariadne_stream.cpp) +target_link_libraries(ariadne_stream PRIVATE cvcGL ${VTK_LIBRARIES}) +vtk_module_autoinit(TARGETS ariadne_stream MODULES ${VTK_LIBRARIES}) + # nav_convoy — a nav-style demo whose entire UI is the reusable .ari component library, with only # the streamed agents + sim/camera verbs + stats readouts in the host (the library-based pattern # the real cvc::nav integration follows). diff --git a/src/cvcGL/examples/ariadne_stream.cpp b/src/cvcGL/examples/ariadne_stream.cpp new file mode 100644 index 00000000..03e823f8 --- /dev/null +++ b/src/cvcGL/examples/ariadne_stream.cpp @@ -0,0 +1,209 @@ +// ariadne_stream — the Phase-3 streaming "holy grail" demo: one live video stream shown TWO ways at +// once, both driven from a single .ari document: +// 1. as the MAIN SCENE — a `type: stream` scene node (a UV quad textured with the live frames, +// filling the viewport), and +// 2. in a draggable/resizable ImGui window — a `type: stream_view` Ariadne widget the SAME stream +// is routed to (it also shows a static image when no stream is live). +// +// The video source is hardware-free by default (a synthetic moving test pattern), so the demo runs +// and verifies headless; pass --camera to feed it from a real SDL3 camera (cvc::gl::capture) when +// one is present. Everything below the stream open + the two registrations is the stock cvcGL +// Ariadne runner (cf. ariadne_hello): load the doc, realize the scene, frame the camera, render — +// a window, or a one-frame offscreen PNG capture for verification. +// +// ariadne_stream stream.ari # navigable window (synthetic) +// ariadne_stream stream.ari --camera # from a real camera if present +// ariadne_stream stream.ari --offscreen --png stream.png # one captured frame (headless) + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include // register_stream_node_type (the viewport quad) +#include // register_stream_view_widget (the ImGui window) +#include + +#if CVC_ENABLE_SDL +#include // --camera: a real SDL3 camera as the source +#endif + +using cvc::gl::CameraController; +using cvc::gl::ImGuiBackend; +using cvc::gl::ImGuiOverlay; +using cvc::gl::SceneGraph; +using cvc::gl::SceneRenderer; +using cvc::gl::TouchGestures; +namespace ari = cvc::ariadne; +namespace sx = cvc::ariadne::stream; + +int main(int argc, char **argv) { + std::string docPath, png; + bool offscreen = false, useCamera = false; + long frames = 0; + int width = 1024, height = 768; + std::vector componentPaths; + for (int i = 1; i < argc; ++i) { + const char *a = argv[i]; + auto next = [&](const char *d) { return (i + 1 < argc) ? argv[++i] : d; }; + if (!std::strcmp(a, "--offscreen")) + offscreen = true; + else if (!std::strcmp(a, "--png")) { + png = next(""); + offscreen = true; + } else if (!std::strcmp(a, "--camera")) + useCamera = true; + else if (!std::strcmp(a, "--frames")) + frames = std::atol(next("0")); + else if (!std::strcmp(a, "--width")) + width = std::atoi(next("1024")); + else if (!std::strcmp(a, "--height")) + height = std::atoi(next("768")); + else if (!std::strcmp(a, "--component-path")) + componentPaths.push_back(next("")); + else if (a[0] != '-' && docPath.empty()) + docPath = a; + } + cvc::ariadne::register_cvc_uri_handler(componentPaths); + const bool capturing = offscreen || !png.empty(); + if (capturing && frames <= 0) + frames = 8; // a few frames so the producer delivers and the texture uploads before the capture + + cvc::app app; + SceneGraph sg(app, "stream"); + SceneRenderer view(sg, width, height, offscreen, "main"); + view.setBackground(0.06, 0.07, 0.09); + CameraController cam(view); + cam.setMode(CameraController::Mode::Orbit); + TouchGestures touch(view, cam); + ImGuiOverlay ui(view); + ui.attachCamera(cam); + ui.setVisible(true); // the stream_view WINDOW is the point of this demo — keep it in captures too + + ari::Runtime rt(app, sg.getStatePrefix()); + ImGuiBackend backend; + rt.set_backend(&backend); + backend.install(rt, ui); + bool quit = false; + rt.on("quit", [&] { quit = true; }); + + // The two Phase-3 stream sinks this demo needs, registered once before the scene loads: + cvc::gl::ariadne::register_stream_node_type(); // the `type: stream` viewport quad + cvc::gl::ariadne::register_stream_view_widget(backend, app); // the `type: stream_view` ImGui widget + + // Open ONE video stream at the app root; both sinks resolve it by this token. The source is a + // synthetic moving test pattern (hardware-free) unless --camera opened a real device. + const int sw = 640, sh = 480; + int fw = sw, fh = sh; + std::unique_ptr source; + std::string srcName = "synthetic test pattern"; +#if CVC_ENABLE_SDL + if (useCamera) { + cvc::gl::capture::camera_open camo = cvc::gl::capture::open_camera(0); + if (camo.source) { + source = std::move(camo.source); + fw = camo.width; + fh = camo.height; + srcName = "camera: " + camo.name; + } else { + std::printf("[ariadne_stream] --camera: no camera opened; using the synthetic source.\n"); + } + } +#else + (void)useCamera; +#endif + if (!source) + source = std::make_unique(fw, fh, 30.0); + + sx::stream_params sp; + sp.id = "cam0"; + sp.format.kind = sx::frame_kind::video_raw; + sp.format.codec = "rgba8"; + sp.format.w = fw; + sp.format.h = fh; + sp.format.stride = fw * 4; + sp.expected_subscribers = 3; // the scene-node quad + the stream_view widget (+ headroom): the pool + // is fixed-size and refuses rather than under-provisions, so BOTH + // sinks must be budgeted here or the second one gets no frames + std::unique_ptr stream = sx::stream::open(app, sp); + if (!stream) { + std::printf("[ariadne_stream] FATAL: stream::open failed\n"); + return 1; + } + stream->start_producer(std::move(source), 30.0); + std::printf("[ariadne_stream] stream '%s' open: %dx%d rgba8 (%s)\n", stream->token().c_str(), fw, + fh, srcName.c_str()); + + // Load the document, realize its scene, frame the camera to it. + cvc::gl::ariadne::RealizedScene realized; + if (docPath.empty()) { + std::printf("[ariadne_stream] FATAL: give a .ari document (e.g. stream.ari)\n"); + return 1; + } + ari::LoadResult lr = ari::load_file(docPath.c_str()); + if (!lr.ok) { + std::printf("[ariadne_stream] load failed: %s\n", lr.error.c_str()); + return 1; + } + for (const std::string &w : lr.warnings) + std::printf("[ariadne_stream] %s\n", w.c_str()); + std::vector init_errs; + ari::run_init(app, sg.getStatePrefix(), lr.init_script, &init_errs); + rt.set_tick_program(lr.on_tick_script); + rt.set_root(std::move(lr.root)); + if (lr.scene.any()) { + std::vector sw2; + realized = cvc::gl::ariadne::realize_scene(sg, lr.scene, sg.getStatePrefix(), &sw2); + for (const std::string &w : sw2) + std::printf("[ariadne_stream] %s\n", w.c_str()); + std::printf("[ariadne_stream] scene: %zu node(s)\n", realized.created.size()); + const cvc::bounding_box bb = sg.computeGraphicsBounds(); + if (!bb.isNull()) + cam.frameBounds(bb.minx, bb.miny, bb.minz, bb.maxx, bb.maxy, bb.maxz); + } + + auto frame_body = [&](double dt) { + view.processUIEvents(); + touch.update(); + cam.update(dt); + rt.drain(); + ari::sync_scene_visibility(app, realized.visibility); + cvc::gl::ariadne::tick_scene(realized, view.renderer()); // services the stream node's texture + }; + + if (capturing) { + for (long f = 0; f < frames; ++f) { + frame_body(1.0 / 30.0); + if (f + 1 == frames && !png.empty()) + view.writePNG(png.c_str()); + else + view.render(); + } + std::printf("[ariadne_stream] captured %ld frame(s)%s%s\n", frames, png.empty() ? "" : " -> ", + png.c_str()); + } else { + std::puts("[ariadne_stream] running — close the window or Sim > Quit to exit."); + while (!view.windowClosed() && !quit) { + frame_body(1.0 / 120.0); + view.render(); + } + } + + stream->close(); // join the producer before teardown (render-thread contract) + return 0; +} diff --git a/src/cvcGL/examples/stream.ari b/src/cvcGL/examples/stream.ari new file mode 100644 index 00000000..30a838f2 --- /dev/null +++ b/src/cvcGL/examples/stream.ari @@ -0,0 +1,43 @@ +# stream.ari — the Phase-3 streaming "holy grail": ONE live video stream, shown two ways from one +# document. The host (ariadne_stream) opens a stream under the token `streams.cam0` (a synthetic +# moving test pattern, or a real camera with --camera) and registers the two stream sinks; this doc +# wires them up declaratively: +# - the SCENE is a `type: stream` node — a quad textured with the live frames, filling the view; +# - a draggable/resizable "Camera" window holds a `type: stream_view` widget fed the SAME stream. +# +# ariadne_stream stream.ari # navigable window +# ariadne_stream stream.ari --offscreen --png stream.png # one captured frame (headless) + +meta: + name: Ariadne stream (holy grail) + author: cvc + min_libcvc: 3.4.0 + +menubar: + - menu: Sim + items: + - menu_item: Quit + on: quit + +windows: + # The stream routed to an Ariadne widget in a draggable/resizable ImGui window. The same widget + # shows a static image when no stream is live for the token (see stream_view_widget.h). + - window: Camera + id: camera + children: + - text: "Live stream routed to a stream_view widget:" + - type: stream_view + stream: streams.cam0 + size: 480 + +# The scene (§9): the stream as the MAIN view — a UV quad textured with the live frames. The host +# frames the camera to it so it fills the viewport. +scene: + chrome: false + background: { top: [0.05, 0.06, 0.09], bottom: [0.01, 0.01, 0.02] } + nodes: + - node: screen + type: stream + stream: streams.cam0 + width: 1.6 + height: 1.2 From 80780eba01791176a064a9050e947a5688eb37f7 Mon Sep 17 00:00:00 2001 From: Joe Rivera Date: Fri, 2 Oct 2026 10:03:43 -0500 Subject: [PATCH 2/2] examples: clang-format ariadne_stream.cpp --- src/cvcGL/examples/ariadne_stream.cpp | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/cvcGL/examples/ariadne_stream.cpp b/src/cvcGL/examples/ariadne_stream.cpp index 03e823f8..093329d7 100644 --- a/src/cvcGL/examples/ariadne_stream.cpp +++ b/src/cvcGL/examples/ariadne_stream.cpp @@ -18,10 +18,6 @@ #include #include #include -#include -#include -#include - #include #include #include @@ -38,6 +34,9 @@ #include // register_stream_node_type (the viewport quad) #include // register_stream_view_widget (the ImGui window) #include +#include +#include +#include #if CVC_ENABLE_SDL #include // --camera: a real SDL3 camera as the source @@ -103,8 +102,9 @@ int main(int argc, char **argv) { rt.on("quit", [&] { quit = true; }); // The two Phase-3 stream sinks this demo needs, registered once before the scene loads: - cvc::gl::ariadne::register_stream_node_type(); // the `type: stream` viewport quad - cvc::gl::ariadne::register_stream_view_widget(backend, app); // the `type: stream_view` ImGui widget + cvc::gl::ariadne::register_stream_node_type(); // the `type: stream` viewport quad + cvc::gl::ariadne::register_stream_view_widget(backend, + app); // the `type: stream_view` ImGui widget // Open ONE video stream at the app root; both sinks resolve it by this token. The source is a // synthetic moving test pattern (hardware-free) unless --camera opened a real device. @@ -137,9 +137,9 @@ int main(int argc, char **argv) { sp.format.w = fw; sp.format.h = fh; sp.format.stride = fw * 4; - sp.expected_subscribers = 3; // the scene-node quad + the stream_view widget (+ headroom): the pool - // is fixed-size and refuses rather than under-provisions, so BOTH - // sinks must be budgeted here or the second one gets no frames + sp.expected_subscribers = 3; // the scene-node quad + the stream_view widget (+ headroom): the + // pool is fixed-size and refuses rather than under-provisions, so + // BOTH sinks must be budgeted here or the second one gets no frames std::unique_ptr stream = sx::stream::open(app, sp); if (!stream) { std::printf("[ariadne_stream] FATAL: stream::open failed\n");