diff --git a/build.sh b/build.sh index 492c033686..b139dc51b1 100755 --- a/build.sh +++ b/build.sh @@ -10,18 +10,21 @@ set -e # ./build.sh breakout --fast # Standalone executable (optimized) # ./build.sh breakout --web # Emscripten web build # ./build.sh breakout --profile # Kernel profiling binary +# ./build.sh breakout --headless # Use raylib 6.0 PLATFORM_MEMORY software renderer # ./build.sh all # Build all envs with default and --float if [ -z "$1" ]; then - echo "Usage: ./build.sh ENV_NAME [--float] [--debug] [--local|--fast|--web|--profile|--cpu|--all]" + echo "Usage: ./build.sh ENV_NAME [--float] [--headless] [--debug] [--local|--fast|--web|--profile|--cpu|--all]" exit 1 fi ENV=$1 shift +HEADLESS=0 for arg in "$@"; do case $arg in --float) PRECISION="-DPRECISION_FLOAT" ;; + --headless) HEADLESS=1 ;; --debug) DEBUG=1 ;; --local) MODE=local ;; --fast) MODE=fast ;; @@ -50,22 +53,60 @@ if [ "$ENV" = "all" ]; then exit 0 fi +RAYLIB_VERSION="6.0" +RAYLIB_RELEASE_PATH="6.0" + +has_lib() { + printf 'int main(void) { return 0; }\n' | ${CC:-cc} -x c - -o /tmp/pufferlib_libcheck "$1" >/dev/null 2>&1 +} + +append_if_lib_exists() { + local lib=$1 + if has_lib "$lib"; then + RAYLIB_DESKTOP_LINUX_LIBS+=("$lib") + fi +} + # Linux/mac PLATFORM="$(uname -s)" if [ "$PLATFORM" = "Linux" ]; then - RAYLIB_NAME='raylib-5.5_linux_amd64' + RAYLIB_NAME="raylib-${RAYLIB_VERSION}_linux_amd64" OMP_LIB=-lomp5 SANITIZE_FLAGS=(-fsanitize=address,undefined,bounds,pointer-overflow,leak -fno-omit-frame-pointer) - STANDALONE_LDFLAGS=(-lGL) - SHARED_LDFLAGS=(-Bsymbolic-functions) + RAYLIB_DESKTOP_LINUX_LIBS=(-ldl) + append_if_lib_exists -lGL + append_if_lib_exists -lX11 + append_if_lib_exists -lXrandr + append_if_lib_exists -lXinerama + append_if_lib_exists -lXi + append_if_lib_exists -lXcursor + STANDALONE_LDFLAGS=("${RAYLIB_DESKTOP_LINUX_LIBS[@]}") + SHARED_LDFLAGS=(-Bsymbolic-functions "${RAYLIB_DESKTOP_LINUX_LIBS[@]}") else - RAYLIB_NAME='raylib-5.5_macos' + RAYLIB_NAME="raylib-${RAYLIB_VERSION}_macos" OMP_LIB=-lomp SANITIZE_FLAGS=() STANDALONE_LDFLAGS=(-framework Cocoa -framework IOKit -framework CoreVideo -framework OpenGL) SHARED_LDFLAGS=(-framework Cocoa -framework OpenGL -framework IOKit -undefined dynamic_lookup) fi +RAYLIB_PLATFORM="-DPLATFORM_DESKTOP" +RAYLIB_LINK_LDFLAGS=("${STANDALONE_LDFLAGS[@]}") +if [ "$HEADLESS" = "1" ]; then + if [ "$MODE" = "web" ]; then + echo "Error: --headless is not compatible with --web" + exit 1 + fi + RAYLIB_NAME="raylib-${RAYLIB_VERSION}_memory" + RAYLIB_PLATFORM="-DPLATFORM_MEMORY" + RAYLIB_LINK_LDFLAGS=() + if [ "$PLATFORM" = "Linux" ]; then + SHARED_LDFLAGS=(-Bsymbolic-functions) + else + SHARED_LDFLAGS=(-undefined dynamic_lookup) + fi +fi + CLANG_WARN=( -Wall -ferror-limit=3 @@ -81,21 +122,60 @@ download() { [ -d "$name" ] && return echo "Downloading $name..." case "$url" in - *.zip) curl -sL "$url" -o "$name.zip" && unzip -q "$name.zip" && rm "$name.zip" ;; - *) curl -sL "$url" -o "$name.tar.gz" && tar xf "$name.tar.gz" && rm "$name.tar.gz" ;; + *.zip) curl -fsL "$url" -o "$name.zip" && unzip -q "$name.zip" && rm "$name.zip" ;; + *) curl -fsL "$url" -o "$name.tar.gz" && tar xf "$name.tar.gz" && rm "$name.tar.gz" ;; esac } -RAYLIB_URL="https://github.com/raysan5/raylib/releases/download/5.5" -if [ "$MODE" = "web" ]; then - RAYLIB_NAME='raylib-5.5_webassembly' - download "$RAYLIB_NAME" "$RAYLIB_URL/$RAYLIB_NAME.zip" +build_raylib_from_source() { + local name=$1 platform=$2 + if [ ! -d "$name" ]; then + echo "Downloading raylib ${RAYLIB_VERSION} source for $platform..." + curl -fsL "$RAYLIB_SOURCE_URL" -o "$name.tar.gz" + tar xf "$name.tar.gz" + rm "$name.tar.gz" + mv "raylib-${RAYLIB_RELEASE_PATH}" "$name" + fi + if [ ! -f "$name/src/libraylib.a" ] || [ ! -f "$name/src/.pufferlib_pic" ]; then + echo "Building raylib ${RAYLIB_VERSION} $platform..." + make -C "$name/src" clean >/dev/null 2>&1 || true + local custom_cflags="-fPIC" + if [ "$platform" = "PLATFORM_MEMORY" ]; then + custom_cflags="$custom_cflags -Wno-unused-label -Wno-unused-variable" + fi + make -C "$name/src" PLATFORM="$platform" RAYLIB_BUILD_MODE=RELEASE CUSTOM_CFLAGS="$custom_cflags" + touch "$name/src/.pufferlib_pic" + fi +} + +RAYLIB_URL="https://github.com/raysan5/raylib/releases/download/${RAYLIB_RELEASE_PATH}" +RAYLIB_SOURCE_URL="https://github.com/raysan5/raylib/archive/refs/tags/${RAYLIB_RELEASE_PATH}.tar.gz" +if [ "$HEADLESS" = "1" ]; then + build_raylib_from_source "$RAYLIB_NAME" PLATFORM_MEMORY + RAYLIB_A="$RAYLIB_NAME/src/libraylib.a" + INCLUDES=(-I./$RAYLIB_NAME/src -I./$RAYLIB_NAME/src/external -I./src -I./vendor) +elif [ "$MODE" = "web" ]; then + RAYLIB_NAME="raylib-${RAYLIB_VERSION}_webassembly" + if download "$RAYLIB_NAME" "$RAYLIB_URL/$RAYLIB_NAME.zip"; then + RAYLIB_A="$RAYLIB_NAME/lib/libraylib.a" + INCLUDES=(-I./$RAYLIB_NAME/include -I./src -I./vendor) + else + echo "Error: prebuilt web raylib ${RAYLIB_VERSION} archive not found" + exit 1 + fi else - download "$RAYLIB_NAME" "$RAYLIB_URL/$RAYLIB_NAME.tar.gz" + if download "$RAYLIB_NAME" "$RAYLIB_URL/$RAYLIB_NAME.tar.gz"; then + RAYLIB_A="$RAYLIB_NAME/lib/libraylib.a" + INCLUDES=(-I./$RAYLIB_NAME/include -I./src -I./vendor) + else + echo "Prebuilt raylib ${RAYLIB_VERSION} archive not found; building from source..." + RAYLIB_NAME="raylib-${RAYLIB_VERSION}_desktop_source" + build_raylib_from_source "$RAYLIB_NAME" PLATFORM_DESKTOP + RAYLIB_A="$RAYLIB_NAME/src/libraylib.a" + INCLUDES=(-I./$RAYLIB_NAME/src -I./$RAYLIB_NAME/src/external -I./src -I./vendor) + fi fi -RAYLIB_A="$RAYLIB_NAME/lib/libraylib.a" -INCLUDES=(-I./$RAYLIB_NAME/include -I./src -I./vendor) LINK_ARCHIVES=("$RAYLIB_A") EXTRA_SRC="" @@ -139,9 +219,9 @@ if [ "$MODE" = "local" ] || [ "$MODE" = "fast" ]; then "${INCLUDES[@]}" "$SRC_DIR/$ENV.c" $EXTRA_SRC -o "$OUTPUT_NAME" "${LINK_ARCHIVES[@]}" - "${STANDALONE_LDFLAGS[@]}" + "${RAYLIB_LINK_LDFLAGS[@]}" -lm -lpthread -fopenmp - -DPLATFORM_DESKTOP + "$RAYLIB_PLATFORM" ) echo "Compiling $ENV..." ${CC:-clang} "${CLANG_OPT[@]}" "${FLAGS[@]}" @@ -208,13 +288,6 @@ if [ -z "$NCCL_LFLAG" ]; then NCCL_LFLAG=$(python -c "import nvidia.nccl, os; print('-L' + os.path.join(nvidia.nccl.__path__[0], 'lib'))" 2>/dev/null || echo "") fi -WHEEL_RPATH_FLAGS=() -for lib_flag in "$CUDNN_LFLAG" "$NCCL_LFLAG"; do - if [[ "$lib_flag" == -L* ]]; then - WHEEL_RPATH_FLAGS+=("-Wl,-rpath,${lib_flag#-L}") - fi -done - export CCACHE_DIR="${CCACHE_DIR:-$HOME/.ccache}" export CCACHE_BASEDIR="$(pwd)" export CCACHE_COMPILERCHECK=content @@ -239,10 +312,10 @@ if [ ! -f "$BINDING_SRC" ]; then fi echo "Compiling static library for $ENV..." -${CC:-clang} -c "${CLANG_OPT[@]}" $EXTRA_CFLAGS \ +${CC:-clang} -c "${CLANG_OPT[@]}" \ -I. -Isrc -I$SRC_DIR -Ivendor \ - -I./$RAYLIB_NAME/include -I$CUDA_HOME/include \ - -DPLATFORM_DESKTOP \ + "${INCLUDES[@]}" -I$CUDA_HOME/include \ + "$RAYLIB_PLATFORM" \ -fno-semantic-interposition -fvisibility=hidden \ -fPIC -fopenmp \ "$BINDING_SRC" -o "$STATIC_OBJ" @@ -260,11 +333,11 @@ if [ -z "$MODE" ]; then $NVCC -c -arch=$ARCH -Xcompiler -fPIC \ -Xcompiler=-D_GLIBCXX_USE_CXX11_ABI=1 \ -Xcompiler=-DNPY_NO_DEPRECATED_API=NPY_1_7_API_VERSION \ - -Xcompiler=-DPLATFORM_DESKTOP \ + -Xcompiler=$RAYLIB_PLATFORM \ -std=c++17 \ -I. -Isrc \ -I$PYTHON_INCLUDE -I$PYBIND_INCLUDE -I$NUMPY_INCLUDE \ - -I$CUDA_HOME/include $CUDNN_IFLAG $NCCL_IFLAG -I$RAYLIB_NAME/include \ + -I$CUDA_HOME/include $CUDNN_IFLAG $NCCL_IFLAG "${INCLUDES[@]}" \ -Xcompiler=-fopenmp \ -DOBS_TENSOR_T=$OBS_TENSOR_T \ -DENV_NAME=$ENV \ @@ -275,7 +348,6 @@ if [ -z "$MODE" ]; then ${CXX:-g++} -shared -fPIC -fopenmp build/bindings.o "$STATIC_LIB" "$RAYLIB_A" -L$CUDA_HOME/lib64 $CUDNN_LFLAG $NCCL_LFLAG - "${WHEEL_RPATH_FLAGS[@]}" -lcudart -lnccl -lnvidia-ml -lcublas -lcusolver -lcurand -lcudnn $OMP_LIB $LINK_OPT "${SHARED_LDFLAGS[@]}" @@ -288,10 +360,11 @@ elif [ "$MODE" = "cpu" ]; then echo "Compiling CPU training backend..." ${CXX:-g++} -c -fPIC -fopenmp \ -D_GLIBCXX_USE_CXX11_ABI=1 \ - -DPLATFORM_DESKTOP \ + "$RAYLIB_PLATFORM" \ -std=c++17 \ -I. -Isrc \ -I$PYTHON_INCLUDE -I$PYBIND_INCLUDE \ + "${INCLUDES[@]}" \ -DOBS_TENSOR_T=$OBS_TENSOR_T \ -DENV_NAME=$ENV \ $PRECISION $LINK_OPT \ @@ -310,10 +383,10 @@ elif [ "$MODE" = "profile" ]; then echo "Compiling profile binary ($ARCH)..." $NVCC $NVCC_OPT -arch=$ARCH -std=c++17 \ -I. -Isrc -I$SRC_DIR -Ivendor \ - -I$CUDA_HOME/include $CUDNN_IFLAG $NCCL_IFLAG -I$RAYLIB_NAME/include \ + -I$CUDA_HOME/include $CUDNN_IFLAG $NCCL_IFLAG "${INCLUDES[@]}" \ -DOBS_TENSOR_T=$OBS_TENSOR_T \ -DENV_NAME=$ENV \ - -Xcompiler=-DPLATFORM_DESKTOP \ + -Xcompiler=$RAYLIB_PLATFORM \ $PRECISION \ -Xcompiler=-fopenmp \ tests/profile_kernels.cu vendor/ini.c \ diff --git a/ocean/checkers/checkers.h b/ocean/checkers/checkers.h index 8976ecf2ce..2c336746d0 100644 --- a/ocean/checkers/checkers.h +++ b/ocean/checkers/checkers.h @@ -752,10 +752,10 @@ void c_render(Checkers *env) { case AGENT_PAWN: piece_color = BLUE; DrawCircle(center_x, center_y, radius, piece_color); - DrawCircleGradient(center_x - radius / 3, center_y - radius / 3, + DrawCircleGradient((Vector2){center_x - radius / 3, center_y - radius / 3}, radius / 3, (Color){255, 255, 255, 80}, (Color){255, 255, 255, 10}); - DrawCircleGradient(center_x, center_y, radius, + DrawCircleGradient((Vector2){center_x, center_y}, radius, (Color){255, 255, 255, 50}, (Color){255, 255, 255, 5}); break; @@ -763,17 +763,17 @@ void c_render(Checkers *env) { case AGENT_KING: piece_color = BLUE; DrawCircle(center_x, center_y, radius, piece_color); - DrawCircleGradient(center_x, center_y, radius, + DrawCircleGradient((Vector2){center_x, center_y}, radius, (Color){255, 255, 255, 50}, (Color){255, 255, 255, 5}); - DrawCircleGradient(center_x, center_y - king_offset / 2, radius, + DrawCircleGradient((Vector2){center_x, center_y - king_offset / 2}, radius, (Color){20, 20, 20, 60}, (Color){20, 20, 20, 30}); DrawCircle(center_x, center_y - king_offset, radius, piece_color); DrawCircleGradient( - center_x - radius / 3, center_y - radius / 3 - king_offset, + (Vector2){center_x - radius / 3, center_y - radius / 3 - king_offset}, radius / 3, (Color){255, 255, 255, 80}, (Color){255, 255, 255, 10}); - DrawCircleGradient(center_x, center_y - king_offset, radius, + DrawCircleGradient((Vector2){center_x, center_y - king_offset}, radius, (Color){255, 255, 255, 50}, (Color){255, 255, 255, 5}); break; @@ -781,10 +781,10 @@ void c_render(Checkers *env) { case OPPONENT_PAWN: piece_color = RED; DrawCircle(center_x, center_y, radius, piece_color); - DrawCircleGradient(center_x - radius / 3, center_y - radius / 3, + DrawCircleGradient((Vector2){center_x - radius / 3, center_y - radius / 3}, radius / 3, (Color){255, 255, 255, 80}, (Color){255, 255, 255, 10}); - DrawCircleGradient(center_x, center_y, radius, + DrawCircleGradient((Vector2){center_x, center_y}, radius, (Color){255, 255, 255, 50}, (Color){255, 255, 255, 5}); break; @@ -792,17 +792,17 @@ void c_render(Checkers *env) { case OPPONENT_KING: piece_color = RED; DrawCircle(center_x, center_y, radius, piece_color); - DrawCircleGradient(center_x, center_y, radius, + DrawCircleGradient((Vector2){center_x, center_y}, radius, (Color){255, 255, 255, 50}, (Color){255, 255, 255, 5}); - DrawCircleGradient(center_x, center_y - king_offset / 2, radius, + DrawCircleGradient((Vector2){center_x, center_y - king_offset / 2}, radius, (Color){20, 20, 20, 60}, (Color){20, 20, 20, 30}); DrawCircle(center_x, center_y - king_offset, radius, piece_color); DrawCircleGradient( - center_x - radius / 3, center_y - radius / 3 - king_offset, + (Vector2){center_x - radius / 3, center_y - radius / 3 - king_offset}, radius / 3, (Color){255, 255, 255, 80}, (Color){255, 255, 255, 10}); - DrawCircleGradient(center_x, center_y - king_offset, radius, + DrawCircleGradient((Vector2){center_x, center_y - king_offset}, radius, (Color){255, 255, 255, 50}, (Color){255, 255, 255, 5}); break; diff --git a/ocean/go/go.h b/ocean/go/go.h index 03c2224e0d..d78e4270ea 100644 --- a/ocean/go/go.h +++ b/ocean/go/go.h @@ -969,11 +969,11 @@ void c_render(CGo* env) { int inner = (env->grid_square_size / 2) - 4; int outer = (env->grid_square_size / 2) - 2; if (position_state == 1) { - DrawCircleGradient(circle_x, circle_y, outer, STONE_GRAY, BLACK); + DrawCircleGradient((Vector2){circle_x, circle_y}, outer, STONE_GRAY, BLACK); } // if enemy draw circle tile for white if (position_state == 2) { - DrawCircleGradient(circle_x, circle_y, inner, WHITE, GRAY); + DrawCircleGradient((Vector2){circle_x, circle_y}, inner, WHITE, GRAY); } } // design a pass button diff --git a/pufferlib/pufferl.py b/pufferlib/pufferl.py index 8fc0c03a89..3c519b7ac7 100644 --- a/pufferlib/pufferl.py +++ b/pufferlib/pufferl.py @@ -12,6 +12,7 @@ import time import argparse import configparser +import subprocess from collections import defaultdict import multiprocessing as mp from copy import deepcopy @@ -398,6 +399,17 @@ def sweep(env_name, args=None, pareto=False): train(env_name, exp_args, range(gpu_id, gpu_id + exp_gpus), sweep_obj=sweep_obj, result_queue=result_queue) +def _start_ffmpeg_gif(path, width, height, fps): + return subprocess.Popen([ + 'ffmpeg', '-y', '-loglevel', 'warning', + '-f', 'rawvideo', + '-pix_fmt', 'rgba', + '-s', f'{width}x{height}', + '-r', str(fps), + '-i', '-', + path, + ], stdin=subprocess.PIPE) + def eval(env_name, args=None, load_path=None): '''Evaluate a trained policy. Supports both native and --slowly torch backends.''' args = args or load_config(env_name) @@ -421,11 +433,38 @@ def eval(env_name, args=None, load_path=None): backend.load_weights(pufferl, load_path) print(f'Loaded weights from {load_path}') - while True: - backend.render(pufferl, 0) - backend.rollouts(pufferl) + frame_count = 0 + ffmpeg = None + save_frames = args.get('save_frames') + for name in ('pipe_frame_fd', 'screen_width', 'screen_height'): + if not hasattr(_C, name): + raise RuntimeError(f'Current native backend does not expose {name}; rebuild _C') + out_dir = os.path.dirname(args['gif_path']) + if out_dir: + os.makedirs(out_dir, exist_ok=True) - backend.close(pufferl) + try: + while True: + backend.render(pufferl, 0) + if ffmpeg is None: + ffmpeg = _start_ffmpeg_gif( + args['gif_path'], _C.screen_width(), _C.screen_height(), args['fps']) + _C.pipe_frame_fd(ffmpeg.stdin.fileno()) + frame_count += 1 + if frame_count % 100 == 0: + if save_frames is None: + print(f'Recorded {frame_count} frames to {args["gif_path"]}') + else: + percent = 100.0 * frame_count / save_frames + print(f'Recorded {frame_count}/{save_frames} frames [{percent:.3f}%] to {args["gif_path"]}') + if save_frames is not None and frame_count >= save_frames: + break + backend.rollouts(pufferl) + finally: + if ffmpeg is not None: + ffmpeg.stdin.close() + ffmpeg.wait() + backend.close(pufferl) def load_config(env_name): parser = argparse.ArgumentParser(formatter_class=RichHelpFormatter, add_help=False) @@ -440,7 +479,8 @@ def load_config(env_name): parser.add_argument('--wandb-group', type=str, default='debug') parser.add_argument('--tag', type=str, default=None, help='Tag for experiment') parser.add_argument('--slowly', action='store_true', help='Use PyTorch training backend') - parser.add_argument('--save-frames', type=int, default=0) + parser.add_argument('--save-frames', type=int, default=None, + help='Number of rendered frames to save to --gif-path with ffmpeg. Omit to record until interrupted.') parser.add_argument('--gif-path', type=str, default='eval.gif') parser.add_argument('--fps', type=float, default=15) parser.description = f':blowfish: PufferLib [bright_cyan]{pufferlib.__version__}[/]' \ diff --git a/src/bindings.cu b/src/bindings.cu index 4469cb512c..0b199f0368 100644 --- a/src/bindings.cu +++ b/src/bindings.cu @@ -129,6 +129,12 @@ void render(pybind11::object pufferl_obj, int env_id) { static_vec_render(pufferl.vec, env_id); } +void pipe_frame_fd(int fd) { + if (!static_vec_pipe_frame_fd(fd)) { + throw std::runtime_error("Failed to pipe frame to ffmpeg"); + } +} + void rollouts(pybind11::object pufferl_obj) { PuffeRL& pufferl = pufferl_obj.cast(); pybind11::gil_scoped_release no_gil; @@ -531,6 +537,9 @@ PYBIND11_MODULE(_C, m) { return now - pufferl.start_time; }); m.def("puff_advantage", &py_puff_advantage); + m.def("pipe_frame_fd", &pipe_frame_fd); + m.def("screen_width", &static_vec_screen_width); + m.def("screen_height", &static_vec_screen_height); m.def("create_vec", &create_vec, py::arg("args"), py::arg("gpu") = 1); py::class_>(m, "VecEnv") .def_readonly("total_agents", &VecEnv::total_agents) diff --git a/src/bindings_cpu.cpp b/src/bindings_cpu.cpp index 5ba4dc81e5..feb352dad5 100644 --- a/src/bindings_cpu.cpp +++ b/src/bindings_cpu.cpp @@ -12,6 +12,12 @@ namespace py = pybind11; +static void pipe_frame_fd(int fd) { + if (!static_vec_pipe_frame_fd(fd)) { + throw std::runtime_error("Failed to pipe frame to ffmpeg"); + } +} + // Stub out CUDA functions that the static lib references (dead code when gpu=0) extern "C" { typedef int cudaError_t; @@ -166,6 +172,9 @@ PYBIND11_MODULE(_C, m) { m.attr("gpu") = 0; m.def("puff_advantage_cpu", &py_puff_advantage_cpu); + m.def("pipe_frame_fd", &pipe_frame_fd); + m.def("screen_width", &static_vec_screen_width); + m.def("screen_height", &static_vec_screen_height); m.def("create_vec", &create_vec, py::arg("args"), py::arg("gpu") = 0); py::class_>(m, "VecEnv") diff --git a/src/vecenv.h b/src/vecenv.h index 3456b794be..09df50fc74 100644 --- a/src/vecenv.h +++ b/src/vecenv.h @@ -8,6 +8,9 @@ #include #include #include +#include +#include +#include "raylib.h" #ifdef __cplusplus extern "C" { @@ -115,6 +118,9 @@ void create_static_threads(StaticVec* vec, int num_threads, int horizon, void static_vec_omp_step(StaticVec* vec); void static_vec_seq_step(StaticVec* vec); void static_vec_render(StaticVec* vec, int env_id); +int static_vec_pipe_frame_fd(int fd); +int static_vec_screen_width(void); +int static_vec_screen_height(void); void static_vec_read_profile(StaticVec* vec, float out[NUM_EVAL_PROF]); // Env info @@ -598,10 +604,76 @@ void static_vec_read_profile(StaticVec* vec, float out[NUM_EVAL_PROF]) { } void static_vec_render(StaticVec* vec, int env_id) { +#if defined(PLATFORM_MEMORY) + SetTraceLogLevel(LOG_WARNING); +#endif Env* envs = (Env*)vec->envs; c_render(&envs[env_id]); +#if defined(PLATFORM_MEMORY) + SetTargetFPS(0); +#endif +} + +static int static_vec_write_all(int fd, const void* data, size_t size) { + const char* cursor = (const char*)data; + while (size > 0) { + ssize_t written = write(fd, cursor, size); + if (written < 0) { + if (errno == EINTR) continue; + return 0; + } + if (written == 0) return 0; + cursor += written; + size -= (size_t)written; + } + return 1; +} + +int static_vec_pipe_frame_fd(int fd) { + static int muted_raylib_info_logs = 0; + if (!muted_raylib_info_logs) { + SetTraceLogLevel(LOG_WARNING); + muted_raylib_info_logs = 1; + } + + Image frame = LoadImageFromScreen(); + ImageFormat(&frame, PIXELFORMAT_UNCOMPRESSED_R8G8B8A8); + int ok = 1; + +#if defined(PLATFORM_MEMORY) || defined(GRAPHICS_API_OPENGL_SOFTWARE) + // rlsw's glReadPixels path emits BGRA rows that LoadImageFromScreen() + // flips again as if they were OpenGL bottom-left-origin RGBA pixels. + // Convert back to top-left-origin RGBA for ffmpeg's `-pix_fmt rgba`. + size_t row_size = (size_t)frame.width * 4; + unsigned char* row = (unsigned char*)malloc(row_size); + if (row == NULL) { + ok = 0; + } else { + unsigned char* pixels = (unsigned char*)frame.data; + for (int y = frame.height - 1; ok && y >= 0; y--) { + unsigned char* src = pixels + (size_t)y * row_size; + for (int x = 0; x < frame.width; x++) { + row[x*4 + 0] = src[x*4 + 2]; + row[x*4 + 1] = src[x*4 + 1]; + row[x*4 + 2] = src[x*4 + 0]; + row[x*4 + 3] = src[x*4 + 3]; + } + ok = static_vec_write_all(fd, row, row_size); + } + free(row); + } +#else + size_t size = (size_t)frame.width * (size_t)frame.height * 4; + ok = static_vec_write_all(fd, frame.data, size); +#endif + + UnloadImage(frame); + return ok; } +int static_vec_screen_width(void) { return GetScreenWidth(); } +int static_vec_screen_height(void) { return GetScreenHeight(); } + int get_obs_size(void) { return OBS_SIZE; } int get_num_atns(void) { return NUM_ATNS; } static int _act_sizes[] = ACT_SIZES;