Add basic thumbnail parsing functionality.

This commit is contained in:
Dario 2025-01-28 22:34:03 -03:00 committed by Mr-Wiseguy
parent 13a22b1504
commit 937f390331
12 changed files with 142 additions and 101 deletions

View File

@ -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<char> &bytes);
void release_image(const std::string &src);
}
#endif

View File

@ -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.

View File

@ -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();

View File

@ -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);
}
};

View File

@ -6,7 +6,7 @@ namespace recompui {
class Image : public Element {
public:
Image(Element *parent);
Image(Element *parent, std::string_view src);
};
} // namespace recompui

View File

@ -23,7 +23,7 @@ ModDetailsPanel::ModDetailsPanel(Element *parent) : Element(parent) {
thumbnail_container = context.create_element<Container>(header_container, FlexDirection::Column, JustifyContent::SpaceEvenly);
thumbnail_container->set_flex(0.0f, 0.0f);
{
thumbnail_image = context.create_element<Image>(thumbnail_container);
thumbnail_image = context.create_element<Image>(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());

View File

@ -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<void(bool)> callback);
void set_mod_configure_pressed_callback(std::function<void()> callback);
private:

View File

@ -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<Image>(this);
thumbnail_image = context.create_element<Image>(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<Container>(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<char> &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<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);
}

View File

@ -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<void(uint32_t)> callback);
void set_mod_drag_callback(std::function<void(uint32_t, EventDrag)> 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<recomp::mods::ModDetails> mod_details{};
std::unordered_set<std::string> loaded_thumbnails;
std::string game_mod_id;
ConfigSubMenu *config_sub_menu;

View File

@ -6,6 +6,10 @@
#include <fstream>
#include <filesystem>
#include <concurrentqueue.h>
#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<RT64::RenderTexture> texture;
std::unique_ptr<RT64::RenderDescriptorSet> set;
bool transitioned = false;
};
static std::vector<char> read_file(const std::filesystem::path& filepath) {
std::vector<char> 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 <typename T>
T from_bytes_le(const char* input) {
return *reinterpret_cast<const T*>(input);
}
typedef std::pair<std::string, std::vector<char>> 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<RT64::RenderFramebuffer> screen_framebuffer_{};
std::unique_ptr<RT64::RenderDescriptorSet> screen_descriptor_set_{};
std::unique_ptr<RT64::RenderBuffer> screen_vertex_buffer_{};
std::unique_ptr<RT64::RenderCommandQueue> copy_command_queue_{};
std::unique_ptr<RT64::RenderCommandList> copy_command_list_{};
std::unique_ptr<RT64::RenderBuffer> copy_buffer_{};
std::unique_ptr<RT64::RenderCommandFence> 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<std::unique_ptr<RT64::RenderBuffer>> stale_buffers_{};
moodycamel::ConcurrentQueue<ImageFromBytes> image_from_bytes_queue;
std::unordered_map<std::string, std::vector<char>> 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<char> 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<uint16_t>(file_data.data() + 8);
uint16_t origin_y = from_bytes_le<uint16_t>(file_data.data() + 10);
uint16_t size_x = from_bytes_le<uint16_t>(file_data.data() + 12);
uint16_t size_y = from_bytes_le<uint16_t>(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<const Rml::byte*>(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<const uint32_t *>(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<RT64::RenderDescriptorSet> 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<char> &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<char> &bytes) {
assert(static_cast<bool>(impl));
impl->queue_image_from_bytes(src, bytes);
}

View File

@ -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<char> &bytes);
};
} // namespace recompui

View File

@ -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<char> &bytes) {
ui_state->render_interface.queue_image_from_bytes(src, bytes);
}
void recompui::release_image(const std::string &src) {
Rml::ReleaseTexture(src);
}