diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..0a40bb8 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,104 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Build and test + +```bash +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): + +```bash +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: + +```bash +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` +(`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>`), read back with +`paramInt`/`paramDouble`/`paramBool`/`paramChoice`. + +Adding an effect: new `src/effects/.{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. + +### Two link targets, deliberately + +`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 +``, 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` 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 +`LoadFailure`s instead. + +## Reference + +`python-reference/` is the original Python prototype, kept only for comparison. +Do not modify it or import from it.