From c18896def036af48d156c02a4ce86a6d64461396 Mon Sep 17 00:00:00 2001 From: asrael Date: Wed, 13 Aug 2025 15:04:49 -0500 Subject: [PATCH] initial commit --- .gitignore | 1 + README.md | 6 + compile_flags.txt | 12 + pxl8.sh | 469 ++++++++++++++++++++++++ res/palettes/gruvbox.ase | Bin 0 -> 485 bytes res/palettes/sweaft-64.ase | Bin 0 -> 904 bytes res/sprites/pxl8.ase | Bin 0 -> 805 bytes res/sprites/pxl8.png | Bin 0 -> 1381 bytes res/sprites/trees.ase | Bin 0 -> 802 bytes src/fnl/demo-effects.fnl | 81 +++++ src/fnl/demo.fnl | 29 ++ src/lua/pxl8.lua | 192 ++++++++++ src/pxl8.c | 479 +++++++++++++++++++++++++ src/pxl8_ase.c | 400 +++++++++++++++++++++ src/pxl8_ase.h | 98 +++++ src/pxl8_blit.c | 58 +++ src/pxl8_blit.h | 19 + src/pxl8_font.c | 60 ++++ src/pxl8_font.h | 130 +++++++ src/pxl8_gfx.c | 713 +++++++++++++++++++++++++++++++++++++ src/pxl8_gfx.h | 123 +++++++ src/pxl8_io.c | 106 ++++++ src/pxl8_io.h | 31 ++ src/pxl8_lua.c | 279 +++++++++++++++ src/pxl8_lua.h | 24 ++ src/pxl8_macros.h | 83 +++++ src/pxl8_simd.h | 189 ++++++++++ src/pxl8_types.h | 72 ++++ src/pxl8_vfx.c | 461 ++++++++++++++++++++++++ src/pxl8_vfx.h | 68 ++++ 30 files changed, 4183 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 compile_flags.txt create mode 100755 pxl8.sh create mode 100644 res/palettes/gruvbox.ase create mode 100644 res/palettes/sweaft-64.ase create mode 100755 res/sprites/pxl8.ase create mode 100755 res/sprites/pxl8.png create mode 100644 res/sprites/trees.ase create mode 100644 src/fnl/demo-effects.fnl create mode 100644 src/fnl/demo.fnl create mode 100644 src/lua/pxl8.lua create mode 100644 src/pxl8.c create mode 100644 src/pxl8_ase.c create mode 100644 src/pxl8_ase.h create mode 100644 src/pxl8_blit.c create mode 100644 src/pxl8_blit.h create mode 100644 src/pxl8_font.c create mode 100644 src/pxl8_font.h create mode 100644 src/pxl8_gfx.c create mode 100644 src/pxl8_gfx.h create mode 100644 src/pxl8_io.c create mode 100644 src/pxl8_io.h create mode 100644 src/pxl8_lua.c create mode 100644 src/pxl8_lua.h create mode 100644 src/pxl8_macros.h create mode 100644 src/pxl8_simd.h create mode 100644 src/pxl8_types.h create mode 100644 src/pxl8_vfx.c create mode 100644 src/pxl8_vfx.h diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7176f4b --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +**.DS_Store diff --git a/README.md b/README.md new file mode 100644 index 0000000..07a4a7c --- /dev/null +++ b/README.md @@ -0,0 +1,6 @@ +

+ pixel 8 logo +

+ +> [!WARNING] +> Heavy development. So... *here be dragons :3* diff --git a/compile_flags.txt b/compile_flags.txt new file mode 100644 index 0000000..37d534d --- /dev/null +++ b/compile_flags.txt @@ -0,0 +1,12 @@ +-std=c23 +-DDEBUG +-DPXL8_GFX_IMPLEMENTATION +-Wall +-Wextra +-Ilib/linenoise +-Ilib/luajit/src +-Ilib/microui/src +-Ilib/SDL_shadercross/include +-I/opt/homebrew/include +-Isrc +-isystem diff --git a/pxl8.sh b/pxl8.sh new file mode 100755 index 0000000..b05e31e --- /dev/null +++ b/pxl8.sh @@ -0,0 +1,469 @@ +#!/bin/bash + +set -e + +CC="${CC:-gcc}" +CFLAGS="-std=c23 -Wall -Wextra" +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" + ;; + Darwin) + ;; + MINGW*|MSYS*|CYGWIN*) + LINKER_FLAGS="$LINKER_FLAGS -Wl,--export-all-symbols" + ;; + *) + LINKER_FLAGS="$LINKER_FLAGS -rdynamic" + ;; +esac + +RED='\033[0;31m' +GREEN='\033[0;32m' +BLUE='\033[0;34m' +BOLD='\033[1m' +NC='\033[0m' + +if [[ "$(uname)" == "Linux" ]]; then + CFLAGS="$CFLAGS -D_GNU_SOURCE" +fi + +print_info() { + echo -e "${BLUE}${BOLD}info:${NC} $1" +} + +print_success() { + echo -e "${GREEN}${BOLD}✓${NC} $1" +} + +print_error() { + echo -e "${RED}${BOLD}error:${NC} $1" >&2 +} + +detect_simd() { + local simd_flags="" + + case "$(uname -m)" in + x86_64|amd64) + if $CC -mavx2 -c -x c /dev/null -o /dev/null 2>/dev/null; then + simd_flags="-mavx2 -msse2" + elif $CC -msse2 -c -x c /dev/null -o /dev/null 2>/dev/null; then + simd_flags="-msse2" + fi + ;; + arm64|aarch64) + if $CC -march=armv8-a+simd -c -x c /dev/null -o /dev/null 2>/dev/null; then + simd_flags="-march=armv8-a+simd" + fi + ;; + esac + + echo "$simd_flags" +} + +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 " build Build pxl8" + echo " run Build and run pxl8 [script.fnl|script.lua]" + echo " clean Remove build artifacts" + echo " update Download/update all dependencies" + echo " vendor Build specific dependencies from source" + echo " help Show this help message" + echo + echo -e "${BOLD}OPTIONS:${NC}" + echo " --all Remove all artifacts including dependencies when cleaning" + echo " --release Build/run in release mode" + echo + echo -e "${BOLD}VENDOR OPTIONS:${NC}" + echo " --sdl Build SDL3 from source" + echo " --all Build all vendorable dependencies" +} + +COMMAND="$1" +shift || true + +for arg in "$@"; do + case $arg in + --release) + MODE="release" + ;; + esac +done + +SIMD_FLAGS=$(detect_simd) + +if [ "$MODE" = "release" ]; then + CFLAGS="$CFLAGS -O3 -march=native -mtune=native -ffast-math -funroll-loops -fno-unwind-tables -fno-asynchronous-unwind-tables $SIMD_FLAGS" + + BUILDDIR="$BUILDDIR/release" + BINDIR="$BINDIR/release" +else + CFLAGS="$CFLAGS -g -O1 -DDEBUG $SIMD_FLAGS" + BUILDDIR="$BUILDDIR/debug" + BINDIR="$BINDIR/debug" +fi + +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) + SDL3_CFLAGS="-I/usr/local/include/SDL3" + SDL3_LIBS="-L/usr/local/lib -lSDL3" + ;; + Linux) + SDL3_CFLAGS="-I/usr/include/SDL3" + SDL3_LIBS="-lSDL3" + ;; + MINGW*|MSYS*|CYGWIN*) + SDL3_CFLAGS="-I/mingw64/include/SDL3" + SDL3_LIBS="-lSDL3" + ;; + esac + fi + print_info "Using system SDL3" + fi + + CFLAGS="$CFLAGS $SDL3_CFLAGS" + LIBS="$LIBS $SDL3_LIBS $SDL3_RPATH" +} + +update_linenoise() { + print_info "Fetching linenoise" + mkdir -p lib/linenoise + + if curl -s --max-time 5 -o lib/linenoise/linenoise.c https://raw.githubusercontent.com/antirez/linenoise/master/linenoise.c 2>/dev/null && \ + curl -s --max-time 5 -o lib/linenoise/linenoise.h https://raw.githubusercontent.com/antirez/linenoise/master/linenoise.h 2>/dev/null; then + print_success "Updated linenoise" + else + print_error "Failed to download linenoise" + return 1 + fi +} + +update_fennel() { + print_info "Fetching Fennel" + mkdir -p lib/fennel + + local version="1.5.3" + + if curl -s --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_success "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_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_success "Updated LuaJIT (${version})" +} + +build_luajit() { + if [[ ! -f "lib/luajit/src/libluajit.a" ]]; then + print_info "Building LuaJIT" + cd lib/luajit + + if [[ "$(uname)" == "Darwin" ]]; then + export MACOSX_DEPLOYMENT_TARGET="10.11" + fi + + make clean >/dev/null 2>&1 || true + make -j$(nproc 2>/dev/null || echo 4) > /dev/null 2>&1 + cd - > /dev/null + print_success "Built LuaJIT" + fi +} + +update_microui() { + print_info "Fetching microui" + mkdir -p lib/microui/src + + if curl -s --max-time 5 -o lib/microui/src/microui.c https://raw.githubusercontent.com/rxi/microui/master/src/microui.c 2>/dev/null && \ + curl -s --max-time 5 -o lib/microui/src/microui.h https://raw.githubusercontent.com/rxi/microui/master/src/microui.h 2>/dev/null; then + print_success "Updated microui" + else + print_error "Failed to download microui" + return 1 + fi +} + +update_miniz() { + print_info "Fetching miniz" + mkdir -p lib/miniz + + local version="3.0.2" + + if curl -sL --max-time 10 -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_success "Updated miniz (${version})" + else + print_error "Failed to extract miniz files" + return 1 + fi + else + print_error "Failed to download miniz" + return 1 + fi +} + +vendor_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_success "Updated SDL3" +} + +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_success "Built SDL3" + fi +} + +case "$COMMAND" in + build) + mkdir -p "$BUILDDIR" + mkdir -p "$BINDIR" + + if [[ ! -f "lib/microui/src/microui.c" ]] || \ + [[ ! -d "lib/luajit" ]] || \ + [[ ! -f "lib/linenoise/linenoise.c" ]] || \ + [[ ! -f "lib/miniz/miniz.c" ]] || \ + [[ ! -f "lib/fennel/fennel.lua" ]]; then + print_info "Missing dependencies, fetching..." + + update_linenoise + update_fennel + update_luajit + update_microui + 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 + + setup_sdl3 + print_info "Building pxl8 ($MODE mode)" + + if [[ -n "$SIMD_FLAGS" ]]; then + case "$(uname -m)" in + x86_64|amd64) + if [[ "$SIMD_FLAGS" == *"mavx2"* ]]; then + print_info "SIMD: AVX2 + SSE2 enabled" + elif [[ "$SIMD_FLAGS" == *"msse2"* ]]; then + print_info "SIMD: SSE2 enabled" + fi + ;; + arm64|aarch64) + print_info "SIMD: ARM NEON enabled" + ;; + esac + else + print_info "SIMD: Scalar fallback" + fi + + INCLUDES="-Isrc -Ilib -Ilib/microui/src -Ilib/luajit/src -Ilib/linenoise -Ilib/miniz" + COMPILE_FLAGS="$CFLAGS $INCLUDES" + + EXECUTABLE="$BINDIR/pxl8" + LIB_SOURCE_FILES="lib/linenoise/linenoise.c lib/miniz/miniz.c" + SRC_SOURCE_FILES="src/pxl8.c src/pxl8_gfx.c src/pxl8_ase.c src/pxl8_font.c src/pxl8_io.c src/pxl8_lua.c src/pxl8_vfx.c src/pxl8_blit.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 + print_info "Compiling: $src_file" + if ! $CC -c $COMPILE_FLAGS "$src_file" -o "$obj_file"; then + print_error "Compilation failed for $src_file" + exit 1 + fi + SOURCES_COMPILED="yes" + fi + done + + for src_file in $SRC_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" ]] || \ + [[ "src/pxl8_types.h" -nt "$obj_file" ]] || \ + [[ "src/pxl8_macros.h" -nt "$obj_file" ]]; then + NEED_LINK=true + print_info "Compiling: $src_file" + if ! $CC -c $COMPILE_FLAGS "$src_file" -o "$obj_file"; then + print_error "Compilation failed for $src_file" + exit 1 + fi + SOURCES_COMPILED="yes" + fi + done + + if [[ "$LUAJIT_LIB" -nt "$EXECUTABLE" ]] || [[ "$NEED_LINK" == true ]]; then + print_info "Linking executable" + if ! $CC $LINKER_FLAGS $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_success "Build complete" + print_info "Binary: $EXECUTABLE" + ;; + + run) + "$0" build "$@" || exit 1 + + SCRIPT="" + for arg in "$@"; do + if [[ "$arg" != "--release" ]]; then + SCRIPT="$arg" + break + fi + done + + if [[ -z "$SCRIPT" ]]; then + SCRIPT="src/fnl/demo.fnl" + fi + + "$BINDIR/pxl8" "$SCRIPT" + ;; + + clean) + if [[ "$1" == "--all" ]] || [[ "$1" == "--deps" ]]; then + print_info "Removing all build artifacts and dependencies" + rm -rf "$BUILDDIR" "$BINDIR" lib + else + print_info "Removing build artifacts" + rm -rf "$BUILDDIR" "$BINDIR" + fi + print_success "Cleaned" + ;; + + update) + update_linenoise + update_fennel + update_luajit + update_microui + update_miniz + print_success "All dependencies updated" + ;; + + vendor) + for arg in "$@"; do + case $arg in + --sdl) + vendor_sdl + ;; + --all) + vendor_sdl + ;; + *) + ;; + esac + done + + if [[ -z "$1" ]]; then + vendor_sdl + fi + ;; + + help|--help|-h|"") + print_usage + ;; + + *) + print_error "Unknown command: $COMMAND" + print_usage + exit 1 + ;; +esac diff --git a/res/palettes/gruvbox.ase b/res/palettes/gruvbox.ase new file mode 100644 index 0000000000000000000000000000000000000000..d667cb470df21083aac1a457a8eba8604089cb82 GIT binary patch literal 485 zcmaFL$iVPmDIZ zB(EM>2qa(M-UK9ne>@H(=Q6MX$ulalAQ4X!Ai3U(8Ax7>Ifj2F*gfH_8q7JlI&>}K+?6f14uS6odG2K?F)hA z#-uhNdAV)&e_5bcSQMC%JOq?rU}f-0tV}I(0x=aJa{rkiGy?-D!Ybw@rzE8%rY1js k@Z{06hfg1i3y27b35p6&pD<<8w24zEhchsUH!ukT09zGkssI20 literal 0 HcmV?d00001 diff --git a/res/palettes/sweaft-64.ase b/res/palettes/sweaft-64.ase new file mode 100644 index 0000000000000000000000000000000000000000..2a48de009fa97f5ca96500d83f4719ce3d8654bf GIT binary patch literal 904 zcmcJN&r1|x7{}kST}yC7gk4$LbqB&icGpGGgamQ4gdz~PQV$A>E95~BmRJW5(H^uA zs)Hm}N>M}|1R*;`gmmc=iC~w&2t0HNZ?eLS-_Pc+f4~{$!|=SrJn#GcJd-Dk*?4W4 zW@8y!k1-iNhnHh4il#*Oaa^a=Key$}clD`3NY6<--||Si1r@@!vsY<8vaL0%aq;Ug`*Ab2_#^3vxeFQbSJ$w3W2QyidBon{X9vZ(piYu2V#cQ)vN@iBX+y7i~Vg9lBJ%hpeEb=wm?*Y%=`(q~KM!KDS`H&^@b4vtpJjo<#i;uY+F!NA>lW*N(K z@r4^QBPI4qe|qtEc0}5E!Y8gVjWLoWHPRxJd?$RWwVn6e6r{}~uGG&F#ujfEMIoYY(bB=@eH3?yIQ-UK9ne>@H( ze}26VB>(;U2PFIL3xVXuq&6UVxotL(TyMn;B(KHt0m*+2l0b4U0~?S$qaq6=e|wq$ z$;uLt?2PGBKyt@!H6XdQOaw??p9ylquf6&}l0B^gNV>Lm0LkX1Gk~O1bp(*in41L> zIZ*Ro3Fved6rTbm7`Pac5|gvji}FkJQsl9!U}q>UDatHJEmj0e1LF@4Kn8F!6jbC` zD3qjDlz0HStO^XkSQTOT4-97rsAG^}sF?HiEF&+20SEJj!avn+$387Fy^s_5P4unF z_l>vTmhHZqxBd3p|LaqTZ zI{|8UlN}Sk?+duSmoKDjK6~$7d;aT5u{(hdU{zpZ;Aap=GAD^a6ll)bsRxA`6gXV= z7CiW0ze8;{Q<w{@`u5eWTBz$DOm)CY<}s`9ydx`?fCszeiP5?yKfB3M*U# E07jnO7ytkO literal 0 HcmV?d00001 diff --git a/res/sprites/pxl8.png b/res/sprites/pxl8.png new file mode 100755 index 0000000000000000000000000000000000000000..ad85c8efd03c3fa1a8255e8c213e6fbf83af7e7d GIT binary patch literal 1381 zcmeAS@N?(olHy`uVBq!ia0y~yU;;838911M)F(f?P!O*;$lZxy-8q?;Ku&CcPlzj! zmQ_&H(9p24Fq_m|vUlC&*S9zQ{&@W7*X#fO{p+_c+?dpMxo!4(E9PsleE%9G=Q6OJ zQIY-aX;N9jH)Fcgj@@co%S5ivRQ$D9pFOR@wY8&p=?tgph>W>e%?E0Z^)sIWS|DE% z9) zkiS>^)#=UGieg5K8DKGS&I9Kajf=yss(qh-v+$}9J%BH1r zIV=xj$kVe+PCK_MigCxzA3ZZIS7)&%99W&|&TnbTFg5PvPo@hiMH?8I0_q#Q)<3BM zx`XS0Q@Q=K)yH=jzrL0J^xW0Dm$Sqe1sE73b{21q-?hw@f#tz(&I5CQeQ{n|Ciiyw zx=g`_v)29JysvNJIPh$)`|qXMQ7j3c&n^CY$yC>$@yFY{?^)N|o$A^(@z>3Jt06in z_v~cI+3_`Re~E85L(VQ##_4j|hKuz}Du4V)*EU}Uv%+`#J&0ZCj7qKp`tR#>E4~JX zUAg-<^yT*C=j~w~?&Uus;%EK`1~|vrm0yGzKpqfiNM<&;RSgVmj&qSWG3hhQ8UB2I z_r58d``*gmKbU=s+V9D&wA8L%KkFyh9ISetGTtaX^NQcpNd4yT+uSE-G6~F?{6?4I zp=?@J|2fZZVhtbojQ6@fSE;V|WcXL{J>Bu`PW#)Y+lwBbIFkbO{^`eOSrtr-Z|cWh zR$@3f^CUR1InMcjf>Ve8z-sxlb(4xO1C7V7gu|gAZ4)q3o@z4u-}8I>gRAZZQoC9 z-p+ks8PJM*%If*|4Ig_kSZ$r^zVhC_?CVwT jjng6dU6W9e;c&A)JNowPS#Q&uKuSGb{an^LB{Ts5$ohvx literal 0 HcmV?d00001 diff --git a/res/sprites/trees.ase b/res/sprites/trees.ase new file mode 100644 index 0000000000000000000000000000000000000000..95c65d73ae3117ce7c178086be71ca678fd878d2 GIT binary patch literal 802 zcmY#lW?*=*l##)JL4kpTfsuiMAq9vLK!Je~B*-8DgtP#Qm>3v-ePjdM$_BJR42ank zz*Ztzmjz_AfLy1r&rAP$ne(eFtDYrDPVPfR}U zYuD+a7ULvoV<+U-$uRS_&Wo@A|NlR5`cC)kEm4^rRxW93+Ab0zs{a{WGAweo26TKY zt$MM2+M(7=8IVmV_WuXlU67fVDvec+nIS(hdp1ytRe=HIM27!BFF^nwg91avoZ1VP zVuuYlSOTP0GcB9T52%S>|W=xj%n! zCaT!*{MR*^Kh1C7baAY1b=tp|-Q;QDGp||IYyY%7Fc(prH6wD~m|dfu_cvqtOp!ejna%RneQ)FSezH;oIlj13r>prty`nH#_}rW6s6}jb9VhI*v%% z_HA9o&eq9suk-E9$m}z2Qa6mM&g6B+PSx>#W2qogto)75^gBoYt{YN|e8RS_3vrTo zaK*p*T=z^J`>x=Pu6s*ADM!!AZ_nAkuwz%khj)&LUOm0Yukqv0p4-=s{XHc0aQTwN zS=qO{wf?!DdHFm%Nmk&6z{NHEPr9G8rHe{(tw|L)xarr;#!oDoFZjI9#u=xaZ|8Jf ok@&>(SgWN|<_Atmq1&@1w#|GhWmQxxoHO&=D)Udw?X23*0e#U7O8@`> literal 0 HcmV?d00001 diff --git a/src/fnl/demo-effects.fnl b/src/fnl/demo-effects.fnl new file mode 100644 index 0000000..c13f092 --- /dev/null +++ b/src/fnl/demo-effects.fnl @@ -0,0 +1,81 @@ +(local pxl8 (require :pxl8)) + +(var time 0) +(var current-effect 1) +(var particles nil) +(var starfield-init false) +(var fire-init false) +(var rain-init false) +(var snow-init false) + +(global init (fn [] + (pxl8.load_palette "res/palettes/gruvbox.ase") + (set particles (pxl8.particles_new 1000)))) + +(global update (fn [dt] + (set time (+ time dt)) + + (when (pxl8.key_pressed "1") + (set current-effect 1)) + (when (pxl8.key_pressed "2") + (set current-effect 2)) + (when (pxl8.key_pressed "3") + (set current-effect 3) + (set fire-init false)) + (when (pxl8.key_pressed "4") + (set current-effect 4)) + (when (pxl8.key_pressed "5") + (set current-effect 5) + (set rain-init false)) + (when (pxl8.key_pressed "6") + (set current-effect 6) + (set snow-init false)) + + (when particles + (pxl8.particles_update particles dt)))) + +(global draw (fn [] + (match current-effect + 1 (pxl8.vfx_plasma time 0.10 0.04 0) + + 2 (pxl8.vfx_tunnel time 2.0 0.25) + + 3 (do + (pxl8.clr 0) + (when particles + (when (not fire-init) + (pxl8.particles_clear particles) + (pxl8.vfx_fire particles 160 140 100 12) + (set fire-init true)) + (pxl8.particles_render particles)) + (pxl8.text "Fire Effect Test" 200 10 15)) + + 4 (do + (pxl8.clr 0) + (local bars [{:base_y 40 :amplitude 20 :height 16 :speed 2.0 :phase 0 :color 14 :fade_color 11} + {:base_y 80 :amplitude 15 :height 16 :speed 2.5 :phase 1.5 :color 20 :fade_color 11} + {:base_y 120 :amplitude 25 :height 16 :speed 1.8 :phase 3.0 :color 26 :fade_color 11}]) + (pxl8.vfx_copper_bars bars time) + (pxl8.text "Copper Bars" 200 10 15)) + + 5 (do + (pxl8.clr 0) + (when particles + (when (not rain-init) + (pxl8.particles_clear particles) + (pxl8.vfx_rain particles 320 10.0) + (set rain-init true)) + (pxl8.particles_render particles)) + (pxl8.text "Rain" 200 10 15)) + + 6 (do + (pxl8.clr 0) + (when particles + (when (not snow-init) + (pxl8.particles_clear particles) + (pxl8.vfx_snow particles 320 5.0) + (set snow-init true)) + (pxl8.particles_render particles)) + (pxl8.text "Snow" 200 10 15)) + + _ (pxl8.clr 0)))) diff --git a/src/fnl/demo.fnl b/src/fnl/demo.fnl new file mode 100644 index 0000000..3418d26 --- /dev/null +++ b/src/fnl/demo.fnl @@ -0,0 +1,29 @@ +(local pxl8 (require :pxl8)) + +(var frame 0) +(var pxl8-sprite-id nil) +(var screen nil) +(var time 0) + +(global init (fn [] + (pxl8.load_palette "./res/palettes/gruvbox.ase") + (set pxl8-sprite-id (pxl8.load_sprite "./res/sprites/pxl8.ase")) + (set screen (pxl8.get_screen)) + (when (not pxl8-sprite-id) + (pxl8.error "Failed to load pxl8 sprite")))) + +(global update (fn [dt])) + +(global draw (fn [] + (pxl8.clr 1) + (local cols 5) + (local rows 1024) + (local sprite-w 128) + (local sprite-h 64) + + (for [i 0 8192] + (local col (% i cols)) + (local row (math.floor (/ i cols))) + (local x (* col (+ sprite-w 4))) + (local y (* row (+ sprite-h 2))) + (pxl8.sprite pxl8-sprite-id x y sprite-w sprite-h)))) diff --git a/src/lua/pxl8.lua b/src/lua/pxl8.lua new file mode 100644 index 0000000..c17a09c --- /dev/null +++ b/src/lua/pxl8.lua @@ -0,0 +1,192 @@ +local ffi = require("ffi") +local C = ffi.C +local gfx = _pxl8_gfx_ctx +local input = _pxl8_input_ctx + +-- pxl8 lua api +-- +local pxl8 = {} + +function pxl8.clr(color) + C.pxl8_clr(gfx, color or 0) +end + +function pxl8.pixel(x, y, color) + if color then + C.pxl8_pixel(gfx, x, y, color) + else + return C.pxl8_get_pixel(gfx, x, y) + end +end + +function pxl8.line(x0, y0, x1, y1, color) + C.pxl8_line(gfx, x0, y0, x1, y1, color) +end + +function pxl8.rect(x, y, w, h, color) + C.pxl8_rect(gfx, x, y, w, h, color) +end + +function pxl8.rect_fill(x, y, w, h, color) + C.pxl8_rect_fill(gfx, x, y, w, h, color) +end + +function pxl8.circle(x, y, r, color) + C.pxl8_circle(gfx, x, y, r, color) +end + +function pxl8.circle_fill(x, y, r, color) + C.pxl8_circle_fill(gfx, x, y, r, color) +end + +function pxl8.text(str, x, y, color) + C.pxl8_text(gfx, str, x or 0, y or 0, color or 15) +end + +function pxl8.sprite(id, x, y, w, h, flip_x, flip_y) + C.pxl8_sprite(gfx, id or 0, x or 0, y or 0, w or 16, h or 16, flip_x or false, flip_y or false) +end + +function pxl8.get_screen() + return C.pxl8_get_screen(gfx) +end + +function pxl8.load_palette(filepath) + return C.pxl8_gfx_load_palette(gfx, filepath) +end + +function pxl8.load_sprite(filepath) + local sprite_id = ffi.new("unsigned int[1]") + local result = C.pxl8_gfx_load_sprite(gfx, filepath, sprite_id) + if result == 0 then + return sprite_id[0] + else + return nil, result + end +end + +-- log +function pxl8.info(msg) + C.pxl8_lua_info(msg) +end + +function pxl8.warn(msg) + C.pxl8_lua_warn(msg) +end + +function pxl8.error(msg) + C.pxl8_lua_error(msg) +end + +function pxl8.debug(msg) + C.pxl8_lua_debug(msg) +end + +function pxl8.trace(msg) + C.pxl8_lua_trace(msg) +end + +function pxl8.key_down(key) + if type(key) == "string" then + key = string.byte(key) + end + return C.pxl8_key_down(input, key) +end + +function pxl8.key_pressed(key) + if type(key) == "string" then + key = string.byte(key) + end + return C.pxl8_key_pressed(input, key) +end + +function pxl8.vfx_copper_bars(bars, time) + local c_bars = ffi.new("pxl8_copper_bar[?]", #bars) + for i, bar in ipairs(bars) do + c_bars[i-1].base_y = bar.base_y or 0 + c_bars[i-1].amplitude = bar.amplitude or 10 + c_bars[i-1].height = bar.height or 5 + c_bars[i-1].speed = bar.speed or 1.0 + c_bars[i-1].phase = bar.phase or 0 + c_bars[i-1].color = bar.color or 15 + c_bars[i-1].fade_color = bar.fade_color or bar.color or 15 + end + C.pxl8_vfx_copper_bars(gfx, c_bars, #bars, time) +end + +function pxl8.vfx_plasma(time, scale1, scale2, palette_offset) + C.pxl8_vfx_plasma(gfx, time, scale1 or 0.05, scale2 or 0.03, palette_offset or 0) +end + +function pxl8.vfx_rotozoom(angle, zoom, cx, cy) + local screen = pxl8.get_screen() + C.pxl8_vfx_rotozoom(gfx, angle, zoom, cx or screen.width/2, cy or screen.height/2) +end + +function pxl8.vfx_tunnel(time, speed, twist) + C.pxl8_vfx_tunnel(gfx, time, speed or 2.0, twist or 0.5) +end + +function pxl8.particles_new(max_count) + local ps = ffi.new("pxl8_particle_system") + C.pxl8_vfx_particles_init(ps, max_count or 1000) + return ps +end + +function pxl8.particles_destroy(ps) + C.pxl8_vfx_particles_destroy(ps) +end + +function pxl8.particles_clear(ps) + C.pxl8_vfx_particles_clear(ps) +end + +function pxl8.particles_emit(ps, count) + C.pxl8_vfx_particles_emit(ps, count or 1) +end + +function pxl8.particles_update(ps, dt) + C.pxl8_vfx_particles_update(ps, dt) +end + +function pxl8.particles_render(ps) + C.pxl8_vfx_particles_render(ps, gfx) +end + +function pxl8.vfx_explosion(ps, x, y, color, force) + C.pxl8_vfx_explosion(ps, x, y, color or 15, force or 200.0) +end + +function pxl8.vfx_fire(ps, x, y, width, palette_start) + C.pxl8_vfx_fire(ps, x, y, width or 50, palette_start or 64) +end + +function pxl8.vfx_rain(ps, width, wind) + C.pxl8_vfx_rain(ps, width or pxl8.get_screen().width, wind or 0.0) +end + +function pxl8.vfx_smoke(ps, x, y, color) + C.pxl8_vfx_smoke(ps, x, y, color or 8) +end + +function pxl8.vfx_snow(ps, width, wind) + C.pxl8_vfx_snow(ps, width or pxl8.get_screen().width, wind or 10.0) +end + +function pxl8.vfx_sparks(ps, x, y, color) + C.pxl8_vfx_sparks(ps, x, y, color or 15) +end + +function pxl8.vfx_starfield(ps, speed, spread) + C.pxl8_vfx_starfield(ps, speed or 5.0, spread or 500.0) +end + +function pxl8.gfx_color_ramp(start, count, from_color, to_color) + C.pxl8_gfx_color_ramp(gfx, start, count, from_color, to_color) +end + +function pxl8.gfx_fade_palette(start, count, amount, target_color) + C.pxl8_gfx_fade_palette(gfx, start, count, amount, target_color) +end + +return pxl8 diff --git a/src/pxl8.c b/src/pxl8.c new file mode 100644 index 0000000..d4c6855 --- /dev/null +++ b/src/pxl8.c @@ -0,0 +1,479 @@ +#define PXL8_AUTHORS "asrael " +#define PXL8_COPYRIGHT "Copyright (c) 2024-2025 pxl8.org" +#define PXL8_VERSION "0.1.0" + +#include +#include +#include +#include + +#include "linenoise.h" +#define SDL_MAIN_USE_CALLBACKS +#include +#include + +#include "pxl8_lua.h" +#include "pxl8_macros.h" +#include "pxl8_types.h" + +#define PXL8_MAX_REPL_COMMANDS 4096 + +typedef struct pxl8_repl_command { + char buffer[PXL8_MAX_REPL_COMMANDS]; + struct pxl8_repl_command* next; +} pxl8_repl_command; + +typedef struct pxl8_repl_state { + pthread_t thread; + pthread_mutex_t mutex; + pxl8_repl_command* queue_head; + pxl8_repl_command* queue_tail; + bool running; +} pxl8_repl_state; + +typedef struct pxl8_app_state { + pxl8_color_mode color_mode; + pxl8_gfx_ctx gfx; + lua_State* lua; + pxl8_repl_state repl; + pxl8_resolution resolution; + + f32 fps_timer; + i32 frame_count; + u64 last_time; + f32 time; + + bool repl_mode; + bool running; + bool script_loaded; + char script_path[256]; + time_t script_mod_time; + + pxl8_input_state input; +} pxl8_app_state; + +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" + }; + + 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" + }; + + auto buf_len = strlen(buf); + + for (size_t 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 (size_t 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]); + } + } +} + +static char* pxl8_repl_hints(const char* buf, int* color, int* bold) { + 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; +} + +static void* pxl8_repl_stdin_thread(void* user_data) { + pxl8_repl_state* repl = (pxl8_repl_state*)user_data; + char* line; + const char* history_file = ".pxl8_history"; + + linenoiseHistorySetMaxLen(100); + linenoiseSetMultiLine(1); + linenoiseSetCompletionCallback(pxl8_repl_completion); + linenoiseSetHintsCallback(pxl8_repl_hints); + linenoiseHistoryLoad(history_file); + + while (repl->running && (line = linenoise(">> "))) { + if (strlen(line) > 0) { + linenoiseHistoryAdd(line); + linenoiseHistorySave(history_file); + + pxl8_repl_command* cmd = (pxl8_repl_command*)SDL_malloc(sizeof(pxl8_repl_command)); + if (cmd) { + strncpy(cmd->buffer, line, PXL8_MAX_REPL_COMMANDS - 1); + cmd->buffer[PXL8_MAX_REPL_COMMANDS - 1] = '\0'; + cmd->next = NULL; + + pthread_mutex_lock(&repl->mutex); + if (repl->queue_tail) { + repl->queue_tail->next = cmd; + repl->queue_tail = cmd; + } else { + repl->queue_head = repl->queue_tail = cmd; + } + pthread_mutex_unlock(&repl->mutex); + } + } + linenoiseFree(line); + } + + return NULL; +} + +static void pxl8_repl_init(pxl8_repl_state* repl) { + repl->queue_head = NULL; + repl->queue_tail = NULL; + repl->running = true; + pthread_mutex_init(&repl->mutex, NULL); + pthread_create(&repl->thread, NULL, pxl8_repl_stdin_thread, repl); +} + +static void pxl8_repl_shutdown(pxl8_repl_state* repl) { + repl->running = false; + pthread_join(repl->thread, NULL); + pthread_mutex_destroy(&repl->mutex); + + pxl8_repl_command* cmd = repl->queue_head; + while (cmd) { + pxl8_repl_command* next = cmd->next; + SDL_free(cmd); + cmd = next; + } +} + +static pxl8_repl_command* pxl8_repl_pop_command(pxl8_repl_state* repl) { + pthread_mutex_lock(&repl->mutex); + pxl8_repl_command* cmd = repl->queue_head; + if (cmd) { + repl->queue_head = cmd->next; + if (!repl->queue_head) { + repl->queue_tail = NULL; + } + } + pthread_mutex_unlock(&repl->mutex); + return cmd; +} + +static void load_script(pxl8_app_state* app) { + const char* ext = strrchr(app->script_path, '.'); + + if (ext && strcmp(ext, ".fnl") == 0) { + pxl8_result result = pxl8_lua_run_fennel_file(app->lua, app->script_path); + if (result == PXL8_OK) { + pxl8_info("Loaded script: %s", app->script_path); + app->script_loaded = true; + lua_getglobal(app->lua, "init"); + if (lua_isfunction(app->lua, -1)) { + lua_pcall(app->lua, 0, 0, 0); + } else { + pxl8_warn("No init function found (type: %s)", lua_typename(app->lua, lua_type(app->lua, -1))); + lua_pop(app->lua, 1); + } + } else { + pxl8_warn("Failed to load script: %s", app->script_path); + app->script_loaded = false; + } + } else if (ext && strcmp(ext, ".lua") == 0) { + pxl8_result result = pxl8_lua_run_file(app->lua, app->script_path); + if (result == PXL8_OK) { + pxl8_info("Loaded script: %s", app->script_path); + app->script_loaded = true; + lua_getglobal(app->lua, "init"); + if (lua_isfunction(app->lua, -1)) { + lua_pcall(app->lua, 0, 0, 0); + } else { + pxl8_warn("No init function found (type: %s)", lua_typename(app->lua, lua_type(app->lua, -1))); + lua_pop(app->lua, 1); + } + } else { + pxl8_warn("Failed to load script: %s", app->script_path); + app->script_loaded = false; + } + } else { + pxl8_warn("Unknown script type for: %s (expected .fnl or .lua)", app->script_path); + app->script_loaded = false; + } +} + +static time_t get_file_mod_time(const char* path) { + struct stat file_stat; + if (stat(path, &file_stat) == 0) { + return file_stat.st_mtime; + } + return 0; +} + +SDL_AppResult SDL_AppInit(void** appstate, int argc, char* argv[]) { + static pxl8_app_state app = {0}; + + if (!SDL_Init(SDL_INIT_VIDEO | SDL_INIT_EVENTS)) { + pxl8_error("SDL_Init failed: %s", SDL_GetError()); + return SDL_APP_FAILURE; + } + + app.color_mode = PXL8_COLOR_MODE_MEGA; + app.resolution = PXL8_RESOLUTION_640x360; + + const char* script_arg = NULL; + for (int i = 1; i < argc; i++) { + if (strcmp(argv[i], "--repl") == 0) { + app.repl_mode = true; + } else if (!script_arg) { + script_arg = argv[i]; + } + } + + if (script_arg) { + strncpy(app.script_path, script_arg, sizeof(app.script_path) - 1); + app.script_path[sizeof(app.script_path) - 1] = '\0'; + } else { + strcpy(app.script_path, "src/fnl/demo.fnl"); + } + + + if (app.repl_mode) { + fprintf(stderr, "\033[38;2;184;187;38m[pxl8]\033[0m Starting in REPL mode with script: %s\n", app.script_path); + } + + pxl8_info("Starting up"); + + if (pxl8_gfx_init(&app.gfx, app.color_mode, app.resolution, "pxl8", 1280, 720) != PXL8_OK) { + pxl8_error("Failed to initialize graphics context"); + return SDL_APP_FAILURE; + } + + if (pxl8_gfx_load_font_atlas(&app.gfx) != PXL8_OK) { + pxl8_error("Failed to load font atlas"); + return SDL_APP_FAILURE; + } + + if (pxl8_gfx_init_atlas(&app.gfx, 1024, 1024) != PXL8_OK) { + pxl8_error("Failed to initialize sprite atlas"); + return SDL_APP_FAILURE; + } + + pxl8_result lua_result = pxl8_lua_init(&app.lua); + if (lua_result != PXL8_OK) { + pxl8_error("Failed to initialize Lua scripting!"); + pxl8_gfx_shutdown(&app.gfx); + return SDL_APP_FAILURE; + } + + pxl8_lua_setup_contexts(app.lua, &app.gfx, &app.input); + + app.script_mod_time = get_file_mod_time(app.script_path); + load_script(&app); + + if (app.repl_mode) { + pxl8_repl_init(&app.repl); + fprintf(stderr, "\033[38;2;184;187;38m[pxl8 REPL]\033[0m Fennel %s - Tab for completions, Ctrl-C to exit\n", "1.5.1"); + + lua_getglobal(app.lua, "require"); + lua_pushstring(app.lua, "pxl8"); + if (lua_pcall(app.lua, 1, 1, 0) == 0) { + lua_setglobal(app.lua, "pxl8"); + } else { + const char* error_msg = lua_tostring(app.lua, -1); + fprintf(stderr, "Warning: Failed to setup pxl8 global: %s\n", error_msg); + lua_pop(app.lua, 1); + } + } + + app.last_time = SDL_GetTicksNS(); + app.running = true; + + *appstate = &app; + return SDL_APP_CONTINUE; +} + +SDL_AppResult SDL_AppIterate(void* appstate) { + pxl8_app_state* app = (pxl8_app_state*)appstate; + int width, height; + + SDL_GetWindowSize(app->gfx.window, &width, &height); + + u64 current_time = SDL_GetTicksNS(); + float dt = (float)(current_time - app->last_time) / 1000000000.0f; + + app->frame_count++; + app->fps_timer += dt; + app->last_time = current_time; + app->time += dt; + + if (app->fps_timer >= 3.0f) { + if (!app->repl_mode) { + float avg_fps = app->frame_count / app->fps_timer; + pxl8_info("FPS: %.1f", avg_fps); + } + app->frame_count = 0; + app->fps_timer = 0.0f; + } + + time_t current_mod_time = get_file_mod_time(app->script_path); + if (current_mod_time != app->script_mod_time && current_mod_time != 0) { + pxl8_info("Script modified, reloading: %s", app->script_path); + app->script_mod_time = current_mod_time; + load_script(app); + } + + if (app->repl_mode) { + pxl8_repl_command* cmd = pxl8_repl_pop_command(&app->repl); + if (cmd) { + pxl8_result result = pxl8_lua_eval_fennel(app->lua, cmd->buffer); + if (result == PXL8_OK) { + if (lua_gettop(app->lua) > 0 && !lua_isnil(app->lua, -1)) { + const char* result_str = lua_tostring(app->lua, -1); + if (result_str) { + fprintf(stdout, "%s\n", result_str); + } else if (lua_isuserdata(app->lua, -1)) { + fprintf(stdout, "\n"); + } else if (lua_iscfunction(app->lua, -1)) { + fprintf(stdout, "\n"); + } else if (lua_istable(app->lua, -1)) { + fprintf(stdout, "\n"); + } else { + fprintf(stdout, "<%s>\n", lua_typename(app->lua, lua_type(app->lua, -1))); + } + lua_pop(app->lua, 1); + } + } else { + const char* error_msg = lua_tostring(app->lua, -1); + pxl8_error("%s", error_msg ? error_msg : "Unknown error"); + lua_pop(app->lua, 1); + } + SDL_free(cmd); + } + } + + if (app->script_loaded) { + lua_getglobal(app->lua, "update"); + if (lua_isfunction(app->lua, -1)) { + lua_pushnumber(app->lua, dt); + lua_pcall(app->lua, 1, 0, 0); + } else { + lua_pop(app->lua, 1); + } + + lua_getglobal(app->lua, "draw"); + if (lua_isfunction(app->lua, -1)) { + int result = lua_pcall(app->lua, 0, 0, 0); + if (result != 0) { + pxl8_error("Error calling draw: %s", lua_tostring(app->lua, -1)); + lua_pop(app->lua, 1); + } + } else { + pxl8_warn("draw is not a function, type: %s", lua_typename(app->lua, lua_type(app->lua, -1))); + lua_pop(app->lua, 1); + } + } else { + pxl8_clr(&app->gfx, 32); + + i32 render_width, render_height; + pxl8_gfx_get_resolution_dimensions(app->resolution, &render_width, &render_height); + + for (int y = 0; y < render_height; y += 24) { + for (int x = 0; x < render_width; x += 32) { + u32 color = ((x / 32) + (y / 24) + (int)(app->time * 2)) % 8; + pxl8_rect_fill(&app->gfx, x, y, 31, 23, color); + } + } + } + + i32 render_width, render_height; + pxl8_gfx_get_resolution_dimensions(app->resolution, &render_width, &render_height); + + float scale = fminf(width / (float)render_width, height / (float)render_height); + int scaled_width = (int)(render_width * scale); + int scaled_height = (int)(render_height * scale); + int offset_x = (width - scaled_width) / 2; + int offset_y = (height - scaled_height) / 2; + + pxl8_gfx_viewport(&app->gfx, offset_x, offset_y, scaled_width, scaled_height); + pxl8_gfx_upload_framebuffer(&app->gfx); + pxl8_gfx_upload_atlas(&app->gfx); + pxl8_gfx_present(&app->gfx); + + SDL_memset(app->input.keys_pressed, 0, sizeof(app->input.keys_pressed)); + + return app->running ? SDL_APP_CONTINUE : SDL_APP_SUCCESS; +} + +SDL_AppResult SDL_AppEvent(void* appstate, SDL_Event* event) { + pxl8_app_state* app = (pxl8_app_state*)appstate; + + switch (event->type) { + case SDL_EVENT_QUIT: + app->running = false; + return SDL_APP_SUCCESS; + + case SDL_EVENT_KEY_DOWN: { + if (event->key.key == SDLK_ESCAPE) { + app->running = false; + return SDL_APP_SUCCESS; + } + + SDL_Keycode key = event->key.key; + if (key < 256) { + if (!app->input.keys[key]) { + app->input.keys_pressed[key] = true; + } + app->input.keys[key] = true; + } + break; + } + + case SDL_EVENT_KEY_UP: { + SDL_Keycode key = event->key.key; + if (key < 256) { + app->input.keys[key] = false; + app->input.keys_pressed[key] = false; + } + break; + } + } + + return SDL_APP_CONTINUE; +} + +void SDL_AppQuit(void* appstate, SDL_AppResult result) { + pxl8_app_state* app = (pxl8_app_state*)appstate; + (void)result; + + if (app) { + pxl8_info("Shutting down"); + if (app->repl_mode) { + pxl8_repl_shutdown(&app->repl); + } + pxl8_lua_shutdown(app->lua); + pxl8_gfx_shutdown(&app->gfx); + } + + SDL_Quit(); +} diff --git a/src/pxl8_ase.c b/src/pxl8_ase.c new file mode 100644 index 0000000..82b21bf --- /dev/null +++ b/src/pxl8_ase.c @@ -0,0 +1,400 @@ +#include +#include +#include + +#define MINIZ_NO_STDIO +#define MINIZ_NO_TIME +#define MINIZ_NO_ARCHIVE_APIS +#define MINIZ_NO_ARCHIVE_WRITING_APIS +#define MINIZ_NO_DEFLATE_APIS +#include "miniz.h" + +#include "pxl8_ase.h" +#include "pxl8_io.h" +#include "pxl8_macros.h" + +static u16 read_u16_le(const u8* data) { + return data[0] | (data[1] << 8); +} + +static u32 read_u32_le(const u8* data) { + return data[0] | (data[1] << 8) | (data[2] << 16) | (data[3] << 24); +} + +static i16 read_i16_le(const u8* data) { + return (i16)read_u16_le(data); +} + +static pxl8_result parse_ase_header(const u8* data, pxl8_ase_header* header) { + header->file_size = read_u32_le(data); + header->magic = read_u16_le(data + 4); + header->frames = read_u16_le(data + 6); + header->width = read_u16_le(data + 8); + header->height = read_u16_le(data + 10); + header->color_depth = read_u16_le(data + 12); + header->flags = read_u32_le(data + 14); + header->speed = read_u16_le(data + 18); + header->transparent_index = read_u32_le(data + 24); + header->n_colors = data[28]; + header->pixel_width = data[29]; + header->pixel_height = data[30]; + header->grid_x = read_i16_le(data + 31); + header->grid_y = read_i16_le(data + 33); + header->grid_width = read_u16_le(data + 35); + header->grid_height = read_u16_le(data + 37); + + if (header->magic != PXL8_ASE_MAGIC) { + pxl8_error("Invalid ASE file magic: 0x%04X", header->magic); + return PXL8_ERROR_ASE_INVALID_MAGIC; + } + + return PXL8_OK; +} + +static pxl8_result parse_old_palette_chunk(const u8* data, pxl8_ase_palette* palette) { + u16 packet_count = read_u16_le(data); + const u8* packet_data = data + 2; + + u32 total_colors = 0; + const u8* temp_data = packet_data; + for (u16 packet = 0; packet < packet_count; packet++) { + u8 skip_colors = temp_data[0]; + u8 colors_in_packet = temp_data[1]; + u32 actual_colors = (colors_in_packet == 0) ? 256 : colors_in_packet; + total_colors = skip_colors + actual_colors; + temp_data += 2 + (actual_colors * 3); + } + + palette->entry_count = total_colors; + palette->first_color = 0; + palette->last_color = total_colors - 1; + palette->colors = (u32*)SDL_malloc(total_colors * sizeof(u32)); + if (!palette->colors) { + return PXL8_ERROR_OUT_OF_MEMORY; + } + + u32 color_index = 0; + for (u16 packet = 0; packet < packet_count; packet++) { + u8 skip_colors = packet_data[0]; + u8 colors_in_packet = packet_data[1]; + u32 actual_colors = (colors_in_packet == 0) ? 256 : colors_in_packet; + + for (u32 i = 0; i < skip_colors && color_index < total_colors; i++, color_index++) { + palette->colors[color_index] = 0xFF000000; + } + + packet_data += 2; + + for (u32 i = 0; i < actual_colors && color_index < total_colors; i++, color_index++) { + u8 r = packet_data[0]; + u8 g = packet_data[1]; + u8 b = packet_data[2]; + + // Store in ABGR format for GPU + palette->colors[color_index] = 0xFF000000 | (b << 16) | (g << 8) | r; + packet_data += 3; + } + } + + return PXL8_OK; +} + +static pxl8_result parse_layer_chunk(const u8* data, pxl8_ase_layer* layer) { + layer->flags = read_u16_le(data); // Offset 0: flags (2 bytes) + layer->layer_type = read_u16_le(data + 2); // Offset 2: layer_type (2 bytes) + layer->child_level = read_u16_le(data + 4); // Offset 4: child_level (2 bytes) + // Offset 6: default_width (2 bytes) - skip + // Offset 8: default_height (2 bytes) - skip + layer->blend_mode = read_u16_le(data + 10); // Offset 10: blend_mode (2 bytes) + layer->opacity = data[12]; // Offset 12: opacity (1 byte) + // Offset 13-15: reserved (3 bytes) - skip + + // Offset 16: name length (2 bytes), then name string at offset 18 + u16 name_len = read_u16_le(data + 16); + if (name_len > 0) { + layer->name = (char*)SDL_malloc(name_len + 1); + if (!layer->name) return PXL8_ERROR_OUT_OF_MEMORY; + memcpy(layer->name, data + 18, name_len); + layer->name[name_len] = '\0'; + } else { + layer->name = NULL; + } + + return PXL8_OK; +} + +static pxl8_result parse_palette_chunk(const u8* data, pxl8_ase_palette* palette) { + palette->entry_count = read_u32_le(data); // Offset 0: entry_count (4 bytes) + palette->first_color = read_u32_le(data + 4); // Offset 4: first_color (4 bytes) + palette->last_color = read_u32_le(data + 8); // Offset 8: last_color (4 bytes) + + u32 color_count = palette->entry_count; + palette->colors = (u32*)SDL_malloc(color_count * sizeof(u32)); + if (!palette->colors) { + return PXL8_ERROR_OUT_OF_MEMORY; + } + + const u8* color_data = data + 20; // Skip palette header (20 bytes) + for (u32 i = 0; i < color_count; i++) { + u16 flags = read_u16_le(color_data); // Offset 0: flags (2 bytes) + u8 r = color_data[2]; // Offset 2: red (1 byte) + u8 g = color_data[3]; // Offset 3: green (1 byte) + u8 b = color_data[4]; // Offset 4: blue (1 byte) + u8 a = color_data[5]; // Offset 5: alpha (1 byte) + + // Store in ABGR format for GPU + palette->colors[i] = (a << 24) | (b << 16) | (g << 8) | r; + color_data += 6; + + if (flags & 1) { + u16 name_len = read_u16_le(color_data); + color_data += 2 + name_len; + } + } + + return PXL8_OK; +} + +static pxl8_result parse_cel_chunk(const u8* data, u32 chunk_size, pxl8_ase_cel* cel) { + if (chunk_size < 9) { + return PXL8_ERROR_ASE_MALFORMED_CHUNK; + } + + cel->layer_index = read_u16_le(data); // Offset 0: layer_index (2 bytes) + cel->x = read_i16_le(data + 2); // Offset 2: x (2 bytes) + cel->y = read_i16_le(data + 4); // Offset 4: y (2 bytes) + cel->opacity = data[6]; // Offset 6: opacity (1 byte) + cel->cel_type = read_u16_le(data + 7); // Offset 7: cel_type (2 bytes) + + if (cel->cel_type == 2) { + if (chunk_size < 20) { + return PXL8_ERROR_ASE_MALFORMED_CHUNK; + } + + // Offset 9: Z-Index (2 bytes) - skip + // Offset 11: Reserved (5 bytes) - skip + cel->width = read_u16_le(data + 16); // Offset 16: width (2 bytes) + cel->height = read_u16_le(data + 18); // Offset 18: height (2 bytes) + + u32 pixel_data_size = cel->width * cel->height; + u32 compressed_data_size = chunk_size - 20; + + cel->pixel_data = (u8*)SDL_malloc(pixel_data_size); + if (!cel->pixel_data) { + return PXL8_ERROR_OUT_OF_MEMORY; + } + + // Decompress ZLIB data + mz_ulong dest_len = pixel_data_size; + int result = mz_uncompress(cel->pixel_data, &dest_len, data + 20, compressed_data_size); + if (result != MZ_OK) { + pxl8_error("Failed to decompress cel data: miniz error %d", result); + SDL_free(cel->pixel_data); + cel->pixel_data = NULL; + return PXL8_ERROR_ASE_MALFORMED_CHUNK; + } + + if (dest_len != pixel_data_size) { + pxl8_warn("Decompressed size mismatch: expected %u, got %lu", pixel_data_size, dest_len); + } + } + + return PXL8_OK; +} + +pxl8_result pxl8_ase_load(const char* filepath, pxl8_ase_file* ase_file) { + if (!filepath || !ase_file) { + return PXL8_ERROR_NULL_POINTER; + } + + memset(ase_file, 0, sizeof(pxl8_ase_file)); + + u8* file_data; + size_t file_size; + pxl8_result result = pxl8_io_read_binary_file(filepath, &file_data, &file_size); + if (result != PXL8_OK) { + return result; + } + + if (file_size < 128) { + pxl8_io_free_binary_data(file_data); + return PXL8_ERROR_ASE_TRUNCATED_FILE; + } + + result = parse_ase_header(file_data, &ase_file->header); + if (result != PXL8_OK) { + pxl8_io_free_binary_data(file_data); + return result; + } + + ase_file->frame_count = ase_file->header.frames; + ase_file->frames = (pxl8_ase_frame*)SDL_calloc(ase_file->frame_count, sizeof(pxl8_ase_frame)); + if (!ase_file->frames) { + pxl8_io_free_binary_data(file_data); + return PXL8_ERROR_OUT_OF_MEMORY; + } + + const u8* frame_data = file_data + 128; + for (u16 frame_idx = 0; frame_idx < ase_file->header.frames; frame_idx++) { + pxl8_ase_frame_header frame_header; + frame_header.frame_bytes = read_u32_le(frame_data); + frame_header.magic = read_u16_le(frame_data + 4); + frame_header.chunks = read_u16_le(frame_data + 6); + frame_header.duration = read_u16_le(frame_data + 8); + + if (frame_header.magic != PXL8_ASE_FRAME_MAGIC) { + pxl8_error("Invalid frame magic: 0x%04X", frame_header.magic); + result = PXL8_ERROR_ASE_INVALID_FRAME_MAGIC; + break; + } + + pxl8_ase_frame* frame = &ase_file->frames[frame_idx]; + frame->frame_id = frame_idx; + frame->width = ase_file->header.width; + frame->height = ase_file->header.height; + frame->duration = frame_header.duration; + + u32 pixel_count = frame->width * frame->height; + frame->pixels = (u8*)SDL_calloc(pixel_count, sizeof(u8)); + if (!frame->pixels) { + result = PXL8_ERROR_OUT_OF_MEMORY; + break; + } + + const u8* chunk_data = frame_data + 16; + for (u16 chunk_idx = 0; chunk_idx < frame_header.chunks; chunk_idx++) { + pxl8_ase_chunk_header chunk_header; + chunk_header.chunk_size = read_u32_le(chunk_data); + chunk_header.chunk_type = read_u16_le(chunk_data + 4); + + const u8* chunk_payload = chunk_data + 6; + + pxl8_debug("Found chunk: type=0x%04X, size=%d", chunk_header.chunk_type, chunk_header.chunk_size); + switch (chunk_header.chunk_type) { + case PXL8_ASE_CHUNK_OLD_PALETTE: // 0x0004 + if (!ase_file->palette.colors) { + result = parse_old_palette_chunk(chunk_payload, &ase_file->palette); + pxl8_debug("Parsed old palette: %d colors, indices %d-%d", ase_file->palette.entry_count, ase_file->palette.first_color, ase_file->palette.last_color); + } else { + pxl8_debug("Ignoring old palette (0x0004) - new palette (0x2019) already loaded"); + } + break; + + case PXL8_ASE_CHUNK_LAYER: { // 0x2004 + // Need to allocate or reallocate layers array + ase_file->layers = (pxl8_ase_layer*)SDL_realloc(ase_file->layers, + (ase_file->layer_count + 1) * sizeof(pxl8_ase_layer)); + if (!ase_file->layers) { + result = PXL8_ERROR_OUT_OF_MEMORY; + break; + } + + result = parse_layer_chunk(chunk_payload, &ase_file->layers[ase_file->layer_count]); + if (result == PXL8_OK) { + pxl8_debug("Parsed layer %d: '%s', blend_mode=%d, opacity=%d", + ase_file->layer_count, + ase_file->layers[ase_file->layer_count].name ? ase_file->layers[ase_file->layer_count].name : "(unnamed)", + ase_file->layers[ase_file->layer_count].blend_mode, + ase_file->layers[ase_file->layer_count].opacity); + ase_file->layer_count++; + } + break; + } + + case PXL8_ASE_CHUNK_CEL: { // 0x2005 + pxl8_ase_cel cel = {0}; + pxl8_debug("Found CEL chunk: size=%d", chunk_header.chunk_size - 6); + result = parse_cel_chunk(chunk_payload, chunk_header.chunk_size - 6, &cel); + if (result == PXL8_OK && cel.pixel_data) { + pxl8_debug("CEL data loaded: %dx%d, type=%d, first pixel = %d", cel.width, cel.height, cel.cel_type, cel.pixel_data[0]); + u32 copy_width = (cel.width < frame->width) ? cel.width : frame->width; + u32 copy_height = (cel.height < frame->height) ? cel.height : frame->height; + + pxl8_debug("Copying cel to frame: cel_pos=(%d,%d), copy_size=%dx%d", cel.x, cel.y, copy_width, copy_height); + for (u32 y = 0; y < copy_height; y++) { + u32 src_offset = y * cel.width; + u32 dst_offset = (y + cel.y) * frame->width + cel.x; + if (dst_offset + copy_width <= pixel_count) { + // Composite layers: only copy non-transparent pixels + // Check if palette color is transparent (#00000000) + for (u32 x = 0; x < copy_width; x++) { + u8 src_pixel = cel.pixel_data[src_offset + x]; + bool is_transparent = false; + + if (src_pixel < ase_file->palette.entry_count && ase_file->palette.colors) { + u32 color = ase_file->palette.colors[src_pixel]; + // Check if color is fully transparent (alpha = 0) + is_transparent = ((color >> 24) & 0xFF) == 0; + } + + if (!is_transparent) { + frame->pixels[dst_offset + x] = src_pixel; + } + } + // Debug: check first few pixels of each row + if (y < 3) { + pxl8_debug("Row %d: cel[%d]=%d, frame[%d]=%d", y, src_offset, cel.pixel_data[src_offset], dst_offset, frame->pixels[dst_offset]); + } + } + } + SDL_free(cel.pixel_data); + } + break; + } + + case PXL8_ASE_CHUNK_PALETTE: // 0x2019 + if (ase_file->palette.colors) { + SDL_free(ase_file->palette.colors); + } + result = parse_palette_chunk(chunk_payload, &ase_file->palette); + pxl8_debug("Parsed new palette: %d colors, indices %d-%d", ase_file->palette.entry_count, ase_file->palette.first_color, ase_file->palette.last_color); + break; + + default: + break; + } + + if (result != PXL8_OK) break; + chunk_data += chunk_header.chunk_size; + } + + if (result != PXL8_OK) break; + frame_data += frame_header.frame_bytes; + } + + pxl8_io_free_binary_data(file_data); + + if (result != PXL8_OK) { + pxl8_ase_free(ase_file); + } + + return result; +} + +void pxl8_ase_free(pxl8_ase_file* ase_file) { + if (!ase_file) return; + + if (ase_file->frames) { + for (u32 i = 0; i < ase_file->frame_count; i++) { + if (ase_file->frames[i].pixels) { + SDL_free(ase_file->frames[i].pixels); + } + } + SDL_free(ase_file->frames); + } + + if (ase_file->palette.colors) { + SDL_free(ase_file->palette.colors); + } + + if (ase_file->layers) { + for (u32 i = 0; i < ase_file->layer_count; i++) { + if (ase_file->layers[i].name) { + SDL_free(ase_file->layers[i].name); + } + } + SDL_free(ase_file->layers); + } + + memset(ase_file, 0, sizeof(pxl8_ase_file)); +} \ No newline at end of file diff --git a/src/pxl8_ase.h b/src/pxl8_ase.h new file mode 100644 index 0000000..2ff3460 --- /dev/null +++ b/src/pxl8_ase.h @@ -0,0 +1,98 @@ +#pragma once + +#include "pxl8_types.h" + +#define PXL8_ASE_MAGIC 0xA5E0 +#define PXL8_ASE_FRAME_MAGIC 0xF1FA +#define PXL8_ASE_CHUNK_CEL 0x2005 +#define PXL8_ASE_CHUNK_LAYER 0x2004 +#define PXL8_ASE_CHUNK_OLD_PALETTE 0x0004 +#define PXL8_ASE_CHUNK_PALETTE 0x2019 + +typedef struct pxl8_ase_header { + u32 file_size; + u16 magic; + u16 frames; + u16 width; + u16 height; + u16 color_depth; + u32 flags; + u16 speed; + u32 transparent_index; + u8 n_colors; + u8 pixel_width; + u8 pixel_height; + i16 grid_x; + i16 grid_y; + u16 grid_width; + u16 grid_height; +} pxl8_ase_header; + +typedef struct pxl8_ase_frame_header { + u32 frame_bytes; + u16 magic; + u16 chunks; + u16 duration; +} pxl8_ase_frame_header; + +typedef struct pxl8_ase_chunk_header { + u32 chunk_size; + u16 chunk_type; +} pxl8_ase_chunk_header; + +typedef struct pxl8_ase_cel { + u16 layer_index; + i16 x; + i16 y; + u8 opacity; + u16 cel_type; + u16 width; + u16 height; + u8* pixel_data; +} pxl8_ase_cel; + +typedef struct pxl8_ase_layer { + u16 flags; + u16 layer_type; + u16 child_level; + u16 blend_mode; + u8 opacity; + char* name; +} pxl8_ase_layer; + +typedef struct pxl8_ase_palette { + u32 entry_count; + u32 first_color; + u32 last_color; + u32* colors; +} pxl8_ase_palette; + +typedef struct pxl8_ase_frame { + u16 frame_id; + u16 width; + u16 height; + i16 x; + i16 y; + u16 duration; + u8* pixels; +} pxl8_ase_frame; + +typedef struct pxl8_ase_file { + u32 frame_count; + pxl8_ase_frame* frames; + pxl8_ase_header header; + u32 layer_count; + pxl8_ase_layer* layers; + pxl8_ase_palette palette; +} pxl8_ase_file; + +#ifdef __cplusplus +extern "C" { +#endif + +pxl8_result pxl8_ase_load(const char* filepath, pxl8_ase_file* ase_file); +void pxl8_ase_free(pxl8_ase_file* ase_file); + +#ifdef __cplusplus +} +#endif diff --git a/src/pxl8_blit.c b/src/pxl8_blit.c new file mode 100644 index 0000000..23e6682 --- /dev/null +++ b/src/pxl8_blit.c @@ -0,0 +1,58 @@ +#include "pxl8_blit.h" +#include "pxl8_simd.h" + +void pxl8_blit_simd_indexed(u8* fb, u32 fb_width, const u8* sprite, u32 atlas_width, + i32 x, i32 y, u32 w, u32 h) { + u8* dest_base = fb + y * fb_width + x; + const u8* src_base = sprite; + + for (u32 row = 0; row < h; row++) { + u8* dest_row = dest_base + row * fb_width; + const u8* src_row = src_base + row * atlas_width; + + u32 col = 0; + for (; col + PXL8_SIMD_WIDTH_U8 <= w; col += PXL8_SIMD_WIDTH_U8) { + pxl8_simd_vec src_vec = pxl8_simd_load_u8(src_row + col); + pxl8_simd_vec dest_vec = pxl8_simd_load_u8(dest_row + col); + pxl8_simd_vec zero = pxl8_simd_zero_u8(); + pxl8_simd_vec mask = pxl8_simd_cmpeq_u8(src_vec, zero); + pxl8_simd_vec result = pxl8_simd_blendv_u8(src_vec, dest_vec, mask); + pxl8_simd_store_u8(dest_row + col, result); + } + + for (; col < w; col++) { + if (src_row[col] != 0) { + dest_row[col] = src_row[col]; + } + } + } +} + +void pxl8_blit_simd_hicolor(u32* fb, u32 fb_width, const u32* sprite, u32 atlas_width, + i32 x, i32 y, u32 w, u32 h) { + u32* dest_base = fb + y * fb_width + x; + const u32* src_base = sprite; + + for (u32 row = 0; row < h; row++) { + u32* dest_row = dest_base + row * fb_width; + const u32* src_row = src_base + row * atlas_width; + + u32 col = 0; + for (; col + PXL8_SIMD_WIDTH_U32 <= w; col += PXL8_SIMD_WIDTH_U32) { + pxl8_simd_vec src_vec = pxl8_simd_load_u32(src_row + col); + pxl8_simd_vec dest_vec = pxl8_simd_load_u32(dest_row + col); + pxl8_simd_vec alpha_mask = pxl8_simd_alpha_mask_u32(); + pxl8_simd_vec has_alpha = pxl8_simd_and(src_vec, alpha_mask); + pxl8_simd_vec zero = pxl8_simd_zero_u8(); + pxl8_simd_vec mask = pxl8_simd_cmpeq_u32(has_alpha, zero); + pxl8_simd_vec result = pxl8_simd_blendv_u32(src_vec, dest_vec, mask); + pxl8_simd_store_u32(dest_row + col, result); + } + + for (; col < w; col++) { + if (src_row[col] & 0xFF000000) { + dest_row[col] = src_row[col]; + } + } + } +} diff --git a/src/pxl8_blit.h b/src/pxl8_blit.h new file mode 100644 index 0000000..39c8004 --- /dev/null +++ b/src/pxl8_blit.h @@ -0,0 +1,19 @@ +#pragma once + +#include "pxl8_types.h" +#include "pxl8_simd.h" + +static inline bool pxl8_is_simd_aligned(u32 w) { + return w >= PXL8_SIMD_WIDTH_U8 && (w % PXL8_SIMD_WIDTH_U8 == 0); +} + +void pxl8_blit_simd_indexed( + u8* fb, u32 fb_width, + const u8* sprite, u32 atlas_width, + i32 x, i32 y, u32 w, u32 h +); +void pxl8_blit_simd_hicolor( + u32* fb, u32 fb_width, + const u32* sprite, u32 atlas_width, + i32 x, i32 y, u32 w, u32 h +); diff --git a/src/pxl8_font.c b/src/pxl8_font.c new file mode 100644 index 0000000..3e21327 --- /dev/null +++ b/src/pxl8_font.c @@ -0,0 +1,60 @@ +#include "pxl8_font.h" +#include +#include + +const pxl8_glyph* pxl8_font_find_glyph(const pxl8_font* font, u32 codepoint) { + if (!font || !font->glyphs) return NULL; + + for (u32 i = 0; i < font->glyph_count; i++) { + if (font->glyphs[i].codepoint == codepoint) { + return &font->glyphs[i]; + } + } + + return NULL; +} + +pxl8_result pxl8_font_create_atlas(const pxl8_font* font, u8** atlas_data, i32* atlas_width, i32* atlas_height) { + if (!font || !atlas_data || !atlas_width || !atlas_height) { + return PXL8_ERROR_NULL_POINTER; + } + + i32 glyphs_per_row = 16; + i32 rows_needed = (font->glyph_count + glyphs_per_row - 1) / glyphs_per_row; + + *atlas_width = glyphs_per_row * font->default_width; + *atlas_height = rows_needed * font->default_height; + + i32 atlas_size = (*atlas_width) * (*atlas_height); + *atlas_data = (u8*)SDL_malloc(atlas_size); + if (!*atlas_data) { + return PXL8_ERROR_OUT_OF_MEMORY; + } + + memset(*atlas_data, 0, atlas_size); + + for (u32 i = 0; i < font->glyph_count; i++) { + const pxl8_glyph* glyph = &font->glyphs[i]; + + i32 atlas_x = (i % glyphs_per_row) * font->default_width; + i32 atlas_y = (i / glyphs_per_row) * font->default_height; + + for (i32 y = 0; y < glyph->height && y < font->default_height; y++) { + for (i32 x = 0; x < glyph->width && x < font->default_width; x++) { + i32 glyph_idx = y * 8 + x; + i32 atlas_idx = (atlas_y + y) * (*atlas_width) + (atlas_x + x); + + if (glyph->format == PXL8_FONT_FORMAT_INDEXED) { + u8 pixel_byte = glyph->data.indexed[glyph_idx / 8]; + u8 pixel_bit = (pixel_byte >> (7 - (glyph_idx % 8))) & 1; + (*atlas_data)[atlas_idx] = pixel_bit ? 255 : 0; + } else { + u32 rgba_pixel = glyph->data.rgba[glyph_idx]; + (*atlas_data)[atlas_idx] = (rgba_pixel >> 24) & 0xFF; + } + } + } + } + + return PXL8_OK; +} \ No newline at end of file diff --git a/src/pxl8_font.h b/src/pxl8_font.h new file mode 100644 index 0000000..590f2f8 --- /dev/null +++ b/src/pxl8_font.h @@ -0,0 +1,130 @@ +#pragma once + +#include "pxl8_types.h" + +#define PXL8_FONT_FORMAT_INDEXED 0 +#define PXL8_FONT_FORMAT_RGBA 1 + +typedef struct pxl8_glyph { + u32 codepoint; + u8 width; + u8 height; + u8 format; + union { + u8 indexed[64]; + u32 rgba[64]; + } data; +} pxl8_glyph; + +typedef struct pxl8_font { + const pxl8_glyph* glyphs; + u32 glyph_count; + u8 default_width; + u8 default_height; +} pxl8_font; + +static const pxl8_glyph pxl8_ascii_glyphs[] = { + { 32, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 } } }, + { 33, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x18, 0x3C, 0x3C, 0x18, 0x18, 0x00, 0x18, 0x00 } } }, + { 34, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x36, 0x36, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 } } }, + { 35, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x36, 0x36, 0x7F, 0x36, 0x7F, 0x36, 0x36, 0x00 } } }, + { 36, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x0C, 0x3E, 0x03, 0x1E, 0x30, 0x1F, 0x0C, 0x00 } } }, + { 37, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x00, 0x63, 0x33, 0x18, 0x0C, 0x66, 0x63, 0x00 } } }, + { 38, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x1C, 0x36, 0x1C, 0x6E, 0x3B, 0x33, 0x6E, 0x00 } } }, + { 39, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x06, 0x06, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00 } } }, + { 40, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x18, 0x0C, 0x06, 0x06, 0x06, 0x0C, 0x18, 0x00 } } }, + { 41, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x06, 0x0C, 0x18, 0x18, 0x18, 0x0C, 0x06, 0x00 } } }, + { 42, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x00, 0x66, 0x3C, 0xFF, 0x3C, 0x66, 0x00, 0x00 } } }, + { 43, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x00, 0x0C, 0x0C, 0x3F, 0x0C, 0x0C, 0x00, 0x00 } } }, + { 44, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x06, 0x00 } } }, + { 45, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x00, 0x00, 0x00, 0x3F, 0x00, 0x00, 0x00, 0x00 } } }, + { 46, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x0C, 0x00 } } }, + { 47, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x60, 0x30, 0x18, 0x0C, 0x06, 0x03, 0x01, 0x00 } } }, + { 48, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x3E, 0x63, 0x73, 0x7B, 0x6F, 0x67, 0x3E, 0x00 } } }, + { 49, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x0C, 0x0E, 0x0C, 0x0C, 0x0C, 0x0C, 0x3F, 0x00 } } }, + { 50, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x1E, 0x33, 0x30, 0x1C, 0x06, 0x33, 0x3F, 0x00 } } }, + { 51, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x1E, 0x33, 0x30, 0x1C, 0x30, 0x33, 0x1E, 0x00 } } }, + { 52, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x38, 0x3C, 0x36, 0x33, 0x7F, 0x30, 0x78, 0x00 } } }, + { 53, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x3F, 0x03, 0x1F, 0x30, 0x30, 0x33, 0x1E, 0x00 } } }, + { 54, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x1C, 0x06, 0x03, 0x1F, 0x33, 0x33, 0x1E, 0x00 } } }, + { 55, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x3F, 0x33, 0x30, 0x18, 0x0C, 0x0C, 0x0C, 0x00 } } }, + { 56, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x1E, 0x33, 0x33, 0x1E, 0x33, 0x33, 0x1E, 0x00 } } }, + { 57, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x1E, 0x33, 0x33, 0x3E, 0x30, 0x18, 0x0E, 0x00 } } }, + { 58, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x00, 0x0C, 0x0C, 0x00, 0x00, 0x0C, 0x0C, 0x00 } } }, + { 59, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x00, 0x0C, 0x0C, 0x00, 0x00, 0x0C, 0x06, 0x00 } } }, + { 60, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x18, 0x0C, 0x06, 0x03, 0x06, 0x0C, 0x18, 0x00 } } }, + { 61, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x00, 0x00, 0x3F, 0x00, 0x00, 0x3F, 0x00, 0x00 } } }, + { 62, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x06, 0x0C, 0x18, 0x30, 0x18, 0x0C, 0x06, 0x00 } } }, + { 63, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x1E, 0x33, 0x30, 0x18, 0x0C, 0x00, 0x0C, 0x00 } } }, + { 64, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x3E, 0x63, 0x7B, 0x7B, 0x7B, 0x03, 0x1E, 0x00 } } }, + { 65, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x0C, 0x1E, 0x33, 0x33, 0x3F, 0x33, 0x33, 0x00 } } }, + { 66, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x3F, 0x66, 0x66, 0x3E, 0x66, 0x66, 0x3F, 0x00 } } }, + { 67, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x3C, 0x66, 0x03, 0x03, 0x03, 0x66, 0x3C, 0x00 } } }, + { 68, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x1F, 0x36, 0x66, 0x66, 0x66, 0x36, 0x1F, 0x00 } } }, + { 69, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x7F, 0x46, 0x16, 0x1E, 0x16, 0x46, 0x7F, 0x00 } } }, + { 70, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x7F, 0x46, 0x16, 0x1E, 0x16, 0x06, 0x0F, 0x00 } } }, + { 71, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x3C, 0x66, 0x03, 0x03, 0x73, 0x66, 0x7C, 0x00 } } }, + { 72, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x33, 0x33, 0x33, 0x3F, 0x33, 0x33, 0x33, 0x00 } } }, + { 73, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x1E, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x1E, 0x00 } } }, + { 74, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x78, 0x30, 0x30, 0x30, 0x33, 0x33, 0x1E, 0x00 } } }, + { 75, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x67, 0x66, 0x36, 0x1E, 0x36, 0x66, 0x67, 0x00 } } }, + { 76, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x0F, 0x06, 0x06, 0x06, 0x46, 0x66, 0x7F, 0x00 } } }, + { 77, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x63, 0x77, 0x7F, 0x7F, 0x6B, 0x63, 0x63, 0x00 } } }, + { 78, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x63, 0x67, 0x6F, 0x7B, 0x73, 0x63, 0x63, 0x00 } } }, + { 79, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x1C, 0x36, 0x63, 0x63, 0x63, 0x36, 0x1C, 0x00 } } }, + { 80, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x3F, 0x66, 0x66, 0x3E, 0x06, 0x06, 0x0F, 0x00 } } }, + { 81, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x1E, 0x33, 0x33, 0x33, 0x3B, 0x1E, 0x38, 0x00 } } }, + { 82, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x3F, 0x66, 0x66, 0x3E, 0x36, 0x66, 0x67, 0x00 } } }, + { 83, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x1E, 0x33, 0x07, 0x0E, 0x38, 0x33, 0x1E, 0x00 } } }, + { 84, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x3F, 0x2D, 0x0C, 0x0C, 0x0C, 0x0C, 0x1E, 0x00 } } }, + { 85, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x3F, 0x00 } } }, + { 86, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x33, 0x33, 0x33, 0x33, 0x33, 0x1E, 0x0C, 0x00 } } }, + { 87, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x63, 0x63, 0x63, 0x6B, 0x7F, 0x77, 0x63, 0x00 } } }, + { 88, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x63, 0x63, 0x36, 0x1C, 0x1C, 0x36, 0x63, 0x00 } } }, + { 89, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x33, 0x33, 0x33, 0x1E, 0x0C, 0x0C, 0x1E, 0x00 } } }, + { 90, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x7F, 0x63, 0x31, 0x18, 0x4C, 0x66, 0x7F, 0x00 } } }, + { 97, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x00, 0x00, 0x1E, 0x30, 0x3E, 0x33, 0x6E, 0x00 } } }, + { 98, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x07, 0x06, 0x06, 0x3E, 0x66, 0x66, 0x3B, 0x00 } } }, + { 99, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x00, 0x00, 0x1E, 0x33, 0x03, 0x33, 0x1E, 0x00 } } }, + { 100, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x38, 0x30, 0x30, 0x3E, 0x33, 0x33, 0x6E, 0x00 } } }, + { 101, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x00, 0x00, 0x1E, 0x33, 0x3F, 0x03, 0x1E, 0x00 } } }, + { 102, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x1C, 0x36, 0x06, 0x0F, 0x06, 0x06, 0x0F, 0x00 } } }, + { 103, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x00, 0x00, 0x6E, 0x33, 0x33, 0x3E, 0x30, 0x1F } } }, + { 104, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x07, 0x06, 0x36, 0x6E, 0x66, 0x66, 0x67, 0x00 } } }, + { 105, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x0C, 0x00, 0x0E, 0x0C, 0x0C, 0x0C, 0x1E, 0x00 } } }, + { 106, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x30, 0x00, 0x30, 0x30, 0x30, 0x33, 0x33, 0x1E } } }, + { 107, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x07, 0x06, 0x66, 0x36, 0x1E, 0x36, 0x67, 0x00 } } }, + { 108, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x0E, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x1E, 0x00 } } }, + { 109, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x00, 0x00, 0x33, 0x7F, 0x7F, 0x6B, 0x63, 0x00 } } }, + { 110, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x00, 0x00, 0x1F, 0x33, 0x33, 0x33, 0x33, 0x00 } } }, + { 111, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x00, 0x00, 0x1E, 0x33, 0x33, 0x33, 0x1E, 0x00 } } }, + { 112, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x00, 0x00, 0x3B, 0x66, 0x66, 0x3E, 0x06, 0x0F } } }, + { 113, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x00, 0x00, 0x6E, 0x33, 0x33, 0x3E, 0x30, 0x78 } } }, + { 114, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x00, 0x00, 0x3B, 0x6E, 0x66, 0x06, 0x0F, 0x00 } } }, + { 115, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x00, 0x00, 0x3E, 0x03, 0x1E, 0x30, 0x1F, 0x00 } } }, + { 116, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x08, 0x0C, 0x3E, 0x0C, 0x0C, 0x2C, 0x18, 0x00 } } }, + { 117, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x00, 0x00, 0x33, 0x33, 0x33, 0x33, 0x6E, 0x00 } } }, + { 118, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x00, 0x00, 0x33, 0x33, 0x33, 0x1E, 0x0C, 0x00 } } }, + { 119, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x00, 0x00, 0x63, 0x6B, 0x7F, 0x7F, 0x36, 0x00 } } }, + { 120, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x00, 0x00, 0x63, 0x36, 0x1C, 0x36, 0x63, 0x00 } } }, + { 121, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x00, 0x00, 0x33, 0x33, 0x33, 0x3E, 0x30, 0x1F } } }, + { 122, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x00, 0x00, 0x3F, 0x19, 0x0C, 0x26, 0x3F, 0x00 } } } +}; + +static const pxl8_font pxl8_default_font = { + pxl8_ascii_glyphs, + sizeof(pxl8_ascii_glyphs) / sizeof(pxl8_glyph), + 8, + 8 +}; + +#ifdef __cplusplus +extern "C" { +#endif + +const pxl8_glyph* pxl8_font_find_glyph(const pxl8_font* font, u32 codepoint); +pxl8_result pxl8_font_create_atlas(const pxl8_font* font, u8** atlas_data, i32* atlas_width, i32* atlas_height); + +#ifdef __cplusplus +} +#endif \ No newline at end of file diff --git a/src/pxl8_gfx.c b/src/pxl8_gfx.c new file mode 100644 index 0000000..084a154 --- /dev/null +++ b/src/pxl8_gfx.c @@ -0,0 +1,713 @@ +#include "pxl8_gfx.h" +#include "pxl8_ase.h" +#include "pxl8_macros.h" +#include "pxl8_types.h" +#include "pxl8_blit.h" +#include + +static u32 pxl8_get_palette_size(pxl8_color_mode mode) { + switch (mode) { + case PXL8_COLOR_MODE_HICOLOR: return 0; + case PXL8_COLOR_MODE_FAMI: return 64; + case PXL8_COLOR_MODE_MEGA: return 512; + case PXL8_COLOR_MODE_GBA: return 32768; + case PXL8_COLOR_MODE_SUPERFAMI: return 32768; + default: return 256; + } +} + +void pxl8_gfx_get_resolution_dimensions(pxl8_resolution resolution, i32* width, i32* height) { + switch (resolution) { + case PXL8_RESOLUTION_240x160: + *width = 240; *height = 160; break; + case PXL8_RESOLUTION_320x180: + *width = 320; *height = 180; break; + case PXL8_RESOLUTION_320x240: + *width = 320; *height = 240; break; + case PXL8_RESOLUTION_640x360: + *width = 640; *height = 360; break; + case PXL8_RESOLUTION_640x480: + *width = 640; *height = 480; break; + case PXL8_RESOLUTION_800x600: + *width = 800; *height = 600; break; + case PXL8_RESOLUTION_960x540: + *width = 960; *height = 540; break; + default: + *width = 640; *height = 360; break; + } +} + +pxl8_result pxl8_gfx_init( + pxl8_gfx_ctx* ctx, + pxl8_color_mode mode, + pxl8_resolution resolution, + const char* title, + i32 window_width, + i32 window_height +) { + memset(ctx, 0, sizeof(pxl8_gfx_ctx)); + + ctx->color_mode = mode; + pxl8_gfx_get_resolution_dimensions(resolution, &ctx->framebuffer_width, &ctx->framebuffer_height); + + ctx->window = SDL_CreateWindow( + title, + window_width, window_height, + SDL_WINDOW_RESIZABLE + ); + + if (!ctx->window) { + pxl8_error("Failed to create window: %s", SDL_GetError()); + return PXL8_ERROR_SYSTEM_FAILURE; + } + + ctx->renderer = SDL_CreateRenderer(ctx->window, NULL); + if (!ctx->renderer) { + pxl8_error("Failed to create renderer: %s", SDL_GetError()); + SDL_DestroyWindow(ctx->window); + return PXL8_ERROR_SYSTEM_FAILURE; + } + + SDL_SetRenderLogicalPresentation(ctx->renderer, + ctx->framebuffer_width, + ctx->framebuffer_height, + SDL_LOGICAL_PRESENTATION_INTEGER_SCALE); + + i32 bytes_per_pixel = (ctx->color_mode == PXL8_COLOR_MODE_HICOLOR) ? 4 : 1; + i32 fb_size = ctx->framebuffer_width * ctx->framebuffer_height * bytes_per_pixel; + ctx->framebuffer = (u8*)SDL_calloc(1, fb_size); + if (!ctx->framebuffer) { + pxl8_error("Failed to allocate framebuffer"); + pxl8_gfx_shutdown(ctx); + return PXL8_ERROR_OUT_OF_MEMORY; + } + + ctx->framebuffer_texture = SDL_CreateTexture( + ctx->renderer, + SDL_PIXELFORMAT_RGBA32, + SDL_TEXTUREACCESS_STREAMING, + ctx->framebuffer_width, + ctx->framebuffer_height + ); + + SDL_SetTextureScaleMode(ctx->framebuffer_texture, SDL_SCALEMODE_NEAREST); + + if (!ctx->framebuffer_texture) { + pxl8_error("Failed to create framebuffer texture: %s", SDL_GetError()); + pxl8_gfx_shutdown(ctx); + return PXL8_ERROR_SYSTEM_FAILURE; + } + + ctx->palette_size = pxl8_get_palette_size(mode); + if (ctx->palette_size > 0) { + ctx->palette = (u32*)SDL_calloc(ctx->palette_size, sizeof(u32)); + if (!ctx->palette) { + pxl8_error("Failed to allocate palette"); + pxl8_gfx_shutdown(ctx); + return PXL8_ERROR_OUT_OF_MEMORY; + } + + for (u32 i = 0; i < 256 && i < ctx->palette_size; i++) { + u8 gray = (u8)(i * 255 / 255); + ctx->palette[i] = 0xFF000000 | (gray << 16) | (gray << 8) | gray; + } + } + + ctx->viewport_x = 0; + ctx->viewport_y = 0; + ctx->viewport_width = ctx->framebuffer_width; + ctx->viewport_height = ctx->framebuffer_height; + + ctx->initialized = true; + return PXL8_OK; +} + +void pxl8_gfx_shutdown(pxl8_gfx_ctx* ctx) { + if (!ctx) return; + + if (ctx->framebuffer_texture) { + SDL_DestroyTexture(ctx->framebuffer_texture); + ctx->framebuffer_texture = NULL; + } + + if (ctx->sprite_atlas_texture) { + SDL_DestroyTexture(ctx->sprite_atlas_texture); + ctx->sprite_atlas_texture = NULL; + } + + if (ctx->renderer) { + SDL_DestroyRenderer(ctx->renderer); + ctx->renderer = NULL; + } + + if (ctx->window) { + SDL_DestroyWindow(ctx->window); + ctx->window = NULL; + } + + SDL_free(ctx->framebuffer); + SDL_free(ctx->palette); + SDL_free(ctx->atlas); + SDL_free(ctx->atlas_entries); + + ctx->initialized = false; +} + +pxl8_result pxl8_gfx_init_atlas(pxl8_gfx_ctx* ctx, u32 width, u32 height) { + if (!ctx || !ctx->initialized) return PXL8_ERROR_INVALID_ARGUMENT; + + ctx->sprite_atlas_width = width; + ctx->sprite_atlas_height = height; + + i32 bytes_per_pixel = (ctx->color_mode == PXL8_COLOR_MODE_HICOLOR) ? 4 : 1; + ctx->atlas = (u8*)SDL_calloc(width * height, bytes_per_pixel); + if (!ctx->atlas) { + return PXL8_ERROR_OUT_OF_MEMORY; + } + + ctx->sprite_atlas_texture = SDL_CreateTexture( + ctx->renderer, + SDL_PIXELFORMAT_RGBA32, + SDL_TEXTUREACCESS_STREAMING, + width, + height + ); + + if (!ctx->sprite_atlas_texture) { + SDL_free(ctx->atlas); + ctx->atlas = NULL; + return PXL8_ERROR_SYSTEM_FAILURE; + } + + ctx->atlas_entries_cap = 256; + ctx->atlas_entries = (pxl8_atlas_entry*)SDL_calloc(ctx->atlas_entries_cap, sizeof(pxl8_atlas_entry)); + if (!ctx->atlas_entries) { + SDL_DestroyTexture(ctx->sprite_atlas_texture); + SDL_free(ctx->atlas); + return PXL8_ERROR_OUT_OF_MEMORY; + } + + ctx->sprite_frames_per_row = width / 128; + if (ctx->sprite_frames_per_row == 0) ctx->sprite_frames_per_row = 1; + + return PXL8_OK; +} + +pxl8_result pxl8_gfx_load_sprite(pxl8_gfx_ctx* ctx, const char* path) { + if (!ctx || !ctx->initialized || !path) return PXL8_ERROR_INVALID_ARGUMENT; + if (!ctx->atlas) { + pxl8_error("Atlas not initialized"); + return PXL8_ERROR_NOT_INITIALIZED; + } + + for (u32 i = 0; i < ctx->atlas_entries_len; i++) { + if (strcmp(ctx->atlas_entries[i].path, path) == 0) { + return PXL8_OK; + } + } + + pxl8_ase_file ase_file; + pxl8_result result = pxl8_ase_load(path, &ase_file); + if (result != PXL8_OK) { + pxl8_error("Failed to load ASE file: %s", path); + return result; + } + + if (ase_file.frame_count == 0) { + pxl8_error("No frames in ASE file"); + pxl8_ase_free(&ase_file); + return PXL8_ERROR_INVALID_FORMAT; + } + + u32 sprite_w = ase_file.header.width; + u32 sprite_h = ase_file.header.height; + + if (ctx->sprite_frame_width == 0 || ctx->sprite_frame_height == 0) { + ctx->sprite_frame_width = sprite_w; + ctx->sprite_frame_height = sprite_h; + } + + u32 id = ctx->atlas_entries_len; + u32 frames_per_row = ctx->sprite_frames_per_row; + u32 atlas_x = (id % frames_per_row) * ctx->sprite_frame_width; + u32 atlas_y = (id / frames_per_row) * ctx->sprite_frame_height; + + if (atlas_x + sprite_w > ctx->sprite_atlas_width || + atlas_y + sprite_h > ctx->sprite_atlas_height) { + pxl8_error("Sprite doesn't fit in atlas"); + pxl8_ase_free(&ase_file); + return PXL8_ERROR_INVALID_SIZE; + } + + i32 bytes_per_pixel = (ctx->color_mode == PXL8_COLOR_MODE_HICOLOR) ? 4 : 1; + + for (u32 y = 0; y < sprite_h; y++) { + for (u32 x = 0; x < sprite_w; x++) { + u32 frame_idx = y * sprite_w + x; + u32 atlas_idx = (atlas_y + y) * ctx->sprite_atlas_width + (atlas_x + x); + + if (bytes_per_pixel == 4) { + ((u32*)ctx->atlas)[atlas_idx] = ((u32*)ase_file.frames[0].pixels)[frame_idx]; + } else { + ctx->atlas[atlas_idx] = ase_file.frames[0].pixels[frame_idx]; + } + } + } + + if (ctx->atlas_entries_len >= ctx->atlas_entries_cap) { + ctx->atlas_entries_cap *= 2; + pxl8_atlas_entry* new_entries = (pxl8_atlas_entry*)SDL_realloc( + ctx->atlas_entries, + ctx->atlas_entries_cap * sizeof(pxl8_atlas_entry) + ); + if (!new_entries) { + pxl8_ase_free(&ase_file); + return PXL8_ERROR_OUT_OF_MEMORY; + } + ctx->atlas_entries = new_entries; + } + + ctx->atlas_entries[id].x = atlas_x; + ctx->atlas_entries[id].y = atlas_y; + ctx->atlas_entries[id].w = sprite_w; + ctx->atlas_entries[id].h = sprite_h; + ctx->atlas_entries[id].sprite_id = id; + strncpy(ctx->atlas_entries[id].path, path, sizeof(ctx->atlas_entries[id].path) - 1); + ctx->atlas_entries[id].path[sizeof(ctx->atlas_entries[id].path) - 1] = '\0'; + ctx->atlas_entries_len++; + + ctx->atlas_dirty = true; + + pxl8_ase_free(&ase_file); + pxl8_debug("Loaded sprite %u: %ux%u at (%u,%u)", id, sprite_w, sprite_h, atlas_x, atlas_y); + + return id; +} + +pxl8_result pxl8_gfx_load_palette(pxl8_gfx_ctx* ctx, const char* path) { + if (!ctx || !ctx->initialized || !path) return PXL8_ERROR_INVALID_ARGUMENT; + if (ctx->color_mode == PXL8_COLOR_MODE_HICOLOR) return PXL8_OK; + + pxl8_debug("Loading palette from: %s", path); + + pxl8_ase_file ase_file; + pxl8_result result = pxl8_ase_load(path, &ase_file); + if (result != PXL8_OK) { + pxl8_error("Failed to load ASE file for palette: %s", path); + return result; + } + + if (ase_file.palette.entry_count == 0) { + pxl8_error("No palette data in ASE file"); + pxl8_ase_free(&ase_file); + return PXL8_ERROR_INVALID_FORMAT; + } + + u32 copy_size = (ase_file.palette.entry_count < ctx->palette_size) ? ase_file.palette.entry_count : ctx->palette_size; + memcpy(ctx->palette, ase_file.palette.colors, copy_size * sizeof(u32)); + + if (ctx->color_mode != PXL8_COLOR_MODE_HICOLOR && ctx->framebuffer_texture) { + SDL_Color colors[256]; + for (u32 i = 0; i < 256 && i < ctx->palette_size; i++) { + colors[i].r = (ctx->palette[i] >> 16) & 0xFF; + colors[i].g = (ctx->palette[i] >> 8) & 0xFF; + colors[i].b = ctx->palette[i] & 0xFF; + colors[i].a = (ctx->palette[i] >> 24) & 0xFF; + } + SDL_SetPaletteColors(SDL_CreateSurfacePalette(NULL), colors, 0, 256); + } + + pxl8_ase_free(&ase_file); + pxl8_debug("Loaded palette with %u colors", copy_size); + + return PXL8_OK; +} + +pxl8_result pxl8_gfx_load_font_atlas(pxl8_gfx_ctx* ctx) { + (void)ctx; + return PXL8_OK; +} + +void pxl8_gfx_upload_framebuffer(pxl8_gfx_ctx* ctx) { + if (!ctx || !ctx->initialized || !ctx->framebuffer_texture) return; + + if (ctx->color_mode == PXL8_COLOR_MODE_HICOLOR) { + SDL_UpdateTexture(ctx->framebuffer_texture, NULL, ctx->framebuffer, + ctx->framebuffer_width * 4); + } else { + static u32* rgba_buffer = NULL; + static size_t buffer_size = 0; + size_t needed_size = ctx->framebuffer_width * ctx->framebuffer_height; + + if (buffer_size < needed_size) { + rgba_buffer = (u32*)SDL_realloc(rgba_buffer, needed_size * 4); + buffer_size = needed_size; + } + + if (!rgba_buffer) return; + + for (i32 i = 0; i < ctx->framebuffer_width * ctx->framebuffer_height; i++) { + u8 index = ctx->framebuffer[i]; + rgba_buffer[i] = (index < ctx->palette_size) ? ctx->palette[index] : 0xFF000000; + } + + SDL_UpdateTexture(ctx->framebuffer_texture, NULL, rgba_buffer, + ctx->framebuffer_width * 4); + } +} + +void pxl8_gfx_upload_atlas(pxl8_gfx_ctx* ctx) { + if (!ctx || !ctx->initialized || !ctx->sprite_atlas_texture || !ctx->atlas_dirty) return; + + if (ctx->color_mode == PXL8_COLOR_MODE_HICOLOR) { + SDL_UpdateTexture(ctx->sprite_atlas_texture, NULL, ctx->atlas, + ctx->sprite_atlas_width * 4); + } else { + u32* rgba_buffer = (u32*)SDL_malloc(ctx->sprite_atlas_width * ctx->sprite_atlas_height * 4); + if (!rgba_buffer) return; + + for (u32 i = 0; i < ctx->sprite_atlas_width * ctx->sprite_atlas_height; i++) { + u8 index = ctx->atlas[i]; + rgba_buffer[i] = (index < ctx->palette_size) ? ctx->palette[index] : 0x00000000; + } + + SDL_UpdateTexture(ctx->sprite_atlas_texture, NULL, rgba_buffer, + ctx->sprite_atlas_width * 4); + SDL_free(rgba_buffer); + } + + ctx->atlas_dirty = false; + pxl8_debug("Atlas uploaded to GPU"); +} + +void pxl8_gfx_present(pxl8_gfx_ctx* ctx) { + if (!ctx || !ctx->initialized) return; + + SDL_SetRenderDrawColor(ctx->renderer, 0, 0, 0, 255); + SDL_RenderClear(ctx->renderer); + + if (ctx->framebuffer_texture) { + SDL_RenderTexture(ctx->renderer, ctx->framebuffer_texture, NULL, NULL); + } + + SDL_RenderPresent(ctx->renderer); +} + +void pxl8_gfx_viewport(pxl8_gfx_ctx* ctx, i32 x, i32 y, i32 width, i32 height) { + if (!ctx) return; + ctx->viewport_x = x; + ctx->viewport_y = y; + ctx->viewport_width = width; + ctx->viewport_height = height; +} + +void pxl8_gfx_project(pxl8_gfx_ctx* ctx, f32 left, f32 right, f32 top, f32 bottom) { + (void)ctx; (void)left; (void)right; (void)top; (void)bottom; +} + +void pxl8_clr(pxl8_gfx_ctx* ctx, u32 color) { + if (!ctx || !ctx->framebuffer) return; + + i32 bytes_per_pixel = (ctx->color_mode == PXL8_COLOR_MODE_HICOLOR) ? 4 : 1; + i32 size = ctx->framebuffer_width * ctx->framebuffer_height; + + if (bytes_per_pixel == 4) { + u32* fb32 = (u32*)ctx->framebuffer; + for (i32 i = 0; i < size; i++) { + fb32[i] = color; + } + } else { + memset(ctx->framebuffer, color & 0xFF, size); + } +} + +void pxl8_pixel(pxl8_gfx_ctx* ctx, i32 x, i32 y, u32 color) { + if (!ctx || !ctx->framebuffer) return; + if (x < 0 || x >= ctx->framebuffer_width || y < 0 || y >= ctx->framebuffer_height) return; + + i32 idx = y * ctx->framebuffer_width + x; + if (ctx->color_mode == PXL8_COLOR_MODE_HICOLOR) { + ((u32*)ctx->framebuffer)[idx] = color; + } else { + ctx->framebuffer[idx] = color & 0xFF; + } +} + +u32 pxl8_get_pixel(pxl8_gfx_ctx* ctx, i32 x, i32 y) { + if (!ctx || !ctx->framebuffer) return 0; + if (x < 0 || x >= ctx->framebuffer_width || y < 0 || y >= ctx->framebuffer_height) return 0; + + i32 idx = y * ctx->framebuffer_width + x; + if (ctx->color_mode == PXL8_COLOR_MODE_HICOLOR) { + return ((u32*)ctx->framebuffer)[idx]; + } else { + return ctx->framebuffer[idx]; + } +} + +void pxl8_line(pxl8_gfx_ctx* ctx, i32 x0, i32 y0, i32 x1, i32 y1, u32 color) { + if (!ctx) return; + + i32 dx = abs(x1 - x0); + i32 dy = abs(y1 - y0); + i32 sx = x0 < x1 ? 1 : -1; + i32 sy = y0 < y1 ? 1 : -1; + i32 err = dx - dy; + + while (1) { + pxl8_pixel(ctx, x0, y0, color); + + if (x0 == x1 && y0 == y1) break; + + i32 e2 = 2 * err; + if (e2 > -dy) { + err -= dy; + x0 += sx; + } + if (e2 < dx) { + err += dx; + y0 += sy; + } + } +} + +void pxl8_rect(pxl8_gfx_ctx* ctx, i32 x, i32 y, i32 w, i32 h, u32 color) { + if (!ctx) return; + + pxl8_line(ctx, x, y, x + w - 1, y, color); + pxl8_line(ctx, x + w - 1, y, x + w - 1, y + h - 1, color); + pxl8_line(ctx, x + w - 1, y + h - 1, x, y + h - 1, color); + pxl8_line(ctx, x, y + h - 1, x, y, color); +} + +void pxl8_rect_fill(pxl8_gfx_ctx* ctx, i32 x, i32 y, i32 w, i32 h, u32 color) { + if (!ctx) return; + + for (i32 py = y; py < y + h; py++) { + for (i32 px = x; px < x + w; px++) { + pxl8_pixel(ctx, px, py, color); + } + } +} + +void pxl8_circle(pxl8_gfx_ctx* ctx, i32 cx, i32 cy, i32 radius, u32 color) { + if (!ctx) return; + + i32 x = radius; + i32 y = 0; + i32 err = 0; + + while (x >= y) { + pxl8_pixel(ctx, cx + x, cy + y, color); + pxl8_pixel(ctx, cx + y, cy + x, color); + pxl8_pixel(ctx, cx - y, cy + x, color); + pxl8_pixel(ctx, cx - x, cy + y, color); + pxl8_pixel(ctx, cx - x, cy - y, color); + pxl8_pixel(ctx, cx - y, cy - x, color); + pxl8_pixel(ctx, cx + y, cy - x, color); + pxl8_pixel(ctx, cx + x, cy - y, color); + + if (err <= 0) { + y += 1; + err += 2 * y + 1; + } else { + x -= 1; + err -= 2 * x + 1; + } + } +} + +void pxl8_circle_fill(pxl8_gfx_ctx* ctx, i32 cx, i32 cy, i32 radius, u32 color) { + if (!ctx) return; + + for (i32 y = -radius; y <= radius; y++) { + for (i32 x = -radius; x <= radius; x++) { + if (x * x + y * y <= radius * radius) { + pxl8_pixel(ctx, cx + x, cy + y, color); + } + } + } +} + +void pxl8_text(pxl8_gfx_ctx* ctx, const char* text, i32 x, i32 y, u32 color) { + if (!ctx || !text) return; + (void)x; (void)y; (void)color; +} + + +void pxl8_sprite(pxl8_gfx_ctx* ctx, u32 sprite_id, i32 x, i32 y, i32 w, i32 h) { + if (!ctx || !ctx->atlas || !ctx->framebuffer || sprite_id >= ctx->atlas_entries_len) return; + + pxl8_atlas_entry* entry = &ctx->atlas_entries[sprite_id]; + + i32 clip_left = (x < 0) ? -x : 0; + i32 clip_top = (y < 0) ? -y : 0; + i32 clip_right = (x + w > ctx->framebuffer_width) ? x + w - ctx->framebuffer_width : 0; + i32 clip_bottom = (y + h > ctx->framebuffer_height) ? y + h - ctx->framebuffer_height : 0; + + i32 draw_width = w - clip_left - clip_right; + i32 draw_height = h - clip_top - clip_bottom; + + if (draw_width <= 0 || draw_height <= 0) return; + + i32 dest_x = x + clip_left; + i32 dest_y = y + clip_top; + + bool is_1to1_scale = (w == entry->w && h == entry->h); + bool is_unclipped = (clip_left == 0 && clip_top == 0 && clip_right == 0 && clip_bottom == 0); + + if (is_1to1_scale && is_unclipped && pxl8_is_simd_aligned(w)) { + const u8* sprite_data = ctx->atlas + entry->y * ctx->sprite_atlas_width + entry->x; + + if (ctx->color_mode == PXL8_COLOR_MODE_HICOLOR) { + pxl8_blit_simd_hicolor((u32*)ctx->framebuffer, ctx->framebuffer_width, + (const u32*)sprite_data, ctx->sprite_atlas_width, x, y, w, h); + } else { + pxl8_blit_simd_indexed(ctx->framebuffer, ctx->framebuffer_width, + sprite_data, ctx->sprite_atlas_width, x, y, w, h); + } + } else { + for (i32 py = 0; py < draw_height; py++) { + for (i32 px = 0; px < draw_width; px++) { + i32 src_x = entry->x + ((px + clip_left) * entry->w) / w; + i32 src_y = entry->y + ((py + clip_top) * entry->h) / h; + i32 src_idx = src_y * ctx->sprite_atlas_width + src_x; + i32 dest_idx = (dest_y + py) * ctx->framebuffer_width + (dest_x + px); + + if (ctx->color_mode == PXL8_COLOR_MODE_HICOLOR) { + u32 pixel = ((u32*)ctx->atlas)[src_idx]; + if (pixel & 0xFF000000) { + ((u32*)ctx->framebuffer)[dest_idx] = pixel; + } + } else { + u8 pixel = ctx->atlas[src_idx]; + if (pixel != 0) { + ctx->framebuffer[dest_idx] = pixel; + } + } + } + } + } +} + +void pxl8_gfx_cycle_palette(pxl8_gfx_ctx* ctx, u8 start, u8 count, i32 step) { + if (!ctx || !ctx->palette || count == 0) return; + + u32 temp[256]; + for (u8 i = 0; i < count; i++) { + temp[i] = ctx->palette[start + i]; + } + + for (u8 i = 0; i < count; i++) { + u8 src_idx = i; + u8 dst_idx = (i + step) % count; + ctx->palette[start + dst_idx] = temp[src_idx]; + } +} + +void pxl8_gfx_swap_palette(pxl8_gfx_ctx* ctx, u8 start, u8 count, u32* new_colors) { + if (!ctx || !ctx->palette || !new_colors) return; + + for (u8 i = 0; i < count && (start + i) < ctx->palette_size; i++) { + ctx->palette[start + i] = new_colors[i]; + } +} + +void pxl8_gfx_fade_palette(pxl8_gfx_ctx* ctx, u8 start, u8 count, f32 amount, u32 target_color) { + if (!ctx || !ctx->palette || count == 0) return; + + if (amount < 0.0f) amount = 0.0f; + if (amount > 1.0f) amount = 1.0f; + + u8 target_r = target_color & 0xFF; + u8 target_g = (target_color >> 8) & 0xFF; + u8 target_b = (target_color >> 16) & 0xFF; + u8 target_a = (target_color >> 24) & 0xFF; + + for (u8 i = 0; i < count && (start + i) < ctx->palette_size; i++) { + u32 current = ctx->palette[start + i]; + u8 cur_r = current & 0xFF; + u8 cur_g = (current >> 8) & 0xFF; + u8 cur_b = (current >> 16) & 0xFF; + u8 cur_a = (current >> 24) & 0xFF; + + u8 new_r = cur_r + (i32)((target_r - cur_r) * amount); + u8 new_g = cur_g + (i32)((target_g - cur_g) * amount); + u8 new_b = cur_b + (i32)((target_b - cur_b) * amount); + u8 new_a = cur_a + (i32)((target_a - cur_a) * amount); + + ctx->palette[start + i] = new_r | (new_g << 8) | (new_b << 16) | (new_a << 24); + } +} + +void pxl8_gfx_interpolate_palettes(pxl8_gfx_ctx* ctx, u32* palette1, u32* palette2, u8 start, u8 count, f32 t) { + if (!ctx || !ctx->palette || !palette1 || !palette2 || count == 0) return; + + if (t < 0.0f) t = 0.0f; + if (t > 1.0f) t = 1.0f; + + for (u8 i = 0; i < count && (start + i) < ctx->palette_size; i++) { + u32 col1 = palette1[i]; + u32 col2 = palette2[i]; + + u8 r1 = col1 & 0xFF; + u8 g1 = (col1 >> 8) & 0xFF; + u8 b1 = (col1 >> 16) & 0xFF; + u8 a1 = (col1 >> 24) & 0xFF; + + u8 r2 = col2 & 0xFF; + u8 g2 = (col2 >> 8) & 0xFF; + u8 b2 = (col2 >> 16) & 0xFF; + u8 a2 = (col2 >> 24) & 0xFF; + + u8 r = r1 + (i32)((r2 - r1) * t); + u8 g = g1 + (i32)((g2 - g1) * t); + u8 b = b1 + (i32)((b2 - b1) * t); + u8 a = a1 + (i32)((a2 - a1) * t); + + ctx->palette[start + i] = r | (g << 8) | (b << 16) | (a << 24); + } +} + +void pxl8_gfx_color_ramp(pxl8_gfx_ctx* ctx, u8 start, u8 count, u32 from_color, u32 to_color) { + if (!ctx || !ctx->palette || count <= 1) return; + + u8 from_r = from_color & 0xFF; + u8 from_g = (from_color >> 8) & 0xFF; + u8 from_b = (from_color >> 16) & 0xFF; + u8 from_a = (from_color >> 24) & 0xFF; + + u8 to_r = to_color & 0xFF; + u8 to_g = (to_color >> 8) & 0xFF; + u8 to_b = (to_color >> 16) & 0xFF; + u8 to_a = (to_color >> 24) & 0xFF; + + for (u8 i = 0; i < count && (start + i) < ctx->palette_size; i++) { + f32 t = (f32)i / (f32)(count - 1); + + u8 r = from_r + (i32)((to_r - from_r) * t); + u8 g = from_g + (i32)((to_g - from_g) * t); + u8 b = from_b + (i32)((to_b - from_b) * t); + u8 a = from_a + (i32)((to_a - from_a) * t); + + ctx->palette[start + i] = r | (g << 8) | (b << 16) | (a << 24); + } +} + +void pxl8_gfx_process_effects(pxl8_gfx_ctx* ctx, pxl8_effects* effects, f32 dt) { + if (!ctx || !effects) return; + + effects->time += dt; + + for (i32 i = 0; i < 8; i++) { + pxl8_palette_cycle* cycle = &effects->palette_cycles[i]; + if (!cycle->active) continue; + + cycle->timer += dt * cycle->speed; + if (cycle->timer >= 1.0f) { + cycle->timer -= 1.0f; + i32 count = cycle->end_index - cycle->start_index + 1; + pxl8_gfx_cycle_palette(ctx, cycle->start_index, count, 1); + } + } +} diff --git a/src/pxl8_gfx.h b/src/pxl8_gfx.h new file mode 100644 index 0000000..bc3b2cd --- /dev/null +++ b/src/pxl8_gfx.h @@ -0,0 +1,123 @@ +#pragma once + +#include +#include + +#include "pxl8_types.h" +#include "pxl8_blit.h" + +typedef struct pxl8_atlas_entry { + char path[256]; + u32 sprite_id; + i32 x, y, w, h; +} pxl8_atlas_entry; + +typedef struct pxl8_gfx_ctx { + SDL_Renderer* renderer; + SDL_Texture* framebuffer_texture; + SDL_Texture* sprite_atlas_texture; + SDL_Window* window; + + u8* framebuffer; + u32* palette; + u32 palette_size; + + i32 framebuffer_width; + i32 framebuffer_height; + pxl8_color_mode color_mode; + + u32 sprite_atlas_width; + u32 sprite_atlas_height; + u32 sprite_frame_width; + u32 sprite_frame_height; + u32 sprite_frames_per_row; + + pxl8_atlas_entry* atlas_entries; + u32 atlas_entries_len; + u32 atlas_entries_cap; + + u8* atlas; + bool atlas_dirty; + + i32 viewport_x, viewport_y; + i32 viewport_width, viewport_height; + + bool initialized; +} pxl8_gfx_ctx; + +typedef enum pxl8_blend_mode { + PXL8_BLEND_NONE, + PXL8_BLEND_ALPHA, + PXL8_BLEND_ADD, + PXL8_BLEND_MULTIPLY +} pxl8_blend_mode; + +typedef struct pxl8_palette_cycle { + u8 start_index; + u8 end_index; + f32 speed; + f32 timer; + bool active; +} pxl8_palette_cycle; + +typedef struct pxl8_scanline_effect { + void (*process)(pxl8_gfx_ctx* ctx, i32 line, f32 time); + bool active; +} pxl8_scanline_effect; + +typedef struct pxl8_mode7_params { + f32 horizon; + f32 scale_x, scale_y; + f32 rotation; + f32 offset_x, offset_y; + bool active; +} pxl8_mode7_params; + +typedef struct pxl8_effects { + pxl8_palette_cycle palette_cycles[8]; + pxl8_scanline_effect scanline_effects[4]; + + f32 time; +} pxl8_effects; + +pxl8_result pxl8_gfx_init( + pxl8_gfx_ctx* ctx, + pxl8_color_mode mode, + pxl8_resolution resolution, + const char* title, + i32 window_width, + i32 window_height +); + +void pxl8_gfx_shutdown(pxl8_gfx_ctx* ctx); + +pxl8_result pxl8_gfx_init_atlas(pxl8_gfx_ctx* ctx, u32 width, u32 height); +pxl8_result pxl8_gfx_load_sprite(pxl8_gfx_ctx* ctx, const char* path); +pxl8_result pxl8_gfx_load_palette(pxl8_gfx_ctx* ctx, const char* path); +pxl8_result pxl8_gfx_load_font_atlas(pxl8_gfx_ctx* ctx); + +void pxl8_gfx_get_resolution_dimensions(pxl8_resolution resolution, i32* width, i32* height); +void pxl8_gfx_upload_framebuffer(pxl8_gfx_ctx* ctx); +void pxl8_gfx_upload_atlas(pxl8_gfx_ctx* ctx); +void pxl8_gfx_present(pxl8_gfx_ctx* ctx); + +void pxl8_gfx_viewport(pxl8_gfx_ctx* ctx, i32 x, i32 y, i32 width, i32 height); +void pxl8_gfx_project(pxl8_gfx_ctx* ctx, f32 left, f32 right, f32 top, f32 bottom); + +void pxl8_clr(pxl8_gfx_ctx* ctx, u32 color); +void pxl8_pixel(pxl8_gfx_ctx* ctx, i32 x, i32 y, u32 color); +u32 pxl8_get_pixel(pxl8_gfx_ctx* ctx, i32 x, i32 y); +void pxl8_line(pxl8_gfx_ctx* ctx, i32 x0, i32 y0, i32 x1, i32 y1, u32 color); +void pxl8_rect(pxl8_gfx_ctx* ctx, i32 x, i32 y, i32 w, i32 h, u32 color); +void pxl8_rect_fill(pxl8_gfx_ctx* ctx, i32 x, i32 y, i32 w, i32 h, u32 color); +void pxl8_circle(pxl8_gfx_ctx* ctx, i32 cx, i32 cy, i32 radius, u32 color); +void pxl8_circle_fill(pxl8_gfx_ctx* ctx, i32 cx, i32 cy, i32 radius, u32 color); +void pxl8_text(pxl8_gfx_ctx* ctx, const char* text, i32 x, i32 y, u32 color); +void pxl8_sprite(pxl8_gfx_ctx* ctx, u32 sprite_id, i32 x, i32 y, i32 w, i32 h); + +void pxl8_gfx_color_ramp(pxl8_gfx_ctx* ctx, u8 start, u8 count, u32 from_color, u32 to_color); +void pxl8_gfx_cycle_palette(pxl8_gfx_ctx* ctx, u8 start, u8 count, i32 step); +void pxl8_gfx_fade_palette(pxl8_gfx_ctx* ctx, u8 start, u8 count, f32 amount, u32 target_color); +void pxl8_gfx_interpolate_palettes(pxl8_gfx_ctx* ctx, u32* palette1, u32* palette2, u8 start, u8 count, f32 t); +void pxl8_gfx_process_effects(pxl8_gfx_ctx* ctx, pxl8_effects* effects, f32 dt); +void pxl8_gfx_swap_palette(pxl8_gfx_ctx* ctx, u8 start, u8 count, u32* new_colors); diff --git a/src/pxl8_io.c b/src/pxl8_io.c new file mode 100644 index 0000000..05485c7 --- /dev/null +++ b/src/pxl8_io.c @@ -0,0 +1,106 @@ +#include "pxl8_io.h" + +pxl8_result pxl8_io_read_file(const char* path, char** content, size_t* size) { + if (!path || !content || !size) return PXL8_ERROR_NULL_POINTER; + + FILE* file = fopen(path, "rb"); + if (!file) { + return PXL8_ERROR_FILE_NOT_FOUND; + } + + fseek(file, 0, SEEK_END); + long file_size = ftell(file); + fseek(file, 0, SEEK_SET); + + if (file_size < 0) { + fclose(file); + return PXL8_ERROR_SYSTEM_FAILURE; + } + + *content = SDL_malloc(file_size + 1); + if (!*content) { + fclose(file); + return PXL8_ERROR_OUT_OF_MEMORY; + } + + size_t bytes_read = fread(*content, 1, file_size, file); + (*content)[bytes_read] = '\0'; + *size = bytes_read; + + fclose(file); + return PXL8_OK; +} + +pxl8_result pxl8_io_write_file(const char* path, const char* content, size_t size) { + if (!path || !content) return PXL8_ERROR_NULL_POINTER; + + FILE* file = fopen(path, "wb"); + if (!file) { + return PXL8_ERROR_SYSTEM_FAILURE; + } + + size_t bytes_written = fwrite(content, 1, size, file); + fclose(file); + + return (bytes_written == size) ? PXL8_OK : PXL8_ERROR_SYSTEM_FAILURE; +} + +pxl8_result pxl8_io_read_binary_file(const char* path, u8** data, size_t* size) { + return pxl8_io_read_file(path, (char**)data, size); +} + +pxl8_result pxl8_io_write_binary_file(const char* path, const u8* data, size_t size) { + return pxl8_io_write_file(path, (const char*)data, size); +} + +bool pxl8_io_file_exists(const char* path) { + if (!path) return false; + + struct stat st; + return stat(path, &st) == 0; +} + +f64 pxl8_io_get_file_modified_time(const char* path) { + if (!path) return 0.0; + + struct stat st; + if (stat(path, &st) == 0) { + return st.st_mtime; + } + return 0.0; +} + +pxl8_result pxl8_io_create_directory(const char* path) { + if (!path) return PXL8_ERROR_NULL_POINTER; + +#ifdef _WIN32 + if (mkdir(path) != 0) { +#else + if (mkdir(path, 0755) != 0) { +#endif + return PXL8_ERROR_SYSTEM_FAILURE; + } + return PXL8_OK; +} + +void pxl8_io_free_file_content(char* content) { + if (content) { + SDL_free(content); + } +} + +void pxl8_io_free_binary_data(u8* data) { + if (data) { + SDL_free(data); + } +} + +bool pxl8_key_down(const pxl8_input_state* input, i32 key) { + if (!input || key < 0 || key >= 256) return false; + return input->keys[key]; +} + +bool pxl8_key_pressed(const pxl8_input_state* input, i32 key) { + if (!input || key < 0 || key >= 256) return false; + return input->keys_pressed[key]; +} diff --git a/src/pxl8_io.h b/src/pxl8_io.h new file mode 100644 index 0000000..fe9557f --- /dev/null +++ b/src/pxl8_io.h @@ -0,0 +1,31 @@ +#pragma once + +#include +#include + +#include + +#include "pxl8_types.h" + +#ifdef __cplusplus +extern "C" { +#endif + +pxl8_result pxl8_io_read_file(const char* path, char** content, size_t* size); +pxl8_result pxl8_io_write_file(const char* path, const char* content, size_t size); +pxl8_result pxl8_io_read_binary_file(const char* path, u8** data, size_t* size); +pxl8_result pxl8_io_write_binary_file(const char* path, const u8* data, size_t size); + +bool pxl8_io_file_exists(const char* path); +f64 pxl8_io_get_file_modified_time(const char* path); +pxl8_result pxl8_io_create_directory(const char* path); + +void pxl8_io_free_file_content(char* content); +void pxl8_io_free_binary_data(u8* data); + +bool pxl8_key_down(const pxl8_input_state* input, i32 key); +bool pxl8_key_pressed(const pxl8_input_state* input, i32 key); + +#ifdef __cplusplus +} +#endif diff --git a/src/pxl8_lua.c b/src/pxl8_lua.c new file mode 100644 index 0000000..1ab9814 --- /dev/null +++ b/src/pxl8_lua.c @@ -0,0 +1,279 @@ +#include "pxl8_lua.h" +#include "pxl8_vfx.h" +#include "pxl8_macros.h" + +static const char* pxl8_ffi_cdefs = +"typedef uint8_t u8;\n" +"typedef uint16_t u16;\n" +"typedef uint32_t u32;\n" +"typedef uint64_t u64;\n" +"typedef int8_t i8;\n" +"typedef int16_t i16;\n" +"typedef int32_t i32;\n" +"typedef int64_t i64;\n" +"typedef float f32;\n" +"typedef double f64;\n" +"typedef struct pxl8_gfx_ctx pxl8_gfx_ctx;\n" +"typedef struct { int x, y, w, h; } pxl8_rect;\n" +"typedef struct { int x, y; } pxl8_point;\n" +"typedef struct {\n" +" int width, height;\n" +" int color_mode;\n" +" unsigned char* pixels;\n" +"} pxl8_screen;\n" +"\n" +"void pxl8_clr(pxl8_gfx_ctx* ctx, u32 color);\n" +"void pxl8_pixel(pxl8_gfx_ctx* ctx, i32 x, i32 y, u32 color);\n" +"u32 pxl8_get_pixel(pxl8_gfx_ctx* ctx, i32 x, i32 y);\n" +"void pxl8_line(pxl8_gfx_ctx* ctx, i32 x0, i32 y0, i32 x1, i32 y1, u32 color);\n" +"void pxl8_rect(pxl8_gfx_ctx* ctx, i32 x, i32 y, i32 w, i32 h, u32 color);\n" +"void pxl8_rect_fill(pxl8_gfx_ctx* ctx, i32 x, i32 y, i32 w, i32 h, u32 color);\n" +"void pxl8_circle(pxl8_gfx_ctx* ctx, i32 x, i32 y, i32 r, u32 color);\n" +"void pxl8_circle_fill(pxl8_gfx_ctx* ctx, i32 x, i32 y, i32 r, u32 color);\n" +"void pxl8_text(pxl8_gfx_ctx* ctx, const char* str, i32 x, i32 y, u32 color);\n" +"void pxl8_sprite(pxl8_gfx_ctx* ctx, i32 id, i32 x, i32 y, i32 w, i32 h, bool flip_x, bool flip_y);\n" +"pxl8_screen* pxl8_get_screen(pxl8_gfx_ctx* ctx);\n" +"i32 pxl8_gfx_load_palette(pxl8_gfx_ctx* ctx, const char* filepath);\n" +"i32 pxl8_gfx_load_sprite(pxl8_gfx_ctx* ctx, const char* filepath, u32* sprite_id);\n" +"void pxl8_gfx_color_ramp(pxl8_gfx_ctx* ctx, u8 start, u8 count, u32 from_color, u32 to_color);\n" +"void pxl8_gfx_fade_palette(pxl8_gfx_ctx* ctx, u8 start, u8 count, f32 amount, u32 target_color);\n" +"void pxl8_lua_info(const char* msg);\n" +"void pxl8_lua_warn(const char* msg);\n" +"void pxl8_lua_error(const char* msg);\n" +"void pxl8_lua_debug(const char* msg);\n" +"void pxl8_lua_trace(const char* msg);\n" +"typedef struct pxl8_input_state pxl8_input_state;\n" +"bool pxl8_key_down(const pxl8_input_state* input, i32 key);\n" +"bool pxl8_key_pressed(const pxl8_input_state* input, i32 key);\n" +"\n" +"typedef struct {\n" +" float x, y, z;\n" +" float vx, vy, vz;\n" +" float ax, ay, az;\n" +" float life;\n" +" float max_life;\n" +" unsigned int color;\n" +" unsigned int start_color;\n" +" unsigned int end_color;\n" +" float size;\n" +" float angle;\n" +" float spin;\n" +" unsigned char flags;\n" +"} pxl8_particle;\n" +"\n" +"typedef struct {\n" +" pxl8_particle* particles;\n" +" unsigned int count;\n" +" unsigned int max_count;\n" +" unsigned int alive_count;\n" +" float spawn_rate;\n" +" float spawn_timer;\n" +" float x, y;\n" +" float spread_x, spread_y;\n" +" float gravity_x, gravity_y;\n" +" float drag;\n" +" float turbulence;\n" +" void* spawn_fn;\n" +" void* update_fn;\n" +" void* render_fn;\n" +" void* userdata;\n" +"} pxl8_particle_system;\n" +"\n" +"typedef struct {\n" +" float base_y;\n" +" float amplitude;\n" +" int height;\n" +" float speed;\n" +" float phase;\n" +" unsigned int color;\n" +" unsigned int fade_color;\n" +"} pxl8_copper_bar;\n" +"\n" +"void pxl8_vfx_copper_bars(pxl8_gfx_ctx* ctx, pxl8_copper_bar* bars, u32 bar_count, f32 time);\n" +"void pxl8_vfx_plasma(pxl8_gfx_ctx* ctx, f32 time, f32 scale1, f32 scale2, u8 palette_offset);\n" +"void pxl8_vfx_rotozoom(pxl8_gfx_ctx* ctx, f32 angle, f32 zoom, i32 cx, i32 cy);\n" +"void pxl8_vfx_tunnel(pxl8_gfx_ctx* ctx, f32 time, f32 speed, f32 twist);\n" +"void pxl8_vfx_water_ripple(pxl8_gfx_ctx* ctx, f32* height_map, i32 drop_x, i32 drop_y, f32 damping);\n" +"\n" +"void pxl8_vfx_particles_clear(pxl8_particle_system* sys);\n" +"void pxl8_vfx_particles_destroy(pxl8_particle_system* sys);\n" +"void pxl8_vfx_particles_emit(pxl8_particle_system* sys, u32 count);\n" +"void pxl8_vfx_particles_init(pxl8_particle_system* sys, unsigned int max_count);\n" +"void pxl8_vfx_particles_render(pxl8_particle_system* sys, pxl8_gfx_ctx* ctx);\n" +"void pxl8_vfx_particles_update(pxl8_particle_system* sys, float dt);\n" +"\n" +"void pxl8_vfx_explosion(pxl8_particle_system* sys, int x, int y, unsigned int color, float force);\n" +"void pxl8_vfx_fire(pxl8_particle_system* sys, int x, int y, int width, unsigned char palette_start);\n" +"void pxl8_vfx_rain(pxl8_particle_system* sys, int width, float wind);\n" +"void pxl8_vfx_smoke(pxl8_particle_system* sys, int x, int y, unsigned char color);\n" +"void pxl8_vfx_snow(pxl8_particle_system* sys, int width, float wind);\n" +"void pxl8_vfx_sparks(pxl8_particle_system* sys, int x, int y, unsigned int color);\n" +"void pxl8_vfx_starfield(pxl8_particle_system* sys, float speed, float spread);\n"; + +typedef struct { + int width, height; + int color_mode; + unsigned char* pixels; +} pxl8_screen; + +pxl8_screen* pxl8_get_screen(pxl8_gfx_ctx* ctx) { + if (!ctx) return NULL; + + static pxl8_screen screen = {0}; + screen.width = ctx->framebuffer_width; + screen.height = ctx->framebuffer_height; + screen.color_mode = ctx->color_mode; + screen.pixels = ctx->framebuffer; + + return &screen; +} + +void pxl8_lua_info(const char* msg) { + pxl8_info("%s", msg); +} + +void pxl8_lua_warn(const char* msg) { + pxl8_warn("%s", msg); +} + +void pxl8_lua_error(const char* msg) { + pxl8_error("%s", msg); +} + +void pxl8_lua_debug(const char* msg) { + pxl8_debug("%s", msg); +} + +void pxl8_lua_trace(const char* msg) { + pxl8_trace("%s", msg); +} + +pxl8_result pxl8_lua_init(lua_State** lua_state) { + if (!lua_state) return PXL8_ERROR_NULL_POINTER; + + *lua_state = luaL_newstate(); + if (!*lua_state) { + return PXL8_ERROR_OUT_OF_MEMORY; + } + + luaL_openlibs(*lua_state); + + lua_getglobal(*lua_state, "require"); + lua_pushstring(*lua_state, "ffi"); + if (lua_pcall(*lua_state, 1, 1, 0) != 0) { + lua_close(*lua_state); + *lua_state = NULL; + return PXL8_ERROR_SYSTEM_FAILURE; + } + + lua_getfield(*lua_state, -1, "cdef"); + lua_pushstring(*lua_state, pxl8_ffi_cdefs); + if (lua_pcall(*lua_state, 1, 0, 0) != 0) { + lua_close(*lua_state); + *lua_state = NULL; + return PXL8_ERROR_SYSTEM_FAILURE; + } + + lua_pop(*lua_state, 1); + + lua_getglobal(*lua_state, "package"); + lua_getfield(*lua_state, -1, "path"); + const char* current_path = lua_tostring(*lua_state, -1); + lua_pop(*lua_state, 1); + + lua_pushfstring(*lua_state, "%s;src/lua/?.lua", current_path); + lua_setfield(*lua_state, -2, "path"); + lua_pop(*lua_state, 1); + + if (luaL_dofile(*lua_state, "lib/fennel/fennel.lua") == 0) { + lua_setglobal(*lua_state, "fennel"); + } + + return PXL8_OK; +} + +void pxl8_lua_shutdown(lua_State* lua_state) { + if (lua_state) { + lua_close(lua_state); + } +} + +pxl8_result pxl8_lua_setup_contexts(lua_State* lua_state, pxl8_gfx_ctx* gfx_ctx, pxl8_input_state* input) { + if (!lua_state || !gfx_ctx || !input) return PXL8_ERROR_NULL_POINTER; + + lua_pushlightuserdata(lua_state, gfx_ctx); + lua_setglobal(lua_state, "_pxl8_gfx_ctx"); + + lua_pushlightuserdata(lua_state, input); + lua_setglobal(lua_state, "_pxl8_input_ctx"); + + return PXL8_OK; +} + +pxl8_result pxl8_lua_run_file(lua_State* lua_state, const char* filename) { + if (!lua_state || !filename) return PXL8_ERROR_NULL_POINTER; + + if (luaL_dofile(lua_state, filename) != 0) { + printf("Lua error: %s\n", lua_tostring(lua_state, -1)); + lua_pop(lua_state, 1); + return PXL8_ERROR_SCRIPT_ERROR; + } + + return PXL8_OK; +} + +pxl8_result pxl8_lua_run_string(lua_State* lua_state, const char* code) { + if (!lua_state || !code) return PXL8_ERROR_NULL_POINTER; + + if (luaL_dostring(lua_state, code) != 0) { + printf("Lua error: %s\n", lua_tostring(lua_state, -1)); + lua_pop(lua_state, 1); + return PXL8_ERROR_SCRIPT_ERROR; + } + + return PXL8_OK; +} + +pxl8_result pxl8_lua_run_fennel_file(lua_State* lua_state, const char* filename) { + if (!lua_state || !filename) return PXL8_ERROR_NULL_POINTER; + + lua_getglobal(lua_state, "fennel"); + if (lua_isnil(lua_state, -1)) { + lua_pop(lua_state, 1); + return PXL8_ERROR_SCRIPT_ERROR; + } + + lua_getfield(lua_state, -1, "dofile"); + lua_pushstring(lua_state, filename); + + if (lua_pcall(lua_state, 1, 0, 0) != 0) { + printf("Fennel error: %s\n", lua_tostring(lua_state, -1)); + lua_pop(lua_state, 1); + return PXL8_ERROR_SCRIPT_ERROR; + } + + lua_pop(lua_state, 1); + return PXL8_OK; +} + +pxl8_result pxl8_lua_eval_fennel(lua_State* lua_state, const char* code) { + if (!lua_state || !code) return PXL8_ERROR_NULL_POINTER; + + lua_getglobal(lua_state, "fennel"); + if (lua_isnil(lua_state, -1)) { + lua_pop(lua_state, 1); + return PXL8_ERROR_SCRIPT_ERROR; + } + + lua_getfield(lua_state, -1, "eval"); + lua_pushstring(lua_state, code); + + if (lua_pcall(lua_state, 1, 1, 0) != 0) { + lua_remove(lua_state, -2); + return PXL8_ERROR_SCRIPT_ERROR; + } + + lua_remove(lua_state, -2); + return PXL8_OK; +} + diff --git a/src/pxl8_lua.h b/src/pxl8_lua.h new file mode 100644 index 0000000..01d3ce2 --- /dev/null +++ b/src/pxl8_lua.h @@ -0,0 +1,24 @@ +#pragma once + +#include +#include +#include + +#include "pxl8_gfx.h" +#include "pxl8_types.h" + +#ifdef __cplusplus +extern "C" { +#endif + +pxl8_result pxl8_lua_init(lua_State** lua_state); +void pxl8_lua_shutdown(lua_State* lua_state); +pxl8_result pxl8_lua_eval_fennel(lua_State* lua_state, const char* code); +pxl8_result pxl8_lua_run_fennel_file(lua_State* lua_state, const char* filename); +pxl8_result pxl8_lua_run_file(lua_State* lua_state, const char* filename); +pxl8_result pxl8_lua_run_string(lua_State* lua_state, const char* code); +pxl8_result pxl8_lua_setup_contexts(lua_State* lua_state, pxl8_gfx_ctx* gfx_ctx, pxl8_input_state* input); + +#ifdef __cplusplus +} +#endif diff --git a/src/pxl8_macros.h b/src/pxl8_macros.h new file mode 100644 index 0000000..cf70710 --- /dev/null +++ b/src/pxl8_macros.h @@ -0,0 +1,83 @@ +#pragma once + +#include +#include + +#define PXL8_LOG_ERROR "\033[38;2;251;73;52m" +#define PXL8_LOG_SUCCESS "\033[38;2;254;128;25m" +#define PXL8_LOG_WARN "\033[38;2;250;189;47m" +#define PXL8_LOG_INFO "\033[38;2;184;187;38m" +#define PXL8_LOG_DEBUG "\033[38;2;131;165;152m" +#define PXL8_LOG_TRACE "\033[38;2;211;134;155m" +#define PXL8_LOG_MUTED "\033[38;2;168;153;132m" +#define PXL8_LOG_RESET "\033[0m" + +static inline void pxl8_log_timestamp(char* buffer, size_t size) { + time_t now = time(NULL); + struct tm* tm_info = localtime(&now); + strftime(buffer, size, "%H:%M:%S", tm_info); +} + +#ifdef DEBUG + #ifndef PXL8_ENABLE_DEBUG_LOGS + #define PXL8_ENABLE_DEBUG_LOGS 1 + #endif + + #if PXL8_ENABLE_DEBUG_LOGS + #define pxl8_debug(...) \ + do { \ + char timestamp[16]; \ + pxl8_log_timestamp(timestamp, sizeof(timestamp)); \ + fprintf(stderr, PXL8_LOG_DEBUG "[%s DEBUG]" PXL8_LOG_RESET \ + " %s:%d: ", timestamp, __FILE__, __LINE__); \ + fprintf(stderr, __VA_ARGS__); \ + fprintf(stderr, "\n"); \ + } while(0) + + #define pxl8_trace(...) \ + do { \ + char timestamp[16]; \ + pxl8_log_timestamp(timestamp, sizeof(timestamp)); \ + fprintf(stderr, PXL8_LOG_TRACE "[%s TRACE]" PXL8_LOG_RESET \ + " %s:%d: ", timestamp, __FILE__, __LINE__); \ + fprintf(stderr, __VA_ARGS__); \ + fprintf(stderr, "\n"); \ + } while(0) + #else + #define pxl8_debug(...) + #define pxl8_trace(...) + #endif +#else + #define pxl8_debug(...) + #define pxl8_trace(...) +#endif + +#define pxl8_error(...) \ + do { \ + char timestamp[16]; \ + pxl8_log_timestamp(timestamp, sizeof(timestamp)); \ + fprintf(stderr, PXL8_LOG_ERROR "[%s ERROR]" PXL8_LOG_RESET \ + " %s:%d: ", timestamp, __FILE__, __LINE__); \ + fprintf(stderr, __VA_ARGS__); \ + fprintf(stderr, "\n"); \ + } while(0) + +#define pxl8_warn(...) \ + do { \ + char timestamp[16]; \ + pxl8_log_timestamp(timestamp, sizeof(timestamp)); \ + fprintf(stderr, PXL8_LOG_WARN "[%s WARN]" PXL8_LOG_RESET \ + " %s:%d: ", timestamp, __FILE__, __LINE__); \ + fprintf(stderr, __VA_ARGS__); \ + fprintf(stderr, "\n"); \ + } while(0) + +#define pxl8_info(...) \ + do { \ + char timestamp[16]; \ + pxl8_log_timestamp(timestamp, sizeof(timestamp)); \ + fprintf(stdout, PXL8_LOG_INFO "[%s INFO]" PXL8_LOG_RESET \ + " ", timestamp); \ + fprintf(stdout, __VA_ARGS__); \ + fprintf(stdout, "\n"); \ + } while(0) diff --git a/src/pxl8_simd.h b/src/pxl8_simd.h new file mode 100644 index 0000000..f794e89 --- /dev/null +++ b/src/pxl8_simd.h @@ -0,0 +1,189 @@ +#pragma once + +#include "pxl8_types.h" + +#if defined(__AVX2__) + #include + #define PXL8_SIMD_AVX2 1 + #define PXL8_SIMD_WIDTH_U8 32 + #define PXL8_SIMD_WIDTH_U32 8 +#elif defined(__SSE2__) + #include + #define PXL8_SIMD_SSE2 1 + #define PXL8_SIMD_WIDTH_U8 16 + #define PXL8_SIMD_WIDTH_U32 4 +#elif defined(__ARM_NEON) + #include + #define PXL8_SIMD_NEON 1 + #define PXL8_SIMD_WIDTH_U8 16 + #define PXL8_SIMD_WIDTH_U32 4 +#else + #define PXL8_SIMD_SCALAR 1 + #define PXL8_SIMD_WIDTH_U8 1 + #define PXL8_SIMD_WIDTH_U32 1 +#endif + +typedef union { +#if defined(PXL8_SIMD_AVX2) + __m256i avx2; +#elif defined(PXL8_SIMD_SSE2) + __m128i sse2; +#elif defined(PXL8_SIMD_NEON) + uint8x16_t neon_u8; + uint32x4_t neon_u32; +#endif + u8 u8_array[32]; + u32 u32_array[8]; +} pxl8_simd_vec; + +static inline pxl8_simd_vec pxl8_simd_load_u8(const u8* src) { + pxl8_simd_vec result; +#if defined(PXL8_SIMD_AVX2) + result.avx2 = _mm256_loadu_si256((__m256i*)src); +#elif defined(PXL8_SIMD_SSE2) + result.sse2 = _mm_loadu_si128((__m128i*)src); +#elif defined(PXL8_SIMD_NEON) + result.neon_u8 = vld1q_u8(src); +#else + result.u8_array[0] = src[0]; +#endif + return result; +} + +static inline pxl8_simd_vec pxl8_simd_load_u32(const u32* src) { + pxl8_simd_vec result; +#if defined(PXL8_SIMD_AVX2) + result.avx2 = _mm256_loadu_si256((__m256i*)src); +#elif defined(PXL8_SIMD_SSE2) + result.sse2 = _mm_loadu_si128((__m128i*)src); +#elif defined(PXL8_SIMD_NEON) + result.neon_u32 = vld1q_u32(src); +#else + result.u32_array[0] = src[0]; +#endif + return result; +} + +static inline void pxl8_simd_store_u8(u8* dest, pxl8_simd_vec vec) { +#if defined(PXL8_SIMD_AVX2) + _mm256_storeu_si256((__m256i*)dest, vec.avx2); +#elif defined(PXL8_SIMD_SSE2) + _mm_storeu_si128((__m128i*)dest, vec.sse2); +#elif defined(PXL8_SIMD_NEON) + vst1q_u8(dest, vec.neon_u8); +#else + dest[0] = vec.u8_array[0]; +#endif +} + +static inline void pxl8_simd_store_u32(u32* dest, pxl8_simd_vec vec) { +#if defined(PXL8_SIMD_AVX2) + _mm256_storeu_si256((__m256i*)dest, vec.avx2); +#elif defined(PXL8_SIMD_SSE2) + _mm_storeu_si128((__m128i*)dest, vec.sse2); +#elif defined(PXL8_SIMD_NEON) + vst1q_u32(dest, vec.neon_u32); +#else + dest[0] = vec.u32_array[0]; +#endif +} + +static inline pxl8_simd_vec pxl8_simd_zero_u8(void) { + pxl8_simd_vec result; +#if defined(PXL8_SIMD_AVX2) + result.avx2 = _mm256_setzero_si256(); +#elif defined(PXL8_SIMD_SSE2) + result.sse2 = _mm_setzero_si128(); +#elif defined(PXL8_SIMD_NEON) + result.neon_u8 = vdupq_n_u8(0); +#else + result.u8_array[0] = 0; +#endif + return result; +} + +static inline pxl8_simd_vec pxl8_simd_alpha_mask_u32(void) { + pxl8_simd_vec result; +#if defined(PXL8_SIMD_AVX2) + result.avx2 = _mm256_set1_epi32(0xFF000000); +#elif defined(PXL8_SIMD_SSE2) + result.sse2 = _mm_set1_epi32(0xFF000000); +#elif defined(PXL8_SIMD_NEON) + result.neon_u32 = vdupq_n_u32(0xFF000000); +#else + result.u32_array[0] = 0xFF000000; +#endif + return result; +} + +static inline pxl8_simd_vec pxl8_simd_and(pxl8_simd_vec a, pxl8_simd_vec b) { + pxl8_simd_vec result; +#if defined(PXL8_SIMD_AVX2) + result.avx2 = _mm256_and_si256(a.avx2, b.avx2); +#elif defined(PXL8_SIMD_SSE2) + result.sse2 = _mm_and_si128(a.sse2, b.sse2); +#elif defined(PXL8_SIMD_NEON) + result.neon_u8 = vandq_u8(a.neon_u8, b.neon_u8); +#else + result.u8_array[0] = a.u8_array[0] & b.u8_array[0]; +#endif + return result; +} + +static inline pxl8_simd_vec pxl8_simd_cmpeq_u8(pxl8_simd_vec a, pxl8_simd_vec b) { + pxl8_simd_vec result; +#if defined(PXL8_SIMD_AVX2) + result.avx2 = _mm256_cmpeq_epi8(a.avx2, b.avx2); +#elif defined(PXL8_SIMD_SSE2) + result.sse2 = _mm_cmpeq_epi8(a.sse2, b.sse2); +#elif defined(PXL8_SIMD_NEON) + result.neon_u8 = vceqq_u8(a.neon_u8, b.neon_u8); +#else + result.u8_array[0] = (a.u8_array[0] == b.u8_array[0]) ? 0xFF : 0x00; +#endif + return result; +} + +static inline pxl8_simd_vec pxl8_simd_cmpeq_u32(pxl8_simd_vec a, pxl8_simd_vec b) { + pxl8_simd_vec result; +#if defined(PXL8_SIMD_AVX2) + result.avx2 = _mm256_cmpeq_epi32(a.avx2, b.avx2); +#elif defined(PXL8_SIMD_SSE2) + result.sse2 = _mm_cmpeq_epi32(a.sse2, b.sse2); +#elif defined(PXL8_SIMD_NEON) + result.neon_u32 = vceqq_u32(a.neon_u32, b.neon_u32); +#else + result.u32_array[0] = (a.u32_array[0] == b.u32_array[0]) ? 0xFFFFFFFF : 0x00000000; +#endif + return result; +} + +static inline pxl8_simd_vec pxl8_simd_blendv_u8(pxl8_simd_vec src, pxl8_simd_vec dest, pxl8_simd_vec mask) { + pxl8_simd_vec result; +#if defined(PXL8_SIMD_AVX2) + result.avx2 = _mm256_blendv_epi8(src.avx2, dest.avx2, mask.avx2); +#elif defined(PXL8_SIMD_SSE2) + pxl8_simd_vec not_mask; not_mask.sse2 = _mm_xor_si128(mask.sse2, _mm_set1_epi8(-1)); + result.sse2 = _mm_or_si128(_mm_and_si128(mask.sse2, dest.sse2), _mm_and_si128(not_mask.sse2, src.sse2)); +#elif defined(PXL8_SIMD_NEON) + result.neon_u8 = vbslq_u8(mask.neon_u8, dest.neon_u8, src.neon_u8); +#else + result.u8_array[0] = mask.u8_array[0] ? dest.u8_array[0] : src.u8_array[0]; +#endif + return result; +} + +static inline pxl8_simd_vec pxl8_simd_blendv_u32(pxl8_simd_vec src, pxl8_simd_vec dest, pxl8_simd_vec mask) { + pxl8_simd_vec result; +#if defined(PXL8_SIMD_AVX2) + result.avx2 = _mm256_blendv_epi8(src.avx2, dest.avx2, mask.avx2); +#elif defined(PXL8_SIMD_SSE2) + pxl8_simd_vec not_mask; not_mask.sse2 = _mm_xor_si128(mask.sse2, _mm_set1_epi32(-1)); + result.sse2 = _mm_or_si128(_mm_and_si128(mask.sse2, dest.sse2), _mm_and_si128(not_mask.sse2, src.sse2)); +#elif defined(PXL8_SIMD_NEON) + result.neon_u32 = vbslq_u32(mask.neon_u32, dest.neon_u32, src.neon_u32); +#else + result.u32_array[0] = mask.u32_array[0] ? dest.u32_array[0] : src.u32_array[0]; +#endif + return result; +} diff --git a/src/pxl8_types.h b/src/pxl8_types.h new file mode 100644 index 0000000..b1a8edb --- /dev/null +++ b/src/pxl8_types.h @@ -0,0 +1,72 @@ +#pragma once + +#include +#include +#include + +typedef uint8_t u8; +typedef uint16_t u16; +typedef uint32_t u32; +typedef uint64_t u64; +typedef int8_t i8; +typedef int16_t i16; +typedef int32_t i32; +typedef int64_t i64; +typedef float f32; +typedef double f64; + +#if defined(__SIZEOF_INT128__) +typedef __uint128_t u128; +typedef __int128_t i128; +#endif + +typedef enum pxl8_color_mode { + PXL8_COLOR_MODE_FAMI, // NES-style 4 colors per sprite, 64 color palette + PXL8_COLOR_MODE_MEGA, // Genesis-style 64 colors, 512 color palette + PXL8_COLOR_MODE_GBA, // GBA-style 64 colors, 32K color palette + PXL8_COLOR_MODE_SUPERFAMI, // SNES-style 256 colors, 32K color palette + PXL8_COLOR_MODE_HICOLOR, // 16-bit high color mode +} pxl8_color_mode; + +typedef struct pxl8_point { + i32 x, y; +} pxl8_point; + +typedef struct pxl8_rectangle { + i32 x, y; + i32 width, height; +} pxl8_rectangle; + +typedef enum pxl8_result { + PXL8_OK = 0, + PXL8_ERROR_NULL_POINTER, + PXL8_ERROR_INVALID_ARGUMENT, + PXL8_ERROR_OUT_OF_MEMORY, + PXL8_ERROR_FILE_NOT_FOUND, + PXL8_ERROR_INVALID_FORMAT, + PXL8_ERROR_SYSTEM_FAILURE, + PXL8_ERROR_INVALID_COORDINATE, + PXL8_ERROR_INVALID_SIZE, + PXL8_ERROR_INITIALIZATION_FAILED, + PXL8_ERROR_NOT_INITIALIZED, + PXL8_ERROR_SCRIPT_ERROR, + PXL8_ERROR_ASE_INVALID_MAGIC, + PXL8_ERROR_ASE_INVALID_FRAME_MAGIC, + PXL8_ERROR_ASE_TRUNCATED_FILE, + PXL8_ERROR_ASE_MALFORMED_CHUNK, +} pxl8_result; + +typedef enum pxl8_resolution { + PXL8_RESOLUTION_240x160, + PXL8_RESOLUTION_320x180, + PXL8_RESOLUTION_320x240, + PXL8_RESOLUTION_640x360, + PXL8_RESOLUTION_640x480, + PXL8_RESOLUTION_800x600, + PXL8_RESOLUTION_960x540, +} pxl8_resolution; + +typedef struct pxl8_input_state { + bool keys[256]; + bool keys_pressed[256]; +} pxl8_input_state; diff --git a/src/pxl8_vfx.c b/src/pxl8_vfx.c new file mode 100644 index 0000000..2451469 --- /dev/null +++ b/src/pxl8_vfx.c @@ -0,0 +1,461 @@ +#include "pxl8_vfx.h" +#include "pxl8_macros.h" +#include +#include +#include + +void pxl8_vfx_copper_bars(pxl8_gfx_ctx* ctx, pxl8_copper_bar* bars, u32 bar_count, f32 time) { + if (!ctx || !bars) return; + + for (u32 i = 0; i < bar_count; i++) { + pxl8_copper_bar* bar = &bars[i]; + f32 y = bar->base_y + bar->amplitude * sinf(time * bar->speed + bar->phase); + i32 y_int = (i32)y; + + for (i32 dy = 0; dy <= bar->height; dy++) { + f32 position = (f32)dy / (f32)bar->height; + f32 gradient = 1.0f - 2.0f * fabsf(position - 0.5f); + + u8 color_idx; + if (gradient > 0.8f) { + color_idx = bar->fade_color; + } else { + u8 range = bar->fade_color - bar->color; + color_idx = bar->color + (u8)(gradient * range); + } + + pxl8_rect_fill(ctx, 0, y_int + dy, ctx->framebuffer_width, 1, color_idx); + } + } +} + +void pxl8_vfx_plasma(pxl8_gfx_ctx* ctx, f32 time, f32 scale1, f32 scale2, u8 palette_offset) { + if (!ctx || !ctx->framebuffer) return; + + for (i32 y = 0; y < ctx->framebuffer_height; y++) { + for (i32 x = 0; x < ctx->framebuffer_width; x++) { + f32 v1 = sinf(x * scale1 + time); + f32 v2 = sinf(y * scale1 + time * 0.7f); + f32 v3 = sinf((x + y) * scale2 + time * 1.3f); + f32 v4 = sinf(sqrtf(x * x + y * y) * 0.05f + time * 0.5f); + + f32 v = v1 + v2 + v3 + v4; + f32 normalized = (1.0f + v / 4.0f) * 0.5f; + if (normalized < 0.0f) normalized = 0.0f; + if (normalized > 1.0f) normalized = 1.0f; + u8 color = palette_offset + (u8)(15.0f * normalized); + + pxl8_pixel(ctx, x, y, color); + } + } +} + +void pxl8_vfx_rotozoom(pxl8_gfx_ctx* ctx, f32 angle, f32 zoom, i32 cx, i32 cy) { + if (!ctx || !ctx->framebuffer) return; + + f32 cos_a = cosf(angle); + f32 sin_a = sinf(angle); + + u8* temp_buffer = (u8*)SDL_malloc(ctx->framebuffer_width * ctx->framebuffer_height); + if (!temp_buffer) return; + + SDL_memcpy(temp_buffer, ctx->framebuffer, ctx->framebuffer_width * ctx->framebuffer_height); + + for (i32 y = 0; y < ctx->framebuffer_height; y++) { + for (i32 x = 0; x < ctx->framebuffer_width; x++) { + f32 dx = x - cx; + f32 dy = y - cy; + + f32 src_x = cx + (dx * cos_a - dy * sin_a) / zoom; + f32 src_y = cy + (dx * sin_a + dy * cos_a) / zoom; + + i32 sx = (i32)src_x; + i32 sy = (i32)src_y; + + if (sx >= 0 && sx < ctx->framebuffer_width && sy >= 0 && sy < ctx->framebuffer_height) { + ctx->framebuffer[y * ctx->framebuffer_width + x] = temp_buffer[sy * ctx->framebuffer_width + sx]; + } + } + } + + SDL_free(temp_buffer); +} + +void pxl8_vfx_tunnel(pxl8_gfx_ctx* ctx, f32 time, f32 speed, f32 twist) { + if (!ctx || !ctx->framebuffer) return; + + f32 cx = ctx->framebuffer_width / 2.0f; + f32 cy = ctx->framebuffer_height / 2.0f; + + for (i32 y = 0; y < ctx->framebuffer_height; y++) { + for (i32 x = 0; x < ctx->framebuffer_width; x++) { + f32 dx = x - cx; + f32 dy = y - cy; + f32 dist = sqrtf(dx * dx + dy * dy); + + if (dist > 1.0f) { + f32 angle = atan2f(dy, dx); + f32 u = angle * 0.159f + twist * sinf(time * 0.5f); + f32 v = 256.0f / dist + time * speed; + + u8 tx = (u8)((i32)(u * 256.0f) & 0xFF); + u8 ty = (u8)((i32)(v * 256.0f) & 0xFF); + u8 color = tx ^ ty; + + pxl8_pixel(ctx, x, y, color); + } + } + } +} + +void pxl8_vfx_water_ripple(pxl8_gfx_ctx* ctx, f32* height_map, i32 drop_x, i32 drop_y, f32 damping) { + if (!ctx || !height_map) return; + + i32 w = ctx->framebuffer_width; + i32 h = ctx->framebuffer_height; + + static f32* prev_height = NULL; + if (!prev_height) { + prev_height = (f32*)SDL_calloc(w * h, sizeof(f32)); + if (!prev_height) return; + } + + if (drop_x >= 0 && drop_x < w && drop_y >= 0 && drop_y < h) { + height_map[drop_y * w + drop_x] = 255.0f; + } + + for (i32 y = 1; y < h - 1; y++) { + for (i32 x = 1; x < w - 1; x++) { + i32 idx = y * w + x; + f32 sum = height_map[idx - w] + height_map[idx + w] + + height_map[idx - 1] + height_map[idx + 1]; + f32 avg = sum / 2.0f; + prev_height[idx] = (avg - prev_height[idx]) * damping; + } + } + + f32* temp = height_map; + height_map = prev_height; + prev_height = temp; +} + +void pxl8_vfx_particles_clear(pxl8_particle_system* sys) { + if (!sys || !sys->particles) return; + + for (u32 i = 0; i < sys->max_count; i++) { + sys->particles[i].life = 0; + sys->particles[i].flags = 0; + } + sys->alive_count = 0; +} + +void pxl8_vfx_particles_destroy(pxl8_particle_system* sys) { + if (!sys) return; + + if (sys->particles) { + SDL_free(sys->particles); + sys->particles = NULL; + } + sys->count = 0; + sys->max_count = 0; + sys->alive_count = 0; +} + +void pxl8_vfx_particles_emit(pxl8_particle_system* sys, u32 count) { + if (!sys || !sys->particles) return; + + for (u32 i = 0; i < count && sys->alive_count < sys->max_count; i++) { + for (u32 j = 0; j < sys->max_count; j++) { + if (sys->particles[j].life <= 0) { + pxl8_particle* p = &sys->particles[j]; + p->life = 1.0f; + p->max_life = 1.0f; + p->x = sys->x + (((f32)rand() / RAND_MAX) - 0.5f) * sys->spread_x; + p->y = sys->y + (((f32)rand() / RAND_MAX) - 0.5f) * sys->spread_y; + p->z = 0; + p->vx = p->vy = p->vz = 0; + p->ax = sys->gravity_x; + p->ay = sys->gravity_y; + p->az = 0; + p->color = p->start_color = p->end_color = 15; + p->size = 1.0f; + p->angle = 0; + p->spin = 0; + p->flags = 1; + + if (sys->spawn_fn) { + sys->spawn_fn(p, sys->userdata); + } + + sys->alive_count++; + break; + } + } + } +} + +void pxl8_vfx_particles_init(pxl8_particle_system* sys, u32 max_count) { + if (!sys) return; + + SDL_memset(sys, 0, sizeof(pxl8_particle_system)); + + sys->particles = (pxl8_particle*)SDL_calloc(max_count, sizeof(pxl8_particle)); + if (!sys->particles) return; + + sys->max_count = max_count; + sys->count = 0; + sys->alive_count = 0; + sys->spawn_rate = 10.0f; + sys->spawn_timer = 0; + sys->drag = 0.98f; + sys->gravity_y = 100.0f; +} + +void pxl8_vfx_particles_render(pxl8_particle_system* sys, pxl8_gfx_ctx* ctx) { + if (!sys || !sys->particles || !ctx) return; + + for (u32 i = 0; i < sys->max_count; i++) { + pxl8_particle* p = &sys->particles[i]; + if (p->life > 0 && p->flags) { + if (sys->render_fn) { + sys->render_fn(ctx, p, sys->userdata); + } else { + i32 x = (i32)p->x; + i32 y = (i32)p->y; + if (x >= 0 && x < ctx->framebuffer_width && y >= 0 && y < ctx->framebuffer_height) { + pxl8_pixel(ctx, x, y, p->color); + } + } + } + } +} + +void pxl8_vfx_particles_update(pxl8_particle_system* sys, f32 dt) { + if (!sys || !sys->particles) return; + + sys->spawn_timer += dt; + f32 spawn_interval = 1.0f / sys->spawn_rate; + while (sys->spawn_timer >= spawn_interval) { + pxl8_vfx_particles_emit(sys, 1); + sys->spawn_timer -= spawn_interval; + } + + for (u32 i = 0; i < sys->max_count; i++) { + pxl8_particle* p = &sys->particles[i]; + if (p->life > 0) { + if (sys->update_fn) { + sys->update_fn(p, dt, sys->userdata); + } else { + p->vx += p->ax * dt; + p->vy += p->ay * dt; + p->vz += p->az * dt; + + p->vx *= sys->drag; + p->vy *= sys->drag; + p->vz *= sys->drag; + + p->x += p->vx * dt; + p->y += p->vy * dt; + p->z += p->vz * dt; + + p->angle += p->spin * dt; + } + + p->life -= dt / p->max_life; + if (p->life <= 0) { + p->flags = 0; + sys->alive_count--; + } + } + } +} + +void pxl8_vfx_explosion(pxl8_particle_system* sys, i32 x, i32 y, u32 color, f32 force) { + if (!sys) return; + + sys->x = x; + sys->y = y; + sys->spread_x = sys->spread_y = 2.0f; + sys->gravity_x = 0; + sys->gravity_y = 200.0f; + sys->drag = 0.95f; + + for (u32 i = 0; i < 50 && i < sys->max_count; i++) { + pxl8_particle* p = &sys->particles[i]; + f32 angle = ((f32)rand() / RAND_MAX) * 6.28f; + f32 speed = force * (0.5f + ((f32)rand() / RAND_MAX) * 0.5f); + + p->x = x; + p->y = y; + p->vx = cosf(angle) * speed; + p->vy = sinf(angle) * speed; + p->life = 1.0f; + p->max_life = 1.0f + ((f32)rand() / RAND_MAX); + p->color = color; + p->flags = 1; + } +} + +static void fire_spawn(pxl8_particle* p, void* userdata) { + uintptr_t palette_start = (uintptr_t)userdata; + p->start_color = palette_start + 6 + (rand() % 3); + p->end_color = palette_start; + p->color = p->start_color; + p->max_life = 1.5f + ((f32)rand() / RAND_MAX) * 1.5f; + p->vy = -80.0f - ((f32)rand() / RAND_MAX) * 120.0f; + p->vx = (((f32)rand() / RAND_MAX) - 0.5f) * 40.0f; +} + +static void fire_update(pxl8_particle* p, f32 dt, void* userdata) { + (void)userdata; + p->vx += p->ax * dt; + p->vy += p->ay * dt; + p->vz += p->az * dt; + + p->x += p->vx * dt; + p->y += p->vy * dt; + p->z += p->vz * dt; + + f32 life_ratio = 1.0f - (p->life / p->max_life); + if (p->start_color >= p->end_color) { + u8 color_range = p->start_color - p->end_color; + p->color = p->start_color - (u8)(life_ratio * color_range); + } else { + u8 color_range = p->end_color - p->start_color; + p->color = p->start_color + (u8)(life_ratio * color_range); + } +} + +void pxl8_vfx_fire(pxl8_particle_system* sys, i32 x, i32 y, i32 width, u8 palette_start) { + if (!sys) return; + + sys->x = x; + sys->y = y; + sys->spread_x = width; + sys->spread_y = 4.0f; + sys->gravity_x = 0; + sys->gravity_y = -100.0f; + sys->drag = 0.97f; + sys->spawn_rate = 120.0f; + sys->spawn_fn = fire_spawn; + sys->update_fn = fire_update; + sys->userdata = (void*)(uintptr_t)palette_start; +} + +static void rain_spawn(pxl8_particle* p, void* userdata) { + (void)userdata; + p->color = 27 + (rand() % 3); + p->max_life = 2.0f; + p->vy = 200.0f + ((f32)rand() / RAND_MAX) * 100.0f; +} + +void pxl8_vfx_rain(pxl8_particle_system* sys, i32 width, f32 wind) { + if (!sys) return; + + sys->x = width / 2.0f; + sys->y = -10; + sys->spread_x = width; + sys->spread_y = 0; + sys->gravity_x = wind; + sys->gravity_y = 300.0f; + sys->drag = 1.0f; + sys->spawn_rate = 100.0f; + sys->spawn_fn = rain_spawn; +} + +static void smoke_spawn(pxl8_particle* p, void* userdata) { + uintptr_t base_color = (uintptr_t)userdata; + p->start_color = base_color; + p->end_color = base_color + 4; + p->color = p->start_color; + p->max_life = 3.0f + ((f32)rand() / RAND_MAX) * 2.0f; + p->vy = -20.0f - ((f32)rand() / RAND_MAX) * 30.0f; + p->vx = (((f32)rand() / RAND_MAX) - 0.5f) * 20.0f; + p->size = 1.0f + ((f32)rand() / RAND_MAX) * 2.0f; +} + +void pxl8_vfx_smoke(pxl8_particle_system* sys, i32 x, i32 y, u8 color) { + if (!sys) return; + + sys->x = x; + sys->y = y; + sys->spread_x = 5.0f; + sys->spread_y = 5.0f; + sys->gravity_x = 0; + sys->gravity_y = -50.0f; + sys->drag = 0.96f; + sys->spawn_rate = 20.0f; + sys->spawn_fn = smoke_spawn; + sys->userdata = (void*)(uintptr_t)color; +} + +static void snow_spawn(pxl8_particle* p, void* userdata) { + (void)userdata; + p->color = 10 + (rand() % 2); + p->max_life = 4.0f; + p->vx = (((f32)rand() / RAND_MAX) - 0.5f) * 20.0f; + p->vy = 30.0f + ((f32)rand() / RAND_MAX) * 20.0f; +} + +void pxl8_vfx_snow(pxl8_particle_system* sys, i32 width, f32 wind) { + if (!sys) return; + + sys->x = width / 2.0f; + sys->y = -10; + sys->spread_x = width; + sys->spread_y = 0; + sys->gravity_x = wind; + sys->gravity_y = 30.0f; + sys->drag = 0.99f; + sys->spawn_rate = 30.0f; + sys->spawn_fn = snow_spawn; +} + +static void sparks_spawn(pxl8_particle* p, void* userdata) { + uintptr_t base_color = (uintptr_t)userdata; + p->start_color = base_color; + p->end_color = base_color > 2 ? base_color - 2 : 0; + p->color = p->start_color; + p->max_life = 0.5f + ((f32)rand() / RAND_MAX) * 1.0f; + + f32 angle = ((f32)rand() / RAND_MAX) * 6.28f; + f32 speed = 100.0f + ((f32)rand() / RAND_MAX) * 200.0f; + p->vx = cosf(angle) * speed; + p->vy = sinf(angle) * speed - 50.0f; +} + +void pxl8_vfx_sparks(pxl8_particle_system* sys, i32 x, i32 y, u32 color) { + if (!sys) return; + + sys->x = x; + sys->y = y; + sys->spread_x = 2.0f; + sys->spread_y = 2.0f; + sys->gravity_x = 0; + sys->gravity_y = 100.0f; + sys->drag = 0.97f; + sys->spawn_rate = 40.0f; + sys->spawn_fn = sparks_spawn; + sys->userdata = (void*)(uintptr_t)color; +} + +void pxl8_vfx_starfield(pxl8_particle_system* sys, f32 speed, f32 spread) { + if (!sys) return; + + sys->spread_x = sys->spread_y = spread; + sys->gravity_x = sys->gravity_y = 0; + sys->drag = 1.0f; + sys->spawn_rate = 0; + + for (u32 i = 0; i < sys->max_count; i++) { + pxl8_particle* p = &sys->particles[i]; + p->x = ((f32)rand() / RAND_MAX) * spread * 2.0f - spread; + p->y = ((f32)rand() / RAND_MAX) * spread * 2.0f - spread; + p->z = ((f32)rand() / RAND_MAX) * spread; + p->vz = -speed; + p->life = 1000.0f; + p->max_life = 1000.0f; + p->color = 8 + (rand() % 8); + p->flags = 1; + } +} \ No newline at end of file diff --git a/src/pxl8_vfx.h b/src/pxl8_vfx.h new file mode 100644 index 0000000..8beb96c --- /dev/null +++ b/src/pxl8_vfx.h @@ -0,0 +1,68 @@ +#pragma once + +#include "pxl8_types.h" +#include "pxl8_gfx.h" + +typedef struct pxl8_copper_bar { + f32 base_y; + f32 amplitude; + i32 height; + f32 speed; + f32 phase; + u32 color; + u32 fade_color; +} pxl8_copper_bar; + +typedef struct pxl8_particle { + f32 x, y, z; + f32 vx, vy, vz; + f32 ax, ay, az; + f32 life; + f32 max_life; + u32 color; + u32 start_color; + u32 end_color; + f32 size; + f32 angle; + f32 spin; + u8 flags; +} pxl8_particle; + +typedef struct pxl8_particle_system { + pxl8_particle* particles; + u32 count; + u32 max_count; + u32 alive_count; + f32 spawn_rate; + f32 spawn_timer; + f32 x, y; + f32 spread_x, spread_y; + f32 gravity_x, gravity_y; + f32 drag; + f32 turbulence; + void (*spawn_fn)(pxl8_particle* p, void* userdata); + void (*update_fn)(pxl8_particle* p, f32 dt, void* userdata); + void (*render_fn)(pxl8_gfx_ctx* ctx, pxl8_particle* p, void* userdata); + void* userdata; +} pxl8_particle_system; + +void pxl8_vfx_copper_bars(pxl8_gfx_ctx* ctx, pxl8_copper_bar* bars, u32 bar_count, f32 time); +void pxl8_vfx_plasma(pxl8_gfx_ctx* ctx, f32 time, f32 scale1, f32 scale2, u8 palette_offset); +void pxl8_vfx_rotozoom(pxl8_gfx_ctx* ctx, f32 angle, f32 zoom, i32 cx, i32 cy); +void pxl8_vfx_tunnel(pxl8_gfx_ctx* ctx, f32 time, f32 speed, f32 twist); +void pxl8_vfx_water_ripple(pxl8_gfx_ctx* ctx, f32* height_map, i32 drop_x, i32 drop_y, f32 damping); + +void pxl8_vfx_particles_clear(pxl8_particle_system* sys); +void pxl8_vfx_particles_destroy(pxl8_particle_system* sys); +void pxl8_vfx_particles_emit(pxl8_particle_system* sys, u32 count); +void pxl8_vfx_particles_init(pxl8_particle_system* sys, u32 max_count); +void pxl8_vfx_particles_render(pxl8_particle_system* sys, pxl8_gfx_ctx* ctx); +void pxl8_vfx_particles_update(pxl8_particle_system* sys, f32 dt); + +void pxl8_vfx_explosion(pxl8_particle_system* sys, i32 x, i32 y, u32 color, f32 force); +void pxl8_vfx_fire(pxl8_particle_system* sys, i32 x, i32 y, i32 width, u8 palette_start); +void pxl8_vfx_rain(pxl8_particle_system* sys, i32 width, f32 wind); +void pxl8_vfx_smoke(pxl8_particle_system* sys, i32 x, i32 y, u8 color); +void pxl8_vfx_snow(pxl8_particle_system* sys, i32 width, f32 wind); +void pxl8_vfx_sparks(pxl8_particle_system* sys, i32 x, i32 y, u32 color); +void pxl8_vfx_starfield(pxl8_particle_system* sys, f32 speed, f32 spread); \ No newline at end of file