Open smoogipoo opened 4 years ago
Huh, so a sort of intermediate representation for draw calls with just-in-time optimisations... Sounds both viable and very cool in concept. The devil is in the details however and I'm afraid there might be very many details as it's graphics APIs we're talking about.
Out of curiosity, have you already hit a situation where you were lacking context knowledge to apply a known optimisation?
Before tightening up vertex batching parameters a while back, I did try to implement the GC-like idea. That requires knowing exactly which vertices are going to be drawn ahead of time and whether they’ve changed/how often they’ve changed. It also requires knowing when a batch change is required (e.g. via masking or uniform changes).
I imagined “generations” of vertices, from a ephemeral (“streaming”) pool containing always-changing vertices, a “dynamic” pool above that that contains vertices that don’t change every frame, and then a “static” pool even further above that contains unchanging vertices. That would reduce the number of uploads, and further heuristics could re-order vertices in the pools to reduce the number of batch changes changes (dependent on the aforementioned masking change and other heuristics like whether regions overlap, etc, that would be treated as a second chance optimisation in my model).
Edit: In general I think giving context opens up a lot more optimisation potential than right now, if only as a result of deferring the actual draw stage. Even in the current models compositedrawnodes are guessing the amount of children actually drawn for their batching, which is why batches can overflow and require careful tuning.
This is documenting something that's been on the back of my mind for a while now, so that documentation isn't lost for the direction I want to take.
What is a
DrawNode
?A
DrawNode
is the logic required for the draw stage to know how to draw aDrawable
to the screen. It's the earliest point where execution is deferred to the draw thread for rendering, meaning it has to maintain the hierarchical structure ofDrawable
s. It's logic in that it tells the draw thread what vertices need to be drawn, what primitive type to use, whether masking needs to be enabled, whether a framebuffer should be drawn to, etc.But right now it serves a secondary purpose which is to paint the screen by sending GL commands. This becomes a bottleneck for optimisation since there is no retained knowledge of what a
DrawNode
actually does, except while the screen is being painted inside eachDrawNode
.Going forward I propose a split of the draw stage into two passes, which I'll call
Draw
andPaint
(name tbd).The "draw" pass
This pass traverses the
DrawNode
s and invokesDraw()
on each one. In turn, theDrawNode
s return a set ofIntent
s (name tbd) that are aggregated into a single array.Examples of
Intent
s include:The final structure will likely look different to the above, but the general idea remains -
DrawNode
s return a list of intents rather than directly painting to the screen.The "paint" pass
Using the list of intents generated through the draw pass, the paint pass aims to convert these intents into GL commands.
This is also where optimisations takes place, since the conversion is done in three stages:
First and second-chance optimisations
First-chance optimisations are immediately available to the paint pass. They require a single linear pass over the hierarchy and generally only deal with the hash code of a
DrawNode
.Example optimisations that can be done:
However not all optimisations are immediately available since they may take additional processing that would slow down the painting process. These are called second-chance optimisations as they become available 1 or more frames in the future. They are computed on a background thread that is given the final intention list, after which they are upgraded to first-change optimisations and become available in the next frame.
The structure I'm imagining is similar to the following:
Notes:
This structure opens up a lot of possibilities for us: