refactor: reorganize pxl8 into client/src/ module structure
- core/: main entry, types, logging, I/O, RNG
- asset/: ase loader, cart, save, embed
- gfx/: graphics, animation, atlas, fonts, tilemap, transitions
- sfx/: audio
- script/: lua/fennel runtime, REPL
- hal/: platform abstraction (SDL3)
- world/: BSP, world, procedural gen
- math/: math utilities
- game/: GUI, replay
- lua/: Lua API modules
2026-01-12 21:46:31 -06:00
|
|
|
#include "pxl8_gfx.h"
|
|
|
|
|
|
|
|
|
|
#include <stdlib.h>
|
|
|
|
|
#include <string.h>
|
|
|
|
|
|
|
|
|
|
#include "pxl8_ase.h"
|
|
|
|
|
#include "pxl8_atlas.h"
|
|
|
|
|
#include "pxl8_backend.h"
|
|
|
|
|
#include "pxl8_blit.h"
|
|
|
|
|
#include "pxl8_color.h"
|
|
|
|
|
#include "pxl8_colormap.h"
|
|
|
|
|
#include "pxl8_font.h"
|
|
|
|
|
#include "pxl8_hal.h"
|
|
|
|
|
#include "pxl8_log.h"
|
|
|
|
|
#include "pxl8_macros.h"
|
|
|
|
|
#include "pxl8_math.h"
|
2026-01-21 23:19:50 -06:00
|
|
|
#include "pxl8_mem.h"
|
refactor: reorganize pxl8 into client/src/ module structure
- core/: main entry, types, logging, I/O, RNG
- asset/: ase loader, cart, save, embed
- gfx/: graphics, animation, atlas, fonts, tilemap, transitions
- sfx/: audio
- script/: lua/fennel runtime, REPL
- hal/: platform abstraction (SDL3)
- world/: BSP, world, procedural gen
- math/: math utilities
- game/: GUI, replay
- lua/: Lua API modules
2026-01-12 21:46:31 -06:00
|
|
|
#include "pxl8_sys.h"
|
|
|
|
|
#include "pxl8_types.h"
|
|
|
|
|
|
|
|
|
|
typedef struct pxl8_sprite_cache_entry {
|
|
|
|
|
char path[256];
|
|
|
|
|
u32 sprite_id;
|
|
|
|
|
bool active;
|
|
|
|
|
} pxl8_sprite_cache_entry;
|
|
|
|
|
|
|
|
|
|
struct pxl8_gfx {
|
|
|
|
|
pxl8_atlas* atlas;
|
|
|
|
|
pxl8_gfx_backend backend;
|
|
|
|
|
pxl8_colormap* colormap;
|
|
|
|
|
u8* framebuffer;
|
|
|
|
|
i32 framebuffer_height;
|
|
|
|
|
i32 framebuffer_width;
|
|
|
|
|
pxl8_frustum frustum;
|
|
|
|
|
const pxl8_hal* hal;
|
|
|
|
|
bool initialized;
|
|
|
|
|
pxl8_palette* palette;
|
2026-01-17 22:52:36 -06:00
|
|
|
pxl8_palette_cube* palette_cube;
|
refactor: reorganize pxl8 into client/src/ module structure
- core/: main entry, types, logging, I/O, RNG
- asset/: ase loader, cart, save, embed
- gfx/: graphics, animation, atlas, fonts, tilemap, transitions
- sfx/: audio
- script/: lua/fennel runtime, REPL
- hal/: platform abstraction (SDL3)
- world/: BSP, world, procedural gen
- math/: math utilities
- game/: GUI, replay
- lua/: Lua API modules
2026-01-12 21:46:31 -06:00
|
|
|
pxl8_pixel_mode pixel_mode;
|
|
|
|
|
void* platform_data;
|
|
|
|
|
pxl8_sprite_cache_entry* sprite_cache;
|
|
|
|
|
u32 sprite_cache_capacity;
|
|
|
|
|
u32 sprite_cache_count;
|
|
|
|
|
pxl8_viewport viewport;
|
2026-01-21 23:19:50 -06:00
|
|
|
pxl8_mat4 view_proj;
|
refactor: reorganize pxl8 into client/src/ module structure
- core/: main entry, types, logging, I/O, RNG
- asset/: ase loader, cart, save, embed
- gfx/: graphics, animation, atlas, fonts, tilemap, transitions
- sfx/: audio
- script/: lua/fennel runtime, REPL
- hal/: platform abstraction (SDL3)
- world/: BSP, world, procedural gen
- math/: math utilities
- game/: GUI, replay
- lua/: Lua API modules
2026-01-12 21:46:31 -06:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
pxl8_bounds pxl8_gfx_get_bounds(pxl8_gfx* gfx) {
|
|
|
|
|
pxl8_bounds bounds = {0};
|
|
|
|
|
if (!gfx) {
|
|
|
|
|
return bounds;
|
|
|
|
|
}
|
|
|
|
|
bounds.w = gfx->framebuffer_width;
|
|
|
|
|
bounds.h = gfx->framebuffer_height;
|
|
|
|
|
return bounds;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pxl8_pixel_mode pxl8_gfx_get_pixel_mode(pxl8_gfx* gfx) {
|
|
|
|
|
return gfx ? gfx->pixel_mode : PXL8_PIXEL_INDEXED;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
u8* pxl8_gfx_get_framebuffer_indexed(pxl8_gfx* gfx) {
|
|
|
|
|
if (!gfx || gfx->pixel_mode == PXL8_PIXEL_HICOLOR) return NULL;
|
|
|
|
|
return gfx->framebuffer;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
u16* pxl8_gfx_get_framebuffer_hicolor(pxl8_gfx* gfx) {
|
|
|
|
|
if (!gfx || gfx->pixel_mode != PXL8_PIXEL_HICOLOR) return NULL;
|
|
|
|
|
return (u16*)gfx->framebuffer;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
i32 pxl8_gfx_get_height(const pxl8_gfx* gfx) {
|
|
|
|
|
return gfx ? gfx->framebuffer_height : 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
i32 pxl8_gfx_get_width(const pxl8_gfx* gfx) {
|
|
|
|
|
return gfx ? gfx->framebuffer_width : 0;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-21 23:19:50 -06:00
|
|
|
pxl8_palette* pxl8_gfx_palette(pxl8_gfx* gfx) {
|
refactor: reorganize pxl8 into client/src/ module structure
- core/: main entry, types, logging, I/O, RNG
- asset/: ase loader, cart, save, embed
- gfx/: graphics, animation, atlas, fonts, tilemap, transitions
- sfx/: audio
- script/: lua/fennel runtime, REPL
- hal/: platform abstraction (SDL3)
- world/: BSP, world, procedural gen
- math/: math utilities
- game/: GUI, replay
- lua/: Lua API modules
2026-01-12 21:46:31 -06:00
|
|
|
return gfx ? gfx->palette : NULL;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-21 23:19:50 -06:00
|
|
|
pxl8_colormap* pxl8_gfx_colormap(pxl8_gfx* gfx) {
|
|
|
|
|
return gfx ? gfx->colormap : NULL;
|
|
|
|
|
}
|
|
|
|
|
|
refactor: reorganize pxl8 into client/src/ module structure
- core/: main entry, types, logging, I/O, RNG
- asset/: ase loader, cart, save, embed
- gfx/: graphics, animation, atlas, fonts, tilemap, transitions
- sfx/: audio
- script/: lua/fennel runtime, REPL
- hal/: platform abstraction (SDL3)
- world/: BSP, world, procedural gen
- math/: math utilities
- game/: GUI, replay
- lua/: Lua API modules
2026-01-12 21:46:31 -06:00
|
|
|
u8 pxl8_gfx_find_color(pxl8_gfx* gfx, u32 color) {
|
|
|
|
|
if (!gfx || !gfx->palette) return 0;
|
|
|
|
|
if (color <= 0xFFFFFF) color = (color << 8) | 0xFF;
|
|
|
|
|
u8 r = (color >> 24) & 0xFF;
|
|
|
|
|
u8 g = (color >> 16) & 0xFF;
|
|
|
|
|
u8 b = (color >> 8) & 0xFF;
|
|
|
|
|
return pxl8_palette_find_closest(gfx->palette, r, g, b);
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-21 23:19:50 -06:00
|
|
|
void pxl8_gfx_set_palette_colors(pxl8_gfx* gfx, const u32* colors, u16 count) {
|
|
|
|
|
if (!gfx || !gfx->palette || !colors) return;
|
|
|
|
|
pxl8_set_palette(gfx->palette, colors, count);
|
|
|
|
|
if (gfx->pixel_mode != PXL8_PIXEL_HICOLOR && gfx->backend.type == PXL8_GFX_BACKEND_CPU) {
|
|
|
|
|
pxl8_cpu_set_palette(gfx->backend.cpu, pxl8_palette_colors(gfx->palette));
|
refactor: reorganize pxl8 into client/src/ module structure
- core/: main entry, types, logging, I/O, RNG
- asset/: ase loader, cart, save, embed
- gfx/: graphics, animation, atlas, fonts, tilemap, transitions
- sfx/: audio
- script/: lua/fennel runtime, REPL
- hal/: platform abstraction (SDL3)
- world/: BSP, world, procedural gen
- math/: math utilities
- game/: GUI, replay
- lua/: Lua API modules
2026-01-12 21:46:31 -06:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
i32 pxl8_gfx_load_palette(pxl8_gfx* gfx, const char* filepath) {
|
2026-01-21 23:19:50 -06:00
|
|
|
if (!gfx || !gfx->palette || !filepath) return -1;
|
|
|
|
|
pxl8_result result = pxl8_palette_load_ase(gfx->palette, filepath);
|
refactor: reorganize pxl8 into client/src/ module structure
- core/: main entry, types, logging, I/O, RNG
- asset/: ase loader, cart, save, embed
- gfx/: graphics, animation, atlas, fonts, tilemap, transitions
- sfx/: audio
- script/: lua/fennel runtime, REPL
- hal/: platform abstraction (SDL3)
- world/: BSP, world, procedural gen
- math/: math utilities
- game/: GUI, replay
- lua/: Lua API modules
2026-01-12 21:46:31 -06:00
|
|
|
if (result != PXL8_OK) return (i32)result;
|
|
|
|
|
if (gfx->pixel_mode != PXL8_PIXEL_HICOLOR) {
|
|
|
|
|
if (gfx->colormap) {
|
2026-01-21 23:19:50 -06:00
|
|
|
pxl8_colormap_generate(gfx->colormap, pxl8_palette_colors(gfx->palette));
|
refactor: reorganize pxl8 into client/src/ module structure
- core/: main entry, types, logging, I/O, RNG
- asset/: ase loader, cart, save, embed
- gfx/: graphics, animation, atlas, fonts, tilemap, transitions
- sfx/: audio
- script/: lua/fennel runtime, REPL
- hal/: platform abstraction (SDL3)
- world/: BSP, world, procedural gen
- math/: math utilities
- game/: GUI, replay
- lua/: Lua API modules
2026-01-12 21:46:31 -06:00
|
|
|
}
|
|
|
|
|
if (gfx->backend.type == PXL8_GFX_BACKEND_CPU) {
|
2026-01-21 23:19:50 -06:00
|
|
|
pxl8_cpu_set_palette(gfx->backend.cpu, pxl8_palette_colors(gfx->palette));
|
refactor: reorganize pxl8 into client/src/ module structure
- core/: main entry, types, logging, I/O, RNG
- asset/: ase loader, cart, save, embed
- gfx/: graphics, animation, atlas, fonts, tilemap, transitions
- sfx/: audio
- script/: lua/fennel runtime, REPL
- hal/: platform abstraction (SDL3)
- world/: BSP, world, procedural gen
- math/: math utilities
- game/: GUI, replay
- lua/: Lua API modules
2026-01-12 21:46:31 -06:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pxl8_gfx* pxl8_gfx_create(
|
|
|
|
|
const pxl8_hal* hal,
|
|
|
|
|
void* platform_data,
|
|
|
|
|
pxl8_pixel_mode mode,
|
|
|
|
|
pxl8_resolution resolution
|
|
|
|
|
) {
|
2026-01-21 23:19:50 -06:00
|
|
|
pxl8_gfx* gfx = (pxl8_gfx*)pxl8_calloc(1, sizeof(pxl8_gfx));
|
refactor: reorganize pxl8 into client/src/ module structure
- core/: main entry, types, logging, I/O, RNG
- asset/: ase loader, cart, save, embed
- gfx/: graphics, animation, atlas, fonts, tilemap, transitions
- sfx/: audio
- script/: lua/fennel runtime, REPL
- hal/: platform abstraction (SDL3)
- world/: BSP, world, procedural gen
- math/: math utilities
- game/: GUI, replay
- lua/: Lua API modules
2026-01-12 21:46:31 -06:00
|
|
|
if (!gfx) {
|
|
|
|
|
pxl8_error("Failed to allocate graphics context");
|
|
|
|
|
return NULL;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
gfx->hal = hal;
|
|
|
|
|
gfx->platform_data = platform_data;
|
|
|
|
|
|
|
|
|
|
gfx->pixel_mode = mode;
|
|
|
|
|
pxl8_size size = pxl8_get_resolution_dimensions(resolution);
|
|
|
|
|
gfx->framebuffer_width = size.w;
|
|
|
|
|
gfx->framebuffer_height = size.h;
|
|
|
|
|
|
|
|
|
|
if (!gfx->platform_data) {
|
|
|
|
|
pxl8_error("Platform data cannot be NULL");
|
2026-01-21 23:19:50 -06:00
|
|
|
pxl8_free(gfx);
|
refactor: reorganize pxl8 into client/src/ module structure
- core/: main entry, types, logging, I/O, RNG
- asset/: ase loader, cart, save, embed
- gfx/: graphics, animation, atlas, fonts, tilemap, transitions
- sfx/: audio
- script/: lua/fennel runtime, REPL
- hal/: platform abstraction (SDL3)
- world/: BSP, world, procedural gen
- math/: math utilities
- game/: GUI, replay
- lua/: Lua API modules
2026-01-12 21:46:31 -06:00
|
|
|
return NULL;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (mode != PXL8_PIXEL_HICOLOR) {
|
|
|
|
|
gfx->palette = pxl8_palette_create();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
gfx->backend.type = PXL8_GFX_BACKEND_CPU;
|
|
|
|
|
gfx->backend.cpu = pxl8_cpu_create(gfx->framebuffer_width, gfx->framebuffer_height);
|
|
|
|
|
if (!gfx->backend.cpu) {
|
|
|
|
|
pxl8_error("Failed to create CPU backend");
|
|
|
|
|
pxl8_gfx_destroy(gfx);
|
|
|
|
|
return NULL;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
gfx->framebuffer = pxl8_cpu_get_framebuffer(gfx->backend.cpu);
|
|
|
|
|
|
|
|
|
|
if (mode != PXL8_PIXEL_HICOLOR) {
|
2026-01-21 23:19:50 -06:00
|
|
|
gfx->colormap = pxl8_calloc(1, sizeof(pxl8_colormap));
|
refactor: reorganize pxl8 into client/src/ module structure
- core/: main entry, types, logging, I/O, RNG
- asset/: ase loader, cart, save, embed
- gfx/: graphics, animation, atlas, fonts, tilemap, transitions
- sfx/: audio
- script/: lua/fennel runtime, REPL
- hal/: platform abstraction (SDL3)
- world/: BSP, world, procedural gen
- math/: math utilities
- game/: GUI, replay
- lua/: Lua API modules
2026-01-12 21:46:31 -06:00
|
|
|
if (gfx->colormap) {
|
|
|
|
|
pxl8_cpu_set_colormap(gfx->backend.cpu, gfx->colormap);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
gfx->viewport.offset_x = 0;
|
|
|
|
|
gfx->viewport.offset_y = 0;
|
|
|
|
|
gfx->viewport.scaled_width = gfx->framebuffer_width;
|
|
|
|
|
gfx->viewport.scaled_height = gfx->framebuffer_height;
|
|
|
|
|
gfx->viewport.scale = 1.0f;
|
|
|
|
|
|
|
|
|
|
gfx->initialized = true;
|
|
|
|
|
return gfx;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void pxl8_gfx_destroy(pxl8_gfx* gfx) {
|
|
|
|
|
if (!gfx) return;
|
|
|
|
|
|
|
|
|
|
pxl8_atlas_destroy(gfx->atlas);
|
|
|
|
|
if (gfx->backend.type == PXL8_GFX_BACKEND_CPU) {
|
|
|
|
|
pxl8_cpu_destroy(gfx->backend.cpu);
|
|
|
|
|
}
|
2026-01-21 23:19:50 -06:00
|
|
|
pxl8_free(gfx->colormap);
|
2026-01-17 22:52:36 -06:00
|
|
|
pxl8_palette_cube_destroy(gfx->palette_cube);
|
refactor: reorganize pxl8 into client/src/ module structure
- core/: main entry, types, logging, I/O, RNG
- asset/: ase loader, cart, save, embed
- gfx/: graphics, animation, atlas, fonts, tilemap, transitions
- sfx/: audio
- script/: lua/fennel runtime, REPL
- hal/: platform abstraction (SDL3)
- world/: BSP, world, procedural gen
- math/: math utilities
- game/: GUI, replay
- lua/: Lua API modules
2026-01-12 21:46:31 -06:00
|
|
|
pxl8_palette_destroy(gfx->palette);
|
2026-01-21 23:19:50 -06:00
|
|
|
pxl8_free(gfx->sprite_cache);
|
refactor: reorganize pxl8 into client/src/ module structure
- core/: main entry, types, logging, I/O, RNG
- asset/: ase loader, cart, save, embed
- gfx/: graphics, animation, atlas, fonts, tilemap, transitions
- sfx/: audio
- script/: lua/fennel runtime, REPL
- hal/: platform abstraction (SDL3)
- world/: BSP, world, procedural gen
- math/: math utilities
- game/: GUI, replay
- lua/: Lua API modules
2026-01-12 21:46:31 -06:00
|
|
|
|
2026-01-21 23:19:50 -06:00
|
|
|
pxl8_free(gfx);
|
refactor: reorganize pxl8 into client/src/ module structure
- core/: main entry, types, logging, I/O, RNG
- asset/: ase loader, cart, save, embed
- gfx/: graphics, animation, atlas, fonts, tilemap, transitions
- sfx/: audio
- script/: lua/fennel runtime, REPL
- hal/: platform abstraction (SDL3)
- world/: BSP, world, procedural gen
- math/: math utilities
- game/: GUI, replay
- lua/: Lua API modules
2026-01-12 21:46:31 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static pxl8_result pxl8_gfx_ensure_atlas(pxl8_gfx* gfx) {
|
|
|
|
|
if (gfx->atlas) return PXL8_OK;
|
|
|
|
|
gfx->atlas = pxl8_atlas_create(PXL8_DEFAULT_ATLAS_SIZE, PXL8_DEFAULT_ATLAS_SIZE, gfx->pixel_mode);
|
|
|
|
|
return gfx->atlas ? PXL8_OK : PXL8_ERROR_OUT_OF_MEMORY;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void pxl8_gfx_clear_textures(pxl8_gfx* gfx) {
|
|
|
|
|
if (!gfx) return;
|
|
|
|
|
|
|
|
|
|
if (gfx->atlas) {
|
|
|
|
|
pxl8_atlas_clear(gfx->atlas, 0);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (gfx->sprite_cache) {
|
|
|
|
|
gfx->sprite_cache_count = 0;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pxl8_result pxl8_gfx_create_texture(pxl8_gfx* gfx, const u8* pixels, u32 width, u32 height) {
|
|
|
|
|
if (!gfx || !gfx->initialized || !pixels) return PXL8_ERROR_INVALID_ARGUMENT;
|
|
|
|
|
|
|
|
|
|
pxl8_result result = pxl8_gfx_ensure_atlas(gfx);
|
|
|
|
|
if (result != PXL8_OK) return result;
|
|
|
|
|
|
|
|
|
|
u32 texture_id = pxl8_atlas_add_texture(gfx->atlas, pixels, width, height, gfx->pixel_mode);
|
|
|
|
|
if (texture_id == UINT32_MAX) {
|
|
|
|
|
pxl8_error("Texture doesn't fit in atlas");
|
|
|
|
|
return PXL8_ERROR_INVALID_SIZE;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return texture_id;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pxl8_result pxl8_gfx_load_sprite(pxl8_gfx* gfx, const char* path) {
|
|
|
|
|
if (!gfx || !gfx->initialized || !path) return PXL8_ERROR_INVALID_ARGUMENT;
|
|
|
|
|
|
|
|
|
|
if (!gfx->sprite_cache) {
|
|
|
|
|
gfx->sprite_cache_capacity = PXL8_DEFAULT_SPRITE_CACHE_CAPACITY;
|
2026-01-21 23:19:50 -06:00
|
|
|
gfx->sprite_cache = (pxl8_sprite_cache_entry*)pxl8_calloc(
|
refactor: reorganize pxl8 into client/src/ module structure
- core/: main entry, types, logging, I/O, RNG
- asset/: ase loader, cart, save, embed
- gfx/: graphics, animation, atlas, fonts, tilemap, transitions
- sfx/: audio
- script/: lua/fennel runtime, REPL
- hal/: platform abstraction (SDL3)
- world/: BSP, world, procedural gen
- math/: math utilities
- game/: GUI, replay
- lua/: Lua API modules
2026-01-12 21:46:31 -06:00
|
|
|
gfx->sprite_cache_capacity, sizeof(pxl8_sprite_cache_entry)
|
|
|
|
|
);
|
|
|
|
|
if (!gfx->sprite_cache) return PXL8_ERROR_OUT_OF_MEMORY;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (u32 i = 0; i < gfx->sprite_cache_count; i++) {
|
|
|
|
|
if (gfx->sprite_cache[i].active && strcmp(gfx->sprite_cache[i].path, path) == 0) {
|
|
|
|
|
return gfx->sprite_cache[i].sprite_id;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pxl8_result result = pxl8_gfx_ensure_atlas(gfx);
|
|
|
|
|
if (result != PXL8_OK) return result;
|
|
|
|
|
|
|
|
|
|
pxl8_ase_file ase_file;
|
|
|
|
|
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_destroy(&ase_file);
|
|
|
|
|
return PXL8_ERROR_INVALID_FORMAT;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
u32 sprite_id = pxl8_atlas_add_texture(
|
|
|
|
|
gfx->atlas,
|
|
|
|
|
ase_file.frames[0].pixels,
|
|
|
|
|
ase_file.header.width,
|
|
|
|
|
ase_file.header.height,
|
|
|
|
|
gfx->pixel_mode
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
pxl8_ase_destroy(&ase_file);
|
|
|
|
|
|
|
|
|
|
if (sprite_id == UINT32_MAX) {
|
|
|
|
|
pxl8_error("Sprite doesn't fit in atlas");
|
|
|
|
|
return PXL8_ERROR_INVALID_SIZE;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (gfx->sprite_cache_count >= gfx->sprite_cache_capacity) {
|
|
|
|
|
u32 new_capacity = gfx->sprite_cache_capacity * 2;
|
2026-01-21 23:19:50 -06:00
|
|
|
pxl8_sprite_cache_entry* new_cache = (pxl8_sprite_cache_entry*)pxl8_realloc(
|
refactor: reorganize pxl8 into client/src/ module structure
- core/: main entry, types, logging, I/O, RNG
- asset/: ase loader, cart, save, embed
- gfx/: graphics, animation, atlas, fonts, tilemap, transitions
- sfx/: audio
- script/: lua/fennel runtime, REPL
- hal/: platform abstraction (SDL3)
- world/: BSP, world, procedural gen
- math/: math utilities
- game/: GUI, replay
- lua/: Lua API modules
2026-01-12 21:46:31 -06:00
|
|
|
gfx->sprite_cache,
|
|
|
|
|
new_capacity * sizeof(pxl8_sprite_cache_entry)
|
|
|
|
|
);
|
|
|
|
|
if (!new_cache) return PXL8_ERROR_OUT_OF_MEMORY;
|
|
|
|
|
gfx->sprite_cache = new_cache;
|
|
|
|
|
gfx->sprite_cache_capacity = new_capacity;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pxl8_sprite_cache_entry* entry = &gfx->sprite_cache[gfx->sprite_cache_count++];
|
|
|
|
|
entry->active = true;
|
|
|
|
|
entry->sprite_id = sprite_id;
|
|
|
|
|
pxl8_strncpy(entry->path, path, sizeof(entry->path));
|
|
|
|
|
|
|
|
|
|
return sprite_id;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pxl8_atlas* pxl8_gfx_get_atlas(pxl8_gfx* gfx) {
|
|
|
|
|
if (!gfx || !gfx->initialized) return NULL;
|
|
|
|
|
|
|
|
|
|
if (pxl8_gfx_ensure_atlas(gfx) != PXL8_OK) return NULL;
|
|
|
|
|
return gfx->atlas;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pxl8_result pxl8_gfx_load_font_atlas(pxl8_gfx* gfx) {
|
|
|
|
|
(void)gfx;
|
|
|
|
|
return PXL8_OK;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void pxl8_gfx_upload_framebuffer(pxl8_gfx* gfx) {
|
|
|
|
|
if (!gfx || !gfx->initialized || !gfx->hal) return;
|
|
|
|
|
|
2026-01-17 22:52:36 -06:00
|
|
|
if (gfx->backend.type == PXL8_GFX_BACKEND_CPU && gfx->pixel_mode == PXL8_PIXEL_INDEXED) {
|
|
|
|
|
pxl8_cpu_resolve(gfx->backend.cpu);
|
|
|
|
|
u32* output = pxl8_cpu_get_output(gfx->backend.cpu);
|
|
|
|
|
if (output) {
|
|
|
|
|
gfx->hal->upload_texture(
|
|
|
|
|
gfx->platform_data,
|
|
|
|
|
output,
|
|
|
|
|
gfx->framebuffer_width,
|
|
|
|
|
gfx->framebuffer_height,
|
|
|
|
|
PXL8_PIXEL_RGBA,
|
|
|
|
|
NULL
|
|
|
|
|
);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
refactor: reorganize pxl8 into client/src/ module structure
- core/: main entry, types, logging, I/O, RNG
- asset/: ase loader, cart, save, embed
- gfx/: graphics, animation, atlas, fonts, tilemap, transitions
- sfx/: audio
- script/: lua/fennel runtime, REPL
- hal/: platform abstraction (SDL3)
- world/: BSP, world, procedural gen
- math/: math utilities
- game/: GUI, replay
- lua/: Lua API modules
2026-01-12 21:46:31 -06:00
|
|
|
u32* colors = gfx->palette ? pxl8_palette_colors(gfx->palette) : NULL;
|
|
|
|
|
gfx->hal->upload_texture(
|
|
|
|
|
gfx->platform_data,
|
|
|
|
|
gfx->framebuffer,
|
|
|
|
|
gfx->framebuffer_width,
|
|
|
|
|
gfx->framebuffer_height,
|
2026-01-17 22:52:36 -06:00
|
|
|
gfx->pixel_mode,
|
|
|
|
|
colors
|
refactor: reorganize pxl8 into client/src/ module structure
- core/: main entry, types, logging, I/O, RNG
- asset/: ase loader, cart, save, embed
- gfx/: graphics, animation, atlas, fonts, tilemap, transitions
- sfx/: audio
- script/: lua/fennel runtime, REPL
- hal/: platform abstraction (SDL3)
- world/: BSP, world, procedural gen
- math/: math utilities
- game/: GUI, replay
- lua/: Lua API modules
2026-01-12 21:46:31 -06:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void pxl8_gfx_present(pxl8_gfx* gfx) {
|
|
|
|
|
if (!gfx || !gfx->initialized || !gfx->hal) return;
|
|
|
|
|
|
|
|
|
|
gfx->hal->present(gfx->platform_data);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pxl8_viewport pxl8_gfx_viewport(pxl8_bounds bounds, i32 width, i32 height) {
|
|
|
|
|
pxl8_viewport vp = {0};
|
|
|
|
|
vp.scale = fminf(bounds.w / (f32)width, bounds.h / (f32)height);
|
|
|
|
|
vp.scaled_width = (i32)(width * vp.scale);
|
|
|
|
|
vp.scaled_height = (i32)(height * vp.scale);
|
|
|
|
|
vp.offset_x = (bounds.w - vp.scaled_width) / 2;
|
|
|
|
|
vp.offset_y = (bounds.h - vp.scaled_height) / 2;
|
|
|
|
|
return vp;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void pxl8_gfx_set_viewport(pxl8_gfx* gfx, pxl8_viewport vp) {
|
|
|
|
|
if (!gfx) return;
|
|
|
|
|
gfx->viewport = vp;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void pxl8_gfx_project(pxl8_gfx* gfx, f32 left, f32 right, f32 top, f32 bottom) {
|
|
|
|
|
(void)gfx; (void)left; (void)right; (void)top; (void)bottom;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void pxl8_2d_clear(pxl8_gfx* gfx, u32 color) {
|
|
|
|
|
if (!gfx) return;
|
|
|
|
|
switch (gfx->backend.type) {
|
|
|
|
|
case PXL8_GFX_BACKEND_CPU:
|
|
|
|
|
pxl8_cpu_clear(gfx->backend.cpu, (u8)color);
|
|
|
|
|
break;
|
|
|
|
|
case PXL8_GFX_BACKEND_GPU:
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void pxl8_2d_pixel(pxl8_gfx* gfx, i32 x, i32 y, u32 color) {
|
|
|
|
|
if (!gfx) return;
|
|
|
|
|
switch (gfx->backend.type) {
|
|
|
|
|
case PXL8_GFX_BACKEND_CPU:
|
|
|
|
|
pxl8_cpu_draw_pixel(gfx->backend.cpu, x, y, (u8)color);
|
|
|
|
|
break;
|
|
|
|
|
case PXL8_GFX_BACKEND_GPU:
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
u32 pxl8_2d_get_pixel(pxl8_gfx* gfx, i32 x, i32 y) {
|
|
|
|
|
if (!gfx) return 0;
|
|
|
|
|
switch (gfx->backend.type) {
|
|
|
|
|
case PXL8_GFX_BACKEND_CPU:
|
|
|
|
|
return pxl8_cpu_get_pixel(gfx->backend.cpu, x, y);
|
|
|
|
|
case PXL8_GFX_BACKEND_GPU:
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void pxl8_2d_line(pxl8_gfx* gfx, i32 x0, i32 y0, i32 x1, i32 y1, u32 color) {
|
|
|
|
|
if (!gfx) return;
|
|
|
|
|
switch (gfx->backend.type) {
|
|
|
|
|
case PXL8_GFX_BACKEND_CPU:
|
|
|
|
|
pxl8_cpu_draw_line_2d(gfx->backend.cpu, x0, y0, x1, y1, (u8)color);
|
|
|
|
|
break;
|
|
|
|
|
case PXL8_GFX_BACKEND_GPU:
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void pxl8_2d_rect(pxl8_gfx* gfx, i32 x, i32 y, i32 w, i32 h, u32 color) {
|
|
|
|
|
if (!gfx) return;
|
|
|
|
|
switch (gfx->backend.type) {
|
|
|
|
|
case PXL8_GFX_BACKEND_CPU:
|
|
|
|
|
pxl8_cpu_draw_rect(gfx->backend.cpu, x, y, w, h, (u8)color);
|
|
|
|
|
break;
|
|
|
|
|
case PXL8_GFX_BACKEND_GPU:
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void pxl8_2d_rect_fill(pxl8_gfx* gfx, i32 x, i32 y, i32 w, i32 h, u32 color) {
|
|
|
|
|
if (!gfx) return;
|
|
|
|
|
switch (gfx->backend.type) {
|
|
|
|
|
case PXL8_GFX_BACKEND_CPU:
|
|
|
|
|
pxl8_cpu_draw_rect_fill(gfx->backend.cpu, x, y, w, h, (u8)color);
|
|
|
|
|
break;
|
|
|
|
|
case PXL8_GFX_BACKEND_GPU:
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void pxl8_2d_circle(pxl8_gfx* gfx, i32 cx, i32 cy, i32 radius, u32 color) {
|
|
|
|
|
if (!gfx) return;
|
|
|
|
|
switch (gfx->backend.type) {
|
|
|
|
|
case PXL8_GFX_BACKEND_CPU:
|
|
|
|
|
pxl8_cpu_draw_circle(gfx->backend.cpu, cx, cy, radius, (u8)color);
|
|
|
|
|
break;
|
|
|
|
|
case PXL8_GFX_BACKEND_GPU:
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void pxl8_2d_circle_fill(pxl8_gfx* gfx, i32 cx, i32 cy, i32 radius, u32 color) {
|
|
|
|
|
if (!gfx) return;
|
|
|
|
|
switch (gfx->backend.type) {
|
|
|
|
|
case PXL8_GFX_BACKEND_CPU:
|
|
|
|
|
pxl8_cpu_draw_circle_fill(gfx->backend.cpu, cx, cy, radius, (u8)color);
|
|
|
|
|
break;
|
|
|
|
|
case PXL8_GFX_BACKEND_GPU:
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void pxl8_2d_text(pxl8_gfx* gfx, const char* text, i32 x, i32 y, u32 color) {
|
|
|
|
|
if (!gfx || !text) return;
|
|
|
|
|
|
|
|
|
|
u8* framebuffer = NULL;
|
2026-01-17 22:52:36 -06:00
|
|
|
u32* light_accum = NULL;
|
refactor: reorganize pxl8 into client/src/ module structure
- core/: main entry, types, logging, I/O, RNG
- asset/: ase loader, cart, save, embed
- gfx/: graphics, animation, atlas, fonts, tilemap, transitions
- sfx/: audio
- script/: lua/fennel runtime, REPL
- hal/: platform abstraction (SDL3)
- world/: BSP, world, procedural gen
- math/: math utilities
- game/: GUI, replay
- lua/: Lua API modules
2026-01-12 21:46:31 -06:00
|
|
|
i32 fb_width = 0;
|
|
|
|
|
i32 fb_height = 0;
|
|
|
|
|
|
|
|
|
|
switch (gfx->backend.type) {
|
|
|
|
|
case PXL8_GFX_BACKEND_CPU: {
|
|
|
|
|
pxl8_cpu_render_target* render_target = pxl8_cpu_get_target(gfx->backend.cpu);
|
|
|
|
|
if (!render_target) return;
|
|
|
|
|
framebuffer = pxl8_cpu_render_target_get_framebuffer(render_target);
|
2026-01-17 22:52:36 -06:00
|
|
|
light_accum = pxl8_cpu_render_target_get_light_accum(render_target);
|
refactor: reorganize pxl8 into client/src/ module structure
- core/: main entry, types, logging, I/O, RNG
- asset/: ase loader, cart, save, embed
- gfx/: graphics, animation, atlas, fonts, tilemap, transitions
- sfx/: audio
- script/: lua/fennel runtime, REPL
- hal/: platform abstraction (SDL3)
- world/: BSP, world, procedural gen
- math/: math utilities
- game/: GUI, replay
- lua/: Lua API modules
2026-01-12 21:46:31 -06:00
|
|
|
fb_width = (i32)pxl8_cpu_render_target_get_width(render_target);
|
|
|
|
|
fb_height = (i32)pxl8_cpu_render_target_get_height(render_target);
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
case PXL8_GFX_BACKEND_GPU:
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!framebuffer) return;
|
|
|
|
|
|
|
|
|
|
const pxl8_font* font = &pxl8_default_font;
|
|
|
|
|
i32 cursor_x = x;
|
|
|
|
|
i32 cursor_y = y;
|
|
|
|
|
|
|
|
|
|
for (const char* c = text; *c; c++) {
|
|
|
|
|
const pxl8_glyph* glyph = pxl8_font_find_glyph(font, (u32)*c);
|
|
|
|
|
if (!glyph) continue;
|
|
|
|
|
|
|
|
|
|
for (i32 gy = 0; gy < glyph->height; gy++) {
|
|
|
|
|
for (i32 gx = 0; gx < glyph->width; gx++) {
|
|
|
|
|
i32 px = cursor_x + gx;
|
|
|
|
|
i32 py = cursor_y + gy;
|
|
|
|
|
|
|
|
|
|
if (px < 0 || px >= fb_width || py < 0 || py >= fb_height) continue;
|
|
|
|
|
|
|
|
|
|
u8 pixel_bit = 0;
|
|
|
|
|
|
|
|
|
|
if (glyph->format == PXL8_FONT_FORMAT_INDEXED) {
|
|
|
|
|
u8 pixel_byte = glyph->data.indexed[gy];
|
|
|
|
|
pixel_bit = (pixel_byte >> gx) & 1;
|
|
|
|
|
} else {
|
|
|
|
|
i32 glyph_idx = gy * 8 + gx;
|
|
|
|
|
u32 rgba_pixel = glyph->data.rgba[glyph_idx];
|
|
|
|
|
pixel_bit = ((rgba_pixel >> 24) & 0xFF) > 128 ? 1 : 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (pixel_bit) {
|
2026-01-17 22:52:36 -06:00
|
|
|
i32 idx = py * fb_width + px;
|
|
|
|
|
framebuffer[idx] = (u8)color;
|
|
|
|
|
if (light_accum) light_accum[idx] = 0;
|
refactor: reorganize pxl8 into client/src/ module structure
- core/: main entry, types, logging, I/O, RNG
- asset/: ase loader, cart, save, embed
- gfx/: graphics, animation, atlas, fonts, tilemap, transitions
- sfx/: audio
- script/: lua/fennel runtime, REPL
- hal/: platform abstraction (SDL3)
- world/: BSP, world, procedural gen
- math/: math utilities
- game/: GUI, replay
- lua/: Lua API modules
2026-01-12 21:46:31 -06:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
cursor_x += font->default_width;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
void pxl8_2d_sprite(pxl8_gfx* gfx, u32 sprite_id, i32 x, i32 y, i32 w, i32 h, bool flip_x, bool flip_y) {
|
|
|
|
|
if (!gfx || !gfx->atlas) return;
|
|
|
|
|
|
|
|
|
|
u8* framebuffer = NULL;
|
2026-01-21 23:19:50 -06:00
|
|
|
u32* light_accum = NULL;
|
refactor: reorganize pxl8 into client/src/ module structure
- core/: main entry, types, logging, I/O, RNG
- asset/: ase loader, cart, save, embed
- gfx/: graphics, animation, atlas, fonts, tilemap, transitions
- sfx/: audio
- script/: lua/fennel runtime, REPL
- hal/: platform abstraction (SDL3)
- world/: BSP, world, procedural gen
- math/: math utilities
- game/: GUI, replay
- lua/: Lua API modules
2026-01-12 21:46:31 -06:00
|
|
|
i32 fb_width = 0;
|
|
|
|
|
i32 fb_height = 0;
|
|
|
|
|
|
|
|
|
|
switch (gfx->backend.type) {
|
|
|
|
|
case PXL8_GFX_BACKEND_CPU: {
|
|
|
|
|
pxl8_cpu_render_target* render_target = pxl8_cpu_get_target(gfx->backend.cpu);
|
|
|
|
|
if (!render_target) return;
|
|
|
|
|
framebuffer = pxl8_cpu_render_target_get_framebuffer(render_target);
|
2026-01-21 23:19:50 -06:00
|
|
|
light_accum = pxl8_cpu_render_target_get_light_accum(render_target);
|
refactor: reorganize pxl8 into client/src/ module structure
- core/: main entry, types, logging, I/O, RNG
- asset/: ase loader, cart, save, embed
- gfx/: graphics, animation, atlas, fonts, tilemap, transitions
- sfx/: audio
- script/: lua/fennel runtime, REPL
- hal/: platform abstraction (SDL3)
- world/: BSP, world, procedural gen
- math/: math utilities
- game/: GUI, replay
- lua/: Lua API modules
2026-01-12 21:46:31 -06:00
|
|
|
fb_width = (i32)pxl8_cpu_render_target_get_width(render_target);
|
|
|
|
|
fb_height = (i32)pxl8_cpu_render_target_get_height(render_target);
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
case PXL8_GFX_BACKEND_GPU:
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!framebuffer) return;
|
|
|
|
|
|
|
|
|
|
const pxl8_atlas_entry* entry = pxl8_atlas_get_entry(gfx->atlas, sprite_id);
|
|
|
|
|
if (!entry || !entry->active) return;
|
|
|
|
|
|
|
|
|
|
i32 clip_left = (x < 0) ? -x : 0;
|
|
|
|
|
i32 clip_top = (y < 0) ? -y : 0;
|
|
|
|
|
i32 clip_right = (x + w > fb_width) ? x + w - fb_width : 0;
|
|
|
|
|
i32 clip_bottom = (y + h > fb_height) ? y + h - fb_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);
|
|
|
|
|
bool is_flipped = flip_x || flip_y;
|
|
|
|
|
|
|
|
|
|
u32 atlas_width = pxl8_atlas_get_width(gfx->atlas);
|
|
|
|
|
const u8* atlas_pixels = pxl8_atlas_get_pixels(gfx->atlas);
|
|
|
|
|
|
|
|
|
|
if (is_1to1_scale && is_unclipped && !is_flipped) {
|
|
|
|
|
const u8* sprite_data = atlas_pixels + entry->y * atlas_width + entry->x;
|
|
|
|
|
pxl8_blit_indexed(framebuffer, fb_width, sprite_data, atlas_width, x, y, w, h);
|
2026-01-21 23:19:50 -06:00
|
|
|
if (light_accum) {
|
|
|
|
|
for (i32 py = 0; py < h; py++) {
|
|
|
|
|
memset(light_accum + (y + py) * fb_width + x, 0, w * sizeof(u32));
|
|
|
|
|
}
|
|
|
|
|
}
|
refactor: reorganize pxl8 into client/src/ module structure
- core/: main entry, types, logging, I/O, RNG
- asset/: ase loader, cart, save, embed
- gfx/: graphics, animation, atlas, fonts, tilemap, transitions
- sfx/: audio
- script/: lua/fennel runtime, REPL
- hal/: platform abstraction (SDL3)
- world/: BSP, world, procedural gen
- math/: math utilities
- game/: GUI, replay
- lua/: Lua API modules
2026-01-12 21:46:31 -06:00
|
|
|
} else {
|
|
|
|
|
for (i32 py = 0; py < draw_height; py++) {
|
|
|
|
|
for (i32 px = 0; px < draw_width; px++) {
|
|
|
|
|
i32 local_x = (px + clip_left) * entry->w / w;
|
|
|
|
|
i32 local_y = (py + clip_top) * entry->h / h;
|
|
|
|
|
|
|
|
|
|
i32 src_x = flip_x ? entry->x + entry->w - 1 - local_x : entry->x + local_x;
|
|
|
|
|
i32 src_y = flip_y ? entry->y + entry->h - 1 - local_y : entry->y + local_y;
|
|
|
|
|
|
|
|
|
|
i32 src_idx = src_y * atlas_width + src_x;
|
|
|
|
|
i32 dest_idx = (dest_y + py) * fb_width + (dest_x + px);
|
|
|
|
|
|
|
|
|
|
framebuffer[dest_idx] = pxl8_blend_indexed(atlas_pixels[src_idx], framebuffer[dest_idx]);
|
2026-01-21 23:19:50 -06:00
|
|
|
if (light_accum) light_accum[dest_idx] = 0;
|
refactor: reorganize pxl8 into client/src/ module structure
- core/: main entry, types, logging, I/O, RNG
- asset/: ase loader, cart, save, embed
- gfx/: graphics, animation, atlas, fonts, tilemap, transitions
- sfx/: audio
- script/: lua/fennel runtime, REPL
- hal/: platform abstraction (SDL3)
- world/: BSP, world, procedural gen
- math/: math utilities
- game/: GUI, replay
- lua/: Lua API modules
2026-01-12 21:46:31 -06:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void pxl8_gfx_update(pxl8_gfx* gfx, f32 dt) {
|
|
|
|
|
if (!gfx) return;
|
|
|
|
|
|
|
|
|
|
if (gfx->palette) {
|
|
|
|
|
u16 delta_ticks = (u16)(dt * 1000.0f);
|
|
|
|
|
pxl8_palette_tick(gfx->palette, delta_ticks);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-17 22:52:36 -06:00
|
|
|
pxl8_3d_frame pxl8_3d_frame_from_camera(const pxl8_3d_camera* camera, const pxl8_3d_uniforms* uniforms) {
|
|
|
|
|
pxl8_3d_frame frame = {0};
|
|
|
|
|
if (!camera) return frame;
|
|
|
|
|
|
|
|
|
|
frame.view = pxl8_3d_camera_get_view(camera);
|
|
|
|
|
frame.projection = pxl8_3d_camera_get_projection(camera);
|
|
|
|
|
frame.camera_pos = pxl8_3d_camera_get_position(camera);
|
|
|
|
|
frame.camera_dir = pxl8_3d_camera_get_forward(camera);
|
|
|
|
|
frame.near_clip = 1.0f;
|
|
|
|
|
frame.far_clip = 4096.0f;
|
|
|
|
|
|
|
|
|
|
if (uniforms) {
|
|
|
|
|
frame.uniforms = *uniforms;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return frame;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-21 23:19:50 -06:00
|
|
|
void pxl8_3d_begin_frame(pxl8_gfx* gfx, const pxl8_3d_camera* camera, const pxl8_lights* lights, const pxl8_3d_uniforms* uniforms) {
|
refactor: reorganize pxl8 into client/src/ module structure
- core/: main entry, types, logging, I/O, RNG
- asset/: ase loader, cart, save, embed
- gfx/: graphics, animation, atlas, fonts, tilemap, transitions
- sfx/: audio
- script/: lua/fennel runtime, REPL
- hal/: platform abstraction (SDL3)
- world/: BSP, world, procedural gen
- math/: math utilities
- game/: GUI, replay
- lua/: Lua API modules
2026-01-12 21:46:31 -06:00
|
|
|
if (!gfx || !camera) return;
|
|
|
|
|
|
2026-01-17 22:52:36 -06:00
|
|
|
pxl8_3d_frame frame = pxl8_3d_frame_from_camera(camera, uniforms);
|
2026-01-21 23:19:50 -06:00
|
|
|
frame.lights = lights ? pxl8_lights_data(lights) : NULL;
|
|
|
|
|
frame.lights_count = lights ? pxl8_lights_count(lights) : 0;
|
|
|
|
|
|
|
|
|
|
pxl8_mat4 vp = pxl8_mat4_multiply(frame.projection, frame.view);
|
refactor: reorganize pxl8 into client/src/ module structure
- core/: main entry, types, logging, I/O, RNG
- asset/: ase loader, cart, save, embed
- gfx/: graphics, animation, atlas, fonts, tilemap, transitions
- sfx/: audio
- script/: lua/fennel runtime, REPL
- hal/: platform abstraction (SDL3)
- world/: BSP, world, procedural gen
- math/: math utilities
- game/: GUI, replay
- lua/: Lua API modules
2026-01-12 21:46:31 -06:00
|
|
|
|
|
|
|
|
gfx->frustum = pxl8_frustum_from_matrix(vp);
|
2026-01-21 23:19:50 -06:00
|
|
|
gfx->view_proj = vp;
|
refactor: reorganize pxl8 into client/src/ module structure
- core/: main entry, types, logging, I/O, RNG
- asset/: ase loader, cart, save, embed
- gfx/: graphics, animation, atlas, fonts, tilemap, transitions
- sfx/: audio
- script/: lua/fennel runtime, REPL
- hal/: platform abstraction (SDL3)
- world/: BSP, world, procedural gen
- math/: math utilities
- game/: GUI, replay
- lua/: Lua API modules
2026-01-12 21:46:31 -06:00
|
|
|
|
|
|
|
|
switch (gfx->backend.type) {
|
|
|
|
|
case PXL8_GFX_BACKEND_CPU:
|
|
|
|
|
pxl8_cpu_begin_frame(gfx->backend.cpu, &frame);
|
|
|
|
|
break;
|
|
|
|
|
case PXL8_GFX_BACKEND_GPU:
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const pxl8_frustum* pxl8_3d_get_frustum(pxl8_gfx* gfx) {
|
|
|
|
|
if (!gfx) return NULL;
|
|
|
|
|
return &gfx->frustum;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-21 23:19:50 -06:00
|
|
|
const pxl8_mat4* pxl8_3d_get_view_proj(pxl8_gfx* gfx) {
|
|
|
|
|
if (!gfx) return NULL;
|
|
|
|
|
return &gfx->view_proj;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
u32 pxl8_3d_project_points(pxl8_gfx* gfx, const pxl8_vec3* in, pxl8_vec3* out, u32 count, const pxl8_mat4* transform) {
|
|
|
|
|
if (!gfx || !in || !out) return 0;
|
|
|
|
|
|
|
|
|
|
pxl8_mat4 mvp = transform ? pxl8_mat4_multiply(gfx->view_proj, *transform) : gfx->view_proj;
|
|
|
|
|
|
|
|
|
|
f32 hw = (f32)gfx->framebuffer_width * 0.5f;
|
|
|
|
|
f32 hh = (f32)gfx->framebuffer_height * 0.5f;
|
|
|
|
|
u32 visible = 0;
|
|
|
|
|
|
|
|
|
|
for (u32 i = 0; i < count; i++) {
|
|
|
|
|
pxl8_vec4 clip = pxl8_mat4_multiply_vec4(mvp, (pxl8_vec4){in[i].x, in[i].y, in[i].z, 1.0f});
|
|
|
|
|
|
|
|
|
|
if (clip.w <= 0.0f) {
|
|
|
|
|
out[i].z = -1.0f;
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
f32 inv_w = 1.0f / clip.w;
|
|
|
|
|
out[i].x = hw + clip.x * inv_w * hw;
|
|
|
|
|
out[i].y = hh - clip.y * inv_w * hh;
|
|
|
|
|
out[i].z = clip.w;
|
|
|
|
|
visible++;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return visible;
|
|
|
|
|
}
|
|
|
|
|
|
refactor: reorganize pxl8 into client/src/ module structure
- core/: main entry, types, logging, I/O, RNG
- asset/: ase loader, cart, save, embed
- gfx/: graphics, animation, atlas, fonts, tilemap, transitions
- sfx/: audio
- script/: lua/fennel runtime, REPL
- hal/: platform abstraction (SDL3)
- world/: BSP, world, procedural gen
- math/: math utilities
- game/: GUI, replay
- lua/: Lua API modules
2026-01-12 21:46:31 -06:00
|
|
|
void pxl8_3d_clear(pxl8_gfx* gfx, u8 color) {
|
|
|
|
|
if (!gfx) return;
|
|
|
|
|
switch (gfx->backend.type) {
|
|
|
|
|
case PXL8_GFX_BACKEND_CPU:
|
|
|
|
|
pxl8_cpu_clear(gfx->backend.cpu, color);
|
|
|
|
|
break;
|
|
|
|
|
case PXL8_GFX_BACKEND_GPU:
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void pxl8_3d_clear_depth(pxl8_gfx* gfx) {
|
|
|
|
|
if (!gfx) return;
|
|
|
|
|
switch (gfx->backend.type) {
|
|
|
|
|
case PXL8_GFX_BACKEND_CPU:
|
|
|
|
|
pxl8_cpu_clear_depth(gfx->backend.cpu);
|
|
|
|
|
break;
|
|
|
|
|
case PXL8_GFX_BACKEND_GPU:
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void pxl8_3d_draw_line(pxl8_gfx* gfx, pxl8_vec3 v0, pxl8_vec3 v1, u8 color) {
|
|
|
|
|
if (!gfx) return;
|
|
|
|
|
switch (gfx->backend.type) {
|
|
|
|
|
case PXL8_GFX_BACKEND_CPU:
|
|
|
|
|
pxl8_cpu_draw_line_3d(gfx->backend.cpu, v0, v1, color);
|
|
|
|
|
break;
|
|
|
|
|
case PXL8_GFX_BACKEND_GPU:
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-21 23:19:50 -06:00
|
|
|
void pxl8_3d_draw_mesh(pxl8_gfx* gfx, const pxl8_mesh* mesh, const pxl8_mat4* model, const pxl8_gfx_material* material) {
|
2026-01-17 22:52:36 -06:00
|
|
|
if (!gfx || !mesh || !model || !material) return;
|
refactor: reorganize pxl8 into client/src/ module structure
- core/: main entry, types, logging, I/O, RNG
- asset/: ase loader, cart, save, embed
- gfx/: graphics, animation, atlas, fonts, tilemap, transitions
- sfx/: audio
- script/: lua/fennel runtime, REPL
- hal/: platform abstraction (SDL3)
- world/: BSP, world, procedural gen
- math/: math utilities
- game/: GUI, replay
- lua/: Lua API modules
2026-01-12 21:46:31 -06:00
|
|
|
switch (gfx->backend.type) {
|
|
|
|
|
case PXL8_GFX_BACKEND_CPU:
|
|
|
|
|
pxl8_cpu_draw_mesh(gfx->backend.cpu, mesh, model, material, gfx->atlas);
|
|
|
|
|
break;
|
|
|
|
|
case PXL8_GFX_BACKEND_GPU:
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void pxl8_3d_end_frame(pxl8_gfx* gfx) {
|
|
|
|
|
if (!gfx) return;
|
|
|
|
|
switch (gfx->backend.type) {
|
|
|
|
|
case PXL8_GFX_BACKEND_CPU:
|
|
|
|
|
pxl8_cpu_end_frame(gfx->backend.cpu);
|
|
|
|
|
break;
|
|
|
|
|
case PXL8_GFX_BACKEND_GPU:
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
u8* pxl8_3d_get_framebuffer(pxl8_gfx* gfx) {
|
|
|
|
|
if (!gfx) return NULL;
|
|
|
|
|
switch (gfx->backend.type) {
|
|
|
|
|
case PXL8_GFX_BACKEND_CPU:
|
|
|
|
|
return pxl8_cpu_get_framebuffer(gfx->backend.cpu);
|
|
|
|
|
case PXL8_GFX_BACKEND_GPU:
|
|
|
|
|
return NULL;
|
|
|
|
|
}
|
|
|
|
|
return NULL;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool pxl8_gfx_push_target(pxl8_gfx* gfx) {
|
|
|
|
|
if (!gfx) return false;
|
|
|
|
|
switch (gfx->backend.type) {
|
|
|
|
|
case PXL8_GFX_BACKEND_CPU:
|
|
|
|
|
return pxl8_cpu_push_target(gfx->backend.cpu);
|
|
|
|
|
case PXL8_GFX_BACKEND_GPU:
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void pxl8_gfx_pop_target(pxl8_gfx* gfx) {
|
|
|
|
|
if (!gfx) return;
|
|
|
|
|
switch (gfx->backend.type) {
|
|
|
|
|
case PXL8_GFX_BACKEND_CPU:
|
|
|
|
|
pxl8_cpu_pop_target(gfx->backend.cpu);
|
|
|
|
|
break;
|
|
|
|
|
case PXL8_GFX_BACKEND_GPU:
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-01-17 22:52:36 -06:00
|
|
|
|
2026-01-21 23:19:50 -06:00
|
|
|
void pxl8_gfx_ensure_blend_tables(pxl8_gfx* gfx) {
|
2026-01-17 22:52:36 -06:00
|
|
|
if (!gfx || !gfx->palette) return;
|
|
|
|
|
|
|
|
|
|
if (!gfx->palette_cube) {
|
|
|
|
|
gfx->palette_cube = pxl8_palette_cube_create(gfx->palette);
|
2026-01-21 23:19:50 -06:00
|
|
|
if (gfx->backend.cpu) {
|
|
|
|
|
pxl8_cpu_set_palette_cube(gfx->backend.cpu, gfx->palette_cube);
|
|
|
|
|
}
|
2026-01-17 22:52:36 -06:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void pxl8_gfx_blend_tables_update(pxl8_gfx* gfx) {
|
|
|
|
|
if (!gfx || !gfx->palette) return;
|
|
|
|
|
|
2026-01-21 23:19:50 -06:00
|
|
|
pxl8_gfx_ensure_blend_tables(gfx);
|
|
|
|
|
|
2026-01-17 22:52:36 -06:00
|
|
|
if (gfx->palette_cube) {
|
|
|
|
|
pxl8_palette_cube_rebuild(gfx->palette_cube, gfx->palette);
|
2026-01-21 23:19:50 -06:00
|
|
|
if (gfx->backend.cpu) {
|
|
|
|
|
pxl8_cpu_set_palette_cube(gfx->backend.cpu, gfx->palette_cube);
|
|
|
|
|
}
|
2026-01-17 22:52:36 -06:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void pxl8_gfx_colormap_update(pxl8_gfx* gfx) {
|
|
|
|
|
if (!gfx || !gfx->palette || !gfx->colormap) return;
|
|
|
|
|
u32* colors = pxl8_palette_colors(gfx->palette);
|
|
|
|
|
if (colors) {
|
2026-01-21 23:19:50 -06:00
|
|
|
pxl8_colormap_generate(gfx->colormap, colors);
|
2026-01-17 22:52:36 -06:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-21 23:19:50 -06:00
|
|
|
u8 pxl8_gfx_find_closest_color(pxl8_gfx* gfx, u8 r, u8 g, u8 b) {
|
|
|
|
|
if (!gfx || !gfx->palette) return 0;
|
|
|
|
|
return pxl8_palette_find_closest(gfx->palette, r, g, b);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
u8 pxl8_gfx_ui_color(pxl8_gfx* gfx, u8 index) {
|
|
|
|
|
if (!gfx || !gfx->palette || index >= PXL8_UI_PALETTE_SIZE) return 0;
|
|
|
|
|
u32 abgr = pxl8_ui_palette[index];
|
|
|
|
|
u8 r = (abgr >> 0) & 0xFF;
|
|
|
|
|
u8 g = (abgr >> 8) & 0xFF;
|
|
|
|
|
u8 b = (abgr >> 16) & 0xFF;
|
|
|
|
|
return pxl8_palette_find_closest(gfx->palette, r, g, b);
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-17 22:52:36 -06:00
|
|
|
void pxl8_gfx_apply_effect(pxl8_gfx* gfx, pxl8_gfx_effect effect, const void* params, u32 count) {
|
|
|
|
|
if (!gfx || !params || count == 0) return;
|
|
|
|
|
|
|
|
|
|
switch (effect) {
|
|
|
|
|
case PXL8_GFX_EFFECT_GLOWS: {
|
2026-01-21 23:19:50 -06:00
|
|
|
const pxl8_glow* glows = (const pxl8_glow*)params;
|
2026-01-17 22:52:36 -06:00
|
|
|
switch (gfx->backend.type) {
|
|
|
|
|
case PXL8_GFX_BACKEND_CPU:
|
2026-01-21 23:19:50 -06:00
|
|
|
pxl8_cpu_render_glows(gfx->backend.cpu, glows, count);
|
2026-01-17 22:52:36 -06:00
|
|
|
break;
|
|
|
|
|
case PXL8_GFX_BACKEND_GPU:
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|