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 }