Skip to content

Commit

Permalink
Refactor callbacks + tuple lock handling
Browse files Browse the repository at this point in the history
This change fundamentally changes how change callbacks work on Dynamics.
Prior to this change, callbacks executed on the thread that was
performing the change. This could lead to situations where multiple
threads were executing callback chains which leads to unpredictable
locking patterns on the dynamics. The basic deadlock detection was not
enough.

This change defers callbacks to a single callback thread. This thread
ensures that no dynamic can have callbacks enqueued more than once.
By limiting execution to one set of callbacks at any given time, this
greatly reduces the surface for locks to contend with each other.

The next issue was how tuple-related functions like for_each/map_each
were acquiring their locks. By calling a.read() then b.read(), this was
causing a to be held in a locked state while b was being aquired. If
users are careful to always acquire their locks in this order,
everything is fine. But with Cushy there can be unexpected situations
where these locks are being held.

This change also refactors lock acquisition for tuples to try to acquire
all the locks in a non-blocking way. If any lock woould block, the initial
locks are dropped while the lock that would block is waited on. After
this is acquired the process starts over again to gain all the locks.
This isn't perfect, but it doesn't require unsafe. With unsafe, we could
in theory create a ring of callbacks that handles acquiring all of the
locks into MaybeUninits. Upon successfully calling all callbacks, the
values can be assumed init. But writing all of this in macro_rules isn't
fun, and the current solution alleviates the main problem
  • Loading branch information
ecton committed Nov 18, 2024
1 parent 477ce9b commit 68a7df1
Show file tree
Hide file tree
Showing 9 changed files with 732 additions and 212 deletions.
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ native-dialogs = ["dep:rfd"]
kludgine = { git = "https://github.com/khonsulabs/kludgine", features = [
"app",
] }
figures = { version = "0.4.0" }
figures = { version = "0.4.2" }
alot = "0.3.2"
interner = "0.2.1"
kempt = "0.2.1"
Expand Down
28 changes: 28 additions & 0 deletions examples/7guis-counter.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
use cushy::value::Dynamic;
use cushy::widget::MakeWidget;
use cushy::widgets::label::Displayable;
use cushy::Run;
use figures::units::Lp;

fn main() -> cushy::Result {
let count = Dynamic::new(0_usize);

count
.to_label()
.expand()
.and(
"Count"
.into_button()
.on_click(move |_| {
*count.lock() += 1;
})
.expand(),
)
.into_columns()
.pad()
.width(Lp::inches(3))
.into_window()
.titled("Counter")
.resize_to_fit(true)
.run()
}
30 changes: 30 additions & 0 deletions examples/7guis-temperature-converter.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
use cushy::value::Dynamic;
use cushy::widget::MakeWidget;
use cushy::widgets::input::InputValue;
use cushy::Run;
use figures::units::Lp;

fn main() -> cushy::Result {
let celsius = Dynamic::new(100f32);
let farenheit = celsius.linked(
|celsius| *celsius * 9. / 5. + 32.,
|farenheit| (*farenheit - 32.) * 5. / 9.,
);

let celsius_string = celsius.linked_string();
let farenheight_string = farenheit.linked_string();

celsius_string
.into_input()
.expand()
.and("Celsius =")
.and(farenheight_string.into_input().expand())
.and("Farenheit")
.into_columns()
.pad()
.width(Lp::inches(4))
.into_window()
.titled("Temperature Converter")
.resize_to_fit(true)
.run()
}
86 changes: 86 additions & 0 deletions examples/7guis-timer.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
use std::time::{Duration, Instant};

use cushy::value::{Destination, Dynamic, DynamicReader, Source};
use cushy::widget::MakeWidget;
use cushy::widgets::progress::Progressable;
use cushy::widgets::slider::Slidable;
use cushy::{Open, PendingApp};
use figures::units::Lp;

#[derive(PartialEq, Debug, Clone)]
struct Timer {
started_at: Instant,
duration: Duration,
}

impl Default for Timer {
fn default() -> Self {
Self {
started_at: Instant::now() - Duration::from_secs(1),
duration: Duration::from_secs(1),
}
}
}

fn main() -> cushy::Result {
let pending = PendingApp::default();
let cushy = pending.cushy().clone();
let _runtime = cushy.enter_runtime();

let timer = Dynamic::<Timer>::default();
let duration = timer.linked_accessor(|timer| &timer.duration, |timer| &mut timer.duration);

let elapsed = spawn_timer(&timer);
let duration_label = duration.map_each(|duration| format!("{}s", duration.as_secs_f32()));

elapsed
.progress_bar_between(
duration
.weak_clone()
.map_each_cloned(|duration| Duration::ZERO..=duration),
)
.fit_horizontally()
.and(duration_label)
.and(
"Duration"
.and(
duration
.slider_between(Duration::ZERO, Duration::from_secs(30))
.expand_horizontally(),
)
.into_columns(),
)
.and("Reset".into_button().on_click(move |_| {
timer.lock().started_at = Instant::now();
}))
.into_rows()
.pad()
.width(Lp::inches(4))
.into_window()
.titled("Timer")
.resize_to_fit(true)
.run_in(pending)
}

fn spawn_timer(timer: &Dynamic<Timer>) -> DynamicReader<Duration> {
let timer = timer.create_reader();
let elapsed = Dynamic::new(timer.map_ref(|timer| timer.duration));
let elapsed_reader = elapsed.weak_clone().into_reader();
std::thread::spawn(move || loop {
let settings = timer.get();

// Update the elapsed time, clamping to the duration of the timer.
let duration_since_started = settings.started_at.elapsed().min(settings.duration);
elapsed.set(duration_since_started);

if duration_since_started < settings.duration {
// The timer is still running, "tick" the timer by sleeping and
// allow the loop to continue.
std::thread::sleep(Duration::from_millis(16));
} else {
// Block the thread until the timer settings have been changed.
timer.block_until_updated();
}
});
elapsed_reader
}
39 changes: 31 additions & 8 deletions src/animation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1053,6 +1053,27 @@ where
}
}

impl LinearInterpolate for Duration {
#[allow(clippy::cast_precision_loss)]
fn lerp(&self, target: &Self, percent: f32) -> Self {
let nanos = self.as_nanos().lerp(&target.as_nanos(), percent);
let seconds = nanos / 1_000_000_000;
let subsec_nanos = nanos % 1_000_000_000;
Self::new(
u64::try_from(seconds).unwrap_or(u64::MAX),
subsec_nanos as u32,
)
}
}
impl PercentBetween for Duration {
fn percent_between(&self, min: &Self, max: &Self) -> ZeroToOne {
let this = self.as_secs_f32();
let min = min.as_secs_f32();
let max = max.as_secs_f32();
this.percent_between(&min, &max)
}
}

#[test]
fn integer_lerps() {
#[track_caller]
Expand Down Expand Up @@ -1156,14 +1177,16 @@ macro_rules! impl_percent_between {
clippy::cast_lossless
)]
fn percent_between(&self, min: &Self, max: &Self) -> ZeroToOne {
assert!(min <= max, "percent_between requires min <= max");
assert!(
self >= min && self <= max,
"self must satisfy min <= self <= max"
);

let range = max.$sub(*min);
ZeroToOne::from(self.$sub(*min) as $float / range as $float)
if min >= max {
return ZeroToOne::ZERO;
} else if self <= min {
ZeroToOne::ZERO
} else if self >= max {
ZeroToOne::ONE
} else {
let range = max.$sub(*min);
ZeroToOne::from(self.$sub(*min) as $float / range as $float)
}
}
}
};
Expand Down
3 changes: 2 additions & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -328,7 +328,8 @@ fn initialize_tracing() {
Targets::new()
.with_target("winit", Level::ERROR)
.with_target("wgpu", Level::ERROR)
.with_target("naga", Level::ERROR),
.with_target("naga", Level::ERROR)
.with_default(MAX_LEVEL),
)
.try_init();
}
Expand Down
Loading

0 comments on commit 68a7df1

Please sign in to comment.