use std::num::NonZeroUsize;
use std::time::Duration;
use bevy::ecs::reflect::ReflectResource;
use bevy::prelude::Reflect;
use bevy::prelude::Resource;
use bevy::tasks::IoTaskPool;
use thiserror::Error;
use crate::DeviceAccessError;
use crate::DeviceIdSource;
use crate::DeviceKey;
use crate::DeviceKind;
use crate::DiscoveryProgress;
use crate::ReporterId;
use crate::SchemeName;
const DEFAULT_MAX_COMPLETIONS_PER_FRAME: usize = 2;
const DEFAULT_MAX_CONCURRENT_JOBS: usize = 2;
const DEFAULT_PROGRESS_AFTER: Duration = Duration::from_millis(500);
#[derive(Clone, Debug, PartialEq, Eq, Reflect)]
pub enum DiscoveryCadence {
OnDemand,
EventDriven {
backstop: Duration,
},
Periodic {
interval: Duration,
},
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Reflect)]
pub enum ReporterActivation {
Enabled,
Disabled,
}
#[derive(Clone, Debug, PartialEq, Eq, Reflect)]
pub enum ReporterCoverage {
MatchingEvidenceOnly,
EstablishesAbsence(AuthoritativeReporterCoverage),
}
impl ReporterCoverage {
pub(crate) fn establishes_absence_for(&self, device_key: &DeviceKey) -> bool {
match self {
Self::MatchingEvidenceOnly => false,
Self::EstablishesAbsence(authoritative_reporter_coverage) => {
authoritative_reporter_coverage.covers(device_key)
},
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, Reflect)]
#[reflect(opaque)]
pub struct AuthoritativeReporterCoverage {
identity_space: CoveredDeviceIdentitySpace,
}
impl AuthoritativeReporterCoverage {
#[must_use]
pub const fn one(covered_device_identity_space: CoveredDeviceIdentitySpace) -> Self {
Self {
identity_space: covered_device_identity_space,
}
}
fn covers(&self, device_key: &DeviceKey) -> bool { self.identity_space.covers(device_key) }
}
#[derive(Clone, Debug, PartialEq, Eq, Reflect)]
pub enum CoveredDeviceIdentitySpace {
AllKeysOfKind {
kind: DeviceKind,
},
ReportedScheme {
kind: DeviceKind,
scheme: SchemeName,
},
SynthesizedKeysOfKind {
kind: DeviceKind,
},
AuthoredKeysOfKind {
kind: DeviceKind,
},
}
impl CoveredDeviceIdentitySpace {
fn covers(&self, device_key: &DeviceKey) -> bool {
match self {
Self::AllKeysOfKind { kind } => device_key.kind == *kind,
Self::ReportedScheme { kind, scheme } => {
device_key.kind == *kind
&& matches!(&device_key.id, DeviceIdSource::Reported { scheme: key_scheme, .. } if key_scheme == scheme)
},
Self::SynthesizedKeysOfKind { kind } => {
device_key.kind == *kind
&& matches!(&device_key.id, DeviceIdSource::Synthesized { .. })
},
Self::AuthoredKeysOfKind { kind } => {
device_key.kind == *kind
&& matches!(&device_key.id, DeviceIdSource::Authored { .. })
},
}
}
}
#[derive(Clone, Reflect)]
#[reflect(opaque)]
pub struct ReporterRegistration {
cadence: DiscoveryCadence,
startup_requirement: StartupRequirement,
activation: ReporterActivation,
coverage: ReporterCoverage,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum StartupRequirement {
Required,
Optional,
}
impl ReporterRegistration {
#[must_use]
pub const fn required(cadence: DiscoveryCadence, coverage: ReporterCoverage) -> Self {
Self {
cadence,
startup_requirement: StartupRequirement::Required,
activation: ReporterActivation::Enabled,
coverage,
}
}
#[must_use]
pub const fn optional(
cadence: DiscoveryCadence,
activation: ReporterActivation,
coverage: ReporterCoverage,
) -> Self {
Self {
cadence,
startup_requirement: StartupRequirement::Optional,
activation,
coverage,
}
}
pub(crate) const fn activation(&self) -> ReporterActivation { self.activation }
pub(crate) const fn requirement(&self) -> StartupRequirement { self.startup_requirement }
pub(crate) const fn cadence(&self) -> &DiscoveryCadence { &self.cadence }
pub(crate) const fn coverage(&self) -> &ReporterCoverage { &self.coverage }
}
#[derive(Clone, Default, Resource, Reflect)]
#[reflect(Resource, opaque)]
pub struct DiscoveryControl {
reporters: Vec<ReporterControl>,
}
#[derive(Clone)]
struct ReporterControl {
reporter: ReporterId,
startup_requirement: StartupRequirement,
activation: ReporterActivation,
request: DiscoveryRequest,
dirty: DiscoveryDirtyState,
}
#[derive(Clone, Copy)]
pub(crate) enum DiscoveryRequest {
NotRequested,
Requested,
}
#[derive(Clone, Copy)]
pub(crate) enum DiscoveryDirtyState {
Clean,
Dirty,
}
impl DiscoveryControl {
pub(crate) fn register(&mut self, reporter: ReporterId, registration: &ReporterRegistration) {
self.reporters.push(ReporterControl {
reporter,
startup_requirement: registration.requirement(),
activation: registration.activation(),
request: DiscoveryRequest::Requested,
dirty: DiscoveryDirtyState::Clean,
});
}
pub fn enable(&mut self, reporter: ReporterId) -> Result<(), DiscoveryControlError> {
let reporter_control = self.find_mut(reporter)?;
reporter_control.activation = ReporterActivation::Enabled;
reporter_control.request = DiscoveryRequest::Requested;
Ok(())
}
pub fn disable(&mut self, reporter: ReporterId) -> Result<(), DiscoveryControlError> {
let reporter_control = self.find_mut(reporter)?;
if reporter_control.startup_requirement == StartupRequirement::Required {
return Err(DiscoveryControlError::RequiredReporterCannotBeDisabled { reporter });
}
reporter_control.activation = ReporterActivation::Disabled;
reporter_control.request = DiscoveryRequest::NotRequested;
reporter_control.dirty = DiscoveryDirtyState::Clean;
Ok(())
}
pub fn request(&mut self, reporter: ReporterId) -> Result<(), DiscoveryControlError> {
self.find_mut(reporter)?.request = DiscoveryRequest::Requested;
Ok(())
}
pub fn mark_dirty(&mut self, reporter: ReporterId) -> Result<(), DiscoveryControlError> {
self.find_mut(reporter)?.dirty = DiscoveryDirtyState::Dirty;
Ok(())
}
pub(crate) fn activation(&self, reporter: ReporterId) -> ReporterActivation {
self.reporters
.iter()
.find(|reporter_control| reporter_control.reporter == reporter)
.map_or(ReporterActivation::Disabled, |reporter_control| {
reporter_control.activation
})
}
pub(crate) fn take_request(&mut self, reporter: ReporterId) -> DiscoveryRequest {
self.take(
reporter,
|reporter_control| &mut reporter_control.request,
DiscoveryRequest::NotRequested,
)
}
pub(crate) fn take_dirty(&mut self, reporter: ReporterId) -> DiscoveryDirtyState {
self.take(
reporter,
|reporter_control| &mut reporter_control.dirty,
DiscoveryDirtyState::Clean,
)
}
fn find_mut(
&mut self,
reporter: ReporterId,
) -> Result<&mut ReporterControl, DiscoveryControlError> {
self.reporters
.iter_mut()
.find(|reporter_control| reporter_control.reporter == reporter)
.ok_or(DiscoveryControlError::ReporterNotRegistered { reporter })
}
fn take<State: Copy>(
&mut self,
reporter: ReporterId,
select: impl FnOnce(&mut ReporterControl) -> &mut State,
default: State,
) -> State {
self.reporters
.iter_mut()
.find(|reporter_control| reporter_control.reporter == reporter)
.map_or(default, |reporter_control| {
std::mem::replace(select(reporter_control), default)
})
}
}
#[derive(Debug, Error, PartialEq, Eq)]
pub enum DiscoveryControlError {
#[error("device reporter `{reporter:?}` is not registered")]
ReporterNotRegistered {
reporter: ReporterId,
},
#[error("required device reporter `{reporter:?}` cannot be disabled")]
RequiredReporterCannotBeDisabled {
reporter: ReporterId,
},
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Reflect)]
#[reflect(opaque)]
pub struct DiscoveryBatchId(pub(crate) u64);
impl DiscoveryBatchId {
#[must_use]
pub const fn get(self) -> u64 { self.0 }
}
#[derive(Clone, Resource, Reflect)]
#[reflect(Resource)]
pub struct DiscoveryLimits {
max_concurrent_jobs: NonZeroUsize,
max_completions_per_frame: NonZeroUsize,
progress_after: Duration,
}
impl Default for DiscoveryLimits {
fn default() -> Self {
Self {
max_concurrent_jobs: NonZeroUsize::new(DEFAULT_MAX_CONCURRENT_JOBS)
.unwrap_or(NonZeroUsize::MIN),
max_completions_per_frame: NonZeroUsize::new(DEFAULT_MAX_COMPLETIONS_PER_FRAME)
.unwrap_or(NonZeroUsize::MIN),
progress_after: DEFAULT_PROGRESS_AFTER,
}
}
}
impl DiscoveryLimits {
#[must_use]
pub(crate) const fn max_completions_per_frame(&self) -> NonZeroUsize {
self.max_completions_per_frame
}
#[must_use]
pub(crate) const fn progress_after(&self) -> Duration { self.progress_after }
pub(crate) fn effective_max_concurrent_jobs(
&self,
) -> Result<NonZeroUsize, DiscoverySchedulerError> {
let io_task_pool =
IoTaskPool::try_get().ok_or(DiscoverySchedulerError::IoTaskPoolUnavailable)?;
Ok(effective_discovery_job_capacity(
self.max_concurrent_jobs,
io_task_pool.thread_num(),
))
}
pub const fn set_max_concurrent_jobs(&mut self, max_concurrent_jobs: NonZeroUsize) {
self.max_concurrent_jobs = max_concurrent_jobs;
}
pub const fn set_max_completions_per_frame(&mut self, max_completions_per_frame: NonZeroUsize) {
self.max_completions_per_frame = max_completions_per_frame;
}
pub const fn set_progress_after(&mut self, progress_after: Duration) {
self.progress_after = progress_after;
}
}
fn effective_discovery_job_capacity(
configured: NonZeroUsize,
io_thread_count: usize,
) -> NonZeroUsize {
let pool_capacity = io_thread_count.saturating_sub(1).max(1);
let effective = configured.get().min(pool_capacity);
NonZeroUsize::new(effective).unwrap_or(NonZeroUsize::MIN)
}
#[derive(Clone, Debug, PartialEq, Eq, Error, Reflect)]
pub enum DiscoverySchedulerError {
#[error("Bevy IoTaskPool is not initialized")]
IoTaskPoolUnavailable,
}
#[derive(Clone, Debug, PartialEq, Eq, Reflect)]
pub enum DiscoverySchedulerState {
Available,
Failed {
error: DiscoverySchedulerError,
},
}
#[derive(Resource, Reflect)]
#[reflect(Resource)]
pub struct DiscoveryStatus {
pub startup: StartupDiscoveryState,
pub scheduler: DiscoverySchedulerState,
reporters: Vec<ReporterStatusRecord>,
}
#[derive(Reflect)]
struct ReporterStatusRecord {
reporter: ReporterId,
status: ReporterDiscoveryStatus,
}
impl Default for DiscoveryStatus {
fn default() -> Self {
Self {
startup: StartupDiscoveryState::Ready,
scheduler: DiscoverySchedulerState::Available,
reporters: Vec::new(),
}
}
}
impl DiscoveryStatus {
pub(crate) fn register(&mut self, reporter: ReporterId, registration: &ReporterRegistration) {
let activity = match registration.activation() {
ReporterActivation::Enabled => ReporterActivity::Idle,
ReporterActivation::Disabled => ReporterActivity::Disabled,
};
self.reporters.push(ReporterStatusRecord {
reporter,
status: ReporterDiscoveryStatus {
activity,
last_outcome: LastDiscoveryOutcome::NotCompleted,
completed_batches: 0,
},
});
if registration.requirement() == StartupRequirement::Required {
self.startup = StartupDiscoveryState::Discovering;
}
}
pub fn reporter_status(
&self,
reporter: ReporterId,
) -> Result<&ReporterDiscoveryStatus, DiscoveryStatusError> {
self.reporters
.iter()
.find(|reporter_status_record| reporter_status_record.reporter == reporter)
.map(|reporter_status_record| &reporter_status_record.status)
.ok_or(DiscoveryStatusError::ReporterNotRegistered { reporter })
}
pub(crate) fn reporter_status_mut(
&mut self,
reporter: ReporterId,
) -> Result<&mut ReporterDiscoveryStatus, DiscoveryStatusError> {
self.reporters
.iter_mut()
.find(|reporter_status_record| reporter_status_record.reporter == reporter)
.map(|reporter_status_record| &mut reporter_status_record.status)
.ok_or(DiscoveryStatusError::ReporterNotRegistered { reporter })
}
}
#[derive(Debug, Error, PartialEq, Eq)]
pub enum DiscoveryStatusError {
#[error("device reporter `{reporter:?}` has no discovery status")]
ReporterNotRegistered {
reporter: ReporterId,
},
}
#[derive(Clone, Debug, PartialEq, Eq, Reflect)]
pub struct ReporterDiscoveryStatus {
pub activity: ReporterActivity,
pub last_outcome: LastDiscoveryOutcome,
pub completed_batches: u64,
}
#[derive(Clone, Debug, PartialEq, Eq, Reflect)]
pub enum ReporterActivity {
Disabled,
Idle,
Queued {
batch: DiscoveryBatchId,
},
Running {
batch: DiscoveryBatchId,
elapsed: Duration,
progress: DiscoveryProgress,
},
}
#[derive(Clone, Debug, PartialEq, Eq, Reflect)]
pub enum LastDiscoveryOutcome {
NotCompleted,
Succeeded {
batch: DiscoveryBatchId,
duration: Duration,
},
Failed {
batch: DiscoveryBatchId,
duration: Duration,
error: DeviceAccessError,
},
}
#[derive(Clone, Debug, PartialEq, Eq, Reflect)]
pub enum StartupDiscoveryState {
Discovering,
Ready,
BlockedByFailure {
reporter: ReporterId,
error: DeviceAccessError,
},
}
#[derive(Clone, Debug, PartialEq, Eq, Reflect)]
pub enum CompletedDiscoveryOutcome {
Succeeded {
duration: Duration,
},
Failed {
duration: Duration,
error: DeviceAccessError,
},
}
pub(crate) enum DiscoveryTransition {
Progressed {
batch: DiscoveryBatchId,
reporter: ReporterId,
progress: DiscoveryProgress,
completed: usize,
total: usize,
running: usize,
queued: usize,
},
Finished {
batch: DiscoveryBatchId,
reporter: ReporterId,
outcome: CompletedDiscoveryOutcome,
},
StartupChanged { startup: StartupDiscoveryState },
}
#[derive(Default, Resource)]
pub(crate) struct DiscoveryTransitionJournal {
transitions: Vec<DiscoveryTransition>,
}
impl DiscoveryTransitionJournal {
pub(crate) fn record(&mut self, capacity: usize, discovery_transition: DiscoveryTransition) {
if self.transitions.len() >= capacity {
return;
}
self.transitions.push(discovery_transition);
}
pub(crate) fn drain(&mut self) -> Vec<DiscoveryTransition> {
std::mem::take(&mut self.transitions)
}
}
pub(crate) const fn discovery_transition_capacity(
reporters: usize,
discovery_limits: &DiscoveryLimits,
) -> usize {
reporters + discovery_limits.max_completions_per_frame().get() + 1
}
#[cfg(test)]
mod tests {
use std::any::TypeId;
use std::num::NonZeroUsize;
use bevy::app::App;
use bevy::ecs::reflect::AppTypeRegistry;
use bevy::reflect::FromReflect;
use bevy::reflect::PartialReflect;
use bevy::reflect::ReflectMut;
use bevy::reflect::ReflectRef;
use bevy::reflect::structs::DynamicStruct;
use super::AuthoritativeReporterCoverage;
use super::CoveredDeviceIdentitySpace;
use super::DEFAULT_MAX_COMPLETIONS_PER_FRAME;
use super::DEFAULT_MAX_CONCURRENT_JOBS;
use super::DEFAULT_PROGRESS_AFTER;
use super::DiscoveryCadence;
use super::DiscoveryControl;
use super::DiscoveryControlError;
use super::DiscoveryDirtyState;
use super::DiscoveryLimits;
use super::DiscoveryRequest;
use super::ReporterActivation;
use super::ReporterCoverage;
use super::ReporterRegistration;
use super::StartupRequirement;
use super::effective_discovery_job_capacity;
use crate::AuthoredId;
use crate::DeviceIdSource;
use crate::DeviceKey;
use crate::DeviceKind;
use crate::ReporterId;
#[test]
fn default_limits_expose_scheduler_and_progress_policy() {
let discovery_limits = DiscoveryLimits::default();
assert_eq!(
discovery_limits.max_concurrent_jobs.get(),
DEFAULT_MAX_CONCURRENT_JOBS
);
assert_eq!(
discovery_limits.max_completions_per_frame().get(),
DEFAULT_MAX_COMPLETIONS_PER_FRAME
);
assert_eq!(discovery_limits.progress_after(), DEFAULT_PROGRESS_AFTER);
}
#[test]
fn effective_capacity_reserves_one_thread_when_possible() {
let configured =
NonZeroUsize::new(DEFAULT_MAX_CONCURRENT_JOBS * 2).unwrap_or(NonZeroUsize::MIN);
assert_eq!(effective_discovery_job_capacity(configured, 4).get(), 3);
assert_eq!(effective_discovery_job_capacity(configured, 2).get(), 1);
assert_eq!(effective_discovery_job_capacity(configured, 1).get(), 1);
assert_eq!(
effective_discovery_job_capacity(NonZeroUsize::MIN, 8).get(),
1
);
}
#[test]
fn coverage_requires_an_explicit_identity_space_and_never_crosses_device_kind()
-> Result<(), Box<dyn std::error::Error>> {
let display_key = DeviceKey {
kind: DeviceKind::Display,
id: DeviceIdSource::Authored {
value: AuthoredId::new("studio-display")?,
},
};
let camera_coverage = ReporterCoverage::EstablishesAbsence(
AuthoritativeReporterCoverage::one(CoveredDeviceIdentitySpace::AllKeysOfKind {
kind: DeviceKind::Camera,
}),
);
assert!(!ReporterCoverage::MatchingEvidenceOnly.establishes_absence_for(&display_key));
assert!(!camera_coverage.establishes_absence_for(&display_key));
assert!(
ReporterCoverage::EstablishesAbsence(AuthoritativeReporterCoverage::one(
CoveredDeviceIdentitySpace::AuthoredKeysOfKind {
kind: DeviceKind::Display,
}
))
.establishes_absence_for(&display_key)
);
Ok(())
}
#[test]
fn reporter_coverage_registers_for_reflection_without_manual_app_calls() {
let app = App::new();
let world = app.world();
let type_registry = world.resource::<AppTypeRegistry>().read();
assert!(type_registry.contains(TypeId::of::<ReporterCoverage>()));
assert!(type_registry.contains(TypeId::of::<AuthoritativeReporterCoverage>()));
assert!(type_registry.contains(TypeId::of::<CoveredDeviceIdentitySpace>()));
drop(type_registry);
}
#[test]
fn reflection_cannot_construct_or_mutate_authoritative_reporter_coverage() {
let display_coverage = CoveredDeviceIdentitySpace::AllKeysOfKind {
kind: DeviceKind::Display,
};
let mut authoritative_reporter_coverage =
AuthoritativeReporterCoverage::one(display_coverage.clone());
assert!(matches!(
authoritative_reporter_coverage.reflect_ref(),
ReflectRef::Opaque(_)
));
assert!(matches!(
authoritative_reporter_coverage.reflect_mut(),
ReflectMut::Opaque(_)
));
let mut unchecked_coverage = DynamicStruct::default();
unchecked_coverage.insert("identity_space", display_coverage.clone());
assert!(AuthoritativeReporterCoverage::from_reflect(&unchecked_coverage).is_none());
assert!(
authoritative_reporter_coverage
.try_apply(&unchecked_coverage)
.is_err()
);
assert_eq!(
authoritative_reporter_coverage,
AuthoritativeReporterCoverage::one(display_coverage)
);
}
#[test]
fn dirty_notifications_coalesce_in_runtime_control() {
let reporter = ReporterId(0);
let registration = ReporterRegistration::required(
DiscoveryCadence::OnDemand,
ReporterCoverage::MatchingEvidenceOnly,
);
assert!(matches!(
registration.coverage(),
ReporterCoverage::MatchingEvidenceOnly
));
let mut discovery_control = DiscoveryControl::default();
discovery_control.register(reporter, ®istration);
assert_eq!(discovery_control.mark_dirty(reporter), Ok(()));
assert_eq!(discovery_control.mark_dirty(reporter), Ok(()));
assert!(matches!(
discovery_control.take_dirty(reporter),
DiscoveryDirtyState::Dirty
));
assert!(matches!(
discovery_control.take_dirty(reporter),
DiscoveryDirtyState::Clean
));
}
#[test]
fn rejected_controls_leave_registered_reporter_state_unchanged() {
let reporter = ReporterId(0);
let unknown_reporter = ReporterId(u32::MAX);
let registration = ReporterRegistration::required(
DiscoveryCadence::OnDemand,
ReporterCoverage::MatchingEvidenceOnly,
);
let mut discovery_control = DiscoveryControl::default();
discovery_control.register(reporter, ®istration);
assert_eq!(
discovery_control.enable(unknown_reporter),
Err(DiscoveryControlError::ReporterNotRegistered {
reporter: unknown_reporter,
})
);
assert_control_state_unchanged(&discovery_control, reporter);
assert_eq!(
discovery_control.disable(unknown_reporter),
Err(DiscoveryControlError::ReporterNotRegistered {
reporter: unknown_reporter,
})
);
assert_control_state_unchanged(&discovery_control, reporter);
assert_eq!(
discovery_control.request(unknown_reporter),
Err(DiscoveryControlError::ReporterNotRegistered {
reporter: unknown_reporter,
})
);
assert_control_state_unchanged(&discovery_control, reporter);
assert_eq!(
discovery_control.mark_dirty(unknown_reporter),
Err(DiscoveryControlError::ReporterNotRegistered {
reporter: unknown_reporter,
})
);
assert_control_state_unchanged(&discovery_control, reporter);
assert_eq!(
discovery_control.disable(reporter),
Err(DiscoveryControlError::RequiredReporterCannotBeDisabled { reporter })
);
assert_control_state_unchanged(&discovery_control, reporter);
}
#[test]
fn reflection_preserves_registration_and_control_invariants() {
let reporter_registration = ReporterRegistration::required(
DiscoveryCadence::OnDemand,
ReporterCoverage::MatchingEvidenceOnly,
);
assert!(matches!(
reporter_registration.reflect_ref(),
ReflectRef::Opaque(_)
));
let mut unchecked_registration = DynamicStruct::default();
unchecked_registration.insert("cadence", DiscoveryCadence::OnDemand);
unchecked_registration.insert("activation", ReporterActivation::Disabled);
assert!(ReporterRegistration::from_reflect(&unchecked_registration).is_none());
let mut discovery_control = DiscoveryControl::default();
assert!(matches!(
discovery_control.reflect_ref(),
ReflectRef::Opaque(_)
));
assert!(matches!(
discovery_control.reflect_mut(),
ReflectMut::Opaque(_)
));
let mut unchecked_control = DynamicStruct::default();
unchecked_control.insert("reporters", Vec::<u8>::new());
assert!(DiscoveryControl::from_reflect(&unchecked_control).is_none());
assert!(discovery_control.try_apply(&unchecked_control).is_err());
}
fn assert_control_state_unchanged(discovery_control: &DiscoveryControl, reporter: ReporterId) {
assert_eq!(discovery_control.reporters.len(), 1);
let reporter_control = &discovery_control.reporters[0];
assert_eq!(reporter_control.reporter, reporter);
assert_eq!(
reporter_control.startup_requirement,
StartupRequirement::Required
);
assert_eq!(reporter_control.activation, ReporterActivation::Enabled);
assert!(matches!(
reporter_control.request,
DiscoveryRequest::Requested
));
assert!(matches!(reporter_control.dirty, DiscoveryDirtyState::Clean));
}
}