How do I avoid race conditions in the Hammer I/O system?

Sappykun

L1: Registered
Nov 4, 2019
13
0
TL;DR What are good patterns for preventing race conditions when using sequential logic?

----

I have a KOTH map with a lot of logic point entities. The operations these entities perform must be completed in a certain order, based on which team caps the point and when.
Previous versions of this map have created race conditions regarding the map's logic that I am unsure how to prevent in the future. Currently I have "fixed" these race conditions using delays, but all that really does is make the race conditions less likely to happen. I would like to know if there is a way to ensure operations are executed in a specific order using Hammer's I/O system.

For example, I need to set a model's animation BEFORE I change the playback rate on said animation. If I set the playback rate first, changing to a new animation will automatically reset the playback rate to 1.
I can fire the SetPlaybackRate output with a delay of 0.01s after SetAnimation, but what if SetAnimation ends up taking longer than 0.01s? I can set the delay higher, but what if SetAnimation ends up taking longer than to whatever value I set the delay?

The only potential solution I can think of is to use two relays. The first relay calls SetAnimation and triggers the second relay. The second relay calls SetPlaybackRate. In this situation, is it guaranteed that SetAnimation occurs before SetPlaybackRate, assuming no command is delayed?

Are there any real solutions to prevent these types of race conditions, or are delays the only way to deal with them?
 

Da Spud Lord

Occasionally I make maps
aa
Mar 23, 2017
1,339
994
I don't think the scenario you're describing is one that you need to worry about. TF2 splits the game time up into different "ticks", which represent roughly equally spaced units of in-game time. Every action that could be done in a single game tick is completed in that tick, and all ticks are done on the same thread, in proper order. I see no reason why calling SetAnimation for a model would take longer than a single game tick. A delay of 0.01 should be sufficient to push the SetPlaybackRate input to the next game tick.

I'm currently working on a map with large logic chains wherein a lot of things need to happen, in a specified order, and they need to happen really fast. The entire system depends heavily on delays of 0.01 seconds. I've encountered no race condition issues or otherwise had issues with things occurring out of order. For as annoying and unreliable as the Source engine is sometimes, this is one thing that I've never had to ever worry about.

What is it that you're trying to achieve that is causing race conditions?
 

Penguin

Clinically Diagnosed with Small Mapper's Syndrome
aa
May 21, 2009
2,039
1,484
as others have said, it is not possible to get a race condition if you use any amount of delay. all I/O in Source is single-threaded.
 

Sappykun

L1: Registered
Nov 4, 2019
13
0
Thanks for the replies. Interesting to hear that the IO system is actually single-threaded; I was getting non-deterministic results when I either didn't set the delay or set it too short.

What is it that you're trying to achieve that is causing race conditions?

The map I am working on has a stage with about a dozen models, split up by RED and BLU teams. When the round starts, two random songs are selected, and each team's model set gets a specific animation set corresponding to the song. The models for each team only appear when the corresponding team caps, and the song and animation start playing. When they lose the point, the models disappear, and the song/animations "pause" (by setting the playback rate on both to 1/1000 the normal speed so they can be resumed later).

I found that on occasion, when setting the animation and the playback rate, the animation gets set before the playback rate is applied, meaning the animation starts playing when the song is paused and becomes desynced with the song. I wasn't sure what was causing this, and my only conclusion was that there was a race condition somewhere (since Source I/O has no concept of specifying operation order beyond delays) due to some processes taking more than one tick for whatever reason.
I've since redone the logic for selecting the music and animations, so race conditions in this situation shouldn't be an issue anymore.

--------

This is kind of an abstract/confusing/demanding topic, so I apologize if I'm not clear or I'm asking too much. Let's say I have the following setup with some generic entities:

BigEntity:
- Call Trigger on EntityChain1 after 0.00s
- Call Trigger on EntityChain2 after 0.01s

EntityChain1:
- A bunch of calls to various things (ends with EndChain1)

EndChain1:
- Call SetValue "a" on SmallEntity after 0.03s

EntityChain2:
- A bunch of calls to various things (ends with EndChain2)

EndChain2:
- Call SetValue "b" on SmallEntity after 0.01s

Assume the EntityChain calls are completely arbitrary and affect no other entities beyond the final call in EndChain.
How would I follow the order of operations in this situation?

What is the final value of SmallEntity if:
- EntityChain1 and EntityChain2 take the same time to complete
- EntityChain1 takes longer than EntityChain2
- EntityChain2 takes exactly 0.01s longer than EntityChain2
- EntityChain2 takes much longer than EntityChain2 (more than 0.01s)
- The delay in the first call to EntityChain2 is set to 0.00s
- All delays outside of the EntityChain entities are set to 0.00s
etc.
 

Da Spud Lord

Occasionally I make maps
aa
Mar 23, 2017
1,339
994
8907fc5d3f477e74436d6336ec220fc5.jpg

Let's find out.

I created a simple test map (attached below, for your own experimentation) to test all of this. It's set up pretty much exactly as you described:
upload_2020-1-15_14-58-26.png
SmallEntity is a logic_branch, which is suitable since we only have two possible values for it. The two entities below it are game_text, which display its value every time that changes. Everything else is a logic_relay.

I ran each test 10 times for a decent sample size. The tests are the same as the ones you listed, in the same order. Some tests are repeated with variables changed. Unless otherwise noted, each EntityChain takes 1 second. Here's a table of my results, listing how many times I got each value as a result for each test:
Test Description|Value A|Value B|Notes
"EntityChain1 and EntityChain2 take the same time to complete"|10|0|
"EntityChain1 takes longer than EntityChain2"|10|0|EntityChain1 takes 1.10 seconds.
"EntityChain1 takes longer than EntityChain2"|10|0|EntityChain1 takes 1.01 seconds.
"EntityChain2 takes exactly 0.01s longer than EntityChain2"|4|6|I presumed you meant EntityChain2 takes longer than EntityChain1. EntityChain2 takes 1.01 seconds. Results were random and unpredictable.
"EntityChain2 takes much longer than EntityChain2"|0|10|I presumed you meant EntityChain2 takes longer than EntityChain1. EntityChain2 takes 1.10 seconds.
"EntityChain2 takes much longer than EntityChain2"|0|10|I presumed you meant EntityChain2 takes longer than EntityChain1. EntityChain2 takes 1.02 seconds.
"The delay in the first call to EntityChain2 is set to 0.00s"|10|0|
"All delays outside of the EntityChain entities are set to 0.00s"|10|0|I expected the results to be random and unpredictable, although this clearly wasn't the case.
"All delays outside of the EntityChain entities are set to 0.00s"|10|0|Second test to make sure everything was set up properly.
 

Attachments

  • test_raceconditions.vmf
    23.4 KB · Views: 179
  • test_raceconditions.bsp
    103.1 KB · Views: 224

Sappykun

L1: Registered
Nov 4, 2019
13
0
Case 4 seems to confirm my suspicions, at least when two logic branches try to affect the same entity.
Thanks for the in-depth tests! I appreciate it.