How to Optimize Mesh Overdraw with `meshopt_optimizeOverdraw`: Threshold Values and Implementation Guide

meshopt_optimizeOverdraw reorders triangle indices to minimize pixel overdraw while constraining vertex cache degradation to a user-defined threshold, typically 1.05 for a 5% allowance.

The meshopt_optimizeOverdraw function in the zeux/meshoptimizer library provides a crucial optimization stage for reducing GPU pixel fill costs. By reordering the index buffer so that front-facing geometry renders before distant occluders, you can significantly reduce the number of pixels that pass the depth test only to be overwritten later. This guide covers the recommended threshold values, implementation steps, and source code references from the official repository.

Understanding meshopt_optimizeOverdraw

What the Function Does

meshopt_optimizeOverdraw reorders the index buffer to minimize overdraw while preserving a specified percentage of the vertex cache efficiency gained from previous optimizations. According to the declaration in src/meshoptimizer.h, the function accepts vertex positions and a threshold parameter that controls the trade-off between pixel fill rate and vertex cache hit ratio.

The algorithm requires that your index buffer is already optimized for vertex cache locality. It accepts the following parameters:

  • destination: Output index buffer (must be pre-allocated with at least index_count elements)
  • indices: Input index buffer already optimized by meshopt_optimizeVertexCache
  • index_count: Number of indices in the buffer
  • vertex_positions: Pointer to the first float x, y, z of each vertex (the vertex type must start with these three floats)
  • vertex_count: Total number of vertices
  • vertex_positions_stride: Byte stride between consecutive vertex positions (typically sizeof(Vertex))
  • threshold: Maximum allowed degradation of the vertex cache hit ratio as a multiplier of the original metric

Why Threshold Matters

The threshold parameter represents a hard limit on how much the optimizer can degrade the ACMR (Average Cache Miss Ratio) or ATVR (Average Transform Vertex Reuse). The value acts as a multiplier of the original cache efficiency metric.

For example, a threshold of 1.05 permits the vertex cache efficiency to drop by up to 5% from the baseline established by meshopt_optimizeVertexCache. Values must be greater than 1.0; smaller values force the optimizer to reject any overdraw-reducing moves that would worsen cache performance beyond your specified limit.

The repository documentation and implementation suggest the following ranges based on your performance constraints:

Threshold Value Cache Degradation Use Case
1.02 2% Conservative optimization for vertex-heavy scenes
1.05 5% Recommended default for balanced performance
1.10 10% Aggressive overdraw reduction for pixel-bound shaders
1.20 20% Maximum overdraw optimization for very simple vertex shaders

Start with 1.05 (5% degradation) and profile your specific workload. If you are heavily pixel-shader bound with simple vertex processing, you can experiment with values up to 1.20.

Prerequisites and When to Use It

Required Precondition

You must run meshopt_optimizeVertexCache (or meshopt_optimizeVertexCacheFifo) on the index buffer before calling meshopt_optimizeOverdraw. The overdraw optimizer assumes the input already has optimal vertex cache locality. Feeding raw, unoptimized geometry will produce poor results and can degrade rendering performance.

Ideal Use Cases

Use meshopt_optimizeOverdraw when:

  • Pixel shader cost dominates your rendering budget (complex lighting, translucency, or post-processing effects)
  • Rendering on mobile or power-constrained GPUs where reducing shaded pixel counts improves battery life
  • Working with opaque geometry where early-Z/depth prepass is available but you want to reduce the initial shaded pixel count
  • Your meshes exhibit significant depth complexity with large overdraw regions

When to Avoid It

Skip this optimization when:

  • Targeting tiled-deferred GPUs (PowerVR, Apple A-series) where early-Z is already extremely cheap
  • Your scene is vertex-heavy with very large vertex buffers and simple vertex shaders
  • The additional vertex cache misses outweigh the overdraw reduction gains (profile with meshopt_analyzeOverdraw)

Implementation Guide

Step 1: Vertex Cache Optimization (Required)

Before overdraw optimization, optimize the vertex cache to establish the baseline ACMR/ATVR metric:

#include "meshoptimizer.h"

// vertices must be a struct starting with float x, y, z
meshopt_optimizeVertexCache(
    indices.data(),           // destination
    indices.data(),           // source
    index_count,
    vertex_count
);

Step 2: Overdraw Optimization

Pass the pre-optimized indices and your chosen threshold. The vertex_positions pointer must point to the first float of the first vertex:

const float threshold = 1.05f;  // Allow 5% cache degradation

meshopt_optimizeOverdraw(
    indices.data(),           // destination (can be same as source)
    indices.data(),           // source (must be cache-optimized)
    index_count,
    &vertices[0].x,           // pointer to first float x
    vertex_count,
    sizeof(Vertex),           // stride between positions
    threshold
);

Step 3: Optional Vertex Fetch Optimization

After overdraw optimization, you can optionally run meshopt_optimizeVertexFetch to improve memory locality:

meshopt_optimizeVertexFetch(
    vertices.data(),
    indices.data(),
    index_count,
    vertices.data(),
    vertex_count,
    sizeof(Vertex)
);

Code Examples

Complete C++ Workflow

This example demonstrates the full pipeline from loading to optimization:

#include "meshoptimizer.h"
#include <vector>
#include <cstdio>

struct Vertex {
    float x, y, z;    // Position must be first three floats
    float u, v;       // Texture coordinates follow
};

int main() {
    std::vector<Vertex> vertices = loadMeshVertices();
    std::vector<unsigned int> indices = loadMeshIndices();
    
    size_t index_count = indices.size();
    size_t vertex_count = vertices.size();
    
    // 1. Vertex cache optimization (required before overdraw)
    meshopt_optimizeVertexCache(
        indices.data(),
        indices.data(),
        index_count,
        vertex_count
    );
    
    // 2. Overdraw optimization with 1.05 threshold
    meshopt_optimizeOverdraw(
        indices.data(),
        indices.data(),
        index_count,
        &vertices[0].x,
        vertex_count,
        sizeof(Vertex),
        1.05f
    );
    
    // 3. Optional: Vertex fetch optimization
    meshopt_optimizeVertexFetch(
        vertices.data(),
        indices.data(),
        index_count,
        vertices.data(),
        vertex_count,
        sizeof(Vertex)
    );
    
    return 0;
}

Threshold Tuning with meshopt_analyzeOverdraw

To empirically determine the best threshold for your mesh, test multiple values and compare the overdraw statistics:

#include <vector>
#include <algorithm>

void tuneOverdrawThreshold(
    std::vector<unsigned int>& indices,
    const std::vector<Vertex>& vertices,
    size_t index_count,
    size_t vertex_count
) {
    // Save the cache-optimized version as baseline
    std::vector<unsigned int> baseline = indices;
    
    float thresholds[] = {1.00f, 1.05f, 1.10f, 1.20f};
    
    for (float th : thresholds) {
        std::vector<unsigned int> test_indices = baseline;
        
        meshopt_optimizeOverdraw(
            test_indices.data(),
            test_indices.data(),
            index_count,
            &vertices[0].x,
            vertex_count,
            sizeof(Vertex),
            th
        );
        
        meshopt_OverdrawStatistics stats = meshopt_analyzeOverdraw(
            test_indices.data(),
            index_count,
            &vertices[0].x,
            vertex_count,
            sizeof(Vertex)
        );
        
        printf("Threshold %.2f: overdraw %.3f (pixels shaded / pixels covered)\n",
               th, stats.overdraw);
    }
}

Select the threshold that minimizes your actual frame time, not just the overdraw metric, as higher thresholds may increase vertex shader costs.

Summary

  • meshopt_optimizeOverdraw requires a vertex-cache-optimized index buffer as input; always run meshopt_optimizeVertexCache first.
  • The threshold parameter controls the maximum allowed degradation of vertex cache efficiency, with 1.05 (5%) being the recommended default.
  • Vertex positions must be passed as a pointer to the first float x of the first vertex, with a stride matching your vertex structure.
  • Use this optimization for pixel-bound scenes with complex shaders, but avoid it for vertex-heavy geometry or tiled-deferred GPU architectures.
  • Profile with meshopt_analyzeOverdraw to validate the overdraw reduction against the vertex cache cost.

Frequently Asked Questions

What happens if I set the threshold to 1.0?

A threshold of 1.0 forces the optimizer to reject any moves that would degrade the vertex cache metric at all. Since the algorithm must trade vertex locality for overdraw reduction, a threshold of exactly 1.0 effectively prevents any reordering from occurring. You must use values greater than 1.0 to achieve overdraw optimization.

Can I skip vertex cache optimization and only use overdraw optimization?

No. The implementation in src/overdraw.cpp assumes the input index buffer already has optimal vertex cache locality. Running meshopt_optimizeOverdraw on unoptimized geometry will produce poor results because the algorithm has no baseline ACMR/ATVR metric to protect, potentially generating index orders that are slow for both vertex and pixel processing.

How does the threshold value affect the final mesh?

The threshold acts as a multiplier on the original vertex cache efficiency metric (ACMR). If your cache-optimized mesh has an ACMR of 0.5 and you specify a threshold of 1.10, the optimizer will only accept index reorderings that keep the final ACMR at or below 0.55. Lower thresholds preserve more vertex cache efficiency but allow less overdraw reduction; higher thresholds permit more aggressive overdraw elimination at the cost of additional vertex shader invocations.

Should I use this on mobile devices?

Yes, but carefully. On mobile GPUs (particularly PowerVR and Apple A-series chips), early-Z testing is already very efficient, so the gains from overdraw optimization may be limited. However, for complex pixel shaders or when targeting older Android devices with less sophisticated early-Z hardware, a threshold of 1.05 can reduce power consumption and improve thermal performance by reducing the number of shaded pixels.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →