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

> Minimize mesh overdraw with meshopt_optimizeOverdraw. Learn optimal threshold values and implementation guide to reduce rendering cost and improve performance in your graphics applications.

- Repository: [Arseny Kapoulkine/meshoptimizer](https://github.com/zeux/meshoptimizer)
- Tags: how-to-guide
- Published: 2026-07-12

---

**`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`](https://github.com/zeux/meshoptimizer/blob/main/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.

## Recommended Threshold Values

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:

```cpp
#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:

```cpp
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:

```cpp
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:

```cpp
#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:

```cpp
#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`](https://github.com/zeux/meshoptimizer/blob/main/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.