1 /**
2 * A GUIGraphics is the interface between a plugin client and a IWindow.
3 *
4 * Copyright: Copyright Auburn Sounds 2015 and later.
5 * License:   $(LINK2 http://www.boost.org/LICENSE_1_0.txt, Boost License 1.0)
6 * Authors:   Guillaume Piolat
7 */
8 module dplug.gui.graphics;
9 
10 import core.atomic;
11 import core.stdc.stdio;
12 
13 import inteli.emmintrin;
14 
15 import dplug.core.math;
16 import dplug.core.thread;
17 
18 import dplug.client.client;
19 import dplug.client.graphics;
20 import dplug.client.daw;
21 
22 import dplug.window.window;
23 
24 import dplug.graphics.mipmap;
25 
26 import dplug.gui.boxlist;
27 import dplug.gui.context;
28 import dplug.gui.element;
29 import dplug.gui.compositor;
30 import dplug.gui.legacypbr;
31 import dplug.gui.sizeconstraints;
32 import dplug.gui.profiler;
33 
34 /// In the whole package:
35 /// The diffuse maps contains:
36 ///   RGBA = red/green/blue/emissiveness
37 /// The depth maps contains depth (0 being lowest, 65535 highest)
38 /// The material map contains:
39 ///   RGBA = roughness / metalness / specular / unused
40 
41 alias RMSP = RGBA; // reminder
42 
43 // Uncomment to enter the marvellous world of dirty rectangles.
44 //debug = resizing;
45 
46 // A GUIGraphics is the interface between a plugin client and a IWindow.
47 // It is also an UIElement and the root element of the plugin UI hierarchy.
48 // You have to derive it to have a GUI.
49 // It dispatches window events to the GUI hierarchy.
50 class GUIGraphics : UIElement, IGraphics
51 {
52 nothrow:
53 @nogc:
54 
55     // Max size of tiles when doing the expensive PBR compositing step.
56     // Difficult trade-off in general: not launching threads (one tile) might be better on low-powered devices and 
57     // in case of a large number of opened UI.
58     // But: having too large tiles also makes a visible delay when operating a single UI, even with only two threads.
59     enum PBR_TILE_MAX_WIDTH = 64;
60     enum PBR_TILE_MAX_HEIGHT = 64;
61 
62     this(SizeConstraints sizeConstraints, UIFlags flags)
63     {
64         _sizeConstraints = sizeConstraints;
65 
66         _uiContext = mallocNew!UIContext(this);
67         super(_uiContext, flags);
68 
69         _windowListener = mallocNew!WindowListener(this);
70 
71         _window = null;
72 
73         // Find what size the UI should at first opening.
74         _sizeConstraints.suggestDefaultSize(&_currentUserWidth, &_currentUserHeight);
75         _currentLogicalWidth = _currentUserWidth;
76         _currentLogicalHeight = _currentUserHeight;
77 
78         int numThreads = 0; // auto
79 
80         // Was lowered to 2 in October 2018 to save CPU usage.
81         // Now in Jan 2023, increased to 3 to have a bit smoother PBR.
82         // FUTURE: could make that 4 eventually, see Issue #752. This has minimal memory and CPU 
83         // costs, but is worse on slower plugins.
84         int maxThreads = 3;
85         _threadPool = mallocNew!ThreadPool(numThreads, maxThreads);
86 
87         // Build the compositor
88         {
89             CompositorCreationContext compositorContext;
90             compositorContext.threadPool = _threadPool;
91             _compositor = buildCompositor(&compositorContext);
92         }
93 
94         _rectsToUpdateDisjointedRaw = makeVec!box2i;
95         _rectsToUpdateDisjointedPBR = makeVec!box2i;
96         _rectsTemp = makeVec!box2i;
97 
98         _updateRectScratch[0] = makeVec!box2i;
99         _updateRectScratch[1] = makeVec!box2i;
100 
101         _rectsToComposite = makeVec!box2i;
102         _rectsToCompositeDisjointed = makeVec!box2i;
103         _rectsToCompositeDisjointedTiled = makeVec!box2i;
104 
105         _rectsToDisplay = makeVec!box2i;
106         _rectsToDisplayDisjointed = makeVec!box2i;
107 
108         _rectsToResize = makeVec!box2i;
109         _rectsToResizeDisjointed = makeVec!box2i;
110 
111         _elemsToDrawRaw = makeVec!UIElement;
112         _elemsToDrawPBR = makeVec!UIElement;
113         _sortScratchBuf = makeVec!UIElement;
114 
115         _diffuseMap = mallocNew!(Mipmap!RGBA)();
116         _materialMap = mallocNew!(Mipmap!RGBA)();
117         _depthMap = mallocNew!(Mipmap!L16)();
118 
119         _compositedBuffer = mallocNew!(OwnedImage!RGBA)();
120         _renderedBuffer = mallocNew!(OwnedImage!RGBA)();
121     }
122 
123     // Don't like the default rendering? Override this function and make another compositor.
124     ICompositor buildCompositor(CompositorCreationContext* context)
125     {
126         return mallocNew!PBRCompositor(context);
127     }
128 
129     /// Want a screenshot? Want to generate a mesh or a voxel out of your render?
130     /// Override this function and call `IUIContext.requestUIScreenshot()`
131     ///
132     /// Params: pf Pixel format. pixelFormat == 0 if pixel format is RGBA8
133     ///                          pixelFormat == 1 if pixel format is BGRA8
134     ///                          pixelFormat == 2 if pixel format is ARGB8
135     ///                          You must support all three, sorry.
136     /// All maps have the same dimension, which is the logical pixel size. 
137     /// Warning: nothing to do with the Screencap key, it doesn't get triggered like that.
138     void onScreenshot(ImageRef!RGBA finalRender,    // the output, as show to the plugin user
139                       WindowPixelFormat pixelFormat,// pixel format of `finalRender`, see above
140                       ImageRef!RGBA diffuseMap,     // the PBR diffuse map
141                       ImageRef!L16 depthMap,        // the PBR depth map
142                       ImageRef!RGBA materialMap)    // the PBR material map
143     {
144         // override this to take programmatic screenshots
145         // eg: generate a .vox, .png, etc.
146     }
147 
148     final ICompositor compositor()
149     {
150         return _compositor;
151     }
152 
153     ~this()
154     {
155         closeUI();
156         _uiContext.destroyFree();
157 
158         _threadPool.destroyFree();
159 
160         _compositor.destroyFree();
161         _diffuseMap.destroyFree();
162         _materialMap.destroyFree();
163         _depthMap.destroyFree();
164 
165         _windowListener.destroyFree();
166 
167         destroyFree(_compositedBuffer);
168         destroyFree(_renderedBuffer);
169 
170         alignedFree(_resizedBuffer, 16);
171     }
172 
173     // <IGraphics implementation>
174 
175     override void* openUI(void* parentInfo,
176                           void* controlInfo,
177                           IClient client,
178                           GraphicsBackend backend)
179     {
180         _client = client;
181 
182         WindowBackend wbackend = void;
183         final switch(backend)
184         {
185             case GraphicsBackend.autodetect: wbackend = WindowBackend.autodetect; break;
186             case GraphicsBackend.win32: wbackend = WindowBackend.win32; break;
187             case GraphicsBackend.cocoa: wbackend = WindowBackend.cocoa; break;
188             case GraphicsBackend.carbon: wbackend = WindowBackend.carbon; break;
189             case GraphicsBackend.x11: wbackend = WindowBackend.x11; break;
190         }
191 
192         position = box2i(0, 0, _currentUserWidth, _currentUserHeight);
193 
194         // Sets the whole UI dirty.
195         // This needs to be done _before_ window creation, else there could be a race
196         // displaying partial updates to the UI.
197         setDirtyWhole(UILayer.allLayers);
198 
199         // We create this window each time.
200         _window = createWindow(WindowUsage.plugin, parentInfo, controlInfo, _windowListener, wbackend, _currentLogicalWidth, _currentLogicalHeight);
201 
202         version(Dplug_ProfileUI) profiler.category("ui").instant("Open UI");
203 
204         return _window.systemHandle();
205     }
206 
207     override void closeUI()
208     {
209         // Destroy window.
210         if (_window !is null)
211         {
212             version(Dplug_ProfileUI)
213             {
214                 profiler.category("ui").instant("Close UI");
215             }
216 
217             _window.destroyFree();
218             _window = null;
219         }
220         _client = null;
221     }
222 
223     override void getGUISize(int* widthLogicalPixels, int* heightLogicalPixels)
224     {
225         *widthLogicalPixels = _currentLogicalWidth;
226         *heightLogicalPixels = _currentLogicalHeight;
227     }
228 
229     override bool isResizeable()
230     {
231         return isUIResizable();
232     }
233 
234     override void getMaxSmallerValidSize(int* inoutWidth, int* inoutHeight)
235     {
236         _sizeConstraints.getMaxSmallerValidSize(inoutWidth, inoutHeight);
237     }
238 
239     override void getNearestValidSize(int* inoutWidth, int* inoutHeight)
240     {
241         _sizeConstraints.getNearestValidSize(inoutWidth, inoutHeight);
242     }
243 
244     override bool nativeWindowResize(int newWidthLogicalPixels, int newHeightLogicalPixels)
245     {
246         // If it's already the same logical size, nothing to do.
247         if ( (newWidthLogicalPixels == _currentLogicalWidth)
248              &&  (newHeightLogicalPixels == _currentLogicalHeight) )
249             return true;
250 
251         // Issue #669.
252         // Can't resize a non-existing window, return failure.
253         // Hosts where this is needed: VST3PluginTestHost
254         // It calls onSize way too soon.
255         if (_window is null)
256             return false;
257 
258         // Here we request the native window to resize.
259         // The actual resize will be received by the window listener, later.
260         return _window.requestResize(newWidthLogicalPixels, newHeightLogicalPixels, false);
261     }
262 
263     // </IGraphics implementation>
264 
265     // This class is only here to avoid name conflicts between
266     // UIElement and IWindowListener methods :|
267     // Explicit outer to avoid emplace crashing
268     static class WindowListener : IWindowListener
269     {
270     nothrow:
271     @nogc:
272         GUIGraphics outer;
273 
274         this(GUIGraphics outer)
275         {
276             this.outer = outer;
277         }
278 
279         override bool onMouseClick(int x, int y, MouseButton mb, bool isDoubleClick, MouseState mstate)
280         {
281             x -= outer._userArea.min.x;
282             y -= outer._userArea.min.y;
283             bool hitSomething = outer.mouseClick(x, y, mb, isDoubleClick, mstate);
284             if (!hitSomething)
285             {
286                 // Nothing was clicked, nothing is focused anymore
287                 outer._uiContext.setFocused(null);
288             }
289             return hitSomething;
290         }
291 
292         override bool onMouseRelease(int x, int y, MouseButton mb, MouseState mstate)
293         {
294             x -= outer._userArea.min.x;
295             y -= outer._userArea.min.y;
296             outer.mouseRelease(x, y, mb, mstate);
297             return true;
298         }
299 
300         override bool onMouseWheel(int x, int y, int wheelDeltaX, int wheelDeltaY, MouseState mstate)
301         {
302             x -= outer._userArea.min.x;
303             y -= outer._userArea.min.y;
304 
305             // Sends the event to the currently dragged element, if any exists.
306             UIElement dragged = outer._uiContext.dragged;
307             if (dragged !is null)
308             {
309                 box2i pos = dragged._position;
310                 if (dragged.onMouseWheel(x - pos.min.x, y - pos.min.y, wheelDeltaX, wheelDeltaY, mstate))
311                     return true;
312             }
313 
314             return outer.mouseWheel(x, y, wheelDeltaX, wheelDeltaY, mstate);
315         }
316 
317         override void onMouseMove(int x, int y, int dx, int dy, MouseState mstate)
318         {
319             x -= outer._userArea.min.x;
320             y -= outer._userArea.min.y;
321             bool hitSomething = outer.mouseMove(x, y, dx, dy, mstate, false);
322             version(futureMouseDrag)
323             {}
324             else
325             {
326                 if (!hitSomething)
327                 {
328                     // Nothing was mouse-over'ed, nothing is `isMouseOver()` anymore
329                     outer._uiContext.setMouseOver(null);
330                 }
331             }
332         }
333 
334         override void recomputeDirtyAreas()
335         {
336             return outer.recomputeDirtyAreas();
337         }
338 
339         override bool onKeyDown(Key key)
340         {
341             // Sends the event to the last clicked element first
342             if (outer._uiContext.focused !is null)
343                 if (outer._uiContext.focused.onKeyDown(key))
344                     return true;
345 
346             // else to all Elements
347             return outer.keyDown(key);
348         }
349 
350         override bool onKeyUp(Key key)
351         {
352             // Sends the event to the last clicked element first
353             if (outer._uiContext.focused !is null)
354                 if (outer._uiContext.focused.onKeyUp(key))
355                     return true;
356             // else to all Elements
357             return outer.keyUp(key);
358         }
359 
360         /// Returns areas affected by updates.
361         override box2i getDirtyRectangle() nothrow @nogc
362         {
363             box2i r = outer._rectsToResize[].boundingBox();
364 
365             // If _userArea changed recently, mark the whole area as in need of redisplay.
366             if (outer._reportBlackBordersAndResizedAreaAsDirty)
367                 r = r.expand( box2i(0, 0, outer._currentLogicalWidth, outer._currentLogicalHeight) );
368 
369             debug(resizing)
370             {
371                 if (!r.empty)
372                 {
373                     debugLogf("getDirtyRectangle returned rectangle(%d, %d, %d, %d)\n", r.min.x, r.min.y, r.width, r.height);
374                 }
375             }
376 
377             return r;
378         }
379 
380         override ImageRef!RGBA onResized(int width, int height)
381         {
382             return outer.doResize(width, height);
383         }
384 
385         override void onDraw(WindowPixelFormat pf) nothrow @nogc
386         {
387             return outer.doDraw(pf);
388         }
389 
390         override void onMouseCaptureCancelled()
391         {
392             // Stop an eventual drag operation
393             outer._uiContext.stopDragging();
394         }
395 
396         override void onMouseExitedWindow()
397         {
398             // Stop an eventual isMouseOver
399             version(futureMouseDrag)
400             {
401                 if (outer._uiContext.dragged is null)
402                     outer._uiContext.setMouseOver(null);
403             }
404             else
405             {
406                 outer._uiContext.setMouseOver(null);
407             }
408         }
409 
410         override void onAnimate(double dt, double time)
411         {
412             version(Dplug_ProfileUI) outer.profiler.category("ui").begin("animate");
413             outer.animate(dt, time);
414             version(Dplug_ProfileUI) outer.profiler.end();
415         }
416 
417         override MouseCursor getMouseCursor()
418         {
419             return outer._uiContext.getCurrentMouseCursor();
420         }
421     }
422 
423     /// Tune this to tune the trade-off between light quality and speed.
424     /// The default value was tuned by hand on very shiny light sources.
425     /// Too high and processing becomes more expensive.
426     /// Too little and the ligth decay doesn't feel natural.
427     /// IMPORTANT: This should be called only inside your main reflow() or at UI creation time.
428     void setUpdateMargin(int margin = 20) nothrow @nogc
429     {
430         _updateMargin = margin; // theoretically it should dirty every PBR rectangle... hence restricting to reflow().
431     }
432 
433 package:
434 
435     // <resizing support>
436 
437     final float getUIScale()
438     {
439         // There is currently no support for this in Dplug, so it is always 1.0f for now.
440         // The OS _might_ upscale the UI without our knowledge though.
441         return 1.0f;
442     }
443 
444     final float getUserScale()
445     {
446         // There is currently no _userArea resize in Dplug, so it is always 1.0f for now.
447         return 1.0f;
448     }
449 
450     final vec2i getDefaultUISizeInPixels()
451     {
452         int w = 0, h = 0;
453         _sizeConstraints.suggestDefaultSize(&w, &h);
454         return vec2i(w, h);
455     }
456 
457     final vec2i getUISizeInPixelsUser()
458     {
459         return vec2i(_currentUserWidth, _currentUserHeight);
460     }
461 
462     final vec2i getUISizeInPixelsLogical()
463     {
464         return vec2i(_currentLogicalWidth, _currentLogicalHeight);
465     }
466 
467     final vec2i getUISizeInPixelsPhysical()
468     {
469         return getUISizeInPixelsLogical(); // no support yet
470     }
471 
472     final void requestUIScreenshot()
473     {
474         atomicStore(_screenShotRequested, true);
475     }
476 
477     final bool requestUIResize(int widthLogicalPixels,
478                                int heightLogicalPixels)
479     {
480         // If it's already the same logical size, nothing to do.
481         if ( (widthLogicalPixels == _currentLogicalWidth)
482             &&  (heightLogicalPixels == _currentLogicalHeight) )
483             return true;
484 
485         bool parentWasResized = _client.requestResize(widthLogicalPixels, heightLogicalPixels);
486 
487         // We are be able to early-exit here in VST3.
488         // This is because once a VST3 host has resized the parent window, it calls a callback
489         // and that leads to `nativeWindowResize` to be called.
490         if (parentWasResized && (_client.getPluginFormat() == PluginFormat.vst3))
491             return true;
492 
493         // Cubase + VST2 + Windows need special treatment to resize parent and grandparent windows manually (Issue #595).
494         bool needResizeParentWindow = false;
495         version(Windows)
496         {
497             if (_client.getPluginFormat() == PluginFormat.vst2 && _client.getDAW() == DAW.Cubase)
498                 needResizeParentWindow = true;
499         }
500 
501         // In VST2, very few hosts also resize the plugin window. We get to do it manually.
502 
503         // Here we request the native window to resize.
504         // The actual resize will be received by the window listener, later.
505         bool success = _window.requestResize(widthLogicalPixels, heightLogicalPixels, needResizeParentWindow);
506 
507         // FL Studio format is different, the host needs to be notified _after_ a manual resize.
508         if (success && _client.getPluginFormat() == PluginFormat.flp)
509         {
510             success = _client.notifyResized;
511         }
512         return success;
513     }
514 
515     final void getUINearestValidSize(int* widthLogicalPixels, int* heightLogicalPixels)
516     {
517         // Convert this size to a user width and height
518         int userWidth = cast(int)(0.5f + *widthLogicalPixels * getUserScale());
519         int userHeight = cast(int)(0.5f + *heightLogicalPixels * getUserScale());
520 
521         _sizeConstraints.getNearestValidSize(&userWidth, &userHeight);
522 
523         // Convert back to logical pixels
524         // Note that because of rounding, there might be small problems yet unsolved.
525         *widthLogicalPixels = cast(int)(0.5f + userWidth / getUserScale());
526         *heightLogicalPixels = cast(int)(0.5f + userHeight / getUserScale());
527     }
528 
529     final bool isUIResizable()
530     {
531         // TODO: allow logic resize if internally user area is resampled
532         return _sizeConstraints.isResizable();
533     }
534     // </resizing support>
535 
536 protected:
537 
538     // The link to act on the client through the interface.
539     // Eventually it may supersedes direct usage of the client, or its IHostCommand in UIElements.
540     // Only valid in a openUI/closeUI pair.
541     IClient _client;
542 
543     ICompositor _compositor;
544 
545     UIContext _uiContext;
546 
547     WindowListener _windowListener;
548 
549     // An interface to the underlying window
550     IWindow _window;
551 
552     // Task pool for multi-threaded image work
553     package ThreadPool _threadPool;
554 
555     // Size constraints of this UI.
556     // Currently can only be a fixed size.
557     SizeConstraints _sizeConstraints;
558 
559     // The _external_ size in pixels of the plugin interface.
560     // This is the size seen by the host/window.
561     int _currentLogicalWidth = 0;
562     int _currentLogicalHeight = 0;
563 
564     // The _internal_ size in pixels of our UI.
565     // This is not the same as the size seen by the window ("logical").
566     int _currentUserWidth = 0;
567     int _currentUserHeight = 0;
568 
569     /// the area in logical area where the user area is drawn.
570     box2i _userArea;
571 
572     // Force userArea refresh on first resize.
573     bool _firstResize = true;
574 
575     // if true, it means the whole resize buffer and accompanying black
576     // borders should be redrawn at the next onDraw
577     bool _redrawBlackBordersAndResizedArea;
578 
579     // if true, it means the whole resize buffer and accompanying black
580     // borders should be reported as dirty at the next recomputeDirtyAreas, and until
581     // it is drawn.
582     bool _reportBlackBordersAndResizedAreaAsDirty;
583 
584     // Diffuse color values for the whole UI.
585     Mipmap!RGBA _diffuseMap;
586 
587     // Depth values for the whole UI.
588     Mipmap!L16 _depthMap;
589 
590     // Depth values for the whole UI.
591     Mipmap!RGBA _materialMap;
592 
593     /// The list of areas to be redrawn at the Raw and PBR levels (composited).
594     /// These are accumulated over possibly several calls of `recomputeDirtyRects`
595     /// and cleared by a call to `onDraw`.
596     /// Other lists of areas are purely derived from `_rectsToUpdateDisjointedRaw`
597     /// and `_rectsToUpdateDisjointedPBR`.
598     Vec!box2i _rectsToUpdateDisjointedRaw;
599     ///ditto
600     Vec!box2i _rectsToUpdateDisjointedPBR;
601 
602     // Used to maintain the _rectsToUpdateXXX invariant of no overlap
603     Vec!box2i _rectsTemp;
604 
605     // Same, but temporary variable for mipmap generation
606     Vec!box2i[2] _updateRectScratch;
607 
608     // The areas that must be effectively re-composited.
609     Vec!box2i _rectsToComposite;
610     Vec!box2i _rectsToCompositeDisjointed; // same list, but reorganized to avoid overlap
611     Vec!box2i _rectsToCompositeDisjointedTiled; // same list, but separated in smaller tiles
612 
613     // The areas that must be effectively redisplayed, which also mean the Raw layer is redrawn.
614     Vec!box2i _rectsToDisplay;
615     Vec!box2i _rectsToDisplayDisjointed; // same list, but reorganized to avoid overlap
616 
617     // The areas that must be effectively redisplayed, in logical space (like _userArea).
618     Vec!box2i _rectsToResize;
619     Vec!box2i _rectsToResizeDisjointed;
620 
621     /// The list of UIElement to potentially call `onDrawPBR` on.
622     Vec!UIElement _elemsToDrawRaw;
623 
624     /// The list of UIElement to potentially call `onDrawPBR` on.
625     Vec!UIElement _elemsToDrawPBR;
626 
627     /// The scratch buffer used to sort the two above list.
628     Vec!UIElement _sortScratchBuf;
629 
630     /// Amount of pixels dirty rectangles are extended with.
631     int _updateMargin = 20;
632 
633     /// The composited buffer, before the Raw layer is applied.
634     OwnedImage!RGBA _compositedBuffer = null;
635 
636     /// The rendered framebuffer.
637     /// This is copied from `_compositedBuffer`, then Raw layer is drawn on top.
638     /// Components are reordered there.
639     /// It must be possible to use a Canvas on it.
640     OwnedImage!RGBA _renderedBuffer = null;
641 
642     /// The final framebuffer.
643     /// It is the only buffer to have a size in logical pixels.
644     /// Internally the UI has an "user" size.
645     /// FUTURE: resize from user size to logical size using a resizer,
646     /// to allow better looking DPI without the OS blurry resizing.
647     /// Or to allow higher internal pixel count.
648     ubyte* _resizedBuffer = null;
649 
650     /// If a screenshot was requested by user widget.
651     shared(bool) _screenShotRequested = false;
652 
653     void recomputeDrawLists()
654     {
655         // recompute draw lists
656         _elemsToDrawRaw.clearContents();
657         _elemsToDrawPBR.clearContents();
658         getDrawLists(_elemsToDrawRaw, _elemsToDrawPBR);
659 
660         // Sort by ascending z-order (high z-order gets drawn last)
661         // This sort must be stable to avoid messing with tree natural order.
662         int compareZOrder(in UIElement a, in UIElement b) nothrow @nogc
663         {
664             return a.zOrder() - b.zOrder();
665         }
666         timSort!UIElement(_elemsToDrawRaw[], _sortScratchBuf, &compareZOrder);
667         timSort!UIElement(_elemsToDrawPBR[], _sortScratchBuf, &compareZOrder);
668     }
669 
670     // Useful to convert 16-byte aligned buffer into an ImageRef!RGBA
671     // This was probably still needed because of Issue #693. This was secretly a 
672     // workaround. FUTURE: replace by regular toRef
673     final ImageRef!RGBA toImageRef(ubyte* alignedBuffer, int width, int height)
674     {
675         ImageRef!RGBA ir = void;
676         ir.w = width;
677         ir.h = height;
678         ir.pitch = byteStride(width);
679         ir.pixels = cast(RGBA*)alignedBuffer;
680         return ir;
681     }
682 
683     IProfiler profiler()
684     {
685         return _uiContext.profiler();
686     }
687 
688     void doDraw(WindowPixelFormat pf) nothrow @nogc
689     {
690         version(Dplug_ProfileUI) profiler.category("ui").begin("doDraw");
691 
692         debug(resizing) debugLogf(">doDraw\n");
693 
694         debug(resizing)
695         {
696             foreach(r; _rectsToUpdateDisjointedPBR[])
697             {
698                 debugLogf("  * this will redraw PBR rectangle(%d, %d, %d, %d)\n", r.min.x, r.min.y, r.width, r.height);
699             }
700             foreach(r; _rectsToUpdateDisjointedRaw[])
701             {
702                 debugLogf("  * this will redraw RAW rectangle(%d, %d, %d, %d)\n", r.min.x, r.min.y, r.width, r.height);
703             }
704         }
705 
706         // A. Recompute draw lists
707         // These are the `UIElement`s that _may_ have their onDrawXXX callbacks called.
708 
709         version(Dplug_ProfileUI) profiler.begin("Recompute Draw Lists");
710         recomputeDrawLists();
711         version(Dplug_ProfileUI) profiler.end();
712 
713         // Composite GUI
714         // Most of the cost of rendering is here
715         // B. 1st PASS OF REDRAW
716         // Some UIElements are redrawn at the PBR level
717         version(Dplug_ProfileUI) profiler.begin("Draw Elements PBR");
718         redrawElementsPBR();
719         version(Dplug_ProfileUI) profiler.end();
720 
721         // C. MIPMAPPING
722         version(Dplug_ProfileUI) profiler.begin("Regenerate Mipmaps");
723         regenerateMipmaps();
724         version(Dplug_ProfileUI) profiler.end();
725 
726         // D. COMPOSITING
727         auto compositedRef = _compositedBuffer.toRef();
728 
729         version(Dplug_ProfileUI) profiler.begin("Composite GUI");
730         compositeGUI(compositedRef); // Launch the possibly-expensive Compositor step, which implements PBR rendering
731         version(Dplug_ProfileUI) profiler.end();
732 
733         // E. COPY FROM "COMPOSITED" TO "RENDERED" BUFFER
734         // Copy _compositedBuffer onto _renderedBuffer for every rect that will be changed on display
735         auto renderedRef = _renderedBuffer.toRef();
736         version(Dplug_ProfileUI) profiler.begin("Copy to renderbuffer");
737         foreach(rect; _rectsToDisplayDisjointed[])
738         {
739             auto croppedComposite = compositedRef.cropImageRef(rect);
740             auto croppedRendered = renderedRef.cropImageRef(rect);
741             croppedComposite.blitTo(croppedRendered); // failure to optimize this: 1
742         }
743         version(Dplug_ProfileUI) profiler.end();
744         
745         // F. 2nd PASS OF REDRAW
746         version(Dplug_ProfileUI) profiler.begin("Draw Elements Raw");
747         redrawElementsRaw();
748         version(Dplug_ProfileUI) profiler.end();
749 
750         // G. Reorder components to the right pixel format
751         version(Dplug_ProfileUI) profiler.begin("Component Reorder");
752         reorderComponents(pf);
753         version(Dplug_ProfileUI) profiler.end();
754 
755         // G.bis
756         // We have a render.
757         // Eventually make a screenshot here, if one was requested asynchronously.
758         if (cas(&_screenShotRequested, true, false))
759         {
760             onScreenshot(_renderedBuffer.toRef(), 
761                          pf, 
762                          _diffuseMap.levels[0].toRef,
763                          _depthMap.levels[0].toRef,
764                          _materialMap.levels[0].toRef);
765         }
766 
767         // H. Copy updated content to the final buffer. (hint: not actually resizing)
768         version(Dplug_ProfileUI) profiler.begin("Copy content");
769         resizeContent(pf);
770         version(Dplug_ProfileUI) profiler.end();
771 
772         // Only then is the list of rectangles to update cleared,
773         // before calling `doDraw` such work accumulates
774         _rectsToUpdateDisjointedPBR.clearContents();
775         _rectsToUpdateDisjointedRaw.clearContents();
776 
777         version(Dplug_ProfileUI) profiler.end();
778         debug(resizing) debugLogf("<doDraw\n");
779     }
780 
781     void recomputeDirtyAreas() nothrow @nogc
782     {
783         // First we pull dirty rectangles from the UI, for the PBR and Raw layers
784         // Note that there is indeed a race here (the same UIElement could have pushed rectangles in both
785         // at around the same time), but that isn't a problem.
786         context().dirtyListRaw.pullAllRectangles(_rectsToUpdateDisjointedRaw);
787         context().dirtyListPBR.pullAllRectangles(_rectsToUpdateDisjointedPBR);
788 
789         recomputePurelyDerivedRectangles();
790     }
791 
792     void recomputePurelyDerivedRectangles()
793     {
794         // If a resize has been made recently, we need to clip rectangles
795         // in the pending lists to the new size.
796         // All other rectangles are purely derived from those.
797         // PERF: this check is necessary because of #597.
798         //       Solveing this is a long-term quest in itself.
799         box2i validUserArea = rectangle(0, 0, _currentUserWidth, _currentUserHeight);
800         foreach (ref r; _rectsToUpdateDisjointedRaw[])
801         {
802             r = r.intersection(validUserArea);
803         }
804         foreach (ref r; _rectsToUpdateDisjointedPBR[])
805         {
806             r = r.intersection(validUserArea);
807         }
808 
809         // The problem here is that if the window isn't shown there may be duplicates in
810         // _rectsToUpdateDisjointedRaw and _rectsToUpdateDisjointedPBR
811         // (`recomputeDirtyAreas`called multiple times without clearing those arrays),
812         //  so we have to maintain unicity again.
813         // Also duplicate can accumulate in case of two successive onResize (to test: Studio One with continuous resizing plugin)
814         //
815         // PERF: when the window is shown, we could overwrite content of _rectsToUpdateDisjointedRaw/_rectsToUpdateDisjointedPBR?
816         //       instead of doing that.
817         {
818             // Make _rectsToUpdateDisjointedRaw disjointed
819             _rectsTemp.clearContents();
820             removeOverlappingAreas(_rectsToUpdateDisjointedRaw, _rectsTemp);
821             _rectsToUpdateDisjointedRaw.clearContents();
822             _rectsToUpdateDisjointedRaw.pushBack(_rectsTemp);
823             assert(haveNoOverlap(_rectsToUpdateDisjointedRaw[]));
824 
825             // Make _rectsToUpdateDisjointedPBR disjointed
826             _rectsTemp.clearContents();
827             removeOverlappingAreas(_rectsToUpdateDisjointedPBR, _rectsTemp);
828             _rectsToUpdateDisjointedPBR.clearContents();
829             _rectsToUpdateDisjointedPBR.pushBack(_rectsTemp);
830             assert(haveNoOverlap(_rectsToUpdateDisjointedPBR[]));
831         }
832 
833         // Compute _rectsToRender and _rectsToDisplay, purely derived from the above.
834         // Note that they are possibly overlapping collections
835         // _rectsToComposite <- margin(_rectsToUpdateDisjointedPBR)
836         // _rectsToDisplay <- union(_rectsToComposite, _rectsToUpdateDisjointedRaw)
837         {
838             _rectsToComposite.clearContents();
839             foreach(rect; _rectsToUpdateDisjointedPBR)
840             {
841                 assert(rect.isSorted);
842                 assert(!rect.empty);
843                 _rectsToComposite.pushBack( convertPBRLayerRectToRawLayerRect(rect, _currentUserWidth, _currentUserHeight) );
844             }
845 
846             // Compute the non-overlapping version
847             _rectsToCompositeDisjointed.clearContents();
848             removeOverlappingAreas(_rectsToComposite, _rectsToCompositeDisjointed);
849 
850             _rectsToDisplay.clearContents();
851             _rectsToDisplay.pushBack(_rectsToComposite);
852             foreach(rect; _rectsToUpdateDisjointedRaw)
853             {
854                 assert(rect.isSorted);
855                 assert(!rect.empty);
856                 _rectsToDisplay.pushBack( rect );
857             }
858 
859             // Compute the non-overlapping version
860             _rectsToDisplayDisjointed.clearContents();
861             removeOverlappingAreas(_rectsToDisplay, _rectsToDisplayDisjointed);
862         }
863 
864         // Compute _rectsToResize and _rectsToDisplayDisjointed to write resized content to (in the logical pixel area).
865         // These rectangle are constrained to update only _userArea.
866         {
867             _rectsToResize.clearContents();
868             foreach(rect; _rectsToDisplay[])
869             {
870                 box2i r = convertUserRectToLogicalRect(rect).intersection(_userArea);
871                 _rectsToResize.pushBack(r);
872             }
873 
874             if (_reportBlackBordersAndResizedAreaAsDirty)
875             {
876                 // Redraw whole resized zone and black borders on next draw, as this will
877                 // be reported to the OS as being repainted.
878                 _redrawBlackBordersAndResizedArea = true;
879             }
880             _rectsToResizeDisjointed.clearContents();
881             removeOverlappingAreas(_rectsToResize, _rectsToResizeDisjointed);
882 
883             // All those rectangles should be strictly in _userArea
884             foreach(r; _rectsToResizeDisjointed)
885                 assert(_userArea.contains(r));
886         }
887     }
888 
889     final box2i convertPBRLayerRectToRawLayerRect(box2i rect, int width, int height) nothrow @nogc
890     {
891         int xmin = rect.min.x - _updateMargin;
892         int ymin = rect.min.y - _updateMargin;
893         int xmax = rect.max.x + _updateMargin;
894         int ymax = rect.max.y + _updateMargin;
895 
896         if (xmin < 0) xmin = 0;
897         if (ymin < 0) ymin = 0;
898         if (xmax > width) xmax = width;
899         if (ymax > height) ymax = height;
900 
901         // This could also happen if an UIElement is moved quickly
902         if (xmax < 0) xmax = 0;
903         if (ymax < 0) ymax = 0;
904         if (xmin > width) xmin = width;
905         if (ymin > height) ymin = height;
906 
907         box2i result = box2i(xmin, ymin, xmax, ymax);
908         assert(result.isSorted);
909         return result;
910     }
911 
912     ImageRef!RGBA doResize(int widthLogicalPixels,
913                            int heightLogicalPixels) nothrow @nogc
914     {
915         version(Dplug_ProfileUI) profiler.category("ui").begin("doResize");
916         debug(resizing) debugLogf(">doResize(%d, %d)\n", widthLogicalPixels, heightLogicalPixels);
917 
918         /// We do receive a new size in logical pixels.
919         /// This is coming from getting the window client area. The reason
920         /// for this resize doesn't matter, we must find a mapping that fits
921         /// between this given logical size and user size.
922 
923         // 1.a Based upon the _sizeConstraints, select a user size in pixels.
924         //     Keep in mind if the _userArea has just moved (just moving the contents elsewhere)
925         //     or if its size has changed (user size), which require a redraw.
926 
927         // Has the logical available size changed?
928         bool logicalSizeChanged = false;
929         if (_currentLogicalWidth != widthLogicalPixels)
930         {
931             _currentLogicalWidth = widthLogicalPixels;
932             logicalSizeChanged = true;
933         }
934         if (_currentLogicalHeight != heightLogicalPixels)
935         {
936             _currentLogicalHeight = heightLogicalPixels;
937             logicalSizeChanged = true;
938         }
939 
940         int newUserWidth = widthLogicalPixels;
941         int newUserHeight = heightLogicalPixels;
942         _sizeConstraints.getMaxSmallerValidSize(&newUserWidth, &newUserHeight);
943 
944         bool userSizeChanged = false;
945         if (_currentUserWidth != newUserWidth)
946         {
947             _currentUserWidth = newUserWidth;
948             userSizeChanged = true;
949         }
950         if (_currentUserHeight != newUserHeight)
951         {
952             _currentUserHeight = newUserHeight;
953             userSizeChanged = true;
954         }
955 
956         // On first onResize, assume both sizes changed
957         if (_firstResize)
958         {
959             logicalSizeChanged = true;
960             userSizeChanged = true;
961             _firstResize = false;
962         }
963 
964         if (userSizeChanged) { assert(logicalSizeChanged); }
965 
966         // 1.b Update user area rect. We find a suitable space in logical area
967         //     to draw the whole UI.
968         if (logicalSizeChanged)
969         {
970             int x, y, w, h;
971             if (_currentLogicalWidth >= _currentUserWidth)
972             {
973                 x = (_currentLogicalWidth - _currentUserWidth) / 2;
974                 w = _currentUserWidth;
975             }
976             else
977             {
978                 x = 0;
979                 w = _currentLogicalWidth;
980             }
981             if (_currentLogicalHeight >= _currentUserHeight)
982             {
983                 y = (_currentLogicalHeight - _currentUserHeight) / 2;
984                 h = _currentUserHeight;
985             }
986             else
987             {
988                 y = 0;
989                 h = _currentLogicalHeight;
990             }
991 
992             _userArea = box2i.rectangle(x, y, w, h);
993 
994             debug(resizing)
995             {
996                 debugLogf("new _userArea is rectangle(%d, %d, %d, %d)\n", x, y, w, h);
997             }
998 
999             _reportBlackBordersAndResizedAreaAsDirty = true;
1000 
1001             // Note: out of range rectangles will still be in the dirtyListRaw/dirtyListPBR
1002             // and also _rectsToUpdateDisjointedPBR/_rectsToUpdateDisjointedRaw
1003             // This is the dreaded Issue #597
1004             // Unicity and boundness is maintained inside recomputePurelyDerivedRectangles().
1005 
1006             // The user size has changed. Force an immediate full redraw, so that no ancient data is used.
1007             // Not that this is on top of previous resizes or pulled rectangles in 
1008             // _rectsToUpdateDisjointedPBR / _rectsToUpdateDisjointedRaw.
1009             if (userSizeChanged)
1010             {
1011                 debug(resizing) debugLogf("  * provoke full redraw\n");
1012                 _rectsToUpdateDisjointedPBR.pushBack( rectangle(0, 0, _userArea.width, _userArea.height) );
1013             }
1014 
1015             // This avoids an onDraw with wrong rectangles
1016             recomputePurelyDerivedRectangles();
1017         }
1018 
1019         // 2. Invalidate UI region if user size change.
1020         //    Note: _resizedBuffer invalidation is managed with flags instead of this.
1021         position = box2i(0, 0, _currentUserWidth, _currentUserHeight);
1022 
1023         // 3. Resize compositor buffers.
1024         _compositor.resizeBuffers(_currentUserWidth, _currentUserHeight, PBR_TILE_MAX_WIDTH, PBR_TILE_MAX_HEIGHT);
1025 
1026         _diffuseMap.size(5, _currentUserWidth, _currentUserHeight);
1027         _depthMap.size(4, _currentUserWidth, _currentUserHeight);
1028 
1029         // The first level of the depth map has a border of 1 pixels and 2 pxiels on the right, to simplify some PBR passes
1030         int border_1 = 1;
1031         int rowAlign_1 = 1;
1032         int xMultiplicity_1 = 1;
1033         int trailingSamples_2 = 2;
1034         _depthMap.levels[0].size(_currentUserWidth, _currentUserHeight, border_1, rowAlign_1, xMultiplicity_1, trailingSamples_2);
1035 
1036         _materialMap.size(0, _currentUserWidth, _currentUserHeight);
1037 
1038         // Extends buffers with user size
1039 
1040         int border_0 = 0;
1041         int rowAlign_16 = 16;
1042         int trailingSamples_0 = 0;
1043         int trailingSamples_3 = 3;
1044         _compositedBuffer.size(_currentUserWidth, _currentUserHeight, border_0, rowAlign_16, xMultiplicity_1, trailingSamples_0);
1045         _renderedBuffer.size(_currentUserWidth, _currentUserHeight, border_0, rowAlign_16, xMultiplicity_1, trailingSamples_3);
1046 
1047         // Extends final buffer with logical size
1048         //
1049         // Why one line more with the +1? This is to fixIssue #741 and all other related macOS bugs.
1050         // This workarounds an Apple bug that made a lot of crashed between Nov2022 and Jan2024.
1051         size_t sizeNeeded = byteStride(_currentLogicalWidth) * (_currentLogicalHeight + 1);
1052 
1053 
1054         _resizedBuffer = cast(ubyte*) alignedRealloc(_resizedBuffer, sizeNeeded, 16);
1055 
1056         debug(resizing) debugLogf("<doResize(%d, %d)\n", widthLogicalPixels, heightLogicalPixels);
1057 
1058         version(Dplug_ProfileUI) profiler.end();
1059 
1060         return toImageRef(_resizedBuffer, _currentLogicalWidth, _currentLogicalHeight);
1061     }
1062 
1063     /// Draw the Raw layer of `UIElement` widgets
1064     void redrawElementsRaw() nothrow @nogc
1065     {
1066         enum bool parallelDraw = true;
1067 
1068         ImageRef!RGBA renderedRef = _renderedBuffer.toRef();
1069 
1070         // No need to launch threads only to have them realize there isn't anything to do
1071         if (_rectsToDisplayDisjointed.length == 0)
1072             return;
1073 
1074         static if (parallelDraw)
1075         {
1076             int drawn = 0;
1077             int N = cast(int)_elemsToDrawRaw.length;
1078 
1079             while(drawn < N)
1080             {
1081                 // See: redrawElementsPBR below for a remark on performance there.
1082 
1083                 int canBeDrawn = 1; // at least one can be drawn without collision
1084 
1085                 // Does this first widget in the FIFO wants to be draw alone?
1086                 if (! _elemsToDrawRaw[drawn].isDrawAloneRaw())
1087                 {
1088                     // Search max number of parallelizable draws until the end of the list or a collision is found
1089                     bool foundIntersection = false;
1090                     for ( ; (drawn + canBeDrawn < N); ++canBeDrawn)
1091                     {
1092                         // Should we include this element to the assembled set of widgets to draw?
1093                         UIElement candidateWidget = _elemsToDrawRaw[drawn + canBeDrawn];
1094 
1095                         if (candidateWidget.isDrawAloneRaw())
1096                             break; // wants to be drawn alone
1097 
1098                         box2i candidatePos = candidateWidget.position;
1099 
1100                         for (int j = 0; j < canBeDrawn; ++j) // PERF: aaaand this is nicely quadratic
1101                         {
1102                             if (_elemsToDrawRaw[drawn + j].position.intersects(candidatePos))
1103                             {
1104                                 foundIntersection = true;
1105                                 break;
1106                             }
1107                         }
1108                         if (foundIntersection)
1109                             break;
1110                     }
1111                 }
1112 
1113                 assert(canBeDrawn >= 1);
1114 
1115                 // Draw a number of UIElement in parallel
1116                 void drawOneItem(int i, int threadIndex) nothrow @nogc
1117                 {
1118                     version(Dplug_ProfileUI) 
1119                     {
1120                         char[maxUIElementIDLength + 16] idstr;
1121                         snprintf(idstr.ptr, 128, 
1122                                  "draw Raw element %s".ptr, _elemsToDrawRaw[drawn + i].getId().ptr);
1123                         profiler.category("draw").begin(idstr);
1124                     }
1125 
1126                     _elemsToDrawRaw[drawn + i].renderRaw(renderedRef, _rectsToDisplayDisjointed[]);
1127 
1128                     version(Dplug_ProfileUI) profiler.end();
1129                 }
1130                 _threadPool.parallelFor(canBeDrawn, &drawOneItem);
1131 
1132                 drawn += canBeDrawn;
1133                 assert(drawn <= N);
1134             }
1135             assert(drawn == N);
1136         }
1137         else
1138         {
1139             foreach(elem; _elemsToDrawRaw)
1140                 elem.renderRaw(renderedRef, _rectsToDisplayDisjointed[]);
1141         }
1142     }
1143 
1144     /// Draw the PBR layer of `UIElement` widgets
1145     void redrawElementsPBR() nothrow @nogc
1146     {
1147         enum bool parallelDraw = true;
1148 
1149         assert(_diffuseMap.levels[0] !is null);
1150         assert(_depthMap.levels[0] !is null);
1151         assert(_materialMap.levels[0] !is null);
1152         auto diffuseRef = _diffuseMap.levels[0].toRef();
1153         auto depthRef = _depthMap.levels[0].toRef();
1154         auto materialRef = _materialMap.levels[0].toRef();
1155 
1156         // No need to launch threads only to have them realize there isn't anything to do
1157         if (_rectsToUpdateDisjointedPBR.length == 0)
1158             return;
1159 
1160         static if (parallelDraw)
1161         {
1162             int drawn = 0;
1163             int N = cast(int)_elemsToDrawPBR.length;
1164 
1165             while(drawn < N)
1166             {
1167                 // <Scheduling remark>
1168                 // PERF: scheduling here is not entirely optimal: consecutive overalapping widgets 
1169                 // would block further parallel draw if the next widget doesn't overlap the other two.
1170                 //
1171                 //  ________          _____
1172                 //  |      |          |   |
1173                 //  |  B   |______    | C |      <---- Will not draw A and C in parallel if
1174                 //  |______|     |    |___|            Z(A) < Z(B) < Z(C)
1175                 //      |    A   |
1176                 //      |________|
1177                 //
1178                 // PERF: to go further, could use the disjointed rects to draw even more in parallel. 
1179                 // Real updated graphics is intersection(position, union(_rectsToUpdateDisjointedPBR)),
1180                 // not simply the widget position.
1181                 // </Scheduling remark>
1182 
1183                 int canBeDrawn = 1; // at least one can be drawn without collision
1184 
1185                 // Does this first widget in the FIFO wants to be draw alone?
1186                 if (! _elemsToDrawPBR[drawn].isDrawAlonePBR())
1187                 {
1188                     // Search max number of parallelizable draws until the end of the list or a collision is found
1189                     bool foundIntersection = false;
1190                     for ( ; (drawn + canBeDrawn < N); ++canBeDrawn)
1191                     {
1192                         // Should we include this element to the assembled set of widgets to draw?
1193                         UIElement candidateWidget = _elemsToDrawPBR[drawn + canBeDrawn];
1194 
1195                         if (candidateWidget.isDrawAlonePBR())
1196                             break; // wants to be drawn alone
1197 
1198                         box2i candidatePos = _elemsToDrawPBR[drawn + canBeDrawn].position;
1199 
1200                         for (int j = 0; j < canBeDrawn; ++j) // check with each former selected widget, PERF quadratic
1201                         {
1202                             if (_elemsToDrawPBR[drawn + j].position.intersects(candidatePos))
1203                             {
1204                                 foundIntersection = true;
1205                                 break;
1206                             }
1207                         }
1208                         if (foundIntersection)
1209                             break;
1210                     }
1211                 }
1212 
1213                 assert(canBeDrawn >= 1);
1214 
1215                 // Draw a number of UIElement in parallel
1216                 void drawOneItem(int i, int threadIndex) nothrow @nogc
1217                 {
1218                     version(Dplug_ProfileUI) 
1219                     {
1220                         char[maxUIElementIDLength + 16] idstr;
1221                         snprintf(idstr.ptr, 128, 
1222                                  "draw PBR element %s", _elemsToDrawPBR[drawn + i].getId().ptr);
1223                         profiler.category("draw").begin(idstr);
1224                     }
1225 
1226                     _elemsToDrawPBR[drawn + i].renderPBR(diffuseRef, depthRef, materialRef, _rectsToUpdateDisjointedPBR[]);
1227 
1228                     version(Dplug_ProfileUI) profiler.end();
1229                 }
1230                 _threadPool.parallelFor(canBeDrawn, &drawOneItem);
1231 
1232                 drawn += canBeDrawn;
1233                 assert(drawn <= N);
1234             }
1235             assert(drawn == N);
1236         }
1237         else
1238         {
1239             // Render required areas in diffuse and depth maps, base level
1240             foreach(elem; _elemsToDraw)
1241                 elem.renderPBR(diffuseRef, depthRef, materialRef, _rectsToUpdateDisjointedPBR[]);
1242         }
1243     }
1244 
1245     /// Do the PBR compositing step. This is the most expensive step in the UI.
1246     void compositeGUI(ImageRef!RGBA wfb) nothrow @nogc
1247     {
1248         _rectsToCompositeDisjointedTiled.clearContents();
1249         tileAreas(_rectsToCompositeDisjointed[],  PBR_TILE_MAX_WIDTH, PBR_TILE_MAX_HEIGHT, _rectsToCompositeDisjointedTiled);
1250 
1251         _compositor.compositeTile(wfb,
1252                                   _rectsToCompositeDisjointedTiled[],
1253                                   _diffuseMap,
1254                                   _materialMap,
1255                                   _depthMap,
1256                                   profiler());
1257     }
1258 
1259     /// Compose lighting effects from depth and diffuse into a result.
1260     /// takes output image and non-overlapping areas as input
1261     /// Useful multithreading code.
1262     void regenerateMipmaps() nothrow @nogc
1263     {
1264         int numAreas = cast(int)_rectsToUpdateDisjointedPBR.length;
1265 
1266         // No mipmap to update, no need to launch threads
1267         if (numAreas == 0)
1268             return;
1269 
1270         // Fill update rect buffer with the content of _rectsToUpdateDisjointedPBR
1271         for (int i = 0; i < 2; ++i)
1272         {
1273             _updateRectScratch[i].clearContents();
1274             _updateRectScratch[i].pushBack(_rectsToUpdateDisjointedPBR[]);
1275         }
1276 
1277         // Mipmapping used to be threaded, however because it's completely memory-bound
1278         // (about 2mb read/sec) and fast enough, it's not worth launching threads for.
1279 
1280         version(Dplug_ProfileUI) profiler.category("mipmap").begin("diffuse mipmap");
1281 
1282         // Generate diffuse mipmap, useful for dealing with emissive
1283         {
1284             // diffuse
1285             Mipmap!RGBA mipmap = _diffuseMap;
1286             foreach(level; 1 .. mipmap.numLevels())
1287             {
1288                 Mipmap!RGBA.Quality quality;
1289                 if (level == 1)
1290                     quality = Mipmap!RGBA.Quality.boxAlphaCovIntoPremul;
1291                 else
1292                     quality = Mipmap!RGBA.Quality.cubic;
1293                 foreach(ref area; _updateRectScratch[0])
1294                 {
1295                     // Note: the rects might be disjointed, but each leveling up makes them
1296                     // Possibly overlapping. It is assumed the cost is minor.
1297                     // Some pixels in higher mipmap levels might be computed several times.
1298                     area = mipmap.generateNextLevel(quality, area, level);
1299                 }
1300             }
1301         }
1302 
1303         version(Dplug_ProfileUI) profiler.end;
1304 
1305         version(Dplug_ProfileUI) profiler.begin("depth mipmap");
1306 
1307         // Generate depth mipmap, useful for dealing with ambient occlusion
1308         {
1309             int W = _currentUserWidth;
1310             int H = _currentUserHeight;
1311 
1312             // Depth is special since it has a border!
1313             // Regenerate the border area that needs to be regenerated
1314             OwnedImage!L16 level0 = _depthMap.levels[0];
1315             foreach(box2i area; _updateRectScratch[1])
1316                 level0.replicateBordersTouching(area);
1317 
1318             // DEPTH MIPMAPPING
1319             Mipmap!L16 mipmap = _depthMap;
1320             foreach(level; 1 .. mipmap.numLevels())
1321             {
1322                 auto quality = level >= 3 ? Mipmap!L16.Quality.cubic : Mipmap!L16.Quality.box;
1323                 foreach(ref area; _updateRectScratch[1])
1324                 {
1325                     area = mipmap.generateNextLevel(quality, area, level);
1326                 }
1327             }
1328         }
1329 
1330         version(Dplug_ProfileUI) profiler.end;
1331     }
1332 
1333     void reorderComponents(WindowPixelFormat pf)
1334     {
1335         auto renderedRef = _renderedBuffer.toRef();
1336 
1337         final switch(pf)
1338         {
1339             case WindowPixelFormat.RGBA8:
1340                 foreach(rect; _rectsToDisplayDisjointed[])
1341                 {
1342                     shuffleComponentsRGBA8ToRGBA8AndForceAlphaTo255(renderedRef.cropImageRef(rect));
1343                 }
1344                 break;
1345 
1346             case WindowPixelFormat.BGRA8:
1347                 foreach(rect; _rectsToDisplayDisjointed[])
1348                 {
1349                     shuffleComponentsRGBA8ToBGRA8AndForceAlphaTo255(renderedRef.cropImageRef(rect));
1350                 }
1351                 break;
1352 
1353             case WindowPixelFormat.ARGB8:
1354                 foreach(rect; _rectsToDisplayDisjointed[])
1355                 {
1356                     shuffleComponentsRGBA8ToARGB8AndForceAlphaTo255(renderedRef.cropImageRef(rect));
1357                 }
1358                 break;
1359         }
1360     }
1361 
1362     // From a user area rectangle, return a logical are rectangle with the same size.
1363     final box2i convertUserRectToLogicalRect(box2i b)
1364     {
1365         return b.translate(_userArea.min);
1366     }
1367 
1368     final box2i convertLogicalRectToUserRect(box2i b)
1369     {
1370         return b.translate(-_userArea.min);
1371     }
1372 
1373     void resizeContent(WindowPixelFormat pf)
1374     {
1375         // TODO: eventually resize?
1376         // For now what we do for logical area is crop and offset.
1377         // In the future, could be beneficial to resample if needed.
1378 
1379         auto renderedRef = _renderedBuffer.toRef();
1380         auto resizedRef = toImageRef(_resizedBuffer, _currentLogicalWidth, _currentLogicalHeight);
1381 
1382         box2i[] rectsToCopy = _rectsToResizeDisjointed[];
1383 
1384         // If invalidated, the whole buffer needs to be redrawn
1385         // (because of borders, or changing offsets of the user area).
1386         if (_redrawBlackBordersAndResizedArea)
1387         {
1388             debug(resizing) debugLogf("  * redrawing black borders, and copy item\n");
1389             RGBA black;
1390             final switch(pf)
1391             {
1392                 case WindowPixelFormat.RGBA8:
1393                 case WindowPixelFormat.BGRA8: black = RGBA(0, 0, 0, 255); break;
1394                 case WindowPixelFormat.ARGB8: black = RGBA(255, 0, 0, 0); break;
1395             }
1396             resizedRef.fillAll(black); // PERF: Only do this in the location of the black border.
1397 
1398             // No need to report that everything is dirty anymore.
1399             _reportBlackBordersAndResizedAreaAsDirty = false;
1400 
1401             // and no need to draw everything in onDraw anymore.
1402             _redrawBlackBordersAndResizedArea = false;
1403 
1404             rectsToCopy = (&_userArea)[0..1];
1405         }
1406 
1407         foreach(rect; rectsToCopy[])
1408         {
1409             int dx = _userArea.min.x;
1410             int dy = _userArea.min.y;
1411 
1412             for (int j = rect.min.y; j < rect.max.y; ++j)
1413             {
1414                 RGBA* src  = renderedRef.scanline(j - dy).ptr;
1415                 RGBA* dest = resizedRef.scanline(j).ptr;
1416                 dest[rect.min.x..rect.max.x] = src[(rect.min.x - dx)..(rect.max.x - dx)];
1417             }
1418         }
1419     }
1420 }
1421 
1422 
1423 // given a width, how long in bytes should scanlines be for the final output buffer.
1424 // Note: it seems win32 needs this exact stride for returned buffer. It mimics BMP.
1425 //       On the other hands, is seems other platforms don't have the same constraints with row pitch.
1426 int byteStride(int width) pure nothrow @nogc
1427 {
1428     // See https://github.com/AuburnSounds/Dplug/issues/563, there
1429     // is currently a coupling with dplug:window and this can't be changed.
1430     enum scanLineAlignment = 4;
1431     int widthInBytes = width * 4;
1432     return (widthInBytes + (scanLineAlignment - 1)) & ~(scanLineAlignment-1);
1433 }
1434 
1435 void shuffleComponentsRGBA8ToARGB8AndForceAlphaTo255(ImageRef!RGBA image) pure nothrow @nogc
1436 {
1437     immutable int w = image.w;
1438     immutable int h = image.h;
1439     for (int j = 0; j < h; ++j)
1440     {
1441         ubyte* scan = cast(ubyte*)image.scanline(j).ptr;
1442 
1443         int i = 0;
1444         for( ; i + 3 < w; i += 4)
1445         {
1446             __m128i inputBytes = _mm_loadu_si128(cast(__m128i*)(&scan[4*i]));
1447             inputBytes = _mm_or_si128(inputBytes, _mm_set1_epi32(0xff000000));
1448 
1449             version(LDC)
1450             {
1451                 import ldc.intrinsics;
1452                 import ldc.simd;
1453                 __m128i outputBytes = cast(__m128i) shufflevector!(byte16, 3, 0,  1,  2,
1454                                                                            7, 4,  5,  6,
1455                                                                            11, 8,  9,  10,
1456                                                                            15, 12, 13, 14)(cast(byte16)inputBytes, cast(byte16)inputBytes);
1457                 _mm_storeu_si128(cast(__m128i*)(&scan[4*i]), outputBytes);
1458             }
1459             else
1460             {
1461                 // convert to ushort
1462                 __m128i zero = _mm_setzero_si128();
1463                 __m128i e0_7 = _mm_unpacklo_epi8(inputBytes, zero);
1464                 __m128i e8_15 = _mm_unpackhi_epi8(inputBytes, zero);
1465 
1466                 enum int swapRB = _MM_SHUFFLE(2, 1, 0, 3);
1467                 e0_7 = _mm_shufflelo_epi16!swapRB(_mm_shufflehi_epi16!swapRB(e0_7));
1468                 e8_15 = _mm_shufflelo_epi16!swapRB(_mm_shufflehi_epi16!swapRB(e8_15));
1469                 __m128i outputBytes = _mm_packus_epi16(e0_7, e8_15);
1470                 _mm_storeu_si128(cast(__m128i*)(&scan[4*i]), outputBytes);
1471             }
1472         }
1473 
1474         for(; i < w; i ++)
1475         {
1476             ubyte r = scan[4*i];
1477             ubyte g = scan[4*i+1];
1478             ubyte b = scan[4*i+2];
1479             scan[4*i] = 255;
1480             scan[4*i+1] = r;
1481             scan[4*i+2] = g;
1482             scan[4*i+3] = b;
1483         }
1484     }
1485 }
1486 
1487 void shuffleComponentsRGBA8ToBGRA8AndForceAlphaTo255(ImageRef!RGBA image) pure nothrow @nogc
1488 {
1489     immutable int w = image.w;
1490     immutable int h = image.h;
1491     for (int j = 0; j < h; ++j)
1492     {
1493         ubyte* scan = cast(ubyte*)image.scanline(j).ptr;
1494 
1495         int i = 0;
1496         for( ; i + 3 < w; i += 4)
1497         {
1498             __m128i inputBytes = _mm_loadu_si128(cast(__m128i*)(&scan[4*i]));
1499             inputBytes = _mm_or_si128(inputBytes, _mm_set1_epi32(0xff000000));
1500 
1501             version(LDC)
1502             {
1503                 import ldc.intrinsics;
1504                 import ldc.simd;
1505                 __m128i outputBytes = cast(__m128i) shufflevector!(byte16, 2,  1,  0,  3,
1506                                                                            6,  5,  4,  7,
1507                                                                           10,  9,  8, 11,
1508                                                                           14, 13, 12, 15)(cast(byte16)inputBytes, cast(byte16)inputBytes);
1509                 _mm_storeu_si128(cast(__m128i*)(&scan[4*i]), outputBytes);
1510             }
1511             else
1512             {
1513                 // convert to ushort
1514                 __m128i zero = _mm_setzero_si128();
1515                 __m128i e0_7 = _mm_unpacklo_epi8(inputBytes, zero);
1516                 __m128i e8_15 = _mm_unpackhi_epi8(inputBytes, zero);
1517 
1518                 // swap red and green
1519                 enum int swapRB = _MM_SHUFFLE(3, 0, 1, 2);
1520                 e0_7 = _mm_shufflelo_epi16!swapRB(_mm_shufflehi_epi16!swapRB(e0_7));
1521                 e8_15 = _mm_shufflelo_epi16!swapRB(_mm_shufflehi_epi16!swapRB(e8_15));
1522                 __m128i outputBytes = _mm_packus_epi16(e0_7, e8_15);
1523                 _mm_storeu_si128(cast(__m128i*)(&scan[4*i]), outputBytes);
1524             }
1525         }
1526 
1527         for(; i < w; i ++)
1528         {
1529             ubyte r = scan[4*i];
1530             ubyte g = scan[4*i+1];
1531             ubyte b = scan[4*i+2];
1532             scan[4*i] = b;
1533             scan[4*i+1] = g;
1534             scan[4*i+2] = r;
1535             scan[4*i+3] = 255;
1536         }
1537     }
1538 }
1539 
1540 void shuffleComponentsRGBA8ToRGBA8AndForceAlphaTo255(ImageRef!RGBA image) pure nothrow @nogc
1541 {
1542     immutable int w = image.w;
1543     immutable int h = image.h;
1544     for (int j = 0; j < h; ++j)
1545     {
1546         ubyte* scan = cast(ubyte*)image.scanline(j).ptr;
1547 
1548         int i = 0;
1549         for( ; i + 3 < w; i += 4)
1550         {
1551             __m128i inputBytes = _mm_loadu_si128(cast(__m128i*)(&scan[4*i]));
1552             inputBytes = _mm_or_si128(inputBytes, _mm_set1_epi32(0xff000000));
1553             // No reordering to do
1554             _mm_storeu_si128(cast(__m128i*)(&scan[4*i]), inputBytes);
1555         }
1556 
1557         for(; i < w; i ++)
1558         {
1559             scan[4*i+3] = 255;
1560         }
1561     }
1562 }