diff --git a/src/libslic3r/PNGReadWrite.cpp b/src/libslic3r/PNGReadWrite.cpp index 1f29de646b..40f88a9447 100644 --- a/src/libslic3r/PNGReadWrite.cpp +++ b/src/libslic3r/PNGReadWrite.cpp @@ -211,6 +211,35 @@ bool decode_colored_png(const ReadBuf &in_buf, ImageColorscale &out_img) return decode_colored_png(stream, out_img); } +bool is_decodable_8bit_png(const ReadBuf &pngbuf) +{ + static const constexpr int PNG_SIG_BYTES = 8; + + struct ReadBufStream stream{pngbuf}; + std::vector sig(PNG_SIG_BYTES, 0); + stream.read(sig.data(), PNG_SIG_BYTES); + if (!png_check_sig(sig.data(), PNG_SIG_BYTES)) + return false; + + PNGDescr dsc; + dsc.png = png_create_read_struct(PNG_LIBPNG_VER_STRING, nullptr, nullptr, nullptr); + if (!dsc.png) + return false; + dsc.info = png_create_info_struct(dsc.png); + if (!dsc.info) + return false; + + if (!png_read_header_guarded(dsc.png, dsc.info, &stream, PNG_SIG_BYTES)) + return false; + + const size_t color_type = png_get_color_type(dsc.png, dsc.info); + const size_t bit_depth = png_get_bit_depth(dsc.png, dsc.info); + const bool grey = color_type == PNG_COLOR_TYPE_GRAY; + const bool colour = color_type == PNG_COLOR_TYPE_RGB || color_type == PNG_COLOR_TYPE_RGB_ALPHA; + return png_get_image_width(dsc.png, dsc.info) > 0 && png_get_image_height(dsc.png, dsc.info) > 0 && + bit_depth == 8 && (grey || colour); +} + // Down to earth function to store a packed RGB image to file. Mostly useful for debugging purposes. // Based on https://www.lemoda.net/c/write-png/ diff --git a/src/libslic3r/PNGReadWrite.hpp b/src/libslic3r/PNGReadWrite.hpp index c2d2149e1f..6b8ed75cae 100644 --- a/src/libslic3r/PNGReadWrite.hpp +++ b/src/libslic3r/PNGReadWrite.hpp @@ -50,6 +50,9 @@ struct ReadBuf { const void *buf = nullptr; const size_t sz = 0; }; bool is_png(const ReadBuf &pngbuf); +// True if decode_png() or decode_colored_png() would accept these bytes, judged from the header alone. +bool is_decodable_8bit_png(const ReadBuf &pngbuf); + struct ReadBufStream: public IStream { const ReadBuf &rbuf_ref; size_t pos = 0; diff --git a/src/libslic3r/TextureDisplacement.cpp b/src/libslic3r/TextureDisplacement.cpp index ab63a75c70..26e8726a72 100644 --- a/src/libslic3r/TextureDisplacement.cpp +++ b/src/libslic3r/TextureDisplacement.cpp @@ -283,7 +283,7 @@ bool height_texture_has_color(const TextureDisplacementLayer &layer) return decode_height_texture(layer).has_color(); } -DecodedHeightTexture decode_height_texture(const TextureDisplacementLayer &layer) +DecodedHeightTexture decode_height_texture(const TextureDisplacementLayer &layer, bool keep_decoded) { DecodedHeightTexture result; if (layer.empty()) @@ -351,13 +351,15 @@ DecodedHeightTexture decode_height_texture(const TextureDisplacementLayer &layer } } - std::lock_guard lock(g_decoded_texture_cache.mutex); - // Opportunistically drop entries for image_data that no longer exists anywhere, so the - // cache doesn't grow without bound across many add/remove-texture cycles in a long session. - auto &entries = g_decoded_texture_cache.entries; - for (auto it = entries.begin(); it != entries.end();) - it = it->second.first.expired() ? entries.erase(it) : std::next(it); - entries[key] = { std::weak_ptr>(layer.image_data), result }; + if (keep_decoded) { + std::lock_guard lock(g_decoded_texture_cache.mutex); + // Opportunistically drop entries for image_data that no longer exists anywhere, so the + // cache doesn't grow without bound across many add/remove-texture cycles in a long session. + auto &entries = g_decoded_texture_cache.entries; + for (auto it = entries.begin(); it != entries.end();) + it = it->second.first.expired() ? entries.erase(it) : std::next(it); + entries[key] = { std::weak_ptr>(layer.image_data), result }; + } } // The image-adjustment panel's live edits, on top of the decode and before the smoothing. Before, diff --git a/src/libslic3r/TextureDisplacement.hpp b/src/libslic3r/TextureDisplacement.hpp index 6bd5d1ea33..919dfad654 100644 --- a/src/libslic3r/TextureDisplacement.hpp +++ b/src/libslic3r/TextureDisplacement.hpp @@ -599,8 +599,9 @@ DecodedHeightTexture downscale_height_texture(const DecodedHeightTexture &src, i // colour PNGs are accepted; for a colour source the height is its luminance, using the same // coefficients wxImage::ConvertToGreyscale() uses, so a texture imported as colour displaces exactly // as it did when the importer flattened it to grey on the way in. Returns an empty -// DecodedHeightTexture if image_data is empty or is not a PNG at all. -DecodedHeightTexture decode_height_texture(const TextureDisplacementLayer &layer); +// DecodedHeightTexture if image_data is empty or is not a PNG at all. keep_decoded = false leaves the +// result out of the decode cache, for one-off reads such as a picker thumbnail that must not hold full-size pixels. +DecodedHeightTexture decode_height_texture(const TextureDisplacementLayer &layer, bool keep_decoded = true); // decode_height_texture(layer).has_color(), answered from the decode cache rather than from a copy of the // texture - cheap enough to ask every frame. Smoothing does not change it, so the raw decode is what is read. diff --git a/src/slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.cpp b/src/slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.cpp index 4597e98fd2..7f0019f86b 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.cpp @@ -174,6 +174,10 @@ constexpr int HEIGHT_TEX_MAX_PX = 2048; // The full-size image is only ever adjusted once, on commit. constexpr int ADJUST_PREVIEW_MAX_PX = 384; +// Library textures the texture picker decodes per frame. Each decode runs on the GUI thread, so the +// rest of the visible tiles fill in over the following frames instead of in one stall. +constexpr int PICKER_LOADS_PER_FRAME = 2; + // How many rings of vertex-adjacent triangles either side of the paint's edge join the border refine // band (see collect_paint_region()). Two is enough to grade the size change without the band's own // cost growing to matter: it is a ring around a perimeter, not an area. @@ -4118,7 +4122,7 @@ const GLGizmoTextureDisplacement::LibraryTexture *GLGizmoTextureDisplacement::ge if (entry.image_data) { TextureDisplacementLayer probe; probe.image_data = entry.image_data; - entry.thumbnail = upload_height_thumbnail(decode_height_texture(probe)); + entry.thumbnail = upload_height_thumbnail(decode_height_texture(probe, /* keep_decoded */ false)); if (!entry.thumbnail) entry.image_data.reset(); // decoded to nothing usable - treat it as a failed load } else { @@ -4331,6 +4335,8 @@ void GLGizmoTextureDisplacement::render_texture_library_popup(const ImVec2 &pane std::optional to_remove; bool import_clicked = false; bool download_clicked = false; + int loads_left = PICKER_LOADS_PER_FRAME; + bool loads_deferred = false; // Tiles are placed on the four-column grid explicitly rather than flowed with SameLine(): a user tile // overlays a remove button on its corner, and that button would otherwise be "the previous item" the @@ -4348,11 +4354,30 @@ void GLGizmoTextureDisplacement::render_texture_library_popup(const ImVec2 &pane }; // One section's tiles, in the order the section holds them (shipped alphabetically, pack textures by // name, the user's own most recently used first - the order texture_library() produced); a user tile - // carries a remove button on hover. Returns how many it drew. + // carries a remove button on hover. Returns how many tiles it laid out, drawn or not. const auto grid = [&](const ImVec2 &origin, const std::vector &entries, bool removable) { int n = 0; for (const TextureLibraryEntry *entry : entries) { - ImGui::SetCursorScreenPos(tile_pos(origin, n++)); + const ImVec2 rect_min = tile_pos(origin, n++); + const ImVec2 rect_max = ImVec2(rect_min.x + tile, rect_min.y + tile); + const bool current = entry->path == layer->path; + // The selected tile is centred even when it is off screen, so it is drawn and loaded next frame. + if (current && m_picker_reveal_selected) { + ImGui::SetScrollFromPosY(0.5f * (rect_min.y + rect_max.y) - ImGui::GetWindowPos().y, 0.5f); + m_parent.request_extra_frame(); + } + if (!ImGui::IsRectVisible(rect_min, rect_max)) + continue; + // Thumbnails decode on this thread, so only a few per frame; the rest fill in on later frames. + const bool cached = m_library_textures.count(entry->path) != 0; + if (!cached && loads_left == 0) { + loads_deferred = true; + dl->AddRectFilled(rect_min, rect_max, ImGui::GetColorU32(ImGuiCol_FrameBg), style.FrameRounding); + continue; + } + if (!cached) + --loads_left; + ImGui::SetCursorScreenPos(rect_min); ImGui::PushID(entry->path.c_str()); const LibraryTexture *tex = get_library_texture(entry->path); GLTexture *thumb = tex != nullptr ? tex->thumbnail.get() : nullptr; @@ -4375,7 +4400,6 @@ void GLGizmoTextureDisplacement::render_texture_library_popup(const ImVec2 &pane if (removable) ImGui::SetItemAllowOverlap(); // the remove button drawn over its corner takes its own clicks const ImVec2 a = ImGui::GetItemRectMin(), b = ImGui::GetItemRectMax(); - const bool current = entry->path == layer->path; const bool hovered = ImGui::IsItemHovered(); // The layer's current texture carries an accent ring, and the tile under the pointer a thinner, // fainter one. Both are drawn inside the tile, where the grid's clip rect never cuts them. @@ -4384,9 +4408,6 @@ void GLGizmoTextureDisplacement::render_texture_library_popup(const ImVec2 &pane dl->AddRect(ImVec2(a.x + 0.5f * w, a.y + 0.5f * w), ImVec2(b.x - 0.5f * w, b.y - 0.5f * w), ImGui::GetColorU32(current ? orca : ImVec4(orca.x, orca.y, orca.z, 0.6f)), 0.f, 0, w); } - // Centred, as far as the scroll range allows, so the textures around it are in view too. - if (current && m_picker_reveal_selected) - ImGui::SetScrollFromPosY(0.5f * (a.y + b.y) - ImGui::GetWindowPos().y, 0.5f); if (hovered) panel_tooltip(entry->name); @@ -4575,6 +4596,8 @@ void GLGizmoTextureDisplacement::render_texture_library_popup(const ImVec2 &pane ImGui::TextDisabled("%s", _u8L("PNG, JPG or BMP - converted to a height map on import. Hover one of yours to remove it.").c_str()); ImGui::PopTextWrapPos(); } + if (loads_deferred) + m_parent.request_extra_frame(); // Done whether or not the current texture was drawn: one in a collapsed section stays where it is. m_picker_reveal_selected = false; ImGui::EndChild(); diff --git a/src/slic3r/GUI/TextureLibrary.cpp b/src/slic3r/GUI/TextureLibrary.cpp index 0388ed4f21..3d76791027 100644 --- a/src/slic3r/GUI/TextureLibrary.cpp +++ b/src/slic3r/GUI/TextureLibrary.cpp @@ -223,23 +223,11 @@ std::vector read_file_bytes(const std::string &path) return std::vector(std::istreambuf_iterator(ifs), std::istreambuf_iterator()); } -// True if these bytes are a PNG libslic3r can decode, i.e. can be stored on a layer as-is. Mirrors -// exactly what decode_height_texture() accepts: 8-bit grayscale, or 8-bit colour (whose luminance is -// the height and whose RGB is available to colour the model). Anything else is converted on load. +// True if decode_height_texture() can read these bytes, so they can be stored on a layer as-is. +// Anything else is converted on load. bool is_supported_height_map(const std::vector &bytes) { - if (bytes.empty()) - return false; - const png::ReadBuf rbuf{ bytes.data(), bytes.size() }; - if (!png::is_png(rbuf)) - return false; - png::ImageGreyscale gray; - if (png::decode_png(rbuf, gray) && gray.cols > 0 && gray.rows > 0) - return true; - png::ImageColorscale color; - return png::decode_colored_png(rbuf, color) && color.cols > 0 && color.rows > 0 && - color.bytes_per_pixel >= 3 && - color.buf.size() == color.cols * color.rows * size_t(color.bytes_per_pixel); + return !bytes.empty() && png::is_decodable_8bit_png(png::ReadBuf{ bytes.data(), bytes.size() }); } std::vector g_library;