-
Notifications
You must be signed in to change notification settings - Fork 0
Performance Roadmap
Companion to Lighting-FX-Roadmap.md and Fractal-Expansion-Roadmap.md.
Tracks interactive-render performance work after the second deferred-wave
landed (GPU tonemap+bloom+edge, HDR DoF in 7 calculators).
P0–P6 + P7 infrastructure + P7a + P7b + P7c.1 + P7c.2 + P7c.3 + P7c.4 shipped — every 3D raymarcher (Mandelbulb, Mandelbox, QJulia, QMandel, Bicomplex, Kleinian) and both KIFS folds (Menger + Sierpinski) now have an opt-in ILGPU GPU path with real 3-light Lambert + per-light soft shadow + DE-cone AO + ambient + sky-tint fog AND single-scattering volumetric in-scatter (Beer–Lambert with per-step shadow toward Light1, FBM cloud-density modulation, cloud self-shadow, height falloff, adaptive step-count LOD) AND one-bounce Fresnel-weighted reflection probe AND full PBR/SSS/Triplanar/ Caustics/IBL (Cook-Torrance GGX D·G·F per-light spec with per-channel F0 ramped by Metallic, Burley back-light SSS lobe per-light, procedural triplanar texture modulating albedo pre-lighting, sky-gradient at the surface normal blended into ambient via IblStrength, procedural caustics on upward-facing surfaces with height falloff and Light1 shadow gating). Albedo still rides the cheap step-hash palette (color-map GPU port remains a separate future phase). The full P7 lift is complete — all P7c sub-phases shipped.
| Phase | Status | Notes |
|---|---|---|
| P0 — Buffer pooling | ✅ shipped |
PostPassBufferPool + GpuBufferPool
|
| P1 — Raymarch micro-opts | ✅ shipped |
1L<<k + ExpNegSmall Padé |
| P2 — Low-res interactive preview | ✅ shipped |
LowResPreview helper + 7 calculators wired |
P3 — IDistanceEstimator struct generic |
✅ shipped | Generic Shade<TDe> + 5 concrete DE structs (Mandelbulb, Mandelbox, QJulia, QMandel, Bicomplex, Kleinian). KIFS + UserBulb still route through DelegateDeAdapter; complex closures, lower-priority follow-up |
| P4 — Adaptive volumetric LOD | ✅ shipped |
VolumeStepsFalloff knob (default 0.5) |
| P5 — Bloom blur SIMD | ✅ shipped | Vector<float> horizontal + vertical interior pass; scalar edge tails |
| P6 — Bundle GPU dispatch single sync | ✅ shipped |
ScreenSpacePost.BeginGpuFrame / EndGpuFrame + shared device color buffer across SSAO/tonemap/edge |
| P7 infrastructure | ✅ shipped |
GpuRaymarchParams, GpuAcceleratorHost (singleton ILGPU context+accelerator), GpuKernelUtils (BuildPrimaryRay / SphereClip / LambertShade / CheapPalette), LightingFxData.UseGpuRender flag (default off) |
| P7a — Mandelbulb / Mandelbox / Menger GPU | ✅ shipped |
MandelbulbGpuCalculator + MandelboxGpuCalculator + MengerGpuCalculator (KIFS Menger fold; Sierpinski still CPU). Cheap-palette shading on the GPU branch — SSAO/tonemap/bloom/shadow/AO/edge/DoF/volumetric drop silently when UseGpuRender is on; P7c lifts those |
| P7b — QJulia / QMandel / Bicomplex / Kleinian / KIFS-Sierpinski GPU | ✅ shipped |
QJuliaGpuCalculator + QMandelGpuCalculator + BicomplexGpuCalculator + KleinianGpuCalculator (fixed 4-sphere tetrahedral preset, centres packed scalar-by-scalar) + SierpinskiGpuCalculator (sibling to Menger; KifsCalculator dispatches by fold). Same cheap-palette shading as P7a — P7c lifts the full pipeline |
| P7c.1 — GPU shading: 3-light + shadow + AO + fog | ✅ shipped |
GpuShadingParams + GpuKernelUtils.ComposePixel/CheapAlbedo/SkyGradient. All 7 P7-pattern kernels (Mandelbulb / Mandelbox / Menger / Sierpinski / QJulia / QMandel / Bicomplex / Kleinian) carry per-fractal inline SoftShadow + DE-cone AO loops calling the local DE — ILGPU can't take a generic DE struct at LoadAutoGroupedStreamKernel level. Cheap-palette albedo. Scalar exp-fog (volumetric ships in P7c.2) |
| P7c.2 — GPU volumetric in-scatter | ✅ shipped |
GpuShadingParams extended with VolumeSteps + FogHeightFalloff + VolumeNoise* + VolumeSelfShadow* + VolumeStepsFalloff + SceneTime. GpuKernelUtils gains kernel-side Hash3D / ValueNoise3D / FbmCloud3D / VolumetricDensityMul / CloudSelfShadow / ExpNegSmall + ComposeSurfaceNoFog / ApplyScalarFog / PackBgra. All 8 P7-pattern kernels run the per-pixel volume march inline (per-step SoftShadow toward Light1 calls each fractal's own DE) and pick volumetric vs scalar exp-fog by VolumeSteps > 0. Unlocks 12b-volumetric on GPU |
| P7c.3 — GPU reflection (secondary ray) | ✅ shipped |
GpuShadingParams extended with ReflectStrength + ReflectSteps + ReflectMaxDist + Metallic (mirrors LightingFxData.ReflectionStrength/ReflectionSteps/Metallic). GpuKernelUtils gains kernel-side Reflect3D + FresnelMix (Schlick F0=0.04+0.96·metallic) + ReflectShade (sky-tint env proxy with exp(-tR·0.15) hit attenuation — HDRI IBL is GPU-blocked until P7c.4). All 8 P7-pattern kernels run a per-pixel reflect-march inline against the local fractal DE (ILGPU can't take a generic DE through LoadAutoGroupedStreamKernel, same reason as P7c.1 SoftShadow + P7c.2 volumetric). Reflection block sits between surface compose and the volumetric/scalar-fog branch so god-rays cover the reflection. ReflectStrength==0 → bit-identical legacy. Unlocks 16b |
| P7c.4 — GPU PBR/SSS/IBL/triplanar/caustics | ✅ shipped |
GpuShadingParams extended with Roughness + SpecularStrength + SubSurfaceStrength + TriplanarKind + TriplanarScale + TriplanarStrength + TriplanarTint + IblStrength + CausticsStrength + CausticsFloorY + CausticsScale + CausticsColor + CausticsAnimSpeed (mirrors the matching LightingFxData fields). GpuKernelUtils gains kernel-side GgxSpecLight (Cook-Torrance D·G·F per directional light, per-channel F0=0.04+(albedo-0.04)·Metallic), BurleySssLight (distortion=0.3 / power=4 back-light lobe), TriplanarSample2D (Wood / Marble / Rock / Checker procedurals indexed by int kind), ApplyTriplanar (squared-normal-weighted projection blend), EvaluateCaustics (mirrors the CPU two-sin-cascade ⁶ pattern), and ComposeSurfacePbr — the new full-pipeline composer that walks triplanar → 3-light diffuse + GGX spec + Burley SSS → AO → IBL ambient blend → metal-suppress-diffuse → albedo multiply → caustics. All 8 P7-pattern kernels swapped their ComposeSurfaceNoFog call for ComposeSurfacePbr. All P7c.4 knobs default-zero — bit-identical legacy when off. Cheap-palette albedo still feeds the PBR layer; per-pixel color-map GPU port stays a separate future phase. HDRI env sampling stays GPU-blocked — sky-gradient at the surface normal is the same MVP fallback the CPU pipe uses |
Current state — frame budget at 1920×1080 on a representative scene (Mandelbulb, key+fill light, SSAO 16 samples, soft shadow 24 steps, AO on, bloom on):
| Stage | Approx CPU cost | Notes |
|---|---|---|
Primary raymarch (Calculate + Shade) |
80–95 % | DE-bound; double math; delegate-dispatched DE |
ApplyToneMapBloom (CPU path) |
3–8 % | GPU path roughly 0.5–1 ms |
ApplyHdrDof (CPU) |
0–4 % | Only when DoF aperture > 0 |
ApplySsao (CPU) |
1–3 % | GPU path ~0.3 ms |
ApplyEdgeInk |
<1 % | GPU path negligible |
ApplyLensPost |
<1 % | One full-res snapshot clone per frame |
After GPU tonemap+bloom landed, the raymarch is the dominant cost on every non-trivial 3D scene. Post-pass work is no longer the bottleneck; raymarch
- DE delegate dispatch is.
- P0 — Buffer pooling (post-pass scratch + GPU device buffers)
-
P1 — Cheap raymarch micro-opts (
Math.Pow(2,k),Math.Expapprox) - P2 — Resolution-scaled interactive preview in 6 raymarchers
-
P3 —
IDistanceEstimatorstruct generic — devirtualize DE inner loops - P4 — Adaptive volumetric LOD (step count by depth)
- P5 — Bloom blur SIMD
- P6 — Bundle GPU dispatch into single device sync
- P7 — DE-GPU port (the big one — unblocks 12b-volumetric, 16b, 20b)
Current state. BuildBloomPyramid allocates 4× float[3*n] per frame
(emissive, two mip levels, output buffer). ApplyHdrDof allocates 3×
more for the skewed-blur passes plus one CoC buffer. ApplyLensPost and
byte ApplyDof clone the entire ColorBuffer. At 1920×1080 that's
~150 MB of fresh allocation every frame.
GpuPostKernels.TryApplySsao / TryApplyToneMapBloom / TryApplyEdgeInk
each call _acc.Allocate1D 3–7× per dispatch. CUDA's allocator is fast
but not free; OpenCL/Velocity allocators are ~10× slower.
Why. GC stutter at interactive framerates. Visible as periodic frame hitches under heavy effect stacks. Also wastes ~3× memory bandwidth reading/writing freshly-zeroed pages.
Scope.
- Add
Engine/Rendering/Lighting/PostPassBufferPool.cs:-
ThreadStaticpool keyed by(byteSize, kind). -
Rent(int n3)returnsfloat[];Return(float[])reclaims. - Bound pool size to 4 buffers per size class so resolution changes don't permanently retain old buffers.
-
- Replace
new float[3 * n]sites inScreenSpacePost.cswithusing var buf = PostPassBufferPool.RentScope(3 * n)(struct that returns on dispose). - GPU side: add
GpuBufferPoolinGpuPostKernels.cskeyed by(typeSize, elementCount). Rent on dispatch, return onSynchronize. Survives one frame inside the pool, recycled next.
Touch points. Engine/Rendering/Lighting/ScreenSpacePost.cs,
Engine/Rendering/Lighting/GpuPostKernels.cs. New file
PostPassBufferPool.cs.
Risk. Pool retains memory across resolution changes — bound the pool
size and clear on ClearGBuffer to avoid leak on resize.
Expected win. Eliminates ~150 MB/frame GC pressure; smooths interactive frametimes by 5–15 % under heavy effect stacks.
Current state.
-
ShadingPipeline.cs:169and:390:double d = i.Epsilon * Math.Pow(2, k);in DE-cone AO. CalledAoSamplestimes per pixel.Math.Pow(2, k)is ~25 ns on x64;1 << kcast to double is ~1 ns. -
ShadingPipeline.cs:236and:578:T *= Math.Exp(-density * stepSize)in volumetric in-scatter. CalledVolumeStepstimes per pixel.Math.Expis ~15 ns; a Padé approximation valid for small arguments is ~3 ns.
Why. Hot inner loops. At 1920×1080 with AO=8 + Volume=32, that's
8M Math.Pow calls + 32M Math.Exp calls per frame.
Scope.
- Replace
Math.Pow(2, k)with(double)(1 << k)in both AO sites. - Introduce
ExpNegSmall(x)static helper using Padé (2,2) approx forx ∈ [0, 0.5], fall back toMath.Expoutside. Call site:double a = density * stepSize; T *= a < 0.5 ? ExpNegSmall(a) : Math.Exp(-a);
Touch points. Engine/Rendering/Lighting/ShadingPipeline.cs.
Risk. Visual divergence at extreme densities. Padé(2,2) accuracy ~1e-4 in the valid range — well below visible.
Expected win. 2–5 % on AO-heavy scenes, 8–15 % on volumetric-heavy scenes.
Current state. UserBulbCalculator already implements a low-res
preview path (lowRes branch in Calculate, renders at half-res then
upscales). The other six 3D raymarchers (Mandelbulb, Mandelbox, KIFS,
QJulia, QMandel, Bicomplex, Kleinian) render at full resolution
unconditionally.
Why. Interactive UI (rotate / pan / zoom) needs sub-100 ms frametimes for smooth navigation. At 1920×1080 a Mandelbulb full-res render is 200–500 ms on a 16-core CPU. Half-res preview cuts that to 50–125 ms with no perceptible quality loss during motion.
Scope.
- Extract
UserBulbCalculator's low-res render → upscale pattern intoEngine/Rendering/LowResPreview.csstatic helper. API:public static (uint[] buffer, float[] depth, float[] normal, float[] hdr) AllocatePreviewBuffers(int width, int height, double scale); public static void UpscaleToFullRes( uint[] preview, int pw, int ph, uint[] full, int fw, int fh); - Wire each of the six remaining 3D calculators to call the helper when
IsInteractiveis true. Final render goes through the existing full-res path. - Tunable:
LowResPreview.ScaleFactorfield onFractalParameters(default 0.5). Mirrors the existing UserBulb knob.
Touch points. All seven 3D calculators' Calculate methods. New
file LowResPreview.cs. UI binding: FractalParamsView numeric input.
Risk. Preview can mask aliasing or detail issues that only show
up at full res. Mitigated by always doing a deferred full-res pass when
the user stops interacting (existing FractalRenderHost debounce).
Expected win. 2–4× interactive framerate on the six raymarchers that don't have preview today.
Current state. Engine/Rendering/Lighting/ShadingPipeline.cs:30:
public delegate double DistanceEstimator(double x, double y, double z);Every call to de(...) inside SoftShadow, AO loop, reflection march,
and volumetric in-scatter is an indirect virtual dispatch (~3 ns
overhead vs ~1 ns for a direct call). On a heavy scene that's
(Shadow=24 + AO=8 + Refl=24 + Vol=32×Shadow=24) = ~830 DE calls / pixel. At 1920×1080 = 1.7 billion indirect calls/frame.
Why. JIT cannot inline through a delegate. Each indirect dispatch is a cache-miss-prone instruction-pointer chase. The DE itself is typically 30–80 ns of math; the dispatch overhead is 3–5 % of that. Devirtualized + inlined, the DE becomes part of the calling function's hot loop body and benefits from cross-function CSE.
Scope.
- Introduce
IDistanceEstimatorinterface inAbstractions/Rendering/Lighting/:public interface IDistanceEstimator { double Evaluate(double x, double y, double z); }
- Refactor each calculator's DE closure into a
readonly structimplementing the interface. Captured parameters become struct fields:public readonly struct MandelbulbDe : IDistanceEstimator { public readonly double Power; public readonly int Iter; public MandelbulbDe(double power, int iter) { Power = power; Iter = iter; } public double Evaluate(double x, double y, double z) => MandelbulbCalculator.MandelbulbDE(x, y, z, Power, Iter, out _); }
- Re-sign
ShadingPipeline.Shade,SoftShadow, etc. as generic onTDe : struct, IDistanceEstimator:public static uint Shade<TDe>(in ShadingInputs i, …, in TDe de) where TDe : struct, IDistanceEstimator { … }
- Call sites pass the struct directly:
var de = new MandelbulbDe(power, iter); ColorBuffer[idx] = ShadingPipeline.Shade(in inputs, baseColor, in fx, in de, idx, depthBuf, normalBuf, hdrBuf);
- Keep the delegate-based
Shadeoverload as a thin wrapper that boxes the delegate into anIDistanceEstimatoradapter struct, so migration is incremental.
Touch points. Engine/Rendering/Lighting/ShadingPipeline.cs
(signature changes + helper generics), all seven 3D calculators
(define DE struct + switch call site), Abstractions/Rendering/Lighting/
(new interface).
Risk. Generic instantiation blowup — each calculator triggers a
fresh Shade<TDe> codegen. ~7 instantiations × ~3 KB IL each = small.
JIT compile time increases by ~50 ms on first frame per fractal —
amortized after warmup.
Expected win. 8–15 % raymarch speedup. Biggest single CPU-side lever before DE-GPU port.
Current state. ShadingPipeline.cs:206:
double stepSize = i.TotalT / vs;Volumetric in-scatter walks VolumeSteps samples uniformly across the
ray length, regardless of distance to camera. Distant pixels with long
ray total-T get the same scattering quality as near pixels but require
the same expensive DE-shadow probe per step.
Why. Visual contribution of in-scatter on distant pixels is
already attenuated by T falling toward zero. Past a certain depth,
extra samples produce no visible difference but cost full DE+shadow.
Scope.
- Adaptive step count:
int vs = fx.VolumeSteps; if (i.TotalT > 4.0) vs = Math.Max(4, (int)(vs / (1.0 + (i.TotalT - 4.0) * 0.5)));
- Add
VolumeStepsFalloffknob toLightingFxData(default 0.5, 0 = no LOD = legacy bit-identical).
Touch points. Engine/Rendering/Lighting/ShadingPipeline.cs,
Abstractions/Rendering/Lighting/LightingFxData.cs,
UI.Avalonia/ViewModels/FractalParamsViewModel.Lighting.cs.
Risk. Visible banding if falloff is too aggressive on scenes with distant dense fog. Default conservative; expose knob.
Expected win. 30–60 % on volumetric-heavy scenes with deep depth range.
Current state. ScreenSpacePost.DownsampleAndBlur runs scalar
5-tap separable Gaussian inside Parallel.For. Each tap reads 3
floats per pixel. AVX2 Vector<float> carries 8 floats / op on
modern x64.
Why. Blur is purely sequential per-pixel math — perfect SIMD fit. Currently ~2–3 ms per bloom build at 1920×1080. SIMD'd: ~0.5–0.8 ms.
Scope.
- Rewrite horizontal blur loop using
Vector<float>over the interleaved BGR layout. Care needed: 3-float stride doesn't divide evenly into 8-float lanes; packfloat[w*h*4](RGBA) for SIMD path, unpack on output. - Vertical pass same pattern.
- Keep scalar fallback for
Vector.IsHardwareAccelerated == false.
Touch points. Engine/Rendering/Lighting/ScreenSpacePost.cs
(DownsampleAndBlur).
Risk. Layout change (3-float → 4-float interleaved internally) adds a copy in / copy out. Net win only when blur cost dominates the copy; verified on AVX2 desktops.
Expected win. ~4× faster bloom build on CPU path. Mostly relevant when GPU bloom is unavailable.
Current state. GpuPostKernels.TryApplySsao,
TryApplyToneMapBloom, and TryApplyEdgeInk each call
_acc.Synchronize() internally and copy results back to host before
returning. With all three enabled the host stalls 3× per frame waiting
for device.
Why. Each Synchronize is a full host-device round trip
(~100–500 µs depending on driver). Bundling all three into a single
device queue with one final sync removes two round trips.
Scope.
- Add
GpuPostKernels.BeginFrame()/EndFrame()API. Inside, allTryApply*calls queue kernels and copy-out commands but skipSynchronize.EndFramedoes the single sync + copy-out. -
ScreenSpacePostdispatches all GPU-eligible passes inside the begin/end pair. CPU-only passes (lens, HUD) wait until afterEndFrame. - Keep current per-call API for callers that don't want to batch.
Touch points. Engine/Rendering/Lighting/GpuPostKernels.cs,
Engine/Rendering/Lighting/ScreenSpacePost.cs, each calculator's
post-pass sequence (one optional wrap).
Risk. Error handling — a failure in one pass currently falls back to CPU for that pass only. Batched mode needs to either fall back to CPU for the whole batch on any kernel failure, or track per-pass failures and recover.
Expected win. 0.5–1.5 ms saved per frame when all GPU passes active. Bigger on slow accelerators.
Current state. Each fractal's distance estimator runs as a C# method
called per-pixel from the calculator's Calculate loop on CPU.
MandelbulbDE, MandelboxDE, MengerSpongeDE, QuatJuliaDE,
QuatMandelDE, BicomplexDE, KleinianDE, UserBulbDE — eight DEs
totalling ~600 lines of math, all double precision.
UserBulbGpuCalculator already runs the UserBulb DE on ILGPU as a
proof-of-concept. The pattern there is the template for the rest.
Why. Three open phases blocked on this:
- 12b-volumetric — GPU port of in-scatter requires DE callable from inside a kernel. Currently impossible (managed delegate).
-
16b — recursive reflection bounces with GGX importance sampling.
Cost scales linearly with
MaxBounces× DE evals; on GPU it's affordable, on CPU it isn't. - 20b — true per-eye stereo doubles the raymarch cost. On GPU that's a non-issue; on CPU it halves framerate.
Plus the raymarch hot loop itself is the 80–95 % cost bucket and the only remaining big CPU lever.
Scope (the multi-day refactor).
-
Define GPU DE convention. Each DE rewritten as a
staticmethod marked[ILGPUKernel]-compatible (noMath.Powwith non-const exponent, nooutparams,floatprecision only on GPU anddoubleon CPU). Match theUserBulbGpuCalculatorpattern. -
Per-fractal GPU kernel. Rewrite each calculator's
Calculateinner loop as an ILGPU kernel. Mirror existing CPU pattern: ray construction → sphere-trace loop → on-hit compute normal via gradient-tetra → write color/depth/normal/HDR. -
Shading on GPU. Lift
ShadingPipeline.Shadeto a static GPU helper that takes the DE as astructgeneric argument (mirrors the P3 CPU plan). Single source compiles to CPU + GPU. - G-buffer co-resident on device. Once Shade runs on GPU, the depth + normal + HDR buffers stay device-resident and feed directly into the 12b GPU post-pass without a host copy.
-
CPU fallback path retained. Every calculator keeps its CPU
Calculatefor headless servers without an accelerator and for bit-identity regression testing. -
Calculator dispatch.
FractalRenderHostselects GPU calculator whenUseGpuRenderflag is on and accelerator is available; otherwise CPU. Mirrors the existingUseGpuPostpattern.
Touch points. Roughly:
- New:
Engine/Calculators/Gpu/*GpuCalculator.cs— seven new files, one per 3D fractal (UserBulb already exists). - Modified:
Engine/Rendering/Lighting/ShadingPipeline.cs— generic TDe parameter, GPU-friendly math (noMath.Powwith var exponent). - Modified:
Engine/Rendering/Lighting/GpuPostKernels.cs— accept device-resident G-buffers as input instead of copying from host. - Modified:
Engine/Rendering/FractalRenderHost.cs— GPU calculator dispatch routing. - New:
Abstractions/Rendering/Lighting/IDistanceEstimator.cs— shared with P3.
Risk.
-
Precision divergence. GPU runs
float; CPU runsdouble. The CPU and GPU paths will not be bit-identical even with the same DE. Document expected magnitude; visual regression tests need a tolerance band. - Driver fragmentation. ILGPU's CUDA + OpenCL backends handle the same kernel differently. Validate on at least one of each before shipping default-on.
-
Maintenance burden. Two implementations of every DE going
forward. Mitigated by keeping the math in a single
statichelper that compiles for both — the GPU calculator just calls the same helper from inside a kernel. - Recursive calls. ILGPU kernels can't recurse. Any DE that uses recursion (Kleinian's inversion cascade is iterative — fine; check KIFS, Mandelbox — fine) is portable. Document any DE that isn't.
Expected win. 10–30× raymarch speedup on a mid-range GPU vs CPU. Unlocks 12b-volumetric, 16b, 20b. Eliminates the hardware-bound ceiling.
Time estimate. One DE per day for the first three (Mandelbulb, Mandelbox, KIFS — established patterns), faster after that. Plus 1–2 days for shading + dispatch refactor. Total: ~10 working days for the full set + plumbing.
-
Visual regression harness. P3 / P5 / P7 all change pixel output.
A scripted
--batch --headlessrender → SHA256 on each of the 7 raymarchers at default knob values, with a tolerance band for the GPU paths, would catch regressions early. -
Frame-time HUD. Phase 19's debug HUD already shows light directions
and param bars. Extend with a per-stage frame-time microbar
(raymarch / SSAO / tonemap / bloom / DoF / lens / edge) so the
user can see at a glance where time is going. Cheap;
Stopwatch.GetTimestamp()deltas around eachApply*call. - Pool telemetry. P0 + P6 add pools. Expose pool hit/miss counts via a debug flag so undersized pools don't silently degrade perf.
-
Default-zero gating remains the rule. Every new perf knob
(
VolumeStepsFalloff,LowResPreview.ScaleFactor,UseGpuRender) defaults to the legacy value so old scenes render identically.
| Phase | Lift | Impact (interactive) | Unlocks |
|---|---|---|---|
| P0 | ½ day | 5–15 % smoother | — |
| P1 | 1 hour | 2–15 % | — |
| P2 | 2 days | 2–4× | — |
| P3 | 1 day | 8–15 % | P7 |
| P4 | 2 hours | 30–60 % (vol scenes) | — |
| P5 | 1 day | 4× bloom CPU | — |
| P6 | ½ day | 0.5–1.5 ms / frame | — |
| P7 | ~10 days | 10–30× raymarch | 12b-vol, 16b, 20b |
After P0–P6 ships, you are CPU-raymarch-bound until P7 lands. After P7, you are GPU-shader-bound — orders of magnitude headroom for higher sample counts, recursive bounces, true stereo, and the remaining deferred Lighting/FX work.