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>
122 lines
5.6 KiB
Markdown
122 lines
5.6 KiB
Markdown
# 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`, `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):
|
|
|
|
```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<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.
|
|
|
|
### 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
|
|
`<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
|
|
`LoadFailure`s instead.
|
|
|
|
## Reference
|
|
|
|
`python-reference/` is the original Python prototype, kept only for comparison.
|
|
Do not modify it or import from it.
|