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