Skip to content

Commit

Permalink
feat: Allow tree to include unhashable nodes, make hash optional on node
Browse files Browse the repository at this point in the history
  • Loading branch information
MaxCWhitehead committed Sep 16, 2024
1 parent 8f1a31d commit 9a73585
Show file tree
Hide file tree
Showing 7 changed files with 159 additions and 84 deletions.
39 changes: 27 additions & 12 deletions framework_crates/bones_ecs/src/components.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,22 +82,29 @@ impl DesyncHash for ComponentStores {
}

impl BuildDesyncNode<DefaultDesyncTreeNode, u64> for ComponentStores {
fn desync_tree_node<H: std::hash::Hasher + Default>(&self) -> DefaultDesyncTreeNode {
let mut hasher = H::default();

fn desync_tree_node<H: std::hash::Hasher + Default>(
&self,
include_unhashable: bool,
) -> DefaultDesyncTreeNode {
let mut any_hashable = false;
let mut child_nodes = self
.components
.read_only_view()
.iter()
.filter_map(|(_, component_store)| {
let component_store = component_store.as_ref().borrow();
if component_store
let is_hashable = component_store
.schema()
.type_data
.get::<SchemaDesyncHash>()
.is_some()
{
let child_node = component_store.desync_tree_node::<H>();
.is_some();

if is_hashable {
any_hashable = true;
}

if include_unhashable || is_hashable {
let child_node = component_store.desync_tree_node::<H>(include_unhashable);

return Some(child_node);
}
Expand All @@ -106,12 +113,20 @@ impl BuildDesyncNode<DefaultDesyncTreeNode, u64> for ComponentStores {
.collect::<Vec<DefaultDesyncTreeNode>>();
child_nodes.sort();

for node in child_nodes.iter() {
// Update parent node hash from data
DesyncHash::hash(&node.get_hash(), &mut hasher);
}
let hash = if any_hashable {
let mut hasher = H::default();
for node in child_nodes.iter() {
// Update parent node hash from data
if let Some(hash) = node.get_hash() {
DesyncHash::hash(&hash, &mut hasher);
}
}
Some(hasher.finish())
} else {
None
};

DefaultDesyncTreeNode::new(hasher.finish(), Some("Components".into()), child_nodes)
DefaultDesyncTreeNode::new(hash, Some("Components".into()), child_nodes)
}
}

Expand Down
35 changes: 24 additions & 11 deletions framework_crates/bones_ecs/src/components/untyped.rs
Original file line number Diff line number Diff line change
Expand Up @@ -83,25 +83,38 @@ impl DesyncHash for UntypedComponentStore {
}

impl BuildDesyncNode<DefaultDesyncTreeNode, u64> for UntypedComponentStore {
fn desync_tree_node<H: std::hash::Hasher + Default>(&self) -> DefaultDesyncTreeNode {
fn desync_tree_node<H: std::hash::Hasher + Default>(
&self,
_include_unhashable: bool,
) -> DefaultDesyncTreeNode {
let mut hasher = H::default();
let child_nodes = self
let child_nodes: Vec<DefaultDesyncTreeNode> = self
.iter()
.map(|component| -> DefaultDesyncTreeNode {
let hash = component.compute_hash::<H>();

// Update parent node hash from data
DesyncHash::hash(&component, &mut hasher);
let hash = if component
.schema()
.type_data
.get::<SchemaDesyncHash>()
.is_some()
{
// Update parent node hash from data
DesyncHash::hash(&component, &mut hasher);
Some(component.compute_hash::<H>())
} else {
None
};

DefaultDesyncTreeNode::new(hash, None, vec![])
})
.collect();

DefaultDesyncTreeNode::new(
hasher.finish(),
Some(self.schema().full_name.to_string()),
child_nodes,
)
let hash = if !child_nodes.is_empty() {
Some(hasher.finish())
} else {
None
};

DefaultDesyncTreeNode::new(hash, Some(self.schema().full_name.to_string()), child_nodes)
}
}

Expand Down
38 changes: 27 additions & 11 deletions framework_crates/bones_ecs/src/resources.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,16 +35,24 @@ impl DesyncHash for UntypedResource {
}

impl BuildDesyncNode<DefaultDesyncTreeNode, u64> for UntypedResource {
fn desync_tree_node<H: std::hash::Hasher + Default>(&self) -> DefaultDesyncTreeNode {
fn desync_tree_node<H: std::hash::Hasher + Default>(
&self,
_include_unhashable: bool,
) -> DefaultDesyncTreeNode {
let name = Some(self.schema().full_name.to_string());

let hashable = self.schema().type_data.get::<SchemaDesyncHash>().is_some();

if let Some(schema_box) = self.cell.borrow().as_ref() {
let hash = schema_box.as_ref().compute_hash::<H>();
let hash = if hashable {
Some(schema_box.as_ref().compute_hash::<H>())
} else {
None
};
return DefaultDesyncTreeNode::new(hash, name, vec![]);
}

// TODO should we only optionally provide node?
DefaultDesyncTreeNode::new(0, name, vec![])
DefaultDesyncTreeNode::new(None, name, vec![])
}
}

Expand Down Expand Up @@ -190,7 +198,10 @@ impl DesyncHash for UntypedResources {
}

impl BuildDesyncNode<DefaultDesyncTreeNode, u64> for UntypedResources {
fn desync_tree_node<H: std::hash::Hasher + Default>(&self) -> DefaultDesyncTreeNode {
fn desync_tree_node<H: std::hash::Hasher + Default>(
&self,
include_unhashable: bool,
) -> DefaultDesyncTreeNode {
let mut hasher = H::default();
let mut child_nodes: Vec<DefaultDesyncTreeNode> = self
.resources
Expand All @@ -202,8 +213,8 @@ impl BuildDesyncNode<DefaultDesyncTreeNode, u64> for UntypedResources {
if !is_shared {
// Only build child node if hashable
let schema = resource_cell.schema();
if schema.type_data.get::<SchemaDesyncHash>().is_some() {
return Some(resource_cell.desync_tree_node::<H>());
if include_unhashable || schema.type_data.get::<SchemaDesyncHash>().is_some() {
return Some(resource_cell.desync_tree_node::<H>(include_unhashable));
}
}
None
Expand All @@ -214,10 +225,12 @@ impl BuildDesyncNode<DefaultDesyncTreeNode, u64> for UntypedResources {

for node in child_nodes.iter() {
// Update parent hash
DesyncHash::hash(&node.get_hash(), &mut hasher);
if let Some(hash) = node.get_hash() {
DesyncHash::hash(&hash, &mut hasher);
}
}

DefaultDesyncTreeNode::new(hasher.finish(), Some("Resources".into()), child_nodes)
DefaultDesyncTreeNode::new(Some(hasher.finish()), Some("Resources".into()), child_nodes)
}
}

Expand Down Expand Up @@ -302,8 +315,11 @@ impl DesyncHash for Resources {
}

impl BuildDesyncNode<DefaultDesyncTreeNode, u64> for Resources {
fn desync_tree_node<H: std::hash::Hasher + Default>(&self) -> DefaultDesyncTreeNode {
self.untyped.desync_tree_node::<H>()
fn desync_tree_node<H: std::hash::Hasher + Default>(
&self,
include_unhashable: bool,
) -> DefaultDesyncTreeNode {
self.untyped.desync_tree_node::<H>(include_unhashable)
}
}

Expand Down
19 changes: 13 additions & 6 deletions framework_crates/bones_ecs/src/world.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,20 +44,27 @@ impl DesyncHash for World {
}

impl BuildDesyncNode<DefaultDesyncTreeNode, u64> for World {
fn desync_tree_node<H: std::hash::Hasher + Default>(&self) -> DefaultDesyncTreeNode {
fn desync_tree_node<H: std::hash::Hasher + Default>(
&self,
include_unhashable: bool,
) -> DefaultDesyncTreeNode {
let mut hasher = H::default();

let mut child_nodes: Vec<DefaultDesyncTreeNode> = vec![];

let components_node = self.components.desync_tree_node::<H>();
components_node.get_hash().hash(&mut hasher);
let components_node = self.components.desync_tree_node::<H>(include_unhashable);
if let Some(hash) = components_node.get_hash() {
hash.hash(&mut hasher);
}
child_nodes.push(components_node);

let resources_node = self.resources.desync_tree_node::<H>();
resources_node.get_hash().hash(&mut hasher);
let resources_node = self.resources.desync_tree_node::<H>(include_unhashable);
if let Some(hash) = resources_node.get_hash() {
hash.hash(&mut hasher);
}
child_nodes.push(resources_node);

DefaultDesyncTreeNode::new(hasher.finish(), Some("World".into()), child_nodes)
DefaultDesyncTreeNode::new(Some(hasher.finish()), Some("World".into()), child_nodes)
}
}

Expand Down
66 changes: 31 additions & 35 deletions framework_crates/bones_framework/src/networking.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use self::{
};
use crate::prelude::*;
use bones_matchmaker_proto::{MATCH_ALPN, PLAY_ALPN};
use desync::DesyncDebugHistoryBuffer;
use desync::{DesyncDebugHistoryBuffer, DetectDesyncs};
use fxhash::FxHasher;
use ggrs::{DesyncDetection, P2PSession};
use instant::Duration;
Expand Down Expand Up @@ -59,7 +59,9 @@ impl From<ggrs::InputStatus> for NetworkInputStatus {

/// Module prelude.
pub mod prelude {
pub use super::{input, lan, online, proto, DisconnectedPlayers, SyncingInfo, RUNTIME};
pub use super::{
desync::DetectDesyncs, input, lan, online, proto, DisconnectedPlayers, SyncingInfo, RUNTIME,
};

#[cfg(feature = "net-debug")]
pub use super::debug::prelude::*;
Expand Down Expand Up @@ -523,20 +525,12 @@ pub struct GgrsSessionRunner<'a, InputTypes: NetworkInputConfig<'a>> {
/// Local input delay ggrs session was initialized with
local_input_delay: usize,

/// Interval in frames of how often to hash state and check for desync with other clients.
/// i.e if set to 10, will check every 10th frame. Desync detection disabled if None.
pub detect_desyncs: Option<u32>,
/// When provided, desync detection is enabled. Contains settings for desync detection.
detect_desyncs: Option<DetectDesyncs>,

/// History buffer for desync debug data to fetch it upon detected desyncs.
/// [`DefaultDesyncTree`] will be generated and saved here if feature `desync-debug` is enabled.
pub desync_debug_history: Option<DesyncDebugHistoryBuffer<DefaultDesyncTree>>,

/// Override of hash function used to hash world for desync detection.
/// By default, [`World`]'s [`DesyncHash`] impl is used.
///
/// This may be useful if you want to hash only a subset of components or resources
/// during testing.
pub world_hash_func: Option<fn(&World) -> u64>,
}

/// The info required to create a [`GgrsSessionRunner`].
Expand All @@ -560,13 +554,8 @@ pub struct GgrsSessionRunnerInfo {
/// `None` will use Bone's default.
pub local_input_delay: Option<usize>,

/// Interval in frames of how often to hash state and check for desync with other clients.
/// i.e if set to 10, will check every 10th frame. Desync detection disabled if None.
pub detect_desyncs: Option<u32>,

/// Override of hash function used to hash world for desync detection.
/// By default, [`World`]'s [`DesyncHash`] impl is used.
pub world_hash_func: Option<fn(&World) -> u64>,
/// When provided, desync detection is enabled. Contains settings for desync detection.
pub detect_desyncs: Option<DetectDesyncs>,
}

impl GgrsSessionRunnerInfo {
Expand All @@ -575,8 +564,7 @@ impl GgrsSessionRunnerInfo {
socket: Socket,
max_prediction_window: Option<usize>,
local_input_delay: Option<usize>,
world_hash_func: Option<fn(&World) -> u64>,
detect_desyncs: Option<u32>,
detect_desyncs: Option<DetectDesyncs>,
) -> Self {
let player_idx = socket.player_idx();
let player_count = socket.player_count();
Expand All @@ -587,7 +575,6 @@ impl GgrsSessionRunnerInfo {
max_prediction_window,
local_input_delay,
detect_desyncs,
world_hash_func,
}
}
}
Expand Down Expand Up @@ -625,8 +612,10 @@ where
.try_send(NetworkDebugMessage::SetMaxPrediction(max_prediction))
.unwrap();

let desync_detection = match info.detect_desyncs {
Some(interval) => DesyncDetection::On { interval },
let desync_detection = match info.detect_desyncs.as_ref() {
Some(config) => DesyncDetection::On {
interval: config.detection_interval,
},
None => DesyncDetection::Off,
};

Expand Down Expand Up @@ -655,9 +644,13 @@ where
let session = builder.start_p2p_session(info.socket.clone()).unwrap();

#[cfg(feature = "desync-debug")]
let desync_debug_history = info
.detect_desyncs
.map(DesyncDebugHistoryBuffer::<DefaultDesyncTree>::new);
let desync_debug_history = if let Some(detect_desync) = info.detect_desyncs.as_ref() {
Some(DesyncDebugHistoryBuffer::<DefaultDesyncTree>::new(
detect_desync.detection_interval,
))
} else {
None
};

#[cfg(not(feature = "desync-debug"))]
let desync_debug_history = None;
Expand All @@ -678,7 +671,6 @@ where
local_input_disabled: false,
detect_desyncs: info.detect_desyncs,
desync_debug_history,
world_hash_func: info.world_hash_func,
}
}
}
Expand Down Expand Up @@ -866,7 +858,9 @@ where
// GGRS should only use hashes from fixed interval.

// If desync detection enabled, hash world.
let checksum = if self.detect_desyncs.is_some() {
let checksum = if let Some(detect_desyncs) =
self.detect_desyncs.as_ref()
{
#[cfg(feature = "desync-debug")]
{
if let Some(desync_debug_history) =
Expand All @@ -875,14 +869,17 @@ where
if desync_debug_history
.is_desync_detect_frame(frame as u32)
{
desync_debug_history.record(
frame as u32,
world.desync_tree_node::<FxHasher>().into(),
let tree = DefaultDesyncTree::from(
world.desync_tree_node::<FxHasher>(
detect_desyncs.include_unhashable_nodes,
),
);
desync_debug_history.record(frame as u32, tree);
}
}
}
if let Some(hash_func) = self.world_hash_func {

if let Some(hash_func) = detect_desyncs.world_hash_func {
Some(hash_func(world) as u128)
} else {
let mut hasher = FxHasher::default();
Expand Down Expand Up @@ -1043,8 +1040,7 @@ where
player_count: self.session.num_players().try_into().unwrap(),
max_prediction_window: Some(self.session.max_prediction()),
local_input_delay: Some(self.local_input_delay),
detect_desyncs: self.detect_desyncs,
world_hash_func: self.world_hash_func,
detect_desyncs: self.detect_desyncs.clone(),
};
*self = GgrsSessionRunner::new(self.original_fps as f32, runner_info);
}
Expand Down
Loading

0 comments on commit 9a73585

Please sign in to comment.