Skip to content

Add Radiant Gallery screensaver: 87 Metal shaders cycling natively on macOS#2

Open
boxabirds wants to merge 51 commits intopbakaus:mainfrom
boxabirds:main
Open

Add Radiant Gallery screensaver: 87 Metal shaders cycling natively on macOS#2
boxabirds wants to merge 51 commits intopbakaus:mainfrom
boxabirds:main

Conversation

@boxabirds
Copy link
Copy Markdown

Summary

A native macOS screensaver that cycles through all 87 Radiant shaders, ported to Metal Shading Language. No WKWebView — pure GPU rendering via CAMetalLayer.

  • 87 fragment shaders translated from the HTML/WebGL/Canvas 2D originals
  • 12s dwell per shader, shuffled order, alpha-blended cross-fade transitions
  • CVDisplayLink for vsync'd 60fps, half-resolution with bilinear upscale
  • Universal binary (arm64 + x86_64), macOS 14.0+
  • ~21,000 lines of MSL + ~400 lines of Swift

Build & test

cd radiant-screensaver && make install
# Then: System Settings > Screen Saver

Requires Xcode Command Line Tools. Metal Toolchain recommended for shader precompilation (xcodebuild -downloadComponent MetalToolchain), falls back to runtime compilation.

Architecture

radiant-screensaver/
  Sources/
    RadiantScreenSaverView.swift    — CVDisplayLink, cycling state machine, cross-fade
    ShaderRegistry.swift            — 87 shader descriptors
    Shaders/
      Common.metal                  — snoise, fbm, hue_rotate (shared via #include)
      Vertex.metal                  — Fullscreen triangle
      Transition.metal              — Noise dissolve (available but unused — alpha blend preferred)
      fragment/*.metal              — One file per shader (87 files)
  Makefile                          — Multi-file metallib build, universal binary, codesign

Disclosure: AI-assisted development

This code was generated with Claude Code (Anthropic), then iteratively tested, debugged, and refined over multiple sessions. Specific areas of human quality review:

  • Visual fidelity — each ported shader compared against its HTML original
  • Performance — confirmed 60fps on Apple Silicon; identified and fixed a 3fps regression from an offscreen texture approach, replaced with direct-to-drawable alpha blending
  • Multi-display — tested on dual monitors, fixed scaling issue (contentsGravity = .resize)
  • Transition smoothness — iterated through several approaches (offscreen composite → noise dissolve → alpha cross-fade) to find one that maintains framerate
  • GLSL→MSL correctness — fixed mod() vs fmod() sign differences, constexpr vs constant address space, variable name collisions with Metal builtins

The AI performed the bulk mechanical GLSL→MSL translation (87 shaders), while the human directed architecture, tested every build on real hardware, and caught issues the AI missed.

Test plan

  • make build succeeds
  • make install places .saver in ~/Library/Screen Savers/
  • Screensaver cycles through shaders with cross-fade transitions at 60fps
  • Works on Retina and non-Retina displays
  • No build artifacts committed (build/ gitignored)

🤖 Generated with Claude Code

boxabirds and others added 30 commits March 25, 2026 00:32
Three new fullscreen immersive pages:

- /zoom: Cycles through gallery shaders in iframes with WebGL
  noise-dissolve morph transitions, mouse-guided zoom, and
  ultra-slow hue cycling.

- /morph: Single WebGL uber-shader with ~25 interpolatable parameters
  expressing noise fields, domain warping, orbs, fabric folds, and
  lighting. Presets morph continuously via parameter interpolation.

- /morph-webgpu: WebGPU rewrite of the morph engine. Single WGSL
  uber-shader with uniform buffer uploaded as Float32Array. Parameters
  driven by three correlated master oscillators (sine-sum, no grid
  discontinuities) controlling warp, orbs, and fold/lighting phases.
  Zero-allocation render loop. Distinct color palettes per mode
  (teal/cyan, violet/blue, magenta/rose).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- FBM loop cap 3→5 octaves, matching original fluid-amber's detail level
- Power-4 winner-take-all (was power-2) with wider drift speed spread
  for stronger attractor dominance and less mid-blend "mixed paint"
- Hold integer uniforms constant across all presets (fbm_octaves=5,
  orb_count=7) to eliminate discrete pops at blend thresholds; control
  detail/visibility via continuous params (fbm_decay, orb_intensity)
- Preset params matched to original shaders: fluid-amber warp strengths,
  chromatic-bloom zero-noise pure orbs, silk-cascade full fold+specular,
  bioluminescence strong wave interference
- Color palettes unified to warm-amber family so linear blending
  produces coherent tones instead of gray; hue_shift handles variety

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
5 new techniques: kaleidoscope fold, spiral warp, metaball orbs,
moiré interference, burn frontier. ~90 WGSL lines, 10 new uniforms.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
MP3 plays through a 4-pole lowpass filter chain (2x BiquadFilterNode)
with slow LFO modulation (~0.08Hz) tracking visual sharpness. Keyboard
only: spacebar toggles, -/= adjusts volume. Lazy-loads on first press.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ral, kaleidoscope

5 new pipeline stages added incrementally, each perf-tested:
- Metaball orbs: mix(gaussian, inverse-distance, orb_sharpness) in existing orb fn
- Moiré interference: 4-center multiplicative ring beats (freq=55, hard gate)
- Burn frontier: threshold sweep on noise field with bright edge glow
- Spiral arms: log-spiral distance field with floor-based modulo
- Kaleidoscope fold: binary select (not mix) to avoid seam artifacts

9 presets total (4 original + 5 new). Uniform buffer 52→60 floats.
All new features use smoothstep(0.3, 0.6) gates to prevent bleed at
low blend weights. Tranche-3 plan saved.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ixes

New techniques:
- Chladni cymatics: 6 standing-wave eigenfunctions (cos(nπx)·cos(mπy))
  with mode interpolation. Preset 9.
- Chromatic aberration: radial RGB split post-processing filter.
  Subtle values on orb/metaball/moiré presets.

Debug HUD (press 'd'):
- GPU fps via onSubmittedWorkDone (measures actual GPU completion, not RAF)
- Dominant preset name + weight percentage

Performance:
- Reverted kaleidoscope to binary select (double warped_field saturated GPU)
- Halved animation speed (time divisor 2000→4000)

Uniform buffer: 60→64 floats (256 bytes). 10 presets total.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Aurora curtain: 8 vertical sine-displaced line SDFs with per-line
  phase variation, tapered width, additive glow. Preset 10.
- Power-8 winner-take-all (was power-4): ~80% dominance with 11 presets,
  matching the separation power-4 gave with 4 presets.
- Fix WGSL let→var for mutable curtain center binding.
- Remove close button overlay.
- 11 presets, 64-float uniform buffer.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Cache wave_field(p,t) result in wave_raw; reuse at wave crest highlight
  site — eliminates one full wave_field() evaluation per pixel
- Gate voronoi_cracks() 3×3 nested loop behind u.voronoi_str > 0.01;
  voronoi_str is always 0 across all presets so the loop was running
  unconditionally every pixel with a ×0 result
- Gate fabric_fold() (2× fbm2 + 4 sines) behind u.fold_str > 0.01;
  declare fold_grad before the gate so downstream normal/aniso code compiles

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…BM loop unroll

- Gate curtain_field(), chladni_field(), spiral_field(), moire_field() each
  behind their respective uniform > 0.01 checks; declare result vars before
  gate so downstream color-pass code still compiles
- Gate burn frontier behind u.burn_str > 0.01; declare burn_gate/burn_edge
  before gate (used in additive color pass) defaulting to 0.0
- Gate compute_orbs() behind u.orb_intensity > 0.01; restructure envelope to
  default to 1.0 (no field modulation) when orbs are off — fixes a latent
  correctness issue where envelope could collapse to 0 at low orb_intensity
- Gate wave_field() call itself behind u.wave_str > 0.01 (result was cached
  in phase 1; now the function isn't called at all when wave is off)
- Add override FBM_MAX_OCTAVES: i32 = 3 constant to WGSL; change fbm() loop
  bound from literal 5 to FBM_MAX_OCTAVES — enables driver loop unrolling
- Pass constants: { FBM_MAX_OCTAVES: 3 } in engine.ts pipeline descriptor
  for both vertex and fragment stages

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…cale

Render the main fragment shader to a ⌊w/2⌋×⌊h/2⌋ intermediate texture,
then blit to the swap chain with bilinear upscaling. At ¼-speed animation
over smooth noise fields with no geometric hard edges, the quality loss is
imperceptible while GPU fragment work drops by ~4×.

engine.ts changes:
- Add halfResTexture: GPUTexture created at canvas.width/2 × canvas.height/2
  with RENDER_ATTACHMENT | TEXTURE_BINDING usage
- Add blit pipeline: separate WGSL module (BLIT_WGSL constant inline in
  engine.ts) with vs_blit fullscreen triangle + fs_blit linear sampler
- Extract _buildHalfResResources() static helper: creates half-res texture,
  blit bind group (linear sampler + texture view), and main render bundle
  targeting the half-res view; called at init and on every resize
- Add public resize(w, h) method: destroys old half-res texture, calls
  _buildHalfResResources to rebuild texture + bind group + render bundle
- render(): two-pass submission — Pass 1 executes the (bundled) main draw
  to halfResTexture; Pass 2 runs the blit pipeline directly (no bundle,
  since swap chain view changes each frame)
- destroy(): also destroys halfResTexture

+page.svelte: call engine?.resize(w, h) in the onResize handler so the
half-res texture is rebuilt to match new canvas dimensions

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Colour variation: slow-drifting hue-rotation patches oscillate in/out
  on a 20s sine cycle (monotone for half, colourful for half)
- Audio VCF ceiling raised from 8kHz to 20kHz (fully open at max sharpness)
- 'f' key fades visuals + audio in/out over 10s (CSS transition + gain ramp)
- Cursor auto-hides after 3s of inactivity, restores on mouse move

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Replaced close button with "By Julian Harris | Based on Radiant" links
- Attribution fades out when cursor auto-hides after 3s inactivity

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Fixed-position children of a fixed parent don't escape stacking context.
Moved attribution + key-guide to top level, z-index 10003.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
svelte:head hides all <nav> elements to suppress gallery nav.
Changed attribution from <nav> to <div>.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Mobile: credits show 15s then fade; sound hint pulses in centre for 15min;
  double-tap toggles audio with "Sound on/off" feedback (5s fade)
- Kaleido: suppressed for first 60s, faster drift speed to cycle out sooner
- Audio LFO: half speed (25s cycle), square-wave shape (3rd+5th harmonics)
  so filter lingers at 20kHz for ~half the cycle

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Native macOS screensaver (.saver bundle) using Metal. Foundation layer:
- Simplex noise, FBM, dual domain warping, ridged noise
- Gaussian/metaball orbs, wave interference, fabric fold
- Voronoi cracks, two-light Blinn-Phong + Fresnel lighting
- 4-color palette, ACES tonemap, grain, vignette, hue rotation
- 4 presets: flowing-warp, orb-field, silk-folds, ocean-waves
- Power-8 winner-take-all blending with incommensurate drift
- Swift + Metal, universal binary (arm64/x86_64), make build/install

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Metal shader gains: kaleido_fold, spiral_field, moire_field, burn
frontier (phase/threshold/mask/edge). All gated with smoothstep.
Swift view expanded to 9 presets with kaleido startup ramp.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
boxabirds and others added 21 commits March 26, 2026 16:17
Chladni field: 6 mode pairs with smooth interpolation.
Chromatic aberration: radial RGB split post-tonemap.
10th preset: chladni-resonance. Final preset count reached.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Spatially-varying hue rotation creates organic colour islands that
drift across the frame. Monotone for half the cycle, colourful
for the other half (sin wave * 0.9 strength).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Gate voronoi, fold, chladni, spiral, moire, burn behind uniform
threshold checks (warp-coherent, zero divergence cost). Cache
wave_field result to eliminate duplicate evaluation. Add README.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Replace ScreenSaverView timer with CVDisplayLink for vsync'd rendering
- Render at half resolution (RESOLUTION_SCALE=0.5), CAMetalLayer upscales
- Timestamp in CFBundleName for cache-busting during development

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Native Metal screensaver framework for cycling through all 87 shaders:
- CVDisplayLink for vsync'd rendering, half-res CAMetalLayer
- Dual offscreen textures for A/B compositing
- Noise dissolve transition shader (ports zoom route's GLSL)
- Cycling state machine: 12s dwell + 3s morph, shuffled order
- Zoom drift during dwell (1.0→1.35), hue cycling (300s)
- ShaderRegistry for adding shaders incrementally
- Multi-file metallib build (Common.metal shared via #include)
- fluid-amber as first ported shader (proof of concept)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Batch GLSL→MSL translation of fragment shaders:
burning-film, silk-cascade, bioluminescence, chromatic-bloom, vortex,
chladni-resonance, moire-interference, golden-throne, kaleidoscope-runway,
neon-drip, eclipse-glow, aurora-veil, moonlit-ripple, diamond-caustics,
smolder, stardust-veil, shifting-veils, painted-strata, liquid-gold

All 20 shaders cycle with 12s dwell + 3s noise dissolve transitions.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
All WebGL fragment shaders now ported to Metal:
artpop-iridescence, aurora-curtain, bass-ripple, crystal-lattice,
dither-gradient, edge-of-chaos, event-horizon, feedback-loop,
gilded-fracture, gilt-mosaic, gilt-thread, gothic-filigree,
hologram-glitch, ink-dissolve, laser-labyrinth, lens-whisper,
lipstick-smear, magma-core, magnetic-field, metamorphosis,
neon-drive, neon-revival, polaroid-burn, radiant-geometry,
sacred-strange, scream-wave, sequin-wave, shattered-plains,
signal-decay, silk-groove, strobe-geometry, sugar-glass,
thunder-sermon, torn-paper, tropical-heat, vertigo,
vinyl-grooves, voltage-arc, rain-on-glass, rain-umbrella

60 shaders cycling with noise dissolve transitions at 60fps.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
All 87 shaders now ported. Canvas 2D effects reimagined as fragment
shaders: particle systems via analytical per-pixel evaluation,
trail effects via noise approximation, geometric drawing via SDF.

analog-drift, champagne-fizz, clockwork-mind, digital-rain, flow-field,
generative-tree, glitter-storm, ink-calligraphy, jazz-chaos, kinetic-grid,
laser-precision, luminous-silt, magnetic-sand, murmuration, pendulum-wave,
phase-transition, phyllotaxis, resonant-strings, rubber-reality,
spark-chamber, strange-attractor, synth-ribbon, tesseract-shadow,
topographic, velvet-spotlight, vintage-static, woven-radiance

87 shaders cycling with noise dissolve transitions at 60fps.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Swap textureA/B pointers when morph completes so dwell starts with
  the same pixels that were fully revealed, eliminating the 1-frame
  re-render discontinuity
- Remove WebGL-era Y-flip from transition shader (Metal is top-down)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
All render passes (shader A, shader B, composite) now encoded into
one MTLCommandBuffer. Eliminates two waitUntilCompleted() CPU stalls
per frame during morph transitions. 60fps maintained throughout.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
During dwell phase, render shader in a single pass directly to the
drawable — no offscreen texture, no composite. Only use the 3-pass
offscreen approach during the 3-second morph transitions.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
CAMetalLayer defaults to .bottomLeft gravity, so half-res drawables
sit in the corner instead of stretching. Explicit .resize ensures
the smaller drawable fills the full view on all displays.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Cross-fade uses setBlendColor alpha with src*blendAlpha + dst*one
blend mode. During morph: draw outgoing at alpha=(1-progress), then
incoming at alpha=progress, both directly to drawable. No offscreen
textures, no composite shader. 60fps maintained during transitions.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Incoming shader during morph is now the same as current during
the subsequent dwell. No shader identity change at the boundary.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Individual shaders have their own speed constants (TIME_SCALE etc).
The morph-webgpu quarter-speed divisor was making them 4x too slow.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add docs/ and music/ to .gitignore (local planning files, source MP3)
- Delete screensaver-webgpu-poc/ (superseded by morph-screensaver/)
- Add morph-webgpu README documentation

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Click/drag (desktop) or touch/drag (mobile) wipes raindrops off glass
- Replace independent drift signals with sequential A→B crossfade
- Increase blur canvas from 1/8 to 1/2 scale for sharper frosted glass
- Decouple mouse from morph shader (no more swirl on click)
- Preserve double-tap audio toggle on mobile

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Keep rain-overlay branch versions: colour_var_str (not curtain),
UI elements (attribution, key-guide, sound hint), rain canvas styles.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Resolve conflicts keeping rain-overlay's sequential crossfade (not
exponential smoothing), colour_var_str shader (not curtain), and
all rain overlay UI elements.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The struct members were already replaced with colour_var_str in the
merge resolution, but the function body and usage were left behind,
causing WGSL parse errors that poisoned the entire WebGPU pipeline.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant