Generic workers
Siril has three generic workers that handle the boilerplate of running an operation — memory checks, threading, locking, progress, undo, idle dispatch — so your code only provides the algorithm (a hook) and a parameter struct. This chapter is the complete reference; Writing a feature is the tutorial that walks you through using them.
The three workers live in src/core/processing.c, with their argument
structs defined (and richly commented) in src/core/processing.h:
generic_image_worker— one operation on one image.generic_sequence_worker— one operation across a sequence.generic_mask_worker— a mask-producing/mask-editing operation.
Important
Your hook runs with the relevant fits already locked on your behalf. Do
not take the fits rwlock on the fits you were handed — the lock is
non-recursive and you would self-deadlock. See Locking.
generic_image_worker
Runs args->image_hook(args, fit, max_threads) on a single image and
installs the result. Driven by struct generic_img_args.
Fields
fit— the input image. When it isgfit, the fast swap path is used (below); otherwise the in-place path.image_hook— your algorithm:int (*)(struct generic_img_args *, fits *, int nb_threads), returning 0 on success. It must operate only on thefitit is handed.mem_ratio— peak memory requirement as a multiple of the image size. When > 0 the worker runs a pre-flight check (default_img_mem_hook):mem_ratio × rx × ry × channels × bytes-per-sampleagainstget_available_memory, and aborts if it does not fit. See Memory management.max_threads— thread budget for the hook; defaults tocom.max_threadwhen left < 1. The hook must not exceed it (see Threading).log_hook—gchar *(*)(gpointer user, log_hook_detail)producing aSUMMARYstring (the undo label and, in condensed form, feeds it) and aDETAILEDstring (logged, and written as a FITS HISTORY card). If NULL,descriptionis used for both.description— terse progress text.verbose— extra logging (forced on unlessfor_preview).command/command_updates_gfit— set only fromcommand.c.commandmarks a command-invoked op;command_updates_gfitsays the command needsgfitrefreshed. GUI operations leave these FALSE and update through an idle instead.user— operation-specific data. By convention its first member must be a destructor, called byfree_generic_img_args(see below).for_preview— a preview op: no undo state is saved.for_roi— the op is applied to an ROI only. See Region of Interest Processing.skip_generic_undo— the worker creates no undo state; the caller owns undo. Used by operations that manage their own undo (e.g. stretches) and by measurement-only commands (stat,bg,bgnoise, …) for which undo would be meaningless.mask_aware— the op respects an active mask (the worker blends the result through the mask).has_mask— internal; captured fromfit->maskbefore the writer unlock.idle_function— a custom completion idle (GUI path). Should be NULL when called fromcommand.c.populate_roi_on_complete— deprecated and ignored; the worker now repopulates the ROI itself. Retained only so old call sites compile.
The two execution paths
The worker chooses its strategy from whether args->fit == gfit:
Swap path (fit == gfit). The worker takes a brief reader lock on
gfit and deep-copies it into a private orig (CP_DEEPCOPY | CP_ALLOC,
so WCS-dependent hooks see the astrometry), then drops the lock. Your hook
runs on orig with no gfit lock held, so the GUI keeps reading gfit
freely (motion handlers, ROI fills). On success the worker takes gfit's writer
lock for the microseconds it takes to
fits_swap_all_except_rwlock(gfit, orig), installing the result; orig
then holds the pre-operation image — exactly what undo_save_state
wants. An aborted op skips the swap, leaving gfit pristine. This is why the
GUI stays responsive during long single-image operations.
In-place path (fit is gui.roi.fit or any other fits). The worker
holds args->fit's writer lock for the whole hook (the legacy pattern).
Because the lock is not on gfit, it does not block GTK main-thread readers.
Undo, mask, HISTORY
Undo is created only on the swap path, and only when not previewing, not scripted, and
skip_generic_undois unset. When no undo state is created but the image changed (command_updates_gfit), a fallback FITS HISTORY card is appended so provenance is not lost.Mask blending: when
mask_awareand the fit has an active mask, the worker blends the hook's output against the pre-op pixels through the mask (blend_fits_with_mask, supporting 8/16/32-bit masks). See Masks.After installing the result the worker calls
notify_gfit_data_modified— outside the writer-lock window (see the deadlock note in Locking) — which invalidates stats, recomputes the histogram and remaps the viewports.
Idle dispatch and ownership
Who frees args and how completion is dispatched depends on the context:
command + headless — the worker calls
stop_processing_threadandfree_generic_img_argsinline.command + GUI — dispatched synchronously via
gui_iface.execute_idle_sync(end_generic_image_update_gfitwhen the command updates gfit, elseend_generic_image).GUI op — dispatched asynchronously via
siril_add_idle, usingargs->idle_functionif provided, elseend_generic_image_update_gfit.
In every case the idle (or the inline headless path) frees args.
Nothing after the idle is registered may touch args — the worker
copies the fields it still needs into locals first.
generic_sequence_worker
Runs an operation over every (filtered) frame of a sequence, driven by
struct generic_seq_args. Call order per run:
compute_mem_limits_hook(defaultseq_compute_mem_limits) →max_parallel_images; thencompute_thread_distributionassignsnb_subthreadsper parallel image. This step only runs in builds with OpenMP (it sits inside#ifdef _OPENMP), and only when the caller has not already setmax_parallel_images.prepare_hook(defaultseq_prepare_hookwhen there is output, which also opens the writer viaseq_prepare_writer).compute_size_hook(ifhas_output) → disk-space check viatest_available_space.Per frame, in parallel: optional
image_read_hook(skip loading pixels when only metadata is needed) → read (full or partial) →image_hook(args, out_frame, in_index, fit, area, nb_subthreads)→save_hook(defaultgeneric_save) whenhas_output.finalize_hook(defaultseq_finalize_hook) — closes the writer; called even on the error path.Idle:
idle_functionorend_generic_sequence.
Key fields
seq— the sequence.filtering_criterion/filtering_parameter— which frames to include (helpers insequence_filtering.h, e.g.seq_filter_included);nb_filtered_imagescaches the count for smoother progress.partial_image/area/layer_for_partial/regdata_for_partial— read only a region; whenregdata_for_partial, the area is moved by the registration transform per frame.has_output— the op writes a new sequence. Thennew_seq_prefix,load_new_sequence,output_type,upscale_ratio, and the SER/fitseq forcing flags (force_ser_output/force_fitseq_output) apply. Output to a SER or fitseq container engages the seqwriter.stop_on_error— TRUE aborts the whole run on a frame failure; FALSE unselects the failing frame and continues.user— operation data, by convention freed infinalize_hook.already_in_a_thread— the run is embedded in an existing thread; the idle function is suppressed and cleanup runs inline. See the reserve-thread contract in Threading.parallel/max_parallel_images— enable and cap frame-parallelism.lock— anomp_lock_tthe worker initialises for hooks that need cross-thread accumulation (use it infinalize-bound shared state).
Parallelism and the seqwriter
Frame-parallelism is enabled only when parallel is set, more than one image
fits in memory, and the format is reentrant: SER and fitseq yes, FFMS2 film
formats no, single-FITS per fits_is_reentrant. When writing a SER or
fitseq output, the seqwriter provides backpressure:
seqwriter_wait_for_memory blocks a producer until a write block frees
up, bounded by the limit seq_prepare_writer sets. Memory sizing splits
the budget between the compute side and the writer side
(for_writer argument to compute_mem_limits_hook). See
Memory management.
generic_mask_worker
Runs args->mask_hook(args) to create or edit a fits's mask, driven by
struct generic_mask_args (fit, mask_hook, mem_ratio,
log_hook, description, verbose, command, user with a
leading destructor, mask_creation, max_threads).
Changed in version 1.5: Unlike generic_image_worker, this worker currently holds
args->fit->rwlock as a writer for the entire duration of the
mask_hook — there is no swap path yet (a code TODO in
processing.c). It is fine while mask ops stay short; a long-running
mask hook would block GUI readers.
When args->fit == gfit and this is not a command, the worker saves an undo
state before running the hook. mask_creation TRUE means the mask is set
active on completion (set_mask_active). Completion dispatches
end_generic_mask (which calls gui_iface.on_mask_state_changed), inline
in the command+headless case.
Gotchas
Each of these has caused, or is designed to prevent, a real bug.
Never touch
argsafter submission. The idle (or headless path) frees it. Inside the worker, copy any field you need past the idle into a local first — the workers do exactly this.The destructor-first convention.
free_generic_img_args(and the mask equivalent) calldestroy_any_args(args->user), which invokes the destructor stored as the first member of youruserstruct. Allocateuserwithcallocand set that destructor. Do not also freeuseryourself after a failedstart_in_new_thread— that double frees.mask_aware = FALSEwith an active mask installs the hook's output without blending. If the op should respect masks and you forget the flag, the result is silently wrong.already_in_a_thread = TRUEsuppresses the idle entirely — the caller is responsible for cleanup, and (for sequence embedding) mustreserve_threadfirst to clear a stale cancel flag.A failed
image_hookwith an active seqwriter does not currently notify the writer of the failed frame (a known TODO in the worker). Choosestop_on_errordeliberately for output sequences.Sequence hooks must be re-entrant — they run concurrently across frames. Use
args->lockfor any shared accumulation destined forfinalize.populate_roi_on_completeis deprecated — the worker repopulates the ROI automatically on the swap path; leave the field alone.