#include "pxl8_sdl3.h" #include "pxl8_atlas.h" #include "pxl8_game.h" #include "pxl8_macros.h" #define SDL_MAIN_USE_CALLBACKS #include #include typedef struct pxl8_sdl3_context { SDL_Texture* atlas_texture; SDL_Texture* framebuffer_texture; SDL_Renderer* renderer; SDL_Window* window; u32* rgba_buffer; size_t rgba_buffer_size; u32 atlas_width; u32 atlas_height; } pxl8_sdl3_context; static void* sdl3_create(pxl8_color_mode mode, pxl8_resolution resolution, const char* title, i32 win_w, i32 win_h) { (void)mode; pxl8_sdl3_context* ctx = (pxl8_sdl3_context*)SDL_calloc(1, sizeof(*ctx)); if (!ctx) { pxl8_error("Failed to allocate SDL3 context"); return NULL; } i32 fb_w, fb_h; switch (resolution) { case PXL8_RESOLUTION_240x160: fb_w = 240; fb_h = 160; break; case PXL8_RESOLUTION_320x180: fb_w = 320; fb_h = 180; break; case PXL8_RESOLUTION_320x240: fb_w = 320; fb_h = 240; break; case PXL8_RESOLUTION_640x360: fb_w = 640; fb_h = 360; break; case PXL8_RESOLUTION_640x480: fb_w = 640; fb_h = 480; break; case PXL8_RESOLUTION_800x600: fb_w = 800; fb_h = 600; break; case PXL8_RESOLUTION_960x540: fb_w = 960; fb_h = 540; break; default: fb_w = 640; fb_h = 360; break; } ctx->window = SDL_CreateWindow(title, win_w, win_h, SDL_WINDOW_RESIZABLE); if (!ctx->window) { pxl8_error("Failed to create window: %s", SDL_GetError()); SDL_free(ctx); return NULL; } ctx->renderer = SDL_CreateRenderer(ctx->window, NULL); if (!ctx->renderer) { pxl8_error("Failed to create renderer: %s", SDL_GetError()); SDL_DestroyWindow(ctx->window); SDL_free(ctx); return NULL; } if (!SDL_SetRenderVSync(ctx->renderer, 1)) { pxl8_error("Failed to enable vsync: %s", SDL_GetError()); } SDL_SetRenderLogicalPresentation(ctx->renderer, fb_w, fb_h, SDL_LOGICAL_PRESENTATION_INTEGER_SCALE); ctx->framebuffer_texture = SDL_CreateTexture(ctx->renderer, SDL_PIXELFORMAT_RGBA32, SDL_TEXTUREACCESS_STREAMING, fb_w, fb_h); if (!ctx->framebuffer_texture) { pxl8_error("Failed to create framebuffer texture: %s", SDL_GetError()); SDL_DestroyRenderer(ctx->renderer); SDL_DestroyWindow(ctx->window); SDL_free(ctx); return NULL; } SDL_SetTextureScaleMode(ctx->framebuffer_texture, SDL_SCALEMODE_NEAREST); ctx->rgba_buffer = NULL; ctx->rgba_buffer_size = 0; return ctx; } static void sdl3_destroy(void* platform_data) { if (!platform_data) return; pxl8_sdl3_context* ctx = (pxl8_sdl3_context*)platform_data; if (ctx->rgba_buffer) { SDL_free(ctx->rgba_buffer); } if (ctx->atlas_texture) { SDL_DestroyTexture(ctx->atlas_texture); } if (ctx->framebuffer_texture) { SDL_DestroyTexture(ctx->framebuffer_texture); } if (ctx->renderer) { SDL_DestroyRenderer(ctx->renderer); } if (ctx->window) { SDL_DestroyWindow(ctx->window); } SDL_free(ctx); } static u64 sdl3_get_ticks(void) { return SDL_GetTicksNS(); } static void sdl3_present(void* platform_data) { if (!platform_data) return; pxl8_sdl3_context* ctx = (pxl8_sdl3_context*)platform_data; 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); } static void sdl3_upload_framebuffer(void* platform_data, const u8* fb, i32 w, i32 h, const u32* palette, pxl8_color_mode mode) { if (!platform_data || !fb) return; pxl8_sdl3_context* ctx = (pxl8_sdl3_context*)platform_data; if (mode == PXL8_COLOR_MODE_HICOLOR) { SDL_UpdateTexture(ctx->framebuffer_texture, NULL, fb, w * 4); } else { size_t needed_size = w * h; if (ctx->rgba_buffer_size < needed_size) { ctx->rgba_buffer = (u32*)SDL_realloc(ctx->rgba_buffer, needed_size * 4); ctx->rgba_buffer_size = needed_size; } if (!ctx->rgba_buffer) return; u32 palette_size; switch (mode) { case PXL8_COLOR_MODE_FAMI: palette_size = 64; break; case PXL8_COLOR_MODE_MEGA: palette_size = 512; break; case PXL8_COLOR_MODE_GBA: palette_size = 32768; break; case PXL8_COLOR_MODE_SNES: palette_size = 32768; break; default: palette_size = 256; break; } for (i32 i = 0; i < w * h; i++) { u8 index = fb[i]; ctx->rgba_buffer[i] = (index < palette_size) ? palette[index] : 0xFF000000; } SDL_UpdateTexture(ctx->framebuffer_texture, NULL, ctx->rgba_buffer, w * 4); } } static void sdl3_upload_atlas(void* platform_data, const pxl8_atlas* atlas, const u32* palette, pxl8_color_mode mode) { if (!platform_data || !atlas) return; pxl8_sdl3_context* ctx = (pxl8_sdl3_context*)platform_data; if (!pxl8_atlas_is_dirty(atlas)) return; u32 atlas_w = pxl8_atlas_get_width(atlas); u32 atlas_h = pxl8_atlas_get_height(atlas); const u8* atlas_pixels = pxl8_atlas_get_pixels(atlas); if (!ctx->atlas_texture || ctx->atlas_width != atlas_w || ctx->atlas_height != atlas_h) { if (ctx->atlas_texture) { SDL_DestroyTexture(ctx->atlas_texture); } ctx->atlas_texture = SDL_CreateTexture( ctx->renderer, SDL_PIXELFORMAT_RGBA32, SDL_TEXTUREACCESS_STREAMING, atlas_w, atlas_h ); if (!ctx->atlas_texture) { pxl8_error("Failed to create atlas texture: %s", SDL_GetError()); return; } SDL_SetTextureScaleMode(ctx->atlas_texture, SDL_SCALEMODE_NEAREST); SDL_SetTextureBlendMode(ctx->atlas_texture, SDL_BLENDMODE_BLEND); ctx->atlas_width = atlas_w; ctx->atlas_height = atlas_h; } if (mode == PXL8_COLOR_MODE_HICOLOR) { SDL_UpdateTexture(ctx->atlas_texture, NULL, atlas_pixels, atlas_w * 4); } else { size_t needed_size = atlas_w * atlas_h; if (ctx->rgba_buffer_size < needed_size) { ctx->rgba_buffer = (u32*)SDL_realloc(ctx->rgba_buffer, needed_size * 4); ctx->rgba_buffer_size = needed_size; } if (!ctx->rgba_buffer) return; u32 palette_size; switch (mode) { case PXL8_COLOR_MODE_FAMI: palette_size = 64; break; case PXL8_COLOR_MODE_MEGA: palette_size = 512; break; case PXL8_COLOR_MODE_GBA: palette_size = 32768; break; case PXL8_COLOR_MODE_SNES: palette_size = 32768; break; default: palette_size = 256; break; } for (u32 i = 0; i < atlas_w * atlas_h; i++) { u8 index = atlas_pixels[i]; ctx->rgba_buffer[i] = (index < palette_size) ? palette[index] : 0x00000000; } SDL_UpdateTexture(ctx->atlas_texture, NULL, ctx->rgba_buffer, atlas_w * 4); } } SDL_AppResult SDL_AppInit(void** appstate, int argc, char* argv[]) { if (!SDL_Init(SDL_INIT_VIDEO | SDL_INIT_EVENTS)) { pxl8_error("SDL_Init failed: %s", SDL_GetError()); return SDL_APP_FAILURE; } pxl8_game* game = (pxl8_game*)SDL_calloc(1, sizeof(pxl8_game)); if (!game) { pxl8_error("Failed to allocate game instance"); SDL_Quit(); return SDL_APP_FAILURE; } game->hal = &pxl8_hal_sdl3; pxl8_game_result result = pxl8_init(game, argc, argv); if (result != PXL8_GAME_CONTINUE) { SDL_free(game); SDL_Quit(); return SDL_APP_FAILURE; } *appstate = game; return SDL_APP_CONTINUE; } SDL_AppResult SDL_AppIterate(void* appstate) { pxl8_game* game = (pxl8_game*)appstate; if (!game) { return SDL_APP_FAILURE; } pxl8_game_result update_result = pxl8_update(game); if (update_result == PXL8_GAME_FAILURE) { return SDL_APP_FAILURE; } if (update_result == PXL8_GAME_SUCCESS) { return SDL_APP_SUCCESS; } pxl8_game_result frame_result = pxl8_frame(game); if (frame_result == PXL8_GAME_FAILURE) { return SDL_APP_FAILURE; } return (frame_result == PXL8_GAME_SUCCESS) ? SDL_APP_SUCCESS : SDL_APP_CONTINUE; } SDL_AppResult SDL_AppEvent(void* appstate, SDL_Event* event) { pxl8_game* game = (pxl8_game*)appstate; if (!game) { return SDL_APP_CONTINUE; } switch (event->type) { case SDL_EVENT_QUIT: game->running = false; break; case SDL_EVENT_KEY_DOWN: { #ifdef DEBUG if (event->key.key == SDLK_ESCAPE) { game->running = false; } #endif SDL_Scancode scancode = event->key.scancode; if (scancode < 256) { if (!game->input.keys_down[scancode]) { game->input.keys_pressed[scancode] = true; } game->input.keys_down[scancode] = true; game->input.keys_released[scancode] = false; } break; } case SDL_EVENT_KEY_UP: { SDL_Scancode scancode = event->key.scancode; if (scancode < 256) { game->input.keys_down[scancode] = false; game->input.keys_pressed[scancode] = false; game->input.keys_released[scancode] = true; } break; } case SDL_EVENT_MOUSE_BUTTON_DOWN: { u8 button = event->button.button - 1; if (button < 3) { if (!game->input.mouse_buttons_down[button]) { game->input.mouse_buttons_pressed[button] = true; } game->input.mouse_buttons_down[button] = true; game->input.mouse_buttons_released[button] = false; } break; } case SDL_EVENT_MOUSE_BUTTON_UP: { u8 button = event->button.button - 1; if (button < 3) { game->input.mouse_buttons_down[button] = false; game->input.mouse_buttons_pressed[button] = false; game->input.mouse_buttons_released[button] = true; } break; } case SDL_EVENT_MOUSE_MOTION: { if (!game->gfx) break; i32 window_mouse_x = (i32)event->motion.x; i32 window_mouse_y = (i32)event->motion.y; SDL_Window* window = SDL_GetWindowFromID(event->motion.windowID); if (!window) break; i32 window_width, window_height; SDL_GetWindowSize(window, &window_width, &window_height); i32 render_width, render_height; pxl8_gfx_get_resolution_dimensions(game->resolution, &render_width, &render_height); pxl8_bounds window_bounds = {0, 0, window_width, window_height}; pxl8_viewport vp = pxl8_gfx_viewport(window_bounds, render_width, render_height); game->input.mouse_x = (i32)((window_mouse_x - vp.offset_x) / vp.scale); game->input.mouse_y = (i32)((window_mouse_y - vp.offset_y) / vp.scale); break; } case SDL_EVENT_MOUSE_WHEEL: { game->input.mouse_wheel_x = (i32)event->wheel.x; game->input.mouse_wheel_y = (i32)event->wheel.y; break; } } return SDL_APP_CONTINUE; } void SDL_AppQuit(void* appstate, SDL_AppResult result) { (void)result; pxl8_game* game = (pxl8_game*)appstate; if (game) { pxl8_quit(game); SDL_free(game); } SDL_Quit(); } const pxl8_hal pxl8_hal_sdl3 = { .create = sdl3_create, .destroy = sdl3_destroy, .get_ticks = sdl3_get_ticks, .present = sdl3_present, .upload_atlas = sdl3_upload_atlas, .upload_framebuffer = sdl3_upload_framebuffer, };