Redo thumbnailing
This commit is contained in:
+6
-5
@@ -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
@@ -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
File diff suppressed because it is too large
Load Diff
+14
-2
@@ -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
@@ -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());
|
||||
|
||||
@@ -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
@@ -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
@@ -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);
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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 ¬ify : 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
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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"));
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user