Skip to content

Conversation

@stacode123
Copy link

Implements a cache for train collision checks for better performance
Before: https://spark.lucko.me/cgjtsUl25f
After: https://spark.lucko.me/p7ZcDxAUFA
Criticism welcome.
Made with the help of Copilot

World used for testing :
world.zip

@sleepy-evelyn
Copy link

sleepy-evelyn commented Nov 8, 2025

If this works properly this is brilliant @stacode123 and definately needed. We've turned train collisions off entirely to help with lag but thats definately not a fix most servers want to implement.

Using Copilot is a little worrying though because you need to know exactly whats going on for this. Make sure you've tested it thoroughly and used the debugger to step through the code to see whats going on and if it aligns with what you expect.

@Jozufozu Jozufozu self-requested a review November 8, 2025 21:19
Copy link
Contributor

@Jozufozu Jozufozu left a comment

Choose a reason for hiding this comment

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

Very excited about this and the spark profile is extremely promising. A 5mspt difference is huge

Image Image

How many trains did you use in your test world? It would be helpful to upload a screenshot of the chaos 😆

Also after the requested changes and before this merges I'd like to request a longer profile to compare. I'm a little nervous the 16 second profiles may not have enough signal to noise

collisionCache.addSegment(lastPoint, start, segmentIndex++);
}

if (segmentIndex < totalSegments) {
Copy link
Contributor

Choose a reason for hiding this comment

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

How will segmentIndex ever be greater or equal to totalSegments? Same for above

Copy link
Contributor

Choose a reason for hiding this comment

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

Again, do we need these checks? They could hide critical bugs for the same reason I described here: #9513 (comment)

if (length > 0.0001) {
direction[index] = diff.normalize();
} else {
direction[index] = Vec3.ZERO;
Copy link
Contributor

Choose a reason for hiding this comment

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

How will this happen?

Copy link
Contributor

Choose a reason for hiding this comment

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

I see you've removed this but please justify it

@stacode123
Copy link
Author

test world has 180 trains more than that would have caused my computer to have a seizure
image
Will address the comments

@stacode123
Copy link
Author

Performance impact on a server with 475 trains:

image-1.webp

Before:
image-21.png

After:
image-24.png

@sleepy-evelyn
Copy link

sleepy-evelyn commented Nov 9, 2025

To be honest it's rare u see such a stark contrast between before and after. A similar number of players are connected to the server as well so the difference likely isn't down to that. :)

@stacode123
Copy link
Author

@Jozufozu Please review the changes :D


Vec3 start = (speed < 0 ? trailingPoint : leadingPoint).getPosition(graph);
Vec3 end = (speed < 0 ? leadingPoint : trailingPoint).getPosition(graph);
Vec3 start = collisionCache.start[0];
Copy link
Contributor

Choose a reason for hiding this comment

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

Where did the speed < 0 checks go? If the train is moving backwards we need to check for collisions with the trailing end as if it were the head

Copy link
Contributor

Choose a reason for hiding this comment

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

Oh also do we know for sure that the collision cache is non-null here?

Copy link
Author

Choose a reason for hiding this comment

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

  1. That is handled when collision cache is created.
  2. Earlier in the train tick we updated the collision cache but i can add a null check just to be sure


Vec3 normedDiff = diff.normalize();
double maxDistanceSqr = Math.pow(AllConfigs.server().trains.maxAssemblyLength.get(), 2.0);
double maxDistanceSqr = AllConfigs.server().trains.maxAssemblyLength.get()*AllConfigs.server().trains.maxAssemblyLength.get();
Copy link
Contributor

Choose a reason for hiding this comment

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

Please only fetch the config value once. This call showed up in your spark profile already and I do not want to rely on the JVM to deduplicate these.
Also formatting

double maxDistance = AllConfigs.server().trains.maxAssemblyLength.get();
double maxDistanceSqr = maxDistance * maxDistance;


// Use cached data if available, otherwise fall back to old method
if (train.collisionCache != null && train.collisionCache.isValid()) {
if (train.collisionCache != null ) {
Copy link
Contributor

Choose a reason for hiding this comment

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

nit invert the check and early continue to save on indentation

if (length > 0.0001) {
direction[index] = diff.normalize();
} else {
direction[index] = Vec3.ZERO;
Copy link
Contributor

Choose a reason for hiding this comment

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

I see you've removed this but please justify it

collisionCache.addSegment(lastPoint, start, segmentIndex++);
}

if (segmentIndex < totalSegments) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Again, do we need these checks? They could hide critical bugs for the same reason I described here: #9513 (comment)

Comment on lines 679 to 681
if(segmentIndex == 0) {
collisionCache=null;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

nit formatting

if (segmentIndex == 0) {
	collisionCache = null;
}

Copy link
Author

Choose a reason for hiding this comment

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

  1. I dont know why it was there in the first place
  2. ok ill remove them
  3. fixed

Comment on lines +222 to +224
for (Train train : movingTrains){
train.updateCollisionCache();
train.earlyTick(level);}
Copy link
Contributor

Choose a reason for hiding this comment

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

formatting please

for (Train train : movingTrains) {
	train.updateCollisionCache();
	train.earlyTick(level);
}

do we not need the collision cache for waiting trains? do we not expect moving trains to collide with trains that are stationary?

Copy link
Author

Choose a reason for hiding this comment

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

Wont stationary trains have already built up cache ? And trying to calculate it again would just yield the same result?

Copy link
Contributor

@Jozufozu Jozufozu Nov 9, 2025

Choose a reason for hiding this comment

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

What about on initial level load? Can we really assume that a train that just became stationary this tick will have valid positions in the cache from the previous tick where it was moving?

Still, that's a good point. If you're feeling ambition you could probably check some easy to compute value at the start of updateCollisionCache to see if the train has moved from the previous tick and decide whether to update the collision cache. If you do though, I'd like to see proof that it works and waiting trains still get collided with

if (derailed || graph == null) {
if (collisionCache != null)
collisionCache.invalidate();
collisionCache = null;
Copy link
Contributor

Choose a reason for hiding this comment

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

probably don't need the collisionCache != null check anymore. just assigning is fine

Copy link
Author

Choose a reason for hiding this comment

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

what do you mean by that

collisionCache = new CollisionCache(totalSegments);
}

Vec3 lastPoint = null;
Copy link
Contributor

Choose a reason for hiding this comment

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

Add a comment in the code please

private void updateCollisionCache() {
if (derailed || graph == null) {
if (collisionCache != null)
collisionCache.invalidate();
Copy link
Contributor

Choose a reason for hiding this comment

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

Add a comment in the code please

@stacode123 stacode123 requested a review from Jozufozu November 16, 2025 12:48
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