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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions crates/hypercolor-daemon/src/domain/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -363,6 +363,42 @@ impl RuntimeSessionProjection {
})
}

pub(crate) async fn persist_snapshot_observed<F, Fut>(
&self,
path: &Path,
before_snapshot: Fut,
update: F,
) -> super::scene_activation::ProjectionWriteEvidence
where
F: FnOnce(&mut RuntimeSessionSnapshot),
Fut: Future<Output = ()>,
{
use super::scene_activation::{ProjectionDurability, ProjectionWriteEvidence};
let mut save = match self.prepare_save(path, before_snapshot).await {
Ok(save) => save,
Err(error) => {
return ProjectionWriteEvidence {
projection: None,
durability: ProjectionDurability::BeforeAdmission(error.to_string()),
};
}
};
update(&mut save.snapshot);
let (projection, result) = save.commit_observed();
let durability = match result {
Ok(AtomicWriteOutcome::Written) => ProjectionDurability::Written,
Ok(AtomicWriteOutcome::Superseded) => ProjectionDurability::Superseded,
Err(error @ runtime_state::RuntimeSessionError::Persist { .. }) => {
ProjectionDurability::Retrying(error.to_string())
}
Err(error) => ProjectionDurability::BeforeAdmission(error.to_string()),
};
ProjectionWriteEvidence {
projection: Some(projection),
durability,
}
}

pub(crate) async fn flush_persistence(
&self,
path: &Path,
Expand Down Expand Up @@ -693,6 +729,17 @@ pub struct SceneContext {
}

impl SceneContext {
pub(crate) async fn set_selected_brightness(
&self,
output: &super::output::OutputContext,
expected: &super::scene_activation::ObservedScene,
brightness: f32,
) -> Result<super::scene_activation::SelectedBrightnessOutcome, DomainError> {
output
.set_selected_brightness(&self.scenes, expected, brightness)
.await
}

pub(crate) fn new(
scenes: SceneService,
runtime_session: RuntimeSessionService,
Expand Down
12 changes: 12 additions & 0 deletions crates/hypercolor-daemon/src/domain/layout.rs
Original file line number Diff line number Diff line change
Expand Up @@ -676,6 +676,18 @@ impl LayoutContext {
.await
}

pub(crate) async fn apply_selected_under_guard(
&self,
guard: &LayoutUpdateGuard,
layout: SpatialLayout,
expected: super::scene_activation::ObservedScene,
runtime: &LayoutRuntime,
) -> super::scene_activation::SelectedLayoutOutcome {
self.publication
.apply_selected_under_guard(guard, layout, expected, runtime.driver_host())
.await
}

pub(crate) async fn converge_persisted_update(
&self,
runtime: &LayoutRuntime,
Expand Down
71 changes: 71 additions & 0 deletions crates/hypercolor-daemon/src/domain/layout/publication.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,77 @@ impl ActiveLayoutBindingMigration {
}

impl LayoutPublication {
pub(super) async fn apply_selected_under_guard(
&self,
guard: &LayoutUpdateGuard,
layout: SpatialLayout,
expected: crate::domain::scene_activation::ObservedScene,
driver_host: Arc<DaemonDriverHost>,
) -> crate::domain::scene_activation::SelectedLayoutOutcome {
use crate::domain::scene_activation::{
LayoutSceneFence, ProjectionDurability, SelectedLayoutOutcome,
};
let fence = LayoutSceneFence::new(expected);
let selected_layout = layout.clone();
let writes = Arc::new(std::sync::Mutex::new(Vec::new()));
let recorded = Arc::clone(&writes);
let context = self.persistence_context(driver_host);
let result = self
.transactions
.apply_selected_under_guard(guard, layout, Arc::clone(&fence), move |phase| {
let context = context.clone();
let recorded = Arc::clone(&recorded);
async move {
let driver_host = Arc::clone(&context.driver_host);
let evidence = context
.runtime_projection
.persist_snapshot_observed(
&context.runtime_state_path,
async move {
driver_host.refresh_driver_inventory().await;
},
move |snapshot| {
if let LayoutPersistencePhase::Precommit(candidate) = phase {
snapshot.active_layout_id = Some(candidate.layout.id);
if candidate.active_scene_id == Some(SceneId::DEFAULT) {
snapshot.default_scene_zones =
candidate.resolved_zones.to_vec();
}
}
},
)
.await;
let outcome = match &evidence.durability {
ProjectionDurability::Written => LayoutPersistenceOutcome::Written,
ProjectionDurability::Superseded => LayoutPersistenceOutcome::Superseded,
ProjectionDurability::BeforeAdmission(error) => {
LayoutPersistenceOutcome::BeforeAdmission(error.clone())
}
ProjectionDurability::Retrying(error) => {
LayoutPersistenceOutcome::RetryArmed(error.clone())
}
};
recorded
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.push(evidence);
outcome
}
})
.await;
let writes = std::mem::take(
&mut *writes
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner),
);
SelectedLayoutOutcome {
layout: selected_layout,
publication: result.map_err(|error| error.to_string()),
writes,
admitted_scene: fence.admitted(),
}
}

pub(super) fn new(
spatial: SpatialService,
scenes: SceneService,
Expand Down
1 change: 1 addition & 0 deletions crates/hypercolor-daemon/src/domain/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ pub mod openrgb_setup;
pub mod output;
pub mod runtime_zone;
pub mod scene;
pub mod scene_activation;
pub mod scene_tree;
pub mod spatial;
pub mod zone;
Expand Down
32 changes: 32 additions & 0 deletions crates/hypercolor-daemon/src/domain/output.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,38 @@ pub struct OutputContext {
}

impl OutputContext {
pub(crate) async fn set_selected_brightness(
&self,
scenes: &super::scene::SceneService,
expected: &super::scene_activation::ObservedScene,
brightness: f32,
) -> Result<super::scene_activation::SelectedBrightnessOutcome, DomainError> {
use super::scene_activation::{BrightnessDurability, SelectedBrightnessOutcome};
if !(0.0..=1.0).contains(&brightness) {
return Err(DomainError::validation_field(
"brightness",
"brightness must be between 0.0 and 1.0",
));
}
let transition = self.output_power.transition().await;
let _definition = scenes.guard_definition(expected).await?;
let (previous, persistence) = transition
.set_brightness_observed(&self.event_bus, brightness, || {})
.await
.map_err(DomainError::Internal)?;
let durability = match persistence {
crate::device_settings::BrightnessPersistence::Durable => BrightnessDurability::Written,
crate::device_settings::BrightnessPersistence::Retrying(error) => {
BrightnessDurability::Retrying(error.to_string())
}
};
Ok(SelectedBrightnessOutcome {
value: brightness,
previous,
durability,
})
}

#[expect(
clippy::too_many_arguments,
reason = "the composition root supplies the complete output ownership boundary"
Expand Down
50 changes: 47 additions & 3 deletions crates/hypercolor-daemon/src/domain/scene.rs
Original file line number Diff line number Diff line change
Expand Up @@ -159,10 +159,10 @@ pub struct ScenePlanReader(Arc<SceneServiceInner>);
/// Named scene library and activation authority shared by every transport.
#[derive(Clone)]
pub struct SceneLibraryContext {
scene: SceneContext,
pub(super) scene: SceneContext,
effects: crate::domain::effect::EffectContext,
layout: LayoutContext,
output: OutputContext,
pub(super) layout: LayoutContext,
pub(super) output: OutputContext,
event_bus: Arc<HypercolorBus>,
}

Expand Down Expand Up @@ -193,6 +193,15 @@ impl ScenePlanReader {
}

impl SceneService {
pub(crate) async fn guard_definition(
&self,
expected: &super::scene_activation::ObservedScene,
) -> Result<tokio::sync::RwLockReadGuard<'_, SceneManager>, DomainError> {
let manager = self.0.manager.read().await;
expected.validate(&manager, self.0.commits.revision())?;
Ok(manager)
}

/// Own a non-durable scene manager for isolated consumers.
#[must_use]
pub fn in_memory(manager: SceneManager, event_bus: Arc<HypercolorBus>) -> Self {
Expand Down Expand Up @@ -683,6 +692,7 @@ impl SceneService {
store.write().await.save_reserved(pending).map(Some)
}

#[cfg(test)]
pub(crate) async fn publish_layout_activation<F>(
&self,
spatial_engine: &SpatialService,
Expand All @@ -692,10 +702,41 @@ impl SceneService {
expected_resolved_zones_revision: u64,
publish_renderer_state: F,
) -> Result<(), LayoutTransactionRejection>
where
F: FnOnce(SpatialEngine) -> Result<(), LayoutTransactionRejection>,
{
self.publish_guarded_layout_activation(
spatial_engine,
candidate_spatial_engine,
expected_layout,
expected_active_scene_id,
expected_resolved_zones_revision,
None,
publish_renderer_state,
)
.await
}

pub(crate) async fn publish_guarded_layout_activation<F>(
&self,
spatial_engine: &SpatialService,
candidate_spatial_engine: SpatialEngine,
expected_layout: &SpatialLayout,
expected_active_scene_id: Option<SceneId>,
expected_resolved_zones_revision: u64,
fence: Option<&super::scene_activation::LayoutSceneFence>,
publish_renderer_state: F,
) -> Result<(), LayoutTransactionRejection>
where
F: FnOnce(SpatialEngine) -> Result<(), LayoutTransactionRejection>,
{
let mut manager = self.0.manager.write().await;
if let Some(fence) = fence {
fence
.expected
.validate(&manager, self.0.commits.revision())
.map_err(|_| LayoutTransactionRejection::Superseded)?;
}
let source_is_current = manager.active_scene_id().copied() == expected_active_scene_id
&& manager.resolved_zones_revision() == expected_resolved_zones_revision
&& spatial_engine.has_layout(expected_layout);
Expand All @@ -710,6 +751,9 @@ impl SceneService {
.plan
.store(Arc::new(manager.plan_snapshot(ticket.generation())));
spatial_engine.replace(candidate_spatial_engine);
if let Some(fence) = fence {
fence.publish(&manager, ticket.generation());
}
ticket.release(Vec::new());
Ok(())
}
Expand Down
Loading