start voronoi

This commit is contained in:
nora 2026-01-02 15:56:10 +01:00
parent c883f37739
commit 6ff558826a
4 changed files with 254 additions and 90 deletions

View file

@ -17,16 +17,17 @@ impl DesktopEntries {
pub fn count(&self) -> usize {
self.entries.len()
}
pub fn colors(&self) -> impl Iterator<Item = Oklab> + ExactSizeIterator {
self.entries.iter().map(|entry| entry.avg_icon_color)
}
pub fn find_entry(&self, color: Oklab) -> Option<&DesktopEntry> {
self.entries.iter().min_by(|x, y| {
f32::total_cmp(
&diff_color(x.avg_icon_color, color),
&diff_color(y.avg_icon_color, color),
)
})
self.entries
.iter()
.min_by_key(|x| OrdFloat(diff_color(x.avg_icon_color, color)))
}
}
// keep it in sync with the gpu implementation
fn diff_color(icon: Oklab, color: Oklab) -> f32 {
icon.distance_squared(color)
}
@ -98,9 +99,14 @@ pub(crate) fn find_desktop_files() -> Result<DesktopEntries> {
.wrap_err_with(|| format!("{}", base.display()))?;
}
Ok(DesktopEntries {
entries: results.into_values().collect(),
})
let mut entries = results.into_values().collect::<Vec<_>>();
entries.sort_by_key(|entry| {
let (l, a, b) = entry.avg_icon_color.into_components();
(OrdFloat(a), OrdFloat(b), OrdFloat(l))
});
Ok(DesktopEntries { entries })
}
fn average_color(image: &image::DynamicImage) -> palette::Oklab {
@ -131,3 +137,22 @@ fn average_color(image: &image::DynamicImage) -> palette::Oklab {
b: total_b / count,
}
}
struct OrdFloat(f32);
impl PartialEq for OrdFloat {
fn eq(&self, other: &Self) -> bool {
self.cmp(other).is_eq()
}
}
impl Eq for OrdFloat {}
impl PartialOrd for OrdFloat {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(&other))
}
}
impl Ord for OrdFloat {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.0.total_cmp(&other.0)
}
}

View file

@ -1,6 +1,7 @@
use std::ptr::NonNull;
use std::{mem::offset_of, ptr::NonNull};
use eyre::{Context, Result};
use palette::Oklab;
use raw_window_handle::{
RawDisplayHandle, RawWindowHandle, WaylandDisplayHandle, WaylandWindowHandle,
};
@ -9,27 +10,42 @@ use wgpu::util::DeviceExt;
pub struct AppGpuState {
instance: wgpu::Instance,
adapter: wgpu::Adapter,
device: wgpu::Device,
queue: wgpu::Queue,
render_pipeline: wgpu::RenderPipeline,
screen_size_bind_group_layout: wgpu::BindGroupLayout,
desktop_colors_bind_group: wgpu::BindGroup,
}
pub struct SurfaceGpuState {
surface: wgpu::Surface<'static>,
screen_size_buffer: wgpu::Buffer,
width: u32,
height: u32,
input_buffer: wgpu::Buffer,
screen_size_bind_group: wgpu::BindGroup,
}
#[repr(C)]
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
struct ScreenSizeUniform {
struct InputUniform {
size: [f32; 2], // width, height
voronoi_progress: f32,
_pad: f32,
}
#[repr(C)]
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
struct DesktopColorsStorage {
l: f32,
a: f32,
b: f32,
_pad: f32,
}
impl AppGpuState {
pub fn new() -> Result<Self> {
pub fn new(
desktop_colors: impl IntoIterator<Item = Oklab> + ExactSizeIterator,
) -> Result<Self> {
let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor::default());
let adapter =
@ -54,10 +70,28 @@ impl AppGpuState {
count: None,
}],
});
let desktop_colors_bind_group_layout =
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("desktop_colors_bind_group_layout"),
entries: &[wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Storage { read_only: true },
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
}],
});
let render_pipeline_layout =
device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("Render Pipeline Layout"),
bind_group_layouts: &[&screen_size_bind_group_layout],
bind_group_layouts: &[
&screen_size_bind_group_layout,
&desktop_colors_bind_group_layout,
],
immediate_size: 0,
});
@ -73,11 +107,7 @@ impl AppGpuState {
fragment: Some(wgpu::FragmentState {
module: &shader,
entry_point: Some("fs_main"),
targets: &[Some(wgpu::ColorTargetState {
format: wgpu::TextureFormat::Rgba8UnormSrgb,
blend: Some(wgpu::BlendState::REPLACE),
write_mask: wgpu::ColorWrites::ALL,
})],
targets: &[Some(wgpu::TextureFormat::Bgra8UnormSrgb.into())],
compilation_options: wgpu::PipelineCompilationOptions::default(),
}),
primitive: wgpu::PrimitiveState {
@ -99,13 +129,42 @@ impl AppGpuState {
cache: None,
});
let desktop_colors = desktop_colors
.into_iter()
.map(|color| DesktopColorsStorage {
l: color.l,
a: color.a,
b: color.b,
_pad: 0.0,
})
.collect::<Vec<_>>();
let desktop_colors_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("desktop_colors_buffer"),
contents: bytemuck::cast_slice::<DesktopColorsStorage, u8>(&desktop_colors),
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
});
let desktop_colors_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
layout: &desktop_colors_bind_group_layout,
entries: &[wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
buffer: &desktop_colors_buffer,
offset: 0,
size: None,
}),
}],
label: Some("desktop_colors_bind_group"),
});
Ok(Self {
instance,
adapter,
device,
queue,
render_pipeline,
screen_size_bind_group_layout,
desktop_colors_bind_group,
})
}
}
@ -135,7 +194,11 @@ impl SurfaceGpuState {
.device
.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("Screen Size Uniform Buffer"),
contents: bytemuck::bytes_of(&ScreenSizeUniform { size: [0.0, 0.0] }),
contents: bytemuck::bytes_of(&InputUniform {
size: [0.0, 0.0],
voronoi_progress: 0.0,
_pad: 0.0,
}),
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
});
@ -157,38 +220,63 @@ impl SurfaceGpuState {
Ok(Self {
surface,
screen_size_buffer,
input_buffer: screen_size_buffer,
screen_size_bind_group,
width: 0,
height: 0,
})
}
pub fn resize(&self, gpu_state: &AppGpuState, width: u32, height: u32) {
pub fn resize(&mut self, gpu_state: &AppGpuState, width: u32, height: u32) {
self.width = width;
self.height = height;
gpu_state.queue.write_buffer(
&self.screen_size_buffer,
&self.input_buffer,
0,
bytemuck::bytes_of(&ScreenSizeUniform {
bytemuck::bytes_of(&InputUniform {
size: [width as f32, height as f32],
voronoi_progress: 0.0,
_pad: 0.0,
}),
);
let cap = self.surface.get_capabilities(&gpu_state.adapter);
self.configure(gpu_state);
}
fn configure(&self, gpu_state: &AppGpuState) {
let surface_config = wgpu::SurfaceConfiguration {
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
format: cap.formats[0],
view_formats: vec![cap.formats[0]],
format: wgpu::TextureFormat::Bgra8UnormSrgb,
view_formats: vec![wgpu::TextureFormat::Bgra8UnormSrgb],
alpha_mode: wgpu::CompositeAlphaMode::Auto,
width,
height,
width: self.width,
height: self.height,
desired_maximum_frame_latency: 2,
// Wayland is inherently a mailbox system.
present_mode: wgpu::PresentMode::Mailbox,
};
self.surface.configure(&gpu_state.device, &surface_config);
}
pub fn set_voronoi_progress(&self, gpu_state: &AppGpuState, voronoi_progress: f32) {
gpu_state.queue.write_buffer(
&self.input_buffer,
offset_of!(InputUniform, voronoi_progress) as u64,
bytemuck::bytes_of(&voronoi_progress),
);
}
pub fn draw(&self, gpu_state: &AppGpuState) {
let surface_texture = match self.surface.get_current_texture() {
Ok(texture) => texture,
Err(wgpu::SurfaceError::Outdated | wgpu::SurfaceError::Lost) => {
self.configure(gpu_state);
self.surface.get_current_texture().unwrap()
}
Err(e) => panic!("failed to acquire next swapchain texture: {e}"),
};
let surface_texture = self
.surface
.get_current_texture()
.expect("failed to acquire next swapchain texture");
let texture_view: wgpu::TextureView = surface_texture
.texture
.create_view(&wgpu::TextureViewDescriptor::default());
@ -219,7 +307,8 @@ impl SurfaceGpuState {
});
render_pass.set_pipeline(&gpu_state.render_pipeline);
render_pass.set_bind_group(0, Some(&self.screen_size_bind_group), &[]);
render_pass.set_bind_group(0, &self.screen_size_bind_group, &[]);
render_pass.set_bind_group(1, &gpu_state.desktop_colors_bind_group, &[]);
render_pass.draw(0..6, 0..1);
}

View file

@ -9,7 +9,7 @@ use std::{
use eyre::{Context, Result, bail, eyre};
use freedesktop_file_parser::EntryType;
use log::{error, info, warn};
use palette::{FromColor, IntoColor, Oklab};
use palette::Oklab;
use smithay_client_toolkit::{
compositor::{CompositorHandler, CompositorState},
output::{OutputHandler, OutputState},
@ -18,7 +18,7 @@ use smithay_client_toolkit::{
registry_handlers,
seat::{
SeatHandler, SeatState,
pointer::{BTN_LEFT, PointerEventKind, PointerHandler},
pointer::{BTN_LEFT, BTN_RIGHT, PointerEventKind, PointerHandler},
},
shell::{
WaylandSurface,
@ -70,7 +70,7 @@ fn main() -> Result<()> {
shm: Shm::bind(&globals, qh).wrap_err("failed to bind shm")?,
seat_state: SeatState::new(&globals, qh),
gpu: AppGpuState::new()?,
gpu: AppGpuState::new(desktop_files.colors())?,
desktop_files,
pointers: HashMap::new(),
@ -114,6 +114,7 @@ struct OutputSurface {
layer_surface: LayerSurface,
width: u32,
height: u32,
voronoi_progress: f32,
}
impl ProvidesRegistryState for App {
@ -165,6 +166,7 @@ impl OutputHandler for App {
layer_surface,
width: 0,
height: 0,
voronoi_progress: 0.0,
});
}
Err(err) => error!(
@ -233,6 +235,7 @@ impl CompositorHandler for App {
_surface: &wayland_client::protocol::wl_surface::WlSurface,
_time: u32,
) {
dbg!("yeet");
}
fn surface_enter(
@ -293,20 +296,20 @@ impl LayerShellHandler for App {
surface.height = height;
surface.gpu.resize(&self.gpu, width, height);
surface.gpu.draw(&self.gpu);
}
}
// keep it in sync with the gpu implementation
fn color_for_pixel(x: u32, y: u32, width: u32, height: u32) -> palette::Srgb<u8> {
fn color_for_pixel(x: u32, y: u32, width: u32, height: u32) -> Oklab {
let xf = x as f32 / width as f32;
let yf = y as f32 / height as f32;
palette::Srgb::from_color(palette::Oklab {
palette::Oklab {
l: 0.7,
a: xf * 0.8 - 0.4,
b: yf * 0.8 - 0.4,
})
.into_format::<u8>()
b: yf * 0.7 - 0.4,
}
}
impl ShmHandler for App {
@ -373,47 +376,68 @@ impl PointerHandler for App {
events: &[smithay_client_toolkit::seat::pointer::PointerEvent],
) {
for event in events {
if let PointerEventKind::Release {
button: BTN_LEFT, ..
} = event.kind
{
let Some(surface) = self
.layer_surfaces
.iter()
.find(|surface| *surface.layer_surface.wl_surface() == event.surface)
else {
return;
};
let Some(surface) = self
.layer_surfaces
.iter_mut()
.find(|surface| *surface.layer_surface.wl_surface() == event.surface)
else {
return;
};
let srgb = color_for_pixel(
event.position.0 as u32,
event.position.1 as u32,
surface.width,
surface.height,
);
match event.kind {
PointerEventKind::Release {
button: BTN_LEFT, ..
} => {
let oklab = color_for_pixel(
event.position.0 as u32,
event.position.1 as u32,
surface.width,
surface.height,
);
let oklab: Oklab = srgb.into_format::<f32>().into_color();
let best_match = self.desktop_files.find_entry(oklab);
let best_match = self.desktop_files.find_entry(oklab);
if let Some(best_match) = best_match
&& let EntryType::Application(app) = &best_match.file.entry.entry_type
&& let Some(exec) = &app.exec
{
// lol terrible implementation that works well enough
// https://specifications.freedesktop.org/desktop-entry/latest/exec-variables.html
let exec = exec.replace("%U", "").replace("%F", "");
if exec.contains("%") {
warn!(
"Trying to execute insuffiently substituded command-line, refusing: {}",
exec
);
return;
}
if let Err(err) = spawn(&exec) {
error!("Failed to spawn program: {}: {:?}", exec, err);
if let Some(best_match) = best_match
&& let EntryType::Application(app) = &best_match.file.entry.entry_type
&& let Some(exec) = &app.exec
{
// lol terrible implementation that works well enough
// https://specifications.freedesktop.org/desktop-entry/latest/exec-variables.html
let exec = exec.replace("%U", "").replace("%F", "");
if exec.contains("%") {
warn!(
"Trying to execute insuffiently substituded command-line, refusing: {}",
exec
);
return;
}
if let Err(err) = spawn(&exec) {
error!("Failed to spawn program: {}: {:?}", exec, err);
}
}
}
PointerEventKind::Press {
button: BTN_RIGHT, ..
} => {
surface.voronoi_progress = 1.0;
surface
.gpu
.set_voronoi_progress(&self.gpu, surface.voronoi_progress);
surface.gpu.draw(&self.gpu);
}
PointerEventKind::Release {
button: BTN_RIGHT, ..
}
| PointerEventKind::Leave { .. } => {
surface.voronoi_progress = 0.0;
surface
.gpu
.set_voronoi_progress(&self.gpu, surface.voronoi_progress);
surface.gpu.draw(&self.gpu);
}
_ => (),
}
}
}

View file

@ -1,14 +1,19 @@
struct Screen {
struct Input {
size: vec2<f32>,
voronoi_progress: f32,
};
@group(0) @binding(0)
var<uniform> screen: Screen;
var<uniform> input: Input;
@group(1) @binding(0)
var<storage, read> desktop_colors: array<vec4f>;
@vertex
fn vs_main(
@builtin(vertex_index) in_vertex_index: u32,
) -> @builtin(position) vec4<f32> {
// full-screen quad
var pos = array<vec2f, 6>(
vec2(-1.0, 1.0),
vec2(-1.0, -1.0),
@ -23,20 +28,42 @@ fn vs_main(
@fragment
fn fs_main(@builtin(position) pos: vec4<f32>) -> @location(0) vec4<f32> {
var posf = pos.xy / screen.size;
var posf = pos.xy / input.size;
// keep it in sync with the cpu implementation
var color = oklab_to_linear_srgb(vec3<f32>(
var color = vec3<f32>(
0.7,
posf.x * 0.8 - 0.4,
posf.y * 0.8 - 0.4,
));
posf.y * 0.7 - 0.4,
);
return vec4<f32>(color.x, color.y, color.z, 1.0);
var best = vec3f(0.0, 0.0, 0.0);
var best_score = 1000000000000.0;
for (var i: u32 = 0; i < arrayLength(&desktop_colors); i++) {
var elem = desktop_colors[i].xyz;
var score = diff_colors(elem, color);
if (score < best_score) {
best = elem;
best_score = score;
}
}
var voronoi_color = best;
color = mix(color, voronoi_color, input.voronoi_progress);
// keep it in sync with the cpu implementation
var srgbcolor = oklab_to_linear_srgb(color);
return vec4<f32>(srgbcolor.x, srgbcolor.y, srgbcolor.z, 1.0);
}
// keep it in sync with the cpu implementation
fn diff_colors(oklab_a: vec3f, oklab_b: vec3f) -> f32 {
var diff = oklab_a - oklab_b;
var diff_sq = diff * diff;
return diff_sq.x + diff_sq.y + diff_sq.z;
}
fn oklab_to_linear_srgb(oklab: vec3<f32>) -> vec3<f32> {
fn oklab_to_linear_srgb(oklab: vec3f) -> vec3f {
let l_ = 0.2158037573 * oklab.z + (0.3963377774 * oklab.y + oklab.x);
let m_ = -0.0638541728 * oklab.z + (-0.1055613458 * oklab.y + oklab.x);
let s_ = -1.2914855480 * oklab.z + (-0.0894841775 * oklab.y + oklab.x);
@ -49,4 +76,3 @@ fn oklab_to_linear_srgb(oklab: vec3<f32>) -> vec3<f32> {
1.7076147010 * s + (-0.0041960863 * l + -0.7034186147 * m),
);
}