Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

8 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

GlavaSharp

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/#request convention) is genuinely well designed, and it seemed worth having that ecosystem sitting on top of a different, more portable host.

image image

Table of contents


Why this exists

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.

How it compares to GLava

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 (windowfftgravityavg) 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.

Status & known issues

This is early alpha software.

  • Working: bars and radial modules render correctly against live PipeWire audio, on both X11 and Wayland sessions.
  • Buggy / not working yet: circle, graph, and wave compile 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."
  • GpuFft is 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 own fft_radix*.glsl compute kernels occupy, and produces the same spectrum as CpuFft (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/glLinkProgram could hang on at least one real driver stack (Mesa/Intel iris) with no error reported when LOGN was 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-invocation shared arrays. Keeping LOGN (and the other tunables) as a uniform instead of a constant avoids this.
    • The u_logN uniform is declared uint in GLSL but was being uploaded with the signed-int GL entry point (glUniform1i instead of glUniform1ui); on drivers that enforce the spec's type-matching rule this left the uniform at 0, silently skipping every FFT stage and producing time-domain noise instead of a spectrum. Fixed by uploading it through the correct unsigned overload. CpuFft remains the default backend (--fft-device cpu); pass --fft-device gpu to use GpuFft instead. See FFT below.
  • No desktop-embedded mode. GLava's -d flag / 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 --shaders points); it doesn't yet look in ~/.config/glavasharp the 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.

Architecture

High-level pipeline

 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.

Project layout

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.

Audio capture (Audio/ + native/pwshim/)

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 name pwshim) uses the pipewire crate to open a capture stream on the default sink's monitor (or a specific node, for --sink/--list-sinks), and exposes a minimal extern "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.cs is the thin LibraryImport (source-generated P/Invoke) layer over that FFI surface.
  • Audio/PipeWireAudioSource.cs wraps it in IAudioSource, 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; AudioWindow sits 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

FFT (Shaders/CpuFft.cs, Shaders/GpuFft.cs)

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

GPU selection (GpuEnumerator.cs)

--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.

Shader preprocessing (Shaders/GlavaPreprocessor.cs)

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-level Process() call (so re-including the same file, which GLava's own shaders do routinely, e.g. bars.glsl via both @ and : paths in bars/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, because util/smooth.glsl reads them back as plain GLSL identifiers (_SMOOTH_FACTOR, _PRE_SMOOTHED_AUDIO). Every other #request line is stripped as a no-op.
  • #expand NAME COUNTNAME(0) NAME(1) ... NAME(COUNT-1), one call per line, when COUNT is 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.

Shader module pipeline (Shaders/ShaderModule.cs)

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.

Windowing (Windowing/AppWindow.cs)

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).

Configuration (Shaders/RcConfig.cs)

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.

Design trade-offs

  • CPU FFT as the default, GPU FFT available as opt-in. GpuFft is 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. CpuFft stays the default until GpuFft has more mileage across GPU vendors/drivers; pass --fft-device gpu to 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 transform chain 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/pwshim is 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/EnableAotAnalyzer as 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 than dotnet publish alone.
  • 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.

Roadmap

  • Validate GpuFft across more GPU vendors/drivers and switch the default FFT path (--fft-device) to GPU once it has enough real-world mileage.
  • Debug circle, graph, and wave against the current preprocessor; extend GlavaPreprocessor/RcConfig as 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/glava convention, instead of only reading shaders/ next to the executable.

Building

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 cmake

Then:

cmake -S . -B build
cmake --build build
# -> build/dist/GlavaSharp

To clean everything, including dotnet's obj/bin and cargo's target/ (not just CMake's own build/ directory):

cmake --build build --target clean-all

You 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

Running

./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 module

See 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).

License

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.

About

GLavaShaprt - OpenGL audio spectrum visualizer In C# and Rust !

Topics

Resources

Stars

Watchers

Forks

Used by

Contributors

Languages