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.seqfiles, sequence handling, the sequence writer, and the Python IPC layer (siril_pythonmodule.c,siril_pythoncommands.c).src/gui-gtk4— all GTK code. Every widget, callback, dialog, the tiled renderer, the CSS, the ROI/preview machinery. Nothing outside this directory (andsrc/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— thesirilpyPython 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 includescore/gui_iface_stubs.c, which provides no-op implementations of everygui_ifaceslot.siril_headless_lib—core/headless_stubs.calone. It provides no-op implementations of the GUI functions that core code still calls directly (i.e. those not yet routed throughgui_iface). It is linked on demand, so its symbols are only pulled in where they would otherwise be unresolved.siril— the GUI binary. Linkssiril_libplus thesrc/gui-gtk4sources compiled directly; those provide the real GTK implementations (includinggui_iface_impl.c), andmain.cdefines theguistructure. Only built with-Dgtk=true(the default).siril-cli— the headless binary. Linkssiril_libplussiril_headless_lib;gui_iface_impl.cis never linked, so the stub vtable fromgui_iface_stubs.cis what runs.main-cli.cdoes not definegui.
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:
GUI: a widget callback in
src/gui-gtk4gathers parameters and submits a worker.Command:
command.cparses the command line and submits the same worker.Python script: a
sirilpycall crosses the IPC socket, is handled insiril_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:
The dialog's apply callback (in
src/gui-gtk4) fills a parameter struct, wraps it in ageneric_img_args, and callsstart_in_new_thread(see generic_image_worker).From the GTK main thread this is fire-and-forget: the job is queued and the callback returns, keeping the UI responsive.
The worker thread picks up the job, snapshots
gfit, runs the operation'simage_hookon the copy without holding the fits lock, then takes a brief writer lock only to swap the result back in.The worker requests undo creation, then schedules an idle on the main thread.
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 aGdkTextureand 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.