commit e0dd8db6ade8472fe3017a0b573617fd24d6d781
Author: Pavel Sanda <[email protected]>
Date:   Sat Jul 11 02:20:55 2026 +0200

    Parallelize GUI load of images.
    
    Load takes quite long on figure-heavy reports, underutilizes usual multicore
    CPUs and its more of us, who scroll through the document just to load all
    images; that's absurd.
    
    Two improvements:
    - default parallelize up to 4 threads if available on the machine
    - new LFUN buffer-load-graphics [all] [<THREADS>|max] which triggers
      load machinery for the whole document ("all" = including childern);
      optionally you can explicitely setup number of <THREADS> or give it
      max, so it uses all cores on the current machine.
    
    https://www.mail-archive.com/[email protected]/msg218082.html
    https://www.mail-archive.com/[email protected]/msg226451.html
    
    Draft by Claude, review by me. As he concludes:
    "Nice bit of archaeology as a bookend: lazy image loading itself - the 
behavior
    buffer-load-graphics now works around - was introduced on 2002-02-27 (commit
    607ad8d3a7, "lazy loading (don't try and load the image until a request to 
draw
    it is received)"), fixing ticket #37 "InsetGraphics previews render
    synchronously". Twenty-four years from "make it lazy" to "let me force it
    eager, in parallel".
    
    Assisted-by: Claude Opus 4.8
---
 lib/RELEASE-NOTES            |  8 ++++++
 src/Buffer.cpp               | 33 ++++++++++++++++++++++++
 src/FuncCode.h               |  1 +
 src/LyXAction.cpp            | 15 +++++++++++
 src/insets/InsetGraphics.cpp |  7 ++++++
 src/insets/InsetGraphics.h   |  2 ++
 src/insets/RenderGraphic.cpp | 12 +++++++++
 src/insets/RenderGraphic.h   |  2 ++
 src/support/ForkedCalls.cpp  | 60 ++++++++++++++++++++++++++------------------
 src/support/ForkedCalls.h    |  3 +++
 10 files changed, 119 insertions(+), 24 deletions(-)

diff --git a/lib/RELEASE-NOTES b/lib/RELEASE-NOTES
index f8a223afab..b6fe35c956 100644
--- a/lib/RELEASE-NOTES
+++ b/lib/RELEASE-NOTES
@@ -5,6 +5,10 @@
 
 !!Documents compilation process and images conversion
 
+- Conversion of images for on-screen display now runs up to 4 conversion
+  processes in parallel. The limit can be changed via the optional
+  argument of the buffer-load-graphics lfun.
+
 
 !!!The following pref variables were added in 2.:
 
@@ -22,6 +26,10 @@
 
 !!!The following new LyX functions have been introduced in 2.6:
 
+- buffer-load-graphics: triggers preview conversion for every image in
+  the current buffer at once, instead of waiting for each image to be
+  scrolled into view.
+
 !!!The following LyX functions have been changed in 2.6:
 
 - The function newpage-insert has been renamed to textbreak-insert
diff --git a/src/Buffer.cpp b/src/Buffer.cpp
index 8122a05843..9c301c36d9 100644
--- a/src/Buffer.cpp
+++ b/src/Buffer.cpp
@@ -70,6 +70,7 @@
 #include "xml.h"
 
 #include "insets/InsetBranch.h"
+#include "insets/InsetGraphics.h"
 #include "insets/InsetInclude.h"
 #include "insets/InsetText.h"
 
@@ -94,6 +95,7 @@
 #include "support/FileName.h"
 #include "support/FileNameList.h"
 #include "support/filetools.h"
+#include "support/ForkedCalls.h"
 #include "support/gettext.h"
 #include "support/gzstream.h"
 #include "support/Lexer.h"
@@ -2888,6 +2890,10 @@ bool Buffer::getStatus(FuncRequest const & cmd, 
FuncStatus & flag) const
                enable = (d->preview_file_).exists() && 
!(d->preview_file_).isFileEmpty();
                break;
 
+       case LFUN_BUFFER_LOAD_GRAPHICS:
+               enable = true;
+               break;
+
        case LFUN_CHANGES_TRACK:
                flag.setEnabled(true);
                flag.setOnOff(params().track_changes);
@@ -3163,6 +3169,33 @@ void Buffer::dispatch(FuncRequest const & func, 
DispatchResult & dr)
                        dr.setMessage(_("Error viewing the output file."));
                break;
 
+       case LFUN_BUFFER_LOAD_GRAPHICS: {
+               ListOfBuffers bufs;
+               for (string const & arg :
+                    getVectorFromString(to_utf8(func.argument()), " ")) {
+                       if (arg == "all")       //include all childern
+                               bufs = getDescendants();
+                       else if (arg == "max")  //use all available cores
+                               ForkedCallQueue::setMaxRunning(0);
+                       else
+                               
ForkedCallQueue::setMaxRunning(convert<int>(arg));
+               }
+               bufs.push_front(this);
+               int counted = 0;
+               for (Buffer * b : bufs) {
+                       InsetIterator it = begin(b->inset());
+                       InsetIterator const itend = end(b->inset());
+                       for (; it != itend; ++it) {
+                               if (it->lyxCode() == GRAPHICS_CODE) {
+                                       static_cast<InsetGraphics const 
&>(*it).preload();
+                                       ++counted;
+                               }
+                       }
+               }
+               //dr.setMessage(bformat(_("Queued %1$d image(s) for preview."), 
counted));
+               break;
+       }
+
        case LFUN_CHANGES_TRACK:
                if (params().save_transient_properties)
                        undo().recordUndoBufferParams();
diff --git a/src/FuncCode.h b/src/FuncCode.h
index 0a56683779..5c3259dd68 100644
--- a/src/FuncCode.h
+++ b/src/FuncCode.h
@@ -461,6 +461,7 @@ enum FuncCode
        LFUN_SEPARATOR_INSERT,          // ef 20140502
        LFUN_SERVER_GET_STATISTICS,     // brokenclock 20141010
        LFUN_BUFFER_VIEW_CACHE,         // skostysh 20150401
+       LFUN_BUFFER_LOAD_GRAPHICS,      // ps 20260623
        // 360
        LFUN_BUFFER_MOVE_NEXT,          // skostysh 20150408
        LFUN_BUFFER_MOVE_PREVIOUS,      // skostysh 20150408
diff --git a/src/LyXAction.cpp b/src/LyXAction.cpp
index 0e0826ead4..390312bfa8 100644
--- a/src/LyXAction.cpp
+++ b/src/LyXAction.cpp
@@ -904,6 +904,21 @@ void LyXAction::init()
  */
                { LFUN_BUFFER_VIEW_CACHE, "buffer-view-cache", ReadOnly, Buffer 
},
 
+/*!
+ * \var lyx::FuncCode lyx::LFUN_BUFFER_LOAD_GRAPHICS
+ * \li Action: Triggers image preview for every image in the current buffer,
+               instead of waiting for each image to be scrolled into view.
+ * \li Syntax: buffer-load-graphics [all] [<THREADS>|max]
+ * \li Params: all: also process all child documents; by default only the
+               current buffer is processed.
+               <THREADS>: fix number os threads for conversion, or 'max' to 
use 
+              all cores available on the system.\n
+              The setting persists until changed again.
+ * \li Origin: ps, 23 Jun 2026
+ * \endvar
+ */
+               { LFUN_BUFFER_LOAD_GRAPHICS, "buffer-load-graphics", ReadOnly, 
Buffer },
+
 /*!
  * \var lyx::FuncCode lyx::LFUN_BUFFER_WRITE
  * \li Action: Saves the current buffer.
diff --git a/src/insets/InsetGraphics.cpp b/src/insets/InsetGraphics.cpp
index 0ca24e0488..c087533905 100644
--- a/src/insets/InsetGraphics.cpp
+++ b/src/insets/InsetGraphics.cpp
@@ -1157,6 +1157,13 @@ InsetGraphicsParams const & InsetGraphics::params() const
 }
 
 
+void InsetGraphics::preload() const
+{
+       if (graphic_)
+               graphic_->preload();
+}
+
+
 void InsetGraphics::editGraphics(InsetGraphicsParams const & p) const
 {
        theFormats().edit(buffer(), p.filename,
diff --git a/src/insets/InsetGraphics.h b/src/insets/InsetGraphics.h
index 95289832cb..7f7cb2e81d 100644
--- a/src/insets/InsetGraphics.h
+++ b/src/insets/InsetGraphics.h
@@ -91,6 +91,8 @@ public:
        docstring layoutName() const override { return from_ascii("Graphics"); }
        /// Get the inset parameters, used by the GUIndependent dialog.
        InsetGraphicsParams const & params() const;
+       /// Force preview generation now (skip the wait-for-paint).
+       void preload() const;
 
        ///
        int topOffset(BufferView const *) const override { return 0; }
diff --git a/src/insets/RenderGraphic.cpp b/src/insets/RenderGraphic.cpp
index 67d44e1bb4..5f88372953 100644
--- a/src/insets/RenderGraphic.cpp
+++ b/src/insets/RenderGraphic.cpp
@@ -51,6 +51,7 @@ RenderBase * RenderGraphic::clone(Inset const * inset) const
        return new RenderGraphic(*this, inset);
 }
 
+
 void RenderGraphic::reload() const
 {
        loader_.reload();
@@ -132,6 +133,17 @@ bool readyToDisplay(graphics::Loader const & loader)
 } // namespace
 
 
+void RenderGraphic::preload() const
+{
+       if (!displayGraphic(params_))
+               return;
+       // ErrorConverting is included so we retry failed conversions.
+       if (loader_.status() == graphics::WaitingToLoad
+           || loader_.status() == graphics::ErrorConverting)
+               loader_.startLoading();
+}
+
+
 void RenderGraphic::metrics(MetricsInfo & mi, Dimension & dim) const
 {
        if (displayGraphic(params_)) {
diff --git a/src/insets/RenderGraphic.h b/src/insets/RenderGraphic.h
index fc631a0c7a..2b175008a3 100644
--- a/src/insets/RenderGraphic.h
+++ b/src/insets/RenderGraphic.h
@@ -35,6 +35,8 @@ public:
 
        /// Refresh the info about which file to display and how to display it.
        void update(graphics::Params const & params);
+       /// Trigger conversion, no-op if the image is already loading, loaded.
+       void preload() const;
        /// Reloads the image if necessary
        void reload() const;
 
diff --git a/src/support/ForkedCalls.cpp b/src/support/ForkedCalls.cpp
index 054290d0dd..c81c8af868 100644
--- a/src/support/ForkedCalls.cpp
+++ b/src/support/ForkedCalls.cpp
@@ -20,11 +20,13 @@
 #include "support/lyxlib.h"
 #include "support/Timeout.h"
 
+#include <algorithm>
 #include <cerrno>
 #include <cstring>
 #include <list>
 #include <queue>
 #include <sstream>
+#include <thread>
 #include <utility>
 #include <vector>
 
@@ -443,18 +445,23 @@ typedef pair<string, ForkedCall::sigPtr> Process;
 /// in-progress queue
 static queue<Process> callQueue_;
 
-/// flag whether queue is running
-static bool running_ = false;
+/// Maximum number of forked children allowed to run concurrently.
+/// Graphics conversion is the only client of this queue.
+/// Default: min(4, #cores), serial if the hardware cannot be queried.
+/// Adjustable at runtime via setMaxRunning().
+static int max_running_ =
+       max(1, min(4, int(std::thread::hardware_concurrency())));
+
+/// Number of children currently running.
+static int running_count_ = 0;
 
 ///
-void startCaller();
-///
-void stopCaller();
+void callNext();
 ///
 void callback(pid_t, int);
 
-/** Add a process to the queue. Processes are forked sequentially
- *  only one is running at a time.
+/** Add a process to the queue. Up to max_running_ processes are
+ *  forked in parallel; the remainder wait for a slot to free up.
  *  Connect to the returned signal and you'll be informed when
  *  the process has ended.
  */
@@ -463,8 +470,10 @@ ForkedCall::sigPtr add(string const & process)
        ForkedCall::sigPtr ptr;
        ptr.reset(new ForkedCall::sig);
        callQueue_.push(Process(process, ptr));
-       if (!running_)
-               startCaller();
+       if (running_count_ == 0)
+               LYXERR(Debug::GRAPHICS, "ForkedCallQueue: waking up");
+       while (running_count_ < max_running_ && !callQueue_.empty())
+               callNext();
        return ptr;
 }
 
@@ -477,6 +486,7 @@ void callNext()
        callQueue_.pop();
        // Bind our chain caller
        pro.second->connect(callback);
+       ++running_count_;
        ForkedCall call;
        //If we fail to fork the process, then emit the signal
        //to tell the outside world that it failed.
@@ -487,31 +497,33 @@ void callNext()
 
 void callback(pid_t, int)
 {
-       if (callQueue_.empty())
-               stopCaller();
-       else
+       --running_count_;
+       while (running_count_ < max_running_ && !callQueue_.empty())
                callNext();
+       if (running_count_ == 0 && callQueue_.empty())
+               LYXERR(Debug::GRAPHICS, "ForkedCallQueue: I'm going to sleep");
 }
 
 
-void startCaller()
-{
-       LYXERR(Debug::GRAPHICS, "ForkedCallQueue: waking up");
-       running_ = true ;
-       callNext();
-}
-
-
-void stopCaller()
+void setMaxRunning(int n)
 {
-       running_ = false ;
-       LYXERR(Debug::GRAPHICS, "ForkedCallQueue: I'm going to sleep");
+       if (n < 1) {    //All cores available on the system.
+               int const hw = int(std::thread::hardware_concurrency());
+               if (hw < 1)     // Keep the current setting if the hardware 
cannot be queried.
+                       return;
+               n = hw;
+       }
+       max_running_ = n;
+       LYXERR(Debug::GRAPHICS, "ForkedCallQueue: max parallel processes set to 
" << n);
+       // If the cap was raised while jobs were waiting, top up the pool.
+       while (running_count_ < max_running_ && !callQueue_.empty())
+               callNext();
 }
 
 
 bool running()
 {
-       return running_;
+       return running_count_ > 0;
 }
 
 } // namespace ForkedCallQueue
diff --git a/src/support/ForkedCalls.h b/src/support/ForkedCalls.h
index 84aeac9679..6c41ca9d9b 100644
--- a/src/support/ForkedCalls.h
+++ b/src/support/ForkedCalls.h
@@ -196,6 +196,9 @@ private:
 namespace ForkedCallQueue {
 
 ForkedCall::sigPtr add(std::string const & process);
+/// Set the maximum number of processes forked in parallel;
+/// n < 1 means all cores available on the system.
+void setMaxRunning(int n);
 /// Query whether the queue is running a forked process now.
 bool running();
 
-- 
lyx-cvs mailing list
[email protected]
https://lists.lyx.org/mailman/listinfo/lyx-cvs

Reply via email to