initial commit
This commit is contained in:
commit
c18896def0
30 changed files with 4183 additions and 0 deletions
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
**.DS_Store
|
||||
6
README.md
Normal file
6
README.md
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
<p align="center">
|
||||
<img src="./res/sprites/pxl8.png" alt="pixel 8 logo" width="512" />
|
||||
</p>
|
||||
|
||||
> [!WARNING]
|
||||
> Heavy development. So... *here be dragons :3*
|
||||
12
compile_flags.txt
Normal file
12
compile_flags.txt
Normal file
|
|
@ -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
|
||||
469
pxl8.sh
Executable file
469
pxl8.sh
Executable file
|
|
@ -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 <command> [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
|
||||
BIN
res/palettes/gruvbox.ase
Normal file
BIN
res/palettes/gruvbox.ase
Normal file
Binary file not shown.
BIN
res/palettes/sweaft-64.ase
Normal file
BIN
res/palettes/sweaft-64.ase
Normal file
Binary file not shown.
BIN
res/sprites/pxl8.ase
Executable file
BIN
res/sprites/pxl8.ase
Executable file
Binary file not shown.
BIN
res/sprites/pxl8.png
Executable file
BIN
res/sprites/pxl8.png
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 1.3 KiB |
BIN
res/sprites/trees.ase
Normal file
BIN
res/sprites/trees.ase
Normal file
Binary file not shown.
81
src/fnl/demo-effects.fnl
Normal file
81
src/fnl/demo-effects.fnl
Normal file
|
|
@ -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))))
|
||||
29
src/fnl/demo.fnl
Normal file
29
src/fnl/demo.fnl
Normal file
|
|
@ -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))))
|
||||
192
src/lua/pxl8.lua
Normal file
192
src/lua/pxl8.lua
Normal file
|
|
@ -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
|
||||
479
src/pxl8.c
Normal file
479
src/pxl8.c
Normal file
|
|
@ -0,0 +1,479 @@
|
|||
#define PXL8_AUTHORS "asrael <asrael@pxl8.org>"
|
||||
#define PXL8_COPYRIGHT "Copyright (c) 2024-2025 pxl8.org"
|
||||
#define PXL8_VERSION "0.1.0"
|
||||
|
||||
#include <math.h>
|
||||
#include <pthread.h>
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include "linenoise.h"
|
||||
#define SDL_MAIN_USE_CALLBACKS
|
||||
#include <SDL3/SDL.h>
|
||||
#include <SDL3/SDL_main.h>
|
||||
|
||||
#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, "<userdata>\n");
|
||||
} else if (lua_iscfunction(app->lua, -1)) {
|
||||
fprintf(stdout, "<function>\n");
|
||||
} else if (lua_istable(app->lua, -1)) {
|
||||
fprintf(stdout, "<table>\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();
|
||||
}
|
||||
400
src/pxl8_ase.c
Normal file
400
src/pxl8_ase.c
Normal file
|
|
@ -0,0 +1,400 @@
|
|||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#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));
|
||||
}
|
||||
98
src/pxl8_ase.h
Normal file
98
src/pxl8_ase.h
Normal file
|
|
@ -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
|
||||
58
src/pxl8_blit.c
Normal file
58
src/pxl8_blit.c
Normal file
|
|
@ -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];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
19
src/pxl8_blit.h
Normal file
19
src/pxl8_blit.h
Normal file
|
|
@ -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
|
||||
);
|
||||
60
src/pxl8_font.c
Normal file
60
src/pxl8_font.c
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
#include "pxl8_font.h"
|
||||
#include <SDL3/SDL.h>
|
||||
#include <string.h>
|
||||
|
||||
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;
|
||||
}
|
||||
130
src/pxl8_font.h
Normal file
130
src/pxl8_font.h
Normal file
|
|
@ -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
|
||||
713
src/pxl8_gfx.c
Normal file
713
src/pxl8_gfx.c
Normal file
|
|
@ -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 <stdlib.h>
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
123
src/pxl8_gfx.h
Normal file
123
src/pxl8_gfx.h
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
#pragma once
|
||||
|
||||
#include <string.h>
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#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);
|
||||
106
src/pxl8_io.c
Normal file
106
src/pxl8_io.c
Normal file
|
|
@ -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];
|
||||
}
|
||||
31
src/pxl8_io.h
Normal file
31
src/pxl8_io.h
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
#pragma once
|
||||
|
||||
#include <stdio.h>
|
||||
#include <sys/stat.h>
|
||||
|
||||
#include <SDL3/SDL_stdinc.h>
|
||||
|
||||
#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
|
||||
279
src/pxl8_lua.c
Normal file
279
src/pxl8_lua.c
Normal file
|
|
@ -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;
|
||||
}
|
||||
|
||||
24
src/pxl8_lua.h
Normal file
24
src/pxl8_lua.h
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
#pragma once
|
||||
|
||||
#include <lua.h>
|
||||
#include <lualib.h>
|
||||
#include <lauxlib.h>
|
||||
|
||||
#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
|
||||
83
src/pxl8_macros.h
Normal file
83
src/pxl8_macros.h
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
#pragma once
|
||||
|
||||
#include <stdio.h>
|
||||
#include <time.h>
|
||||
|
||||
#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)
|
||||
189
src/pxl8_simd.h
Normal file
189
src/pxl8_simd.h
Normal file
|
|
@ -0,0 +1,189 @@
|
|||
#pragma once
|
||||
|
||||
#include "pxl8_types.h"
|
||||
|
||||
#if defined(__AVX2__)
|
||||
#include <immintrin.h>
|
||||
#define PXL8_SIMD_AVX2 1
|
||||
#define PXL8_SIMD_WIDTH_U8 32
|
||||
#define PXL8_SIMD_WIDTH_U32 8
|
||||
#elif defined(__SSE2__)
|
||||
#include <emmintrin.h>
|
||||
#define PXL8_SIMD_SSE2 1
|
||||
#define PXL8_SIMD_WIDTH_U8 16
|
||||
#define PXL8_SIMD_WIDTH_U32 4
|
||||
#elif defined(__ARM_NEON)
|
||||
#include <arm_neon.h>
|
||||
#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;
|
||||
}
|
||||
72
src/pxl8_types.h
Normal file
72
src/pxl8_types.h
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
#pragma once
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
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;
|
||||
461
src/pxl8_vfx.c
Normal file
461
src/pxl8_vfx.c
Normal file
|
|
@ -0,0 +1,461 @@
|
|||
#include "pxl8_vfx.h"
|
||||
#include "pxl8_macros.h"
|
||||
#include <math.h>
|
||||
#include <stdlib.h>
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
68
src/pxl8_vfx.h
Normal file
68
src/pxl8_vfx.h
Normal file
|
|
@ -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);
|
||||
Loading…
Add table
Add a link
Reference in a new issue