From a324d68b170931844d9a2922786ce485f137feda Mon Sep 17 00:00:00 2001 From: offtkp Date: Tue, 20 Feb 2024 10:42:56 +0200 Subject: [PATCH] Refactor to use texture atlas for achievements --- src/main.c | 81 +++++--------- src/retro_achievements.cpp | 210 +++++++++++++++++++++++++++++++++---- src/retro_achievements.h | 19 +++- 3 files changed, 233 insertions(+), 77 deletions(-) diff --git a/src/main.c b/src/main.c index a159571fd..f5c0ad0e0 100644 --- a/src/main.c +++ b/src/main.c @@ -518,18 +518,18 @@ typedef struct se_ra_tracker_node{ }se_ra_tracker_node_t; typedef struct se_ra_challenge_indicator_node{ uint32_t id; - sg_image image; + ra_image image; struct se_ra_challenge_indicator_node* next; } se_ra_challenge_indicator_node_t; typedef struct{ char username[256]; char password[256]; - sg_image image; - sg_image** achievement_images; + ra_image image; + ra_image** achievement_images; // TODO: make widgets that use these lists and progress indicator se_ra_tracker_node_t* tracker_list; se_ra_challenge_indicator_node_t* challenge_indicator_list; - sg_image progress_indicator_image; + ra_image progress_indicator_image; bool progress_indicator_shown; char measured_progress[24]; mutex_t mutex; // for synchronization between ui thread and threads opened by http requests @@ -1733,41 +1733,14 @@ static uint32_t se_ra_read_memory_callback(uint32_t address, uint8_t* buffer, ui } return 0; } -static void se_ra_load_image_callback(const uint8_t* pixel_data, size_t image_size, int width, int height, void* user_data){ - sg_image* achievement_image=(sg_image*)user_data; - if(pixel_data){ - sg_image_data im_data={0}; - im_data.subimage[0][0].ptr = pixel_data; - im_data.subimage[0][0].size = width*height*4; - sg_image_desc desc={ - .type= SG_IMAGETYPE_2D, - .render_target= false, - .width= width, - .height= height, - .num_slices= 1, - .num_mipmaps= 1, - .usage= SG_USAGE_IMMUTABLE, - .pixel_format= SG_PIXELFORMAT_RGBA8, - .sample_count= 1, - .min_filter= SG_FILTER_LINEAR, - .mag_filter= SG_FILTER_LINEAR, - .wrap_u= SG_WRAP_CLAMP_TO_EDGE, - .wrap_v= SG_WRAP_CLAMP_TO_EDGE, - .wrap_w= SG_WRAP_CLAMP_TO_EDGE, - .border_color= SG_BORDERCOLOR_OPAQUE_BLACK, - .max_anisotropy= 1, - .min_lod= 0.0f, - .max_lod= 1e9f, - .data= im_data - }; - *achievement_image = sg_make_image(&desc); - }else{ - printf("[rcheevos]: failed to load game image\n"); - } +static void se_ra_load_image_callback(ra_image image, void* user_data){ + // TODO: if this works, remove the callback and instead of user_data pass ra_image* + ra_image* destination = (ra_image*)user_data; + *destination = image; } static void se_ra_game_cleanup(){ if(ra_info.image.id != SG_INVALID_ID){ - se_free_image_deferred(ra_info.image); + sg_destroy_image((sg_image){ra_info.image.id}); ra_info.image.id = SG_INVALID_ID; } rc_client_achievement_list_t* list = ra_get_achievements(); @@ -1776,11 +1749,6 @@ static void se_ra_game_cleanup(){ { if(ra_info.achievement_images[i] != NULL) { - for (int j = 0; j < list->buckets[i].num_achievements; j++) - { - if(ra_info.achievement_images[i][j].id != SG_INVALID_ID) - se_free_image_deferred(ra_info.achievement_images[i][j]); - } free(ra_info.achievement_images[i]); } } @@ -1822,12 +1790,12 @@ static void se_ra_load_game_callback(int result, const char* error_message, rc_c mutex_lock(ra_info.mutex); ra_invalidate_achievements(); rc_client_achievement_list_t* list = ra_get_achievements(); - ra_info.achievement_images = (sg_image**)malloc(sizeof(sg_image*)*list->num_buckets); + ra_info.achievement_images = (ra_image**)malloc(sizeof(ra_image*)*list->num_buckets); for (int i = 0; i < list->num_buckets; i++) { uint32_t num_achievements=list->buckets[i].num_achievements; - ra_info.achievement_images[i] = (sg_image*)malloc(sizeof(sg_image)*num_achievements); - memset(ra_info.achievement_images[i], 0, sizeof(sg_image)*num_achievements); + ra_info.achievement_images[i] = (ra_image*)malloc(sizeof(ra_image)*num_achievements); + memset(ra_info.achievement_images[i], 0, sizeof(ra_image)*num_achievements); for (int j = 0; j < num_achievements; j++) { char url[512]; @@ -4663,7 +4631,7 @@ bool se_selectable_with_box(const char * first_label, const char* second_label, return clicked; } -void se_boxed_image_dual_label(const char * first_label, const char* second_label, const char* box, sg_image* image, int reduce_width){ +void se_boxed_image_dual_label(const char * first_label, const char* second_label, const char* box, sg_image image, int reduce_width, ImVec2 uv0, ImVec2 uv1){ ImVec2 win_min,win_sz,win_max; win_min.x=0; win_min.y=0; // content boundaries min (roughly (0,0)-Scroll), in window coordinates @@ -4699,7 +4667,7 @@ void se_boxed_image_dual_label(const char * first_label, const char* second_labe igSetCursorPosY(igGetCursorPosY()-5); se_text_disabled(second_label); igSetCursorPos(curr_pos); - if(image)igImageButton((ImTextureID)(intptr_t)image->id,(ImVec2){box_w,box_h},(ImVec2){0,0},(ImVec2){1,1},0,(ImVec4){1,1,1,1},(ImVec4){1,1,1,1}); + if(image.id != SG_INVALID_ID)igImageButton((ImTextureID)(intptr_t)image.id,(ImVec2){box_w,box_h},uv0,uv1,0,(ImVec4){1,1,1,1},(ImVec4){1,1,1,1}); else se_text_centered_in_box((ImVec2){0,0}, (ImVec2){box_w,box_h},box); igDummy((ImVec2){1,1}); igSetCursorPos(next_pos); @@ -6367,16 +6335,16 @@ void se_draw_menu_panel(){ }else { const rc_client_game_t* game = rc_client_get_game_info(ra_get_client()); ImVec2 pos; - sg_image * image = NULL; + sg_image image; const char* play_string = "No Game Loaded"; char line1[256]; char line2[256]; snprintf(line1,256,se_localize_and_cache("Logged in as %s"),user->display_name); if(game){ - if(ra_info.image.id!=SG_INVALID_ID)image=&ra_info.image; + image.id=ra_info.image.id; snprintf(line2,256,se_localize_and_cache("Playing: %s"),game->title); }else snprintf(line2,256,"%s",se_localize_and_cache("No Game Loaded")); - se_boxed_image_dual_label(line1,line2, ICON_FK_TROPHY, image, 0); + se_boxed_image_dual_label(line1,line2, ICON_FK_TROPHY, image, 0, (ImVec2){0,0}, (ImVec2){1,1}); if(se_button(ICON_FK_SIGN_OUT " Logout", (ImVec2){0,0})){ char login_info_path[SB_FILE_PATH_SIZE]; snprintf(login_info_path,SB_FILE_PATH_SIZE,"%sra_token.txt",se_get_pref_path()); @@ -6389,14 +6357,17 @@ void se_draw_menu_panel(){ for (int i = 0; i < list->num_buckets; i++){ se_text(ICON_FK_LOCK " %s",list->buckets[i].label); for (int j = 0; j < list->buckets[i].num_achievements; j++){ - sg_image * image = NULL; - // TODO: some games need a lot of images for achievements - instead of creating all sg_images at once, we should create them on demand - // or at least increase the sg_images limit. - if(ra_info.achievement_images && ra_info.achievement_images[i] && ra_info.achievement_images[i][j].id!=SG_INVALID_ID){ - image = &ra_info.achievement_images[i][j]; + sg_image image; + ImVec2 uv0, uv1; + if(ra_info.achievement_images && ra_info.achievement_images[i]){ + image.id = ra_info.achievement_images[i][j].id; + float size = ra_get_atlas_size(); + ra_image* ra_image = &ra_info.achievement_images[i][j]; + uv0 = (ImVec2){ (float)ra_image->offset_x / size, (float)ra_image->offset_y / size }; + uv1 = (ImVec2){ (float)(ra_image->offset_x + ra_image->width) / size, (float)(ra_image->offset_y + ra_image->height) / size }; } se_boxed_image_dual_label(list->buckets[i].achievements[j]->title, - list->buckets[i].achievements[j]->description, ICON_FK_SPINNER, image, 0); + list->buckets[i].achievements[j]->description, ICON_FK_SPINNER, image, 0, uv0, uv1); } } } diff --git a/src/retro_achievements.cpp b/src/retro_achievements.cpp index 80844f240..0043f3797 100644 --- a/src/retro_achievements.cpp +++ b/src/retro_achievements.cpp @@ -1,3 +1,4 @@ +#include extern "C" { #include "retro_achievements.h" } @@ -12,21 +13,24 @@ extern "C" { #include #define STBI_ONLY_PNG #include "stb_image.h" +#include "sokol_gfx.h" -rc_client_t* ra_client = nullptr; +static rc_client_t* ra_client = nullptr; static rc_client_achievement_list_t* achievements = nullptr; +uint32_t achievement_count = 0; static bool pending_login = false; -struct Image -{ - int width = 0, height = 0; - std::vector pixel_data; -}; - -std::unordered_map image_cache; -std::mutex image_cache_mutex; -std::vector> pending_callbacks; -std::mutex achievements_list_mutex; +static sg_image atlas; +static std::vector atlas_data; +static const int atlas_tile_size = 96; // all images in the atlas will be 96x96 +static const int atlas_spacing = 4; +static int atlas_pixel_stride = 0; +static int atlas_offset_x = 0; // to keep track of where next tile needs to be placed, in pixels +static int atlas_offset_y = 0; +static std::unordered_map image_cache; +static std::mutex image_cache_mutex; +static std::vector> pending_callbacks; +static std::mutex achievements_list_mutex; static void server_callback(const rc_api_request_t* request, rc_client_server_callback_t callback, void* callback_data, rc_client_t* client) @@ -77,11 +81,23 @@ static void log_message(const char* message, const rc_client_t* client) printf("[rcheevos]: %s\n", message); } +static void update_achievement_count() { + if (achievement_count == 0) { + if (achievements) { + for (int i = 0; i < achievements->num_buckets; i++) { + achievement_count += achievements->buckets[i].num_achievements; + } + } + } else { + return; + } +} + void ra_initialize_client(rc_client_read_memory_func_t memory_read_func) { if(ra_client) { - printf("[rcheevos]: client already initialized!\n"); + printf("[rcheevos]: client already initialized\n"); } else { @@ -97,6 +113,141 @@ void ra_initialize_client(rc_client_read_memory_func_t memory_read_func) } } +void ra_create_atlas() +{ + update_achievement_count(); + if (achievement_count == 0) { + printf("[rcheevos]: could not update achievement count\n"); + return; + } + + // Cleanup old atlas if it exists + if (atlas.id != SG_INVALID_ID) + { + sg_destroy_image(atlas); + atlas_data.clear(); + } + + uint64_t pixel_count = (atlas_tile_size + atlas_spacing) * (atlas_tile_size + atlas_spacing) * achievement_count; + + // 2048x2048 is more than enough to fit more than 400 achievements + spacing + // if more is needed, assume that something is wrong with the achievement count + if (pixel_count > 2048 * 2048) { + printf("[rcheevos]: error, texture size too big when creating atlas\n"); + return; + } + + // Now we need to find a square texture that can fit all the images + // We'll start with a 256x256 texture and double the size until it fits + uint64_t texture_size = 256; + while (texture_size * texture_size < pixel_count) + { + texture_size *= 2; + } + + atlas_pixel_stride = texture_size; + atlas_offset_x = 0; + atlas_offset_y = 0; + + // Create the texture + atlas_data.resize(texture_size * texture_size * 4); + sg_image_data im_data = {0}; + im_data.subimage[0][0].ptr = atlas_data.data(); + im_data.subimage[0][0].size = atlas_data.size(); + sg_image_desc desc={ + .type= SG_IMAGETYPE_2D, + .render_target= false, + .width= atlas_pixel_stride, + .height= atlas_pixel_stride, + .num_slices= 1, + .num_mipmaps= 1, + .usage= SG_USAGE_DYNAMIC, + .pixel_format= SG_PIXELFORMAT_RGBA8, + .sample_count= 1, + .min_filter= SG_FILTER_LINEAR, + .mag_filter= SG_FILTER_LINEAR, + .wrap_u= SG_WRAP_CLAMP_TO_EDGE, + .wrap_v= SG_WRAP_CLAMP_TO_EDGE, + .wrap_w= SG_WRAP_CLAMP_TO_EDGE, + .border_color= SG_BORDERCOLOR_TRANSPARENT_BLACK, + .max_anisotropy= 1, + .min_lod= 0.0f, + .max_lod= 1e9f, + .data= im_data, + }; + + atlas = sg_make_image(&desc); +} + +void ra_add_image_atlas(ra_image image, get_image_callback_t callback, void* user_data) { + if (atlas.id == SG_INVALID_ID) { + ra_create_atlas(); + + if (atlas.id == SG_INVALID_ID) { + printf("[rcheevos]: failed to create atlas\n"); + return; + } + } + + image.id = atlas.id; + int offset_x = image.offset_x; + int offset_y = image.offset_y; + + // Copy tile to atlas + for (int y = 0; y < atlas_tile_size; y++) { + for (int x = 0; x < atlas_tile_size; x++) { + uint32_t atlas_offset = ((offset_x + x) * 4) + (((offset_y + y) * atlas_pixel_stride) * 4); + atlas_data[atlas_offset + 0] = image.pixel_data[x + y * atlas_tile_size + 0]; + atlas_data[atlas_offset + 1] = image.pixel_data[x + y * atlas_tile_size + 1]; + atlas_data[atlas_offset + 2] = image.pixel_data[x + y * atlas_tile_size + 2]; + atlas_data[atlas_offset + 3] = image.pixel_data[x + y * atlas_tile_size + 3]; + } + } + stbi_image_free(image.pixel_data); + image.pixel_data = nullptr; + + // Update texture + sg_image_data data = {0}; + data.subimage[0][0].ptr = atlas_data.data(); + data.subimage[0][0].size = atlas_data.size(); + sg_update_image(atlas, data); + + callback(image, user_data); +} + +void ra_add_image(ra_image image, get_image_callback_t callback, void* user_data) { + sg_image_data im_data = {0}; + im_data.subimage[0][0].ptr = image.pixel_data; + im_data.subimage[0][0].size = image.width * image.height * 4; + sg_image_desc desc={ + .type= SG_IMAGETYPE_2D, + .render_target= false, + .width= image.width, + .height= image.height, + .num_slices= 1, + .num_mipmaps= 1, + .usage= SG_USAGE_IMMUTABLE, + .pixel_format= SG_PIXELFORMAT_RGBA8, + .sample_count= 1, + .min_filter= SG_FILTER_LINEAR, + .mag_filter= SG_FILTER_LINEAR, + .wrap_u= SG_WRAP_CLAMP_TO_EDGE, + .wrap_v= SG_WRAP_CLAMP_TO_EDGE, + .wrap_w= SG_WRAP_CLAMP_TO_EDGE, + .border_color= SG_BORDERCOLOR_TRANSPARENT_BLACK, + .max_anisotropy= 1, + .min_lod= 0.0f, + .max_lod= 1e9f, + .data= im_data, + }; + + image.id = sg_make_image(&desc).id; + stbi_image_free(image.pixel_data); + image.pixel_data = nullptr; + + callback(image, user_data); +} + void ra_unset_pending_login() { pending_login = false; @@ -133,7 +284,7 @@ void ra_get_image(const char* url, get_image_callback_t callback, void* user_dat { auto& image = image_cache[url]; pending_callbacks.push_back([callback, user_data, &image](){ - callback(image.pixel_data.data(), image.pixel_data.size(), image.width, image.height, user_data); + callback(image, user_data); }); return; } @@ -149,12 +300,28 @@ void ra_get_image(const char* url, get_image_callback_t callback, void* user_dat response.body_length = result.size(); response.http_status_code = 200; auto& image = image_cache[url_str]; - uint8_t* pixel_data = stbi_load_from_memory((const uint8_t*)response.body, response.body_length, &image.width, &image.height, NULL, 4); - image.pixel_data.resize(image.width * image.height * 4); - memcpy(image.pixel_data.data(), pixel_data, image.pixel_data.size()); - stbi_image_free(pixel_data); - pending_callbacks.push_back([callback, user_data, &image](){ - callback(image.pixel_data.data(), image.pixel_data.size(), image.width, image.height, user_data); + image.pixel_data = stbi_load_from_memory((const uint8_t*)response.body, response.body_length, &image.width, &image.height, NULL, 4); + + bool is_atlas_tile = image.width == atlas_tile_size && image.height == atlas_tile_size; + if (is_atlas_tile) { + image.offset_x = atlas_offset_x; + image.offset_y = atlas_offset_y; + + // Prepare offsets for next tile + atlas_offset_x += atlas_tile_size + atlas_spacing; + if (atlas_offset_x + atlas_tile_size > atlas_pixel_stride) { + atlas_offset_x = 0; + atlas_offset_y += atlas_tile_size + atlas_spacing; + } + } + + // These need to be called from the GL thread, so they are queued here + pending_callbacks.push_back([callback, user_data, &image, is_atlas_tile](){ + if (is_atlas_tile) { + ra_add_image_atlas(image, callback, user_data); + } else { + ra_add_image(image, callback, user_data); + } }); }); #ifndef EMSCRIPTEN @@ -195,9 +362,14 @@ void ra_invalidate_achievements() { if(achievements) { + achievement_count = 0; rc_client_destroy_achievement_list(achievements); } achievements = rc_client_create_achievement_list(ra_client, RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE_AND_UNOFFICIAL, RC_CLIENT_ACHIEVEMENT_LIST_GROUPING_PROGRESS); +} + +int ra_get_atlas_size() { + return atlas_pixel_stride; } \ No newline at end of file diff --git a/src/retro_achievements.h b/src/retro_achievements.h index eaef90d50..8eeebedc5 100644 --- a/src/retro_achievements.h +++ b/src/retro_achievements.h @@ -8,18 +8,31 @@ struct rc_client_t; typedef struct rc_client_t rc_client_t; +// Wrapper around sg_image for the image cache, also storing width/height and offsets. +// In many images, sg_image is unique and offsets are 0, but for achievement images, +// because there's usually 100+, a texture atlas is used and sg_image here always points +// to that atlas, and offsets point to the tile we need. +typedef struct { + uint32_t id; + uint8_t* pixel_data; + int offset_x, offset_y; + int width, height; +} ra_image; + typedef uint32_t (*rc_client_read_memory_func_t)(uint32_t address, uint8_t* buffer, uint32_t num_bytes, rc_client_t* client); -typedef void (*rc_client_callback_t)(int result, const char* error_message, rc_client_t* client, void* userdata); -typedef void(*get_image_callback_t)(const uint8_t* buffer, size_t buffer_size, int width, int height, void* userdata); +typedef void (*rc_client_callback_t)(int result, const char* error_message, rc_client_t* client, void* user_data); +typedef void(*get_image_callback_t)(ra_image image, void* user_data); void ra_initialize_client(rc_client_read_memory_func_t memory_read_func); +void ra_create_atlas(int achievement_count); void ra_login(const char* username, const char* password, bool is_token, rc_client_callback_t login_callback); void ra_load_game(const uint8_t* rom, size_t rom_size, int console_id, rc_client_callback_t callback); -void ra_get_image(const char* url, get_image_callback_t callback, void* userdata); +void ra_get_image(const char* url, get_image_callback_t callback, void* user_data); void ra_run_pending_callbacks(); bool ra_pending_login(); void ra_unset_pending_login(); rc_client_t* ra_get_client(); rc_client_achievement_list_t* ra_get_achievements(); void ra_invalidate_achievements(); +int ra_get_atlas_size(); #endif \ No newline at end of file