From 67113fa5e9aeed34aa86d0fc04bec4b80c975eae Mon Sep 17 00:00:00 2001 From: tittu Date: Sat, 11 Jul 2026 12:03:27 +0530 Subject: [PATCH] animations --- shaders/blur.comp | 16 +- shaders/zoom.comp | 115 +++++++++ src/colors.rs | 3 +- src/filters.rs | 107 ++++++-- src/ipc.rs | 143 +++++++++++ src/main.rs | 189 ++++++++------ src/transitions.rs | 527 +++++++++++++++++++++++++++++++++++++++ src/vulkan.rs | 238 +++++++----------- src/wallbash.rs | 601 +++++++++++++++++++++++---------------------- 9 files changed, 1392 insertions(+), 547 deletions(-) create mode 100644 shaders/zoom.comp create mode 100644 src/ipc.rs create mode 100644 src/transitions.rs diff --git a/shaders/blur.comp b/shaders/blur.comp index df4affa..14ea38f 100644 --- a/shaders/blur.comp +++ b/shaders/blur.comp @@ -4,19 +4,19 @@ layout(local_size_x = 16, local_size_y = 16) in; layout(set = 0, binding = 0) uniform sampler2D inputTex; layout(set = 0, binding = 1, rgba8) uniform writeonly image2D outputTex; +layout(push_constant) uniform PC { int horizontal; } pc; + void main() { ivec2 gid = ivec2(gl_GlobalInvocationID.xy); vec2 size = vec2(imageSize(outputTex)); vec2 texelSize = 1.0 / size; vec4 color = vec4(0.0); - int count = 0; - for (int dx = -7; dx <= 7; dx++) { - for (int dy = -7; dy <= 7; dy++) { - vec2 offset = vec2(float(dx), float(dy)) * texelSize; - color += texture(inputTex, (vec2(gid) + 0.5) / size + offset); - count++; - } + for (int i = -7; i <= 7; i++) { + vec2 offset = pc.horizontal == 1 + ? vec2(float(i), 0.0) * texelSize + : vec2(0.0, float(i)) * texelSize; + color += texture(inputTex, (vec2(gid) + 0.5) / size + offset); } - color /= float(count); + color /= 15.0; imageStore(outputTex, gid, color); } diff --git a/shaders/zoom.comp b/shaders/zoom.comp new file mode 100644 index 0000000..721fd03 --- /dev/null +++ b/shaders/zoom.comp @@ -0,0 +1,115 @@ +#version 450 + +layout(local_size_x = 16, local_size_y = 16) in; + +layout(set = 0, binding = 0) uniform sampler2D img_old; +layout(set = 0, binding = 1) uniform sampler2D img_new; +layout(set = 0, binding = 2, rgba8) uniform writeonly image2D output_img; + +layout(push_constant) uniform PushConstants { + float t; // eased progress 0..1 + float max_zoom; // e.g. 0.2 = 20% zoom + float old_w; // old wallpaper width + float old_h; // old wallpaper height + float new_w; // new wallpaper width + float new_h; // new wallpaper height + float dst_w; // screen width + float dst_h; // screen height + int mode; // 0=cover, 1=fit, 2=original + float anchor_x; // 0..1 + float anchor_y; // 0..1 +} pc; + +void main() { + ivec2 coord = ivec2(gl_GlobalInvocationID.xy); + ivec2 size = imageSize(output_img); + if (coord.x >= size.x || coord.y >= size.y) return; + + float img_w, img_h; + bool use_old = (pc.t <= 0.5); + if (use_old) { + img_w = pc.old_w; + img_h = pc.old_h; + } else { + img_w = pc.new_w; + img_h = pc.new_h; + } + + float img_aspect = img_w / img_h; + float scr_aspect = pc.dst_w / pc.dst_h; + + vec2 src; + bool visible = true; + + if (pc.mode == 1) { + float scale = min(pc.dst_w / img_w, pc.dst_h / img_h); + vec2 sw = vec2(img_w, img_h) * scale; + vec2 off = (vec2(pc.dst_w, pc.dst_h) - sw) * vec2(pc.anchor_x, pc.anchor_y); + vec2 img_coord = vec2(coord) - off; + if (img_coord.x >= 0.0 && img_coord.x < sw.x && img_coord.y >= 0.0 && img_coord.y < sw.y) { + src = img_coord / scale; + } else { + visible = false; + } + } else if (pc.mode == 2) { + if (img_w <= pc.dst_w && img_h <= pc.dst_h) { + vec2 off = (vec2(pc.dst_w, pc.dst_h) - vec2(img_w, img_h)) + * vec2(pc.anchor_x, pc.anchor_y); + vec2 img_coord = vec2(coord) - off; + if (img_coord.x >= 0.0 && img_coord.x < img_w && img_coord.y >= 0.0 && img_coord.y < img_h) { + src = img_coord; + } else { + visible = false; + } + } else { + if (img_aspect > scr_aspect) { + float u_range = scr_aspect / img_aspect; + float u_min = (1.0 - u_range) * pc.anchor_x; + src.x = (u_min + u_range * (coord.x / pc.dst_w)) * img_w; + src.y = coord.y / pc.dst_h * img_h; + } else { + float v_range = img_aspect / scr_aspect; + float v_min = (1.0 - v_range) * pc.anchor_y; + src.x = coord.x / pc.dst_w * img_w; + src.y = (v_min + v_range * (coord.y / pc.dst_h)) * img_h; + } + } + } else { + if (img_aspect > scr_aspect) { + float u_range = scr_aspect / img_aspect; + float u_min = (1.0 - u_range) * pc.anchor_x; + src.x = (u_min + u_range * (coord.x / pc.dst_w)) * img_w; + src.y = coord.y / pc.dst_h * img_h; + } else { + float v_range = img_aspect / scr_aspect; + float v_min = (1.0 - v_range) * pc.anchor_y; + src.x = coord.x / pc.dst_w * img_w; + src.y = (v_min + v_range * (coord.y / pc.dst_h)) * img_h; + } + } + + vec4 color = vec4(0.0, 0.0, 0.0, 1.0); + if (visible) { + float zoom_scale; + if (use_old) { + float local_t = pc.t * 2.0; + zoom_scale = 1.0 + pc.max_zoom * local_t; + } else { + float local_t = (pc.t - 0.5) * 2.0; + zoom_scale = 1.0 + pc.max_zoom * (1.0 - local_t); + } + + vec2 center = vec2(img_w, img_h) * 0.5; + vec2 sample_coord = center + (src - center) / zoom_scale; + + vec2 uv = sample_coord / vec2(img_w, img_h); + if (use_old) { + color = texture(img_old, uv); + } else { + color = texture(img_new, uv); + } + } + + imageStore(output_img, coord, color); +} + diff --git a/src/colors.rs b/src/colors.rs index f4a1285..0c9d90d 100644 --- a/src/colors.rs +++ b/src/colors.rs @@ -615,6 +615,7 @@ fn deploy_palette(colors: &[ColorPalette]) { let mut handles = Vec::new(); for (out, cmd, rendered) in deployments { handles.push(std::thread::spawn(move || { + let t0 = std::time::Instant::now(); // resolve target file let resolved_path = out.as_deref().and_then(|p| eval_shell(p)); @@ -633,7 +634,6 @@ fn deploy_palette(colors: &[ColorPalette]) { eprintln!("[shell] failed to write {} {}", target, e); return; } - println!("[shell] deployed -> {}", target); // execute post deployment command if let Some(post_cmd) = &cmd { @@ -643,6 +643,7 @@ fn deploy_palette(colors: &[ColorPalette]) { eprintln!("[shell] failed to resolve {}", post_cmd); } } + println!("[shell] deployed in {:.2?} -> {}", t0.elapsed(), target); })); } diff --git a/src/filters.rs b/src/filters.rs index 3637cb5..648f928 100644 --- a/src/filters.rs +++ b/src/filters.rs @@ -52,22 +52,24 @@ pub fn compute_pipeline( device: &ash::Device, spv: &[u32], bindings: &[vk::DescriptorSetLayoutBinding], + push_constant_size: u32, // NEW ) -> Result<(vk::ShaderModule, vk::Pipeline, vk::DescriptorSetLayout), Box> { - - // shader module let create_info = vk::ShaderModuleCreateInfo::default().code(spv); let module = unsafe { device.create_shader_module(&create_info, None)? }; - - // descriptor set layout let layout_info = vk::DescriptorSetLayoutCreateInfo::default().bindings(bindings); let desc_layout = unsafe { device.create_descriptor_set_layout(&layout_info, None)? }; - - // pipeline layout let set_layouts = [desc_layout]; - let pipeline_layout_info = vk::PipelineLayoutCreateInfo::default().set_layouts(&set_layouts); + let push_range = vk::PushConstantRange { + stage_flags: vk::ShaderStageFlags::COMPUTE, + offset: 0, + size: push_constant_size, + }; + let push_ranges = [push_range]; + let mut pipeline_layout_info = vk::PipelineLayoutCreateInfo::default().set_layouts(&set_layouts); + if push_constant_size > 0 { + pipeline_layout_info = pipeline_layout_info.push_constant_ranges(&push_ranges); + } let pipeline_layout = unsafe { device.create_pipeline_layout(&pipeline_layout_info, None)? }; - - // compute pipeline let stage = vk::PipelineShaderStageCreateInfo::default() .stage(vk::ShaderStageFlags::COMPUTE) .module(module) @@ -79,7 +81,6 @@ pub fn compute_pipeline( device.create_compute_pipelines(vk::PipelineCache::null(), &[pipeline_info], None) }.expect("Failed to create compute pipeline"); let pipeline = pipelines[0]; - unsafe { device.destroy_pipeline_layout(pipeline_layout, None) }; Ok((module, pipeline, desc_layout)) } @@ -111,7 +112,7 @@ pub fn filter_pipeline( .descriptor_count(1) .stage_flags(vk::ShaderStageFlags::COMPUTE), ]; - compute_pipeline(device, &blur_words, &bindings) + compute_pipeline(device, &blur_words, &bindings, 4) } _ => Err(format!("unknown filter: {}", filter).into()), } @@ -122,12 +123,13 @@ pub fn filter_pipeline( pub unsafe fn compute_filter( vk_core: &VulkanCore, - _input_image: vk::Image, output_image: vk::Image, width: u32, height: u32, pipeline: vk::Pipeline, descriptor_set_layout: vk::DescriptorSetLayout, + push_constants: &[u8], + dst_stage: vk::PipelineStageFlags, configure: impl FnOnce(vk::DescriptorSet), ) -> Result<(), Box> { let pool_sizes = [ @@ -141,12 +143,20 @@ pub unsafe fn compute_filter( let alloc_info = vk::DescriptorSetAllocateInfo::default() .descriptor_pool(desc_pool) .set_layouts(&set_layouts); - let desc_sets = unsafe { vk_core.device.allocate_descriptor_sets(&alloc_info)? }; - let desc_set = desc_sets[0]; + let desc_set = unsafe { vk_core.device.allocate_descriptor_sets(&alloc_info)? }[0]; configure(desc_set); - let pipeline_layout_info = vk::PipelineLayoutCreateInfo::default().set_layouts(&set_layouts); + let push_range = vk::PushConstantRange { + stage_flags: vk::ShaderStageFlags::COMPUTE, + offset: 0, + size: push_constants.len() as u32, + }; + let push_ranges = [push_range]; + let mut pipeline_layout_info = vk::PipelineLayoutCreateInfo::default().set_layouts(&set_layouts); + if !push_constants.is_empty() { + pipeline_layout_info = pipeline_layout_info.push_constant_ranges(&push_ranges); + } let pipeline_layout = unsafe { vk_core.device.create_pipeline_layout(&pipeline_layout_info, None)? }; vk_core.record_commands(|command_buffer| { @@ -179,6 +189,15 @@ pub unsafe fn compute_filter( pipeline_layout, 0, &[desc_set], &[], ); + if !push_constants.is_empty() { + vk_core.device.cmd_push_constants( + command_buffer, + pipeline_layout, + vk::ShaderStageFlags::COMPUTE, + 0, + push_constants, + ); + } } let group_x = (width + 15) / 16; @@ -200,7 +219,7 @@ pub unsafe fn compute_filter( vk_core.device.cmd_pipeline_barrier( command_buffer, vk::PipelineStageFlags::COMPUTE_SHADER, - vk::PipelineStageFlags::FRAGMENT_SHADER, + dst_stage, vk::DependencyFlags::empty(), &[], &[], &[barrier2], ); @@ -226,29 +245,72 @@ pub fn blur_texture( blur_pipeline: vk::Pipeline, blur_desc_layout: vk::DescriptorSetLayout, ) -> Result> { + let (output_image, output_memory) = vk_core.create_texture( width, height, vk::ImageUsageFlags::TRANSFER_SRC | vk::ImageUsageFlags::STORAGE, vk::Format::R8G8B8A8_UNORM, )?; + let (mid_image, mid_memory) = vk_core.create_texture( + width, height, + vk::ImageUsageFlags::SAMPLED | vk::ImageUsageFlags::STORAGE, + vk::Format::R8G8B8A8_UNORM, + )?; + let input_view = image_view(&vk_core.device, input_texture.image, vk::Format::R8G8B8A8_SRGB)?; + let mid_view = image_view(&vk_core.device, mid_image, vk::Format::R8G8B8A8_UNORM)?; let output_view = image_view(&vk_core.device, output_image, vk::Format::R8G8B8A8_UNORM)?; let sampler = linear_sampler(&vk_core.device)?; unsafe { compute_filter( vk_core, - input_texture.image, - output_image, + mid_image, width, height, blur_pipeline, blur_desc_layout, + &1i32.to_ne_bytes(), + vk::PipelineStageFlags::COMPUTE_SHADER, |desc_set| { let input_info = vk::DescriptorImageInfo::default() .sampler(sampler) .image_view(input_view) .image_layout(vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL); + let output_info = vk::DescriptorImageInfo::default() + .image_view(mid_view) + .image_layout(vk::ImageLayout::GENERAL); + let input_infos = [input_info]; + let output_infos = [output_info]; + let writes = [ + vk::WriteDescriptorSet::default() + .dst_set(desc_set).dst_binding(0) + .descriptor_type(vk::DescriptorType::COMBINED_IMAGE_SAMPLER) + .image_info(&input_infos), + vk::WriteDescriptorSet::default() + .dst_set(desc_set).dst_binding(1) + .descriptor_type(vk::DescriptorType::STORAGE_IMAGE) + .image_info(&output_infos), + ]; + vk_core.device.update_descriptor_sets(&writes, &[]); + }, + )?; + } + + unsafe { + compute_filter( + vk_core, + output_image, + width, height, + blur_pipeline, + blur_desc_layout, + &0i32.to_ne_bytes(), + vk::PipelineStageFlags::FRAGMENT_SHADER, + |desc_set| { + let input_info = vk::DescriptorImageInfo::default() + .sampler(sampler) + .image_view(mid_view) + .image_layout(vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL); let output_info = vk::DescriptorImageInfo::default() .image_view(output_view) .image_layout(vk::ImageLayout::GENERAL); @@ -256,13 +318,11 @@ pub fn blur_texture( let output_infos = [output_info]; let writes = [ vk::WriteDescriptorSet::default() - .dst_set(desc_set) - .dst_binding(0) + .dst_set(desc_set).dst_binding(0) .descriptor_type(vk::DescriptorType::COMBINED_IMAGE_SAMPLER) .image_info(&input_infos), vk::WriteDescriptorSet::default() - .dst_set(desc_set) - .dst_binding(1) + .dst_set(desc_set).dst_binding(1) .descriptor_type(vk::DescriptorType::STORAGE_IMAGE) .image_info(&output_infos), ]; @@ -273,8 +333,11 @@ pub fn blur_texture( unsafe { vk_core.device.destroy_image_view(input_view, None); + vk_core.device.destroy_image_view(mid_view, None); vk_core.device.destroy_image_view(output_view, None); vk_core.device.destroy_sampler(sampler, None); + vk_core.device.destroy_image(mid_image, None); + vk_core.device.free_memory(mid_memory, None); } Ok(VulkanTexture { image: output_image, _memory: output_memory, width, height }) diff --git a/src/ipc.rs b/src/ipc.rs new file mode 100644 index 0000000..bfe6060 --- /dev/null +++ b/src/ipc.rs @@ -0,0 +1,143 @@ +// --------------------------------------------------------------------- / tittu +// wallbash +// an inter process communication module for HyDE +// + + +// --------------------------------------------------------------------- / imports + +use std::{ + os::unix::net::{UnixListener, UnixStream}, + io::{BufRead, BufReader}, + sync::mpsc, +}; + + +// --------------------------------------------------------------------- / datatypes + +#[derive(Debug, PartialEq)] +pub enum ScalingMode { + Cover, + Fit, + Original, +} + +#[derive(Debug, PartialEq)] +pub enum PaletteMode { + Auto, + Dark, + Light, + Skip, +} + +pub struct IpcMessage { + pub cmd: String, + pub stream: UnixStream, +} + +pub enum Command { + Stop, + Set { + palette: PaletteMode, + bezier: String, + scale: ScalingMode, + anchor_x: f32, + anchor_y: f32, + path: String, + } +} + + +// --------------------------------------------------------------------- / parser + +impl ScalingMode { + pub fn from_str(s: &str) -> Self { + match s.trim().to_lowercase().as_str() { + "fit" => Self::Fit, + "original" => Self::Original, + _ => Self::Cover, + } + } + pub fn as_str(&self) -> &'static str { + match self { + Self::Cover => "cover", + Self::Fit => "fit", + Self::Original => "original", + } + } +} + +impl PaletteMode { + pub fn from_str(s: &str) -> Self { + match s.trim().to_lowercase().as_str() { + "auto" => Self::Auto, + "dark" => Self::Dark, + "light" => Self::Light, + _ => Self::Skip, + } + } + pub fn as_str(&self) -> &'static str { + match self { + Self::Auto => "auto", + Self::Dark => "dark", + Self::Light => "light", + Self::Skip => "skip", + } + } +} + +impl Command { + pub fn parse_raw(raw: &str) -> Result { + let raw = raw.trim(); + if raw == "stop" { return Ok(Command::Stop); } + if raw.starts_with("set") { + let payload = &raw[3..]; + let mut parts = payload.splitn(6, '\x01'); + let palette = PaletteMode::from_str(parts.next().ok_or("missing palette")?); + let bezier = parts.next().ok_or("missing bezier")?.to_string(); + let scale = ScalingMode::from_str(parts.next().ok_or("missing mode")?); + let anchor_x = parts.next().ok_or("missing anchor_x")?.parse().map_err(|_| "invalid anchor_x")?; + let anchor_y = parts.next().ok_or("missing anchor_y")?.parse().map_err(|_| "invalid anchor_y")?; + let path = parts.next().ok_or("missing path")?.to_string(); + return Ok(Command::Set { palette, bezier, scale, anchor_x, anchor_y, path}); + } + Err(format!("unknown internal command: {}", raw)) + } +} + + +// --------------------------------------------------------------------- / listener + +pub fn start_ipc(socket: &str) -> Result, Box> { + + // remove any stale socket from previous run + let _ = std::fs::remove_file(socket); + + // create listener and channel + let listener = UnixListener::bind(socket)?; + let (tx, rx) = mpsc::channel::(); + println!("[ipc] listening: {}", socket); + + // start listener thread + std::thread::spawn(move || { for stream in listener.incoming() { + match stream { + + // read and send the message + Ok(stream) => { + let reader = BufReader::new(&stream); + if let Some(Ok(cmd)) = reader.lines().next() { + let cmd = cmd.trim().to_string(); + if !cmd.is_empty() { + if tx.send(IpcMessage { cmd, stream }).is_err() { return; } + } + } + } + Err(e) => { + eprintln!("[ipc] accept error: {}", e); + break; + } + } + }}); + Ok(rx) +} + diff --git a/src/main.rs b/src/main.rs index 253f934..07471bb 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,33 +7,35 @@ // --------------------------------------------------------------------- / imports pub mod wallbash; +pub mod ipc; pub mod wayland; pub mod vulkan; pub mod filters; +pub mod transitions; pub mod colors; use std::{ - env, io::Write, + fs, env, error, process, os::unix::net::UnixStream, - process::Command, - thread::sleep, - time::Duration, + io::{Write,Read}, path::PathBuf, + time::Duration, + thread::sleep, }; // --------------------------------------------------------------------- / datatypes +const SOCKET: &str = "/tmp/wallbash.sock"; + struct CachedState { wall: String, palette: String, + bezier: String, mode: String, anchor_x: f32, anchor_y: f32, } -const SOCKET_PATH: &str = "/tmp/wallbash.sock"; -const LOG_FILE: &str = "/tmp/wallbash.log"; - // --------------------------------------------------------------------- / help @@ -47,9 +49,10 @@ fn print_usage() { ::Options wallbash set [option] - -w, --wall | Wallpaper file /path/to/file.img + -w, --wall | Wallpaper file '/path/to/file.img' -c, --cycle | Cycle in current folder (+1, -2, etc.) -p, --palette | Generate color palette (auto, dark, light) + -b, --bezier | Custom animation curve (ex. '0.64,0.56,0.17,0.84') -m, --mode | Scaling mode (cover, fit, original) -a, --anchor <1-9> | Anchor point (1=top-left ... 9=bottom-right) " ); @@ -58,75 +61,98 @@ fn print_usage() { // --------------------------------------------------------------------- / sock -fn send_command(cmd: &str) -> Result<(), Box> { - let mut stream = UnixStream::connect(SOCKET_PATH)?; +fn send_command(cmd: &str) -> Result<(), Box> { + let mut stream = UnixStream::connect(SOCKET)?; writeln!(stream, "{}", cmd)?; + if cmd.starts_with("set") { + let mut buf = [0u8; 1]; + stream.read_exact(&mut buf)?; // hey daemon, are you done? + } Ok(()) } fn check_daemon() -> bool { - UnixStream::connect(SOCKET_PATH).is_ok() + UnixStream::connect(SOCKET).is_ok() } -fn wait_loop() -> Result<(), Box> { +fn wait_loop() -> Result<(), Box> { for _ in 0..100 { if check_daemon() { return Ok(()); } sleep(Duration::from_millis(100)); } - Err("Waiting for daemon...".into()) + Err("waiting for daemon...".into()) +} + + +// --------------------------------------------------------------------- / log + +fn cache_dir() -> PathBuf { + let base = env::var("XDG_CACHE_HOME").ok().or_else(|| env::var("HOME") + .ok().map(|home| format!("{}/.cache", home))).unwrap_or_default(); + let dir = PathBuf::from(base).join("wallbash"); + let _ = fs::create_dir_all(&dir); + dir +} + +fn cache_log() -> fs::File { + let path = cache_dir().join("wallbash.log"); + fs::File::create(&path).expect("cannot create log") } -// --------------------------------------------------------------------- / cache +// --------------------------------------------------------------------- / state -fn cache_file() -> PathBuf { - let cache = env::var("XDG_CACHE_HOME").ok().or_else(|| env::var("HOME") - .ok().map(|home| format!("{}/.cache", home))).unwrap_or_default(); - PathBuf::from(cache).join("wallbash/state") +impl CachedState { + fn default() -> Self { + Self { + wall: String::new(), + palette: "skip".into(), + bezier: "0.64,0.56,0.17,0.84".into(), + mode: "cover".into(), + anchor_x: 0.5, + anchor_y: 0.5, + } + } +} + +fn cache_state() -> PathBuf { + cache_dir().join("state") } -fn save_cache(state: &CachedState) { - let resolved = std::fs::canonicalize(&state.wall) +fn save_state(state: &CachedState) { + let resolved = fs::canonicalize(&state.wall) .map(|p| p.to_string_lossy().to_string()) .unwrap_or_else(|_| state.wall.clone()); let content = format!( - "{}\n{}\n{}\n{}\n{}", - resolved, state.palette, state.mode, state.anchor_x, state.anchor_y + "{}\n{}\n{}\n{}\n{}\n{}", + resolved, state.palette, state.bezier, state.mode, state.anchor_x, state.anchor_y ); - if let Some(parent) = cache_file().parent() { - let _ = std::fs::create_dir_all(parent); - } - let _ = std::fs::write(cache_file(), content); + let _ = fs::write(cache_state(), content); } -fn load_cache() -> CachedState { - let default = CachedState { - wall: String::new(), - palette: "skip".into(), - mode: "cover".into(), - anchor_x: 0.5, - anchor_y: 0.5, - }; - let content = match std::fs::read_to_string(cache_file()) { +fn load_state() -> CachedState { + let content = match fs::read_to_string(cache_state()) { Ok(c) => c, - Err(_) => return default, + Err(_) => return CachedState::default(), }; + let default = CachedState::default(); let mut lines = content.lines(); - let wall = lines.next().map(|s| s.to_string()).unwrap_or_else(|| default.wall.clone()); - let palette = lines.next().map(|s| s.to_string()).unwrap_or_else(|| default.palette.clone()); - let mode = lines.next().map(|s| s.to_string()).unwrap_or_else(|| default.mode.clone()); + let wall = lines.next().map(|s| s.to_string()).unwrap_or(default.wall); + let palette = lines.next().map(|s| s.to_string()).unwrap_or(default.palette); + let bezier = lines.next().map(|s| s.to_string()).unwrap_or(default.bezier); + let mode = lines.next().map(|s| s.to_string()).unwrap_or(default.mode); let anchor_x: f32 = lines.next().and_then(|s| s.parse().ok()).unwrap_or(default.anchor_x); let anchor_y: f32 = lines.next().and_then(|s| s.parse().ok()).unwrap_or(default.anchor_y); - CachedState { wall, palette, mode, anchor_x, anchor_y } + CachedState { wall, palette, bezier, mode, anchor_x, anchor_y } } // --------------------------------------------------------------------- / cycle -fn scan_images(dir: &std::path::Path) -> Vec { - let mut files: Vec = match std::fs::read_dir(dir) { +fn scan_images(dir: &std::path::Path) -> Vec { + let mut files: Vec = match fs::read_dir(dir) { Ok(entries) => entries.filter_map(|e| e.ok()).map(|e| e.path()) .filter(|p| { p.extension().and_then(|e| e.to_str()) @@ -143,15 +169,15 @@ fn cycle_wallpaper(current: &str, cycle: i32) -> String { // resolve parent dir let dir = std::path::Path::new(¤t).parent().unwrap_or_else(|| { - eprintln!("Cached directory not found"); - std::process::exit(1); + eprintln!("[error] cached directory not found"); + process::exit(1); }); // scan parent dir let images = scan_images(dir); if images.is_empty() { - eprintln!("No images found in {:?}", dir); - std::process::exit(1); + eprintln!("[error] no images found in {:?}", dir); + process::exit(1); } // cycle logic @@ -167,7 +193,8 @@ fn cycle_wallpaper(current: &str, cycle: i32) -> String { fn parse_args(args: &[String]) -> CachedState { // get previous state - let mut state = load_cache(); + let mut state = load_state(); + let default = CachedState::default(); // wallpaper – default "cached" let wall = args.iter().position(|a| a == "--wall" || a == "-w") @@ -186,9 +213,8 @@ fn parse_args(args: &[String]) -> CachedState { }).flatten().last() }); if let Some(wall) = wall { - let resolved = std::fs::canonicalize(&wall) - .map(|p| p.to_string_lossy().to_string()) - .unwrap_or(wall); + let resolved = fs::canonicalize(&wall) + .map(|p| p.to_string_lossy().to_string()).unwrap_or(wall); state.wall = resolved; } @@ -199,30 +225,37 @@ fn parse_args(args: &[String]) -> CachedState { .unwrap_or(0); if cycle != 0 { if state.wall.is_empty() { - eprintln!("No cached wallpaper"); - std::process::exit(1); + eprintln!("[error] no cached wallpaper"); + process::exit(1); } state.wall = cycle_wallpaper(&state.wall, cycle); } if state.wall.is_empty() { - eprintln!("Missing wallpaper (use --wall or bare path)"); + eprintln!("[error] missing wallpaper (use --wall or bare path)"); print_usage(); - std::process::exit(1); + process::exit(1); } // color generation - default "skip" if let Some(pos) = args.iter().position(|a| a == "--palette" || a == "-p") { let pal = args.get(pos + 1) .filter(|s| matches!(s.as_str(), "auto" | "dark" | "light")) - .map(|s| s.clone()).unwrap_or_else(|| "skip".into()); + .map(|s| s.clone()).unwrap_or_else(|| default.palette); state.palette = pal; } + // bezier curve - default "linear" + if let Some(pos) = args.iter().position(|a| a == "--bezier" || a == "-b") { + let val = args.get(pos + 1) + .map(|s| s.clone()).unwrap_or_else(|| default.bezier); + state.bezier = val; + } + // mode – default "cover" if let Some(pos) = args.iter().position(|a| a == "--mode" || a == "-m") { let m = args.get(pos + 1) .filter(|s| matches!(s.as_str(), "cover" | "fit" | "original")) - .map(|s| s.clone()).unwrap_or_else(|| "cover".into()); + .map(|s| s.clone()).unwrap_or_else(|| default.mode); state.mode = m; } @@ -241,14 +274,14 @@ fn parse_args(args: &[String]) -> CachedState { Some(7) => (0.0, 1.0), Some(8) => (0.5, 1.0), Some(9) => (1.0, 1.0), - _ => (0.5, 0.5), + _ => (default.anchor_x, default.anchor_y), }; state.anchor_x = ax; state.anchor_y = ay; } - // cache and return state - save_cache(&state); + // save and return + save_state(&state); state } @@ -258,52 +291,64 @@ fn parse_args(args: &[String]) -> CachedState { fn main() { let args: Vec = env::args().collect(); match args.get(1).map(|s| s.as_str()) { + + // hey, do your job! Some("start") => { if check_daemon() { - eprintln!("Daemon is already running."); + eprintln!("[wallbash] daemon is already running..."); return; } - if let Err(e) = wallbash::run(SOCKET_PATH) { - eprintln!("Failed to start daemon {}", e); + if let Err(e) = wallbash::run(SOCKET) { + eprintln!("[error] {}", e); } } + + // your wish is my command! Some("set") => { let state = parse_args(&args); - let cmd = format!("set{}\x01{}\x01{}\x01{}\x01{}", - state.palette, state.mode, state.anchor_x, state.anchor_y, state.wall); + let cmd = format!("set{}\x01{}\x01{}\x01{}\x01{}\x01{}", + state.palette, state.bezier, state.mode, state.anchor_x, state.anchor_y, state.wall); + + // hey daemon, wake up! if !check_daemon() { - println!("Starting daemon"); - let log = std::fs::File::create(LOG_FILE).expect("Cannot create log"); - let mut child = Command::new(env::current_exe().unwrap()) + println!("[wallbash] starting daemon..."); + let log = cache_log(); + let mut child = process::Command::new(env::current_exe().unwrap()) .arg("start").stdout(log.try_clone().unwrap()).stderr(log) - .spawn().expect("Failed to start daemon"); + .spawn().expect("[error] failed to start daemon!"); if let Err(e) = wait_loop() { - eprintln!("Error {}", e); + eprintln!("[error] {}", e); let _ = child.kill(); return; } } if let Err(e) = send_command(&cmd) { - eprintln!("Failed to set wallpaper {}. Is the daemon running?", e); + eprintln!("[error] {}. Is it even running?", e); } } + + // stop it, enough! Some("stop") => { + println!("[wallbash] goodbye..."); if let Err(e) = send_command("stop") { - eprintln!("Failed to stop daemon {}. Is it even running?", e); + eprintln!("[error] {}. Is it even running?", e); } } + + // hey, are you alive? Some("status") => { if check_daemon() { println!("[wallbash] :: Daemon is running"); } else { println!("[wallbash] :: Daemon is not running"); } - let state = load_cache(); + let state = load_state(); if state.wall.is_empty() { println!("[wallbash] :: No wallpaper cached yet"); } else { println!("Wallpaper :: {}", state.wall); println!("Palette :: {}", state.palette); + println!("bezier :: {}", state.bezier); println!("Mode :: {}", state.mode); println!("Anchor :: ({:.1}, {:.1})", state.anchor_x, state.anchor_y); } diff --git a/src/transitions.rs b/src/transitions.rs new file mode 100644 index 0000000..40dd8e6 --- /dev/null +++ b/src/transitions.rs @@ -0,0 +1,527 @@ +// --------------------------------------------------------------------- / tittu +// wallbash +// a transitions module for HyDE +// + + +// --------------------------------------------------------------------- / imports + +use crate::{ + filters::{image_view, linear_sampler}, + vulkan::{VulkanCore, VulkanTexture}, +}; +use ash::vk; +use std::{error::Error, time::Instant}; + + +// --------------------------------------------------------------------- / datatypes + +pub struct CubicBezier { + x1: f32, y1: f32, + x2: f32, y2: f32, +} + +pub struct TransitionConfig { + pub kind: String, + pub duration_ms: u64, + pub bezier: CubicBezier, +} + +pub enum TransitionResources { + None, + Zoom(ZoomFrameResources), +} + +pub struct TransitionCore { + pub cfg: TransitionConfig, + pub prev: VulkanTexture, + pub start: Instant, + pub total_frames: u32, + pub current_frame: u32, + pub scale: String, + pub anchor_x: f32, + pub anchor_y: f32, + pub background: Option, + pub resources: TransitionResources, +} + +pub struct ZoomFrameResources { + sampler: vk::Sampler, + prev_view: vk::ImageView, + next_view: vk::ImageView, + output_view: vk::ImageView, + output_image: vk::Image, + desc_pool: vk::DescriptorPool, + desc_set: vk::DescriptorSet, + old_w: f32, old_h: f32, + new_w: f32, new_h: f32, + dst_w: f32, dst_h: f32, +} + +pub struct TransitionNone; + +pub struct TransitionPipeline { + pub module: vk::ShaderModule, + pub pipeline: vk::Pipeline, + pub desc_layout: vk::DescriptorSetLayout, + pub pipe_layout: vk::PipelineLayout, +} + +pub struct TransitionZoom { + pipeline: TransitionPipeline, +} + +pub struct TransitionRegistry { + transitions: Vec>, +} + + +// --------------------------------------------------------------------- / bezier + +impl CubicBezier { + pub fn new(x1: f32, y1: f32, x2: f32, y2: f32) -> Self { + Self { x1, y1, x2, y2 } + } + + pub fn apply(&self, t: f32) -> f32 { + if t <= 0.0 { return 0.0; } + if t >= 1.0 { return 1.0; } + let mut s = t; + for _ in 0..8 { + let x = Self::sample(self.x1, self.x2, s) - t; + let dx = Self::sample_dx(self.x1, self.x2, s); + if dx.abs() < 1e-6 { break; } + s -= x / dx; + s = s.clamp(0.0, 1.0); + } + Self::sample(self.y1, self.y2, s) + } + + fn sample(a: f32, b: f32, s: f32) -> f32 { + let s2 = s * s; + let s3 = s2 * s; + let t = 1.0 - s; + 3.0 * t * t * s * a + 3.0 * t * s2 * b + s3 + } + + fn sample_dx(a: f32, b: f32, s: f32) -> f32 { + let s2 = s * s; + let t = 1.0 - s; + 3.0 * t * t * a + 6.0 * t * s * (b - a) + 3.0 * s2 * (1.0 - b) + } +} + + +// --------------------------------------------------------------------- / config + +impl TransitionConfig { + pub fn parse(kind: &str, duration_ms: u64, bezier_str: &str) -> Self { + let bezier = if bezier_str.is_empty() { + CubicBezier::new(0.0, 0.0, 1.0, 1.0) + } else { + let parts: Vec = bezier_str.split(',') + .filter_map(|p| p.trim().parse().ok()) + .collect(); + if parts.len() == 4 { + CubicBezier::new( + parts[0].clamp(0.0, 1.0), parts[1], + parts[2].clamp(0.0, 1.0), parts[3], + ) + } else { + CubicBezier::new(0.0, 0.0, 1.0, 1.0) + } + }; + Self { kind: kind.to_string(), duration_ms, bezier } + } +} + + +// --------------------------------------------------------------------- / pipeline + +impl TransitionPipeline { + + pub fn new(device: &ash::Device, spv: &[u32]) -> Result> { + let module = unsafe { + device.create_shader_module( + &vk::ShaderModuleCreateInfo::default().code(spv), None, + )? + }; + + let bindings = [ + vk::DescriptorSetLayoutBinding::default() + .binding(0) + .descriptor_type(vk::DescriptorType::COMBINED_IMAGE_SAMPLER) + .descriptor_count(1) + .stage_flags(vk::ShaderStageFlags::COMPUTE), + vk::DescriptorSetLayoutBinding::default() + .binding(1) + .descriptor_type(vk::DescriptorType::COMBINED_IMAGE_SAMPLER) + .descriptor_count(1) + .stage_flags(vk::ShaderStageFlags::COMPUTE), + vk::DescriptorSetLayoutBinding::default() + .binding(2) + .descriptor_type(vk::DescriptorType::STORAGE_IMAGE) + .descriptor_count(1) + .stage_flags(vk::ShaderStageFlags::COMPUTE), + ]; + let desc_layout = unsafe { + device.create_descriptor_set_layout( + &vk::DescriptorSetLayoutCreateInfo::default().bindings(&bindings), None, + )? + }; + + let push_range = vk::PushConstantRange { + stage_flags: vk::ShaderStageFlags::COMPUTE, + offset: 0, + size: 44, + }; + let set_layouts = [desc_layout]; + let pipe_layout = unsafe { + device.create_pipeline_layout( + &vk::PipelineLayoutCreateInfo::default() + .set_layouts(&set_layouts) + .push_constant_ranges(std::slice::from_ref(&push_range)), + None, + )? + }; + + let stage = vk::PipelineShaderStageCreateInfo::default() + .stage(vk::ShaderStageFlags::COMPUTE) + .module(module) + .name(c"main"); + let pipeline_info = vk::ComputePipelineCreateInfo::default() + .stage(stage) + .layout(pipe_layout); + let pipelines = unsafe { + device.create_compute_pipelines(vk::PipelineCache::null(), &[pipeline_info], None) + }.map_err(|(_, e)| e)?; + + Ok(Self { module, pipeline: pipelines[0], desc_layout, pipe_layout }) + } + + pub fn destroy(&self, device: &ash::Device) { + unsafe { + device.destroy_pipeline(self.pipeline, None); + device.destroy_pipeline_layout(self.pipe_layout, None); + device.destroy_descriptor_set_layout(self.desc_layout, None); + device.destroy_shader_module(self.module, None); + } + } +} + + +// --------------------------------------------------------------------- / interface + +pub trait Transition: Send + Sync { + fn name(&self) -> &'static str; + + fn prepare( + &self, + vk_core: &VulkanCore, + prev: &VulkanTexture, + next: &VulkanTexture, + output: &VulkanTexture, + ) -> Result>; + + fn render_frame( + &self, + vk_core: &VulkanCore, + resources: &TransitionResources, + t: f32, + mode: &str, + anchor_x: f32, + anchor_y: f32, + ) -> Result<(), Box>; + + fn cleanup(&self, device: &ash::Device, resources: TransitionResources); + fn destroy(&self, device: &ash::Device); +} + + +// --------------------------------------------------------------------- / instant cut + +impl Transition for TransitionNone { + fn name(&self) -> &'static str { "none" } + fn destroy(&self, _: &ash::Device) {} + fn cleanup(&self, _device: &ash::Device, _resources: TransitionResources) {} + + fn prepare( + &self, + vk_core: &VulkanCore, + _prev: &VulkanTexture, + next: &VulkanTexture, + output: &VulkanTexture, + ) -> Result> { + vk_core.record_commands(|cmd| { + + barrier(vk_core, cmd, next.image, + vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL, vk::ImageLayout::TRANSFER_SRC_OPTIMAL, + vk::AccessFlags::SHADER_READ, vk::AccessFlags::TRANSFER_READ, + vk::PipelineStageFlags::TOP_OF_PIPE, vk::PipelineStageFlags::TRANSFER, + ); + barrier(vk_core, cmd, output.image, + vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL, vk::ImageLayout::TRANSFER_DST_OPTIMAL, + vk::AccessFlags::SHADER_READ, vk::AccessFlags::TRANSFER_WRITE, + vk::PipelineStageFlags::TOP_OF_PIPE, vk::PipelineStageFlags::TRANSFER, + ); + + let region = vk::ImageCopy::default() + .src_subresource(vk::ImageSubresourceLayers { aspect_mask: vk::ImageAspectFlags::COLOR, mip_level: 0, base_array_layer: 0, layer_count: 1 }) + .dst_subresource(vk::ImageSubresourceLayers { aspect_mask: vk::ImageAspectFlags::COLOR, mip_level: 0, base_array_layer: 0, layer_count: 1 }) + .extent(vk::Extent3D { width: next.width, height: next.height, depth: 1 }); + unsafe { + vk_core.device.cmd_copy_image( + cmd, + next.image, vk::ImageLayout::TRANSFER_SRC_OPTIMAL, + output.image, vk::ImageLayout::TRANSFER_DST_OPTIMAL, + &[region], + ); + } + + barrier(vk_core, cmd, next.image, + vk::ImageLayout::TRANSFER_SRC_OPTIMAL, vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL, + vk::AccessFlags::TRANSFER_READ, vk::AccessFlags::SHADER_READ, + vk::PipelineStageFlags::TRANSFER, vk::PipelineStageFlags::COMPUTE_SHADER, + ); + barrier(vk_core, cmd, output.image, + vk::ImageLayout::TRANSFER_DST_OPTIMAL, vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL, + vk::AccessFlags::TRANSFER_WRITE, vk::AccessFlags::SHADER_READ, + vk::PipelineStageFlags::TRANSFER, vk::PipelineStageFlags::COMPUTE_SHADER, + ); + })?; + + Ok(TransitionResources::None) + } + + fn render_frame( + &self, + _vk_core: &VulkanCore, + _resources: &TransitionResources, + _t: f32, + _mode: &str, + _anchor_x: f32, + _anchor_y: f32, + ) -> Result<(), Box> { + Ok(()) + } +} + + +// --------------------------------------------------------------------- / shared dispatch + +fn dispatch_zoom_frame( + vk_core: &VulkanCore, + pipeline: &TransitionPipeline, + res: &ZoomFrameResources, + t: f32, + max_zoom: f32, + mode: &str, + anchor_x: f32, + anchor_y: f32, +) -> Result<(), Box> { + let device = &vk_core.device; + let w = res.dst_w as u32; + let h = res.dst_h as u32; + + vk_core.record_commands(|cmd| { + barrier(vk_core, cmd, res.output_image, + vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL, vk::ImageLayout::GENERAL, + vk::AccessFlags::SHADER_READ, vk::AccessFlags::SHADER_WRITE, + vk::PipelineStageFlags::COMPUTE_SHADER, vk::PipelineStageFlags::COMPUTE_SHADER, + ); + + unsafe { + device.cmd_bind_pipeline(cmd, vk::PipelineBindPoint::COMPUTE, pipeline.pipeline); + device.cmd_bind_descriptor_sets(cmd, vk::PipelineBindPoint::COMPUTE, pipeline.pipe_layout, 0, &[res.desc_set], &[]); + + let mode_id: i32 = match mode { + "cover" => 0, + "fit" => 1, + "original" => 2, + _ => 0, + }; + let mut push_data = [0u8; 44]; + push_data[ 0.. 4].copy_from_slice(&t.to_ne_bytes()); + push_data[ 4.. 8].copy_from_slice(&max_zoom.to_ne_bytes()); + push_data[ 8..12].copy_from_slice(&res.old_w.to_ne_bytes()); + push_data[12..16].copy_from_slice(&res.old_h.to_ne_bytes()); + push_data[16..20].copy_from_slice(&res.new_w.to_ne_bytes()); + push_data[20..24].copy_from_slice(&res.new_h.to_ne_bytes()); + push_data[24..28].copy_from_slice(&res.dst_w.to_ne_bytes()); + push_data[28..32].copy_from_slice(&res.dst_h.to_ne_bytes()); + push_data[32..36].copy_from_slice(&mode_id.to_ne_bytes()); + push_data[36..40].copy_from_slice(&anchor_x.to_ne_bytes()); + push_data[40..44].copy_from_slice(&anchor_y.to_ne_bytes()); + + device.cmd_push_constants(cmd, pipeline.pipe_layout, vk::ShaderStageFlags::COMPUTE, 0, &push_data); + device.cmd_dispatch(cmd, (w + 15) / 16, (h + 15) / 16, 1); + } + + barrier(vk_core, cmd, res.output_image, + vk::ImageLayout::GENERAL, vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL, + vk::AccessFlags::SHADER_WRITE, vk::AccessFlags::SHADER_READ, + vk::PipelineStageFlags::COMPUTE_SHADER, vk::PipelineStageFlags::COMPUTE_SHADER, + ); + })?; + + Ok(()) +} + +fn barrier( + vk_core: &VulkanCore, + cmd: vk::CommandBuffer, + image: vk::Image, + old: vk::ImageLayout, + new: vk::ImageLayout, + src_acc: vk::AccessFlags, + dst_acc: vk::AccessFlags, + src_stage: vk::PipelineStageFlags, + dst_stage: vk::PipelineStageFlags, +) { + let b = vk::ImageMemoryBarrier::default() + .image(image) + .old_layout(old).new_layout(new) + .src_access_mask(src_acc).dst_access_mask(dst_acc) + .subresource_range(vk::ImageSubresourceRange { + aspect_mask: vk::ImageAspectFlags::COLOR, + base_mip_level: 0, level_count: 1, + base_array_layer: 0, layer_count: 1, + }); + unsafe { + vk_core.device.cmd_pipeline_barrier( + cmd, src_stage, dst_stage, vk::DependencyFlags::empty(), &[], &[], &[b], + ); + } +} + + +// --------------------------------------------------------------------- / zoom + +impl TransitionZoom { + pub fn new(device: &ash::Device) -> Result> { + let spv = spv_words(include_bytes!(concat!(env!("OUT_DIR"), "/zoom.comp.spv"))); + Ok(Self { pipeline: TransitionPipeline::new(device, &spv)? }) + } +} + +impl Transition for TransitionZoom { + fn name(&self) -> &'static str { "zoom" } + fn destroy(&self, device: &ash::Device) { self.pipeline.destroy(device); } + + fn prepare( + &self, + vk_core: &VulkanCore, + prev: &VulkanTexture, + next: &VulkanTexture, + output: &VulkanTexture, + ) -> Result> { + let device = &vk_core.device; + let sampler = linear_sampler(device)?; + + let prev_view = image_view(device, prev.image, vk::Format::R8G8B8A8_SRGB)?; + let next_view = image_view(device, next.image, vk::Format::R8G8B8A8_SRGB)?; + let output_view = image_view(device, output.image, vk::Format::R8G8B8A8_UNORM)?; + + let pool_sizes = [ + vk::DescriptorPoolSize { ty: vk::DescriptorType::COMBINED_IMAGE_SAMPLER, descriptor_count: 2 }, + vk::DescriptorPoolSize { ty: vk::DescriptorType::STORAGE_IMAGE, descriptor_count: 1 }, + ]; + let desc_pool = unsafe { + device.create_descriptor_pool( + &vk::DescriptorPoolCreateInfo::default().max_sets(1).pool_sizes(&pool_sizes), None, + )? + }; + let set_layouts = [self.pipeline.desc_layout]; + let desc_set = unsafe { + device.allocate_descriptor_sets( + &vk::DescriptorSetAllocateInfo::default() + .descriptor_pool(desc_pool) + .set_layouts(&set_layouts), + )?[0] + }; + + let prev_info = vk::DescriptorImageInfo::default().sampler(sampler).image_view(prev_view).image_layout(vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL); + let next_info = vk::DescriptorImageInfo::default().sampler(sampler).image_view(next_view).image_layout(vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL); + let output_info = vk::DescriptorImageInfo::default().image_view(output_view).image_layout(vk::ImageLayout::GENERAL); + let prev_infos = [prev_info]; + let next_infos = [next_info]; + let output_infos = [output_info]; + let writes = [ + vk::WriteDescriptorSet::default().dst_set(desc_set).dst_binding(0).descriptor_type(vk::DescriptorType::COMBINED_IMAGE_SAMPLER).image_info(&prev_infos), + vk::WriteDescriptorSet::default().dst_set(desc_set).dst_binding(1).descriptor_type(vk::DescriptorType::COMBINED_IMAGE_SAMPLER).image_info(&next_infos), + vk::WriteDescriptorSet::default().dst_set(desc_set).dst_binding(2).descriptor_type(vk::DescriptorType::STORAGE_IMAGE).image_info(&output_infos), + ]; + unsafe { device.update_descriptor_sets(&writes, &[]); } + + Ok(TransitionResources::Zoom(ZoomFrameResources { + sampler, prev_view, next_view, output_view, + output_image: output.image, + desc_pool, desc_set, + old_w: prev.width as f32, old_h: prev.height as f32, + new_w: next.width as f32, new_h: next.height as f32, + dst_w: output.width as f32, dst_h: output.height as f32, + })) + } + + fn render_frame( + &self, + vk_core: &VulkanCore, + resources: &TransitionResources, + t: f32, + mode: &str, + anchor_x: f32, + anchor_y: f32, + ) -> Result<(), Box> { + let res = match resources { + TransitionResources::Zoom(r) => r, + _ => return Err("zoom_focus: prepare() was not called with matching resources".into()), + }; + dispatch_zoom_frame(vk_core, &self.pipeline, res, t, 0.2, mode, anchor_x, anchor_y) + } + + fn cleanup(&self, device: &ash::Device, resources: TransitionResources) { + if let TransitionResources::Zoom(r) = resources { + unsafe { + device.destroy_descriptor_pool(r.desc_pool, None); + device.destroy_image_view(r.prev_view, None); + device.destroy_image_view(r.next_view, None); + device.destroy_image_view(r.output_view, None); + device.destroy_sampler(r.sampler, None); + } + } + } +} + + +// --------------------------------------------------------------------- / factory + +impl TransitionRegistry { + pub fn new(device: &ash::Device) -> Result> { + Ok(Self { + transitions: vec![ + Box::new(TransitionZoom::new(device)?), + ], + }) + } + + pub fn get(&self, name: &str) -> &dyn Transition { + self.transitions.iter() + .find(|t| t.name() == name) + .map(|t| t.as_ref()) + .unwrap_or(&TransitionNone) + } + + pub fn destroy(&self, device: &ash::Device) { + for t in &self.transitions { t.destroy(device); } + } +} + + +// --------------------------------------------------------------------- / helpers + +fn spv_words(bytes: &[u8]) -> Vec { + bytes.chunks_exact(4) + .map(|c| u32::from_ne_bytes([c[0], c[1], c[2], c[3]])) + .collect() +} + diff --git a/src/vulkan.rs b/src/vulkan.rs index 3345379..2b5ca28 100644 --- a/src/vulkan.rs +++ b/src/vulkan.rs @@ -6,7 +6,7 @@ // --------------------------------------------------------------------- / imports -use std::ffi::c_void; +use std::{ffi::c_void,sync::Arc}; use ash::{ vk, Entry, khr::{ wayland_surface, surface, swapchain, @@ -27,10 +27,12 @@ pub struct VulkanCore { pub instance: ash::Instance, pub physical_device: vk::PhysicalDevice, pub graphics_family_index: u32, - pub device: ash::Device, + pub device: Arc, pub graphics_queue: vk::Queue, + pub swapchain_loader: swapchain::Device, pub command_pool: vk::CommandPool, pub command_buffer: vk::CommandBuffer, + pub submit_fence: vk::Fence, } pub struct VulkanSurfchain { @@ -118,10 +120,13 @@ pub fn vulkan_core() -> Result> { let device_info = vk::DeviceCreateInfo::default() .queue_create_infos(std::slice::from_ref(&queue_info)) .enabled_extension_names(&device_extensions); - let device: ash::Device = unsafe { instance.create_device(physical_device, &device_info, None)? }; + let device = Arc::new(unsafe { instance.create_device(physical_device, &device_info, None)? }); let graphics_queue = unsafe { device.get_device_queue(graphics_family_index, 0) }; println!("[v{}] logical device {:?} >> graphics queue {:?}", graphics_family_index, device.handle(), graphics_queue); + // cache swapchain device + let swapchain_loader = swapchain::Device::new(&instance, &device); + // create persistent command pool and buffer let command_pool_info = vk::CommandPoolCreateInfo::default() .queue_family_index(graphics_family_index); @@ -135,6 +140,10 @@ pub fn vulkan_core() -> Result> { let command_buffer = unsafe { device.allocate_command_buffers(&alloc_info)? }[0]; println!("[v] command pool {:?} >> command buffer {:?}", command_pool, command_buffer); + // create fence + let fence_info = vk::FenceCreateInfo::default().flags(vk::FenceCreateFlags::SIGNALED); + let submit_fence = unsafe { device.create_fence(&fence_info, None)? }; + Ok(VulkanCore { entry, instance, @@ -142,8 +151,10 @@ pub fn vulkan_core() -> Result> { graphics_family_index, device, graphics_queue, + swapchain_loader, command_pool, command_buffer, + submit_fence, }) } @@ -151,11 +162,7 @@ pub fn vulkan_core() -> Result> { // --------------------------------------------------------------------- / surface swapchain pub fn vulkan_surfchain( - entry: &ash::Entry, - instance: &ash::Instance, - physical_device: vk::PhysicalDevice, - graphics_family_index: u32, - device: &ash::Device, + vk_core: &VulkanCore, disp: &WlDisplay, surf: &WlSurface, width: u32, @@ -173,11 +180,11 @@ pub fn vulkan_surfchain( }; // check queue family for wayland support - let wayland_surface_loader = wayland_surface::Instance::new(entry, instance); + let wayland_surface_loader = wayland_surface::Instance::new(&vk_core.entry, &vk_core.instance); let supports_present = unsafe { wayland_surface_loader.get_physical_device_wayland_presentation_support( - physical_device, - graphics_family_index, + vk_core.physical_device, + vk_core.graphics_family_index, &mut *wl_display_ptr, )}; if !supports_present { @@ -192,12 +199,12 @@ pub fn vulkan_surfchain( println!("[v] vulkan surface: {:#?} x {:#?} >> {:#?}", disp.id(), surf.id(), surface); // query surface capabilities and formats - let surface_loader = surface::Instance::new(entry, instance); + let surface_loader = surface::Instance::new(&vk_core.entry, &vk_core.instance); let caps = unsafe { - surface_loader.get_physical_device_surface_capabilities(physical_device, surface)? + surface_loader.get_physical_device_surface_capabilities(vk_core.physical_device, surface)? }; let formats = unsafe { - surface_loader.get_physical_device_surface_formats(physical_device, surface)? + surface_loader.get_physical_device_surface_formats(vk_core.physical_device, surface)? }; // configure surface @@ -219,7 +226,6 @@ pub fn vulkan_surfchain( println!("[v] surface config: {:?} | {:?}", chosen_format, extent); // configure swapchain - let swapchain_loader = swapchain::Device::new(instance, device); let swapchain_create_info = vk::SwapchainCreateInfoKHR::default() .surface(surface) .min_image_count(2.max(caps.min_image_count)) @@ -235,8 +241,8 @@ pub fn vulkan_surfchain( .clipped(true); // create the swapchain (ability to present rendering results to a surface) - let swapchain = unsafe { swapchain_loader.create_swapchain(&swapchain_create_info, None)? }; - let images = unsafe { swapchain_loader.get_swapchain_images(swapchain)? }; + let swapchain = unsafe { vk_core.swapchain_loader.create_swapchain(&swapchain_create_info, None)? }; + let images = unsafe { vk_core.swapchain_loader.get_swapchain_images(swapchain)? }; println!("[v{}] swapchain: {}x{}", images.len(), width, height); Ok(VulkanSurfchain { @@ -249,16 +255,16 @@ pub fn vulkan_surfchain( // --------------------------------------------------------------------- / set mode -fn mode_set( +fn scale_set( img_w: u32, img_h: u32, scr_w: u32, scr_h: u32, anchor_x: f32, anchor_y: f32, - mode: &str, + scale: &str, ) -> (u32, u32, u32, u32, i32, i32, u32, u32, bool) { - if mode == "fit" { + if scale == "fit" { let scale = (scr_w as f64 / img_w as f64).min(scr_h as f64 / img_h as f64); let sw = (img_w as f64 * scale) as u32; let sh = (img_h as f64 * scale) as u32; @@ -266,7 +272,7 @@ fn mode_set( let dy = ((scr_h - sh) as f32 * anchor_y) as i32; return (0, 0, img_w, img_h, dx, dy, sw, sh, true); } - if mode == "original" { + if scale == "original" { if img_w <= scr_w && img_h <= scr_h { let dx = ((scr_w - img_w) as f32 * anchor_x) as i32; let dy = ((scr_h - img_h) as f32 * anchor_y) as i32; @@ -357,41 +363,26 @@ impl VulkanCore { // --------------------------------------------------------------------- / record commands impl VulkanCore { - pub(crate) fn record_commands( - &self, - f: impl FnOnce(vk::CommandBuffer), - ) -> Result<(), Box> { + pub(crate) fn record_commands(&self, f: impl FnOnce(vk::CommandBuffer)) + -> Result<(), Box> { unsafe { - self.device - .reset_command_pool(self.command_pool, vk::CommandPoolResetFlags::empty())?; + self.device.reset_command_buffer(self.command_buffer, vk::CommandBufferResetFlags::empty())?; } - - let begin_info = vk::CommandBufferBeginInfo::default() - .flags(vk::CommandBufferUsageFlags::ONE_TIME_SUBMIT); + let begin_info = vk::CommandBufferBeginInfo::default().flags(vk::CommandBufferUsageFlags::ONE_TIME_SUBMIT); unsafe { - self.device - .begin_command_buffer(self.command_buffer, &begin_info)?; + self.device.begin_command_buffer(self.command_buffer, &begin_info)?; } - f(self.command_buffer); unsafe { self.device.end_command_buffer(self.command_buffer)?; } - - let submit_info = vk::SubmitInfo::default() - .command_buffers(std::slice::from_ref(&self.command_buffer)); - let fence = unsafe { - self.device - .create_fence(&vk::FenceCreateInfo::default(), None)? - }; + let submit_info = vk::SubmitInfo::default().command_buffers(std::slice::from_ref(&self.command_buffer)); unsafe { - self.device - .queue_submit(self.graphics_queue, &[submit_info], fence)?; - self.device - .wait_for_fences(&[fence], true, u64::MAX)?; - self.device.destroy_fence(fence, None); + self.device.wait_for_fences(&[self.submit_fence], true, u64::MAX)?; + self.device.reset_fences(&[self.submit_fence])?; + self.device.queue_submit(self.graphics_queue, &[submit_info], self.submit_fence)?; + self.device.wait_for_fences(&[self.submit_fence], true, u64::MAX)?; } - Ok(()) } } @@ -549,91 +540,43 @@ impl VulkanCore { // --------------------------------------------------------------------- / load texture impl VulkanCore { - fn load_texture( - &self, - buffer: vk::Buffer, - image: vk::Image, - width: u32, - height: u32, - ) -> Result<(), Box> { - unsafe { - self.device - .reset_command_pool(self.command_pool, vk::CommandPoolResetFlags::empty())?; - } - - let begin_info = vk::CommandBufferBeginInfo::default() - .flags(vk::CommandBufferUsageFlags::ONE_TIME_SUBMIT); - unsafe { - self.device - .begin_command_buffer(self.command_buffer, &begin_info)?; - } - - // transition texture to transfer dst - self.image_barrier( - self.command_buffer, - image, - vk::ImageLayout::UNDEFINED, - vk::ImageLayout::TRANSFER_DST_OPTIMAL, - vk::AccessFlags::empty(), - vk::AccessFlags::TRANSFER_WRITE, - vk::PipelineStageFlags::TOP_OF_PIPE, - vk::PipelineStageFlags::TRANSFER, - ); - - // copy buffer to image - let region = vk::BufferImageCopy::default() - .buffer_offset(0) - .buffer_row_length(0) - .buffer_image_height(0) - .image_subresource(vk::ImageSubresourceLayers { - aspect_mask: vk::ImageAspectFlags::COLOR, - mip_level: 0, - base_array_layer: 0, - layer_count: 1, - }) - .image_offset(vk::Offset3D { x: 0, y: 0, z: 0 }) - .image_extent(vk::Extent3D { width, height, depth: 1 }); - unsafe { - self.device.cmd_copy_buffer_to_image( - self.command_buffer, - buffer, - image, - vk::ImageLayout::TRANSFER_DST_OPTIMAL, - &[region], + fn load_texture(&self, buffer: vk::Buffer, image: vk::Image, width: u32, height: u32) + -> Result<(), Box> { + self.record_commands(|cmd| { + + // transition texture to transfer dst + self.image_barrier( + cmd, image, + vk::ImageLayout::UNDEFINED, vk::ImageLayout::TRANSFER_DST_OPTIMAL, + vk::AccessFlags::empty(), vk::AccessFlags::TRANSFER_WRITE, + vk::PipelineStageFlags::TOP_OF_PIPE, vk::PipelineStageFlags::TRANSFER, ); - } - - // transition texture to shader read only - self.image_barrier( - self.command_buffer, - image, - vk::ImageLayout::TRANSFER_DST_OPTIMAL, - vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL, - vk::AccessFlags::TRANSFER_WRITE, - vk::AccessFlags::SHADER_READ, - vk::PipelineStageFlags::TRANSFER, - vk::PipelineStageFlags::FRAGMENT_SHADER, - ); - unsafe { - self.device.end_command_buffer(self.command_buffer)?; - } - - let submit_info = vk::SubmitInfo::default() - .command_buffers(std::slice::from_ref(&self.command_buffer)); - let fence = unsafe { - self.device - .create_fence(&vk::FenceCreateInfo::default(), None)? - }; - unsafe { - self.device - .queue_submit(self.graphics_queue, &[submit_info], fence)?; - self.device - .wait_for_fences(&[fence], true, u64::MAX)?; - self.device.destroy_fence(fence, None); - } + // copy buffer to image + let region = vk::BufferImageCopy::default() + .buffer_offset(0) + .buffer_row_length(0) + .buffer_image_height(0) + .image_subresource(vk::ImageSubresourceLayers { + aspect_mask: vk::ImageAspectFlags::COLOR, + mip_level: 0, base_array_layer: 0, layer_count: 1, + }) + .image_offset(vk::Offset3D { x: 0, y: 0, z: 0 }) + .image_extent(vk::Extent3D { width, height, depth: 1 }); + unsafe { + self.device.cmd_copy_buffer_to_image( + cmd, buffer, image, vk::ImageLayout::TRANSFER_DST_OPTIMAL, &[region], + ); + } - Ok(()) + // transition texture to shader read only + self.image_barrier( + cmd, image, + vk::ImageLayout::TRANSFER_DST_OPTIMAL, vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL, + vk::AccessFlags::TRANSFER_WRITE, vk::AccessFlags::SHADER_READ, + vk::PipelineStageFlags::TRANSFER, vk::PipelineStageFlags::FRAGMENT_SHADER, + ); + }) } } @@ -690,11 +633,10 @@ impl VulkanCore { anchor_x: f32, anchor_y: f32, background: Option<(vk::Image, u32, u32)>, - mode: &str, + scale: &str, ) -> Result<(), Box> { - let swapchain_loader = ash::khr::swapchain::Device::new(&self.instance, &self.device); let (image_index, _suboptimal) = match unsafe { - swapchain_loader.acquire_next_image( + self.swapchain_loader.acquire_next_image( surfchain.swapchain, u64::MAX, vk::Semaphore::null(), @@ -735,13 +677,13 @@ impl VulkanCore { vk::PipelineStageFlags::TRANSFER, ); - // compute source and destination rectangles based on mode + // compute source and destination rectangles based on scaling mode let (src_x, src_y, src_w, src_h, dst_x, dst_y, dst_w, dst_h, needs_clear) = - mode_set( + scale_set( texture.width, texture.height, layer_width, layer_height, anchor_x, anchor_y, - mode, + scale, ); if needs_clear { @@ -797,7 +739,7 @@ impl VulkanCore { .swapchains(std::slice::from_ref(&surfchain.swapchain)) .image_indices(std::slice::from_ref(&image_index)); - let result = unsafe { swapchain_loader.queue_present(self.graphics_queue, &present_info) }; + let result = unsafe { self.swapchain_loader.queue_present(self.graphics_queue, &present_info) }; match result { Ok(_) => Ok(()), Err(vk::Result::ERROR_OUT_OF_DATE_KHR) => { @@ -810,7 +752,16 @@ impl VulkanCore { } -// --------------------------------------------------------------------- / destroy core +// --------------------------------------------------------------------- / destroy + +impl VulkanCore { + pub fn destroy_texture(&self, texture: &VulkanTexture) { + unsafe { + self.device.destroy_image(texture.image, None); + self.device.free_memory(texture._memory, None); + } + } +} pub fn destroy_wallbash( vk_core: &VulkanCore, @@ -835,11 +786,8 @@ pub fn destroy_wallbash( if level >= 1 { if let Some(sc) = config.surfchain { unsafe { - let swapchain_loader = - ash::khr::swapchain::Device::new(&vk_core.instance, &vk_core.device); - swapchain_loader.destroy_swapchain(sc.swapchain, None); - let surface_loader = - ash::khr::surface::Instance::new(&vk_core.entry, &vk_core.instance); + vk_core.swapchain_loader.destroy_swapchain(sc.swapchain, None); + let surface_loader = ash::khr::surface::Instance::new(&vk_core.entry, &vk_core.instance); surface_loader.destroy_surface(sc.surface, None); } } @@ -854,11 +802,9 @@ pub fn destroy_wallbash( } } unsafe { - vk_core.device - .device_wait_idle() - .expect("device wait failed"); - vk_core.device - .destroy_command_pool(vk_core.command_pool, None); + vk_core.device.device_wait_idle().expect("device wait failed"); + vk_core.device.destroy_fence(vk_core.submit_fence, None); + vk_core.device.destroy_command_pool(vk_core.command_pool, None); vk_core.device.destroy_device(None); vk_core.instance.destroy_instance(None); } diff --git a/src/wallbash.rs b/src/wallbash.rs index 9eda3e6..8117bff 100644 --- a/src/wallbash.rs +++ b/src/wallbash.rs @@ -6,119 +6,46 @@ // --------------------------------------------------------------------- / imports -use crate::{vulkan, wayland, filters, colors}; +use crate::{ipc, wayland, vulkan, filters, transitions, colors}; use ash::vk; use std::{ - os::unix::net::{UnixListener, UnixStream}, - io::{BufRead, BufReader}, - sync::mpsc, time::Instant, + io::Write, time::Instant, collections::VecDeque, + os::unix::net::UnixStream, + sync::{mpsc,Arc}, }; // --------------------------------------------------------------------- / datatypes -enum Command { - Stop, - Status, - Set { palette: String, mode: String, anchor_x: f32, anchor_y: f32, path: String }, -} - struct DaemonState { - vk_core: vulkan::VulkanCore, wl_core: wayland::WaylandCore, + vk_core: vulkan::VulkanCore, vk_surfchain: Option, wallpaper: Option, - blur_module: vk::ShaderModule, - blur_pipeline: vk::Pipeline, - blur_desc_layout: vk::DescriptorSetLayout, -} - - -// --------------------------------------------------------------------- / implementations - -impl Command { - fn parse_raw(raw: &str) -> Self { - let raw = raw.trim(); - if raw == "stop" { return Command::Stop; } - if raw == "status" { return Command::Status; } - if raw.starts_with("set") { - let payload = &raw[3..]; - let mut parts = payload.splitn(5, '\x01'); - let palette = parts.next().unwrap().to_string(); - let mode = parts.next().unwrap().to_string(); - let anchor_x = parts.next().unwrap().parse().unwrap(); - let anchor_y = parts.next().unwrap().parse().unwrap(); - let path = parts.next().unwrap().to_string(); - return Command::Set { palette, mode, anchor_x, anchor_y, path }; - } - panic!("unknown internal command: {}", raw); - } + blur_state: BlurState, + transition_state: TransitionState, + decoded_cache: VecDeque<(String, image::DynamicImage)>, } -impl DaemonState { - fn new() -> Result> { - let wl_core = wayland::wayland_core()?; - let vk_core = vulkan::vulkan_core()?; - let (blur_module, blur_pipeline, blur_desc_layout) = filters::filter_pipeline(&vk_core.device, "blur")?; - let vk_surfchain = Some(set_surfchain(&vk_core, &wl_core, None)?); - Ok(Self { - vk_core, - wl_core, - vk_surfchain, - wallpaper: None, - blur_module, - blur_pipeline, - blur_desc_layout, - }) - } +struct BlurState { + device: Arc, + module: vk::ShaderModule, + pipeline: vk::Pipeline, + desc_layout: vk::DescriptorSetLayout, } - -// --------------------------------------------------------------------- / listener - -fn start_ipc(socket_path: &str) -> Result, Box> { - - // remove any stale socket file from a previous run - let _ = std::fs::remove_file(socket_path); - let listener = UnixListener::bind(socket_path)?; - let (tx, rx) = mpsc::channel::(); - println!("[ipc] listening: {}", socket_path); - - // start listener thread - std::thread::spawn(move || { - for stream in listener.incoming() { - match stream { - Ok(stream) => { - let reader = BufReader::new(stream); - for line in reader.lines() { - if let Ok(path) = line { - let path = path.trim().to_string(); - if !path.is_empty() { - if tx.send(path).is_err() { - return; // main thread has dropped the receiver - } - } - } - } - } - Err(e) => { - eprintln!("[ipc] accept error: {}", e); - break; - } - } - } - }); - - Ok(rx) +struct TransitionState { + device: Arc, + scratch: vulkan::VulkanTexture, + registry: transitions::TransitionRegistry, + pending: Option, } -// --------------------------------------------------------------------- / timer +// --------------------------------------------------------------------- / benchmarking fn timer(label: &str, f: F) -> R -where - F: FnOnce() -> R, -{ +where F: FnOnce() -> R { let start = Instant::now(); let result = f(); println!("[perf] {}: {:.2?}", label, start.elapsed()); @@ -126,233 +53,311 @@ where } -// --------------------------------------------------------------------- / wallpaper - -fn set_wallpaper( - path: &str, - vk_core: &vulkan::VulkanCore, - vk_surfchain: &vulkan::VulkanSurfchain, - layer_width: u32, - layer_height: u32, - wallpaper: &mut Option, - anchor_x: f32, - anchor_y: f32, - mode: &str, - effect: impl FnOnce(&vulkan::VulkanTexture) -> Option, - palette: &String, -) -> Result<(), Box> { - - // load the wallpaper - let (img, pixel_bytes) = timer("load+decode", || { - let img = image::open(path)?; - let rgba = img.to_rgba8(); - let bytes = rgba.into_raw(); - Ok::<_, Box>((img, bytes)) - })?; - - // call the vulkan pipeline - let texture = timer("upload", || { - vk_core.upload_texture(&pixel_bytes, img.width(), img.height()) - })?; - - // drop the old texture resources (if any) - if let Some(old_tex) = wallpaper.take() { +// --------------------------------------------------------------------- / blur state + +impl BlurState { + fn new(vk_core: &vulkan::VulkanCore) -> Result> { + let device = Arc::clone(&vk_core.device); + let (module, pipeline, desc_layout) = filters::filter_pipeline(&device, "blur")?; + Ok(Self { device, module, pipeline, desc_layout }) + } +} + +impl Drop for BlurState { + fn drop(&mut self) { unsafe { - vk_core.device.destroy_image(old_tex.image, None); - vk_core.device.free_memory(old_tex._memory, None); + self.device.destroy_pipeline(self.pipeline, None); + self.device.destroy_descriptor_set_layout(self.desc_layout, None); + self.device.destroy_shader_module(self.module, None); } } - *wallpaper = Some(texture); +} - // create a blurred version for fit/original modes - let background_texture = timer("effect+draw", || { - let bg = effect(wallpaper.as_ref().unwrap()); - let bg_params = bg.as_ref().map(|b| (b.image, b.width, b.height)); - vk_core.draw_wallpaper( - vk_surfchain, - wallpaper.as_ref().unwrap(), +// --------------------------------------------------------------------- / transition state + +impl TransitionState { + fn new( + vk_core: &vulkan::VulkanCore, + layer_width: u32, + layer_height: u32, + ) -> Result> { + let device = Arc::clone(&vk_core.device); + let (img, mem) = vk_core.create_texture( layer_width, layer_height, + ash::vk::ImageUsageFlags::TRANSFER_SRC | ash::vk::ImageUsageFlags::STORAGE, + ash::vk::Format::R8G8B8A8_UNORM, + )?; + let scratch = vulkan::VulkanTexture { + image: img, + _memory: mem, + width: layer_width, + height: layer_height, + }; + let registry = transitions::TransitionRegistry::new(&device)?; + Ok(Self { + device, + scratch, + registry, + pending: None, + }) + } +} + +impl TransitionState { + pub fn start( + &mut self, + vk_core: &vulkan::VulkanCore, + old: vulkan::VulkanTexture, + new_tex: &vulkan::VulkanTexture, + bezier: &str, + scale: ipc::ScalingMode, + anchor_x: f32, + anchor_y: f32, + background: Option, + ) { + let cfg = transitions::TransitionConfig::parse("zoom", 400, bezier); + let target_fps = 60.0_f64; + let total_frames = ((cfg.duration_ms as f64 / 1000.0) * target_fps).ceil() as u32; + + let anim = self.registry.get(&cfg.kind); + let resources = match anim.prepare(vk_core, &old, new_tex, &self.scratch) { + Ok(r) => r, + Err(e) => { + eprintln!("[wallbash] error preparing transition: {}", e); + transitions::TransitionResources::None + } + }; + + self.pending = Some(transitions::TransitionCore { + cfg, + prev: old, + start: Instant::now(), + total_frames, + current_frame: 0, + scale: scale.as_str().to_string(), anchor_x, anchor_y, - bg_params, - mode, - )?; + background, + resources, + }); + } +} - Ok::<_, Box>(bg) - })?; +impl TransitionState { + pub fn advance( + &mut self, + vk_core: &vulkan::VulkanCore, + wl_core: &wayland::WaylandCore, + vk_surfchain: &vulkan::VulkanSurfchain, + ) { + let pending = match self.pending.as_mut() { + Some(p) => p, + None => { + std::thread::sleep(std::time::Duration::from_millis(100)); + return; + } + }; - // destroy the temporary blurred texture (if any) - if let Some(bg) = background_texture { - unsafe { - vk_core.device.destroy_image(bg.image, None); - vk_core.device.free_memory(bg._memory, None); + if pending.current_frame <= pending.total_frames { + let raw_t = (pending.current_frame as f32 / pending.total_frames as f32).clamp(0.0, 1.0); + let t = pending.cfg.bezier.apply(raw_t); + + let anim = self.registry.get(&pending.cfg.kind); + if let Err(e) = anim.render_frame( + &vk_core, + &pending.resources, + t, + &pending.scale, + pending.anchor_x, + pending.anchor_y, + ) { eprintln!("[wallbash] transition frame error: {}", e); } + + let bg_params = pending.background.as_ref().map(|b| (b.image, b.width, b.height)); + if let Err(e) = vk_core.draw_wallpaper( + vk_surfchain, + &self.scratch, + wl_core.state.layer_width, + wl_core.state.layer_height, + pending.anchor_x, + pending.anchor_y, + bg_params, + &pending.scale, + ) { eprintln!("[wallbash] error drawing transition frame: {}", e); } + + pending.current_frame += 1; + let frame_duration = std::time::Duration::from_secs_f64(1.0 / 60.0); + let elapsed = pending.start.elapsed(); + let target = frame_duration.mul_f64((pending.current_frame + 1) as f64); + if elapsed < target { + std::thread::sleep(target - elapsed); + } + } else { + self.cancel(vk_core); } } +} - // generate colors - if palette != "skip" { - timer("dcols", || colors::dcol(&img, palette)); +impl TransitionState { + pub fn cancel(&mut self, vk_core: &vulkan::VulkanCore) { + if let Some(mut core) = self.pending.take() { + vk_core.destroy_texture(&core.prev); + if let Some(bg) = core.background.take() { + vk_core.destroy_texture(&bg); + } + let anim = self.registry.get(&core.cfg.kind); + anim.cleanup(&vk_core.device, core.resources); + } } +} - Ok(()) +impl Drop for TransitionState { + fn drop(&mut self) { + unsafe { + self.device.destroy_image(self.scratch.image, None); + self.device.free_memory(self.scratch._memory, None); + } + self.registry.destroy(&self.device); + } } -// --------------------------------------------------------------------- / surfchain +// --------------------------------------------------------------------- / daemon state -fn set_surfchain( - vk_core: &vulkan::VulkanCore, - wl_core: &wayland::WaylandCore, - old: Option, -) -> Result> { +impl DaemonState { + fn new() -> Result> { + let wl_core = wayland::wayland_core()?; + let vk_core = vulkan::vulkan_core()?; + let blur_state = BlurState::new(&vk_core)?; + let vk_surfchain = Some(vulkan::vulkan_surfchain( + &vk_core, &wl_core.display, &wl_core.surface, wl_core.state.layer_width, wl_core.state.layer_height + )?); + + let transition_state = TransitionState::new( + &vk_core, + wl_core.state.layer_width, + wl_core.state.layer_height, + )?; - // destroy only the swapchain (level 1) – no filter resources - if let Some(old_sc) = old { - vulkan::destroy_wallbash( + Ok(Self { + wl_core, vk_core, - vulkan::VulkanCleanup { - surfchain: Some(old_sc), - filter_module: None, - filter_pipeline: None, - filter_desc_layout: None, - wallpaper_texture: None, - }, - 1, - ); + vk_surfchain, + wallpaper: None, + blur_state, + transition_state, + decoded_cache: VecDeque::new(), + }) } - - vulkan::vulkan_surfchain( - &vk_core.entry, - &vk_core.instance, - vk_core.physical_device, - vk_core.graphics_family_index, - &vk_core.device, - &wl_core.display, - &wl_core.surface, - wl_core.state.layer_width, - wl_core.state.layer_height, - ) } - -// --------------------------------------------------------------------- / command +impl DaemonState { + fn load(&mut self, path: &str) -> Result<(image::DynamicImage, Vec), Box> { + let cache_hit = self.decoded_cache.iter().position(|(p, _)| p == path); + let label = if cache_hit.is_some() { "load-cached" } else { "load+decode" }; + + timer(label, || -> Result<_, Box> { + let img = if let Some(idx) = cache_hit { + self.decoded_cache.remove(idx).expect("cache index invalid") + } else { + (path.to_string(), image::open(path)?) + }.1; + + let bytes = img.to_rgba8().into_raw(); + self.decoded_cache.push_back((path.to_string(), img.clone())); + if self.decoded_cache.len() > 30 { self.decoded_cache.pop_front(); } + + Ok((img, bytes)) + }) + } +} impl DaemonState { fn set_command( &mut self, - palette: String, - mode: String, - anchor_x: f32, - anchor_y: f32, - path: String, - ) -> Result<(), ()> { - let resolved = std::fs::canonicalize(&path) - .map(|p| p.to_string_lossy().to_string()) - .unwrap_or(path); - println!("[wallbash] loading '{}' ({}|{}|ax:{:.1}|ay:{:.1})", resolved, palette, mode, anchor_x, anchor_y); - - let effect = |tex: &vulkan::VulkanTexture| { - if mode != "cover" { + palette: ipc::PaletteMode, + bezier: String, + scale: ipc::ScalingMode, + ax: f32, + ay: f32, + path: String + ) -> Result<(), Box> { + let resolved = std::fs::canonicalize(&path).map(|p| p.to_string_lossy().to_string()).unwrap_or(path); + println!("[wallbash] loading '{}' | {:?} | {:?} | bz:{} | a(x,y):({:.1},{:.1})", resolved, palette, scale, bezier, ax, ay); + + self.transition_state.cancel(&self.vk_core); + let (img, pixel_bytes) = self.load(&resolved)?; + println!("[wallbash] decoded images: {}/30", self.decoded_cache.len()); + + let texture = timer("upload", || self.vk_core.upload_texture(&pixel_bytes, img.width(), img.height()))?; + + let background = if scale != ipc::ScalingMode::Cover { + timer("blur", || { filters::blur_texture( &self.vk_core, - tex, - tex.width, - tex.height, - self.blur_pipeline, - self.blur_desc_layout, - ) - .ok() - } else { None } - }; + &texture, + texture.width, + texture.height, + self.blur_state.pipeline, + self.blur_state.desc_layout + ).ok().map(|b| vulkan::VulkanTexture { image: b.image, _memory: b._memory, width: b.width, height: b.height }) + }) + } else { None }; + + if palette != ipc::PaletteMode::Skip { + let p_str = palette.as_str(); + std::thread::spawn(move || { let _ = timer("dcols", || colors::dcol(&img, p_str)); }); + } - match set_wallpaper( - &resolved, - &self.vk_core, + let old_tex = self.wallpaper.replace(texture); + let new_tex = self.wallpaper.as_ref().ok_or_else(|| "[error] wallpaper state corruption")?; + + if let Some(old) = old_tex { + self.transition_state.start(&self.vk_core, old, new_tex, &bezier, scale, ax, ay, background); + } else { + let bg_params = background.as_ref().map(|b| (b.image, b.width, b.height)); + self.vk_core.draw_wallpaper( self.vk_surfchain.as_ref().unwrap(), + new_tex, self.wl_core.state.layer_width, self.wl_core.state.layer_height, - &mut self.wallpaper, - anchor_x, - anchor_y, - &mode, - effect, - &palette, - ) { - Ok(()) => { - println!("[wallbash] wallpaper set."); - Ok(()) - } - Err(e) if e.to_string().contains("out of date") => { - println!("[wallbash] swapchain out of date, recreating..."); - - match vulkan::vulkan_surfchain( - &self.vk_core.entry, - &self.vk_core.instance, - self.vk_core.physical_device, - self.vk_core.graphics_family_index, - &self.vk_core.device, - &self.wl_core.display, - &self.wl_core.surface, - self.wl_core.state.layer_width, - self.wl_core.state.layer_height, - ) { - Ok(new_sc) => { - let old = self.vk_surfchain.take().unwrap(); - vulkan::destroy_wallbash( - &self.vk_core, - vulkan::VulkanCleanup { - surfchain: Some(old), - filter_module: None, - filter_pipeline: None, - filter_desc_layout: None, - wallpaper_texture: None, - }, - 1, - ); - self.vk_surfchain = Some(new_sc); - } - Err(e2) => { - eprintln!("[wallbash] failed to recreate swapchain {}", e2); - return Err(()); - } - } - - if let Err(e3) = set_wallpaper( - &resolved, - &self.vk_core, - self.vk_surfchain.as_ref().unwrap(), - self.wl_core.state.layer_width, - self.wl_core.state.layer_height, - &mut self.wallpaper, - anchor_x, - anchor_y, - &mode, - effect, - &palette, - ) { - eprintln!("[wallbash] error after swapchain recreation {}", e3); - } - Ok(()) - } - Err(e) => { - eprintln!("[wallbash] error {}", e); - Ok(()) - } + ax, + ay, + bg_params, + scale.as_str())?; } + + println!("[wallbash] wallpaper set."); + Ok(()) + } +} + +impl Drop for DaemonState { + fn drop(&mut self) { + vulkan::destroy_wallbash( + &self.vk_core, + vulkan::VulkanCleanup { + surfchain: self.vk_surfchain.take(), + filter_module: None, + filter_pipeline: None, + filter_desc_layout: None, + wallpaper_texture: self.wallpaper.take(), + }, + 2, + ); + println!("[wallbash] GPU resources safely released."); } } -// --------------------------------------------------------------------- / daemon +// --------------------------------------------------------------------- / daemon run pub fn run(socket_path: &str) -> Result<(), Box> { if UnixStream::connect(socket_path).is_ok() { return Err("Daemon is already running.".into()); } - let _ = std::fs::remove_file(socket_path); - let rx = start_ipc(socket_path)?; + let rx: mpsc::Receiver = ipc::start_ipc(socket_path)?; let mut state = DaemonState::new()?; println!("[wallbash] ready, press Ctrl+C to quit."); @@ -360,38 +365,38 @@ pub fn run(socket_path: &str) -> Result<(), Box> { let mut running = true; while running { state.wl_core.event.dispatch_pending(&mut state.wl_core.state)?; - if let Ok(raw) = rx.try_recv() { - match Command::parse_raw(&raw) { - Command::Stop => { + + if let Ok(mut msg) = rx.try_recv() { + match ipc::Command::parse_raw(&msg.cmd) { + Ok(ipc::Command::Stop) => { println!("[wallbash] stopping daemon."); running = false; } - Command::Status => { - println!("[wallbash] daemon is running."); - } - Command::Set { palette, mode, anchor_x, anchor_y, path } => { - if state.set_command(palette, mode, anchor_x, anchor_y, path).is_err() - { - continue; + Ok(ipc::Command::Set { .. }) => { + let mut last_msg = msg; + while let Ok(new_msg) = rx.try_recv() { + if matches!(ipc::Command::parse_raw(&new_msg.cmd), Ok(ipc::Command::Set { .. })) { + last_msg = new_msg; + } else { + break; + } } + if let Ok(ipc::Command::Set { palette, bezier, scale, anchor_x, anchor_y, path }) = ipc::Command::parse_raw(&last_msg.cmd) { + let result = state.set_command(palette, bezier, scale, anchor_x, anchor_y, path); + let _ = last_msg.stream.write(&[0u8]); + let _ = last_msg.stream.set_nonblocking(true); + if result.is_err() { continue; } + } + } + Err(e) => { + eprintln!("[ipc] invalid command: {}", e); + let _ = msg.stream.write(&[0u8]); } } } - std::thread::sleep(std::time::Duration::from_millis(16)); + state.transition_state.advance(&state.vk_core, &state.wl_core, state.vk_surfchain.as_ref().unwrap()); } - vulkan::destroy_wallbash( - &state.vk_core, - vulkan::VulkanCleanup { - surfchain: state.vk_surfchain.take(), - filter_module: Some(state.blur_module), - filter_pipeline: Some(state.blur_pipeline), - filter_desc_layout: Some(state.blur_desc_layout), - wallpaper_texture: state.wallpaper.take(), - }, - 2, - ); - println!("[wallbash] daemon stopped."); Ok(()) }