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 */ ///