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) @safe 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 }