GlavaSharp is a from-scratch, C#/.NET reimplementation of GLava's
rendering model: an audio-reactive OpenGL visualizer driven by chains of
numbered GLSL fragment shaders, configured through GLava's own rc.glsl /
module directory convention. It captures system audio via PipeWire, runs an
FFT, and feeds the resulting spectrum into your choice of shader module
(bars, radial, etc.) as a pair of 1D textures.
Status: early alpha. It runs, it renders, and the core pipeline works, but this is not yet a polished GLava replacement — see Status & known issues below before you file a bug about a broken module.
GlavaSharp is an independent reimplementation and is not affiliated with or endorsed by the GLava project. It exists because GLava's shader ecosystem (the module/
rc.glsl/#requestconvention) is genuinely well designed, and it seemed worth having that ecosystem sitting on top of a different, more portable host.
- Why this exists
- How it compares to GLava
- Status & known issues
- Architecture
- High-level pipeline
- Project layout
- Audio capture (
Audio/+native/pwshim/) - FFT (
Shaders/CpuFft.cs,Shaders/GpuFft.cs) - GPU selection (
GpuEnumerator.cs) - Shader preprocessing (
Shaders/GlavaPreprocessor.cs) - Shader module pipeline (
Shaders/ShaderModule.cs) - Windowing (
Windowing/AppWindow.cs) - Configuration (
Shaders/RcConfig.cs)
- Design trade-offs
- Roadmap
- Building
- Running
- License
GLava is a mature, well-designed C project: a small preprocessor layered
over GLSL, a module system where a visualizer is just a numbered stack of
.frag passes, and a config format (rc.glsl + #request directives) that
lets you reconfigure behavior without touching the host program at all.
That design is worth keeping. What this project changes is everything
underneath it:
- a memory-safe, garbage-collected host (C#) instead of hand-rolled C,
- a portable windowing/GL layer (GLFW via OpenTK) instead of directly targeting Xlib,
- a sandboxed, memory-safe native audio backend (Rust + PipeWire) instead of linking libpulse directly into the main process,
- a single statically-linked Native AOT executable as the distributable artifact, rather than a dynamically linked C binary plus shared config/ module directories.
None of that changes what a module author sees: rc.glsl and the module
.frag files are still ordinary GLava shader source. GlavaSharp reuses
GLava's actual shader tree (bundled under src/GlavaSharp/shaders/glava/)
essentially unmodified.
| GLava | GlavaSharp | |
|---|---|---|
| Host language | C | C# (.NET, Native AOT) |
| Windowing/GL | Xlib directly (GLFW on the unstable branch) |
GLFW via OpenTK, always |
| Display server support | X11 only | X11 and Wayland (GLFW's PlatformPreference.Any picks whichever the session is running) |
| Audio backend | PulseAudio (libpulse), linked into the main process | PipeWire, isolated in a separate Rust static library behind a tiny FFI shim |
| Shader preprocessor | Full custom C preprocessor (#request, #include, #expand, @fg:/@bg: compositing, GLava's transform pipeline for FFT/window/gravity/avg as chained shaders) |
A deliberately small subset (see below) — enough to load real GLava module files, not a full reimplementation of every directive |
| FFT | Runs as GLava's own chained compute-shader "transform" passes (window → fft → gravity → avg) on the GPU |
Two interchangeable backends, selected with --fft-device: a CPU FFT (CpuFft, the default) and a single-workgroup GLSL compute-shader FFT (GpuFft) that's bit-for-bit equivalent to it — see FFT |
| GPU selection | N/A | --list-gpus enumerates DRM render nodes; --gpu <index> pins rendering to one — see GPU selection |
| Distributable artifact | Dynamically linked binary + installed shader/config tree under /etc/xdg or ~/.config/glava |
Single self-contained Native AOT executable with the Rust audio shim statically linked in; shader tree ships alongside it, not installed system-wide (yet) |
Desktop-embedded mode (glava -d / setxwintype "desktop") |
Supported, X11 EWMH-based | Not yet implemented — planned, GNOME first (see Roadmap) |
| Module maturity | All bundled modules (bars, radial, circle, graph, wave, ...) are production-quality | Only bars and radial are verified working; circle, graph, and wave render but have known bugs (see Status) |
| Build system | Meson (2.x) / legacy Makefile (1.x) | CMake orchestrating cargo + dotnet publish |
The short version: GlavaSharp is GLava's shader-facing design ported onto
a different, more memory-safe, cross-compositor host stack. It is not a
drop-in replacement, doesn't read GLava's installed config paths, and is
missing GLava features (desktop embedding, most of the #request surface,
IPC/pipe control) that GLava has had for years.
This is early alpha software.
- Working:
barsandradialmodules render correctly against live PipeWire audio, on both X11 and Wayland sessions. - Buggy / not working yet:
circle,graph, andwavecompile and run but produce incorrect or broken output — they haven't been debugged against GlavaSharp's smaller preprocessor yet and may be relying on#request/compositing behavior that isn't implemented (see Shader preprocessing). Treat them as "known incomplete," not "regressions to report." GpuFftis implemented and working. The GPU compute-shader FFT (Shaders/GpuFft.cs) is a complete, from-scratch radix-2 Cooley-Tukey implementation, architecturally in the same spot GLava's ownfft_radix*.glslcompute kernels occupy, and produces the same spectrum asCpuFft(windowing, bit-reversal, twiddle math, and log-compressed normalization all match). Two driver-level issues surfaced during bring-up and are now worked around/fixed:glCompileShader/glLinkProgramcould hang on at least one real driver stack (Mesa/Intel iris) with no error reported whenLOGNwas baked in as a compile-time constant, since the driver's NIR unroller would try to fully unroll the stage loop and scalar-replace the per-invocationsharedarrays. KeepingLOGN(and the other tunables) as auniforminstead of a constant avoids this.- The
u_logNuniform is declareduintin GLSL but was being uploaded with the signed-int GL entry point (glUniform1iinstead ofglUniform1ui); on drivers that enforce the spec's type-matching rule this left the uniform at0, silently skipping every FFT stage and producing time-domain noise instead of a spectrum. Fixed by uploading it through the correct unsigned overload.CpuFftremains the default backend (--fft-device cpu); pass--fft-device gputo useGpuFftinstead. See FFT below.
- No desktop-embedded mode. GLava's
-dflag /setxwintype "desktop"behavior (rendering pinned behind desktop icons, EWMH-managed) has no equivalent yet. - No installed config tree. GlavaSharp reads
shaders/glava/next to the executable (or wherever--shaderspoints); it doesn't yet look in~/.config/glavasharpthe way GLava looks in~/.config/glava.
If you hit something outside this list, it's a real bug — please file an
issue with the --list-gpus/--list-sinks output and the module you were
running.
PipeWire "what you hear" monitor
│ (Rust: native/pwshim, staticlib, statically linked via Native AOT)
▼
PipeWireAudioSource (Audio/) ──▶ RingBuffer ──▶ AudioWindow (tail buffer)
│
▼
IFft.Process() (Shaders/CpuFft.cs or GpuFft.cs,
per --fft-device) windowed FFT →
log-compressed, gravity-smoothed
magnitude spectra (left, right)
│
▼
AudioSpectrumTexture × 2 (1D R32F textures)
│
▼
ShaderModule (GLava module dir: 1.frag, 2.frag, ...)
each pass: fullscreen triangle, samples audio_l/audio_r + tex0
(previous pass's output), renders to ping-pong FBOs, last pass
to the default framebuffer
│
▼
AppWindow: GLFW SwapBuffers
Every frame (AppWindow.Run): pump whatever new PCM PipeWire has produced
into the ring buffer, run one FFT over the most recent window, upload the
two resulting spectra as 1D textures, run the active module's pass chain,
swap buffers.
GlavaSharp/
├── GlavaSharp.slnx solution file (new XML .slnx format)
├── CMakeLists.txt orchestrates: cargo build (native/pwshim) → dotnet publish (AOT)
├── .github/workflows/ci.yml CI: rust job, dotnet job, full AOT integration job
├── src/
│ └── GlavaSharp/ the .NET project — all C# source lives here
│ ├── GlavaSharp.csproj
│ ├── Program.cs CLI parsing, wiring, entry point
│ ├── FftSettings.cs
│ ├── GpuEnumerator.cs --list-gpus / --gpu N (DRI_PRIME etc.)
│ ├── Audio/ PipeWire capture (P/Invoke into native/pwshim)
│ ├── Shaders/ FFT, shader preprocessing, module pass pipeline
│ ├── Windowing/ GLFW window/context/frame-loop
│ └── shaders/glava/ GLava's own shader tree, bundled as-is
└── native/
└── pwshim/ standalone Rust crate — NOT nested in the
C# project, since it isn't C# code and has
its own independent build/test lifecycle
├── Cargo.toml
└── src/lib.rs PipeWire stream capture, exposed via a
small extern "C" FFI surface
native/pwshim/ living outside src/GlavaSharp/ (rather than e.g. a
native-rs/ folder nested inside the C# project, which is how this
started out) is deliberate: it's a fully independent build unit with its
own Cargo.lock, its own CI job, and its own release cadence — nesting it
inside the .NET project directory implied an ownership relationship that
doesn't reflect how the two are actually built, versioned, or tested.
PipeWire has no first-class C# bindings, and hand-writing raw P/Invoke
signatures against PipeWire's C API (which leans heavily on callbacks,
spa_pods, and manual memory management) is exactly the kind of surface
where memory-safety bugs live. Instead:
native/pwshim(Rust, crate namepwshim) uses thepipewirecrate to open a capture stream on the default sink's monitor (or a specific node, for--sink/--list-sinks), and exposes a minimalextern "C"surface:pwshim_start,pwshim_stop,pwshim_list_targets,pwshim_free_string. All the PipeWire-specific complexity (stream negotiation, format callbacks, buffer lifetime) stays inside Rust, where the type system and borrow checker actually catch misuse.Audio/PipeWireNative.csis the thinLibraryImport(source-generated P/Invoke) layer over that FFI surface.Audio/PipeWireAudioSource.cswraps it inIAudioSource, GlavaSharp's own abstraction, so the rest of the app (AudioWindow,CpuFft, ...) never touches PipeWire types directly.Audio/RingBuffer.cs+Audio/AudioWindow.cs: the ring buffer is a destructive read cursor fed by the native callback thread;AudioWindowsits on top to keep a fixed-size tail of the most recent N interleaved stereo frames available to the render thread every frame, even when a given frame produces fewer new samples than the FFT window needs.- Built as
crate-type = ["staticlib"]and statically linked into the final Native AOT executable via<NativeLibrary>+<DirectPInvoke Include="pwshim"/>in the.csproj
Both FFT backends implement the same IFft interface and are
interchangeable at runtime via --fft-device {cpu,gpu} (FftSettings.Device);
AppWindow doesn't care which one it got.
CpuFft is an iterative radix-2 Cooley-Tukey FFT with precomputed
bit-reversal and twiddle-factor tables, a Hann window, and gravity
smoothing (fast attack, slow decay — the same feel as GLava's
util/gravity_pass.frag). It runs entirely on the CPU and uploads the
resulting spectra as textures every frame. It's the default backend.
GpuFft is the "real" architectural target: a single-workgroup GLSL 4.3
compute shader doing the same radix-2 transform on the GPU, matching the
spot GLava's own fft_radix*.glsl kernels occupy, so the CPU only feeds
windowed PCM in via SSBOs and reads magnitude bins back out. It's a
bit-for-bit-equivalent port of CpuFft: same Hann window, same
bit-reversal permutation, same iterative stage loop (both channels'
butterflies share the same per-stage twiddle math, so they run together
in one loop rather than as two passes), and same normalization/log
-compression formula. Only the windowing/bit-reversal (trivial,
memory-bound) and the gravity smoothing (inherently serial across frames)
stay on the CPU; the O(N log N) butterfly work happens on the GPU. It's
opt-in today (pass --fft-device gpu) while it gets more mileage across
different GPU vendors/drivers — see
Status & known issues
--list-gpus enumerates the system's DRM render nodes and prints an
indexed list of the GPUs GlavaSharp can render on:
Available GPUs (use --gpu <index>):
[0] AMD (pci id 0x1002:0x73df, driver amdgpu) [card0]
[1] Intel (pci id 0x8086:0x4680, driver i915) [card1]
Each entry shows the vendor, PCI device ID, kernel driver, and DRM card
node backing that index. Pass the index to --gpu <index> to pin
rendering to that GPU (useful on hybrid-graphics laptops where the
default render node isn't the one you want driving the visualizer).
--gpu affects which GPU renders the window/shader pipeline; it's
independent of --fft-device gpu, which only controls where the FFT
itself runs.
GLava's own preprocessor is a full custom language extension: it handles
#request (configuring the host and defining values shaders read back),
#include (with @ = module-relative and : = shader-root-relative
paths), #expand, hex-color literals, and a @fg:/@bg:
foreground/background compositing model with a dedicated blending pass.
GlavaPreprocessor implements a deliberately small subset — enough to
load real, unmodified GLava module files as plain GLSL:
#include "@x"/#include ":x"— resolved and inlined recursively, deduplicated per top-levelProcess()call (so re-including the same file, which GLava's own shaders do routinely, e.g.bars.glslvia both@and:paths inbars/1.frag, doesn't redefine macros), capped at depth 32 as a sanity backstop against genuine include cycles.#request setsmoothfactor <n>/#request setsmoothpass <bool>— turned into#defines, becauseutil/smooth.glslreads them back as plain GLSL identifiers (_SMOOTH_FACTOR,_PRE_SMOOTHED_AUDIO). Every other#requestline is stripped as a no-op.#expand NAME COUNT→NAME(0) NAME(1) ... NAME(COUNT-1), one call per line, whenCOUNTis already a literal integer.#RRGGBB[AA]hex-color literals →vec4(...).- The
@fg:/@bg:tags are stripped rather than driving a real compositing pass — GlavaSharp just draws the resulting color with normal alpha blending. - A redundant
in vec4 gl_FragCoord;redeclaration (legacy GLSL-version compat in GLava's own shaders) is stripped, since core-profile GLSL already declares it implicitly and strict drivers reject the redeclaration.
What's explicitly not implemented: GLava's #request transform ...
pipeline (chaining window/fft/gravity/avg as GPU shader passes —
GlavaSharp does windowing/FFT/gravity natively in CpuFft/GpuFft
instead) and the full compositing model behind @fg:/@bg:. This is the
most likely source of the circle/graph/wave bugs noted in
Status: those modules may lean on #request
directives or compositing behavior this preprocessor silently drops
instead of honoring.
A GLava "module" is a directory of numbered fragment passes (1.frag,
2.frag, ...). ShaderModule loads them in order, wraps each in a shared
trivial vertex shader (a fullscreen triangle, no vertex buffer needed),
and compiles/links each into its own program. A pass containing GLava's
#error __disablestage sentinel (e.g. bars/2.frag is a no-op unless
USE_ALPHA=1) is recognized and skipped rather than treated as a compile
failure, and its predecessor's output passes straight through to the next
real pass.
At render time, passes ping-pong between two offscreen FBOs (_fboA/
_fboB), each one receiving the previous enabled pass's output as
tex0, plus the two audio spectrum textures as audio_l/audio_r. The
last enabled pass (not necessarily the last file — a trailing disabled
pass shouldn't swallow the real output) renders directly to the default
framebuffer.
A deliberately thin GLFW wrapper — not OpenTK's GameWindow — so init
hints, platform selection, and the frame loop are all explicit and
GLava-shaped rather than inherited from a general-purpose game-engine
loop. PlatformPreference.Any lets GLFW pick Wayland when running inside
a Wayland session and fall back to X11 otherwise, which is what actually
gives GlavaSharp both-compositor support — GLava's mainline branch talks
to Xlib directly and is X11-only (its unstable branch has experimented
with GLFW too, for the same reason).
A tiny reader for the handful of top-level rc.glsl values GlavaSharp
actually acts on today (module name, window title/size, FFT buffer size,
sample rate) — not a general #request interpreter. Most of rc.glsl's
directive surface (window decoration/floating/opacity hints, geometry,
setxwintype, etc.) is parsed by nothing yet and simply has no effect.
- CPU FFT as the default, GPU FFT available as opt-in.
GpuFftis a real, working, bit-for-bit-equivalent GPU implementation (see FFT), but it's newer and has already surfaced two driver-level gotchas (a compile hang, a uniform type mismatch) during bring-up on a single machine.CpuFftstays the default untilGpuFfthas more mileage across GPU vendors/drivers; pass--fft-device gputo try it. - A small preprocessor subset instead of a full GLava-language
reimplementation. Enough to run real, unmodified GLava shader files
for the common cases, at the cost of some modules (relying on the
#request transformchain or full@fg:/@bg:compositing) not working yet. Reimplementing 100% of GLava's preprocessor was judged not worth doing before validating the rest of the pipeline end to end. - Rust for the audio backend instead of direct P/Invoke onto PipeWire's
C API. Adds a second toolchain and a second CI job, in exchange for
keeping the trickiest, most callback-heavy native surface in a language
that can actually check it.
native/pwshimis deliberately narrow — start/stop/list/read, nothing else — specifically so this trade stays worth it rather than becoming "half the app is now in Rust." - Native AOT + static linking instead of a dynamically linked
executable. Produces self-contained binary and no shared shader/config install step (yet), at the cost of AOT's
usual constraints (no runtime codegen, trimming-sensitive reflection —
hence
EnableTrimAnalyzer/EnableAotAnalyzeras build errors, not warnings, in the.csproj) and a build pipeline that requires both the Rust and .NET toolchains present together at publish time, orchestrated by CMake rather thandotnet publishalone. - GLFW via OpenTK instead of talking to Xlib/Wayland directly. Costs a dependency, buys X11 and Wayland support from day one instead of committing to one compositor API, which is also why this is one of the two concrete feature wins over upstream GLava's mainline branch.
- Validate
GpuFftacross more GPU vendors/drivers and switch the default FFT path (--fft-device) to GPU once it has enough real-world mileage. - Debug
circle,graph, andwaveagainst the current preprocessor; extendGlavaPreprocessor/RcConfigas needed rather than assuming the modules themselves are at fault. - Desktop-embedded rendering mode, equivalent to GLava's
-d/setxwintype "desktop". Initial target is GNOME only; other desktop environments (KDE, Sway, etc.) after that, contributions welcome. - Installed config path (
~/.config/glavasharp), mirroring GLava's~/.config/glavaconvention, instead of only readingshaders/next to the executable.
Requires: .NET 10 SDK, a Rust toolchain (via rustup),
clang/libclang-dev (for PipeWire's bindgen-based bindings), and
libpipewire-0.3-dev. On Ubuntu/Debian:
sudo apt install libpipewire-0.3-dev pkg-config clang libclang-dev cmakeThen:
cmake -S . -B build
cmake --build build
# -> build/dist/GlavaSharpTo clean everything, including dotnet's obj/bin and cargo's target/
(not just CMake's own build/ directory):
cmake --build build --target clean-allYou can also build each side independently:
# Rust shim only
./native/pwshim/build.sh
# .NET project only (plain build, no AOT, doesn't need the Rust lib at all)
dotnet build GlavaSharp.slnx./build/dist/GlavaSharp # default sink monitor, bars module from rc.glsl
./build/dist/GlavaSharp --list-sinks # see capture targets
./build/dist/GlavaSharp --list-gpus # see DRM render nodes (for --gpu)
./build/dist/GlavaSharp --gpu 1 # render on GPU index 1 from --list-gpus
./build/dist/GlavaSharp --fft-device gpu # run the FFT on the GPU instead of the CPU
./build/dist/GlavaSharp --module radial # force a specific moduleSee the top of src/GlavaSharp/Program.cs for the full CLI flag reference
(--shaders, --gpu, --fft-device, --fft-size, --fft-attack/
--fft-decay/--fft-gain, --sample-rate).
This project is licensed under the MIT License. See the LICENSE file for the full license text.
The bundled shader tree under src/GlavaSharp/shaders/glava/ originates from GLava and remains subject to its own license. See the original GLava project for the licensing terms that apply to those files.