From 6d2639f62fd9db13fe547fa2b039325e4114af26 Mon Sep 17 00:00:00 2001 From: CPritch Date: Sat, 10 Oct 2026 19:31:30 +0100 Subject: [PATCH] refactor: move the renderer into its own crate shiftpaper-render holds the renderer, the depth map code and the shader, with no Wayland code, so other front ends can draw with it. The caller now creates the wgpu instance and adapter and picks the output format, which lets each platform choose the backends and formats it offers. Depth maps go to the GPU as R16Float instead of R16Unorm, which is optional on phones, and the device asks for downlevel limits, so the renderer also runs on GLES. CI checks the crate builds for Android. --- .github/workflows/ci.yml | 5 +++ CHANGELOG.md | 7 +++ Cargo.lock | 19 +++++++-- Cargo.toml | 4 +- crates/daemon/Cargo.toml | 8 +--- crates/daemon/src/main.rs | 2 - crates/daemon/src/slideshow.rs | 2 +- crates/daemon/src/wayland.rs | 23 ++++++++-- crates/render/Cargo.toml | 23 ++++++++++ crates/{daemon => render}/src/depth.rs | 9 +++- crates/render/src/lib.rs | 9 ++++ crates/{daemon => render}/src/renderer.rs | 52 ++++++++++++----------- crates/{daemon => render}/src/shader.wgsl | 0 packaging/aur/shiftpaper-git/PKGBUILD | 3 +- packaging/aur/shiftpaper/PKGBUILD | 3 +- 15 files changed, 126 insertions(+), 43 deletions(-) create mode 100644 crates/render/Cargo.toml rename crates/{daemon => render}/src/depth.rs (98%) create mode 100644 crates/render/src/lib.rs rename crates/{daemon => render}/src/renderer.rs (97%) rename crates/{daemon => render}/src/shader.wgsl (100%) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7e5fe19..92aa437 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,6 +16,8 @@ jobs: uses: dtolnay/rust-toolchain@stable with: components: rustfmt, clippy + # The render crate should build for Android too. + targets: aarch64-linux-android - name: Install system dependencies run: sudo apt-get update -qq && sudo apt-get install -y libwayland-dev libxkbcommon-dev @@ -29,6 +31,7 @@ jobs: run: | cargo check -p shiftpaper-cli --no-default-features --features load-dynamic cargo check -p shiftpaper-daemon + cargo check -p shiftpaper-render --target aarch64-linux-android - name: cargo fmt run: cargo fmt --all -- --check @@ -37,10 +40,12 @@ jobs: run: | cargo clippy -p shiftpaper-config --all-targets --all-features -- -D warnings cargo clippy -p shiftpaper-cli --all-targets --no-default-features --features load-dynamic -- -D warnings + cargo clippy -p shiftpaper-render --all-targets -- -D warnings cargo clippy -p shiftpaper-daemon --all-targets -- -D warnings - name: cargo test run: | cargo test -p shiftpaper-config --all-features cargo test -p shiftpaper-cli --no-default-features --features load-dynamic + cargo test -p shiftpaper-render cargo test -p shiftpaper-daemon diff --git a/CHANGELOG.md b/CHANGELOG.md index 4ffd74a..1677d76 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## Unreleased + +### Changed + +- shiftpaperd no longer needs a GPU with 16-bit normalised textures, as depth maps now go to the GPU as 16-bit floats. +- The renderer is now its own crate, `shiftpaper-render`, with no Wayland code, so other front ends can use it. + ## 0.4.3 - 2026-10-09 ### Changed diff --git a/Cargo.lock b/Cargo.lock index 0efa80b..e86b393 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -648,6 +648,7 @@ version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" dependencies = [ + "bytemuck", "cfg-if", "crunchy", "num-traits", @@ -1715,16 +1716,14 @@ name = "shiftpaper-daemon" version = "0.4.3" dependencies = [ "anyhow", - "bytemuck", "calloop", "calloop-wayland-source", "clap", - "image", - "naga", "nix", "pollster", "raw-window-handle", "shiftpaper-config", + "shiftpaper-render", "smithay-client-toolkit", "tracing", "tracing-subscriber", @@ -1735,6 +1734,20 @@ dependencies = [ "wgpu", ] +[[package]] +name = "shiftpaper-render" +version = "0.4.3" +dependencies = [ + "anyhow", + "bytemuck", + "half", + "image", + "naga", + "shiftpaper-config", + "tracing", + "wgpu", +] + [[package]] name = "shlex" version = "2.0.1" diff --git a/Cargo.toml b/Cargo.toml index 4917745..e02918f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [workspace] resolver = "3" -members = ["crates/config", "crates/daemon", "crates/cli"] +members = ["crates/config", "crates/render", "crates/daemon", "crates/cli"] [workspace.package] version = "0.4.3" @@ -16,3 +16,5 @@ tracing-subscriber = { version = "0.3", features = ["env-filter"] } image = { version = "0.25", default-features = false } serde = { version = "1", features = ["derive"] } shiftpaper-config = { path = "crates/config" } +shiftpaper-render = { path = "crates/render" } +wgpu = "30" diff --git a/crates/daemon/Cargo.toml b/crates/daemon/Cargo.toml index 9b26565..c02c330 100644 --- a/crates/daemon/Cargo.toml +++ b/crates/daemon/Cargo.toml @@ -18,13 +18,11 @@ wayland-protocols = { version = "0.32", features = ["client", "unstable", "stagi wayland-protocols-wlr = { version = "0.3", features = ["client"] } wayland-backend = { version = "0.3", features = ["client_system"] } -wgpu = "30" +wgpu = { workspace = true } raw-window-handle = "0.6" -bytemuck = { version = "1", features = ["derive"] } - -image = { workspace = true, features = ["png"] } shiftpaper-config = { workspace = true } +shiftpaper-render = { workspace = true } calloop = "0.14" calloop-wayland-source = "0.4" @@ -40,5 +38,3 @@ clap = { version = "4", features = ["derive", "wrap_help"] } [dev-dependencies] shiftpaper-config = { workspace = true, features = ["man"] } -# Same version wgpu uses internally, to validate shader.wgsl in tests. -naga = { version = "30", features = ["wgsl-in"] } diff --git a/crates/daemon/src/main.rs b/crates/daemon/src/main.rs index ffdd717..fea2e22 100644 --- a/crates/daemon/src/main.rs +++ b/crates/daemon/src/main.rs @@ -1,9 +1,7 @@ mod battery; mod config; mod cursor; -mod depth; mod idle; -mod renderer; mod slideshow; mod wayland; diff --git a/crates/daemon/src/slideshow.rs b/crates/daemon/src/slideshow.rs index 5b99371..5914f34 100644 --- a/crates/daemon/src/slideshow.rs +++ b/crates/daemon/src/slideshow.rs @@ -1,8 +1,8 @@ //! Steps through [slideshow]'s images on a timer. -use crate::renderer::{DecodedWallpaper, Renderer, Wallpaper, WallpaperFiles}; use anyhow::{Result, anyhow}; use shiftpaper_config::SlideshowConfig; +use shiftpaper_render::{DecodedWallpaper, Renderer, Wallpaper, WallpaperFiles}; use std::thread::JoinHandle; use std::time::SystemTime; use tracing::{info, warn}; diff --git a/crates/daemon/src/wayland.rs b/crates/daemon/src/wayland.rs index 7e4221a..783f7ea 100644 --- a/crates/daemon/src/wayland.rs +++ b/crates/daemon/src/wayland.rs @@ -39,12 +39,12 @@ use wayland_protocols::wp::{ }; use crate::cursor::HyprlandCursor; -use crate::renderer::{DecodedWallpaper, OutputRenderState, Renderer, Wallpaper, WallpaperFiles}; use crate::slideshow::Slideshow; use raw_window_handle::{ RawDisplayHandle, RawWindowHandle, WaylandDisplayHandle, WaylandWindowHandle, }; use shiftpaper_config::{Config, TrackingMode, Transition}; +use shiftpaper_render::{DecodedWallpaper, OutputRenderState, Renderer, Wallpaper, WallpaperFiles}; pub struct OutputInfo { pub name: String, @@ -512,6 +512,23 @@ struct PendingReload { slide: Option<(usize, WallpaperFiles)>, } +/// A renderer on the low-power GPU through Vulkan, drawing in the format +/// Wayland compositors take. +async fn new_renderer() -> Result { + let instance = wgpu::Instance::new(wgpu::InstanceDescriptor { + backends: wgpu::Backends::VULKAN, + ..wgpu::InstanceDescriptor::new_without_display_handle() + }); + let adapter = instance + .request_adapter(&wgpu::RequestAdapterOptions { + power_preference: wgpu::PowerPreference::LowPower, + ..Default::default() + }) + .await + .context("no suitable GPU adapter found")?; + Renderer::new(instance, adapter, wgpu::TextureFormat::Bgra8UnormSrgb).await +} + /// Decode a wallpaper on a background thread. fn decode( files: WallpaperFiles, @@ -734,7 +751,7 @@ impl LayerShellHandler for App { if self.renderer.is_none() { info!("initializing wgpu renderer..."); - match pollster::block_on(Renderer::new()) { + match pollster::block_on(new_renderer()) { Ok(r) => self.renderer = Some(r), Err(e) => { warn!("failed to init renderer: {e:#}"); @@ -798,7 +815,7 @@ impl LayerShellHandler for App { let surface_config = wgpu::SurfaceConfiguration { usage: wgpu::TextureUsages::RENDER_ATTACHMENT, - format: wgpu::TextureFormat::Bgra8UnormSrgb, + format: renderer.format, width: buffer_w, height: buffer_h, present_mode: wgpu::PresentMode::Fifo, diff --git a/crates/render/Cargo.toml b/crates/render/Cargo.toml new file mode 100644 index 0000000..87da167 --- /dev/null +++ b/crates/render/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "shiftpaper-render" +description = "Shiftpaper's depth parallax renderer, with no Wayland code, for any front end" +version.workspace = true +edition.workspace = true +license.workspace = true + +[dependencies] +wgpu = { workspace = true } +bytemuck = { version = "1", features = ["derive"] } +# Depth maps go to the GPU as 16-bit floats. +half = { version = "2", features = ["bytemuck"] } + +image = { workspace = true, features = ["png"] } + +shiftpaper-config = { workspace = true } + +tracing = { workspace = true } +anyhow = { workspace = true } + +[dev-dependencies] +# Same version wgpu uses internally, to validate shader.wgsl in tests. +naga = { version = "30", features = ["wgsl-in"] } diff --git a/crates/daemon/src/depth.rs b/crates/render/src/depth.rs similarity index 98% rename from crates/daemon/src/depth.rs rename to crates/render/src/depth.rs index d94c835..99e41df 100644 --- a/crates/daemon/src/depth.rs +++ b/crates/render/src/depth.rs @@ -3,7 +3,7 @@ use image::imageops::{self, FilterType}; use std::path::Path; /// Depth map at the wallpaper's native resolution. -/// Values are u16 normalized, the GPU treats them as [0.0, 1.0] via R16Unorm. +/// Values are u16 normalized, from 0 (the farthest) to 1 (the nearest). pub struct DepthMap { pub data: Vec, pub width: u32, @@ -29,6 +29,13 @@ pub fn load_depth_map(path: &Path) -> Result { pub const RANK_POINTS: usize = 257; impl DepthMap { + /// The map as 16-bit floats from 0 to 1, as the GPU takes it. + pub fn to_f16(&self) -> Vec { + (self.data.iter()) + .map(|&d| half::f16::from_f32(f32::from(d) / f32::from(u16::MAX))) + .collect() + } + /// The map resized to `width` x `height`. pub fn resized(self, width: u32, height: u32) -> Self { let image = diff --git a/crates/render/src/lib.rs b/crates/render/src/lib.rs new file mode 100644 index 0000000..c2bed05 --- /dev/null +++ b/crates/render/src/lib.rs @@ -0,0 +1,9 @@ +//! Shiftpaper's depth parallax renderer. It draws baked wallpapers to wgpu +//! surfaces and knows nothing about Wayland, so any front end can use it: +//! shiftpaperd creates the surfaces from its layer shell windows. + +mod depth; +mod renderer; + +pub use renderer::{DecodedWallpaper, OutputRenderState, Renderer, Wallpaper, WallpaperFiles}; +pub use shiftpaper_config::Transition; diff --git a/crates/daemon/src/renderer.rs b/crates/render/src/renderer.rs similarity index 97% rename from crates/daemon/src/renderer.rs rename to crates/render/src/renderer.rs index 04a4023..88719d9 100644 --- a/crates/daemon/src/renderer.rs +++ b/crates/render/src/renderer.rs @@ -741,6 +741,8 @@ pub struct Renderer { pub adapter: wgpu::Adapter, pub device: wgpu::Device, pub queue: wgpu::Queue, + /// The format it draws in, which surfaces must be configured with. + pub format: wgpu::TextureFormat, pub pipeline: wgpu::RenderPipeline, pub bind_group_layout: wgpu::BindGroupLayout, pub sampler: wgpu::Sampler, @@ -749,27 +751,24 @@ pub struct Renderer { } impl Renderer { - pub async fn new() -> Result { - let instance = wgpu::Instance::new(wgpu::InstanceDescriptor { - backends: wgpu::Backends::VULKAN, - ..wgpu::InstanceDescriptor::new_without_display_handle() - }); - - let adapter = instance - .request_adapter(&wgpu::RequestAdapterOptions { - power_preference: wgpu::PowerPreference::LowPower, - ..Default::default() - }) - .await - .context("no suitable GPU adapter found")?; - + /// Draw with `adapter`, from `instance`, to surfaces in `format`. The + /// caller picks all three, so each front end can choose the backends + /// and format its platform offers. + pub async fn new( + instance: wgpu::Instance, + adapter: wgpu::Adapter, + format: wgpu::TextureFormat, + ) -> Result { info!(name = adapter.get_info().name, "using GPU"); let (device, queue) = adapter .request_device(&wgpu::DeviceDescriptor { label: Some("shiftpaper"), - required_features: wgpu::Features::TEXTURE_FORMAT_16BIT_NORM, - required_limits: wgpu::Limits::default(), + required_features: wgpu::Features::empty(), + // What even GLES phones manage, with the texture sizes this + // adapter supports. + required_limits: wgpu::Limits::downlevel_webgl2_defaults() + .using_resolution(adapter.limits()), ..Default::default() }) .await @@ -883,7 +882,7 @@ impl Renderer { module: &shader, entry_point: Some("fs_main"), targets: &[Some(wgpu::ColorTargetState { - format: wgpu::TextureFormat::Bgra8UnormSrgb, + format, blend: None, write_mask: wgpu::ColorWrites::ALL, })], @@ -916,6 +915,7 @@ impl Renderer { adapter, device, queue, + format, pipeline, bind_group_layout, sampler, @@ -924,8 +924,8 @@ impl Renderer { }) } - /// Create a uniform buffer for an output. Called once per output during - /// layer surface configure, and filled in by `step_and_write` before + /// Create a uniform buffer for an output. Called once per output when + /// its surface is set up, and filled in by `step_and_write` before /// the first draw. pub fn create_uniform_buffer(&self) -> wgpu::Buffer { self.device @@ -992,13 +992,16 @@ impl Renderer { depth_or_array_layers: 1, }; + // R16Unorm would hold the map as it is, but it's optional on phones. + // A 16-bit float loses precision only near 1, where it's a fraction + // of a pixel at any sensible intensity. let texture = self.device.create_texture(&wgpu::TextureDescriptor { label: Some("depth_map"), size, mip_level_count: 1, sample_count: 1, dimension: wgpu::TextureDimension::D2, - format: wgpu::TextureFormat::R16Unorm, + format: wgpu::TextureFormat::R16Float, usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST, view_formats: &[], }); @@ -1010,7 +1013,7 @@ impl Renderer { origin: wgpu::Origin3d::ZERO, aspect: wgpu::TextureAspect::All, }, - bytemuck::cast_slice(&depth.data), + bytemuck::cast_slice(&depth.to_f16()), wgpu::TexelCopyBufferLayout { offset: 0, bytes_per_row: Some(2 * depth.width), @@ -1117,8 +1120,8 @@ impl Renderer { /// so the caller can retry on the next frame callback. pub fn render_frame(&self, output: &OutputRenderState) -> bool { let frame = match output.surface.get_current_texture() { - // A suboptimal frame is still usable, and our size only changes on - // a compositor configure, which reconfigures the surface anyway. + // A suboptimal frame is still usable, and the front end + // reconfigures the surface whenever its size changes anyway. CurrentSurfaceTexture::Success(frame) | CurrentSurfaceTexture::Suboptimal(frame) => { frame } @@ -1182,7 +1185,8 @@ fn create_placeholder_depth(device: &wgpu::Device, queue: &wgpu::Queue) -> wgpu: mip_level_count: 1, sample_count: 1, dimension: wgpu::TextureDimension::D2, - format: wgpu::TextureFormat::R16Unorm, + // Zero as a 16-bit float is all zero bytes, like the data below. + format: wgpu::TextureFormat::R16Float, usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST, view_formats: &[], }); diff --git a/crates/daemon/src/shader.wgsl b/crates/render/src/shader.wgsl similarity index 100% rename from crates/daemon/src/shader.wgsl rename to crates/render/src/shader.wgsl diff --git a/packaging/aur/shiftpaper-git/PKGBUILD b/packaging/aur/shiftpaper-git/PKGBUILD index aa9461f..f2a4ee3 100644 --- a/packaging/aur/shiftpaper-git/PKGBUILD +++ b/packaging/aur/shiftpaper-git/PKGBUILD @@ -40,7 +40,8 @@ check() { cd "$pkgname" export RUSTUP_TOOLCHAIN=stable export CARGO_TARGET_DIR=target - cargo test --frozen --release --package shiftpaper-config --package shiftpaper-daemon + cargo test --frozen --release --package shiftpaper-config --package shiftpaper-render \ + --package shiftpaper-daemon cargo test --frozen --release --package shiftpaper-cli \ --no-default-features --features load-dynamic } diff --git a/packaging/aur/shiftpaper/PKGBUILD b/packaging/aur/shiftpaper/PKGBUILD index cb960b3..cbdc596 100644 --- a/packaging/aur/shiftpaper/PKGBUILD +++ b/packaging/aur/shiftpaper/PKGBUILD @@ -34,7 +34,8 @@ check() { cd "$pkgname-$pkgver" export RUSTUP_TOOLCHAIN=stable export CARGO_TARGET_DIR=target - cargo test --frozen --release --package shiftpaper-config --package shiftpaper-daemon + cargo test --frozen --release --package shiftpaper-config --package shiftpaper-render \ + --package shiftpaper-daemon cargo test --frozen --release --package shiftpaper-cli \ --no-default-features --features load-dynamic }