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 }