#!/usr/bin/env dub
/+ dub.sdl:
name "manim_frame_capture"
targetPath "build"
+/
/**
* The native output pipeline in miniature: rasterise a frame into an RGBA
* buffer, read the pixels back, and reduce them to a checksum — the exact
* shape of the "render → framebuffer readback → encode" path every native
* Manim-class engine runs, minus the GPU and the codec.
*
* The *output & encoding* axis of the analysis spine. A real backend fills
* this buffer for you: Cairo hands back `cairo_image_surface_get_data`,
* raylib exposes it through `TakeScreenshot` / an offscreen `RenderTexture`
* (already exercised in this repo's `apps/terminal/src/app.d`), and the raw
* RGBA bytes are then piped to ffmpeg's stdin (`-f rawvideo -pix_fmt rgba`,
* ManimGL's exact command) or handed to libav. This probe stands in a pure
* software rasteriser so it compiles and runs with zero dependencies and no
* display, while grounding the claim that a frame is *just an addressable
* RGBA buffer* an encoder consumes — the interface the proposal's renderer
* `readback()` capability returns.
*
* It rasterises a background clear plus a filled disc (analytic coverage AA
* on the boundary — the same anti-aliasing concern §axis 3 raises), reads the
* buffer back, counts non-background pixels, and prints a deterministic
* FNV-1a checksum. Determinism of this checksum across runs is precisely what
* makes per-`play()` content-hash caching (§axis 8) correct.
*
* Companion to docs/research/manim/rendering-backends/gpu-vector.md
* § "Framebuffer readback" and docs/research/manim/video-encoding.md
* § "The raw-RGBA pipe".
* Run with: dub run --single frame-capture.d
*
* Portability: pure software rasterisation, no GPU / display / external
* dependency — deterministic on every host (unlike a live raylib window,
* which would need a display and is covered by apps/terminal instead).
*/
module (module) manim_frame_captureThe native output pipeline in miniature: rasterise a frame into an RGBA
buffer, read the pixels back, and reduce them to a checksum — the exact
shape of the "render → framebuffer readback → encode" path every native
Manim-class engine runs, minus the GPU and the codec.
The output & encoding axis of the analysis spine. A real backend fills
this buffer for you: Cairo hands back cairo_image_surface_get_data,
raylib exposes it through TakeScreenshot / an offscreen RenderTexture
(already exercised in this repo's apps/terminal/src/app.d), and the raw
RGBA bytes are then piped to ffmpeg's stdin (-f rawvideo -pix_fmt rgba,
ManimGL's exact command) or handed to libav. This probe stands in a pure
software rasteriser so it compiles and runs with zero dependencies and no
display, while grounding the claim that a frame is just an addressable
RGBA buffer an encoder consumes — the interface the proposal's renderer
readback() capability returns.
It rasterises a background clear plus a filled disc (analytic coverage AA
on the boundary — the same anti-aliasing concern §axis 3 raises), reads the
buffer back, counts non-background pixels, and prints a deterministic
FNV-1a checksum. Determinism of this checksum across runs is precisely what
makes per-play() content-hash caching (§axis 8) correct.
Companion to docs/research/manim/rendering-backends/gpu-vector.md
§ "Framebuffer readback" and docs/research/manim/video-encoding.md
§ "The raw-RGBA pipe".
Run with: dub run --single frame-capture.d
Portability
pure software rasterisation, no GPU / display / external
dependency — deterministic on every host (unlike a live raylib window,
which would need a display and is covered by apps/terminal instead).
manim_frame_capture;
import (package) stdstd.(module) std.mathContains the elementary mathematical functions (powers, roots,
and trigonometric functions), and low-level floating-point operations.
Mathematical special functions are available in std.mathspecial.
Category Members Constants E PI PI_2 PI4 M1_PI M2_PI M2_SQRTPI LN10 LN2 LOG2 LOG2E LOG2T LOG10E SQRT2 SQRT1_2 Algebraic abs fabs sqrt cbrt hypot poly nextPow2 truncPow2 Trigonometry sin cos tan asin acos atan atan2 sinh cosh tanh asinh acosh atanh Rounding ceil floor round lround trunc rint lrint nearbyint rndtol quantize Exponentiation & Logarithms pow powmod exp exp2 expm1 ldexp frexp log log2 log10 logb ilogb log1p scalbn Remainder fmod modf remainder remquo Floating-point operations approxEqual feqrel fdim fmax fmin fma isClose nextDown nextUp nextafter NaN getNaNPayload cmp Introspection isFinite isIdentical isInfinity isNaN isNormal isSubnormal signbit sgn copysign isPowerOf2 Hardware Control IeeeFlags ieeeFlags resetIeeeFlags FloatingPointControl
The functionality closely follows the IEEE754-2008 standard for
floating-point arithmetic, including the use of camelCase names rather
than C99-style lower case names. All of these functions behave correctly
when presented with an infinity or NaN.
The following IEEE 'real' formats are currently supported:
64 bit Big-endian 'double' (eg PowerPC)
128 bit Big-endian 'quadruple' (eg SPARC)
64 bit Little-endian 'double' (eg x86-SSE2)
80 bit Little-endian, with implied bit 'real80' (eg x87, Itanium)
128 bit Little-endian 'quadruple' (not implemented on any known processor!)
Non-IEEE 128 bit Big-endian 'doubledouble' (eg PowerPC) has partial support
Unlike C, there is no global 'errno' variable. Consequently, almost all of
these functions are pure nothrow.
Source
std/math/package.d
math : (alias) manim_frame_capture.sqrt = float std.math.algebraic.sqrt(float x) pure nothrow @nogc @safeCompute square root of x.
x sqrt(x) invalid? -0.0 -0.0 no <0.0 yes + + no
sqrt;
import (package) stdstd.(module) std.stdioCategory Symbols File handles _popen File isFileHandle openNetwork stderr stdin stdout Reading chunks lines readf readfln readln Writing toFile write writef writefln writeln Misc KeepTerminator LockType StdioException
Standard I/O functions that extend core.stdc.stdio. core.stdc.stdio
is publically imported when importing std.stdio.
There are three layers of I/O:
The lowest layer is the operating system layer. The two main schemes are Windows and Posix.
C's stdio.h which unifies the two operating system schemes.
std.stdio, this module, unifies the various stdio.h implementations into
a high level package for D programs.
Source
std/stdio.d
stdio : (alias template) manim_frame_capture.writefln = std.stdio.writefln(alias fmt, A...)(A args) if (isSomeString!(typeof(fmt)))Equivalent to writef(fmt, args, '\n').
writefln, (alias template) manim_frame_capture.writeln = std.stdio.writeln(T...)(T args)Equivalent to write(args, '\n'). Calling writeln without
arguments is valid and just prints a newline to the standard
output.
Example
Reads stdin and writes it to stdout with an argument
counter.
import std.stdio;
void main()
{
string line;
for (size_t count = 0; (line = readln) !is null; count++)
{
writeln("Input ", count, ": ", line);
}
}
writeln;
enum (constant) int manim_frame_capture.W = 64W = 64, (constant) int manim_frame_capture.H = 48H = 48;
struct (struct) manim_frame_capture.FramebufferFramebuffer
{
ubyte[(constant) int manim_frame_capture.W = 64W * (constant) int manim_frame_capture.H = 48H * 4] (field) ubyte[12288] manim_frame_capture.Framebuffer.pxpx; // RGBA8, row-major
void void manim_frame_capture.Framebuffer.clear(ubyte r, ubyte g, ubyte b, ubyte a) pure nothrow @nogc @safeclear(ubyte (parameter) ubyte rr, ubyte (parameter) ubyte gg, ubyte (parameter) ubyte bb, ubyte (parameter) ubyte aa) @safe pure nothrow @nogc
{
foreach ((local variable) int ii; 0 .. (constant) int manim_frame_capture.W = 64W * (constant) int manim_frame_capture.H = 48H)
{
(field) ubyte[12288] manim_frame_capture.Framebuffer.pxpx[(local variable) int ii * 4 + 0] = (parameter) ubyte rr;
(field) ubyte[12288] manim_frame_capture.Framebuffer.pxpx[(local variable) int ii * 4 + 1] = (parameter) ubyte gg;
(field) ubyte[12288] manim_frame_capture.Framebuffer.pxpx[(local variable) int ii * 4 + 2] = (parameter) ubyte bb;
(field) ubyte[12288] manim_frame_capture.Framebuffer.pxpx[(local variable) int ii * 4 + 3] = (parameter) ubyte aa;
}
}
/// Alpha-composite a color over pixel (x,y) with coverage in [0,1].
void void manim_frame_capture.Framebuffer.blend(int x, int y, ubyte r, ubyte g, ubyte b, double cov) pure nothrow @nogc @safeAlpha-composite a color over pixel (x,y) with coverage in 0,1.
blend(int (parameter) int xx, int (parameter) int yy, ubyte (parameter) ubyte rr, ubyte (parameter) ubyte gg, ubyte (parameter) ubyte bb, double (parameter) double covcov) @safe pure nothrow @nogc
{
if ((parameter) int xx < 0 || (parameter) int xx >= (constant) int manim_frame_capture.W = 64W || (parameter) int yy < 0 || (parameter) int yy >= (constant) int manim_frame_capture.H = 48H || (parameter) double covcov <= 0)
return;
const (local variable) const(int) ii = ((parameter) int yy * (constant) int manim_frame_capture.W = 64W + (parameter) int xx) * 4;
void void manim_frame_capture.Framebuffer.blend.over(ulong k, ubyte src) pure nothrow @nogc @safeover((alias) object.size_t = ulongsize_t (parameter) ulong kk, ubyte (parameter) ubyte srcsrc)
{
(field) ubyte[12288] manim_frame_capture.Framebuffer.pxpx[(local variable) const(int) ii + (parameter) ulong kk] = cast(ubyte)((parameter) ubyte srcsrc * (parameter) double covcov + (field) ubyte[12288] manim_frame_capture.Framebuffer.pxpx[(local variable) const(int) ii + (parameter) ulong kk] * (1 - (parameter) double covcov) + 0.5);
}
void manim_frame_capture.Framebuffer.blend.over(ulong k, ubyte src) pure nothrow @nogc @safeover(0, (parameter) ubyte rr);
void manim_frame_capture.Framebuffer.blend.over(ulong k, ubyte src) pure nothrow @nogc @safeover(1, (parameter) ubyte gg);
void manim_frame_capture.Framebuffer.blend.over(ulong k, ubyte src) pure nothrow @nogc @safeover(2, (parameter) ubyte bb);
}
/// Filled disc with 1px analytic-coverage anti-aliased edge.
void void manim_frame_capture.Framebuffer.disc(double cx, double cy, double rad, ubyte r, ubyte g, ubyte b) pure nothrow @nogc @safeFilled disc with 1px analytic-coverage anti-aliased edge.
disc(double (parameter) double cxcx, double (parameter) double cycy, double (parameter) double radrad, ubyte (parameter) ubyte rr, ubyte (parameter) ubyte gg, ubyte (parameter) ubyte bb) @safe pure nothrow @nogc
{
foreach ((local variable) int yy; 0 .. (constant) int manim_frame_capture.H = 48H)
foreach ((local variable) int xx; 0 .. (constant) int manim_frame_capture.W = 64W)
{
const (local variable) const(double) dd = double std.math.algebraic.sqrt(double x) pure nothrow @nogc @safeCompute square root of x.
x sqrt(x) invalid? -0.0 -0.0 no <0.0 yes + + no
sqrt(((local variable) int xx + 0.5 - (parameter) double cxcx) ^^ 2 + ((local variable) int yy + 0.5 - (parameter) double cycy) ^^ 2);
const (local variable) const(double) covcov = (local variable) const(double) dd <= (parameter) double radrad - 0.5 ? 1.0 : ((local variable) const(double) dd >= (parameter) double radrad + 0.5 ? 0.0 : (parameter) double radrad + 0.5 - (local variable) const(double) dd);
void manim_frame_capture.Framebuffer.blend(int x, int y, ubyte r, ubyte g, ubyte b, double cov) pure nothrow @nogc @safeAlpha-composite a color over pixel (x,y) with coverage in 0,1.
blend((local variable) int xx, (local variable) int yy, (parameter) ubyte rr, (parameter) ubyte gg, (parameter) ubyte bb, (local variable) const(double) covcov);
}
}
}
/// FNV-1a over the whole readback buffer — a stand-in for a content hash.
ulong ulong manim_frame_capture.fnv1a(in ubyte[] bytes) pure nothrow @nogc @safeFNV-1a over the whole readback buffer — a stand-in for a content hash.
fnv1a(in ubyte[] (parameter) const(ubyte[]) bytesbytes) @safe pure nothrow @nogc
{
ulong (local variable) ulong hh = 0xcbf29ce484222325;
foreach ((parameter) const(ubyte) bb; (parameter) const(ubyte[]) bytesbytes)
{
(local variable) ulong hh ^= (local variable) const(ubyte) bb;
(local variable) ulong hh *= 0x100000001b3;
}
return (local variable) ulong hh;
}
int int D main() @safemain() @safe
{
(struct) manim_frame_capture.FramebufferFramebuffer (local variable) manim_frame_capture.Framebuffer fbfb;
(local variable) manim_frame_capture.Framebuffer fbfb.void manim_frame_capture.Framebuffer.clear(ubyte r, ubyte g, ubyte b, ubyte a) pure nothrow @nogc @safeclear(16, 16, 24, 255); // dark background
(local variable) manim_frame_capture.Framebuffer fbfb.void manim_frame_capture.Framebuffer.disc(double cx, double cy, double rad, ubyte r, ubyte g, ubyte b) pure nothrow @nogc @safeFilled disc with 1px analytic-coverage anti-aliased edge.
disc((constant) int manim_frame_capture.W = 64W / 2.0, (constant) int manim_frame_capture.H = 48H / 2.0, 16, 240, 120, 40); // one filled, AA-edged disc
// "Read back" the buffer — exactly what an encoder is handed.
const (local variable) const(ubyte[]) bytesbytes = (local variable) manim_frame_capture.Framebuffer fbfb.(field) ubyte[12288] manim_frame_capture.Framebuffer.pxpx[];
(alias) object.size_t = ulongsize_t (local variable) ulong nonBgnonBg;
foreach ((local variable) int ii; 0 .. (constant) int manim_frame_capture.W = 64W * (constant) int manim_frame_capture.H = 48H)
if (!((local variable) const(ubyte[]) bytesbytes[(local variable) int ii * 4] == 16 && (local variable) const(ubyte[]) bytesbytes[(local variable) int ii * 4 + 1] == 16 && (local variable) const(ubyte[]) bytesbytes[(local variable) int ii * 4 + 2] == 24))
(local variable) ulong nonBgnonBg++;
void std.stdio.writeln!string(string __param_0) @safeEquivalent to write(args, '\n'). Calling writeln without
arguments is valid and just prints a newline to the standard
output.
Example
Reads stdin and writes it to stdout with an argument
counter.
import std.stdio;
void main()
{
string line;
for (size_t count = 0; (line = readln) !is null; count++)
{
writeln("Input ", count, ": ", line);
}
}
writeln("== software frame capture (stand-in for Cairo/raylib readback) ==");
void std.stdio.writefln!(char, int, int, ulong)(in char[] fmt, int __param_1, int __param_2, ulong __param_3) @safeEquivalent to writef(fmt, args, '\n').
writefln(" frame : %d x %d RGBA8 (%d bytes)", (constant) int manim_frame_capture.W = 64W, (constant) int manim_frame_capture.H = 48H, (local variable) const(ubyte[]) bytesbytes.(field) ulong const(ubyte[]).lengthlength);
void std.stdio.writefln!(char, ulong, int)(in char[] fmt, ulong __param_1, int __param_2) @safeEquivalent to writef(fmt, args, '\n').
writefln(" drawn pixels : %d / %d (disc + AA edge over background)", (local variable) ulong nonBgnonBg, (constant) int manim_frame_capture.W = 64W * (constant) int manim_frame_capture.H = 48H);
void std.stdio.writefln!(char, ulong)(in char[] fmt, ulong __param_1) @safeEquivalent to writef(fmt, args, '\n').
writefln(" readback checksum: 0x%016x (FNV-1a — deterministic across runs)", ulong manim_frame_capture.fnv1a(in ubyte[] bytes) pure nothrow @nogc @safeFNV-1a over the whole readback buffer — a stand-in for a content hash.
fnv1a((local variable) const(ubyte[]) bytesbytes));
void std.stdio.writeln!string(string __param_0) @safeEquivalent to write(args, '\n'). Calling writeln without
arguments is valid and just prints a newline to the standard
output.
Example
Reads stdin and writes it to stdout with an argument
counter.
import std.stdio;
void main()
{
string line;
for (size_t count = 0; (line = readln) !is null; count++)
{
writeln("Input ", count, ": ", line);
}
}
writeln(" → these raw RGBA bytes are the input to `ffmpeg -f rawvideo -pix_fmt rgba`");
void std.stdio.writeln!string(string __param_0) @safeEquivalent to write(args, '\n'). Calling writeln without
arguments is valid and just prints a newline to the standard
output.
Example
Reads stdin and writes it to stdout with an argument
counter.
import std.stdio;
void main()
{
string line;
for (size_t count = 0; (line = readln) !is null; count++)
{
writeln("Input ", count, ": ", line);
}
}
writeln(" or libav; the stable checksum is what makes play()-level caching correct.");
return 0;
}