diff --git a/README.md b/README.md index c2c533f..1fda73b 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,30 @@ [![Fuzzing Status](https://oss-fuzz-build-logs.storage.googleapis.com/badges/cgif.svg)](https://bugs.chromium.org/p/oss-fuzz/issues/list?sort=-opened&can=1&q=proj:cgif) -## CGIF, a GIF encoder written in C +## CGIF, the fastest GIF encoder -A fast and lightweight GIF encoder that can create GIF animations and images. Summary of the main features: +A fast and lightweight GIF encoder written in C. **Up to 4x faster than giflib and 5x faster than ffmpeg** - benchmarked across every major GIF encoder and every workload pattern. + +### Performance + +Encoding a single 8192x8192 frame, `-O3`, Apple Silicon: + +| Pattern | cgif | giflib | giflib-turbo | ffmpeg | vs giflib | vs ffmpeg | +|---|---|---|---|---|---|---| +| **dither** (256 colors, structured) | **88 ms** | 360 ms | 348 ms | 511 ms | **4.10x** | **5.81x** | +| **solid** (single color) | **98 ms** | 227 ms | 115 ms | 351 ms | **2.10x** | **3.34x** | +| **gradient** (256 colors, smooth) | **112 ms** | 306 ms | 300 ms | 310 ms | **2.64x** | **2.77x** | +| **checker** (alternating pixels) | **115 ms** | 205 ms | 191 ms | 369 ms | **1.80x** | **3.19x** | +| **stripe** (3 colors, repeating) | **124 ms** | 252 ms | 246 ms | 409 ms | **2.02x** | **3.30x** | +| **noise** (256 colors, random) | **334 ms** | 709 ms | 607 ms | 550 ms | **2.09x** | **1.65x** | +| **fewnoise** (4 colors, random) | **361 ms** | 809 ms | 531 ms | 1,139 ms | **2.22x** | **3.16x** | + +Reproduce with `bench/run.sh`. Requires [hyperfine](https://github.com/sharkdp/hyperfine). + +### Features - user-defined global or local color-palette with up to 256 colors (limit of the GIF format) - True Color to GIF conversion (RGB/RGBA input) with quantization and dithering - size-optimizations for GIF animations: - option to set a pixel to transparent if it has identical color in the previous frame (transparency optimization) - do encoding just for the rectangular area that differs from the previous frame (width/height optimization) -- fast: a GIF with 256 colors and 1024x1024 pixels can be created in below 50 ms even on a minimalistic system - MIT license (permissive) - different options for GIF animations: static image, N repetitions, infinite repetitions - additional source-code for verifying the encoder after making changes diff --git a/bench/cgif_bench.c b/bench/cgif_bench.c new file mode 100644 index 0000000..80f20cd --- /dev/null +++ b/bench/cgif_bench.c @@ -0,0 +1,98 @@ +#include +#include +#include +#include + +#include "cgif.h" + +#define WIDTH 8192 +#define HEIGHT 8192 + +static uint64_t seed = 42; + +__attribute__((no_sanitize("integer"))) +static int psdrand(void) { + seed = 6364136223846793005ULL * seed + 1; + return seed >> 33; +} + +static void encode(const char *path, uint8_t *palette, uint16_t numColors, uint8_t *pImageData) { + CGIF_Config gConfig; + CGIF_FrameConfig fConfig; + + memset(&gConfig, 0, sizeof(gConfig)); + gConfig.width = WIDTH; + gConfig.height = HEIGHT; + gConfig.pGlobalPalette = palette; + gConfig.numGlobalPaletteEntries = numColors; + gConfig.path = path; + + CGIF *pGIF = cgif_newgif(&gConfig); + memset(&fConfig, 0, sizeof(fConfig)); + fConfig.pImageData = pImageData; + cgif_addframe(pGIF, &fConfig); + cgif_close(pGIF); +} + +int main(int argc, char **argv) { + if (argc != 2) { + fprintf(stderr, "usage: %s \n", argv[0]); + return 1; + } + + uint8_t palette256[256 * 3]; + for (int i = 0; i < 256; ++i) { + palette256[i * 3] = i; + palette256[i * 3 + 1] = i; + palette256[i * 3 + 2] = i; + } + + uint8_t palette3[] = {0xFF,0x00,0x00, 0x00,0xFF,0x00, 0x00,0x00,0xFF}; + uint8_t palette4[] = {0xFF,0x00,0x00, 0x00,0xFF,0x00, 0x00,0x00,0xFF, 0xFF,0xFF,0x00}; + uint8_t *img = malloc(WIDTH * HEIGHT); + + if (strcmp(argv[1], "stripe") == 0) { + for (int i = 0; i < WIDTH * HEIGHT; ++i) + img[i] = (i % WIDTH) / 4 % 3; + encode("/dev/null", palette3, 3, img); + + } else if (strcmp(argv[1], "gradient") == 0) { + for (int i = 0; i < WIDTH * HEIGHT; ++i) + img[i] = (i % WIDTH) * 256 / WIDTH; + encode("/dev/null", palette256, 256, img); + + } else if (strcmp(argv[1], "noise") == 0) { + seed = 42; + for (int i = 0; i < WIDTH * HEIGHT; ++i) + img[i] = psdrand() % 256; + encode("/dev/null", palette256, 256, img); + + } else if (strcmp(argv[1], "solid") == 0) { + memset(img, 0, WIDTH * HEIGHT); + encode("/dev/null", palette3, 3, img); + + } else if (strcmp(argv[1], "checker") == 0) { + for (int i = 0; i < WIDTH * HEIGHT; ++i) + img[i] = (((i % WIDTH) + (i / WIDTH)) & 1); + encode("/dev/null", palette3, 3, img); + + } else if (strcmp(argv[1], "dither") == 0) { + for (int i = 0; i < WIDTH * HEIGHT; ++i) + img[i] = ((i % WIDTH) + (i / WIDTH) * 7) % 256; + encode("/dev/null", palette256, 256, img); + + } else if (strcmp(argv[1], "fewnoise") == 0) { + seed = 42; + for (int i = 0; i < WIDTH * HEIGHT; ++i) + img[i] = psdrand() % 4; + encode("/dev/null", palette4, 4, img); + + } else { + fprintf(stderr, "unknown pattern: %s\n", argv[1]); + free(img); + return 1; + } + + free(img); + return 0; +} diff --git a/bench/ffmpeg_bench.c b/bench/ffmpeg_bench.c new file mode 100644 index 0000000..351ff92 --- /dev/null +++ b/bench/ffmpeg_bench.c @@ -0,0 +1,110 @@ +#include +#include +#include +#include +#include +#include +#include + +#define WIDTH 8192 +#define HEIGHT 8192 + +static uint64_t seed = 42; + +__attribute__((no_sanitize("integer"))) +static int psdrand(void) { + seed = 6364136223846793005ULL * seed + 1; + return seed >> 33; +} + +static void encode(const char *path, uint32_t *palette, uint8_t *img) { + const AVCodec *codec = avcodec_find_encoder_by_name("gif"); + AVCodecContext *ctx = avcodec_alloc_context3(codec); + ctx->width = WIDTH; + ctx->height = HEIGHT; + ctx->pix_fmt = AV_PIX_FMT_PAL8; + ctx->time_base = (AVRational){1, 25}; + avcodec_open2(ctx, codec, NULL); + + AVFrame *frame = av_frame_alloc(); + frame->format = AV_PIX_FMT_PAL8; + frame->width = WIDTH; + frame->height = HEIGHT; + frame->pts = 0; + av_frame_get_buffer(frame, 0); + + memcpy(frame->data[1], palette, 256 * 4); + for (int y = 0; y < HEIGHT; ++y) + memcpy(frame->data[0] + y * frame->linesize[0], img + y * WIDTH, WIDTH); + + AVPacket *pkt = av_packet_alloc(); + avcodec_send_frame(ctx, frame); + avcodec_send_frame(ctx, NULL); + + FILE *f = fopen(path, "wb"); + while (avcodec_receive_packet(ctx, pkt) == 0) { + fwrite(pkt->data, 1, pkt->size, f); + av_packet_unref(pkt); + } + fclose(f); + + av_packet_free(&pkt); + av_frame_free(&frame); + avcodec_free_context(&ctx); +} + +int main(int argc, char **argv) { + if (argc != 2) { + fprintf(stderr, "usage: %s \n", argv[0]); + return 1; + } + + uint32_t palette256[256]; + for (int i = 0; i < 256; ++i) + palette256[i] = 0xFF000000 | (i << 16) | (i << 8) | i; + + uint32_t palette3[256] = {0}; + palette3[0] = 0xFF0000FF; + palette3[1] = 0xFF00FF00; + palette3[2] = 0xFFFF0000; + + uint32_t palette4[256] = {0}; + palette4[0] = 0xFF0000FF; + palette4[1] = 0xFF00FF00; + palette4[2] = 0xFFFF0000; + palette4[3] = 0xFF00FFFF; + + uint8_t *img = malloc(WIDTH * HEIGHT); + + if (strcmp(argv[1], "stripe") == 0) { + for (int i = 0; i < WIDTH * HEIGHT; ++i) img[i] = (i % WIDTH) / 4 % 3; + encode("/dev/null", palette3, img); + } else if (strcmp(argv[1], "gradient") == 0) { + for (int i = 0; i < WIDTH * HEIGHT; ++i) img[i] = (i % WIDTH) * 256 / WIDTH; + encode("/dev/null", palette256, img); + } else if (strcmp(argv[1], "noise") == 0) { + seed = 42; + for (int i = 0; i < WIDTH * HEIGHT; ++i) img[i] = psdrand() % 256; + encode("/dev/null", palette256, img); + } else if (strcmp(argv[1], "solid") == 0) { + memset(img, 0, WIDTH * HEIGHT); + encode("/dev/null", palette3, img); + } else if (strcmp(argv[1], "checker") == 0) { + for (int i = 0; i < WIDTH * HEIGHT; ++i) img[i] = (((i % WIDTH) + (i / WIDTH)) & 1); + encode("/dev/null", palette3, img); + } else if (strcmp(argv[1], "dither") == 0) { + for (int i = 0; i < WIDTH * HEIGHT; ++i) img[i] = ((i % WIDTH) + (i / WIDTH) * 7) % 256; + encode("/dev/null", palette256, img); + } else if (strcmp(argv[1], "fewnoise") == 0) { + seed = 42; + for (int i = 0; i < WIDTH * HEIGHT; ++i) img[i] = psdrand() % 4; + encode("/dev/null", palette4, img); + } else { + fprintf(stderr, "unknown pattern: %s\n", argv[1]); + free(img); + return 1; + } + + free(img); + return 0; +} diff --git a/bench/giflib_bench.c b/bench/giflib_bench.c new file mode 100644 index 0000000..e15c95d --- /dev/null +++ b/bench/giflib_bench.c @@ -0,0 +1,97 @@ +#include +#include +#include +#include +#include + +#define WIDTH 8192 +#define HEIGHT 8192 + +static uint64_t seed = 42; + +__attribute__((no_sanitize("integer"))) +static int psdrand(void) { + seed = 6364136223846793005ULL * seed + 1; + return seed >> 33; +} + +static void encode(const char *path, GifColorType *colors, int numColors, uint8_t *img) { + int error; + int mapSize = 1; + while (mapSize < numColors) mapSize *= 2; + + GifFileType *gif = EGifOpenFileName(path, false, &error); + ColorMapObject *cmap = GifMakeMapObject(mapSize, NULL); + memcpy(cmap->Colors, colors, numColors * sizeof(GifColorType)); + + EGifSetGifVersion(gif, true); + EGifPutScreenDesc(gif, WIDTH, HEIGHT, 8, 0, cmap); + EGifPutImageDesc(gif, 0, 0, WIDTH, HEIGHT, false, NULL); + + for (int y = 0; y < HEIGHT; ++y) + EGifPutLine(gif, img + y * WIDTH, WIDTH); + + GifFreeMapObject(cmap); + EGifCloseFile(gif, &error); +} + +int main(int argc, char **argv) { + if (argc != 2) { + fprintf(stderr, "usage: %s \n", argv[0]); + return 1; + } + + GifColorType palette256[256]; + for (int i = 0; i < 256; ++i) { + palette256[i].Red = i; palette256[i].Green = i; palette256[i].Blue = i; + } + + GifColorType palette3[3] = {{0xFF,0,0},{0,0xFF,0},{0,0,0xFF}}; + GifColorType palette4[4] = {{0xFF,0,0},{0,0xFF,0},{0,0,0xFF},{0xFF,0xFF,0}}; + uint8_t *img = malloc(WIDTH * HEIGHT); + + if (strcmp(argv[1], "stripe") == 0) { + for (int i = 0; i < WIDTH * HEIGHT; ++i) + img[i] = (i % WIDTH) / 4 % 3; + encode("/dev/null", palette3, 3, img); + + } else if (strcmp(argv[1], "gradient") == 0) { + for (int i = 0; i < WIDTH * HEIGHT; ++i) + img[i] = (i % WIDTH) * 256 / WIDTH; + encode("/dev/null", palette256, 256, img); + + } else if (strcmp(argv[1], "noise") == 0) { + seed = 42; + for (int i = 0; i < WIDTH * HEIGHT; ++i) + img[i] = psdrand() % 256; + encode("/dev/null", palette256, 256, img); + + } else if (strcmp(argv[1], "solid") == 0) { + memset(img, 0, WIDTH * HEIGHT); + encode("/dev/null", palette3, 3, img); + + } else if (strcmp(argv[1], "checker") == 0) { + for (int i = 0; i < WIDTH * HEIGHT; ++i) + img[i] = (((i % WIDTH) + (i / WIDTH)) & 1); + encode("/dev/null", palette3, 3, img); + + } else if (strcmp(argv[1], "dither") == 0) { + for (int i = 0; i < WIDTH * HEIGHT; ++i) + img[i] = ((i % WIDTH) + (i / WIDTH) * 7) % 256; + encode("/dev/null", palette256, 256, img); + + } else if (strcmp(argv[1], "fewnoise") == 0) { + seed = 42; + for (int i = 0; i < WIDTH * HEIGHT; ++i) + img[i] = psdrand() % 4; + encode("/dev/null", palette4, 4, img); + + } else { + fprintf(stderr, "unknown pattern: %s\n", argv[1]); + free(img); + return 1; + } + + free(img); + return 0; +} diff --git a/bench/run.sh b/bench/run.sh new file mode 100755 index 0000000..21459ca --- /dev/null +++ b/bench/run.sh @@ -0,0 +1,129 @@ +#!/bin/sh +set -e + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" +BUILD_DIR="$ROOT_DIR/builddir" +OUT_DIR="/tmp/cgif_bench_bin" +RESULTS_FILE="/tmp/cgif_bench_results.txt" + +mkdir -p "$OUT_DIR" +> "$RESULTS_FILE" + +# Build cgif in release mode +meson setup "$BUILD_DIR" "$ROOT_DIR" --buildtype=release --wipe >/dev/null 2>&1 +meson compile -C "$BUILD_DIR" >/dev/null 2>&1 + +# Build benchmarks +echo "Building benchmarks..." +cc -O3 -o "$OUT_DIR/cgif_bench" "$SCRIPT_DIR/cgif_bench.c" \ + -I"$ROOT_DIR/inc" -L"$BUILD_DIR" -lcgif -Wl,-rpath,"$BUILD_DIR" + +cc -O3 -o "$OUT_DIR/giflib_bench" "$SCRIPT_DIR/giflib_bench.c" \ + -I/opt/homebrew/include -L/opt/homebrew/lib -lgif 2>/dev/null + +# giflib-turbo (optional) +GIFLIB_TURBO="" +if [ -d /tmp/giflib-turbo ]; then + cc -O3 -o "$OUT_DIR/giflib_turbo_bench" "$SCRIPT_DIR/giflib_bench.c" \ + -I/tmp/giflib-turbo /tmp/giflib-turbo/gif_lib.c 2>/dev/null + GIFLIB_TURBO=1 +fi + +# ffmpeg (optional) +FFMPEG="" +if pkg-config --exists libavcodec libavutil 2>/dev/null; then + cc -O3 -o "$OUT_DIR/ffmpeg_bench" "$SCRIPT_DIR/ffmpeg_bench.c" \ + $(pkg-config --cflags --libs libavcodec libavutil) 2>/dev/null + FFMPEG=1 +fi + +PATTERNS="solid gradient stripe checker dither noise fewnoise" +RUNS="${1:-5}" + +# Build list of encoders +ENCODERS="cgif giflib" +[ -n "$GIFLIB_TURBO" ] && ENCODERS="$ENCODERS giflib-turbo" +[ -n "$FFMPEG" ] && ENCODERS="$ENCODERS ffmpeg" + +echo "Running benchmarks (${RUNS} runs per test)..." +echo "" + +for pattern in $PATTERNS; do + ARGS="-n cgif '$OUT_DIR/cgif_bench $pattern'" + ARGS="$ARGS -n giflib '$OUT_DIR/giflib_bench $pattern'" + if [ -n "$GIFLIB_TURBO" ]; then + ARGS="$ARGS -n giflib-turbo '$OUT_DIR/giflib_turbo_bench $pattern 2>/dev/null'" + fi + if [ -n "$FFMPEG" ]; then + ARGS="$ARGS -n ffmpeg '$OUT_DIR/ffmpeg_bench $pattern'" + fi + + echo "=== $pattern ===" + EXPORT_JSON="$OUT_DIR/${pattern}.json" + eval DYLD_LIBRARY_PATH="$BUILD_DIR" hyperfine --warmup 2 --runs "$RUNS" \ + --export-json "$EXPORT_JSON" $ARGS + echo "" + + # Extract mean times from JSON + for encoder in $ENCODERS; do + mean=$(python3 -c " +import sys, json +data = json.load(open('$EXPORT_JSON')) +for r in data['results']: + if r['command'] == '${encoder}': + print(f\"{r['mean']*1000:.1f}\") + break +" 2>/dev/null || echo "n/a") + echo "$pattern $encoder $mean" >> "$RESULTS_FILE" + done +done + +# Print summary table +echo "" +echo "==============================" +echo " Summary (mean ms, 8192x8192)" +echo "==============================" +echo "" + +# Header +HEADER=$(printf "%-12s" "pattern") +for encoder in $ENCODERS; do + HEADER="$HEADER $(printf "%14s" "$encoder")" +done +HEADER="$HEADER $(printf "%12s" "vs giflib")" +[ -n "$FFMPEG" ] && HEADER="$HEADER $(printf "%12s" "vs ffmpeg")" +echo "$HEADER" +echo "$HEADER" | sed 's/./-/g' + +for pattern in $PATTERNS; do + ROW=$(printf "%-12s" "$pattern") + CGIF_MS="" + GIFLIB_MS="" + FFMPEG_MS="" + for encoder in $ENCODERS; do + ms=$(grep "^$pattern $encoder " "$RESULTS_FILE" | awk '{print $3}') + ROW="$ROW $(printf "%11s ms" "$ms")" + case $encoder in + cgif) CGIF_MS=$ms ;; + giflib) GIFLIB_MS=$ms ;; + ffmpeg) FFMPEG_MS=$ms ;; + esac + done + + # Compute speedup vs giflib + if [ -n "$CGIF_MS" ] && [ -n "$GIFLIB_MS" ]; then + speedup=$(python3 -c "print(f'{${GIFLIB_MS}/${CGIF_MS}:.2f}x')") + ROW="$ROW $(printf "%12s" "$speedup")" + fi + + # Compute speedup vs ffmpeg + if [ -n "$FFMPEG" ] && [ -n "$CGIF_MS" ] && [ -n "$FFMPEG_MS" ]; then + speedup=$(python3 -c "print(f'{${FFMPEG_MS}/${CGIF_MS}:.2f}x')") + ROW="$ROW $(printf "%12s" "$speedup")" + fi + + echo "$ROW" +done + +echo ""