Optimizing UGUI Performance

Core Concepts

All UI elements are rendered using mesh-based geometry. An Image component consists of two triangles forming four vertices. A draw call represents a GPU command submission for rendering an object or batch of objects. Each draw call involves sending rendering instructions to the graphics processor. Fill rate refers to the number of pixels the GPU can process per second, indicating its rendering capability. The screen resolution determines how many pixels need to be rendered; higher resolutions increase pixel count and impact fill rate. Overdraw occurs when multiple draw calls render the same pixel during rendering, typically seen in overlapping or occluded scenes.

Batching reduces the number of draw calls sent to the GPU by combining similar objects into single batches. Static batching applies to non-moving objects, while dynamic batching handles moving ones that share materials. This optimization minimizes CPU overhead from frequent draw call submissions and decreases GPU load.

In this scenario, two Image components generate separate draw calls with potential overdraw where overlapping areas are rendered twice. By merging their meshes into one, a single draw call suffices instead of individual calls, reducing both CPU and GPU processing costs.

Key Classes

MaskableGraphic Class

Components inheriting from MaskableGraphic support masking operations.

Mask Implementation Differences

RectMask2D

The IClipper interface defines clipping behavior:

public interface IClipper
{
    void PerformClipping();
}

Mask Component

The GetModifiedMaterial method uses stencil buffers for masking:

public virtual Material GetModifiedMaterial(Material baseMaterial)
{
    if (!MaskEnabled())
        return baseMaterial;

    var rootSortCanvas = MaskUtilities.FindRootSortOverrideCanvas(transform);
    var stencilDepth = MaskUtilities.GetStencilDepth(transform, rootSortCanvas);
    if (stencilDepth >= 8)
    {
        Debug.LogWarning("Attempting to use a stencil mask with depth > 8", gameObject);
        return baseMaterial;
    }

    int desiredStencilBit = 1 << stencilDepth;

    if (desiredStencilBit == 1)
    {
        var maskMaterial = StencilMaterial.Add(baseMaterial, 1, StencilOp.Replace, CompareFunction.Always, m_ShowMaskGraphic ? ColorWriteMask.All : 0);
        StencilMaterial.Remove(m_MaskMaterial);
        m_MaskMaterial = maskMaterial;

        var unmaskMaterial = StencilMaterial.Add(baseMaterial, 1, StencilOp.Zero, CompareFunction.Always, 0);
        StencilMaterial.Remove(m_UnmaskMaterial);
        m_UnmaskMaterial = unmaskMaterial;
        graphic.canvasRenderer.popMaterialCount = 1;
        graphic.canvasRenderer.SetPopMaterial(m_UnmaskMaterial, 0);

        return m_MaskMaterial;
    }

    var maskMaterial2 = StencilMaterial.Add(baseMaterial, desiredStencilBit | (desiredStencilBit - 1), StencilOp.Replace, CompareFunction.Equal, m_ShowMaskGraphic ? ColorWriteMask.All : 0, desiredStencilBit - 1, desiredStencilBit | (desiredStencilBit - 1));
    StencilMaterial.Remove(m_MaskMaterial);
    m_MaskMaterial = maskMaterial2;

    graphic.canvasRenderer.hasPopInstruction = true;
    var unmaskMaterial2 = StencilMaterial.Add(baseMaterial, desiredStencilBit - 1, StencilOp.Replace, CompareFunction.Equal, 0, desiredStencilBit - 1, desiredStencilBit | (desiredStencilBit - 1));
    StencilMaterial.Remove(m_UnmaskMaterial);
    m_UnmaskMaterial = unmaskMaterial2;
    graphic.canvasRenderer.popMaterialCount = 1;
    graphic.canvasRenderer.SetPopMaterial(m_UnmaskMaterial, 0);

    return m_MaskMaterial;
}

Different mask implementations require distinct optimization strategies.

Rendering Fundamentals

Depth Testing

Depth testing ensures correct pixel visibility by comparing depth values. It determines whether a pixel should appear infront of others based on distance from the camera.

How Depth Testing Works

  1. Depth Buffer: Before rendering, Unity writes each pixel's depth value to a special buffer called the depth buffer — a 2D array matching the screen size.

  2. Comparison Process: When rendering a pixel, Unity compares its depth value with the stored one in the depth buffer. If closer than the existing value, it renders and updates the buffer. Otherwise, it discards the pixel.

Render Queues

Render queues manage the order in which objects are rendered to optimize performance. They ensure efficient GPU utilization.

Transparent Object Handling

Transparent objects follow a back-to-front rendering order because they do not write to the depth buffer. The steps are:

  1. Sorting: Sort transparent objects by depth (distance from viewer).
  2. Back-to-Front Rendering: Render from farthest to nearest.
  3. Blending: Mix colors using alpha blending techniques.
  4. Disable Depth Writing: Prevents incorrect occlusion.
  5. Enable Depth Testing: Ensures proper layering over opaque objects.

This approach maintains visual correctness but introduces performance costs.

Performance Impacts of Transparency

Transparency causes performance issues due to:

  1. Overdraw: Pixels may be drawn multiple times since transparent objects don't write depth.
  2. Sorting Overhead: Sorting large numbers of transparent objects consumes CPU cycles.
  3. Alpha Blending Complexity: Requires complex calculations per pixel, increasing GPU workload.

Mesh Reconstruction

Mesh rebuilding occurs in two scenarios:

  1. Rebatch: When a canvas re-batches all child elements, causing full redraws. Any change triggers a complete rebuild of the entire canvas.
  2. Rebuild: Individual UI elements trigger specific redraws via the BuildBatch method.

CanvasUpdateRegistry Source

The CanvasUpdateRegistry class binds PerformUpdate to Canvas.willRenderCanvases:

protected CanvasUpdateRegistry()
{
    Canvas.willRenderCanvases += PerformUpdate;
}

PerformUpdate handles layout rebuidls in three phases: layout, clipping, and graphic rendering:

private void PerformUpdate()
{
    UISystemProfilerApi.BeginSample(UISystemProfilerApi.SampleType.Layout);
    CleanInvalidItems();

    m_PerformingLayoutUpdate = true;

    m_LayoutRebuildQueue.Sort(s_SortLayoutFunction);

    for (int i = 0; i <= (int)CanvasUpdate.PostLayout; i++)
    {
        UnityEngine.Profiling.Profiler.BeginSample(m_CanvasUpdateProfilerStrings[i]);

        for (int j = 0; j < m_LayoutRebuildQueue.Count; j++)
        {
            var rebuild = m_LayoutRebuildQueue[j];
            try
            {
                // Layout rebuild logic
            }
            catch (Exception ex)
            {
                // Error handling
            }
        }

        UnityEngine.Profiling.Profiler.EndSample();
    }
}

Tags: unity ugui Performance Optimization rendering

Posted on Fri, 08 May 2026 02:04:05 +0000 by Trek15