-
Notifications
You must be signed in to change notification settings - Fork 0
CrossPlatform ImplementationPlan
Companion: Cross-Platform Roadmap · Technical Index · Architecture Overview
Created 2026-06-12 on branch
feature/cross-platform-full(forked frommain@b8c7312). This document is the execution plan for the phases listed inCrossPlatform-Roadmap.md; the roadmap is the "what" and "why", this is the "how" and "in what order".
-
Tracking branch:
feature/cross-platform-full. All slices below land on this branch via small commits with the slice number in the subject (e.g.XPlat S0.1 — add FracturingFog.Engine csproj). - PR cadence: open a PR per phase (X.0, X.A, X.B, …) once the phase exit criteria are green in CI. Slices land directly on the tracking branch; the PR is the rollup.
-
WinExe regression guard: every slice must keep
dotnet build FracturingFogCLD.csprojgreen on Windows. The legacy WinExe is the production binary today; nothing here removes it until the new App reaches parity in a later branch. -
CI:
.github/workflows/cross-platform-build.ymlalready runs onfeature/**branches. Each slice that adds a new csproj or moves files adds the correspondingdotnet build -r <rid>step to the matrix.
Exit:
dotnet build FracturingFog.App.csproj -r linux-x64succeeds on a Linux CI runner. The legacy WinExe still builds and runs unchanged on Windows.
Files to add:
-
Engine/FracturingFog.Engine.csproj—<TargetFramework>net10.0</TargetFramework>,<AllowUnsafeBlocks>true</AllowUnsafeBlocks>, ProjectReferences toAbstractions,CalculatorGen,ColorGen.
Files to move (git mv) into Engine/:
-
Rendering/FractalRenderHost.cs,Rendering/FractalRenderHost.Video.cs -
Rendering/MandelbrotGpuKernel.cs(new GPU compute) Rendering/PerfStats.cs-
Rendering/FractalOverlayCompositor.cs(will be SkiaSharp-swapped in Phase X.A) -
Calculators/**(all of them — they have no UI deps) -
Models/**(the engine-side model types; ColorScheme files that referenceSystem.Drawing.Colorneed the swap from Phase X.A, but Phase X.0 just moves them and the build will fail with CS0246 — fix in X.A) Math/**-
Interefaces/**(typo'd folder, leave the name to minimise churn)
FracturingFogCLD.csproj change: delete the in-tree compile glob and add
<ProjectReference Include="..\Engine\FracturingFog.Engine.csproj" />. The
strip-globbing pattern (<Compile Remove="…\**" />) already in the csproj makes this a
near-mechanical edit.
Validation: dotnet build FracturingFogCLD.csproj on Windows. Worktree dirty state
in the legacy WinExe is OK; expect only ref-resolution errors that fix themselves once
all calc + model files are inside Engine/.
Files to add:
-
Audio/FracturingFog.Audio.csproj—<TargetFramework>net10.0</TargetFramework>, referencesAbstractions. Does not reference NAudio at csproj level; the NAudio backend lands in a sibling Win-only project (Slice 0.5 / Phase X.B). -
Abstractions/Audio/IAudioCaptureBackend.cs— minimum surface (Start/Stop/DataAvailable event + Capabilities enum). -
Abstractions/Audio/AudioFormat.cs— sample rate, channels, bit depth.
Files to move:
-
Audio/AudioEngine.cs— split into backend-neutral driver (AudioCaptureDriverinAudio/) + the NAudio-specific source pump (moves to a newAudio.Win/project in Slice 0.5). For Phase X.0, the move is mechanical; the backend abstraction is introduced in Phase X.B. -
Audio/BeatAnalyzer.cs— usesNAudio.Dsp.Fft. Either swap toMathNet.Numerics(already referenced by PaletteBuilder.Lib) or copy the tinyFft.Forwardwe need intoAudio/Dsp.cs. Defer the swap to X.B; for X.0 keep the NAudio reference (the project is Win-only until X.B finishes). -
Audio/FractalSynth.cs— usesNAudio.Wave.WaveFormat+ISampleProvider. Same treatment: keep NAudio ref temporarily. -
Audio/AudioSettingsStore.cs— POCO, no NAudio dep. Moves clean.
Temporary TFM: because the move keeps NAudio refs, the new Audio.csproj will
target net10.0-windows for one slice. Phase X.B flips it to net10.0.
Files to add:
-
Hosting/FracturingFog.Hosting.csproj—<TargetFramework>net10.0</TargetFramework>. ReferencesAbstractions,Engine,UI.Avalonia,Rendering.Silk,Rendering.Skia.
Files to move:
-
Hosting/AvaloniaShellBootstrap.cs,Hosting/AvaloniaDialogs.cs -
Hosting/HostColorThemeService.cs,Hosting/HostHelpContentProvider.cs -
Hosting/ColorThemeDefAdapter.cs,Hosting/HostPaletteExtractionService.cs Hosting/PngSlideshowFrameRecorder.cs
Files that stay in the Windows fragment (Slice 0.5):
-
Hosting/NativeMouseForwarder.cs(comctl32 subclass) -
Hosting/FfmpegSetupDialog.cs(WinForms dialog — until Phase X.2 rewrites it)
Files to add:
-
Rendering.D3D/FracturingFog.Rendering.D3D.csproj—<TargetFramework>net10.0-windows</TargetFramework>,<UseWindowsForms>false</UseWindowsForms>. ReferencesAbstractions,Engine, all Vortice packages (moved out ofFracturingFogCLD.csproj).
Files to move:
-
Rendering/DirectXRenderer.cs,Rendering/DirectX12Renderer.cs,Rendering/RendererFactory.cs -
Imaging/MP4Writer.cs(Media Foundation P/Invoke — also Win-only by construction)
New: FracturingFog.Win/FracturingFog.Win.csproj (net10.0-windows) holding the
remaining Windows-only-but-not-WinForms surface:
Hosting/NativeMouseForwarder.cs-
Audio.Win/NAudioCaptureBackend.cs(extracted from old AudioEngine)
New: FracturingFog.App/FracturingFog.App.csproj — the cross-platform exe.
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<Nullable>enable</Nullable>
<LangVersion>latest</LangVersion>
<AssemblyName>FracturingFog</AssemblyName>
<RootNamespace>FracturingFog</RootNamespace>
<ApplicationIcon>..\Resources\FracturingFog.ico</ApplicationIcon>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Abstractions\FracturingFog.Abstractions.csproj" />
<ProjectReference Include="..\Engine\FracturingFog.Engine.csproj" />
<ProjectReference Include="..\Audio\FracturingFog.Audio.csproj" />
<ProjectReference Include="..\Hosting\FracturingFog.Hosting.csproj" />
<ProjectReference Include="..\UI.Avalonia\FracturingFog.UI.Avalonia.csproj">
<ExcludeAssets>analyzers</ExcludeAssets>
</ProjectReference>
<ProjectReference Include="..\Rendering.Silk\FracturingFog.Rendering.Silk.csproj" />
<ProjectReference Include="..\Rendering.Skia\FracturingFog.Rendering.Skia.csproj" />
<ProjectReference Include="..\Server\FracturingFog.Server.csproj" />
<ProjectReference Include="..\Client\FracturingFog.Client.csproj" />
<ProjectReference Include="..\CalculatorGen\CalculatorGen.csproj" />
<ProjectReference Include="..\ColorGen\ColorGen.csproj" />
</ItemGroup>
<!-- Windows-only project refs (DX + MP4Writer + NAudio backend + native mouse forwarder) -->
<ItemGroup Condition="'$([MSBuild]::IsOSPlatform(`Windows`))' == 'true'">
<ProjectReference Include="..\Rendering.D3D\FracturingFog.Rendering.D3D.csproj" />
<ProjectReference Include="..\FracturingFog.Win\FracturingFog.Win.csproj" />
<ProjectReference Include="..\Audio.Win\FracturingFog.Audio.Win.csproj" />
<!-- PaletteBuilder.Lib still net10.0-windows in Phase X.0; pulled in unconditionally
after Phase X.1 flips its TFM. -->
<ProjectReference Include="..\PaletteBuilder\PaletteBuilder.Lib.csproj">
<ExcludeAssets>analyzers</ExcludeAssets>
</ProjectReference>
</ItemGroup>
</Project>Program.cs move: the entry point moves into FracturingFog.App/Program.cs. The
benchmark / saprobe / gentest CLI arms come with it. The --winforms arm stays in
FracturingFogCLD.csproj (the legacy WinExe keeps its own entry point so existing
users running the old .exe see no behaviour change).
-
FracturingFogCLD.slnadds:FracturingFog.App,FracturingFog.Engine,FracturingFog.Audio,FracturingFog.Audio.Win,FracturingFog.Hosting,FracturingFog.Rendering.D3D,FracturingFog.Win. -
cross-platform-build.ymladds:- On every leg:
dotnet build FracturingFog.App -r ${{ matrix.rid }} --self-contained false - On every leg:
dotnet build FracturingFog.Engine -r ${{ matrix.rid }} - On every leg:
dotnet build FracturingFog.Hosting -r ${{ matrix.rid }} - Windows leg only:
dotnet build FracturingFog.Rendering.D3D,FracturingFog.Win,FracturingFog.Audio.Win
- On every leg:
- Solution filter
FracturingFog-XPlat.slnflisting only the cross-platform projects (so Rider / VS can open the slim graph on macOS/Linux without choking on the*-windowsTFM projects).
Exit:
FracturingFog.EngineandFracturingFog.Appbuild under-r linux-x64 --self-contained falsewith zeroSystem.Drawingreferences, and the headless--batchPNG flow round-trips on Linux CI.
Introduce a tiny Rgba32 struct (4-byte BGRA pack identical to the existing uint
buffers — but typed) and an IImageFormat enum (Png | Jpeg | Tiff | Bmp | Webp).
All call sites currently pass ImageFormat.Png etc. via System.Drawing.Imaging;
swap to the new enum and adapt at the encode site.
- Add
<PackageReference Include="SkiaSharp" Version="3.119.4" />toFracturingFog.Engine.csproj. (Already shipping via Avalonia + Rendering.Skia, so no new RID-specific native lands.) -
SavePixelsToFilebecomes:using var info = new SKImageInfo(w, h, SKColorType.Bgra8888, SKAlphaType.Premul); using var bitmap = new SKBitmap(info); unsafe { fixed (uint* src = pixels) { Buffer.MemoryCopy(src, (void*)bitmap.GetPixels(), w*h*4, w*h*4); } } if (dpi > 0f) { /* no SKBitmap DPI; SKDocument or PNG pHYs chunk via SkiaSharp.SKData */ } using var image = SKImage.FromBitmap(bitmap); using var data = image.Encode(SkiaFormat(format), quality: 100); using var fs = File.OpenWrite(path); data.SaveTo(fs);
- Watermark composition: load the bundled Inter font (already shipping via
Avalonia.Fonts.Inter) viaSKTypeface.FromFamilyName("Inter")with a fallback chain. Outline =SKPaint { Style=Stroke, StrokeWidth=2 }; fill =SKPaint { Style=Fill }. - Contrast colour sampler: keep the pixel-loop math as-is; only the
Colortype changes (Rgba32from Slice A.1). - DPI metadata: PNG's
pHYschunk is reachable viaSKPngEncoderOptions.zLibLevelplus a manual pixel-density write. For TIFF/BMP, SkiaSharp doesn't expose DPI directly — accept this as a feature gap on the cross-platform path (Windows path viaMP4Writer-adjacent GDI keeps DPI; cross-platform PNG keeps it via pHYs; TIFF/BMP DPI degrades to "the saved file declares 96 DPI").
-
Bitmapctor +LockBitsblock becomesSKBitmap.InstallPixelsover the existinguint[]copy. -
SavePngbecomesSKImage.FromBitmap(bitmap).Encode(SKEncodedImageFormat.Png, 100).SaveTo(stream). - The semaphore-gated background save preserves exactly. Per-frame allocation count is
unchanged (one
SKBitmapper frame instead of oneBitmap).
- Same swap as Slice A.2 at the save tail.
- Rotation (
PosterRequest.Rotate) — useSKBitmap.Resize+SKCanvas.RotateDegrees(90)or pre-rotate via a 2D index transform during the BGRA copy (faster, no extra alloc). - Custom watermark composition:
WatermarkResolver.Resolvealready returns aWatermarkDefPOCO; theRendercall swaps fromGraphics.DrawStringtoSKCanvas.DrawTextoverSKPaint.
- Halo grid + ROI outline composition becomes
SKCanvas.DrawLine+DrawPath. - Existing
Graphics.SmoothingMode = AntiAliasmaps toSKPaint.IsAntialias = true. - Used by both interactive (overlay on framebuffer) and headless export (slideshow recording) paths; behaviour preserved bit-identical for grid alignment.
-
All these files use
Color.FromArgb(a,r,g,b)as a struct constructor — they never call GDI+ APIs. Two options:-
Adopt
System.Drawing.Primitives: shipping in the BCL, no GDI+ dependency. TheColorstruct works on every RID. This is the smallest diff: change nothing in source, just stop shippingSystem.Drawing.Common. -
Adopt
Rgba32from Slice A.1: enforces zeroSystem.Drawingreferences engine-wide. Bigger diff (one-line perColor.FromArgbcall) but cleaner.
Recommendation: go with
System.Drawing.Primitivesfor X.A (small diff, fast). Park a follow-up to migrate toRgba32after the rest of the cross-platform launch lands. -
Adopt
- Strip
<PackageReference Include="System.Drawing.Common">from any project still referencing it inside the App closure. Add a CI assertion (PowerShell + grep) that fails the Linux leg if any closure project pulls it transitively.
Exit: opening the audio-reactive slideshow dialog on Linux/macOS shows the source picker, picks "File" or "Synth" without crashing, drives the beat analyzer. System loopback is greyed with a banner.
-
Abstractions/Audio/IAudioCaptureBackend.cs:public interface IAudioCaptureBackend : IDisposable { AudioBackendCapabilities Capabilities { get; } void Start(AudioSourceKind source, AudioFormat preferredFormat, string? filePath); void Stop(); event Action<ReadOnlyMemory<float>, AudioFormat>? DataAvailable; event Action<Exception>? Failed; } [Flags] public enum AudioBackendCapabilities { None=0, SystemLoopback=1, Microphone=2, FilePlayback=4, SynthPlayback=8 }
-
Audio/AudioCaptureDriver.cs(inFracturingFog.Audio) — owns the backend, wraps the beat analyzer, exposesIBeatSource(existing) to consumers.
- Extract the WASAPI loopback + WaveOutEvent path from the old
AudioEngine.StartCoreswitch into a cleanIAudioCaptureBackendimpl.Audio.Win.csprojtargetsnet10.0-windows, references NAudio, ProjectReferencesFracturingFog.Audio(for the interface). -
Capabilities = SystemLoopback | Microphone | FilePlayback | SynthPlayback.
- Cross-platform fallback. Supports only
FilePlayback(viaNAudio.AudioFileReader— yes this works cross-platform because file decode doesn't touch WASAPI/MM) andSynthPlayback(analyzer-only, no speaker output — host doesn't push samples to a speaker, only to the analyzer). - Spawn a
System.Threading.Channelsbased pump that decodes the file in chunks and pushes float samples toDataAvailable. No platform-specific API. -
Capabilities = FilePlayback | SynthPlayback.
-
AvaloniaShellBootstrappicksWindowsNAudioBackendon Windows (via reflection load to avoid Windows-only ref leaking into the cross-platform host — or via the Win-only ProjectReference inApp.csprojresolving the type at load time). Otherwise picksNoopAudioBackend. -
UI.Avalonia/Views/AudioSettingsView.axaml— grey + tooltip the SystemLoopback / Microphone options whenbackend.Capabilitieslacks the flag. Add a one-line banner at the top: "System audio capture is not supported on this OS." - Audio-reactive sweep settings persist either way (saved to the user store); the running sweep receives a flat beat stream on hosts without capture.
- Replace
NAudio.Dsp.Fft.FFTwithMathNet.Numerics.IntegralTransforms.Fourier.Forward(MathNet is already in the PaletteBuilder.Lib closure, easy to add toFracturingFog.Audio). Verify spectrum bins match within 1e-6 of the prior NAudio output on a sine sweep test. -
FractalSynth.cs'sISampleProviderinterface andWaveFormatPOCO are pulled intoAbstractions/Audio/as local copies so the synth survives without NAudio.
- Remove the temporary
net10.0-windowsTFM fromAudio.csproj. CI Linux leg builds it clean.
Exit:
From Image…round-trips an input PNG on Linux.PaletteBuilder.LibTFM isnet10.0.
- Add
<PackageReference Include="QuestPDF" Version="2026.x.x" />toPaletteBuilder.Lib.csproj. Remove<PackageReference Include="PDFsharp-gdi" />and<PackageReference Include="System.Drawing.Common" />. - Rewrite
PaletteBuilder/Services/PdfPaletteExporter.csagainstQuestPDF.Fluent:Document.Create(container => { container.Page(page => { … }); })- Page size / margin from existing
PdfExportOptions. - Cover page, swatch grid, CVD rows ported one-to-one. The QuestPDF API is closer to Razor than to PDFsharp, so the rewrite is mostly structural.
- Honour
QuestPDF.Settings.License = LicenseType.Communityfor the open-source license.
-
PaletteBuilder.Lib.csproj:<TargetFramework>net10.0-windows</TargetFramework>→<TargetFramework>net10.0</TargetFramework>. Remove<UseWindowsForms>false</UseWindowsForms>(default already). -
PaletteBuilder.csproj(Avalonia WinExe wrapper) andFracturingFogCLD.csprojreference the Lib via TFM downcompat — both Windows-targeted parents can reference anet10.0Lib. Build both on Windows to confirm.
- A short doc in
Docs/Technical/CrossPlatform-SmokeTests.mdlisting the manual steps. Not blocking on automation — CI builds the lib; user smoke confirms the UI flow.
Exit: 100-frame slideshow export succeeds on Linux via bundled or PATH ffmpeg.
-
Abstractions/Imaging/IVideoWriter.cs—Init(width, height, fps),WriteFrame(uint[]),Finish(). Pure abstraction. - Wrap the existing
Imaging/FfmpegEncoder.cs(which is already process-based) inImaging/FfmpegVideoWriter.csadapter exposingIVideoWriter. Internally it spools PNGs to a temp directory and callsEncodeAsyncatFinish()— or streams BGRA to ffmpeg's stdin via a pipe for lower disk I/O. Recommendation: PNG-on-disk first (mirrors today's behaviour), pipe streaming as a later optimisation.
-
Imaging/MP4Writer.csalready in the Win-only fragment from Phase X.0 (Slice 0.4). Wrap it in anIVideoWriteradapter (Win32Mp4VideoWriter) at the same boundary.
-
FfmpegEncoder.FindFfmpeg:- Probe
ffmpeg.exe(Windows) andffmpeg(Linux/macOS). - Probe
Tools/<rid>/ffmpeg{.exe}based onRuntimeInformation.RuntimeIdentifier. - PATH fallback already handles
Path.PathSeparatorcorrectly.
- Probe
-
FracturingFog.App.csproj:<ItemGroup> <None Include="..\Tools\win-x64\ffmpeg.exe" Pack="false" CopyToOutputDirectory="PreserveNewest" Condition="'$(RuntimeIdentifier)' == 'win-x64'" /> <None Include="..\Tools\linux-x64\ffmpeg" Pack="false" CopyToOutputDirectory="PreserveNewest" Condition="'$(RuntimeIdentifier)' == 'linux-x64'" /> <None Include="..\Tools\linux-arm64\ffmpeg" Pack="false" CopyToOutputDirectory="PreserveNewest" Condition="'$(RuntimeIdentifier)' == 'linux-arm64'" /> <None Include="..\Tools\osx-arm64\ffmpeg" Pack="false" CopyToOutputDirectory="PreserveNewest" Condition="'$(RuntimeIdentifier)' == 'osx-arm64'" /> <None Include="..\Tools\osx-x64\ffmpeg" Pack="false" CopyToOutputDirectory="PreserveNewest" Condition="'$(RuntimeIdentifier)' == 'osx-x64'" /> </ItemGroup>
-
Licensing audit: ffmpeg builds vary in license (GPL vs LGPL, codec set). Recommend
not bundling on Linux/macOS for the first launch — rely on
apt/brewinstall. Ship bundled ffmpeg only on Windows (matches today). Document this in the user guide.
- Move the WinForms
Hosting/FfmpegSetupDialog.csinto a Win-only subproject asFfmpegSetupDialog.WinForms(kept for the legacy--winformsshell). - Add
UI.Avalonia/Views/FfmpegSetupView.axamlwith OS-aware copy:- Windows: existing gyan.dev auto-download.
- Linux/macOS: instructions panel — copy-pasteable
sudo apt install ffmpeg/brew install ffmpeg. "I've installed it" button rescans PATH.
-
FfmpegInstaller.cs: gate the download path behindOperatingSystem.IsWindows().
-
AvaloniaShellBootstrap.OnSurfaceReady: probe Media Foundation (on Win) →Win32Mp4VideoWriter; else probe ffmpeg →FfmpegVideoWriter; else null. - Video-export UI surfaces "Not available — install ffmpeg" banner when null.
Exit:
dotnet buildon Linux emits zeroCA1416warnings fromFracturingFog.App,Engine,Hosting,UI.Avalonia,Audio.
-
UI.Avalonia/Views/MainWindow.axaml.csToyDragWindow:Hook to the title-barprivate void OnPointerPressedForToyDrag(object? sender, PointerPressedEventArgs e) { if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed) BeginMoveDrag(e); }
PointerPressed. Delete[DllImport("user32.dll")] ReleaseCapture,SendMessage, and theWM_NCLBUTTONDOWN/HTCAPTIONconstants. -
Hosting/AvaloniaShellBootstrap.csClientToScreen:Or unconditionally useif (OperatingSystem.IsWindows()) ClientToScreen(handle, ref pt); else { var screenPt = control.PointToScreen(new Point(pt.X, pt.Y)); pt.X = (int)screenPt.X; pt.Y = (int)screenPt.Y; }
Control.PointToScreenif the Win32 path doesn't drift.
-
ServerHost/ServerEntry.cs,Batch/BatchEntry.cs,Benchmarks/MandelbrotBench.cs: wrap everyAttachConsole/AllocConsolecall inif (OperatingSystem.IsWindows()).
- Add to all types in
Rendering.D3D,FracturingFog.Win,Audio.Win, and to the Win-only fragment ofHosting(NativeMouseForwarder + FfmpegSetupDialog while it lives). -
OperatingSystem.IsWindows()replacesRuntimeInformation.IsOSPlatform(OSPlatform.Windows)at every site so the analyzer can prove the gating.
-
FracturingFogCLD.csprojalready net10.0-windows, soCA1416is silent there. Keep the--winformslegacy shell out of scope for this phase.
Exit: Avalonia paints a fractal on the 6-RID matrix from the roadmap.
- Parse in
Program.cs. Default: DX on Win, Silk on Linux/macOS. Override values:dx,silk,skia. -
RendererFactorypicks based on the flag +OperatingSystem.IsWindows()+ the presence of a Win32 HWND surface.
-
cross-platform-build.ymladdslinux-waylandmatrix entry:sudo apt install weston libwayland-egl1weston --backend=headless &- Run the Silk smoke under
WAYLAND_DISPLAY=wayland-0
- Open one Silk window on macOS CI (already passing for FBO smoke), confirm the GL
fragment shader compiles. If
#version 330 coreis rejected, fall back to#version 410 core.
Exit: smoke test asserts ILGPU device kind per RID; CPU device used on Apple Silicon without crash.
-
Rendering.Silk.Smokealready runs the Silk path. Add a parallelCompute.SmokeCLI that constructsILGPU.Context, picks an accelerator, runs a 64×64 Mandelbrot kernel, asserts no crash. Compare device kind against the expected per-RID set.
-
Hosting/HostHelpContentProvider.csHardware text already mentions DXGI adapters + CPU SIMD. Add audio backend (backend.GetType().Name + Capabilities) and ILGPU device kind to the tab.
Exit: GitHub Release on tag push has 5 self-contained archives.
-
FracturingFog.App/Properties/PublishProfiles/{win-x64,linux-x64,linux-arm64,osx-arm64,osx-x64}.pubxmlwithSelfContained=true,PublishSingleFile=true,IncludeNativeLibrariesForSelfExtract=true,EnableCompressionInSingleFile=true.
-
Tools/Packaging/build-appimage.sh—appimagetoolover the publish output. Optional.desktopfile inResources/Linux/.
-
Tools/Packaging/build-mac-app.sh—Info.plisttemplate inResources/macOS/, copies publish output intoFracturingFog.app/Contents/MacOS/. Code-signing is a separate manual step until Apple Developer cert lands.
- New
.github/workflows/release.ymltriggered onv*tags. Matrix runs publish per RID, uploads artifacts. Optional: GitHub Release auto-create with checksums.
Sections:
- Install per OS (Win exe, Linux AppImage, macOS .app).
- Renderer selection — when to use
--renderer skiaor--renderer silk. - Audio capability matrix.
- Video export — ffmpeg install for Linux/macOS.
- Known limitations: Apple Silicon GPU compute via CPU, Wayland + NVIDIA caveats.
Replace "Windows-only" with the per-OS install table.
Three columns: Win / Linux / macOS. Rows: every feature group (Render, Compute, Audio, Video Export, Slideshow, Palette Builder, etc.). Tick / N/A / Limited.
Files most likely to need careful review or non-mechanical edits:
| Phase | File | Why |
|---|---|---|
| X.0 | FracturingFogCLD.csproj |
The Compile-glob strip pattern is fragile; moving Hosting/Imaging/Rendering out shrinks the strip list. Keep the analyzer-strip target. |
| X.0 | Hosting/AvaloniaShellBootstrap.cs |
Single static class holds the whole boot graph. Moving it splits ref closure cleanly but check no internal types leak. |
| X.A | Imaging/ImageExport.cs |
The unsafe BGRA copy and DPI metadata are easy to get wrong; pin a golden-pixel A/B against a known render before merging. |
| X.A | Rendering/FractalOverlayCompositor.cs |
Pixel-aligned grid lines drift if SkiaSharp anti-aliasing differs from GDI+. Use SKPaint.IsAntialias=false for the grid path; AA is fine for the watermark. |
| X.B | Audio/AudioEngine.cs |
The state machine across StartCore/StopCore/Reconfigure is intricate; preserve the lock + Stopped event contract when refactoring into the driver + backend split. |
| X.B | Audio/BeatAnalyzer.cs |
FFT swap from NAudio to MathNet. Window function, hop size, magnitude scaling must match — write a spectrum comparison test first. |
| X.1 | PaletteBuilder/Services/PdfPaletteExporter.cs |
QuestPDF API is fluent; the cover page + CVD row logic needs structural rethink, not line-by-line port. |
| X.2 | Imaging/FfmpegEncoder.cs |
Already process-based — easy adapter. The only landmine is the frame_%06d.png glob and -start_number 1 which must survive. |
| X.3 | UI.Avalonia/Views/MainWindow.axaml.cs |
Toy-mode drag rewrite touches event handlers; verify the title-bar drag still triggers cleanly on Avalonia's Pointer events vs the previous WM_NCLBUTTONDOWN trick. |
| X.6 | FracturingFog.App.csproj |
Publish-profile interaction with <ProjectReference Condition="…IsOSPlatform…"> can break on dotnet publish -r osx-arm64 from a Win runner; test cross-RID publish on every leg. |
- Phase X.0 (project geometry) — week of 2026-06-12. No behavioural change visible to users. Pure refactor.
- Phase X.A (Skia swap) — week of 2026-06-19. PNG export A/B testing. WinForms shell unchanged (it still uses GDI+ paths in the legacy partials).
- Phase X.B (audio abstraction) — week of 2026-06-26.
- Phase X.3 (P/Invoke sweep — done first across X.B / X.A overlap) — runs in parallel where possible.
- Phase X.1 (palette PDF) — week of 2026-07-03.
- Phase X.2 (video) — week of 2026-07-03 (parallel with X.1).
- Phase X.4 (renderer polish + Wayland CI) — week of 2026-07-10.
- Phase X.5 (compute smoke) — same week.
- Phase X.6 (packaging) — week of 2026-07-17.
- Phase X.7 (docs) — week of 2026-07-24.
Total: ~6-7 calendar weeks of focused work for one developer. Phases X.A and X.B are the deepest cuts; X.6 / X.7 are mostly mechanical.
- Removing the legacy WinForms shell. Tracked separately; happens once
FracturingFog.Appreaches parity and is the default on Windows installs. - ARM64 NEON SIMD lane in CalculatorGen —
CalculatorGen-Roadmap.mdfollow-up. - Metal GPU compute on Apple Silicon — needs ILGPU's not-yet-merged Metal backend.
- OpenAL system-audio capture (Linux/macOS WASAPI-loopback equivalent) — Phase X.B ships with a noop backend; OpenAL backend is a follow-up enhancement.
- Mobile (iOS / Android) and Wasm browser host.
- Touch UI affordances.