Live Control Channel & Shader Hot-Reload¶
Control/, Shaders/ShaderModule.cs
Shader hot-reload¶
ShaderModule tracks, per compiled pass, the full set of files it pulled
in via #include (transitively — GlavaPreprocessor.Process returns
that set alongside the preprocessed source). A FileSystemWatcher over
the shader tree (RootDir, plus a second watcher over the sibling
glavasharp/ 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 .frag recompiles just that pass;
editing a shared file like util/smooth.glsl or aurora.glsl recompiles
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.
Live-tweakable per-module properties¶
A pass can declare #request property "name" float default min max (a
GlavaSharp extension to GLava's #request convention, parsed the same
way #request uniform already was) right next to the uniform float
name; it already needs — see shaders/glavasharp/aurora/1.frag's
amplify for the worked example (replaced what used to be a
#define AMPLIFY 2.6 in aurora.glsl). ShaderModule re-applies the
current value to every pass that declared it on each Render() call, so
a change takes effect on the very next frame with no recompile involved.
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.cs is a small, deliberately non-pluggable
name → Func<float> lookup (one entry today: "clock" →
DateTime.Now.TimeOfDay.TotalSeconds). PropertyStore tracks 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.Run
calls PropertyStore.ApplyFeeds once per frame, right after
DrainPending, which samples every enabled feed and routes it through the
exact same ApplyPropertyChange dispatch a manual slider edit uses — so
from ShaderModule's point of view a fed value is indistinguishable from
one a slider set.
The control server¶
Control/ControlServer.cs is a plain System.Net.HttpListener
(deliberately not Kestrel/ASP.NET Core — HttpListener is already in the
BCL, trims cleanly under PublishAot, 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 over
Control/PropertyStore.cs. Every registered property —
fft.attack/fft.decay/fft.gain (the same knobs
--fft-attack/-decay/-gain set 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 from AppWindow.Run, the only thread with the GL context
current) is what actually applies it, via
IFft.SetAttack/SetDecay/SetGain or ShaderModule.SetProperty.
System.Text.Json's reflection-based serializer would trip the csproj's
IL2026/IL3050 warnings-as-errors without a source-generated
JsonSerializerContext — 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.
- 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.
See Status & Roadmap for the live verification of each of these pieces.