Skip to content

Library

Gianni Rosato edited this page Jul 14, 2026 · 2 revisions

fmetrics exposes a C ABI shared by two frontends:

  • C API declared in fmetrics.h (linked via libfmetrics.a)
  • Zig API exported by the fmetrics Zig module.

Both wrap the same underlying metric implementations.

Common

This section covers functionality common to both C & Zig.

Package Management

The Zig build system is also a build system for C, so you may use either API while still using the Zig build system.

Fetch the Zig package:

zig fetch --save git+https://github.com/halidecx/fmetrics.git

This should show something like this in build.zig.zon:

.dependencies = .{
    .fmetrics = .{
        .url = "git+https://github.com/halidecx/fmetrics.git#<commit>",
        .hash = "fmetrics-<version>-<hash>",
    },
},

Then you can link it from your build.zig:

There are two supported integration styles:

  1. Link the C ABI artifact and include the header, for projects looking to access the C API:
const fmetrics_dep = b.dependency("fmetrics", .{
    .target = target,
    .optimize = optimize,
});
const fmetrics = fmetrics_dep.artifact("fmetrics");
exe.root_module.linkLibrary(fmetrics);
exe.root_module.addIncludePath(fmetrics.getEmittedIncludeTree());
  1. Import the native Zig module, for Zig projects to access the native Zig API:
const fmetrics_dep = b.dependency("fmetrics", .{
    .target = target,
    .optimize = optimize,
});
const fmetrics_mod = fmetrics_dep.module("fmetrics");
exe.root_module.addImport("fmetrics", fmetrics_mod);
exe.root_module.linkLibrary(fmetrics_dep.artifact("libfmetrics"));

Image Representation

Images are 8-bit packed RGB data in the sRGB colorspace. There is currently one supported pixel format / colorspace pair:

Field C constant Zig enum value
Pixel format FMETRICS_PIX_FMT_RGB_UINT8 .rgb_uint8
Colorspace FMETRICS_COLORSPACE_SRGB .srgb
  • width and height are in pixels.
  • stride is the number of bytes per row. For tightly packed RGB it is width * 3, but it may be larger to allow row padding. It must be >= width * 3.
  • The data buffer must contain at least stride * height bytes.

Workspace

Every metric call requires a Workspace; this is an opaque handle that owns scratch memory reused across calls. Create one once, reuse it for many comparisons, and destroy it when done. The workspace grows its internal buffer on demand, so reusing it across images of similar size avoids reallocations.

Metrics

fmetrics supports:

Metric Higher is better? Supports --err-map? Notes
iwssim yes no Five-scale; needs >=161x161 images
msssim yes no Multi-scale SSIM
ssimu2 yes yes SSIMULACRA2
butteraugli no yes Takes intensity_target & pnorm

Butteraugli options:

Field Type Default Meaning
intensity_target f32 203.0 Peak luminance in nits (SDR ~ 203, HDR 1000)
pnorm i32 3 p-norm for the aggregate score

Error Map

For ssimu2 and butteraugli, you can request a per-pixel error map by passing a uint32_t* (C) or []u32 (Zig) buffer of length width * height. The metric fills it in addition to returning the scalar score.

Error Codes

C constant Zig error Meaning
FMETRICS_OK (success) No error
FMETRICS_ERR_INVALID_ARGUMENT InvalidArgument Null pointer, zero size, bad stride
FMETRICS_ERR_UNSUPPORTED_FORMAT UnsupportedFormat Format/colorspace not RGB_UINT8/sRGB
FMETRICS_ERR_DIMENSION_MISMATCH DimensionMismatch Reference and distorted differ in W/H
FMETRICS_ERR_OUT_OF_MEMORY OutOfMemory Allocation failed
FMETRICS_ERR_IWSSIM_IMG_TOO_SMALL IwssimImageTooSmall Image < 161x161 for five-scale IW-SSIM
FMETRICS_ERR_INTERNAL Internal Unexpected internal error

C Library Usage

Build & Link

Compilation emits the static library to be used with C. libfmetrics.a statically depends on libspng and libcvvdp (pulled in by the fmetrics build). When linking a C program, include zig-out/include on the include path and link the archive plus its transitive static dependencies and libm. Example:

cc -O3 -Izig-out/include my_program.c \
    zig-out/lib/libfmetrics.a \
    -lm -lpthread

Examples

C: Minimal Example

#include <stdio.h>
#include <stdlib.h>
#include "fmetrics.h"

int main(void) {
    /* 2x2 tightly-packed sRGB RGB images (3 bytes/pixel). */
    const uint8_t ref_rgb[] = {
        255,   0,   0,    0, 255,   0,
          0,   0, 255,  255, 255,   0,
    };
    const uint8_t dst_rgb[] = {
        240,  10,  10,   10, 240,  10,
         10,  10, 240,  240, 240,  10,
    };

    const uint32_t width = 2, height = 2;
    const uint32_t stride = width * 3;

    FmetricsImg ref = {
        .data = ref_rgb,
        .width = width,
        .height = height,
        .stride = stride,
        .format = FMETRICS_PIX_FMT_RGB_UINT8,
        .colorspace = FMETRICS_COLORSPACE_SRGB,
    };
    FmetricsImg dst = {
        .data = dst_rgb,
        .width = width,
        .height = height,
        .stride = stride,
        .format = FMETRICS_PIX_FMT_RGB_UINT8,
        .colorspace = FMETRICS_COLORSPACE_SRGB,
    };

    FmetricsWorkspace *ws = fmetrics_workspace_create();
    if (ws == NULL) { fprintf(stderr, "OOM\n"); return 1; }

    double score = 0.0;
    FmetricsErr err = fmetrics_msssim_cmp(ws, &ref, &dst, &score);
    if (err != FMETRICS_OK) {
        fprintf(stderr, "msssim: %s\n", fmetrics_error_str(err));
        fmetrics_workspace_destroy(ws);
        return 1;
    }
    printf("MS-SSIM: %f\n", score);

    fmetrics_workspace_destroy(ws);
    return 0;
}

C: Butteraugli's Options

FmetricsButteraugliOptions opts = {
    .intensity_target = 203.0f,  /* nits; use 1000.0f for HDR */
    .pnorm = 3,
};
double score = 0.0;
FmetricsErr err = fmetrics_butteraugli_cmp(ws, &ref, &dst, &opts, &score);
if (err != FMETRICS_OK) {
    fprintf(stderr, "butteraugli: %s\n", fmetrics_error_str(err));
}

C: Error Map (SSIMULACRA2)

const size_t pixels = (size_t)width * (size_t)height;
uint32_t *map = malloc(pixels * sizeof(uint32_t));
if (!map) { /* handle OOM */ }

double score = 0.0;
FmetricsErr err = fmetrics_ssimu2_cmp_map(ws, &ref, &dst, &score, map);
if (err == FMETRICS_OK) {
    printf("SSIMULACRA2: %f\n", score);
    /* map[y * width + x] holds the per-pixel error. */
}
free(map);

C: Reusing the Workspace

The workspace is the only allocation you need to keep alive between calls. Reuse it to avoid repeated reallocations:

FmetricsWorkspace *ws = fmetrics_workspace_create();
for (size_t i = 0; i < n_pairs; i++) {
    double score = 0.0;
    FmetricsErr e = fmetrics_ssimu2_cmp(ws, &refs[i], &dsts[i], &score);
    if (e != FMETRICS_OK) { /* ... */ }
}
fmetrics_workspace_destroy(ws);

C: Version Introspection

printf("fmetrics %s\n", fmetrics_version_str());

Zig Library Usage

Integration with Zig projects is covered in the Common section of this document.

Minimal Zig Example

const std = @import("std");
const fmetrics = @import("fmetrics");

pub fn main() !void {
    const ref_rgb = [_]u8{
        255,   0,   0,    0, 255,   0,
          0,   0, 255,  255, 255,   0,
    };
    const dst_rgb = [_]u8{
        240,  10,  10,   10, 240,  10,
         10,  10, 240,  240, 240,  10,
    };

    const width: u32 = 2;
    const height: u32 = 2;

    const ref = try fmetrics.Image.init(&ref_rgb, width, height);
    const dst = try fmetrics.Image.init(&dst_rgb, width, height);

    var workspace = try fmetrics.Workspace.init();
    defer workspace.deinit();

    const score = try fmetrics.msssim(&workspace, ref, dst);
    std.debug.print("MS-SSIM: {d}\n", .{score});
}

Zig: Constructing an Image

Image.init assumes tightly packed RGB (stride = width * 3). Use initWithStride for row padding:

const ref = try fmetrics.Image.init(rgb_slice, width, height);

const padded = try fmetrics.Image.initWithStride(
    rgb_with_padding, width, height, stride_bytes,
);

The Zig API validates inputs and returns a typed fmetrics.Error:

fmetrics.Error When it occurs
InvalidArgument Zero dims, bad stride, buffer too small
UnsupportedFormat Format/colorspace not rgb_uint8/srgb
DimensionMismatch Reference and distorted differ in W/H
OutOfMemory Workspace or internal allocation failed
IwssimImageTooSmall Image < 161x161 for five-scale IW-SSIM
Internal Unexpected internal error

Zig: Butteraugli with Options

const opts = fmetrics.ButteraugliOptions{
    .intensity_target = 203.0,  // nits; 1000.0 for HDR
    .pnorm = 3,
};
const score = try fmetrics.butteraugli(&workspace, ref, dst, opts);
std.debug.print("Butteraugli: {d}\n", .{score});

Zig: Error Map (SSIMULACRA2)

var allocator = std.heap.page_allocator;
const pixels = @as(usize, width) * @as(usize, height);
const map = try allocator.alloc(u32, pixels);
defer allocator.free(map);

const score = try fmetrics.ssimu2Map(&workspace, ref, dst, map);
std.debug.print("SSIMULACRA2: {d}\n", .{score});
// map[y * width + x] holds the per-pixel error.

Zig: Error Handling

const score = fmetrics.iwssim(&workspace, ref, dst) catch |err| {
    std.debug.print("iwssim failed: {s}\n", .{fmetrics.errorString(err)});
    return err;
};

Zig: All Metrics at a Glance

const ws = try fmetrics.Workspace.init();
defer ws.deinit();

const iw = try fmetrics.iwssim(ws, ref, dst);
const ms = try fmetrics.msssim(ws, ref, dst);
const u2 = try fmetrics.ssimu2(ws, ref, dst);

const opts = fmetrics.ButteraugliOptions{};
const ba = try fmetrics.butteraugli(ws, ref, dst, opts);

const map = try allocator.alloc(u32, pixels);
defer allocator.free(map);
const u2_map = try fmetrics.ssimu2Map(ws, ref, dst, map);
const ba_map = try fmetrics.butteraugliMap(ws, ref, dst, opts, map);

Zig: Version

std.debug.print("fmetrics {s}\n", .{fmetrics.version});  // "0.0.2"

Notes

Notes apply to both C and Zig.

Thread Safety

  • A single Workspace must not be used concurrently from multiple threads. Create one Workspace per worker thread (as our main.zig's VideoWorker does).
  • Image values are immutable views over caller-owned buffers and may be shared across threads freely, as long as the underlying data is not mutated during the call.

Lifetime & Ownership

  • Workspace: caller owns the handle returned by fmetrics_workspace_create (C) / Workspace.init (Zig). Free it with fmetrics_workspace_destroy (C) or workspace.deinit() (Zig).
  • Image data: the caller owns the pixel buffer. Image / FmetricsImg only borrows it for the duration of the metric call.
  • Error maps: caller allocates and owns the buffer; the metric writes into it.