#!/bin/bash set -e CC="${CC:-clang}" if command -v ccache >/dev/null 2>&1; then CC="ccache $CC" fi CFLAGS="-std=c23 -Wall -Wextra -Wno-missing-braces" LIBS="-lm" MODE="debug" BUILDDIR=".build" BINDIR="bin" if command -v mold >/dev/null 2>&1; then LINKER_FLAGS="-fuse-ld=mold" else LINKER_FLAGS="" fi case "$(uname)" in Linux) LINKER_FLAGS="$LINKER_FLAGS -rdynamic" LIBS="$LIBS -ldl" ;; Darwin) export MACOSX_DEPLOYMENT_TARGET="$(sw_vers -productVersion | cut -d '.' -f 1)" ;; MINGW*|MSYS*|CYGWIN*) LINKER_FLAGS="$LINKER_FLAGS -Wl,--export-all-symbols" ;; *) LINKER_FLAGS="$LINKER_FLAGS -rdynamic" LIBS="$LIBS -ldl" ;; esac BOLD='\033[1m' GREEN='\033[38;2;184;187;38m' NC='\033[0m' RED='\033[38;2;251;73;52m' YELLOW='\033[38;2;250;189;47m' if [[ "$(uname)" == "Linux" ]]; then CFLAGS="$CFLAGS -D_GNU_SOURCE" fi build_luajit() { if [[ ! -f "lib/luajit/src/libluajit.a" ]]; then print_info "Building LuaJIT" cd lib/luajit make clean >/dev/null 2>&1 || true make -j$(nproc 2>/dev/null || echo 4) > /dev/null 2>&1 cd - > /dev/null print_info "Built LuaJIT" fi } build_server() { local mode="$1" local server_bin if [[ "$mode" == "release" ]]; then server_bin="pxl8d/target/release/pxl8d" else server_bin="pxl8d/target/debug/pxl8d" fi if [[ -d "pxl8d" ]]; then print_info "Building pxl8d ($mode mode)" cd pxl8d if [[ "$mode" == "release" ]]; then cargo build --release --quiet else cargo build --quiet fi local status=$? cd - > /dev/null if [[ $status -eq 0 ]]; then mkdir -p "$BINDIR" cp "$server_bin" "$BINDIR/pxl8d" print_info "Built pxl8d" else print_error "pxl8d build failed" fi fi } start_server() { local server_bin="$BINDIR/pxl8d" if [[ -f "$server_bin" ]]; then print_info "Starting server..." ./$server_bin & SERVER_PID=$! print_info "Server started with PID $SERVER_PID" sleep 0.5 else print_error "pxl8d binary not found: $server_bin" print_error "Build first with: ./pxl8.sh build" fi } stop_server() { if [[ -n "$SERVER_PID" ]]; then print_info "Stopping server" kill $SERVER_PID 2>/dev/null || true wait $SERVER_PID 2>/dev/null || true fi } clean_server() { if [[ -d "pxl8d" ]]; then print_info "Cleaning pxl8d" cd pxl8d cargo clean 2>/dev/null || true cd - > /dev/null fi } build_sdl() { if [[ ! -f "lib/SDL/build/libSDL3.so" ]] && [[ ! -f "lib/SDL/build/libSDL3.a" ]] && [[ ! -f "lib/SDL/build/libSDL3.dylib" ]]; then print_info "Building SDL3" mkdir -p lib/SDL/build cd lib/SDL/build cmake .. -DCMAKE_BUILD_TYPE=Release > /dev/null 2>&1 cmake --build . --parallel $(nproc 2>/dev/null || echo 4) > /dev/null 2>&1 cd - > /dev/null print_info "Built SDL3" fi } prefix_output() { local in_warning=false while IFS= read -r line; do if [[ "$line" == *": warning:"* ]] || [[ "$line" == *": note:"* ]]; then in_warning=true echo -e "${YELLOW}${BOLD}[$(timestamp) WARN]${NC} $line" >&2 elif [[ "$line" == *": error:"* ]] || [[ "$line" == *": fatal error:"* ]]; then in_warning=false echo -e "${RED}${BOLD}[$(timestamp) ERROR]${NC} $line" >&2 elif [[ "$line" =~ ^[0-9]+\ (warning|error)s?\ generated ]] || [[ -z "$line" ]]; then echo -e "${YELLOW}${BOLD}[$(timestamp) WARN]${NC} $line" >&2 elif $in_warning; then echo -e "${YELLOW}${BOLD}[$(timestamp) WARN]${NC} $line" >&2 else echo -e "${RED}${BOLD}[$(timestamp) ERROR]${NC} $line" >&2 fi done } compile_source_file() { local src_file="$1" local obj_file="$2" local compile_flags="$3" print_info "Compiling: $src_file" local output local exit_code output=$($CC -c $compile_flags "$src_file" -o "$obj_file" 2>&1) || exit_code=$? if [[ -n "$output" ]]; then echo "$output" | prefix_output fi if [[ -n "$exit_code" ]]; then print_error "Compilation failed for $src_file" exit 1 fi } compile_shaders() { local build_mode="$1" local shader_dir="src/gfx/shaders/cpu" local so_dir=".build/$build_mode/shaders/cpu" local obj_dir=".build/$build_mode/shaders/cpu/obj" if [[ ! -d "$shader_dir" ]]; then return 0 fi [[ "$build_mode" == "debug" ]] && mkdir -p "$so_dir" mkdir -p "$obj_dir" local SHADER_INCLUDES="-Isrc/core -Isrc/gfx -Isrc/math" case "$(uname)" in Darwin) SO_EXT="dylib" ;; *) SO_EXT="so" ;; esac # Compile C shaders directly for shader in $(find "$shader_dir" -maxdepth 1 -name "*.c" 2>/dev/null); do local shader_name=$(basename "${shader%.c}") local so_file="$so_dir/${shader_name}.$SO_EXT" local obj_file="$obj_dir/${shader_name}.o" # Debug: compile to .so for hot-reload if [[ "$build_mode" == "debug" ]]; then if [[ "$shader" -nt "$so_file" ]] || [[ ! -f "$so_file" ]]; then $CC -shared -fPIC -O2 $SHADER_INCLUDES "$shader" -o "$so_file" 2>&1 | prefix_output || { print_error "Shader build failed: $shader_name" } fi fi # Compile to .o for release static linking if [[ "$shader" -nt "$obj_file" ]] || [[ ! -f "$obj_file" ]]; then $CC -c -O2 $SHADER_INCLUDES "$shader" -o "$obj_file" 2>&1 | prefix_output || { print_error "Shader compile failed: $shader_name" } fi done # Export shader object files for linking SHADER_OBJECTS="" for obj in $(find "$obj_dir" -name "*.o" 2>/dev/null); do SHADER_OBJECTS="$SHADER_OBJECTS $obj" done } make_lib_dirs() { mkdir -p lib/linenoise lib/fennel lib/miniz } print_error() { echo -e "${RED}${BOLD}[$(timestamp) ERROR]${NC} $1" >&2 } print_info() { echo -e "${GREEN}${BOLD}[$(timestamp) INFO]${NC} $1" } print_usage() { echo -e "${BOLD}pxl8${NC} - framework build tool" echo echo -e "${BOLD}USAGE:${NC}" echo " ./pxl8.sh [options]" echo echo -e "${BOLD}COMMANDS:${NC}" echo " ase Aseprite tools (use 'ase help' for subcommands)" echo " build Build pxl8" echo " clean Remove build artifacts" echo " help Show this help message" echo " install Install pxl8 to ~/.local/bin" echo " profile Profile with perf and generate flamegraph (Linux)" echo " run Build and run pxl8 (optional: cart.pxc or folder)" echo " update Download/update all dependencies" echo " vendor Fetch source for dependencies (ex. SDL3)" echo echo -e "${BOLD}OPTIONS:${NC}" echo " --all Clean both build artifacts and dependencies" echo " --cache Clear ccache (use with clean)" echo " --deps Clean only dependencies" echo " --duration=N Profile duration in seconds (default: 30)" echo " --release Build/run/clean in release mode (default: debug)" } setup_sdl3() { if [[ -d "lib/SDL/build" ]]; then SDL3_CFLAGS="-Ilib/SDL/include" SDL3_LIBS="-Llib/SDL/build -lSDL3" SDL3_RPATH="-Wl,-rpath,$(pwd)/lib/SDL/build" print_info "Using vendored SDL3" else SDL3_CFLAGS=$(pkg-config --cflags sdl3 2>/dev/null || echo "") SDL3_LIBS=$(pkg-config --libs sdl3 2>/dev/null || echo "") SDL3_RPATH="" if [[ -z "$SDL3_LIBS" ]]; then case "$(uname)" in Darwin) if [[ -f "/usr/local/include/SDL3/SDL.h" ]]; then SDL3_CFLAGS="-I/usr/local/include/SDL3" SDL3_LIBS="-L/usr/local/lib -lSDL3" fi ;; Linux) if [[ -f "/usr/include/SDL3/SDL.h" ]]; then SDL3_CFLAGS="-I/usr/include/SDL3" SDL3_LIBS="-lSDL3" fi ;; MINGW*|MSYS*|CYGWIN*) if [[ -f "/mingw64/include/SDL3/SDL.h" ]]; then SDL3_CFLAGS="-I/mingw64/include/SDL3" SDL3_LIBS="-lSDL3" fi ;; esac fi if [[ -z "$SDL3_LIBS" ]]; then print_info "System SDL3 not found, vendoring SDL3..." update_sdl build_sdl SDL3_CFLAGS="-Ilib/SDL/include" SDL3_LIBS="-Llib/SDL/build -lSDL3" SDL3_RPATH="-Wl,-rpath,$(pwd)/lib/SDL/build" print_info "Using vendored SDL3" else print_info "Using system SDL3" fi fi CFLAGS="$CFLAGS $SDL3_CFLAGS" LIBS="$LIBS $SDL3_LIBS $SDL3_RPATH" } timestamp() { date +"%H:%M:%S" } update_fennel() { print_info "Fetching Fennel" local version="1.6.1" if curl -sL --max-time 5 -o lib/fennel/fennel.lua "https://fennel-lang.org/downloads/fennel-${version}.lua" 2>/dev/null; then if [[ -f "lib/fennel/fennel.lua" ]] && [[ -s "lib/fennel/fennel.lua" ]]; then print_info "Updated Fennel (${version})" else print_error "Downloaded file is empty or missing" return 1 fi else print_error "Failed to download Fennel" return 1 fi } update_flamegraph() { print_info "Fetching FlameGraph" if [[ -d "lib/FlameGraph/.git" ]]; then cd lib/FlameGraph && git pull --quiet origin master cd - > /dev/null else rm -rf lib/FlameGraph git clone --quiet https://github.com/brendangregg/FlameGraph.git lib/FlameGraph fi print_info "Updated FlameGraph" } update_linenoise() { print_info "Fetching linenoise" if curl -sL --max-time 5 -o lib/linenoise/linenoise.c https://raw.githubusercontent.com/antirez/linenoise/master/linenoise.c 2>/dev/null && \ curl -sL --max-time 5 -o lib/linenoise/linenoise.h https://raw.githubusercontent.com/antirez/linenoise/master/linenoise.h 2>/dev/null; then print_info "Updated linenoise" else print_error "Failed to download linenoise" return 1 fi } update_luajit() { print_info "Fetching LuaJIT" local version="2.1" local branch="v${version}" if [[ -d "lib/luajit/.git" ]]; then cd lib/luajit && git pull --quiet origin ${branch} cd - > /dev/null else rm -rf lib/luajit git clone --quiet --branch ${branch} https://github.com/LuaJIT/LuaJIT.git lib/luajit fi print_info "Updated LuaJIT (${version})" } update_miniz() { print_info "Fetching miniz" local version="3.1.0" if curl -sL --max-time 5 -o /tmp/miniz.zip "https://github.com/richgel999/miniz/releases/download/${version}/miniz-${version}.zip" 2>/dev/null; then unzip -qjo /tmp/miniz.zip miniz.c miniz.h -d lib/miniz/ 2>/dev/null rm -f /tmp/miniz.zip if [[ -f "lib/miniz/miniz.c" ]] && [[ -f "lib/miniz/miniz.h" ]]; then print_info "Updated miniz (${version})" else print_error "Failed to extract miniz files" return 1 fi else print_error "Failed to download miniz" return 1 fi } update_sdl() { print_info "Fetching SDL3" if [[ -d "lib/SDL/.git" ]]; then cd lib/SDL && git pull --quiet origin main cd - > /dev/null else rm -rf lib/SDL git clone --quiet https://github.com/libsdl-org/SDL.git lib/SDL fi print_info "Updated SDL3" } COMMAND="$1" shift || true for arg in "$@"; do case $arg in --release) MODE="release" ;; esac done if [ "$MODE" = "release" ]; then CFLAGS="$CFLAGS -O3 -flto -ffast-math -funroll-loops -fno-unwind-tables -fno-asynchronous-unwind-tables" LINKER_FLAGS="$LINKER_FLAGS -flto" BUILDDIR="$BUILDDIR/release" BINDIR="$BINDIR/release" else CFLAGS="$CFLAGS -g -O1 -DDEBUG" BUILDDIR="$BUILDDIR/debug" BINDIR="$BINDIR/debug" fi DEP_CFLAGS="-O3 -funroll-loops" case "$COMMAND" in build) mkdir -p "$BUILDDIR" mkdir -p "$BINDIR" if [[ ! -d "lib/luajit" ]] || \ [[ ! -f "lib/linenoise/linenoise.c" ]] || \ [[ ! -f "lib/miniz/miniz.c" ]] || \ [[ ! -f "lib/fennel/fennel.lua" ]]; then print_info "Missing dependencies, fetching..." make_lib_dirs update_fennel update_linenoise update_luajit update_miniz fi if [[ ! -f "bin/.gitignore" ]]; then echo "*" > "bin/.gitignore" fi if [[ ! -f ".build/.gitignore" ]]; then echo "*" > ".build/.gitignore" fi if [[ ! -f "lib/.gitignore" ]]; then echo "*" > "lib/.gitignore" fi if [[ -d "lib/luajit" ]]; then build_luajit fi if [[ -d "lib/SDL" ]]; then build_sdl fi build_server "$MODE" setup_sdl3 print_info "Building pxl8 ($MODE mode)" if [[ "$CC" == ccache* ]]; then print_info "Compiler cache: ccache enabled" fi INCLUDES="-Isrc/asset -Isrc/bsp -Isrc/core -Isrc/gfx -Isrc/gui -Isrc/hal -Isrc/math -Isrc/net -Isrc/procgen -Isrc/script -Isrc/shader -Isrc/sfx -Isrc/sim -Isrc/vxl -Isrc/world -Ilib/linenoise -Ilib/luajit/src -Ilib/miniz -I.build/shaders/c" COMPILE_FLAGS="$CFLAGS $INCLUDES" DEP_COMPILE_FLAGS="$DEP_CFLAGS $INCLUDES" EXECUTABLE="$BINDIR/pxl8" LIB_SOURCE_FILES="lib/linenoise/linenoise.c lib/miniz/miniz.c" PXL8_SOURCE_FILES=" src/asset/pxl8_ase.c src/asset/pxl8_cart.c src/asset/pxl8_save.c src/bsp/pxl8_bsp.c src/bsp/pxl8_bsp_render.c src/core/pxl8.c src/core/pxl8_bytes.c src/core/pxl8_io.c src/core/pxl8_log.c src/core/pxl8_replay.c src/core/pxl8_rng.c src/gfx/pxl8_3d_camera.c src/gfx/pxl8_anim.c src/gfx/pxl8_atlas.c src/gfx/pxl8_blit.c src/gfx/pxl8_colormap.c src/gfx/pxl8_dither.c src/gfx/pxl8_render.c src/gfx/pxl8_shader_registry.c src/gfx/pxl8_shader_runtime.c src/gfx/pxl8_font.c src/gfx/pxl8_gfx.c src/gfx/pxl8_lightmap.c src/gfx/pxl8_lights.c src/gfx/pxl8_mesh.c src/gfx/pxl8_palette.c src/gfx/pxl8_particles.c src/gfx/pxl8_tilemap.c src/gfx/pxl8_tilesheet.c src/gfx/pxl8_transition.c src/gui/pxl8_gui.c src/hal/pxl8_hal_sdl3.c src/hal/pxl8_mem_sdl3.c src/hal/pxl8_thread_sdl3.c src/math/pxl8_math.c src/math/pxl8_noise.c src/net/pxl8_net.c src/net/pxl8_protocol.c src/procgen/pxl8_graph.c src/script/pxl8_repl.c src/script/pxl8_script.c src/sfx/pxl8_sfx.c src/sim/pxl8_sim.c src/vxl/pxl8_vxl.c src/vxl/pxl8_vxl_render.c src/world/pxl8_entity.c src/world/pxl8_world.c src/world/pxl8_world_chunk.c src/world/pxl8_world_chunk_cache.c " LUAJIT_LIB="lib/luajit/src/libluajit.a" OBJECT_DIR="$BUILDDIR/obj" mkdir -p "$OBJECT_DIR" OBJECTS="" NEED_LINK=false SOURCES_COMPILED="" for src_file in $LIB_SOURCE_FILES; do obj_name=$(basename "$src_file" .c).o obj_file="$OBJECT_DIR/$obj_name" OBJECTS="$OBJECTS $obj_file" if [[ "$src_file" -nt "$obj_file" ]]; then NEED_LINK=true compile_source_file "$src_file" "$obj_file" "$DEP_COMPILE_FLAGS" SOURCES_COMPILED="yes" fi done compile_shaders "$MODE" for src_file in $PXL8_SOURCE_FILES; do obj_name=$(basename "$src_file" .c).o obj_file="$OBJECT_DIR/$obj_name" OBJECTS="$OBJECTS $obj_file" NEEDS_REBUILD=false if [[ "$src_file" -nt "$obj_file" ]] || \ [[ "src/core/pxl8_types.h" -nt "$obj_file" ]] || \ [[ "src/core/pxl8_macros.h" -nt "$obj_file" ]] || \ [[ "src/net/pxl8_protocol.h" -nt "$obj_file" ]]; then NEEDS_REBUILD=true fi if [[ "$src_file" == "src/script/pxl8_script.c" ]]; then for lua_file in src/lua/*.lua src/lua/pxl8/*.lua lib/fennel/fennel.lua; do if [[ -f "$lua_file" ]] && [[ "$lua_file" -nt "$obj_file" ]]; then NEEDS_REBUILD=true break fi done fi if [[ "$NEEDS_REBUILD" == true ]]; then NEED_LINK=true compile_source_file "$src_file" "$obj_file" "$COMPILE_FLAGS" SOURCES_COMPILED="yes" fi done if [[ "$LUAJIT_LIB" -nt "$EXECUTABLE" ]] || [[ "$NEED_LINK" == true ]]; then print_info "Linking executable" if ! $CC $LINKER_FLAGS $OBJECTS $SHADER_OBJECTS $LUAJIT_LIB $LIBS -o "$EXECUTABLE"; then print_error "Linking failed" exit 1 fi elif [[ -z "$SOURCES_COMPILED" ]]; then print_info "All files are up to date, skipping build" fi print_info "Build complete" print_info "Binary: $EXECUTABLE" ;; run) "$0" build "$@" || exit 1 CART="" EXTRA_ARGS="" for arg in "$@"; do if [[ "$arg" == "--release" ]]; then MODE="release" elif [[ "$arg" == "--repl" ]]; then EXTRA_ARGS="$EXTRA_ARGS --repl" elif [[ -z "$CART" ]]; then CART="$arg" fi done start_server "$MODE" trap stop_server EXIT if [[ -z "$CART" ]]; then "$BINDIR/pxl8" demo $EXTRA_ARGS else "$BINDIR/pxl8" "$CART" $EXTRA_ARGS fi ;; clean) CLEAN_ALL=false CLEAN_CACHE=false CLEAN_DEPS=false CLEAN_RELEASE=false for arg in "$@"; do case "$arg" in --all) CLEAN_ALL=true ;; --cache) CLEAN_CACHE=true ;; --deps) CLEAN_DEPS=true ;; --release) CLEAN_RELEASE=true ;; esac done if [[ "$CLEAN_CACHE" == true ]] && command -v ccache >/dev/null 2>&1; then print_info "Clearing ccache" ccache -C >/dev/null fi if [[ "$CLEAN_RELEASE" == true ]]; then BUILD_PATH=".build/release" BIN_PATH="bin/release" MODE="release" else BUILD_PATH=".build/debug" BIN_PATH="bin/debug" MODE="debug" fi if [[ "$CLEAN_ALL" == true ]]; then print_info "Removing build artifacts and dependencies" rm -rf "$BUILD_PATH" "$BIN_PATH" .build/shaders lib clean_server print_info "Cleaned all" elif [[ "$CLEAN_DEPS" == true ]]; then print_info "Removing dependencies" rm -rf lib print_info "Cleaned dependencies" else print_info "Removing build artifacts" rm -rf "$BUILD_PATH" "$BIN_PATH" .build/shaders clean_server print_info "Cleaned" fi ;; update) make_lib_dirs update_fennel update_linenoise update_luajit update_miniz print_info "All dependencies updated" ;; vendor) update_sdl ;; install) "$0" build --release || exit 1 INSTALL_DIR="${HOME}/.local/bin" mkdir -p "$INSTALL_DIR" cp "bin/release/pxl8" "$INSTALL_DIR/pxl8" chmod +x "$INSTALL_DIR/pxl8" print_info "Installed pxl8 to $INSTALL_DIR/pxl8" if [[ ":$PATH:" != *":$INSTALL_DIR:"* ]]; then print_info "Note: Add $INSTALL_DIR to your PATH if not already" fi ;; ase) bash tools/aseprite/pxl8-ase.sh "$@" ;; profile) if [[ "$(uname)" != "Linux" ]]; then print_error "Profiling with perf is only supported on Linux" exit 1 fi if ! command -v perf >/dev/null 2>&1; then print_error "perf not found. Install linux-tools or perf package." exit 1 fi if [[ ! -d "lib/FlameGraph" ]]; then mkdir -p lib update_flamegraph fi "$0" build || exit 1 PROFILE_DIR=".build/debug/profile" mkdir -p "$PROFILE_DIR" CART="" PERF_DURATION=30 for arg in "$@"; do if [[ "$arg" =~ ^--duration=([0-9]+)$ ]]; then PERF_DURATION="${BASH_REMATCH[1]}" elif [[ "$arg" != "--release" ]] && [[ -z "$CART" ]]; then CART="$arg" fi done [[ -z "$CART" ]] && CART="demo" TIMESTAMP=$(date +"%Y%m%d_%H%M%S") PERF_DATA="$PROFILE_DIR/perf_${TIMESTAMP}.data" PERF_SCRIPT="$PROFILE_DIR/perf_${TIMESTAMP}.perf" FOLDED="$PROFILE_DIR/perf_${TIMESTAMP}.folded" SVG="$PROFILE_DIR/flamegraph_${TIMESTAMP}.svg" print_info "Starting server..." ./bin/debug/pxl8d & SERVER_PID=$! sleep 0.5 trap "kill $SERVER_PID 2>/dev/null; wait $SERVER_PID 2>/dev/null" EXIT print_info "Profiling pxl8 for ${PERF_DURATION}s (Ctrl+C to stop early)..." perf record -F 99 -g --call-graph dwarf -o "$PERF_DATA" -- \ timeout "${PERF_DURATION}s" ./bin/debug/pxl8 "$CART" 2>/dev/null || true print_info "Processing profile data..." perf script -i "$PERF_DATA" > "$PERF_SCRIPT" print_info "Generating flamegraph..." lib/FlameGraph/stackcollapse-perf.pl "$PERF_SCRIPT" > "$FOLDED" lib/FlameGraph/flamegraph.pl --cp --colors orange --title "pxl8 profile" "$FOLDED" > "$SVG" rm -f "$PERF_DATA" "$PERF_SCRIPT" "$FOLDED" print_info "Flamegraph: $SVG" if command -v xdg-open >/dev/null 2>&1; then xdg-open "$SVG" 2>/dev/null & fi ;; help|--help|-h|"") print_usage ;; *) print_error "Unknown command: $COMMAND" print_usage exit 1 ;; esac