1 /+ 2 == pixmappresenter == 3 Copyright Elias Batek (0xEAB) 2023 - 2024. 4 Distributed under the Boost Software License, Version 1.0. 5 +/ 6 /++ 7 $(B Pixmap Presenter) is a high-level display library for one specific scenario: 8 Blitting fully-rendered frames to the screen. 9 10 This is useful for software-rendered applications. 11 Think of old-skool games, emulators etc. 12 13 This library builds upon [arsd.simpledisplay] and [arsd.color]. 14 It wraps a [arsd.simpledisplay.SimpleWindow|SimpleWindow] and displays the provided frame data. 15 Each frame is automatically centered on, and optionally scaled to, the carrier window. 16 This processing is done with hardware acceleration (OpenGL). 17 Later versions might add a software-mode. 18 19 Several $(B scaling) modes are supported. 20 Most notably [pixmappresenter.Scaling.contain|contain] that scales pixmaps to the window’s current size 21 while preserving the original aspect ratio. 22 See [Scaling] for details. 23 24 $(PITFALL 25 This module is $(B work in progress). 26 API is subject to changes until further notice. 27 ) 28 29 ## Usage examples 30 31 ### Basic usage 32 33 This example displays a blue frame that increases in color intensity, 34 then jumps back to black and the process repeats. 35 36 --- 37 void main() { 38 // Internal resolution of the images (“frames”) we will render. 39 // From the PixmapPresenter’s perspective, 40 // these are the “fully-rendered frames” that it will blit to screen. 41 // They may be up- & down-scaled to the window’s actual size 42 // (according to the chosen scaling mode) by the presenter. 43 const resolution = Size(240, 120); 44 45 // Let’s create a new presenter. 46 // (For more fine-grained control there’s also a constructor overload that 47 // accepts a [PresenterConfig] instance). 48 auto presenter = new PixmapPresenter( 49 "Demo", // window title 50 resolution, // internal resolution 51 Size(960, 480), // initial window size (optional; default: =resolution) 52 ); 53 54 // This variable will be “shared” across events (and frames). 55 int blueChannel = 0; 56 57 // Run the eventloop. 58 // The callback delegate will get executed every ~16ms (≙ ~60FPS) and schedule a redraw. 59 presenter.eventLoop(16, delegate() { 60 // Update the pixmap (“framebuffer”) here… 61 62 // Construct an RGB color value. 63 auto color = Pixel(0x00, 0x00, blueChannel); 64 // For demo purposes, apply it to the whole pixmap. 65 presenter.framebuffer.clear(color); 66 67 // Increment the amount of blue to be used by the next frame. 68 ++blueChannel; 69 // reset if greater than 0xFF (=ubyte.max) 70 if (blueChannel > 0xFF) 71 blueChannel = 0; 72 }); 73 } 74 --- 75 76 ### Minimal example 77 78 --- 79 void main() { 80 auto pmp = new PixmapPresenter("My Pixmap App", Size(640, 480)); 81 pmp.framebuffer.clear(rgb(0xFF, 0x00, 0x99)); 82 pmp.eventLoop(); 83 } 84 --- 85 86 ### Advanced example 87 88 --- 89 import arsd.pixmappresenter; 90 import arsd.simpledisplay : MouseEvent; 91 92 int main() { 93 // Internal resolution of the images (“frames”) we will render. 94 // For further details, check out the “Basic usage” example. 95 const resolution = Size(240, 120); 96 97 // Configure our presenter in advance. 98 auto cfg = PresenterConfig(); 99 cfg.window.title = "Demo II"; 100 cfg.window.size = Size(960, 480); 101 cfg.renderer.resolution = resolution; 102 cfg.renderer.scaling = Scaling.integer; // integer scaling 103 // → The frame on-screen will 104 // always have a size that is a 105 // multiple of the internal 106 // resolution. 107 // The gentle reader might have noticed that the integer scaling will result 108 // in a padding/border area around the image for most window sizes. 109 // How about changing its color? 110 cfg.renderer.background = ColorF(Pixel.white); 111 112 // Let’s instantiate a new presenter with the previously created config. 113 auto presenter = new PixmapPresenter(cfg); 114 115 // Start with a green frame, so we can easily observe what’s going on. 116 presenter.framebuffer.clear(rgb(0x00, 0xDD, 0x00)); 117 118 int line = 0; 119 ubyte color = 0; 120 byte colorDelta = 2; 121 122 // Run the eventloop. 123 // Note how the callback delegate returns a [LoopCtrl] instance. 124 return presenter.eventLoop(delegate() { 125 // Determine the start and end index of the current line in the 126 // framebuffer. 127 immutable x0 = line * resolution.width; 128 immutable x1 = x0 + resolution.width; 129 130 // Change the color of the current line 131 presenter.framebuffer.data[x0 .. x1] = rgb(color, color, 0xFF); 132 133 // Determine the color to use for the next line 134 // (to be applied on the next update). 135 color += colorDelta; 136 if (color == 0x00) 137 colorDelta = 2; 138 else if (color >= 0xFE) 139 colorDelta = -2; 140 141 // Increment the line counter; reset to 0 once we’ve reached the 142 // end of the framebuffer (=the final/last line). 143 ++line; 144 if (line == resolution.height) 145 line = 0; 146 147 // Schedule a redraw in ~16ms. 148 return LoopCtrl.redrawIn(16); 149 }, delegate(MouseEvent ev) { 150 // toggle fullscreen mode on double-click 151 if (ev.doubleClick) { 152 presenter.toggleFullscreen(); 153 } 154 }); 155 } 156 --- 157 +/ 158 module arsd.pixmappresenter; 159 160 import arsd.color; 161 import arsd.simpledisplay; 162 163 /* 164 ## TODO 165 166 - More comprehensive documentation 167 - Additional renderer implementations: 168 - a `ScreenPainter`-based renderer 169 - a legacy OpenGL renderer (maybe) 170 - Is there something in arsd that serves a similar purpose to `Pixmap`? 171 - Can we convert to/from it? 172 - Minimum window size 173 - to ensure `Scaling.integer` doesn’t break “unexpectedly” 174 - More control over timing 175 - that’s a simpledisplay thing, though 176 */ 177 178 /// 179 alias Pixel = Color; 180 181 /// 182 alias ColorF = arsd.color.ColorF; 183 184 /// 185 alias Size = arsd.color.Size; 186 187 /// 188 alias Point = arsd.color.Point; 189 190 /// 191 alias WindowResizedCallback = void delegate(Size); 192 193 // verify assumption(s) 194 static assert(Pixel.sizeof == uint.sizeof); 195 196 // is the Timer class available on this platform? 197 private enum hasTimer = is(Timer == class); 198 199 /// casts value `v` to type `T` 200 auto ref T typeCast(T, S)(auto ref S v) { 201 return cast(T) v; 202 } 203 204 @safe pure nothrow @nogc { 205 /// 206 Pixel rgba(ubyte r, ubyte g, ubyte b, ubyte a = 0xFF) { 207 return Pixel(r, g, b, a); 208 } 209 210 /// 211 Pixel rgb(ubyte r, ubyte g, ubyte b) { 212 return rgba(r, g, b, 0xFF); 213 } 214 } 215 216 /++ 217 Pixel data container 218 +/ 219 struct Pixmap { 220 221 /// Pixel data 222 Pixel[] data; 223 224 /// Pixel per row 225 int width; 226 227 @safe pure nothrow: 228 229 /// 230 this(Size size) { 231 this.size = size; 232 } 233 234 /// 235 this(Pixel[] data, int width) @nogc 236 in (data.length % width == 0) { 237 this.data = data; 238 this.width = width; 239 } 240 241 // undocumented: really shouldn’t be used. 242 // carries the risks of `length` and `width` getting out of sync accidentally. 243 deprecated("Use `size` instead.") 244 void length(int value) { 245 data.length = value; 246 } 247 248 /++ 249 Changes the size of the buffer 250 251 Reallocates the underlying pixel array. 252 +/ 253 void size(Size value) { 254 data.length = value.area; 255 width = value.width; 256 } 257 258 /// ditto 259 void size(int totalPixels, int width) 260 in (totalPixels % width == 0) { 261 data.length = totalPixels; 262 this.width = width; 263 } 264 265 @safe pure nothrow @nogc: 266 267 /// Height of the buffer, i.e. the number of lines 268 int height() inout { 269 if (width == 0) { 270 return 0; 271 } 272 273 return typeCast!int(data.length / width); 274 } 275 276 /// Rectangular size of the buffer 277 Size size() inout { 278 return Size(width, height); 279 } 280 281 /// Length of the buffer, i.e. the number of pixels 282 int length() inout { 283 return typeCast!int(data.length); 284 } 285 286 /++ 287 Number of bytes per line 288 289 Returns: 290 width × Pixel.sizeof 291 +/ 292 int pitch() inout { 293 return (width * int(Pixel.sizeof)); 294 } 295 296 /// Clears the buffer’s contents (by setting each pixel to the same color) 297 void clear(Pixel value) { 298 data[] = value; 299 } 300 } 301 302 // viewport math 303 private @safe pure nothrow @nogc { 304 305 // keep aspect ratio (contain) 306 bool karContainNeedsDownscaling(const Size drawing, const Size canvas) { 307 return (drawing.width > canvas.width) 308 || (drawing.height > canvas.height); 309 } 310 311 // keep aspect ratio (contain) 312 int karContainScalingFactorInt(const Size drawing, const Size canvas) { 313 const int w = canvas.width / drawing.width; 314 const int h = canvas.height / drawing.height; 315 316 return (w < h) ? w : h; 317 } 318 319 // keep aspect ratio (contain; FP variant) 320 float karContainScalingFactorF(const Size drawing, const Size canvas) { 321 const w = float(canvas.width) / float(drawing.width); 322 const h = float(canvas.height) / float(drawing.height); 323 324 return (w < h) ? w : h; 325 } 326 327 // keep aspect ratio (cover) 328 float karCoverScalingFactorF(const Size drawing, const Size canvas) { 329 const w = float(canvas.width) / float(drawing.width); 330 const h = float(canvas.height) / float(drawing.height); 331 332 return (w > h) ? w : h; 333 } 334 335 Size deltaPerimeter(const Size a, const Size b) { 336 return Size( 337 a.width - b.width, 338 a.height - b.height, 339 ); 340 } 341 342 Point offsetCenter(const Size drawing, const Size canvas) { 343 auto delta = canvas.deltaPerimeter(drawing); 344 return (typeCast!Point(delta) >> 1); 345 } 346 } 347 348 /// 349 struct Viewport { 350 Size size; /// 351 Point offset; /// 352 } 353 354 /++ 355 Calls `glViewport` with the data from the provided [Viewport]. 356 +/ 357 void glViewportPMP(const ref Viewport vp) { 358 glViewport(vp.offset.x, vp.offset.y, vp.size.width, vp.size.height); 359 } 360 361 /++ 362 Calculates the dimensions and position of the viewport for the provided config. 363 364 $(TIP 365 Primary use case for this is [PixmapRenderer] implementations. 366 ) 367 +/ 368 Viewport calculateViewport(const ref PresenterConfig config) @safe pure nothrow @nogc { 369 Size size; 370 371 final switch (config.renderer.scaling) { 372 373 case Scaling.none: 374 size = config.renderer.resolution; 375 break; 376 377 case Scaling.stretch: 378 size = config.window.size; 379 break; 380 381 case Scaling.contain: 382 const float scaleF = karContainScalingFactorF(config.renderer.resolution, config.window.size); 383 size = Size( 384 typeCast!int(scaleF * config.renderer.resolution.width), 385 typeCast!int(scaleF * config.renderer.resolution.height), 386 ); 387 break; 388 389 case Scaling.integer: 390 const int scaleI = karContainScalingFactorInt(config.renderer.resolution, config.window.size); 391 size = (config.renderer.resolution * scaleI); 392 break; 393 394 case Scaling.intHybrid: 395 if (karContainNeedsDownscaling(config.renderer.resolution, config.window.size)) { 396 goto case Scaling.contain; 397 } 398 goto case Scaling.integer; 399 400 case Scaling.cover: 401 const float fillF = karCoverScalingFactorF(config.renderer.resolution, config.window.size); 402 size = Size( 403 typeCast!int(fillF * config.renderer.resolution.width), 404 typeCast!int(fillF * config.renderer.resolution.height), 405 ); 406 break; 407 } 408 409 const Point offset = offsetCenter(size, config.window.size); 410 411 return Viewport(size, offset); 412 } 413 414 /++ 415 Scaling/Fit Modes 416 417 Each scaling modes has unique behavior for different window-size to pixmap-size ratios. 418 419 $(NOTE 420 Unfortunately, there are no universally applicable naming conventions for these modes. 421 In fact, different implementations tend to contradict each other. 422 ) 423 424 $(SMALL_TABLE 425 Mode feature matrix 426 Mode | Aspect Ratio | Pixel Ratio | Cropping | Border | Comment(s) 427 `none` | preserved | preserved | yes | 4 | Crops if the `window.size < pixmap.size`. 428 `stretch` | no | no | no | none | 429 `contain` | preserved | no | no | 2 | Letterboxing/Pillarboxing 430 `integer` | preserved | preserved | no | 4 | Works only if `window.size >= pixmap.size`. 431 `intHybrid` | preserved | when up | no | 4 or 2 | Hybrid: int upscaling, decimal downscaling 432 `cover` | preserved | no | yes | none | 433 ) 434 435 $(NOTE 436 Integer scaling – Note that the resulting integer ratio of a window smaller than a pixmap is `0`. 437 438 Use `intHybrid` to prevent the pixmap from disappearing on disproportionately small window sizes. 439 It uses $(I integer)-mode for upscaling and the regular $(I contain)-mode for downscaling. 440 ) 441 442 $(SMALL_TABLE 443 Feature | Definition 444 Aspect Ratio | Whether the original aspect ratio (width ÷ height) of the input frame is preserved 445 Pixel Ratio | Whether the orignal pixel ratio (= square) is preserved 446 Cropping | Whether the outer areas of the input frame might get cut off 447 Border | The number of padding-areas/borders that can potentially appear around the frame 448 ) 449 450 For your convience, aliases matching the [`object-fit`](https://developer.mozilla.org/en-US/docs/Web/CSS/object-fit) 451 CSS property are provided, too. These are prefixed with `css`. 452 Currently there is no equivalent for `scale-down` as it does not appear to be particularly useful here. 453 +/ 454 enum Scaling { 455 none = 0, /// 456 stretch, /// 457 contain, /// 458 integer, /// 459 intHybrid, /// 460 cover, /// 461 462 // aliases 463 center = none, /// 464 keepAspectRatio = contain, /// 465 466 // CSS `object-fit` style aliases 467 cssNone = none, /// equivalent CSS: `object-fit: none;` 468 cssContain = contain, /// equivalent CSS: `object-fit: contain;` 469 cssFill = stretch, /// equivalent CSS: `object-fit: fill;` 470 cssCover = cover, /// equivalent CSS: `object-fit: cover;` 471 } 472 473 /// 474 enum ScalingFilter { 475 nearest, /// nearest neighbor → blocky/pixel’ish 476 linear, /// (bi-)linear interpolation → smooth/blurry 477 } 478 479 /// 480 struct PresenterConfig { 481 Window window; /// 482 Renderer renderer; /// 483 484 /// 485 static struct Renderer { 486 /++ 487 Internal resolution 488 +/ 489 Size resolution; 490 491 /++ 492 Scaling method 493 to apply when `window.size` != `resolution` 494 +/ 495 Scaling scaling = Scaling.keepAspectRatio; 496 497 /++ 498 Filter 499 +/ 500 ScalingFilter filter = ScalingFilter.nearest; 501 502 /++ 503 Background color 504 +/ 505 ColorF background = ColorF(0.0f, 0.0f, 0.0f, 1.0f); 506 507 /// 508 void setPixelPerfect() { 509 scaling = Scaling.integer; 510 filter = ScalingFilter.nearest; 511 } 512 } 513 514 /// 515 static struct Window { 516 string title = "ARSD Pixmap Presenter"; 517 Size size; 518 } 519 } 520 521 // undocumented 522 struct PresenterObjectsContainer { 523 Pixmap framebuffer; 524 SimpleWindow window; 525 PresenterConfig config; 526 } 527 528 /// 529 struct WantsOpenGl { 530 ubyte vMaj; /// Major version 531 ubyte vMin; /// Minor version 532 bool compat; /// Compatibility profile? → true = Compatibility Profile; false = Core Profile 533 534 @safe pure nothrow @nogc: 535 536 /// Is OpenGL wanted? 537 bool wanted() const { 538 return vMaj > 0; 539 } 540 } 541 542 /++ 543 Renderer abstraction 544 545 A renderer scales, centers and blits pixmaps to screen. 546 +/ 547 interface PixmapRenderer { 548 /++ 549 Does this renderer use OpenGL? 550 551 Returns: 552 Whether the renderer requires an OpenGL-enabled window 553 and which version is expected. 554 +/ 555 public WantsOpenGl wantsOpenGl() @safe pure nothrow @nogc; 556 557 /++ 558 Setup function 559 560 Called once during setup. 561 Perform initialization tasks in here. 562 563 $(NOTE 564 The final thing a setup function does 565 is usually to call `reconfigure()` on the renderer. 566 ) 567 568 Params: 569 container = Pointer to the [PresenterObjectsContainer] of the presenter. To be stored for later use. 570 +/ 571 public void setup(PresenterObjectsContainer* container); 572 573 /++ 574 Reconfigures the renderer 575 576 Called upon configuration changes. 577 The new config can be found in the [PresenterObjectsContainer] received during `setup()`. 578 +/ 579 public void reconfigure(); 580 581 /++ 582 Schedules a redraw 583 +/ 584 public void redrawSchedule(); 585 586 /++ 587 Triggers a redraw 588 +/ 589 public void redrawNow(); 590 } 591 592 /++ 593 OpenGL 3.0 implementation of a [PixmapRenderer] 594 +/ 595 final class OpenGl3PixmapRenderer : PixmapRenderer { 596 597 private { 598 PresenterObjectsContainer* _poc; 599 600 bool _clear = true; 601 602 GLfloat[16] _vertices; 603 OpenGlShader _shader; 604 GLuint _vao; 605 GLuint _vbo; 606 GLuint _ebo; 607 GLuint _texture = 0; 608 } 609 610 /// 611 public this() { 612 } 613 614 public WantsOpenGl wantsOpenGl() @safe pure nothrow @nogc { 615 return WantsOpenGl(3, 0, false); 616 } 617 618 public void setup(PresenterObjectsContainer* pro) { 619 _poc = pro; 620 _poc.window.visibleForTheFirstTime = &this.visibleForTheFirstTime; 621 _poc.window.redrawOpenGlScene = &this.redrawOpenGlScene; 622 } 623 624 private { 625 void visibleForTheFirstTime() { 626 _poc.window.setAsCurrentOpenGlContext(); 627 gl3.loadDynamicLibrary(); 628 629 this.compileLinkShader(); 630 this.setupVertexObjects(); 631 632 this.reconfigure(); 633 } 634 635 void redrawOpenGlScene() { 636 if (_clear) { 637 glClearColor( 638 _poc.config.renderer.background.r, 639 _poc.config.renderer.background.g, 640 _poc.config.renderer.background.b, 641 _poc.config.renderer.background.a 642 ); 643 glClear(GL_COLOR_BUFFER_BIT); 644 _clear = false; 645 } 646 647 glActiveTexture(GL_TEXTURE0); 648 glBindTexture(GL_TEXTURE_2D, _texture); 649 glTexSubImage2D( 650 GL_TEXTURE_2D, 651 0, 652 0, 0, 653 _poc.config.renderer.resolution.width, _poc.config.renderer.resolution.height, 654 GL_RGBA, GL_UNSIGNED_BYTE, 655 typeCast!(void*)(_poc.framebuffer.data.ptr) 656 ); 657 658 glUseProgram(_shader.shaderProgram); 659 glBindVertexArray(_vao); 660 glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, null); 661 } 662 } 663 664 private { 665 void compileLinkShader() { 666 _shader = new OpenGlShader( 667 OpenGlShader.Source(GL_VERTEX_SHADER, ` 668 #version 330 core 669 layout (location = 0) in vec2 aPos; 670 layout (location = 1) in vec2 aTexCoord; 671 672 out vec2 TexCoord; 673 674 void main() { 675 gl_Position = vec4(aPos.x, aPos.y, 0.0, 1.0); 676 TexCoord = aTexCoord; 677 } 678 `), 679 OpenGlShader.Source(GL_FRAGMENT_SHADER, ` 680 #version 330 core 681 out vec4 FragColor; 682 683 in vec2 TexCoord; 684 685 uniform sampler2D sampler; 686 687 void main() { 688 FragColor = texture(sampler, TexCoord); 689 } 690 `), 691 ); 692 } 693 694 void setupVertexObjects() { 695 glGenVertexArrays(1, &_vao); 696 glBindVertexArray(_vao); 697 698 glGenBuffers(1, &_vbo); 699 glGenBuffers(1, &_ebo); 700 701 glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, _ebo); 702 glBufferDataSlice(GL_ELEMENT_ARRAY_BUFFER, indices, GL_STATIC_DRAW); 703 704 glBindBuffer(GL_ARRAY_BUFFER, _vbo); 705 glBufferDataSlice(GL_ARRAY_BUFFER, vertices, GL_STATIC_DRAW); 706 707 glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 4 * GLfloat.sizeof, null); 708 glEnableVertexAttribArray(0); 709 710 glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 4 * GLfloat.sizeof, typeCast!(void*)(2 * GLfloat.sizeof)); 711 glEnableVertexAttribArray(1); 712 } 713 714 void setupTexture() { 715 if (_texture == 0) { 716 glGenTextures(1, &_texture); 717 } 718 719 glBindTexture(GL_TEXTURE_2D, _texture); 720 721 final switch (_poc.config.renderer.filter) with (ScalingFilter) { 722 case nearest: 723 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); 724 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); 725 break; 726 case linear: 727 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); 728 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); 729 break; 730 } 731 732 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); 733 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); 734 glTexImage2D( 735 GL_TEXTURE_2D, 736 0, 737 GL_RGBA8, 738 _poc.config.renderer.resolution.width, _poc.config.renderer.resolution.height, 739 0, 740 GL_RGBA, GL_UNSIGNED_BYTE, 741 null 742 ); 743 744 glBindTexture(GL_TEXTURE_2D, 0); 745 } 746 } 747 748 public void reconfigure() { 749 const Viewport viewport = calculateViewport(_poc.config); 750 glViewportPMP(viewport); 751 752 this.setupTexture(); 753 _clear = true; 754 } 755 756 void redrawSchedule() { 757 _poc.window.redrawOpenGlSceneSoon(); 758 } 759 760 void redrawNow() { 761 _poc.window.redrawOpenGlSceneNow(); 762 } 763 764 private { 765 static immutable GLfloat[] vertices = [ 766 //dfmt off 767 // positions // texture coordinates 768 1.0f, 1.0f, 1.0f, 0.0f, 769 1.0f, -1.0f, 1.0f, 1.0f, 770 -1.0f, -1.0f, 0.0f, 1.0f, 771 -1.0f, 1.0f, 0.0f, 0.0f, 772 //dfmt on 773 ]; 774 775 static immutable GLuint[] indices = [ 776 //dfmt off 777 0, 1, 3, 778 1, 2, 3, 779 //dfmt on 780 ]; 781 } 782 } 783 784 /++ 785 Legacy OpenGL (1.x) renderer implementation 786 787 Uses what is often called the $(I Fixed Function Pipeline). 788 +/ 789 final class OpenGl1PixmapRenderer : PixmapRenderer { 790 791 private { 792 PresenterObjectsContainer* _poc; 793 bool _clear = true; 794 795 GLuint _texture = 0; 796 } 797 798 public @safe pure nothrow @nogc { 799 /// 800 this() { 801 } 802 803 WantsOpenGl wantsOpenGl() pure nothrow @nogc @safe { 804 return WantsOpenGl(1, 1, true); 805 } 806 807 } 808 809 public void setup(PresenterObjectsContainer* poc) { 810 _poc = poc; 811 _poc.window.visibleForTheFirstTime = &this.visibleForTheFirstTime; 812 _poc.window.redrawOpenGlScene = &this.redrawOpenGlScene; 813 } 814 815 private { 816 817 void visibleForTheFirstTime() { 818 //_poc.window.setAsCurrentOpenGlContext(); 819 // ↑-- reconfigure() does this, too. 820 // |-- Uncomment if this functions does something else in the future. 821 822 this.reconfigure(); 823 } 824 825 void setupTexture() { 826 if (_texture == 0) { 827 glGenTextures(1, &_texture); 828 } 829 830 glBindTexture(GL_TEXTURE_2D, _texture); 831 832 final switch (_poc.config.renderer.filter) with (ScalingFilter) { 833 case nearest: 834 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); 835 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); 836 break; 837 case linear: 838 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); 839 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); 840 break; 841 } 842 843 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); 844 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); 845 glTexImage2D( 846 GL_TEXTURE_2D, 847 0, 848 GL_RGBA8, 849 _poc.config.renderer.resolution.width, 850 _poc.config.renderer.resolution.height, 851 0, 852 GL_RGBA, GL_UNSIGNED_BYTE, 853 null 854 ); 855 856 glBindTexture(GL_TEXTURE_2D, 0); 857 } 858 859 void setupMatrix() { 860 glMatrixMode(GL_PROJECTION); 861 glLoadIdentity(); 862 glOrtho( 863 0, _poc.config.renderer.resolution.width, 864 _poc.config.renderer.resolution.height, 0, 865 -1, 1 866 ); 867 //glMatrixMode(GL_MODELVIEW); 868 } 869 870 void redrawOpenGlScene() { 871 if (_clear) { 872 glClearColor( 873 _poc.config.renderer.background.r, 874 _poc.config.renderer.background.g, 875 _poc.config.renderer.background.b, 876 _poc.config.renderer.background.a, 877 ); 878 glClear(GL_COLOR_BUFFER_BIT); 879 _clear = false; 880 } 881 882 glBindTexture(GL_TEXTURE_2D, _texture); 883 glEnable(GL_TEXTURE_2D); 884 { 885 glTexSubImage2D( 886 GL_TEXTURE_2D, 887 0, 888 0, 0, 889 _poc.config.renderer.resolution.width, _poc.config.renderer.resolution.height, 890 GL_RGBA, GL_UNSIGNED_BYTE, 891 typeCast!(void*)(_poc.framebuffer.data.ptr) 892 ); 893 894 glBegin(GL_QUADS); 895 { 896 glTexCoord2f(0, 0); 897 glVertex2i(0, 0); 898 899 glTexCoord2f(0, 1); 900 glVertex2i(0, _poc.config.renderer.resolution.height); 901 902 glTexCoord2f(1, 1); 903 glVertex2i(_poc.config.renderer.resolution.width, _poc.config.renderer.resolution.height); 904 905 glTexCoord2f(1, 0); 906 glVertex2i(_poc.config.renderer.resolution.width, 0); 907 } 908 glEnd(); 909 } 910 glDisable(GL_TEXTURE_2D); 911 glBindTexture(GL_TEXTURE_2D, 0); 912 } 913 } 914 915 public void reconfigure() { 916 _poc.window.setAsCurrentOpenGlContext(); 917 918 const Viewport viewport = calculateViewport(_poc.config); 919 glViewportPMP(viewport); 920 921 this.setupTexture(); 922 this.setupMatrix(); 923 924 _clear = true; 925 } 926 927 public void redrawSchedule() { 928 _poc.window.redrawOpenGlSceneSoon(); 929 } 930 931 public void redrawNow() { 932 _poc.window.redrawOpenGlSceneNow(); 933 } 934 } 935 936 /// 937 struct LoopCtrl { 938 int interval; /// in milliseconds 939 bool redraw; /// 940 941 /// 942 @disable this(); 943 944 @safe pure nothrow @nogc: 945 946 private this(int interval, bool redraw) { 947 this.interval = interval; 948 this.redraw = redraw; 949 } 950 951 /// 952 static LoopCtrl waitFor(int intervalMS) { 953 return LoopCtrl(intervalMS, false); 954 } 955 956 /// 957 static LoopCtrl redrawIn(int intervalMS) { 958 return LoopCtrl(intervalMS, true); 959 } 960 } 961 962 /++ 963 Pixmap Presenter window 964 965 A high-level window class that displays fully-rendered frames in the form of [Pixmap|Pixmaps]. 966 The pixmap will be centered and (optionally) scaled. 967 +/ 968 final class PixmapPresenter { 969 970 private { 971 PresenterObjectsContainer* _poc; 972 PixmapRenderer _renderer; 973 974 static if (hasTimer) { 975 Timer _timer; 976 } 977 978 WindowResizedCallback _onWindowResize; 979 } 980 981 // ctors 982 public { 983 984 /// 985 this(const PresenterConfig config, bool useOpenGl = true) { 986 if (useOpenGl) { 987 this(config, new OpenGl3PixmapRenderer()); 988 } else { 989 assert(false, "Not implemented"); 990 } 991 } 992 993 /// 994 this(const PresenterConfig config, PixmapRenderer renderer) { 995 _renderer = renderer; 996 997 // create software framebuffer 998 auto framebuffer = Pixmap(config.renderer.resolution); 999 1000 // OpenGL? 1001 auto openGlOptions = OpenGlOptions.no; 1002 const openGl = _renderer.wantsOpenGl; 1003 if (openGl.wanted) { 1004 setOpenGLContextVersion(openGl.vMaj, openGl.vMin); 1005 openGLContextCompatible = openGl.compat; 1006 1007 openGlOptions = OpenGlOptions.yes; 1008 } 1009 1010 // spawn window 1011 auto window = new SimpleWindow( 1012 config.window.size, 1013 config.window.title, 1014 openGlOptions, 1015 Resizability.allowResizing, 1016 ); 1017 1018 window.windowResized = &this.windowResized; 1019 1020 // alloc objects 1021 _poc = new PresenterObjectsContainer( 1022 framebuffer, 1023 window, 1024 config, 1025 ); 1026 1027 _renderer.setup(_poc); 1028 } 1029 } 1030 1031 // additional convenience ctors 1032 public { 1033 1034 /// 1035 this( 1036 string title, 1037 const Size resolution, 1038 const Size initialWindowSize, 1039 Scaling scaling = Scaling.contain, 1040 ScalingFilter filter = ScalingFilter.nearest, 1041 ) { 1042 auto cfg = PresenterConfig(); 1043 1044 cfg.window.title = title; 1045 cfg.renderer.resolution = resolution; 1046 cfg.window.size = initialWindowSize; 1047 cfg.renderer.scaling = scaling; 1048 cfg.renderer.filter = filter; 1049 1050 this(cfg); 1051 } 1052 1053 /// 1054 this( 1055 string title, 1056 const Size resolution, 1057 Scaling scaling = Scaling.contain, 1058 ScalingFilter filter = ScalingFilter.nearest, 1059 ) { 1060 this(title, resolution, resolution, scaling, filter,); 1061 } 1062 } 1063 1064 // public functions 1065 public { 1066 1067 /++ 1068 Runs the event loop (with a pulse timer) 1069 1070 A redraw will be scheduled automatically each pulse. 1071 +/ 1072 int eventLoop(T...)(long pulseTimeout, void delegate() onPulse, T eventHandlers) { 1073 // run event-loop with pulse timer 1074 return _poc.window.eventLoop( 1075 pulseTimeout, 1076 delegate() { onPulse(); this.scheduleRedraw(); }, 1077 eventHandlers, 1078 ); 1079 } 1080 1081 //dfmt off 1082 /++ 1083 Runs the event loop 1084 1085 Redraws have to manually scheduled through [scheduleRedraw] when using this overload. 1086 +/ 1087 int eventLoop(T...)(T eventHandlers) if ( 1088 (T.length == 0) || (is(T[0] == delegate) && !is(typeof(() { return T[0](); }()) == LoopCtrl)) 1089 ) { 1090 return _poc.window.eventLoop(eventHandlers); 1091 } 1092 //dfmt on 1093 1094 static if (hasTimer) { 1095 /++ 1096 Runs the event loop 1097 with [LoopCtrl] timing mechanism 1098 +/ 1099 int eventLoop(T...)(LoopCtrl delegate() callback, T eventHandlers) { 1100 if (callback !is null) { 1101 LoopCtrl prev = LoopCtrl(1, true); 1102 1103 _timer = new Timer(prev.interval, delegate() { 1104 // redraw if requested by previous ctrl message 1105 if (prev.redraw) { 1106 _renderer.redrawNow(); 1107 prev.redraw = false; // done 1108 } 1109 1110 // execute callback 1111 const LoopCtrl ctrl = callback(); 1112 1113 // different than previous ctrl message? 1114 if (ctrl.interval != prev.interval) { 1115 // update timer 1116 _timer.changeTime(ctrl.interval); 1117 } 1118 1119 // save ctrl message 1120 prev = ctrl; 1121 }); 1122 } 1123 1124 // run event-loop 1125 return _poc.window.eventLoop(0, eventHandlers); 1126 } 1127 } 1128 1129 /++ 1130 The [Pixmap] to be presented. 1131 1132 Use this to “draw” on screen. 1133 +/ 1134 Pixmap pixmap() @safe pure nothrow @nogc { 1135 return _poc.framebuffer; 1136 } 1137 1138 /// ditto 1139 alias framebuffer = pixmap; 1140 1141 /++ 1142 Updates the configuration of the presenter. 1143 1144 Params: 1145 resizeWindow = if false, `config.window.size` will be ignored. 1146 +/ 1147 void reconfigure(PresenterConfig config, const bool resizeWindow = false) { 1148 // override requested window-size to current size if no resize requested 1149 if (!resizeWindow) { 1150 config.window.size = _poc.config.window.size; 1151 } 1152 1153 this.reconfigureImpl(config); 1154 } 1155 1156 private void reconfigureImpl(const ref PresenterConfig config) { 1157 _poc.window.title = config.window.title; 1158 1159 if (config.renderer.resolution != _poc.config.renderer.resolution) { 1160 _poc.framebuffer.size = config.renderer.resolution; 1161 } 1162 1163 immutable resize = (config.window.size != _poc.config.window.size); 1164 1165 // update stored configuration 1166 _poc.config = config; 1167 1168 if (resize) { 1169 _poc.window.resize(config.window.size.width, config.window.size.height); 1170 // resize-handler will call `_renderer.reconfigure()` 1171 } else { 1172 _renderer.reconfigure(); 1173 } 1174 } 1175 1176 /++ 1177 Schedules a redraw 1178 +/ 1179 void scheduleRedraw() { 1180 _renderer.redrawSchedule(); 1181 } 1182 1183 /++ 1184 Fullscreen mode 1185 +/ 1186 bool isFullscreen() { 1187 return _poc.window.fullscreen; 1188 } 1189 1190 /// ditto 1191 void isFullscreen(bool enabled) { 1192 _poc.window.fullscreen = enabled; 1193 } 1194 1195 /++ 1196 Toggles the fullscreen state of the window. 1197 1198 Turns a non-fullscreen window into fullscreen mode. 1199 Exits fullscreen mode for fullscreen-windows. 1200 +/ 1201 void toggleFullscreen() { 1202 this.isFullscreen = !this.isFullscreen; 1203 } 1204 1205 /++ 1206 Returns the underlying [arsd.simpledisplay.SimpleWindow|SimpleWindow] 1207 1208 $(WARNING 1209 This is unsupported; use at your own risk. 1210 1211 Tinkering with the window directly can break all sort of things 1212 that a presenter or renderer could possibly have set up. 1213 ) 1214 +/ 1215 SimpleWindow tinkerWindow() @safe pure nothrow @nogc { 1216 return _poc.window; 1217 } 1218 1219 /++ 1220 Returns the underlying [PixmapRenderer] 1221 1222 $(TIP 1223 Type-cast the returned reference to the actual implementation type for further use. 1224 ) 1225 1226 $(WARNING 1227 This is quasi unsupported; use at your own risk. 1228 1229 Using the result of this function is pratictically no different than 1230 using a reference to the renderer further on after passing it the presenter’s constructor. 1231 It can’t be prohibited but it resembles a footgun. 1232 ) 1233 +/ 1234 PixmapRenderer tinkerRenderer() @safe pure nothrow @nogc { 1235 return _renderer; 1236 } 1237 } 1238 1239 // event (handler) properties 1240 public @safe pure nothrow @nogc { 1241 1242 /++ 1243 Event handler: window resize 1244 +/ 1245 void onWindowResize(WindowResizedCallback value) { 1246 _onWindowResize = value; 1247 } 1248 } 1249 1250 // event handlers 1251 private { 1252 void windowResized(int width, int height) { 1253 const newSize = Size(width, height); 1254 1255 _poc.config.window.size = newSize; 1256 _renderer.reconfigure(); 1257 // ↑ In case this call gets removed, update `reconfigure()`. 1258 // Current implementation takes advantage of the `_renderer.reconfigure()` call here. 1259 1260 if (_onWindowResize !is null) { 1261 _onWindowResize(newSize); 1262 } 1263 } 1264 } 1265 }