#include "pxl8_gfx.h" #include #include #include "pxl8_ase.h" #include "pxl8_atlas.h" #include "pxl8_blit.h" #include "pxl8_color.h" #include "pxl8_colormap.h" #include "pxl8_font.h" #include "pxl8_glows.h" #include "pxl8_hal.h" #include "pxl8_log.h" #include "pxl8_macros.h" #include "pxl8_math.h" #include "pxl8_mem.h" #include "pxl8_render.h" #include "pxl8_shader_registry.h" #include "pxl8_sys.h" #include "pxl8_types.h" void pxl8_renderer_update_stats(pxl8_renderer* r, f32 dt); pxl8_gfx_stats* pxl8_renderer_get_stats(pxl8_renderer* r); #define PXL8_MAX_TARGET_STACK 8 typedef struct pxl8_sprite_cache_entry { char path[256]; u32 sprite_id; bool active; } pxl8_sprite_cache_entry; typedef struct pxl8_target_entry { pxl8_gfx_texture color; pxl8_gfx_texture depth; pxl8_gfx_texture light; } pxl8_target_entry; #define PXL8_MAX_FRAME_RESOURCES 512 #define PXL8_STREAM_VB_SIZE (256 * 1024) #define PXL8_STREAM_IB_SIZE (512 * 1024) typedef struct pxl8_frame_resources { pxl8_gfx_texture textures[PXL8_MAX_FRAME_RESOURCES]; pxl8_gfx_buffer buffers[PXL8_MAX_FRAME_RESOURCES]; pxl8_gfx_pipeline pipelines[PXL8_MAX_FRAME_RESOURCES]; pxl8_gfx_bindings bindings[PXL8_MAX_FRAME_RESOURCES]; u32 texture_count; u32 buffer_count; u32 pipeline_count; u32 bindings_count; } pxl8_frame_resources; struct pxl8_gfx { pxl8_atlas* atlas; pxl8_renderer* renderer; pxl8_gfx_texture color_target; pxl8_gfx_texture depth_target; pxl8_gfx_cmdbuf* cmdbuf; pxl8_target_entry target_stack[PXL8_MAX_TARGET_STACK]; u32 target_stack_depth; const pxl8_bsp* bsp; pxl8_colormap* colormap; i32 framebuffer_height; i32 framebuffer_width; pxl8_frustum frustum; const pxl8_hal* hal; bool initialized; u32* output; pxl8_palette* palette; pxl8_palette_cube* palette_cube; pxl8_gfx_pass frame_pass; pxl8_frame_resources frame_res; 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; pxl8_mat4 view_proj; pxl8_3d_frame frame; pxl8_gfx_buffer stream_vb; pxl8_gfx_buffer stream_ib; u32 stream_vb_capacity; u32 stream_ib_capacity; u32 stream_vb_offset; u32 stream_ib_offset; bool wireframe; }; 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 (u8*)pxl8_texture_get_data(gfx->renderer, gfx->color_target); } u16* pxl8_gfx_get_framebuffer_hicolor(pxl8_gfx* gfx) { if (!gfx || gfx->pixel_mode != PXL8_PIXEL_HICOLOR) return NULL; return (u16*)pxl8_texture_get_data(gfx->renderer, gfx->color_target); } i32 pxl8_gfx_get_height(const pxl8_gfx* gfx) { return gfx ? gfx->framebuffer_height : 0; } u32* pxl8_gfx_get_output(pxl8_gfx* gfx) { if (!gfx) return NULL; return gfx->output; } void pxl8_gfx_resolve(pxl8_gfx* gfx) { if (!gfx || !gfx->initialized) return; if (gfx->pixel_mode != PXL8_PIXEL_INDEXED) return; const u32* pal = gfx->palette ? pxl8_palette_colors(gfx->palette) : NULL; if (!pal) { pxl8_error("resolve: no palette!"); return; } pxl8_resolve_to_rgba( gfx->renderer, gfx->color_target, pal, gfx->output ); } const u32* pxl8_gfx_palette_colors(pxl8_gfx* gfx) { if (!gfx || !gfx->palette) return NULL; return pxl8_palette_colors(gfx->palette); } u16* pxl8_gfx_get_zbuffer(pxl8_gfx* gfx) { if (!gfx) return NULL; return (u16*)pxl8_texture_get_data(gfx->renderer, gfx->depth_target); } i32 pxl8_gfx_get_width(const pxl8_gfx* gfx) { return gfx ? gfx->framebuffer_width : 0; } pxl8_palette* pxl8_gfx_palette(pxl8_gfx* gfx) { return gfx ? gfx->palette : NULL; } pxl8_colormap* pxl8_gfx_colormap(pxl8_gfx* gfx) { return gfx ? gfx->colormap : NULL; } 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); } 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); } i32 pxl8_gfx_load_palette(pxl8_gfx* gfx, const char* filepath) { if (!gfx || !gfx->palette || !filepath) return -1; pxl8_result result = pxl8_palette_load_ase(gfx->palette, filepath); if (result != PXL8_OK) return (i32)result; if (gfx->pixel_mode != PXL8_PIXEL_HICOLOR) { if (gfx->colormap) { pxl8_colormap_generate(gfx->colormap, pxl8_palette_colors(gfx->palette)); } } return 0; } pxl8_gfx* pxl8_gfx_create( const pxl8_hal* hal, void* platform_data, pxl8_pixel_mode mode, pxl8_resolution resolution ) { pxl8_shader_registry_init(); pxl8_gfx* gfx = (pxl8_gfx*)pxl8_calloc(1, sizeof(pxl8_gfx)); 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"); pxl8_free(gfx); return NULL; } if (mode != PXL8_PIXEL_HICOLOR) { gfx->palette = pxl8_palette_create(); } gfx->renderer = pxl8_renderer_create(gfx->framebuffer_width, gfx->framebuffer_height); if (!gfx->renderer) { pxl8_error("Failed to create renderer"); pxl8_gfx_destroy(gfx); return NULL; } pxl8_gfx_texture_desc color_desc = { .width = (u32)gfx->framebuffer_width, .height = (u32)gfx->framebuffer_height, .format = PXL8_GFX_FORMAT_INDEXED8, .render_target = true, }; gfx->color_target = pxl8_create_texture(gfx->renderer, &color_desc); pxl8_gfx_texture_desc depth_desc = { .width = (u32)gfx->framebuffer_width, .height = (u32)gfx->framebuffer_height, .format = PXL8_GFX_FORMAT_DEPTH16, .render_target = true, }; gfx->depth_target = pxl8_create_texture(gfx->renderer, &depth_desc); u32 pixel_count = (u32)gfx->framebuffer_width * (u32)gfx->framebuffer_height; gfx->output = pxl8_calloc(pixel_count, sizeof(u32)); if (!gfx->output) { pxl8_error("Failed to allocate output buffer"); pxl8_gfx_destroy(gfx); return NULL; } gfx->cmdbuf = pxl8_cmdbuf_create(1024); if (!gfx->cmdbuf) { pxl8_error("Failed to create command buffer"); pxl8_gfx_destroy(gfx); return NULL; } if (mode != PXL8_PIXEL_HICOLOR) { gfx->colormap = pxl8_calloc(1, sizeof(pxl8_colormap)); } gfx->target_stack[0] = (pxl8_target_entry){ .color = gfx->color_target, .depth = gfx->depth_target, }; gfx->target_stack_depth = 1; 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; pxl8_gfx_buffer_desc vb_desc = { .capacity = PXL8_STREAM_VB_SIZE, .type = PXL8_GFX_BUFFER_VERTEX, .usage = PXL8_GFX_USAGE_STREAM, }; gfx->stream_vb = pxl8_create_buffer(gfx->renderer, &vb_desc); gfx->stream_vb_capacity = PXL8_STREAM_VB_SIZE; gfx->stream_vb_offset = 0; pxl8_gfx_buffer_desc ib_desc = { .capacity = PXL8_STREAM_IB_SIZE, .type = PXL8_GFX_BUFFER_INDEX, .usage = PXL8_GFX_USAGE_STREAM, }; gfx->stream_ib = pxl8_create_buffer(gfx->renderer, &ib_desc); gfx->stream_ib_capacity = PXL8_STREAM_IB_SIZE; gfx->stream_ib_offset = 0; gfx->initialized = true; return gfx; } void pxl8_gfx_destroy(pxl8_gfx* gfx) { if (!gfx) return; pxl8_atlas_destroy(gfx->atlas); pxl8_cmdbuf_destroy(gfx->cmdbuf); pxl8_free(gfx->colormap); pxl8_free(gfx->output); pxl8_palette_cube_destroy(gfx->palette_cube); pxl8_palette_destroy(gfx->palette); pxl8_free(gfx->sprite_cache); pxl8_renderer_destroy(gfx->renderer); pxl8_free(gfx); } 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; gfx->sprite_cache = (pxl8_sprite_cache_entry*)pxl8_calloc( 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; pxl8_sprite_cache_entry* new_cache = (pxl8_sprite_cache_entry*)pxl8_realloc( 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; if (gfx->pixel_mode == PXL8_PIXEL_INDEXED) { pxl8_gfx_resolve(gfx); gfx->hal->upload_texture( gfx->platform_data, gfx->output, gfx->framebuffer_width, gfx->framebuffer_height, PXL8_PIXEL_RGBA, NULL ); return; } u32* colors = gfx->palette ? pxl8_palette_colors(gfx->palette) : NULL; u8* framebuffer = (u8*)pxl8_texture_get_data(gfx->renderer, gfx->color_target); gfx->hal->upload_texture( gfx->platform_data, framebuffer, gfx->framebuffer_width, gfx->framebuffer_height, gfx->pixel_mode, colors ); } 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; } static pxl8_gfx_texture gfx_current_color(pxl8_gfx* gfx) { if (gfx->target_stack_depth == 0) return gfx->color_target; return gfx->target_stack[gfx->target_stack_depth - 1].color; } static pxl8_gfx_texture gfx_current_depth(pxl8_gfx* gfx) { if (gfx->target_stack_depth == 0) return gfx->depth_target; return gfx->target_stack[gfx->target_stack_depth - 1].depth; } static void gfx_composite_over(pxl8_gfx* gfx, const pxl8_target_entry* src, pxl8_target_entry* dst) { if (!gfx || !src || !dst) return; u8* src_color = (u8*)pxl8_texture_get_data(gfx->renderer, src->color); u8* dst_color = (u8*)pxl8_texture_get_data(gfx->renderer, dst->color); if (!src_color || !dst_color) return; u32 width = pxl8_texture_get_width(gfx->renderer, src->color); u32 height = pxl8_texture_get_height(gfx->renderer, src->color); if (width == 0 || height == 0) return; u32 dst_width = pxl8_texture_get_width(gfx->renderer, dst->color); u32 dst_height = pxl8_texture_get_height(gfx->renderer, dst->color); if (dst_width != width || dst_height != height) return; u32* src_light = (u32*)pxl8_texture_get_data(gfx->renderer, src->light); u32* dst_light = (u32*)pxl8_texture_get_data(gfx->renderer, dst->light); u32 count = width * height; for (u32 i = 0; i < count; i++) { u8 sc = src_color[i]; if (sc == 0) continue; dst_color[i] = sc; if (dst_light) { dst_light[i] = src_light ? src_light[i] : 0; } } } void pxl8_2d_clear(pxl8_gfx* gfx, u32 color) { if (!gfx) return; pxl8_clear(gfx->renderer, gfx_current_color(gfx), (u8)color); } void pxl8_2d_pixel(pxl8_gfx* gfx, i32 x, i32 y, u32 color) { if (!gfx) return; pxl8_draw_pixel(gfx->renderer, gfx_current_color(gfx), x, y, (u8)color); } u32 pxl8_2d_get_pixel(pxl8_gfx* gfx, i32 x, i32 y) { if (!gfx) return 0; return pxl8_get_pixel(gfx->renderer, gfx_current_color(gfx), x, y); } void pxl8_2d_line(pxl8_gfx* gfx, i32 x0, i32 y0, i32 x1, i32 y1, u32 color) { if (!gfx) return; pxl8_draw_line(gfx->renderer, gfx_current_color(gfx), x0, y0, x1, y1, (u8)color); } void pxl8_2d_rect(pxl8_gfx* gfx, i32 x, i32 y, i32 w, i32 h, u32 color) { if (!gfx) return; pxl8_draw_rect(gfx->renderer, gfx_current_color(gfx), x, y, w, h, (u8)color); } void pxl8_2d_rect_fill(pxl8_gfx* gfx, i32 x, i32 y, i32 w, i32 h, u32 color) { if (!gfx) return; pxl8_draw_rect_fill(gfx->renderer, gfx_current_color(gfx), x, y, w, h, (u8)color); } void pxl8_2d_circle(pxl8_gfx* gfx, i32 cx, i32 cy, i32 radius, u32 color) { if (!gfx) return; pxl8_draw_circle(gfx->renderer, gfx_current_color(gfx), cx, cy, radius, (u8)color); } void pxl8_2d_circle_fill(pxl8_gfx* gfx, i32 cx, i32 cy, i32 radius, u32 color) { if (!gfx) return; pxl8_draw_circle_fill(gfx->renderer, gfx_current_color(gfx), cx, cy, radius, (u8)color); } void pxl8_2d_text(pxl8_gfx* gfx, const char* text, i32 x, i32 y, u32 color) { if (!gfx || !text) return; pxl8_gfx_texture target = gfx_current_color(gfx); u8* framebuffer = (u8*)pxl8_texture_get_data(gfx->renderer, target); i32 fb_width = (i32)pxl8_texture_get_width(gfx->renderer, target); i32 fb_height = (i32)pxl8_texture_get_height(gfx->renderer, target); 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) { i32 idx = py * fb_width + px; framebuffer[idx] = (u8)color; } } } 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; pxl8_gfx_texture target = gfx_current_color(gfx); u8* framebuffer = (u8*)pxl8_texture_get_data(gfx->renderer, target); i32 fb_width = (i32)pxl8_texture_get_width(gfx->renderer, target); i32 fb_height = (i32)pxl8_texture_get_height(gfx->renderer, target); 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); } 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]); } } } } 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); } } pxl8_3d_frame pxl8_3d_frame_from_camera(const pxl8_3d_camera* camera, const pxl8_shader_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; } void pxl8_3d_set_bsp(pxl8_gfx* gfx, const pxl8_bsp* bsp) { if (!gfx) return; gfx->bsp = bsp; } void pxl8_3d_begin_frame(pxl8_gfx* gfx, const pxl8_3d_camera* camera, const pxl8_lights* lights, const pxl8_shader_uniforms* uniforms) { if (!gfx || !camera) return; pxl8_3d_frame frame = pxl8_3d_frame_from_camera(camera, uniforms); frame.bsp = gfx->bsp; frame.uniforms.camera_pos = frame.camera_pos; frame.uniforms.lights = lights ? pxl8_lights_data(lights) : NULL; frame.uniforms.lights_count = lights ? pxl8_lights_count(lights) : 0; pxl8_mat4 vp = pxl8_mat4_multiply(frame.projection, frame.view); gfx->frustum = pxl8_frustum_from_matrix(vp); gfx->view_proj = vp; gfx->frame = frame; gfx->frame_res.texture_count = 0; gfx->frame_res.buffer_count = 0; gfx->frame_res.pipeline_count = 0; gfx->frame_res.bindings_count = 0; gfx->stream_vb_offset = 0; gfx->stream_ib_offset = 0; pxl8_cmdbuf_reset(gfx->cmdbuf); pxl8_gfx_pass_desc pass_desc = { .color = { .texture = gfx_current_color(gfx), .load = PXL8_GFX_LOAD_CLEAR, .clear_value = 0 }, .depth = { .texture = gfx_current_depth(gfx), .load = PXL8_GFX_LOAD_CLEAR, .clear_value = 0xFFFF }, }; gfx->frame_pass = pxl8_create_pass(gfx->renderer, &pass_desc); pxl8_begin_pass(gfx->cmdbuf, gfx->frame_pass); } const pxl8_frustum* pxl8_3d_get_frustum(pxl8_gfx* gfx) { if (!gfx) return NULL; return &gfx->frustum; } 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; } void pxl8_3d_clear(pxl8_gfx* gfx, u8 color) { if (!gfx) return; pxl8_clear(gfx->renderer, gfx_current_color(gfx), color); } void pxl8_3d_clear_depth(pxl8_gfx* gfx) { if (!gfx) return; pxl8_cmdbuf_clear_depth(gfx->cmdbuf, gfx_current_depth(gfx)); } void pxl8_3d_draw_line(pxl8_gfx* gfx, pxl8_vec3 v0, pxl8_vec3 v1, u8 color) { if (!gfx) return; pxl8_vec4 c0 = pxl8_mat4_multiply_vec4(gfx->view_proj, (pxl8_vec4){v0.x, v0.y, v0.z, 1.0f}); pxl8_vec4 c1 = pxl8_mat4_multiply_vec4(gfx->view_proj, (pxl8_vec4){v1.x, v1.y, v1.z, 1.0f}); if (c0.w <= 0.0f && c1.w <= 0.0f) return; f32 hw = (f32)gfx->framebuffer_width * 0.5f; f32 hh = (f32)gfx->framebuffer_height * 0.5f; i32 x0 = (i32)(hw + c0.x / c0.w * hw); i32 y0 = (i32)(hh - c0.y / c0.w * hh); i32 x1 = (i32)(hw + c1.x / c1.w * hw); i32 y1 = (i32)(hh - c1.y / c1.w * hh); pxl8_draw_line(gfx->renderer, gfx_current_color(gfx), x0, y0, x1, y1, color); } void pxl8_3d_draw_mesh(pxl8_gfx* gfx, const pxl8_mesh* mesh, const pxl8_mat4* model, const pxl8_gfx_material* material) { if (!gfx || !mesh || !model || !material) return; if (!pxl8_gfx_handle_valid(gfx->frame_pass)) return; pxl8_frame_resources* res = &gfx->frame_res; bool is_wireframe = gfx->wireframe; const char* shader_name = "lit"; if (material->emissive || (!material->dynamic_lighting && !material->per_pixel)) { shader_name = "unlit"; } pxl8_gfx_pipeline_desc pipe_desc = { .blend = { .enabled = false, .src = PXL8_GFX_BLEND_ONE, .dst = PXL8_GFX_BLEND_ZERO, .alpha_test = false, .alpha_ref = 0, }, .depth = { .test = true, .write = true, .compare = PXL8_GFX_COMPARE_LESS }, .dither = material->dither, .double_sided = material->double_sided, .emissive = material->emissive, .rasterizer = { .cull = PXL8_GFX_CULL_BACK, .fill = is_wireframe ? PXL8_GFX_FILL_WIREFRAME : PXL8_GFX_FILL_SOLID }, .shader = pxl8_shader_registry_get(shader_name), }; switch (material->blend_mode) { case PXL8_BLEND_ALPHA_TEST: pipe_desc.blend.alpha_test = true; pipe_desc.blend.alpha_ref = material->alpha; break; case PXL8_BLEND_ALPHA: pipe_desc.blend.enabled = true; pipe_desc.blend.src = PXL8_GFX_BLEND_SRC_ALPHA; pipe_desc.blend.dst = PXL8_GFX_BLEND_INV_SRC_ALPHA; break; case PXL8_BLEND_ADDITIVE: pipe_desc.blend.enabled = true; pipe_desc.blend.src = PXL8_GFX_BLEND_ONE; pipe_desc.blend.dst = PXL8_GFX_BLEND_ONE; break; case PXL8_BLEND_OPAQUE: default: break; } pxl8_gfx_pipeline pipeline = pxl8_create_pipeline(gfx->renderer, &pipe_desc); if (res->pipeline_count < PXL8_MAX_FRAME_RESOURCES) { res->pipelines[res->pipeline_count++] = pipeline; } pxl8_gfx_bindings_desc bind_desc = { .atlas = gfx->atlas, .colormap = gfx->colormap, .palette = gfx->palette ? pxl8_palette_colors(gfx->palette) : NULL, .texture_id = material->texture_id, }; pxl8_gfx_bindings bindings = pxl8_create_bindings(gfx->renderer, &bind_desc); if (res->bindings_count < PXL8_MAX_FRAME_RESOURCES) { res->bindings[res->bindings_count++] = bindings; } u32 vb_size = mesh->vertex_count * sizeof(pxl8_vertex); u32 ib_size = mesh->index_count * sizeof(u16); u32 base_vertex = 0; u32 first_index = 0; pxl8_gfx_buffer vb, ib; bool use_stream = pxl8_gfx_handle_valid(gfx->stream_vb) && pxl8_gfx_handle_valid(gfx->stream_ib) && (gfx->stream_vb_offset + vb_size <= gfx->stream_vb_capacity) && (gfx->stream_ib_offset + ib_size <= gfx->stream_ib_capacity); if (use_stream) { pxl8_gfx_range vb_data = { .ptr = mesh->vertices, .size = vb_size }; pxl8_gfx_range ib_data = { .ptr = mesh->indices, .size = ib_size }; i32 vb_offset = pxl8_append_buffer(gfx->renderer, gfx->stream_vb, &vb_data); i32 ib_offset = pxl8_append_buffer(gfx->renderer, gfx->stream_ib, &ib_data); if (vb_offset >= 0 && ib_offset >= 0) { gfx->stream_vb_offset += vb_size; gfx->stream_ib_offset += ib_size; base_vertex = (u32)vb_offset / sizeof(pxl8_vertex); first_index = (u32)ib_offset / sizeof(u16); vb = gfx->stream_vb; ib = gfx->stream_ib; } else { use_stream = false; } } if (!use_stream) { pxl8_gfx_buffer_desc vb_desc = { .type = PXL8_GFX_BUFFER_VERTEX, .data = { .ptr = mesh->vertices, .size = vb_size }, }; vb = pxl8_create_buffer(gfx->renderer, &vb_desc); if (res->buffer_count < PXL8_MAX_FRAME_RESOURCES) { res->buffers[res->buffer_count++] = vb; } pxl8_gfx_buffer_desc ib_desc = { .type = PXL8_GFX_BUFFER_INDEX, .data = { .ptr = mesh->indices, .size = ib_size }, }; ib = pxl8_create_buffer(gfx->renderer, &ib_desc); if (res->buffer_count < PXL8_MAX_FRAME_RESOURCES) { res->buffers[res->buffer_count++] = ib; } } pxl8_gfx_cmd_draw_params draw_params = { .model = *model, .view = gfx->frame.view, .projection = gfx->frame.projection, .shader = gfx->frame.uniforms, }; pxl8_set_pipeline(gfx->cmdbuf, pipeline); pxl8_set_bindings(gfx->cmdbuf, bindings); pxl8_set_draw_params(gfx->cmdbuf, &draw_params); pxl8_draw(gfx->cmdbuf, vb, ib, first_index, mesh->index_count, base_vertex); } void pxl8_3d_end_frame(pxl8_gfx* gfx) { if (!gfx) { pxl8_error("end_frame: gfx is NULL"); return; } if (!pxl8_gfx_handle_valid(gfx->frame_pass)) { pxl8_error("end_frame: frame_pass invalid (id=%u)", gfx->frame_pass.id); return; } pxl8_end_pass(gfx->cmdbuf); pxl8_gfx_submit(gfx->renderer, gfx->cmdbuf); pxl8_frame_resources* res = &gfx->frame_res; for (u32 i = 0; i < res->buffer_count; i++) { pxl8_destroy_buffer(gfx->renderer, res->buffers[i]); } for (u32 i = 0; i < res->pipeline_count; i++) { pxl8_destroy_pipeline(gfx->renderer, res->pipelines[i]); } for (u32 i = 0; i < res->bindings_count; i++) { pxl8_destroy_bindings(gfx->renderer, res->bindings[i]); } for (u32 i = 0; i < res->texture_count; i++) { pxl8_destroy_texture(gfx->renderer, res->textures[i]); } pxl8_destroy_pass(gfx->renderer, gfx->frame_pass); gfx->frame_pass = (pxl8_gfx_pass){PXL8_GFX_INVALID_ID}; res->buffer_count = 0; res->pipeline_count = 0; res->bindings_count = 0; res->texture_count = 0; } u8* pxl8_3d_get_framebuffer(pxl8_gfx* gfx) { if (!gfx) return NULL; return (u8*)pxl8_texture_get_data(gfx->renderer, gfx_current_color(gfx)); } bool pxl8_gfx_push_target(pxl8_gfx* gfx) { if (!gfx || gfx->target_stack_depth >= PXL8_MAX_TARGET_STACK) return false; pxl8_gfx_texture_desc color_desc = { .width = (u32)gfx->framebuffer_width, .height = (u32)gfx->framebuffer_height, .format = PXL8_GFX_FORMAT_INDEXED8, .render_target = true, }; pxl8_gfx_texture new_color = pxl8_create_texture(gfx->renderer, &color_desc); pxl8_gfx_texture_desc depth_desc = { .width = (u32)gfx->framebuffer_width, .height = (u32)gfx->framebuffer_height, .format = PXL8_GFX_FORMAT_DEPTH16, .render_target = true, }; pxl8_gfx_texture new_depth = pxl8_create_texture(gfx->renderer, &depth_desc); gfx->target_stack[gfx->target_stack_depth] = (pxl8_target_entry){ .color = new_color, .depth = new_depth, }; gfx->target_stack_depth++; return true; } void pxl8_gfx_pop_target(pxl8_gfx* gfx) { if (!gfx || gfx->target_stack_depth <= 1) return; u32 top = gfx->target_stack_depth - 1; pxl8_target_entry* entry = &gfx->target_stack[top]; pxl8_target_entry* parent = &gfx->target_stack[top - 1]; gfx_composite_over(gfx, entry, parent); gfx->target_stack_depth--; pxl8_destroy_texture(gfx->renderer, entry->color); pxl8_destroy_texture(gfx->renderer, entry->depth); pxl8_destroy_texture(gfx->renderer, entry->light); } void pxl8_gfx_ensure_blend_tables(pxl8_gfx* gfx) { if (!gfx || !gfx->palette) return; if (!gfx->palette_cube) { gfx->palette_cube = pxl8_palette_cube_create(gfx->palette); } } void pxl8_gfx_blend_tables_update(pxl8_gfx* gfx) { if (!gfx || !gfx->palette) return; pxl8_gfx_ensure_blend_tables(gfx); if (gfx->palette_cube) { pxl8_palette_cube_rebuild(gfx->palette_cube, gfx->palette); } } void pxl8_gfx_colormap_update(pxl8_gfx* gfx) { if (!gfx || !gfx->palette || !gfx->colormap) return; u32* colors = pxl8_palette_colors(gfx->palette); if (colors) { pxl8_colormap_generate(gfx->colormap, colors); } } 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); } void pxl8_gfx_set_wireframe(pxl8_gfx* gfx, bool enabled) { if (gfx) gfx->wireframe = enabled; } bool pxl8_gfx_get_wireframe(const pxl8_gfx* gfx) { return gfx ? gfx->wireframe : false; } void pxl8_gfx_apply_effect(pxl8_gfx* gfx, pxl8_gfx_effect effect, const void* params, u32 count) { if (!gfx || !params || count == 0) return; if (effect == PXL8_GFX_EFFECT_GLOWS) { u8* fb = (u8*)pxl8_texture_get_data(gfx->renderer, gfx_current_color(gfx)); if (!fb || !gfx->colormap) return; u16* zb = (u16*)pxl8_texture_get_data(gfx->renderer, gfx_current_depth(gfx)); i32 w = gfx->framebuffer_width; i32 h = gfx->framebuffer_height; const pxl8_glow* glows = (const pxl8_glow*)params; for (u32 i = 0; i < count; i++) { const pxl8_glow* g = &glows[i]; i32 cx = g->x; i32 cy = g->y; i32 r = (i32)g->radius; if (r <= 0) continue; bool depth_test = g->depth > 0 && zb; f32 base_intensity = (f32)g->intensity / 255.0f; f32 inv_r = 1.0f / (f32)r; i32 x0 = cx - r; i32 y0 = cy - r; i32 x1 = cx + r; i32 y1 = cy + r; if (g->shape == PXL8_GLOW_SHAFT) { i32 sh = (i32)g->height; if (sh <= 0) sh = 1; y0 = cy - sh; y1 = cy + sh; } if (x0 < 0) x0 = 0; if (y0 < 0) y0 = 0; if (x1 >= w) x1 = w - 1; if (y1 >= h) y1 = h - 1; for (i32 py = y0; py <= y1; py++) { i32 dy = py - cy; for (i32 px = x0; px <= x1; px++) { i32 idx = py * w + px; if (depth_test && zb[idx] < g->depth) continue; u8 src = fb[idx]; if (src == PXL8_TRANSPARENT) continue; i32 dx = px - cx; f32 falloff; switch (g->shape) { case PXL8_GLOW_CIRCLE: { f32 dist_sq = (f32)(dx * dx + dy * dy); f32 norm = dist_sq * inv_r * inv_r; if (norm >= 1.0f) continue; falloff = 1.0f - norm; falloff *= falloff; break; } case PXL8_GLOW_DIAMOND: { f32 manhattan = (f32)(abs(dx) + abs(dy)); f32 norm = manhattan * inv_r; if (norm >= 1.0f) continue; falloff = 1.0f - norm; falloff *= falloff; break; } case PXL8_GLOW_SHAFT: { f32 abs_dx = (f32)abs(dx); if (abs_dx >= (f32)r) continue; f32 x_falloff = 1.0f - abs_dx * inv_r; i32 sh = (i32)g->height; if (sh <= 0) sh = 1; f32 abs_dy = (f32)abs(dy); f32 y_falloff = 1.0f - abs_dy / (f32)sh; falloff = x_falloff * y_falloff; falloff *= falloff; break; } default: continue; } f32 brightness = falloff * base_intensity; f32 raw = brightness * 255.0f; if (raw > 255.0f) raw = 255.0f; u8 light_u8 = (u8)raw; if (light_u8 < 4) continue; fb[idx] = pxl8_colormap_additive_dithered( gfx->colormap, src, g->color, light_u8, (u32)px, (u32)py ); } } } } } u8 pxl8_gfx_get_ambient(const pxl8_gfx* gfx) { return gfx ? gfx->frame.uniforms.ambient : 0; } const pxl8_gfx_stats* pxl8_gfx_get_stats(const pxl8_gfx* gfx) { return gfx ? pxl8_renderer_get_stats(gfx->renderer) : NULL; } void pxl8_gfx_update_stats(pxl8_gfx* gfx, f32 dt) { if (gfx) pxl8_renderer_update_stats(gfx->renderer, dt); }