|
40 | 40 | - **Backend**: Vulkan / OpenGL / Metal (via `wgpu` abstraction) |
41 | 41 | - **Shader Pipeline**: Single-pass WGSL shader (`effects.wgsl`) |
42 | 42 | - **Uniform Buffer**: Tracks separate old/new image aspect ratios, screen resolution, animation progress (`0.0..1.0`), active effect type index (`fade`, `blur`, `wipe`, `slide`, `zoom`, `pixelate`, `ripple`, `dissolve`, `wave`, `grow`, `outer`), effect parameters (`param_a` to `param_d`), effect origin (`origin`), travel direction (`direction`), easing mode (`easing`: `0` linear, `1` ease-in, `2` ease-out, `3` ease-in-out), and scaling mode (`scaling_mode`: `0` fill, `1` fit, `2` stretch, `3` center, `4` tile). Struct is 80 bytes (`Vec2`-aligned, size padded for WGSL uniform layout). |
43 | | -- **Aspect Correction**: Computes aspect-ratio scaling directly inside the fragment shader, avoiding CPU-side cropping or image scaling overhead. Five scaling modes are supported: `fill` (cover, crops to fill screen), `fit` (contain, letterbox/pillarbox), `stretch` (ignores aspect ratio), `center` (1:1 centered), and `tile` (repeat). The mode is passed to the GPU via a uniform and applied per-pixel in the `scale_uv` function. Circular effects (`grow`, `outer`, `ripple`) additionally convert UVs into pixel-aspect-corrected space before taking `distance()`, so expanding rings are true circles on any monitor, never ovals. |
| 43 | +- **Aspect Correction**: Static images larger than their useful render size are reduced once before upload while preserving enough pixels for the selected output and scaling mode. The fragment shader still owns the final aspect-ratio mapping: `fill` covers and crops, `fit` contains with letterboxing/pillarboxing, `stretch` ignores aspect ratio, `center` remains 1:1, and `tile` repeats. Center/tile sources retain native dimensions and return a clear error when they exceed the adapter limit rather than silently changing their pixel-sensitive scale. Circular effects (`grow`, `outer`, `ripple`) additionally convert UVs into pixel-aspect-corrected space before taking `distance()`, so expanding rings are true circles on any monitor, never ovals. |
44 | 44 | - **Stable Image Registration**: The old and new source textures each keep their own immutable `fill` crop for the whole transition. Effects animate blend values and reveal masks in screen space; they do not translate or rescale the wallpaper texture. This prevents the visible “jump” that occurs when images with different source dimensions are changed mid-transition. |
45 | 45 | - **Smoothness**: Every transition is eased with a configurable curve (`linear` / `ease_in` / `ease_out` / `ease_in_out`, default smoothstep cubic ease-in-out) and rendered one frame per vsync. The daemon presents with `PresentMode::Fifo`, so `get_current_texture()` blocks until the previous frame is displayed, pacing animations to the monitor refresh rate. Progress is derived from wall-clock time rather than a frame counter, so a transition lasts exactly its configured `duration` on any refresh rate (frame-count pacing would run too fast on high-refresh panels and too slow on low ones). The ease-in-out tail keeps visible motion almost to the last frame, so a blur radius or crossfade never appears to stagger to a halt before the transition finishes. |
46 | 46 | - **Non-blocking transitions**: The daemon commits the new wallpaper state immediately and renders the visual transition on a detached background task, serialized by a render lock. `wallr set` returns as soon as the image is committed and themed, never waiting on GPU presents. If the compositor stops presenting (monitor off, suspend), the render task parks inside the present without freezing the IPC loop, and later transitions simply queue behind it. |
47 | 47 | - **Live wallpapers**: when the committed file is an animated GIF, `AnimatedImage` decodes every frame once at load time. If the raw RGBA total fits the 256MB budget, frames are stored as-is and playback is a memcpy per frame; larger animations are stored as zstd-compressed streams (roughly 30:1) and decompressed through a persistent `zstd::bulk::Decompressor` context, so neither path ever re-decodes the source file during looping playback. The first frame becomes the transition's incoming texture, and when the transition ends the render task switches to playback: it computes the absolute wall-clock boundary of the next frame (`frame_start(index+1)` plus whole-loop offsets, so pacing survives animation wrap-around) and presents at that deadline, uploading the next frame into an idle double-buffered texture during the sleep via a mapped staging buffer (copied with `copy_buffer_to_texture`; unaligned widths fall back to `update_texture`). A playback generation counter is bumped on every commit, so a queued playback loop stops itself the moment a newer wallpaper supersedes it. GIF playback respects `wallr ipc pause/resume` commands and preserves timeline position when paused by tracking accumulated pause time. Static images skip playback entirely and present a single frame, and the preview window follows the same flow. |
48 | 48 | - **Video playback**: `video::VideoPlayback` decodes MP4/WebM/MKV with FFmpeg. Hardware acceleration can be set to `auto` (tries all backends in priority order: NVDEC, VAAPI, VideoToolbox), a specific backend (tries that backend then falls back to software), or `software` (software-only, no hardware attempts). The active decoder backend is reported immediately after successful initialization. Frames are delivered on PTS timing through a small bounded queue and uploaded with the same texture pipeline; `wallpaper.loop_video` restarts the stream at EOF for seamless looping. `wallr ipc pause/resume/seek/info` control playback, and the video path is disabled for static images. |
49 | | -- **Previous-frame compositing**: The daemon keeps the previous decoded wallpaper texture as the outgoing source and reveals the new texture over it. The persisted last wallpaper is restored at daemon startup, so a restart also has a real outgoing frame. The preview window reads the same persisted path and uses the last applied wallpaper as its outgoing frame, falling back to a solid black frame only when nothing was ever applied (or when the outgoing image is the same file as the incoming one). |
| 49 | +- **Previous-frame compositing**: The daemon keeps the previous decoded wallpaper texture as the outgoing source and reveals the new texture over it. Per-output state is atomically persisted only after the replacement commits, rotating the former path into a previous-wallpaper slot. Startup retries explicitly transient failures and falls back to that previous valid path when restoration still fails. The preview window reads the persisted path and uses the last applied wallpaper as its outgoing frame, falling back to a solid black frame only when nothing was ever applied (or when the outgoing image is the same file as the incoming one). |
50 | 50 |
|
51 | 51 | ### 2b. Animation → Uniforms Path |
52 | 52 | Every transition (from a YAML package, `wallr set --effect ...`, or the preview window) resolves to a single `animation::Effect` value, which `compute_effect_uniforms(effect, progress)` converts into an `EffectUniforms` struct (effect type, eased progress, `param_a` to `param_d`, origin, direction, easing mode). The daemon and preview both feed this into `Renderer::render_frame`, so CLI flags, YAML packages, and previews share one identical code path: |
|
0 commit comments