Architecture

This page is the map of the world. Read it first: it tells you where everything lives and how a keypress becomes pixels on screen. Every later chapter zooms into one region of this map.

Directory map

All the source lives under src/. The important subdirectories are:

  • src/core — the heart of Siril. Command parsing and dispatch, the generic processing workers, the persistent worker thread and its job queue, settings/preferences, the global structures (com, gfit), the mask system, the memory allocator, the abstract GUI interface (gui_iface) and all the OS glue (OS_utils.c, siril_spawn.c, signals.c).

  • src/algos — image-processing algorithms (background extraction, star detection, PSF, geometry, statistics, …).

  • src/filters — user-facing filters (asinh, banding, deconvolution, wavelets, …). These are the classic "write a feature" targets.

  • src/io — everything that reads or writes bytes: FITS, SER, fitseq, film formats, the .seq files, sequence handling, the sequence writer, and the Python IPC layer (siril_pythonmodule.c, siril_pythoncommands.c).

  • src/gui-gtk4all GTK code. Every widget, callback, dialog, the tiled renderer, the CSS, the ROI/preview machinery. Nothing outside this directory (and src/main.c) is allowed to include GTK headers (see The separation rules).

  • src/registration — sequence registration (global, planetary/MPP, apply-existing-registration).

  • src/stacking — the stacking engines and their memory hooks.

  • src/opencv — the C++/OpenCV bridge (geometry, resampling).

  • src/rt — RawTherapee-derived code used by some filters.

  • src/tests — unit tests and diagnostic harnesses.

  • python_module/sirilpy — the sirilpy Python package that scripts import; it talks to the C core over the IPC socket.

Changed in version 1.5: All GTK code moved from src/gui (GTK3, Glade-based) to src/gui-gtk4 (GTK4). If a document or a stale patch references src/gui, treat it as pre-1.5.

The two binaries

Meson builds two executables from a shared core library. The build strategy is documented in src/meson.build; in summary:

  • siril_lib — a static library of the core (non-GUI) sources. It includes core/gui_iface_stubs.c, which provides no-op implementations of every gui_iface slot.

  • siril_headless_libcore/headless_stubs.c alone. It provides no-op implementations of the GUI functions that core code still calls directly (i.e. those not yet routed through gui_iface). It is linked on demand, so its symbols are only pulled in where they would otherwise be unresolved.

  • siril — the GUI binary. Links siril_lib plus the src/gui-gtk4 sources compiled directly; those provide the real GTK implementations (including gui_iface_impl.c), and main.c defines the gui structure. Only built with -Dgtk=true (the default).

  • siril-cli — the headless binary. Links siril_lib plus siril_headless_lib; gui_iface_impl.c is never linked, so the stub vtable from gui_iface_stubs.c is what runs. main-cli.c does not define gui.

The consequence for you as a contributor: core code must never call GTK directly. It calls through gui_iface (see gui_iface in depth), which is a real implementation in the GUI binary and a no-op in siril-cli. The headless build is what CI uses to catch separation violations.

The three access paths to a feature

Any user-visible operation is reachable three ways, and they converge on the same worker:

  1. GUI: a widget callback in src/gui-gtk4 gathers parameters and submits a worker.

  2. Command: command.c parses the command line and submits the same worker.

  3. Python script: a sirilpy call crosses the IPC socket, is handled in siril_pythoncommands.c, and issues the same command / worker.

Because all three funnel into one place, you normally implement the logic once (a worker hook plus a parameter struct) and wire the three front-ends to it. This is exactly the arc that Writing a feature walks you through.

Threads

At runtime Siril has several kinds of thread; knowing which code runs on which is essential (details in Threading):

  • GTK main thread — the only thread allowed to touch GTK. Runs callbacks, idle functions, and the renderer.

  • The persistent processing worker thread — one thread for the whole application lifetime, fed by a FIFO job queue. Every heavy operation (a filter, a stack, a registration) runs here, one at a time. See src/core/processing_thread.c.

  • OpenMP pools — spun up inside a job for data-parallel loops. The thread budget for these is handed to the worker, not read from globals.

  • Python worker threads — one C thread per running Python script, each servicing that script's IPC connection.

Warning

Only the GTK main thread may call GTK. Worker and Python threads reach the GUI through gui_iface slots that dispatch to the main thread via idle functions (siril_add_idle / execute_idle_sync). Never call execute_idle_sync from the main thread — it deadlocks. See Locking.

End-to-end: a filter, from click to pixels

Following one operation all the way through ties the pieces together. When a user clicks Apply in, say, the asinh dialog:

  1. The dialog's apply callback (in src/gui-gtk4) fills a parameter struct, wraps it in a generic_img_args, and calls start_in_new_thread (see generic_image_worker).

  2. From the GTK main thread this is fire-and-forget: the job is queued and the callback returns, keeping the UI responsive.

  3. The worker thread picks up the job, snapshots gfit, runs the operation's image_hook on the copy without holding the fits lock, then takes a brief writer lock only to swap the result back in.

  4. The worker requests undo creation, then schedules an idle on the main thread.

  5. The idle calls notify_gfit_data_modified, which invalidates the statistics cache, recomputes the histogram, merges any ROI, and remaps every viewport — the tiled renderer turns the new pixels into a GdkTexture and the widget redraws (see Display and rendering).

The command and Python paths differ only at step 1 (who fills the struct and submits) and in whether the submission blocks; steps 3–5 are identical.