mirror of
https://github.com/steredbits/pelagia.git
synced 2026-10-17 23:01:24 +02:00
208 lines
6.0 KiB
C++
208 lines
6.0 KiB
C++
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
// Copyright (C) 2026 steredbits and Pelagia contributors
|
|
#include "ui/poster_cache.h"
|
|
|
|
#include <cstdio>
|
|
|
|
#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<size_t>(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<std::atomic<bool>>(true);
|
|
++loading_;
|
|
++loads_;
|
|
const uint64_t serial = entry->serial;
|
|
std::shared_ptr<std::atomic<bool>> 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> image = std::make_shared<Image>();
|
|
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> 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<size_t>(img.w) * img.h * 4;
|
|
}
|
|
e.image.reset();
|
|
--budget;
|
|
}
|
|
decoded_.erase(decoded_.begin(), decoded_.begin() + static_cast<long>(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<uint64_t>(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
|