A high-performance, hybrid CPU/GPU shader engine and video generator.
Aurora![]() |
Deep Sea![]() |
Fractal![]() |
Polar![]() |
Raymarch![]() |
Ripple![]() |
Starfield![]() |
Tzozen![]() |
Voronoi![]() |
Bubbles![]() |
Neon![]() |
Seascape![]() |
Eshi (Japanese for "painter" or "artist") is a minimal C++ framework that turns mathematical formulas into video. It allows you to write GLSL-style logic directly in C++, rendering procedural art to high-quality video files (.mp4) or a live window preview.
"Painting pixels with math." 🥴🍹💫 -me
This project is heavily inspired by and builds upon the foundational concepts of Tzozen's checker.c.
Eshi evolves this concept by embedding the encoding pipeline directly into the application, supporting multi-threaded CPU rendering (OpenMP), GPU acceleration (CUDA & OpenGL), and live previews (SDL2).
- Hybrid Rendering: Seamlessly switch between CPU (OpenMP), GPU (CUDA/OpenGL), and GPU (Metal) rendering engines.
- Live Preview: Tweak your shaders in real-time with an SDL2 window (
--live). - Zero-IO Video: Renders directly to H.264 (
.mp4) in memory using linked FFmpeg libraries. - Modern CLI: Beautiful terminal UI powered by Gum.
- C++ Shaders: A robust math library (
glsl_core.h) that emulates GLSL types (vec2,vec4) and intrinsics in standard C++. - Zig Shader Examples: Link Zig implementations into the CPU renderer while using portable GPU companions for accelerated rendering.
- Arm64 Support: Native compilation and hardware acceleration on Windows on Arm (Snapdragon) devices.
1. System Libraries:
# Core Dependencies
sudo apt install pkg-config libavcodec-dev libavformat-dev libavutil-dev libswscale-dev libomp-dev libsdl2-dev
# Install Gum (for the CLI dashboard)
sudo mkdir -p /etc/apt/keyrings
curl -fsSL [https://repo.charm.sh/apt/gpg.key](https://repo.charm.sh/apt/gpg.key) | sudo gpg --dearmor -o /etc/apt/keyrings/charm.gpg
echo "deb [signed-by=/etc/apt/keyrings/charm.gpg] [https://repo.charm.sh/apt/](https://repo.charm.sh/apt/) * *" | sudo tee /etc/apt/sources.list.d/charm.list
sudo apt update && sudo apt install gum2. Fetch libsumi:
./vendor.sh
# or: make vendorIf you are actively developing libsumi in a sibling checkout, skip vendoring and pass it directly:
make SUMI_PATH=../libsumi -j$(nproc)By default, the Makefile uses ./libsumi when it exists and falls back to ../libsumi for local libsumi development.
3. Compile:
make -j$(nproc)(Note: If nvcc is found in your PATH, Eshi automatically compiles with CUDA support.)
Troubleshooting CUDA Paths:
If the build fails with /usr/bin/ld: cannot find -lcudart, your CUDA installation may be in a non-standard location (common on Arch/CachyOS).
You can fix this by editing the CUDA_PATH_LINUX variable at the top of the Makefile, or by passing it directly to make:
# Example for Arch Linux / CachyOS
make CUDA_PATH_LINUX=/opt/cuda -j$(nproc)1. System Libraries:
brew install pkg-config ffmpeg sdl2 sdl2_ttf libomp zlib bzip2 freetype2. Fetch libsumi:
./vendor.sh
# or: make vendor3. Compile:
make -j$(sysctl -n hw.ncpu)The Makefile automatically prepends Homebrew pkg-config paths for keg-only and macOS shim dependencies such as zlib and bzip2.
The Makefile uses zig c++ by default. (zig cc can infer C++ from a .cpp
input, but it does not select the C++ runtime when it performs the final link.)
You can still select another C++ compiler explicitly:
make CXX=clang++The native Zig build graph produces the same main program and examples under
zig-out/bin:
# Everything, optimized with ReleaseFast by default
zig build
# Only one artifact
zig build eshi
zig build warp
zig build harlequin
# Useful overrides
zig build eshi -Doptimize=Debug -Dopenmp=false
zig build eshi -Dsumi-path=../libsumi
# Build the OpenGL renderer path (off by default)
zig build examples -Dopengl=trueGlow Pong is the first vertical slice of Larimar's data-oriented engine: the
game owns only its scene, components, systems, and material while the SDL host
and renderer remain interchangeable. Its Ink, Paper, and direct-Metal paths are
built by zig build larimar.
To run the same ECS-driven game through Google Filament, first install the vendored Filament distribution (one-time, incremental afterward), then enable the optional backend:
cd resources/filament
./build.sh -i release filament matc
cd ../..
zig build larimar -Dfilament=true
./zig-out/bin/pong --grade brush --liveThe build compiles examples/pong/pong.mat with Filament's matc, installs the
package at zig-out/share/eshi/pong.filamat, and maps Kantei Brush to Filament.
The public C API does not expose Filament types. To consume an already-installed
distribution instead, pass -Dfilament-path=/path/to/filament; use
-Dfilament-arch=... when its library directory is not the inferred arm64 or
x86_64.
Pong's scene is a description submitted through the engine's command buffer,
not a sequence of eshi_entity_create() calls. A reconciler diffs it against
the live world by stable key, so re-submitting it is a no-op — which is what
makes Flutter's hot reload safe later, since it re-runs build() without
unwinding native state.
--reload N re-submits that description every N frames. Retained mode means the
digest must not move:
./zig-out/bin/pong --grade ink --res 320x180 --frames 400 --seed 42 --hash
./zig-out/bin/pong --grade ink --res 320x180 --frames 400 --seed 42 --hash --reload 1Both print digest=b9321cc6ecbe3e26 … entities=5 nodes=5, the second having
re-described the whole scene 399 times along the way. Built imperatively, those
same reloads would have left 2000 entities behind.
-Dopengl builds the OpenGL backend in renderer_gl.h, which needs the
system GL library (-framework OpenGL on macOS, libGL elsewhere). It is off
by default for two reasons: on macOS it takes precedence over Metal, because
main.cpp tries CUDA, then OpenGL, then Metal; and elsewhere it would add a
libGL requirement to builds that are content on the CPU path.
Prefer enabling it when changing anything shared with the GPU backends — especially the shader transpiler, whose rules differ per target and are easy to fix on one backend while breaking the other.
To check that every gallery shader actually reaches the GPU backend rather than falling back to the CPU:
scripts/check_gpu_shaders.shIt runs each binary with --validate, which initializes the renderer, reports
which backend came up, and exits non-zero if a requested GPU backend fell
through. Pass a directory to check a non-default build, e.g.
scripts/check_gpu_shaders.sh build-gl/bin. CI runs this on Linux under Xvfb
with Mesa's llvmpipe; see .github/workflows/ci.yml.
Run the shaders one at a time. Concurrent runs contend over GL contexts badly enough that unrelated shaders report empty output and look like compile failures — the script is sequential on purpose.
Both build paths use pkg-config for SDL2 and FFmpeg. On macOS, the Zig build
enables Metal and uses Homebrew's libomp; override a nonstandard installation
with -Dlibomp-prefix=/path/to/libomp.
Outside macOS the Zig build currently exercises the CPU/OpenMP path; CUDA
auto-detection remains in the Makefile path.
Shader examples may implement the CPU entry point in Zig. Because GPU drivers
cannot execute that host object directly, those examples provide a matching
portable GPU companion at examples/gpu/<name>.cpp. Eshi selects the companion
automatically for --gpu; the same source is consumed by Metal, OpenGL, and
CUDA backends. The --gpu flag does not generate or emit this .cpp file: the
companion is checked into the repository as shader source for GPU compilers.
For example, harlequin links examples/harlequin.zig for CPU rendering and
loads examples/gpu/harlequin.cpp when --gpu selects a GPU backend:
make build/harlequin
./build/harlequin --live --gpuZig 0.16 cannot use LTO for Mach-O: Zig requires LLD for LTO, while its LLD
backend cannot link Mach-O. Consequently, both Zig-based paths disable LTO on
macOS. -fuse-ld=lld does not work around this. On other platforms LTO remains
enabled by default; disable it with make USE_LTO=0 or zig build -Dlto=false.
For isolated build/runtime benchmarks, use different prefixes so neither path can reuse or overwrite the other's artifacts:
make BUILD_DIR=build-make -j$(sysctl -n hw.ncpu)
zig build -p build-zig
# Example with hyperfine installed; benchmark the same renderer and arguments.
hyperfine './build-make/warp --res 320x180' './build-zig/bin/warp --res 320x180'For cold build-time measurements, also give each command a different
ZIG_GLOBAL_CACHE_DIR; separate output directories do not separate Zig's
compiler cache.
To remove Make outputs and Zig's default local artifacts (.zig-cache and
zig-out), run:
make cleanIf you supplied custom BUILD_DIR, ZIG_CACHE_DIR, or ZIG_OUT_DIR values,
pass the same values to make clean.
1. Prerequisites:
- Visual Studio (MSVC C++ Compiler)
- CUDA Toolkit (Must be installed and
nvccreachable) - vcpkg package manager
2. Install Dependencies: Open a terminal in the project root and run:
vcpkg install --triplet x64-windows3. Configure & Compile:
- Open
build_all.batin a text editor. - Update
VCPKG_ROOTandCUDA_PATHvariables. - Run
build_all.bat.
1. Prerequisites:
- Visual Studio 2022 (Ensure "ARM64 build tools" are installed)
- vcpkg package manager
- OpenCL™ and OpenGL® Compatibility Pack (Install from Microsoft Store to enable Adreno GPU support)
2. Install Dependencies: Open a terminal in the project root and run:
vcpkg install --triplet arm64-windows3. Configure & Compile:
- Open the ARM64 Native Tools Command Prompt for VS 2022.
- Open
build.arm64.batand updateVCPKG_ROOT. - Run the build script:
build.arm64.bat
- Executables will be generated in
build/.
The easiest way to run demos is with the beautified shell script (Linux/WSL only currently):
./run_demos.shThis launches a GUI-like menu in your terminal to select CPU/GPU modes and shows progress bars for renders.
You can also run the built binaries directly from the build/ folder:
Render to Video:
# Linux
./build/deepsea
# Windows
.\build\deepsea.exe
# Output: deepsea.mp4 (in build/)Live Preview:
./build/deepsea --live
# Opens a window. Press ESC to close.Options:
--gpu: Use hardware acceleration (CUDA on x64, OpenGL on Arm64).--live: Render to window instead of file.--res WxH: Set resolution (e.g.,--res 1920x1080). Default is 960x540.--encoder auto|hw|sw: Video encoder. Defaultauto.--hevc: Encode HEVC instead of H.264.--validate: Initialize the renderer, report the backend, and exit.
On Apple Silicon, --encoder auto (the default) routes H.264 and HEVC through
VideoToolbox and the dedicated media engine instead of libx264. That is roughly
2.4× faster end to end at 1080p and leaves the CPU cores for rendering —
which matters most on the CPU tier, where the renderer wants all of them.
Measured against the raw rendered frames rather than against each other, both encoders are faithful: 46.3 dB PSNR for libx264, 45.6 dB for VideoToolbox. The ~0.7 dB is the usual hardware-encoder trade at equal bitrate, and both sit well above the ~40 dB that reads as visually identical.
hw demands hardware and fails if it is unavailable; sw forces libx264.
Use sw when output needs to be comparable across machines — hardware encoders
make no bit-reproducibility guarantee across silicon or driver revisions.
auto falls back to software if VideoToolbox is missing or refuses a session,
which it can do under virtualization.
Define your art in shader.cpp using the mainImage function (Shadertoy style):
void mainImage(vec4 &fragColor, vec2 fragCoord, vec2 iResolution, float iTime) {
vec2 uv = fragCoord / iResolution.y;
// ... your GLSL logic here ...
fragColor = vec4(uv.x, uv.y, 0.5f + 0.5f*sinf(iTime), 1.0f);
}Rebuild to update the binary.
This project retains the MIT License of the original code.
З.Ы. Если вы дочитали до этого места, буду благодарен за звезду!
🥂 За здоровье!












