Scythe
SCYTHE is a lightweight, C#-based game engine focused on modifiability and rapid iteration using Raylib.
Cross-platform GPU rendering, composition, vector, text, compute, and DirectX/LibreWPF/WinUI/Avalonia interop on WebGPU…
ProGPU is a high-performance, GPU-first UI framework and composition substrate for .NET, built on top of Silk.NET and WebGPU (wgpu-native). It provides a lightweight, low-allocation alternative to traditional heavyweight UI frameworks by routing all vector graphics, text layout, and composition operations directly to the GPU using native WebGPU draw pipelines.
ProGPU runtime packages are built from eng/progpu-package-list.sh by the
Release GitHub Actions workflow. The same workflow builds the Avalonia
renderer and Silk.NET windowing packages from the separate versioned
integration lane in scripts/progpu-package-list.sh, publishes them after
their ProGPU runtime dependencies, and attaches both lanes to the GitHub
release. Samples, tests, diagnostics, and framework shim projects are
intentionally not packed.
Local package build:
PROGPU_PACKAGE_VERSION=0.1.0-preview.31 ./eng/progpu-pack.sh
Pack both Avalonia integration lanes after the portable ProGPU runtime packages:
PROGPU_PACKAGE_GROUP=portable ./eng/progpu-pack.sh ./scripts/progpu-pack.sh
See docs/progpu-packaging.md for package-only
consumer validation, version overrides, and publishing.
The mobile packages contain the managed hosts and buildTransitive native-link
contracts. Until native binaries are distributed independently, applications set
ProGpuWgpuNativeAndroidRoot or ProGpuWgpuNativeXCFramework to outputs from the
repository's pinned wgpu-native build scripts before deployment.
Packing the complete set requires macOS with the Android and iOS workloads;
PROGPU_PACKAGE_GROUP=portable packs the 31 cross-platform packages on Linux.
ProGPU.Samples.iOS is a thin native host for the same ProGPU.Samples gallery used by desktop and browser. It renders directly into a physical-pixel CAMetalLayer through a Metal-only wgpu-native XCFramework and the existing Silk.NET WebGPU API. It has no MAUI, Uno, WKWebView, or JavaScript rendering dependency.
./eng/build-wgpu-native-ios.sh dotnet build src/ProGPU.Samples.iOS/ProGPU.Samples.iOS.csproj \ -c Debug -r iossimulator-arm64
The iOS-specific solution, native build details, ABI pin, architecture research, simulator commands, physical-device guidance, and current limitations are in docs/ios.md.
ProGPU.Samples.Android is the corresponding thin .NET Android host for the
shared gallery. It renders directly to an ANativeWindow-backed WebGPU surface
through Vulkan, with no browser, Android Canvas, MAUI, Uno, or readback path.
export ANDROID_NDK_ROOT=/absolute/path/to/android-sdk/ndk/your-version ./eng/build-wgpu-native-android.sh arm64 dotnet build src/ProGPU.Samples.Android/ProGPU.Samples.Android.csproj \ -c Debug -f net10.0-android
The Android architecture, exact native ABI pin, ARM64/x64 build lane, AOT
publishing guidance, input/IME/inset/storage contracts, clean-room research,
and device validation gates are in docs/android.md.
The gallery is split into a shared ProGPU.Samples library and thin ProGPU.Samples.Desktop, ProGPU.Samples.Browser, ProGPU.Samples.iOS, and ProGPU.Samples.Android hosts. The browser host publishes with the .NET WebAssembly SDK, negotiates WebGPU capabilities, sends aligned binary command packets directly from WASM memory, and passes embedded WGSL unchanged to GPUDevice.createShaderModule.
wasm-tools workload for native relinking and AOT publishing.http://localhost and http://127.0.0.1 are suitable for local development; do not open the published index.html through a file:// URL.Install or restore the WebAssembly workload once:
dotnet workload install wasm-tools dotnet workload restore src/ProGPU.slnx
ProGPU.Browser is the reusable browser backend. It supplies the navigator.gpu implementation of IWebGpuApi, the batched command protocol, browser input and storage adapters, and the external WinUI-shaped window host. ProGPU.Samples.Browser shows the complete startup wiring while reusing every page and shader from the ProGPU.Samples library.
To add a browser host to another ProGPU application:
Microsoft.NET.Sdk.WebAssembly executable targeting net10.0 with RuntimeIdentifier set to browser-wasm.ProGPU.Browser and the project containing the shared application and views.src/ProGPU.Browser/BrowserAssets/progpu-browser.js to the host's wwwroot as its JavaScript entry point, and provide an index.html containing the canvas selected by BrowserAppHostOptions.CanvasSelector.BrowserGpuRuntime.InitializeAsync, install its capabilities in a BrowserWindowHost, assign that host to WindowHostServices.Current, and run the shared application through AppBuilder<TApplication>.The browser sample's ProGPU.Samples.Browser.csproj, Program.cs, and index.html are a minimal working template. The shared UI belongs in ProGPU.Samples; only startup, canvas/DOM assets, and browser platform wiring belong in the thin browser host. See docs/browser.md for the command protocol, transport modes, and deployment model.
The browser backend is an implementation of the same typed IWebGpuApi boundary used by desktop. Renderer, compositor, vector, text, compute, control, and sample code do not branch into a browser renderer. Only the host and WebGPU transport change:
flowchart LR
App["Shared C# application and controls"] --> Scene["Retained scene, text, vector, and compute"]
Scene --> Api["IWebGpuApi"]
Api --> Desktop["SilkWebGpuApi<br/>wgpu-native"]
Api --> BrowserApi["BrowserWebGpuApi<br/>browser-wasm"]
BrowserApi --> Packet["Reusable aligned binary command arena"]
Packet --> Interop["One coarse JS interop dispatch"]
Interop --> Decoder["progpu-browser.js packet decoder"]
Decoder --> WebGPU["navigator.gpu"]
Shader["Embedded WGSL shader resources"] --> Scene
Shader -->|"unchanged UTF-8 source"| WebGPU
Loading
The browser path is composed of these layers:
ProGPU.Samples contains the application, windows, pages, retained visuals, and every shader-using sample. It is a library shared unchanged by both hosts.ProGPU.Samples.Desktop is a thin Silk.NET executable. ProGPU.Samples.Browser is a thin Microsoft.NET.Sdk.WebAssembly executable with the canvas, DOM shell, and JavaScript module.BrowserGpuRuntime.InitializeAsync selects the canvas, requests an adapter/device/profile, chooses a transport, and returns a typed BrowserGpuCapabilities result. Unsupported capabilities fail during negotiation rather than rewriting application shaders.BrowserWindowHost installs browser input, storage, clipboard, fallback font, and window services. Its requestAnimationFrame loop drains one fixed-width input batch, reads physical canvas size and DPI, then calls the ordinary Window.RenderExternalFrame path.BrowserWebGpuApi implements renderer-facing WebGPU operations. It serializes resource descriptors, pass commands, copies, uploads, submission, mapping, and destruction into a versioned little-endian protocol. Packets have a 16-byte PGPU header, eight-byte-aligned commands, and generation-bearing handles that reject stale JavaScript resources.progpu-browser.js reads packets directly from the current WebAssembly linear-memory view. It creates the corresponding GPUDevice, buffers, textures, bind groups, pipelines, render/compute passes, and queue operations without JSON or one JS call per draw. Upload payloads and mapped-buffer completion use separate coarse asynchronous seams where WebGPU requires them.MainThread, Worker, and IsolatedWorker transports use the same packet format. Auto prefers a cross-origin-isolated shared-memory worker, then an OffscreenCanvas worker, and finally the main thread. All packets produced by one managed frame are handed to a worker as one ordered batch so the acquired surface texture remains valid for the complete frame.Shaders/ directory and is embedded by ShaderResource. The exact source used by desktop is carried through ShaderModuleWGSLDescriptor and passed directly to GPUDevice.createShaderModule; the browser does not transpile, fork, or maintain copies of those shaders.The standard browser canvas host currently supports one top-level Window. Popups remain ordinary compositor layers, so menus, flyouts, tooltips, and dialogs still share the same frame, input, and hot-reload tree.
Desktop and browser Debug hosts use the same framework-level metadata update pipeline. ProGPU.WinUI registers HotReloadManager with the runtime through MetadataUpdateHandlerAttribute; the browser project additionally enables the .NET WebAssembly Hot Reload component in Debug.
flowchart LR
Edit["C# edit from dotnet watch or IDE"] --> Delta[".NET metadata/IL delta"]
Delta --> Clear["HotReloadManager.ClearCache"]
Delta --> Update["HotReloadManager.UpdateApplication"]
Clear --> Queue["Coalesced UIThread generation"]
Update --> Queue
Queue --> Maps["Original/replacement type mapping"]
Maps --> Hooks["Application and registered update hooks"]
Hooks --> Roots["Active windows, popups, and registered roots"]
Roots --> Choice{"Reload strategy"}
Choice --> InPlace["IHotReloadable.Reload"]
Choice --> Factory["Typed factory or development activator"]
Choice --> Page["Static NavigationView page factory refresh"]
Factory --> State["Capture and restore interaction state"]
Page --> State
InPlace --> Done["Invalidate layout/render and report result"]
State --> Done
Loading
Each runtime callback only queues work; reconciliation runs on UIThread before rendering. Multiple deltas arriving before the next UI turn become one generation. The manager clears reusable theme/style factory caches, maps replacement types back to their original live types, invokes application hooks, and walks active Window content, popup roots, plus roots registered by embedded hosts. Recursive theme-resource reevaluation is reserved for ThemeManager, Style, ResourceDictionary, ThemeResource, and conservative all-types updates; unrelated method-body edits leave the retained scene and unaffected controls intact.
For an updated element, the manager uses these strategies in order:
IHotReloadable element rebuilds itself in place. This is the preferred choice for controls with explicit construction state or an existing Build method.HotReloadManager.RegisterFactory<TElement> recreates the element without reflection and is suitable for required constructor arguments.NavigationViewItem pages created by static page factories are recreated when their factory owner type changes. Inactive cached pages are cleared and will use the updated factory when selected.Replacement captures state recursively by stable Name with structural-path fallback. The built-in state store preserves local DataContext, text/password and caret/selection, check/toggle state, selector and pivot selection, navigation selection and pane state, data-grid selection, scroll/virtualization offsets, attached layout properties such as Grid.Row, focus, and custom IHotReloadStateful values. Loading/unloaded lifecycle events are isolated, immediate state is restored before the next draw, and layout-dependent scrolling/focus is restored on the following UI turn.
Application authors can opt into the pipeline with small typed contracts:
using Microsoft.UI.Xaml.HotReload;
public sealed class LiveChart : Grid, IHotReloadable, IHotReloadStateful
{
public void Reload(HotReloadContext context)
{
ClearChildren();
BuildChart();
}
public object? CaptureHotReloadState() => SelectedSeries;
public void RestoreHotReloadState(object? state) => SelectedSeries = state as string;
}
// Retain this IDisposable for as long as the factory should be active.
var registration = HotReloadManager.RegisterFactory(
() => new ConstructorBoundControl(applicationService));
An Application may implement IHotReloadable to reapply window- or application-level configuration. Static shell builders can register an update callback and call HotReloadManager.ReloadWindowContent; custom containers whose backing collection must change with a visual child can implement IHotReloadChildReplacer. Embedded/non-window hosts register stable visual roots with HotReloadManager.RegisterRoot(root). When the registered root itself can be recreated, use HotReloadManager.RegisterRoot(root, replacement => host.Root = replacement) so the framework can replace it atomically while preserving state.
HotReloadManager.UpdateStarted, UpdateCompleted, LastResult, and Diagnostic expose generation, duration, replacement/reload/invalidation/failure counts, and isolated exceptions. Set PROGPU_HOT_RELOAD=0 to disable framework reconciliation. Live metadata updates are a Debug development feature: Release AOT artifacts retain the same UI/browser architecture but do not activate metadata-update handlers.
From the repository root, restore and run the normal Debug build:
dotnet restore src/ProGPU.Samples.Browser/ProGPU.Samples.Browser.csproj dotnet run --project src/ProGPU.Samples.Browser/ProGPU.Samples.Browser.csproj -c Debug
Open the HTTP URL printed by the command. The sample itself is used like the desktop gallery: select pages from the navigation pane, interact with the controls and GPU samples, and use Settings → Show Browser WebGPU Diagnostics when transport or adapter details are needed.
Debug configuration uses the managed WebAssembly interpreter and keeps browser Hot Reload enabled. Build it without starting a server with:
dotnet build src/ProGPU.Samples.Browser/ProGPU.Samples.Browser.csproj -c Debug
For the normal edit/run loop, start the local server with dotnet watch:
dotnet watch --project src/ProGPU.Samples.Browser/ProGPU.Samples.Browser.csproj run -c Debug
Open the printed URL and leave dotnet watch running. Supported C# method-body edits are applied to the running browser application; use Ctrl+R in the watch terminal when an edit requires a restart and Ctrl+C to stop it. dotnet run can be used instead when Hot Reload is not needed.
To create a deployable interpreter artifact—useful for fast deployment iterations or comparison with AOT—publish with AOT disabled explicitly:
dotnet publish src/ProGPU.Samples.Browser/ProGPU.Samples.Browser.csproj \ -c Release \ -p:RunAOTCompilation=false \ -o artifacts/browser-interpreter python3 -m http.server 8080 --directory artifacts/browser-interpreter/wwwroot
Then browse to http://localhost:8080/.
Release configuration enables managed WebAssembly AOT compilation, trimming, SIMD, and native relinking by default. AOT is a publish-time mode, so use dotnet publish rather than dotnet run:
dotnet publish src/ProGPU.Samples.Browser/ProGPU.Samples.Browser.csproj \ -c Release \ -o artifacts/browser-aot python3 -m http.server 8080 --directory artifacts/browser-aot/wwwroot
Open http://localhost:8080/. All ProGPU and sample assemblies are AOT compiled. netDxf.netstandard is intentionally retained in supported mixed interpreter/AOT mode because the .NET 10 Mono AOT compiler currently asserts while compiling that upstream assembly.
The deployable application is the complete contents of artifacts/browser-aot/wwwroot; upload that directory to any static HTTP(S) host. Do not deploy its parent directory and do not open index.html with a file:// URL.
To confirm that a publish actually used AOT, inspect the publish log for AOT'ing and native WebAssembly linking steps. Browser Hot Reload is a Debug development feature and is not active in the trimmed Release AOT artifact. For a clean comparison between modes, remove the selected output directory before republishing so stale fingerprinted framework assets cannot be served.
The two local deployment modes can use different ports when comparing them side by side:
python3 -m http.server 8080 --directory artifacts/browser-interpreter/wwwroot python3 -m http.server 8081 --directory artifacts/browser-aot/wwwroot
The Browser Pages workflow publishes the Release browser host with WebAssembly AOT and deploys artifacts/browser-aot/wwwroot whenever main changes. It can also be started manually from Actions → Browser Pages → Run workflow. The public gallery is available at https://wieslawsoltes.github.io/ProGPU/ after the first successful deployment.
The browser page uses relative asset URLs so the .NET runtime, worker module, styles, and fingerprinted framework files resolve beneath the /ProGPU/ repository path. The published .nojekyll marker ensures GitHub Pages serves the _framework directory unchanged.
The browser host runs the shared retained gallery through the complete IWebGpuApi dispatcher. It supports main-thread, OffscreenCanvas worker, and cross-origin-isolated worker transports; batches DOM input once per frame; preserves asynchronous mapped-buffer read/write behavior; and sends the same embedded WGSL used by desktop directly to GPUDevice.createShaderModule.
The status bar at the bottom of the gallery remains visible. The larger browser WebGPU diagnostics overlay reports the active transport, adapter/profile, frame count, dispatch count, and command bytes; it starts hidden and can be enabled from Settings → Show Browser WebGPU Diagnostics. Startup and WebGPU errors reveal it automatically.
For cross-origin-isolated worker mode, configure the server to send:
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp
Without these headers, Auto uses the ordinary OffscreenCanvas worker when available and falls back to main-thread rendering when canvas transfer is unavailable. See the browser backend guide for the full protocol, hosting, and capability details.
Local publishing reads the API key from NUGET_API_KEY without storing it in the repository:
PROGPU_PACKAGE_VERSION=0.1.0-preview.31 ./eng/progpu-publish.sh
The release workflow validates docs, restores, builds, tests, packs .nupkg/.snupkg artifacts, and can publish to NuGet.org when NUGET_API_KEY is configured. See docs/release.md.
LibreWPF ports the managed WPF runtime and SDK to the ProGPU/Silk.NET platform. Applications can switch to the custom SDK while retaining familiar WPF source, XAML, controls, and Windows-shaped APIs on supported non-Windows hosts.
LibreWinForms provides portable WinForms-shaped APIs hosted by the ProGPU/LibreWPF stack. It preserves the common System.Windows.Forms development model while replacing native GDI rendering with the ProGPU-backed compatibility layer.
The Avalonia ProGPU renderer replaces the Skia renderer with a GPU-first WebGPU implementation while preserving Avalonia's rendering contracts. It also exposes an API lease for issuing custom ProGPU vector operations and WebGPU shaders inside an Avalonia frame.
The Silk.NET Avalonia backend supplies cross-platform desktop windowing, input, surfaces, and WebGPU integration. It is designed to pair with the ProGPU renderer but can host another compatible Avalonia renderer.
| Package | Purpose | NuGet |
|---|---|---|
ProGPU.Avalonia.Rendering |
ProGPU and WebGPU rendering platform for Avalonia. | |
ProGPU.Avalonia.SilkNet |
Cross-platform Silk.NET windowing platform for Avalonia. |
Each compatibility lane produces its own .nupkg and .snupkg artifacts from
separate project files. The v11 and v12 artifacts intentionally share the same
two NuGet package IDs and are distinguished by their package versions:
| Avalonia | Rendering package | Silk.NET package |
|---|---|---|
| 12.0.5 | ProGPU.Avalonia.Rendering 12.0.5-preview.31 |
ProGPU.Avalonia.SilkNet 12.0.5-preview.31 |
| 11.3.18 | ProGPU.Avalonia.Rendering 11.3.18-preview.31 |
ProGPU.Avalonia.SilkNet 11.3.18-preview.31 |
The Avalonia 12 artifacts are built from
src/ProGPU.Avalonia.Rendering and src/ProGPU.Avalonia.SilkNet. The Avalonia
11 artifacts are built separately from the thin
src/ProGPU.Avalonia.Rendering.V11 and src/ProGPU.Avalonia.SilkNet.V11
projects, which source-link the shared implementation and define
AVALONIA11.
Install the Avalonia 12 packages with:
dotnet add package ProGPU.Avalonia.Rendering --version 12.0.5-preview.31 dotnet add package ProGPU.Avalonia.SilkNet --version 12.0.5-preview.31
For Avalonia 11, use the same package IDs with 11.3.18-preview.31 and pin all
Avalonia packages to 11.3.18. Configure the application with
UseSilkNet().UseProGpu() before starting the desktop lifetime. Complete
startup, API-lease, local packaging, and package-only validation instructions
are in docs/progpu-packaging.md.
The shim provides a managed SkiaSharp 4.148-shaped API over ProGPU vector, text, imaging, path, and compositing primitives without loading native Skia binaries. Compatibility consumers such as Svg.Skia can use the ProGPU renderer while CPU-only metadata and geometry operations remain independent of WebGPU initialization.
Detailed API coverage, rendering behavior, algorithms, and complexity guarantees are maintained in the SkiaSharp compatibility log.
| Package | Purpose | NuGet |
|---|---|---|
ProGPU.SkiaSharp |
ProGPU-backed SkiaSharp API compatibility layer. |
Svg.Skia renders SVG 1.1 documents and its supported static SVG 2 subset through a SkiaSharp-shaped canvas. Its W3C and resvg test lanes also exercise ProGPU.SkiaSharp, providing broad compatibility and rendering-parity coverage for the shim.
The Svg.Skia parity workflow pins the exact Svg.Skia source commit and runs separate native-SkiaSharp and ProGPU-shim checkouts on macOS. The native W3C lane must remain 530 passes with 3 skips; resvg and the non-W3C remainder must stay fully green through the shim. The ProGPU W3C lane validates its exact reviewed difference inventory and uploads both TRX files plus actual PNGs. A resolved row, a new failure, or a changed failure set intentionally fails the inventory gate until the images are reviewed and eng/svg-skia-w3c-known-differences.txt is updated in the same change.
The ProGPU framework is built in a modular, layered stack that bridges native graphics APIs and system windowing up to a modern, declarative WinUI-compatible user interface layer.
graph TD
subgraph L6 ["Layer 6: Application Layer"]
App["Gallery Dashboard / LOL/s & MotionMark Benchmarks"]
end
subgraph L5 ["Layer 5: WinUI Framework Layer"]
Controls["Grid, StackPanel, ScrollViewer, Border, Pivot, RichTextBlock"]
FE["FrameworkElement"]
LN["LayoutNode - Measure & Arrange Sizing Negotiation"]
end
subgraph L4 ["Layer 4: Scene Graph & Effects Layer"]
CV["ContainerVisual / DrawingVisual / Visual with ChangeVersion"]
ILN["ILayoutNode Interface - Layout and Scene Invalidation"]
FX["GPGPU Multi-Pass Effects Pipeline - Blur & DropShadow"]
end
subgraph L3 ["Layer 3: Compositor, Text & GPGPU Rasterizer"]
Cache["Compiled Scene Cache - Versions, Targets, Atlases, Layers"]
Comp["Compositor - Z-Ordered Draw Lists and GPU Buffer Compiler"]
Text["TTF Layout, Rich Text Command Cache, Glyph Atlas"]
Rast["Compute-Bound 4x SSAA Glyph and Path Rasterizers"]
end
subgraph L2 ["Layer 2: Graphics Infrastructure"]
Wgpu["WgpuContext - WebGPU Adapter/Device & Swapchain Management"]
end
subgraph L1 ["Layer 1: System & Windowing"]
Silk["Silk.NET Windowing & GLFW OS Event Loop"]
end
App --> Controls
Controls --> FE
FE --> LN
LN --> CV
CV --> ILN
CV --> FX
ILN --> Cache
Cache --> Comp
FX --> Rast
Comp --> Rast
Rast --> Wgpu
Wgpu --> Silk
Loading
ContainerVisual, DrawingVisual, and Visual hierarchy. Mutations propagate ChangeVersion and dirty state so layout, compiled-scene, and CacheAsLayer reuse remain correct. Mask and effect passes use offscreen textures and intentionally stay on the dynamic compilation path.Measure and Arrange, controls, input, and CPU visual-tree hit testing. The WinUI host disables the compositor's duplicate GPU hit-test index while direct compositor consumers retain it by default.Fixed GPU programs live as individual .wgsl, .glsl, or .hlsl files under the owning project's Shaders/ directory. Directory.Build.props embeds these resources into each assembly, and ShaderResource decodes each source once into a process-wide cache. Pipeline owners retain the returned reference in static readonly fields, so rendering and compute hot paths perform no shader file I/O, manifest lookup, UTF-8 decoding, helper concatenation, or per-frame source allocation.
Each resource documents its algorithm, time complexity, and storage or bandwidth complexity at the top of the file. Final render and compute modules are self-contained, including analytic curve helpers that were previously concatenated from C# strings. Dynamic systems keep only input-dependent generation in C#: the DirectX HLSL translator emits WGSL from caller programs, WPF effects generate active sampler declarations, and ShaderToy appends user code. Their fixed headers and fragment wrappers are still resource-backed and cached.
ShaderResourceTests verifies that every source file is present in its assembly, loaded text matches the checked-in resource, required cost-model documentation exists, and fixed production stage modules do not reappear as C# literals. This keeps shader reuse and performance properties enforceable as the renderer evolves.
Microsoft.UI.Xaml.Window.RenderFrameCore records each host phase independently: dispatcher work, rendering callbacks, framebuffer/DPI setup, animation, layout, surface acquisition, compositor work, and presentation. Compositor.RenderScene then chooses between a retained fast path and a dynamic compile path:
flowchart TD
Frame["Window frame"] --> Dispatch["Drain bounded UI work"]
Dispatch --> Update["Rendering callbacks, animation, cached layout"]
Update --> Acquire["Acquire physical-pixel surface"]
Acquire --> Validate{"Compiled scene still valid?"}
Validate -- Yes --> Reuse["Reuse draw lists, GPU buffers, brushes, hit index, and atlas entries"]
Validate -- No --> Compile["Compile visual tree and external layers"]
Compile --> AtlasStable{"PathAtlas coordinates stable?"}
AtlasStable -- Yes --> Upload["Upload changed geometry, brushes, and uniforms"]
AtlasStable -- "No, first failure" --> ResetAtlas["Reset once and discard CPU compilation"]
ResetAtlas --> Compile
AtlasStable -- "No after retry" --> FailFrame["Fail explicitly; live paths exceed capacity"]
Upload --> Raster["Batch pending glyph and path rasterization"]
Raster --> Capture{"Scene safe to retain?"}
Capture -- Yes --> Remember["Capture versions, target, atlas generations, and layer textures"]
Capture -- No --> Dynamic["Keep dynamic path for effects, masks, diagnostics, or DrawingVisual"]
Remember --> Render["Execute ordered WebGPU render pass"]
Dynamic --> Render
Reuse --> Render
Render --> Present["Present"]
Loading
The compiled scene cache is enabled by default with CompositorOptions.EnableCompiledSceneCache. A hit requires the same root identity and ChangeVersion, logical and physical target, viewport, DPI scale, glyph/path atlas generations, tooltip, external layer versions, and valid CacheAsLayer textures. Dynamic diagnostics force a miss. Mutable DrawingVisual content, masks, and effects are deliberately not retained because their output can change without a stable immutable command contract.
CacheAsLayer and compiled-scene reuse solve different costs. CacheAsLayer turns a stable subtree into one texture draw while the rest of a scene may still compile. Whole-scene reuse preserves the already compiled draw lists and GPU buffers for a stable frame. Atlas Generation values make both paths safe when a glyph/path atlas is cleared or repacked.
The WinUI host sets EnableGpuHitTesting = false because InputSystem already performs CPU visual-tree hit testing. Direct scene consumers keep the compositor GPU hit-test index enabled by default. This avoids building two indexes for every WinUI frame without changing input behavior.
The opt-in sample harness reports wall-clock FPS, per-phase timings, allocation rate, scene-cache decisions, draw counts, and workload throughput. A July 2026 macOS 120 Hz reference run of the current architecture produced the following results; hardware, window size, and page state affect absolute values.
| Workload | VSync | Wall FPS | Workload throughput | Scene cache |
|---|---|---|---|---|
| LOL/s Benchmark | On | 120.21 | 11,996 LOL/s | Dynamic, 0/480 hits |
| LOL/s Benchmark | Off | 245.04 | 48,905 LOL/s | Dynamic, 0/480 hits |
| Markdown Playground | Off | 519.02 | Static after warmup | 299/300 hits |
| DXF CAD Viewer | Off | 535.55 | Static after warmup | 299/300 hits |
Run the same deterministic workload from the repository root:
dotnet build src/ProGPU.Samples.Desktop/ProGPU.Samples.Desktop.csproj -c Release PROGPU_SAMPLE_BENCHMARK_PAGE='LOL/s Benchmark' \ PROGPU_SAMPLE_BENCHMARK_WARMUP_FRAMES=240 \ PROGPU_SAMPLE_BENCHMARK_MEASURE_FRAMES=480 \ PROGPU_SAMPLE_BENCHMARK_VSYNC=true \ dotnet run --project src/ProGPU.Samples.Desktop/ProGPU.Samples.Desktop.csproj -c Release --no-build
Set PROGPU_SAMPLE_BENCHMARK_VSYNC=false for uncapped throughput, or change the page to Markdown Playground, Text & Documents, or DXF CAD Viewer. The Text & Documents scroll workload builds a 20,000-paragraph mixed-script RichEditBox and reports realized paragraphs and visible characters alongside frame, layout, compositor, allocation, and scene-cache metrics. The first measured static frame may populate the cache; subsequent frames should report hits unless the page intentionally animates or invalidates.
Rendering quality remains part of the performance contract. The optimized text path retains the glyph index chosen during layout, hoists transform/raster invariants out of glyph loops, and skips color/bitmap table probes only when the parsed font has no such tables. Avalonia solid outline text records one retained glyph run instead of one path per glyph: shaped indices are retained, Vector2 positions are converted once when the platform glyph run is created, and redraws reuse both arrays. Recording is O(1) with no glyph-count-dependent allocation; compositor compilation is O(G) for G glyphs. Gradient brushes and color/bitmap fonts keep their path or texture fallbacks.
Vector glyphs keep 8x8 path-atlas coverage and use a device-pixel-size transfer calibrated against native Skia: small axis-aligned text preserves fine edge detail, large text receives the slightly stronger coverage needed to match Skia's visual weight, and rotated/reflected text keeps its separately calibrated branch. The physical-size classification includes display DPI, transform scale, and static-buffer zoom, and is computed once per text command for reuse by every glyph. Glyph geometry, subpixel placement, physical DPI rasterization, winding rules, brush opacity, and blend behavior remain unchanged.
Texture resampling follows the same contract. SKCubicResampler is an immutable two-coefficient SkiaSharp value with native float equality, hashing, and named Mitchell (1/3, 1/3) and Catmull-Rom (0, 1/2) kernels. SKSamplingOptions is the matching immutable discriminated value for nearest/linear filtering, mipmapping, cubic coefficients, and requested anisotropy. Cubic draws retain B/C through the recorded command and texture vertices, and the WGSL texture shader evaluates the full Mitchell-Netravali kernel. Named and custom kernels therefore remain distinct. Anisotropic draws retain their requested value through recording, clamp only at the portable WebGPU boundary to [1, 16], generate mipmaps, and reuse one lazily created sampler and persistent texture bind group per effective value; ordinary draws do not enter this cache. Value construction, reads, comparison, and hashing are allocation-free O(1) CPU operations. Sampler lookup is amortized O(1) with at most 15 anisotropic sampler objects, and the common Catmull-Rom render path keeps its original compact polynomial and pixel output.
The sections below describe the cooperating layout, scene, text, atlas, batching, effects, and platform optimizations. They share one invariant: cached work is reused only while every input that can affect pixels remains valid.
Traditional layout systems recursively traverse the entire scene graph every frame to negotiate sizing, causing massive $O(N)$ CPU overhead on complex visual trees even when the UI is static.
ProGPU introduces a cached sizing negotiation model that short-circuits measurements using layout dirty flags and cached input boundaries:
flowchart TD
Start["Measure Pass availableSize"] --> Cached{"_isMeasureValid and availableSize == _previousAvailableSize?"}
Cached -- Yes --> O1Exit["O1 Early Exit - Return Cached DesiredSize"]
Cached -- No --> Calc["Calculate Margin Insets & Bounds Constraints"]
Calc --> Override["Execute MeasureOverride child passes recursively"]
Override --> CacheResult["Store DesiredSize, _previousAvailableSize & set _isMeasureValid = true"]
CacheResult --> ArrangeStart["Arrange Pass finalRect"]
ArrangeStart --> CachedArr{"_isArrangeValid and _isMeasureValid and finalRect == _previousFinalRect?"}
CachedArr -- Yes --> O1ExitArr["O1 Early Exit - Return Immediately"]
CachedArr -- No --> Align["Calculate Offset Coordinates & Horizontal/Vertical Alignments"]
Align --> OverrideArr["Execute ArrangeOverride child placements recursively"]
OverrideArr --> CacheResultArr["Store Offset/Size, _previousFinalRect & set _isArrangeValid = true"]
Loading
LayoutNode.Measure(), if _isMeasureValid is true and the incoming availableSize matches _previousAvailableSize, the pass returns immediately. MeasureOverride and recursive child traversals are fully bypassed in $O(1)$ time.LayoutNode.Arrange(), if _isArrangeValid and _isMeasureValid are true and the incoming finalRect matches _previousFinalRect, the pass short-circuits. Children offsets are not recalculated, and recursive child arrangements are bypassed.Margin, Padding, WidthConstraint, HeightConstraint, alignments, or child mutations) are changed, they invoke InvalidateMeasure() or InvalidateArrange(). These clear local flags and bubble up the invalidation recursively to parent nodes, forcing only the dirty subtrees to be re-evaluated during the next frame's deferred layout pass.To prevent circular dependencies between the ProGPU.Scene assembly (base visual layer) and the ProGPU.Layout assembly (WinUI framework layer), the ILayoutNode interface is defined in ProGPU.Scene:
public interface ILayoutNode
{
void InvalidateMeasure();
}
Visual tree mutation methods (ContainerVisual.AddChild, RemoveChild, ClearChildren) check if this implements ILayoutNode. If so, they invoke InvalidateMeasure(), ensuring that any changes in visual tree structure automatically mark the layout path dirty without explicit parent layout references.
Layout validity and pixel validity are separate. A layout property can alter clipping, alignment, padding, or descendant placement even when the final Size and Offset happen to compare equal. The LayoutNode setters therefore invalidate measure or arrange and call visual Invalidate(), which advances ChangeVersion to the compiled-scene root. This prevents a retained frame from displaying stale text or geometry after a layout-only mutation.
Text interaction uses the narrowest valid invalidation path. RichTextBlock observes every retained TextElement in its inline tree, so changing Run.Text, font, size, or foreground advances the owning visual version and invalidates layout without waiting for pointer activity. Hyperlink hover, caret movement, and selection changes dirty only render commands and pixels; they retain positioned characters and do not discard document layout. Markdown uses one immutable process-wide Markdig pipeline, warmed asynchronously during application startup. Single-column Markdown measured with infinite height performs one engine layout instead of a measurement layout followed by an identical second pass, while multi-column measurement reuses scratch lists. Width and multi-column-height changes explicitly invalidate layout, and empty content transactionally clears positioned characters, embedded children, and retained commands.
Page navigation keeps stable ownership boundaries:
NavigationView.Content changes only the SplitView content child. The pane and all menu-item visuals remain parented, preserving their layout, theme resources, and cached layer.SplitView removes and inserts only the child that changed instead of clearing and rebuilding both children.Large indexed pages remain virtual from source to pixels. ItemsControl retains an IList source rather than eagerly copying and boxing every item. Attaching a 65,535-glyph source is O(1) time and storage; UniformVirtualizingGridPanel realizes and binds only V visible/overscan items in O(V), reuses its recycler-index buffer, and does not allocate a binding closure on each viewport update. Selection and syntax-highlight changes rebind the V active containers in place instead of recycling, reparenting, and remeasuring them. Scrollbar z-order uses an in-parent reorder that invalidates pixels but not layout. The glyph browser represents indices through a read-only computed list, so it allocates neither a 65,535-entry source array nor an internal duplicate. FontIcon records the cached raw outline with a public DrawPath transform, preserving line, quadratic, cubic, and arc segments without allocating a transformed path per cell or render.
Layout caching relies heavily on comparing boundary structs (Thickness and Rect) on every node. Standard C# struct comparison utilizes generic ValueType.Equals, which triggers CPU reflection, runtime boxing, and high memory allocations.
To eliminate this bottleneck, we implemented type-safe, non-boxing, custom equality overloads for both structs:
Thickness (Margins and Paddings)Rect (Layout arrangements and clipping boundaries)Each struct now overrides Equals(Thickness/Rect other), Equals(object? obj), GetHashCode(), and provides high-speed operators:
public bool Equals(Rect other)
{
return X == other.X && Y == other.Y && Width == other.Width && Height == other.Height;
}
public static bool operator ==(Rect left, Rect right)
{
return left.Equals(right);
}
public static bool operator !=(Rect left, Rect right)
{
return !left.Equals(right);
}
These overloads compile down to direct float comparison instructions, achieving zero-allocation, ultra-fast boundary checks.
To allow graphics and layout benchmarks to be evaluated at their true physical limit, we disabled vertical synchronization (VSync) throttling across all layers of the GPU pipeline:
options.VSync = false;
WgpuContext.ConfigureSwapChain(), the surface capabilities of the GPU adapter are queried. If PresentMode.Immediate is supported, the swapchain present configuration bypasses synchronization lockups:
PresentMode presentMode = PresentMode.Fifo; // Fallback VSync
for (uint i = 0; i < capabilities.PresentModeCount; i++)
{
if (capabilities.PresentModes[i] == PresentMode.Immediate)
{
presentMode = PresentMode.Immediate; // VSync Off
break;
}
}
This enables the graphics swapchain to present frames as quickly as the GPU queue is filled, releasing the 60 FPS constraint and allowing framerates to soar into the hundreds or thousands of FPS.
The LOL/s benchmark stresses the visual framework by constantly removing and adding hundreds of poolable text controls to a canvas using a background thread loop.
AddChild or property changes) to the main thread's dispatcher loop as fast as possible without throttling, it will quickly overflow the main thread's event queue. The main thread then spends entire frame cycles acquiring queue locks to process actions, creating massive lock contention that completely starves the UI thread and freezes the application.PendingCount property to the main UIThread queue. The background benchmark thread loops continuously without fixed sleep periods, but monitors queue occupancy:
flowchart TD
Start["Background Task Loop"] --> CheckBackpressure{"UIThread.PendingCount > 100?"}
CheckBackpressure -- Yes --> Sleep["Thread.Sleep 1ms / Release Monitor Locks"]
Sleep --> Start
CheckBackpressure -- No --> Post["Post Action immediately / No Sleep"]
Post --> UIThread["UIThread.RunPending - Main Thread drains queue"]
UIThread --> AddChild["AddChild/RemoveChild visual tree mutation"]
Loading
1ms. This releases the queue monitor lock completely and relinquishes the CPU slice, allowing the main UI thread to drain the event queue with zero lock contention. The application remains 100% responsive and immune to livelocks.In real-time GPU-based vector rendering, compiling high-level primitives (such as Rectangles, Ellipses, Rounded Rectangles, Paths, Lines, and Bezier curves) into dynamic vertex and index buffers is a major CPU bottleneck. Standard implementation using sequential .Add(...) calls on List<T> invokes continuous bounds checks, potential array resizing/reallocations, and element copying overhead.
To maximize throughput, the Compositor is optimized using high-performance Span<T> memory writes:
CollectionsMarshal.SetCount(list, newCount) to avoid iterative dynamic reallocation/growth logic inside .NET's List<T>.CollectionsMarshal.AsSpan(list).Slice(offset, count).Span<T> or pre-filled using Span.Fill(defaultValue) for uniform values.Span<VectorVertex> reference, bypassing indexed list getters.int originalVertexCount = _vectorVerticesList.Count; int vertexToAdd = 2 * (N + 1); CollectionsMarshal.SetCount(_vectorVerticesList, originalVertexCount + vertexToAdd); var vertexSpan = CollectionsMarshal.AsSpan(_vectorVerticesList).Slice(originalVertexCount, vertexToAdd); vertexSpan.Fill(baseVertex);
This ensures that the mesh compiler achieves zero-allocation dynamic buffer construction, minimal instruction-level overhead, and runs at near-native C-speed.
In traditional UI and vector engines, every active visual element in an animation loop is modeled as a heap-allocated class object. During high-count stress tests (such as the MotionMark benchmark rendering thousands of dynamically moving curves), these allocations put immense pressure on the .NET Garbage Collector (GC), leading to periodic micro-stutters and frame drops.
ProGPU eliminates this overhead using densely stored value types, retained geometry, and explicit pre-render scheduling:
Element and GridPoint value types, avoiding one managed object allocation per segment:
public struct Element
{
public SegmentKind Kind;
public GridPoint Start;
public GridPoint Control1;
public GridPoint Control2;
public GridPoint End;
public Vector4 Color;
public float Width;
public bool Split;
public SolidColorBrush CachedBrush;
public Pen CachedPen;
public PathGeometry CachedPath;
}
PathGeometry; steady frames submit the same geometry references through the public DrawingContext.DrawPath API instead of allocating paths and segment objects in OnRender.PathGeometry/PathFigure containers and retained segment references for each split-delimited group, reducing command compilation without bypassing the renderer.IAnimatedElement, so the normal sample-tree update traversal advances it every frame while it is attached. Update(delta) mutates split state and invalidates before compositor compilation; OnRender is side-effect free. Pointer movement is no longer needed to keep the benchmark active.0.3 * N * delta toggles. Selecting K due toggles is O(K); rebuilding split-delimited retained groups before rendering is O(N + G) for N segments and G groups, and OnRender then records only O(G) grouped commands without mutating geometry. Persistent geometry storage is O(N); the retained group pool and end-index table are O(Gmax), bounded by N, after warmup. Pens, brushes, HUD strings, and path containers are refreshed only when their owning settings, theme, geometry, or viewport change.Root version changed cache miss on every animated frame.Standard graphics engines struggle to apply dynamic blurred effects (such as Gaussian backdrop blurs, soft ambient drop shadows, and neon glowing halos) to standard layout elements in real-time due to high composition and memory transfer overhead. ProGPU overcomes this with a multi-pass offscreen composition and compute processing system.
graph TD
Subtree["Subtree Render Pass"] -->|Draw Elements 1x MSAA| Src["Source Offscreen Texture"]
Src -->|Horiz. Dispatch| HCompute["Gaussian Blur Compute Shader Pass 1"]
HCompute -->|Vert. Dispatch| VCompute["Gaussian Blur Compute Shader Pass 2"]
VCompute -->|Output Framebuffer| Dest["Destination Blurs/Shadows Texture"]
Dest -->|Matrix Align and Z-Order Bind| Framebuffer["Primary Swapchain Framebuffer"]
Loading
Source, Temp, and Destination buffers) are cached per-element in a specialized dictionary (_effectTextures). They are dynamically resized only when the element's actual visual bounds mutate, eliminating frame-by-frame allocation/deallocation thrashing._vectorPipelineOffscreen, _textPipelineOffscreen, _texturePipelineOffscreen). When an element has an active effect:
Source texture using an isolated orthographic projection matrix.Source texture and executes a horizontal-pass WGSL compute shader, writing intermediate results to the Temp texture. It then binds the Temp texture to execute a vertical-pass compute shader, outputting the final blurred mask to the Destination texture.Source texture is composited cleanly on top, maintaining crisp bounds.To bypass CPU bottlenecks (e.g. flattening Bezier curves into thousands of lines and performing heavy triangulation), ProGPU integrates a pure GPU-bound vector path rasterizer. The engine computes vector fills analytically directly inside custom WebGPU WGSL compute shaders.
To satisfy WebGPU/WGSL uniform and storage buffer packing requirements, layout metrics are organized into sequentially packed structs matching exact 16-byte memory alignments:
[StructLayout(LayoutKind.Sequential, Pack = 16)]
public struct PathUniforms
{
public float XStart; public float YStart;
public float Scale; public uint PathIndex;
public uint AtlasX; public uint AtlasY;
public uint Width; public uint Height;
}
[StructLayout(LayoutKind.Sequential, Pack = 16)]
public struct GpuPathRecord
{
public uint StartSegment; public uint SegmentCount;
public float MinX; public float MinY;
public float MaxX; public float MaxY;
public uint Pad0; public uint Pad1;
}
[StructLayout(LayoutKind.Sequential, Pack = 16)]
public struct GpuPathSegment
{
public Vector2 P0; public Vector2 P1;
public Vector2 P2; public Vector2 P3;
public uint SegmentType; public uint Pad0;
public uint Pad1; public uint Pad2;
}
The rasterizer counts curve intersections analytically using a horizontal ray casting winding-number algorithm directly in WGSL:
solve_cubic helper in WGSL) to find up to 3 real roots, updating the winding number according to the cubic tangent derivative:
$$P'_y(t) = 3 a t^2 + 2 b t + c$$
_pathGeometryCache): Compiled segment arrays and pre-calculated local bounds are cached for each unique PathGeometry. Dynamic frames skip CPU figures traversal, and copy segment spans directly, reducing CPU path compilation times to 0.30ms for 100,000 shapes.if (px < inst.screenMinX || px > inst.screenMaxX || py < inst.screenMinY || py > inst.screenMaxY) {
continue;
}
Pixels outside the shape boundaries immediately bypass local coordinate transforms, 4-sample subpixel loops, and expensive winding calculations. This discards ~95% of active operations per pixel, resulting in a 15x rendering speedup.PathRasterizerShader. This ensures that under high multisampling/supersampling, anti-aliased edge pixels align perfectly, delivering sharp, hardware-accurate vector strokes and fills.Standard Signed Distance Field (SDF) rendering often clips the outer half of strokes or the edges of anti-aliasing gradients because the generated quad boundaries are drawn exactly at the shape's mathematical dimensions. This limits pixel operations outside the bounding box, resulting in a rough, aliased border.
To achieve state-of-the-art vector quality with zero performance degradation, we implemented a dual-stage quad inflation and pixel-distance anti-aliasing framework:
Compositor.cs, drawing of Rectangles, Ellipses, and Rounded Rectangles is divided into independent Brush (fill) and Pen (stroke) passes.
texCoord offset variables outwards by a padding of 1.5 pixels.texCoord offsets by thickness / 2.0 + 1.5 pixels.
This expansion guarantees that the outer half of a stroke of width $T$, as well as its smooth anti-aliasing gradient, are fully rendered without quad boundary clipping.gridIndex. The fragment shader evaluates anti-aliasing dynamically using:
let d_pixels = abs(input.gridIndex); let d_shape = d_pixels - input.strokeThickness * 0.5; shapeAlpha = 1.0 - smoothstep(-0.5, 0.5, d_shape);This calculates a crisp, subpixel-accurate smoothstep edge transition directly in screen-space pixel coordinates, eliminating aliased jagged edges on all lines and splines.
ProGPU implements a lightweight, high-performance, and memory-safe theming, styling, and templating engine designed to emulate the logical capabilities of WinUI 3 but operating with minimal CPU and memory overhead.
flowchart TD
Reg["DependencyProperty.Register"] -->|Sequential Indexing| DP["Index-Based Property Mapped Arrays"]
DP -->|Precedence Resolution| GetVal["O1 GetValue Precedence Sweep"]
Theme["ThemeManager.ThemeChanged"] -->|Lazy Invalidation| Dirty["Set IsThemeDirty = true"]
Dirty -->|On-Demand Query| GetVal
subgraph Storage ["O(1) Parallel Contiguous Value and Theme Arrays"]
Local["_localValues"]
Style["_styleValues"]
DStyle["_defaultStyleValues"]
LocalTheme["_localThemeResources"]
StyleTheme["_styleThemeResources"]
DStyleTheme["_defaultStyleThemeResources"]
end
Loading
Traditional XAML frameworks store DependencyObject property values in heavy dictionaries (Dictionary<DependencyProperty, object>), which trigger expensive hash calculation, collisions, and lookup overhead inside tight render or layout loops.
ProGPU bypasses dictionaries entirely by introducing sequential indexing:
DependencyProperty is assigned a unique, sequential, zero-based Index from a thread-safe static list during bootstrap.DependencyObject stores properties in a set of parallel contiguous flat arrays (_localValues, _styleValues, _defaultStyleValues, _effectiveValues, and _valueSources) matching the index sizes.GetValue(dp)) is simplified to direct index checks on these arrays in $O(1)$ time, resolving values via native priority precedence:
$$\text{Local} \succ \text{Style} \succ \text{Default Style} \succ \text{Inherited} \succ \text{Default}$$
Eagerly traversing and updating dynamic brushes across the entire visual tree on every theme change triggers substantial CPU frame stutters. ProGPU bypasses this via a lazy evaluation pipeline:
ThemeManager.ThemeChanged fires. The system recursively propagates a cheap IsThemeDirty = true flag down the scene graph (NotifyThemeChanged), avoiding immediate value updates._localThemeResources, _styleThemeResources, and _defaultStyleThemeResources). During subsequent property reads (GetValue(dp)), if the dirty flag is set, the system sweeps these parallel arrays, re-evaluates active key lookups against the theme palette, and rebuilds only the affected elements' effective values in a single sequential linear pass.To support lightweight control customization without the heavy reflection, expression compilation, or string-matching of traditional bindings:
DependencyObject maintains an index-sequential list of callbacks registered via RegisterPropertyChangedCallback(dp, callback).long token, allowing surgical unregistration via UnregisterPropertyChangedCallback(dp, token).Templatmore like this
SCYTHE is a lightweight, C#-based game engine focused on modifiability and rapid iteration using Raylib.
.Net 10 (.Net 8+) webassembly starter project using raylib-cs nuget 7.0.2 and raylib 5.5
Provides the ability to show various dialogs and child windows in a DI injectable application dialog service ready to p…
search projects, people, and tags