# MegaFractal

An UltraFractal parameter file (.upr) renderer using OpenCL GPU compute.

Parses UltraFractal formula language, translates it to OpenCL C, and renders
fractal images on the GPU.

## Building

Requires CMake 3.16+, a C++17 compiler, and an OpenCL implementation.
Single precision is the default and works on any OpenCL GPU; the optional
`-double` mode requires `cl_khr_fp64`.

```sh
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build -j$(nproc)
```

### Build options

| Option | Default | Description |
|---|---|---|
| `SAFE_MODE` | `ON` | Guard formula-internal loops with an iteration limit to prevent GPU hangs |

## Usage

```sh
build/megafractal [-v] [-double] [-sl N] [-device NAME] <input.upr> [output.png]
```

| Flag | Description |
|---|---|
| `-v` | Print the generated OpenCL kernel source |
| `-double` | Use double precision (`double`) instead of the default single precision |
| `-sl N` | Supersampling level: render `2^N` samples per pixel |
| `-spp N` | Samples per pass (JIT-unrolled); default is device-driven |
| `-tile N` | Tile size `2^N`; default is probe-driven (TDR-safe) |
| `-unlimited` | Dispatch the whole image in one NDRange (disables TDR-safety tiling) |
| `-device NAME` | Prefer the named OpenCL device |
| `-list-devices` | Print all OpenCL devices and exit |

A GUI front-end is built alongside the CLI: `build/megafractal_gui` opens a
progressive-preview window with live exposure/contrast sliders.

If no output path is given, the output is written alongside the input with a
`_rendered.png` suffix.

### Precision

The fractal iteration core (`real_t` / `real2_t` in the generated kernel) is
single precision by default. Pass `-double` to use double precision at the cost
of throughput — many GPUs run FP64 at a small fraction of FP32 speed.

Color processing (transfer functions, gradient lookup, RGB blending) always
uses single precision since 8-bit output does not benefit from higher precision.

Host-side parameters (coordinates, magnification, angle) are computed at
double precision and cast to the kernel's `real_t` at compile time, preserving
the best available accuracy.

## Codebase overview

| File | Purpose |
|---|---|
| `src/main.cpp` | CLI entry point and argument parsing |
| `src/gui.cpp` | SDL2 + ImGui progressive-preview GUI (built as `megafractal_gui`) |
| `src/parser.cpp` | Parses `.upr` files: fractal parameters, gradient stops, formula/coloring blocks, enum conditionals |
| `src/translator.cpp` | Translates UltraFractal formula expressions to OpenCL C (recursive descent parser with type inference) |
| `src/kernel_gen.cpp` | Assembles the complete OpenCL kernel: complex math library, gradient LUT, per-AA-sample compositor, imaging kernel |
| `src/renderer.cpp` | OpenCL device pick, tile/spp auto-tune, `RenderSession` driving progressive passes and tonemap |
| `src/types.h` | Shared data structures (`Scene`, `Layer`, `MergeMode`, `ColoringFormula`, `GradientStop`, `Precision`) |
| `third_party/` | Vendored `stb_image_write.h` and ImGui docking branch |

### Rendering pipeline

1. **Parse** the `.upr` file into a `Scene` containing one `Layer` per `layer:` block (formula sections, coloring formulas, gradient, coordinate mapping, per-layer mergemode/opacity/transparent attrs)
2. **Translate** UltraFractal formula language to OpenCL C expressions, resolving compile-time enum conditionals
3. **Generate** one self-contained OpenCL scene kernel: for each sub-pixel AA sample it evaluates every layer and composites bottom-up in a register-local float4, then accumulates into a linear-light buffer. A separate `imaging` kernel normalizes by samples-so-far and applies exposure / contrast / sRGB OETF
4. **Dispatch** progressively: `probe_tile_log2` picks a tile size whose kernel runtime fits a 500 ms budget (4× under the 2 s Windows TDR); `tiles_per_flush` adapts per flush; passes accumulate until the target sampling level is reached
5. **Tonemap** by invoking the imaging kernel on the accumulator — cheap enough that exposure/contrast edits are instant in the GUI

### Supported UltraFractal features

- Formula sections: init, loop, bailout with `@test` enum conditional resolution
- Coloring formulas: init, loop, final sections (inside and outside)
- Transfer functions: linear, log, sqrt, sqr
- Gradient: linear and smooth (cubic Hermite) interpolation with rotation baked into stop positions
- Transforms (pre-iteration coordinate transforms), disabled transforms (`enabled=no`) skipped
- User-defined functions within formulas
- Control flow: if/elseif/else/endif, while/endwhile, repeat/until — case-insensitive keywords
- Complex number arithmetic, trigonometric and hyperbolic functions
- External formula resolution — `.upr` files referencing `.ufm`/`.ucl`/`.uxf` files in the scene's directory, `$MEGAFRACTAL_FORMULAS`, or `./UF_formulas/`
- Implicit function parameters (bare `fn1`/`fn2`/... treated the same as `@fn1`/`@fn2`/...)
- Multi-layer files: every sub-pixel AA sample evaluates all layers and composites bottom-up in a register-local float4 with per-layer mergemode (normal, multiply, screen, overlay, darken, lighten, subtraction, addition, color, luminance, saturation, hsladd, passthrough), opacity (0–100), and `transparent=yes` (makes unspecified inside pixels alpha=0)
- Supersampling: Roberts R2 quasi-random + per-pixel blue-noise Cranley–Patterson rotation, tent reconstruction filter. Progressive multi-pass accumulation — `-sl N` requests `2^N` samples/pixel split across passes
- TDR-safe dispatch: a one-tile probe picks the largest tile whose kernel runtime fits a 500 ms budget (4× below the 2 s Windows TDR default); `tiles_per_flush` adapts so each `finish()` targets ~500 ms of GPU time

### External formula search path

The loader looks up `filename="..."` references in this order:

1. The directory containing the input `.upr`
2. Colon-separated list in `$MEGAFRACTAL_FORMULAS`, if set
3. `./UF_formulas/` (relative to the working directory)

`UF_formulas/` is not checked in. Populate it by running:

```sh
python3 scripts/fetch_formulas.py
```

The script downloads the third-party files our corpus references (dmj,
dmj3, jock, om, rdw, sam) from `formulas.ultrafractal.com` and writes a
minimal `Standard.ufm` stub. `Standard.ucl` is not fetched — the entries
our scenes use are either inlined in the `.upr` or supplied by the
translator's compat shim. If a future scene needs additional
`Standard.ucl` entries, copy the file from your Ultra Fractal install
into `UF_formulas/`.

## Tests

### Dependencies

The test runner requires [ImageMagick](https://imagemagick.org/) (`compare`,
`identify`) and Python 3.

### Running

```sh
./scripts/run_tests.py [-d DEVICE] [path/to/megafractal]
```

Each test lives in `tests/UF_params/<name>/` with a `.upr` parameter file
and a `reference.png`. The runner renders each scene and compares the
output against the reference using RMSE (root mean square error), with a
5% threshold.

After running, `tests/results/<device_slug>.tsv` holds per-test timings
for the OpenCL device that ran the suite, and `tests/report.html`
(regenerated via `scripts/gen_test_report.py`) shows every test's
rendered output, reference image, result, RMSE, and one timing column
per device TSV present. Re-running on the same device updates that
device's column; running on a different device adds a new column. Both
are gitignored; force-add a specific TSV to share numbers across
machines.

### Contributing scenes

If you use UltraFractal and have scenes you'd like MegaFractal to stay
compatible with, please open an issue or pull request with:

1. The `.upr` parameter file.
2. A reference PNG rendered by UltraFractal. A 1024 x 640 render is a good
   target — large enough to expose detail, small enough to keep the repo
   light.
3. A one-line note on what the scene exercises (formula, coloring technique,
   layer layout, any features you'd like covered).

Drop them under a new `tests/UF_params/<scene_name>/` directory with files
`<scene_name>.upr` and `reference.png`. `scripts/run_tests.py` picks up new
scenes automatically, writes its per-device timing into `tests/results/`,
and regenerates the table below.

### Test inventory

**UF reference** tests compare against images rendered by UltraFractal and
must match within 5% RMSE. **Regression** tests use our own renders as
baselines for scenes where exact UF matching is not possible (multi-layer
compositing, formula-specific differences). The inventory below is
regenerated by `scripts/run_tests.py` from the committed per-device TSVs
under `tests/results/`.

<!-- test-inventory-start -->
_Generated 2026-04-19 from_:
- **Intel(R) UHD Graphics 630** — run 2026-04-19T04:20:20+02:00 (timing at 8spp)
- **NVIDIA GeForce RTX 4090** — run 2026-04-19T04:16:18+02:00 (timing at 8spp)
- **gfx1151** — run 2026-04-19T05:25:25+02:00 (timing at 8spp)

| Scene | Reference | Type | RMSE vs ref | Result | Intel(R) UHD Graphics 630 | NVIDIA GeForce RTX 4090 | gfx1151 |
| :-- | :-: | :-- | --: | :-- | --: | --: | --: |
| `basic_mb` | <img src="tests/UF_params/basic_mb/reference.png" width="160"> | regression | 0.0099 | PASS | 702 ms | 252 ms | 1022 ms |
| `composite_2layer` | <img src="tests/UF_params/composite_2layer/reference.png" width="160"> | regression | 0.0058 | PASS | 825 ms | 233 ms | 780 ms |
| `external_formula` | <img src="tests/UF_params/external_formula/reference.png" width="160"> | regression | 0.0016 | PASS | 702 ms | 227 ms | 781 ms |
| `gnarls_highiter` | <img src="tests/UF_params/gnarls_highiter/reference.png" width="160"> | UF reference | 0.0952 | FAIL | 6860 ms | 317 ms | 1191 ms |
| `gradient_linear_smooth` | <img src="tests/UF_params/gradient_linear_smooth/reference.png" width="160"> | UF reference | 0.0035 | PASS | 750 ms | 247 ms | 799 ms |
| `gradient_sqr_smooth` | <img src="tests/UF_params/gradient_sqr_smooth/reference.png" width="160"> | UF reference | 0.0034 | PASS | 726 ms | 235 ms | 769 ms |
| `gradient_sqrt_smooth` | <img src="tests/UF_params/gradient_sqrt_smooth/reference.png" width="160"> | regression | 0.0011 | PASS | 749 ms | 231 ms | 763 ms |
| `greenpurple` | <img src="tests/UF_params/greenpurple/reference.png" width="160"> | regression | 0.0011 | PASS | 13251 ms | 352 ms | 1023 ms |
| `layered_loops` | <img src="tests/UF_params/layered_loops/reference.png" width="160"> | skipped | — | SKIP | — | — | — |
| `layers_functions_gnarls` | <img src="tests/UF_params/layers_functions_gnarls/reference.png" width="160"> | regression | 0.0026 | PASS | 5442 ms | 226 ms | 817 ms |
| `loopy` | <img src="tests/UF_params/loopy/reference.png" width="160"> | regression | 0.1175 | FAIL | 19608 ms | 951 ms | 1453 ms |
| `mb_exp_smoothing` | <img src="tests/UF_params/mb_exp_smoothing/reference.png" width="160"> | UF reference | 0.0159 | PASS | 720 ms | 209 ms | 800 ms |
| `mb_zoom_exp_smoothing` | <img src="tests/UF_params/mb_zoom_exp_smoothing/reference.png" width="160"> | UF reference | 0.0003 | PASS | 774 ms | 256 ms | 760 ms |
| `mb_zoom_rotation` | <img src="tests/UF_params/mb_zoom_rotation/reference.png" width="160"> | UF reference | 0.0342 | PASS | 679 ms | 216 ms | 772 ms |
| `sinh_loop_julia` | <img src="tests/UF_params/sinh_loop_julia/reference.png" width="160"> | regression | 0.0107 | PASS | 95572 ms | 14117 ms | 32992 ms |
<!-- test-inventory-end -->
