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