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 }