diff --git a/CMakeLists.txt b/CMakeLists.txt index df7fbcb..5a32d38 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -178,6 +178,7 @@ set (SOURCES ${CMAKE_SOURCE_DIR}/src/ui/ui_mod_menu.cpp ${CMAKE_SOURCE_DIR}/src/ui/ui_api.cpp ${CMAKE_SOURCE_DIR}/src/ui/ui_api_events.cpp + ${CMAKE_SOURCE_DIR}/src/ui/ui_api_images.cpp ${CMAKE_SOURCE_DIR}/src/ui/util/hsv.cpp ${CMAKE_SOURCE_DIR}/src/ui/core/ui_context.cpp ${CMAKE_SOURCE_DIR}/src/ui/elements/ui_button.cpp diff --git a/include/recomp_ui.h b/include/recomp_ui.h index a41f6a1..acae932 100644 --- a/include/recomp_ui.h +++ b/include/recomp_ui.h @@ -110,7 +110,9 @@ namespace recompui { Rml::ElementPtr create_custom_element(Rml::Element* parent, std::string tag); Rml::ElementDocument* load_document(const std::filesystem::path& path); Rml::ElementDocument* create_empty_document(); - void queue_image_from_bytes(const std::string &src, const std::vector &bytes); + + void queue_image_from_bytes_rgba32(const std::string &src, const std::vector &bytes, uint32_t width, uint32_t height); + void queue_image_from_bytes_file(const std::string &src, const std::vector &bytes); void release_image(const std::string &src); } diff --git a/src/ui/ui_api.cpp b/src/ui/ui_api.cpp index 7e029da..cd48a84 100644 --- a/src/ui/ui_api.cpp +++ b/src/ui/ui_api.cpp @@ -1,5 +1,8 @@ #include "recomp_ui.h" +#include "ui_helpers.h" +#include "ui_api_images.h" + #include "core/ui_context.h" #include "core/ui_resource.h" @@ -25,80 +28,6 @@ using namespace recompui; -constexpr ResourceId root_element_id{ 0xFFFFFFFE }; - -// Helpers - -ContextId get_context(uint8_t* rdram, recomp_context* ctx) { - uint32_t context_id = _arg<0, uint32_t>(rdram, ctx); - return ContextId{ .slot_id = context_id }; -} - -template -ResourceId arg_resource_id(uint8_t* rdram, recomp_context* ctx) { - uint32_t slot_id = _arg(rdram, ctx); - - return ResourceId{ .slot_id = slot_id }; -} - -template -Element* arg_element(uint8_t* rdram, recomp_context* ctx, ContextId ui_context) { - ResourceId resource = arg_resource_id(rdram, ctx); - - if (resource == ResourceId::null()) { - return nullptr; - } - else if (resource == root_element_id) { - return ui_context.get_root_element(); - } - - return resource.as_element(); -} - -template -Style* arg_style(uint8_t* rdram, recomp_context* ctx) { - ResourceId resource = arg_resource_id(rdram, ctx); - - if (resource == ResourceId::null()) { - return nullptr; - } - else if (resource == root_element_id) { - ContextId ui_context = recompui::get_current_context(); - return ui_context.get_root_element(); - } - - return *resource; -} - -template -Color arg_color(uint8_t* rdram, recomp_context* ctx) { - PTR(u8) color_arg = _arg(rdram, ctx); - - Color ret{}; - - ret.r = MEM_B(0, color_arg); - ret.g = MEM_B(1, color_arg); - ret.b = MEM_B(2, color_arg); - ret.a = MEM_B(3, color_arg); - - return ret; -} - -void return_resource(recomp_context* ctx, ResourceId resource) { - _return(ctx, resource.slot_id); -} - -void return_string(uint8_t* rdram, recomp_context* ctx, const std::string& ret) { - gpr addr = (reinterpret_cast(recomp::alloc(rdram, ret.size() + 1)) - rdram) + 0xFFFFFFFF80000000ULL; - - for (size_t i = 0; i < ret.size(); i++) { - MEM_B(i, addr) = ret[i]; - } - MEM_B(ret.size(), addr) = '\x00'; - - _return(ctx, addr); -} - // Contexts void recompui_create_context(uint8_t* rdram, recomp_context* ctx) { (void)rdram; @@ -915,4 +844,5 @@ void recompui::register_ui_exports() { REGISTER_FUNC(recompui_get_input_text); REGISTER_FUNC(recompui_set_input_text); REGISTER_FUNC(recompui_register_callback); + register_ui_image_exports(); } diff --git a/src/ui/ui_api_images.cpp b/src/ui/ui_api_images.cpp new file mode 100644 index 0000000..8c7d412 --- /dev/null +++ b/src/ui/ui_api_images.cpp @@ -0,0 +1,108 @@ +#include +#include + +#include "recomp_ui.h" +#include "librecomp/overlays.hpp" +#include "librecomp/helpers.hpp" +#include "ultramodern/error_handling.hpp" + +#include "ui_helpers.h" +#include "ui_api_images.h" +#include "elements/ui_image.h" + +using namespace recompui; + +struct { + std::mutex mutex; + std::unordered_set textures{}; + uint32_t textures_created = 0; +} TextureState; + +const std::string mod_texture_prefix = "?/mod_api/"; + +static std::string get_texture_name(uint32_t texture_id) { + return mod_texture_prefix + std::to_string(texture_id); +} + +static uint32_t get_new_texture_id() { + std::lock_guard lock{TextureState.mutex}; + uint32_t cur_id = TextureState.textures_created++; + TextureState.textures.emplace(cur_id); + + return cur_id; +} + +static void release_texture(uint32_t texture_id) { + std::string texture_name = get_texture_name(texture_id); + std::lock_guard lock{TextureState.mutex}; + + if (TextureState.textures.erase(texture_id) == 0) { + recompui::message_box("Fatal error in mod - attempted to destroy texture that doesn't exist!"); + assert(false); + ultramodern::error_handling::quick_exit(__FILE__, __LINE__, __FUNCTION__); + } + + recompui::release_image(texture_name); +} + +void recompui_create_texture_rgba32(uint8_t* rdram, recomp_context* ctx) { + thread_local std::vector data; + PTR(void) data_in = _arg<0, PTR(void)>(rdram, ctx); + uint32_t width = _arg<1, uint32_t>(rdram, ctx); + uint32_t height = _arg<2, uint32_t>(rdram, ctx); + uint32_t cur_id = get_new_texture_id(); + + // The size in bytes of the image's pixel data. + size_t size_bytes = width * height * 4 * sizeof(uint8_t); + data.resize(size_bytes); + + // Byteswap copy the pixel data. + for (size_t i = 0; i < size_bytes; i++) { + data[i] = MEM_B(i, data_in); + } + + // Create a texture name from the ID and queue its bytes. + std::string texture_name = get_texture_name(cur_id); + recompui::queue_image_from_bytes_rgba32(texture_name, data, width, height); + + // Return the new texture ID. + _return(ctx, cur_id); +} + +void recompui_destroy_texture(uint8_t* rdram, recomp_context* ctx) { + uint32_t texture_id = _arg<0, uint32_t>(rdram, ctx); + + release_texture(texture_id); +} + +void recompui_create_imageview(uint8_t* rdram, recomp_context* ctx) { + ContextId ui_context = get_context(rdram, ctx); + Element* parent = arg_element<1>(rdram, ctx, ui_context); + uint32_t texture_id = _arg<2, uint32_t>(rdram, ctx); + + Element* ret = ui_context.create_element(parent, get_texture_name(texture_id)); + return_resource(ctx, ret->get_resource_id()); +} + +void recompui_set_imageview_texture(uint8_t* rdram, recomp_context* ctx) { + Style* resource = arg_style<0>(rdram, ctx); + uint32_t texture_id = _arg<1, uint32_t>(rdram, ctx); + + if (!resource->is_element()) { + recompui::message_box("Fatal error in mod - attempted to set texture of non-element"); + assert(false); + ultramodern::error_handling::quick_exit(__FILE__, __LINE__, __FUNCTION__); + } + + Element* element = static_cast(resource); + element->set_src(get_texture_name(texture_id)); +} + +#define REGISTER_FUNC(name) recomp::overlays::register_base_export(#name, name) + +void recompui::register_ui_image_exports() { + REGISTER_FUNC(recompui_create_texture_rgba32); + REGISTER_FUNC(recompui_destroy_texture); + REGISTER_FUNC(recompui_create_imageview); + REGISTER_FUNC(recompui_set_imageview_texture); +} diff --git a/src/ui/ui_api_images.h b/src/ui/ui_api_images.h new file mode 100644 index 0000000..59a8a8b --- /dev/null +++ b/src/ui/ui_api_images.h @@ -0,0 +1,10 @@ +#ifndef __UI_API_IMAGES_H__ +#define __UI_API_IMAGES_H__ + +#include + +namespace recompui { + void register_ui_image_exports(); +} + +#endif diff --git a/src/ui/ui_helpers.h b/src/ui/ui_helpers.h new file mode 100644 index 0000000..5ba9ed5 --- /dev/null +++ b/src/ui/ui_helpers.h @@ -0,0 +1,87 @@ +#ifndef __UI_HELPERS_H__ +#define __UI_HELPERS_H__ + +#include "librecomp/helpers.hpp" +#include "librecomp/addresses.hpp" + +#include "elements/ui_element.h" +#include "elements/ui_types.h" +#include "core/ui_context.h" +#include "core/ui_resource.h" + +namespace recompui { + +constexpr ResourceId root_element_id{ 0xFFFFFFFE }; + +inline ContextId get_context(uint8_t* rdram, recomp_context* ctx) { + uint32_t context_id = _arg<0, uint32_t>(rdram, ctx); + return ContextId{ .slot_id = context_id }; +} + +template +ResourceId arg_resource_id(uint8_t* rdram, recomp_context* ctx) { + uint32_t slot_id = _arg(rdram, ctx); + + return ResourceId{ .slot_id = slot_id }; +} + +template +Element* arg_element(uint8_t* rdram, recomp_context* ctx, ContextId ui_context) { + ResourceId resource = arg_resource_id(rdram, ctx); + + if (resource == ResourceId::null()) { + return nullptr; + } + else if (resource == root_element_id) { + return ui_context.get_root_element(); + } + + return resource.as_element(); +} + +template +Style* arg_style(uint8_t* rdram, recomp_context* ctx) { + ResourceId resource = arg_resource_id(rdram, ctx); + + if (resource == ResourceId::null()) { + return nullptr; + } + else if (resource == root_element_id) { + ContextId ui_context = recompui::get_current_context(); + return ui_context.get_root_element(); + } + + return *resource; +} + +template +Color arg_color(uint8_t* rdram, recomp_context* ctx) { + PTR(u8) color_arg = _arg(rdram, ctx); + + Color ret{}; + + ret.r = MEM_B(0, color_arg); + ret.g = MEM_B(1, color_arg); + ret.b = MEM_B(2, color_arg); + ret.a = MEM_B(3, color_arg); + + return ret; +} + +inline void return_resource(recomp_context* ctx, ResourceId resource) { + _return(ctx, resource.slot_id); +} + +inline void return_string(uint8_t* rdram, recomp_context* ctx, const std::string& ret) { + gpr addr = (reinterpret_cast(recomp::alloc(rdram, ret.size() + 1)) - rdram) + 0xFFFFFFFF80000000ULL; + + for (size_t i = 0; i < ret.size(); i++) { + MEM_B(i, addr) = ret[i]; + } + MEM_B(ret.size(), addr) = '\x00'; + + _return(ctx, addr); +} +} + +#endif \ No newline at end of file diff --git a/src/ui/ui_mod_menu.cpp b/src/ui/ui_mod_menu.cpp index 57ecaa4..206c5de 100644 --- a/src/ui/ui_mod_menu.cpp +++ b/src/ui/ui_mod_menu.cpp @@ -441,7 +441,7 @@ void ModMenu::create_mod_list() { const std::vector &thumbnail = recomp::mods::get_mod_thumbnail(mod_details[mod_index].mod_id); std::string thumbnail_name = generate_thumbnail_src_for_mod(mod_details[mod_index].mod_id); if (!thumbnail.empty()) { - recompui::queue_image_from_bytes(thumbnail_name, thumbnail); + recompui::queue_image_from_bytes_file(thumbnail_name, thumbnail); loaded_thumbnails.emplace(thumbnail_name); } diff --git a/src/ui/ui_renderer.cpp b/src/ui/ui_renderer.cpp index cac34bc..5b35658 100644 --- a/src/ui/ui_renderer.cpp +++ b/src/ui/ui_renderer.cpp @@ -72,7 +72,19 @@ T from_bytes_le(const char* input) { return *reinterpret_cast(input); } -typedef std::pair> ImageFromBytes; +enum class ImageType { + File, + RGBA32 +}; + +struct ImageFromBytes { + ImageType type; + // Dimensions only used for RGBA32 data. Files pull the size from the file data. + uint32_t width; + uint32_t height; + std::string name; + std::vector bytes; +}; namespace recompui { class RmlRenderInterface_RT64_impl : public Rml::RenderInterfaceCompatibility { @@ -138,7 +150,7 @@ class RmlRenderInterface_RT64_impl : public Rml::RenderInterfaceCompatibility { bool scissor_enabled_ = false; std::vector> stale_buffers_{}; moodycamel::ConcurrentQueue image_from_bytes_queue; - std::unordered_map> image_from_bytes_map; + std::unordered_map image_from_bytes_map; public: RmlRenderInterface_RT64_impl(RT64::RenderInterface* interface, RT64::RenderDevice* device) { interface_ = interface; @@ -415,11 +427,30 @@ public: return true; } - // TODO: This data copy can be avoided when RT64::TextureCache::loadTextureFromBytes's function is updated to only take a pointer and size as the input. - std::vector data_copy(it->second.data(), it->second.data() + it->second.size()); + RT64::Texture* texture = nullptr; std::unique_ptr texture_buffer; + ImageFromBytes& img = it->second; copy_command_list_->begin(); - RT64::Texture *texture = RT64::TextureCache::loadTextureFromBytes(device_, copy_command_list_.get(), data_copy, texture_buffer); + + switch (img.type) { + case ImageType::RGBA32: + { + // Read the image header (two 32-bit values for width and height respectively). + uint32_t rowPitch = img.width * 4; + size_t byteCount = img.height * rowPitch; + texture = new RT64::Texture(); + RT64::TextureCache::setRGBA32(texture, device_, copy_command_list_.get(), reinterpret_cast(img.bytes.data()), byteCount, img.width, img.height, rowPitch, texture_buffer, nullptr); + } + break; + case ImageType::File: + { + // TODO: This data copy can be avoided when RT64::TextureCache::loadTextureFromBytes's function is updated to only take a pointer and size as the input. + std::vector data_copy(img.bytes.data(), img.bytes.data() + img.bytes.size()); + texture = RT64::TextureCache::loadTextureFromBytes(device_, copy_command_list_.get(), data_copy, texture_buffer); + } + break; + } + copy_command_list_->end(); copy_command_queue_->executeCommandLists(copy_command_list_.get(), copy_command_fence_.get()); copy_command_queue_->waitForCommandFence(copy_command_fence_.get()); @@ -626,14 +657,21 @@ public: list_ = nullptr; } - void queue_image_from_bytes(const std::string &src, const std::vector &bytes) { - image_from_bytes_queue.enqueue(ImageFromBytes(src, bytes)); + void queue_image_from_bytes_file(const std::string &src, const std::vector &bytes) { + // Width and height aren't used for file images, so set them to 0. + image_from_bytes_queue.enqueue(ImageFromBytes(ImageType::File, 0, 0, src, bytes)); + } + + void queue_image_from_bytes_rgba32(const std::string &src, const std::vector &bytes, uint32_t width, uint32_t height) { + image_from_bytes_queue.enqueue(ImageFromBytes(ImageType::RGBA32, width, height, src, bytes)); } void flush_image_from_bytes_queue() { ImageFromBytes image_from_bytes; while (image_from_bytes_queue.try_dequeue(image_from_bytes)) { - image_from_bytes_map.emplace(image_from_bytes.first, std::move(image_from_bytes.second)); + // We can move the name into the map since the name in the actual entry is no longer needed. + // After that, move the entry itself into the map. + image_from_bytes_map.emplace(std::move(image_from_bytes.name), std::move(image_from_bytes)); } } }; @@ -669,8 +707,14 @@ void recompui::RmlRenderInterface_RT64::end(RT64::RenderCommandList* list, RT64: impl->end(list, framebuffer); } -void recompui::RmlRenderInterface_RT64::queue_image_from_bytes(const std::string &src, const std::vector &bytes) { +void recompui::RmlRenderInterface_RT64::queue_image_from_bytes_file(const std::string &src, const std::vector &bytes) { assert(static_cast(impl)); - impl->queue_image_from_bytes(src, bytes); -} \ No newline at end of file + impl->queue_image_from_bytes_file(src, bytes); +} + +void recompui::RmlRenderInterface_RT64::queue_image_from_bytes_rgba32(const std::string &src, const std::vector &bytes, uint32_t width, uint32_t height) { + assert(static_cast(impl)); + + impl->queue_image_from_bytes_rgba32(src, bytes, width, height); +} diff --git a/src/ui/ui_renderer.h b/src/ui/ui_renderer.h index 0382c48..83a6912 100644 --- a/src/ui/ui_renderer.h +++ b/src/ui/ui_renderer.h @@ -2,6 +2,7 @@ #define __UI_RENDERER_H__ #include +#include "recomp_ui.h" namespace RT64 { struct RenderInterface; @@ -29,7 +30,8 @@ namespace recompui { void start(RT64::RenderCommandList* list, int image_width, int image_height); void end(RT64::RenderCommandList* list, RT64::RenderFramebuffer* framebuffer); - void queue_image_from_bytes(const std::string &src, const std::vector &bytes); + void queue_image_from_bytes_file(const std::string &src, const std::vector &bytes); + void queue_image_from_bytes_rgba32(const std::string &src, const std::vector &bytes, uint32_t width, uint32_t height); }; } // namespace recompui diff --git a/src/ui/ui_state.cpp b/src/ui/ui_state.cpp index 28bf055..0a04c57 100644 --- a/src/ui/ui_state.cpp +++ b/src/ui/ui_state.cpp @@ -822,8 +822,12 @@ Rml::ElementDocument* recompui::create_empty_document() { return ui_state->context->CreateDocument(); } -void recompui::queue_image_from_bytes(const std::string &src, const std::vector &bytes) { - ui_state->render_interface.queue_image_from_bytes(src, bytes); +void recompui::queue_image_from_bytes_file(const std::string &src, const std::vector &bytes) { + ui_state->render_interface.queue_image_from_bytes_file(src, bytes); +} + +void recompui::queue_image_from_bytes_rgba32(const std::string &src, const std::vector &bytes, uint32_t width, uint32_t height) { + ui_state->render_interface.queue_image_from_bytes_rgba32(src, bytes, width, height); } void recompui::release_image(const std::string &src) {