diff --git a/include/recomp_ui.h b/include/recomp_ui.h index d4dc290..8d127fd 100644 --- a/include/recomp_ui.h +++ b/include/recomp_ui.h @@ -109,6 +109,8 @@ 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 release_image(const std::string &src); } #endif diff --git a/src/ui/elements/ui_element.cpp b/src/ui/elements/ui_element.cpp index 8ba9d21..53e437a 100644 --- a/src/ui/elements/ui_element.cpp +++ b/src/ui/elements/ui_element.cpp @@ -252,6 +252,10 @@ void Element::set_text(std::string_view text) { base->SetInnerRML(std::string(text)); } +void Element::set_src(std::string_view src) { + base->SetAttribute("src", std::string(src)); +} + void Element::set_style_enabled(std::string_view style_name, bool enable) { if (enable && style_active_set.find(style_name) == style_active_set.end()) { // Style was disabled and will be enabled. diff --git a/src/ui/elements/ui_element.h b/src/ui/elements/ui_element.h index 279a567..eecfc44 100644 --- a/src/ui/elements/ui_element.h +++ b/src/ui/elements/ui_element.h @@ -51,6 +51,7 @@ public: void set_enabled(bool enabled); bool is_enabled() const; void set_text(std::string_view text); + void set_src(std::string_view src); void set_style_enabled(std::string_view style_name, bool enabled); bool is_element() override { return true; } float get_absolute_left(); diff --git a/src/ui/elements/ui_image.cpp b/src/ui/elements/ui_image.cpp index 1b326ae..e29742c 100644 --- a/src/ui/elements/ui_image.cpp +++ b/src/ui/elements/ui_image.cpp @@ -4,8 +4,8 @@ namespace recompui { - Image::Image(Element *parent) : Element(parent) { - + Image::Image(Element *parent, std::string_view src) : Element(parent, 0, "img") { + set_src(src); } }; \ No newline at end of file diff --git a/src/ui/elements/ui_image.h b/src/ui/elements/ui_image.h index 7e2049e..b075961 100644 --- a/src/ui/elements/ui_image.h +++ b/src/ui/elements/ui_image.h @@ -6,7 +6,7 @@ namespace recompui { class Image : public Element { public: - Image(Element *parent); + Image(Element *parent, std::string_view src); }; } // namespace recompui \ No newline at end of file diff --git a/src/ui/ui_mod_details_panel.cpp b/src/ui/ui_mod_details_panel.cpp index 468dd68..dc851df 100644 --- a/src/ui/ui_mod_details_panel.cpp +++ b/src/ui/ui_mod_details_panel.cpp @@ -23,7 +23,7 @@ ModDetailsPanel::ModDetailsPanel(Element *parent) : Element(parent) { thumbnail_container = context.create_element(header_container, FlexDirection::Column, JustifyContent::SpaceEvenly); thumbnail_container->set_flex(0.0f, 0.0f); { - thumbnail_image = context.create_element(thumbnail_container); + thumbnail_image = context.create_element(thumbnail_container, ""); thumbnail_image->set_width(100.0f); thumbnail_image->set_height(100.0f); thumbnail_image->set_background_color(Color{ 190, 184, 219, 25 }); @@ -67,9 +67,11 @@ ModDetailsPanel::ModDetailsPanel(Element *parent) : Element(parent) { ModDetailsPanel::~ModDetailsPanel() { } -void ModDetailsPanel::set_mod_details(const recomp::mods::ModDetails& details, bool mod_enabled, bool toggle_enabled) { +void ModDetailsPanel::set_mod_details(const recomp::mods::ModDetails& details, const std::string &thumbnail, bool mod_enabled, bool toggle_enabled) { cur_details = details; + thumbnail_image->set_src(thumbnail); + title_label->set_text(cur_details.display_name); version_label->set_text(cur_details.version.to_string()); diff --git a/src/ui/ui_mod_details_panel.h b/src/ui/ui_mod_details_panel.h index 46cf436..99744e1 100644 --- a/src/ui/ui_mod_details_panel.h +++ b/src/ui/ui_mod_details_panel.h @@ -14,7 +14,7 @@ class ModDetailsPanel : public Element { public: ModDetailsPanel(Element *parent); virtual ~ModDetailsPanel(); - void set_mod_details(const recomp::mods::ModDetails& details, bool mod_enabled, bool toggle_enabled); + void set_mod_details(const recomp::mods::ModDetails& details, const std::string &thumbnail, bool mod_enabled, bool toggle_enabled); void set_mod_toggled_callback(std::function callback); void set_mod_configure_pressed_callback(std::function callback); private: diff --git a/src/ui/ui_mod_menu.cpp b/src/ui/ui_mod_menu.cpp index 27a4543..fa0a0f9 100644 --- a/src/ui/ui_mod_menu.cpp +++ b/src/ui/ui_mod_menu.cpp @@ -15,6 +15,10 @@ namespace recompui { +static std::string generate_thumbnail_src_for_mod(const std::string &mod_id) { + return "?/mods/" + mod_id + "/thumb"; +} + // ModEntryView ModEntryView::ModEntryView(Element *parent) : Element(parent) { @@ -34,13 +38,14 @@ ModEntryView::ModEntryView(Element *parent) : Element(parent) { set_cursor(Cursor::Pointer); { - thumbnail_image = context.create_element(this); + thumbnail_image = context.create_element(this, ""); thumbnail_image->set_width(100.0f); thumbnail_image->set_height(100.0f); thumbnail_image->set_min_width(100.0f); thumbnail_image->set_min_height(100.0f); thumbnail_image->set_background_color(Color{ 190, 184, 219, 25 }); + body_container = context.create_element(this, FlexDirection::Column, JustifyContent::FlexStart); body_container->set_width_auto(); body_container->set_height(100.0f); @@ -63,6 +68,10 @@ void ModEntryView::set_mod_details(const recomp::mods::ModDetails &details) { description_label->set_text(details.short_description); } +void ModEntryView::set_mod_thumbnail(const std::string &thumbnail) { + thumbnail_image->set_src(thumbnail); +} + // ModEntryButton ModEntryButton::ModEntryButton(Element *parent, uint32_t mod_index) : Element(parent, Events(EventType::Click, EventType::Hover, EventType::Focus, EventType::Drag)) { @@ -90,6 +99,10 @@ void ModEntryButton::set_mod_details(const recomp::mods::ModDetails &details) { view->set_mod_details(details); } +void ModEntryButton::set_mod_thumbnail(const std::string &thumbnail) { + view->set_mod_thumbnail(thumbnail); +} + void ModEntryButton::process_event(const Event& e) { switch (e.type) { case EventType::Click: @@ -110,6 +123,10 @@ void ModEntryButton::process_event(const Event& e) { // ModMenu void ModMenu::refresh_mods() { + for (const std::string &thumbnail : loaded_thumbnails) { + recompui::release_image(thumbnail); + } + recomp::mods::scan_mods(); mod_details = recomp::mods::get_mod_details(game_mod_id); create_mod_list(); @@ -137,10 +154,11 @@ void ModMenu::mod_toggled(bool enabled) { void ModMenu::mod_selected(uint32_t mod_index) { active_mod_index = mod_index; if (active_mod_index >= 0) { + std::string thumbnail_src = generate_thumbnail_src_for_mod(mod_details[mod_index].mod_id); bool mod_enabled = recomp::mods::is_mod_enabled(mod_details[mod_index].mod_id); bool auto_enabled = recomp::mods::is_mod_auto_enabled(mod_details[mod_index].mod_id); bool toggle_enabled = !auto_enabled && (mod_details[mod_index].runtime_toggleable || !ultramodern::is_game_started()); - mod_details_panel->set_mod_details(mod_details[mod_index], mod_enabled, toggle_enabled); + mod_details_panel->set_mod_details(mod_details[mod_index], thumbnail_src, mod_enabled, toggle_enabled); } } @@ -161,6 +179,7 @@ void ModMenu::mod_dragged(uint32_t mod_index, EventDrag drag) { mod_entry_buttons[mod_index]->set_display(Display::None); mod_entry_floating_view->set_display(Display::Flex); mod_entry_floating_view->set_mod_details(mod_details[mod_index]); + mod_entry_floating_view->set_mod_thumbnail(generate_thumbnail_src_for_mod(mod_details[mod_index].mod_id)); mod_entry_floating_view->set_left(left, Unit::Px); mod_entry_floating_view->set_top(top, Unit::Px); mod_entry_floating_view->set_width(width, Unit::Px); @@ -218,7 +237,9 @@ void ModMenu::mod_dragged(uint32_t mod_index, EventDrag drag) { mod_details = recomp::mods::get_mod_details(game_mod_id); for (size_t i = 0; i < mod_entry_buttons.size(); i++) { mod_entry_buttons[i]->set_mod_details(mod_details[i]); + mod_entry_buttons[i]->set_mod_thumbnail(generate_thumbnail_src_for_mod(mod_details[i].mod_id)); } + break; } default: @@ -311,6 +332,13 @@ void ModMenu::create_mod_list() { // Create the child elements for the list scroll. for (size_t mod_index = 0; mod_index < mod_details.size(); mod_index++) { + 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); + loaded_thumbnails.emplace(thumbnail_name); + } + Element *spacer = context.create_element(list_scroll_container); mod_entry_spacers.emplace_back(spacer); @@ -318,6 +346,7 @@ void ModMenu::create_mod_list() { mod_entry->set_mod_selected_callback(std::bind(&ModMenu::mod_selected, this, std::placeholders::_1)); mod_entry->set_mod_drag_callback(std::bind(&ModMenu::mod_dragged, this, std::placeholders::_1, std::placeholders::_2)); mod_entry->set_mod_details(mod_details[mod_index]); + mod_entry->set_mod_thumbnail(thumbnail_name); mod_entry_buttons.emplace_back(mod_entry); } diff --git a/src/ui/ui_mod_menu.h b/src/ui/ui_mod_menu.h index 96d3282..9a851c5 100644 --- a/src/ui/ui_mod_menu.h +++ b/src/ui/ui_mod_menu.h @@ -15,6 +15,7 @@ public: ModEntryView(Element *parent); virtual ~ModEntryView(); void set_mod_details(const recomp::mods::ModDetails &details); + void set_mod_thumbnail(const std::string &thumbnail); private: Image *thumbnail_image = nullptr; Container *body_container = nullptr; @@ -29,6 +30,7 @@ public: void set_mod_selected_callback(std::function callback); void set_mod_drag_callback(std::function callback); void set_mod_details(const recomp::mods::ModDetails &details); + void set_mod_thumbnail(const std::string &thumbnail); protected: virtual void process_event(const Event &e); private: @@ -71,6 +73,7 @@ private: uint32_t mod_drag_target_index = 0; float mod_drag_spacer_height = 0.0f; std::vector mod_details{}; + std::unordered_set loaded_thumbnails; std::string game_mod_id; ConfigSubMenu *config_sub_menu; diff --git a/src/ui/ui_renderer.cpp b/src/ui/ui_renderer.cpp index 6bfea72..74b0d71 100644 --- a/src/ui/ui_renderer.cpp +++ b/src/ui/ui_renderer.cpp @@ -6,6 +6,10 @@ #include #include +#include + +#include "stb/stb_image.h" + #include "rt64_render_hooks.h" #include "rt64_render_interface_builders.h" @@ -61,33 +65,16 @@ struct RmlPushConstants { struct TextureHandle { std::unique_ptr texture; std::unique_ptr set; + bool transitioned = false; }; -static std::vector read_file(const std::filesystem::path& filepath) { - std::vector ret{}; - std::ifstream input_file{ filepath, std::ios::binary }; - - if (!input_file) { - return ret; - } - - input_file.seekg(0, std::ios::end); - std::streampos filesize = input_file.tellg(); - input_file.seekg(0, std::ios::beg); - - ret.resize(filesize); - - input_file.read(ret.data(), filesize); - - return ret; -} - - template T from_bytes_le(const char* input) { return *reinterpret_cast(input); } +typedef std::pair> ImageFromBytes; + namespace recompui { class RmlRenderInterface_RT64_impl : public Rml::RenderInterfaceCompatibility { struct DynamicBuffer { @@ -140,12 +127,19 @@ class RmlRenderInterface_RT64_impl : public Rml::RenderInterfaceCompatibility { std::unique_ptr screen_framebuffer_{}; std::unique_ptr screen_descriptor_set_{}; std::unique_ptr screen_vertex_buffer_{}; + std::unique_ptr copy_command_queue_{}; + std::unique_ptr copy_command_list_{}; + std::unique_ptr copy_buffer_{}; + std::unique_ptr copy_command_fence_; + uint64_t copy_buffer_size_ = 0; uint64_t screen_vertex_buffer_size_ = 0; uint32_t gTexture_descriptor_index; RT64::RenderInputSlot vertex_slot_{ 0, sizeof(Rml::Vertex) }; RT64::RenderCommandList* list_ = nullptr; bool scissor_enabled_ = false; std::vector> stale_buffers_{}; + moodycamel::ConcurrentQueue image_from_bytes_queue; + std::unordered_map> image_from_bytes_map; public: RmlRenderInterface_RT64_impl(RT64::RenderInterface* interface, RT64::RenderDevice* device) { interface_ = interface; @@ -251,6 +245,10 @@ public: vertices[2] = Rml::Vertex{ Rml::Vector2f(3.0f, 1.0f), white, Rml::Vector2f(2.0f, 0.0f) }; screen_vertex_buffer_->unmap(); } + + copy_command_queue_ = device->createCommandQueue(RT64::RenderCommandListType::COPY); + copy_command_list_ = device->createCommandList(RT64::RenderCommandListType::COPY); + copy_command_fence_ = device->createCommandFence(); } void reset_dynamic_buffer(DynamicBuffer &dynamic_buffer) { @@ -360,7 +358,15 @@ public: list_->setIndexBuffer(&index_view); RT64::RenderVertexBufferView vertex_view{vertex_buffer_.buffer_->at(vertex_buffer_offset), vert_size_bytes}; list_->setVertexBuffers(0, &vertex_view, 1, &vertex_slot_); - list_->setGraphicsDescriptorSet(textures_.at(texture).set.get(), 1); + + TextureHandle &texture_handle = textures_.at(texture); + if (!texture_handle.transitioned) { + // Prepare the texture for being read from a pixel shader. + list_->barriers(RT64::RenderBarrierStage::GRAPHICS, RT64::RenderTextureBarrier(texture_handle.texture.get(), RT64::RenderTextureLayout::SHADER_READ)); + texture_handle.transitioned = true; + } + + list_->setGraphicsDescriptorSet(texture_handle.set.get(), 1); RmlPushConstants constants{ .transform = mvp_, @@ -384,72 +390,32 @@ public: } bool LoadTexture(Rml::TextureHandle& texture_handle, Rml::Vector2i& texture_dimensions, const Rml::String& source) override { - std::filesystem::path image_path{ source.c_str() }; + flush_image_from_bytes_queue(); - if (image_path.extension() == ".tga") { - std::vector file_data = read_file(image_path); - - if (file_data.empty()) { - printf(" File not found or empty\n"); - return false; - } - - // Make sure ID length is zero - if (file_data[0] != 0) { - printf(" Nonzero ID length not supported\n"); - return false; - } - - // Make sure no color map is used - if (file_data[1] != 0) { - printf(" Color maps not supported\n"); - return false; - } - - // Make sure the image is uncompressed - if (file_data[2] != 2) { - printf(" Only uncompressed tga files supported\n"); - return false; - } - - uint16_t origin_x = from_bytes_le(file_data.data() + 8); - uint16_t origin_y = from_bytes_le(file_data.data() + 10); - uint16_t size_x = from_bytes_le(file_data.data() + 12); - uint16_t size_y = from_bytes_le(file_data.data() + 14); - - // Nonzero origin not supported - if (origin_x != 0 || origin_y != 0) { - printf(" Nonzero origin not supported\n"); - return false; - } - - uint8_t pixel_depth = file_data[16]; - - if (pixel_depth != 32) { - printf(" Only 32bpp images supported\n"); - return false; - } - - uint8_t image_descriptor = file_data[17]; - - if ((image_descriptor & 0b1111) != 8) { - printf(" Only 8bpp alpha supported\n"); - } - - if (image_descriptor & 0b110000) { - printf(" Only bottom-to-top, left-to-right pixel order supported\n"); - } - - texture_dimensions.x = size_x; - texture_dimensions.y = size_y; - - texture_handle = texture_count_++; - create_texture(texture_handle, reinterpret_cast(file_data.data() + 18), texture_dimensions, true, true); - - return true; + auto it = image_from_bytes_map.find(source); + if (it == image_from_bytes_map.end()) { + return false; } + + constexpr uint32_t PNG_MAGIC = 0x474E5089; + uint32_t magicNumber = *reinterpret_cast(it->second.data()); + if (magicNumber == PNG_MAGIC) { + int width, height; + stbi_uc *stbi_data = stbi_load_from_memory((const stbi_uc *)(it->second.data()), it->second.size(), &width, &height, nullptr, 4); + if (stbi_data == nullptr) { + return false; + } - return false; + texture_dimensions.x = width; + texture_dimensions.y = height; + + bool texture_generated = GenerateTexture(texture_handle, stbi_data, texture_dimensions); + stbi_image_free(stbi_data); + return texture_generated; + } + else { + return false; + } } bool GenerateTexture(Rml::TextureHandle& texture_handle, const Rml::byte* source, const Rml::Vector2i& source_dimensions) override { @@ -479,11 +445,13 @@ public: uint32_t uploaded_size_bytes = row_byte_width * source_dimensions.y; // Allocate room in the upload buffer for the uploaded data. - uint32_t upload_buffer_offset = allocate_dynamic_data_aligned(upload_buffer_, uploaded_size_bytes, 512); + if (uploaded_size_bytes > copy_buffer_size_) { + copy_buffer_size_ = (uploaded_size_bytes * 3) / 2; + copy_buffer_ = device_->createBuffer(RT64::RenderBufferDesc::UploadBuffer(copy_buffer_size_)); + } // Copy the source data into the upload buffer. - uint8_t* dst_data = upload_buffer_.mapped_data_ + upload_buffer_offset; - + uint8_t* dst_data = (uint8_t *)(copy_buffer_->map()); if (row_byte_padding == 0) { // Copy row-by-row if the image is flipped. if (flip_y) { @@ -508,23 +476,30 @@ public: } } + copy_buffer_->unmap(); + + // Reset the command list. + copy_command_list_->begin(); + // Prepare the texture to be a destination for copying. - list_->barriers(RT64::RenderBarrierStage::COPY, RT64::RenderTextureBarrier(texture.get(), RT64::RenderTextureLayout::COPY_DEST)); + copy_command_list_->barriers(RT64::RenderBarrierStage::COPY, RT64::RenderTextureBarrier(texture.get(), RT64::RenderTextureLayout::COPY_DEST)); // Copy the upload buffer into the texture. - list_->copyTextureRegion( + copy_command_list_->copyTextureRegion( RT64::RenderTextureCopyLocation::Subresource(texture.get()), - RT64::RenderTextureCopyLocation::PlacedFootprint(upload_buffer_.buffer_.get(), RmlTextureFormat, source_dimensions.x, source_dimensions.y, 1, row_width, upload_buffer_offset)); + RT64::RenderTextureCopyLocation::PlacedFootprint(copy_buffer_.get(), RmlTextureFormat, source_dimensions.x, source_dimensions.y, 1, row_width)); - // Prepare the texture for being read from a pixel shader. - list_->barriers(RT64::RenderBarrierStage::GRAPHICS, RT64::RenderTextureBarrier(texture.get(), RT64::RenderTextureLayout::SHADER_READ)); + // End the command list, execute it and wait. + copy_command_list_->end(); + copy_command_queue_->executeCommandLists(copy_command_list_.get(), copy_command_fence_.get()); + copy_command_queue_->waitForCommandFence(copy_command_fence_.get()); // Create a descriptor set with this texture in it. std::unique_ptr set = texture_set_builder_->create(device_); set->setTexture(gTexture_descriptor_index, texture.get(), RT64::RenderTextureLayout::SHADER_READ); - textures_.emplace(texture_handle, TextureHandle{ std::move(texture), std::move(set) }); + textures_.emplace(texture_handle, TextureHandle{ std::move(texture), std::move(set), false }); return true; } @@ -625,6 +600,17 @@ 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 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)); + } + } }; } // namespace recompui @@ -657,3 +643,9 @@ 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) { + assert(static_cast(impl)); + + impl->queue_image_from_bytes(src, bytes); +} \ No newline at end of file diff --git a/src/ui/ui_renderer.h b/src/ui/ui_renderer.h index 433b4c9..0382c48 100644 --- a/src/ui/ui_renderer.h +++ b/src/ui/ui_renderer.h @@ -29,6 +29,7 @@ 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); }; } // namespace recompui diff --git a/src/ui/ui_state.cpp b/src/ui/ui_state.cpp index 899ed14..ad34baa 100644 --- a/src/ui/ui_state.cpp +++ b/src/ui/ui_state.cpp @@ -775,3 +775,10 @@ 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::release_image(const std::string &src) { + Rml::ReleaseTexture(src); +}