OverviewFilesCommitsIssuesDocsReleasesPeers

hypercore-kb commits / 4fb28c9

skills: the ifsrenderer-linux skill that did not exist
The one active project here without one, and the gap cost real time: the repo's own issue tracker is EMPTY, so everything about it lives on labs org issues #3 and #6-#10, and nothing said so. Carries what had to be re-derived to fix the animation curve: the model under IFSEngine/Animation, the Catmull-Rom tangent and the measured 9.62% wobble it puts on a held value, where each shell hides its keyframe controls (a right-click context menu on Windows, a selection-gated panel on Linux), and the fact that .ifsjson writes enums as bare INTEGERS - so a new enum member breaks the upstream Windows app while a new field does not. Also the things that are simply not guessable: dotnet is at ~/.dotnet/dotnet and not on PATH, IFSEngine is NOT byte-for-byte upstream whatever the README says, CI gates the release on grep -qE "^ *Passed! " over the test log, and the staged app name must stay "ifsrenderer" because pear install extracts <appName>.desktop from the AppImage and throws if it is absent. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
4fb28c90c47c6f661a290b982c275d071b317882
by plankatron <fleeky@gmail.com> · 2026-09-05T18:49:16.000Z

Changed files (1) +254 -0

.claude/skills/ifsrenderer-linux/SKILL.md +254 -0
……@@ -0,0 +1,254 @@
1---
2name: ifsrenderer-linux
3description: IFSRenderer Linux fork — a 3D IFS fractal flame renderer/editor (C#/.NET 10). Adds an Avalonia editor, a headless CLI and a Blender add-on beside upstream's Windows-only WPF app. Read before touching the animation/keyframe model, either shell's UI, the OpenGL compute renderer, the .ifsjson format, or the AppImage/pear release path.
4category: project
5priority: normal
6status: active
7updated: 2026-09-05
8---
9
10# ifsrenderer-linux
11
12Fork of **[bezo97/IFSRenderer](https://github.com/bezo97/IFSRenderer)** (GPLv3, ©
13Dócs Zoltán & contributors) that adds Linux support. The fractal flame engine, the
14GLSL kernel, the `.ifstf` transform plugins and the file format are upstream's.
15Engine bugs get reported there; UI work happens here.
16
17- **Working copy:** `hyper-fleeky/IFSRenderer/` (top-level, canonical)
18- **Branch:** `linux-port` — all fork work. `master` tracks upstream.
19- **Remotes:** `origin` = github bezo97/IFSRenderer · `pear` = the forge
20- **Forge repo:** `ifsrenderer-linux`, under the **`labs`** org
21
22## Where the knowledge actually lives
23
24**The repo's own issue tracker is empty** (`pgh issue ifsrenderer-linux --counts`
25→ 0/0/0). Everything is on the **labs org**:
26
27| labs # | Covers |
28|---|---|
29| **#3** | UI parity audit — Avalonia shell vs canonical WPF, panel by panel (21KB, the big one) |
30| **#6** | Animation fixes + the Blender add-on; three IFSEngine defects |
31| **#7** | Blender scene integration + ffmpeg export |
32| **#8** | Bake orientation, point-cloud path |
33| **#9** | Release-path fixes, volume bake |
34| **#10** | A release fix that shipped; add-on work reverted |
35
36```bash
37pgh issue --org labs --status all # all of it
38pgh issue ifsrenderer-linux # empty — don't stop here
39```
40
41In-repo: `PARITY-AUDIT.md`, `POST-MORTEM.md`, `CHANGELOG.md`, `README.md`,
42`blender/*.md`. Animation coverage in those is thin — the org issues are better.
43
44## Layout
45
46| Path | What |
47|---|---|
48| `IFSEngine/` | Upstream engine: model, animation, `RendererGL` (OpenGL 4.5 compute), serialization |
49| `WpfDisplay/` | Upstream's Windows editor (~9.9k lines). **The parity reference** — read it before inventing UI |
50| `AvaloniaShell/` | Our Linux editor (~9.7k lines). `MainWindow.axaml.cs` is 3380 of them |
51| `AvaloniaShell.Tests/` | Headless-Avalonia xUnit suite (~3.3k lines, 22 files) |
52| `Shared/` | Portable: `ParamPath`, undo, palettes, PNG metadata, settings |
53| `LinuxCli/` | Headless PNG/EXR renderer, no display server |
54| `Probe/` | Round-trips against the real shipped `.ifsjson` data |
55| `Transforms/` | Vendored `.ifstf` plugins — vendored, not the submodule, because CI uses `git archive` |
56| `blender/` | Pure-Python Blender add-on (see below) |
57| `packaging/` | `run-tests.sh`, `build-tarball.sh`, `build-appimage.sh` |
58
59**`IFSEngine/` is NOT byte-for-byte upstream, whatever the README says.** 14 files
60differ from `origin/master` — mostly the upstream `palette-manager` merge, plus our
61own `c57ac9e` (`EasingDirection.Out`). Modifying the engine is established, but it
62is where file-format compatibility gets broken, so read the compat note below first.
63
64## Build, test, release
65
66**`dotnet` is not on `PATH`.** It lives at `~/.dotnet/dotnet`, SDK **10.0.302**.
67`global.json` pins `6.0.x`; builds succeed anyway — don't "fix" it.
68
69```bash
70export PATH="$HOME/.dotnet:$PATH"
71dotnet build IFSEngine/IFSEngine.csproj -c Release # ~4s
72dotnet build AvaloniaShell/AvaloniaShell.csproj -c Release
73sh packaging/run-tests.sh # the gate CI uses
74```
75
76`run-tests.sh` unsets `Version`/`VersionPrefix`/`VersionSuffix` and passes
77`-p:Version=` explicitly: the pear-git runner exports `Version=<branch>-<sha>`,
78which NuGet rejects as a version string. Keep both halves.
79
80**Deploy is `git push pear linux-port`** — `.pear-git/ci.yaml` runs the test suite,
81**gates the release on `grep -qE "^ *Passed! " test.log`**, builds the tarball and
82AppImage, and stages `by-arch/linux-x64/app/ifsrenderer.AppImage` + `package.json`
83for `pear install`. `vdrive: true` gives each release a durable `pear://`.
84Channels: `experimental` ← branches `linux-port`/`master`; `production` ← `v*` tags.
85
86The staged app name must stay **`ifsrenderer`**: `pear install` derives appName from
87`productName ?? name`, then extracts `<appName>.desktop` from the AppImage and throws
88if absent. Every failing CI step writes to a log file and a separate always-succeeding
89step prints it, because the runner discards output from failing steps.
90
91## The animation model — read this before touching keyframes
92
93Five small files under `IFSEngine/Animation/`. The whole evaluator is
94`Channel.EvaluateAt` (99 lines).
95
96```
97Dopesheet ──has──▶ Dictionary<string path, Channel>
98Channel ──has──▶ List<Keyframe> + optional AudioChannelDriver
99Keyframe = { t, Value, Type, InterpolationMode, EasingDirection, EasingPower,
100 Left/RightHandleType, Left/RightHandleT, Left/RightHandleValue,
101 BackOvershoot, ElasticAmplitude, ElasticPeriod }
102IFS.EvaluateAt(t) → per channel: Channel.EvaluateAt(t) → NestedReflectionHelper
103 .SetMemberValueByPath(this, path, val) ← writes onto the fractal
104```
105
106**There are TWO evaluators, chosen per keyframe by `Type`.**
107
108- `Type` (`KeyframeType`) = `Legacy` + Blender's 13 presets: Constant, Linear, Bezier,
109 Sinusoidal→Circular, Back, Bounce, Elastic. `Legacy = 0`, so a keyframe from a file
110 written before this existed deserializes to it and evaluates exactly as it always
111 did. **`Legacy` does NOT mean Catmull-Rom** — it means "evaluate through
112 `InterpolationMode`", which may be any of the three.
113- `InterpolationMode` = `{ Linear, Constant, CatmullRom }`, the legacy path only, and
114 **still defaults to `CatmullRom`** (`Keyframe.cs:5`) so old files are untouched.
115- Setting `Type` projects `InterpolationMode` onto the nearest legacy equivalent, so a
116 file written here still evaluates — approximately — in the upstream Windows app.
117- A segment is governed by its **left** keyframe's mode — except `InOut` easing,
118 which also reads the **right** key's `EasingPower` (`Channel.cs:51`).
119- `Dopesheet.AddOrUpdateChannel` — the ◇ diamond path, and the only creation site —
120 stamps **`Bezier` + `AutoClamped` handles**, which is Blender's default. This is where
121 the fix for the wobble lives: it is a creation-time default, NOT a change to
122 `Keyframe`'s own default, precisely so loaded files keep their behaviour.
123- Keys within `0.99/Fps` of an existing key update it instead of inserting.
124
125### The Catmull-Rom tangent, and why holds wobbled
126
127Still the behaviour of any `Legacy` keyframe. `Channel.cs` — `t1 = (p2 - p0) / 2`, from
128the neighbours' **values only**:
129
130- It **never flattens.** Two equal keys with unequal neighbours still get a slope,
131 so a hold must leave its value and come back. Measured on keys
132 `(3s,0) (5s,1) (7s,1) (9s,2)` with engine defaults: **min 0.9519 @ 6.58s,
133 max 1.0481 @ 5.42s — 9.62% peak-to-peak** on a value the user pinned twice.
134- It **ignores Δt** (uniform, not centripetal), so tight keys beside wide ones
135 overshoot.
136- Blender does not have this problem because its default is Bezier with
137 **Auto Clamped** handles, which zeroes the tangent at a local extremum.
138
139**`BezierHandles.cs` is the replacement path.** Handles for `Automatic`, `AutoClamped`
140and `Vector` are **derived on every read**, never cached on the keyframe — `Keyframe` is
141a plain mutable object in a plain `List<T>`, so a moved key raises no notification and a
142cache would go stale the first time one was dragged. Only `Free` and `Aligned` store
143coordinates, and only those two are serialized. `AutoClamped` zeroes the tangent at a
144local extremum (which two equal keys in a row always are) and clamps handles into the
145neighbour range; `Correct` shortens handles that reach past the segment in time, so the
146curve cannot fold back; the time→parameter inversion is bisection, because it is
147unconditionally convergent on a monotonic x.
148
149### Where the keyframe controls hide
150
151Both shells expose interpolation/easing/power; neither is discoverable.
152
153- **WPF (Windows):** right-click the dope diamond → context menu
154 (`WpfDisplay/Views/Animation/DopeButtonResources.xaml:38-64`).
155- **Avalonia (Linux):** the `KEYFRAME` editor in the animation tab's left column,
156 `IsVisible=false` until a key is selected. Landed `450d3aa` (2026-08-16) when the tab
157 was uncrowded. Selection is wired from both canvases. It now leads with a **type**
158 dropdown carrying Blender's presets in Blender's order; `ShowRowsFor` reveals only the
159 rows that type uses (handles for Bezier, easing for the easing/dynamic presets, interp
160 + power for Legacy alone). Bezier handles draw and drag in `CurveCanvas` — dragging a
161 derived handle makes it `Free`, as Blender does, or the position would spring back.
162
163Upstream's `WpfDisplay/Views/Animation/CurveEditor.xaml` is a **29-line
164placeholder** reading "🚧 Under development" — there is no curve editor upstream to
165reach parity with, and PARITY-AUDIT.md was wrong to list one (corrected on labs #3).
166`AvaloniaShell/CurveCanvas.cs` is ours, and its plot is sampled from
167`Channel.EvaluateAt` per pixel column so it cannot drift from what renders.
168
169### `.ifsjson` compatibility — enums are bare integers
170
171`IfsSerializer` uses **Newtonsoft** with no `StringEnumConverter`, so enums
172serialize as numbers. Measured:
173
174```json
175{ "InterpolationMode": 2, "EasingPower": 1.0, "EasingDirection": 2, "t": 0.0, "Value": 1.0 }
176```
177
178This is why `KeyframeType` is a separate field rather than new members on
179`InterpolationMode`: **adding a member to `InterpolationMode` writes an integer upstream
180cannot read.**
181`Channel.EvaluateAt` ends in `else throw new NotImplementedException()`, so a file
182using a new mode crashes the Windows app at evaluation time — breaking the README's
183"fully compatible with the Windows app". Newtonsoft ignores **unknown properties**
184by default, so *new fields* on `Keyframe` degrade gracefully where new enum values
185do not. Prefer additive fields, and keep `InterpolationMode` set to the closest
186legacy mode so upstream still renders something sane.
187
188`Keyframe` uses **`ShouldSerialize<Property>()`** (a Newtonsoft feature) so a keyframe
189writes only the fields its type actually uses. A `Legacy` keyframe is byte-identical to
190what it was before any of this existed; a new one adds three fields:
191
192```json
193{"InterpolationMode":2,"EasingPower":1.0,"EasingDirection":2,"t":5.0,"Value":1.0,
194 "Type":3,"LeftHandleType":4,"RightHandleType":4}
195```
196
197Verified through `IfsSerializer` itself, not just `JsonConvert` — `IfsConverter` does not
198special-case the dopesheet, so the default object serializer (and thus `ShouldSerialize`)
199is the real path.
200
201### Viewport refresh asymmetry (fixed)
202
203`AnimationView.TimeChanged` posts `f.EvaluateAt(t)` + `r.InvalidateParamsBuffer()`.
204`AnimationView.Changed` — raised by **every keyframe edit** — used to call only
205`PushStateToUi` + `NotifyAnimationStateChanged` and **never re-evaluate**, so an edit
206reached the viewport only once the playhead moved. It now goes through
207`PostThenRefresh`, which orders the UI push after the evaluation on the render thread.
208Use that helper for anything that mutates the fractal and then reads it back: a bare
209`Post` plus an immediate UI read is a race.
210
211## Engine defects this port surfaced (all affect Windows)
212
2131. **`EasingDirection.Out` ran every segment backwards** — `1 - t^p` is 1 at t=0.
214 Fixed here (`c57ac9e`, `Channel.cs:50`); **still open upstream**.
2152. **`alias_tables`** declared `vec4[MAX_ITERATORS]`, indexed to `itnum²` — 160KB
216 against a 64KB UBO cap.
2173. **`GradientResolution = 256`** against `vec4 palette[150]` — over-runs by 106.
218
219(2) and (3) are out-of-bounds UBO reads NVIDIA tolerates. Report to bezo97.
220
221## Renderer gotcha
222
223`RendererGL` clears the histogram and zeroes `TotalIterations` **inside**
224`DispatchCompute`, when it sees the flags `LoadParams` raised — invalidating alone
225changes no counter. Any accumulation gate must be `do/while`, never `while`:
226**the dispatch IS the reset.** A `while` gate shipped 300 identical frames from
227"Export animation".
228
229## The Blender add-on (`blender/`)
230
231Pure Python, no .NET at runtime; `build_addon.py` makes the zip. **Your GLSL is used
232verbatim** — the kernel and all 46 `.ifstf` plugins compile unmodified; only the host
233layer (`FromString`, std140 packing, `AliasMethod`, the loader) is reimplemented.
234Verified 0 mismatches on plugins and alias tables.
235
236Blender's GPU module forces: no `GPUStorageBuf` (histogram is an R32UI image with
237fixed-point `imageAtomicAdd` + a drain pass), no float image atomics, **`max_images
238= 8`** and a declared-but-unused image still burns a slot, unused push constants are
239**stripped** (so `uniform_int` raises where GL returns -1), default image qualifier is
240write-only, and the Vulkan backend segfaults at 13/16/32 images. Samplers are a
241separate 32-slot budget.
242
243Run its tests with Blender's Python, not the system one.
244
245## Traps
246
247- **The repo issues page is empty.** Org issues are the record.
248- **`dotnet` is not on `PATH`.**
249- **`IFSEngine/` is modified** despite the README's "byte-for-byte unmodified".
250- **Enum values are file-format breaking; new fields are not.**
251- Almost every bug this project has produced was **silent** — a wrong std140 stride,
252 a mirrored camera basis, an unclamped tangent. They render a fractal, just not the
253 right one. Verify differentially (against the engine's own C#, or Blender's own
254 projection), never by reasoning about it.