Hosting and rendering

Three thin hosts over one renderer. A host maps input, supplies a frame clock and hands the frame to the platform; the grid, its interaction and its drawing are the same code on all three.

The three hosts

HostControl, target, how the frame reaches the screen
ArionUI.Rendering.AvaloniaArionGrid, net10.0. Draws on the GPU through a Skia lease from Avalonia's compositor; without one, into a bitmap on the CPU.
ArionUI.Rendering.WpfArionGrid, net10.0-windows. Skia draws through ANGLE on Direct3D 11, on the grid's own render thread, into a shared texture shown by a D3DImage.
ArionUI.Rendering.WinFormsArionGridControl, net10.0-windows10.0.19041. Renders on an ANGLE window of its own. Not a package yet: reference the project from source.

Each control is an ordinary element of its framework: put it in a window, a tab, a split panel, several per window. A view belongs to one control at a time; attaching a view that is already shown by another control throws and changes nothing. Development and the automated tests run on Windows.

WinForms
var view = new GridView<OrderRecord>(
    [
        Column.Text<OrderRecord>("Customer", o => o.Customer).WithKey("customer").WithWidth(180),
        TypedColumn.Number<OrderRecord>("Amount", o => o.Amount, format: "#,##0.00").WithKey("amount"),
    ],
    new ListItemsSource<OrderRecord>(orders))
    .AsDataGrid();

Controls.Add(new ArionGridControl(view)
{
    Dock = DockStyle.Fill,
    VisualTheme = ArionTheme.Light(),
});

GPU path and CPU path

By default the grid draws on the GPU. Where Avalonia hands the grid no GPU context, the Avalonia host falls back to a CPU path on its own: it shifts the existing bitmap when scrolling and repaints only the strip that scrolled into view. Neither path scales with the number of rows; the CPU path scales with the pixel area it has to repaint.

The fallback is not silent. It is written to the diagnostic log as

Log line
[ArionUI] GPU-Lease nicht verfügbar → CPU-Fallback: <reason>

and the control says which path ran:

Avalonia: which path ran
if (grid.ActiveRenderer == RendererKind.Bitmap)
{
    // the CPU path: the reason is also written to ArionGrid.DiagnosticLog
    Log($"GPU path not available: {grid.FallbackReason}");
}

The WPF and WinForms hosts render through ANGLE; RendererName shows the adapter they got, which can be a software rasterizer such as the Microsoft Basic Render Driver. Charts draw their lines and areas on the GPU too and fall back to the surface by themselves. The 3D views of ArionUI.Charts3D have no CPU path; without a GPU they show their FallbackText.

Diagnostics

On every host
ArionGrid.DiagnosticLog = line => Log(line);   // default: Console.WriteLine

grid.RendererFailed += (_, e) => Log($"drawing failed: {e.Exception.Message}");
grid.FrameRendered += stats => Log($"{stats.FrameMs:0.00} ms, {stats.DrawnSlots} rows drawn");

string? adapter = grid.RendererName;     // the GL_RENDERER string, once a GPU frame ran
string? failure = grid.RendererFailure;  // why the last frame could not be drawn, or null

DiagnosticLog is static per host type and defaults to Console.WriteLine. RendererFailed is raised once per distinct failure, not once per frame. FrameRendered delivers FrameStats after each frame: its duration, the rows visible and drawn, and whether it was a full redraw; set ProfileFrames for a breakdown by phase. TryWriteFramePng(stream) forces a complete frame and writes the control's own pixels to a PNG — for bug reports and image tests.

Choosing the graphics adapter

On a laptop with two GPUs the display usually hangs on the integrated one, and that is what a host gets unless something asks for the other. The environment variable ARION_GPU decides before start-up:

ValueAdapter
highThe hardware adapter with the most dedicated video memory.
lowThe one with the least.
any other textThe first adapter whose description contains it, ignoring case (nvidia, intel).
unsetThe host's default.

RendererName then tells you which adapter really drew. On WPF the texture has to reach a Direct3D 9 device for D3DImage, and a shared texture does not cross adapters; where that does not work the host refuses rather than showing an empty window.

Fonts and text

The default font family is Segoe UI; the Skia package bundles JetBrains Mono. Any other font is registered once at start-up and then named in the theme:

ArionFonts
ArionFonts.Register("Inter", File.OpenRead("Fonts/Inter-Regular.ttf"));
ArionFonts.Register("Inter", File.OpenRead("Fonts/Inter-SemiBold.ttf"), FontWeight.SemiBold);
grid.VisualTheme = ArionTheme.Light() with { FontFamily = "Inter" };

ArionFonts.Shaping = TextShaping.Simple;   // one glyph per character, no HarfBuzz

Text is shaped with HarfBuzz by default, with fallback to an installed font for characters the requested one lacks. Programming ligatures are off (ArionFonts.Ligatures), so -> stays two characters. TextShaping.Simple draws one glyph per character, which is also what happens when the native HarfBuzz library is missing — fine for Latin, Greek and Cyrillic, wrong for joined scripts such as Arabic. The environment variable ARION_SHAPING=0 forces the simple path whatever the application sets.