diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..95c7016 --- /dev/null +++ b/Makefile @@ -0,0 +1,499 @@ +# pxl8 - Makefile +# +# Usage: +# make Build everything (debug) +# make release Build everything (release) +# make run Build and run (CART=script.fnl) +# make server Build pxl8d only +# make client Build pxl8 only +# make install Install to ~/.local/bin (release) +# make profile Profile with perf + flamegraph (Linux) +# make ase Aseprite tools (ASE_CMD=package|clean) +# make clean Remove build artifacts +# make clean-all Remove build artifacts and dependencies +# make clean-deps Remove only dependencies +# make clean-cache Clear ccache +# make update Fetch/update all dependencies +# make vendor-sdl Vendor SDL3 from source +# make help Show available targets +# +# Variables: +# MODE=release Build in release mode (default: debug) +# CART=demo Cart/script to run (default: demo) +# PROFILE_DURATION=30 Perf recording seconds (default: 30) +# ASE_CMD= Aseprite subcommand (package, clean, ...) + +export PATH := $(PATH):/usr/bin:/bin + +# -- Platform detection ------------------------------------------------------- + +UNAME := $(shell uname) +PLATFORM := posix + +ifneq (,$(findstring MINGW,$(UNAME))) + PLATFORM := windows +endif +ifneq (,$(findstring MSYS,$(UNAME))) + PLATFORM := windows +endif +ifneq (,$(findstring CYGWIN,$(UNAME))) + PLATFORM := windows +endif + +# -- Toolchain ---------------------------------------------------------------- + +CC := clang +CCACHE := $(shell command -v ccache 2>/dev/null) +ifdef CCACHE + CC := ccache $(CC) +endif + +NPROC := $(shell nproc 2>/dev/null || sysctl -n hw.ncpu 2>/dev/null || echo 4) + +# -- Directories -------------------------------------------------------------- + +BUILDDIR = .build/$(MODE) +OBJDIR = $(BUILDDIR)/obj +BINDIR = bin/$(MODE) +SHADERDIR = $(BUILDDIR)/shaders/cpu + +# -- Mode (debug/release) ---------------------------------------------------- + +MODE ?= debug + +ifeq ($(MODE),release) + CFLAGS = -std=c23 -Wall -Wextra -Wno-missing-braces \ + -O3 -flto -ffast-math -funroll-loops \ + -fno-unwind-tables -fno-asynchronous-unwind-tables + LDFLAGS = -flto +else + CFLAGS = -std=c23 -Wall -Wextra -Wno-missing-braces -g -O1 -DDEBUG + LDFLAGS = +endif + +DEP_CFLAGS = -O3 -funroll-loops -MMD -MP +CFLAGS += -MMD -MP + +# -- Linker ------------------------------------------------------------------- + +MOLD := $(shell command -v mold 2>/dev/null) +ifdef MOLD + LDFLAGS += -fuse-ld=mold +endif + +ifeq ($(PLATFORM),windows) + LIBS = -lws2_32 + PXL8_DEF = $(BUILDDIR)/pxl8.def + EXE_EXT = .exe +else ifeq ($(UNAME),Darwin) + LIBS = -lm + export MACOSX_DEPLOYMENT_TARGET := $(shell sw_vers -productVersion | cut -d '.' -f 1) +else + LIBS = -lm + LDFLAGS += -rdynamic + LIBS += -ldl +endif + +EXE_EXT ?= + +# -- SDL3 detection ----------------------------------------------------------- + +ifeq ($(PLATFORM),windows) + # Windows: always vendor SDL3, ship SDL3.dll alongside the .exe + SDL3_BUILD_DIR := $(firstword $(wildcard lib/SDL/build/Release lib/SDL/build/Debug)) + ifneq ($(SDL3_BUILD_DIR),) + SDL3_CFLAGS = -Ilib/SDL/include + SDL3_LIBS = -L$(SDL3_BUILD_DIR) -lSDL3 + HAS_SDL3 = 1 + endif +else + # Unix: check vendored build, then system + ifneq (,$(wildcard lib/SDL/build/libSDL3.*)) + SDL3_CFLAGS = -Ilib/SDL/include + SDL3_LIBS = -Llib/SDL/build -lSDL3 -Wl,-rpath,$(CURDIR)/lib/SDL/build + HAS_SDL3 = 1 + else + SDL3_CFLAGS := $(shell pkg-config --cflags sdl3 2>/dev/null) + SDL3_LIBS := $(shell pkg-config --libs sdl3 2>/dev/null) + ifneq ($(SDL3_LIBS),) + HAS_SDL3 = 1 + endif + endif +endif + +CFLAGS += $(SDL3_CFLAGS) +LIBS += $(SDL3_LIBS) + +# -- Linenoise ---------------------------------------------------------------- + +ifeq ($(PLATFORM),windows) + LINENOISE_DIR = lib/linenoise-win32 + LINENOISE_SRCS = $(LINENOISE_DIR)/linenoise.c $(LINENOISE_DIR)/stringbuf.c $(LINENOISE_DIR)/utf8.c + CFLAGS += -DPXL8_WIN32_LINENOISE -D_CRT_SECURE_NO_WARNINGS +else + LINENOISE_DIR = lib/linenoise + LINENOISE_SRCS = $(LINENOISE_DIR)/linenoise.c +endif + +# -- LuaJIT ------------------------------------------------------------------ + +LUAJIT_LIB = lib/luajit/src/libluajit.a + +ifeq ($(PLATFORM),windows) + LUAJIT_BUILD = $(MAKE) -C lib/luajit -j$(NPROC) \ + CC="$(firstword $(CC))" HOST_CC="$(firstword $(CC))" \ + TARGET_SYS=Windows BUILDMODE=static \ + MINILUA_LIBS= TARGET_XLIBS= Q= +else + LUAJIT_BUILD = $(MAKE) -C lib/luajit -j$(NPROC) \ + CC="$(firstword $(CC))" HOST_CC="$(firstword $(CC))" Q= > /dev/null 2>&1 +endif + +# -- Include paths ------------------------------------------------------------ + +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/world \ + -I$(LINENOISE_DIR) -Ilib/luajit/src -Ilib/miniz -I.build/shaders/c + +# -- Source files ------------------------------------------------------------- + +LIB_SRCS = $(LINENOISE_SRCS) lib/miniz/miniz.c + +PXL8_SRCS = \ + 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_glows.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_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/world/pxl8_entity.c \ + src/world/pxl8_world.c \ + src/world/pxl8_world_chunk.c \ + src/world/pxl8_world_chunk_cache.c + +ifeq ($(HAS_SDL3),1) + PXL8_SRCS += src/hal/pxl8_io_sdl3.c src/hal/pxl8_mem_sdl3.c +else + PXL8_SRCS += src/hal/pxl8_mem.c +endif + +# -- Object files ------------------------------------------------------------- + +LIB_OBJS = $(patsubst %.c,$(OBJDIR)/%.o,$(notdir $(LIB_SRCS))) +PXL8_OBJS = $(patsubst %.c,$(OBJDIR)/%.o,$(notdir $(PXL8_SRCS))) +ALL_OBJS = $(LIB_OBJS) $(PXL8_OBJS) + +# -- Shader objects ----------------------------------------------------------- + +SHADER_SRCS = $(wildcard src/gfx/shaders/cpu/*.c) +SHADER_OBJS = $(patsubst src/gfx/shaders/cpu/%.c,$(SHADERDIR)/obj/%.o,$(SHADER_SRCS)) +SHADER_INCLUDES = -Isrc/core -Isrc/gfx -Isrc/math + +# -- Targets ------------------------------------------------------------------ + +CLIENT = $(BINDIR)/pxl8$(EXE_EXT) +SERVER = $(BINDIR)/pxl8d$(EXE_EXT) + +CART ?= demo +PROFILE_DURATION ?= 30 +ASE_CMD ?= + +.PHONY: all release client server run clean clean-all clean-deps clean-cache \ + update vendor-sdl deps luajit install ase profile help + +all: client server + +release: + $(MAKE) MODE=release all + +# -- Dependencies ------------------------------------------------------------- + +DEPS_OK = lib/luajit/src/luajit.c lib/miniz/miniz.c lib/fennel/fennel.lua $(LINENOISE_DIR)/linenoise.c + +deps: $(DEPS_OK) + +lib/luajit/src/luajit.c: + @echo "[INFO] Fetching LuaJIT" + @rm -rf lib/luajit + @git clone --quiet --branch v2.1 https://github.com/LuaJIT/LuaJIT.git lib/luajit + +lib/linenoise/linenoise.c: + @mkdir -p lib/linenoise + @echo "[INFO] Fetching linenoise" + @curl -sL -o lib/linenoise/linenoise.c https://raw.githubusercontent.com/antirez/linenoise/master/linenoise.c + @curl -sL -o lib/linenoise/linenoise.h https://raw.githubusercontent.com/antirez/linenoise/master/linenoise.h + +lib/linenoise-win32/linenoise.c: + @echo "[INFO] Fetching linenoise-win32" + @rm -rf lib/linenoise-win32 + @git clone --quiet --depth 1 https://github.com/msteveb/linenoise.git lib/linenoise-win32 + +lib/miniz/miniz.c: + @mkdir -p lib/miniz + @echo "[INFO] Fetching miniz" + @curl -sL -o /tmp/miniz.zip "https://github.com/richgel999/miniz/releases/download/3.1.0/miniz-3.1.0.zip" + @unzip -qjo /tmp/miniz.zip miniz.c miniz.h -d lib/miniz/ + @rm -f /tmp/miniz.zip + +lib/fennel/fennel.lua: + @mkdir -p lib/fennel + @echo "[INFO] Fetching Fennel" + @curl -sL -o lib/fennel/fennel.lua "https://fennel-lang.org/downloads/fennel-1.6.1.lua" + +update: + @rm -f lib/linenoise/linenoise.c lib/miniz/miniz.c lib/fennel/fennel.lua + @rm -rf lib/luajit lib/linenoise-win32 + $(MAKE) deps + +vendor-sdl: + @echo "[INFO] Fetching SDL3" + @if [ -d lib/SDL/.git ]; then cd lib/SDL && git pull --quiet origin main; \ + else rm -rf lib/SDL && git clone --quiet https://github.com/libsdl-org/SDL.git lib/SDL; fi + @echo "[INFO] Building SDL3" + @mkdir -p lib/SDL/build && cd lib/SDL/build && \ + cmake .. -DCMAKE_BUILD_TYPE=Release && \ + cmake --build . --config Release --parallel $(NPROC) + @echo "[INFO] Built SDL3" + +# -- LuaJIT build ------------------------------------------------------------ + +luajit: $(LUAJIT_LIB) + +$(LUAJIT_LIB): lib/luajit/src/luajit.c + @echo "[INFO] Building LuaJIT" + @$(LUAJIT_BUILD) + @echo "[INFO] Built LuaJIT" + +# -- Server (Rust) ------------------------------------------------------------ + +server: $(SERVER) + +$(SERVER): $(wildcard pxl8d/src/*.rs pxl8d/Cargo.toml) + @mkdir -p $(BINDIR) + @echo "[INFO] Building pxl8d ($(MODE) mode)" +ifeq ($(MODE),release) + @cd pxl8d && PATH="$$(echo $$PATH | tr ':' '\n' | grep -v '/usr/bin' | tr '\n' ':')" cargo build --release --quiet + @cp pxl8d/target/release/pxl8d$(EXE_EXT) $(SERVER) +else + @cd pxl8d && PATH="$$(echo $$PATH | tr ':' '\n' | grep -v '/usr/bin' | tr '\n' ':')" cargo build --quiet + @cp pxl8d/target/debug/pxl8d$(EXE_EXT) $(SERVER) +endif + @echo "[INFO] Built pxl8d" + +# -- Client (C23) ------------------------------------------------------------- + +client: deps $(CLIENT) + +$(CLIENT): $(ALL_OBJS) $(SHADER_OBJS) $(LUAJIT_LIB) + @mkdir -p $(BINDIR) + @echo "[INFO] Linking pxl8" +ifeq ($(PLATFORM),windows) + @(echo EXPORTS && llvm-nm --defined-only $(ALL_OBJS) $(SHADER_OBJS) | grep ' T pxl8_\| T SDL' | awk '{print $$3}') > $(PXL8_DEF) + @MSYS_NO_PATHCONV=1 $(CC) $(LDFLAGS) $(ALL_OBJS) $(SHADER_OBJS) $(LUAJIT_LIB) $(LIBS) -Wl,/DEF:$(PXL8_DEF) -o $@ + @cp -u $(SDL3_BUILD_DIR)/SDL3.dll $(BINDIR)/ +else + @$(CC) $(LDFLAGS) $(ALL_OBJS) $(SHADER_OBJS) $(LUAJIT_LIB) $(LIBS) -o $@ +endif + @echo "[INFO] Built pxl8 -> $@" + +# -- Compile rules ------------------------------------------------------------ + +# Lib sources (dep flags, no warnings) +$(OBJDIR)/linenoise.o: $(LINENOISE_DIR)/linenoise.c | $(OBJDIR) + @echo "[CC] $<" + @$(CC) -c $(DEP_CFLAGS) $(INCLUDES) $< -o $@ + +$(OBJDIR)/stringbuf.o: $(LINENOISE_DIR)/stringbuf.c | $(OBJDIR) + @echo "[CC] $<" + @$(CC) -c $(DEP_CFLAGS) $(INCLUDES) $< -o $@ + +$(OBJDIR)/utf8.o: $(LINENOISE_DIR)/utf8.c | $(OBJDIR) + @echo "[CC] $<" + @$(CC) -c $(DEP_CFLAGS) $(INCLUDES) $< -o $@ + +$(OBJDIR)/miniz.o: lib/miniz/miniz.c | $(OBJDIR) + @echo "[CC] $<" + @$(CC) -c $(DEP_CFLAGS) $(INCLUDES) $< -o $@ + +# Pattern rules for pxl8 sources by directory +define PXL8_COMPILE_RULE +$(OBJDIR)/%.o: $(1)/%.c | $(OBJDIR) + @echo "[CC] $$<" + @$$(CC) -c $$(CFLAGS) $$(INCLUDES) $$< -o $$@ +endef + +$(foreach dir,src/asset src/bsp src/core src/gfx src/gui src/hal src/math src/net src/procgen src/script src/shader src/sfx src/sim src/world,$(eval $(call PXL8_COMPILE_RULE,$(dir)))) + +# pxl8_script.o depends on embedded Lua/Fennel sources +LUA_SRCS = $(wildcard src/lua/*.lua src/lua/pxl8/*.lua) lib/fennel/fennel.lua +$(OBJDIR)/pxl8_script.o: $(LUA_SRCS) + +# Shader objects +$(SHADERDIR)/obj/%.o: src/gfx/shaders/cpu/%.c | $(SHADERDIR)/obj + @echo "[SHADER] $<" + @$(CC) -c -O2 $(SHADER_INCLUDES) $< -o $@ + +# -- Directory creation ------------------------------------------------------- + +$(OBJDIR) $(SHADERDIR)/obj: + @mkdir -p $@ + +# -- Gitignore bookkeeping --------------------------------------------------- + +$(shell mkdir -p bin .build lib 2>/dev/null) +$(shell [ -f bin/.gitignore ] || echo '*' > bin/.gitignore) +$(shell [ -f .build/.gitignore ] || echo '*' > .build/.gitignore) +$(shell [ -f lib/.gitignore ] || echo '*' > lib/.gitignore) + +# -- Run ---------------------------------------------------------------------- + +run: all + @$(BINDIR)/pxl8d$(EXE_EXT) & SERVER_PID=$$!; \ + sleep 0.5; \ + $(BINDIR)/pxl8$(EXE_EXT) $(CART) || true; \ + kill $$SERVER_PID 2>/dev/null; wait $$SERVER_PID 2>/dev/null || true + +# -- Clean -------------------------------------------------------------------- + +clean: + rm -rf .build/debug .build/release bin/debug bin/release .build/shaders + @if [ -d pxl8d ]; then cd pxl8d && cargo clean 2>/dev/null; fi || true + +clean-all: clean + rm -rf lib + +clean-deps: + rm -rf lib + +clean-cache: + @if command -v ccache >/dev/null 2>&1; then ccache -C; else echo "ccache not found"; fi + +# -- Install ------------------------------------------------------------------ + +install: + $(MAKE) MODE=release client + @mkdir -p $(HOME)/.local/bin + @cp bin/release/pxl8$(EXE_EXT) $(HOME)/.local/bin/pxl8$(EXE_EXT) + @chmod +x $(HOME)/.local/bin/pxl8$(EXE_EXT) + @echo "[INFO] Installed pxl8 to $(HOME)/.local/bin/pxl8$(EXE_EXT)" + +# -- Aseprite tools ----------------------------------------------------------- + +ase: + @bash tools/aseprite/pxl8-ase.sh $(ASE_CMD) + +# -- Profile (Linux) ---------------------------------------------------------- + +profile: client + @if [ "$$(uname)" != "Linux" ]; then echo "[ERROR] Profiling requires Linux + perf"; exit 1; fi + @if ! command -v perf >/dev/null 2>&1; then echo "[ERROR] perf not found"; exit 1; fi + @if [ ! -d lib/FlameGraph ]; then \ + echo "[INFO] Fetching FlameGraph"; \ + git clone --quiet https://github.com/brendangregg/FlameGraph.git lib/FlameGraph; \ + fi + @mkdir -p .build/$(MODE)/profile; \ + TIMESTAMP=$$(date +%Y%m%d_%H%M%S); \ + PDIR=.build/$(MODE)/profile; \ + echo "[INFO] Starting server..."; \ + $(BINDIR)/pxl8d$(EXE_EXT) & SERVER_PID=$$!; \ + sleep 0.5; \ + echo "[INFO] Profiling for $(PROFILE_DURATION)s (Ctrl+C to stop early)..."; \ + perf record -F 99 -g --call-graph dwarf -o $$PDIR/perf_$$TIMESTAMP.data -- \ + timeout $(PROFILE_DURATION)s $(BINDIR)/pxl8$(EXE_EXT) $(CART) 2>/dev/null || true; \ + kill $$SERVER_PID 2>/dev/null; wait $$SERVER_PID 2>/dev/null || true; \ + echo "[INFO] Generating flamegraph..."; \ + perf script -i $$PDIR/perf_$$TIMESTAMP.data > $$PDIR/perf_$$TIMESTAMP.perf; \ + lib/FlameGraph/stackcollapse-perf.pl $$PDIR/perf_$$TIMESTAMP.perf > $$PDIR/perf_$$TIMESTAMP.folded; \ + lib/FlameGraph/flamegraph.pl --cp --colors orange --title "pxl8 profile" \ + $$PDIR/perf_$$TIMESTAMP.folded > $$PDIR/flamegraph_$$TIMESTAMP.svg; \ + rm -f $$PDIR/perf_$$TIMESTAMP.data $$PDIR/perf_$$TIMESTAMP.perf $$PDIR/perf_$$TIMESTAMP.folded; \ + echo "[INFO] Flamegraph: $$PDIR/flamegraph_$$TIMESTAMP.svg"; \ + command -v xdg-open >/dev/null 2>&1 && xdg-open $$PDIR/flamegraph_$$TIMESTAMP.svg 2>/dev/null & + +# -- Help --------------------------------------------------------------------- + +help: + @echo "pxl8 - framework build system" + @echo "" + @echo "TARGETS:" + @echo " make Build everything (debug)" + @echo " make release Build everything (release)" + @echo " make run Build and run (CART=\"demo --repl\")" + @echo " make client Build pxl8 only" + @echo " make server Build pxl8d only" + @echo " make install Install to ~/.local/bin (release)" + @echo " make profile Profile with perf (PROFILE_DURATION=30, Linux only)" + @echo " make ase Aseprite tools (ASE_CMD=package|clean)" + @echo " make clean Remove build artifacts" + @echo " make clean-all Remove artifacts + dependencies" + @echo " make clean-deps Remove only dependencies" + @echo " make clean-cache Clear ccache" + @echo " make update Fetch/update all dependencies" + @echo " make vendor-sdl Vendor SDL3 from source" + @echo "" + @echo "VARIABLES:" + @echo " MODE=release Build mode (default: debug)" + @echo " CART=demo Cart or script to run" + +# -- Auto-generated header dependencies -------------------------------------- + +-include $(ALL_OBJS:.o=.d) +-include $(SHADER_OBJS:.o=.d) + +# -- compile_commands.json ---------------------------------------------------- + +compile_commands.json: $(LIB_SRCS) $(PXL8_SRCS) + @echo "[INFO] Generating compile_commands.json" + @echo '[' > $@.tmp + @first=true; \ + for f in $(LIB_SRCS); do \ + if $$first; then first=false; else printf ',\n' >> $@.tmp; fi; \ + printf ' {"directory":"%s","command":"clang -c %s %s","file":"%s"}' \ + "$(CURDIR)" "$(DEP_CFLAGS) $(INCLUDES)" "$$f" "$$f" >> $@.tmp; \ + done; \ + for f in $(PXL8_SRCS); do \ + if $$first; then first=false; else printf ',\n' >> $@.tmp; fi; \ + printf ' {"directory":"%s","command":"clang -c %s %s","file":"%s"}' \ + "$(CURDIR)" "$(CFLAGS) $(INCLUDES)" "$$f" "$$f" >> $@.tmp; \ + done + @printf '\n]\n' >> $@.tmp + @mv $@.tmp $@ diff --git a/README.md b/README.md index cfd18da..f352858 100644 --- a/README.md +++ b/README.md @@ -3,10 +3,10 @@ ### Quick Start ```bash -./pxl8.sh build # Build pxl8 system -./pxl8.sh install # Install pxl8 binary to ~/.local/bin -./pxl8.sh run [game.cart | project_dir] # Run pxl8 demo (or a specific game) -./pxl8.sh run [game.cart | project_dir] --repl # Run pxl8 demo (or a specific game) with a REPL +make # Build pxl8 system +make install # Install pxl8 binary to ~/.local/bin +make run # Run pxl8 demo +make run CART="game.cart --repl" # Run a specific game with a REPL ``` ### Requirements @@ -14,6 +14,7 @@ - **Clang 19+** (or GCC 15+) - Required for C23 `#embed` support - **Rust nightly** (edition 2024) - Required for [pxl8d](pxl8d) server - **SDL3** - System package or auto-vendored +- **MSYS2** (Windows only) - Provides `make` and Unix tools (`scoop install msys2`) ### License diff --git a/pxl8.sh b/pxl8.sh deleted file mode 100755 index 33e265d..0000000 --- a/pxl8.sh +++ /dev/null @@ -1,818 +0,0 @@ -#!/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' - -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 -} - -generate_compile_commands() { - local project_dir - project_dir="$(pwd)" - local tmpfile - tmpfile=$(mktemp) - local first=true - - echo "[" > "$tmpfile" - - for src_file in $LIB_SOURCE_FILES; do - src_file="${src_file## }" - src_file="${src_file%% }" - [[ -z "$src_file" ]] && continue - if $first; then first=false; else printf ",\n" >> "$tmpfile"; fi - printf ' {\n "directory": "%s",\n "command": "clang -c %s %s",\n "file": "%s"\n }' \ - "$project_dir" "$DEP_COMPILE_FLAGS" "$src_file" "$src_file" >> "$tmpfile" - done - - for src_file in $PXL8_SOURCE_FILES; do - src_file="${src_file## }" - src_file="${src_file%% }" - [[ -z "$src_file" ]] && continue - if $first; then first=false; else printf ",\n" >> "$tmpfile"; fi - printf ' {\n "directory": "%s",\n "command": "clang -c %s %s",\n "file": "%s"\n }' \ - "$project_dir" "$COMPILE_FLAGS" "$src_file" "$src_file" >> "$tmpfile" - done - - printf "\n]\n" >> "$tmpfile" - mv "$tmpfile" "compile_commands.json" -} - -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" - HAS_SDL3=1 -} - -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/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_glows.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_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/world/pxl8_entity.c - src/world/pxl8_world.c - src/world/pxl8_world_chunk.c - src/world/pxl8_world_chunk_cache.c - " - - if [[ "$HAS_SDL3" -eq 1 ]]; then - PXL8_SOURCE_FILES="$PXL8_SOURCE_FILES src/hal/pxl8_io_sdl3.c src/hal/pxl8_mem_sdl3.c" - else - PXL8_SOURCE_FILES="$PXL8_SOURCE_FILES src/hal/pxl8_mem.c" - fi - - generate_compile_commands - - 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 diff --git a/pxl8d/src/main.rs b/pxl8d/src/main.rs index da3d2a9..2fb3993 100644 --- a/pxl8d/src/main.rs +++ b/pxl8d/src/main.rs @@ -26,7 +26,7 @@ fn get_time_ns() -> u64 { static mut FREQ: i64 = 0; unsafe { if FREQ == 0 { - QueryPerformanceFrequency(&mut FREQ); + QueryPerformanceFrequency(&raw mut FREQ); } let mut count: i64 = 0; QueryPerformanceCounter(&mut count); diff --git a/pxl8d/src/transport.rs b/pxl8d/src/transport.rs index 654bda4..d8451cd 100644 --- a/pxl8d/src/transport.rs +++ b/pxl8d/src/transport.rs @@ -79,6 +79,8 @@ mod sys { pub type RawSocket = c_int; pub const INVALID_SOCKET: RawSocket = -1; + pub fn init() {} + pub fn socket() -> RawSocket { unsafe { libc::socket(libc::AF_INET, libc::SOCK_DGRAM, 0) } } @@ -139,6 +141,8 @@ mod sys { pub type RawSocket = c_int; pub const INVALID_SOCKET: RawSocket = -1; + pub fn init() {} + pub fn socket() -> RawSocket { unsafe { libc::socket(libc::AF_INET, libc::SOCK_DGRAM, 0) } } @@ -195,59 +199,66 @@ mod sys { #[cfg(windows)] mod sys { - use windows_sys::Win32::Networking::WinSock::*; + use windows_sys::Win32::Networking::WinSock as ws; - pub type RawSocket = SOCKET; - pub const INVALID_SOCKET_VAL: RawSocket = INVALID_SOCKET; + pub type RawSocket = ws::SOCKET; + pub const INVALID_SOCKET: RawSocket = ws::INVALID_SOCKET; + + pub fn init() { + unsafe { + let mut wsa: ws::WSADATA = core::mem::zeroed(); + ws::WSAStartup(0x0202, &mut wsa); + } + } pub fn socket() -> RawSocket { - unsafe { socket(AF_INET as i32, SOCK_DGRAM as i32, 0) } + unsafe { ws::socket(ws::AF_INET as i32, ws::SOCK_DGRAM as i32, 0) } } pub fn bind(sock: RawSocket, port: u16) -> i32 { - let addr = SOCKADDR_IN { - sin_family: AF_INET, + let addr = ws::SOCKADDR_IN { + sin_family: ws::AF_INET, sin_port: port.to_be(), - sin_addr: IN_ADDR { S_un: IN_ADDR_0 { S_addr: u32::from_be_bytes([127, 0, 0, 1]).to_be() } }, + sin_addr: ws::IN_ADDR { S_un: ws::IN_ADDR_0 { S_addr: 0 } }, sin_zero: [0; 8], }; - unsafe { bind(sock, &addr as *const _ as *const SOCKADDR, core::mem::size_of::() as i32) } + unsafe { ws::bind(sock, &addr as *const _ as *const ws::SOCKADDR, core::mem::size_of::() as i32) } } pub fn set_nonblocking(sock: RawSocket) -> i32 { let mut nonblocking: u32 = 1; - unsafe { ioctlsocket(sock, FIONBIO as i32, &mut nonblocking) } + unsafe { ws::ioctlsocket(sock, ws::FIONBIO as i32, &mut nonblocking) } } - pub fn recvfrom(sock: RawSocket, buf: &mut [u8], addr: &mut SOCKADDR_IN) -> i32 { - let mut addr_len = core::mem::size_of::() as i32; + pub fn recvfrom(sock: RawSocket, buf: &mut [u8], addr: &mut ws::SOCKADDR_IN) -> i32 { + let mut addr_len = core::mem::size_of::() as i32; unsafe { - recvfrom( + ws::recvfrom( sock, buf.as_mut_ptr(), buf.len() as i32, 0, - addr as *mut _ as *mut SOCKADDR, + addr as *mut _ as *mut ws::SOCKADDR, &mut addr_len, ) } } - pub fn sendto(sock: RawSocket, buf: &[u8], addr: &SOCKADDR_IN) -> i32 { + pub fn sendto(sock: RawSocket, buf: &[u8], addr: &ws::SOCKADDR_IN) -> i32 { unsafe { - sendto( + ws::sendto( sock, buf.as_ptr(), buf.len() as i32, 0, - addr as *const _ as *const SOCKADDR, - core::mem::size_of::() as i32, + addr as *const _ as *const ws::SOCKADDR, + core::mem::size_of::() as i32, ) } } pub fn close(sock: RawSocket) { - unsafe { closesocket(sock) }; + unsafe { ws::closesocket(sock) }; } } @@ -257,6 +268,7 @@ type SockAddr = libc::sockaddr_in; #[cfg(windows)] type SockAddr = windows_sys::Win32::Networking::WinSock::SOCKADDR_IN; + pub struct Transport { client_addr: SockAddr, has_client: bool, @@ -267,6 +279,7 @@ pub struct Transport { impl Transport { pub fn bind(port: u16) -> Option { + sys::init(); let sock = sys::socket(); if sock == sys::INVALID_SOCKET { return None; diff --git a/src/gfx/pxl8_shader_runtime.c b/src/gfx/pxl8_shader_runtime.c index 14de4f7..e6b689f 100644 --- a/src/gfx/pxl8_shader_runtime.c +++ b/src/gfx/pxl8_shader_runtime.c @@ -1,7 +1,13 @@ #include "pxl8_shader_runtime.h" #include "pxl8_log.h" +#ifdef _WIN32 +#define WIN32_LEAN_AND_MEAN +#include +#else #include +#endif + #include #include #include @@ -11,10 +17,45 @@ struct pxl8_shader_lib { char path[256]; }; +static void* lib_open(const char* path) { +#ifdef _WIN32 + return (void*)LoadLibraryA(path); +#else + return dlopen(path, RTLD_NOW); +#endif +} + +static void lib_close(void* handle) { +#ifdef _WIN32 + FreeLibrary((HMODULE)handle); +#else + dlclose(handle); +#endif +} + +static void* lib_sym(void* handle, const char* name) { +#ifdef _WIN32 + return (void*)GetProcAddress((HMODULE)handle, name); +#else + return dlsym(handle, name); +#endif +} + +static const char* lib_error(void) { +#ifdef _WIN32 + static char buf[256]; + FormatMessageA(FORMAT_MESSAGE_FROM_SYSTEM, NULL, GetLastError(), + 0, buf, sizeof(buf), NULL); + return buf; +#else + return dlerror(); +#endif +} + pxl8_shader_lib* pxl8_shader_lib_load(const char* path) { - void* handle = dlopen(path, RTLD_NOW); + void* handle = lib_open(path); if (!handle) { - pxl8_error("Failed to load shader library: %s - %s", path, dlerror()); + pxl8_error("Failed to load shader library: %s - %s", path, lib_error()); return NULL; } @@ -29,7 +70,7 @@ pxl8_shader_lib* pxl8_shader_lib_load(const char* path) { void pxl8_shader_lib_unload(pxl8_shader_lib* lib) { if (!lib) return; if (lib->handle) { - dlclose(lib->handle); + lib_close(lib->handle); } free(lib); } @@ -38,12 +79,12 @@ bool pxl8_shader_lib_reload(pxl8_shader_lib* lib) { if (!lib) return false; if (lib->handle) { - dlclose(lib->handle); + lib_close(lib->handle); } - lib->handle = dlopen(lib->path, RTLD_NOW); + lib->handle = lib_open(lib->path); if (!lib->handle) { - pxl8_error("Failed to reload shader library: %s - %s", lib->path, dlerror()); + pxl8_error("Failed to reload shader library: %s - %s", lib->path, lib_error()); return false; } @@ -56,9 +97,9 @@ pxl8_shader_fn pxl8_shader_lib_get(pxl8_shader_lib* lib, const char* name) { char symbol[128]; snprintf(symbol, sizeof(symbol), "pxl8_shader_%s", name); - void* fn = dlsym(lib->handle, symbol); + void* fn = lib_sym(lib->handle, symbol); if (!fn) { - pxl8_error("Failed to find shader: %s - %s", symbol, dlerror()); + pxl8_error("Failed to find shader: %s - %s", symbol, lib_error()); return NULL; } diff --git a/src/net/pxl8_net.c b/src/net/pxl8_net.c index b60ac4c..62093bb 100644 --- a/src/net/pxl8_net.c +++ b/src/net/pxl8_net.c @@ -21,6 +21,7 @@ #include #include typedef SOCKET socket_t; + typedef int ssize_t; #define INVALID_SOCK INVALID_SOCKET #define close_socket closesocket #else @@ -108,7 +109,7 @@ pxl8_result pxl8_net_connect(pxl8_net* net) { #endif int rcvbuf = 1024 * 1024; - setsockopt(net->sock, SOL_SOCKET, SO_RCVBUF, &rcvbuf, sizeof(rcvbuf)); + setsockopt(net->sock, SOL_SOCKET, SO_RCVBUF, (const char*)&rcvbuf, sizeof(rcvbuf)); memset(&net->server_addr, 0, sizeof(net->server_addr)); #ifdef __APPLE__ diff --git a/src/script/pxl8_repl.c b/src/script/pxl8_repl.c index 048a1ac..42bdb8a 100644 --- a/src/script/pxl8_repl.c +++ b/src/script/pxl8_repl.c @@ -1,13 +1,16 @@ #include "pxl8_repl.h" #include -#include #include #include #include #include #include + +#ifndef _WIN32 +#include #include +#endif #include "pxl8_log.h" #include "pxl8_mem.h" @@ -37,38 +40,71 @@ struct pxl8_repl { static pxl8_repl* g_repl = NULL; -static void pxl8_repl_completion(const char* buf, linenoiseCompletions* lc) { - const char* fennel_keywords[] = { - "fn", "let", "var", "set", "global", "local", - "if", "when", "do", "while", "for", "each", - "lambda", "λ", "partial", "macro", "macros", - "require", "include", "import-macros", - "values", "select", "table", "length", - ".", "..", ":", "->", "->>", "-?>", "-?>>", - "doto", "match", "case", "pick-values", - "collect", "icollect", "accumulate" - }; +static const char* fennel_keywords[] = { + "fn", "let", "var", "set", "global", "local", + "if", "when", "do", "while", "for", "each", + "lambda", "λ", "partial", "macro", "macros", + "require", "include", "import-macros", + "values", "select", "table", "length", + ".", "..", ":", "->", "->>", "-?>", "-?>>", + "doto", "match", "case", "pick-values", + "collect", "icollect", "accumulate" +}; - const char* pxl8_functions[] = { - "pxl8.clr", "pxl8.pixel", "pxl8.get_pixel", - "pxl8.line", "pxl8.rect", "pxl8.rect_fill", - "pxl8.circle", "pxl8.circle_fill", "pxl8.text", - "pxl8.get_screen", "pxl8.info", "pxl8.warn", - "pxl8.error", "pxl8.debug", "pxl8.trace" - }; +static const char* pxl8_functions[] = { + "pxl8.clr", "pxl8.pixel", "pxl8.get_pixel", + "pxl8.line", "pxl8.rect", "pxl8.rect_fill", + "pxl8.circle", "pxl8.circle_fill", "pxl8.text", + "pxl8.get_screen", "pxl8.info", "pxl8.warn", + "pxl8.error", "pxl8.debug", "pxl8.trace" +}; +#ifdef PXL8_WIN32_LINENOISE + +static void pxl8_repl_completion(const char* buf, linenoiseCompletions* lc, void* userdata) { + (void)userdata; usize buf_len = strlen(buf); - for (usize i = 0; i < sizeof(fennel_keywords) / sizeof(fennel_keywords[0]); i++) { - if (strncmp(buf, fennel_keywords[i], buf_len) == 0) { + if (strncmp(buf, fennel_keywords[i], buf_len) == 0) linenoiseAddCompletion(lc, fennel_keywords[i]); - } } - for (usize i = 0; i < sizeof(pxl8_functions) / sizeof(pxl8_functions[0]); i++) { - if (strncmp(buf, pxl8_functions[i], buf_len) == 0) { + if (strncmp(buf, pxl8_functions[i], buf_len) == 0) + linenoiseAddCompletion(lc, pxl8_functions[i]); + } +} + +static char* pxl8_repl_hints(const char* buf, int* color, int* bold, void* userdata) { + (void)userdata; + if (strncmp(buf, "pxl8.", 5) == 0 && strlen(buf) == 5) { + *color = 35; + *bold = 0; + return "clr|pixel|line|rect|circle|text|get_screen"; + } + if (strcmp(buf, "(fn") == 0) { + *color = 36; + *bold = 0; + return " [args] body)"; + } + if (strcmp(buf, "(let") == 0) { + *color = 36; + *bold = 0; + return " [bindings] body)"; + } + return NULL; +} + +#else + +static void pxl8_repl_completion(const char* buf, linenoiseCompletions* lc) { + usize buf_len = strlen(buf); + for (usize i = 0; i < sizeof(fennel_keywords) / sizeof(fennel_keywords[0]); i++) { + if (strncmp(buf, fennel_keywords[i], buf_len) == 0) + linenoiseAddCompletion(lc, fennel_keywords[i]); + } + for (usize i = 0; i < sizeof(pxl8_functions) / sizeof(pxl8_functions[0]); i++) { + if (strncmp(buf, pxl8_functions[i], buf_len) == 0) linenoiseAddCompletion(lc, pxl8_functions[i]); - } } } @@ -78,22 +114,21 @@ static char* pxl8_repl_hints(const char* buf, int* color, int* bold) { *bold = 0; return "clr|pixel|line|rect|circle|text|get_screen"; } - if (strcmp(buf, "(fn") == 0) { *color = 36; *bold = 0; return " [args] body)"; } - if (strcmp(buf, "(let") == 0) { *color = 36; *bold = 0; return " [bindings] body)"; } - return NULL; } +#endif + static void pxl8_repl_flush_logs(pxl8_repl* repl) { u32 log_read_idx = atomic_load(&repl->log_read_idx); u32 log_write_idx = atomic_load(&repl->log_write_idx); @@ -106,6 +141,68 @@ static void pxl8_repl_flush_logs(pxl8_repl* repl) { fflush(stdout); } +#ifdef _WIN32 + +static int pxl8_repl_thread(void* arg) { + pxl8_repl* repl = (pxl8_repl*)arg; + + printf("[pxl8 REPL] Fennel 1.6.0 - Tab for completion, Ctrl-Z to exit\n"); + fflush(stdout); + + while (!atomic_load(&repl->should_quit)) { + pxl8_repl_flush_logs(repl); + + const char* prompt = (repl->accumulator[0] != '\0') ? ".. " : ">> "; + char* line = linenoise(prompt); + + if (line == NULL) { + atomic_store(&repl->should_quit, true); + break; + } + + bool in_multiline = (repl->accumulator[0] != '\0'); + + if (strlen(line) > 0 || in_multiline) { + if (!in_multiline) { + linenoiseHistoryAdd(line); + linenoiseHistorySave(".pxl8_history"); + } + + if (repl->accumulator[0] != '\0') { + strncat(repl->accumulator, "\n", + PXL8_MAX_REPL_COMMAND_SIZE - strlen(repl->accumulator) - 1); + } + strncat(repl->accumulator, line, + PXL8_MAX_REPL_COMMAND_SIZE - strlen(repl->accumulator) - 1); + + u32 write_idx = atomic_load(&repl->cmd_write_idx); + u32 read_idx = atomic_load(&repl->cmd_read_idx); + u32 next_write = (write_idx + 1) % PXL8_REPL_QUEUE_SIZE; + + if (next_write != read_idx) { + strncpy(repl->commands[write_idx], repl->accumulator, PXL8_MAX_REPL_COMMAND_SIZE - 1); + repl->commands[write_idx][PXL8_MAX_REPL_COMMAND_SIZE - 1] = '\0'; + atomic_store(&repl->cmd_write_idx, next_write); + } + } + + free(line); + + while (!atomic_load(&repl->should_quit) && + (atomic_load(&repl->cmd_write_idx) != atomic_load(&repl->cmd_read_idx) || + !atomic_load(&repl->cmd_complete))) { + pxl8_repl_flush_logs(repl); + SDL_Delay(1); + } + atomic_store(&repl->cmd_complete, false); + } + + pxl8_repl_flush_logs(repl); + return 0; +} + +#else + static int pxl8_repl_thread(void* arg) { pxl8_repl* repl = (pxl8_repl*)arg; @@ -217,6 +314,8 @@ static int pxl8_repl_thread(void* arg) { return 0; } +#endif + pxl8_repl* pxl8_repl_create(void) { pxl8_repl* repl = (pxl8_repl*)pxl8_calloc(1, sizeof(pxl8_repl)); if (!repl) return NULL; @@ -231,8 +330,13 @@ pxl8_repl* pxl8_repl_create(void) { linenoiseHistorySetMaxLen(100); linenoiseSetMultiLine(1); +#ifdef PXL8_WIN32_LINENOISE + linenoiseSetCompletionCallback(pxl8_repl_completion, NULL); + linenoiseSetHintsCallback(pxl8_repl_hints, NULL); +#else linenoiseSetCompletionCallback(pxl8_repl_completion); linenoiseSetHintsCallback(pxl8_repl_hints); +#endif linenoiseHistoryLoad(".pxl8_history"); g_repl = repl; @@ -264,7 +368,9 @@ void pxl8_repl_destroy(pxl8_repl* repl) { g_repl = NULL; pxl8_log_set_handler(NULL); +#ifndef _WIN32 system("stty sane 2>/dev/null"); +#endif pxl8_free(repl); } diff --git a/src/script/pxl8_script.c b/src/script/pxl8_script.c index 9acd828..6a518fb 100644 --- a/src/script/pxl8_script.c +++ b/src/script/pxl8_script.c @@ -4,6 +4,10 @@ #include #include +#ifndef PATH_MAX +#define PATH_MAX 260 +#endif + #include #include #include