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 enum ScalingFilter { 377 nearest, /// nearest neighbor → blocky/pixel’ish 378 linear, /// (bi-)linear interpolation → smooth/blurry 379 } 380 381 /// 382 struct PresenterConfig { 383 Window window; /// 384 Renderer renderer; /// 385 386 /// 387 static struct Renderer { 388 /++ 389 Internal resolution 390 +/ 391 Size resolution; 392 393 /++ 394 Scaling method 395 to apply when `window.size` != `resolution` 396 +/ 397 Scaling scaling = Scaling.keepAspectRatio; 398 399 /++ 400 Filter 401 +/ 402 ScalingFilter filter = ScalingFilter.nearest; 403 404 /++ 405 Background color 406 +/ 407 ColorF background = ColorF(0.0f, 0.0f, 0.0f, 1.0f); 408 409 /// 410 void setPixelPerfect() { 411 scaling = Scaling.integer; 412 filter = ScalingFilter.nearest; 413 } 414 } 415 416 /// 417 static struct Window { 418 /// 419 string title = "ARSD Pixmap Presenter"; 420 421 /// 422 Size size; 423 424 /++ 425 Window corner style 426 427 $(NOTE 428 At the time of writing, this is only implemented on Windows. 429 It has no effect elsewhere for now but does no harm either. 430 431 Windows: Requires Windows 11 or later. 432 ) 433 434 History: 435 Added September 10, 2024. 436 +/ 437 CornerStyle corners = CornerStyle.rectangular; 438 } 439 } 440 441 // undocumented 442 struct PresenterObjectsContainer { 443 Pixmap framebuffer; 444 SimpleWindow window; 445 PresenterConfig config; 446 } 447 448 /// 449 struct WantsOpenGl { 450 ubyte vMaj; /// Major version 451 ubyte vMin; /// Minor version 452 bool compat; /// Compatibility profile? → true = Compatibility Profile; false = Core Profile 453 454 @safe pure nothrow @nogc: 455 456 /// Is OpenGL wanted? 457 bool wanted() const { 458 return vMaj > 0; 459 } 460 } 461 462 /++ 463 Renderer abstraction 464 465 A renderer scales, centers and blits pixmaps to screen. 466 +/ 467 interface PixmapRenderer { 468 /++ 469 Does this renderer use OpenGL? 470 471 Returns: 472 Whether the renderer requires an OpenGL-enabled window 473 and which version is expected. 474 +/ 475 public WantsOpenGl wantsOpenGl() @safe pure nothrow @nogc; 476 477 /++ 478 Setup function 479 480 Called once during setup. 481 Perform initialization tasks in here. 482 483 $(NOTE 484 The final thing a setup function does 485 is usually to call `reconfigure()` on the renderer. 486 ) 487 488 Params: 489 container = Pointer to the [PresenterObjectsContainer] of the presenter. To be stored for later use. 490 +/ 491 public void setup(PresenterObjectsContainer* container); 492 493 /++ 494 Reconfigures the renderer 495 496 Called upon configuration changes. 497 The new config can be found in the [PresenterObjectsContainer] received during `setup()`. 498 +/ 499 public void reconfigure(); 500 501 /++ 502 Schedules a redraw 503 +/ 504 public void redrawSchedule(); 505 506 /++ 507 Triggers a redraw 508 +/ 509 public void redrawNow(); 510 } 511 512 /++ 513 OpenGL 3.0 implementation of a [PixmapRenderer] 514 +/ 515 final class OpenGl3PixmapRenderer : PixmapRenderer { 516 517 private { 518 PresenterObjectsContainer* _poc; 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 glClearColor( 556 _poc.config.renderer.background.r, 557 _poc.config.renderer.background.g, 558 _poc.config.renderer.background.b, 559 _poc.config.renderer.background.a 560 ); 561 glClear(GL_COLOR_BUFFER_BIT); 562 563 glActiveTexture(GL_TEXTURE0); 564 glBindTexture(GL_TEXTURE_2D, _texture); 565 glTexSubImage2D( 566 GL_TEXTURE_2D, 567 0, 568 0, 0, 569 _poc.config.renderer.resolution.width, _poc.config.renderer.resolution.height, 570 GL_RGBA, GL_UNSIGNED_BYTE, 571 castTo!(void*)(_poc.framebuffer.data.ptr) 572 ); 573 574 glUseProgram(_shader.shaderProgram); 575 glBindVertexArray(_vao); 576 glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, null); 577 } 578 } 579 580 private { 581 void compileLinkShader() { 582 _shader = new OpenGlShader( 583 OpenGlShader.Source(GL_VERTEX_SHADER, ` 584 #version 330 core 585 layout (location = 0) in vec2 aPos; 586 layout (location = 1) in vec2 aTexCoord; 587 588 out vec2 TexCoord; 589 590 void main() { 591 gl_Position = vec4(aPos.x, aPos.y, 0.0, 1.0); 592 TexCoord = aTexCoord; 593 } 594 `), 595 OpenGlShader.Source(GL_FRAGMENT_SHADER, ` 596 #version 330 core 597 out vec4 FragColor; 598 599 in vec2 TexCoord; 600 601 uniform sampler2D sampler; 602 603 void main() { 604 FragColor = texture(sampler, TexCoord); 605 } 606 `), 607 ); 608 } 609 610 void setupVertexObjects() { 611 glGenVertexArrays(1, &_vao); 612 glBindVertexArray(_vao); 613 614 glGenBuffers(1, &_vbo); 615 glGenBuffers(1, &_ebo); 616 617 glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, _ebo); 618 glBufferDataSlice(GL_ELEMENT_ARRAY_BUFFER, indices, GL_STATIC_DRAW); 619 620 glBindBuffer(GL_ARRAY_BUFFER, _vbo); 621 glBufferDataSlice(GL_ARRAY_BUFFER, vertices, GL_STATIC_DRAW); 622 623 glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 4 * GLfloat.sizeof, null); 624 glEnableVertexAttribArray(0); 625 626 glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 4 * GLfloat.sizeof, castTo!(void*)(2 * GLfloat.sizeof)); 627 glEnableVertexAttribArray(1); 628 } 629 630 void setupTexture() { 631 if (_texture == 0) { 632 glGenTextures(1, &_texture); 633 } 634 635 glBindTexture(GL_TEXTURE_2D, _texture); 636 637 final switch (_poc.config.renderer.filter) with (ScalingFilter) { 638 case nearest: 639 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); 640 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); 641 break; 642 case linear: 643 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); 644 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); 645 break; 646 } 647 648 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); 649 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); 650 glTexImage2D( 651 GL_TEXTURE_2D, 652 0, 653 GL_RGBA8, 654 _poc.config.renderer.resolution.width, _poc.config.renderer.resolution.height, 655 0, 656 GL_RGBA, GL_UNSIGNED_BYTE, 657 null 658 ); 659 660 glBindTexture(GL_TEXTURE_2D, 0); 661 } 662 } 663 664 public void reconfigure() { 665 const Viewport viewport = calculateViewport(_poc.config); 666 glViewportPMP(viewport); 667 668 this.setupTexture(); 669 } 670 671 void redrawSchedule() { 672 _poc.window.redrawOpenGlSceneSoon(); 673 } 674 675 void redrawNow() { 676 _poc.window.redrawOpenGlSceneNow(); 677 } 678 679 private { 680 static immutable GLfloat[] vertices = [ 681 //dfmt off 682 // positions // texture coordinates 683 1.0f, 1.0f, 1.0f, 0.0f, 684 1.0f, -1.0f, 1.0f, 1.0f, 685 -1.0f, -1.0f, 0.0f, 1.0f, 686 -1.0f, 1.0f, 0.0f, 0.0f, 687 //dfmt on 688 ]; 689 690 static immutable GLuint[] indices = [ 691 //dfmt off 692 0, 1, 3, 693 1, 2, 3, 694 //dfmt on 695 ]; 696 } 697 } 698 699 /++ 700 Legacy OpenGL (1.x) renderer implementation 701 702 Uses what is often called the $(I Fixed Function Pipeline). 703 +/ 704 final class OpenGl1PixmapRenderer : PixmapRenderer { 705 706 private { 707 PresenterObjectsContainer* _poc; 708 GLuint _texture = 0; 709 } 710 711 public @safe pure nothrow @nogc { 712 /// 713 this() { 714 } 715 716 WantsOpenGl wantsOpenGl() pure nothrow @nogc @safe { 717 return WantsOpenGl(1, 1, true); 718 } 719 720 } 721 722 public void setup(PresenterObjectsContainer* poc) { 723 _poc = poc; 724 _poc.window.suppressAutoOpenglViewport = true; 725 _poc.window.visibleForTheFirstTime = &this.visibleForTheFirstTime; 726 _poc.window.redrawOpenGlScene = &this.redrawOpenGlScene; 727 } 728 729 private { 730 731 void visibleForTheFirstTime() { 732 //_poc.window.setAsCurrentOpenGlContext(); 733 // ↑-- reconfigure() does this, too. 734 // |-- Uncomment if this functions does something else in the future. 735 736 this.reconfigure(); 737 } 738 739 void setupTexture() { 740 if (_texture == 0) { 741 glGenTextures(1, &_texture); 742 } 743 744 glBindTexture(GL_TEXTURE_2D, _texture); 745 746 final switch (_poc.config.renderer.filter) with (ScalingFilter) { 747 case nearest: 748 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); 749 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); 750 break; 751 case linear: 752 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); 753 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); 754 break; 755 } 756 757 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); 758 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); 759 glTexImage2D( 760 GL_TEXTURE_2D, 761 0, 762 GL_RGBA8, 763 _poc.config.renderer.resolution.width, 764 _poc.config.renderer.resolution.height, 765 0, 766 GL_RGBA, GL_UNSIGNED_BYTE, 767 null 768 ); 769 770 glBindTexture(GL_TEXTURE_2D, 0); 771 } 772 773 void setupMatrix() { 774 glMatrixMode(GL_PROJECTION); 775 glLoadIdentity(); 776 glOrtho( 777 0, _poc.config.renderer.resolution.width, 778 _poc.config.renderer.resolution.height, 0, 779 -1, 1 780 ); 781 //glMatrixMode(GL_MODELVIEW); 782 } 783 784 void redrawOpenGlScene() { 785 glClearColor( 786 _poc.config.renderer.background.r, 787 _poc.config.renderer.background.g, 788 _poc.config.renderer.background.b, 789 _poc.config.renderer.background.a, 790 ); 791 glClear(GL_COLOR_BUFFER_BIT); 792 793 glBindTexture(GL_TEXTURE_2D, _texture); 794 glEnable(GL_TEXTURE_2D); 795 { 796 glTexSubImage2D( 797 GL_TEXTURE_2D, 798 0, 799 0, 0, 800 _poc.config.renderer.resolution.width, _poc.config.renderer.resolution.height, 801 GL_RGBA, GL_UNSIGNED_BYTE, 802 castTo!(void*)(_poc.framebuffer.data.ptr) 803 ); 804 805 glBegin(GL_QUADS); 806 { 807 glTexCoord2f(0, 0); 808 glVertex2i(0, 0); 809 810 glTexCoord2f(0, 1); 811 glVertex2i(0, _poc.config.renderer.resolution.height); 812 813 glTexCoord2f(1, 1); 814 glVertex2i(_poc.config.renderer.resolution.width, _poc.config.renderer.resolution.height); 815 816 glTexCoord2f(1, 0); 817 glVertex2i(_poc.config.renderer.resolution.width, 0); 818 } 819 glEnd(); 820 } 821 glDisable(GL_TEXTURE_2D); 822 glBindTexture(GL_TEXTURE_2D, 0); 823 } 824 } 825 826 public void reconfigure() { 827 _poc.window.setAsCurrentOpenGlContext(); 828 829 const Viewport viewport = calculateViewport(_poc.config); 830 glViewportPMP(viewport); 831 832 this.setupTexture(); 833 this.setupMatrix(); 834 } 835 836 public void redrawSchedule() { 837 _poc.window.redrawOpenGlSceneSoon(); 838 } 839 840 public void redrawNow() { 841 _poc.window.redrawOpenGlSceneNow(); 842 } 843 } 844 845 /// 846 struct LoopCtrl { 847 int interval; /// in milliseconds 848 bool redraw; /// 849 850 /// 851 @disable this(); 852 853 @safe pure nothrow @nogc: 854 855 private this(int interval, bool redraw) { 856 this.interval = interval; 857 this.redraw = redraw; 858 } 859 860 /// 861 static LoopCtrl waitFor(int intervalMS) { 862 return LoopCtrl(intervalMS, false); 863 } 864 865 /// 866 static LoopCtrl redrawIn(int intervalMS) { 867 return LoopCtrl(intervalMS, true); 868 } 869 } 870 871 /++ 872 Pixmap Presenter window 873 874 A high-level window class that displays fully-rendered frames in the form of [Pixmap|Pixmaps]. 875 The pixmap will be centered and (optionally) scaled. 876 +/ 877 final class PixmapPresenter { 878 879 private { 880 PresenterObjectsContainer* _poc; 881 PixmapRenderer _renderer; 882 883 static if (hasTimer) { 884 Timer _timer; 885 } 886 887 WindowResizedCallback _onWindowResize; 888 } 889 890 // ctors 891 public { 892 893 /// 894 this(const PresenterConfig config, bool useOpenGl = true) { 895 if (useOpenGl) { 896 this(config, new OpenGl3PixmapRenderer()); 897 } else { 898 assert(false, "Not implemented"); 899 } 900 } 901 902 /// 903 this(const PresenterConfig config, PixmapRenderer renderer) { 904 _renderer = renderer; 905 906 // create software framebuffer 907 auto framebuffer = Pixmap.makeNew(config.renderer.resolution); 908 909 // OpenGL? 910 auto openGlOptions = OpenGlOptions.no; 911 const openGl = _renderer.wantsOpenGl; 912 if (openGl.wanted) { 913 setOpenGLContextVersion(openGl.vMaj, openGl.vMin); 914 openGLContextCompatible = openGl.compat; 915 916 openGlOptions = OpenGlOptions.yes; 917 } 918 919 // spawn window 920 auto window = new SimpleWindow( 921 config.window.size, 922 config.window.title, 923 openGlOptions, 924 Resizability.allowResizing, 925 ); 926 927 window.windowResized = &this.windowResized; 928 window.cornerStyle = config.window.corners; 929 930 // alloc objects 931 _poc = new PresenterObjectsContainer( 932 framebuffer, 933 window, 934 config, 935 ); 936 937 _renderer.setup(_poc); 938 } 939 } 940 941 // additional convenience ctors 942 public { 943 944 /// 945 this( 946 string title, 947 const Size resolution, 948 const Size initialWindowSize, 949 Scaling scaling = Scaling.contain, 950 ScalingFilter filter = ScalingFilter.nearest, 951 ) { 952 auto cfg = PresenterConfig(); 953 954 cfg.window.title = title; 955 cfg.renderer.resolution = resolution; 956 cfg.window.size = initialWindowSize; 957 cfg.renderer.scaling = scaling; 958 cfg.renderer.filter = filter; 959 960 this(cfg); 961 } 962 963 /// 964 this( 965 string title, 966 const Size resolution, 967 Scaling scaling = Scaling.contain, 968 ScalingFilter filter = ScalingFilter.nearest, 969 ) { 970 this(title, resolution, resolution, scaling, filter,); 971 } 972 } 973 974 // public functions 975 public { 976 977 /++ 978 Runs the event loop (with a pulse timer) 979 980 A redraw will be scheduled automatically each pulse. 981 +/ 982 int eventLoop(T...)(long pulseTimeout, void delegate() onPulse, T eventHandlers) { 983 // run event-loop with pulse timer 984 return _poc.window.eventLoop( 985 pulseTimeout, 986 delegate() { onPulse(); this.scheduleRedraw(); }, 987 eventHandlers, 988 ); 989 } 990 991 //dfmt off 992 /++ 993 Runs the event loop 994 995 Redraws have to manually scheduled through [scheduleRedraw] when using this overload. 996 +/ 997 int eventLoop(T...)(T eventHandlers) if ( 998 (T.length == 0) || (is(T[0] == delegate) && !is(typeof(() { return T[0](); }()) == LoopCtrl)) 999 ) { 1000 return _poc.window.eventLoop(eventHandlers); 1001 } 1002 //dfmt on 1003 1004 static if (hasTimer) { 1005 /++ 1006 Runs the event loop 1007 with [LoopCtrl] timing mechanism 1008 +/ 1009 int eventLoop(T...)(LoopCtrl delegate() callback, T eventHandlers) { 1010 if (callback !is null) { 1011 LoopCtrl prev = LoopCtrl(1, true); 1012 1013 _timer = new Timer(prev.interval, delegate() { 1014 // redraw if requested by previous ctrl message 1015 if (prev.redraw) { 1016 _renderer.redrawNow(); 1017 prev.redraw = false; // done 1018 } 1019 1020 // execute callback 1021 const LoopCtrl ctrl = callback(); 1022 1023 // different than previous ctrl message? 1024 if (ctrl.interval != prev.interval) { 1025 // update timer 1026 _timer.changeTime(ctrl.interval); 1027 } 1028 1029 // save ctrl message 1030 prev = ctrl; 1031 }); 1032 } 1033 1034 // run event-loop 1035 return _poc.window.eventLoop(0, eventHandlers); 1036 } 1037 } 1038 1039 /++ 1040 The [Pixmap] to be presented. 1041 1042 Use this to “draw” on screen. 1043 +/ 1044 Pixmap pixmap() @safe pure nothrow @nogc { 1045 return _poc.framebuffer; 1046 } 1047 1048 /// ditto 1049 alias framebuffer = pixmap; 1050 1051 /++ 1052 Updates the configuration of the presenter. 1053 1054 Params: 1055 resizeWindow = if false, `config.window.size` will be ignored. 1056 +/ 1057 void reconfigure(PresenterConfig config, const bool resizeWindow = false) { 1058 // override requested window-size to current size if no resize requested 1059 if (!resizeWindow) { 1060 config.window.size = _poc.config.window.size; 1061 } 1062 1063 this.reconfigureImpl(config); 1064 } 1065 1066 private void reconfigureImpl(const ref PresenterConfig config) { 1067 _poc.window.title = config.window.title; 1068 1069 if (config.renderer.resolution != _poc.config.renderer.resolution) { 1070 _poc.framebuffer.size = config.renderer.resolution; 1071 } 1072 1073 immutable resize = (config.window.size != _poc.config.window.size); 1074 1075 // update stored configuration 1076 _poc.config = config; 1077 1078 if (resize) { 1079 _poc.window.resize(config.window.size.width, config.window.size.height); 1080 // resize-handler will call `_renderer.reconfigure()` 1081 } else { 1082 _renderer.reconfigure(); 1083 } 1084 } 1085 1086 /++ 1087 Schedules a redraw 1088 +/ 1089 void scheduleRedraw() { 1090 _renderer.redrawSchedule(); 1091 } 1092 1093 /++ 1094 Fullscreen mode 1095 +/ 1096 bool isFullscreen() { 1097 return _poc.window.fullscreen; 1098 } 1099 1100 /// ditto 1101 void isFullscreen(bool enabled) { 1102 _poc.window.fullscreen = enabled; 1103 } 1104 1105 /++ 1106 Toggles the fullscreen state of the window. 1107 1108 Turns a non-fullscreen window into fullscreen mode. 1109 Exits fullscreen mode for fullscreen-windows. 1110 +/ 1111 void toggleFullscreen() { 1112 this.isFullscreen = !this.isFullscreen; 1113 } 1114 1115 /++ 1116 Returns the underlying [arsd.simpledisplay.SimpleWindow|SimpleWindow] 1117 1118 $(WARNING 1119 This is unsupported; use at your own risk. 1120 1121 Tinkering with the window directly can break all sort of things 1122 that a presenter or renderer could possibly have set up. 1123 ) 1124 +/ 1125 SimpleWindow tinkerWindow() @safe pure nothrow @nogc { 1126 return _poc.window; 1127 } 1128 1129 /++ 1130 Returns the underlying [PixmapRenderer] 1131 1132 $(TIP 1133 Type-cast the returned reference to the actual implementation type for further use. 1134 ) 1135 1136 $(WARNING 1137 This is quasi unsupported; use at your own risk. 1138 1139 Using the result of this function is pratictically no different than 1140 using a reference to the renderer further on after passing it the presenter’s constructor. 1141 It can’t be prohibited but it resembles a footgun. 1142 ) 1143 +/ 1144 PixmapRenderer tinkerRenderer() @safe pure nothrow @nogc { 1145 return _renderer; 1146 } 1147 } 1148 1149 // event (handler) properties 1150 public @safe pure nothrow @nogc { 1151 1152 /++ 1153 Event handler: window resize 1154 +/ 1155 void onWindowResize(WindowResizedCallback value) { 1156 _onWindowResize = value; 1157 } 1158 } 1159 1160 // event handlers 1161 private { 1162 void windowResized(int width, int height) { 1163 const newSize = Size(width, height); 1164 1165 _poc.config.window.size = newSize; 1166 _renderer.reconfigure(); 1167 // ↑ In case this call gets removed, update `reconfigure()`. 1168 // Current implementation takes advantage of the `_renderer.reconfigure()` call here. 1169 1170 if (_onWindowResize !is null) { 1171 _onWindowResize(newSize); 1172 } 1173 } 1174 } 1175 }