-
Notifications
You must be signed in to change notification settings - Fork 0
CalculatorGen 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.
These don't change rendering output. Each is bounded and safe to land between bigger features.
- 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.
-
Better parse error messages — line/column from the lexer,
"expected X, got Y" hints, suggestion when a keyword is misspelled
(
cong→ did you meanconj?). - Higher integer power cap — Pow exponent currently capped at 16. Bump to 64. SA detector + emitters already iterate; no algorithmic change required.
-
Per-equation custom bailout radius — CLI flag
--bailout Rembeds the radius² as the template'sBailout2const. Phoenix / Nova / some user fractals need < 4; others want > 32. -
Compile cache for hot-load —
CalculatorGenHotLoadhashes the (equation, name) tuple, returns the previously-loadedTypeif hash matches and the assembly is still loaded. Saves a Roslyn round-trip on duplicate compiles. -
Auto-unload previous assembly — when a fresh compile succeeds,
unload the prior
AssemblyLoadContext. Prevents process bloat on rapid iteration.
- 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.
- 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.
- 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).
- 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.
- 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.
- 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.
- 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.
-
Trig + transcendental ops —
sin,cos,exp,logAST nodes. Lexer + parser keywords. Differentiator rules. Emitters delegate toMath.Sin/Math.Cos/Math.Exp/Math.Log. Non-polynomial but holomorphic; distance estimate preserved. -
Conditional / piecewise equations —
if Re(z) > 0 then a else bgrammar. Branch in emitted code. Distance estimate breaks at the discontinuity but smooth count works. Anti-holomorphic-equivalent flag for SupportsDe. -
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
prevreference node, plumb second buffer through ref orbit + perturbation.
-
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 —
ODstruct +OdEmitter+ threshold property. -
DD-precision BLA tables — at the deepest zooms, BLA
A_nis 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).
- 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.
- Progressive rendering — first render at ¼ resolution, present, refine to ½, then full. The renderer's overlay system can pop the refined frames in place.
- 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.
- 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.
- 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.
-
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.
- 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).
- 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".
- 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.
- Unit tests — parser, differentiator, emitter golden-output tests. Catch regressions when adding new ops in Phase D-3.
-
Roslyn source generator ✅ Shipped 2026-06-22 (Wave 2.13). Compile-time
IIncrementalGeneratorinCalculatorGen.SourceGen/consumes[assembly: GeneratedCalculator(equation, name, IncludeSelfTest?, Bailout?)]declarations and emits one calculator per attribute instance viacontext.AddSource. Stock calcs migrated to a 10-line registry atEngine/Calculators/Generated/GeneratedCalculatorAttributes.cs; the 20 hand-checked-in*Calculator.cs+*CalculatorSelfTest.csfiles (~33 K lines) replaced by.g.csoutputs inobj/. Legacydotnet run -p CalculatorGenCLI +CalculatorGenApi.TryCompileAndLoadhot-load path unchanged — only the checked-in generated tree is eliminated. See Wave 2.13 status log entry inDocs/Open-Work-Plan.mdfor the slice breakdown.
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.
-
Iteration index (
n/iter) keyword — user-equation bug: legacy C#Step(Complex z, Complex c, int n)signature usesnfor iter count; pastingz*z + c + 0.001*ninto Compile & Load hit CalcGen lexer with "Unknown identifier 'n'". AddedIterRefAST node (real-valued scalar leaf),iterkeyword withnas alias (both lex to TokenKind.Iter → IterRef). Differentiator:IterRef → 0(opaque, like PrevRef/DeltaRef). SaDetector rejects. CalculatorGenApi:hasIterflag gatessupportsPerturbation = false(δ-Taylor can't linearise iter- dependent step — δ doesn't change n across ref orbit vs pixel);supportsDestays true (iter is real scalar, no holomorphic chain participation). EmitterBase: newIterRevirtual binding plusIterImLiteralfor 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: ScalarEmitteriter; Avx2Emitteriter_v(Vector256 broadcast); QdEmitter / QdDirectEmitteriter_q(QD); DdDirectEmitteriter_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 (itorn) 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 MandelbrotZ20-diff. Sample equationz*z + c + 0.001*nproduces clean bodies: scalardouble iter = (double)it; ... (0.001 * iter); AVX2Vector256<double> iter_v = Vector256.Create((double)n); ... Avx.Multiply(z_re5, iter_v); QDQD 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 → generatedusing 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 (legacyUserEquationCalculatorRoslyn-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 rejectedComplexas unknown identifier. NewEquationPreprocessor.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-rejectsComplex.ImaginaryOne,new Complex(a, b),Complex.Abs(x)(DSLabsis squared-mag, not sqrt) with crisp error messages. UnknownComplex.Xflagged with full supported list. Fixed-point loop handles nested calls (Complex.Pow(Complex.Pow(z, 2), 3)). Wired into bothOnHotLoadViaCalcGenandOnGenerateViaCalcGeninUserEquationViewModel. Tests: 85/85 PASS (17 new preprocessor tests covering all translations + reject paths + the user's actual reported equationreturn z * Complex.Pow(z,-3) + c * Complex.Pow(c,-2);). -
1.
--analyzeflag —Program.csin 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 Rflag — embedsR²into templateBailout2. -
5. Compile cache for hot-load —
_cachedict inCalculatorGenHotLoad, keyed on(equation, name). -
6. Auto-unload previous assembly —
_lastContext.Unload()on next successful compile; cache entries from dying context dropped. -
22.
--benchmarkflag — hostProgram.csarg; hot-compiles an equation and times the ladder default/shallow/mid-1e3/deep-1e6 /deep-1e9. Writesbenchmark.outnext 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) + _cachedSaStartinCalculator.template.cs, gated oncacheHit && _cachedUseSa == UseSa && _cachedScale == scale. Skips SA-build loop on zoom-only / theme-change frames. -
8. Higher SA orders (3 → 8) —
SaRecurrenceEmitterrewritten to take anorderparameter, 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 jaggedsaSr[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, changeSaOrders = 8const in template, expand the unrolled Sr/Si declarations + save/load blocks, and update the order default inCalculatorGenApi.Generate. -
11. SA-skip derivative seed — drv/div seeded inside the
if (saStart > 0)per-pixel block via the polynomial derivativedz/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 > 1e150short-circuits NaN/Inf propagation (S_N overflow → Inf−Inf = NaN slips past relative tolerance becauseNaN > xis false); (2)aMag * maxEps > 0.25bounds 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 loweredExtendedRefZoomThreshold1e12 → 1e9 to close the gap where plain-double ref orbit lost precision before QD engaged (legacyMandelbrotCalculatoruses DD ref orbit always; adding the DD codegen path is a future task). Reported againstMandelbrotZ2at 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 switchedSaTolerancedefault 1e-6 → 1e-3 matching legacy's proven value. -
10. Generic symbolic SA emitter —
AstSaDetectorextended withDetectPolyInZPlusC(root)that matches any polynomial in z (no CRef, Conj, Folded, Div anywhere) added to a single+c. Returns(polyZ, degree).SaRecurrenceEmitter.EmitGenericthen derives the recurrence symbolically: for each k=1..degree it computes(1/k!) ∂^k F / ∂z^kviaAstDifferentiator, simplifies, and renders the (Re, Im) expression at Z_n viaScalarEmitterinto named localspk1_Re/Im..pkN_Re/Im. Convolution + Σ logic shared with the pure z^d+c fast path.CalculatorGenApiprefers the fast path when applicable; falls back to generic for cases likez²+az+c,2z³-z+c,z^6+c,z^4-z²+c. Test coverage:--calcgen-test42/42 PASS (6 new generic-detector tests). -
12. BLA hierarchy — flat per-iter
blaAr/Ai/Br/Bi/Rarrays replaced withFracturingFog.FFMath.BlaTable(hierarchical,log2(refLen)levels, 2^k-step merged BLAs per level). AddedBlaTable(Bla[] level0, int refLen, double dcMaxAbs)overload toMath/Bla.csso 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 callsbla.Lookup(it, dMag2, maxIt); largest valid level applied viaδ ← A·δ + B·ε,drv ← A·drv,it += L − 1. Cache:_cachedBlaTable + _cachedBlaDcMaxAbsgated on scale equality (BLA radii are scale-dependent, ref orbit isn't). Self-test PASS 36/36,--gentest MandelbrotZ20 diff. Note: tested as potential fix for task #14 detail gap — ineffective; gap roots elsewhere. -
16. Phoenix / multi-var two-step recurrence — new
PrevRefAST node referencing z_{n-1}. Lexer keywordprev(Levenshtein suggestion list updated;prv → prevtest). Parser atom case. Differentiator treatsprevas opaque (∂prev/∂z = 0) — produces a WRONG dz/dc for Phoenix equations, but the value is never consumed becausehasPrevgatessupportsDe = falseinCalculatorGenApi(proper Phoenix DE needs a paralleldprev/dcderivative-state vector updated asdprev := dzeach iter — deferred).supportsPerturbationalso gated off: Taylor δ-expansion needs a δ_prev companion to δ_z, also deferred. Printer printsprev. SaDetector + IsPureZPolynomial reject. EmitterBase: new abstractPrevRe/PrevImbindings (default throw) +Emitdispatcher 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_qTemplate carries state via new{{PREV_DECL_*}}/{{PREV_UPDATE_*}}substitution pairs — empty strings whenhasPrev=falseso non- Phoenix calcs generate byte-identical bodies vs pre-Item-16. Per iter, update sequence iscompute 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 byactiveMaskL(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*prev→MandelbrotPhoenixCalculator. 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 MandelbrotZ20-diff (substitutions empty on non-Phoenix calcs so byte-identical output). -
15. Conditional / piecewise equations —
if cond then a else bgrammar. AST: newIf(Cond, Then, Else)node (complex-valued) plus a separateCondNode/CondTermmini-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/abskeywords;>,<,>=,<=,==,!=operators with=and!alone rejected with hints to==/!=. Parser:ifconsumed atParseExprtop 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: newhasCondflag drivessupportsPerturbation = !(hasConj || hasFolded || hasDiv || hasTrans || hasCond);supportsDestays 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/.X0high-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 seeIf(PerturbDeriv via dz/dc chain even when perturbation is disabled, Qd because the QD reference orbit always runs the source equation); both gainedOpIfaccordingly. Tests: 63/63 PASS (9 new — round-trip × 4, diff × 1, SA × 1, flag × 1, lexer × 2). All 6 stock calcs regenerated;--gentest MandelbrotZ20-diff. Sample equationif abs(z) > 4 then z else z*z + cgenerates clean bodies in every path;if re(z) > 0 then z*z + c else z*z*z + cproduces correct branched derivatives end-to-end. -
14. Trig + transcendental ops —
Sin,Cos,Exp,LogAST 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 addshasTransflag; 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 MandelbrotZ20 diff. -
13. Histogram equalization —
public int[] IterationBuffer+public float[] SmoothBufferexposed on every generated calculator. Allocated inResize(). ColorFor + ColorForDd take an optionalbufIdxparam; 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+ApplyHistogramEqualizationWithCdfported from legacyMandelbrotCalculator, simplified — distance / normal / finalZ extras not plumbed (themes that need them degrade to smooth-count-only viaIColorMapdefaults). Tests: 36/36 PASS,--gentest MandelbrotZ20 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 ofAbstractions/Math/QuadDouble.cs). Uses Dekker's split-basedTwoProductinstead ofMath.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 aRefOrbitSlotstruct so the typed kernel loader stays at 4 generic params (8 parallelArrayView<double>blew past the loader's practical ceiling; kernel JIT failed identically to the FMA case). Private CUDA-preferred accelerator (TryAcquireFp64) bypasses the sharedGpuAcceleratorHost: that singleton'sGetPreferredDevice(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.cs—UseGpuReferenceOrbitstatic toggle (default off).CalculateHighPrecision's QD branch routes throughTryComputeReferenceOrbitQDGpu, which mirrors the centre-cache short-circuit inComputeReferenceOrbitQD, runs the GPU compute, updates the_refZr / _refZrLo / _refZrX2 / _refZrX3orbit arrays + cache fields. Silent fallback to CPU on any GPU failure (init / JIT / copy); failure reason logged viaDebug.WriteLine. Default off keeps the HP path bit-identical to pre-2.12.Program.cs—--gpurefprobethree-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. Writesgpurefprobe.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 toGpuQDMath.{Mul,Square,Add,Sub,MulD}calls. Blocker: ILGPU 1.5.3 trips onRenorm5/ThreeSumdeep(s, e1, e2) = ThreeSum(...)deconstruction cascades during IR inlining. Two options: (a) rewriteGpuQDMathprimitives 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,Index1Dbecomes 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; mirrorComputeReferenceOrbitODfor 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.
-
Pan/keyboard input fails at zoom ≥ ~1e24 — UI input layer updates
CenterX/CenterYonly, not the QD limbs. Pan delta at zoom 1e24 is ~1e-27, well belowCenterX'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).
-
Scalar perturbation δ-update term order (PRIMARY FIX).
AstPerturbation.BuildDeltaUpdatebuilds the Taylor δ-step by summing partial-derivative terms with outer loopk(δ powers), innerm(ε 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·δ)istiny + big→ ε is rounded to ULP of 2Z·δ → per-pixel signal is lost → adjacent pixels compute identical δ → high-detail regions collapse to solid colour. LegacyMandelbrotCalculatorhand-codes(2Z+δ)·δ + dcwhich adds dc LAST as a fresh, ε-scale addition. Fix: swap outer/inner loops tomouter,kinner — 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 inAstPerturbation.BuildDeltaUpdate. Fixes both scalar tail and AVX-512 lane (both consume the same AST). Generates for all polynomials — Z3 becomes3Z²δ + 3Z·δ² + δ³ + ε, Z4/Z5 similar.--calcgen-test90/90 PASS;--gentest MandelbrotZ20-diff (low zoom unaffected; the fix only changes ULP-level rounding behaviour at deep zoom). -
AVX-512 lane DD-promoted smooth count (secondary). SIMD
lane was calling
ColorFor(plain doublelog(|z|)) instead ofColorForDd(DD via QD ref orbit Lo limbs). Plain-doublelog(|z|)collapses past zoom 1e12 because|z|≈√Bailoutis constant across adjacent pixels at single-precision. SIMD lane now captures per-lane δ at escape (finalDrVec / finalDiVec), reconstructs|z|²as DD viaDD(refZr[it], refZrLo[it]) + DD(dr, 0), routes toColorForDd. 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.TriggerGZ2 branch:UsePerturbation = true; UseBla = true; UseSa = true(workaround removed). User-side verification at coords (-1.1727, -0.2968) zoom 1e12-1e16 still needed. -
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. NewPerturbGlitchToleranceproperty (default 1e-6, Pauldelbrot's classic value): pixel flagged glitched when|δ|² < tolerance · |z|²ANDit > 4(skip early iters where small δ is legitimate). Glitched pixels fall to per-pixel HP-direct viaComputePixelQdContinue(orComputePixelDdDirectat 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'sglitchedflag 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. -
Gate scalar-tail glitch fallback by zoom (perf). After
parts 1-3 the soft-glitch detection promoted slow zoom-in to
per-pixel
ComputePixelQdContinueat the moment status flipped fromPTto mentionQD-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.
-
Scalar perturbation δ-update term order (PRIMARY FIX).
-
AVX-2 SIMD DD-direct fallback (DD4) — IMPLEMENTED. Whole-frame
TryRenderHpDirectDD path now vectorises 4 pixels at a time using the existingDD4type (4-lane SIMD double-double via AVX-2 + FMA). New private methodComputePixel4Dd4Directiteratesz = p(z, c)in DD4 across 4 adjacent pixels per row, tracks per-lane escape viaDD4.EscapeMask, snapshots per-lane Hi/Lo limbs at moment of escape into stack spans, then scatters to ColorBuffer via 4ColorForDdcalls. Gating: only engages for plain polynomial equations (z^d + c, d ∈ 2..16) —DD4lacks Conj/Fold/transcendental/piecewise/prev operations the scalarDdDirectEmitteremits for those equations. Detection viaAstSaDetector.DetectZdPlusC(root) >= 2. For non-polynomial equations, the constSupportsDd4Direct = falseDCEs 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 scalarDdDirectEmitteroutput and does textual substitution (zr_dd→zr_dd4, etc.;DD→DD4). Same expression compiles against DD4 because the operators (+, -, *, FromCenterOffset) have identical signatures. Trade-off:dz/dcderivative 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 viaIColorMapdefaults. 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(wasDD-HP) when DD4 path engaged. 4 pixels per row's hot inner loop expected speedup: ~3-4×.--calcgen-test90/90 PASS;--gentest MandelbrotZ20-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,ProcessClusterRebasespatial-partitions the bag into clusters via a 16×16-cell occupancy grid + 8-conn BFS flood-fill on occupied cells. Each cluster runs throughProcessSingleCluster: 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 viaBuildRebaseRefOrbitQd(no BLA / no SA), cross-frame cache lookup (4-slot LRU viaTryGetCachedRebaseOrbit/InsertCachedRebaseOrbitkeyed by centroid withinscale·16tolerance + maxIt), sample-probe of the first 8 pixels (commit only when ≥ 50% land), then parallelTryIterateRebasePixelover the remainder. Pixels that glitch again or whose rebase orbit exhausts fall to per-pixel HP-direct (HpDirectGlitchPixel). BelowMinClusterSizeForRebase = 32pixels 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-test90/90 PASS;--gentest MandelbrotZ20-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,
TryRenderPerturbationused toreturn false→ whole-frameTryRenderHpDirect(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). BLAdcMaxAbsand SAmaxOffX/Yextended to bound the worst- corner |ε| against the shifted ref. Cache:_cachedRefOffsetX/Yalongside 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 theMinAcceptLen=64floor, falls back to the existing whole-frame HP-direct path — same behaviour as before, plus 12 fast probes. Status bar still showsPT/QD-PT(perturbation active) instead ofDD-HP/QD-HPwhen an alternate is found. Helpers:TryFindAlternateRefQd,ProbeRefOrbitLengthQdin the template.--calcgen-test90/90 PASS;--gentest MandelbrotZ20-diff (alternate search only engages when the centre escapes; the gentest's centres are inside the set).
- high maxIt,