New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[Queue approach] Fix entity destruction in unticking chunks #2986
[Queue approach] Fix entity destruction in unticking chunks #2986
Conversation
I had to have a single new entity variable: m_ParentChunk. Because the parentChunk is needed for efficient queueing. |
This can be used as a basis for a problem @madmaxoft once mentioned: Entities may tick twice when moving between two chunks. But I want the basic thing to be merged first. |
@@ -129,7 +129,6 @@ cClientHandle::~cClientHandle() | |||
World->RemovePlayer(m_Player, true); // Must be called before cPlayer::Destroy() as otherwise cChunk tries to delete the player, and then we do it again | |||
m_Player->Destroy(); | |||
} | |||
delete m_Player; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You sure about this? In the current system, the ClientHandle manages the player pointer's life.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well, no, I am not sure. m_Player->Destroy();
will add the player destruction to the queue. The problem I can see is that m_Player->Destroy();
is inside a conditional "if" so it's not guaranteed to free it. Guess I'll change this and make the player destruct here as usual.
Could you not use DoWithChunk() as you have done before, without storing a parent chunk? |
bool m_FreeMemory; | ||
}; | ||
|
||
std::list<EntityRemovalQueueItem> m_EntityRemovalQueue; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
cWorld already has a task system, I was thinking of something like:
cWorld::QueueTask([...](...)
{
DoWithChunk(..., ..., [...](...)
{
a_Chunk->RemoveEntity(); // New function to be added; delete if not player
}
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is the task guaranteed to be executed just after the mob tick and before the next tick cycle?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I just checked. The tasks are done before the mobs are ticked.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh yeah, mobs are ticked separately (ugh). In that case, move the TickMobs call to after m_ChunkMap->Tick, and now its guaranteed :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
TickMobs is already after m_ChunkMap->Tick. How does that help?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As in directly afterwards/before TickQueuedTasks.
@tigerw That technique was dangerous. It returns the chunk the player is currently standing at. At the start of each tick this is exactly the same as the parent chunk, but this will change if the entity crosses a border mid-tick, (and will re-converge to the same chunk when cChunk::tick calls MoveEntityToNewChunk.) So, if a player teleports while walking, it may do nasty things. The only thing guaranteed to do it right is the parent chunk (the curretly owning chunk). I am actually considering removing the |
Hmm, There's a chance I'm wrong in my last post. I'll need to re-check this. |
Not wrong. |
Also, regardless of my last argument, I think it'd be wise to have the chunk stored for convenience, rather than passing around |
Are you certain? GetChunkX/Z() returns the current chunk based on the entity's position, DoWithChunk gets that chunk. The task will execute after Tick (after our fix); and Tick will have ensured that the entity is in the chunk that its position says it is in. |
(this is all so confusing, I've already forgotten why this whole thing is needed about four times, and had to re-examine your explanation of it) |
Oh, yes, you're correct, I forgot that this will happen after the tick now that there's a fix. Confusing indeed! But my guts tell me this is dangerous futurwise. It's the Cuberite equavilant of tinkering with undefined compiler behavior. Any future change may break this guarantee. m_ParentChunk seems a lot safer to me. |
But then again, some future change may accidentally try to update ParentChunk instead of comparing it or some future chunk refactor may completely miss the fact that there's an additional variable which needs maintenance. I personally would do the "as little amount of variables as possible" route but I think both have pitfalls, so you decide. |
Well, I've explicitly mentioned in the docs that only cChunk::addEntity has the right to update it. |
Very well, go for it then :) |
One more question first: Should I really go through the cWorld's existing task system? How will it fare performance-wise? Perhaps it'd be wise to have a second, entity-specific task system instead? Entities will produce a lot of tasks if chunk switching is ever made a task, and maybe it'd be significantly faster without those capture lambdas? Just wondering. |
My guess is that the current roll-your-own entity destructor queue is faster than the cWorld task system. I don't have any real numbers though. |
Dunno. Maybe, maybe not, maybe the compiler does magic to it, @worktycho knows. Everything else is using QueueTask though - imagine what it would be like if they all added their own queue structures to the world! Entities shouldn't be an exception. |
Done conversion to cWorld's tasks. Rebased and squashed. I like this. |
@@ -139,6 +139,7 @@ cChunk::~cChunk() | |||
{ | |||
if (!(*itr)->IsPlayer()) | |||
{ | |||
(*itr)->SetParentChunk(nullptr); // To prevent the entity from queueing a future chunk removal. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
change comment
Doubletick prevention seems to be working :) |
I got a "moving to same chunk" error. I guess I'll leave the doubletick thing to a seperate PR ^_^ |
When you move an entity to a new chunk which hasn't yet ticked, that entity will tick twice. The solution is moving it post-tick, preventing such doubleticks. Hence the cChunk::tick task queue. But it seems to be messing something. |
Rebased and removed doubletick prevention attempt. |
LOGD("Warping player (%s) from world (%s) to (%s). Source chunk: (%d, %d) ", | ||
this->GetName().c_str(), | ||
a_OldWorld.GetName().c_str(), a_World->GetName().c_str(), | ||
ParentChunk->GetPosX(), ParentChunk->GetPosZ() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is markDirty required for players?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Better call it, so that it's consistent with the other entities.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But I see if entity is not a player then call markDirty
everywhere.
I realized this commit name is misleading. It's not about unloaded chunks, it's about loaded, nonticking chunks. There's no trivial way to access entities in unloaded chunks. (You'd need to load and check all unloaded chunks from storage). |
@sphinxc0re done. |
Please do not merge yet, I want to be completely sure I got all pointers right in all cases first. |
// Mark as dirty if it was a server-generated entity: | ||
if (!(*itr)->IsPlayer()) | ||
{ | ||
MarkDirty(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this line will save everyone thousands of false dirties a day. It seems that previously, anywhere you walk into would be marked dirty because the if player
condition was missing.
I have replaced |
I put all the player destruction logic in |
void Destroy(bool a_ShouldBroadcast = true); | ||
|
||
/** Destroy the entity without scheduling memory freeing. This should only be used by cChunk for internal memory management. */ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
by cChunk and cClientHandler
What is wrong with the CI? |
Nothing. GCC is throwing so many errors, that it stops counting and aborts |
It is throwing errors in unchanged code. |
if (m_Player != nullptr) | ||
{ | ||
cWorld * World = m_Player->GetWorld(); | ||
ASSERT(World != nullptr); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should there really be an assert here?
great, IsTicking came back... |
@tigerw |
Could you explain a bit more the problem? Why does anything need to distinguish between these states? |
@tigerw Okay.
Actually, we do NOT need to distinguish between teleportation and destruction, and this is why I made this change. For instance, cChunk previously had this:
The problem here is that teleporting entities will get ticked. So I replaced it with
|
/** Returns true if the entity is valid and ticking. Returns false if the entity is not ticking. | ||
When the entity is not ticking, it is either scheduled for teleportation or for destruction. | ||
Either way, using it is dangerous. */ | ||
bool IsTicking(void) const; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I removed IsDestroyed
. How do I deal with the LUA API here?
Perhaps I should export IsTicking
, and make IsDestroyed
a deprecated alias for !IsTicking()
?.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
...don't bother...
(don't tell anyone I said that)
Todo: Document the new API function |
Done. |
Looks good. I shall, of course, take every available opportunity to tease you about ParentChunk :D |
@tigerw is evil! |
[Queue approach] Fix entity destruction in unticking chunks
Closes #2731
Closes #2973
@tigerw Is that the vision you had in mind?