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