o
odinpkg.dev
packages / app / nn-odinray

nn-odinray

v0.2.0app

Interactive 2D CNN/RNN architecture visualizer in Odin + raylib — native graph editor with animated forward pass, drag-to-connect, undo/redo, and JSON persistence.

MIT · updated 2 months ago

nn-odinray

Release Build License: MIT Odin raylib Platform

An interactive 2D visualizer for CNN and RNN architectures, written in Odin on top of raylib. Native, lightweight, real-time. Designed as a faster, more focused alternative to web-only tools like TensorFlow Playground.

CNN demo with picker

Features

  • Layer types: Input, Conv2D, MaxPool, AveragePool, Flatten, Dense, SimpleRNN, LSTM, GRU, Output.
  • Live animated forward pass: colored pulses propagate along connections; speed adjustable at runtime.
  • Recurrent self-loops: SimpleRNN/LSTM/GRU layers render with a t-1 self-loop arc.
  • Time-unrolled RNN view: press U to expand recurrent layers into N copies labelled t-1, t, t+1, t+2 and connected by explicit time-step arrows. Downstream layers shift right automatically; render-only, no data-model change.
  • Multi-channel feature maps: layers with channels > 1 render with offset "stacked card" shadows scaled by channel count.
  • Parameter count and FLOPs: live totals in the status bar, plus per-layer numbers in the property panel. Standard formulas (Conv: (k²·in_ch+1)·filters; LSTM: 4·(in+units+1)·units; MAC counted as 2 FLOPs). Bump a filters value and watch the totals jump.
  • Full graph editor:
    • Right-click any layer to open a property panel with -/+ and </> controls.
    • N (or the + Add Layer toolbar button) opens a layer-type picker.
    • Drag from a layer's right-edge handle to another layer to create a connection.
    • Click a connection to select it; Delete removes it.
    • Move any layer with left-drag; pan with middle-drag; zoom around the cursor with the mouse wheel.
  • Live shape propagation: editing any param (filters, kernel, units, etc.) re-derives output_shape and propagates input_shape through the rest of the graph via a topological forward pass.
  • Undo / Redo with Ctrl+Z / Ctrl+Y (depth 64), plus toolbar buttons that show stack depth.
  • Copy / Paste any layer with Ctrl+C / Ctrl+V. Pasted copies land near the camera target (consecutive pastes step diagonally so they don't fully overlap) and inherit the source's parameters; connections are not copied.
  • JSON persistence: S saves to architecture.json, L loads. Pretty-printed and hand-editable.
  • Screenshot export: P writes visualizer_NNN.png.
  • Four built-in demos: CNN, SimpleRNN, LSTM, GRU. Tab cycles between them.

Screenshots

CNN LSTM with self-loops
CNN forward pass with animated pulses, multi-channel stacked feature maps. LSTM with recurrent self-loop arcs and output handles.
Property panel Layer-type picker
Property panel with live in/out shapes, param count and FLOPs. Layer picker modal — pick any of 9 layer types.
Time-unrolled view
Time-unrolled LSTM: each recurrent layer expanded into 4 copies (t-1t+2) with explicit time-step arrows.

Building

Requires the Odin compiler with the vendored raylib bindings (default in current Odin builds).

odin build . -out:nn-odinray
./nn-odinray

Tested on Odin dev-2026-05 with raylib 5.5. The code uses only core: and vendor:raylib, so no external dependencies beyond the toolchain.

CLI flags

Flag Effect
--demo cnn|rnn|lstm|gru Pick the startup demo (default: cnn).
--unrolled Start with the time-unrolled RNN view enabled.
--shot <path.png> Render ~30 frames then save the screen to a PNG and exit. Useful for headless verification.
--save-test <path.json> Save the chosen demo, immediately re-load it, and print a round-trip summary.
--shape-test Run shape-propagation tests (mutate, delete, insert) and print derived shapes.

Controls

Mouse

Input Action
Left-drag on layer Move layer
Left-drag from output handle (right-edge dot) Create new connection (drop on target layer)
Left-click on connection Select connection (then Delete to remove)
Left-drag on empty canvas Pan
Middle-drag Pan
Right-click on layer Open property panel
Wheel Zoom (anchored at cursor)

Keyboard

Key Action
N Open layer-type picker
Delete / Backspace Remove selected connection (or hovered/selected layer)
Ctrl+Z / Ctrl+Y Undo / Redo
Ctrl+C / Ctrl+V Copy hovered/selected layer / paste near the camera target
Tab Cycle between demo architectures
U Toggle time-unrolled RNN view
S / L Save / Load architecture.json
R Re-run auto-layout and re-fit camera
+ / - Adjust animation speed
P Save screenshot PNG
H Toggle the help panel
Esc Close picker / cancel connection drag / close panel

Architecture file format

The JSON format is intentionally flat and human-editable:

{
  "version": 1,
  "anim_speed": 0.6,
  "layers": [
    {
      "id": 0,
      "type": "input",
      "pos": [80.0, 308.0],
      "input_shape": [3, 32, 32],
      "output_shape": [3, 32, 32],
      "params": { "channels": 3, "height": 32, "width": 32 }
    },
    {
      "id": 1,
      "type": "conv2d",
      "pos": [260.0, 270.0],
      "input_shape": [3, 32, 32],
      "output_shape": [16, 32, 32],
      "params": { "filters": 16, "kernel": 3, "stride": 1, "padding": 1 }
    }
  ],
  "connections": [
    { "from": 0, "to": 1, "weight": 1.0 }
  ]
}

Layer type values: input, conv2d, maxpool, avgpool, flatten, dense, rnn, lstm, gru, output.

Param fields not relevant to a given layer type are written as zero/empty but ignored on load.

Source layout

main.odin         window/loop, input handling, CLI flags, undo/redo wiring
model.odin        layer/connection types, params, shape propagation, param/FLOPs, mutations, hit testing
demos.odin        built-in CNN/RNN/LSTM/GRU architectures
visualizer.odin   layout, drawing (cards, connections, self-loops, unrolled view), pulse animation
io.odin           JSON persistence + undo/redo history snapshots
ui.odin           toolbar, status bar, help panel, property panel, layer picker

~2600 lines of Odin across six files. No external libraries beyond Odin's core: and vendor:raylib.

Non-goals

This project is visualization only. It deliberately does not:

  • Train models or run backpropagation.
  • Render activation values per-pixel (deferred — would require a sampling backend).
  • Render in 3D (raylib's 2D pipeline is enough for this scope).
  • Implement a full deep-learning framework.

License

MIT © phiat