Files
effector/CLAUDE.md
T
SiandClaude Opus 5 89985cb989 Cascade effects through a chain
renderChain runs a list of stages in order: stage 0 works on the loaded
sources, every later one on the output of the stage before it, so blur
after scanlines softens the scanlines rather than the photographs. The
effects themselves are untouched — none of them knows whether its input
came from disk. All stages share one Rng and run in sequence, so a seed
still reproduces the cascade byte for byte.

The sidebar grows an "Effect chain" list: add a stage from a menu built
from the registry, reorder it, remove it, and edit the selected stage's
params underneath. Each stage keeps its own values. The CLI takes the
same chain from repeated -e, with every flag going to the stage whose
effect declares it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 15:21:29 +02:00

5.6 KiB

CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

Build and test

cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release
cmake --build build
ctest --test-dir build --output-on-failure

Everything is warning-clean under -Wall -Wextra -Wpedantic; keep it that way.

The test suite is a single hand-rolled binary (no framework), so there is no per-test filter: run ./build/tests/effector_tests directly to see the section headings (rng, registry, scanlines, blur, chain, batch loading) and the ok/FAIL lines. To iterate on one area, comment out the other blocks in tests/core_tests.cpp or add a new std::cout << "section\n" block — checks are plain check(condition, "description") calls and main() returns the failure count.

Headless render (no display needed, the fastest end-to-end smoke test):

build/effector -o /tmp/out.png -s 800x600 --strip-height 6 --seed 1 a.jpg b.png

Running the GUI from this shell

GTK picks Wayland from an inherited WAYLAND_DISPLAY and will not draw into an Xvfb display, which silently yields black screenshots. Under Xvfb always use:

env -u WAYLAND_DISPLAY GDK_BACKEND=x11 DISPLAY=:79 build/effector ...

Architecture

The plug-in contract is declarative

Effect (src/core/effect.hpp) exposes params() returning std::vector<ParamSpec> (src/core/params.hpp). Those specs are the only thing the GUI and CLI know about an effect: src/gui/window.cpp builds SpinButtons/Switches/DropDowns from them and src/cli/options.cpp generates flags from them (strip_height → --strip-height). Never add effect-specific branches to the GUI or the parser — add a ParamSpec instead. Values travel as ParamValues (a map<string, variant<long,double,bool,string>>), read back with paramInt/paramDouble/paramBool/paramChoice.

Adding an effect: new src/effects/<name>.{hpp,cpp} subclassing Effect, one line in the registry list in src/core/registry.cpp, one line in the effector_core sources in CMakeLists.txt. Nothing else — it shows up in the CLI, in the chain menu and as a cascade stage on its own.

Effects cascade

Effects are never applied on their own: renderChain (src/core/chain.cpp) runs a std::vector<ChainStage> in order, handing stage 0 the loaded sources and every later stage the previous stage's output as its only source. That is the whole of the cascade — an effect neither knows nor cares whether its input came from disk. All stages share one Rng and run strictly in sequence, so a seed still reproduces the cascade byte for byte.

The interface builds the chain in the sidebar ("Effect chain": add from the menu, reorder, remove, one stage selected at a time with its params below), the CLI builds it from repeated -e: -e scanlines --strip-height 6 -e blur --radius 5. Flags share one namespace across all effects, so buildChain (src/cli/options.cpp) hands each flag to the stage whose effect declares it — two effects must not declare the same param key.

effector_core (static) holds image IO, params, effects, rng and links only gdk-pixbuf-2.0 + Threads — no gtkmm, no display. The effector executable adds src/main.cpp, src/cli/, src/gui/ and links gtkmm. Keep GTK out of src/core/ and src/effects/; the tests link the core alone and rely on this.

Determinism is a contract

Rng (src/core/rng.hpp) is a hand-written xoshiro256** rather than <random>, because libstdc++ distributions are not portable across implementations. Same sources + settings + seed must produce byte-identical output on any machine — the tests assert this. Effects must draw every random value from the Rng& they are handed, and must not depend on thread scheduling: the scanlines effect first draws the whole strip plan serially from the rng, then copies pixels in parallel.

Threading model

  • parallelFor (src/core/parallel.hpp) splits fixed contiguous chunks across hardware threads — used for pixel work, where items cost the same.
  • loadImages (src/core/image.cpp) deliberately does not use parallelFor: files vary wildly in size, so workers pull the next index from a shared std::atomic cursor. It reports through LoadProgress::onFile (called from decoding threads: must be thread-safe and non-blocking) and aborts when LoadProgress::cancel is set, in which case the partial result is discarded by the caller, not used.
  • The GUI never blocks the main thread. Both decoding and rendering run on a std::thread and signal back with a Glib::Dispatcher (loadProgress_/loadFinished_/renderDone_ in src/gui/window.hpp); the handlers are the only code that touches widgets. Renders are coalesced through a debounce timer (requestRender() → startRender()), and the progress window is created on a 250 ms delay so short batches never flash a dialog. loadWindow_ is rebuilt from scratch each time, so loadBar_/loadFileLabel_ are raw pointers that must be nulled in closeLoadWindow().
  • Sources are held as an immutable shared_ptr<const SourceList> and edited copy-on-write via mutateSources(), so a render in flight can never race with an add/remove.

Image buffer

Image is tightly packed 8-bit RGB with stride exactly width * 3 — effects rely on this for memcpy, and GDK uploads the buffer as is. Decoding applies EXIF orientation. Image::load throws; loadImages collects per-file LoadFailures instead.

Reference

python-reference/ is the original Python prototype, kept only for comparison. Do not modify it or import from it.