Skip to content

WAR Hazard From GPU's POV ​

Consider the scenario

java
bufferData(data);
draw();

This is generally considered as safe, and we've talked about it in "GL Execution Model".

java
bufferData(data1);
draw();
bufferData(data2);
draw();

However, this is less elegant and more fragile. Draw 1 may be running (or not even started) while bufferData(data2) triggers. Passing data to a slice of memory that is currently/to be accessed by a draw command is known as a write-after-read hazard.

Nevertheless, drivers will most likely fix this issue for you (drivers guarantee correctness but not performance). Still:

  • Driver behavior is implicit and unpredictable
  • You may experience different behaviors on different platforms/drivers
  • You may experience fluctuations

Most importantly, the consequence of a WAR hazard is that read action will be waited by write action, breaking parallelism. Or in short:

  • Stalling the pipeline
  • Slowing down

Orphaning ​

One strategy is to orphan the old underlying storage that is "locked" by the draw command and allocate a new underlying storage, but we still use the same buffer object namely (GL/CPU side).

java
bufferData(data1);
draw();
bufferData(null);
bufferSubData(data2);
draw();

bufferData(null) will most likely trigger orphaning, but it's not guaranteed. Orphaning is widely used but still relies on driver heuristics.

Level of predictability:

  • No guard : LOW
  • Orphaning : ABOVE AVERAGE ~ HIGH

Notice:
An intuitive way to visualize WAR hazard is to imagine a lock.

java
draw() { lock.tryLock(); }
java
bufferData() { lock.tryLock(); }

But draw right after upload is totally fine, causes no stall, and is recommended. Even though upload is not an immediate action too.
A lock is not 100% accurate here since the actual reason behind the stall is related to the GPU command queue.

Buffering ​

Another strategy is to have multiple buffers explicitly, so we don't have to rely on so-called underlying behaviors.

java
b1.bufferData(data1);
draw();
b2.bufferData(data2);
draw();

Having two buffer objects totally fix the hazard for a two-draw-call scenario, but we don't want n buffers for n draw calls anyway.

java
b1.bufferData(data1);
draw();
f1 = fence();

b2.bufferData(data2);
draw();
f2 = fence();

when f1 finishes:
  reuse b1

when f2 finishes:
  reuse b2

Let's consider the worst case first, during the next update, both f1, f2 aren't finished, so we got nowhere to upload our data3, resulting in a small latency. To be more specific, this is the capacity of double buffering.

If fences finish in a regular way, you can then expect a Draw b1 - Upload b2 - Draw b2 - Upload b1 pattern.

Moreover, as you can see, the pseudocode can't really demonstrate the situation since we must introduce timing/lifecycle as a new dimension.

RAW Hazard From GPU's POV ​

Consider the scenario

java
compute();
drawUsingComputeResult();

This is a typical RAW hazard setup since compute is an async process. The way to fix it is using fences and buffering like we mentioned above.

Contributors

© 2024 CleanroomMC. All Rights Reserved.