Status & Roadmap¶
This is early alpha software. Every item below is a checkbox — checked
items are done (with the full writeup of what the bug/feature actually
was, kept for context), unchecked items are open. If you hit something not
listed here, it's a real bug — please file an issue with the
--list-gpus/--list-sinks output and the module you were running.
Modules¶
- [x]
barsrenders correctly against live PipeWire audio. - [x]
radialrenders correctly against live PipeWire audio. - [x]
circle,graph,waverender correctly. These were broken until a real bug got fixed: every GLava module with a real (non-default-disabled) multi-pass chain declares its previous-pass sampler via#request uniform "prev" tex"— the bundled tree always names ittex, nevertex0— butShaderModule.csbound the previous pass's output to a hardcoded uniform name"tex0", which doesn't exist in any of these shaders, so the sampler was never actually bound and the pass rendered nothing.bars/radialonly have a disabled-by-default second pass (USE_ALPHA/_PREMULTIPLY_ALPHAoff), so this never got exercised there.GlavaPreprocessor.Processnow captures#request uniform "<role>" <name>bindings (not just thesetsmoothfactor/setsmoothpassones it already handled) andShaderModulebinds each pass's previous-output sampler by the name the shader actually declared, falling back to"tex0"only if a pass declares none. - [x]
waterfall(GlavaSharp-original, not part of GLava) — a scrolling spectrogram: the audio spectrum's history over time, color-mapped (blue → cyan → green → yellow → red → white) and falling downward as new data arrives. See GlavaSharp-Original Modules. - [x]
aurora(GlavaSharp-original, not part of GLava) — a calming ambient desktop visualizer: soft curtains of color drift upward and sway like the northern lights, fading into a fully transparent background. Reuses the same persistent "history" feedback buffer aswaterfall, but as a decay+drift loop instead of a hard scroll — no time/clock uniform involved, the motion comes entirely from re-sampling the buffer's own previous frame through a fixed sideways sway each frame. Two bugs found and fixed during initial live testing, both specific to feedback-loop shaders rather than typical GLava modules: (1) the feedback read direction was inverted (sampling above instead of below the current row), which pulled content down into the injection zone instead of letting it rise, so it never visibly drifted; (2) additively combining the decayed feedback with freshly injected energy, inside the injection zone, is an unbounded integrator (steady state ≈injected/(1-DECAY), hundreds of times over at a DECAY this close to 1) that clips straight to white within seconds — switched tomax(prev, injected), which lets new energy refresh the zone without the two terms compounding. See GlavaSharp-Original Modules. - [x]
aurorarewritten into a curl-noise-driven volumetric sim — something GLava's own module format has no path to at all. The original single-sine-sway version above still works exactly as described, but the currentaurora.glsl/1.frag/2.frag/noise.glslgo considerably further: a real (divergence-free) curl-noise flow field with domain warping stands in for the one sine wave, blended across three differently-tuned virtual layers for a parallax look, sampled anisotropically along the local flow direction so feedback reads as transported rather than blurred, separated chromatically for a faint prismatic trailing edge, and thinned along high-curl regions so curtains fray into branching filaments instead of staying one solid sheet — still with zero time/clock uniform anywhere in the pipeline and zero new host-side plumbing: it's still just the onehistorybufferwaterfallalready established, read back through a much richer static field. Every bundled GLava module redraws from scratch every frame with no persistent state at all, and the shipped GLava shader tree has no noise/FBM/curl primitives anywhere in it — this isn't a GLava feature ported over, it's a category of effect GLava's format has no mechanism to express. Full technique breakdown in GlavaSharp-Original Modules. - [x]
clock— an analog clock face (real hour/minute/second hands) drawn over an ordinary audio-reactive radial spectrum.1.fragis#include ":radial/1.frag"and nothing else — the glowing circle+bars areradial's own first pass, reused verbatim rather than copy-pasted, sinceRootDirstays fixed throughout a nested#includeregardless ofclock/'s own directory being resolved via the sibling-module fallback.2.frag's hands are driven by one#request property "seconds_since_midnight"uniform carrying a#request feed "seconds_since_midnight" clockbinding (see Live Control Channel & Hot-Reload below) — proves the feed mechanism end to end: no host code anywhere has a hardcoded notion of "time,"AppWindowjust samples whateverControl/FeedRegistry.cs's"clock"entry returns (DateTime.Now.TimeOfDay.TotalSeconds) into a property that would otherwise be a completely ordinary manually-tunable slider. See GlavaSharp-Original Modules. - [ ] GLava's
#request transform ...pipeline (chainingwindow/fft/gravity/avgas GPU shader passes) isn't implemented — GlavaSharp does windowing/FFT/gravity natively inCpuFft/GpuFftinstead. None of the six current modules need it. A future module that leans on it would show the same symptom class as thetex0/texbug: compiles fine, renders wrong or empty. - [ ] GLava's full
@fg:/@bg:foreground/background compositing model isn't implemented — GlavaSharp strips the tags and just draws the resulting color with normal alpha blending. Not currently needed by any bundled module.
FFT¶
- [x]
GpuFftimplemented 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).- [x]
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. Fixed by keepingLOGN(and the other tunables) as auniforminstead of a constant. - [x] 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.
- [x]
- [ ] Validate
GpuFftacross more GPU vendors/drivers and switch the default FFT path (--fft-device) to GPU once it has enough real-world mileage.CpuFftremains the default (--fft-device cpu); pass--fft-device gputo try it. See FFT & Frequency Bucketing. - [x] Bug found and fixed: bass reads as static/underused, treble as
disproportionately "active," across every module. Root cause:
every module's
util/smooth.glslmaps screen position to a raw, linearly-spaced FFT bin viascale_audio's log-ish warp (-log(1 - SAMPLE_RANGE*idx) / SAMPLE_SCALE). With the stock constants, that warp's slope is nearly flat nearidx=0-- a wide swath of screen space samples nearly the same few bass bins, which reads as static even though that's where the real energy is -- and steep nearidx=1, where adjacent screen positions sample meaningfully different (sparser, noisier) high bins, which reads as "active" from frame-to-frame variance alone regardless of actual magnitude. Confirmed live:bars --freq-scale linear(the old, only, behavior) showed real bar height in roughly the first 5 of ~30 visible bars, everything past that reading as flat, against broadband pink noise. Fixed with proper perceptual bucketing instead of retuning the existing warp's constants: raw FFT bins are now redistributed by actual frequency (Hz), on a user-selectable perceptual scale (--freq-scale log2default, ormel/bark/erb;linearkeeps the old raw-bin behavior), before any shader sees them -- see FFT & Frequency Bucketing for the bucket-edge math and why it lives once, shared by bothCpuFftandGpuFft, rather than in GLSL. Since the redistribution now happens upstream,util/smooth.glsl's own warp has to become a no-op or it'd warp the (already-correctly-spaced) spectrum a second time -- gated behind a new GlavaSharp-original_FREQ_PREBUCKETEDmacro (same injection mechanism as_USE_ALPHA), defaulting to today's exact behavior unless a module was compiled with bucketing active. Verified live:bars --freq-scale log2against the same pink-noise signal showed real, varied bar height across roughly 30 bars instead of 5, and all seven bundled/original modules (bars,radial,circle,graph,wave,waterfall,aurora) still compile+link cleanly across all five--freq-scalevalues.
Desktop-embedded mode¶
- [x] X11/xfwm4 (XFCE) implemented and verified live.
--desktop(or rc.glsl'ssetxwintype "desktop", whichenv_Xfwm4.glslalready requests) pins a transparent-background, click-through window behind desktop icons via EWMH hints — see Desktop-Embedded Mode. Confirmed against a live xfwm4/XFCE session:- [x]
_NET_WM_WINDOW_TYPE_DESKTOPand decoration-stripping (via_MOTIF_WM_HINTS) both land correctly. - [x] The window resizes/repositions to the requested rect (whole screen
by default, or a specific one via
--desktop-geometry/rc.glsl'ssetgeometry— see below). - [x] Transparency.
AppWindowrequests GLFW'sTransparentFramebufferhint whenever--desktopis set — without it, GLava's shaders already writingalpha = 0for "nothing here" pixels (e.g.shaders/glava/bars/1.frag's defaultfragment = vec4(0,0,0,0)) didn't matter, because the X server still composited the window as fully opaque. With the hint set and a compositor running (xfwm4 ships one), the wallpaper/icons show through everywhere the active module doesn't draw. Verified visually on a live session. - [x] Click-through. First attempt: restack the window explicitly
below xfdesktop's own window (matched by
WM_CLASS), relying on stacking order alone to keep clicks reaching desktop icons. Traded away later (see the stacking bug below) once it became clear the window needs to stay above xfdesktop to be visible at all, which would have put input-routing and visibility at odds. Fixed properly:--desktopgives the window an empty SHAPE-extension input region (x11rb'sshape_rectangles(SET, INPUT, ..., &[])), making the entire window click-through unconditionally, fully independent of stacking order — verified viapython-xlib(win.shape_get_rectangles(Input)returns[]) on a live session. - [x] One thing observed but not chased down:
xpropon the running window showed_NET_WM_STATEasSTICKY, SKIP_PAGER, SKIP_TASKBAR— xfwm4 appears to overwrite theBELOWstate GlavaSharp sets with its own computed set rather than keeping it. Didn't visibly matter (aDESKTOP-typed window is already implicitly bottom-of-normal-stack for xfwm4), so left as-is.
- [x]
- [x] Desktop-mode geometry control (
--desktop-geometry X,Y,W,H, GLava'ssetgeometryequivalent for-d). Previously the window always covered the whole screen;RcConfigwas already parsingsetgeometry's width/height but silently discarding x/y. Now the CLI flag can place/size the desktop-mode window at an exact rect instead. Verified live:--desktop --desktop-geometry 200,150,900,500produced a window at exactly900x500+200+150(confirmed viaxwininfo), rendering correctly and staying transparent/click-through at that size.- [x] Bug found and fixed: desktop mode wasn't actually fullscreen by
default. The first version of this feature also fell back to
rc.glsl's own
setgeometry(x/y/width/height) whenever--desktop-geometrywasn't passed. That was wrong: GLava's stockrc.glslships#request setgeometry 0 0 800 600unconditionally as the default windowed-mode size, so "rc.glsl has asetgeometryline" is true for nearly every rc.glsl, not just ones where the user actually wants desktop mode constrained. Result:--desktopalone silently shrank to an 800x600 box instead of covering the screen, on any unmodified rc.glsl. Fixed by removing the rc.glsl fallback entirely — only an explicit--desktop-geometry(or--desktop-monitor, below) constrains desktop mode now; no flag means "cover the whole screen," always. Verified live: same window went from800x600+0+0to the full3286x1080+0+0virtual screen after the fix.
- [x] Bug found and fixed: desktop mode wasn't actually fullscreen by
default. The first version of this feature also fell back to
rc.glsl's own
- [x] Per-monitor desktop mode (
--desktop-monitor <index>,--list-monitors).--desktop-geometryneeds the user to already know pixel coordinates;--desktop-monitor Ncovers exactly monitorN's rect instead, resolved via GLFW's own cross-platform monitor API (GetMonitors/GetMonitorPos/GetVideoMode— the same RandR data--list-monitorsprints), not new X11 code. Resolved insideAppWindowrather thanProgram.cs, since monitor enumeration needs GLFW already initialized. Mutually exclusive with--desktop-geometry. Verified live on a real two-monitor setup:--desktop-monitor 0(a1920x1080 at (1366,0)monitor, confirmed via--list-monitors) produced a window at exactly1920x1080+1366+0, rendering only on that monitor — the other monitor's wallpaper was completely untouched. - [x] Bug found and fixed: window went permanently invisible after
clicking empty desktop space. Root-caused live against a real
two-monitor xfwm4/XFCE session (not a compositor setting — first
suspected xfwm4's "unredirect fullscreen windows"
/general/unredirect_overlays, toggled it off, bug persisted, ruled it out).xprop -root _NET_CLIENT_LIST_STACKINGwhile the window was hidden showed it sandwiched below the xfdesktop window for its own monitor (xfdesktop maps one window per monitor;xwininfoconfirmed both windows shared the identical1920x1080+1366+0rect). Since xfdesktop paints the wallpaper opaquely across its whole window, GlavaSharp's alpha-blended output is only ever visible while stacked above xfdesktop — being below it means fully hidden, not "behind icons." Two compounding bugs inx11shim's original restack logic: (1) it deliberately requestedStackMode::BELOWxfdesktop, which is backwards from what actually makes the window visible; (2) it picked its restack target as "topmost mapped window withWM_CLASScontaining xfdesktop," not the one actually covering the monitor GlavaSharp renders to — wrong on any multi-monitor xfdesktop setup. xfwm4'sclick_to_focus+raise_on_click(both on by default) raise the clicked monitor's xfdesktop window on every click; the old relower thread only ever lowered GlavaSharp further in response, never raised it back, so the first click on that monitor's desktop permanently sank the window. Fixed:x11shimnow matches the xfdesktop window by root-relative geometry overlap with its own rect (TranslateCoordinates+GetGeometry, not justWM_CLASS) and restacks above it, re-asserting on every_NET_CLIENT_LIST_STACKINGchange so a later click-triggered raise gets immediately countered. Verified live: after the fix,_NET_CLIENT_LIST_STACKINGplaced the window above both xfdesktop windows, and a screenshot confirmed the module rendering over the wallpaper on its target monitor. - [x] Follow-up: same disappear-on-click symptom reported after the fix
above, on a single-monitor XFCE/X11 session. Two robustness gaps in
the re-raise loop, not the restack-target logic: (1) the loop's error
path treated any failed re-raise attempt as fatal and
breaks out of the thread entirely -- a single transient X error (very plausible when actively fighting the WM for z-order) silently and permanently disabled re-raising for the rest of the process's life, which reads exactly like "disappears once and never comes back." (2) the loop only ever woke up on_NET_CLIENT_LIST_STACKINGPropertyNotify, with no fallback if xfwm4 doesn't regenerate that property for every internal restack it performs. Fixed: failed re-raises now log to stderr and keep the loop alive instead of killing it, and aPERIODIC_RERAISE_INTERVAL(500ms) timer re-asserts stacking independently of the property watch as a self-healing fallback. Live re-test showed this wasn't the actual cause: no stderr output appeared when the window disappeared on click, meaning the re-raise attempt either wasn't the failure point or was failing silently rather than erroring -- see the next entry. - [x] Real root cause of the disappear-on-click symptom: claiming
_NET_WM_WINDOW_TYPE_DESKTOPon our own window, not a bug in the re-raise loop. This window is WM-managed (xfwm4 reparented it), so aConfigureWindowrestack issued against it isn't applied directly by the X server -- root hasSubstructureRedirectset, so the request is redirected to xfwm4 as aConfigureRequestevent, and xfwm4 decides whether to actually honor it..check()only surfaces X protocol errors, not "the WM silently declined it," so a declined restack looks identical to a successful one from our side -- explaining the lack of any log output. Per EWMH,_NET_WM_WINDOW_TYPE_DESKTOPwindows are meant to always sit beneath every other window type, enforced by the WM, not just requested -- and the entry above (in the first "Bug found and fixed" section) already had the evidence for this without it being connected yet:xpropshowed xfwm4 silently overwriting our requested_NET_WM_STATE(droppingBELOW) whenever this window claimed typeDESKTOP. By also claiming_DESKTOP(to mirror xfdesktop, on the theory that matching its type would make relative stacking requests meaningful), this window landed in the same bottom-of-everything bucket as xfdesktop itself, where xfwm4 tie-breaks in xfdesktop's favor on every stacking recalculation -- exactly what a desktop click triggers viaclick_to_focus+raise_on_click. Cross-checked againstxwinwrap(the standard X11 "app as desktop background" tool), which sidesteps this entire class of problem via override-redirect (exempting the window from WM management/redirect entirely) -- not adopted here since it would mean creating the X11 window ourselves instead of letting GLFW own creation. Fixed instead: the window now claims_NET_WM_WINDOW_TYPE_NORMALwith_NET_WM_STATE_BELOW+_NET_WM_STATE_STICKY+_NET_WM_STATE_SKIP_TASKBAR+_NET_WM_STATE_SKIP_PAGERrequested explicitly (DESKTOPno longer implies them).BELOWon aNORMALwindow is a different, ordinary, well-honored layer that every EWMH-compliant WM keeps above the desktop layer as a structural invariant rather than a per-window tie-break, so there's no fight to lose. The restack-above-xfdesktop thread (previously the primary mechanism) stays as a defensive fallback only. Verified live: the user confirmed the disappear-on-click symptom is gone after this change. - [x] Follow-up: fallback re-raise thread logging a repeating
BadWindow(error code 3) onConfigureWindow, major opcode 12, after the NORMAL+BELOW fix above. Harmless to visibility (the primary BELOW-layer fix already made the fallback thread unnecessary for correctness) but pointed at a real staleness bug:my_toplevel(this window's frame, pertoplevel_ancestor) was resolved exactly once at startup, immediately aftermap_window, then cached for the thread's entire lifetime.map_windowon a WM-managed window is itself redirected (aMapRequestevent, not an immediate map), so xfwm4's actual reparenting happens asynchronously -- and separately, xfwm4 appears to swap in a fresh frame shortly after seeing the Motif "no decorations" hint on this same remap. Either way, the once-at-startuptoplevel_ancestorcall could race ahead of xfwm4 and cache a frame ID that got destroyed moments later, which then never becomes valid again -- explaining the repeating (not one-off) error on every subsequent retry. Fixed:raise_above_desktop_ownernow resolvestoplevel_ancestorfresh on every call (initial raise and every re-raise alike) instead of accepting a cached value, mirroring how the xfdesktop-side sibling was already being re-resolved fresh each time. - [ ] GNOME — Mutter doesn't honor
_NET_WM_WINDOW_TYPE_DESKTOPstacking the way xfwm4 does, so this likely needs its own approach rather than reusingx11shim's as-is. Not started. - [ ] Native Wayland — no EWMH at all under Wayland; would need a
completely different mechanism (likely a
wlr-layer-shellclient for wlroots-based compositors, with no obvious equivalent on GNOME/ KDE's Wayland sessions). Not started. - [ ] Other desktop environments (KDE, Sway, etc.) — not started, contributions welcome.
Live control channel & shader hot-reload¶
- [x] Shader hot-reload.
ShaderModuletracks, per compiled pass, the full set of files it pulled in via#include(transitively —GlavaPreprocessor.Processnow returns that set alongside the usual preprocessed source). AFileSystemWatcherover the shader tree (RootDir, plus a second watcher over the siblingglavasharp/directory for modules resolved via that fallback — see GlavaSharp-Original Modules) marks a file dirty on save;ShaderModule.ReloadIfDirty(), called once per frame from the render thread (never from the watcher's own callback thread — no GL context there), recompiles every pass whose dependency set contains a dirty file. Editing a module's own.fragrecompiles just that pass; editing a shared file likeutil/smooth.glsloraurora.glslrecompiles every pass across the module that included it. A failed recompile logs an error and leaves the previous, still-working GL program running rather than tearing anything down. - [x] Live-tweakable per-module properties. A pass can declare
#request property "name" float default min max(a GlavaSharp extension to GLava's#requestconvention, parsed the same way#request uniformalready was) right next to theuniform float name;it already needs — seeshaders/glavasharp/aurora/1.frag'samplifyfor the worked example (replaced what used to be a#define AMPLIFY 2.6inaurora.glsl).ShaderModulere-applies the current value to every pass that declared it on eachRender()call, so a change takes effect on the very next frame with no recompile involved. - [x] Feed-driven properties. A property can also declare
#request feed "name" source(a second, separate line from#request property— feed-eligibility is an orthogonal annotation on an already-complete property declaration, not a different kind of property) to opt into being driven by a named built-in data source instead of manual slider input — e.g.shaders/glavasharp/clock/2.frag's#request feed "seconds_since_midnight" clock.Control/FeedRegistry.csis a small, deliberately non-pluggable name →Func<float>lookup (one entry today:"clock"→DateTime.Now.TimeOfDay.TotalSeconds).PropertyStoretracks a mutable enabled flag per feed-eligible property, on by default (a clock with its time feed off at startup would just show frozen hands, never what you want), and the control page renders a checkbox (auto: clock) next to the slider, disabling the slider while the feed is active.AppWindow.RuncallsPropertyStore.ApplyFeedsonce per frame, right afterDrainPending, which samples every enabled feed and routes it through the exact sameApplyPropertyChangedispatch a manual slider edit uses — so fromShaderModule's point of view a fed value is indistinguishable from one a slider set. Verified live: two samples ofseconds_since_midnighttwo seconds apart advanced by ~2.0s; toggling the feed off and setting a manual value froze it there (confirmed unchanged two seconds later); this required zero host-side special-casing of "time" as a concept anywhere outside the oneFeedRegistryentry. - [x] Live control channel.
Control/ControlServer.csis a plainSystem.Net.HttpListener(deliberately not Kestrel/ASP.NET Core — HttpListener is already in the BCL, trims cleanly underPublishAot, and doesn't grow the single-file AppImage) serving one self-contained HTML/JS page (inline CSS/JS, no CDN, no build step) with a small hand- written-JSON API overControl/PropertyStore.cs. Every registered property —fft.attack/fft.decay/fft.gain(the same knobs--fft-attack/-decay/-gainset at startup) plus whatever the active module declared via#request property— shows up there as a slider automatically; no per-property UI code to write as new properties get added.PropertyStore.TrySet(called from the HTTP handler thread) only validates and queues a change;PropertyStore.DrainPending(called once per frame fromAppWindow.Run, the only thread with the GL context current) is what actually applies it, viaIFft.SetAttack/SetDecay/SetGainorShaderModule.SetProperty.System.Text.Json's reflection-based serializer would trip the csproj'sIL2026/IL3050warnings-as-errors without a source-generatedJsonSerializerContext— not worth the ceremony for a payload this small, so the JSON is hand-written instead.- Binds
127.0.0.1:8642by default (--control-port);--control-bind 0.0.0.0opts into LAN access (e.g. a phone/tablet on the same network) — there's no authentication, so only widen this on a network you trust.--no-controldisables it entirely,--no-hot-reloaddisables the file watcher. - A bind failure (most commonly: another GlavaSharp instance already
holds the port) is non-fatal — logged as a warning, the app keeps
running without a control channel. Verified this path directly:
starting a second instance on the same default port logs
Live control channel disabled: ... Address already in useand renders normally. - Independent of
--desktop/pinned-embedded mode by construction — the control server is a background thread that doesn't know or care which windowing mode is active. Verified live:--desktopwith the control channel on a distinct--control-portserves properties and reflects changes exactly the same as windowed mode.
- Binds
Everything else¶
- [x] Fixed: AOT static-linking regression. Found while testing
desktop mode live — the working tree had uncommitted edits to
GlavaSharp.csprojthat had dropped<DirectPInvoke Include="pwshim"/>and the-lpipewire-0.3linker arg. WithoutDirectPInvoke, ILC doesn't bindpwshim_start/pwshim_stopat compile time, so the AOT build falls back todlopen("pwshim.so")at runtime — which doesn't exist, sincepwshimis meant to be statically linked — crashing withDllNotFoundExceptionthe moment audio capture started. Restored both, and added the matchingDirectPInvokeentry forx11shim(which would have hit the exact same bug). - [x] Fixed: GLFW
glfwGetPlatform()crash. The bundled GLFW build predatesglfwGetPlatform()(a GLFW 3.4+ API) — already guarded elsewhere inAppWindow(LogSelectedPlatform), but the desktop-mode X11 platform check didn't have the sameEntryPointNotFoundExceptionfallback, so--desktopcrashed instead of just skipping the check and proceeding on the assumption that the X11 init hint already forced GLFW onto X11 (which it does, orInit()would have failed earlier). - [ ] 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. - [x] Bug found and fixed: "single self-contained executable" was only
true of the one
GlavaSharpfile, not the directory you'd actually need to distribute.ls build/dist/showsGlavaSharp,GlavaSharp.dbg,libglfw.so.3.3,libglfw-wayland.so.3.3, andshaders/— Native AOT statically links the Rust shims (pwshim/x11shim) but not GLFW (OpenTK's native GLFW package is dynamically loaded), and the shader tree was always meant to ship alongside rather than be embedded (see Shader Module Pipeline). Not a regression, just a README claim ("single self-contained executable ... no sibling.sofiles to lose track of") that didn't match whatbuild/dist/actually contained. Fixed by adding a packaging step rather than re-architecting the linking:cmake --build build --target appimage(packaging/build-appimage.sh) packsbuild/dist/into one real single-file AppImage viaappimagetool— see Packaging for the full writeup, including the.desktop-file validation gotcha (Categories=needs registered/X--prefixed values) hit during setup. Verified live:GlavaSharp-x86_64.AppImage --module aurora, run from/tmp(nowhere near the repo), correctly resolved and compiled shaders from its own FUSE mount point.