The OpenD Programming Language

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