// SPDX-License-Identifier: GPL-3.0-or-later // Copyright (C) 2026 steredbits and Pelagia contributors #include "ui/poster_cache.h" #include #include "util/log.h" namespace ui { std::string PosterKey::str() const { char width_text[16]; std::snprintf(width_text, sizeof(width_text), "_w%d", width); return item_id + "_" + tag + width_text; } PosterCache::PosterCache(Canvas* canvas, TaskRunner* io, MainQueue* main, PosterFetcher* fetcher, util::DiskCache* disk, const Options& options) : canvas_(canvas), io_(io), main_(main), fetcher_(fetcher), disk_(disk), options_(options) {} PosterCache::~PosterCache() { for (auto& item : entries_) { if (item.second.wanted) { *item.second.wanted = false; // tasks not started yet: nothing to download } release(&item.second); } } void PosterCache::release(Entry* entry) { if (entry->poster.texture != 0) { canvas_->destroy_texture(entry->poster.texture); memory_bytes_ -= static_cast(entry->poster.w) * entry->poster.h * 4; entry->poster = Poster{}; } } const Poster* PosterCache::get(const PosterKey& key) { if (key.item_id.empty() || key.tag.empty() || key.width <= 0) { return nullptr; // no poster on the server side } const std::string id = key.str(); auto it = entries_.find(id); if (it == entries_.end()) { it = entries_.emplace(id, Entry()).first; start_load(id, key, &it->second); } Entry& e = it->second; e.last_frame = frame_; switch (e.state) { case State::Ready: return &e.poster; case State::Failed: if (now_ms_ - e.failed_at_ms >= options_.retry_failed_ms) { start_load(id, key, &e); } return nullptr; default: return nullptr; } } void PosterCache::start_load(const std::string& id, const PosterKey& key, Entry* entry) { entry->state = State::Loading; entry->serial = next_serial_++; entry->wanted = std::make_shared>(true); ++loading_; ++loads_; const uint64_t serial = entry->serial; std::shared_ptr> wanted = entry->wanted; PosterFetcher* fetcher = fetcher_; util::DiskCache* disk = disk_; MainQueue* main = main_; const Lifetime::Token token = lifetime_.token(); PosterCache* self = this; io_->post([=]() { if (!wanted->load()) { return; // no longer displayed: request abandoned before any access } const std::string disk_key = key.str(); std::string bytes; bool ok = disk && disk->get(disk_key, &bytes); const bool from_disk = ok; if (!ok) { ok = fetcher->fetch(key, &bytes); } std::shared_ptr image = std::make_shared(); if (!ok || !decode_image(bytes, key.width, key.width * 2, image.get())) { if (ok) { LOG_DEBUG("Unreadable poster: %s", disk_key.c_str()); if (disk && from_disk) { disk->remove(disk_key); } } image.reset(); } else if (disk && !from_disk) { disk->put(disk_key, bytes); } main->post([=]() { if (Lifetime::alive(token)) { self->on_loaded(id, serial, image); } }); }); } void PosterCache::on_loaded(const std::string& id, uint64_t serial, std::shared_ptr image) { auto it = entries_.find(id); if (it == entries_.end() || it->second.serial != serial || it->second.state != State::Loading) { return; // request abandoned in the meantime } Entry& e = it->second; --loading_; e.wanted.reset(); if (!image) { e.state = State::Failed; e.failed_at_ms = now_ms_; return; } e.state = State::Decoded; e.image = image; decoded_.push_back(id); } void PosterCache::upload_pending() { int budget = options_.uploads_per_frame; size_t consumed = 0; for (; consumed < decoded_.size() && budget > 0; ++consumed) { auto it = entries_.find(decoded_[consumed]); if (it == entries_.end() || it->second.state != State::Decoded) { continue; } Entry& e = it->second; const Image& img = *e.image; const TextureId tex = canvas_->create_texture(img.w, img.h, PixelFormat::Rgba8); if (tex == 0 || !canvas_->update_texture(tex, 0, 0, img.w, img.h, img.rgba.data(), img.w * 4)) { if (tex != 0) { canvas_->destroy_texture(tex); } e.state = State::Failed; e.failed_at_ms = now_ms_; } else { e.state = State::Ready; e.poster.texture = tex; e.poster.w = img.w; e.poster.h = img.h; memory_bytes_ += static_cast(img.w) * img.h * 4; } e.image.reset(); --budget; } decoded_.erase(decoded_.begin(), decoded_.begin() + static_cast(consumed)); } void PosterCache::evict() { while (memory_bytes_ > options_.memory_budget) { // Least recently drawn, excluding posters drawn in the previous frame // (visible on screen). auto victim = entries_.end(); for (auto it = entries_.begin(); it != entries_.end(); ++it) { if (it->second.state == State::Ready && it->second.last_frame + 1 < frame_ && (victim == entries_.end() || it->second.last_frame < victim->second.last_frame)) { victim = it; } } if (victim == entries_.end()) { return; // everything is visible: temporary overrun accepted } release(&victim->second); entries_.erase(victim); } } void PosterCache::begin_frame(uint64_t now_ms) { ++frame_; now_ms_ = now_ms; for (auto it = entries_.begin(); it != entries_.end();) { Entry& e = it->second; if (e.state == State::Loading && frame_ - e.last_frame > static_cast(options_.unwanted_after_frames)) { *e.wanted = false; --loading_; it = entries_.erase(it); } else { ++it; } } upload_pending(); evict(); } bool PosterCache::busy() const { return loading_ > 0 || !decoded_.empty(); } size_t PosterCache::texture_count() const { size_t n = 0; for (const auto& item : entries_) { n += item.second.poster.texture != 0; } return n; } } // namespace ui