How to prevent stack overflows when using CreateUnit API

Started by Vertu, July 07, 2022, 05:00:39 PM

Previous topic - Next topic

Vertu

I have been harassed by Crashes caused by Stack Overflows when using the CreateUnit API to create volatile shrapnel spawned by a larger explosion. I know it is from this API as when commenting out a single line which is CreateUnit the crashes never happen(ed).
I managed to fix this type of Crash from my SML CPACK (my unreleased version as it is unbalanced for the moment) by having the shrapnel be excluded from the explosion that created it. For what ever reason when the SML missile damages existing shrapnel it can crash the game but I fixed this as said by having said shrapnel be untargetable as the explosion searches for units to damage.

However, the SML can still cause stack overflow crashes and this time I can't replicate it as it only happened once. I suspect a combination of multiple units being damaged in a single frame and landing on a terrain corner.
Then my V-Power CPACK caused a crash from the V-Reactor exploding which would release the same shrapnel as the SML CPACK explosion so I hypothesis the SML and V-Power explosions have a common problem.

In a conclusive sense, I believe this is all happening as units created by the CreateUnit API(s) and are damaged by the DamageUnit API in the same frame cause these stack overflow crashes. If so then I would like this pointed out in the API's reference and I will get to work in ensuring shrapnel can not be targeted in a chain reaction scenario.

If I am wrong than please tell me because I don't want to remove shrapnel from the SML and V-Power CPACKs but I have had 32 crashes from this (most from the SML CPACK which as said, was mostly fixed). 30 Crashes pre implementing the fix for the SML CPACK, 1 from a new source of the same crash from the SML CPACK I have been unable to replicate, and 1 from V-Power which was the first time I have seen it happen and suspect is a V-Rod exploded from the V-Reactor exploding and released shrapnel, which the V-Rod then damaged in the same frame and caused the crash.

Note: This is not related to my "Inconsistent Crash" post as the crash mentioned there does not involve my use of shrapnel.

Thank you for your time.
Life isn't fair because we say it isn't. Not because it is unfair. In fact, it is so fair we want to say it isn't and do.

Karsten75

It is near impossible to figure things out from a verbal description only.

The game provides a log file - can you please provide that.

Also, if you have a method for us to recreate the crash, that help immensely. in that case, upload that was well. Preferably not a million lines of code, though.

See this article for why/how to write small, self-contained coding examples.
https://meta.stackexchange.com/questions/22754/sscce-how-to-provide-examples-for-programming-questions

Vertu

How to recreate:

  • For the sake of keeping everything constant, my map VPAC [POI-1] Valley Battle should be where this is tested as a quick test on another map proved otherwise and that this may somehow be map specific (hence why I wanted some answers).
  • Comment/remove line 94 within the script "BerthaShot" of the SML CPACK (this is a heavily modified Bertha) along with its corresponding endif.
  • While paused and after recompiling, place 4 or more SMLs and use the 4RPL console script provided to build and give full ammo.
  • Unpause.
  • Wait for missiles to land (a lone SML landing in an area won't cause a crash).
  • Done.
I can even record the process if needed.

Line 94: if(GetUnitType(<-victims[I]) neq("bdf2a7b3-a42e-409d-8741-91c1bd62ff39"))

Fully Build Units Console Script:
# Build selected units
#By Vertu.
 
GetSelectedUnits ->units
if(GetListCount(<-units) gt0)
do(GetListCount(<-units) 0)
ConstructUnit(<-units[I] 10000)
SetUnitAmmo(<-units[I] GetUnitMaxAmmo(<-units[I]))
SetUnitHealth(<-units[I] GetUnitMaxHealth(<-units[I]))
loop
ClearTraceLog
Trace3("Fully built " GetListCount(<-units) " selected units.")
else
if(GetUnitUpdateCount lt(2))
Trace("No units where selected for building.")
else
if(GetUnitUpdateCount 10 % eq0)
ClearTraceLog
if(<-count eq0)
Trace("Operating. |")
1 ->count
else
if(<-count eq(1))
Trace("Operating. /")
2 ->count
else
if(<-count eq(2))
Trace("Operating. -")
3 ->count
else
Trace("Operating. \")
0 ->count
endif
endif
endif
endif
endif
endif


Note: I have been able to cause a crash by doing this but after that first replicated crash I have been unable to cause additional crashes. However, after doing it a 4th time it did crash. So multiple trials may be necessary.
Life isn't fair because we say it isn't. Not because it is unfair. In fact, it is so fair we want to say it isn't and do.

GoodMorning

For the quickest and easiest recreation, it's helpful to upload a save which has steps 1-4 done, and which you have observed resulting in the crash with no more effort required than to unpause.
A narrative is a lightly-marked path to another reality.

Karsten75

Looking at the log file, I'd love to know on what basis you characterize this as a "stack overflow" issue?

Vertu

Quote from: Karsten75 on July 08, 2022, 10:09:06 AM
Looking at the log file, I'd love to know on what basis you characterize this as a "stack overflow" issue?
I have been trying to figure out this kind of stuff on my own so I don't bother admins (like you, even if you are not an admin). Sorry if its a misunderstanding but I somehow figured it out by looking at the error.log or at least, wondered somehow in the right direction.
Life isn't fair because we say it isn't. Not because it is unfair. In fact, it is so fair we want to say it isn't and do.

Karsten75

OK, so this is the file you've not uploaded previously. Thanks

knucracker

#7
Ok, so this and the previous problem you posted about (with the v-rods) is likely the same thing.  Both involve stack overflows.  And in these cases it is the C#/mono/il2cpp/game's call stack we are talking about (not the 4rpl call stack).

In short, inside :Destroyed you find nearby units and call DamageUnit on them.  DamageUnit can in turn destroy the unit which causes its :Destroyed to get called, which will in turn find nearby units and call DamageUnit on them.  There is no problem with a call loop.  The game detects call loops and prevents call loops.  The problem is just that the chain of unit calls gets so long that the call stack memory limit is exceeded. The chain is:  DamagUnit -> :Destroyed -> DamageUnit -> :Destroyed ... with each :Destroyed being called on a new unit.

In the attached map I reproduce it using your v-rod unit (what you reported on in your prior post). Now, unity will hard crash when the stack is blown (sometimes).  That is talked about here and Unity isn't fixing it: https://issuetracker.unity3d.com/issues/unity-editor-crashes-after-using-circular-dependency

That's the explanation. Now, what to do about it....  I could attempt to lighten the stack memory used, but that just moves the problem from 25 units to 30 units, for instance. I could attempt to break the call chain, but that requires some special case work for how certain APIs, like DamageUnit, would work when being called from :Destroyed. In short, that's work that could break things and is a bridge too far for where the game is currently. It also isn't flexible as to what should actually happen... see below.

But, you may be able to do something on your own... If you think about it, what you are trying to do is create a 'dust cloud explosion', or a 'chain reaction', or a 'Little Doctor' explosion from Ender's Game.  One thing destroys another in a chain reaction. If you were to code that up in some other language on some other platform, you'd experience the same problem if the solution was done with functions that call each other in a chain.  At N particles, you'd exceed some call stack limit.

What you'd ultimately need to do instead (and there are several different types of solutions I think), is 'flatten' the call to destroy/damage and replace it with a loop. You could perhaps use a global table that tracks the damage done to units.  So inside Destroyed, instead of calling DamageUnit on nearby units, you record the damage you want to do inside a table.  Then you later go through the table in a loop and apply the actual damage.  Breaking the chain means not calling DamageUnit from :Destroyed.  You have to replace it with your own "MyDamageUnit" call that just tracks the damage, and then at some other point apply the damage.  Whether you want to resolve all of that in one frame or across frames, depends on the explosion effect you would want.

Vertu

Quote from: knucracker on July 11, 2022, 10:49:45 AM
Ok, so this and the previous problem you posted about (with the v-rods) is likely the same thing.  Both involve stack overflows.  And in these cases it is the C#/mono/il2cpp/game's call stack we are talking about (not the 4rpl call stack).

In short, inside :Destroyed you find nearby units and call DamageUnit on them.  DamageUnit can in turn destroy the unit which causes its :Destroyed to get called, which will in turn find nearby units and call DamageUnit on them.  There is no problem with a call loop.  The game detects call loops and prevents call loops.  The problem is just that the chain of unit calls gets so long that the call stack memory limit is exceeded. The chain is:  DamagUnit -> :Destroyed -> DamageUnit -> :Destroyed ... with each :Destroyed being called on a new unit.

In the attached map I reproduce it using your v-rod unit (what you reported on in your prior post). Now, unity will hard crash when the stack is blown (sometimes).  That is talked about here and Unity isn't fixing it: https://issuetracker.unity3d.com/issues/unity-editor-crashes-after-using-circular-dependency

That's the explanation. Now, what to do about it....  I could attempt to lighten the stack memory used, but that just moves the problem from 25 units to 30 units, for instance. I could attempt to break the call chain, but that requires some special case work for how certain APIs, like DamageUnit, would work when being called from :Destroyed. In short, that's work that could break things and is a bridge too far for where the game is currently. It also isn't flexible as to what should actually happen... see below.

But, you may be able to do something on your own... If you think about it, what you are trying to do is create a 'dust cloud explosion', or a 'chain reaction', or a 'Little Doctor' explosion from Ender's Game.  One thing destroys another in a chain reaction. If you were to code that up in some other language on some other platform, you'd experience the same problem if the solution was done with functions that call each other in a chain.  At N particles, you'd exceed some call stack limit.

What you'd ultimately need to do instead (and there are several different types of solutions I think), is 'flatten' the call to destroy/damage and replace it with a loop. You could perhaps use a global table that tracks the damage done to units.  So inside Destroyed, instead of calling DamageUnit on nearby units, you record the damage you want to do inside a table.  Then you later go through the table in a loop and apply the actual damage.  Breaking the chain means not calling DamageUnit from :Destroyed.  You have to replace it with your own "MyDamageUnit" call that just tracks the damage, and then at some other point apply the damage.  Whether you want to resolve all of that in one frame or across frames, depends on the explosion effect you would want.
Thank you so much for this information.
The primary reason I have it in :Destroyed is to account for when the unit is destroyed by any means.

I appreciate all the effort that was put into solving this and will enact fixes right away.
Life isn't fair because we say it isn't. Not because it is unfair. In fact, it is so fair we want to say it isn't and do.