The Arm Mali-G78AE's tile-based rendering architecture is designed to minimize memory bandwidth usage by splitting the frame buffer into smaller tiles, each processed independently. This approach dramatically reduces the need for frequent memory accesses, as most operations can be handled in fast on-chip memory, reducing power consumption, and improving performance.
Key Advantages of Tile-based Rendering
- Deferred Shading
- Geometry is processed tile-by-tile, and only visible fragments are shaded, avoiding unnecessary computations for occluded pixels.
- On-Chip Tile Memory
- Intermediate results are stored in local memory, reducing external memory traffic and improving performance.
Efficient Render Pass Handling in OpenGL ES
In the Mali-G78AE tile-based rendering architecture, tile memory is initialized at the start of each render pass, and only necessary outputs are written back to system memory at the end. The intermediate frame buffer data remains in tile memory, minimizing external memory writes. To optimize render passes, follow these best practices:
- Clear or Invalidate Attachments
- At the start of each render pass, use
glClear()orglInvalidateFramebuffer()to clear unnecessary data from attachments, unless the content is required for further rendering.
- Invalidate Unused Attachments
- Before switching to the next frame buffer, invalidate any attachments that are no longer needed to avoid unnecessary memory writes.
- Avoid Multiple Frame-buffer Switches
- Do not switch back and forth between the same frame buffer multiple times in a frame, as this increases memory flushes and introduces overhead.
- Use Scissor Box for Sub-region Rendering
- For rendering to a sub-region of the frame buffer, use a scissor box to restrict the area being cleared and rendered, reducing memory usage, and improving performance.
The optimized pipeline, illustrated in the Figure 1 diagram, leverages tile-based rendering to handle multiple operations, such as velocity buffer generation, shadow mapping, and post-processing, on a per-tile basis. This approach reduces memory bandwidth overhead and power consumption.
Efficient Render Pass Handling in Vulkan
Unlike OpenGL ES, Vulkan provides explicit control over render passes with operations such as loadOp and storeOp, which define how tile memory is initialized and what is written back at the beginning and end of each pass. This control allows for more efficient render pass management, making it possible to fine-tune memory operations.
Key optimization techniques for Vulkan:
- Use loadOp = LOAD_OP_CLEAR or LOAD_OP_DONT_CARE
- At the start of a render pass, use these operations to clear or invalidate attachments, ensuring efficient memory initialization.
- Transient Attachments
- Attachments that are only used within a single render pass should be marked as TRANSIENT_ATTACHMENT, backed by LAZILY_ALLOCATED memory, eliminating the need for physical storage.
- Use storeOp = STORE_OP_DONT_CARE
- At the end of a pass, invalidate attachments that are no longer needed. For read-only attachments that were not modified during the pass, but still need to be retained, use storeOp = STORE_OP_NONE to avoid unnecessary memory writes.
Practices to avoid in Vulkan:
- Avoid vkCmdClearAttachments()
- Instead of clearing attachments inside a render pass, use a loadOp operation at the start of the pass to minimize overhead.
- Avoid Shader-Based Clears
- Instead of clearing attachments inside a render pass, use a loadOp operation at the start of the pass to minimize overhead.
- Limit the Use of LOAD_OP_LOAD
- Instead of clearing attachments inside a render pass, use a loadOp operation at the start of the pass to minimize overhead.
- Skip Unnecessary loadOp or storeOp Operations
- For attachments that are not needed in a render pass, do not define a loadOp or storeOp, as these can generate unnecessary memory transactions.
- Avoid vkCmdBlitImage() for Upscaling
- Up-scaling low-resolution frames using this command is inefficient, as it causes extra memory transfers.
Optimized Code for Rendering Pipeline
The following code demonstrates an optimized approach to implementing the rendering pipeline efficiently, minimizing memory operations and redundant passes.
#define CLEAR_ALL (GL_COLOR_BUFFER_BIT | \
GL_DEPTH_BUFFER_BIT | \
GL_STENCIL_BUFFER_BIT)
static const GLenum INVALIDATE_ATTACHMENTS[2] = {
GL_DEPTH_ATTACHMENT,
GL_STENCIL_ATTACHMENT
};
// Helper function to bind, clear, and draw to a framebuffer
void RenderToFramebuffer(GLuint framebuffer, GLenum clearBits) {
glBindFramebuffer(GL_FRAMEBUFFER, framebuffer);
glClear(clearBits);
glDrawElements(...); // Render elements
}
// Helper function to invalidate framebuffer attachments
void InvalidateFramebufferAttachments(GLuint framebuffer) {
glInvalidateFramebuffer(GL_FRAMEBUFFER, 2, INVALIDATE_ATTACHMENTS);
}
void RenderScene() {
// Render off-screen shadow map pass
RenderToFramebuffer(2, CLEAR_ALL);
// Render off-screen velocity map pass
RenderToFramebuffer(1, CLEAR_ALL);
InvalidateFramebufferAttachments(2);
// Render the main 3D scene
RenderToFramebuffer(3, CLEAR_ALL);
InvalidateFramebufferAttachments(3);
// Render final output with motion blur
RenderToFramebuffer(0, CLEAR_ALL);
eglSwapBuffers(); // Present the final frame
}
By optimizing the render pass structure with techniques such as tile-based rendering, sub-pass utilization, and render target reuse, significant improvements can be achieved in memory usage, power consumption, and performance. This optimized approach reduces unnecessary memory operations and minimizes frame-buffer switches, making it ideal for platforms like the Versal AI Edge Series Gen 2 and Versal Prime Series Gen 2 adaptive SoCs that use the Arm Mali-G78AE GPU.