# How to Encode and Decode Vertex Buffers with Meshoptimizer Compression Codec

> Learn to encode and decode vertex buffers efficiently with meshoptimizer compression codec. Use meshopt_encodeVertexBuffer and meshopt_decodeVertexBuffer for fast, lossless data conversion.

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

---

**Meshoptimizer provides a fast, lossless compression codec that converts raw vertex data into compact byte streams using delta encoding and SIMD acceleration, accessible via `meshopt_encodeVertexBuffer` and `meshopt_decodeVertexBuffer` defined in [`src/meshoptimizer.h`](https://github.com/zeux/meshoptimizer/blob/main/src/meshoptimizer.h).**

The `zeux/meshoptimizer` library implements a specialized vertex compression codec designed for real-time graphics pipelines. This codec reduces vertex buffer size through block-based delta encoding and per-channel analysis while guaranteeing bit-exact reconstruction. All public API functions are declared in [`src/meshoptimizer.h`](https://github.com/zeux/meshoptimizer/blob/main/src/meshoptimizer.h) with the implementation residing in [`src/vertexcodec.cpp`](https://github.com/zeux/meshoptimizer/blob/main/src/vertexcodec.cpp).

## Encoding Vertex Buffers

The primary entry point for compression is `meshopt_encodeVertexBuffer`, which processes raw vertex data and outputs a compact byte stream. For fine-grained control over compression ratios, use `meshopt_encodeVertexBufferLevel` to specify trade-offs between encoding speed and output size.

### Compression Levels and Versioning

The codec supports format versioning via `meshopt_encodeVertexVersion(int version)`, where version 0 indicates legacy format and version 1 (default) enables per-channel control bytes. The **level** parameter accepts values from 0 to 3:
- **Level 0**: Fastest encoding, minimal analysis
- **Level 3**: Maximum compression, full channel analysis

When level is 2 or higher, the encoder invokes `estimateRotate` and `estimateChannel` in [`src/vertexcodec.cpp`](https://github.com/zeux/meshoptimizer/blob/main/src/vertexcodec.cpp) to analyze vertex data for rotational or predictive patterns, improving compression efficiency.

### Block-Based Encoding Process

In [`src/vertexcodec.cpp`](https://github.com/zeux/meshoptimizer/blob/main/src/vertexcodec.cpp), the encoder processes vertices in blocks sized by `vertex_block_size` to maximize SIMD throughput. For each block, the implementation performs the following steps:

1. Writes a header byte combining `kVertexHeader` with the format version
2. Stores per-channel control bytes (version 1 only) that specify encoding strategies: literal, zero, or delta-compressed
3. Applies **delta encoding** to store only differences between consecutive vertices
4. Quantizes deltas using variable-length byte coding via `encodeBytesSimd`
5. Interleaves bytes for cache-friendly output

After processing all blocks, the encoder writes a tail section containing the first vertex (required for delta reconstruction) and the channel control bytes. The function returns the total encoded size in bytes, or 0 if the output buffer is insufficient.

## Decoding Vertex Buffers

Reconstruction uses `meshopt_decodeVertexBuffer`, which reverses the encoding process safely. The decoder first reads the header byte to determine the format version, then extracts per-channel control bytes when present.

### Block-Based Decompression

For each 4-byte channel, the implementation calls `decodeVertexBlockSimd` in [`src/vertexcodec.cpp`](https://github.com/zeux/meshoptimizer/blob/main/src/vertexcodec.cpp). This routine:
- Interprets control bytes to select between literal, zero, or compressed data paths
- Decompresses the byte stream using `decodeBytesSimd`
- Reconstructs original values via `decodeDeltas4Simd` to reverse delta encoding
- Applies the first vertex from the tail section to restore absolute positions

The function returns 0 on success or a non-zero error code if the input data is malformed or truncated.

### Safety and Threading

The decoder is safe for untrusted input; it performs bounds checking and will never write outside the destination buffer. However, malformed data may produce garbage vertices rather than crashing the application. Note that encoding functions are **not thread-safe** when called simultaneously on shared buffers; use separate output buffers per thread or serialize access to `meshopt_encodeVertexBuffer`.

## Complete Code Example

The following example demonstrates encoding and decoding a vertex buffer containing 1000 vertices with 32 bytes per vertex (e.g., position, normal, and texture coordinate data):

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

// Encode a vertex buffer
size_t encodeVertices(const void* srcVertices,
                      size_t vertexCount,
                      size_t vertexSize,
                      std::vector<unsigned char>& outEncoded)
{
    // Allocate worst-case buffer
    size_t maxSize = meshopt_encodeVertexBufferBound(vertexCount, vertexSize);
    outEncoded.resize(maxSize);

    // Encode with default level (2) and default version (1)
    size_t encodedSize = meshopt_encodeVertexBuffer(
        outEncoded.data(), maxSize,
        srcVertices, vertexCount, vertexSize);

    if (encodedSize == 0) {
        fprintf(stderr, "Encoding failed: output buffer too small\n");
        outEncoded.clear();
        return 0;
    }

    outEncoded.resize(encodedSize);   // shrink to actual size
    return encodedSize;
}

// Decode a vertex buffer
bool decodeVertices(const std::vector<unsigned char>& encoded,
                    void* dstVertices,
                    size_t vertexCount,
                    size_t vertexSize)
{
    int err = meshopt_decodeVertexBuffer(
        dstVertices, vertexCount, vertexSize,
        encoded.data(), encoded.size());

    if (err != 0) {
        fprintf(stderr, "Decoding error %d\n", err);
        return false;
    }
    return true;
}

// Usage example
int main()
{
    const size_t vertexCount = 1000;
    const size_t vertexSize = 32; // 8 floats: pos(3) + normal(3) + uv(2)
    std::vector<float> vertices(vertexCount * vertexSize / sizeof(float));

    // ... fill vertices with data ...

    // Encode
    std::vector<unsigned char> encoded;
    encodeVertices(vertices.data(), vertexCount, vertexSize, encoded);
    printf("Encoded %zu bytes (original %zu bytes)\n",
           encoded.size(), vertexCount * vertexSize);

    // Decode
    std::vector<float> decoded(vertexCount * vertexSize / sizeof(float));
    if (decodeVertices(encoded, decoded.data(), vertexCount, vertexSize))
        printf("Decoding succeeded!\n");

    // decoded now matches the original `vertices` bit-exactly
    return 0;
}

```

## Summary

- The meshoptimizer vertex codec provides **lossless compression** via `meshopt_encodeVertexBuffer` and `meshopt_decodeVertexBuffer`
- Primary implementation resides in [`src/meshoptimizer.h`](https://github.com/zeux/meshoptimizer/blob/main/src/meshoptimizer.h) (API) and [`src/vertexcodec.cpp`](https://github.com/zeux/meshoptimizer/blob/main/src/vertexcodec.cpp) (logic)
- Encoding uses **block-based delta compression** with optional channel analysis at compression levels ≥ 2
- Version 1 format includes per-channel control bytes that improve compression ratios over legacy version 0
- The decoder is safe for untrusted input, but encoding requires application-level thread synchronization

## Frequently Asked Questions

### What is the difference between meshopt_encodeVertexBuffer and meshopt_encodeVertexBufferLevel?

`meshopt_encodeVertexBuffer` uses a default compression level of 2, providing a balance between speed and ratio. `meshopt_encodeVertexBufferLevel` accepts an explicit level parameter (0-3) where level 0 prioritizes encoding speed and level 3 maximizes compression by enabling full channel analysis via `estimateRotate` and `estimateChannel`.

### Is the meshoptimizer vertex codec lossless?

Yes, the compression is **lossless and bit-exact**. The delta encoding and quantization process preserves all original vertex data, ensuring that decoded vertices match the source data exactly.

### Is the vertex codec safe for untrusted input?

The **decoder** is safe for untrusted input; it performs bounds checking and will not write outside the destination buffer even with malformed data. However, malformed data may produce garbage vertices. The **encoder** is not safe for concurrent access without external synchronization.

### Can I encode vertex buffers from multiple threads simultaneously?

You must synchronize access or use separate output buffers per thread. The encoding functions in [`src/vertexcodec.cpp`](https://github.com/zeux/meshoptimizer/blob/main/src/vertexcodec.cpp) are not thread-safe when writing to shared buffers simultaneously, though the decoding functions can be called concurrently on different input/output buffers.