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 }