fix: one frame-callback chain per animated surface; hex never panics

This commit is contained in:
elwrcl committed 2026-10-03 21:37:05 +03:00
1 parent 45bcde0b9c
commit 50ad31017f
3 files changed
+126 -34

No files matched your search

+35 -29
View File
@@ -21,8 +21,8 @@ use wayland_client::{
use wayland_protocols::wp::viewporter::client::wp_viewport::WpViewport; use wayland_protocols::wp::viewporter::client::wp_viewport::WpViewport;
use crate::lock::Lock; use crate::lock::Lock;
use crate::theme::Palette;
use crate::theme::gl::{Frame, Paint}; use crate::theme::gl::{Frame, Paint};
use crate::theme::{Pacer, Palette, Step};
use crate::wayland::{State, Wayland}; use crate::wayland::{State, Wayland};
pub const NAMESPACE: &str = "valw-backdrop"; pub const NAMESPACE: &str = "valw-backdrop";
@@ -75,8 +75,7 @@ struct Surface {
changed: Instant, changed: Instant,
/// A wallpaper palette being worked out on a thread. /// A wallpaper palette being worked out on a thread.
incoming: Option<mpsc::Receiver<Option<Palette>>>, incoming: Option<mpsc::Receiver<Option<Palette>>>,
/// Frame callbacks since the last draw. pacer: Pacer,
skipped: u32,
} }
#[derive(Default)] #[derive(Default)]
@@ -143,7 +142,7 @@ pub fn add(state: &mut State, qh: &QueueHandle<State>, output: &WlOutput) {
to: palette, to: palette,
changed: now, changed: now,
incoming: None, incoming: None,
skipped: 0, pacer: Pacer::new(DRAW_EVERY),
}); });
} }
} }
@@ -193,27 +192,24 @@ impl Backdrop {
} }
}, },
} }
s.draw(qh); let step = s.pacer.configure();
s.step(step, qh);
} }
pub fn frame_done(&mut self, surface: &WlSurface, qh: &QueueHandle<State>) { pub fn frame_done(&mut self, surface: &WlSurface, qh: &QueueHandle<State>) {
if let Some(i) = self.index(surface) { if let Some(i) = self.index(surface) {
self.surfaces[i].draw(qh); let s = &mut self.surfaces[i];
let step = s.pacer.frame();
s.step(step, qh);
} }
} }
} }
impl Surface { impl Surface {
fn draw(&mut self, qh: &QueueHandle<State>) { /// Every configure and frame callback: notice a pause (the overview was
// Between draws, only ask for the next frame (an empty commit). /// closed) and pick up a new wallpaper palette, then draw or wait as
self.skipped += 1; /// the pacer says.
if self.skipped < DRAW_EVERY && self.last_frame.is_some() { fn step(&mut self, step: Step, qh: &QueueHandle<State>) {
let surface = self.layer.wl_surface();
surface.frame(qh, FrameCallbackData(surface.clone()));
surface.commit();
return;
}
self.skipped = 0;
let now = Instant::now(); let now = Instant::now();
if should_refresh(self.last_frame, now) && self.incoming.is_none() { if should_refresh(self.last_frame, now) && self.incoming.is_none() {
let (tx, rx) = mpsc::channel(); let (tx, rx) = mpsc::channel();
@@ -237,21 +233,31 @@ impl Surface {
} }
self.incoming = None; self.incoming = None;
} }
// Every callback counts, drawn or not: skipped frames are not a pause.
self.last_frame = Some(now); self.last_frame = Some(now);
let Some(paint) = &self.paint else {
return;
};
let surface = self.layer.wl_surface(); let surface = self.layer.wl_surface();
surface.frame(qh, FrameCallbackData(surface.clone())); match step {
let frame = Frame { Step::Idle => {}
time: (now - self.start).as_secs_f32(), Step::Wait => {
motion: 0.0, surface.frame(qh, FrameCallbackData(surface.clone()));
speed: SPEED, surface.commit();
scale: 1.0, }
palette: ease(&self.from, &self.to, now - self.changed), Step::Draw => {
}; let Some(paint) = &self.paint else {
if let Err(e) = paint.draw(&frame) { return;
tracing::warn!("backdrop on {}: {e:#}", self.name); };
surface.frame(qh, FrameCallbackData(surface.clone()));
let frame = Frame {
time: (now - self.start).as_secs_f32(),
motion: 0.0,
speed: SPEED,
scale: 1.0,
palette: ease(&self.from, &self.to, now - self.changed),
};
if let Err(e) = paint.draw(&frame) {
tracing::warn!("backdrop on {}: {e:#}", self.name);
}
}
} }
} }
} }
+8 -3
View File
@@ -32,8 +32,8 @@ use wayland_protocols::wp::viewporter::client::wp_viewport::WpViewport;
use self::layout::Rect; use self::layout::Rect;
use crate::editor::text; use crate::editor::text;
use crate::error::{Cancelled, HintExt}; use crate::error::{Cancelled, HintExt};
use crate::theme::Palette;
use crate::theme::gl::{Frame, Paint}; use crate::theme::gl::{Frame, Paint};
use crate::theme::{Pacer, Palette, Step};
use crate::wayland::{Output, State, Wayland}; use crate::wayland::{Output, State, Wayland};
pub const NAMESPACE: &str = "valw-picker"; pub const NAMESPACE: &str = "valw-picker";
@@ -65,6 +65,8 @@ pub struct Picker {
from: Palette, from: Palette,
changed: Instant, changed: Instant,
overlay_dirty: bool, overlay_dirty: bool,
/// One frame-callback chain, however often the surface is configured.
pacer: Pacer,
shift: bool, shift: bool,
outcome: Option<Option<usize>>, outcome: Option<Option<usize>>,
error: Option<anyhow::Error>, error: Option<anyhow::Error>,
@@ -136,6 +138,7 @@ pub fn run(wl: &mut Wayland, output: &Output) -> Result<Window> {
from: first, from: first,
changed: now, changed: now,
overlay_dirty: true, overlay_dirty: true,
pacer: Pacer::new(1),
shift: false, shift: false,
outcome: None, outcome: None,
error: None, error: None,
@@ -223,7 +226,9 @@ impl Picker {
}, },
} }
self.overlay_dirty = true; self.overlay_dirty = true;
self.draw(qh); if self.pacer.configure() == Step::Draw {
self.draw(qh);
}
} }
pub fn closed(&mut self, layer: &LayerSurface) { pub fn closed(&mut self, layer: &LayerSurface) {
@@ -234,7 +239,7 @@ impl Picker {
} }
pub fn frame_done(&mut self, surface: &WlSurface, qh: &QueueHandle<State>) { pub fn frame_done(&mut self, surface: &WlSurface, qh: &QueueHandle<State>) {
if self.is(surface) { if self.is(surface) && self.pacer.frame() == Step::Draw {
self.draw(qh); self.draw(qh);
} }
} }
+83 -2
View File
@@ -9,7 +9,7 @@ use image::RgbaImage;
pub mod gl; pub mod gl;
/// Three colours, dark to light, as linear 0–1 RGB. /// Three colours, dark to light, as 0–1 sRGB.
#[derive(Debug, Clone, Copy, PartialEq)] #[derive(Debug, Clone, Copy, PartialEq)]
pub struct Palette { pub struct Palette {
pub base: [f32; 3], pub base: [f32; 3],
@@ -37,7 +37,7 @@ impl Palette {
pub fn hex(s: &str) -> Option<[f32; 3]> { pub fn hex(s: &str) -> Option<[f32; 3]> {
let s = s.trim().trim_start_matches('#'); let s = s.trim().trim_start_matches('#');
if s.len() != 6 { if s.len() != 6 || !s.is_ascii() {
return None; return None;
} }
let v = |i: usize| { let v = |i: usize| {
@@ -48,6 +48,59 @@ pub fn hex(s: &str) -> Option<[f32; 3]> {
Some([v(0)?, v(2)?, v(4)?]) Some([v(0)?, v(2)?, v(4)?])
} }
/// What an animated surface does on a configure or a frame callback.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Step {
/// Draw (and ask for the next frame).
Draw,
/// Only ask for the next frame (an empty commit).
Wait,
/// Nothing: a frame is already on its way.
Idle,
}
/// Frame-callback pacing for an animated surface: exactly one chain of
/// callbacks, however often the surface is configured, drawing on every
/// `every`-th callback.
#[derive(Debug, Clone)]
pub struct Pacer {
every: u32,
count: u32,
running: bool,
}
impl Pacer {
pub fn new(every: u32) -> Pacer {
Pacer {
every: every.max(1),
count: 0,
running: false,
}
}
/// A configure: draw now, unless the chain already runs.
pub fn configure(&mut self) -> Step {
if self.running {
Step::Idle
} else {
self.running = true;
self.count = 0;
Step::Draw
}
}
/// A frame callback arrived.
pub fn frame(&mut self) -> Step {
self.count += 1;
if self.count >= self.every {
self.count = 0;
Step::Draw
} else {
Step::Wait
}
}
}
/// A Noctalia community palette file at `mode`. /// A Noctalia community palette file at `mode`.
pub fn from_noctalia_file(text: &str, mode: &str) -> Option<Palette> { pub fn from_noctalia_file(text: &str, mode: &str) -> Option<Palette> {
let json: serde_json::Value = serde_json::from_str(text).ok()?; let json: serde_json::Value = serde_json::from_str(text).ok()?;
@@ -273,6 +326,34 @@ mod tests {
assert_eq!(theme_choice("not toml ["), None); assert_eq!(theme_choice("not toml ["), None);
} }
#[test]
fn hex_rejects_non_ascii_without_panicking() {
assert_eq!(hex("aé€"), None);
assert_eq!(hex("#ééé"), None);
}
#[test]
fn one_frame_chain_per_surface() {
let mut p = Pacer::new(2);
assert_eq!(
p.configure(),
Step::Draw,
"first configure starts the chain"
);
assert_eq!(
p.configure(),
Step::Idle,
"a reconfigure doesn't start a second one"
);
assert_eq!(p.frame(), Step::Wait);
assert_eq!(p.frame(), Step::Draw);
assert_eq!(p.frame(), Step::Wait);
let mut every = Pacer::new(1);
every.configure();
assert_eq!(every.frame(), Step::Draw);
assert_eq!(every.frame(), Step::Draw);
}
#[test] #[test]
fn lerp_eases() { fn lerp_eases() {
let a = Palette::DEFAULT; let a = Palette::DEFAULT;