Files
effector/CLAUDE.md
T
2026-09-18 14:59:34 +02:00

4.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, 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.

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.