From 7266c4883ab3ef1787a3e9f9c83e186e532f786e Mon Sep 17 00:00:00 2001
From: Elias Batek <desisma@heidel.beer>
Date: Sat, 16 Dec 2023 04:31:51 +0100
Subject: [PATCH 01/16] Add arsd.pixelpresenter

---
 dub.json         | 13 +++++++++++++
 pixelpresenter.d | 12 ++++++++++++
 2 files changed, 25 insertions(+)
 create mode 100644 pixelpresenter.d

diff --git a/dub.json b/dub.json
index 8a5f3c0..e402217 100644
--- a/dub.json
+++ b/dub.json
@@ -681,6 +681,19 @@
 				"arsd-official:color_base":"*"
 			}
 		},
+		{
+			"name": "pixelpresenter",
+			"description": "Pixel display",
+			"targetType": "library",
+			"sourceFiles": ["pixelpresenter.d"],
+			"dependencies": {
+				"arsd-official:color_base":"*",
+				"arsd-official:simpledisplay":"*"
+			},
+			"dflags-dmd": ["-mv=arsd.pixelpresenter=$PACKAGE_DIR/pixelpresenter.d"],
+			"dflags-ldc": ["--mv=arsd.pixelpresenter=$PACKAGE_DIR/pixelpresenter.d"],
+			"dflags-gdc": ["-fmodule-file=arsd.pixelpresenter=$PACKAGE_DIR/pixelpresenter.d"]
+		},
 		{
 			"name": "ttf",
 			"description": "port of stb_ttf to D",
diff --git a/pixelpresenter.d b/pixelpresenter.d
new file mode 100644
index 0000000..dc47945
--- /dev/null
+++ b/pixelpresenter.d
@@ -0,0 +1,12 @@
+/+
+	== pixelpresenter ==
+	Copyright Elias Batek (0xEAB) 2023.
+	Distributed under the Boost Software License, Version 1.0.
+ +/
+/++
+	# Pixel Presenter
+ +/
+module arsd.pixelpresenter;
+
+import arsd.color;
+import arsd.simpledisplay;

From 1d2e57f61ab894b6e3444a8f04170cc48b055a10 Mon Sep 17 00:00:00 2001
From: Elias Batek <desisma@heidel.beer>
Date: Thu, 21 Dec 2023 02:34:19 +0100
Subject: [PATCH 02/16] Implement OpenGL 3 renderer for pixelpresenter

---
 README.md        |   2 +
 package.d        |   2 +
 pixelpresenter.d | 656 ++++++++++++++++++++++++++++++++++++++++++++++-
 simpledisplay.d  |   1 +
 4 files changed, 660 insertions(+), 1 deletion(-)

diff --git a/README.md b/README.md
index bea4e17..e048ba3 100644
--- a/README.md
+++ b/README.md
@@ -22,6 +22,8 @@ Future release, likely May 2024 or later.
 
 Nothing is planned for it at this time.
 
+arsd.pixelpresenter was added.
+
 ## 11.0
 
 Released: Planned for May 2023, actually out August 2023.
diff --git a/package.d b/package.d
index 6479071..78f6504 100644
--- a/package.d
+++ b/package.d
@@ -86,6 +86,8 @@
 		$(H4 $(ID desktop-game) Games)
 			See [arsd.simpledisplay] and [arsd.gamehelpers].
 
+			Check out [arsd.pixelpresenter] for old-skool games that blit fully-rendered frames to the screen.
+
 		$(H4 $(ID desktop-gui) GUIs)
 			See [arsd.minigui], [arsd.nanovega], and also: https://github.com/drug007/nanogui
 
diff --git a/pixelpresenter.d b/pixelpresenter.d
index dc47945..270d44e 100644
--- a/pixelpresenter.d
+++ b/pixelpresenter.d
@@ -4,9 +4,663 @@
 	Distributed under the Boost Software License, Version 1.0.
  +/
 /++
-	# Pixel Presenter
+	$(B Pixel Presenter) is a high-level display library for one specific scenario:
+	Blitting fully-rendered frames to the screen.
+
+	This is useful for software-rendered applications.
+	Think of old-skool games, emulators etc.
+
+	This library builds upon [arsd.simpledisplay] and [arsd.color].
+	It wraps a [arsd.simpledisplay.SimpleWindow|SimpleWindow]) and displays the provided frame data.
+	Each frame is automatically centered on, and optionally scaled to, the carrier window.
+	This processing is done with hardware acceleration (OpenGL).
+	Later versions might add a software-mode.
+
+	Several $(B scaling) modes are supported.
+	Most notably `keepAspectRatio` that scales frames to the while preserving the original aspect ratio.
+	See [Scaling] for details.
+
+	$(PITFALL
+		This module is $(B work in progress).
+		API is subject to changes until further notice.
+	)
  +/
 module arsd.pixelpresenter;
 
 import arsd.color;
 import arsd.simpledisplay;
+
+/*
+	## TODO
+
+	- Complete documentation
+	- Usage example(s)
+	- Additional renderer implementations:
+		- a `ScreenPainter`-based renderer
+		- a legacy OpenGL renderer (maybe)
+	- Is there something in arsd that serves a similar purpose to `PixelBuffer`?
+	- Minimum window size
+		- or something similar
+		- to ensure `Scaling.integer` doesn’t break “unexpectedly”
+	- Hybrid scaling mode: integer up, FP down
+ */
+
+///
+alias Pixel = Color;
+
+///
+alias ColorF = arsd.color.ColorF;
+
+///
+alias Size = arsd.color.Size;
+
+///
+alias Point = arsd.color.Point;
+
+// verify assumption(s)
+static assert(Pixel.sizeof == uint.sizeof);
+
+/// casts value `v` to type `T`
+auto ref T typeCast(T, S)(auto ref S v) {
+	return cast(T) v;
+}
+
+@safe pure nothrow @nogc {
+	///
+	Pixel rgba(ubyte r, ubyte g, ubyte b, ubyte a = 0xFF) {
+		return Pixel(r, g, b, a);
+	}
+
+	///
+	Pixel rgb(ubyte r, ubyte g, ubyte b) {
+		return rgba(r, g, b, 0xFF);
+	}
+}
+
+/++
+	Pixel data container
+ +/
+struct PixelBuffer {
+
+	/// Pixel data
+	Pixel[] data;
+
+	/// Pixel per row
+	int width;
+
+@safe pure nothrow:
+
+	this(Size size) {
+		this.size = size;
+	}
+
+	// undocumented: really shouldn’t be used.
+	// carries the risks of `length` and `width` getting out of sync accidentally.
+	deprecated("Use `size` instead.")
+	void length(int value) {
+		data.length = value;
+	}
+
+	/++
+		Changes the size of the buffer
+
+		Reallocates the underlying pixel array.
+	 +/
+	void size(Size value) {
+		data.length = value.area;
+		width = value.width;
+	}
+
+	/// ditto
+	void size(int totalPixels, int width)
+	in (length % width == 0) {
+		data.length = totalPixels;
+		this.width = width;
+	}
+
+@safe pure nothrow @nogc:
+
+	/// Height of the buffer, i.e. the number of lines
+	int height() inout {
+		if (data.length == 0)
+			return 0;
+		return (cast(int) data.length / width);
+	}
+
+	/// Rectangular size of the buffer
+	Size size() inout {
+		return Size(width, height);
+	}
+
+	/// Length of the buffer, i.e. the number of pixels
+	int length() inout {
+		return cast(int) data.length;
+	}
+
+	/++
+		Number of bytes per line
+
+		Returns:
+			width × Pixel.sizeof
+	 +/
+	int pitch() inout {
+		return (width * int(Pixel.sizeof));
+	}
+
+	/// Clears the buffers contents (by setting each pixel to the same color)
+	void clear(Pixel value) {
+		data[] = value;
+	}
+}
+
+@safe pure nothrow @nogc {
+
+	// keep aspect ratio (contain)
+	int karContainScalingFactorInt(const Size drawing, const Size canvas) {
+		const int w = canvas.width / drawing.width;
+		const int h = canvas.height / drawing.height;
+
+		return (w < h) ? w : h;
+	}
+
+	// keep aspect ratio (contain; FP variant)
+	float karContainScalingFactorF(const Size drawing, const Size canvas) {
+		const w = float(canvas.width) / float(drawing.width);
+		const h = float(canvas.height) / float(drawing.height);
+
+		return (w < h) ? w : h;
+	}
+
+	// keep aspect ratio (cover)
+	float karCoverScalingFactorF(const Size drawing, const Size canvas) {
+		const w = float(canvas.width) / float(drawing.width);
+		const h = float(canvas.height) / float(drawing.height);
+
+		return (w > h) ? w : h;
+	}
+
+	Size deltaPerimeter(const Size a, const Size b) {
+		return Size(
+			a.width - b.width,
+			a.height - b.height,
+		);
+	}
+
+	Point offsetCenter(const Size drawing, const Size canvas) {
+		auto delta = canvas.deltaPerimeter(drawing);
+		return (cast(Point) delta) >> 1;
+	}
+}
+
+/++
+	Scaling/Fit Modes
+
+	Each scaling modes has unique behavior for different window-size to frame-size ratios.
+
+	Unfortunately, there are no universally applicable naming conventions for these modes.
+	In fact, different implementations tend to contradict each other.
+
+	$(SMALL_TABLE
+		Mode feature matrix
+		Mode        | Aspect Ratio | Pixel Ratio | Cropping | Border | Comment(s)
+		`none`      | preserved    | preserved   | yes      | 4      |
+		`stretch`	| no           | no          | no       | none   |
+		`contain`   | preserved    | no          | no       | 4      | letterboxing/pillarboxing
+		`integer`   | preserved    | preserved   | no       | 2      | works only if `window.size >= frame.size`
+		`cover`     | preserved    | no          | yes      | none   |
+	)
+
+	$(SMALL_TABLE
+		Feature      | Definition
+		Aspect Ratio | Whether the original aspect ratio (width ÷ height) of the input frame is preserved
+		Pixel Ratio  | Whether the orignal pixel ratio (= square) is preserved
+		Cropping     | Whether the outer areas of the input frame might get cut off
+		Border       | The number of padding-areas/borders that can potentially appear around the frame
+	)
+
+	For your convience, aliases matching the [`object-fit`](https://developer.mozilla.org/en-US/docs/Web/CSS/object-fit)
+	CSS property are provided, too. These are prefixed with `css`.
+	Currently there is no equivalent for `scale-down` as it does not appear to be particularly useful here.
+ +/
+enum Scaling {
+	none = 0, ///
+	stretch, ///
+	contain, ///
+	integer, ///
+	cover, ///
+
+	// aliases
+	center = none, ///
+	keepAspectRatio = contain, ///
+
+	// CSS `object-fit` style aliases
+	cssNone = none, ///
+	cssContain = contain, ///
+	cssFill = stretch, ///
+	cssCover = cover, ///
+}
+
+///
+enum ScalingFilter {
+	nearest, /// nearest neighbor → blocky/pixel’ish
+	linear, /// (bi-)linear interpolation → smooth/blurry
+}
+
+///
+struct PresenterConfig {
+	Window window; ///
+	Renderer renderer; ///
+
+	///
+	static struct Renderer {
+		/++
+			Internal resolution
+		 +/
+		Size resolution;
+
+		/++
+			Scaling method
+			to apply when `window.size` != `resolution`
+		 +/
+		Scaling scaling = Scaling.keepAspectRatio;
+
+		/++
+			Filter
+		 +/
+		ScalingFilter filter = ScalingFilter.nearest;
+
+		/++
+			Background color
+		 +/
+		ColorF background = ColorF(0.0f, 0.0f, 0.0f, 1.0f);
+
+		///
+		void setPixelPerfect() {
+			scaling = Scaling.integer;
+			filter = ScalingFilter.nearest;
+		}
+	}
+
+	///
+	static struct Window {
+		string title = "ARSD Pixel Presenter";
+		Size size;
+	}
+}
+
+// undocumented
+struct PresenterObjects {
+	PixelBuffer framebuffer;
+	SimpleWindow window;
+	PresenterConfig config;
+}
+
+///
+struct WantsOpenGl {
+	bool wanted; /// Is OpenGL wanted?
+	ubyte vMaj; /// major version
+	ubyte vMin; /// minor version
+}
+
+///
+interface PixelRenderer {
+	/++
+		Does this renderer use OpenGL?
+
+		Returns:
+			Whether the renderer requires an OpenGL-enabled window
+			and which version is expected.
+	 +/
+	public WantsOpenGl wantsOpenGl() @safe pure nothrow @nogc;
+
+	/++
+		Setup function
+
+		Called once during setup.
+		Perform initialization tasks in here.
+
+		Params:
+			pro = Pointer to the [PresenterObjects] of the presenter. To be stored for later use.
+	 +/
+	public void setup(PresenterObjects* pro);
+
+	/++
+		Reconfigure renderer
+
+		Called upon configuration changes.
+		The new config can be found in the [PresenterObjects] received during `setup()`.
+	 +/
+	public void reconfigure();
+}
+
+///
+final class OpenGL3PixelRenderer : PixelRenderer {
+
+	private {
+		PresenterObjects* _pro;
+
+		bool _clear = true;
+
+		GLfloat[16] _vertices;
+		OpenGlShader _shader;
+		GLuint _vao;
+		GLuint _vbo;
+		GLuint _ebo;
+		GLuint _texture = 0;
+	}
+
+	///
+	public this() {
+	}
+
+	public WantsOpenGl wantsOpenGl() @safe pure nothrow @nogc {
+		return WantsOpenGl(true, 3, 0);
+	}
+
+	// TODO: make this ctor?
+	public void setup(PresenterObjects* pro) {
+		_pro = pro;
+		_pro.window.visibleForTheFirstTime = &this.visibleForTheFirstTime;
+		_pro.window.redrawOpenGlScene = &this.redrawOpenGlScene;
+	}
+
+	private {
+		void visibleForTheFirstTime() {
+			_pro.window.setAsCurrentOpenGlContext();
+			gl3.loadDynamicLibrary();
+
+			this.compileLinkShader();
+			this.setupVertexObjects();
+
+			this.reconfigure();
+		}
+
+		void redrawOpenGlScene() {
+			if (_clear) {
+				glClearColor(
+					_pro.config.renderer.background.r,
+					_pro.config.renderer.background.g,
+					_pro.config.renderer.background.b,
+					_pro.config.renderer.background.a
+				);
+				glClear(GL_COLOR_BUFFER_BIT);
+				_clear = false;
+			}
+
+			glActiveTexture(GL_TEXTURE0);
+			glBindTexture(GL_TEXTURE_2D, _texture);
+			glTexSubImage2D(
+				GL_TEXTURE_2D,
+				0,
+				0, 0,
+				_pro.config.renderer.resolution.width, _pro.config.renderer.resolution.height,
+				GL_RGBA, GL_UNSIGNED_BYTE,
+				cast(void*) _pro.framebuffer.data.ptr
+			);
+
+			glUseProgram(_shader.shaderProgram);
+			glBindVertexArray(_vao);
+			glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, null);
+		}
+	}
+
+	private {
+		void compileLinkShader() {
+			_shader = new OpenGlShader(
+				OpenGlShader.Source(GL_VERTEX_SHADER, `
+					#version 330 core
+					layout (location = 0) in vec2 aPos;
+					layout (location = 1) in vec2 aTexCoord;
+
+					out vec2 TexCoord;
+
+					void main() {
+						gl_Position = vec4(aPos.x, aPos.y, 0.0, 1.0);
+						TexCoord = aTexCoord;
+					}
+				`),
+				OpenGlShader.Source(GL_FRAGMENT_SHADER, `
+					#version 330 core
+					out vec4 FragColor;
+
+					in vec2 TexCoord;
+
+					uniform sampler2D sampler;
+
+					void main() {
+						FragColor = texture(sampler, TexCoord);
+					}
+				`),
+			);
+		}
+
+		void setupVertexObjects() {
+			glGenVertexArrays(1, &_vao);
+			glBindVertexArray(_vao);
+
+			glGenBuffers(1, &_vbo);
+			glGenBuffers(1, &_ebo);
+
+			glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, _ebo);
+			glBufferDataSlice(GL_ELEMENT_ARRAY_BUFFER, indices, GL_STATIC_DRAW);
+
+			glBindBuffer(GL_ARRAY_BUFFER, _vbo);
+			glBufferDataSlice(GL_ARRAY_BUFFER, vertices, GL_STATIC_DRAW);
+
+			glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 4 * GLfloat.sizeof, null);
+			glEnableVertexAttribArray(0);
+
+			glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 4 * GLfloat.sizeof, cast(void*)(2 * GLfloat.sizeof));
+			glEnableVertexAttribArray(1);
+		}
+
+		void setupTexture() {
+			if (_texture == 0) {
+				glGenTextures(1, &_texture);
+			}
+
+			glBindTexture(GL_TEXTURE_2D, _texture);
+
+			final switch (_pro.config.renderer.filter) with (ScalingFilter) {
+			case nearest:
+				glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
+				glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
+				break;
+			case linear:
+				glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
+				glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
+				break;
+			}
+
+			glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
+			glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
+			glTexImage2D(
+				GL_TEXTURE_2D,
+				0,
+				GL_RGBA8,
+				_pro.config.renderer.resolution.width, _pro.config.renderer.resolution.height,
+				0,
+				GL_RGBA, GL_UNSIGNED_BYTE,
+				null
+			);
+
+			glBindTexture(GL_TEXTURE_2D, 0);
+		}
+	}
+
+	public void reconfigure() {
+		Size viewport;
+
+		final switch (_pro.config.renderer.scaling) {
+
+		case Scaling.none:
+			viewport = _pro.config.renderer.resolution;
+			break;
+
+		case Scaling.stretch:
+			viewport = _pro.config.window.size;
+			break;
+
+		case Scaling.contain:
+			const float scaleF = karContainScalingFactorF(_pro.config.renderer.resolution, _pro.config.window.size);
+			viewport = Size(
+				typeCast!int(scaleF * _pro.config.renderer.resolution.width),
+				typeCast!int(scaleF * _pro.config.renderer.resolution.height),
+			);
+			break;
+
+		case Scaling.integer:
+			const int scaleI = karContainScalingFactorInt(_pro.config.renderer.resolution, _pro.config.window.size);
+			viewport = (_pro.config.renderer.resolution * scaleI);
+			break;
+
+		case Scaling.cover:
+			const float fillF = karCoverScalingFactorF(_pro.config.renderer.resolution, _pro.config.window.size);
+			viewport = Size(
+				typeCast!int(fillF * _pro.config.renderer.resolution.width),
+				typeCast!int(fillF * _pro.config.renderer.resolution.height),
+			);
+			break;
+		}
+
+		const Point viewportPos = offsetCenter(viewport, _pro.config.window.size);
+		glViewport(viewportPos.x, viewportPos.y, viewport.width, viewport.height);
+		this.setupTexture();
+		_clear = true;
+	}
+
+	private {
+		static immutable GLfloat[] vertices = [
+			//dfmt off
+			// positions     // texture coordinates
+			 1.0f,  1.0f,    1.0f, 0.0f,
+			 1.0f, -1.0f,    1.0f, 1.0f,
+			-1.0f, -1.0f,    0.0f, 1.0f,
+			-1.0f,  1.0f,    0.0f, 0.0f,
+			//dfmt on
+		];
+
+		static immutable GLuint[] indices = [
+			//dfmt off
+			0, 1, 3,
+			1, 2, 3,
+			//dfmt on
+		];
+	}
+}
+
+/++
+ +/
+final class PixelPresenter {
+
+	private {
+		PresenterObjects* _pro;
+		PixelRenderer _renderer;
+	}
+
+	// ctors
+	public {
+
+		///
+		this(const PresenterConfig config, bool useOpenGl = true) {
+			if (useOpenGl) {
+				this(config, new OpenGL3PixelRenderer());
+			} else {
+				assert(false, "Not implemented");
+			}
+		}
+
+		///
+		this(const PresenterConfig config, PixelRenderer renderer) {
+			_renderer = renderer;
+
+			// create software framebuffer
+			auto framebuffer = PixelBuffer(config.renderer.resolution);
+
+			// OpenGL?
+			auto openGlOptions = OpenGlOptions.no;
+			const openGl = _renderer.wantsOpenGl;
+			if (openGl.wanted) {
+				setOpenGLContextVersion(openGl.vMaj, openGl.vMin);
+				openGLContextCompatible = false;
+
+				openGlOptions = OpenGlOptions.yes;
+			}
+
+			// spawn window
+			auto window = new SimpleWindow(
+				config.window.size,
+				config.window.title,
+				openGlOptions,
+				Resizability.allowResizing,
+			);
+
+			window.windowResized = &this.windowResized;
+
+			// alloc objects
+			_pro = new PresenterObjects(
+				framebuffer,
+				window,
+				config,
+			);
+
+			_renderer.setup(_pro);
+		}
+	}
+
+	// public functions
+	public {
+
+		///
+		int eventLoop(T...)(T eventHandlers) if (T.length == 0 || is(T[0] == delegate)) {
+			return _pro.window.eventLoop(
+				16,
+				delegate() { eventHandlers[0](); _pro.window.redrawOpenGlSceneSoon(); },
+			);
+		}
+
+		///
+		PixelBuffer framebuffer() @safe pure nothrow @nogc {
+			return _pro.framebuffer;
+		}
+
+		///
+		void reconfigure(const PresenterConfig config) {
+			assert(false, "Not implemented");
+			//_framebuffer.size = config.internalResolution;
+			//_renderer.reconfigure(config);
+		}
+
+		///
+		bool isFullscreen() {
+			return _pro.window.fullscreen;
+		}
+
+		/// ditto
+		void isFullscreen(bool enabled) {
+			return _pro.window.fullscreen = enabled;
+		}
+
+		/++
+			Returns the underlying `SimpleWindow`
+
+			$(WARNING
+				This is unsupported; use at your own risk.
+
+				Tinkering with the window directly can break all sort of things
+				that a presenter or renderer could possibly have set up.
+			)
+		 +/
+		SimpleWindow tinker() @safe pure nothrow @nogc {
+			return _pro.window;
+		}
+	}
+
+	// event handlers
+	private {
+		void windowResized(int width, int height) {
+			_pro.config.window.size = Size(width, height);
+			_renderer.reconfigure();
+		}
+	}
+}
diff --git a/simpledisplay.d b/simpledisplay.d
index 8ecafd0..81e6d7d 100644
--- a/simpledisplay.d
+++ b/simpledisplay.d
@@ -18767,6 +18767,7 @@ extern(System) nothrow @nogc {
 	enum uint GL_RGB = 0x1907;
 	enum uint GL_BGRA = 0x80e1;
 	enum uint GL_RGBA = 0x1908;
+	enum uint GL_RGBA8 = 0x8058;
 	enum uint GL_TEXTURE_2D =   0x0DE1;
 	enum uint GL_TEXTURE_MIN_FILTER = 0x2801;
 	enum uint GL_NEAREST = 0x2600;

From 3f0e52f875c2ae9db57d0e7e39d11ec0efec901b Mon Sep 17 00:00:00 2001
From: Elias Batek <desisma@heidel.beer>
Date: Fri, 22 Dec 2023 02:21:56 +0100
Subject: [PATCH 03/16] Fix pixelpresenter not passing along event-handlers

---
 pixelpresenter.d | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/pixelpresenter.d b/pixelpresenter.d
index 270d44e..949dee4 100644
--- a/pixelpresenter.d
+++ b/pixelpresenter.d
@@ -43,6 +43,7 @@ import arsd.simpledisplay;
 		- or something similar
 		- to ensure `Scaling.integer` doesn’t break “unexpectedly”
 	- Hybrid scaling mode: integer up, FP down
+	- Fix timing
  */
 
 ///
@@ -614,8 +615,9 @@ final class PixelPresenter {
 		///
 		int eventLoop(T...)(T eventHandlers) if (T.length == 0 || is(T[0] == delegate)) {
 			return _pro.window.eventLoop(
-				16,
+				16, // ~60 FPS
 				delegate() { eventHandlers[0](); _pro.window.redrawOpenGlSceneSoon(); },
+				eventHandlers[1 .. $],
 			);
 		}
 

From f29fcd25d836a6548bc03505711ea448aefb0278 Mon Sep 17 00:00:00 2001
From: Elias Batek <desisma@heidel.beer>
Date: Fri, 22 Dec 2023 03:29:17 +0100
Subject: [PATCH 04/16] Add convenience constructors to PixelPresenter

---
 pixelpresenter.d | 33 +++++++++++++++++++++++++++++++++
 1 file changed, 33 insertions(+)

diff --git a/pixelpresenter.d b/pixelpresenter.d
index 949dee4..e36e1b4 100644
--- a/pixelpresenter.d
+++ b/pixelpresenter.d
@@ -609,6 +609,39 @@ final class PixelPresenter {
 		}
 	}
 
+	// additional convience ctors
+	public {
+
+		///
+		this(
+			string title,
+			const Size resolution,
+			const Size initialWindowSize,
+			Scaling scaling = Scaling.contain,
+			ScalingFilter filter = ScalingFilter.nearest,
+		) {
+			auto cfg = PresenterConfig();
+
+			cfg.window.title = title;
+			cfg.renderer.resolution = resolution;
+			cfg.window.size = initialWindowSize;
+			cfg.renderer.scaling = scaling;
+			cfg.renderer.filter = filter;
+
+			this(cfg);
+		}
+
+		///
+		this(
+			string title,
+			const Size resolution,
+			Scaling scaling = Scaling.contain,
+			ScalingFilter filter = ScalingFilter.nearest,
+		) {
+			this(title, resolution, resolution, scaling, filter,);
+		}
+	}
+
 	// public functions
 	public {
 

From 7b481e2d32512b4bb7a27f642fd2f4167b485f18 Mon Sep 17 00:00:00 2001
From: Elias Batek <desisma@heidel.beer>
Date: Fri, 22 Dec 2023 03:29:47 +0100
Subject: [PATCH 05/16] Add code examples to pixelpresenter

---
 pixelpresenter.d | 113 +++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 113 insertions(+)

diff --git a/pixelpresenter.d b/pixelpresenter.d
index e36e1b4..55370a5 100644
--- a/pixelpresenter.d
+++ b/pixelpresenter.d
@@ -24,6 +24,119 @@
 		This module is $(B work in progress).
 		API is subject to changes until further notice.
 	)
+
+	## Usage examples
+
+	### Basic usage
+
+	This example displays a blue frame that increases in color intensity,
+	then jumps back to black and the process repeats.
+
+	---
+	void main() {
+		// Internal resolution of the images (“frames”) we will render.
+		// From the PixelPresenter’s perspective,
+		// these are the “fully-rendered frames” that it will blit to screen.
+		// They may be up- & down-scaled to the window’s actual size
+		// (according to the chosen scaling mode) by the presenter.
+		const resolution = Size(240, 120);
+
+		// Let’s create a new presenter.
+		// (For more fine-grained control there’s also a constructor overload that
+		// accepts a [PresenterConfig] instance).
+		auto presenter = new PixelPresenter(
+			"Demo",         // window title
+			resolution,     // internal resolution
+			Size(960, 480), // initial window size (optional; default: =resolution)
+		);
+
+		// This variable will be “shared” across events (and frames).
+		int blueChannel = 0;
+
+		// Run the eventloop
+		presenter.eventLoop(delegate() {
+			// Update the frame(buffer) here…
+
+			// Construct an RGB color value.
+			auto color = Pixel(0x00, 0x00, blueChannel);
+			// For demo purposes, apply it to the whole framebuffer.
+			presenter.framebuffer.clear(color);
+
+			// Increment the amount of blue to be used by the next frame.
+			++blueChannel;
+			// reset if greater than 0xFF (=ubyte.max)
+			if (blueChannel > 0xFF)
+				blueChannel = 0;
+		});
+	}
+	---
+
+	### Advanced example
+
+	---
+	import arsd.pixelpresenter;
+	import arsd.simpledisplay : MouseEvent;
+
+	void main() {
+		// Internal resolution of the images (“frames”) we will render.
+		// For further details, check out the previous example.
+		const resolution = Size(240, 120);
+
+		// Configure our presenter in advance.
+		auto cfg = PresenterConfig();
+		cfg.window.title = "Demo II";
+		cfg.window.size = Size(960, 480);
+		cfg.renderer.resolution = resolution;
+		cfg.renderer.scaling = Scaling.integer; // integer scaling
+		                                        // → The frame on-screen will
+		                                        // always have a size that is a
+		                                        // multiple of the internal
+		                                        // resolution.
+		// The gentle reader might have noticed that the integer scaling will result
+		// in a padding/border area around the image for most window sizes. Let’s
+		// How about changing its color?
+		cfg.renderer.background = ColorF(Pixel.white);
+
+		// Let’s instantiate a new presenter with the previously created config.
+		auto presenter = new PixelPresenter(cfg);
+
+		// Start with a green frame, so we can easily observe what’s going on.
+		presenter.framebuffer.clear(rgb(0x00, 0xDD, 0x00));
+
+		int line = 0;
+		ubyte color = 0;
+		byte colorDelta = 2;
+
+		// Run the eventloop
+		presenter.eventLoop(delegate() {
+			// Determine the start and end index of the current line in the
+			// framebuffer.
+			immutable x0 = line * resolution.width;
+			immutable x1 = x0 + resolution.width;
+
+			// Change the color of the current line
+			presenter.framebuffer.data[x0 .. x1] = rgb(color, color, 0xFF);
+
+			// Determine the color to use for the next line
+			// (to be applied on the next update).
+			color += colorDelta;
+			if (color == 0x00)
+				colorDelta = 2;
+			else if (color >= 0xFE)
+				colorDelta = -2;
+
+			// Increment the line counter; reset to 0 once we’ve reached the
+			// end of the framebuffer (=the final/last line).
+			++line;
+			if (line == resolution.height)
+				line = 0;
+		}, delegate(MouseEvent ev) {
+			// toggle fullscreen mode on double-click
+			if (ev.doubleClick) {
+				presenter.isFullscreen = !presenter.isFullscreen;
+			}
+		});
+	---
  +/
 module arsd.pixelpresenter;
 

From a7401aa1c3f6a4a03fcf9cf712e0122a551c715a Mon Sep 17 00:00:00 2001
From: Elias Batek <desisma@heidel.beer>
Date: Fri, 22 Dec 2023 03:36:51 +0100
Subject: [PATCH 06/16] Remove dangling "Let's" from example code

---
 pixelpresenter.d | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/pixelpresenter.d b/pixelpresenter.d
index 55370a5..94426fa 100644
--- a/pixelpresenter.d
+++ b/pixelpresenter.d
@@ -93,7 +93,7 @@
 		                                        // multiple of the internal
 		                                        // resolution.
 		// The gentle reader might have noticed that the integer scaling will result
-		// in a padding/border area around the image for most window sizes. Let’s
+		// in a padding/border area around the image for most window sizes.
 		// How about changing its color?
 		cfg.renderer.background = ColorF(Pixel.white);
 

From d9b25be21266af46ae7a43330178e4e1d64b0c52 Mon Sep 17 00:00:00 2001
From: Elias Batek <desisma@heidel.beer>
Date: Fri, 22 Dec 2023 03:45:08 +0100
Subject: [PATCH 07/16] Add missing '{'

---
 pixelpresenter.d | 1 +
 1 file changed, 1 insertion(+)

diff --git a/pixelpresenter.d b/pixelpresenter.d
index 94426fa..b4adb42 100644
--- a/pixelpresenter.d
+++ b/pixelpresenter.d
@@ -136,6 +136,7 @@
 				presenter.isFullscreen = !presenter.isFullscreen;
 			}
 		});
+	}
 	---
  +/
 module arsd.pixelpresenter;

From 92796f8e29dea5e180b2600d90a293487c805d20 Mon Sep 17 00:00:00 2001
From: Elias Batek <desisma@heidel.beer>
Date: Sat, 23 Dec 2023 14:23:40 +0100
Subject: [PATCH 08/16] Update description of pixelpresenter in DUB recipe

---
 dub.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/dub.json b/dub.json
index e402217..4be70a1 100644
--- a/dub.json
+++ b/dub.json
@@ -683,7 +683,7 @@
 		},
 		{
 			"name": "pixelpresenter",
-			"description": "Pixel display",
+			"description": "High-level display library. Designed to blit fully-rendered frames to the screen.",
 			"targetType": "library",
 			"sourceFiles": ["pixelpresenter.d"],
 			"dependencies": {

From 7bd2675b1fc923a269e77c2d7c620a13c1c86ba4 Mon Sep 17 00:00:00 2001
From: Elias Batek <desisma@heidel.beer>
Date: Sat, 23 Dec 2023 15:01:12 +0100
Subject: [PATCH 09/16] Implement hybrid ("int" up, "contain" down) scaling
 mode

---
 pixelpresenter.d | 23 ++++++++++++++++++-----
 1 file changed, 18 insertions(+), 5 deletions(-)

diff --git a/pixelpresenter.d b/pixelpresenter.d
index b4adb42..f423f70 100644
--- a/pixelpresenter.d
+++ b/pixelpresenter.d
@@ -156,7 +156,6 @@ import arsd.simpledisplay;
 	- Minimum window size
 		- or something similar
 		- to ensure `Scaling.integer` doesn’t break “unexpectedly”
-	- Hybrid scaling mode: integer up, FP down
 	- Fix timing
  */
 
@@ -270,6 +269,12 @@ struct PixelBuffer {
 
 @safe pure nothrow @nogc {
 
+	// keep aspect ratio (contain)
+	bool karContainNeedsDownscaling(const Size drawing, const Size canvas) {
+		return (drawing.width > canvas.width)
+			|| (drawing.height > canvas.height);
+	}
+
 	// keep aspect ratio (contain)
 	int karContainScalingFactorInt(const Size drawing, const Size canvas) {
 		const int w = canvas.width / drawing.width;
@@ -318,10 +323,11 @@ struct PixelBuffer {
 	$(SMALL_TABLE
 		Mode feature matrix
 		Mode        | Aspect Ratio | Pixel Ratio | Cropping | Border | Comment(s)
-		`none`      | preserved    | preserved   | yes      | 4      |
-		`stretch`	| no           | no          | no       | none   |
-		`contain`   | preserved    | no          | no       | 4      | letterboxing/pillarboxing
-		`integer`   | preserved    | preserved   | no       | 2      | works only if `window.size >= frame.size`
+		`none`      | preserved    | preserved   | yes      | 4      | Crops if the `window.size < frame.size`.
+		`stretch`   | no           | no          | no       | none   |
+		`contain`   | preserved    | no          | no       | 2      | Letterboxing/Pillarboxing
+		`integer`   | preserved    | preserved   | no       | 4      | Works only if `window.size >= frame.size`.
+		`integerFP` | preserved    | when up     | no       | 4 or 2 | Hybrid: int upscaling, floating-point downscaling
 		`cover`     | preserved    | no          | yes      | none   |
 	)
 
@@ -342,6 +348,7 @@ enum Scaling {
 	stretch, ///
 	contain, ///
 	integer, ///
+	integerFP, ///
 	cover, ///
 
 	// aliases
@@ -629,6 +636,12 @@ final class OpenGL3PixelRenderer : PixelRenderer {
 			viewport = (_pro.config.renderer.resolution * scaleI);
 			break;
 
+		case Scaling.integerFP:
+			if (karContainNeedsDownscaling(_pro.config.renderer.resolution, _pro.config.window.size)) {
+				goto case Scaling.contain;
+			}
+			goto case Scaling.integer;
+
 		case Scaling.cover:
 			const float fillF = karCoverScalingFactorF(_pro.config.renderer.resolution, _pro.config.window.size);
 			viewport = Size(

From 8ffc0b327dc7ca0edd33099233ca2a92189feb2c Mon Sep 17 00:00:00 2001
From: Elias Batek <desisma@heidel.beer>
Date: Sat, 23 Dec 2023 23:45:36 +0100
Subject: [PATCH 10/16] Implement Timer.changeTime() for Linux targets

---
 simpledisplay.d | 32 +++++++++++++++++++++++++-------
 1 file changed, 25 insertions(+), 7 deletions(-)

diff --git a/simpledisplay.d b/simpledisplay.d
index 81e6d7d..3178bfe 100644
--- a/simpledisplay.d
+++ b/simpledisplay.d
@@ -5605,12 +5605,7 @@ class Timer {
 
 			mapping[fd] = this;
 
-			itimerspec value;
-			value.it_value.tv_sec = cast(int) (intervalInMilliseconds / 1000);
-			value.it_value.tv_nsec = (intervalInMilliseconds % 1000) * 1000_000;
-
-			value.it_interval.tv_sec = cast(int) (intervalInMilliseconds / 1000);
-			value.it_interval.tv_nsec = (intervalInMilliseconds % 1000) * 1000_000;
+			itimerspec value = makeItimerspec(intervalInMilliseconds);
 
 			if(timerfd_settime(fd, 0, &value, null) == -1)
 				throw new Exception("couldn't make pulse timer");
@@ -5683,7 +5678,6 @@ class Timer {
 		}
 	}
 
-
 	void changeTime(int intervalInMilliseconds)
 	{
 		this.intervalInMilliseconds = intervalInMilliseconds;
@@ -5696,6 +5690,15 @@ class Timer {
 				if(handle is null || !SetWaitableTimer(handle, cast(LARGE_INTEGER*)&initialTime, intervalInMilliseconds, &timerCallback, handle, false))
 					throw new WindowsApiException("couldn't change pulse timer", GetLastError());
 			}
+		} else version(linux) {
+			import core.sys.linux.timerfd;
+
+			itimerspec value = makeItimerspec(intervalInMilliseconds);
+			if(timerfd_settime(fd, 0, &value, null) == -1) {
+				throw new Exception("couldn't change pulse timer");
+			}
+		} else {
+			assert(false, "Timer.changeTime(int) is not implemented for this platform");
 		}
 	}
 
@@ -5706,6 +5709,21 @@ class Timer {
 
 	int lastEventLoopRoundTriggered;
 
+	version(linux) {
+		static auto makeItimerspec(int intervalInMilliseconds) {
+			import core.sys.linux.timerfd;
+
+			itimerspec value;
+			value.it_value.tv_sec = cast(int) (intervalInMilliseconds / 1000);
+			value.it_value.tv_nsec = (intervalInMilliseconds % 1000) * 1000_000;
+
+			value.it_interval.tv_sec = cast(int) (intervalInMilliseconds / 1000);
+			value.it_interval.tv_nsec = (intervalInMilliseconds % 1000) * 1000_000;
+
+			return value;
+		}
+	}
+
 	void trigger() {
 		version(linux) {
 			import unix = core.sys.posix.unistd;

From 0d81bbff41f52304e5dda244d173e973e2ad7ed9 Mon Sep 17 00:00:00 2001
From: Elias Batek <desisma@heidel.beer>
Date: Sat, 23 Dec 2023 23:56:19 +0100
Subject: [PATCH 11/16] =?UTF-8?q?Overhaul=20pixelpresenter=E2=80=99s=20tim?=
 =?UTF-8?q?ing?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 pixelpresenter.d | 140 +++++++++++++++++++++++++++++++++++++++++++----
 1 file changed, 128 insertions(+), 12 deletions(-)

diff --git a/pixelpresenter.d b/pixelpresenter.d
index f423f70..52b2e41 100644
--- a/pixelpresenter.d
+++ b/pixelpresenter.d
@@ -33,7 +33,7 @@
 	then jumps back to black and the process repeats.
 
 	---
-	void main() {
+	int main() {
 		// Internal resolution of the images (“frames”) we will render.
 		// From the PixelPresenter’s perspective,
 		// these are the “fully-rendered frames” that it will blit to screen.
@@ -53,8 +53,9 @@
 		// This variable will be “shared” across events (and frames).
 		int blueChannel = 0;
 
-		// Run the eventloop
-		presenter.eventLoop(delegate() {
+		// Run the eventloop.
+		// The callback delegate will get executed every ~16ms (≙ ~60FPS) and schedule a redraw.
+		return presenter.eventLoop(16, delegate() {
 			// Update the frame(buffer) here…
 
 			// Construct an RGB color value.
@@ -107,7 +108,8 @@
 		ubyte color = 0;
 		byte colorDelta = 2;
 
-		// Run the eventloop
+		// Run the eventloop.
+		// Note how the callback delegate returns a [LoopCtrl] instance.
 		presenter.eventLoop(delegate() {
 			// Determine the start and end index of the current line in the
 			// framebuffer.
@@ -130,6 +132,9 @@
 			++line;
 			if (line == resolution.height)
 				line = 0;
+
+			// Schedule a redraw in ~16ms.
+			return LoopCtrl.redrawIn(16);
 		}, delegate(MouseEvent ev) {
 			// toggle fullscreen mode on double-click
 			if (ev.doubleClick) {
@@ -148,7 +153,6 @@ import arsd.simpledisplay;
 	## TODO
 
 	- Complete documentation
-	- Usage example(s)
 	- Additional renderer implementations:
 		- a `ScreenPainter`-based renderer
 		- a legacy OpenGL renderer (maybe)
@@ -156,9 +160,10 @@ import arsd.simpledisplay;
 	- Minimum window size
 		- or something similar
 		- to ensure `Scaling.integer` doesn’t break “unexpectedly”
-	- Fix timing
  */
 
+private enum hasTimer = is(Timer == class);
+
 ///
 alias Pixel = Color;
 
@@ -441,18 +446,33 @@ interface PixelRenderer {
 		Called once during setup.
 		Perform initialization tasks in here.
 
+		$(NOTE
+			The final thing a setup function does
+			is usually to call `reconfigure()` on the renderer.
+		)
+
 		Params:
 			pro = Pointer to the [PresenterObjects] of the presenter. To be stored for later use.
 	 +/
 	public void setup(PresenterObjects* pro);
 
 	/++
-		Reconfigure renderer
+		Reconfigures the renderer
 
 		Called upon configuration changes.
 		The new config can be found in the [PresenterObjects] received during `setup()`.
 	 +/
 	public void reconfigure();
+
+	/++
+		Schedules a redraw
+	 +/
+	public void redrawSchedule();
+
+	/++
+		Triggers a redraw
+	 +/
+	public void redrawNow();
 }
 
 ///
@@ -657,6 +677,14 @@ final class OpenGL3PixelRenderer : PixelRenderer {
 		_clear = true;
 	}
 
+	void redrawSchedule() {
+		_pro.window.redrawOpenGlSceneSoon();
+	}
+
+	void redrawNow() {
+		_pro.window.redrawOpenGlSceneNow();
+	}
+
 	private {
 		static immutable GLfloat[] vertices = [
 			//dfmt off
@@ -677,6 +705,32 @@ final class OpenGL3PixelRenderer : PixelRenderer {
 	}
 }
 
+///
+struct LoopCtrl {
+	int interval; /// in milliseconds
+	bool redraw; ///
+
+	///
+	@disable this();
+
+@safe pure nothrow @nogc:
+
+	private this(int interval, bool redraw) {
+		this.interval = interval;
+		this.redraw = redraw;
+	}
+
+	///
+	static LoopCtrl waitFor(int intervalMS) {
+		return LoopCtrl(intervalMS, false);
+	}
+
+	///
+	static LoopCtrl redrawIn(int intervalMS) {
+		return LoopCtrl(intervalMS, true);
+	}
+}
+
 /++
  +/
 final class PixelPresenter {
@@ -684,6 +738,10 @@ final class PixelPresenter {
 	private {
 		PresenterObjects* _pro;
 		PixelRenderer _renderer;
+
+		static if (hasTimer) {
+			Timer _timer;
+		}
 	}
 
 	// ctors
@@ -772,15 +830,68 @@ final class PixelPresenter {
 	// public functions
 	public {
 
-		///
-		int eventLoop(T...)(T eventHandlers) if (T.length == 0 || is(T[0] == delegate)) {
+		/++
+			Runs the event loop (with a pulse timer)
+
+			A redraw will be scheduled automatically each pulse.
+		 +/
+		int eventLoop(T...)(long pulseTimeout, void delegate() onPulse, T eventHandlers) {
+			// run event-loop with pulse timer
 			return _pro.window.eventLoop(
-				16, // ~60 FPS
-				delegate() { eventHandlers[0](); _pro.window.redrawOpenGlSceneSoon(); },
-				eventHandlers[1 .. $],
+				pulseTimeout,
+				delegate() { onPulse(); this.scheduleRedraw(); },
+				eventHandlers,
 			);
 		}
 
+		//dfmt off
+		/++
+			Runs the event loop
+
+			Redraws have to manually scheduled through [scheduleRedraw] when using this overload.
+		 +/
+		int eventLoop(T...)(T eventHandlers) if (
+			(T.length == 0) || (is(T[0] == delegate) && !is(typeof(() { return T[0](); }()) == LoopCtrl))
+		) {
+			return _pro.window.eventLoop(eventHandlers);
+		}
+		//dfmt on
+
+		static if (hasTimer) {
+			/++
+				Runs the event loop
+				with [LoopCtrl] timing mechanism
+			 +/
+			int eventLoop(T...)(LoopCtrl delegate() callback, T eventHandlers) {
+				if (callback !is null) {
+					LoopCtrl prev = LoopCtrl(1, true);
+
+					_timer = new Timer(prev.interval, delegate() {
+						// redraw if requested by previous ctrl message
+						if (prev.redraw) {
+							_renderer.redrawNow();
+							prev.redraw = false; // done
+						}
+
+						// execute callback
+						const LoopCtrl ctrl = callback();
+
+						// different than previous ctrl message?
+						if (ctrl.interval != prev.interval) {
+							// update timer
+							_timer.changeTime(ctrl.interval);
+						}
+
+						// save ctrl message
+						prev = ctrl;
+					});
+				}
+
+				// run event-loop
+				return _pro.window.eventLoop(0, eventHandlers);
+			}
+		}
+
 		///
 		PixelBuffer framebuffer() @safe pure nothrow @nogc {
 			return _pro.framebuffer;
@@ -793,6 +904,11 @@ final class PixelPresenter {
 			//_renderer.reconfigure(config);
 		}
 
+		///
+		void scheduleRedraw() {
+			_renderer.redrawSchedule();
+		}
+
 		///
 		bool isFullscreen() {
 			return _pro.window.fullscreen;

From d57ecc9cb97ffd8c79fa112cd71e469f53552825 Mon Sep 17 00:00:00 2001
From: Elias Batek <desisma@heidel.beer>
Date: Sun, 24 Dec 2023 00:01:48 +0100
Subject: [PATCH 12/16] Improve documentation

---
 pixelpresenter.d | 20 ++++++++++++--------
 1 file changed, 12 insertions(+), 8 deletions(-)

diff --git a/pixelpresenter.d b/pixelpresenter.d
index 52b2e41..3977ca5 100644
--- a/pixelpresenter.d
+++ b/pixelpresenter.d
@@ -160,10 +160,11 @@ import arsd.simpledisplay;
 	- Minimum window size
 		- or something similar
 		- to ensure `Scaling.integer` doesn’t break “unexpectedly”
+	- Add my fast&accurate int8 alphaBlending()
+		- add unittests
+		- benchmark vs. CPUblit
  */
 
-private enum hasTimer = is(Timer == class);
-
 ///
 alias Pixel = Color;
 
@@ -179,6 +180,9 @@ alias Point = arsd.color.Point;
 // verify assumption(s)
 static assert(Pixel.sizeof == uint.sizeof);
 
+// is the Timer class available on this platform?
+private enum hasTimer = is(Timer == class);
+
 /// casts value `v` to type `T`
 auto ref T typeCast(T, S)(auto ref S v) {
 	return cast(T) v;
@@ -266,13 +270,13 @@ struct PixelBuffer {
 		return (width * int(Pixel.sizeof));
 	}
 
-	/// Clears the buffers contents (by setting each pixel to the same color)
+	/// Clears the buffer’s contents (by setting each pixel to the same color)
 	void clear(Pixel value) {
 		data[] = value;
 	}
 }
 
-@safe pure nothrow @nogc {
+private @safe pure nothrow @nogc {
 
 	// keep aspect ratio (contain)
 	bool karContainNeedsDownscaling(const Size drawing, const Size canvas) {
@@ -361,10 +365,10 @@ enum Scaling {
 	keepAspectRatio = contain, ///
 
 	// CSS `object-fit` style aliases
-	cssNone = none, ///
-	cssContain = contain, ///
-	cssFill = stretch, ///
-	cssCover = cover, ///
+	cssNone = none, /// equivalent CSS: `object-fit: none;`
+	cssContain = contain, /// equivalent CSS: `object-fit: contain;`
+	cssFill = stretch, /// equivalent CSS: `object-fit: fill;`
+	cssCover = cover, /// equivalent CSS: `object-fit: cover;`
 }
 
 ///

From f9e029b9cdcb7f62b01a5cfee8adcfc4f19799a1 Mon Sep 17 00:00:00 2001
From: Elias Batek <desisma@heidel.beer>
Date: Sun, 24 Dec 2023 00:09:21 +0100
Subject: [PATCH 13/16] Rebrand PixelPresenter to PixmapPresenter

---
 README.md                             |  2 +-
 dub.json                              | 10 +++++-----
 package.d                             |  2 +-
 pixelpresenter.d => pixmappresenter.d | 18 +++++++++---------
 4 files changed, 16 insertions(+), 16 deletions(-)
 rename pixelpresenter.d => pixmappresenter.d (98%)

diff --git a/README.md b/README.md
index e048ba3..e061c88 100644
--- a/README.md
+++ b/README.md
@@ -22,7 +22,7 @@ Future release, likely May 2024 or later.
 
 Nothing is planned for it at this time.
 
-arsd.pixelpresenter was added.
+arsd.pixmappresenter was added.
 
 ## 11.0
 
diff --git a/dub.json b/dub.json
index 4be70a1..ca5268c 100644
--- a/dub.json
+++ b/dub.json
@@ -682,17 +682,17 @@
 			}
 		},
 		{
-			"name": "pixelpresenter",
+			"name": "pixmappresenter",
 			"description": "High-level display library. Designed to blit fully-rendered frames to the screen.",
 			"targetType": "library",
-			"sourceFiles": ["pixelpresenter.d"],
+			"sourceFiles": ["pixmappresenter.d"],
 			"dependencies": {
 				"arsd-official:color_base":"*",
 				"arsd-official:simpledisplay":"*"
 			},
-			"dflags-dmd": ["-mv=arsd.pixelpresenter=$PACKAGE_DIR/pixelpresenter.d"],
-			"dflags-ldc": ["--mv=arsd.pixelpresenter=$PACKAGE_DIR/pixelpresenter.d"],
-			"dflags-gdc": ["-fmodule-file=arsd.pixelpresenter=$PACKAGE_DIR/pixelpresenter.d"]
+			"dflags-dmd": ["-mv=arsd.pixmappresenter=$PACKAGE_DIR/pixmappresenter.d"],
+			"dflags-ldc": ["--mv=arsd.pixmappresenter=$PACKAGE_DIR/pixmappresenter.d"],
+			"dflags-gdc": ["-fmodule-file=arsd.pixmappresenter=$PACKAGE_DIR/pixmappresenter.d"]
 		},
 		{
 			"name": "ttf",
diff --git a/package.d b/package.d
index 78f6504..c55a662 100644
--- a/package.d
+++ b/package.d
@@ -86,7 +86,7 @@
 		$(H4 $(ID desktop-game) Games)
 			See [arsd.simpledisplay] and [arsd.gamehelpers].
 
-			Check out [arsd.pixelpresenter] for old-skool games that blit fully-rendered frames to the screen.
+			Check out [arsd.pixmappresenter] for old-skool games that blit fully-rendered frames to the screen.
 
 		$(H4 $(ID desktop-gui) GUIs)
 			See [arsd.minigui], [arsd.nanovega], and also: https://github.com/drug007/nanogui
diff --git a/pixelpresenter.d b/pixmappresenter.d
similarity index 98%
rename from pixelpresenter.d
rename to pixmappresenter.d
index 3977ca5..f1b096c 100644
--- a/pixelpresenter.d
+++ b/pixmappresenter.d
@@ -1,10 +1,10 @@
 /+
-	== pixelpresenter ==
+	== pixmappresenter ==
 	Copyright Elias Batek (0xEAB) 2023.
 	Distributed under the Boost Software License, Version 1.0.
  +/
 /++
-	$(B Pixel Presenter) is a high-level display library for one specific scenario:
+	$(B Pixmap Presenter) is a high-level display library for one specific scenario:
 	Blitting fully-rendered frames to the screen.
 
 	This is useful for software-rendered applications.
@@ -35,7 +35,7 @@
 	---
 	int main() {
 		// Internal resolution of the images (“frames”) we will render.
-		// From the PixelPresenter’s perspective,
+		// From the PixmapPresenter’s perspective,
 		// these are the “fully-rendered frames” that it will blit to screen.
 		// They may be up- & down-scaled to the window’s actual size
 		// (according to the chosen scaling mode) by the presenter.
@@ -44,7 +44,7 @@
 		// Let’s create a new presenter.
 		// (For more fine-grained control there’s also a constructor overload that
 		// accepts a [PresenterConfig] instance).
-		auto presenter = new PixelPresenter(
+		auto presenter = new PixmapPresenter(
 			"Demo",         // window title
 			resolution,     // internal resolution
 			Size(960, 480), // initial window size (optional; default: =resolution)
@@ -75,7 +75,7 @@
 	### Advanced example
 
 	---
-	import arsd.pixelpresenter;
+	import arsd.pixmappresenter;
 	import arsd.simpledisplay : MouseEvent;
 
 	void main() {
@@ -99,7 +99,7 @@
 		cfg.renderer.background = ColorF(Pixel.white);
 
 		// Let’s instantiate a new presenter with the previously created config.
-		auto presenter = new PixelPresenter(cfg);
+		auto presenter = new PixmapPresenter(cfg);
 
 		// Start with a green frame, so we can easily observe what’s going on.
 		presenter.framebuffer.clear(rgb(0x00, 0xDD, 0x00));
@@ -144,7 +144,7 @@
 	}
 	---
  +/
-module arsd.pixelpresenter;
+module arsd.pixmappresenter;
 
 import arsd.color;
 import arsd.simpledisplay;
@@ -414,7 +414,7 @@ struct PresenterConfig {
 
 	///
 	static struct Window {
-		string title = "ARSD Pixel Presenter";
+		string title = "ARSD Pixmap Presenter";
 		Size size;
 	}
 }
@@ -737,7 +737,7 @@ struct LoopCtrl {
 
 /++
  +/
-final class PixelPresenter {
+final class PixmapPresenter {
 
 	private {
 		PresenterObjects* _pro;

From 61e7df3e44a962555e16f1b169f63b07b8246d00 Mon Sep 17 00:00:00 2001
From: Elias Batek <desisma@heidel.beer>
Date: Sun, 24 Dec 2023 00:12:00 +0100
Subject: [PATCH 14/16] Improve pixmappresenter example code

---
 pixmappresenter.d | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/pixmappresenter.d b/pixmappresenter.d
index f1b096c..54b8d53 100644
--- a/pixmappresenter.d
+++ b/pixmappresenter.d
@@ -33,7 +33,7 @@
 	then jumps back to black and the process repeats.
 
 	---
-	int main() {
+	void main() {
 		// Internal resolution of the images (“frames”) we will render.
 		// From the PixmapPresenter’s perspective,
 		// these are the “fully-rendered frames” that it will blit to screen.
@@ -55,7 +55,7 @@
 
 		// Run the eventloop.
 		// The callback delegate will get executed every ~16ms (≙ ~60FPS) and schedule a redraw.
-		return presenter.eventLoop(16, delegate() {
+		presenter.eventLoop(16, delegate() {
 			// Update the frame(buffer) here…
 
 			// Construct an RGB color value.
@@ -78,7 +78,7 @@
 	import arsd.pixmappresenter;
 	import arsd.simpledisplay : MouseEvent;
 
-	void main() {
+	int main() {
 		// Internal resolution of the images (“frames”) we will render.
 		// For further details, check out the previous example.
 		const resolution = Size(240, 120);
@@ -110,7 +110,7 @@
 
 		// Run the eventloop.
 		// Note how the callback delegate returns a [LoopCtrl] instance.
-		presenter.eventLoop(delegate() {
+		return presenter.eventLoop(delegate() {
 			// Determine the start and end index of the current line in the
 			// framebuffer.
 			immutable x0 = line * resolution.width;

From a4ed17e0a1dec36a80ab09a8cffd3fd1e1a11d53 Mon Sep 17 00:00:00 2001
From: Elias Batek <desisma@heidel.beer>
Date: Sun, 24 Dec 2023 00:16:53 +0100
Subject: [PATCH 15/16] Add minimal PixmapPresenter example

---
 pixmappresenter.d | 10 ++++++++++
 1 file changed, 10 insertions(+)

diff --git a/pixmappresenter.d b/pixmappresenter.d
index 54b8d53..4e30740 100644
--- a/pixmappresenter.d
+++ b/pixmappresenter.d
@@ -72,6 +72,16 @@
 	}
 	---
 
+	### Minimal example
+
+	---
+	void main() {
+		auto pmp = new PixmapPresenter("My Pixmap App", Size(640, 480));
+		pmp.framebuffer.clear(rgb(0xFF, 0x00, 0x99));
+		pmp.eventLoop();
+	}
+	---
+
 	### Advanced example
 
 	---

From 63578639d93c60060855d02838f6e0517730a72b Mon Sep 17 00:00:00 2001
From: Elias Batek <desisma@heidel.beer>
Date: Sun, 24 Dec 2023 00:30:17 +0100
Subject: [PATCH 16/16] Cleanup to-do list

---
 pixmappresenter.d | 3 ---
 1 file changed, 3 deletions(-)

diff --git a/pixmappresenter.d b/pixmappresenter.d
index 4e30740..0985b69 100644
--- a/pixmappresenter.d
+++ b/pixmappresenter.d
@@ -170,9 +170,6 @@ import arsd.simpledisplay;
 	- Minimum window size
 		- or something similar
 		- to ensure `Scaling.integer` doesn’t break “unexpectedly”
-	- Add my fast&accurate int8 alphaBlending()
-		- add unittests
-		- benchmark vs. CPUblit
  */
 
 ///