Skip to content

CalculatorGen Roadmap

Bradley Brown edited this page Aug 13, 2026 · 1 revision

CalculatorGen — Phase D+ Roadmap

Companion pages: Technical Index · CalculatorGen Architecture · CalculatorGen Authoring · Performance Development Plan · Resources & Bibliography

Note

Status as of 2026-06-11. Tier 6 (ILGPU GPU JIT) is now live for User Bulb + User Equation on feature/gpu-compute. The fixed self-test grid passes with 0 mismatches against scalar on the CUDA accelerator probe. Roadmap items dated Q4 2025 and earlier should be cross-checked against current code before being treated as pending.

Forward dev plan after the deep-zoom + hot-load + ops expansion landed. Ordered top-down by recommended execution sequence: each phase generally builds on capabilities from earlier phases. Items can be re-ordered when one's blocked, but the dependency notes call out which predecessors actually matter.

Phase D-1 — Tooling polish (small, additive)

These don't change rendering output. Each is bounded and safe to land between bigger features.

  1. CalcGen --analyze flag — pretty-print parsed AST, simplified ∂p/∂z, ∂p/∂c, derived dz/dc update, SA recurrence (when applicable). No file output. Helps users debug equations before generating.
  2. Better parse error messages — line/column from the lexer, "expected X, got Y" hints, suggestion when a keyword is misspelled (cong → did you mean conj?).
  3. Higher integer power cap — Pow exponent currently capped at 16. Bump to 64. SA detector + emitters already iterate; no algorithmic change required.
  4. Per-equation custom bailout radius — CLI flag --bailout R embeds the radius² as the template's Bailout2 const. Phoenix / Nova / some user fractals need < 4; others want > 32.
  5. Compile cache for hot-loadCalculatorGenHotLoad hashes the (equation, name) tuple, returns the previously-loaded Type if hash matches and the assembly is still loaded. Saves a Roslyn round-trip on duplicate compiles.
  6. Auto-unload previous assembly — when a fresh compile succeeds, unload the prior AssemblyLoadContext. Prevents process bloat on rapid iteration.

Phase D-2 — Quality + perf wins (medium)

  1. Per-pixel rebase — DONE through Wave 6 closeout (Item 7 MVP + guards A/B/C + multi-cluster D + cross-frame cache E; see Known issues entry below for the implementation summary). When pixels glitch the perturbation path, the renderer collects them and spatial-partitions into independent clusters via grid bucketing + 8-connectivity flood-fill; each cluster builds its own rebase reference orbit at its centroid, cached cross-frame so pan/zoom- only updates reuse warm orbits. Three early-exit guards (zoom > 1e25, bbox density ≥ 2%, sample-probe ≥ 50% hits) kill bad-fit clusters before per-pixel commit.
  2. Higher SA orders (3 → 16+) — current SA tracks A, B, C only. Each extra order extends the valid skip range exponentially. Legacy uses ~64. Lifting to 16 captures most of the win.
  3. Cached SA tables — extend the existing ref-orbit cache to include the SA coefficient arrays. Same cache key works (SA depends on the same inputs as ref orbit).
  4. Generic SA emitter (symbolic) — current SA is hardcoded for z^d+c degrees 2..5. Derive the recurrence symbolically from any polynomial AST. Unlocks SA for sqr(z)+c with extra terms, for z²+az+c, etc. Builds on the AstPerturbation Taylor builder.
  5. DD-precision SA derivative — track dz/dc through the SA skip using parallel coefficient arrays D₀, D₁, D₂, D₃. Restores distance estimate + surface normals in the SA-skipped region. Currently drv/div reset to zero at saStart.
  6. BLA hierarchy — nested levels (single step, pairs, quads, …, log2(maxIt) levels). Each per-pixel iter tries the highest-level block that satisfies its validity radius. Legacy: 1.5–2× at deep zoom.
  7. Histogram equalization — adaptive contrast pass over the iteration buffer. Legacy MandelbrotCalculator has it; generated calculators don't expose the underlying buffer the legacy histogram works against. Plumb the smooth-count buffer through.

Phase D-3 — New equation capability (medium-large)

  1. Trig + transcendental opssin, cos, exp, log AST nodes. Lexer + parser keywords. Differentiator rules. Emitters delegate to Math.Sin / Math.Cos / Math.Exp / Math.Log. Non-polynomial but holomorphic; distance estimate preserved.
  2. Conditional / piecewise equationsif Re(z) > 0 then a else b grammar. Branch in emitted code. Distance estimate breaks at the discontinuity but smooth count works. Anti-holomorphic-equivalent flag for SupportsDe.
  3. Multi-variable poly in (z, c, z_{n-1}) — Phoenix-style two-step recurrence: z_{n+1} = z_n² + c + p · z_{n-1}. Add prev reference node, plumb second buffer through ref orbit + perturbation.

Phase D-4 — Precision extensions (large)

  1. Octuple-double (OD) ref orbit — 8-limb extended precision, ~124 decimal digits. Push zoom ceiling past 1e50. Big build, but follows the QD pattern exactly — OD struct + OdEmitter + threshold property.
  2. DD-precision BLA tables — at the deepest zooms, BLA A_n is near 1.0 + tiny; double precision in the table itself loses ULPs after thousands of accumulation steps. DD tables fix this. Pairs well with #12 (hierarchy).

Phase D-5 — Visual quality (medium)

  1. Anti-aliasing — 2×2 or 4×4 sub-pixel sampling option. Each pixel renders N internal samples, averages in colour space. Big perf cost — gate behind a Quality preset.
  2. Progressive rendering — first render at ¼ resolution, present, refine to ½, then full. The renderer's overlay system can pop the refined frames in place.
  3. TAA / temporal accumulation — when the camera is still, blend successive frames over time. Each frame's noise (sub-pixel jitter, palette dithering) averages out → smooth final image after a second of stillness.

Phase D-6 — UX + tooling (medium)

  1. CalcGen --benchmark flag — compile + time the generated calculator against a fixed set of viewpoints. Reports ms/frame at each zoom level. Useful for evaluating optimisation work in Phase D-2.
  2. Equation cookbook + gallery — curated library of equation strings with metadata (default centre/zoom, thumbnail). UI dialog picks one, populates the UserEquation editor with the source.
  3. Live equation preview — in UserEquation editor, show parsed AST
    • derived dz/dc + SA support flag as the user types. Surfaces parse errors immediately and confirms generator support.
  4. Animation: morph equations — interpolate between two equations (z²+c → z³+c via t ∈ [0, 1]). Renders intermediate frames as a video. Requires SA support to be off during morph (recurrence changes per frame).
  5. Save hot-loaded calculator to permanent .cs — button on the UserEquation dialog: takes the just-hot-loaded equation, writes the generated source to disk. Permanent variant of "Compile & Load".
  6. GPU reference orbit — compute the ref orbit on the GPU alongside per-pixel work. Faster for large maxIt. Requires moving QD math onto the GPU; the existing ILGPU kernel only does plain double.
  7. Unit tests — parser, differentiator, emitter golden-output tests. Catch regressions when adding new ops in Phase D-3.

Phase D-7 — Architecture (large)

  1. Roslyn source generator ✅ Shipped 2026-06-22 (Wave 2.13). Compile-time IIncrementalGenerator in CalculatorGen.SourceGen/ consumes [assembly: GeneratedCalculator(equation, name, IncludeSelfTest?, Bailout?)] declarations and emits one calculator per attribute instance via context.AddSource. Stock calcs migrated to a 10-line registry at Engine/Calculators/Generated/GeneratedCalculatorAttributes.cs; the 20 hand-checked-in *Calculator.cs + *CalculatorSelfTest.cs files (~33 K lines) replaced by .g.cs outputs in obj/. Legacy dotnet run -p CalculatorGen CLI + CalculatorGenApi.TryCompileAndLoad hot-load path unchanged — only the checked-in generated tree is eliminated. See Wave 2.13 status log entry in Docs/Open-Work-Plan.md for the slice breakdown.

Status snapshot

Tasks tracked in the session TaskList. Each phase item maps to one task. Items 1–6 are estimated < 1 hour each; 7–18 are 1–4 hours; 19+ are larger refactors. Proceed in numbered order unless a dependency flips priority.

Completed

  • Iteration index (n / iter) keyword — user-equation bug: legacy C# Step(Complex z, Complex c, int n) signature uses n for iter count; pasting z*z + c + 0.001*n into Compile & Load hit CalcGen lexer with "Unknown identifier 'n'". Added IterRef AST node (real-valued scalar leaf), iter keyword with n as alias (both lex to TokenKind.Iter → IterRef). Differentiator: IterRef → 0 (opaque, like PrevRef/DeltaRef). SaDetector rejects. CalculatorGenApi: hasIter flag gates supportsPerturbation = false (δ-Taylor can't linearise iter- dependent step — δ doesn't change n across ref orbit vs pixel); supportsDe stays true (iter is real scalar, no holomorphic chain participation). EmitterBase: new IterRe virtual binding plus IterImLiteral for the zero literal in each emitter's complex type ("0.0" / "Vector256.Zero" / "(DD)0.0" / "(QD)0.0"); dispatcher routes IterRef → (IterRe, IterImLiteral) with ImZero=true so downstream Add/Mul elide dead-zero terms. Per-emitter binding: ScalarEmitter iter; Avx2Emitter iter_v (Vector256 broadcast); QdEmitter / QdDirectEmitter iter_q (QD); DdDirectEmitter iter_dd (DD). Template: {{ITER_DECL_*}} substitution pairs injected at every loop body site (8 sites: scalar fast × 3 incl GPU kernel, AVX2 × 2, QD ref orbit, scalar ref orbit, DD/QD direct + continue) — pulls from whatever loop counter is in scope (it or n) and casts to the per-emitter complex type. Empty when !hasIter so non-iter calcs generate byte-identical bodies. Tests: 90/90 PASS (5 new — both keyword forms, flag, diff, SA reject). All 6 stock calcs regenerate identically; --gentest MandelbrotZ2 0-diff. Sample equation z*z + c + 0.001*n produces clean bodies: scalar double iter = (double)it; ... (0.001 * iter); AVX2 Vector256<double> iter_v = Vector256.Create((double)n); ... Avx.Multiply(z_re5, iter_v); QD QD iter_q = (QD)(double)n; ... (0.001 * iter_q).

  • Hot-load ILGPU ref + C# Complex preprocessor — two UserEquation Compile-&-Load fixes landed together: 1. CalculatorGenHotLoad.GatherReferences() walks AppDomain assemblies for Roslyn refs. ILGPU loads lazily (only on first GPU render attempt) — if user clicks Compile & Load before GPU runs, ILGPU is absent → generated using ILGPU; fails with CS0246. Fix: TryLoadByName("ILGPU") + "ILGPU.Runtime" + "ILGPU.Algorithms" before GatherReferences. Assembly probing path finds DLLs alongside host EXE. 2. UserEquation textbox historically held C# Complex.* expressions (legacy UserEquationCalculator Roslyn-compiles them); CalcGen DSL is a restricted grammar (z, c, sin, cos, exp, log, +, -, , /, ^Int, sqr, conj, fold, if/then/else, prev, abs). Same textbox, two grammars → CalcGen lexer rejected Complex as unknown identifier. New EquationPreprocessor.Preprocess(string, out string?) in CalculatorGen project translates: return X; → X Complex.Zero → 0 Complex.One → 1 Complex.Sin/Cos/Exp/Log/Conjugate(x) → sin/cos/exp/log/conj(x) Complex.Pow(x, k_int) → x^k (k>=2), x (k==1), 1 (k==0), 1/x^|k| (k<0) Complex.Pow(x, expr) → exp(exprlog(x)) Hard-rejects Complex.ImaginaryOne, new Complex(a, b), Complex.Abs(x) (DSL abs is squared-mag, not sqrt) with crisp error messages. Unknown Complex.X flagged with full supported list. Fixed-point loop handles nested calls (Complex.Pow(Complex.Pow(z, 2), 3)). Wired into both OnHotLoadViaCalcGen and OnGenerateViaCalcGen in UserEquationViewModel. Tests: 85/85 PASS (17 new preprocessor tests covering all translations + reject paths + the user's actual reported equation return z * Complex.Pow(z,-3) + c * Complex.Pow(c,-2);).

  • 1. --analyze flag — Program.cs in CalculatorGen.

  • 2. Better parse errors — line/col positions, "expected X, got Y" friendly token names in Describe(), Levenshtein keyword suggestion.

  • 3. Higher integer power cap — 0..64 in EquationParser.ParseFactor.

  • 4. --bailout R flag — embeds into template Bailout2.

  • 5. Compile cache for hot-load — _cache dict in CalculatorGenHotLoad, keyed on (equation, name).

  • 6. Auto-unload previous assembly — _lastContext.Unload() on next successful compile; cache entries from dying context dropped.

  • 22. --benchmark flag — host Program.cs arg; hot-compiles an equation and times the ladder default/shallow/mid-1e3/deep-1e6 /deep-1e9. Writes benchmark.out next to the exe.

  • 28. Unit tests — CalculatorGenUnitTests.Run() covers parser round-trip, lexer diagnostics (col + suggestions), differentiator ∂p/∂z + ∂p/∂c, simplifier identity rules, SA detector edges, anti-holomorphic feature flags. Invoke via --calcgen-test. Current: 36/36 PASS.

  • 9. Cached SA tables — _cachedSaSr/_cachedSaSi (jagged) + _cachedSaStart in Calculator.template.cs, gated on cacheHit && _cachedUseSa == UseSa && _cachedScale == scale. Skips SA-build loop on zoom-only / theme-change frames.

  • 8. Higher SA orders (3 → 8) — SaRecurrenceEmitter rewritten to take an order parameter, generates the full degree-d polynomial-mult unroll (dPow_m_k = m-fold convolution of S coefficients) for any (degree ∈ 2..5, order ≥ 2). Template switched from named A,B,C scalars to unrolled Sr1..Sr8/Si1..Si8 with jagged saSr[k][n] storage. Tail-validity check is |S_N · ε^N| < tol · |S_1 · ε|. All 6 generated calculators regenerated; --calcgen-test 36/36 PASS, --gentest MandelbrotZ2 PASS (scalar/AVX2/PT/BLA/QD-PT all agree). To bump to N=16, change SaOrders = 8 const in template, expand the unrolled Sr/Si declarations + save/load blocks, and update the order default in CalculatorGenApi.Generate.

  • 11. SA-skip derivative seed — drv/div seeded inside the if (saStart > 0) per-pixel block via the polynomial derivative dz/dc = Σ_{k=1..N} k · S_k · ε^(k-1). Without the seed, drv/div restarted at 0 at saStart so distance estimate + surface normals collapsed across the SA-skipped band; the seed restores chain- rule continuity. Iteration count unchanged — self-test still 0-diff. Note: roadmap originally called for parallel DD-precision D₀..D₃ arrays, but the closed-form polynomial derivative subsumes that: d(S_k)/d(c_pixel) = 0 because S_k depends only on the (constant) reference center, so the only surviving term is the chain through ε.

  • 8-fix. SA overflow + δ-magnitude guards at deep zoom — three-layer stop criterion in the SA-build validity check: (1) IsFinite + abs-cap > 1e150 short-circuits NaN/Inf propagation (S_N overflow → Inf−Inf = NaN slips past relative tolerance because NaN > x is false); (2) aMag * maxEps > 0.25 bounds the SA-skipped |δ| inside the perturbation linearisation radius — without it, S_1 grows like |2Z|^n and the SA-skipped δ exceeds |Z|, breaking |δ| ≪ |Z| and pixels escape on iter saStart → solid-colour blob over frame centre; (3) original relative tail-vs-head tolerance preserved. Also lowered ExtendedRefZoomThreshold 1e12 → 1e9 to close the gap where plain-double ref orbit lost precision before QD engaged (legacy MandelbrotCalculator uses DD ref orbit always; adding the DD codegen path is a future task). Reported against MandelbrotZ2 at center (-1.1727, -0.2968) zoom range 1e12-1e16 where the SA-induced blob appeared.

  • 8-fix2. SA per-order divergence check + tightened δ-bound — first fix's single tail-vs-head + δ<0.25 still let the blob reappear oscillating with correct frames as zoom advanced. Root cause from legacy SeriesApproximation.FindSkip: tail check misses "deep-k overskip" where a higher-order term grows faster than its predecessor, so the truncation tail dominates the kept terms even when the absolute tail bound passes. Added |S_{k+1}|·maxEps ≤ tol·|S_k| check for every consecutive pair (k=1..7), tightened δ-magnitude bound to BlaRelative (1e-3), and switched SaTolerance default 1e-6 → 1e-3 matching legacy's proven value.

  • 10. Generic symbolic SA emitter — AstSaDetector extended with DetectPolyInZPlusC(root) that matches any polynomial in z (no CRef, Conj, Folded, Div anywhere) added to a single +c. Returns (polyZ, degree). SaRecurrenceEmitter.EmitGeneric then derives the recurrence symbolically: for each k=1..degree it computes (1/k!) ∂^k F / ∂z^k via AstDifferentiator, simplifies, and renders the (Re, Im) expression at Z_n via ScalarEmitter into named locals pk1_Re/Im..pkN_Re/Im. Convolution + Σ logic shared with the pure z^d+c fast path. CalculatorGenApi prefers the fast path when applicable; falls back to generic for cases like z²+az+c, 2z³-z+c, z^6+c, z^4-z²+c. Test coverage: --calcgen-test 42/42 PASS (6 new generic-detector tests).

  • 12. BLA hierarchy — flat per-iter blaAr/Ai/Br/Bi/R arrays replaced with FracturingFog.FFMath.BlaTable (hierarchical, log2(refLen) levels, 2^k-step merged BLAs per level). Added BlaTable(Bla[] level0, int refLen, double dcMaxAbs) overload to Math/Bla.cs so generated calcs (any polynomial — A/B per equation via emitter) build level-0 from the per-iter {{BLA_A_BODY}} + {{BLA_B_BODY}} and the merge logic stays shared. Per-pixel iter calls bla.Lookup(it, dMag2, maxIt); largest valid level applied via δ ← A·δ + B·ε, drv ← A·drv, it += L − 1. Cache: _cachedBlaTable + _cachedBlaDcMaxAbs gated on scale equality (BLA radii are scale-dependent, ref orbit isn't). Self-test PASS 36/36, --gentest MandelbrotZ2 0 diff. Note: tested as potential fix for task #14 detail gap — ineffective; gap roots elsewhere.

  • 16. Phoenix / multi-var two-step recurrence — new PrevRef AST node referencing z_{n-1}. Lexer keyword prev (Levenshtein suggestion list updated; prv → prev test). Parser atom case. Differentiator treats prev as opaque (∂prev/∂z = 0) — produces a WRONG dz/dc for Phoenix equations, but the value is never consumed because hasPrev gates supportsDe = false in CalculatorGenApi (proper Phoenix DE needs a parallel dprev/dc derivative-state vector updated as dprev := dz each iter — deferred). supportsPerturbation also gated off: Taylor δ-expansion needs a δ_prev companion to δ_z, also deferred. Printer prints prev. SaDetector + IsPureZPolynomial reject. EmitterBase: new abstract PrevRe/PrevIm bindings (default throw) + Emit dispatcher case routes PrevRef to them. Per-emitter bindings: - ScalarEmitter: pr/pi - Avx2Emitter: pr/pi (Vector256) - QdEmitter: pr_q/pi_q - DdDirectEmitter: pr_dd/pi_dd - QdDirectEmitter: pr_q/pi_q Template carries state via new {{PREV_DECL_*}} / {{PREV_UPDATE_*}} substitution pairs — empty strings when hasPrev=false so non- Phoenix calcs generate byte-identical bodies vs pre-Item-16. Per iter, update sequence is compute z_new from (zr, zi, pr, pi); pr := zr; pi := zi; zr := zr_new; zi := zi_new — must save old (zr, zi) to (pr, pi) BEFORE the new-z commit. AVX2 prev update is masked by activeMaskL (BlendVariable) so escaped lanes keep their pre-escape prev. Wired into every loop that uses {{SCALAR_Z_BODY}} or {{AVX2_Z_BODY}} or {{QD_Z_BODY}} or {{QD_DIRECT_BODY}} / {{DD_DIRECT_BODY}} — scalar fast path (3 variants: IteratePixelScalar, IteratePixelScalarRaw, GPU kernel), AVX2 lane (2 variants), DD direct, QD direct, QD continue, QD ref orbit, scalar ref orbit. Generated Phoenix stock calc: z*z + c + 0.5*prevMandelbrotPhoenixCalculator. Compiles, runs, no DE / no perturbation (HpDirect path active for any zoom > QdDirectZoomThreshold; otherwise scalar/AVX2 double-only). Tests: 68/68 PASS (5 new — round-trip, flag, diff, SA, lexer suggestion). All 6 prior stock calcs regenerated; --gentest MandelbrotZ2 0-diff (substitutions empty on non-Phoenix calcs so byte-identical output).

  • 15. Conditional / piecewise equations — if cond then a else b grammar. AST: new If(Cond, Then, Else) node (complex-valued) plus a separate CondNode / CondTerm mini-hierarchy used only inside conditions (Cmp(op, l, r), CondRe, CondIm, CondAbs2, CondConst). Keeping cond terms in a separate type means the differentiator never has to differentiate non-holomorphic Re/Im/Abs2 nodes — they live exclusively on the boolean side. Lexer: if/then/else/re/im/abs keywords; >, <, >=, <=, ==, != operators with = and ! alone rejected with hints to == / !=. Parser: if consumed at ParseExpr top so it binds loosest (whole then/else branches greedy); re(...) / im(...) extract scalars, abs(...) is sugar for |x|² (squared magnitude — saves the sqrt and matches typical bailout-style thresholds users think in). Differentiator: d(If(c,t,e))/dz = If(c, dt/dz, de/dz) — branches differentiate independently, cond stays untouched. Simplifier + printer + AstHelpers.Contains recurse into branches and through CondTerms' embedded AstNodes. AstSaDetector + IsPureZPolynomial reject (piecewise is non-polynomial). CalculatorGenApi: new hasCond flag drives supportsPerturbation = !(hasConj || hasFolded || hasDiv || hasTrans || hasCond); supportsDe stays true (each branch is holomorphic on its own — the boundary locus is the only discontinuity and is measure-zero). Emitters: eager-evaluate both branches (matches SIMD lane semantics — every lane evaluates every branch), select on the rendered cond at the end. - Scalar / DdDirect / Qd / PerturbDeriv: C# ternary on a rendered cond expression. DD/QD compare via .Hi / .X0 high-limb access (sufficient for the threshold use cases — the low limbs only matter on a measure-zero locus). - Avx2Emitter: Avx.Compare(left, right, FloatComparisonMode .Ordered{Gt,Lt,Ge,Le,Eq,Ne}NonSignaling) → Vector256 mask → Vector256.ConditionalSelect(mask, then, else). Ordered chosen so NaN operands compare false (matches C#). - Avx512DerivEmitter: same approach, 8 Vector512 lanes, Avx512F.Compare + Vector512.ConditionalSelect. Generation pipeline first attempt revealed PerturbDerivEmitter and QdEmitter also see If (PerturbDeriv via dz/dc chain even when perturbation is disabled, Qd because the QD reference orbit always runs the source equation); both gained OpIf accordingly. Tests: 63/63 PASS (9 new — round-trip × 4, diff × 1, SA × 1, flag × 1, lexer × 2). All 6 stock calcs regenerated; --gentest MandelbrotZ2 0-diff. Sample equation if abs(z) > 4 then z else z*z + c generates clean bodies in every path; if re(z) > 0 then z*z + c else z*z*z + c produces correct branched derivatives end-to-end.

  • 14. Trig + transcendental ops — Sin, Cos, Exp, Log AST nodes. Lexer keywords + parser atoms. Differentiator chain rules: d/dz sin(u)=cos(u)·u', cos→-sin·u', exp→exp(u)·u', log→u'/u. Simplifier pass-through. Printer cases. AstHelpers Contains recurses. AstSaDetector + IsPureZPolynomial reject (non-polynomial). CalculatorGenApi adds hasTrans flag; gates SupportsPerturbation off (Taylor δ-step not derived for non-poly nodes — perturbation/BLA/SA disabled when trig present). Emitters: ScalarEmitter uses complex identities sin(a+bi)=sin(a)cosh(b)+i·cos(a)sinh(b) (etc); Avx2Emitter + Avx512DerivEmitter emit per-lane scalar fallback (4 / 8 lanes via GetElement + Vector*.Create) since System.Math has no SIMD sin/cos; DdEmitter / DdDirectEmitter / QdEmitter / QdDirectEmitter promote .Hi / .X0 → double, call Math.X, demote back (precision degrades to ~16 digits inside the transcendental call; surrounding ±× preserved). DD/QD native transcendentals deferred to Phase D-4. PerturbDerivEmitter + Avx512DerivEmitter also need OpDiv (chain rule d(log(u))/dz injects u'/u even when source equation has no Div). Unit tests: 54/54 PASS (12 new — parser round-trip × 5, diff × 5, SA detector × 1, lexer suggestion × 1). All 6 stock calcs regenerated; --gentest MandelbrotZ2 0 diff.

  • 13. Histogram equalization — public int[] IterationBuffer + public float[] SmoothBuffer exposed on every generated calculator. Allocated in Resize(). ColorFor + ColorForDd take an optional bufIdx param; when ≥ 0 they write iter + smooth to buffers alongside the colour map call. Every render path (SP scalar, AVX2 SP, GPU post-process, AVX-512 perturbation, scalar perturbation, HpDirect QD, HpDirect DD, PerPixelQdContinue) passes its pixel index through. ApplyHistogramEqualization + BuildHistogramCdf + ApplyHistogramEqualizationWithCdf ported from legacy MandelbrotCalculator, simplified — distance / normal / finalZ extras not plumbed (themes that need them degrade to smooth-count-only via IColorMap defaults). Tests: 36/36 PASS, --gentest MandelbrotZ2 0 diff.

  • [~] 27. GPU reference orbit — SCAFFOLD shipped 2026-06-22 (Wave 2.12, Hi-only kernel; QD body deferred). Files: Engine/Calculators/Gpu/GpuQD.cs — ILGPU-friendly QD math primitive port (mirror of Abstractions/Math/QuadDouble.cs). Uses Dekker's split-based TwoProduct instead of Math.FusedMultiplyAdd — ILGPU 1.5.3 doesn't intercept the BCL FMA intrinsic and JIT throws "An internal compiler error has been detected" otherwise. Verified compiles; not yet invoked by a kernel. Engine/Calculators/Gpu/MandelbrotRefOrbitGpu.cs — single-thread sequential ref-orbit kernel + host shim. Packs 8 limbs/iter into a RefOrbitSlot struct so the typed kernel loader stays at 4 generic params (8 parallel ArrayView<double> blew past the loader's practical ceiling; kernel JIT failed identically to the FMA case). Private CUDA-preferred accelerator (TryAcquireFp64) bypasses the shared GpuAcceleratorHost: that singleton's GetPreferredDevice(preferCPU:false) on this dev box returns Intel UHD OpenCL, which has no FP64 support → "Float64 (double) type is not supported on this device". Walks devices CUDA → CPU (no cheap pre-flight FP64 probe for arbitrary OpenCL — skip). Engine/Calculators/MandelbrotCalculator.csUseGpuReferenceOrbit static toggle (default off). CalculateHighPrecision's QD branch routes through TryComputeReferenceOrbitQDGpu, which mirrors the centre-cache short-circuit in ComputeReferenceOrbitQD, runs the GPU compute, updates the _refZr / _refZrLo / _refZrX2 / _refZrX3 orbit arrays + cache fields. Silent fallback to CPU on any GPU failure (init / JIT / copy); failure reason logged via Debug.WriteLine. Default off keeps the HP path bit-identical to pre-2.12. Program.cs--gpurefprobe three-way bench at 1e15 + 1e30 saprobe coords: CPU-QD (truth), CPU-Hi (Hi-only baseline matching kernel math), GPU-Hi (kernel output). Reports ms + Δ(GPU-Hi vs CPU-Hi) — expected at FP64 round-off; Δ(GPU-Hi vs CPU-QD) — chaos- amplified, expected large until QD kernel slice lands. Writes gpurefprobe.out. Smoke on dev hardware (CUDA GeForce GT 710, FP64 1/24-rate): kernel JITs (~490 ms first call, cached after); second call 1.54 ms vs CPU-QD 0.54 ms. Δ(GPU-Hi vs CPU-Hi)=346 — CUDA's FMA-fused mul-add diverges from x86's two-step mul+add at the ULP, chaos-amplified across 282 iters. Not a bug — IEEE-FP64 semantics differ between backends. Build clean (0 errors); 140/140 server tests pass with toggle off. Remaining slices (separate WIP): 1. QD body in the kernel — currently iterates Hi-only doubles. Swap to GpuQDMath.{Mul,Square,Add,Sub,MulD} calls. Blocker: ILGPU 1.5.3 trips on Renorm5 / ThreeSum deep (s, e1, e2) = ThreeSum(...) deconstruction cascades during IR inlining. Two options: (a) rewrite GpuQDMath primitives to use mutable struct outputs instead of value tuples (large mechanical port — primitives + every algebra op); (b) bump ILGPU to 2.x (different IR pipeline; needs full GPU-kernel regression sweep against the 7 existing GPU calcs). 2. Perf-win on modern CUDA — GT 710's 1/24-rate FP64 will never beat CPU on a sequential dep chain. Re-bench on a consumer-grade FP64 card (Quadro / RTX A-series) to size the actual offload win and decide whether to auto-enable the toggle when the GPU path measures faster at first call. 3. Multi-orbit batch kernel — true parallelism win lands when Wave 6 (multi-cluster glitch rebase) needs N candidate orbits per frame. Same kernel, Index1D becomes the orbit index, output ArrayView grows to N × maxIter slots. Lets the GPU bury its launch + JIT cost across hundreds of orbits. 4. OD ref-orbit GPU port — follow-on once QD kernel lands; mirror ComputeReferenceOrbitOD for zoom > 1e50. ILGPU 1.5.3 tuple-chain blocker likely worse with 8-limb math; ties to the same struct-output rewrite or ILGPU upgrade.

Known issues (separate tasks)

  • Pan/keyboard input fails at zoom ≥ ~1e24 — UI input layer updates CenterX/CenterY only, not the QD limbs. Pan delta at zoom 1e24 is ~1e-27, well below CenterX's ULP (~2.6e-16 for |center| ~ 1). Update accumulates to zero. Fix lives in the Avalonia/WinForms pan-zoom command pipeline, not in CalcGen.

  • Generated perturbation loses detail past zoom 1e12 — FIXED (two-part). User-visible symptoms: high-detail regions render as solid-colour blobs at zoom 1e12+, worsens with zoom; small solid- colour dot at exact frame centre; double-clicking eventually resolves both (each click re-centres → new ref orbit, sometimes lucky). Reported on AVX-2-only hardware (no AVX-512).

    1. Scalar perturbation δ-update term order (PRIMARY FIX). AstPerturbation.BuildDeltaUpdate builds the Taylor δ-step by summing partial-derivative terms with outer loop k (δ powers), inner m (ε powers). For z²+c this emitted terms in order (k=0,m=1) ε, (k=1,m=0) 2Z·δ, (k=2,m=0) δ² → AST ((0+ε)+2Z·δ)+δ². At deep zoom |2Z·δ| ≫ |ε|, so the middle step (ε + 2Z·δ) is tiny + big → ε is rounded to ULP of 2Z·δ → per-pixel signal is lost → adjacent pixels compute identical δ → high-detail regions collapse to solid colour. Legacy MandelbrotCalculator hand-codes (2Z+δ)·δ + dc which adds dc LAST as a fresh, ε-scale addition. Fix: swap outer/inner loops to m outer, k inner — pure-δ terms (m=0) accumulate first at their |Z|·|δ| scale, then ε terms (m≥1) added last at their own scale. AST becomes ((0+2Z·δ)+δ²)+ε. Algebraically identical, numerically correct at deep zoom. Single 1-line code change in AstPerturbation.BuildDeltaUpdate. Fixes both scalar tail and AVX-512 lane (both consume the same AST). Generates for all polynomials — Z3 becomes 3Z²δ + 3Z·δ² + δ³ + ε, Z4/Z5 similar. --calcgen-test 90/90 PASS; --gentest MandelbrotZ2 0-diff (low zoom unaffected; the fix only changes ULP-level rounding behaviour at deep zoom).
    2. AVX-512 lane DD-promoted smooth count (secondary). SIMD lane was calling ColorFor (plain double log(|z|)) instead of ColorForDd (DD via QD ref orbit Lo limbs). Plain-double log(|z|) collapses past zoom 1e12 because |z|≈√Bailout is constant across adjacent pixels at single-precision. SIMD lane now captures per-lane δ at escape (finalDrVec / finalDiVec), reconstructs |z|² as DD via DD(refZr[it], refZrLo[it]) + DD(dr, 0), routes to ColorForDd. In-set lanes short-circuit. Affects AVX-512 hardware only — the user's machine (AVX-2 only) never enters this lane, but the fix lands for correctness on AVX-512 systems. FractalRenderHost.Trigger GZ2 branch: UsePerturbation = true; UseBla = true; UseSa = true (workaround removed). User-side verification at coords (-1.1727, -0.2968) zoom 1e12-1e16 still needed.
    3. Pauldelbrot relative-magnitude glitch detection (third fix). After parts 1+2 user reported the same blob pattern still present on AVX-2-only hardware at coords (-1.7687788..., 0.001738...) zoom 3.4e19 — large centre formation renders perfectly but smaller Julia formations have solid-colour centres. Double-clicking on a mini eventually resolves it (recentres ref orbit onto that mini). Classic cluster- glitch pattern: single reference orbit at the view centre can't represent the local dynamics of every mini-Julia in the frame. Pixels in off-centre minis converge to similar δ values → per-pixel signal lost → all pixels in the mini bail at the same iter → solid colour. Legacy MandelbrotCalculator's strict-equality glitch check (zr == Zr) misses this "soft glitch" because δ is tiny- but-nonzero. New PerturbGlitchTolerance property (default 1e-6, Pauldelbrot's classic value): pixel flagged glitched when |δ|² < tolerance · |z|² AND it > 4 (skip early iters where small δ is legitimate). Glitched pixels fall to per-pixel HP-direct via ComputePixelQdContinue (or ComputePixelDdDirect at zoom < QdDirectZoomThreshold). Added to both scalar tail and AVX-512 SIMD lane. Cost: more HP-direct calls in mini-Julia regions → slower but correct. Tighten (smaller tolerance) for perf, loosen for cleaner images. Set 0.0 to disable (revert to strict- equality only — matches legacy behaviour). Note legacy MandelbrotCalculator also lacks this check but renders these views correctly via a separate mechanism we haven't fully isolated (possibly the AVX-2 SIMD path's glitched flag escalating an entire 4-pixel group to QD when ANY lane glitches — see line 1989 in MandelbrotCalculator.cs; gen's per-pixel scalar path can't aggregate this way). True fix is cluster rebase (Item 7) — shared ref orbit per cluster instead of per-pixel HP-direct — but Pauldelbrot detection is the correctness floor that unblocks it.
    4. Gate scalar-tail glitch fallback by zoom (perf). After parts 1-3 the soft-glitch detection promoted slow zoom-in to per-pixel ComputePixelQdContinue at the moment status flipped from PT to mention QD-PT. QD-continue does ~50 FLOPs/iter, ~5× the cost of DD-direct. AVX-512 SIMD lane already had a zoom gate (Zoom >= QdDirectZoomThreshold ? QD-continue : DD-direct); scalar tail's glitch fallback didn't. Added the same gate. Result: deep-zoom-but-not-extreme renders stay on the DD-direct slow path which is fast enough for interactive use. Status bar showing [DD] instead of [QD-PT] confirms gate engaged.
  • AVX-2 SIMD DD-direct fallback (DD4) — IMPLEMENTED. Whole-frame TryRenderHpDirect DD path now vectorises 4 pixels at a time using the existing DD4 type (4-lane SIMD double-double via AVX-2 + FMA). New private method ComputePixel4Dd4Direct iterates z = p(z, c) in DD4 across 4 adjacent pixels per row, tracks per-lane escape via DD4.EscapeMask, snapshots per-lane Hi/Lo limbs at moment of escape into stack spans, then scatters to ColorBuffer via 4 ColorForDd calls. Gating: only engages for plain polynomial equations (z^d + c, d ∈ 2..16) — DD4 lacks Conj/Fold/transcendental/piecewise/prev operations the scalar DdDirectEmitter emits for those equations. Detection via AstSaDetector.DetectZdPlusC(root) >= 2. For non-polynomial equations, the const SupportsDd4Direct = false DCEs the DD4 path at JIT time; the body placeholder substitutes to a no-op stub so the dead method still compiles. DD4 body generation: CalcGen takes the scalar DdDirectEmitter output and does textual substitution (zr_ddzr_dd4, etc.; DD DD4 ). Same expression compiles against DD4 because the operators (+, -, *, FromCenterOffset) have identical signatures. Trade-off: dz/dc derivative NOT tracked in DD4 path (would double per-iter work without a Vector256-typed derivative rewrite). Themes that consume DE / normal channels degrade gracefully to smooth-count-only via IColorMap defaults. Acceptable because DD-HP triggers only when perturbation has failed entirely (ref escaped + alternate-ref search + cluster rebase all unviable) — user wants any frame, not pretty 3D. Status bar: DD-HP4 (was DD-HP) when DD4 path engaged. 4 pixels per row's hot inner loop expected speedup: ~3-4×. --calcgen-test 90/90 PASS; --gentest MandelbrotZ2 0-diff (gentest's centres don't trigger HP-direct so the DD4 path isn't exercised there).

  • Cluster rebase on perturbation glitch (Item 7) — IMPLEMENTED through Wave 6 closeout (MVP + guards A/B/C + multi-cluster D + cross-frame cache E). Glitched pixels deferred during the main perturbation pass into a ConcurrentBag<(int x, int y)>. After Parallel.For completes, ProcessClusterRebase spatial-partitions the bag into clusters via a 16×16-cell occupancy grid + 8-conn BFS flood-fill on occupied cells. Each cluster runs through ProcessSingleCluster: zoom gate (skip below 1e25 — DD-direct cheaper than rebase build there), bbox-cohesion guard (skip long-thin tendrils with density < 2%), centroid build of a shared QD reference orbit via BuildRebaseRefOrbitQd (no BLA / no SA), cross-frame cache lookup (4-slot LRU via TryGetCachedRebaseOrbit / InsertCachedRebaseOrbit keyed by centroid within scale·16 tolerance + maxIt), sample-probe of the first 8 pixels (commit only when ≥ 50% land), then parallel TryIterateRebasePixel over the remainder. Pixels that glitch again or whose rebase orbit exhausts fall to per-pixel HP-direct (HpDirectGlitchPixel). Below MinClusterSizeForRebase = 32 pixels per cluster, HP-direct straight away. Properties: UseClusterRebase (default true since Wave 6 — AVX-2 perturbation lane parity reached, multi-cluster + cache reduce wasted-work to acceptable), MinClusterSizeForRebase (default 32). Both scalar tail and AVX-2 + AVX-512 SIMD lanes defer to the same bag. Cost: one QD orbit build (~10-50 ms at maxIt=10000) per cluster per frame (cached cross-frame), zero for cached hits. Win: 5-20× speedup on mini-Julia clusters vs per-pixel HP-direct (per-glitch cost drops from ~500 µs DD-direct to ~50 µs perturbation iter). Scattered mini-Julias each get their own rebase orbit instead of one centroid orbit that fits none of them. --calcgen-test 90/90 PASS; --gentest MandelbrotZ2 0-diff (gentest's interior centres produce no glitches so the rebase path doesn't engage on its sample grid).

  • Smarter ref-orbit selection at iter-0 escape — FIXED. When the view centre's own orbit escapes at iter 0 at deep zoom

    • high maxIt, TryRenderPerturbation used to return false → whole-frame TryRenderHpDirect (DD per pixel, seconds per render on the user's AVX-2 hardware). Now searches a fixed pattern of 12 candidate ref points within the visible frame (4 corners, 4 mid- edges, 4 inner-ring at 0.45 / 0.45 / 0.22 fractions of half-frame extent), picks the one with the longest-surviving orbit (early- exits at the first that reaches maxIt). Per-pixel ε is shifted by the chosen ref offset (subtracted from the view-centre-relative ε in scalar tail, SIMD lane, and SA prelude — refOffsetX/Y). BLA dcMaxAbs and SA maxOffX/Y extended to bound the worst- corner |ε| against the shifted ref. Cache: _cachedRefOffsetX/Y alongside the orbit so subsequent frames at the same centre reuse the chosen alternate. Probe is cheap (QD iteration, no BLA, no array writes); 12 probes at maxIt=10000 add ~5-30 ms to the first frame at a centre, free on subsequent frames. When no candidate meets the MinAcceptLen=64 floor, falls back to the existing whole-frame HP-direct path — same behaviour as before, plus 12 fast probes. Status bar still shows PT / QD-PT (perturbation active) instead of DD-HP / QD-HP when an alternate is found. Helpers: TryFindAlternateRefQd, ProbeRefOrbitLengthQd in the template. --calcgen-test 90/90 PASS; --gentest MandelbrotZ2 0-diff (alternate search only engages when the centre escapes; the gentest's centres are inside the set).

Clone this wiki locally