File indexing completed on 2024-05-12 04:44:29
0001 // SPDX-FileCopyrightText: Lukas Sommer <sommerluk@gmail.com> 0002 // SPDX-License-Identifier: BSD-2-Clause OR MIT 0003 0004 #ifndef ASYNCIMAGEPROVIDER_H 0005 #define ASYNCIMAGEPROVIDER_H 0006 0007 #include "asyncimageproviderbase.h" 0008 #include "asyncimagerenderthread.h" 0009 #include "rgbcolorspacefactory.h" 0010 #include <optional> 0011 #include <qimage.h> 0012 #include <qobject.h> 0013 #include <qvariant.h> 0014 0015 namespace PerceptualColor 0016 { 0017 /** @internal 0018 * 0019 * @brief Support for image caching and asynchronous rendering. 0020 * 0021 * This class template is intended for images whose calculation is expensive. 0022 * You need a (thread-safe) rendering function, and this class template will 0023 * provide automatically thread-support and image caching. 0024 * 0025 * @note This class template requires a running event loop. 0026 * 0027 * @tparam T The data type which will be used to parameterize 0028 * the image. 0029 * 0030 * @section asyncimageproviderfeatures Features 0031 * 0032 * - Asynchronous API: The image calculation is done in background 0033 * thread(s). Results are communicated by means of the 0034 * signal @ref interlacingPassCompleted as soon as they are available. 0035 * - Optional interlacing support: The rendering function can 0036 * provide a low-quality image first, and then progressively 0037 * better images until the final full-quality image. Since today’s 0038 * high-DPI screens have more and more pixels (4K screens, perhaps 0039 * one day 8K screens?), interlacing becomes increasingly important, 0040 * especially with complex image calculation. The @ref InterlacingPass 0041 * helper class makes it easy to implement Adam7-like interlacing. 0042 * - Cache: As the image calculation might be expensive, resulting image is 0043 * cached for further usage. 0044 * 0045 * @section asyncimagecreate How to create an object 0046 * 0047 * @snippet testasyncimageprovider.cpp How to create 0048 * 0049 * @section asyncimageuse How to use an object 0050 * 0051 * The cache can be accessed with @ref getCache(). Note that the 0052 * cache is <em>not</em> refreshed implicitly after changing the 0053 * @ref imageParameters(); therefore the cache can be out-of-date. 0054 * Use @ref refreshAsync() to request explicitly a refresh. 0055 * 0056 * @section asyncimagefurther Further reading 0057 * 0058 * @sa @ref AsyncImageRenderThread::pointerToRenderFunction. 0059 * 0060 * @note This class template is reentrant, but <em>not</em> thread-safe! 0061 * 0062 * @internal 0063 * 0064 * @section asyncimageinternals Internals 0065 * 0066 * @note <a href="https://stackoverflow.com/a/63021891">The <tt>Q_OBJECT</tt> 0067 * macro and templates cannot be combined.</a> Therefore, 0068 * @ref AsyncImageProviderBase serves as a base class to provide 0069 * signals for @ref AsyncImageProvider. 0070 * 0071 * @todo Possible (or even necessary?) improvement: When 0072 * a widget that uses this class becomes invisible (see 0073 * @ref AbstractDiagram::actualVisibilityToggledEvent for 0074 * details about the type of visibility we are talking about) 0075 * it might make sense to delete the cache once the image 0076 * parameters change. This might reduce memory consumption (though 0077 * in the moment of changing from one tab to another, anyway 0078 * both widgets on these tabs will need a cache). If this is <em>not</em> 0079 * implemented, @ref AbstractDiagram::actualVisibilityToggledEvent 0080 * can be removed. 0081 * 0082 * @todo Possible (or even necessary?) improvement: If a requested image 0083 * is yet either available or in computation at another object of the same 0084 * template class, that this object should not trigger a new computation, 0085 * but use the yet available/running one of the other object. In practice, 0086 * this might be interesting for the @ref ColorWheelImage, which is likely to 0087 * be used twice within the same @ref ColorDialog at exactly the same size. 0088 * This requires probably a thread-safe management of instances through 0089 * static class members, to make sure that the resulting objects are 0090 * (while still not thread-safe themselves) at least reentrant. 0091 * 0092 * @todo Possible (or even necessary?) improvement: Render an image 0093 * could be split to more than one thread (if actually the current computer 0094 * we are running on has more than one core) to speed up the rendering. 0095 * 0096 * @todo Possible (or even necessary?) improvement: For @ref ChromaHueDiagram 0097 * and @ref ChromaLightnessDiagram, the image cache is quite big, because 0098 * we cache both, the center of the diagram and also the surrounding 0099 * @ref ColorWheelImage. Could we combine both into one single cache? But 0100 * if so, wouldn’t this make problems with anti-aliasing if in future versions 0101 * we do not want to preserve a distance between the color wheel and the 0102 * inner content anymore? And: Would this be compatible with sharing 0103 * computations between various objects of the same template class to 0104 * safe computation power? 0105 * 0106 * @todo Possible (or even necessary?) improvement: Cancel the current 0107 * rendering (if any) when new image parameters are set. 0108 * 0109 * @todo Possible (or even necessary?) improvement: Do not cancel rendering 0110 * until the first (interlacing) result has been delivered to make sure that 0111 * slowly but continuously moving slider see at least sometimes updates… (and 0112 * it's more likely the current value is near to the last value than to the 0113 * old value still in the buffer before the user started moving the cursor 0114 * at all). The performance impact should be minimal when interlacing is 0115 * used. And if no interlacing is available (though we might even decide not 0116 * ever to do non-interlacing rendering), the impact should also not be 0117 * catastrophic either. 0118 * 0119 * @todo xxx Use this class for all image providers, and not only for 0120 * @ref ChromaHueImageParameters. 0121 * 0122 * @note It would be nice to merge @ref AsyncImageProviderBase and 0123 * @ref AsyncImageProvider into one single class (that is <em>not</em> a 0124 * template, but image parameters are now given in form of a QVariant). 0125 * It would take @ref AsyncImageRenderThread::pointerToRenderFunction as 0126 * argument in the constructor to be able to call the constructor of 0127 * @ref AsyncImageRenderThread. 0128 * <br/> 0129 * <b>Advantage:</b> 0130 * <br/> 0131 * → Only one class is compiled, instead of a whole bunch of template classes. 0132 * The binary will therefore be smaller. 0133 * <br/> 0134 * <b>Disadvantage:</b> 0135 * <br/> 0136 * → In the future, maybe we could add support within the template for a 0137 * per-class inter-object cache, so that if two objects of the same class 0138 * have the same @ref imageParameters then the rendering is done only once 0139 * and the result is shared between these two instances. This would 0140 * obviously be impossible if there are no longer different classes 0141 * for different type of images. Or it would at least require a special 0142 * solution… 0143 * <br/> 0144 * → Calling @ref setImageParameters would be done with a <tt>QVariant</tt> 0145 * (or an <tt>std::any</tt>?), so there would be no compile-time error 0146 * anymore if the data type of the parameters is wrong – but is this 0147 * really a big issue in practice? */ 0148 template<typename T> 0149 class AsyncImageProvider final : public AsyncImageProviderBase 0150 { 0151 // Here is no Q_OBJECT macro because it cannot be combined with templates. 0152 // See https://stackoverflow.com/a/63021891 for more information. 0153 0154 public: 0155 explicit AsyncImageProvider(QObject *parent = nullptr); 0156 virtual ~AsyncImageProvider() noexcept override; 0157 0158 [[nodiscard]] QImage getCache() const; 0159 [[nodiscard]] T imageParameters() const; 0160 void refreshAsync(); 0161 void refreshSync(); 0162 void setImageParameters(const T &newImageParameters); 0163 0164 private: 0165 Q_DISABLE_COPY(AsyncImageProvider) 0166 0167 /** @internal @brief Only for unit tests. */ 0168 friend class TestAsyncImageProvider; 0169 0170 void processInterlacingPassResult(const QImage &deliveredImage); 0171 0172 /** @brief The image cache. */ 0173 QImage m_cache; 0174 /** @brief Internal storage for the image parameters. 0175 * 0176 * @sa @ref imageParameters() 0177 * @sa @ref setImageParameters() */ 0178 T m_imageParameters; 0179 /** @brief Information about deliverd images of the last rendering 0180 * request. 0181 * 0182 * Is <tt>true</tt> if the last rendering request has yet 0183 * delivered at least <em>one</em> image, regardless of the 0184 * @ref AsyncImageRenderCallback::InterlacingState of the 0185 * delivered image. Is <tt>false</tt> otherwise. */ 0186 bool m_lastRenderingRequestHasYetDeliveredAnImage = false; 0187 /** @brief The parameters of the last rendering that has been started 0188 * (if any). */ 0189 std::optional<T> m_lastRenderingRequestImageParameters; 0190 /** @brief Provides a render thread. */ 0191 AsyncImageRenderThread m_renderThread; 0192 }; 0193 0194 /** @brief Constructor 0195 * @param parent The object’s parent object. This parameter will be passed 0196 * to the base class’s constructor. */ 0197 template<typename T> 0198 AsyncImageProvider<T>::AsyncImageProvider(QObject *parent) 0199 : AsyncImageProviderBase(parent) 0200 , m_renderThread(&T::render) 0201 { 0202 // Calling qRegisterMetaType is safe even if a given type has yet 0203 // been registered before. 0204 qRegisterMetaType<T>(); 0205 connect( // 0206 &m_renderThread, // 0207 &AsyncImageRenderThread::interlacingPassCompleted, // 0208 this, // 0209 &AsyncImageProvider<T>::processInterlacingPassResult); 0210 } 0211 0212 /** @brief Destructor */ 0213 template<typename T> 0214 AsyncImageProvider<T>::~AsyncImageProvider() noexcept 0215 { 0216 } 0217 0218 /** @brief Provides the content of the cache. 0219 * 0220 * @returns The content of the cache. Note that a cached image might 0221 * be out-of-date. The cache might also be empty, which is represented 0222 * by a null image. */ 0223 template<typename T> 0224 QImage AsyncImageProvider<T>::getCache() const 0225 { 0226 // m_cache is supposed to be a null image if the cache is empty. 0227 return m_cache; 0228 } 0229 0230 /** @brief Setter for the image parameters. 0231 * 0232 * @param newImageParameters The new image parameters. 0233 * 0234 * @note This function does <em>not</em> trigger a new image calculation. 0235 * Only @ref refreshAsync() can trigger a new image calculation. 0236 * 0237 * @sa @ref imageParameters() 0238 * 0239 * @internal 0240 * 0241 * @sa @ref m_imageParameters */ 0242 // NOTE This cannot be a Q_PROPERTY as its type depends on the template 0243 // parameter, and Q_PROPERTY is based on Q_OBJECT which cannot be used 0244 // within templates. 0245 template<typename T> 0246 void AsyncImageProvider<T>::setImageParameters(const T &newImageParameters) 0247 { 0248 m_imageParameters = newImageParameters; 0249 } 0250 0251 /** @brief Getter for the image parameters. 0252 * 0253 * @returns The current image parameters. 0254 * 0255 * @sa @ref setImageParameters() 0256 * 0257 * @internal 0258 * 0259 * @sa @ref m_imageParameters */ 0260 // NOTE This cannot be a Q_PROPERTY as its type depends on the template 0261 // parameter, and Q_PROPERTY is based on Q_OBJECT which cannot be used 0262 // within templates. */ 0263 template<typename T> 0264 T AsyncImageProvider<T>::imageParameters() const 0265 { 0266 return m_imageParameters; 0267 } 0268 0269 /** @brief Receives and processes newly rendered images that are 0270 * delivered from the background render process. 0271 * 0272 * @param deliveredImage The image (either interlaced or full-quality) 0273 * 0274 * @post The new image will be put into the cache and the signal 0275 * @ref interlacingPassCompleted() is emitted. 0276 * 0277 * This function is meant to be called by the background render process to 0278 * deliver more data. It <em>must</em> be called after each interlacing pass 0279 * exactly one time. (If the background process does not support interlacing, 0280 * it is called only once when the image rendering is done.) 0281 * 0282 * @note Like the whole class template, this function is not thread-safe. 0283 * You <em>must</em> call it from the thread within this object lives. It is 0284 * not declared as slot either (because templates and <em>Q_OBJECT</em> are 0285 * incompatible). To call it from a background thread, you can however use 0286 * the functor-based <tt>Qt::connect()</tt> syntax to connect to this function 0287 * as long as the connection type is not direct, but queued. */ 0288 template<typename T> 0289 void AsyncImageProvider<T>::processInterlacingPassResult(const QImage &deliveredImage) 0290 { 0291 m_cache = deliveredImage; 0292 Q_EMIT interlacingPassCompleted(); 0293 } 0294 0295 /** @brief Asynchronously triggers a refresh of the image cache (if 0296 * necessary). */ 0297 template<typename T> 0298 void AsyncImageProvider<T>::refreshAsync() 0299 { 0300 if (imageParameters() == m_lastRenderingRequestImageParameters) { 0301 return; 0302 } 0303 m_renderThread.startRenderingAsync(QVariant::fromValue(imageParameters())); 0304 m_lastRenderingRequestImageParameters = imageParameters(); 0305 } 0306 0307 /** @brief Synchronously refreshes the image cache (if necessary). */ 0308 template<typename T> 0309 void AsyncImageProvider<T>::refreshSync() 0310 { 0311 refreshAsync(); 0312 m_renderThread.waitForIdle(); 0313 } 0314 0315 } // namespace PerceptualColor 0316 0317 #endif // ASYNCIMAGEPROVIDER_H