GPU framebuffer / render-target readback for sokol applications.
  • Zig 88.7%
  • Shell 11.3%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Mikael Säker 2d54f126f1 deps: track sokol 261ddd4, which owns its CAMetalLayer
The fork stopped over-releasing the Metal layer, which crashed every quit
from a hidden (closed-to-background) window. Moved in reveal, dvui-sokol,
sokol-capture and sokey together: two sokol pins are two gfx.Image types.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-07 09:22:44 +02:00
scripts Migrate to Zig 0.17 2026-10-06 15:52:42 +02:00
src zig fmt (0.17) 2026-10-06 15:52:55 +02:00
.gitignore scripts: setup-tools, the one every repo shares 2026-09-22 21:06:15 +02:00
build.zig Initial commit: Metal-backend sg.Image readback + pure-Zig PNG encoder 2026-05-30 12:07:23 +02:00
build.zig.zon deps: track sokol 261ddd4, which owns its CAMetalLayer 2026-10-07 09:22:44 +02:00
LICENSE Add MIT license 2026-07-25 12:00:21 +02:00
README.md Migrate to Zig 0.17 2026-10-06 15:52:42 +02:00

sokol-capture

GPU framebuffer / render-target readback for sokol-gpu applications. Pure Zig, no dvui dependency — usable from raw-sokol engines and dvui-sokol apps alike.

Status

  • Backend: Metal (macOS) only. Reads MTLStorageModePrivate render targets via a blit to a shared MTLBuffer, then getBytes. Other backends return error.UnsupportedBackend; D3D11 / GL / WGPU paths are TODO.
  • Capture: synchronous one-shot — capturePixels(allocator, img) blocks on waitUntilCompleted. Async / per-frame video-style capture is planned, not implemented.
  • Encoding: built-in pure-Zig PNG encoder (RGBA8, filter 0, zlib via std.compress.flate).

Usage

const std = @import("std");
const sg = @import("sokol").gfx;
const capture = @import("sokol_capture");

// `img` is any sokol render-target sg.Image.
var pixels = try capture.capturePixels(allocator, img);
defer pixels.deinit();
// pixels.pixels is tightly packed RGBA8, top-to-bottom.

const png_bytes = try capture.encodePng(allocator, pixels);
defer allocator.free(png_bytes);
// Write png_bytes to disk with your own file API.

Or in one shot:

const png_bytes = try capture.capturePngBytes(allocator, img);
defer allocator.free(png_bytes);

The module is Io-agnostic — it returns bytes; callers handle file writes with whatever they already have (Zig std's std.Io, a runtime shim, etc).

Why a separate module?

  • A raw-sokol game engine needs screenshot capability without depending on dvui.
  • A dvui-sokol app (wk / cue / quote) gets the same primitive without duplicating Metal FFI in dvui-sokol.
  • Apple Silicon's private-storage render targets need a blit step; this module owns that pattern so consumers don't reinvent it.

Build

Requires Zig 0.17. Depends only on https://git.urverk.org/urverk/sokol-zig (same fork dvui-sokol pins, so the build graph dedupes when both are used).

zig build check   # compile-check
zig build test    # unit tests (PNG encoder)

Consumers add it as a path dep:

.sokol_capture = .{ .path = "../sokol-capture" },

then in build.zig:

const sokol_capture_dep = b.dependency("sokol_capture", .{
    .target = target,
    .optimize = optimize,
});
// add to your exe's imports:
.{ .name = "sokol_capture", .module = sokol_capture_dep.module("sokol_capture") },