Best practices for building a fast, robust and maintainable real-time application in C# / .NET — and the kind of libraries worth reaching for along the way.
This is guidance, not a rulebook. The principles below apply to most real-time, media-rich desktop apps; adapt them to your project. C# / .NET has a huge ecosystem, so treat the libraries listed here as a useful starting point — there are many alternatives you can integrate to suit your needs.
Start on solid foundations
Target a current runtime (.NET 9) and pin your dependencies so builds are reproducible.
Pick one UI framework and commit to it — a cross-platform toolkit like Avalonia keeps your options open, though native hardware interop will usually tie the shipping build to Windows.
Separate intent from work: the UI captures what the user wants; a dedicated loop does the heavy lifting. A clear, one-directional data flow is worth more than any clever abstraction.
Keep the hot path boring. Prefer plain data and simple functions over deep inheritance and per-frame allocations.
Design around your data
For anything you touch every frame, favour a data-oriented layout: parallel arrays with a live count, rather than arrays of rich objects. It's cache-friendly, vectorises well, and maps straight onto GPU buffers.
Have one canonical "working set" that everything feeds into — models, text, particles, video and live streams should all synthesise into the same representation and flow through the same effect and lighting passes.
Allocate once and reuse. Grow buffers on demand, never shrink-and-reallocate mid-loop.
Keep the UI responsive
Never block the UI thread. Do rendering and processing on a worker/render thread and let the UI stay interactive.
Share state through one object that the UI writes and the worker reads, and avoid locks on the hot path — a mix of volatile scalars (for values that can lag a frame), atomic reference swaps (so readers never see a half-updated array), and one-shot command flags covers most needs.
Drive continuous input (held keys, sliders) from a polling timer rather than OS key-repeat, so motion feels smooth.
Make it run anywhere
Pair every GPU path with an equivalent CPU fallback so the app still runs with no capable GPU, on a CI machine, or in a headless test. Detect capability once and latch failures off so you don't retry a broken device every frame.
Bind to native SDKs at runtime and treat missing entry points as optional — probe what's available and degrade gracefully rather than crashing.
Always support a "no hardware" mode. An on-screen simulator lets people develop, test and demo without a display attached.
Design for the volume
A volumetric display is comparatively low-resolution. Lean into that — bold shapes, strong colour and clear motion read far better than fine texture detail, and let the viewer's eye fill in the rest.
Show only what should be seen: cull interior points that sit behind the outer surface, so a solid model doesn't reveal its hollow insides.
Keep visual density stable as the subject is scaled or moved, so content doesn't flicker or thin out when the viewer zooms.
Use the device's real-world bounds to size content, so the same scene looks right on both a VX2 and a VX2-XL.
Be disciplined about performance
Don't re-upload or recompute what hasn't changed — use dirty flags and cache results keyed by their inputs.
Batch work. On the GPU, per-call overhead is often the real cost, so pack many arrays into one transfer instead of many small ones.
Know your crossover point: below a certain size, the latency of dispatching to the GPU and reading back is slower than just doing it on the CPU.
Measure before optimising, and keep a lightweight on-screen profiler so regressions are obvious.
Robust interop & I/O
When you shell out to an external tool (for example ffmpeg for encoding), always drain its output streams — an un-read pipe will deadlock the process.
For cross-process/live input, choose one transport (named pipe, memory-mapped file) and make both ends agree on it; a silent mismatch looks like "nothing connects".
Make native structs blittable and explicit about layout, and locate native DLLs via a predictable search chain (app directory → PATH → registry).
Persist data that survives change
Design your save format to be forward- and backward-compatible from day one. A flat key/value document (JSON) is easy to evolve.
Persist enums by a stable ordinal and only ever append — never renumber existing values.
Dual-read renamed keys, tolerate type drift (int vs float), and never overwrite keys owned by another subsystem on save.
Keep user preferences separate from documents, and load them first so a document can override them.
Test without hardware
Make the app testable with no display and no GPU: feed synthetic inputs directly and force the CPU fallback path in tests.
Assert that GPU and CPU paths produce the same result — that's your safety net for the fallback actually working.
Expose internals to your test project (InternalsVisibleTo) rather than loosening real APIs.
Ship a clean build
Prefer a self-contained publish so users don't need a matching runtime installed.
If you trim, watch for anything loaded reflectively or via P/Invoke — reflection-based JSON, COM interop for some audio codecs, and libraries with native glue often need to be preserved explicitly, or they'll fail only in the published build.
Avoid spaces and surprises in your product name and output paths where downstream tooling has to parse them.
Libraries worth knowing
A starting toolkit for real-time, media-rich C# apps. All are optional — pick what fits, and swap in alternatives freely.