Redo thumbnailing

This commit is contained in:
2026-08-30 04:15:44 +02:00
parent a7c8bf5e09
commit 1b752be290
22 changed files with 1298 additions and 505 deletions
+6 -5
View File
@@ -191,10 +191,16 @@ configure_file(
"${PROJECT_BINARY_DIR}/dawn-config.h")
include_directories("${PROJECT_SOURCE_DIR}")
include(CTest)
add_subdirectory(libdn)
add_subdirectory(dnthumbd)
add_subdirectory(dn)
if (BUILD_TESTING)
add_subdirectory(tests)
endif()
if (WIN32)
install(TARGETS dn libdn
RUNTIME DESTINATION .)
@@ -258,11 +264,6 @@ else()
endif()
endif()
include(CTest)
if (BUILD_TESTING)
add_subdirectory(tests)
endif()
set(CPACK_PACKAGE_VENDOR "The Dawn Authors")
set(CPACK_PACKAGE_CONTACT "Přemysl Eric Janouch <p@janouch.name>")
set(CPACK_RESOURCE_FILE_LICENSE "${PROJECT_SOURCE_DIR}/LICENSE")
+2 -3
View File
@@ -62,6 +62,7 @@ add_dependencies(dn dn-cmf-header)
dawn_embed_shader(dn dn-overlay.vert dn_overlay_vert)
dawn_embed_shader(dn dn-overlay.frag dn_overlay_frag)
dawn_embed_shader(dn dn-thumb.frag dn_thumb_frag)
dawn_embed_shader(dn dn-dither.frag dn_dither_frag)
dawn_embed_shader(dn fullscreen.vert fullscreen_vert)
@@ -327,7 +328,5 @@ else()
endif()
if (BUILD_TESTING)
add_test(NAME dn-help COMMAND dn --help)
set_tests_properties(dn-help PROPERTIES
ENVIRONMENT "QT_QPA_PLATFORM=offscreen")
add_test(NAME dn-help COMMAND $<TARGET_FILE:dn> --help)
endif()
+406 -314
View File
File diff suppressed because it is too large Load Diff
+14 -2
View File
@@ -48,10 +48,13 @@ struct Browser : Widget {
uint32_t image_h = 0;
int ram_w = 0;
int ram_h = 0;
int ram_tier = -1;
std::vector<uint16_t> ram;
bool ram_interim = false;
bool ram_pending = false;
bool cache_bypass = false;
bool persistent_checked = false;
bool generation_needed = false;
Thumbnailer::Reservation reservation = 0;
bool regen_failed = false;
dawn::Transfer transfer = dawn::Transfer::Srgb;
Sheet::Packed gpu;
@@ -78,6 +81,14 @@ struct Browser : Widget {
uint32_t w = 0;
uint32_t h = 0;
};
struct ThumbInflight {
uint64_t generation = 0;
int64_t mtime = 0;
uint64_t size = 0;
int tier = 0;
Thumbnailer::Priority priority = Thumbnailer::Priority::Dimensions;
bool regeneration = false;
};
Kit &kit_;
Thumbnailer &thumbnailer_;
@@ -90,6 +101,7 @@ struct Browser : Widget {
QUrl dir_url_;
std::shared_ptr<dawn::Cmm> cmm_;
std::shared_ptr<dawn::Profile> screen_profile_;
std::shared_ptr<const std::vector<uint8_t>> screen_icc_;
bool show_names_ = false;
// The toolbar search field, whose text narrows the listing; it is
@@ -123,7 +135,7 @@ struct Browser : Widget {
std::vector<File> files_;
std::vector<DirRow> side_dirs_;
std::unordered_map<std::string, CachedSize> size_cache_;
std::unordered_map<std::string, Thumbnailer::Priority> thumb_inflight_;
std::unordered_map<std::string, ThumbInflight> thumb_inflight_;
struct HistEntry {
QUrl url;
+16 -17
View File
@@ -293,7 +293,7 @@ struct OpenJob {
uint64_t epoch = 0;
string path;
string uri;
vector<uint8_t> screen_icc;
shared_ptr<const vector<uint8_t>> screen_icc;
int dpi = 96;
bool enable_cms = true;
};
@@ -309,7 +309,7 @@ struct ScaleJob {
uint64_t gen = 0;
dawn::ImagePtr page;
float scale = 1.f;
vector<uint8_t> screen_icc;
shared_ptr<const vector<uint8_t>> screen_icc;
bool enable_cms = true;
};
@@ -338,10 +338,11 @@ join_load_text(const vector<string> &warnings, const dawn::Error &error,
}
shared_ptr<dawn::Profile>
profile_from_icc(dawn::Cmm &cmm, const vector<uint8_t> &icc)
profile_from_icc(
dawn::Cmm &cmm, const shared_ptr<const vector<uint8_t>> &icc)
{
if (!icc.empty()) {
if (auto profile = cmm.get_profile(icc))
if (icc && !icc->empty()) {
if (auto profile = cmm.get_profile(*icc))
return profile;
}
return cmm.get_profile_sRGB();
@@ -412,14 +413,6 @@ post_gui(const Viewer &v, function<void()> fn)
v.kit_.post(std::move(fn));
}
static vector<uint8_t>
screen_icc_bytes(const Viewer &v)
{
if (!v.screen_profile_)
return {};
return v.screen_profile_->to_bytes();
}
static void
upload_frame(const Viewer &v, const dawn::Image &image)
{
@@ -1035,7 +1028,7 @@ make_open_job(const Viewer &v, const string &path)
path_to_url(QString::fromStdString(path)).toEncoded().toStdString();
job.dpi = 96;
job.enable_cms = v.enable_cms_;
job.screen_icc = v.enable_cms_ ? screen_icc_bytes(v) : vector<uint8_t>{};
job.screen_icc = v.enable_cms_ ? v.screen_icc_ : nullptr;
return job;
}
@@ -1178,7 +1171,7 @@ post_scale(Viewer &v)
job.page = v.current_;
job.scale = v.scale_;
job.enable_cms = v.enable_cms_;
job.screen_icc = v.enable_cms_ ? screen_icc_bytes(v) : vector<uint8_t>{};
job.screen_icc = v.enable_cms_ ? v.screen_icc_ : nullptr;
{
lock_guard<mutex> lock(v.worker_->mu);
v.worker_->pending_scale = std::move(job);
@@ -2019,9 +2012,15 @@ void
Viewer::set_screen_profile(
shared_ptr<dawn::Cmm> cmm, shared_ptr<dawn::Profile> profile, bool fallback)
{
const bool reload = this->enable_cms_ && !this->url_.isEmpty() &&
!profiles_equal(this->screen_profile_.get(), profile.get());
auto screen_icc = profile
? make_shared<const vector<uint8_t>>(profile->to_bytes())
: nullptr;
const bool changed = bool(this->screen_icc_) != bool(screen_icc) ||
(this->screen_icc_ && *this->screen_icc_ != *screen_icc);
const bool reload =
this->enable_cms_ && !this->url_.isEmpty() && changed;
this->cmm_ = std::move(cmm);
this->screen_icc_ = std::move(screen_icc);
this->screen_profile_ = std::move(profile);
this->screen_profile_fallback_ = fallback;
this->kit_.bake_colours(this->cmm_.get(), this->screen_profile_.get());
+1
View File
@@ -74,6 +74,7 @@ struct Viewer : Widget {
std::string basename_;
std::shared_ptr<dawn::Cmm> cmm_;
std::shared_ptr<dawn::Profile> screen_profile_;
std::shared_ptr<const std::vector<uint8_t>> screen_icc_;
bool screen_profile_fallback_ = true;
dawn::ImagePtr image_;
dawn::ImagePtr current_;
+98 -4
View File
@@ -9,6 +9,7 @@
#include "dn-overlay-frag-spv.h"
#include "dn-overlay-vert-spv.h"
#include "dn-thumb-frag-spv.h"
#include "libdn/vk-device.hpp"
#include <QtLogging>
@@ -282,10 +283,21 @@ OverlayList::add_image(float x0, float y0, float x1, float y1, float u0,
void
OverlayList::add_thumb(float x0, float y0, float x1, float y1, float u0,
float v0, float u1, float v1, Colour col)
float v0, float u1, float v1, int transfer, Colour col)
{
this->tex_ = kOverlayTexThumbs;
add_quad(x0, y0, x1, y1, u0, v0, u1, v1, col, col, col, col);
const size_t first = this->mesh_.vertices.size() - 4;
for (size_t i = first; i < this->mesh_.vertices.size(); ++i) {
OverlayVertex &vertex = this->mesh_.vertices[i];
vertex.atlas_x0 = u0;
vertex.atlas_y0 = v0;
vertex.atlas_x1 = u1;
vertex.atlas_y1 = v1;
vertex.dest_w = abs(x1 - x0);
vertex.dest_h = abs(y1 - y0);
vertex.transfer = float(transfer);
}
}
bool
@@ -422,12 +434,20 @@ OverlayVulkan::create_pipeline()
.codeSize = dn_overlay_frag_words * sizeof(uint32_t),
.pCode = dn_overlay_frag,
};
VkShaderModuleCreateInfo thumb_frag_info{
.sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO,
.codeSize = dn_thumb_frag_words * sizeof(uint32_t),
.pCode = dn_thumb_frag,
};
check_vk(
vkCreateShaderModule(this->device_, &vert_info, nullptr, &this->vert_),
"vkCreateShaderModule overlay vert");
check_vk(
vkCreateShaderModule(this->device_, &frag_info, nullptr, &this->frag_),
"vkCreateShaderModule overlay frag");
check_vk(vkCreateShaderModule(this->device_, &thumb_frag_info, nullptr,
&this->thumb_frag_),
"vkCreateShaderModule thumb frag");
VkPushConstantRange push{
.stageFlags = VK_SHADER_STAGE_VERTEX_BIT,
@@ -460,7 +480,7 @@ OverlayVulkan::create_pipeline()
.stride = sizeof(OverlayVertex),
.inputRate = VK_VERTEX_INPUT_RATE_VERTEX,
};
VkVertexInputAttributeDescription attributes[3]{
VkVertexInputAttributeDescription attributes[6]{
{.location = 0,
.binding = 0,
.format = VK_FORMAT_R32G32_SFLOAT,
@@ -473,12 +493,24 @@ OverlayVulkan::create_pipeline()
.binding = 0,
.format = VK_FORMAT_R32G32B32A32_SFLOAT,
.offset = offsetof(OverlayVertex, col)},
{.location = 3,
.binding = 0,
.format = VK_FORMAT_R32G32B32A32_SFLOAT,
.offset = offsetof(OverlayVertex, atlas_x0)},
{.location = 4,
.binding = 0,
.format = VK_FORMAT_R32G32_SFLOAT,
.offset = offsetof(OverlayVertex, dest_w)},
{.location = 5,
.binding = 0,
.format = VK_FORMAT_R32_SFLOAT,
.offset = offsetof(OverlayVertex, transfer)},
};
VkPipelineVertexInputStateCreateInfo vertex_input{
.sType = VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO,
.vertexBindingDescriptionCount = 1,
.pVertexBindingDescriptions = &binding,
.vertexAttributeDescriptionCount = 3,
.vertexAttributeDescriptionCount = 6,
.pVertexAttributeDescriptions = attributes,
};
VkPipelineInputAssemblyStateCreateInfo input_assembly{
@@ -541,6 +573,10 @@ OverlayVulkan::create_pipeline()
check_vk(vkCreateGraphicsPipelines(this->device_, VK_NULL_HANDLE, 1,
&pipeline_info, nullptr, &this->pipeline_),
"vkCreateGraphicsPipelines overlay");
stages[1].module = this->thumb_frag_;
check_vk(vkCreateGraphicsPipelines(this->device_, VK_NULL_HANDLE, 1,
&pipeline_info, nullptr, &this->thumb_pipeline_),
"vkCreateGraphicsPipelines thumbnails");
return true;
}
@@ -818,10 +854,12 @@ OverlayVulkan::upload_thumb(const uint16_t *pixels, int width, int height,
{
if (recreated)
*recreated = false;
if (!this->device_ || !pixels || width <= 0 || height <= 0 || dst_x < 0 ||
dst_y < 0 || atlas_side <= 0 || dst_x + width > atlas_side ||
dst_y + height > atlas_side)
return false;
const bool fresh = !this->thumb_image_ || this->thumb_side_ != atlas_side;
if (fresh) {
vkDeviceWaitIdle(this->device_);
@@ -851,6 +889,48 @@ OverlayVulkan::upload_thumb(const uint16_t *pixels, int width, int height,
return true;
}
bool
OverlayVulkan::rebuild_thumbs(
const vector<ThumbUpload> &uploads, int atlas_side)
{
if (!this->device_ || atlas_side <= 0 || uploads.empty())
return false;
VkImage image = VK_NULL_HANDLE;
VkDeviceMemory memory = VK_NULL_HANDLE;
VkImageView view = VK_NULL_HANDLE;
if (!create_sampled(atlas_side, atlas_side, &image, &memory))
return false;
bool first = true;
for (const ThumbUpload &upload : uploads) {
if (!upload.pixels || upload.width <= 0 || upload.height <= 0 ||
upload.x < 0 || upload.y < 0 ||
upload.x + upload.width > atlas_side ||
upload.y + upload.height > atlas_side ||
!copy_rgba16(upload.pixels, upload.width, upload.height, image,
first ? VK_IMAGE_LAYOUT_UNDEFINED
: VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL,
upload.x, upload.y)) {
destroy_sampled(&image, &memory, &view);
return false;
}
first = false;
}
const VkComponentMapping bgra{VK_COMPONENT_SWIZZLE_B,
VK_COMPONENT_SWIZZLE_G, VK_COMPONENT_SWIZZLE_R,
VK_COMPONENT_SWIZZLE_A};
bind_sampled(image, &view, this->descriptor_sets_[kOverlayTexThumbs], bgra);
vkDeviceWaitIdle(this->device_);
destroy_thumbs();
this->thumb_image_ = image;
this->thumb_memory_ = memory;
this->thumb_view_ = view;
this->thumb_side_ = atlas_side;
return true;
}
void
OverlayVulkan::reset_thumbs()
{
@@ -985,7 +1065,6 @@ OverlayVulkan::record(
.maxDepth = 1.f,
};
vkCmdSetViewport(cmd, 0, 1, &viewport);
vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, this->pipeline_);
VkDeviceSize offset = 0;
vkCmdBindVertexBuffers(cmd, 0, 1, &this->vertex_buffer_, &offset);
vkCmdBindIndexBuffer(cmd, this->index_buffer_, 0, VK_INDEX_TYPE_UINT32);
@@ -999,6 +1078,7 @@ OverlayVulkan::record(
0, sizeof(push), &push);
uint32_t bound_tex = ~0u;
VkPipeline bound_pipeline = VK_NULL_HANDLE;
for (const OverlayCmd &draw_cmd : mesh.cmds) {
if (draw_cmd.idx_count == 0)
continue;
@@ -1007,6 +1087,14 @@ OverlayVulkan::record(
if (draw_cmd.tex > kOverlayTexThumbs ||
(draw_cmd.tex == kOverlayTexFont && !this->font_view_))
continue;
const VkPipeline pipeline = draw_cmd.tex == kOverlayTexThumbs
? this->thumb_pipeline_ : this->pipeline_;
if (!pipeline)
continue;
if (pipeline != bound_pipeline) {
vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline);
bound_pipeline = pipeline;
}
if (draw_cmd.tex != bound_tex) {
vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS,
this->pipeline_layout_, 0, 1,
@@ -1098,16 +1186,22 @@ OverlayVulkan::destroy_pipeline()
return;
if (this->pipeline_)
vkDestroyPipeline(this->device_, this->pipeline_, nullptr);
if (this->thumb_pipeline_)
vkDestroyPipeline(this->device_, this->thumb_pipeline_, nullptr);
if (this->pipeline_layout_)
vkDestroyPipelineLayout(this->device_, this->pipeline_layout_, nullptr);
if (this->vert_)
vkDestroyShaderModule(this->device_, this->vert_, nullptr);
if (this->frag_)
vkDestroyShaderModule(this->device_, this->frag_, nullptr);
if (this->thumb_frag_)
vkDestroyShaderModule(this->device_, this->thumb_frag_, nullptr);
this->pipeline_ = VK_NULL_HANDLE;
this->thumb_pipeline_ = VK_NULL_HANDLE;
this->pipeline_layout_ = VK_NULL_HANDLE;
this->vert_ = VK_NULL_HANDLE;
this->frag_ = VK_NULL_HANDLE;
this->thumb_frag_ = VK_NULL_HANDLE;
}
void
+19 -1
View File
@@ -28,6 +28,13 @@ struct OverlayVertex {
float u = 0;
float v = 0;
Colour col{};
float atlas_x0 = 0;
float atlas_y0 = 0;
float atlas_x1 = 0;
float atlas_y1 = 0;
float dest_w = 0;
float dest_h = 0;
float transfer = 0;
};
constexpr uint32_t kOverlayTexFont = 0;
@@ -51,6 +58,14 @@ struct OverlayMesh {
float display_h = 0;
};
struct ThumbUpload {
const uint16_t *pixels = nullptr;
int width = 0;
int height = 0;
int x = 0;
int y = 0;
};
class OverlayList
{
struct Clip {
@@ -87,7 +102,7 @@ public:
void add_image(float x0, float y0, float x1, float y1, float u0, float v0,
float u1, float v1, Colour col);
void add_thumb(float x0, float y0, float x1, float y1, float u0, float v0,
float u1, float v1, Colour col = {1, 1, 1, 1});
float u1, float v1, int transfer, Colour col = {1, 1, 1, 1});
[[nodiscard]] const OverlayMesh &mesh() const { return this->mesh_; }
};
@@ -125,11 +140,13 @@ class OverlayVulkan
VkDescriptorSetLayout set_layout_ = VK_NULL_HANDLE;
VkPipelineLayout pipeline_layout_ = VK_NULL_HANDLE;
VkPipeline pipeline_ = VK_NULL_HANDLE;
VkPipeline thumb_pipeline_ = VK_NULL_HANDLE;
VkSampler sampler_ = VK_NULL_HANDLE;
VkDescriptorPool descriptor_pool_ = VK_NULL_HANDLE;
VkDescriptorSet descriptor_sets_[2]{};
VkShaderModule vert_ = VK_NULL_HANDLE;
VkShaderModule frag_ = VK_NULL_HANDLE;
VkShaderModule thumb_frag_ = VK_NULL_HANDLE;
VkImage font_image_ = VK_NULL_HANDLE;
VkDeviceMemory font_memory_ = VK_NULL_HANDLE;
@@ -168,6 +185,7 @@ public:
[[nodiscard]] int thumb_atlas_max() const { return this->thumb_atlas_max_; }
bool upload_thumb(const uint16_t *pixels, int width, int height, int dst_x,
int dst_y, int atlas_side, bool *recreated = nullptr);
bool rebuild_thumbs(const std::vector<ThumbUpload> &uploads, int atlas_side);
void reset_thumbs();
void record(
VkCommandBuffer cmd, uint32_t image_index, const OverlayMesh &mesh);
+6
View File
@@ -560,6 +560,12 @@ Renderer::upload_thumb(const uint16_t *pixels, int width, int height, int dst_x,
pixels, width, height, dst_x, dst_y, atlas_side, recreated);
}
bool
Renderer::rebuild_thumbs(const vector<ThumbUpload> &uploads, int atlas_side)
{
return this->overlay_.rebuild_thumbs(uploads, atlas_side);
}
void
Renderer::reset_thumbs()
{
+1
View File
@@ -122,6 +122,7 @@ public:
[[nodiscard]] int thumb_atlas_max() const;
bool upload_thumb(const uint16_t *pixels, int width, int height, int dst_x,
int dst_y, int atlas_side, bool *recreated = nullptr);
bool rebuild_thumbs(const std::vector<ThumbUpload> &uploads, int atlas_side);
void reset_thumbs();
void resize(Extent pixel);
// False means no swapchain image was immediately available.
+105 -70
View File
@@ -1,5 +1,5 @@
//
// thumbnail-cache.cpp: shared thumbnail-spec cache
// thumbnail-cache.cpp: shared {wide-,}thumbnail-spec cache
//
// Copyright The Dawn Authors
// SPDX-License-Identifier: MPL-2.0
@@ -57,6 +57,7 @@ parse_metadata(const uint8_t *data, size_t size, Metadata *out)
{
if (!out || !data || !size || data[size - 1] != 0)
return false;
out->values.clear();
vector<string> fields;
const char *p = reinterpret_cast<const char *>(data);
@@ -84,10 +85,12 @@ number(const string &text, uint64_t *out)
{
if (!out || text.empty())
return false;
uint64_t value = 0;
auto result = from_chars(text.data(), text.data() + text.size(), value);
if (result.ec != errc{} || result.ptr != text.data() + text.size())
return false;
*out = value;
return true;
}
@@ -143,6 +146,7 @@ webp_metadata(const QByteArray &bytes, Metadata *meta)
WebPDemuxer *demux = WebPDemux(&data);
if (!demux)
return false;
WebPChunkIterator chunk{};
const bool found = WebPDemuxGetChunk(demux, "THUM", 1, &chunk);
const bool ok =
@@ -184,6 +188,7 @@ read_wide(const QString &path, const ThumbnailSource &source, int tier,
if (bytes.isEmpty() || !webp_metadata(bytes, &meta) ||
!valid_metadata(meta, source) || !cmm || !screen)
return hit;
const string *tag = value(meta, kColorSpace);
const bool p3 = tag && *tag == "Display P3";
shared_ptr<dawn::Profile> src =
@@ -191,6 +196,7 @@ read_wide(const QString &path, const ThumbnailSource &source, int tier,
dawn::ImagePtr image = decode_webp(bytes, *cmm, src.get(), screen);
if (!image)
return hit;
hit.width = image->width;
hit.height = image->height;
hit.pixels.resize(size_t(hit.width) * hit.height * 4);
@@ -198,7 +204,7 @@ read_wide(const QString &path, const ThumbnailSource &source, int tier,
memcpy(hit.pixels.data() + size_t(y) * hit.width * 4,
row_u16(*image, y), size_t(hit.width) * dawn::kBytesPerPixel);
hit.tier = tier;
hit.interim = !p3 || tier < desired_tier;
hit.interim = !p3 || tier != desired_tier;
read_image_dimensions(meta, &hit);
return hit;
}
@@ -211,6 +217,7 @@ read_png(const QString &path, const ThumbnailSource &source, int tier,
const QByteArray bytes = read_file(path);
if (bytes.isEmpty() || !cmm || !screen)
return hit;
dawn::OpenContext ctx;
ctx.uri = path.toStdString();
ctx.cmm = cmm;
@@ -222,14 +229,17 @@ read_png(const QString &path, const ThumbnailSource &source, int tier,
ctx, &error);
if (!image)
return hit;
Metadata meta;
meta.values = image->text;
if (!valid_metadata(meta, source))
return {};
shared_ptr<dawn::Profile> srgb = cmm->get_profile_sRGB();
if (!cmm->transform_bgra16(image->data.data(), image->width, image->height,
srgb.get(), screen, true, true))
return {};
hit.width = image->width;
hit.height = image->height;
hit.pixels.resize(size_t(hit.width) * hit.height * 4);
@@ -279,7 +289,7 @@ make_metadata(
bool
remove_thumbnail(const QString &path, const QString &reason)
{
qWarning("%s: deleting: %s", qUtf8Printable(path), qUtf8Printable(reason));
qInfo("%s: deleting: %s", qUtf8Printable(path), qUtf8Printable(reason));
if (QFile::remove(path))
return true;
qWarning("%s: cannot delete", qUtf8Printable(path));
@@ -313,10 +323,12 @@ bool
thumbnail_cache_contains(const QString &path)
{
const QString clean = QDir::cleanPath(QFileInfo(path).absoluteFilePath());
#if defined(Q_OS_LINUX)
#if !defined Q_OS_WIN && !defined Q_OS_MACOS
const QString root = QDir::cleanPath(thumbnail_cache_root());
return !root.isEmpty() &&
(clean == root || clean.startsWith(root + QDir::separator()));
if (!root.isEmpty() &&
(clean == root || clean.startsWith(root + QDir::separator())))
return true;
// TODO(p): Also plainly look for a "/.cache/thumbnails/" substring.
#else
const QStringList parts =
QDir::fromNativeSeparators(clean).split(u'/', Qt::SkipEmptyParts);
@@ -324,14 +336,14 @@ thumbnail_cache_contains(const QString &path)
if (part.compare(QStringLiteral("thumbnails"), Qt::CaseInsensitive) ==
0)
return true;
return false;
#endif
return false;
}
int
thumbnail_tier_for_height(int pixels)
{
for (int i = 0; i < int(kHeights.size()); ++i)
for (int i = 0; i < int(kHeights.size()); i++)
if (pixels <= kHeights[size_t(i)])
return i;
return int(kHeights.size()) - 1;
@@ -352,20 +364,35 @@ thumbnail_cache_lookup(const ThumbnailSource &source, int desired_tier,
if (thumbnail_cache_root().isEmpty() ||
thumbnail_cache_contains(source.path))
return {};
desired_tier = clamp(desired_tier, 0, int(kNames.size()) - 1);
for (int i = 0; i < int(kNames.size()); ++i) {
int tier = desired_tier + i;
if (tier >= int(kNames.size()))
tier = int(kNames.size()) - 1 - i;
ThumbnailHit hit = read_wide(cache_path(source, tier, true), source,
tier, desired_tier, cmm, screen_profile);
if (!hit.pixels.empty())
return hit;
hit = read_png(
auto wide = [&](int tier) {
return read_wide(cache_path(source, tier, true), source, tier,
desired_tier, cmm, screen_profile);
};
auto png = [&](int tier) {
return read_png(
cache_path(source, tier, false), source, tier, cmm, screen_profile);
if (!hit.pixels.empty())
};
ThumbnailHit hit = wide(desired_tier);
if (!hit.pixels.empty())
return hit;
if (hit = png(desired_tier); !hit.pixels.empty())
return hit;
for (int tier = desired_tier + 1; tier < int(kNames.size()); tier++)
if (hit = wide(tier); !hit.pixels.empty())
return hit;
for (int tier = desired_tier - 1; tier >= 0; --tier)
if (hit = wide(tier); !hit.pixels.empty())
return hit;
for (int tier = desired_tier + 1; tier < int(kNames.size()); tier++)
if (hit = png(tier); !hit.pixels.empty())
return hit;
for (int tier = desired_tier - 1; tier >= 0; --tier)
if (hit = png(tier); !hit.pixels.empty())
return hit;
}
return {};
}
@@ -376,12 +403,14 @@ thumbnail_cache_write(const ThumbnailSource &source, int tier,
{
if (error)
error->clear();
if (!pixels || !width || !height || tier < 0 ||
tier >= int(kNames.size()) || thumbnail_cache_root().isEmpty() ||
thumbnail_cache_contains(source.path))
return false;
vector<uint8_t> bgra(size_t(width) * height * 4);
for (size_t i = 0, n = size_t(width) * height; i < n; ++i) {
for (size_t i = 0, n = size_t(width) * height; i < n; i++) {
const uint32_t a = pixels[i * 4 + 3];
bgra[i * 4 + 3] = uint8_t((a + 128) / 257);
for (int c = 0; c < 3; ++c) {
@@ -401,7 +430,7 @@ thumbnail_cache_write(const ThumbnailSource &source, int tier,
WebPMux *mux = nullptr;
bool ok = WebPConfigInit(&config) && WebPConfigLosslessPreset(&config, 6);
config.near_lossless = 95;
config.thread_level = 1;
config.thread_level = 0;
ok = ok && WebPValidateConfig(&config) && WebPPictureInit(&picture);
if (ok) {
picture.use_argb = 1;
@@ -473,6 +502,60 @@ thumbnail_cache_write(const ThumbnailSource &source, int tier,
return true;
}
static void
thumbnail_cache_invalidate_one(const QString &path)
{
Metadata meta;
if (!webp_metadata(read_file(path), &meta)) {
remove_thumbnail(
path, QStringLiteral("invalid thumbnail metadata"));
return;
}
const string *uri_text = value(meta, kUri);
const string *mtime_text = value(meta, kMtime);
uint64_t mtime = 0, size = 0;
if (!uri_text || !mtime_text || !number(*mtime_text, &mtime)) {
remove_thumbnail(
path, QStringLiteral("missing thumbnail identity"));
return;
}
const QByteArray uri(uri_text->data(), qsizetype(uri_text->size()));
const QByteArray expected =
QCryptographicHash::hash(uri, QCryptographicHash::Md5).toHex() +
".webp";
if (QFileInfo(path).fileName().toLatin1() != expected) {
remove_thumbnail(path, QStringLiteral("URI checksum mismatch"));
return;
}
const QUrl url = QUrl::fromEncoded(uri);
if (!url.isLocalFile()) {
qWarning(
"%s: cannot verify non-local URI", qUtf8Printable(path));
return;
}
const QFileInfo target(url.toLocalFile());
if (!target.exists()) {
remove_thumbnail(
path, QStringLiteral("source no longer exists"));
return;
}
if (!target.isReadable()) {
qWarning("%s: source is not readable", qUtf8Printable(path));
return;
}
if (target.lastModified().toSecsSinceEpoch() != qint64(mtime)) {
remove_thumbnail(
path, QStringLiteral("modification time mismatch"));
return;
}
if (const string *size_text = value(meta, kSize)) {
if (!number(*size_text, &size) ||
uint64_t(target.size()) != size)
remove_thumbnail(
path, QStringLiteral("file size mismatch"));
}
}
void
thumbnail_cache_invalidate()
{
@@ -493,55 +576,7 @@ thumbnail_cache_invalidate()
QDirIterator it(dir, {QStringLiteral("*.webp")}, QDir::Files);
while (it.hasNext()) {
const QString path = it.next();
Metadata meta;
if (!webp_metadata(read_file(path), &meta)) {
remove_thumbnail(
path, QStringLiteral("invalid thumbnail metadata"));
continue;
}
const string *uri_text = value(meta, kUri);
const string *mtime_text = value(meta, kMtime);
uint64_t mtime = 0, size = 0;
if (!uri_text || !mtime_text || !number(*mtime_text, &mtime)) {
remove_thumbnail(
path, QStringLiteral("missing thumbnail identity"));
continue;
}
const QByteArray uri(uri_text->data(), qsizetype(uri_text->size()));
const QByteArray expected =
QCryptographicHash::hash(uri, QCryptographicHash::Md5).toHex() +
".webp";
if (QFileInfo(path).fileName().toLatin1() != expected) {
remove_thumbnail(path, QStringLiteral("URI checksum mismatch"));
continue;
}
const QUrl url = QUrl::fromEncoded(uri);
if (!url.isLocalFile()) {
qWarning(
"%s: cannot verify non-local URI", qUtf8Printable(path));
continue;
}
const QFileInfo target(url.toLocalFile());
if (!target.exists()) {
remove_thumbnail(
path, QStringLiteral("source no longer exists"));
continue;
}
if (!target.isReadable()) {
qWarning("%s: source is not readable", qUtf8Printable(path));
continue;
}
if (target.lastModified().toSecsSinceEpoch() != qint64(mtime)) {
remove_thumbnail(
path, QStringLiteral("modification time mismatch"));
continue;
}
if (const string *size_text = value(meta, kSize)) {
if (!number(*size_text, &size) ||
uint64_t(target.size()) != size)
remove_thumbnail(
path, QStringLiteral("file size mismatch"));
}
thumbnail_cache_invalidate_one(path);
}
}
}
+2 -1
View File
@@ -1,5 +1,5 @@
//
// thumbnail-cache.hpp: shared thumbnail-spec cache
// thumbnail-cache.hpp: shared {wide-,}thumbnail-spec cache
//
// Copyright The Dawn Authors
// SPDX-License-Identifier: MPL-2.0
@@ -42,6 +42,7 @@ ThumbnailSource thumbnail_source(
const QString &path, int64_t mtime_ms, uint64_t size);
bool thumbnail_cache_contains(const QString &path);
/// Return the appropriate "tier" while respecting high-DPI.
int thumbnail_tier_for_height(int pixels);
int thumbnail_tier_height(int tier);
+255 -4
View File
@@ -6,6 +6,7 @@
//
#include "thumbnailer.hpp"
#include "thumbnail-cache.hpp"
#include <QMetaObject>
#include <QThread>
@@ -32,6 +33,7 @@ namespace
{
constexpr uint64_t kThumbRingBytes = 256ull * 1024 * 1024;
constexpr size_t kPendingBundleBytes = 1ull << 30;
constexpr size_t kPriorityCount = 4;
constexpr size_t kGuiBatch = 32;
@@ -40,6 +42,12 @@ constexpr size_t kGuiBatch = 32;
// completion has at least been placed on the GUI queue.
thread_local shared_ptr<atomic_bool> current_cpu_gate;
bool
same_source(const ThumbnailSource &a, const ThumbnailSource &b)
{
return a.uri == b.uri && a.mtime == b.mtime && a.size == b.size;
}
size_t
priority_index(Thumbnailer::Priority priority)
{
@@ -89,6 +97,7 @@ struct Thumbnailer::Impl {
struct GuiTask {
Client client = 0;
uint64_t epoch = 0;
Priority priority = Priority::Maintenance;
Completion completion;
};
struct GpuTask {
@@ -100,6 +109,20 @@ struct Thumbnailer::Impl {
shared_ptr<atomic_bool> gate;
optional<dawn::ThumbScaler::Result> result;
};
struct BundleSlot {
Reservation id = 0;
Client client = 0;
uint64_t epoch = 0;
ThumbnailSource source;
int top_tier = 0;
size_t reserved_bytes = 0;
Priority priority = Priority::Maintenance;
shared_ptr<const ThumbnailBundle> bundle;
};
struct EncodeJob {
Reservation reservation = 0;
shared_ptr<const ThumbnailBundle> bundle;
};
Thumbnailer *owner = nullptr;
mutable mutex mu;
@@ -110,12 +133,18 @@ struct Thumbnailer::Impl {
unordered_map<Client, ClientState> clients;
unordered_map<uint64_t, GpuTask> gpu_tasks;
vector<std::thread> workers;
vector<std::thread> encoders;
deque<EncodeJob> encode;
deque<Reservation> encoded;
unordered_map<Reservation, BundleSlot> bundles;
size_t worker_count = 1;
size_t bundle_bytes = 0;
unique_ptr<dawn::ThumbScaler> scaler;
QTimer timer;
atomic_bool pump_posted = false;
uint64_t next_client = 1;
uint64_t next_gpu = 1;
uint64_t next_reservation = 1;
size_t cpu_running = 0;
size_t background_running = 0;
size_t background_max = 1;
@@ -126,12 +155,14 @@ struct Thumbnailer::Impl {
bool have_cpu() const;
bool pop_cpu(shared_ptr<CpuTask> *task);
void worker_loop();
void encoder_loop();
void erase_queued(Client id, ClientState &state);
void erase_gui(Client id, ClientState &state);
void erase_gpu(Client id, ClientState &state);
void drop_gpu(uint64_t gpu_id);
void fail_gpu(uint64_t gpu_id, string path);
bool scaler_queue(const dawn::ThumbScaler::Job &job);
void erase_unpublished(Client id);
};
Thumbnailer::Impl::Impl(Thumbnailer *thumbnailer, unsigned worker_count)
@@ -140,6 +171,7 @@ Thumbnailer::Impl::Impl(Thumbnailer *thumbnailer, unsigned worker_count)
timer.setSingleShot(true);
QObject::connect(&timer, &QTimer::timeout, owner,
[thumbnailer] { thumbnailer->pump(); });
unsigned count =
worker_count ? worker_count : std::thread::hardware_concurrency();
if (!count)
@@ -149,6 +181,9 @@ Thumbnailer::Impl::Impl(Thumbnailer *thumbnailer, unsigned worker_count)
workers.reserve(count);
for (unsigned i = 0; i < count; ++i)
workers.emplace_back([this] { worker_loop(); });
encoders.reserve(count);
for (unsigned i = 0; i < count; ++i)
encoders.emplace_back([this] { encoder_loop(); });
}
Thumbnailer::Impl::~Impl()
@@ -164,6 +199,7 @@ Thumbnailer::Impl::shutdown()
stop = true;
for (auto &queue : cpu)
queue.clear();
encode.clear();
gui.clear();
gpu_tasks.clear();
clients.clear();
@@ -174,6 +210,15 @@ Thumbnailer::Impl::shutdown()
for (std::thread &worker : workers)
if (worker.joinable())
worker.join();
for (std::thread &encoder : encoders)
if (encoder.joinable())
encoder.join();
{
lock_guard lock(mu);
bundles.clear();
encoded.clear();
bundle_bytes = 0;
}
timer.stop();
scaler.reset();
}
@@ -223,7 +268,7 @@ Thumbnailer::Impl::pop_cpu(shared_ptr<CpuTask> *task)
void
Thumbnailer::Impl::worker_loop()
{
for (;;) {
while (true) {
shared_ptr<CpuTask> task;
{
unique_lock lock(mu);
@@ -268,8 +313,8 @@ Thumbnailer::Impl::worker_loop()
if (client != clients.end() && completion &&
client->second.epoch == task->epoch) {
client->second.gui++;
gui.push_back(
{task->client, task->epoch, std::move(completion)});
gui.push_back({task->client, task->epoch, task->running_priority,
std::move(completion)});
}
}
task->gate->store(true, memory_order_release);
@@ -278,6 +323,59 @@ Thumbnailer::Impl::worker_loop()
}
}
void
Thumbnailer::Impl::encoder_loop()
{
while (true) {
EncodeJob job;
{
unique_lock lock(mu);
cv.wait(lock, [this] { return stop || !encode.empty(); });
if (stop && encode.empty())
return;
job = std::move(encode.front());
encode.pop_front();
}
if (job.bundle) {
for (const ThumbnailTierPixels &tier : job.bundle->tiers) {
if (!tier.pixels || tier.pixels->empty())
continue;
QString error;
if (!thumbnail_cache_write(job.bundle->source, tier.tier,
tier.pixels->data(), tier.width, tier.height,
job.bundle->image_width, job.bundle->image_height,
&error)) {
if (error.isEmpty())
error = QStringLiteral("thumbnail cache write failed");
qWarning("%s: tier %d: %s",
qUtf8Printable(job.bundle->source.path), tier.tier,
qUtf8Printable(error));
}
}
}
{
lock_guard lock(mu);
if (stop)
continue;
encoded.push_back(job.reservation);
}
owner->schedule_pump();
}
}
void
Thumbnailer::Impl::erase_unpublished(Client id)
{
for (auto it = bundles.begin(); it != bundles.end();) {
if (it->second.client != id || it->second.bundle) {
++it;
continue;
}
bundle_bytes -= it->second.reserved_bytes;
it = bundles.erase(it);
}
}
void
Thumbnailer::Impl::erase_queued(Client id, ClientState &state)
{
@@ -305,6 +403,8 @@ Thumbnailer::Impl::erase_gpu(Client id, ClientState &state)
{
for (auto it = gpu_tasks.begin(); it != gpu_tasks.end();) {
if (it->second.client == id) {
if (scaler)
scaler->cancel(it->first);
state.gpu--;
it = gpu_tasks.erase(it);
} else {
@@ -371,6 +471,7 @@ Thumbnailer::init(const GpuContext &gpu)
return true;
if (!gpu.phys() || !gpu.device() || !gpu.queue())
return false;
auto scaler = make_unique<dawn::ThumbScaler>();
string error;
if (!scaler->init(gpu.phys(), gpu.device(), gpu.queue(), gpu.queue_family(),
@@ -401,9 +502,11 @@ Thumbnailer::remove_client(Client id)
auto found = impl_->clients.find(id);
if (found == impl_->clients.end())
return;
impl_->erase_queued(id, found->second);
impl_->erase_gui(id, found->second);
impl_->erase_gpu(id, found->second);
impl_->erase_unpublished(id);
impl_->clients.erase(found);
}
@@ -417,6 +520,7 @@ Thumbnailer::set_epoch(Client id, uint64_t epoch)
impl_->erase_queued(id, found->second);
impl_->erase_gui(id, found->second);
impl_->erase_gpu(id, found->second);
impl_->erase_unpublished(id);
found->second.epoch = epoch;
found->second.activity_pending = true;
schedule_pump();
@@ -452,7 +556,7 @@ Thumbnailer::submit(
client->second.queued++;
client->second.activity_pending = true;
}
impl_->cv.notify_one();
impl_->cv.notify_all();
schedule_pump();
return true;
}
@@ -467,6 +571,7 @@ Thumbnailer::reprioritize(
if (impl_->stop || client == impl_->clients.end() ||
client->second.epoch != epoch)
return false;
bool found_any = false;
if (auto found = client->second.keyed.find(key);
found != client->second.keyed.end()) {
@@ -557,6 +662,103 @@ Thumbnailer::submit_gpu(Client id, uint64_t epoch, Priority priority,
return true;
}
Thumbnailer::Reservation
Thumbnailer::reserve_bundle(Client id, uint64_t epoch,
const ThumbnailSource &source, int top_tier, size_t bytes, Priority priority)
{
if (!bytes || bytes > kPendingBundleBytes)
return 0;
lock_guard lock(impl_->mu);
auto client = impl_->clients.find(id);
if (impl_->stop || client == impl_->clients.end() ||
client->second.epoch != epoch ||
impl_->bundles.size() >= impl_->encoders.size() ||
bytes > kPendingBundleBytes - impl_->bundle_bytes)
return 0;
for (auto &[reservation, slot] : impl_->bundles) {
if (same_source(slot.source, source) && slot.top_tier == top_tier) {
if (priority_index(priority) < priority_index(slot.priority))
slot.priority = priority;
return 0;
}
}
Reservation reservation = impl_->next_reservation++;
if (!reservation)
reservation = impl_->next_reservation++;
impl_->bundles.emplace(reservation,
Impl::BundleSlot{reservation, id, epoch, source, top_tier, bytes,
priority, {}});
impl_->bundle_bytes += bytes;
return reservation;
}
void
Thumbnailer::cancel_bundle(Reservation reservation)
{
if (!reservation)
return;
lock_guard lock(impl_->mu);
auto found = impl_->bundles.find(reservation);
if (found == impl_->bundles.end() || found->second.bundle)
return;
impl_->bundle_bytes -= found->second.reserved_bytes;
impl_->bundles.erase(found);
}
bool
Thumbnailer::publish_bundle(
Reservation reservation, shared_ptr<const ThumbnailBundle> bundle)
{
if (!reservation || !bundle || bundle->tiers.empty())
return false;
{
lock_guard lock(impl_->mu);
auto found = impl_->bundles.find(reservation);
if (impl_->stop || found == impl_->bundles.end() ||
found->second.bundle ||
!same_source(found->second.source, bundle->source) ||
found->second.top_tier != bundle->top_tier ||
bundle->bytes() > found->second.reserved_bytes)
return false;
found->second.client = 0;
found->second.epoch = 0;
found->second.bundle = bundle;
impl_->encode.push_back({reservation, std::move(bundle)});
}
impl_->cv.notify_all();
return true;
}
shared_ptr<const ThumbnailBundle>
Thumbnailer::pending_bundle(const ThumbnailSource &source, int tier) const
{
lock_guard lock(impl_->mu);
for (const auto &[reservation, slot] : impl_->bundles) {
(void) reservation;
if (slot.bundle && same_source(slot.source, source) &&
slot.bundle->find(tier))
return slot.bundle;
}
return {};
}
size_t
Thumbnailer::pending_bundle_limit() const
{
return impl_->encoders.size();
}
size_t
Thumbnailer::pending_bundle_bytes() const
{
lock_guard lock(impl_->mu);
return impl_->bundle_bytes;
}
size_t
Thumbnailer::background_limit() const
{
@@ -574,11 +776,37 @@ Thumbnailer::busy(Client id) const
return state.queued || state.running || state.gpu || state.gui;
}
bool
Thumbnailer::foreground_busy(Client id) const
{
lock_guard lock(impl_->mu);
auto client = impl_->clients.find(id);
if (client == impl_->clients.end())
return false;
for (const auto &[key, task] : client->second.keyed) {
(void) key;
if (priority_index(task->priority) < priority_index(Priority::Dimensions))
return true;
}
for (const auto &[gpu_id, task] : impl_->gpu_tasks) {
(void) gpu_id;
if (task.client == id && priority_index(task.priority) <
priority_index(Priority::Dimensions))
return true;
}
for (const Impl::GuiTask &task : impl_->gui)
if (task.client == id && priority_index(task.priority) <
priority_index(Priority::Dimensions))
return true;
return false;
}
void
Thumbnailer::schedule_pump()
{
if (impl_->pump_posted.exchange(true))
return;
QMetaObject::invokeMethod(this, [this] { pump(); }, Qt::QueuedConnection);
}
@@ -586,6 +814,28 @@ void
Thumbnailer::pump()
{
impl_->pump_posted = false;
vector<Completion> encoder_activity;
{
lock_guard lock(impl_->mu);
bool released = false;
while (!impl_->encoded.empty()) {
const Reservation reservation = impl_->encoded.front();
impl_->encoded.pop_front();
auto found = impl_->bundles.find(reservation);
if (found == impl_->bundles.end())
continue;
impl_->bundle_bytes -= found->second.reserved_bytes;
impl_->bundles.erase(found);
released = true;
}
if (released)
for (auto &entry : impl_->clients)
if (entry.second.activity)
encoder_activity.push_back(entry.second.activity);
}
for (Completion &notify : encoder_activity)
notify();
// CPU completions establish browser state needed by any GPU job they
// queued. Always apply them before polling those jobs.
size_t gui_count = 0;
@@ -652,6 +902,7 @@ Thumbnailer::pump()
lock_guard lock(impl_->mu);
if (!impl_->gui.empty())
schedule_pump();
// A registered GPU task can be between copying a tile and publishing it
// to the scaler, in which case scaler->busy() is momentarily false.
// Keep polling until queue() has returned and the task has been
+56 -9
View File
@@ -8,6 +8,7 @@
#pragma once
#include "gpu.hpp"
#include "thumbnail-cache.hpp"
#include "libdn/thumb-scaler.hpp"
@@ -21,10 +22,53 @@
namespace dn
{
struct ThumbnailTierPixels {
int tier = 0;
uint32_t width = 0;
uint32_t height = 0;
std::shared_ptr<const std::vector<uint16_t>> pixels;
[[nodiscard]] size_t bytes() const
{
return pixels ? pixels->size() * sizeof(uint16_t) : 0;
}
};
struct ThumbnailBundle {
ThumbnailSource source;
uint32_t image_width = 0;
uint32_t image_height = 0;
int top_tier = 0;
std::vector<ThumbnailTierPixels> tiers;
[[nodiscard]] const ThumbnailTierPixels *find(int wanted) const
{
for (const ThumbnailTierPixels &tier : this->tiers)
if (tier.tier == wanted && tier.pixels && !tier.pixels->empty())
return &tier;
return nullptr;
}
[[nodiscard]] size_t bytes() const
{
size_t total = 0;
for (const ThumbnailTierPixels &tier : this->tiers)
total += tier.bytes();
return total;
}
};
class Thumbnailer final : public QObject
{
struct Impl;
std::unique_ptr<Impl> impl_;
void schedule_pump();
void pump();
public:
using Client = uint64_t;
using Reservation = uint64_t;
using Completion = std::function<void()>;
using Work = std::function<Completion()>;
using GpuCompletion = std::function<void(dawn::ThumbScaler::Result)>;
@@ -51,20 +95,23 @@ public:
std::string key = {});
bool reprioritize(Client client, uint64_t epoch, Priority priority,
const std::string &key);
/// Copies job pixels on a worker. Safe on the GUI thread; keep `job.pixels`
/// alive until the completion runs.
/// Copies owned job pixels on a worker. Safe on the GUI thread.
bool submit_gpu(Client client, uint64_t epoch, Priority priority,
dawn::ThumbScaler::Job job, GpuCompletion completion,
std::string key = {});
Reservation reserve_bundle(Client client, uint64_t epoch,
const ThumbnailSource &source, int top_tier, size_t bytes,
Priority priority);
void cancel_bundle(Reservation reservation);
bool publish_bundle(
Reservation reservation, std::shared_ptr<const ThumbnailBundle> bundle);
[[nodiscard]] std::shared_ptr<const ThumbnailBundle> pending_bundle(
const ThumbnailSource &source, int tier) const;
[[nodiscard]] size_t pending_bundle_limit() const;
[[nodiscard]] size_t pending_bundle_bytes() const;
[[nodiscard]] bool busy(Client client) const;
[[nodiscard]] bool foreground_busy(Client client) const;
[[nodiscard]] size_t background_limit() const;
private:
struct Impl;
std::unique_ptr<Impl> impl_;
void schedule_pump();
void pump();
};
} // namespace dn
+2
View File
@@ -27,6 +27,8 @@ be applied strictly. On top of that, these additional rules keep authors sane:
* Never use `static_cast<T>(x)`. Prefer `T(x)`, or `(T)(x)`.
* Unless meaning differs, it is `i++` and not `++i`.
* The preprocessor's `defined` is an operator, not a function. No parentheses.
* `sizeof` on objects is an operator, not a function. No parentheses.
+155 -67
View File
@@ -24,6 +24,7 @@
#include <optional>
#include <string>
#include <unordered_map>
#include <unordered_set>
#include <utility>
#include <vector>
@@ -120,15 +121,21 @@ make_shader(
bool
job_size(const ThumbScaler::Job &job, uint64_t *row, uint64_t *bytes)
{
if (!job.pixels || !job.src_w || !job.src_h || !job.out_w || !job.out_h)
if (!job.pixels || job.pixels->empty() || !job.src_w || !job.src_h ||
job.outputs.empty() || job.outputs.size() > kMaxBatchReqs)
return false;
for (const ThumbScaler::Job::Output &output : job.outputs)
if (!output.width || !output.height)
return false;
const uint64_t row_bytes = uint64_t(job.src_w) * kBytesPerPixel;
if (row_bytes > job.stride || row_bytes > UINT64_MAX / job.src_h)
return false;
if (row)
*row = row_bytes;
*bytes = row_bytes * job.src_h;
return *bytes <= SIZE_MAX;
const uint64_t available = job.pixels->size() * sizeof(uint16_t);
const uint64_t needed = uint64_t(job.stride) * (job.src_h - 1) + row_bytes;
return *bytes <= SIZE_MAX && needed <= available;
}
} // namespace
@@ -145,6 +152,8 @@ struct ThumbScaler::Impl {
Transfer transfer = Transfer::Srgb;
bool opaque = true;
uint32_t out_w = 0, out_h = 0;
int output_tag = 0;
vector<Job::Output> outputs;
uint64_t user = 0;
Priority priority = Priority::Maintenance;
string path;
@@ -159,7 +168,7 @@ struct ThumbScaler::Impl {
uint64_t user = 0;
Priority priority = Priority::Maintenance;
uint32_t src_w = 0, src_h = 0;
uint32_t out_w = 0, out_h = 0;
vector<Job::Output> outputs;
uint32_t k = 0, tile_count = 0;
Orientation orientation = Orientation::Rotate0;
Transfer transfer = Transfer::Srgb;
@@ -246,6 +255,7 @@ struct ThumbScaler::Impl {
unordered_map<uint32_t, Range> live;
vector<Waiter *> waiters;
unordered_map<uint64_t, Priority> priority_overrides;
unordered_set<uint64_t> canceled;
vector<Pending> pending;
vector<Result> failed_results;
array<Batch, kBatchSlots> batches;
@@ -771,7 +781,7 @@ ThumbScaler::Impl::fail_items(Batch &b)
for (const Item &item : b.items) {
if (item.slot)
release_range(item.slot);
if (item.kind == Item::Kind::Full)
if (item.kind == Item::Kind::Full && item.slot)
fail_result(item.req);
else if (Session *s = session(item.session))
fail_session(*s);
@@ -786,13 +796,15 @@ ThumbScaler::Impl::build_batch(
VkDeviceSize source_bytes = 0, mid_bytes = 0, output_bytes = 0;
VkDeviceSize readback_bytes = 0, scratch_bytes = 0;
for (Pending &job : jobs) {
Item item;
item.req = std::move(job.req);
item.ring = job.ring;
item.slot = item.req.slot;
item.source_off = align_up(source_bytes, alignment);
source_bytes = item.source_off + item.ring.size;
if (item.req.session) {
Request request = std::move(job.req);
const VkDeviceSize source_off = align_up(source_bytes, alignment);
source_bytes = source_off + job.ring.size;
if (request.session) {
Item item;
item.req = std::move(request);
item.ring = job.ring;
item.slot = item.req.slot;
item.source_off = source_off;
item.kind = Item::Kind::Tile;
item.session = item.req.session;
Session *s = nullptr;
@@ -817,7 +829,22 @@ ThumbScaler::Impl::build_batch(
const uint32_t rh = ceil_div(item.req.tile_h, 2);
scratch_bytes =
max(scratch_bytes, VkDeviceSize(rw) * rh * kBytesPerPixel);
} else {
b.items.push_back(std::move(item));
continue;
}
bool owns_slot = true;
for (const Job::Output &output : request.outputs) {
Item item;
item.req = request;
item.req.outputs.clear();
item.req.out_w = output.width;
item.req.out_h = output.height;
item.req.output_tag = output.tag;
item.ring = job.ring;
item.slot = owns_slot ? request.slot : 0;
item.source_off = source_off;
owns_slot = false;
item.kind = Item::Kind::Full;
orientation_display_size(item.req.src_w, item.req.src_h,
item.req.orientation, &item.display_w, &item.display_h);
@@ -830,8 +857,8 @@ ThumbScaler::Impl::build_batch(
VkDeviceSize(item.req.out_w) * item.req.out_h * kBytesPerPixel;
output_bytes = item.output_off + n;
readback_bytes = item.readback_off + n;
b.items.push_back(std::move(item));
}
b.items.push_back(std::move(item));
}
for (uint32_t id : fits) {
Session *s = nullptr;
@@ -844,31 +871,34 @@ ThumbScaler::Impl::build_batch(
if (!ensure_reduced(*s, error))
continue;
Item item;
item.kind = Item::Kind::Fit;
item.session = id;
item.owner = s;
item.req.user = s->info.user;
item.req.path = s->info.path;
item.req.src_w = s->reduced_w;
item.req.src_h = s->reduced_h;
item.req.out_w = s->info.out_w;
item.req.out_h = s->info.out_h;
item.req.orientation = s->info.orientation;
item.req.transfer = s->info.transfer;
item.req.opaque = s->info.opaque;
orientation_display_size(item.req.src_w, item.req.src_h,
item.req.orientation, &item.display_w, &item.display_h);
item.mid_off = align_up(mid_bytes, alignment);
mid_bytes = item.mid_off +
VkDeviceSize(item.req.out_w) * item.display_h * kBytesPerPixel;
item.output_off = align_up(output_bytes, alignment);
item.readback_off = align_up(readback_bytes, alignment);
const VkDeviceSize n =
VkDeviceSize(item.req.out_w) * item.req.out_h * kBytesPerPixel;
output_bytes = item.output_off + n;
readback_bytes = item.readback_off + n;
b.items.push_back(std::move(item));
for (const Job::Output &output : s->info.outputs) {
Item item;
item.kind = Item::Kind::Fit;
item.session = id;
item.owner = s;
item.req.user = s->info.user;
item.req.path = s->info.path;
item.req.src_w = s->reduced_w;
item.req.src_h = s->reduced_h;
item.req.out_w = output.width;
item.req.out_h = output.height;
item.req.output_tag = output.tag;
item.req.orientation = s->info.orientation;
item.req.transfer = s->info.transfer;
item.req.opaque = s->info.opaque;
orientation_display_size(item.req.src_w, item.req.src_h,
item.req.orientation, &item.display_w, &item.display_h);
item.mid_off = align_up(mid_bytes, alignment);
mid_bytes = item.mid_off + VkDeviceSize(item.req.out_w) *
item.display_h * kBytesPerPixel;
item.output_off = align_up(output_bytes, alignment);
item.readback_off = align_up(readback_bytes, alignment);
const VkDeviceSize n = VkDeviceSize(item.req.out_w) *
item.req.out_h * kBytesPerPixel;
output_bytes = item.output_off + n;
readback_bytes = item.readback_off + n;
b.items.push_back(std::move(item));
}
}
if (b.items.empty())
return false;
@@ -1104,6 +1134,8 @@ ThumbScaler::Impl::claim(
if (!ready || !bytes || align_up(bytes, alignment) > ring_bytes)
return false;
unique_lock lock(mu);
if (canceled.contains(user))
return false;
if (auto found = priority_overrides.find(user);
found != priority_overrides.end())
priority = found->second;
@@ -1112,6 +1144,11 @@ ThumbScaler::Impl::claim(
next_waiter = 1;
waiters.push_back(&waiter);
while (!stop) {
if (canceled.contains(user)) {
erase(waiters, &waiter);
cv.notify_all();
return false;
}
if (auto found = priority_overrides.find(user);
found != priority_overrides.end())
waiter.priority = found->second;
@@ -1138,7 +1175,7 @@ ThumbScaler::Impl::enqueue(const Request &req)
lock_guard lock(mu);
auto found = live.find(req.slot);
if (!ready || stop || found == live.end() || !req.src_w || !req.src_h ||
!req.out_w || !req.out_h) {
(!req.session && req.outputs.empty()) || canceled.contains(req.user)) {
release_range(req.slot);
return false;
}
@@ -1174,8 +1211,8 @@ ThumbScaler::Impl::plan_tiles(
bool
ThumbScaler::Impl::begin_session(const SessionInfo &info, uint32_t *id)
{
if (!ready || !id || !info.src_w || !info.src_h || !info.out_w ||
!info.out_h || !info.tile_count)
if (!ready || !id || !info.src_w || !info.src_h || info.outputs.empty() ||
!info.tile_count)
return false;
auto s = make_unique<Session>();
@@ -1222,7 +1259,7 @@ ThumbScaler::Impl::queue_full(const Job &job)
bool opaque = true;
auto *dst = static_cast<uint8_t *>(slot.mapped);
const auto *src = reinterpret_cast<const uint8_t *>(job.pixels);
const auto *src = reinterpret_cast<const uint8_t *>(job.pixels->data());
for (uint32_t y = 0; y < job.src_h; ++y) {
const auto *row =
reinterpret_cast<const uint16_t *>(src + y * job.stride);
@@ -1242,8 +1279,7 @@ ThumbScaler::Impl::queue_full(const Job &job)
req.slot = slot.id;
req.src_w = job.src_w;
req.src_h = job.src_h;
req.out_w = job.out_w;
req.out_h = job.out_h;
req.outputs = job.outputs;
req.orientation = job.orientation;
req.transfer = job.transfer;
req.opaque = opaque;
@@ -1285,8 +1321,7 @@ ThumbScaler::queue(const Job &job)
info.priority = job.priority;
info.src_w = job.src_w;
info.src_h = job.src_h;
info.out_w = job.out_w;
info.out_h = job.out_h;
info.outputs = job.outputs;
info.k = k;
info.tile_count = uint32_t(tiles.size());
info.orientation = job.orientation;
@@ -1298,7 +1333,7 @@ ThumbScaler::queue(const Job &job)
if (!e.begin_session(info, &session))
return fail();
const auto *base = reinterpret_cast<const uint8_t *>(job.pixels);
const auto *base = reinterpret_cast<const uint8_t *>(job.pixels->data());
for (const Impl::Tile &tile : tiles) {
Impl::Slot slot;
const size_t tile_row = size_t(tile.w) * kBytesPerPixel;
@@ -1315,8 +1350,6 @@ ThumbScaler::queue(const Job &job)
req.slot = slot.id;
req.src_w = tile.w;
req.src_h = tile.h;
req.out_w = job.out_w;
req.out_h = job.out_h;
req.orientation = job.orientation;
req.transfer = job.transfer;
req.opaque = false;
@@ -1367,6 +1400,39 @@ ThumbScaler::reprioritize(uint64_t user, Priority priority)
return found;
}
bool
ThumbScaler::cancel(uint64_t user)
{
if (!impl_ || !user)
return false;
Impl &e = *impl_;
lock_guard lock(e.mu);
bool found = false;
e.canceled.insert(user);
for (auto it = e.pending.begin(); it != e.pending.end();) {
if (it->req.user != user) {
++it;
continue;
}
found = true;
e.release_range(it->req.slot);
it = e.pending.erase(it);
}
for (auto &[id, session] : e.sessions) {
(void) id;
if (session->info.user != user)
continue;
found = true;
session->failed = true;
session->emitted = true;
}
for (Impl::Waiter *waiter : e.waiters)
found |= waiter->user == user;
e.priority_overrides.erase(user);
e.cv.notify_all();
return found;
}
void
ThumbScaler::flush()
{
@@ -1399,23 +1465,25 @@ ThumbScaler::flush()
}
if (!selected)
return;
for (auto it = e.pending.begin();
it != e.pending.end() && jobs.size() < kMaxBatchReqs;) {
if (it->req.priority == *selected) {
size_t item_room = kMaxBatchReqs;
for (auto it = e.pending.begin(); it != e.pending.end();) {
const size_t cost = it->req.session ? 1 : it->req.outputs.size();
if (it->req.priority == *selected && cost <= item_room) {
jobs.push_back(std::move(*it));
it = e.pending.erase(it);
item_room -= cost;
} else {
++it;
}
}
size_t fit_room = kMaxBatchReqs - jobs.size();
for (auto &entry : e.sessions) {
Impl::Session &s = *entry.second;
if (fit_room && s.ended && !s.failed && !s.fitted &&
const size_t cost = s.info.outputs.size();
if (cost <= item_room && s.ended && !s.failed && !s.fitted &&
s.done == s.info.tile_count && s.info.priority == *selected) {
s.fitted = true;
fits.push_back(s.id);
--fit_room;
item_room -= cost;
}
}
}
@@ -1478,6 +1546,8 @@ ThumbScaler::poll(vector<Result> *done)
vkInvalidateMappedMemoryRanges(e.device, 1, &range);
}
vector<uint32_t> finished;
vector<Result> batch_results;
unordered_map<uint64_t, size_t> result_index;
for (const Impl::Item &item : batch.items) {
if (item.kind == Impl::Item::Kind::Tile) {
lock_guard lock(e.mu);
@@ -1485,30 +1555,47 @@ ThumbScaler::poll(vector<Result> *done)
s->done++;
continue;
}
Result result;
result.user = item.req.user;
result.path = item.req.path;
result.out_w = item.req.out_w;
result.out_h = item.req.out_h;
const size_t values = size_t(result.out_w) * result.out_h * 4;
result.data.resize(values);
memcpy(result.data.data(),
size_t index = 0;
if (auto found = result_index.find(item.req.user);
found != result_index.end()) {
index = found->second;
} else {
index = batch_results.size();
result_index.emplace(item.req.user, index);
Result result;
result.user = item.req.user;
result.path = item.req.path;
batch_results.push_back(std::move(result));
}
Result::Output output;
output.width = item.req.out_w;
output.height = item.req.out_h;
output.tag = item.req.output_tag;
const size_t values = size_t(output.width) * output.height * 4;
output.data.resize(values);
memcpy(output.data.data(),
static_cast<const uint8_t *>(batch.readback.mapped) +
item.readback_off,
values * sizeof(uint16_t));
batch_results[index].outputs.push_back(std::move(output));
if (item.kind == Impl::Item::Kind::Fit) {
lock_guard lock(e.mu);
if (Impl::Session *s = e.session(item.session)) {
s->emitted = true;
finished.push_back(s->id);
if (find(finished.begin(), finished.end(), s->id) ==
finished.end())
finished.push_back(s->id);
}
e.priority_overrides.erase(result.user);
e.priority_overrides.erase(item.req.user);
e.canceled.erase(item.req.user);
} else {
lock_guard lock(e.mu);
e.priority_overrides.erase(result.user);
e.priority_overrides.erase(item.req.user);
e.canceled.erase(item.req.user);
}
done->push_back(std::move(result));
}
for (Result &result : batch_results)
done->push_back(std::move(result));
{
lock_guard lock(e.mu);
for (const Impl::Item &item : batch.items)
@@ -1532,6 +1619,7 @@ ThumbScaler::poll(vector<Result> *done)
lock_guard lock(e.mu);
for (Result &result : e.failed_results) {
e.priority_overrides.erase(result.user);
e.canceled.erase(result.user);
done->push_back(std::move(result));
}
e.failed_results.clear();
+22 -4
View File
@@ -13,6 +13,7 @@
#include <cstddef>
#include <cstdint>
#include <memory>
#include <string>
#include <vector>
@@ -36,10 +37,17 @@ public:
};
struct Job {
const uint16_t *pixels = nullptr; // BGRA16, premultiplied
struct Output {
uint32_t width = 0;
uint32_t height = 0;
int tag = 0;
};
std::shared_ptr<const std::vector<uint16_t>> pixels;
// BGRA16, premultiplied. Ownership lasts through staging.
size_t stride = 0;
uint32_t src_w = 0, src_h = 0;
uint32_t out_w = 0, out_h = 0;
std::vector<Output> outputs;
Orientation orientation = Orientation::Rotate0;
Transfer transfer = Transfer::Srgb;
Priority priority = Priority::Maintenance;
@@ -47,11 +55,17 @@ public:
std::string path;
};
struct Result {
struct Output {
uint32_t width = 0;
uint32_t height = 0;
int tag = 0;
std::vector<uint16_t> data;
};
uint64_t user = 0;
std::string path;
uint32_t out_w = 0, out_h = 0;
bool failed = false;
std::vector<uint16_t> data; // destination-sized BGRA16
std::vector<Output> outputs;
};
ThumbScaler();
@@ -63,8 +77,12 @@ public:
bool init(VkPhysicalDevice phys, VkDevice device, VkQueue queue,
uint32_t queue_family, uint64_t ring_bytes, std::string *error);
/// Worker. Copies and queues one image, blocking only for staging space.
/// Every output is independently filtered from that common input.
/// True guarantees one eventual success or failure Result.
bool queue(const Job &job);
/// Cancel work that has not been submitted to Vulkan. Submitted work may
/// finish, but its result is discarded by the caller's generation gate.
bool cancel(uint64_t user);
/// Change the priority of a job waiting for staging or submission.
bool reprioritize(uint64_t user, Priority priority);
void flush(); // GUI: submit one bounded batch
+9
View File
@@ -3,6 +3,9 @@
layout(location = 0) in vec2 aPos;
layout(location = 1) in vec2 aUV;
layout(location = 2) in vec4 aColor;
layout(location = 3) in vec4 aAtlasRect;
layout(location = 4) in vec2 aDestSize;
layout(location = 5) in float aTransfer;
layout(push_constant) uniform uPushConstant {
vec2 scale;
@@ -11,11 +14,17 @@ layout(push_constant) uniform uPushConstant {
layout(location = 0) out vec2 vUV;
layout(location = 1) out vec4 vColor;
layout(location = 2) flat out vec4 vAtlasRect;
layout(location = 3) flat out vec2 vDestSize;
layout(location = 4) flat out float vTransfer;
void
main()
{
vUV = aUV;
vColor = aColor;
vAtlasRect = aAtlasRect;
vDestSize = aDestSize;
vTransfer = aTransfer;
gl_Position = vec4(aPos * pc.scale + pc.translate, 0.0, 1.0);
}
+64
View File
@@ -0,0 +1,64 @@
#version 450
#extension GL_GOOGLE_include_directive : require
#define DN_FILTER 1
#include "common.glsl"
layout(set = 0, binding = 0) uniform sampler2D sTexture;
layout(location = 0) in vec2 vUV;
layout(location = 1) in vec4 vColor;
layout(location = 2) flat in vec4 vAtlasRect;
layout(location = 3) flat in vec2 vDestSize;
layout(location = 4) flat in float vTransfer;
layout(location = 0) out vec4 fColor;
vec4 fetch_entry(ivec2 p, ivec2 lo, ivec2 hi, int transfer)
{
vec4 encoded = texelFetch(sTexture, clamp(p, lo, hi), 0);
return associated_to_linear(encoded, transfer, encoded.a >= 1.0);
}
void main()
{
ivec2 texture_size = textureSize(sTexture, 0);
ivec2 lo = ivec2(round(vAtlasRect.xy * vec2(texture_size)));
ivec2 end = ivec2(round(vAtlasRect.zw * vec2(texture_size)));
ivec2 source_size = max(end - lo, ivec2(1));
ivec2 hi = lo + source_size - ivec2(1);
vec2 extent = max(vAtlasRect.zw - vAtlasRect.xy, vec2(1e-12));
vec2 local = clamp((vUV - vAtlasRect.xy) / extent, vec2(0.0), vec2(1.0));
vec2 pos = local * vec2(source_size) - vec2(0.5);
vec2 scale = max(vDestSize / vec2(source_size), vec2(1e-6));
int transfer = int(round(vTransfer));
vec4 linear;
if (all(equal(source_size, ivec2(round(vDestSize))))) {
linear = fetch_entry(lo + ivec2(clamp(floor(pos + vec2(0.5)),
vec2(0.0), vec2(source_size - 1))), lo, hi, transfer);
} else {
vec2 kernel_scale = min(scale, vec2(1.0));
vec2 radius = vec2(1.0) / kernel_scale;
ivec2 first = ivec2(floor(pos - radius)) + ivec2(1);
ivec2 last = ivec2(ceil(pos + radius)) - ivec2(1);
linear = vec4(0.0);
float weights = 0.0;
for (int y = first.y; y <= last.y; ++y) {
float wy = filter_weight((pos.y - float(y)) * kernel_scale.y);
for (int x = first.x; x <= last.x; ++x) {
float wx = filter_weight((pos.x - float(x)) * kernel_scale.x);
float w = wx * wy;
linear += fetch_entry(lo + ivec2(x, y), lo, hi, transfer) * w;
weights += w;
}
}
linear /= max(weights, 1e-6);
}
float a = clamp(linear.a, 0.0, 1.0);
vec3 rgb = clamp(linear.rgb, vec3(0.0), vec3(a));
vec3 encoded = a > 0.0
? encode_rgb(rgb / a, transfer) * a : vec3(0.0);
fColor = vColor * vec4(encoded, a);
}
+6 -2
View File
@@ -98,14 +98,18 @@ add_test(NAME thumbnail_cache COMMAND test-thumbnail-cache)
add_executable(test-thumbnailer
test-thumbnailer.cpp
"${PROJECT_SOURCE_DIR}/dn/thumbnailer.cpp")
"${PROJECT_SOURCE_DIR}/dn/thumbnailer.cpp"
"${PROJECT_SOURCE_DIR}/dn/thumbnail-cache.cpp")
target_include_directories(test-thumbnailer PRIVATE
"${PROJECT_SOURCE_DIR}/dn"
"${PROJECT_SOURCE_DIR}/libdn")
target_link_libraries(test-thumbnailer PRIVATE
dawn::libdn
Qt6::Gui
Threads::Threads)
Threads::Threads
PkgConfig::WEBP
PkgConfig::WEBPDEMUX
PkgConfig::WEBPMUX)
add_test(NAME thumbnailer COMMAND test-thumbnailer)
add_executable(test-lxdr test-lxdr.cpp)
+1 -1
View File
@@ -153,7 +153,7 @@ main(int argc, char **argv)
CHECK(dn::thumbnail_cache_write(
source, 2, pixels.data(), 2, 1, 2, 1, &error));
hit = dn::thumbnail_cache_lookup(source, 1, cmm, p3.get());
CHECK(!hit.pixels.empty() && hit.tier == 2 && !hit.interim);
CHECK(!hit.pixels.empty() && hit.tier == 2 && hit.interim);
const QString png_input =
QDir(inputs.path()).filePath(QStringLiteral("legacy-source.png"));
+52 -1
View File
@@ -8,6 +8,7 @@
#include "thumbnailer.hpp"
#include <QCoreApplication>
#include <QTemporaryDir>
#include <QTimer>
#include <chrono>
@@ -23,6 +24,17 @@ using namespace std::chrono_literals;
namespace
{
int failures = 0;
#define CHECK(x) \
do { \
if (!(x)) { \
fprintf( \
stderr, "%s:%d: CHECK(%s) failed\n", __FILE__, __LINE__, #x); \
++failures; \
} \
} while (0)
bool
wait_for(condition_variable &cv, unique_lock<mutex> &lock,
const function<bool()> &predicate)
@@ -231,14 +243,53 @@ test_reprioritization_order()
return true;
}
bool
test_bundle_reservations()
{
dn::Thumbnailer thumbnailer(nullptr, 2);
const auto client = thumbnailer.add_client(7);
dn::ThumbnailSource a;
a.uri = QByteArrayLiteral("file:///a");
a.mtime = 1;
a.size = 2;
dn::ThumbnailSource b = a;
b.uri = QByteArrayLiteral("file:///b");
dn::ThumbnailSource c = a;
c.uri = QByteArrayLiteral("file:///c");
const auto first = thumbnailer.reserve_bundle(client, 7, a, 2, 4096,
dn::Thumbnailer::Priority::Dimensions);
const auto second = thumbnailer.reserve_bundle(client, 7, b, 2, 8192,
dn::Thumbnailer::Priority::Visible);
if (!first || !second || thumbnailer.pending_bundle_limit() != 2 ||
thumbnailer.pending_bundle_bytes() != 12288 ||
thumbnailer.reserve_bundle(client, 7, c, 2, 4096,
dn::Thumbnailer::Priority::Prefetch))
return false;
thumbnailer.cancel_bundle(first);
const auto third = thumbnailer.reserve_bundle(client, 7, c, 2, 4096,
dn::Thumbnailer::Priority::Prefetch);
if (!third || thumbnailer.pending_bundle_bytes() != 12288)
return false;
thumbnailer.set_epoch(client, 8);
if (thumbnailer.pending_bundle_bytes() != 0)
return false;
thumbnailer.remove_client(client);
return true;
}
} // namespace
int
main(int argc, char **argv)
{
QTemporaryDir cache;
QTemporaryDir inputs;
CHECK(cache.isValid());
CHECK(inputs.isValid());
qputenv("XDG_CACHE_HOME", cache.path().toUtf8());
QCoreApplication app(argc, argv);
if (!test_background_reserve() || !test_visible_reserve() ||
!test_reprioritization_order())
!test_reprioritization_order() || !test_bundle_reservations())
return 1;
dn::Thumbnailer thumbnailer;