-
Notifications
You must be signed in to change notification settings - Fork 0
PHASE2_AVALONIA_MIGRATION
Branch: feature/phase2-avalonia-ui
Goal: Replace WinForms UI with Avalonia 11 for proper DPI scaling and cross-platform readiness (Win/Mac/Linux). Introduce a renderer abstraction so the Vortice DirectX 11/12 path stays on Windows while future Skia/Vulkan/Metal backends can slot in.
Scope warning: Large multi-week effort. Touches MainForm.cs (183 KB monolith), 17 dialog/view files, all event wiring, and the DirectXRenderer / DirectX12Renderer HWND hosting model.
- Current UI uses hardcoded pixel
ClientSize, manualLeft/Top/Widtharithmetic, noTableLayoutPanel/FlowLayoutPanel/AutoSize. Breaks at non-100% DPI and unusual resolutions. - WinForms
PerMonitorV2only helps if controls cooperate — they don't here. -
Vortice.Direct3D11/12locks renderer to Windows. To open Mac/Linux later we need anIGpuSurfaceboundary. -
MainForm.csmixes view, view-model, and controller logic. Avalonia's MVVM-friendly bindings require a split anyway, so this is the natural cut point.
FracturingFog.Core (netstandard2.1 / net10.0)
├── Calculators/ (existing, no UI deps)
├── Models/ (existing)
├── Math/ (existing)
├── Interefaces/
│ ├── IFractalRenderer.cs (existing)
│ └── IGpuSurface.cs (NEW — abstracts swapchain/HWND/CAMetalLayer/VkSurface)
└── ViewModels/ (NEW — extracted from MainForm.cs)
├── MainViewModel.cs
├── FloatingMenuViewModel.cs
├── ColorThemeEditorViewModel.cs
└── ...
FracturingFog.Rendering.D3D (net10.0-windows)
├── DirectXRenderer.cs (moved, implements IGpuSurface)
├── DirectX12Renderer.cs
└── HwndGpuSurface.cs (NEW — wraps HWND)
FracturingFog.UI.Avalonia (net10.0, cross-platform)
├── App.axaml + App.axaml.cs
├── Views/
│ ├── MainWindow.axaml
│ ├── FloatingMenu.axaml
│ ├── ColorThemeEditor.axaml
│ └── ... (one per current Views/*.cs)
├── Controls/
│ └── GpuSurfaceControl.cs (NativeControlHost wrapper around IGpuSurface)
└── Program.cs (BuildAvaloniaApp — replaces existing Program.cs)
FracturingFog.Legacy.WinForms (net10.0-windows, deletable when port complete)
└── MainForm.cs (kept building during transition behind a build flag)
- Create branch
feature/phase2-avalonia-ui - Add
FracturingFog.Abstractionsproject (UI-free shared contracts; replaces the originalCoreplan for the bootstrap step) - Add
FracturingFog.UI.Avaloniaproject (Avalonia 11.2.3,Avalonia.Desktop,Avalonia.Themes.Fluent,Avalonia.ReactiveUI) - Update
.slnto include both new projects - Define
IGpuSurface.csin Abstractions — abstraction over native window handle + resize events - Stub
GpuSurfaceControlin Avalonia project usingNativeControlHost - Wire minimal Avalonia
MainWindowthat hostsGpuSurfaceControl(renderer wiring deferred to step 2.1) - Add
--avaloniaCLI flag toProgram.csso both UIs build during transition - All three projects build clean (
dotnet build FracturingFogCLD.sln→ 0 errors)
- Avalonia XAML NameGenerator analyzer leaks transitively from
UI.Avaloniainto the WinExe even withExcludeAssets/PrivateAssetsset. Worked around with aStripAvaloniaAnalyzersMSBuild target in the WinExe csproj that removes any@(Analyzer)whose path contains "Avalonia". - Avalonia XAML compiler also auto-globs
*.axamlunder the project root. WinExe csproj now explicitlyRemovesAvaloniaResource,AvaloniaXaml,ApplicationDefinition,Page, andAdditionalFilesunderUI.Avalonia\**. - WinExe also
RemovesCompile/None/EmbeddedResource/ContentunderAbstractions\**andUI.Avalonia\**to stop the implicit SDK glob from compiling sibling-project sources into the WinExe twice.
- Move
Rendering/DirectXRenderer.cs+DirectX12Renderer.csintoFracturingFog.Rendering.D3Dproject (deferred — not required for proof of life; will move when WinForms shell is retired in step 2.3) -
HwndGpuSurface : IGpuSurfacewraps current HWND-based init (deferred — only needed once the WinForms shell also speaks IGpuSurface; the legacy MainForm still uses raw HWND directly) -
RendererFactoryreturns surface-aware renderer viaCreate(IGpuSurface)overload that validates surface kind, clamps size, and wires Resized / HandleLost - WinForms
MainFormpath untouched — legacy HWND-basedCreate(IntPtr, int, int)overload preserved; full solution builds green - Avalonia shell renders animated test pattern through the live DX renderer (
AvaloniaBootstrap.csin WinExe;AvaloniaShell.OnSurfaceReadyhook in UI.Avalonia keeps the shell renderer-agnostic). Real fractal output arrives with the calculator wiring in step 2.3.
Priority order is now by line count ascending (re-ordered after measuring the WinForms files — FloatingHelp.cs is 3,431 lines of static help text and not the easiest target despite being a "help" dialog).
-
SlideshowSettingsView.axaml(wasSlideshowSettingsDialog.cs, 223 lines) -
MiniDepthControl.axaml(wasMiniDepthPanel.cs, 307 lines) -
FractalParamsView.axaml(wasFractalParamsDialog.cs, 349 lines) -
UserEquationView.axaml(wasUserEquationDialog.cs, 435 lines) -
AudioSettingsView.axaml(wasAudioSettingsDialog.cs, 437 lines) -
SandboxView.axaml(wasSandboxDialog.cs, 483 lines) -
MiniMapControl.axaml(wasMiniMapPanel.cs, 512 lines — render-only Avalonia control consuming host-supplied thumbnail bitmaps; bg calculator pipeline stays in main project) -
ImagePaletteView.axaml(wasImagePaletteDialog.cs, 801 lines — addedIPaletteExtractionService+ neutral DTOs in Abstractions so UI.Avalonia stays free of System.Drawing and the palette-extractor classes; host wires the impl) -
UserBulbView.axaml(wasUserBulbDialog.cs, 1,203 lines — VM exposes split CompileRequested/RenderRequested channels; host drivesAnimationTick(dt)from its own 30 Hz timer and callsNotifyRenderDone()on upload to gate ticks against long raymarches) -
ColorThemeEditorView.axaml(wasColorThemeEditor.cs, 1,448 lines — neutralColorThemeDef/LightSourceDef/PbrMaterialBandDef/InSetColorDef/PbrLightingModeDefDTOs added to Abstractions alongside legacyColorThemeDataso UI.Avalonia stays free of System.Drawing and the runtimeLightSource/PbrLightingModeclasses; newIColorThemeServiceinterface lets the host bridge toColorPalette/DataDrivenColorThemes/UserColorThemeLibraryand own JSON+C# serialization; VM holds threeLightSourceRowVminstances +ObservableCollection<ColorStopRowVm>+ObservableCollection<MaterialBandRowVm>with shared 150 ms debounce → PreviewRequested; host wires PreviewRequested, RegionRequested, EditorThemeSelected, ThemeSavedToLibrary, HelpRequested, ThemeMessageEventArgs, ThemeSaveFileEventArgs, ThemeFromImageEventArgs) -
FloatingMenuView.axaml(wasFloatingMenu.cs, 1,541 lines — VM is a thin command/state surface: 22 ReactiveCommands bubble button clicks to host as events, four ObservableCollection combo lists (regions/themes/resolutions/qualities) get populated by host viaSetRegions/SetThemes/etc., parallelSetXxxSilentvariants suppress change notifications for cross-shell mirroring; post-FX sliders expose both round-tripping setters (Brightness→BrightnessSlideevent) and silent setters (SetBrightnessSilentfor theme-switch snap);IterLockEventArgscarries current iter count when lock toggles) -
FloatingHelpView.axaml(wasFloatingHelp.cs, 3,431 lines — definedIHelpContentProvider(+HelpSubTab,HelpLinkrecords) inFracturingFog.Helpso the ~2,500 lines of static text + Math sub-tabs + live DXGI/D3D11 enumeration stay in the main project and UI.Avalonia just renders tab bodies; VM exposes one string per tab +ObservableCollection<HelpSubTab>for the nested MathTabControl; About-tabHelpLinkbuttons raiseLinkRequested(url)for the host to launch in the system browser; Refresh button re-fetchesHardwareText; Esc closes) -
SlideshowVcrControl.axaml(wasSlideshowVcrPanel.cs, 152 lines) -
MiniMapDefaultsmoved to Abstractions (66 lines; namespace nowFracturingFog.Models, visibilitypublicfor cross-shell use)
Models migrate to the shared FracturingFog.Abstractions assembly as each dialog needs them (namespace stays FracturingFog.Models so legacy WinForms code compiles untouched). Done so far: SlideshowSettings, FractalParameters + AffineMap + UserBulbParam + UserBulbChainStep + UserBulbStore + UserEquationStore + SandboxEquationStore + Enums.cs (FractalType, QualityLevel, RenderProfile). Plus AudioSettings + IBeatSource + MiniMapDefaults and the host-only IPaletteExtractionService / DTOs in FracturingFog.Imaging.
Each port:
- Extract view-model class from current code-behind (commands, observable props).
- Build
.axamlwithGrid/StackPanel/DockPanel— no pixel literals; use*/Autosizing andMargin/Paddingin DIPs. - Bind to view-model. Unit test the VM.
- Wire from new
MainViewModel. - Remove old
Views/*.csonce parity confirmed.
Survey done. Total monolith = 4,247 lines (MainForm.cs) + 818 (Slideshow.cs) + 1,850 (VideoZoom.cs) — all sealed partial class MainForm, 110 methods in the main file. Cut plan:
A. Pure view state → Abstractions (no UI, no renderer)
-
Abstractions/ViewState/FractalViewState.cs— POCO holdingCenterX/Yquad-precision limbs,Zoom,QualityPreset,FractalType, brightness/contrast/adaptive, iter lock state, and a reference to the existingFractalParameters(already in Abstractions). 3D camera state already lives onFractalParameters.
B. Input → Abstractions (mouse + keyboard, precision-aware pan/zoom math)
-
Abstractions/Input/InputEvents.cs— neutral event records (PointerInput,WheelInput,KeyInput) so the input layer is shell-agnostic. -
Abstractions/Input/IFractalInputController.cs+FractalInputController.cs— owns pan/zoom state, picks DD/QD/double math tier from_zoom, handles 2D and 3D key bindings (W/S zoom, A/D/Q/E pan, arrows for 3D camera, PgUp/PgDn/Home/End for 3D light). RaisesViewChangedso the renderer host re-triggers.
C. Render orchestration → main project (renderer + 11 calculators)
-
FractalRenderHost.cs(stays in main; depends on all calculator types +IFractalRenderer) — wrapsTriggerCalculation/TriggerCalculationFast/UploadProcessedBuffer/BlendWatermarkOverlay/BlendGridOverlay/SelectAltCalculator/ApplyViewState. Surface:void ApplyView(FractalViewState),void Trigger(bool progressive),void TriggerFast(),void Resize(int,int),event Action<RenderFrameInfo> FrameCompleted.
D. MainViewModel → UI.Avalonia/ViewModels/ — top-level: holds FractalViewState, drives FractalRenderHost, owns FractalInputController, mirrors selected region/theme/quality/fractal-type into combos, manages brightness/contrast/adaptive + lock flags.
E. ShellViewModel → UI.Avalonia/ViewModels/ — owns FloatingMenuViewModel, lazy ColorThemeEditorViewModel, lazy FloatingHelpViewModel, mini-map + mini-depth panels, VCR + slideshow settings. Glues child VMs to the MainViewModel.
F. Avalonia MainWindow.axaml binds to ShellViewModel with the existing GpuSurfaceControl as the render surface.
G. Delete MainForm.cs + Slideshow.cs + VideoZoom.cs (or carve Slideshow + VideoZoom into engines that the ShellViewModel orchestrates), MainForm.resx, the WinForms project entry point.
WinForms shell stays green during steps A–E by having MainForm consume the new objects; only step G removes it.
- Survey + cut plan written (above)
- A. Extract
FractalViewStatePOCO to Abstractions (also movedQualityPreset+QualityTierfromModels/toAbstractions/Models/since it's pure POCO;FromNameraised frominternaltopublicso the cross-assembly caller inFractalRegion.csstill compiles) - B. Extract
IFractalInputController+ neutral input events to Abstractions (InputEvents.csdefinesPointerInput/WheelInput/KeyInputrecords +PointerButton/InputModifiers/InputKey/InputCursorenums;FractalInputController.csports the precision-aware pan/zoom math from MainForm verbatim — double/DD/QD tiers, cursor-anchor wheel zoom, 3D right-drag camera rotation, 2D+3D key bindings. Also movedMath/DoubleDouble.cs+Math/QuadDouble.cstoAbstractions/Math/since the input controller references them. Controller raisesViewChanged(RenderHint)(Full or Fast),StatusRequestedfor quality auto-promotion notices,CursorRequestedfor drag-state cursor changes. WinForms shell still unchanged — adapter glue lands in step C.) - C. Extract
FractalRenderHostto main project (Abstractions/Render/IFractalRenderHost.csdefines the shell-neutral surface +RenderFrameInforecord; concreteRendering/FractalRenderHost.cslives in main since it depends on all 11 calculator types + the VorticeIFractalRenderer. PortsTriggerCalculation/TriggerCalculationFast/ApplyViewState/Resize/UploadProcessedBuffer/RepaintWithBrightnessContrast/SelectAltCalculatorfrom MainForm verbatim, reading from the sharedFractalViewState. Brightness + contrast pure-CPU pass kept; grid + watermark overlays (System.Drawing-based) intentionally skipped — they will redraw via Avalonia.Media in step F. WinForms shell still untouched; MainForm continues with its own private renderer + calculator instances during the transition.) - D. Extract
MainViewModelto UI.Avalonia (thin facade overFractalViewState+IFractalInputController+IFractalRenderHost. WiresViewChanged(Full)→Trigger()andViewChanged(Fast)→TriggerFast()+ 300 ms pan-stop debounce that firesTrigger()once motion ends. Brightness/Contrast write through to view state and triggerRepaintWithPostFx(no recalc); Adaptive triggers a full recalc because it lives on the calculator's escape buffer. MirrorsFrameCompletedinto the legacy MainForm status string. ExposesQualityPresets/FractalTypesobservable collections,SelectedRegion/SelectedTheme/SelectedQuality/SelectedFractalType, post-FX with lock flags, IterLocked + LockedIterations,ResetViewCommand. Dialog ownership stays inShellViewModel(step E).) - E. Extract
ShellViewModelto UI.Avalonia (top-level composition VM: ownsMainViewModel+FloatingMenuViewModel+ lazyColorThemeEditorViewModel/FloatingHelpViewModel. Constructor takes host-provided services —IFractalRenderHost,IFractalInputController,IColorThemeService,IHelpContentProvider, optionalIPaletteExtractionService. Wires FloatingMenu events into Main (region/theme combos, reset, post-FX sliders) and bubbles ColorThemeEditor + FloatingHelp events back up to the host for the System.Drawing-bound bits (ColorThemePreviewRequested,FromImageRequested,SaveFileRequested,MessageRequested,LinkRequested). Visibility flags (IsFloatingMenuVisibleetc.) bind directly to Window.IsVisible in MainWindow.axaml.) - F.1 Avalonia input adapter (
UI.Avalonia/Input/AvaloniaInputAdapter.cs— bridges PointerPressed/Moved/Released/DoubleTapped/PointerWheelChanged/KeyDown into IFractalInputController; wheel delta scaled ×120 to match WinForms; Ctrl+Shift+S/A diag toggles; cursor translation from InputCursor → Avalonia StandardCursorType) - F.2
MainWindow.axamltoolbar + status + render surface (top toolbar bound to ShellViewModel — FractalType/Quality combos from Main, Region/Theme combos from FloatingMenu, Reset/Edit Theme/Menu/Help buttons; status bar bound to Main.StatusText; center hosts GpuSurfaceControl with transparent InputSponge Border above it since native HWND children don't forward pointer events back into Avalonia; code-behind tracks IsXxxVisible flags + lazily shows/hides FloatingMenuView, ColorThemeEditorView, FloatingHelpView; each child cancels its OS Close and flips the shell flag; shutdown flag suppresses cancel during app exit) - F.3 Host service impls + bootstrap (
Hosting/HostColorThemeService.csbridges ColorPalette + UserColorThemeLibrary + DataDrivenColorThemes.Export via newHosting/ColorThemeDefAdapter.csfor full Def↔Data translation;Hosting/HostHelpContentProvider.csstubs the 7-tab help with short placeholders + environment-derived system info (full ~2,500 lines of FloatingHelp text migration queued as follow-up);Hosting/AvaloniaShellBootstrap.csreplaces the proof-of-life AvaloniaBootstrap: constructs FractalRenderHost + FractalInputController + services + ShellViewModel, wires host-handled events (ColorThemePreview → IColorMap → render host; LinkRequested → ProcessStartInfo with UseShellExecute; SaveFileRequested → temp file write; MessageRequested → console), assigns DataContext to MainWindow on UI thread once surface ready, 60 Hz System.Threading.Timer drives swap-chain presents. Program.cs --avalonia path routes through the new bootstrap.) - G. Delete
MainForm.cs+Slideshow.cs+VideoZoom.cs+MainForm.resx+ WinForms entry point (deferred — user wants legacy intact)
- Real Avalonia
SaveFileDialogviaTopLevel.StorageProvider(done —Hosting/AvaloniaDialogs.SaveFileAsyncparses WinFormsName (*.ext)|*.ext|...filters intoFilePickerFileType, callsTopLevel.StorageProvider.SaveFilePickerAsync, writes viaStreamWriter.AvaloniaShellBootstrap.SaveFileRequestedroutes through it and fillsargs.Saved) - Avalonia
MessageBoximpl (done —AvaloniaDialogs.ShowMessageAsyncbuilds 480-dip modal AvaloniaWindowwith OK or Yes/No buttons +TaskCompletionSource<bool>; marshals onto UI thread for worker-thread callers.AvaloniaShellBootstrap.MessageRequestedroutes through it) -
IPaletteExtractionServiceconcrete wiring throughHosting/HostPaletteExtractionService.cs(done — bridges BitmapSampler + 4 extractors + PaletteStopBuilder; AvaloniaShellBootstrap defaultsPaletteServiceto it andFromImageRequestednow popsImagePaletteViewwith browse + drag-drop, returning ColorStopDef list to the editor) - Full FloatingHelp text migration (~2,500 lines) — extract from
Views/FloatingHelp.csinto shared resource bundle both shells read (done —Abstractions/Help/HelpTextBundle.csnow holds every tabIHelpContentProviderexposes plus the full 17-entry Math sub-tab list: Overview / Mandelbrot / Julia / Burning Ship / Tricorn / Multibrot / Phoenix / Newton / Nova / Buddhabrot / IFS / L-System / Attractor / Mandelbulb / User Equation / User Bulb 3D / Sandbox.HostHelpContentProvider.MathSubTabsreads them in legacy display order so both shells render identical content. LegacyViews/FloatingHelp.cskeeps its inline copies until step G lands) - DXGI / D3D11 adapter enumeration in Hardware tab (currently env-info only) (done —
HostHelpContentProvider.GetSystemInfoTextnow mirrors legacyFloatingHelp.BuildSystemInfoText: DXGI adapter table + D3D11 feature level + CPU/OS + memory + SIMD width. Windows-only branches gated withOperatingSystem.IsWindows()so Linux/macOS shells render a friendly fallback) - Extract
BuildCSharpSourcefromViews/ColorThemeEditor.csinto a shared helper soHostColorThemeService.GenerateCSharpemits real class source instead of a JSON-comment stub (done —Models/ColorThemeCsExporter.cs, both shells call it; legacy editor +HostColorThemeService.GenerateCSharpswapped over) - Grid + watermark overlays via Avalonia.Media (FractalRenderHost intentionally skipped these from the legacy MainForm) (done —
UI.Avalonia/Controls/FractalOverlayControl.cs; toolbarGrid+WatermarkToggleButtons bindMain.ShowGrid/Main.ShowWatermark. Overlay sits in the render Grid cell withIsHitTestVisible="False"so input still flows to the sponge. Contrast colour now driven by a pre-sampled mid-band luma byte the host derives from the active IColorMap (FractalRenderHost.OverlayContrastLuma), surfaced throughIFractalRenderHost.OverlayContrastLuma+ theColorMapChangedevent and mirrored onMainViewModel.OverlayContrastLumaso UI.Avalonia stays free of the main-projectIColorMaptype; bound ontoFractalOverlayControl.ContrastLuma. White ink on dark themes, near-black on light)
- Add
FracturingFog.Rendering.Silk(OpenGL 3.3 via Silk.NET 2.21) —SilkGLRenderer : IFractalRendererports the DX full-screen-triangle textured blit to GL using a GL_BGRA + UnsignedInt8888Rev upload path that matches the existing CPU BGRA buffer format byte-for-byte. Renderer is context-agnostic: it consumes a Silk.NETGLhandle plusmakeCurrent/swapdelegates so the host owns context creation (WGL on Windows, GLX/EGL on Linux, CGL on macOS, or Avalonia.OpenGLGlInterface).SilkRendererFactory.Create(GL, IGpuSurface, …)+ProbeDescription()mirror the DX factory shape. Also movedIFractalRendereritself out ofInterefaces/intoFracturingFog.Abstractions(namespace unchanged → zero source breakage for DX consumers) so the Silk assembly can implement the contract without taking a ProjectReference back into the WinExe. -
RendererFactory.NonWin32Backendhook lets the Avalonia host register a Silk/Skia/Metal factory for non-HWND surfaces. Default behaviour on Windows (DX wins) unchanged;Create(IGpuSurface)now routes X11/Wayland/CAMetalLayer surfaces through the hook and only throws if it is null. Avalonia bootstrap on Linux/macOS will register the Silk factory once foreign-window GL context glue (WGL/GLX/EGL) lands — currently a follow-up. - CI build matrix: win-x64, linux-x64, osx-arm64 (
.github/workflows/cross-platform-build.yml). Builds + publishes Abstractions / Rendering.Silk / UI.Avalonia on every leg; WinExe leg is Windows-only. Pinned to net10.0 SDK; usesactions/setup-dotnet@v4. - ILGPU compute path validated on Linux/Mac (CUDA optional, CPU fallback required) —
Calculators/AcceleratorProbe.csexposesDescribeDevices(),HasGpuAccelerator(), andTryCreateCpuAccelerator(). The existingUserBulbGpuCalculator.TryInitchain (Context.Default()+GetPreferredDevice(preferCPU:false)) already falls through CUDA → OpenCL → CPU; the probe makes that path visible and lets smoke tests assert the managed CPU device exists on the Linux/macOS CI runners that lack GPU drivers.
- Foreign-window GL context adoption —
Rendering.Silk/Platform/SilkWin32ContextAdapter.cs(WGL:GetDC+ChoosePixelFormat/SetPixelFormat+wglCreateContextAttribsARBfor 3.3 core, falls back to legacy ctx when the ARB extension is absent, exposesINativeContextsoGL.GetApi(this)works) andRendering.Silk/Platform/SilkGLXContextAdapter.cs(GLX: opens its ownXOpenDisplay(null)to avoid coupling to Avalonia's internal Display*,glXChooseFBConfigfor RGBA8/Depth24/Stencil8 +glXCreateContextAttribsARBfor 3.3 core, libGL.so.1 / libX11.so.6 P/Invokes). macOS NSOpenGL adapter remains queued — see follow-up below. - Standalone runner via
Silk.NET.Windowing(GLFW backend) —Rendering.Silk/SilkStandaloneRunner.csowns anIWindow, builds aSilkGLRendereragainst the window's GL context, pumps frames from a caller-suppliedFunc<int, int, uint[]>, and ships aSmokeOneFrame()convenience that opens a 256×256 window, uploads one grey frame, returns the renderer description. NewRendering.Silk.Smokeconsole exe (net10.0, multi-RID) wraps it for CI; main WinExe also exposes--silk-smokeso the same path runs on Windows. -
AvaloniaShellBootstrapregistersRendererFactory.NonWin32Backendin a static ctor that switches onIGpuSurface.Kind: X11Window →SilkGLXContextAdapter→SilkRendererFactory.Create; Win32Hwnd (only reached when DX declined) →SilkWin32ContextAdapter→ same; CAMetalLayer + WaylandSurface → null + diagnostic warning until their adapters land. Init failures are logged, return null, and letRendererFactory.Createthrow with the original surface kind so the shell crash log still names the platform. - CI workflow smoke step (
.github/workflows/cross-platform-build.yml) builds + runsRendering.Silk.Smokeon the win-x64 and linux-x64 legs; Linux leg wrapsdotnet runinxvfb-run -aafterapt-get install libgl1 libglu1-mesa xvfb x11-utils libxrandr2 libxinerama1 libxcursor1 libxi6. macOS leg skips the run step (GLFW on osx-arm64 needs an interactive session for GL ctx creation; will switch to an offscreen FBO smoke onceSilkCglContextAdapterlands).
-
SilkCglContextAdapter—Rendering.Silk/Platform/SilkCglContextAdapter.csdrives NSOpenGL via the Objective-C runtime:objc_msgSend/sel_registerName/objc_getClassagainstNSOpenGLPixelFormat(3.2 core profile attrib list — the highest profile token the macOS GL stack exposes; SilkGLRenderer's 3.3 GLSL still compiles because Apple ships 4.1 core under that single token) andNSOpenGLContext.setView:against the NSView* IGpuSurface exposes under theCoreAnimationMetalLayerenum.INativeContext.GetProcAddressresolves viadlsymon a dlopen'dOpenGL.framework. Wired intoAvaloniaShellBootstrap.TryCreateSilkRendererso macOS now has a working Silk path. -
SilkEglContextAdapterfor Wayland —Rendering.Silk/Platform/SilkEglContextAdapter.csopens its ownwl_display_connect(NULL), callseglGetDisplay/eglInitialize/eglBindAPI(EGL_OPENGL_API)/eglChooseConfig/eglCreateContextwithEGL_CONTEXT_OPENGL_CORE_PROFILE_BIT+ forward-compatible flag, then builds awl_egl_windowagainst the IGpuSurface'swl_surface*and pumps it througheglCreateWindowSurface/eglMakeCurrent/eglSwapBuffers. Resized event hooked towl_egl_window_resize. Wired into bootstrap so Avalonia's native Wayland backend no longer falls through to XWayland + GLX. - Offscreen FBO smoke variant —
SilkStandaloneRunner.SmokeOneFrameOffscreen()opens an invisible GLFW window (WindowOptions.IsVisible = false) solely to obtain a 3.3 core context, then renders into a renderbuffer-backed FBO (RGBA8 + ColorAttachment0) and reads the centre pixel back viaglReadPixelsto verify the BGRA upload + textured-blit path round-trips end-to-end. CLI default switched to offscreen;--windowedflag keeps the original visible variant for swap-chain parity testing. Linux CI still wrapsxvfb-runbecause GLFW links X11 at startup (true surfaceless needs OSMesa/EGL — parked behind the EGL adapter path); macOS CI now runs the smoke without the previous skip because invisible windows do not require an interactive session. - Sibling
FracturingFog.Rendering.Skia(SkiaSharp 2.88) —SkiaCpuRendererwraps the calculator'suint[]BGRA buffer into anSKBitmapwithSKColorType.Bgra8888(zero-copy viaInstallPixelson a pinned GCHandle), takes a host-suppliedSkiaPresent(SKImage, w, h)delegate for the actual present step, and otherwise satisfiesIFractalRendererwith the sameUpdateTexture/Render/Resizecontract the DX + Silk backends already implement.SkiaRendererFactory.Create(IGpuSurface, present)mirrors the Silk factory shape;ProbeDescription()reads the loaded SkiaSharp'sAssemblyInformationalVersionAttributeat runtime. New project + sln entry + CI build/publish leg + WinExe ProjectReference all wired; default behaviour on every host unchanged (Skia is opt-in via the bootstrap'sNonWin32Backendoverride).
- Mobile/touch UI (defer until cross-platform desktop ships).
- Rewriting
MandelbrotCalculator/ kernels — they are UI-agnostic already. - Theming overhaul — match current dark theme; cosmetic redesign is a separate task.
- Removing Vortice — it stays as the Windows renderer.
-
DirectX hosting in Avalonia:
NativeControlHostworks but resize/devicelost handling needs care. Validate early in step 2.0. -
MVVM extraction depth:
MainForm.cshas tight coupling between input, view, and renderer state. Expect leaky abstractions during transition. - Build time: split projects increase first-build time. Acceptable tradeoff.
- DPI on multi-monitor mixed scaling: Avalonia handles per-monitor DPI natively; verify on a 100% + 150% dual-monitor setup.
-
Vortice swapchain rebuild on resize: must hook Avalonia's
SizeChangednot WinFormsResize.
- One commit per step (or sub-step) above. Format per repo convention:
<imperative summary> - BAB <yyyymmdd>. - Keep WinForms build green at every commit until step 2.3 deletes it.
- Tag
phase2-avalonia-bootstrapafter step 2.0 completes.
Phase 1 (low-effort WinForms scaling fixes) is a separate parallel track on a different branch and is not blocked by this work.