Skip to content

Conversation

@Shatur
Copy link
Contributor

@Shatur Shatur commented Nov 2, 2025

Objective

We'll need this for per-component visibility, but this change is useful on its own because it fixes replication for rules with multiple components that were inserted on different ticks.

For example, you have a replication rule for (A, B). You spawn an entity with A, and on the next tick you insert B. Without this change, B is replicated, but A is wrongly considered as sent. I added a test for it.

Implementation details

Inside ClientTicks, we now store a HashSet<ComponentId> for each entity in addition to ticks. To make it look nicer, I wrapped them in an EntityTicks struct. I also renamed mutations to entities because it fits better (it's not only for mutation ticks - it's also updated on insertions and removals). To avoid double hashing for mutations, I switched to using the entry API and made the field mutable. Because of this change, I also had to remove the is_new_for_client helper (in one place I need entry API and in another it's a simple contains check). I adjusted the doc comment and with the new naming it should still be clear.

MutateInfo now stores a Vec<ComponentId> for each entity, and I update the acknowledged components inside EntityTicks.

During removals, we now need to remove components from the acknowledged lists, so I adjusted the logic similarly to how we collect changes.

@Shatur Shatur requested a review from UkoeHB November 2, 2025 18:50
@codecov
Copy link

codecov bot commented Nov 2, 2025

Codecov Report

❌ Patch coverage is 93.69748% with 15 lines in your changes missing coverage. Please review.
✅ Project coverage is 91.81%. Comparing base (df63540) to head (c9815a0).
⚠️ Report is 1 commits behind head on master.

Files with missing lines Patch % Lines
src/server/client_pools.rs 75.00% 6 Missing ⚠️
src/server.rs 94.11% 4 Missing ⚠️
src/shared/replication/registry/component_mask.rs 88.88% 2 Missing ⚠️
src/server/removal_buffer.rs 90.00% 1 Missing ⚠️
src/server/replication_messages/mutations.rs 94.44% 1 Missing ⚠️
src/server/replication_messages/updates.rs 97.56% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master     #604      +/-   ##
==========================================
- Coverage   91.90%   91.81%   -0.09%     
==========================================
  Files          58       61       +3     
  Lines        3311     3397      +86     
==========================================
+ Hits         3043     3119      +76     
- Misses        268      278      +10     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

We'll need this for per-component visibility, but this change is useful
on its own because it fixes replication for rules with multiple
components that were inserted on different ticks.

For example, you have a replication rule for `(A, B)`. You spawn an
entity with `A`, and on the next tick you insert `B`. Without this
change, `B` is replicated, but `A` is wrongly considered as sent. I
added a test for it.
@Shatur Shatur force-pushed the component-tracking branch from 800f5b6 to 2066211 Compare November 3, 2025 16:11
src/server.rs Outdated
Comment on lines 269 to 271
for (_, components) in entities.drain(..) {
pools.components.push(components);
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Change to method on pools that takes entities.drain(...) iterator as input.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense, already addressed in the hashmap replacement commit.

src/server.rs Outdated
Comment on lines 245 to 247
for (_, components) in mutate_info.entities.drain(..) {
pools.components.push(components);
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Change to method on pools that takes mutate_info.entities.drain(...) iterator as input.

Comment on lines +317 to +319
for entity_ticks in ticks.entities.values_mut() {
entity_ticks.system_tick.check_tick(*check);
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This logic should be inside ClientTicks.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a tiny loop and since we already expose the access to tick.entities, I think it reads better this way.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not only about readability. These functions are suffering from growing line bloat.

Copy link
Contributor Author

@Shatur Shatur Nov 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function is only 9 lines.

It's collect_* functions that are large.

Comment on lines 587 to 592
let Some(entity_ticks) = ticks.entities.get_mut(&entity) else {
continue;
};
if !entity_ticks.components.remove(&component_id) {
continue;
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should also filter on visibility.hidden(entity) to avoid redundantly sending component removals when visibility is lost (which will also send a despawn).

Copy link
Contributor Author

@Shatur Shatur Nov 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This executed after processing despawns, where the entity removed from ticks.entities if it was despawned or lost visibility.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a comment about it.

src/server.rs Outdated
client_ticks.mutation_tick(entity.id())
&& !ticks.is_added(change_tick.last_run(), change_tick.this_run())
if let Some(entity_ticks) = client_ticks.entities.get(&entity.id())
&& entity_ticks.components.contains(&component_id)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
&& entity_ticks.components.contains(&component_id)
&& entity_ticks.components.contains(&component_id)

Hide this behind a method that clearly reveals its meaning here: was the component added this tick or not?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We actually check whether client received the component. But the struct exposes full access to components, it's documented and I think entity_ticks.components.contains(&component_id) is quite clear. So I'd prefer to leave this as is.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's documented and I think entity_ticks.components.contains(&component_id) is quite clear.

The meaning was not clear to me, and would be even less clear to anyone other than us two reading it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let me add a comment about it.
I think having entity_ticks.is_new(component_index) won't clarify things enough.

src/server.rs Outdated
}
ticks.set_mutation_tick(entity.id(), change_tick.this_run(), server_tick);

match entity_entry {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Move all this to a method on ClientTicksEntity.

}

signature.is_added() || ticks.is_new_for_client(entity)
signature.is_added() || !ticks.entities.contains_key(&entity)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't like removing clarity like this. Keep the method or use ClientTicksEntity.is_new().

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as for contains above.

src/server.rs Outdated
.extend(updates.drain_changed_entity_ids());
}
Entry::Vacant(entry) => {
let mut components = pools.component_sets.pop().unwrap_or_default();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
let mut components = pools.component_sets.pop().unwrap_or_default();
let mut components = pools.component_sets.pop().unwrap_or_default();
components.clear();

Either here or when pushing into the pool. Test to catch this?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We clear all data before pushing into the pool.
In the hashmap replacement commit I added a method that clears it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pushed a change that properly abstracts this, shouldn't be any confusion anymore.

@Shatur
Copy link
Contributor Author

Shatur commented Nov 5, 2025

Pushed 2 commits.
First is a small, but related refactor. And second replaces all hashmap lookups with a bitmask check.

I'll address your suggestions in a moment.

Reduces the unsafety and the mount of passed arguments.
@Shatur Shatur force-pushed the component-tracking branch from a77a706 to 42d8392 Compare November 5, 2025 00:26
Shatur and others added 3 commits November 5, 2025 11:20
This significantly increases performances.
Co-authored-by: UkoeHB <37489173+UkoeHB@users.noreply.github.com>
Since it's used in many places, it's better to abstract it to avoid
forgetting to call `clear`.
@Shatur Shatur force-pushed the component-tracking branch from a538b5f to 34bb995 Compare November 5, 2025 17:36
@Shatur Shatur requested a review from UkoeHB November 5, 2025 19:08
@Shatur Shatur merged commit 4628b2e into master Nov 8, 2025
9 checks passed
@Shatur Shatur deleted the component-tracking branch November 8, 2025 20:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants