use std::sync::Arc;
use crate::engine::boundary::{BoundaryContext, BoundaryResource};
use crate::engine::error::ECSResult;
use crate::engine::types::ChannelID;
use super::store::Environment;
#[cfg(feature = "gpu")]
use super::uniform::EnvUniformBuffer;
pub struct EnvironmentBoundary {
env: Arc<Environment>,
owned_channels: Vec<ChannelID>,
#[cfg(feature = "gpu")]
uniform: Option<EnvUniformBuffer>,
}
impl EnvironmentBoundary {
pub fn new(env: Arc<Environment>) -> Self {
let owned_channels = env.all_channel_ids();
Self {
env,
owned_channels,
#[cfg(feature = "gpu")]
uniform: None,
}
}
pub fn environment(&self) -> &Arc<Environment> {
&self.env
}
#[cfg(feature = "gpu")]
pub fn with_uniform(mut self, uniform: EnvUniformBuffer) -> Self {
self.uniform = Some(uniform);
self
}
#[cfg(feature = "gpu")]
pub fn uniform(&self) -> Option<&EnvUniformBuffer> {
self.uniform.as_ref()
}
#[cfg(feature = "gpu")]
pub fn uniform_mut(&mut self) -> Option<&mut EnvUniformBuffer> {
self.uniform.as_mut()
}
}
impl BoundaryResource for EnvironmentBoundary {
fn name(&self) -> &str {
"EnvironmentBoundary"
}
fn channels(&self) -> &[ChannelID] {
&self.owned_channels
}
fn begin_tick(&mut self, _ctx: &mut BoundaryContext<'_>) -> ECSResult<()> {
Ok(())
}
fn finalise(
&mut self,
_ctx: &mut BoundaryContext<'_>,
channels: &[ChannelID],
) -> ECSResult<()> {
if !self.env.has_any_dirty_channels(channels.iter().copied())? {
return Ok(());
}
#[cfg(feature = "gpu")]
if let Some(ref mut uniform) = self.uniform {
let touched_owned = channels.iter().try_fold(false, |touched, &id| {
Ok::<_, crate::environment::EnvironmentError>(
touched || (uniform.owns_channel(id) && self.env.is_channel_dirty(id)?),
)
})?;
if touched_owned {
uniform.mark_cpu_dirty();
}
}
self.env.clear_dirty_for_channels(channels)?;
Ok(())
}
fn end_tick(&mut self, _ctx: &mut BoundaryContext<'_>) -> ECSResult<()> {
let remaining = self.env.dirty_channel_ids()?;
if remaining.is_empty() {
return Ok(());
}
#[cfg(feature = "gpu")]
if let Some(ref mut uniform) = self.uniform {
if remaining.iter().any(|&id| uniform.owns_channel(id)) {
uniform.mark_cpu_dirty();
}
}
self.env.clear_dirty()?;
Ok(())
}
fn as_any(&self) -> &dyn std::any::Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
self
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::engine::error::ECSError;
use crate::environment::builder::EnvironmentBuilder;
use crate::environment::EnvironmentError;
use std::sync::Arc;
fn make_boundary() -> (Arc<Environment>, EnvironmentBoundary) {
let env = EnvironmentBuilder::new()
.register::<f32>("interest_rate", 0.05)
.unwrap()
.register::<u32>("world_width", 100)
.unwrap()
.build()
.unwrap();
let boundary = EnvironmentBoundary::new(Arc::clone(&env));
(env, boundary)
}
#[test]
fn begin_tick_is_noop() {
let (_env, mut boundary) = make_boundary();
let mut ctx = BoundaryContext::empty();
boundary.begin_tick(&mut ctx).unwrap();
}
#[test]
fn finalise_clears_specified_dirty_channels() {
let (env, mut boundary) = make_boundary();
let id_rate = env.channel_of("interest_rate").unwrap();
let id_width = env.channel_of("world_width").unwrap();
env.set::<f32>("interest_rate", 0.10).unwrap();
env.set::<u32>("world_width", 200).unwrap();
let mut ctx = BoundaryContext::empty();
boundary.finalise(&mut ctx, &[id_rate]).unwrap();
let dirty = env.dirty_channel_ids().unwrap();
assert!(
!dirty.contains(&id_rate),
"interest_rate should have been cleared"
);
assert!(
dirty.contains(&id_width),
"world_width should still be dirty"
);
}
#[test]
fn finalise_ignores_unrelated_channels() {
let (env, mut boundary) = make_boundary();
let id_rate = env.channel_of("interest_rate").unwrap();
env.set::<f32>("interest_rate", 0.10).unwrap();
let unrelated_id: ChannelID = 9999;
let mut ctx = BoundaryContext::empty();
boundary.finalise(&mut ctx, &[unrelated_id]).unwrap();
let dirty = env.dirty_channel_ids().unwrap();
assert!(dirty.contains(&id_rate));
}
#[test]
fn finalise_noop_when_env_is_clean() {
let (env, mut boundary) = make_boundary();
let id_rate = env.channel_of("interest_rate").unwrap();
let mut ctx = BoundaryContext::empty();
boundary.finalise(&mut ctx, &[id_rate]).unwrap();
assert!(env.dirty_channel_ids().unwrap().is_empty());
}
#[test]
fn end_tick_clears_all_remaining_dirty_channels() {
let (env, mut boundary) = make_boundary();
env.set::<f32>("interest_rate", 0.10).unwrap();
env.set::<u32>("world_width", 200).unwrap();
let mut ctx = BoundaryContext::empty();
boundary.end_tick(&mut ctx).unwrap();
assert!(env.dirty_channel_ids().unwrap().is_empty());
}
#[test]
fn end_tick_noop_when_already_clean() {
let (env, mut boundary) = make_boundary();
let mut ctx = BoundaryContext::empty();
boundary.end_tick(&mut ctx).unwrap();
assert!(env.dirty_channel_ids().unwrap().is_empty());
}
#[test]
fn finalise_then_end_tick_leaves_env_clean() {
let (env, mut boundary) = make_boundary();
let id_rate = env.channel_of("interest_rate").unwrap();
let id_width = env.channel_of("world_width").unwrap();
env.set::<f32>("interest_rate", 0.10).unwrap();
env.set::<u32>("world_width", 200).unwrap();
let mut ctx = BoundaryContext::empty();
boundary.finalise(&mut ctx, &[id_rate]).unwrap();
boundary.end_tick(&mut ctx).unwrap();
assert!(env.dirty_channel_ids().unwrap().is_empty());
let _ = id_width; }
#[test]
fn finalise_propagates_poisoned_dirty_channel_error() {
use std::thread;
let (env, mut boundary) = make_boundary();
let id_rate = env.channel_of("interest_rate").unwrap();
let env_for_thread = Arc::clone(&env);
let _ = thread::spawn(move || env_for_thread.poison_dirty_channels_for_test()).join();
let err = boundary
.finalise(&mut BoundaryContext::empty(), &[id_rate])
.unwrap_err();
assert!(matches!(
err,
ECSError::Environment(EnvironmentError::LockPoisoned {
what: "environment dirty channels"
})
));
}
#[test]
fn end_tick_propagates_poisoned_dirty_channel_error() {
use std::thread;
let (env, mut boundary) = make_boundary();
let env_for_thread = Arc::clone(&env);
let _ = thread::spawn(move || env_for_thread.poison_dirty_channels_for_test()).join();
let err = boundary
.end_tick(&mut BoundaryContext::empty())
.unwrap_err();
assert!(matches!(
err,
ECSError::Environment(EnvironmentError::LockPoisoned {
what: "environment dirty channels"
})
));
}
}
#[cfg(all(test, feature = "gpu"))]
mod gpu_tests {
use super::*;
use crate::environment::builder::EnvironmentBuilder;
use crate::environment::uniform::EnvUniformBuffer;
use std::sync::Arc;
fn make_gpu_boundary() -> (Arc<Environment>, EnvironmentBoundary, ChannelID, ChannelID) {
let env = EnvironmentBuilder::new()
.register::<f32>("rate", 0.05_f32)
.unwrap()
.register::<u32>("width", 100_u32)
.unwrap()
.build()
.unwrap();
let id_rate = env.channel_of("rate").unwrap();
let id_width = env.channel_of("width").unwrap();
let uniform = EnvUniformBuffer::builder(Arc::clone(&env))
.include::<f32>("rate")
.unwrap()
.build();
let boundary = EnvironmentBoundary::new(Arc::clone(&env)).with_uniform(uniform);
(env, boundary, id_rate, id_width)
}
#[test]
fn finalise_marks_uniform_when_owned_channel_written() {
let (env, mut boundary, id_rate, _id_width) = make_gpu_boundary();
assert!(!boundary.uniform().unwrap().is_cpu_dirty());
env.set::<f32>("rate", 0.10).unwrap();
let mut ctx = BoundaryContext::empty();
boundary.finalise(&mut ctx, &[id_rate]).unwrap();
assert!(
boundary.uniform().unwrap().is_cpu_dirty(),
"uniform should be marked dirty when its owned channel was written"
);
}
#[test]
fn finalise_does_not_mark_uniform_when_only_untracked_channel_written() {
let (env, mut boundary, _id_rate, id_width) = make_gpu_boundary();
env.set::<u32>("width", 200).unwrap();
let mut ctx = BoundaryContext::empty();
boundary.finalise(&mut ctx, &[id_width]).unwrap();
assert!(
!boundary.uniform().unwrap().is_cpu_dirty(),
"uniform should NOT be marked dirty when only an untracked channel was written"
);
}
#[test]
fn finalise_does_not_mark_uniform_when_owned_channel_in_slice_but_not_dirty() {
let (env, mut boundary, id_rate, id_width) = make_gpu_boundary();
env.set::<u32>("width", 200).unwrap();
let mut ctx = BoundaryContext::empty();
boundary.finalise(&mut ctx, &[id_rate, id_width]).unwrap();
assert!(
!boundary.uniform().unwrap().is_cpu_dirty(),
"uniform should stay clean: rate is in `channels` but was not written, \
and width was written but is not uniform-owned"
);
}
#[test]
fn finalise_marks_uniform_when_both_owned_and_untracked_written() {
let (env, mut boundary, id_rate, id_width) = make_gpu_boundary();
env.set::<f32>("rate", 0.10).unwrap();
env.set::<u32>("width", 200).unwrap();
let mut ctx = BoundaryContext::empty();
boundary.finalise(&mut ctx, &[id_rate, id_width]).unwrap();
assert!(
boundary.uniform().unwrap().is_cpu_dirty(),
"uniform should be marked dirty because owned channel `rate` was written"
);
}
#[test]
fn end_tick_marks_uniform_on_uncleared_owned_dirty_channel() {
let (env, mut boundary, _id_rate, _id_width) = make_gpu_boundary();
assert!(!boundary.uniform().unwrap().is_cpu_dirty());
env.set::<f32>("rate", 0.10).unwrap();
let mut ctx = BoundaryContext::empty();
boundary.end_tick(&mut ctx).unwrap();
assert!(
boundary.uniform().unwrap().is_cpu_dirty(),
"end_tick should mark the uniform dirty for any still-dirty owned channel"
);
assert!(
env.dirty_channel_ids().unwrap().is_empty(),
"end_tick should drain the dirty set"
);
}
}