Week 4: lineplot finishing touches, bugs and minimal waterfall
Last week ended with a working multi-sink line plot, but with one inconvenient problem: the macOS main-thread constraint still forced every flowgraph to include a GRC Snippet block calling cyberether.present(). That block worked, it’s inconvenient for users to have to remember the block on every. I also added input-type variants (complex / float)
and exposed Superluminal’s renderer-device choice to GRC. and Finished with a minimal waterfall sink implementation that is yet to be pushed.
Custom GRC workflow
The Snippet was a workaround, not an integration. I wanted the GR-side
solution. I Asked on Matrix and Marcus from the core dev has pointed at the fix:
looked at grc/blocks/python_qt_gui.workflow.yml.
Reading that file unlocked the model. GRC’s generate_options values
(no_gui, qt_gui, python_bokeh_gui, …) are each backed by a
*.workflow.yml descriptor plus a Python generator module under
gnuradio.grc.workflows.<name>. The YAML is just a regular block file
that GRC discovers via its block-path scan, exactly the way an OOT
installs .block.yml. And generator_module inside the YAML points at
any Python module. So OOTs can register their own generate_options value with zero upstream changes to GR.
The actual implementation is just four files in the OOT:
- A workflow descriptor:
grc/cyberether_standalone_gui.workflow.yml. The key lines aregenerate_options: cyberether_gui(the new dropdown value) andgenerator_module: gnuradio.cyberether.workflows.cyberether_standalone_gui(the Python module we ship). - A generator class
(
python/cyberether/workflows/cyberether_standalone_gui/top_block.py) that inherits fromgnuradio.grc.workflows.common.PythonGeneratorBaseand points at our.makotemplate. - The Mako template itself, modelled on the in-tree
flow_graph_nogui.py.makowith one change: the bottom ofmain()callscyberether.present(tb, device=...)instead oftb.wait(). - CMake glue so the YAML lands in
share/gnuradio/grc/blocks/and the Python module ships under the OOT’s package.
The result, from examples/lineplot_demo.py:
def main(top_block_cls=lineplot_demo, options=None):
tb = top_block_cls()
def sig_handler(...): tb.stop(); tb.wait(); sys.exit(0)
signal.signal(signal.SIGINT, sig_handler)
signal.signal(signal.SIGTERM, sig_handler)
cyberether.present(tb, device=cyberether.DeviceType.Auto)
No Snippet, no tb.wait(), no qtgui imports. cyberether.present() is
the main-thread loop, exactly the way qapp.exec_() is for Qt
flowgraphs.
Line sink input types
Letting the user pick the input type from GRC. Same pattern gr-blocks
uses for add_const_xxx, multiply_const_xxx, etc. — C++ class
template + per-type explicit instantiations + GRC option_attributes
mapping a dropdown to the factory name.
template <typename T>
class cyber_lineplot_sink : virtual public gr::sync_block { ... };
typedef cyber_lineplot_sink<gr_complex> cyber_lineplot_sink_c;
typedef cyber_lineplot_sink<float> cyber_lineplot_sink_f;
The only type-aware code is the per-sample write in work():
if constexpr (std::is_same_v<T, gr_complex>) {
display[d_display_write_ptr] = in[i];
} else {
display[d_display_write_ptr] = CF32{static_cast<float>(in[i]), 0.0f};
}
Display tensor stays CF32; non-complex inputs get imag=0. Explicit
instantiations for gr_complex and float go at the bottom of the
.cc. Back-compat is a Python alias —
cyber_lineplot_sink = cyber_lineplot_sink_c — so pre-W4 flowgraphs
keep working untouched. int/short/byte skipped; a
blocks.int_to_float in front covers the rare case.
Renderer device on the window
Same week, different surface. Superluminal’s InstanceConfig has a
device field that selects which GPU API to render with (Metal, Vulkan,
CUDA, WebGPU) or leaves it on auto. I exposed it all the way through:
cyber_context::present() takes a Jetstream::DeviceType parameter,
pybind11 binds the enum as cyberether.DeviceType, the workflow YAML
adds a “Renderer Device” dropdown, and the Mako template emits
cyberether.present(tb, device=cyberether.DeviceType.${device}).
Superluminal::InstanceConfig config;
config.device = device;
if (Superluminal::Initialize(config) != Result::SUCCESS) { ... }
// ... existing snapshot + for-loop calling Superluminal::Plot() ...
Minimal waterfall sink
What’s minimal:
- Complex input only.
- Same lifecycle pattern as the line sink, register with
cyber_contextinstart(), write tensor inwork(), unregister instop(). The context handles the main-thread lifecycle. source=Time, display=Frequency, operation=Amplitude, Type=WaterfallSuperluminal does the FFT internally.- Single magnitude scale (Superluminal’s built-in
-100..0dB).
The first version was wrong on the buffer shape, and the way it failed taught me what Superluminal’s waterfall pipeline actually expects.
d_tensor(DeviceType::CPU, TypeToDataType<CF32>(),
{1, static_cast<U64>(d_fft_size)})
Superluminal::PlotConfig config = {
.buffer = d_tensor,
.type = Superluminal::Type::Waterfall,
.source = Superluminal::Domain::Time,
.display = Superluminal::Domain::Frequency,
.operation = Superluminal::Operation::Amplitude,
};
config.options["height"] = static_cast<I32>(d_height);
cyber_context::instance().register_plot({ this, d_name, config });
Where this leaves the repo
After four weeks:
- The OOT ships its own GRC workflow (the Snippet workaround is gone).
- The line sink supports complex and float inputs picked from a GRC dropdown.
- The window picks its renderer device through the same GRC options block (default Auto, deterministic priority Metal → Vulkan).
- A minimal complex-input waterfall sink works end-to-end. The multi-sink design (one CyberEther window, multiple sinks tiled in a shared mosaic grid) now has something visually distinct from a line plot.
- All three sink types share one
cyber_contextlifecycle, one registration model, one main-thread entry point.
Repo: gr-cyberether — main
branch has the workflow + types + device work; the waterfall sink
lives on the minimal-waterfall-sink branch and will merge after the
meeting feedback.
Mentors: Luigi Cruz (CyberEther),
Håkon Vågsether (GNU Radio). Thanks also
to Marcus Müller for pointing at grc/workflows/ on Matrix.