1 // Copyright Ferdinand Majerech 2014. 2 // Distributed under the Boost Software License, Version 1.0. 3 // (See accompanying file LICENSE_1_0.txt or copy at 4 // http://www.boost.org/LICENSE_1_0.txt) 5 6 7 /// Default (and at the moment, only) Despiker GUI, based on SDL and OpenGL, using dimgui. 8 module openglgui; 9 10 import std.algorithm: find, map, max, min; 11 import std.exception: enforce, assumeWontThrow; 12 import std.experimental.logger; 13 import std.math: pow; 14 import std..string: format; 15 16 import derelict.opengl3.gl3; 17 18 import imgui; 19 20 import platform.inputdevice; 21 import platform.videodevice; 22 23 import despiker.despiker; 24 25 26 /// Exception thrown at GUI errors. 27 class GUIException : Exception 28 { 29 this(string msg, string file = __FILE__, int line = __LINE__) @safe pure nothrow 30 { 31 super(msg, file, line); 32 } 33 } 34 35 /// Base class for despiker GUIs. 36 abstract class DespikerGUI 37 { 38 /// Run the GUI (its event loop). 39 void run() @safe nothrow; 40 } 41 42 /// Default (and at the moment, only) Despiker GUI, based on SDL and OpenGL, using dimgui. 43 class OpenGLGUI: DespikerGUI 44 { 45 private: 46 // Video device used to access window size, do manual rendering, etc. 47 VideoDevice video_; 48 // Access to input. 49 InputDevice input_; 50 51 // Main log. 52 Logger log_; 53 54 // Despiker implementation. 55 Despiker despiker_; 56 57 // GUI layout. 58 Layout layout_; 59 60 // Renders the viewed zone graphs. 61 ViewRenderer view_; 62 63 // Current positions of scroll area scrollbars. 64 int sidebarScroll, sideinfoScroll, sidevarsScroll; 65 66 // If true, the variable sidebar display will be toggled next frame. 67 bool toggleVars_; 68 69 // Buffer to store goToFrame_ text input. 70 char[9] goToFrameBuf_; 71 // Entered text of the 'Go to Frame' text input. 72 char[] goToFrame_; 73 74 // Used to override default imgui color scheme. Currently only passed to scroll areas. 75 ColorScheme guiScheme_; 76 77 // Current zoom exponent. Mouse wheel and the -/= keys affect this. 78 // 79 // 0 means no zoom. The actual zoom is 1.25 ^ zoomExponent_. 80 int zoomExponent_ = 0; 81 82 // Panning value. RMB dragging and A/D keys affect this. 83 // 84 // Higher means the view is shifted to the left; lower to the right. 85 // Independent of zoom; the same value of pan_ will result on the view being centered 86 // at the same location regardless of zoom. 87 double pan_ = 0; 88 89 public: 90 /** Construct the GUI. 91 * 92 * Params: 93 * 94 * log = Main program log. 95 * despiker = Despiker implementation. 96 */ 97 this(Logger log, Despiker despiker) @trusted 98 { 99 log_ = log; 100 despiker_ = despiker; 101 layout_ = new Layout(); 102 103 // Init Derelict and SDL. 104 enum baseMsg = "Failed to initialize GUI: failed to"; 105 enforce(loadDerelict(log_), new GUIException(baseMsg ~ "load Derelict")); 106 scope(failure) { unloadDerelict(); } 107 enforce(initSDL(log_), new GUIException(baseMsg ~ "initialize SDL")); 108 scope(failure) { deinitSDL(); } 109 110 // Init video and input. 111 video_ = new VideoDevice(log_); 112 scope(failure) { destroy(video_); } 113 enforce(initVideo(video_, log_), new GUIException(baseMsg ~ "initialize VideoDevice")); 114 input_ = new InputDevice(&video_.height, log_); 115 scope(failure) { destroy(input_); } 116 117 // Prepare GL state for GUI drawing. 118 glClearColor(0.25f, 0.25f, 0.25f, 0.25f); 119 glEnable(GL_BLEND); 120 glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); 121 glDisable(GL_DEPTH_TEST); 122 123 // We only override the scroll area color scheme (to remove transparency). 124 guiScheme_ = defaultColorScheme; 125 guiScheme_.scroll.area.back = RGBA(32, 32, 32, 255); 126 127 goToFrame_ = goToFrameBuf_[0 .. 0]; 128 129 view_ = new ViewRenderer(video_, log_, layout_); 130 scope(failure) { destroy(view_); } 131 132 enforce(imguiInit(findFont(), 512), new GUIException(baseMsg ~ "initialize imgui")); 133 scope(failure) { imguiDestroy(); } 134 } 135 136 /// Destroy the GUI. Must be called to properly free GL resources. 137 ~this() @trusted 138 { 139 imguiDestroy(); 140 destroy(view_); 141 destroy(input_); 142 destroy(video_); 143 SDL_Quit(); 144 unloadDerelict(); 145 } 146 147 /// Run the GUI (its event loop). 148 override void run() @trusted nothrow 149 { 150 for(;;) 151 { 152 // Get keyboard/mouse input. 153 input_.update(); 154 if(input_.quit) { break; } 155 // React to window resize events. 156 if(input_.resized) 157 { 158 video_.resizeViewport(input_.resized.width, input_.resized.height); 159 } 160 161 // Update the Despiker. 162 despiker_.update(); 163 // Update the GUI. 164 update(); 165 166 // Log GL errors, if any. 167 video_.gl.runtimeCheck(); 168 169 // Swap the back buffer to the front, showing it in the window. 170 // Outside of the frameLoad zone because VSync could break our profiling. 171 video_.swapBuffers(); 172 } 173 } 174 175 176 private: 177 /// GUI update (frame). 178 void update() @system nothrow 179 { 180 // Clear the screen. 181 glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); 182 183 // Get mouse input and pass it to imgui. 184 const mouse = input_.mouse; 185 const keyboard = input_.keyboard; 186 ubyte mouseButtons; 187 if(mouse.button(Mouse.Button.Left)) { mouseButtons |= MouseButton.left; } 188 if(mouse.button(Mouse.Button.Right)) { mouseButtons |= MouseButton.right; } 189 190 const int width = cast(int)video_.width; 191 const int height = cast(int)video_.height; 192 193 // Start drawing the GUI. 194 imguiBeginFrame(mouse.x, mouse.y, mouseButtons, mouse.wheelYMovement, input_.unicode) 195 .assumeWontThrow; 196 scope(exit) 197 { 198 imguiEndFrame().assumeWontThrow; 199 imguiRender(width, height).assumeWontThrow; 200 } 201 layout_.update(width, height, toggleVars_ ? (!layout_.showVars) : layout_.showVars); 202 203 import std.array: empty; 204 auto view = despiker_.currentFrameView; 205 const noView = view.threads.empty; 206 207 if(!noView) 208 { 209 // Get the real start/end time of the frame containing execution in all threads. 210 const start = view.threads.map!((ref t) => t.frameInfo.startTime) 211 .reduce!min.assumeWontThrow; 212 const end = view.threads.map!((ref t) => t.frameInfo.endTime) 213 .reduce!max.assumeWontThrow; 214 const duration = end - start; 215 216 // Get input for the ViewRenderer (zooming, panning). 217 getViewInput(); 218 // Draw the view first so any widgets are on top of it. 219 view_.startDrawing(zoom, pan_, start, duration); 220 foreach(ref thread; view.threads) 221 { 222 view_.drawThreadZones(thread.zones.save); 223 } 224 view_.endDrawing(); 225 226 infoSidebar(start, duration).assumeWontThrow; 227 } 228 229 // Sidebars rendering and input. 230 actionsSidebar().assumeWontThrow; 231 232 if(layout_.showVars) 233 { 234 variablesSidebar(view).assumeWontThrow; 235 } 236 } 237 238 /// Get input for the view renderer (zooming and panning). 239 void getViewInput() @safe pure nothrow @nogc 240 { 241 const kb = input_.keyboard; 242 const mouse = input_.mouse; 243 244 // Zooming ('=' is the same key as '+' on most (at least QWERTY) keyboards). 245 const zoomKb = (kb.pressed(Key.Equals) ? 1 : 0) + (kb.pressed(Key.Minus) ? -1 : 0); 246 zoomExponent_ = min(40, max(-8, zoomExponent_ + mouse.wheelYMovement + zoomKb)); 247 // Panning 248 pan_ -= 128 * ((kb.pressed(Key.D) ? 1 : 0) + (kb.pressed(Key.A) ? -1 : 0)) / zoom; 249 if(mouse.button(Mouse.Button.Right)) { pan_ += mouse.xMovement / zoom; } 250 } 251 252 /** Render (and handle input from) the actions sidebar. 253 * 254 * While not nothrow, actionsSidebar() should be assumed to never throw. 255 */ 256 void actionsSidebar() @trusted 257 { 258 // The actions sidebar. 259 with(layout_) 260 { 261 imguiBeginScrollArea("Actions", sidebarX, sidebarY, sidebarW, sidebarH, 262 &sidebarScroll, guiScheme_); 263 } 264 scope(exit) { imguiEndScrollArea(); } 265 266 // Draws a button with a key shortcut to do the same action. 267 auto button = (string text, Key key) => 268 imguiButton(text) || input_.keyboard.pressed(key); 269 270 final switch(despiker_.mode) 271 { 272 case Despiker.Mode.NewestFrame: 273 if(button("Pause <Space>", Key.Space)) { despiker_.pause(); } 274 if(button("Worst Frame <1>", Key.One)) { despiker_.worstFrame(); } 275 break; 276 case Despiker.Mode.Manual: 277 if(button("Resume <Space>", Key.Space)) { despiker_.resume(); } 278 if(button("Next Frame <L>", Key.L)) { despiker_.nextFrame(); } 279 if(button("Previous Frame <H>", Key.H)) { despiker_.previousFrame(); } 280 if(button("Worst Frame <1>", Key.One)) { despiker_.worstFrame(); } 281 try if(imguiTextInput("Jump", goToFrameBuf_, goToFrame_)) 282 { 283 import std.conv; 284 scope(exit) { goToFrame_ = goToFrameBuf_[0 .. 0]; } 285 despiker_.frame = to!size_t(goToFrame_); 286 } 287 catch(Exception e) 288 { 289 log_.info("Invalid 'Go to Frame' input from user (expected " 290 "number, got " ~ goToFrame_); 291 } 292 break; 293 } 294 toggleVars_ = button((layout_.showVars ? "Hide" : "Show") ~ " Variables <V>", Key.V); 295 } 296 297 /** Render the info sidebar. 298 * 299 * Params: 300 * 301 * start = Start time of the currently viewed frame. 302 * duration = Duration of the currently viewed frame. 303 * 304 * While not nothrow, infoSidebar() should be assumed to never throw. 305 */ 306 void infoSidebar(ulong start, ulong duration) @trusted 307 { 308 // The info sidebar. 309 with(layout_) 310 { 311 imguiBeginScrollArea("Info", sideinfoX, sideinfoY, sideinfoW, sideinfoH, 312 &sideinfoScroll, guiScheme_); 313 } 314 scope(exit) { imguiEndScrollArea(); } 315 316 const frame = despiker_.frame; 317 const noFrame = frame == size_t.max; 318 319 imguiLabel("Frame: " ~ (noFrame ? "N/A" : "%s".format(frame))); 320 imguiLabel("Duration: " ~ (noFrame ? "N/A" : "%.2fms".format(duration * 0.0001))); 321 imguiLabel("Start: " ~ (noFrame ? "N/A" : "%.5fs".format(start * 0.0000001))); 322 imguiLabel("Frames: %s".format(despiker_.frameCount)); 323 } 324 325 /** Render the variables sidebar. 326 * 327 * Params: 328 * 329 * view = View of the current frame, including variables. 330 * 331 * While not nothrow, infoSidebar() should be assumed to never throw. 332 */ 333 void variablesSidebar(View)(ref View view) @trusted 334 { 335 // The info sidebar. 336 with(layout_) 337 { 338 imguiBeginScrollArea("Variables", sidevarsX, sidevarsY, sidevarsW, sidevarsH, 339 &sidevarsScroll, guiScheme_); 340 } 341 scope(exit) { imguiEndScrollArea(); } 342 343 foreach(t, ref thread; view.threads) 344 { 345 imguiLabel("Thread %s".format(t)); 346 imguiIndent(); 347 foreach(var; thread.vars) 348 { 349 imguiLabel("%s: %s".format(var.name, var.variable)); 350 } 351 imguiUnindent(); 352 } 353 } 354 355 /// Get the current zoom ratio (not exponent). 356 double zoom() @safe pure nothrow const @nogc 357 { 358 return pow(1.25, zoomExponent_); 359 } 360 361 import derelict.sdl2.sdl; 362 /** Find the font file to use for text drawing and return its filename. 363 * 364 * Throws: GUIException on failure. 365 */ 366 static string findFont() @trusted 367 { 368 import std.file: thisExePath, exists, isFile; 369 import std.path: dirName, buildPath; 370 371 string[] fontDirs = [thisExePath().dirName()]; 372 // For (eventual) root Despiker installations. 373 version(linux) { fontDirs ~= "/usr/share/despiker"; } 374 375 // Find the font in fontDirs. 376 enum fontName = "DroidSans.ttf"; 377 auto found = fontDirs.map!(dir => dir.buildPath(fontName)) 378 .find!(p => p.exists && p.isFile); 379 if(!found.empty) { return found.front; } 380 foreach(path; fontDirs.map!(dir => dir.buildPath(fontName))) 381 { 382 if(path.exists && path.isFile) { return path; } 383 } 384 385 throw new GUIException("Despiker font %s not found in any of expected directories: %s" 386 .format(fontName, fontDirs)); 387 } 388 389 /// Load libraries using through Derelict (currently, this is SDL2). 390 static bool loadDerelict(Logger log) @system nothrow 391 { 392 import derelict.util.exception; 393 // Load (but don't init) SDL2. 394 try 395 { 396 DerelictSDL2.load(); 397 return true; 398 } 399 catch(SharedLibLoadException e) 400 { 401 log.critical("SDL2 not found: ", e.msg).assumeWontThrow; 402 } 403 catch(SymbolLoadException e) 404 { 405 log.critical("Missing SDL2 symbol (old SDL2 version?): ", e.msg).assumeWontThrow; 406 } 407 catch(Exception e) 408 { 409 assert(false, "Unexpected exception in DerelictSDL2.load()"); 410 } 411 412 return false; 413 } 414 415 /// Unload Derelict libraries. 416 static void unloadDerelict() @system nothrow 417 { 418 DerelictSDL2.unload().assumeWontThrow; 419 } 420 421 /// Initialize the SDL library. 422 static bool initSDL(Logger log) @system nothrow 423 { 424 // Initialize SDL Video subsystem. 425 if(SDL_Init(SDL_INIT_VIDEO) < 0) 426 { 427 // SDL_Init returns a negative number on error. 428 log.critical("SDL Video subsystem failed to initialize").assumeWontThrow; 429 return false; 430 } 431 return true; 432 } 433 434 /// Deinitialize the SDL library. 435 static void deinitSDL() @system nothrow @nogc 436 { 437 SDL_Quit(); 438 } 439 440 /// Initialize the video device (setting video mode and initializing OpenGL). 441 static bool initVideo(VideoDevice video_, Logger log) @system nothrow 442 { 443 // Initialize the video device. 444 const width = 1024; 445 const height = 768; 446 import std.typecons; 447 const fullscreen = No.fullscreen; 448 449 if(!video_.initWindow(width, height, fullscreen)) { return false; } 450 if(!video_.initGL()) { return false; } 451 video_.windowTitle = "Despiker"; 452 return true; 453 } 454 } 455 456 457 private: 458 459 /// GUI layout. 460 class Layout 461 { 462 // Margin between GUI elements. 463 int margin = 4; 464 465 // Size and position of the sidebar. 466 int sidebarW, sidebarH, sidebarX, sidebarY; 467 468 // Size and position of the 'info' sidebar. 469 int sideinfoW, sideinfoH, sideinfoX, sideinfoY; 470 471 // Size and position of the view. 472 int viewW, viewH, viewX, viewY; 473 474 // Show the variables sidebar? 475 bool showVars; 476 477 // Size and position of the variables sidebar. 478 int sidevarsW, sidevarsH, sidevarsX, sidevarsY; 479 480 /** Update the layout. 481 * 482 * Called at the beginning of a GUI update. 483 * 484 * Params: 485 * 486 * width = Window width. 487 * height = Window height. 488 * showVars = Show the variables sidebar? 489 */ 490 void update(int width, int height, bool showVars) @safe pure nothrow @nogc 491 { 492 // max() avoids impossibly small areas when the window is very small. 493 494 import gfm.math.funcs; 495 496 sidebarW = clamp(cast(int)width.pow(0.75), 20, 192); 497 sidebarH = max(20, (height - 3 * margin) / 2); 498 sidebarY = max(20, margin * 2 + sidebarH); 499 sidebarX = max(20, width - sidebarW - margin); 500 501 sideinfoW = sidebarW; 502 sideinfoH = sidebarH; 503 sideinfoX = sidebarX; 504 sideinfoY = margin; 505 506 this.showVars = showVars; 507 if(showVars) 508 { 509 sidevarsX = sidevarsY = margin; 510 sidevarsW = clamp(cast(int)width.pow(0.8), 20, 448); 511 sidevarsH = max(20, height - 2 * margin); 512 } 513 else 514 { 515 sidevarsH = sidevarsW = sidevarsX = sidevarsY = 0; 516 } 517 518 viewX = margin + sidevarsX + sidevarsW + margin; 519 viewY = margin; 520 viewW = max(20, width - sidebarW - 2 * margin - viewX); 521 viewH = max(20, height - 2 * margin); 522 } 523 } 524 525 526 import gl3n_extra.linalg; 527 /// Simple 2D camera (used for the view area). 528 final class Camera 529 { 530 private: 531 // Pushing/popping camera state can be added when needed since we use MatrixStack 532 import gfmod.opengl.matrixstack; 533 534 // Projection matrix stack. 535 MatrixStack!(float, 4) projectionStack_; 536 537 // 2D extents of the camera. Signed to avoid issues with negative values. 538 long width_, height_; 539 // Center of the camera (the point the camera is looking at in 2D space). 540 vec2 center_; 541 542 public: 543 @safe pure nothrow: 544 /** Construct a Camera with window size. 545 * 546 * Params: 547 * 548 * width = Camera width in pixels. 549 * height = Camera height in pixels. 550 */ 551 this(size_t width, size_t height) 552 { 553 width_ = width; 554 height_ = height; 555 center_ = vec2(0.0f, 0.0f); 556 projectionStack_ = new MatrixStack!(float, 4)(); 557 updateProjection(); 558 } 559 560 @nogc: 561 /// Get the current projection matrix. 562 mat4 projection() const { return projectionStack_.top; } 563 564 /// Set the center of the camera (the point the camera is looking at). 565 void center(const vec2 rhs) 566 { 567 center_ = rhs; 568 updateProjection(); 569 } 570 571 /// Set camera size in pixels. Both width and height must be greater than zero. 572 void size(size_t width, size_t height) 573 { 574 assert(width > 0 && height > 0, "Can't have camera width/height of 0"); 575 width_ = width; 576 height_ = height; 577 updateProjection(); 578 } 579 580 private: 581 /// Update the orthographic projection matrix. 582 void updateProjection() 583 { 584 const hWidth = max(width_ * 0.5f, 1.0f); 585 const hHeight = max(height_ * 0.5f, 1.0f); 586 projectionStack_.loadIdentity(); 587 projectionStack_.ortho(center_.x - hWidth, center_.x + hWidth, 588 center_.y - hHeight, center_.y + hHeight, -8000, 8000); 589 } 590 } 591 592 593 /// Renders the zone graph view. 594 class ViewRenderer 595 { 596 private: 597 // Video device used to access window size, do manual rendering, etc. 598 VideoDevice video_; 599 600 // Main log. 601 Logger log_; 602 603 // GUI layout. 604 Layout layout_; 605 606 607 import gl3n_extra.color; 608 // A simple 2D vertex. 609 struct Vertex 610 { 611 vec2 position; 612 Color color; 613 } 614 615 // Source of the shader used for drawing. 616 enum shaderSrc = 617 q{#version 130 618 #if VERTEX_SHADER 619 uniform mat4 projection; 620 in vec2 position; 621 in vec4 color; 622 smooth out vec4 fsColor; 623 624 void main() 625 { 626 gl_Position = projection * vec4(position, 0.0, 1.0); 627 fsColor = color; 628 } 629 #elif FRAGMENT_SHADER 630 smooth in vec4 fsColor; 631 out vec4 resultColor; 632 633 void main() { resultColor = fsColor; } 634 #endif 635 }; 636 637 import gfmod.opengl; 638 // OpenGL wrapper. 639 OpenGL gl_; 640 641 /* GLSL program used for drawing, compiled from shaderSrc. 642 * 643 * If null (failed to compile), nothing is drawn. 644 */ 645 GLProgram program_; 646 647 // Specification of uniform variables that should be in program_. 648 struct UniformsSpec 649 { 650 mat4 projection; 651 } 652 653 import gfmod.opengl.uniform; 654 // Provides access to uniform variables in program_. 655 GLUniforms!UniformsSpec uniforms_; 656 657 // Triangles in the zone graph (used for zone rectangles). 658 VertexArray!Vertex trisBatch_; 659 // Lines in the zone graph (used for dividing lines between rectangles). 660 VertexArray!Vertex linesBatch_; 661 // View area border. 662 VertexArray!Vertex border_; 663 664 // Base colors for zone rectangles. 665 static immutable Color[16] baseColors = 666 [rgb!"FF0000", rgb!"00FF00", rgb!"6060FF", rgb!"FFFF00", 667 rgb!"FF00FF", rgb!"00FFFF", rgb!"FF8000", rgb!"80FF00", 668 669 rgb!"FF0080", rgb!"8000FF", rgb!"00FF80", rgb!"80FF00", 670 rgb!"800000", rgb!"008000", rgb!"606080", rgb!"808000"]; 671 672 673 // 2D camera used to build the projection matrix. Not used for zoom/panning. 674 Camera camera_; 675 676 // Current state of the ViewRenderer. 677 enum State 678 { 679 NotDrawing, 680 Drawing 681 } 682 683 // Current state of the ViewRenderer. 684 State state_ = State.NotDrawing; 685 686 // Minimum and maximum height of a nesting level (zone + gap above it). 687 enum minNestLevelHeight = 8; enum maxNestLevelHeight = 24; 688 689 // Y offset of zones in currently drawn thread. Increases between drawThreadZones() calls. 690 uint yOffset_; 691 692 // Horizontal panning of the view (passed to startDrawing()). 693 double pan_; 694 // Horizontal zoom of the view (passed to startDrawing()). Higher means closer. 695 double zoom_; 696 697 // Start time of the currently drawn frame (passed to startDrawing()). 698 ulong frameStartTime_; 699 // Duration of the currently drawn frame (passed to startDrawing()). 700 ulong frameDuration_; 701 702 public: 703 /** Construct the view renderer. 704 * 705 * Params: 706 * 707 * video = Video device used for drawing. 708 * log = Main log. 709 * layout = GUI layout used for extents of the view. 710 */ 711 this(VideoDevice video, Logger log, Layout layout) @trusted nothrow 712 { 713 video_ = video; 714 log_ = log; 715 layout_ = layout; 716 camera_ = new Camera(layout_.viewW, layout_.viewH); 717 gl_ = video_.gl; 718 719 // Try to initialize the GL program. On failure, we will set program_ to null and 720 // do nothing in drawing functions. 721 try 722 { 723 program_ = new GLProgram(gl_, shaderSrc); 724 uniforms_ = GLUniforms!UniformsSpec(program_); 725 } 726 catch(OpenGLException e) 727 { 728 log_.error("Failed to construct zone view GLSL program or to load uniforms " 729 "from the program. Zone view will not be drawn.").assumeWontThrow; 730 log_.error(e).assumeWontThrow; 731 program_ = null; 732 } 733 catch(Exception e) 734 { 735 log_.error(e).assumeWontThrow; 736 assert(false, "Unexpected exception in ViewRenderer.this()"); 737 } 738 739 trisBatch_ = new VertexArray!Vertex(gl_, new Vertex[32768]); 740 linesBatch_ = new VertexArray!Vertex(gl_, new Vertex[16384]); 741 border_ = new VertexArray!Vertex(gl_, new Vertex[8]); 742 } 743 744 /// Destroy the view renderer. Must be called manually. 745 ~this() 746 { 747 // Null if program initialization failed. 748 if(program_ !is null) { destroy(program_); } 749 destroy(border_); 750 destroy(linesBatch_); 751 destroy(trisBatch_); 752 } 753 754 /** Start drawing in a new frame. 755 * 756 * Params: 757 * 758 * zoom = Horizontal zoom. 759 * pan = Horizontal panning. 760 * startTime = Start time of the frame in hnsecs. 761 * duration = Duration of the frame in hnsecs. 762 */ 763 void startDrawing(double zoom, double pan, ulong startTime, ulong duration) 764 @trusted nothrow 765 { 766 if(program_ is null) { return; } 767 scope(exit) { gl_.runtimeCheck(); } 768 769 assert(zoom > 0.0, "Zoom must be greater than 0"); 770 assert(state_ == State.NotDrawing, "Called startTime() twice"); 771 state_ = State.Drawing; 772 773 zoom_ = zoom; 774 pan_ = pan; 775 frameStartTime_ = startTime; 776 frameDuration_ = duration; 777 778 const w = video_.width; 779 const h = video_.height; 780 const lx = layout_.viewX; 781 const ly = layout_.viewY; 782 const lw = layout_.viewW; 783 const lh = layout_.viewH; 784 785 camera_.size(cast(uint)w, cast(uint)h); 786 // Align the camera with the view (so 0,0 is the bottom-left corner of the view). 787 camera_.center = vec2(w / 2 - lx, h / 2 - ly); 788 uniforms_.projection = camera_.projection; 789 790 // Cut off parts of draws outside of the view area. 791 glEnable(GL_SCISSOR_TEST); 792 glScissor(lx, ly, lw, lh); 793 794 795 // A thin black border around the view area (mainly to see the view is correct). 796 vec2[8] borderLines = [vec2(1, 0), vec2(1, lh - 1), 797 vec2(1, lh - 1), vec2(lw, lh - 1), 798 vec2(lw, lh - 1), vec2(lw, 0), 799 vec2(lw, 0), vec2(1, 0)]; 800 foreach(v; borderLines) { border_.put(Vertex(v, rgba!"000000FF")); } 801 drawBatch(border_, PrimitiveType.Lines); 802 803 // Reset the y offset for zone draws. 804 yOffset_ = 0; 805 } 806 807 import std.range: isInputRange, ElementType; 808 import tharsis.prof; 809 810 /** Draw zones executed during current frame in one thread. 811 * 812 * Must be called between startDrawing() and endDrawing(). The first drawThreadZones call draws 813 * zones from thread 0, the second from thread 1, etc. 814 * 815 * Params: 816 * 817 * zones = Zone range of all zones executed during the frame in the thread. 818 */ 819 void drawThreadZones(ZRange)(ZRange zones) @safe nothrow 820 if(isInputRange!ZRange && is(ElementType!ZRange == ZoneData)) 821 { 822 assert(state_ == State.Drawing, "drawThreadZones() called without calling startDrawing()"); 823 824 uint maxNestLevel = 0; 825 foreach(zone; zones) 826 { 827 maxNestLevel = max(zone.nestLevel, maxNestLevel); 828 drawZone(zone); 829 } 830 831 yOffset_ += 8 + (maxNestLevel + 1) * nestLevelHeight; 832 } 833 834 /// End drawing a frame. 835 void endDrawing() @trusted nothrow 836 { 837 if(program_ is null) { return; } 838 839 assert(state_ == State.Drawing, "endDrawing() called twice or without startDrawing()"); 840 state_ = State.NotDrawing; 841 scope(exit) { gl_.runtimeCheck(); } 842 843 if(!trisBatch_.empty) { drawBatch(trisBatch_, PrimitiveType.Triangles); } 844 if(!linesBatch_.empty) { drawBatch(linesBatch_, PrimitiveType.Lines); } 845 // Ensure any following draws aren't cut off by the scissor. 846 glDisable(GL_SCISSOR_TEST); 847 } 848 849 private: 850 /// Get the height (in pixels) of a zone nest level (zone height + gap above it). 851 uint nestLevelHeight() @safe pure nothrow const @nogc 852 { 853 return max(minNestLevelHeight, min(maxNestLevelHeight, layout_.viewH / 32)); 854 } 855 856 /** Draw one zone (called by drawThreadZones). 857 * 858 * Params: 859 * 860 * zone = Zone to draw. 861 */ 862 void drawZone(ref const ZoneData zone) @trusted nothrow 863 { 864 const layoutW = layout_.viewW; 865 866 const double frameDurationF = cast(double)frameDuration_; 867 const double relativeStartTime = zone.startTime - frameStartTime_; 868 869 // Zooms X coordinates around the center of the view. 870 double zoomX(double x) @safe nothrow @nogc 871 { 872 const center = layoutW / 2; 873 return center + (pan_ + x - center) * zoom_; 874 } 875 876 const double minXNoZoom = (relativeStartTime / frameDurationF) * layoutW; 877 // Zone rectangle coordinates. Using doubles to avoid some precision issues. 878 const double minX = zoomX(minXNoZoom); 879 const double maxX = zoomX(minXNoZoom + (zone.duration / frameDurationF) * layoutW); 880 const double minY = yOffset_ + (zone.nestLevel - 1) * nestLevelHeight; 881 const double maxY = minY + nestLevelHeight - 2; 882 883 // Don't draw zones that would end up being too small (<=1px) on the screen 884 if(maxX - minX <= 1.0) { return; } 885 886 // Zone rectangle that will be drawn (2 triangles). 887 vec2[6] rect = [vec2(minX, minY), vec2(maxX, maxY), vec2(minX, maxY), 888 vec2(minX, minY), vec2(maxX, minY), vec2(maxX, maxY)]; 889 // Lines on the sides of the rectangle (to separate it from other zones). 890 vec2[4] lines = [rect[0], rect[2], rect[4], rect[1]]; 891 892 // Draw and empty any batches that are out of space. 893 if(trisBatch_.capacity - trisBatch_.length < rect.length) 894 { 895 drawBatch(trisBatch_, PrimitiveType.Triangles); 896 } 897 if(linesBatch_.capacity - linesBatch_.length < lines.length) 898 { 899 drawBatch(linesBatch_, PrimitiveType.Lines); 900 } 901 902 const color = zoneColor(zone); 903 // Darker color for lines 904 const dark = Color(ubyte(color.r / 2), ubyte(color.g / 2), ubyte(color.b / 2), 905 ubyte(255)); 906 907 // Add the triangles/lines to batches. 908 foreach(v; rect) { trisBatch_.put(Vertex(v, color)); } 909 foreach(v; lines) { linesBatch_.put(Vertex(v, dark)); } 910 911 // This is drawn by imgui and is not affected by our camera, so we need to 912 // position this manually. 913 drawZoneText(zone, maxX - minX, 914 layout_.viewX + (minX + maxX) / 2, 915 layout_.viewY + (minY + maxY) / 2 - 4); 916 } 917 918 /** Draw text on a zone rectangle. 919 * 920 * Params: 921 * 922 * zone = Zone being drawn. 923 * zoneWidth = Width of the zone rectangle. 924 * centerX = X center of the zone rectangle. 925 * centerY = Y center of the zone rectangle. 926 */ 927 void drawZoneText(ref const ZoneData zone, double zoneWidth, double centerX, double centerY) 928 @system nothrow 929 { 930 // May change or even be dynamic if we have different font sizes in future. 931 enum charWidthPx = 9; 932 const space = cast(int)(zoneWidth / charWidthPx); 933 // Smallest strings returned by labelText() are 3 chars long or empty. No point 934 // drawing if we can't fit 3 chars. 935 if(space < 3) { return; } 936 937 const label = labelText(space, zone); 938 const long textWidthEst = charWidthPx * label.length; 939 // Don't draw if outside the view area. 940 if(centerX < 0 - textWidthEst || centerX > layout_.viewX + layout_.viewW + textWidthEst) 941 { 942 return; 943 } 944 945 imguiDrawText(cast(int)(centerX), cast(int)(centerY), TextAlign.center, label) 946 .assumeWontThrow; 947 } 948 949 /** Generate text for a zone label. 950 * 951 * Intelligently shortens the text if we can't fit everything into the zone rectangle. 952 * Params: 953 * 954 * space = Number of characters we can fit onto the zone rectangle. 955 * zone = Zone being drawn. 956 */ 957 string labelText(int space, ref const ZoneData zone) @system nothrow 958 { 959 const durMs = zone.duration * 0.0001; 960 const durPc = zone.duration / cast(double)frameDuration_ * 100.0; 961 962 string label; 963 const needed = zone.info.length + ": 0.00ms, 00.00%".length; 964 const info = zone.info; 965 966 return () 967 { 968 // Enough space; show everything. 969 if(space >= needed) { return "%s: %.2fms, %.1f%%".format(info, durMs, durPc); } 970 // Remove spaces and decimal point from percentage time. 971 if(needed - space <= 4) { return "%s:%.2fms|%.0f%%".format(info, durMs, durPc); } 972 // Remove percentage time. 973 if(needed - space <= 7) { return "%s:%.2fms".format(info, durMs); } 974 975 // Shorten the info string while still displaying milliseconds. 976 const infoSpace = max(0, space - cast(int)":0.00ms".length); 977 const infoCut = info[0 .. min(infoSpace, info.length)]; 978 if(infoSpace > 0) { return "%s:%.2fms".format(infoCut, durMs); } 979 980 // Just milliseconds. 981 if(space >= "0.00ms".length) { return "%.2fms".format(durMs); } 982 // Just milliseconds without the 'ms'. 983 if(space >= "0.00".length) { return "%.2f".format(durMs); } 984 // Just milliseconds with only single decimal point precision. 985 if(space >= "0.0".length) { return "%.1f".format(durMs); } 986 // Not enough space for anything. 987 return ""; 988 }().assumeWontThrow; 989 } 990 991 /** Generate the color to draw specified zone with. 992 * 993 * The same zone should always have the same color, even between frames. 994 */ 995 Color zoneColor(ref const ZoneData zone) @safe nothrow 996 { 997 // Using a hash of zone info ensures the same zone always has the same color. 998 union HashBytes 999 { 1000 ulong hash; 1001 ubyte[8] bytes; 1002 } 1003 HashBytes hash; 1004 hash.hash = typeid(zone.info).getHash(&zone.info); 1005 1006 // Get a base color and slightly tweak it based on the hash. 1007 const base = baseColors[hash.bytes[3] % baseColors.length]; 1008 auto mix = (uint i) => cast(ubyte)max(0, base.vector[i] - 32 + (hash.bytes[i] >> 3)); 1009 return Color(mix(0), mix(1), mix(2), ubyte(255)); 1010 } 1011 1012 /// Draw all primitives in a batch and empty the batch. 1013 void drawBatch(VertexArray!Vertex batch, PrimitiveType type) @safe nothrow 1014 { 1015 scope(exit) { gl_.runtimeCheck(); } 1016 1017 program_.use(); 1018 scope(exit) { program_.unuse(); } 1019 1020 batch.lock(); 1021 scope(exit) 1022 { 1023 batch.unlock(); 1024 batch.clear(); 1025 } 1026 1027 if(!batch.bind(program_)) 1028 { 1029 log_.error("Failed to bind VertexArray \"%s\"; probably missing vertex " 1030 " attribute in a GLSL program. Will not draw.").assumeWontThrow; 1031 return; 1032 } 1033 1034 batch.draw(type, 0, batch.length); 1035 batch.release(); 1036 } 1037 }