-
Notifications
You must be signed in to change notification settings - Fork 0
Vulkan Compute DevelopmentPlan
Companion pages: Technical Index · Architecture Overview · Cross-Platform Roadmap · Performance Development Plan · Deep-Zoom Perturbation
Important
Snapshot 2026-07-18. Session decision: keep the multi-renderer architecture (DX stays primary on Windows, Silk GL + Skia CPU stay). Add a Vulkan compute-only backend for Linux/macOS to close the one real gap — GPU-computed fractal + colour on non-Windows hosts. This is a forward-looking plan, not shipped work. Nothing here touches the WinForms shell (deprecated) or the proven Windows DX path.
Branch baseline: feature/ui-overhaul (working) → cut feature/vulkan-compute from main for this work.
Goal: GPU-accelerated fractal iter/smooth + EvalPalette colour on linux-x64 / linux-arm64, reusing the existing HLSL kernel via HLSL→SPIR-V (DXC). macOS via MoltenVK is a stretch, not a gate.
Non-goal (this doc): replacing DX on Windows; a WSI swapchain/present path in Vulkan; a GLSL emitter twin; touching the ILGPU calculator path.
Decided across the 2026-07-18 design session. Summary of the reasoning:
-
The gap is compute, not present. Present is already a thin, CPU-buffer contract
(
IFractalRenderer.UpdateTexture(uint[] BGRA)). Only Windows (D3D11 HLSL compute) computes the fractal + colour on GPU; Silk GL and Skia are CPU-compute. Linux GPU rendering means adding GPU compute, not a new display path. -
Shader reuse decides the API. Vulkan consumes SPIR-V; DXC compiles the existing HLSL
kernel → SPIR-V. The shallow escape-time kernel (
MandelbrotKernelSource) + colour emitter (ColorGenHlslEmitter) survive largely intact. This retires the previously-scoped GLSL-emitter twin.Correction (2026-07-19, V4): this bullet originally read "the 856-line perturbation kernel … survive largely intact." There is no HLSL perturbation kernel — the HLSL path is a ~290-line FP32 escape-time kernel used only at
Zoom ≤ MaxGpuZoom (1e4); deep-zoom perturbation lives on the CPU. The reuse argument holds for the shallow kernel + colour pipeline (what V1–V3 shipped); a GPU perturbation kernel is net-new, tracked in #82. See §13. -
Compute-only skips Vulkan's worst boilerplate. The current D3D kernel already round-trips
through CPU (writes
gColorstructured buffer →Map→UpdateTexture). Mirror that: Vulkan headless compute → map → existing Silk GL blit for present. No swapchain, no WSI surface, no present-sync, no framebuffer. Drops into the current interface with zero present rewrite. - Native on Linux. No MoltenVK translation pain (that only bites the macOS stretch goal). Silk.NET.Vulkan is same-vendor with the existing Silk GL usage — no new dependency ecosystem.
Runner-up rejected: GL 4.3 compute (reuses existing GL context, fastest to first-pixel) — rejected because it forces a permanent GLSL emitter + GLSL kernel port to maintain in parallel with HLSL. WebGPU rejected (WGSL = third shader lang, worse HLSL reuse). OpenCL/CUDA rejected (declining / NVIDIA-only). Full detail in session notes.
| Asset | Where | Relevance |
|---|---|---|
| Present contract | IFractalRenderer |
UpdateTexture / Render / Resize. Vulkan compute output feeds this unchanged. |
| Kernel interface | IGpuKernel |
Comments already name "future Metal/Vulkan backend" + "HLSL/SPIR-V-compatible palette". Designed for this. |
| Surface abstraction | IGpuSurface |
Exposes X11 Window XID "consumed by Vulkan/OpenGL". |
| Backend dispatch | RendererFactory.NonWin32Backend |
Open slot; --renderer flag already wired. Add a Vulkan enum member. |
| HLSL kernel | MandelbrotGpuKernel |
The CS source to compile → SPIR-V. BuildHlsl(...) composes cbuffer + IO + EvalPalette. |
| HLSL colour emit | ColorGenHlslEmitter |
Per-theme float3 EvalPalette(...); reused as-is via DXC. |
| Present backend | SilkGLRenderer |
Existing GL blit (fColor = texture(uTex,vUv).bgra); reused for present. |
| Smoke-gate pattern |
Compute.Smoke, Rendering.Silk.Smoke
|
Exit 0/1/2; device enumerate → run kernel → histogram sanity. New Vulkan gate mirrors this. |
| Golden colour gate |
--colorprobe (Program.cs) |
Vulkan colour output must match the same embedded golden digest as CPU/HLSL. |
Note — ILGPU coexists, is not replaced. The ILGPU path (C# kernels → CUDA/OpenCL/CPU) drives
the fractal calculators + Compute.Smoke. This Vulkan work is the renderer-side HLSL compute
(iter/smooth + EvalPalette), a different kernel. See §7 open question O1 — the spike must decide
whether ILGPU could deliver Linux compute more cheaply before committing to Vulkan build-out.
Windows Linux (new)
┌───────────────┐ ┌───────────────────┐
calc/colour │ D3D11 HLSL CS │ │ Vulkan compute CS │ ← SPIR-V (DXC of same HLSL)
│ → gColor buf │ │ → gColor buf │
└──────┬────────┘ └─────────┬─────────┘
│ Map(Read) │ vkMapMemory
▼ ▼
uint[] BGRA ───────────────────► uint[] BGRA
│ IFractalRenderer.UpdateTexture(...)
▼ ▼
DX present Silk GL blit (existing)
-
One HLSL source, two compilers. D3DCompiler (Windows) and DXC→SPIR-V (Linux) both consume
BuildHlsl(...)output. Guard the tiny dialect deltas (if any surface) behind a#ifdef VULKANor a compile-time string swap inBuildHlsl. -
Compute + present split by GPU queue, not process. Vulkan owns compute; GL owns present.
Interop is a CPU copy (the existing round-trip) — deliberately, to avoid Vulkan↔GL shared-memory
extensions in v1. A zero-copy
VK_KHR_external_memorypresent is a later optimisation (§7 O3).
Each slice lands behind a smoke gate and is independently revertible. Checkbox = acceptance.
| Slice | Issue |
|---|---|
| V0 spike | #39 |
| V1 kernel | #40 |
| V2 colour | #41 |
| V3 backend/present | #42 |
| V4 deep-zoom (parity — resolved trivially, §13) | #43 |
| V6 GPU perturbation kernel (net-new; spike CLEARED §14; full build LANDED Vulkan §15, D3D + GUI-enable fast-follow) | #82 |
| V5 macOS (stretch) | #44 |
V0 — Spike: HLSL→SPIR-V compute proof (headless, no UI) ← #39 ✅ DONE — see §9
-
dotnetprojectRendering.Vulkan.Smoke(mirrorsCompute.Smoke): enumerate Vulkan devices, create instance/device/compute queue, no swapchain. - DXC toolchain wired: compile a trivial HLSL CS (
gColor[i] = pack(uv.x, uv.y, 0)) → SPIR-V, load, dispatch 64×64,vkMapMemory, read back. - Histogram sanity like
Compute.Smoke(≥3 distinct values; corners bit-exact vs CPU pack). Exit 0/1/2. - Runs on linux-x64 in CI under Mesa lavapipe (software Vulkan) so no GPU-in-CI needed.
-
Gate:
Rendering.Vulkan.Smokeexit 0 on lavapipe. Answered O1/O2 — see §9.
- DXC-compile the shared
MandelbrotKernelSource.BuildBase()(the exact HLSL FXC compiles for the D3D base variant) → SPIR-V. - Wire cbuffer
Params(b0) → UBO;RWStructuredBufferu0–u2 (iter/smooth/finalZD) → storage buffers;StructuredBuffert0 (per-row) → storage buffer. Bindings via DXC-fvk-*-shift(b→0, t→+100, u→+200) so the shared source needs novk::bindingattributes. - iter/smooth matches a C# float-mirror reference for a fixed view within a documented boundary-chaos band (see §10). Bit-exact is impossible cross-vendor (FMA/transcendental).
-
Gate:
--vulkanprobein Program.cs; ULP-band parity vs the CPU reference (not a strict golden digest — set-boundary pixels are chaotic). Green on real GPU + lavapipe CI.
- DXC-compile the
emitColor:truevariant with a real theme'sHlslPaletteBody+ prelude (Greyscale;gColoru3 → binding 203). - Per-theme SPIR-V compile-cache keyed on
PaletteId— deferred to V3: the probe compiles one theme per run, so a cache has no consumer until the renderer switches themes at runtime. -
Gate:
--colorprobepasses on the Vulkan backend — embedded golden digest of a deterministic CPU mirror (reference stability) + GPU-vs-mirror byte-disagreement band (cross-vendor). Colour parity is non-negotiable (palette fidelity is the product). ✅ GT 710; lavapipe CI.
- Vulkan compute at the
IGpuKernelboundary —VulkanComputeKernel(Rendering.Vulkan lib), the same boundary the D3D kernel uses. (Design note:IFractalRenderercan't compute — it receives a finished buffer — so "Vulkan compute" plugs in as anIGpuKernel, present staysSilkGLRenderer. Chosen over aVulkanComputeRenderer : IFractalRenderercomposite.) -
RendererBackend.Vulkanenum member +ProbeDescription()composite string +Create()present-via-GL routing (scaffolding). — CLI--renderer vulkanparse + bootstrap kernel-injection into the calculator deferred to the V3 GUI follow-up. -
ProbeDescription()returns "Vulkan (compute) + OpenGL (present)". - Live interactive pan/zoom on a Linux host + no-device fallback — V3 GUI follow-up (needs a
display; the headless
--vulkanrenderprobegate proves the kernel end-to-end first). -
Gate:
--vulkanrenderprobe(headless) drives the real kernel — colour + base parity, buffer persistence, resize realloc. ✅ GT 710; lavapipe CI. Interactive gate is the follow-up.
- Confirmed the deep-zoom render path is backend-independent, so Vulkan selection cannot
regress it, and the
--focusprobegate is green on HEAD (0.00 px focus error to 1e60). -
Correction (2026-07-19): the premise below was wrong — no HLSL perturbation kernel exists.
The HLSL compute kernel (
MandelbrotKernelSource) is a shallow FP32 split-centre escape-time kernel gated atMandelbrotCalculator.MaxGpuZoom = 1e4; above that the calculator never dispatches aGpuKernelat all — deep zoom is all-CPU (SIMD perturbation + DD/QD/OD reference orbit;MandelbrotRefOrbitGpuis an ILGPU reference-orbit helper, per-pixel δ stays on CPU). D3D and Vulkan therefore compute identical deep-zoom frames because neither touches the GPU past 1e4.--focusprobe/--navreproare headless calculator self-tests that run before any--rendererparse, so "re-run them on the Vulkan backend" was never meaningful. A real GPU perturbation kernel (ref-orbit SSBO + per-pixel δ + rebasing in-shader) is genuine net-new work that no backend has today — re-filed as its own spike-gated slice, #82. -
Gate (as-shipped):
--focusprobegreen on HEAD; deep-zoom output invariant under--rendererselection by construction (Zoom > MaxGpuZoom⇒ no GPU dispatch).
- Same SPIR-V through MoltenVK; validate the Vulkan-subset quirks. Gated
osx-arm64CI leg. - Gate: best-effort; CPU fallback stays the macOS default until this proves out.
-
Compiler: DXC (
dxc -T cs_6_0 -spirv). Ship the DXC native per-RID, or invoke a NuGet-packaged DXC. Decide in V0 (O2). -
Dialect deltas to watch:
RWStructuredBufferregister spaces → SPIR-V descriptor set/binding mapping (DXC-fvk-*-bindingflags);cbufferpacking → std140/push-constant layout; row-major vs column-major (irrelevant here, no matrices);fmod/fracalready normalised by the emitter. -
No GLSL, ever. The
ColorGenGlslEmitterproposed in the prior session is cancelled by this approach — one fewer emitter to maintain.
| Risk | Mitigation |
|---|---|
| DXC HLSL→SPIR-V feature gaps in the |
V0 spike de-risks with a trivial kernel first; V1 ports incrementally with per-variant gates. (The GPU perturbation kernel's feared DXC risk — QD/DD limb math in HLSL — was retired by the #82 spike, §14: the default δ path is plain double, no limbs; DXC cs_6_0 + FXC cs_5_0 both compile it, GT710 runs it. Needs shaderFloat64.) |
| Vulkan boilerplate cost (solo dev) | Compute-only headless removes ~60% of it (no WSI/swapchain/present). Silk.NET.Vulkan gives thin bindings. |
| CI has no GPU | Mesa lavapipe (software Vulkan) runs the smoke gates headless; real-GPU validation is manual/local. |
| Colour drift on a new backend |
--colorprobe golden gate is the hard stop; V2 cannot merge without it green. |
| Deep-zoom precision regression |
N/A for V1–V5 — deep zoom is all-CPU (Zoom > MaxGpuZoom 1e4 ⇒ no GPU dispatch), so no backend can regress it; --focusprobe green on HEAD confirms. Becomes a live risk only once #82 puts perturbation on the GPU. |
| Scope creep into Vulkan present | Explicit non-goal. Present stays GL. Zero-copy interop is a later optimisation, not v1. |
-
O1 — ILGPU instead of Vulkan? ILGPU already gives Linux GPU compute (OpenCL/CUDA/CPU) and is
in-tree. Could the renderer's iter/smooth + a C#
EvalPalette(via the existingColorGenEmitter, not HLSL) run on ILGPU and skip Vulkan entirely? Trade: ILGPU reuses the C# colour emitter but not the tuned HLSL kernel; Vulkan reuses the HLSL kernel but not the calculator infra. V0 must A/B this before build-out. - O2 — DXC delivery. Native per-RID binary vs NuGet package vs build-time SPIR-V bake? Bake at build avoids shipping a runtime compiler but loses per-theme JIT (themes are dynamic). Likely: JIT the base kernel + per-theme SPIR-V at runtime like D3D does.
-
O3 — Zero-copy present.
VK_KHR_external_memory+ GLEXT_memory_objectto skip the CPU round-trip. Real speedup only if compute→CPU→GL becomes the bottleneck (it isn't today — compute dominates). Defer. - O4 — Vulkan compute + GL present on the same GPU/context — confirm no driver conflict sharing the surface between a Vulkan compute queue and the GL present context on Mesa/NVIDIA Linux.
See the session response / issue for the actionable list; folded into slices above where they gate
work (lavapipe CI, --vulkanprobe, colour-golden reuse, ILGPU A/B in V0).
9. V0 spike findings (issue #39) — DONE
Landed on branch feature/vulkan-compute. New standalone project Rendering.Vulkan.Smoke
(Silk.NET.Vulkan 2.23, mirrors Compute.Smoke, exit 0/1/2), in four commits: (1) instance +
device enumeration (--list) + logical device + compute queue, no WSI; (2) DXC HLSL→SPIR-V + full
compute pipeline, dispatch 64×64, vkMapMemory read-back + sanity; (3) lavapipe CI leg;
(4) findings. Plus two CI fixups (see below).
Proven both ways. Local (real GPU, GeForce GT 710): DXC-compiles the trivial uv-gradient kernel
→ dispatch → read-back distinct=4096, all four corners bit-exact vs the CPU pack → exit 0. CI
(software Vulkan, Mesa lavapipe on linux-x64): Run Vulkan smoke exits 0 —
run 29682409585.
The trivial kernel reuses the exact cg_pack_bgra convention from
MandelbrotGpuKernel, so V1 swaps in the real body
with no packing/endianness surprises.
DXC→SPIR-V works end-to-end on the trivial kernel with no dialect surprises. That de-risks the
decisive advantage: the tuned HLSL escape-time kernel + ColorGenHlslEmitter survive intact
via DXC — zero re-port. (2026-07-19 correction: this originally said "856-line perturbation kernel";
that kernel does not exist — the reused HLSL is the ~290-line shallow escape-time + colour kernel. See
§13 / #82.) The ILGPU alternative would re-express iter/smooth + EvalPalette as C#
kernels (ColorGenEmitter), reusing the C# colour emitter but abandoning the tuned HLSL kernel and
adding a second colour-parity surface. HLSL reuse is the higher-value asset; proceed with Vulkan.
ILGPU stays the calculator-side path, unchanged (§2 note holds).
V0 invokes the dxc -spirv CLI at runtime (located DXC_PATH → VULKAN_SDK/Bin → PATH),
mirroring D3D's runtime compile. Themes are dynamic, so a build-time SPIR-V bake cannot cover
per-theme EvalPalette variants. Recommendation for V2+: ship the dxcompiler native per-RID and
call it in-proc (matches in-proc D3DCompiler on Windows; drops the per-compile process spawn + the
CLI dependency); NuGet Microsoft.Direct3D.DXC is a viable managed-resolved source. Keep the CLI as
the dev/CI fallback. On CI, DXC ships fine from the LunarG vulkan-sdk apt package
(/usr/bin/dxc, libdxcompiler 1.8).
-
Binding map: DXC needs explicit
[[vk::binding(set,binding)]]— do not rely on the default u-register→binding mapping. V1 maps cbufferb0→ push-constants/UBO andu0–u3storage buffers with explicitvk::binding. -
Memory: V0 uses one
HOST_VISIBLE|HOST_COHERENTstorage buffer (direct map, no staging). ADEVICE_LOCAL+ staging copy is a later perf option. -
Parity band: corners asserted bit-exact; interior pixels not (float ULP). V1's
--vulkanprobeshould adopt the documented ULP band, not strict bit-exact, for iter/smooth.
The Linux/macOS legs already fail at Build FracturingFog.App — it multi-targets
net10.0;net10.0-windows, and the CI step builds with no -f, so the net10.0-windows TFM pulls
the Windows-only FracturingFog.Win (WinForms) and trips NETSDK1073. Pre-existing on main; the
Vulkan steps are guarded !cancelled() so the gate still reports. Tracked in
#49 (fix: build App -f net10.0
on non-Windows legs). Fixed in #50.
10. V1 findings (issue #40) — DONE
The real SP Mandelbrot base kernel (iter/smooth, no colour) now runs on Vulkan compute and matches the D3D float math within a documented band.
One source, two compilers — realised. MandelbrotKernelSource
holds the cbuffer/IO header + CSMain body, extracted verbatim from MandelbrotGpuKernel. FXC
compiles it on Windows (unchanged D3D path); DXC compiles the same string to SPIR-V. The Vulkan
smoke <Compile Include ... Link>s that one dependency-free file — no Rendering.D3D/Engine
ProjectReference closure.
Binding map (the O1/O2 §9 note, resolved). The shared source carries no [[vk::binding]]
(those break FXC). DXC assigns bindings from register class via shift flags — -fvk-b-shift 0 0,
-fvk-t-shift 100 0, -fvk-u-shift 200 0 — giving deterministic, collision-free bindings
(Params UBO → set0/binding0; gPerRow t0 → 100; gIter/gSmooth/gFinalZD u0..u2 → 200/201/202).
Without the shift, DXC starts each register class at binding 0 and u0/t0 collide.
cbuffer → UBO packing. Params is 15 consecutive scalars (60 B) padded to a float4 multiple
(64 B). std140 packs scalars at 4-byte offsets with no interior padding, so the 64-byte C# blob maps
1:1 onto the DXC-generated UBO — the same bytes the D3D constant buffer uses.
Parity result. --vulkanprobe runs a fixed 128×128 shallow view and compares GPU iter/smooth
against a C# reference that mirrors the exact float ops. A pixel "agrees" when iter is equal and
(if escaped) smooth is within 0.05. Set-boundary pixels are chaotic — a 1-ULP FMA/transcendental
spread explodes the escape iteration there — so the gate bounds the disagreement fraction (≤1%)
- in-set drift (≤1%), robust to that noise while a broken kernel (wrong bindings/endianness/math) trips it at ~100%. Measured: GeForce GT 710 in-set drift 0.012%, disagree 0.171% (4 iter + 24 smooth); Mesa lavapipe (CI) green. A strict golden digest was rejected — it can't survive cross-vendor float without a false-fail.
- Colour splice points (
inSetColor/escapeColor/bulbSkipColor) already parameterised inMandelbrotKernelSource.HlslEntry; V2 supplies them + theEvalPalette/cg_pack_bgraprelude (still inMandelbrotGpuKernel.BuildHlsl— extract to the shared source when V2 needs it). - Per-theme SPIR-V compile-cache keyed on
PaletteId, mirroring the D3D_csByPaletteIdcache. -
--colorprobecolour-golden reuse is the V2 gate (colour parity is non-negotiable).
11. V2 findings (issue #41) — DONE
The colour-emitting kernel (EvalPalette → packed BGRA) now runs on Vulkan compute and matches a
deterministic CPU mirror of the same HLSL palette + pack within a documented band.
Colour source extracted (the §10 carry-over, done). The gColor UAV, ordered-dither
cg_pack_bgra, EvalPalette signature, and the three CSMain colour-write splices moved out of
MandelbrotGpuKernel.BuildHlsl into MandelbrotKernelSource.{ColorPreludeHead/Tail, *ColorSplice, BuildColor}. Same "one source, two compilers" rule as V1 — FXC (D3D, unchanged; BuildHlsl now
delegates) and DXC (V2) consume the identical string. Still no [[vk::binding]].
Binding, extended. The colour variant adds register class u3 (gColor). The same
-fvk-u-shift 200 0 puts it at binding 203; the probe wires a 6th descriptor (UBO@0,
SSBO@100/200/201/202/203). No new flag — the shift covers every u#.
Gate — two parts, cross-vendor-robust (--colorprobe). Colour parity is "non-negotiable" per
the plan, but a strict GPU digest can't survive cross-vendor float any more than V1's iter could.
So the gate splits the concern:
- an embedded golden digest of a deterministic CPU mirror (the Greyscale
EvalPalettebody +cg_pack_bgra, dither off) — pins that the reference view/theme/pack itself did not drift; and - a byte-disagreement band of the GPU colour vs that mirror (±1/channel to absorb rounding-at-noise, ≤2% of pixels) — robust across GPUs. Boundary pixels flip black↔grey when the GPU iter and mirror iter disagree on in-set membership, exactly the V1 chaos, now in colour.
Plus a degenerate guard (<3 distinct colours ⇒ unbound gColor/collapsed pack) and an opaque-alpha
guard (every pixel's top byte must be 0xFF). The Greyscale theme was chosen because its C# Map
maps 1:1 to a short HLSL body using only in_smooth/in_isInSet, so the mirror is a faithful twin
with no Engine ProjectReference dragged into the standalone smoke project.
Parity result. --colorprobe on a fixed 128×128 view: GeForce GT 710 distinct=132,
alphaBad=0, disagree 14/16384 (0.085%), exit 0. Mesa lavapipe (CI, LLVM 20.1.2) distinct=130,
alphaBad=0, disagree 24/16384 (0.146%), exit 0.
Note on the golden's determinism — held. The pinned digest is over the CPU mirror, not the GPU,
so it must be reproducible wherever the gate runs. sqrt is IEEE-correctly-rounded (safe cross-OS);
MathF.Log/MathF.Sin are not guaranteed bit-identical Windows↔Linux, but in practice the
lavapipe (Linux) CPU-mirror digest came back byte-identical to the Windows-regenerated golden
(4e725d…f111) — so the exact-digest half is safe here. If a future runtime/libm change ever
diverges across a rounding boundary, the band half stays green and --colorprobe regen on the
gate's own platform re-pins it.
- The colour kernel is proven headless; V3 wires
VulkanComputeRenderer : IFractalRenderer(compute → map → hand buffer to the GL blit) and the--renderer vulkanselector. - Per-theme SPIR-V caching (keyed on
PaletteId) is still a V2-scoped nicety not yet built — the probe compiles one theme per run. Add the cache when the renderer switches themes at runtime.
12. V3 findings (issue #42) — DONE (headless half)
Vulkan compute is now a real, referenced backend at the IGpuKernel boundary, driven through the
exact API the calculator uses, and gated headless. The interactive GL-present wiring is a follow-up.
Interface choice — IGpuKernel, not IFractalRenderer. The issue framed a
VulkanComputeRenderer : IFractalRenderer that computes, but IFractalRenderer.UpdateTexture
receives an already-finished BGRA buffer and has no fractal params — a renderer can't compute the
frame. The compute boundary is IGpuKernel (exactly where the D3D MandelbrotGpuKernel lives), so
VulkanComputeKernel : IGpuKernel is the faithful fit; present stays SilkGLRenderer. The
"renderer" is a thin selector: RendererBackend.Vulkan + a composite probe string, with present
routed through the same NonWin32Backend GL blit (Vulkan is compute-only, no swapchain).
Library promote. The plumbing (VulkanContext, DxcCompiler) moved out of the standalone
Rendering.Vulkan.Smoke gate into a referenced cross-platform Rendering.Vulkan library
(net10.0, refs Engine + Abstractions, Silk.NET.Vulkan) — mirroring how Rendering.D3D hosts the D3D
kernel. The smoke project is now the gate harness over that library.
VulkanComputeKernel design. Persistent device objects: a base (no-colour) pipeline + per-
PaletteId colour pipelines (SetPalette compiles + caches via BuildColor, mirroring the D3D
_csByPaletteId cache — so V2's deferred per-theme cache is delivered here); HOST_VISIBLE buffers
re-allocated only on a dimension change; one command pool. Per-Run it creates and destroys a
small descriptor pool + set + command buffer, so a mid-session resize can never leave a descriptor
bound to a freed buffer. Run() fills iter/smooth/finalZD (+ optional packed BGRA) exactly like the
D3D kernel, including the split hi/lo centre+scale and the GradientColorMap dither knob for byte
parity. DXC -fvk-*-shift binding maps unchanged (u3/gColor → 203).
Gate — --vulkanrenderprobe. Drives the production kernel object (not a hand-rolled dispatch)
and checks four things the renderer relies on: (1) colour Run parity vs the CPU mirror; (2) base Run
iter/smooth parity; (3) buffer persistence — a repeat identical Run is byte-for-byte identical;
(4) resize — a Run at a different W×H re-allocates and still matches a CPU reference. Uses the
real Engine GrayscalePalette, reached transitively through the Rendering.Vulkan → Engine
reference. Measured GeForce GT 710: frameA colour 0.085% / iter 0.171%, persistence identical,
resize 96×160 colour 0.052% / iter 0.156%, base 0.171%, exit 0. Mesa lavapipe (CI, LLVM 20.1.2):
frameA colour 0.146% / iter 0.269%, persistence identical, resize 0.078% / 0.221%, base 0.269%,
exit 0.
Build note. The legacy WinExe (FracturingFogCLD.csproj) globs the repo root and <Compile Remove>s each sibling project; the new Rendering.Vulkan\** folder needed its own exclude (same
class as the V2 Rendering.Vulkan.Smoke fix). Any new sibling net10 project needs one.
- Parse
--renderer vulkaninProgram.cs→RendererFactory.PreferredBackend = Vulkan. - In the Avalonia/host bootstrap: construct one
VulkanContext+VulkanComputeKernel, attach it to the calculator (calc.GpuKernel = kernel; calc.UseGpuCompute = true) when Vulkan is selected, and setRendererFactory.VulkanProbeBackendto the live device string. - No-Vulkan-device fallback to Silk-CPU / Skia + the interactive pan/zoom gate on the Linux host.
- Zero-copy Vulkan→GL interop (external memory) is explicitly out of scope — V3 maps to CPU then uploads via the existing GL texture path, same as the D3D readback.
13. V4 findings (issue #43) — RESOLVED: deep-zoom parity holds by construction; GPU perturbation re-filed as #82
V4 was scoped as "validate the reference-orbit upload path deep on Vulkan and re-run the deep-zoom gates." Investigation showed the scope rested on a false premise inherited from §1: that an HLSL perturbation kernel exists and merely needed a Vulkan backend. It does not.
What the code actually does (verified 2026-07-19, HEAD e3ebc23):
- The only HLSL compute kernel is
Rendering.D3D/MandelbrotKernelSource.cs(~290 lines): a plain FP32, split-hi/lo-centre, z²+c escape-time kernel. No reference orbit, no δ-iteration, no rebasing — nothing perturbation-related. -
MandelbrotCalculatordispatches aGpuKernel(D3DMandelbrotGpuKernelor VulkanVulkanComputeKernel) only underUseGpuCompute && GpuKernel != null && Zoom <= MaxGpuZoom, andMaxGpuZoom = 1e4. Past 1e4 there is no GPU dispatch on any backend. - Deep zoom is therefore all-CPU: SIMD perturbation (
ComputeRowPT/ComputePixelPTRebased) over a DD/QD/OD reference orbit.MandelbrotRefOrbitGpu(ILGPU) can compute the reference orbit on a CUDA/OpenCL-FP64 device, but it is a single sequential orbit — the per-pixel δ loop never leaves the CPU. None of this is HLSL/SPIR-V; the Vulkan backend has no involvement. -
--focusprobe/--navreproare headless calculator self-tests. InProgram.csthey are dispatched before the--rendererparse and never construct a renderer — "run them on the Vulkan backend" is a no-op distinction.
Conclusion — parity holds trivially. Because Zoom > MaxGpuZoom short-circuits every GPU path,
--renderer vulkan and --renderer dx compute bit-identical deep-zoom frames (same CPU code).
There is nothing to port and nothing to gate beyond confirming the CPU path is healthy on HEAD:
--focusprobe 96 → focus-err 0.00 px through 1e60, then the expected flat dead-zone past
maxUseful = 1e62 (a location property, see Deep-Zoom-Perturbation.md
§3 — not a bug, not a regression).
The genuine feature — a GPU perturbation kernel — is net-new and re-filed as #82.
It would upload the reference orbit as an SSBO and run per-pixel δ + Zhuoran rebasing in-shader,
raising MaxGpuZoom far past 1e4. It does not exist on D3D either, so it is a cross-backend
capability, not a Vulkan port. Its headline risk is new: QD/DD limb arithmetic in HLSL (the CPU
reference orbit is up to OD/~124-digit; a float/double δ is fine but the on-GPU reference and the
dc term need enough precision). That risk is unproven under DXC→SPIR-V and FXC, so #82 leads with
a limb-math spike before any full build.
14. V6 spike findings (issue #82) — SPIKE CLEARS: double perturbation loop ports to GPU; the feared QD/DD-limb-in-HLSL risk is avoidable
Spike gate (--vulkanpturbprobe, Rendering.Vulkan.Smoke/PerturbSpikeProbe.cs) run 2026-07-19 on
GT710 (Kepler, FP64 at 1/24 rate). It runs a self-contained double δ-rebased perturbation kernel
— δ_{n+1} = (2·Z[m] + δ)·δ + dc with Zhuoran rebasing (SM-2), the line-for-line twin of
ComputePixelPTRebased — as both a C# mirror and an HLSL kernel (DXC→SPIR-V), and compares.
The headline #82 risk (QD/DD limb math in HLSL) turned out to be a non-issue at the loop level, for a
structural reason: the production default deep-zoom path (ComputePixelPTRebased) already runs
the δ-chain in plain double — it reads the reference orbit's Hi limb only (_refZr[m] /
_refZi[m], doubles) and a single-rounded double dc. The QD/OD precision is spent building the
reference orbit accurately (and carrying the OD centre), not in the per-pixel loop. So a GPU port
consumes a double reference-orbit SSBO and runs a double δ loop — no limb arithmetic in the
shader. The DD variant (ComputePixelPTRebasedDD, UseDdRebaseReference) is off by default and was
measured "insufficient" (§SM-11a); the spike confirms it is unnecessary at the tested depth.
Results (centre = seahorse boundary point -0.743643887037151 + 0.13182590420533i, zoom 1e6,
96×96, maxIter 6000; ref orbit escapes at 3090, amplification ≈1e18.7 → non-degenerate frame,
distinct=1045, in-set 3904/9216):
-
(1) Loop parity — PASS. GPU vs CPU-
double: 11/9216 (0.119%) pixels disagree, all on filament pixels at the escape-time knife-edge (maxΔiter 68 there — expected boundary chaos, doc §2). The CPU-doublevs a DD oracle (DD dc + DD δ, local TwoProduct/TwoSum, no FMA) disagrees by 9/9216 (0.098%) — the same order. The GPU divergence sits at the CPU precision noise floor, i.e. no GPU/DXC dialect gap; it is the inherent chaotic sensitivity, not a port error. Gate is on the disagreement fraction vs the noise floor (same ULP-band philosophy as--vulkanprobe), never maxΔiter or a strict digest. -
(2) dc precision — single
doubleSUFFICES at 1e6 (double-vs-DD divergence is at the δ noise floor). Open for the full build: re-run the same double-vs-DD comparison at 1e15/1e20 with a real OD centre — dc shrinks to ~1e-22 there and may need a split hi/lodc(δ still stays double, doc §2). -
(3) Both compilers accept the double math — PASS. DXC
-spirv cs_6_0emits theFloat64capability and GT710 runs it; FXCcs_5_0compiles the identical HLSL clean (exit 0, 2636-byte CSO — verifies the D3D leg of the eventual sharedMandelbrotKernelSourcevariant). No ICE on either (the ILGPU-FMA-ICE precedent did not bite — the loop uses only +,−,× on doubles). -
(4) FP64 device gating — REQUIRED and wired.
doublecompute needsshaderFloat64enabled at device creation;VulkanContextdid not request any feature. Fixed: it now queries + enablesshaderFloat64when the device supports it and exposesSupportsFloat64; the probe prints SKIP (exit 0) on parts without it. Inert for the FP32 base/colour kernels (--vulkanrenderprobeunchanged: colour 0.085% / iter 0.171%). Perf is not gated here — GT710's 1/24-rate FP64 ran the correctness spike fine, but consumer-GPU throughput is a full-build measurement.
Verdict — the full build is LOWER risk than #82 feared: a plain-double kernel port
(reference-orbit SSBO + double δ + rebasing), spliced into MandelbrotKernelSource like the existing
base/colour variants, consumed by both FXC (D3D) and DXC (Vulkan). No in-shader limb math for the
default path. Remaining full-build work is engineering + the deep-dc precision re-check (2), not a
feasibility unknown. The full build stays a separate, user-gated slice (it touches production
render paths and raises MaxGpuZoom) — the spike's job was to de-risk it, and it has.
15. V6 full build (issue #82) — LANDED (Vulkan): shared kernel + backend + calculator wiring, gated; D3D backend + GUI-enable are the fast-follow
Built 2026-07-19 on the spike's findings, in two validated slices (commits d4bae1c, db7274c).
Shared kernel + interface + Vulkan backend (d4bae1c).
-
MandelbrotKernelSource.BuildPerturb()— standalonedoubleδ-rebased kernel (CSPerturb), the GPU twin ofComputePixelPTRebased: reference orbit as twodoubleSSBOs (Hi-limb, the CPU_refZr/_refZi), Zhuoran rebasing,dc = pixelOffset·scale, outputs iter + smooth + finalZD(zr,zi,drv,div). No in-shader limb math (spike §14). Same two-compiler rule — FXCcs_5_0and DXCcs_6_0both compile it (verified; 3920-byte CSO under FXC). -
IGpuKernelgainsSupportsPerturbation(default false) +RunPerturb(default throws) as C# default-interface members, so the D3D kernel compiles unchanged and opts in later; deep zoom stays CPU whereverSupportsPerturbationis false. -
VulkanComputeKernel:SupportsPerturbation => ctx.SupportsFloat64, andRunPerturb— dedicated perturb pipeline (b0 + refZr/refZi SSBOs + iter/smooth/finalZD), reference-orbit + 48-byte double param buffers.BuildProgramrefactored to take an entry point + explicit binding numbers; base/colour paths unchanged (--vulkanprobe/--colorprobe/--vulkanrenderprobestill green). -
--vulkanpturbprobenow drives the production kernel object: GT710 GPU-vs-CPU 0.119% at the CPU precision noise floor (double-vs-DD 0.098%), non-degenerate, exit 0.
Calculator wiring (db7274c).
-
MandelbrotCalculator.CalculateHighPrecision: after the reference orbit is built, ifUseGpuPerturbationand a perturbation-capable kernel is attached, dispatch the whole frame's δ-rebased loop to the GPU (TryRunGpuPerturbation) and do colour/dist/normal writeback on the CPU via the existingFillAuxAndColorHP— then skip the CPU SIMD/BLA/SA path. Runs the rebased loop for every pixel (glitch-free, SM-2). Offsets mirrorComputeRowPTScalarexactly. Any kernel failure falls through to the CPU path. - Gated to the plain rebased regime it mirrors: recycle off, no per-tile cap,
UseDdRebaseReference/ForceScalarPtPathoff,Zoom <= MaxGpuPerturbZoom. -
MaxGpuPerturbZoom=ODZoomThreshold(1e50) — conservative ceiling; the δ loop is depth-independent (rebasing) but the deep-dcre-check (checkbox 2) gates lifting it.UseGpuPerturbation— master toggle, DEFAULT OFF, independent ofUseGpuCompute. - Gate
--vulkanpturbcalcdrives a realMandelbrotCalculatorboth ways at zoom 1e14: GPU-perturb frame vs CPU deep frame disagree 0/16384 (0.000%). CPU deep path unregressed —--focusprobegreen (0.00 px to 1e60, flat pastmaxUseful=1e62); the GPU path is gated off by default.
Remaining (cross-backend fast-follow, each needs a validation surface this repo can't exercise headlessly):
-
D3D
MandelbrotGpuKernel.RunPerturb— CODE LANDED (2026-07-19), awaiting live-D3D parity smoke.SupportsPerturbationgates onCheckFeatureSupport<FeatureDataDoubles>(Feature.Doubles).DoublePrecisionFloatShaderOps(cached).RunPerturbcompiles the sharedMandelbrotKernelSource.BuildPerturb()HLSL (entryCSPerturb) under FXCcs_5_0— the same source the Vulkan backend runs — uploads the Hi-limb reference orbit into twoStructuredBuffer<double>SRVs (t0/t1) + a 48-byte double param cbuffer (b0), reuses the existing iter/smooth/finalZD UAVs (u0/u1/u2), dispatches 8×8 groups, reads back exactly likeRun. Colour stays CPU (calculatorFillAuxAndColorHPconsumesfinalZD). Serialised on the shared_d3dGate. GUI enable held behind an explicit opt-in (envFF_GPU_PERTURB=1) inAvaloniaShellBootstrap— NOT default-on — mirroring the Vulkan posture: forcesUseGpuComputeon so the D3D kernel attaches, then flipsUseGpuPerturbation; the per-frame gate still checks the live kernel'sSupportsPerturbation, so a device without FP64 shader ops self-disables and deep zoom stays CPU. Builds clean (Rendering.D3D + WinExe + App); the Vulkan gates (--vulkanpturbcalc0/16384) stay green — the default-interface member the D3D class now overrides did not disturb the Vulkan path. NEXT: user runs a live D3D deep-zoom parity smoke (FF_GPU_PERTURB=1, D3D renderer) — this repo has no FP64-D3D headless surface. Interface default still keeps any other IGpuKernel opt-out. -
Enable in the GUI — DONE + smoke-signed-off (2026-07-19):
AvaloniaShellBootstrap, on an explicit--renderer vulkansession, probesVulkanComputeKernel.ProbeSupportsFloat64()and setsMandelbrotCalculator.UseGpuPerturbationaccordingly (logs ENABLED / disabled). Off everywhere else. Visual deep-zoom smoke passed on Windows (GT710) and Linux (Intel UHD 630 / CML GT2) — the Linux session loggeddeep-zoom GPU perturbation ENABLED (shaderFloat64 present)on real integrated hardware. -
Deep-
dcprecision (checkbox 2) — DONE (2026-07-19). New gate--vulkanpturbdcbuilds the PRODUCTION OD reference orbit (ComputeReferenceOrbitODPublic) at the canonical deep boundary centre and sweeps 1e6 → 1e50, comparing the single-doubledcmirror (colOffsetX*scale, exactly what the GPU kernel + default CPUComputePixelPTRebasedrun) against a DD-dcoracle (twin of the SM-11aComputePixelPTRebasedDDA/B), ref held Hi-only so onlydc/δ precision varies. Result: disagree 0/4096 (0.000%, maxΔiter 0) at 1e6, 1e15, 1e20, 1e30, 1e40 — single-doubledcis bit-for-bit the DD result across 34 orders of zoom, confirming perturbation is precision-zoom-invariant (dc·amplificationstays O(1), sodc's ½-ULP rounding never reaches escape-time significance). 1e50 is INCONCLUSIVE — this centre's orbit escapes at len 3744 so its amplification caps ≈1e40–45; the deepest frame is uniformly interior and has nothing to resolve (a centre-depth limit, not adcfailure). Verdict: the flat 0-disagreement trend + the zoom-invariance argument support liftingMaxGpuPerturbZoomtoward thescale-denormal limit; kept at the conservative 1e50 pending an on-device deep-zoom sign-off (same posture as the GUI enable). Pure CPU numeric — GPU runs the identical doubledc, so the CPU verdict transfers; no Vulkan device required, runs on any host. - Later: SA/BLA on GPU (non-goal for now), per-tile-cap support in the perturbation kernel.