From 2e9606b0a90e50fafaa50ce01cd1515f79341d19 Mon Sep 17 00:00:00 2001 From: offtkp Date: Thu, 15 Aug 2024 03:04:48 +0300 Subject: [PATCH 1/8] Atlas stuff --- CMakeLists.txt | 2 +- src/atlas.cpp | 83 ++++++++++++++++++++++++++++++++++++++ src/atlas.h | 53 ++++++++++++++++++++++++ src/retro_achievements.cpp | 7 ---- src/retro_achievements.h | 8 +--- 5 files changed, 138 insertions(+), 15 deletions(-) create mode 100644 src/atlas.cpp create mode 100644 src/atlas.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 49f1fbfb8..e2f495094 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -292,7 +292,7 @@ if(ENABLE_HTTP_CONTROL_SERVER) set(SKYEMU_SRC ${SKYEMU_SRC} src/http_control_server.cpp) endif() -set(SKYEMU_SRC ${SKYEMU_SRC} src/cloud.cpp src/https.cpp) +set(SKYEMU_SRC ${SKYEMU_SRC} src/cloud.cpp src/https.cpp src/atlas.cpp) if(UNICODE_GUI) set(SKYEMU_SRC ${SKYEMU_SRC} src/utf8proc/utf8proc.c) diff --git a/src/atlas.cpp b/src/atlas.cpp new file mode 100644 index 000000000..05d90739b --- /dev/null +++ b/src/atlas.cpp @@ -0,0 +1,83 @@ +#include "atlas.h" +#include "sokol_gfx.h" + +#include +#include +#include + +const int atlas_spacing = 4; // leaving some space between tiles to avoid bleeding + +struct tile_size_t { + uint16_t x, y; +}; + +struct cached_image_t { + uint8_t* data; // always RGBA + int width; + int height; +}; + +// Atlases are always square and power of two +// This always starts as a single tile image, but if a new tile needs to be +// added, it's resized to the next power of two +struct atlas_t { + atlas_t(uint32_t tile_width, uint32_t tile_height) + : tile_width(tile_width), tile_height(tile_height) + { + image.id = SG_INVALID_ID; + } + + ~atlas_t() = default; + atlas_t(const atlas_t&) = delete; + atlas_t& operator=(const atlas_t&) = delete; + atlas_t(atlas_t&&) = default; + atlas_t& operator=(atlas_t&&) = default; + + std::vector data; // we construct the atlas here before uploading it to the GPU + sg_image image = {}; + int pixel_stride = 0; + int offset_x = 0, + offset_y = 0; // to keep track of where next tile needs to be placed, in pixels + int tile_width, tile_height; + bool resized = false; + bool dirty = false; // needs the data to be reuploaded to the GPU + + void add_image(cached_image_t* image) + { + dirty = true; + + uint32_t tile_offset_x = offset_x; + uint32_t tile_offset_y = offset_y; + + offset_x += tile_width + atlas_spacing; + if (offset_x + tile_width > pixel_stride) + { + offset_x = 0; + offset_y += tile_width + atlas_spacing; + } + + assert(image->width == tile_width); + + for (int y = 0; y < tile_height; y++) + { + for (int x = 0; x < tile_width; x++) + { + uint32_t atlas_offset = + ((tile_offset_x + x) * 4) + (((tile_offset_y + y) * pixel_stride) * 4); + uint32_t tile_offset = x * 4 + (y * 4 * tile_width); + + assert(atlas_offset + 3 < data.size()); + assert(tile_offset + 3 < tile_width * tile_height * 4); + + data[atlas_offset + 0] = image->data[tile_offset + 0]; + data[atlas_offset + 1] = image->data[tile_offset + 1]; + data[atlas_offset + 2] = image->data[tile_offset + 2]; + data[atlas_offset + 3] = image->data[tile_offset + 3]; + } + } + } +}; + +struct atlas_map_t { + std::unordered_map atlases; +}; \ No newline at end of file diff --git a/src/atlas.h b/src/atlas.h new file mode 100644 index 000000000..38eacd728 --- /dev/null +++ b/src/atlas.h @@ -0,0 +1,53 @@ +#ifndef SE_ATLAS_H +#define SE_ATLAS_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include +#include + +// A tile in an atlas. atlas_id is the sg_image id of the atlas itself +// x1, y1, x2, y2 are the coordinates of the tile in the atlas +typedef struct { + uint32_t atlas_id; + float x1, y1; + float x2, y2; +} atlas_tile_t; + +typedef uint32_t atlas_tile_id; + +// An atlas map is a map of atlases, which themselves are images with multiple tiles +typedef struct atlas_map_t atlas_map_t; + +atlas_map_t* atlas_create_map(); + +void atlas_destroy_map(atlas_map_t* map); + +// Downloads an image from the url and adds it to the atlas map +atlas_tile_id atlas_add_tile_from_url(atlas_map_t* map, const char* url); + +// Loads an image from a path and adds it to the atlas map +atlas_tile_id atlas_add_tile_from_path(atlas_map_t* map, const char* path, bool use_separate_thread); + +// Checks if a tile is ready to be used +bool atlas_has_tile(atlas_map_t* map, atlas_tile_id id); + +// Must be called after atlas_has_tile ensures the tile is ready +atlas_tile_t atlas_get_tile(atlas_map_t* map, atlas_tile_id id); + +// Called from the main thread at the end of the frame to update the atlases if needed, for example if there's a need to resize +// or if there are new tiles to add +void atlas_update_all(); + +void atlas_update_immediately(atlas_map_t* map); + +void atlas_update_all_immediately(); + +#ifdef __cplusplus +} +#endif + +#endif \ No newline at end of file diff --git a/src/retro_achievements.cpp b/src/retro_achievements.cpp index c558a8c31..cb35b34c9 100644 --- a/src/retro_achievements.cpp +++ b/src/retro_achievements.cpp @@ -69,13 +69,6 @@ struct ra_game_state_t; using ra_game_state_ptr = std::shared_ptr; -struct downloaded_image_t -{ - uint8_t* data; // always RGBA - int width; - int height; -}; - struct ra_achievement_t { atlas_tile_t* tile = nullptr; diff --git a/src/retro_achievements.h b/src/retro_achievements.h index e3c0845a1..eac80a3b8 100644 --- a/src/retro_achievements.h +++ b/src/retro_achievements.h @@ -9,18 +9,12 @@ #define RETRO_ACHIEVEMENTS #include #include +#include "atlas.h" #define SE_RC_BUFFER_SIZE (256*1024) #ifdef ENABLE_RETRO_ACHIEVEMENTS -typedef struct { - uint32_t atlas_id; - uint32_t width, height; - float x1, y1; - float x2, y2; -} atlas_tile_t; - void retro_achievements_initialize(void* emu_state, bool hardcore, bool is_mobile); void retro_achievements_shutdown(); From 46454938c38d39593a28ac39ee2b4a26571e5db1 Mon Sep 17 00:00:00 2001 From: offtkp Date: Thu, 15 Aug 2024 04:43:29 +0300 Subject: [PATCH 2/8] Concurrency hell --- src/atlas.cpp | 139 +++++++++++++++++++++++++++++++++++++++++--------- src/atlas.h | 36 ++++++++----- 2 files changed, 137 insertions(+), 38 deletions(-) diff --git a/src/atlas.cpp b/src/atlas.cpp index 05d90739b..82ed75249 100644 --- a/src/atlas.cpp +++ b/src/atlas.cpp @@ -1,15 +1,30 @@ #include "atlas.h" #include "sokol_gfx.h" +#include +#include #include +#include +#include +#include +#include +#include #include #include -const int atlas_spacing = 4; // leaving some space between tiles to avoid bleeding +static_assert(sizeof(atlas_tile_id_t) == 8, "atlas_tile_id_t must be 8 bytes"); +static_assert(sizeof(atlas_map_id_t) == 1, "atlas_map_id_t must be 1 byte"); +static_assert(sizeof(atlas_tile_id_t::tile_id) == 3, "atlas_tile_id_t::tile_id must be 3 bytes"); -struct tile_size_t { - uint16_t x, y; -}; +[[noreturn]] void atlas_error(const char* msg, ...) { + va_list args; + va_start(args, msg); + vprintf(msg, args); + va_end(args); + exit(1); +} + +const int atlas_spacing = 4; // leaving some space between tiles to avoid bleeding struct cached_image_t { uint8_t* data; // always RGBA @@ -17,11 +32,17 @@ struct cached_image_t { int height; }; +struct atlas_map_t; + +std::mutex cached_images_mutex; +std::unordered_map cached_images; +std::atomic atlas_maps[256] = {}; + // Atlases are always square and power of two // This always starts as a single tile image, but if a new tile needs to be // added, it's resized to the next power of two struct atlas_t { - atlas_t(uint32_t tile_width, uint32_t tile_height) + atlas_t(uint16_t tile_width, uint16_t tile_height) : tile_width(tile_width), tile_height(tile_height) { image.id = SG_INVALID_ID; @@ -30,24 +51,32 @@ struct atlas_t { ~atlas_t() = default; atlas_t(const atlas_t&) = delete; atlas_t& operator=(const atlas_t&) = delete; - atlas_t(atlas_t&&) = default; - atlas_t& operator=(atlas_t&&) = default; + atlas_t(atlas_t&&) = delete; + atlas_t& operator=(atlas_t&&) = delete; + std::mutex mutex; std::vector data; // we construct the atlas here before uploading it to the GPU + std::unordered_map tiles; // maps a tile id to its position in the atlas + // this position can change if the atlas is resized + std::vector images; // images that are already added to the atlas sg_image image = {}; - int pixel_stride = 0; - int offset_x = 0, - offset_y = 0; // to keep track of where next tile needs to be placed, in pixels - int tile_width, tile_height; + uint32_t pixel_stride = 0; + uint16_t offset_x = 0, offset_y = 0; + const uint16_t tile_width, tile_height; bool resized = false; bool dirty = false; // needs the data to be reuploaded to the GPU void add_image(cached_image_t* image) { + std::unique_lock lock(mutex); dirty = true; - uint32_t tile_offset_x = offset_x; - uint32_t tile_offset_y = offset_y; + // First, check if we need to resize the atlas + // TODO: + + + uint16_t new_tile_offset_x = offset_x; + uint16_t new_tile_offset_y = offset_y; offset_x += tile_width + atlas_spacing; if (offset_x + tile_width > pixel_stride) @@ -62,22 +91,84 @@ struct atlas_t { { for (int x = 0; x < tile_width; x++) { - uint32_t atlas_offset = - ((tile_offset_x + x) * 4) + (((tile_offset_y + y) * pixel_stride) * 4); - uint32_t tile_offset = x * 4 + (y * 4 * tile_width); + uint32_t atlas_index = + ((new_tile_offset_x + x) * 4) + (((new_tile_offset_y + y) * pixel_stride) * 4); + uint32_t tile_index = x * 4 + (y * 4 * tile_width); - assert(atlas_offset + 3 < data.size()); - assert(tile_offset + 3 < tile_width * tile_height * 4); + assert(atlas_index + 3 < data.size()); + assert(tile_index + 3 < tile_width * tile_height * 4); - data[atlas_offset + 0] = image->data[tile_offset + 0]; - data[atlas_offset + 1] = image->data[tile_offset + 1]; - data[atlas_offset + 2] = image->data[tile_offset + 2]; - data[atlas_offset + 3] = image->data[tile_offset + 3]; + data[atlas_index + 0] = image->data[tile_index + 0]; + data[atlas_index + 1] = image->data[tile_index + 1]; + data[atlas_index + 2] = image->data[tile_index + 2]; + data[atlas_index + 3] = image->data[tile_index + 3]; } } } + +private: + void resize(); }; struct atlas_map_t { - std::unordered_map atlases; -}; \ No newline at end of file + TODO, use shared_ptr everywhere to make it fully thread safe? + std::unordered_map atlases; +}; + +atlas_map_id_t atlas_create_map() { + atlas_map_t* map = new atlas_map_t(); + for (int i = 0; i < 256; i++) { + atlas_map_t* expected = nullptr; + bool success = atlas_maps[i].compare_exchange_strong(expected, map); + if (success) { + return i; + } + } + + atlas_error("atlas_create_map: too many maps\n"); +} + +void atlas_destroy_map(atlas_map_id_t id) { + atlas_map_t* map = atlas_maps[id].exchange(nullptr); + delete map; +} + +atlas_tile_id_t atlas_add_tile_from_url(atlas_map_t* map, const char* url) { + +} + +bool atlas_has_tile(atlas_tile_id_t id) { + atlas_map_t* map = atlas_maps[id.map_id].load(); + + if (map == nullptr) { + atlas_error("atlas_has_tile: map is null\n"); + } + + atlas_t* atlas = nullptr; + + // Find atlas in atlas map + { + std::unique_lock lock(map->atlases_mutex); + + auto it = map->atlases.find(id.tile_width << 16 | id.tile_height); + if (it == map->atlases.end()) { + atlas_error("atlas_has_tile: atlas not found\n"); + } + + atlas = it->second; + if (atlas == nullptr) { + atlas_error("atlas_has_tile: atlas is null\n"); + } + } + + // Check for the tile in the atlas + { + std::unique_lock lock(atlas->mutex); + uint32_t tile_id = id.tile_id[0] | (id.tile_id[1] << 8) | (id.tile_id[2] << 16); + return atlas->tiles.find(tile_id) != atlas->tiles.end(); + } +} + +atlas_tile_t atlas_get_tile(atlas_tile_id_t id) { + +} \ No newline at end of file diff --git a/src/atlas.h b/src/atlas.h index 38eacd728..c951578bd 100644 --- a/src/atlas.h +++ b/src/atlas.h @@ -5,7 +5,6 @@ extern "C" { #endif -#include #include #include @@ -17,34 +16,43 @@ typedef struct { float x2, y2; } atlas_tile_t; -typedef uint32_t atlas_tile_id; +// There can be up to 256 atlas maps at a time, and each of them can have any number of atlases +// using different tile sizes +typedef uint8_t atlas_map_id_t; -// An atlas map is a map of atlases, which themselves are images with multiple tiles -typedef struct atlas_map_t atlas_map_t; +// Each atlas tile id is not a pointer, but a descriptor that contains the map id +// and the necessary info to find a tile in said atlas map +typedef struct { + uint16_t tile_width; + uint16_t tile_height; + atlas_map_id_t map_id; + uint8_t tile_id[3]; // 24-bits of precision, more than enough to represent any number of tiles in our + // supported tile sizes (width/height >= 8, atlas size <= 4096) +} atlas_tile_id_t; -atlas_map_t* atlas_create_map(); +atlas_map_id_t atlas_create_map(); -void atlas_destroy_map(atlas_map_t* map); +void atlas_destroy_map(atlas_map_id_t map); // Downloads an image from the url and adds it to the atlas map -atlas_tile_id atlas_add_tile_from_url(atlas_map_t* map, const char* url); +atlas_tile_id_t atlas_add_tile_from_url(atlas_map_id_t map, const char* url); // Loads an image from a path and adds it to the atlas map -atlas_tile_id atlas_add_tile_from_path(atlas_map_t* map, const char* path, bool use_separate_thread); +atlas_tile_id_t atlas_add_tile_from_path(atlas_map_id_t map, const char* path, bool use_separate_thread); // Checks if a tile is ready to be used -bool atlas_has_tile(atlas_map_t* map, atlas_tile_id id); +bool atlas_has_tile(atlas_tile_id_t id); // Must be called after atlas_has_tile ensures the tile is ready -atlas_tile_t atlas_get_tile(atlas_map_t* map, atlas_tile_id id); +atlas_tile_t atlas_get_tile(atlas_tile_id_t id); // Called from the main thread at the end of the frame to update the atlases if needed, for example if there's a need to resize // or if there are new tiles to add -void atlas_update_all(); - -void atlas_update_immediately(atlas_map_t* map); +void atlas_upload_all(); -void atlas_update_all_immediately(); +// Useful to upload a single atlas map immediately. For example if you add a bunch of images from paths and want to +// make them immediately available +void atlas_upload_single(atlas_map_id_t map); #ifdef __cplusplus } From 65a8dc08a2f629abd9148d95addbb474caab80a5 Mon Sep 17 00:00:00 2001 From: offtkp Date: Thu, 15 Aug 2024 15:26:56 +0300 Subject: [PATCH 3/8] Atlas progress --- src/atlas.cpp | 90 +++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 73 insertions(+), 17 deletions(-) diff --git a/src/atlas.cpp b/src/atlas.cpp index 82ed75249..d1884daf5 100644 --- a/src/atlas.cpp +++ b/src/atlas.cpp @@ -1,12 +1,12 @@ #include "atlas.h" #include "sokol_gfx.h" -#include #include #include #include #include #include +#include #include #include #include @@ -32,11 +32,19 @@ struct cached_image_t { int height; }; +struct atlas_tile_info_t { + cached_image_t* image_data; + float x1, y1, x2, y2; // coordinates in the atlas + std::atomic_bool ready = { false }; // whether the tile has been uploaded to the GPU +}; + struct atlas_map_t; std::mutex cached_images_mutex; std::unordered_map cached_images; -std::atomic atlas_maps[256] = {}; + +std::mutex atlas_maps_mutex; +std::shared_ptr atlas_maps[256] = {}; // Atlases are always square and power of two // This always starts as a single tile image, but if a new tile needs to be @@ -56,9 +64,9 @@ struct atlas_t { std::mutex mutex; std::vector data; // we construct the atlas here before uploading it to the GPU - std::unordered_map tiles; // maps a tile id to its position in the atlas - // this position can change if the atlas is resized - std::vector images; // images that are already added to the atlas + + // Maps a tile id to its image data and its position in the atlas, which can change + std::unordered_map tiles; sg_image image = {}; uint32_t pixel_stride = 0; uint16_t offset_x = 0, offset_y = 0; @@ -111,16 +119,15 @@ struct atlas_t { }; struct atlas_map_t { - TODO, use shared_ptr everywhere to make it fully thread safe? - std::unordered_map atlases; + std::mutex atlases_mutex; + std::unordered_map> atlases; }; atlas_map_id_t atlas_create_map() { - atlas_map_t* map = new atlas_map_t(); + std::unique_lock lock(atlas_maps_mutex); for (int i = 0; i < 256; i++) { - atlas_map_t* expected = nullptr; - bool success = atlas_maps[i].compare_exchange_strong(expected, map); - if (success) { + if (atlas_maps[i] == nullptr) { + atlas_maps[i] = std::make_shared(); return i; } } @@ -129,8 +136,11 @@ atlas_map_id_t atlas_create_map() { } void atlas_destroy_map(atlas_map_id_t id) { - atlas_map_t* map = atlas_maps[id].exchange(nullptr); - delete map; + if (atlas_maps[id] == nullptr) { + atlas_error("atlas_destroy_map: map is null\n"); + } + + atlas_maps[id] = nullptr; } atlas_tile_id_t atlas_add_tile_from_url(atlas_map_t* map, const char* url) { @@ -138,13 +148,13 @@ atlas_tile_id_t atlas_add_tile_from_url(atlas_map_t* map, const char* url) { } bool atlas_has_tile(atlas_tile_id_t id) { - atlas_map_t* map = atlas_maps[id.map_id].load(); - + std::shared_ptr map = atlas_maps[id.map_id]; + if (map == nullptr) { atlas_error("atlas_has_tile: map is null\n"); } - atlas_t* atlas = nullptr; + std::shared_ptr atlas = nullptr; // Find atlas in atlas map { @@ -165,10 +175,56 @@ bool atlas_has_tile(atlas_tile_id_t id) { { std::unique_lock lock(atlas->mutex); uint32_t tile_id = id.tile_id[0] | (id.tile_id[1] << 8) | (id.tile_id[2] << 16); - return atlas->tiles.find(tile_id) != atlas->tiles.end(); + auto it = atlas->tiles.find(tile_id); + if (it == atlas->tiles.end()) { + return false; + } + + return it->second.ready; } } atlas_tile_t atlas_get_tile(atlas_tile_id_t id) { + std::shared_ptr map = atlas_maps[id.map_id]; + + if (map == nullptr) { + atlas_error("atlas_has_tile: map is null\n"); + } + + std::shared_ptr atlas = nullptr; + + // Find atlas in atlas map + { + std::unique_lock lock(map->atlases_mutex); + + auto it = map->atlases.find(id.tile_width << 16 | id.tile_height); + if (it == map->atlases.end()) { + atlas_error("atlas_has_tile: atlas not found\n"); + } + + atlas = it->second; + if (atlas == nullptr) { + atlas_error("atlas_has_tile: atlas is null\n"); + } + } + // Check for the tile in the atlas + { + std::unique_lock lock(atlas->mutex); + uint32_t tile_id = id.tile_id[0] | (id.tile_id[1] << 8) | (id.tile_id[2] << 16); + auto it = atlas->tiles.find(tile_id); + if (it == atlas->tiles.end()) { + atlas_error("atlas_get_tile: tile not found\n"); + } + + atlas_tile_info_t& tile_info = it->second; + atlas_tile_t tile = {}; + tile.atlas_id = atlas->image.id; + tile.x1 = tile_info.x1; + tile.y1 = tile_info.y1; + tile.x2 = tile_info.x2; + tile.y2 = tile_info.y2; + + return tile; + } } \ No newline at end of file From b976987199a3bc8c3f7eee67ac5f7214394b2535 Mon Sep 17 00:00:00 2001 From: offtkp Date: Thu, 15 Aug 2024 18:53:39 +0300 Subject: [PATCH 4/8] More atlas shenanigans --- src/atlas.cpp | 331 ++++++++++++++++++++++++-------------------------- src/atlas.h | 37 ++---- 2 files changed, 164 insertions(+), 204 deletions(-) diff --git a/src/atlas.cpp b/src/atlas.cpp index d1884daf5..a1a1ecfa0 100644 --- a/src/atlas.cpp +++ b/src/atlas.cpp @@ -1,230 +1,211 @@ -#include "atlas.h" #include "sokol_gfx.h" - -#include -#include -#include -#include -#include -#include +#include "atlas.h" #include #include #include #include -static_assert(sizeof(atlas_tile_id_t) == 8, "atlas_tile_id_t must be 8 bytes"); -static_assert(sizeof(atlas_map_id_t) == 1, "atlas_map_id_t must be 1 byte"); -static_assert(sizeof(atlas_tile_id_t::tile_id) == 3, "atlas_tile_id_t::tile_id must be 3 bytes"); - -[[noreturn]] void atlas_error(const char* msg, ...) { - va_list args; - va_start(args, msg); - vprintf(msg, args); - va_end(args); +[[noreturn]] void atlas_error(const char* message) { + fprintf(stderr, "Atlas error: %s\n", message); exit(1); } -const int atlas_spacing = 4; // leaving some space between tiles to avoid bleeding - struct cached_image_t { - uint8_t* data; // always RGBA - int width; - int height; -}; - -struct atlas_tile_info_t { - cached_image_t* image_data; - float x1, y1, x2, y2; // coordinates in the atlas - std::atomic_bool ready = { false }; // whether the tile has been uploaded to the GPU + const uint8_t* data; + uint32_t width, height; }; -struct atlas_map_t; +std::mutex image_cache_mutex; +std::unordered_map image_cache; -std::mutex cached_images_mutex; -std::unordered_map cached_images; +struct atlas_t { + atlas_t(uint32_t tile_width, uint32_t tile_height); + ~atlas_t(); -std::mutex atlas_maps_mutex; -std::shared_ptr atlas_maps[256] = {}; + atlas_tile_t* add_image(cached_image_t* image); + void upload(); -// Atlases are always square and power of two -// This always starts as a single tile image, but if a new tile needs to be -// added, it's resized to the next power of two -struct atlas_t { - atlas_t(uint16_t tile_width, uint16_t tile_height) - : tile_width(tile_width), tile_height(tile_height) - { - image.id = SG_INVALID_ID; - } +private: + void copy_to_data(cached_image_t* image); - ~atlas_t() = default; - atlas_t(const atlas_t&) = delete; - atlas_t& operator=(const atlas_t&) = delete; - atlas_t(atlas_t&&) = delete; - atlas_t& operator=(atlas_t&&) = delete; + const uint32_t tile_width, tile_height; std::mutex mutex; - std::vector data; // we construct the atlas here before uploading it to the GPU - - // Maps a tile id to its image data and its position in the atlas, which can change - std::unordered_map tiles; - sg_image image = {}; - uint32_t pixel_stride = 0; - uint16_t offset_x = 0, offset_y = 0; - const uint16_t tile_width, tile_height; - bool resized = false; - bool dirty = false; // needs the data to be reuploaded to the GPU - - void add_image(cached_image_t* image) - { - std::unique_lock lock(mutex); - dirty = true; - - // First, check if we need to resize the atlas - // TODO: - - - uint16_t new_tile_offset_x = offset_x; - uint16_t new_tile_offset_y = offset_y; - - offset_x += tile_width + atlas_spacing; - if (offset_x + tile_width > pixel_stride) - { - offset_x = 0; - offset_y += tile_width + atlas_spacing; - } + sg_image image; + uint32_t atlas_dimension; + uint32_t offset_x, offset_y; + std::vector data; + std::unordered_map images; + std::vector images_to_add; + bool dirty; // new data needs to be uploaded to the GPU + bool resized; // atlas needs to be destroyed and created at new size + + constexpr static uint32_t padding = 4; +}; - assert(image->width == tile_width); +struct atlas_map_t { + atlas_tile_t* add_image_from_url(const char* url); + atlas_tile_t* add_image_from_path(const char* path); - for (int y = 0; y < tile_height; y++) - { - for (int x = 0; x < tile_width; x++) - { - uint32_t atlas_index = - ((new_tile_offset_x + x) * 4) + (((new_tile_offset_y + y) * pixel_stride) * 4); - uint32_t tile_index = x * 4 + (y * 4 * tile_width); +private: + std::mutex atlases_mutex; + std::vector atlases; +}; - assert(atlas_index + 3 < data.size()); - assert(tile_index + 3 < tile_width * tile_height * 4); +atlas_t::atlas_t(uint32_t tile_width, uint32_t tile_height) : tile_width(tile_width), tile_height(tile_height) { + image.id = SG_INVALID_ID; + offset_x = 0; + offset_y = 0; + dirty = false; + resized = false; + atlas_dimension = 16; + uint32_t minimum_width = tile_width + padding; + uint32_t minimum_height = tile_height + padding; + while (atlas_dimension < minimum_width || atlas_dimension < minimum_height) { + atlas_dimension *= 2; + } - data[atlas_index + 0] = image->data[tile_index + 0]; - data[atlas_index + 1] = image->data[tile_index + 1]; - data[atlas_index + 2] = image->data[tile_index + 2]; - data[atlas_index + 3] = image->data[tile_index + 3]; - } - } + data.resize(atlas_dimension * atlas_dimension * 4); +} + +void atlas_t::copy_to_data(cached_image_t* cached_image) { + if (cached_image->width != tile_width || cached_image->height != tile_height) { + atlas_error("Image dimensions do not match atlas tile dimensions"); } -private: - void resize(); -}; + dirty = true; -struct atlas_map_t { - std::mutex atlases_mutex; - std::unordered_map> atlases; -}; + uint32_t tile_offset_x = offset_x; + uint32_t tile_offset_y = offset_y; + + offset_x += tile_width + padding; + if (offset_x + tile_width > atlas_dimension) { + offset_x = 0; + offset_y += tile_height + padding; -atlas_map_id_t atlas_create_map() { - std::unique_lock lock(atlas_maps_mutex); - for (int i = 0; i < 256; i++) { - if (atlas_maps[i] == nullptr) { - atlas_maps[i] = std::make_shared(); - return i; + if (offset_y + tile_height > atlas_dimension) { + atlas_error("Atlas is full somehow"); } } - atlas_error("atlas_create_map: too many maps\n"); -} + for (int y = 0; y < tile_height; y++) { + for (int x = 0; x < tile_width; x++) { + uint32_t atlas_offset = ((tile_offset_x + x) * 4) + (((tile_offset_y + y) * atlas_dimension) * 4); + uint32_t tile_offset = x * 4 + (y * 4 * tile_width); -void atlas_destroy_map(atlas_map_id_t id) { - if (atlas_maps[id] == nullptr) { - atlas_error("atlas_destroy_map: map is null\n"); + data[atlas_offset + 0] = cached_image->data[tile_offset + 0]; + data[atlas_offset + 1] = cached_image->data[tile_offset + 1]; + data[atlas_offset + 2] = cached_image->data[tile_offset + 2]; + data[atlas_offset + 3] = cached_image->data[tile_offset + 3]; + } } - atlas_maps[id] = nullptr; -} - -atlas_tile_id_t atlas_add_tile_from_url(atlas_map_t* map, const char* url) { + auto it = images.find(cached_image); + if (it == images.end()) { + atlas_error("Image not found in atlas"); + } + atlas_tile_t* tile = it->second; + tile->atlas_id = image.id; + tile->x1 = (float)tile_offset_x / atlas_dimension; + tile->y1 = (float)tile_offset_y / atlas_dimension; + tile->x2 = (float)(tile_offset_x + tile_width) / atlas_dimension; + tile->y2 = (float)(tile_offset_y + tile_height) / atlas_dimension; } -bool atlas_has_tile(atlas_tile_id_t id) { - std::shared_ptr map = atlas_maps[id.map_id]; - - if (map == nullptr) { - atlas_error("atlas_has_tile: map is null\n"); - } +atlas_tile_t* atlas_t::add_image(cached_image_t* image_to_add) { + std::unique_lock lock(mutex); - std::shared_ptr atlas = nullptr; + // These are the dimensions that would occur after adding a tile + uint32_t minimum_x = offset_x + tile_width + padding; + uint32_t minimum_y = offset_y + tile_height + padding; - // Find atlas in atlas map - { - std::unique_lock lock(map->atlases_mutex); + // If the atlas is too small, resize it + if (minimum_x > atlas_dimension || minimum_y > atlas_dimension) { + resized = true; + atlas_dimension *= 2; - auto it = map->atlases.find(id.tile_width << 16 | id.tile_height); - if (it == map->atlases.end()) { - atlas_error("atlas_has_tile: atlas not found\n"); - } + std::vector new_data; + new_data.resize(atlas_dimension * atlas_dimension * 4); + data.swap(new_data); + + offset_x = 0; + offset_y = 0; - atlas = it->second; - if (atlas == nullptr) { - atlas_error("atlas_has_tile: atlas is null\n"); + for (auto& pair : images) { + images_to_add.push_back(pair.first); } } - // Check for the tile in the atlas - { - std::unique_lock lock(atlas->mutex); - uint32_t tile_id = id.tile_id[0] | (id.tile_id[1] << 8) | (id.tile_id[2] << 16); - auto it = atlas->tiles.find(tile_id); - if (it == atlas->tiles.end()) { - return false; - } + float current_x = offset_x; + float current_y = offset_y; - return it->second.ready; - } + atlas_tile_t* tile = new atlas_tile_t(); + images[image_to_add] = tile; + + copy_to_data(image_to_add); + + return tile; } -atlas_tile_t atlas_get_tile(atlas_tile_id_t id) { - std::shared_ptr map = atlas_maps[id.map_id]; - - if (map == nullptr) { - atlas_error("atlas_has_tile: map is null\n"); +void atlas_t::upload() { + std::unique_lock lock(mutex); + if (resized) { + sg_destroy_image(image); + image.id = SG_INVALID_ID; } - std::shared_ptr atlas = nullptr; - - // Find atlas in atlas map - { - std::unique_lock lock(map->atlases_mutex); + if (image.id == SG_INVALID_ID) { + sg_image_desc desc = {0}; + desc.type = SG_IMAGETYPE_2D; + desc.render_target = false; + desc.width = atlas_dimension; + desc.height = atlas_dimension; + desc.num_slices = 1; + desc.num_mipmaps = 1; + desc.usage = SG_USAGE_DYNAMIC; + desc.pixel_format = SG_PIXELFORMAT_RGBA8; + desc.sample_count = 1; + desc.min_filter = SG_FILTER_LINEAR; + desc.mag_filter = SG_FILTER_LINEAR; + desc.wrap_u = SG_WRAP_CLAMP_TO_EDGE; + desc.wrap_v = SG_WRAP_CLAMP_TO_EDGE; + desc.wrap_w = SG_WRAP_CLAMP_TO_EDGE; + desc.border_color = SG_BORDERCOLOR_TRANSPARENT_BLACK; + desc.max_anisotropy = 1; + desc.min_lod = 0.0f; + desc.max_lod = 1e9f; + + image = sg_make_image(desc); + + if (resized) { + for (cached_image_t* image_to_add : images_to_add) { + copy_to_data(image_to_add); + } - auto it = map->atlases.find(id.tile_width << 16 | id.tile_height); - if (it == map->atlases.end()) { - atlas_error("atlas_has_tile: atlas not found\n"); + images_to_add.clear(); + resized = false; } + } - atlas = it->second; - if (atlas == nullptr) { - atlas_error("atlas_has_tile: atlas is null\n"); - } + if (dirty) { + sg_image_data sg_data = {0}; + sg_data.subimage[0][0].ptr = data.data(); + sg_data.subimage[0][0].size = data.size(); + sg_update_image(image, sg_data); + dirty = false; } +} - // Check for the tile in the atlas - { - std::unique_lock lock(atlas->mutex); - uint32_t tile_id = id.tile_id[0] | (id.tile_id[1] << 8) | (id.tile_id[2] << 16); - auto it = atlas->tiles.find(tile_id); - if (it == atlas->tiles.end()) { - atlas_error("atlas_get_tile: tile not found\n"); - } +atlas_tile_t* atlas_map_t::add_image_from_url(const char* url) { + const std::string url_str(url); + +} - atlas_tile_info_t& tile_info = it->second; - atlas_tile_t tile = {}; - tile.atlas_id = atlas->image.id; - tile.x1 = tile_info.x1; - tile.y1 = tile_info.y1; - tile.x2 = tile_info.x2; - tile.y2 = tile_info.y2; +atlas_map_t* atlas_create_map() { + return new atlas_map_t(); +} - return tile; - } +void atlas_destroy_map(atlas_map_t* map) { + delete map; } \ No newline at end of file diff --git a/src/atlas.h b/src/atlas.h index c951578bd..0a6ab7fff 100644 --- a/src/atlas.h +++ b/src/atlas.h @@ -11,49 +11,28 @@ extern "C" { // A tile in an atlas. atlas_id is the sg_image id of the atlas itself // x1, y1, x2, y2 are the coordinates of the tile in the atlas typedef struct { - uint32_t atlas_id; + uint32_t atlas_id = 0; float x1, y1; float x2, y2; } atlas_tile_t; -// There can be up to 256 atlas maps at a time, and each of them can have any number of atlases -// using different tile sizes -typedef uint8_t atlas_map_id_t; +struct atlas_map_t; -// Each atlas tile id is not a pointer, but a descriptor that contains the map id -// and the necessary info to find a tile in said atlas map -typedef struct { - uint16_t tile_width; - uint16_t tile_height; - atlas_map_id_t map_id; - uint8_t tile_id[3]; // 24-bits of precision, more than enough to represent any number of tiles in our - // supported tile sizes (width/height >= 8, atlas size <= 4096) -} atlas_tile_id_t; - -atlas_map_id_t atlas_create_map(); +struct atlas_map_t* atlas_create_map(); -void atlas_destroy_map(atlas_map_id_t map); +void atlas_destroy_map(atlas_map_t* map); // Downloads an image from the url and adds it to the atlas map -atlas_tile_id_t atlas_add_tile_from_url(atlas_map_id_t map, const char* url); +// TODO: add hint of atlas total size, so that we can tell it to allocate a bigger atlas or a smaller one if we expect less tiles +atlas_tile_t* atlas_add_tile_from_url(atlas_map_t* map, const char* url); // Loads an image from a path and adds it to the atlas map -atlas_tile_id_t atlas_add_tile_from_path(atlas_map_id_t map, const char* path, bool use_separate_thread); - -// Checks if a tile is ready to be used -bool atlas_has_tile(atlas_tile_id_t id); - -// Must be called after atlas_has_tile ensures the tile is ready -atlas_tile_t atlas_get_tile(atlas_tile_id_t id); +atlas_tile_t* atlas_add_tile_from_path(atlas_map_t* map, const char* path); // Called from the main thread at the end of the frame to update the atlases if needed, for example if there's a need to resize -// or if there are new tiles to add +// or if there are new tiles to add, or if some atlases need to be cleaned up void atlas_upload_all(); -// Useful to upload a single atlas map immediately. For example if you add a bunch of images from paths and want to -// make them immediately available -void atlas_upload_single(atlas_map_id_t map); - #ifdef __cplusplus } #endif From cbb6718f7e06c6595a7731d48eab0089bf50166d Mon Sep 17 00:00:00 2001 From: offtkp Date: Thu, 15 Aug 2024 21:17:17 +0300 Subject: [PATCH 5/8] Sorta works --- src/atlas.cpp | 237 ++++++++++++++++---- src/atlas.h | 20 +- src/main.c | 13 +- src/retro_achievements.cpp | 440 ++++--------------------------------- src/retro_achievements.h | 8 +- 5 files changed, 260 insertions(+), 458 deletions(-) diff --git a/src/atlas.cpp b/src/atlas.cpp index a1a1ecfa0..a493857ad 100644 --- a/src/atlas.cpp +++ b/src/atlas.cpp @@ -1,5 +1,9 @@ -#include "sokol_gfx.h" #include "atlas.h" +#include "https.hpp" +#include "sokol_gfx.h" +#include "stb_image.h" +#include +#include #include #include #include @@ -11,32 +15,53 @@ } struct cached_image_t { - const uint8_t* data; + const uint8_t* data = nullptr; uint32_t width, height; }; +struct atlas_tile_t { + std::atomic_uint32_t atlas_id; + std::atomic x1, y1, x2, y2; +}; + std::mutex image_cache_mutex; -std::unordered_map image_cache; +std::unordered_map image_cache; + +std::mutex atlas_maps_mutex; +std::vector atlas_maps; struct atlas_t { atlas_t(uint32_t tile_width, uint32_t tile_height); ~atlas_t(); - atlas_tile_t* add_image(cached_image_t* image); + atlas_tile_t* get_tile(const std::string& url) { + std::unique_lock lock(mutex); + if (images.find(url) == images.end()) { + atlas_error("Tile not found"); + } + + atlas_tile_t* tile = images[url]; + return tile; + } + bool has_tile(const std::string& url) { + std::unique_lock lock(mutex); + return images.find(url) != images.end(); + } + void add_tile(const std::string& url, atlas_tile_t* tile, cached_image_t* cached_image); void upload(); private: - void copy_to_data(cached_image_t* image); + void copy_to_data(atlas_tile_t* tile, cached_image_t* image); const uint32_t tile_width, tile_height; std::mutex mutex; - sg_image image; + sg_image atlas_image; uint32_t atlas_dimension; uint32_t offset_x, offset_y; std::vector data; - std::unordered_map images; - std::vector images_to_add; + std::unordered_map images; + std::vector images_to_add; bool dirty; // new data needs to be uploaded to the GPU bool resized; // atlas needs to be destroyed and created at new size @@ -44,16 +69,33 @@ struct atlas_t { }; struct atlas_map_t { - atlas_tile_t* add_image_from_url(const char* url); - atlas_tile_t* add_image_from_path(const char* path); + atlas_tile_t* add_tile_from_url(const char* url); + atlas_tile_t* add_tile_from_path(const char* path); + void upload_all(); + + std::atomic_int requests = {0}; private: + atlas_t* get_atlas(uint32_t tile_width, uint32_t tile_height) { + std::unique_lock lock(atlases_mutex); + uint32_t key = tile_width << 16 | tile_height; + + atlas_t* atlas = atlases[key]; + + if (atlas == nullptr) { + atlas = new atlas_t(tile_width, tile_height); + atlases[key] = atlas; + } + + return atlas; + } + std::mutex atlases_mutex; - std::vector atlases; + std::unordered_map atlases; }; atlas_t::atlas_t(uint32_t tile_width, uint32_t tile_height) : tile_width(tile_width), tile_height(tile_height) { - image.id = SG_INVALID_ID; + atlas_image.id = SG_INVALID_ID; offset_x = 0; offset_y = 0; dirty = false; @@ -68,7 +110,15 @@ atlas_t::atlas_t(uint32_t tile_width, uint32_t tile_height) : tile_width(tile_wi data.resize(atlas_dimension * atlas_dimension * 4); } -void atlas_t::copy_to_data(cached_image_t* cached_image) { +void atlas_t::copy_to_data(atlas_tile_t* tile, cached_image_t* cached_image) { + if (tile == nullptr) { + atlas_error("Tile is null"); + } + + if (cached_image->data == nullptr) { + atlas_error("Cached image data is null"); + } + if (cached_image->width != tile_width || cached_image->height != tile_height) { atlas_error("Image dimensions do not match atlas tile dimensions"); } @@ -82,10 +132,6 @@ void atlas_t::copy_to_data(cached_image_t* cached_image) { if (offset_x + tile_width > atlas_dimension) { offset_x = 0; offset_y += tile_height + padding; - - if (offset_y + tile_height > atlas_dimension) { - atlas_error("Atlas is full somehow"); - } } for (int y = 0; y < tile_height; y++) { @@ -100,20 +146,14 @@ void atlas_t::copy_to_data(cached_image_t* cached_image) { } } - auto it = images.find(cached_image); - if (it == images.end()) { - atlas_error("Image not found in atlas"); - } - - atlas_tile_t* tile = it->second; - tile->atlas_id = image.id; + tile->atlas_id = atlas_image.id; tile->x1 = (float)tile_offset_x / atlas_dimension; tile->y1 = (float)tile_offset_y / atlas_dimension; tile->x2 = (float)(tile_offset_x + tile_width) / atlas_dimension; tile->y2 = (float)(tile_offset_y + tile_height) / atlas_dimension; } -atlas_tile_t* atlas_t::add_image(cached_image_t* image_to_add) { +void atlas_t::add_tile(const std::string& url, atlas_tile_t* tile, cached_image_t* cached_image) { std::unique_lock lock(mutex); // These are the dimensions that would occur after adding a tile @@ -140,22 +180,19 @@ atlas_tile_t* atlas_t::add_image(cached_image_t* image_to_add) { float current_x = offset_x; float current_y = offset_y; - atlas_tile_t* tile = new atlas_tile_t(); - images[image_to_add] = tile; - - copy_to_data(image_to_add); + images[url] = tile; - return tile; + copy_to_data(tile, cached_image); } void atlas_t::upload() { std::unique_lock lock(mutex); if (resized) { - sg_destroy_image(image); - image.id = SG_INVALID_ID; + sg_destroy_image(atlas_image); + atlas_image.id = SG_INVALID_ID; } - if (image.id == SG_INVALID_ID) { + if (atlas_image.id == SG_INVALID_ID) { sg_image_desc desc = {0}; desc.type = SG_IMAGETYPE_2D; desc.render_target = false; @@ -176,36 +213,156 @@ void atlas_t::upload() { desc.min_lod = 0.0f; desc.max_lod = 1e9f; - image = sg_make_image(desc); + atlas_image = sg_make_image(desc); if (resized) { - for (cached_image_t* image_to_add : images_to_add) { - copy_to_data(image_to_add); + { + std::unique_lock lock(image_cache_mutex); + for (const std::string& image_url : images_to_add) { + cached_image_t* cached_image = image_cache[image_url]; + atlas_tile_t* tile = images[image_url]; + copy_to_data(tile, cached_image); + } } images_to_add.clear(); + resized = false; } + + for (auto& pair : images) { + atlas_tile_t* tile = pair.second; + tile->atlas_id = atlas_image.id; + } } if (dirty) { sg_image_data sg_data = {0}; sg_data.subimage[0][0].ptr = data.data(); sg_data.subimage[0][0].size = data.size(); - sg_update_image(image, sg_data); + sg_update_image(atlas_image, sg_data); dirty = false; } } -atlas_tile_t* atlas_map_t::add_image_from_url(const char* url) { +atlas_tile_t* atlas_map_t::add_tile_from_url(const char* url) { const std::string url_str(url); - + + { + std::unique_lock lock(image_cache_mutex); + if (image_cache.find(url_str) != image_cache.end()) { + cached_image_t* cached_image = image_cache[url_str]; + atlas_t* atlas = get_atlas(cached_image->width, cached_image->height); + atlas_tile_t* tile = nullptr; + if (atlas->has_tile(url_str)) { + atlas->get_tile(url_str); + } else { + tile = new atlas_tile_t(); + atlas->add_tile(url_str, tile, cached_image); + } + return tile; + } + } + + atlas_tile_t* tile = new atlas_tile_t(); + + requests++; + https_request(http_request_e::GET, url_str, {}, {}, [this, url_str, tile] (const std::vector& result) { + if (result.empty()) { + printf("Failed to download image for atlas\n"); + delete tile; + requests--; + return; + } + + cached_image_t* cached_image = new cached_image_t(); + int width, height; + cached_image->data = stbi_load_from_memory(result.data(), result.size(), &width, &height, NULL, 4); + cached_image->width = width; + cached_image->height = height; + + if (!cached_image->data) + { + printf("Failed to load image for atlas\n"); + delete tile; + delete cached_image; + } else { + { + std::unique_lock lock(image_cache_mutex); + image_cache[url_str] = cached_image; + } + + atlas_t* atlas = get_atlas(cached_image->width, cached_image->height); + atlas->add_tile(url_str, tile, cached_image); + } + + requests--; + printf("Outstanding requests: %d\n", requests.load()); + }); + + return tile; +} + +atlas_tile_t* atlas_map_t::add_tile_from_path(const char* path) { + atlas_error("Not implemented"); + return nullptr; +} + +void atlas_map_t::upload_all() { + std::unique_lock lock(atlases_mutex); + for (auto& pair : atlases) { + pair.second->upload(); + } } atlas_map_t* atlas_create_map() { - return new atlas_map_t(); + atlas_map_t* map = new atlas_map_t(); + { + std::unique_lock lock(atlas_maps_mutex); + atlas_maps.push_back(map); + } + return map; } void atlas_destroy_map(atlas_map_t* map) { + { + std::unique_lock lock(atlas_maps_mutex); + auto it = std::find(atlas_maps.begin(), atlas_maps.end(), map); + if (it != atlas_maps.end()) { + atlas_maps.erase(it); + } + } delete map; +} + +atlas_tile_t* atlas_add_tile_from_url(atlas_map_t* map, const char* url) { + return map->add_tile_from_url(url); +} + +atlas_tile_t* atlas_add_tile_from_path(atlas_map_t* map, const char* path) { + atlas_error("Not implemented"); + return nullptr; +} + +void atlas_upload_all() { + std::unique_lock lock(atlas_maps_mutex); + for (atlas_map_t* map : atlas_maps) { + if (map->requests > 0) { + continue; // probably a lot of outstanding requests and we don't wanna update too often + } + + map->upload_all(); + } +} + +uint32_t atlas_get_tile_id(atlas_tile_t* tile) { + if (tile == nullptr) { + return 0; + } + + return tile->atlas_id; +} + +atlas_uvs_t atlas_get_tile_uvs(atlas_tile_t* tile) { + return {tile->x1, tile->y1, tile->x2, tile->y2}; } \ No newline at end of file diff --git a/src/atlas.h b/src/atlas.h index 0a6ab7fff..d63b700d2 100644 --- a/src/atlas.h +++ b/src/atlas.h @@ -8,31 +8,31 @@ extern "C" { #include #include -// A tile in an atlas. atlas_id is the sg_image id of the atlas itself -// x1, y1, x2, y2 are the coordinates of the tile in the atlas -typedef struct { - uint32_t atlas_id = 0; - float x1, y1; - float x2, y2; -} atlas_tile_t; +struct atlas_uvs_t { + float x1, y1, x2, y2; +}; +struct atlas_tile_t; struct atlas_map_t; struct atlas_map_t* atlas_create_map(); -void atlas_destroy_map(atlas_map_t* map); +void atlas_destroy_map(struct atlas_map_t* map); // Downloads an image from the url and adds it to the atlas map // TODO: add hint of atlas total size, so that we can tell it to allocate a bigger atlas or a smaller one if we expect less tiles -atlas_tile_t* atlas_add_tile_from_url(atlas_map_t* map, const char* url); +struct atlas_tile_t* atlas_add_tile_from_url(struct atlas_map_t* map, const char* url); // Loads an image from a path and adds it to the atlas map -atlas_tile_t* atlas_add_tile_from_path(atlas_map_t* map, const char* path); +struct atlas_tile_t* atlas_add_tile_from_path(struct atlas_map_t* map, const char* path); // Called from the main thread at the end of the frame to update the atlases if needed, for example if there's a need to resize // or if there are new tiles to add, or if some atlases need to be cleaned up void atlas_upload_all(); +uint32_t atlas_get_tile_id(struct atlas_tile_t* tile); +struct atlas_uvs_t atlas_get_tile_uvs(struct atlas_tile_t* tile); + #ifdef __cplusplus } #endif diff --git a/src/main.c b/src/main.c index 04eac413a..1daf05ca7 100644 --- a/src/main.c +++ b/src/main.c @@ -6284,12 +6284,13 @@ void se_draw_menu_panel(){ sg_image image = {SG_INVALID_ID}; ImVec2 offset1 = {0, 0}; ImVec2 offset2 = {1, 1}; - atlas_tile_t* user_image = retro_achievements_get_user_image(); + struct atlas_tile_t* user_image = retro_achievements_get_user_image(); if (user_image) { - image.id = user_image->atlas_id; - offset1 = (ImVec2){user_image->x1, user_image->y1}; - offset2 = (ImVec2){user_image->x2, user_image->y2}; + image.id = atlas_get_tile_id(user_image); + struct atlas_uvs_t uvs = atlas_get_tile_uvs(user_image); + offset1 = (ImVec2){uvs.x1, uvs.y1}; + offset2 = (ImVec2){uvs.x2, uvs.y2}; } static char line1[256]; static char line2[256]; @@ -7369,8 +7370,6 @@ static void frame(void) { se_draw_emulated_system_screen(false); #ifdef ENABLE_RETRO_ACHIEVEMENTS - retro_achievements_update_atlases(); - float left = screen_x; float top = menu_height; float right = screen_x+screen_width/se_dpi_scale(); @@ -7560,7 +7559,7 @@ static void frame(void) { se_emscripten_flush_fs(); gui_state.last_saved_settings=gui_state.settings; } - retro_achievements_delete_retired_atlases(); + atlas_upload_all(); } void se_load_settings(){ se_load_recent_games_list(); diff --git a/src/retro_achievements.cpp b/src/retro_achievements.cpp index cb35b34c9..1f89ffc76 100644 --- a/src/retro_achievements.cpp +++ b/src/retro_achievements.cpp @@ -52,18 +52,8 @@ const float notification_start_secondary_text_seconds = notification_start_secon const float notification_end_seconds = 4.0f; const float notification_fade_seconds = notification_end_seconds - notification_start_seconds; bool only_one_notification = false; -const int atlas_spacing = 4; // leaving some space between tiles to avoid bleeding const float padding = 7; -// atlases -> the currently existing atlases, each serving a different image -// width/height combo image_cache -> a mapping of image urls to their atlas and -// the coordinates within the atlas download_cache -> a cache of downloaded -// images, so we don't download the same image multiple times - -// download_cache has the lifetime of the program -// atlases and image_cache are reset every time the user loads a *different* -// game - struct atlas_t; struct ra_game_state_t; @@ -121,34 +111,25 @@ struct ra_notification_t float start_time = 0; }; -static std::mutex global_cache_mutex; -static std::unordered_map download_cache; -static std::vector images_to_destroy; - struct ra_game_state_t { - ~ra_game_state_t(); + ra_game_state_t() { + atlas_map = atlas_create_map(); + game_image = nullptr; + } - atlas_tile_t* game_image = nullptr; - std::vector atlases{}; - std::unordered_map image_cache{}; + ~ra_game_state_t() { + atlas_destroy_map(atlas_map); + } + + std::mutex mutex; + atlas_map_t* atlas_map; + atlas_tile_t* game_image; ra_achievement_list_t achievement_list; std::unordered_map leaderboard_trackers; std::unordered_map challenges; ra_progress_indicator_t progress_indicator; std::vector notifications; - std::atomic_int outstanding_requests; - std::mutex mutex; - - void inc() - { - outstanding_requests++; - } - - void dec() - { - outstanding_requests--; - } }; struct ra_state_t @@ -179,76 +160,11 @@ struct ra_state_t // global state solution is not viable. ra_game_state_ptr game_state; - void download(ra_game_state_ptr game_state, const std::string& url, - const std::function& callback); - void handle_downloaded(ra_game_state_ptr game_state, const std::string& url); void rebuild_achievement_list(ra_game_state_ptr game_state); }; static ra_state_t* ra_state = nullptr; -// Atlases are always square and power of two -// This always starts as a single tile image, but if a new tile needs to be -// added, it's resized to the next power of two -struct atlas_t -{ - atlas_t(uint32_t tile_width, uint32_t tile_height) - : tile_width(tile_width), tile_height(tile_height) - { - image.id = SG_INVALID_ID; - } - - ~atlas_t() = default; - atlas_t(const atlas_t&) = delete; - atlas_t& operator=(const atlas_t&) = delete; - atlas_t(atlas_t&&) = default; - atlas_t& operator=(atlas_t&&) = default; - - std::vector data; // we construct the atlas here before uploading it to the GPU - sg_image image = {}; - int pixel_stride = 0; - int offset_x = 0, - offset_y = 0; // to keep track of where next tile needs to be placed, in pixels - int tile_width, tile_height; - bool resized = false; - bool dirty = false; // needs the data to be reuploaded to the GPU - - void copy_image(downloaded_image_t* image) - { - dirty = true; - - uint32_t tile_offset_x = offset_x; - uint32_t tile_offset_y = offset_y; - - offset_x += tile_width + atlas_spacing; - if (offset_x + tile_width > pixel_stride) - { - offset_x = 0; - offset_y += tile_width + atlas_spacing; - } - - assert(image->width == tile_width); - - for (int y = 0; y < tile_height; y++) - { - for (int x = 0; x < tile_width; x++) - { - uint32_t atlas_offset = - ((tile_offset_x + x) * 4) + (((tile_offset_y + y) * pixel_stride) * 4); - uint32_t tile_offset = x * 4 + (y * 4 * tile_width); - - assert(atlas_offset + 3 < data.size()); - assert(tile_offset + 3 < tile_width * tile_height * 4); - - data[atlas_offset + 0] = image->data[tile_offset + 0]; - data[atlas_offset + 1] = image->data[tile_offset + 1]; - data[atlas_offset + 2] = image->data[tile_offset + 2]; - data[atlas_offset + 3] = image->data[tile_offset + 3]; - } - } - } -}; - namespace { void retro_achievements_game_image_loaded(ra_game_state_ptr game_state) @@ -384,20 +300,14 @@ namespace if (rc_client_achievement_get_image_url( rc_achievement, RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED, &url[0], url.size()) == RC_OK) { - notification->tile = &game_state->image_cache[url]; uint32_t id = rc_achievement->id; uint8_t bucket = rc_achievement->bucket; std::unique_lock lock(game_state->mutex); ra_achievement_t* achievement = retro_achievements_move_bucket(game_state, id, bucket); notification->start_time = se_time(); + notification->tile = atlas_add_tile_from_url(game_state->atlas_map, url.c_str()); + achievement->tile = notification->tile; game_state->notifications.push_back(*notification); - ra_state->download(game_state, url, [game_state, notification, url, id, bucket, achievement]() { - if (achievement) - { - achievement->tile = &game_state->image_cache[url]; - notification->tile = achievement->tile; - } - }); } } @@ -414,9 +324,7 @@ namespace &url[0], url.size()) == RC_OK) { std::unique_lock lock(game_state->mutex); - ra_state->download(game_state, url, [game_state, url]() { - game_state->progress_indicator.tile = &game_state->image_cache[url]; - }); + game_state->progress_indicator.tile = atlas_add_tile_from_url(game_state->atlas_map, url.c_str()); } } @@ -462,17 +370,13 @@ namespace if (rc_client_game_get_image_url(game, &url[0], url.size()) == RC_OK) { std::unique_lock lock(game_state->mutex); - ra_state->download(game_state, url, [game_state, url]() { - game_state->game_image = &game_state->image_cache[url]; - retro_achievements_game_image_loaded(game_state); - }); + game_state->game_image = atlas_add_tile_from_url(game_state->atlas_map, url.c_str()); + retro_achievements_game_image_loaded(game_state); } ra_state->rebuild_achievement_list(game_state); } - game_state->dec(); - delete game_state_ptr; // delete the pointer that was allocated to pass through ffi } @@ -607,9 +511,7 @@ namespace &url[0], url.size()) == RC_OK) { std::unique_lock lock(game_state->mutex); - ra_state->download(game_state, url, [game_state, url, id]() { - game_state->challenges[id].tile = &game_state->image_cache[url]; - }); + game_state->challenges[id].tile = atlas_add_tile_from_url(game_state->atlas_map, url.c_str()); } break; } @@ -755,9 +657,10 @@ namespace ImVec2 uv1 = ImVec2{1, 1}; if (tile) { - image.id = tile->atlas_id; - uv0 = ImVec2{tile->x1, tile->y1}; - uv1 = ImVec2{tile->x2, tile->y2}; + image.id = atlas_get_tile_id(tile); + atlas_uvs_t uvs = atlas_get_tile_uvs(tile); + uv0 = ImVec2{uvs.x1, uvs.y1}; + uv1 = ImVec2{uvs.x2, uvs.y2}; } se_boxed_image_triple_label(title.c_str(), description.c_str(), hardcore_str, hardcore_color, ICON_FK_GAMEPAD, image, 0, uv0, uv1, false); @@ -779,9 +682,10 @@ namespace if (bucket->achievements[j]->tile) { atlas_tile_t* tile = bucket->achievements[j]->tile; - uv0 = ImVec2{tile->x1, tile->y1}; - uv1 = ImVec2{tile->x2, tile->y2}; - image.id = tile->atlas_id; + image.id = atlas_get_tile_id(tile); + atlas_uvs_t uvs = atlas_get_tile_uvs(tile); + uv0 = ImVec2{uvs.x1, uvs.y1}; + uv1 = ImVec2{uvs.x2, uvs.y2}; } const auto& achievement = bucket->achievements[j]; @@ -790,7 +694,7 @@ namespace : achievement->rarity; bool unlocked = bucket->bucket_id == RC_CLIENT_ACHIEVEMENT_BUCKET_RECENTLY_UNLOCKED || bucket->bucket_id == RC_CLIENT_ACHIEVEMENT_BUCKET_UNLOCKED; - bool glow = rarity < 10.0f && unlocked; // glow if less than 10% of players have it and you have it too + bool glow = rarity < 5.0f && unlocked; // glow if less than 5% of players have it and you have it too std::stringstream stream; stream << std::fixed << std::setprecision(2) << rarity; std::string players = stream.str() + "% of players have this achievement"; @@ -853,10 +757,7 @@ void ra_achievement_list_t::initialize(ra_game_state_ptr game_state, { uint32_t id = achievement->id; ra_achievement_t* achievement_ptr = buckets[i].achievements[j].get(); - ra_state->download(game_state, url, [game_state, url, achievement_ptr]() { - atlas_tile_t* tile = &game_state->image_cache[url]; - achievement_ptr->tile = tile; - }); + achievement_ptr->tile = atlas_add_tile_from_url(game_state->atlas_map, url.c_str()); } printf("[rcheevos]: Achievement %s, ", achievement->title); @@ -874,160 +775,6 @@ void ra_achievement_list_t::initialize(ra_game_state_ptr game_state, rc_client_destroy_achievement_list(list); } -void ra_state_t::download(ra_game_state_ptr game_state, const std::string& url, - const std::function& callback) -{ - std::unique_lock lock(global_cache_mutex); - - if (download_cache.find(url) != download_cache.end()) - { - // Great, image was already downloaded in the past and is in the cache - // First, let's see if there's already an atlas for this image - if (game_state->image_cache.find(url) != game_state->image_cache.end()) - { - callback(); - return; - } - else - { - // We have the image downloaded, but we need to create an atlas for it - handle_downloaded(game_state, url); - callback(); - return; - } - } - lock.unlock(); - - game_state->inc(); - // The image is not already downloaded, let's download it - https_request(http_request_e::GET, url, {}, {}, - [url, game_state, callback](const std::vector& result) { - if (result.empty()) - { - printf("[rcheevos]: empty response from: %s\n", url.c_str()); - game_state->dec(); - return; - } - - rc_api_server_response_t response; - response.body = (const char*)result.data(); - response.body_length = result.size(); - response.http_status_code = 200; - - downloaded_image_t* image = new downloaded_image_t(); - image->data = - stbi_load_from_memory((const uint8_t*)response.body, response.body_length, - &image->width, &image->height, NULL, 4); - - if (!image->data) - { - printf("[rcheevos]: failed to load image from memory\n"); - } - else - { - std::unique_lock glock(game_state->mutex); - std::unique_lock lock(global_cache_mutex); - download_cache[url] = image; - ra_state->handle_downloaded(game_state, url); - callback(); - } - - game_state->dec(); - }); -} - -void ra_state_t::handle_downloaded(ra_game_state_ptr game_state, const std::string& url) -{ - downloaded_image_t* image = download_cache[url]; - atlas_t* atlas = nullptr; - - // Check if we already have an atlas for this exact tile size - for (atlas_t* a : game_state->atlases) - { - if (a->tile_width == image->width && a->tile_height == image->height) - { - atlas = a; - break; - } - } - - // Otherwise, create a new atlas - if (!atlas) - { - atlas_t* new_atlas = new atlas_t(image->width, image->height); - game_state->atlases.push_back(new_atlas); - atlas = new_atlas; - } - - // Check if we need to resize the atlas - uint32_t minimum_width = atlas->offset_x + atlas->tile_width + atlas_spacing; - uint32_t minimum_height = atlas->offset_y + atlas->tile_height + atlas_spacing; - - if (minimum_width > atlas->pixel_stride || minimum_height > atlas->pixel_stride) - { - // We need to resize and upload the atlas later - atlas->resized = true; - - // Find a sufficient power of two - uint32_t power = 2; - uint32_t max = std::max(minimum_width, minimum_height); - while (power < max) - { - power *= 2; - - if (power > 4096) - { - printf("Atlas too large\n"); - exit(1); - } - } - - uint32_t old_stride = atlas->pixel_stride; - atlas->pixel_stride = power; - atlas->offset_x = 0; - atlas->offset_y = 0; - - std::vector new_data; - new_data.resize(power * power * 4); - atlas->data.swap(new_data); - - // Copy every existing downloaded image of this size - for (auto& cached_image : game_state->image_cache) - { - if (cached_image.second.width == image->width && - cached_image.second.height == image->height) - { - auto& tile = game_state->image_cache[cached_image.first]; - uint32_t tile_offset_x = atlas->offset_x; - uint32_t tile_offset_y = atlas->offset_y; - tile.x1 = (float)tile_offset_x / atlas->pixel_stride; - tile.y1 = (float)tile_offset_y / atlas->pixel_stride; - tile.x2 = (float)(tile_offset_x + cached_image.second.width) / atlas->pixel_stride; - tile.y2 = (float)(tile_offset_y + cached_image.second.height) / atlas->pixel_stride; - - atlas->copy_image(download_cache[cached_image.first]); - } - } - } - - // At this point we should have an atlas that has enough room for our incoming - // tile - int offset_x = atlas->offset_x; - int offset_y = atlas->offset_y; - - atlas->copy_image(image); - - atlas_tile_t* tile = &game_state->image_cache[url]; - - tile->atlas_id = atlas->image.id; - tile->width = image->width; - tile->height = image->height; - tile->x1 = (float)offset_x / atlas->pixel_stride; - tile->y1 = (float)offset_y / atlas->pixel_stride; - tile->x2 = (float)(offset_x + image->width) / atlas->pixel_stride; - tile->y2 = (float)(offset_y + image->height) / atlas->pixel_stride; -} - void ra_state_t::rebuild_achievement_list(ra_game_state_ptr game_state) { game_state->achievement_list.initialize( @@ -1038,16 +785,6 @@ void ra_state_t::rebuild_achievement_list(ra_game_state_ptr game_state) RC_CLIENT_ACHIEVEMENT_LIST_GROUPING_PROGRESS)); } -ra_game_state_t::~ra_game_state_t() -{ - std::unique_lock lock(global_cache_mutex); - for (auto& atlas : atlases) - { - images_to_destroy.push_back(atlas->image); - delete atlas; - } -} - extern "C" uint32_t retro_achievements_read_memory_callback(uint32_t address, uint8_t* buffer, uint32_t num_bytes, rc_client_t* client); @@ -1129,19 +866,7 @@ void retro_achievements_shutdown() ra_state->game_state.reset(); - for (auto& image : images_to_destroy) - { - sg_destroy_image(image); - } - images_to_destroy.clear(); - delete ra_state; - - for (auto& download : download_cache) - { - stbi_image_free(download.second->data); - delete download.second; - } } bool retro_achievements_load_game() @@ -1154,9 +879,6 @@ bool retro_achievements_load_game() return false; // not logged in or login in progress, in which case the game will be loaded // when the login is done - if (ra_state->game_state && ra_state->game_state->outstanding_requests.load() != 0) - return false; - // the old one will be destroyed when the last reference is gone ra_state->game_state.reset(new ra_game_state_t()); @@ -1166,20 +888,17 @@ bool retro_achievements_load_game() switch (ra_state->emu_state->system) { case SYSTEM_GB: - (*game_state)->inc(); rc_client_begin_identify_and_load_game( ra_state->rc_client, RC_CONSOLE_GAMEBOY, NULL, ra_state->emu_state->rom_data, ra_state->emu_state->rom_size, retro_achievements_load_game_callback, game_state); break; case SYSTEM_GBA: - (*game_state)->inc(); rc_client_begin_identify_and_load_game( ra_state->rc_client, RC_CONSOLE_GAMEBOY_ADVANCE, NULL, ra_state->emu_state->rom_data, ra_state->emu_state->rom_size, retro_achievements_load_game_callback, game_state); break; case SYSTEM_NDS: - (*game_state)->inc(); rc_client_begin_identify_and_load_game( ra_state->rc_client, RC_CONSOLE_NINTENDO_DS, NULL, ra_state->emu_state->rom_data, ra_state->emu_state->rom_size, retro_achievements_load_game_callback, game_state); @@ -1248,81 +967,6 @@ atlas_tile_t* retro_achievements_get_user_image() return nullptr; } -void retro_achievements_update_atlases() -{ - if (!ra_state->game_state) - return; - - if (ra_state->game_state->outstanding_requests.load() != 0) - return; // probably a lot of outstanding requests hold the mutex, let's wait for them to - // finish before we try to lock ourselves to prevent stuttering - - std::unique_lock lock(ra_state->game_state->mutex); - - for (atlas_t* atlas : ra_state->game_state->atlases) - { - if (atlas->resized) - { - if (atlas->image.id != SG_INVALID_ID) - { - std::unique_lock glock(global_cache_mutex); - images_to_destroy.push_back(atlas->image); - } - atlas->image.id = SG_INVALID_ID; - } - - if (atlas->image.id == SG_INVALID_ID) - { - sg_image_desc desc = {0}; - desc.type = SG_IMAGETYPE_2D, desc.render_target = false, - desc.width = atlas->pixel_stride, desc.height = atlas->pixel_stride, - desc.num_slices = 1, desc.num_mipmaps = 1, desc.usage = SG_USAGE_DYNAMIC, - desc.pixel_format = SG_PIXELFORMAT_RGBA8, desc.sample_count = 1, - desc.min_filter = SG_FILTER_LINEAR, desc.mag_filter = SG_FILTER_LINEAR, - desc.wrap_u = SG_WRAP_CLAMP_TO_EDGE, desc.wrap_v = SG_WRAP_CLAMP_TO_EDGE, - desc.wrap_w = SG_WRAP_CLAMP_TO_EDGE, - desc.border_color = SG_BORDERCOLOR_TRANSPARENT_BLACK, desc.max_anisotropy = 1, - desc.min_lod = 0.0f, desc.max_lod = 1e9f, - - atlas->image = sg_make_image(&desc); - - if (atlas->resized) - { - for (auto& image : ra_state->game_state->image_cache) - { - // Update the images to point to the new atlas instead - if (image.second.width == atlas->tile_width && - image.second.height == atlas->tile_height) - { - image.second.atlas_id = atlas->image.id; - } - } - } - } - - if (atlas->dirty) - { - 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->image, data); - } - - atlas->dirty = false; - atlas->resized = false; - } -} - -void retro_achievements_delete_retired_atlases() -{ - std::unique_lock lock(global_cache_mutex); - for (auto& image : images_to_destroy) - { - sg_destroy_image(image); - } - images_to_destroy.clear(); -} - bool retro_achievements_has_game_loaded() { return rc_client_get_game_info(ra_state->rc_client)!=NULL; @@ -1438,11 +1082,13 @@ void retro_achievements_draw_notifications(float left, float top, float screen_w // Image, or a gray square if it's still loading ImVec2 img_top_left = {top_left.x + padding_adj, top_left.y + padding_adj}; ImVec2 img_bottom_right = {img_top_left.x + image_width - padding_adj, img_top_left.y + image_height - padding_adj}; - if (notification.tile && notification.tile->atlas_id != SG_INVALID_ID) + uint32_t id = atlas_get_tile_id(notification.tile); + if (notification.tile && id != SG_INVALID_ID) { - ImVec2 uv0 = {notification.tile->x1, notification.tile->y1}; - ImVec2 uv1 = {notification.tile->x2, notification.tile->y2}; - ImDrawList_AddImageRounded(ig, (ImTextureID)(intptr_t)notification.tile->atlas_id, img_top_left, img_bottom_right, uv0, uv1, 0xffffff | ALPHA(255), 8.0f, ImDrawCornerFlags_All); + atlas_uvs_t uvs = atlas_get_tile_uvs(notification.tile); + ImVec2 uv0 = {uvs.x1, uvs.y1}; + ImVec2 uv1 = {uvs.x2, uvs.y2}; + ImDrawList_AddImageRounded(ig, (ImTextureID)(intptr_t)id, img_top_left, img_bottom_right, uv0, uv1, 0xffffff | ALPHA(255), 8.0f, ImDrawCornerFlags_All); } else { @@ -1533,12 +1179,14 @@ void retro_achievements_draw_progress_indicator(float right, float top) ImVec2{x + placard_width - (padding / 2), y + placard_height - (padding / 2)}, 0x80000000, 8.0f, ImDrawCornerFlags_All, 2.0f); - if (indicator.tile && indicator.tile->atlas_id != SG_INVALID_ID) + uint32_t id = atlas_get_tile_id(indicator.tile); + if (indicator.tile && id != SG_INVALID_ID) { - ImVec2 uv0 = {indicator.tile->x1, indicator.tile->y1}; - ImVec2 uv1 = {indicator.tile->x2, indicator.tile->y2}; + atlas_uvs_t uvs = atlas_get_tile_uvs(indicator.tile); + ImVec2 uv0 = {uvs.x1, uvs.y1}; + ImVec2 uv1 = {uvs.x2, uvs.y2}; ImDrawList_AddImageRounded(igGetWindowDrawList(), - (ImTextureID)(intptr_t)indicator.tile->atlas_id, + (ImTextureID)(intptr_t)id, ImVec2{x + padding, y + padding}, ImVec2{x + padding + image_width, y + padding + image_height}, uv0, uv1, 0x80ffffff, 8.0f, ImDrawCornerFlags_All); @@ -1635,13 +1283,15 @@ void retro_achievements_draw_challenge_indicators(float right, float bottom) { const ra_challenge_indicator_t& challenge = item.second; - if (challenge.tile && challenge.tile->atlas_id != SG_INVALID_ID) + uint32_t id = atlas_get_tile_id(challenge.tile); + if (challenge.tile && id != SG_INVALID_ID) { + atlas_uvs_t uvs = atlas_get_tile_uvs(challenge.tile); ImDrawList_AddImage(igGetWindowDrawList(), - (ImTextureID)(intptr_t)challenge.tile->atlas_id, ImVec2{x, y}, + (ImTextureID)(intptr_t)id, ImVec2{x, y}, ImVec2{x + 32, y + 32}, - ImVec2{challenge.tile->x1, challenge.tile->y1}, - ImVec2{challenge.tile->x2, challenge.tile->y2}, 0x80ffffff); + ImVec2{uvs.x1, uvs.y1}, + ImVec2{uvs.x2, uvs.y2}, 0x80ffffff); } if (i++ % 3 != 2) diff --git a/src/retro_achievements.h b/src/retro_achievements.h index eac80a3b8..37e8e77ab 100644 --- a/src/retro_achievements.h +++ b/src/retro_achievements.h @@ -25,9 +25,9 @@ void retro_achievements_frame(); void retro_achievements_draw_panel(); -atlas_tile_t* retro_achievements_get_game_image(); +struct atlas_tile_t* retro_achievements_get_game_image(); -atlas_tile_t* retro_achievements_get_user_image(); +struct atlas_tile_t* retro_achievements_get_user_image(); void retro_achievements_login(const char* username, const char* password); @@ -37,10 +37,6 @@ struct rc_client_t* retro_achievements_get_client(); const char* retro_achievements_get_login_error(); -void retro_achievements_update_atlases(); - -void retro_achievements_delete_retired_atlases(); - void retro_achievements_keep_alive(); void retro_achievements_draw_notifications(float left, float top, float screen_width); From ea9b16b853188c7ae068f9d7f9a99cbc7fab5043 Mon Sep 17 00:00:00 2001 From: offtkp Date: Thu, 15 Aug 2024 22:26:03 +0300 Subject: [PATCH 6/8] Some cleanup --- src/atlas.cpp | 54 +++++++++++++++++++++++++++++++++++--- src/retro_achievements.cpp | 9 +++++++ 2 files changed, 60 insertions(+), 3 deletions(-) diff --git a/src/atlas.cpp b/src/atlas.cpp index a493857ad..f915ccfba 100644 --- a/src/atlas.cpp +++ b/src/atlas.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include @@ -30,6 +31,9 @@ std::unordered_map image_cache; std::mutex atlas_maps_mutex; std::vector atlas_maps; +std::mutex to_delete_mutex; +std::vector images_to_delete; + struct atlas_t { atlas_t(uint32_t tile_width, uint32_t tile_height); ~atlas_t(); @@ -71,6 +75,7 @@ struct atlas_t { struct atlas_map_t { atlas_tile_t* add_tile_from_url(const char* url); atlas_tile_t* add_tile_from_path(const char* path); + void wait_all(); void upload_all(); std::atomic_int requests = {0}; @@ -91,6 +96,7 @@ struct atlas_map_t { } std::mutex atlases_mutex; + std::mutex adding_mutex; std::unordered_map atlases; }; @@ -110,6 +116,15 @@ atlas_t::atlas_t(uint32_t tile_width, uint32_t tile_height) : tile_width(tile_wi data.resize(atlas_dimension * atlas_dimension * 4); } +atlas_t::~atlas_t() { + for (auto& pair : images) { + delete pair.second; + } + + std::unique_lock lock(to_delete_mutex); + images_to_delete.push_back(atlas_image); +} + void atlas_t::copy_to_data(atlas_tile_t* tile, cached_image_t* cached_image) { if (tile == nullptr) { atlas_error("Tile is null"); @@ -246,20 +261,28 @@ void atlas_t::upload() { } atlas_tile_t* atlas_map_t::add_tile_from_url(const char* url) { + std::unique_lock lock(adding_mutex); const std::string url_str(url); { std::unique_lock lock(image_cache_mutex); if (image_cache.find(url_str) != image_cache.end()) { cached_image_t* cached_image = image_cache[url_str]; + lock.unlock(); + atlas_t* atlas = get_atlas(cached_image->width, cached_image->height); atlas_tile_t* tile = nullptr; if (atlas->has_tile(url_str)) { - atlas->get_tile(url_str); + tile = atlas->get_tile(url_str); } else { tile = new atlas_tile_t(); atlas->add_tile(url_str, tile, cached_image); } + + if (tile == nullptr) { + atlas_error("Tile is null"); + } + return tile; } } @@ -297,17 +320,26 @@ atlas_tile_t* atlas_map_t::add_tile_from_url(const char* url) { } requests--; - printf("Outstanding requests: %d\n", requests.load()); }); return tile; } atlas_tile_t* atlas_map_t::add_tile_from_path(const char* path) { + std::unique_lock lock(adding_mutex); atlas_error("Not implemented"); return nullptr; } +void atlas_map_t::wait_all() { + // this mutex solely exists to wait for the add_* functions to finish and begin their requests + // it's realistically very unlikely anything bad would happen without it but possible + std::unique_lock lock(adding_mutex); + while (requests > 0) { + std::this_thread::sleep_for(std::chrono::milliseconds(15)); + } +} + void atlas_map_t::upload_all() { std::unique_lock lock(atlases_mutex); for (auto& pair : atlases) { @@ -332,10 +364,19 @@ void atlas_destroy_map(atlas_map_t* map) { atlas_maps.erase(it); } } - delete map; + + std::thread delete_thread([map] { + map->wait_all(); + delete map; + }); + delete_thread.detach(); } atlas_tile_t* atlas_add_tile_from_url(atlas_map_t* map, const char* url) { + if (map == nullptr) { + atlas_error("Map is null"); + } + return map->add_tile_from_url(url); } @@ -353,6 +394,13 @@ void atlas_upload_all() { map->upload_all(); } + lock.unlock(); + + std::unique_lock dlock(to_delete_mutex); + for (sg_image image : images_to_delete) { + sg_destroy_image(image); + } + images_to_delete.clear(); } uint32_t atlas_get_tile_id(atlas_tile_t* tile) { diff --git a/src/retro_achievements.cpp b/src/retro_achievements.cpp index 1f89ffc76..ab73bf0e0 100644 --- a/src/retro_achievements.cpp +++ b/src/retro_achievements.cpp @@ -59,6 +59,8 @@ struct ra_game_state_t; using ra_game_state_ptr = std::shared_ptr; +std::atomic_bool loading_game = { false }; + struct ra_achievement_t { atlas_tile_t* tile = nullptr; @@ -378,6 +380,7 @@ namespace } delete game_state_ptr; // delete the pointer that was allocated to pass through ffi + loading_game = false; } void retro_achievements_download_user_image(const std::string& url) @@ -530,6 +533,7 @@ namespace ra_game_state_ptr game_state = ra_state->game_state; game_state->progress_indicator.show = true; retro_achievements_progress_indicator_updated(game_state, event->achievement); + break; } case RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_UPDATE: { @@ -879,12 +883,17 @@ bool retro_achievements_load_game() return false; // not logged in or login in progress, in which case the game will be loaded // when the login is done + if (loading_game) + return false; + // the old one will be destroyed when the last reference is gone ra_state->game_state.reset(new ra_game_state_t()); // We need to create a shared_ptr*, so we can pass it to the C api. ra_game_state_ptr* game_state = new ra_game_state_ptr(ra_state->game_state); + loading_game = true; + switch (ra_state->emu_state->system) { case SYSTEM_GB: From 60d14636cbd77037d4fd362b23a8378c5165a283 Mon Sep 17 00:00:00 2001 From: offtkp Date: Thu, 15 Aug 2024 22:35:50 +0300 Subject: [PATCH 7/8] User image --- src/retro_achievements.cpp | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/retro_achievements.cpp b/src/retro_achievements.cpp index ab73bf0e0..278cb23ff 100644 --- a/src/retro_achievements.cpp +++ b/src/retro_achievements.cpp @@ -147,6 +147,8 @@ struct ra_state_t std::atomic error_message = { nullptr }; sb_emu_state_t* emu_state = nullptr; rc_client_t* rc_client = nullptr; + atlas_map_t* user_image_atlas = nullptr; + atlas_tile_t* user_image = nullptr; std::atomic_bool pending_login = { false }; @@ -383,11 +385,6 @@ namespace loading_game = false; } - void retro_achievements_download_user_image(const std::string& url) - { - // TODO: implement me, requires generalizing atlas stuff - } - void retro_achievements_login_callback(int result, const char* error_message, rc_client_t* client, void* userdata) { @@ -413,7 +410,7 @@ namespace url.resize(256); if (rc_client_user_get_image_url(user, &url[0], url.size()) == RC_OK) { - retro_achievements_download_user_image(url); + ra_state->user_image = atlas_add_tile_from_url(ra_state->user_image_atlas, url.c_str()); } } else { snprintf(buffer, sizeof(buffer), "Login failed: %s", error_message); @@ -801,6 +798,7 @@ void retro_achievements_initialize(void* state, bool hardcore, bool is_mobile) ra_state = new ra_state_t((sb_emu_state_t*)state); ra_state->rc_client = rc_client_create(retro_achievements_read_memory_callback, retro_achievements_server_callback); + ra_state->user_image_atlas = atlas_create_map(); rc_client_enable_logging(ra_state->rc_client, RC_CLIENT_LOG_LEVEL_VERBOSE, retro_achievements_log_callback); @@ -973,7 +971,7 @@ atlas_tile_t* retro_achievements_get_game_image() atlas_tile_t* retro_achievements_get_user_image() { - return nullptr; + return ra_state->user_image; } bool retro_achievements_has_game_loaded() From 183d85a5b8bbe9bf5e4e14c7d5c28b651cf0044d Mon Sep 17 00:00:00 2001 From: offtkp Date: Fri, 16 Aug 2024 01:48:20 +0300 Subject: [PATCH 8/8] Fix some bugs --- src/atlas.cpp | 118 +++++++++++++++++++++++--------------------------- src/main.c | 3 +- 2 files changed, 55 insertions(+), 66 deletions(-) diff --git a/src/atlas.cpp b/src/atlas.cpp index f915ccfba..653dd987e 100644 --- a/src/atlas.cpp +++ b/src/atlas.cpp @@ -35,28 +35,16 @@ std::mutex to_delete_mutex; std::vector images_to_delete; struct atlas_t { - atlas_t(uint32_t tile_width, uint32_t tile_height); + atlas_t(atlas_map_t* map, uint32_t tile_width, uint32_t tile_height); ~atlas_t(); - atlas_tile_t* get_tile(const std::string& url) { - std::unique_lock lock(mutex); - if (images.find(url) == images.end()) { - atlas_error("Tile not found"); - } - - atlas_tile_t* tile = images[url]; - return tile; - } - bool has_tile(const std::string& url) { - std::unique_lock lock(mutex); - return images.find(url) != images.end(); - } void add_tile(const std::string& url, atlas_tile_t* tile, cached_image_t* cached_image); void upload(); private: void copy_to_data(atlas_tile_t* tile, cached_image_t* image); + atlas_map_t* map; const uint32_t tile_width, tile_height; std::mutex mutex; @@ -64,8 +52,7 @@ struct atlas_t { uint32_t atlas_dimension; uint32_t offset_x, offset_y; std::vector data; - std::unordered_map images; - std::vector images_to_add; + std::vector image_urls; bool dirty; // new data needs to be uploaded to the GPU bool resized; // atlas needs to be destroyed and created at new size @@ -73,34 +60,49 @@ struct atlas_t { }; struct atlas_map_t { + ~atlas_map_t() { + for (auto& pair : atlases) { + delete pair.second; + } + + for (auto& pair : total_tiles) { + delete pair.second; + } + } + atlas_tile_t* add_tile_from_url(const char* url); atlas_tile_t* add_tile_from_path(const char* path); void wait_all(); void upload_all(); + std::mutex atlases_mutex; std::atomic_int requests = {0}; + // When downloading images we don't know what size they will be until they are downloaded, so we don't know what atlas to put them in + // This means we need to map urls->atlas_tile_t before we have the image data + // Creating an atlas_tile_t without mapping it here would mean that if two threads tried to download the same image + // at the same time, they would both create a new atlas_tile_t and map it later and it would result in a race. + // With this, the second thread will see that the tile already exists and return that instead. + std::unordered_map total_tiles; + private: atlas_t* get_atlas(uint32_t tile_width, uint32_t tile_height) { - std::unique_lock lock(atlases_mutex); uint32_t key = tile_width << 16 | tile_height; atlas_t* atlas = atlases[key]; if (atlas == nullptr) { - atlas = new atlas_t(tile_width, tile_height); + atlas = new atlas_t(this, tile_width, tile_height); atlases[key] = atlas; } return atlas; } - std::mutex atlases_mutex; - std::mutex adding_mutex; std::unordered_map atlases; }; -atlas_t::atlas_t(uint32_t tile_width, uint32_t tile_height) : tile_width(tile_width), tile_height(tile_height) { +atlas_t::atlas_t(atlas_map_t* map, uint32_t tile_width, uint32_t tile_height) : map(map), tile_width(tile_width), tile_height(tile_height) { atlas_image.id = SG_INVALID_ID; offset_x = 0; offset_y = 0; @@ -117,10 +119,6 @@ atlas_t::atlas_t(uint32_t tile_width, uint32_t tile_height) : tile_width(tile_wi } atlas_t::~atlas_t() { - for (auto& pair : images) { - delete pair.second; - } - std::unique_lock lock(to_delete_mutex); images_to_delete.push_back(atlas_image); } @@ -171,6 +169,12 @@ void atlas_t::copy_to_data(atlas_tile_t* tile, cached_image_t* cached_image) { void atlas_t::add_tile(const std::string& url, atlas_tile_t* tile, cached_image_t* cached_image) { std::unique_lock lock(mutex); + if (std::find(image_urls.begin(), image_urls.end(), url) != image_urls.end()) { + atlas_error("Image already added to atlas"); + } + + image_urls.push_back(url); + // These are the dimensions that would occur after adding a tile uint32_t minimum_x = offset_x + tile_width + padding; uint32_t minimum_y = offset_y + tile_height + padding; @@ -187,16 +191,17 @@ void atlas_t::add_tile(const std::string& url, atlas_tile_t* tile, cached_image_ offset_x = 0; offset_y = 0; - for (auto& pair : images) { - images_to_add.push_back(pair.first); + std::unique_lock lock(image_cache_mutex); + for (auto& image_url : image_urls) { + atlas_tile_t* old_tile = map->total_tiles[image_url]; + cached_image_t* old_cached_image = image_cache[image_url]; + copy_to_data(old_tile, old_cached_image); } } float current_x = offset_x; float current_y = offset_y; - images[url] = tile; - copy_to_data(tile, cached_image); } @@ -205,6 +210,7 @@ void atlas_t::upload() { if (resized) { sg_destroy_image(atlas_image); atlas_image.id = SG_INVALID_ID; + resized = false; } if (atlas_image.id == SG_INVALID_ID) { @@ -230,23 +236,8 @@ void atlas_t::upload() { atlas_image = sg_make_image(desc); - if (resized) { - { - std::unique_lock lock(image_cache_mutex); - for (const std::string& image_url : images_to_add) { - cached_image_t* cached_image = image_cache[image_url]; - atlas_tile_t* tile = images[image_url]; - copy_to_data(tile, cached_image); - } - } - - images_to_add.clear(); - - resized = false; - } - - for (auto& pair : images) { - atlas_tile_t* tile = pair.second; + for (auto& image_url : image_urls) { + atlas_tile_t* tile = map->total_tiles[image_url]; tile->atlas_id = atlas_image.id; } } @@ -261,39 +252,38 @@ void atlas_t::upload() { } atlas_tile_t* atlas_map_t::add_tile_from_url(const char* url) { - std::unique_lock lock(adding_mutex); + std::unique_lock lock(atlases_mutex); const std::string url_str(url); + // same image can be requested multiple times, so we need to check if it's already in *some* atlas + if (total_tiles.find(url_str) != total_tiles.end()) { + return total_tiles[url_str]; + } + { std::unique_lock lock(image_cache_mutex); if (image_cache.find(url_str) != image_cache.end()) { cached_image_t* cached_image = image_cache[url_str]; lock.unlock(); + // If this is reached, the image is in our download cache but not in any atlas + // This can happen if you restart a game for example, so we add it to an atlas + // We know the dimensions of the image, so we can add it to the correct atlas immediately atlas_t* atlas = get_atlas(cached_image->width, cached_image->height); - atlas_tile_t* tile = nullptr; - if (atlas->has_tile(url_str)) { - tile = atlas->get_tile(url_str); - } else { - tile = new atlas_tile_t(); - atlas->add_tile(url_str, tile, cached_image); - } - - if (tile == nullptr) { - atlas_error("Tile is null"); - } - + atlas_tile_t* tile = new atlas_tile_t(); + total_tiles[url_str] = tile; + atlas->add_tile(url_str, tile, cached_image); return tile; } } atlas_tile_t* tile = new atlas_tile_t(); + total_tiles[url_str] = tile; requests++; https_request(http_request_e::GET, url_str, {}, {}, [this, url_str, tile] (const std::vector& result) { if (result.empty()) { printf("Failed to download image for atlas\n"); - delete tile; requests--; return; } @@ -307,7 +297,6 @@ atlas_tile_t* atlas_map_t::add_tile_from_url(const char* url) { if (!cached_image->data) { printf("Failed to load image for atlas\n"); - delete tile; delete cached_image; } else { { @@ -315,6 +304,7 @@ atlas_tile_t* atlas_map_t::add_tile_from_url(const char* url) { image_cache[url_str] = cached_image; } + std::unique_lock lock(atlases_mutex); atlas_t* atlas = get_atlas(cached_image->width, cached_image->height); atlas->add_tile(url_str, tile, cached_image); } @@ -326,15 +316,13 @@ atlas_tile_t* atlas_map_t::add_tile_from_url(const char* url) { } atlas_tile_t* atlas_map_t::add_tile_from_path(const char* path) { - std::unique_lock lock(adding_mutex); + std::unique_lock lock(atlases_mutex); atlas_error("Not implemented"); return nullptr; } void atlas_map_t::wait_all() { - // this mutex solely exists to wait for the add_* functions to finish and begin their requests - // it's realistically very unlikely anything bad would happen without it but possible - std::unique_lock lock(adding_mutex); + std::unique_lock lock(atlases_mutex); while (requests > 0) { std::this_thread::sleep_for(std::chrono::milliseconds(15)); } diff --git a/src/main.c b/src/main.c index 1daf05ca7..e1a89c8a3 100644 --- a/src/main.c +++ b/src/main.c @@ -7333,7 +7333,8 @@ static void frame(void) { gui_state.menubar_hide_timer=se_time(); } #ifdef ENABLE_RETRO_ACHIEVEMENTS - if(gui_state.retro_achievements_sidebar_open){ + bool logged_in = rc_client_get_user_info(retro_achievements_get_client()); + if(gui_state.retro_achievements_sidebar_open&&logged_in){ igSetNextWindowPos((ImVec2){screen_x,menu_height}, ImGuiCond_Always, (ImVec2){0,0}); igSetNextWindowSize((ImVec2){sidebar_w, (gui_state.screen_height-menu_height*se_dpi_scale())/se_dpi_scale()}, ImGuiCond_Always); igBegin(se_localize_and_cache(ICON_FK_TROPHY " RetroAchievements"),&gui_state.retro_achievements_sidebar_open, ImGuiWindowFlags_NoCollapse|ImGuiWindowFlags_NoResize);