Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
19 changes: 16 additions & 3 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -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"
Expand All @@ -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"
8 changes: 2 additions & 6 deletions crates/daemon/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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"] }
2 changes: 0 additions & 2 deletions crates/daemon/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
mod battery;
mod config;
mod cursor;
mod depth;
mod idle;
mod renderer;
mod slideshow;
mod wayland;

Expand Down
2 changes: 1 addition & 1 deletion crates/daemon/src/slideshow.rs
Original file line number Diff line number Diff line change
@@ -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};
Expand Down
23 changes: 20 additions & 3 deletions crates/daemon/src/wayland.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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<Renderer> {
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,
Expand Down Expand Up @@ -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:#}");
Expand Down Expand Up @@ -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,
Expand Down
23 changes: 23 additions & 0 deletions crates/render/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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"] }
9 changes: 8 additions & 1 deletion crates/daemon/src/depth.rs → crates/render/src/depth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<u16>,
pub width: u32,
Expand All @@ -29,6 +29,13 @@ pub fn load_depth_map(path: &Path) -> Result<DepthMap> {
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<half::f16> {
(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 =
Expand Down
9 changes: 9 additions & 0 deletions crates/render/src/lib.rs
Original file line number Diff line number Diff line change
@@ -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;
52 changes: 28 additions & 24 deletions crates/daemon/src/renderer.rs → crates/render/src/renderer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -749,27 +751,24 @@ pub struct Renderer {
}

impl Renderer {
pub async fn new() -> Result<Self> {
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<Self> {
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
Expand Down Expand Up @@ -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,
})],
Expand Down Expand Up @@ -916,6 +915,7 @@ impl Renderer {
adapter,
device,
queue,
format,
pipeline,
bind_group_layout,
sampler,
Expand All @@ -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
Expand Down Expand Up @@ -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: &[],
});
Expand All @@ -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),
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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: &[],
});
Expand Down
File renamed without changes.
3 changes: 2 additions & 1 deletion packaging/aur/shiftpaper-git/PKGBUILD
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
Loading
Loading