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