-
Notifications
You must be signed in to change notification settings - Fork 3
Renderers
Renderers represent a single render/compute pass. They are comprised of:
- Zero or more Resources
- A set of shaders
- Optionally, a renderer output
- A set of flags, called renderer options.
- The render state.
Before creating a renderer, you must fill in all of this information in a tz::gl::renderer_info
object.
ℹ️ Information
There is a common distinction between a "graphics renderer" and a "compute renderer". Any renderers that were created with a compute shader are considered compute renderers, everything else is a graphics renderer.
When you create a renderer, you should populate the Render State.
For graphics renderers, there is the following state:
- Index Buffer, a resource handle (defaults to a null handle, meaning there is no index buffer)
- Draw Indirect Buffer, a resource handle (defaults to a null handle, meaning there is no draw indirect buffer)
- The clear colour, a four-dimensional floating-point vector clamped between [0.0f-1.0f], representing the background colour. (defaults to
{0.0f, 0.0f, 0.0f, 0.0f}
, i.e fully transparent black). - The triangle count, an unsigned integer representing the number of triangles that will be drawn in a single invocation of the renderer. (defaults to zero)
If you're just looking to render some test triangles, most likely the triangle count is the only state you will want to set. The triangle count and the clear colour can be changed later if you like, via a renderer edit!
ℹ️ Information
If a draw indirect buffer is specified, the triangle count is ignored, and the draws are sourced from the buffer instead.
For compute renderers, there is the following state:
- A compute kernel, a three-dimensional unsigned integer vector, representing the workgroup sizes [X, Y, Z] that will be dispatched by the compute renderer. (defaults to
{1u, 1u, 1u}
)
To create a renderer, you must first access the global Device and then pass your filled-in tz::gl::renderer_info
object to tz::gl::device::create_renderer()
. This function returns a tz::gl::renderer_handle
, an opaque handle representing the created renderer.
You should keep a hold of this handle for the duration of the renderer's lifetime, as you will need it to perform any operations or do any rendering with the renderer.
Once you have created a renderer, you will have already supplied it with any resources you need, and a shader created previously. If you did not specify a renderer output, the renderer will draw directly to the window images.
To use a renderer, you will need to do add it to the render graph using its handle. To better explain this, see the following example:
// Create our renderer.
tz::gl::renderer_info rinfo;
populate_my_renderer_info(&rinfo);
tz::gl::renderer_handle ren = tz::gl::get_device().create_renderer(rinfo);
// Add the renderer to the end of the timeline.
tz::gl::get_device().render_graph().timeline = {ren};
Everything should look reasonable here, aside from the last line.
We can proceed no further without understanding what a render graph is. However, this is a complicated topic.
In short, a render graph is a way of organising all of the components that perform rendering, handling dependencies, layout transitions, synchronisation etc... Without a render graph, it quickly becomes unfeasible to create complicated rendering setups.
Render graphs vary in their implementations between game engines. As a result, their meaning might be slightly different depending on context. For example, a Topaz Render Graph does not work exactly like a Granite Render Graph, or any other engine.
If you're not satisfied, render graphs have their own page, which I highly recommend you read.
At this point, we have created a renderer, and added it to the render graph timeline. Now, we'd like to start rendering. In the following example, we will continually invoke the device's render graph until the window is closed:
while(!tz::window().is_close_requested())
{
tz::begin_frame();
tz::gl::get_device().render();
tz::end_frame();
}
When we invoke tz::gl::device::render()
, the engine will iterate through the timeline in-order, invoking each renderer. The GPU work for each renderer will be synchronised with respect to any dependencies specified between renderers. In our example case though, we only added one renderer to the render graph, so we have no dependencies to worry about.
Edits represent heavy-duty modifications or operations to be carried out on a single renderer. Renderer edits are expensive, but allow you to greatly customise a renderer. Because of this, you should only perform an edit when you need to, and avoid doing it on your common code paths at all costs.
See here for a list of renderer edits.
To perform a renderer edit, you must first obtain a previously-created renderer object. It is recommended that you populate an object of type RendererEditBuilder with your specified edits. When you're ready, you should then invoke tz::gl::renderer::edit(RendererEditBuilder)
to carry out the changes.
When carrying out the edit:
- The edits are applied in chronological order. This means that it is valid for example to
buffer_resize
to increase the size of a buffer, and thenresource_write
to write data into it. - The operation is synchronous, meaning that
edit(...)
returns once all the changes are made. - If the renderer has GPU work currently in-flight, the call to
edit(...)
will block the current thread until it is complete, before carrying out the specified edits. - If an edit is considered superfluous (e.g, resizing a buffer to the same size), then it may be discarded. This means that if all the specified edits are superfluous, the entire operation will early-out, not waiting on any in-flight work to be complete.
- It is invalid to use a renderer edit for any kind of synchronisation.