Creating Custom Draw Targets in GFX

Updated on 2022-03-25

A more in-depth guide to creating drivers and other custom draw targets for GFX

Introduction

GFX is a feature rich, high performance graphics library for IoT devices. It uses a novel and extremely flexible method of consuming and producing graphical data. This article aims to show you how to create your own consumers and producers. With this article, you can create new device drivers for display hardware, or your own custom draw targets to transform, store, and/or otherwise manipulate graphical data.

GFX

Background

Major Concepts

Draw Targets

GFX presents the concept of draw targets, which are flexible objects that can serve as a source for pixel data, and/or a destination for drawing operations.

There are two types of draw target:

Draw sources present pixel data to downstream consumers, like draw::bitmap<>(), which draws from a draw source to a draw destination. The methods a draw source supports are covered under Capabilities.

Draw destinations provide a canvas for drawing operations. You can use things like draw::line<>(), draw::filled_rectangle<>() and draw::bitmap<>() to draw onto them.

All draw destinations must support gfx::gfx_result clear(const gfx::rect16& bounds) which clears a portion of the draw destination, gfx::gfx_result point(gfx::point16 location, pixel_type color) which draws a single pixel and gfx::gfx_result fill(const gfx::rect16 bounds, pixel_type color) which fills a rectangular region with the specified color.

Every driver for display hardware is a draw destination. Often, they are also draw sources, which enables reading the pixel data back from the frame buffer, and facilitates features like alpha blending.

For another example, a bitmap<> is a draw source and a draw destination because it holds graphical data to be drawn to and used later.

A const_bitmap<> is a draw source only since it cannot be changed. Very few draw targets are not also draw destinations.

Sometimes, a driver will provide no way to read back pixel data due to hardware limitations. An example is the SSD1351 display controller over SPI. The driver for this device is a draw destination only.

Pixel Type

Every draw target has an associated pixel type. This governs the format of the graphical data in memory or possibly in the device's hardware framebuffer. It also basically dictates the "quality" of the display. A grayscale device gsc_pixel<8> for example, will obviously have less fidelity than an rgb_pixel<16>, but also takes half the memory. You must expose a pixel_type member alias from your draw target type. An example would be using pixel_type = gfx::rgb_pixel<16>; for the 16-bit RGB pixel format.

Palette Type

Some draw targets use indexed pixels and a palette. A good example of this is a color e-paper display. They typically have a very limited primary color palette, 7-colors - including black and white, being the most that I've seen.

If you use an indexed pixel type, you must expose a palette_type which indicates the type of the palette you are going to use. For e-paper displays, this is typically a custom type with hard coded color values. You would typically hold an instance of this type around in your target. We'll cover implementing them downthread. An example declaration would be using palette_type = mypalette;.

In addition to this, you must also provide a way to access the instance of the palette associated with this draw target. You do so by exposing a palette method with the signature palette_type* palette() const. Typically, you'd simply return the address of your palette_type instance.

Capabilities

Each target must expose a type alias for an instantiation of the gfx::gfx_caps<> template. This instantiation dictates what features/members this target exposes. Most of the features are for optimizing draw destinations though a few effect draw sources: An example would be using caps = gfx::gfx_caps< false,false,false,false,true,true,false>;.

Let's explore the booleans in order from left to right:

Blt

If true, this target has a uint8_t* begin() and uint8_t* end() method that return the beginning and just past the end of the buffer that holds the pixel data, in the binary format dictated by pixel_type. These members expose the data directly so that it can be written as well as read. Blting is typically the most direct and efficient way of transferring graphical data.

Async

If true, this is a draw target that supports asynchronous versions of its graphics operations. In practice, this means many methods have a corollary method ending in _async that performs the operation asynchronously if possible, though this is not guaranteed. It also exposes a gfx::gfx_result wait_all_async() method that waits for any and all pending asynchronous operations to complete. It's fine to forward to a non-asynchronous method if the particular operation or conditions do not support completing it asynchronously.

Batch

This draw destination supports batched writes. Batched writes are writes wherein a rectangular region of the display is selected and then filled with pixels left to right, top to bottom without specifying their individual coordinates. This is a way to cut down on bus traffic. The draw destination exposes gfx::gfx_result begin_batch(const gfx::rect16& bounds) which starts a batch operation, specifying the destination window. It also exposes gfx::gfx_result write_batch(pixel_type color) which writes a single pixel at the current batch cursor position within the window, and gfx::gfx_result commit_batch() which commits the pending batch operation. If another operation is started during a batch, the batch is committed. It's important to balance these calls, and to write out every pixel necessary to fill the window. If you do not, the results are undefined.

Copy From

This draw destination supports an efficient mechanism for copying pixel data from a draw source. It is template method with the signature:

template<typename Source>
gfx::gfx_result copy_from(const gfx::rect16& src_rect,
                        const Source& src,
                        gfx::point16 location)

It will copy the source rectangle to this draw destination at the specified location within the draw surface. This method should use the fastest possible means for retrieving data available given the parameters of copy_from<>() and capabilities of the draw source. For example, blting should be done if possible.

Suspend

This draw destination supports double buffering by way of gfx::gfx_result suspend() and gfx::gfx_result resume(bool force=false). When the draws are suspended, no updates to the display occur, but drawing continues to modify the frame buffer. When resumed, the display is updated. This can reduce flicker, and for an in memory frame buffer, that used by the SSD1306, it can sometimes decrease bus traffic, improving performance. Furthermore, it is necessary for e-paper displays since they are not able to animate.

When suspend is called, typically an internal counter is incremented. When resume is called, that counter is decremented, or if force is true it is set to zero. If the counter reaches zero, the display is updated with the contents of the frame buffer. The reason a counter is needed is because suspend() and resume() calls must be able to be nested.

Suspend and resume are always implemented when the frame buffer is in local RAM.

Read

This target is a draw source and exposes gfx::gfx_result point(point16 location, pixel_type* out_pixel) const. This method returns the color at the given location. Unlike writing methods, this method is not required to initialize the device if it is not already initialized.

Copy To

This draw source supports efficient copying of pixel data from this instance to draw destination. It should use the most efficient means possible, with the exception of using the destination's copy_from<>() method. This signature is as follows:

template<typename Source>
gfx::gfx_result copy_from(const gfx::rect16& src_rect,
                        const Source& src,
                        gfx::point16 location)

This draw source must also support Read if it supports this feature.

Metrics

All draw targets support a way to retrieve their dimensions and their bounding rectangle, anchored to (0, 0). The method signatures are size16 dimensions() const and rect16 bounds() const.

Notable Behavior

If a draw target must be initialized, it must do so on first call to any of the draw destination methods, including suspend() and resume(). Draw source methods do not have this requirement. The copy_from<>() and copy_to<>() methods are templates so that they can take various types of targets and pixel types. They must do automatic conversion where necessary.

Coding this Mess

In order to provide a concrete example, we'll be exploring the htcw_ili9341 driver since it has most of the major capabilities covered above. We'll start with the code and then give it a brief going over after the fact. It's quite lengthy, but most of that is due to the actual communication with the hardware, not the GFX bindings themselves, with the exception of the copy method helpers.

#include <Arduino.h>

#include <gfx_core.hpp>
#include <gfx_draw_helpers.hpp>
#include <gfx_palette.hpp>
#include <gfx_pixel.hpp>
#include <gfx_positioning.hpp>
#include <tft_driver.hpp>
namespace arduino {
// the ILI9341 driver for GFX
template <int8_t PinDC, int8_t PinRst, int8_t PinBL, typename Bus,
          uint8_t Rotation = 0, bool BacklightHigh = false,
          unsigned int WriteSpeedPercent = 200,
          unsigned int ReadSpeedPercent = WriteSpeedPercent>
struct ili9341 final {
    constexpr static const int8_t pin_dc = PinDC;
    constexpr static const int8_t pin_rst = PinRst;
    constexpr static const int8_t pin_bl = PinBL;
    constexpr static const uint8_t rotation = Rotation & 3;
    constexpr static const size_t max_dma_size = 320 * 240 * 2;
    constexpr static const bool backlight_high = BacklightHigh;
    constexpr static const float write_speed_multiplier =
        (WriteSpeedPercent / 100.0);
    constexpr static const float read_speed_multiplier =
        (ReadSpeedPercent / 100.0);
    using type = ili9341;
    // this is used for high level device communication
    using driver = tft_driver<PinDC, PinRst, PinBL, Bus>;
    // this is used for low level device communication
    using bus = Bus;
    // GFX binding for the pixel format. Natively we use 16-bit RGB
    using pixel_type = gfx::rgb_pixel<16>;
    // GFX binding for capabilities. Note that some of them, like async
    // or reading depend on if the bus supports it.
    using caps = gfx::gfx_caps<false, (bus::dma_size > 0), true, true, false,
                               bus::readable, true>;
    ili9341()
        : m_initialized(false), m_dma_initialized(false), m_in_batch(false) {}
    // don't need virtual due to final
    ~ili9341() {
        if (m_dma_initialized) {
            bus::deinitialize_dma();
        }
        if (m_initialized) {
            driver::deinitialize();
        }
    }
    // initialize the driver
    bool initialize() {
        if (!m_initialized) {
            // initialize the TFT driver
            if (driver::initialize()) {
                // we're telling it the driver is in initialization mode
                // not necessary for this driver, but we do it for
                // completeness
                bus::begin_initialization();
                // set the speed of the bus
                bus::set_speed_multiplier(write_speed_multiplier);
                // tell it we're writing
                bus::begin_write();
                // being a transaction
                bus::begin_transaction();
                // send a bunch of stuff
                driver::send_command(0xEF);
                driver::send_data8(0x03);
                driver::send_data8(0x80);
                driver::send_data8(0x02);

                driver::send_command(0xCF);
                driver::send_data8(0x00);
                driver::send_data8(0XC1);
                driver::send_data8(0X30);

                driver::send_command(0xED);
                driver::send_data8(0x64);
                driver::send_data8(0x03);
                driver::send_data8(0X12);
                driver::send_data8(0X81);

                driver::send_command(0xE8);
                driver::send_data8(0x85);
                driver::send_data8(0x00);
                driver::send_data8(0x78);

                driver::send_command(0xCB);
                driver::send_data8(0x39);
                driver::send_data8(0x2C);
                driver::send_data8(0x00);
                driver::send_data8(0x34);
                driver::send_data8(0x02);

                driver::send_command(0xF7);
                driver::send_data8(0x20);

                driver::send_command(0xEA);
                driver::send_data8(0x00);
                driver::send_data8(0x00);
                driver::send_command(0xC0);  // Power control
                driver::send_data8(0x23);    // VRH[5:0]

                driver::send_command(0xC1);  // Power control
                driver::send_data8(0x10);    // SAP[2:0];BT[3:0]

                driver::send_command(0xC5);  // VCM control
                driver::send_data8(0x3e);
                driver::send_data8(0x28);

                driver::send_command(0xC7);  // VCM control2
                driver::send_data8(0x86);    //--

                driver::send_command(0x36);       // Memory Access Control
                driver::send_data8(0x40 | 0x08);  // Rotation 0 (portrait mode)

                driver::send_command(0x3A);
                driver::send_data8(0x55);

                driver::send_command(0xB1);
                driver::send_data8(0x00);
                driver::send_data8(
                    0x13);  // 0x18 79Hz, 0x1B default 70Hz, 0x13 100Hz

                driver::send_command(0xB6);  // Display Function Control
                driver::send_data8(0x08);
                driver::send_data8(0x82);
                driver::send_data8(0x27);

                driver::send_command(0xF2);  // 3Gamma Function Disable
                driver::send_data8(0x00);

                driver::send_command(0x26);  // Gamma curve selected
                driver::send_data8(0x01);

                driver::send_command(0xE0);  // Set Gamma
                driver::send_data8(0x0F);
                driver::send_data8(0x31);
                driver::send_data8(0x2B);
                driver::send_data8(0x0C);
                driver::send_data8(0x0E);
                driver::send_data8(0x08);
                driver::send_data8(0x4E);
                driver::send_data8(0xF1);
                driver::send_data8(0x37);
                driver::send_data8(0x07);
                driver::send_data8(0x10);
                driver::send_data8(0x03);
                driver::send_data8(0x0E);
                driver::send_data8(0x09);
                driver::send_data8(0x00);

                driver::send_command(0xE1);  // Set Gamma
                driver::send_data8(0x00);
                driver::send_data8(0x0E);
                driver::send_data8(0x14);
                driver::send_data8(0x03);
                driver::send_data8(0x11);
                driver::send_data8(0x07);
                driver::send_data8(0x31);
                driver::send_data8(0xC1);
                driver::send_data8(0x48);
                driver::send_data8(0x08);
                driver::send_data8(0x0F);
                driver::send_data8(0x0C);
                driver::send_data8(0x31);
                driver::send_data8(0x36);
                driver::send_data8(0x0F);

                driver::send_command(0x11);  // Exit Sleep
                // done with the transaction
                bus::end_transaction();
                // done writing
                bus::end_write();
                delay(120);
                bus::begin_write();
                bus::begin_transaction();
                driver::send_command(0x29);  // Display on
                bus::end_transaction();
                bus::end_write();
                bus::end_initialization();
                bus::begin_write();
                bus::begin_transaction();
                // set the rotation to what we selected
                apply_rotation();
                bus::end_transaction();
                bus::end_write();
                // enable the backlight
                if (pin_bl != -1) {
                    pinMode(pin_bl, OUTPUT);
                    digitalWrite(pin_bl, backlight_high);
                }

                m_initialized = true;
            }
        }
        return m_initialized;
    }
    // the dimensions
    inline gfx::size16 dimensions() const {
        return rotation & 1 ? gfx::size16(320, 240) : gfx::size16(240, 320);
    }
    // the bounding rectangle
    inline gfx::rect16 bounds() const { return dimensions().bounds(); }
    // draws a pixel
    inline gfx::gfx_result point(gfx::point16 location, pixel_type color) {
        return fill({location.x, location.y, location.x, location.y}, color);
    }
    // draws a pixel asynchronously
    inline gfx::gfx_result point_async(gfx::point16 location,
                                       pixel_type color) {
        return point(location, color);
    }
    // retrieves a pixel
    gfx::gfx_result point(gfx::point16 location, pixel_type* out_color) const {
        if (out_color == nullptr) return gfx::gfx_result::invalid_argument;
        if (!m_initialized || m_in_batch) return gfx::gfx_result::invalid_state;
        if (!bounds().intersects(location)) {
            *out_color = pixel_type();
            return gfx::gfx_result::success;
        }
        bus::dma_wait();
        bus::set_speed_multiplier(read_speed_multiplier);
        bus::begin_read();
        bus::cs_low();
        set_window({location.x, location.y, location.x, location.y}, true);
        bus::direction(INPUT);
        bus::read_raw8();  // throw away
        out_color->native_value = ((bus::read_raw8() & 0xF8) << 8) |
                                  ((bus::read_raw8() & 0xFC) << 3) |
                                  (bus::read_raw8() >> 3);
        bus::cs_high();
        bus::end_read();
        bus::set_speed_multiplier(write_speed_multiplier);
        bus::direction(OUTPUT);
        return gfx::gfx_result::success;
    }
    // fills a rectangular region
    gfx::gfx_result fill(const gfx::rect16& bounds, pixel_type color) {
        if (!initialize())
            return gfx::gfx_result::device_error;
        else
            bus::dma_wait();
        gfx::gfx_result rr = commit_batch();

        if (rr != gfx::gfx_result::success) {
            return rr;
        }
        if (!bounds.intersects(this->bounds())) return gfx::gfx_result::success;
        const gfx::rect16 r = bounds.normalize().crop(this->bounds());
        bus::begin_write();
        bus::begin_transaction();
        set_window(r);
        bus::write_raw16_repeat(color.native_value,
                                (r.x2 - r.x1 + 1) * (r.y2 - r.y1 + 1));
        bus::end_transaction();
        bus::end_write();
        return gfx::gfx_result::success;
    }
    // fills a rectangular region asynchronously
    inline gfx::gfx_result fill_async(const gfx::rect16& bounds,
                                      pixel_type color) {
        return fill(bounds, color);
    }
    // clears a region
    inline gfx::gfx_result clear(const gfx::rect16& bounds) {
        return fill(bounds, pixel_type());
    }
    // clears a region asynchronously
    inline gfx::gfx_result clear_async(const gfx::rect16& bounds) {
        return clear(bounds);
    }
    // copies from a draw source
    template <typename Source>
    inline gfx::gfx_result copy_from(const gfx::rect16& src_rect,
                                     const Source& src, gfx::point16 location) {
        if (!initialize()) return gfx::gfx_result::device_error;
        gfx::gfx_result rr = commit_batch();
        if (rr != gfx::gfx_result::success) {
            return rr;
        }
        return copy_from_impl(src_rect, src, location, false);
    }
    // copies from a draw source asynchronously
    template <typename Source>
    inline gfx::gfx_result copy_from_async(const gfx::rect16& src_rect,
                                           const Source& src,
                                           gfx::point16 location) {
        if (!initialize()) return gfx::gfx_result::device_error;
        gfx::gfx_result rr = commit_batch();
        if (rr != gfx::gfx_result::success) {
            return rr;
        }
        if (!m_dma_initialized) {
            if (!bus::initialize_dma()) return gfx::gfx_result::device_error;
            m_dma_initialized = true;
        }
        return copy_from_impl(src_rect, src, location, true);
    }
    // copies to a draw destination
    template <typename Destination>
    inline gfx::gfx_result copy_to(const gfx::rect16& src_rect,
                                   Destination& dst,
                                   gfx::point16 location) const {
        if (!src_rect.intersects(bounds())) return gfx::gfx_result::success;
        gfx::rect16 srcr = src_rect.crop(bounds());
        gfx::rect16 dstr =
            gfx::rect16(location, srcr.dimensions()).crop(dst.bounds());
        srcr = gfx::rect16(srcr.location(), dstr.dimensions());
        return copy_to_helper<Destination,
                              !(pixel_type::template has_channel_names<
                                  gfx::channel_name::A>::value)>::copy_to(*this,
                                                                          srcr,
                                                                          dst,
                                                                          dstr);
    }
    // copies to a draw destination asynchronously
    template <typename Destination>
    inline gfx::gfx_result copy_to_async(const gfx::rect16& src_rect,
                                         Destination& dst,
                                         gfx::point16 location) const {
        return copy_to(src_rect, dst, location);
    }
    // commits the current batch
    gfx::gfx_result commit_batch() {
        if (m_in_batch) {
            bus::end_transaction();
            bus::end_write();
            m_in_batch = false;
        }
        return gfx::gfx_result::success;
    }
    // commits the batch asynchronously
    inline gfx::gfx_result commit_batch_async() {
        return commit_batch();
    }
    // begins a batch operation
    gfx::gfx_result begin_batch(const gfx::rect16& bounds) {
        if (!initialize()) return gfx::gfx_result::device_error;
        gfx::gfx_result rr = commit_batch();
        if (rr != gfx::gfx_result::success) {
            return rr;
        }
        const gfx::rect16 r = bounds.normalize();
        bus::begin_write();
        bus::begin_transaction();
        set_window(r);
        m_in_batch = true;
        return gfx::gfx_result::success;
    }
    // begins a batch asynchronously
    inline gfx::gfx_result begin_batch_async(const gfx::rect16& bounds) {
        return begin_batch(bounds);
    }
    // write a pixel at the current cursor position within the
    // batch window
    gfx::gfx_result write_batch(pixel_type color) {
        bus::write_raw16(color.native_value);
        return gfx::gfx_result::success;
    }
    // writes a pixel asynchronously at the current cursor
    // position within the batch window
    inline gfx::gfx_result write_batch_async(pixel_type color) {
        return write_batch(color);
    }
    // waits for all pending asynchronous operations
    // to complete
    inline gfx::gfx_result wait_all_async() {
        bus::dma_wait();
        return gfx::gfx_result::success;
    }

   private:
    // initialized yet?
    bool m_initialized;
    // dma initialized?
    bool m_dma_initialized;
    // in a batch?
    bool m_in_batch;
    static void set_window(const gfx::rect16& bounds, bool read = false) {
        bus::busy_check();
        driver::dc_command();
        bus::write_raw8(0x2A);
        driver::dc_data();
        bus::write_raw16(bounds.x1);
        bus::write_raw16(bounds.x2);
        driver::dc_command();
        bus::write_raw8(0x2B);
        driver::dc_data();
        bus::write_raw16(bounds.y1);
        bus::write_raw16(bounds.y2);
        driver::dc_command();
        bus::write_raw8(read ? 0x2E : 0x2C);
        driver::dc_data();
    }
    template <typename Destination, bool AllowBlt = true>
    struct copy_to_helper {
        inline static gfx::gfx_result copy_to(const type& src,
                                              const gfx::rect16& srcr,
                                              Destination& dst,
                                              const gfx::rect16& dstr) {
            if (gfx::helpers::template is_same<typename Destination::pixel_type,
                                               pixel_type>::value &&
                dstr.top_left() == gfx::point16(0, 0)) {
                if (AllowBlt && dstr.width() == srcr.width() &&
                    dstr.height() == srcr.height()) {
                    if (dstr.top_left() == gfx::point16(0, 0)) {
                        return copy_to_fast_helper<Destination>::do_copy(srcr,
                                                                         dst);
                    }
                }
            }

            size_t dy = 0, dye = dstr.height();
            size_t dx, dxe = dstr.width();
            gfx::gfx_result r;
            gfx::helpers::suspender<Destination, Destination::caps::suspend,
                                    false>
                sustok(dst);
            r = gfx::helpers::batcher<Destination, Destination::caps::batch,
                                      false>::begin_batch(dst, dstr, false);
            if (gfx::gfx_result::success != r) {
                return r;
            }
            int sox = srcr.left(), soy = srcr.top();
            int dox = dstr.left(), doy = dstr.top();
            while (dy < dye) {
                dx = 0;

                while (dx < dxe) {
                    pixel_type spx;
                    r = src.point(gfx::point16(sox + dx, soy + dy), &spx);
                    if (gfx::gfx_result::success != r) return r;
                    typename Destination::pixel_type dpx;
                    if (pixel_type::template has_channel_names<
                            gfx::channel_name::A>::value) {
                        r = gfx::helpers::blend_helper<
                            type, Destination,
                            Destination::caps::read>::do_blend(src, spx, dst,
                                                               gfx::point16(
                                                                   dox + dx,
                                                                   doy + dy),
                                                               &dpx);
                        if (gfx::gfx_result::success != r) {
                            return r;
                        }
                    } else {
                        r = convert_palette(dst, src, spx, &dpx, nullptr);
                        if (gfx::gfx_result::success != r) {
                            return r;
                        }
                    }
                    r = gfx::helpers::batcher<
                        Destination, Destination::caps::batch,
                        false>::write_batch(dst,
                                            gfx::point16(dox + dx, doy + dy),
                                            dpx, false);
                    if (gfx::gfx_result::success != r) return r;
                    ++dx;
                }
                ++dy;
            }
            return gfx::helpers::batcher<Destination, Destination::caps::batch,
                                         false>::commit_batch(dst, false);
        }
    };

    template <typename Destination>
    struct copy_to_fast_helper {
        static gfx::gfx_result do_copy(const gfx::rect16& src_rect,
                                       Destination& dst) {
            gfx::rect16 r = src_rect.normalize();
            bool entire = false;
            gfx::size16 bssz = r.dimensions();
            size_t bsz = bssz.width * bssz.height * 3;
            uint8_t* buf = (uint8_t*)malloc(bsz);
            if (buf != nullptr) {
                entire = true;
            } else {
                bsz = bssz.width * 3;
                buf = (uint8_t*)malloc(bsz);
            }
            if (buf != nullptr) {
                free(buf);
                buf = nullptr;
            }
            using batch =
                gfx::helpers::batcher<Destination, Destination::caps::batch,
                                      Destination::caps::async>;
            if (buf == nullptr) {
                gfx::helpers::suspender<Destination, Destination::caps::suspend,
                                        Destination::caps::async>
                    stok(dst, false);
                gfx::gfx_result rr = batch::begin_batch(
                    dst,
                    {0, 0, uint16_t(r.width() - 1), uint16_t(r.height() - 1)},
                    false);
                if (rr != gfx::gfx_result::success) {
                    return rr;
                }
                pixel_type px;
                for (int y = r.y1; y <= r.y2; ++y) {
                    for (int x = r.x1; x <= r.x2; ++x) {
                        uint8_t buf3[3];
                        buf = buf3;
                        fetch_buffer({uint16_t(x), uint16_t(y), uint16_t(x),
                                      uint16_t(y)},
                                     buf, 3);
                        px.native_value = (((*buf) & 0xF8) << 8);
                        ++buf;
                        px.native_value |= (((*buf) & 0xFC) << 3);
                        ++buf;
                        px.native_value |= (*buf >> 3);
                        batch::write_batch(
                            dst, {uint16_t(x - r.x1), uint16_t(y - r.y1)}, px,
                            false);
                    }
                }
                rr = batch::commit_batch(dst, false);
                if (rr != gfx::gfx_result::success) {
                    return rr;
                }
                buf = nullptr;
            } else {
                if (entire) {
                    fetch_buffer(r, buf, bsz);
                    gfx::helpers::suspender<Destination,
                                            Destination::caps::suspend,
                                            Destination::caps::async>
                        stok(dst, false);
                    gfx::gfx_result rr =
                        batch::begin_batch(dst,
                                           {0, 0, uint16_t(r.width() - 1),
                                            uint16_t(r.height() - 1)},
                                           false);
                    if (rr != gfx::gfx_result::success) {
                        free(buf);
                        return rr;
                    }
                    uint8_t* bbuf = buf;
                    while (bsz) {
                        pixel_type px;
                        uint16_t x, y;
                        x = 0;
                        y = 0;
                        px.native_value = (((*bbuf) & 0xF8) << 8);
                        ++bbuf;
                        px.native_value |= (((*bbuf) & 0xFC) << 3);
                        ++bbuf;
                        px.native_value |= (*bbuf >> 3);
                        ++bbuf;
                        ++x;
                        if (x > r.x2 - r.x1) {
                            x = 0;
                            ++y;
                        }
                        batch::write_batch(dst, {x, y}, px, false);
                        bsz -= 3;
                    }
                    rr = batch::commit_batch(dst, false);
                    if (rr != gfx::gfx_result::success) {
                        free(buf);
                        return rr;
                    }
                } else {
                    gfx::helpers::suspender<Destination,
                                            Destination::caps::suspend,
                                            Destination::caps::async>
                        stok(dst, false);
                    for (int y = r.y1; y <= r.y2; ++y) {
                        fetch_buffer(r, buf, bsz);
                        gfx::gfx_result rr =
                            batch::begin_batch(dst,
                                               {0, 0, uint16_t(r.width() - 1),
                                                uint16_t(r.height() - 1)},
                                               false);
                        if (rr != gfx::gfx_result::success) {
                            free(buf);
                            return rr;
                        }
                        size_t bbsz = bsz;
                        uint8_t* bbuf = buf;
                        while (bbsz) {
                            pixel_type px;
                            uint16_t x = 0;
                            px.native_value = (((*bbuf) & 0xF8) << 8);
                            ++bbuf;
                            px.native_value |= (((*bbuf) & 0xFC) << 3);
                            ++bbuf;
                            px.native_value |= (*bbuf >> 3);
                            ++bbuf;
                            ++x;
                            batch::write_batch(dst, {x, uint16_t(y - r.y1)}, px,
                                               false);
                            bbsz -= 3;
                        }
                        rr = batch::commit_batch(dst, false);
                        if (rr != gfx::gfx_result::success) {
                            free(buf);
                            return rr;
                        }
                    }
                }
            }
            if (buf != nullptr) {
                free(buf);
            }
            return gfx::gfx_result::success;
        }

        static void fetch_buffer(const gfx::rect16& r, uint8_t* buf,
                                 size_t len) {
            bus::dma_wait();
            bus::set_speed_multiplier(read_speed_multiplier);
            bus::begin_read();
            bus::cs_low();
            set_window(r, true);
            bus::direction(INPUT);
            bus::read_raw8();  // throw away
            bus::read_raw(buf, len);
            bus::cs_high();
            bus::end_read();
            bus::set_speed_multiplier(write_speed_multiplier);
            bus::direction(OUTPUT);
        }
    };
    template <typename Source, bool Blt>
    struct copy_from_helper {
        static gfx::gfx_result do_draw(type* this_, const gfx::rect16& dstr,
                                       const Source& src, gfx::rect16 srcr,
                                       bool async) {
            uint16_t w = dstr.dimensions().width;
            uint16_t h = dstr.dimensions().height;
            gfx::gfx_result rr;

            rr = this_->begin_batch(dstr);

            if (gfx::gfx_result::success != rr) {
                return rr;
            }
            for (uint16_t y = 0; y < h; ++y) {
                for (uint16_t x = 0; x < w; ++x) {
                    typename Source::pixel_type pp;
                    rr = src.point(gfx::point16(x + srcr.x1, y + srcr.y1), &pp);
                    if (rr != gfx::gfx_result::success) return rr;
                    pixel_type p;
                    rr = gfx::convert_palette_to(src, pp, &p, nullptr);
                    if (gfx::gfx_result::success != rr) {
                        return rr;
                    }

                    rr = this_->write_batch(p);

                    if (gfx::gfx_result::success != rr) {
                        return rr;
                    }
                }
            }

            rr = this_->batch_commit();

            return rr;
        }
    };

    template <typename Source>
    struct copy_from_helper<Source, true> {
        static gfx::gfx_result do_draw(type* this_, const gfx::rect16& dstr,
                                       const Source& src, gfx::rect16 srcr,
                                       bool async) {
            if (async) {
                bus::dma_wait();
            }
            // direct blt
            if (src.bounds().width() == srcr.width() && srcr.x1 == 0) {
                bus::begin_write();
                set_window(dstr);
                if (async) {
                    bus::write_raw_dma(
                        src.begin() + (srcr.y1 * src.dimensions().width * 2),
                        (srcr.y2 - srcr.y1 + 1) * src.dimensions().width * 2);
                } else {
                    bus::write_raw(
                        src.begin() + (srcr.y1 * src.dimensions().width * 2),
                        (srcr.y2 - srcr.y1 + 1) * src.dimensions().width * 2);
                }

                bus::end_write();
                return gfx::gfx_result::success;
            }
            // line by line blt
            uint16_t yy = 0;
            uint16_t hh = srcr.height();
            uint16_t ww = src.dimensions().width;
            uint16_t pitch = (srcr.x2 - srcr.x1 + 1) * 2;
            bus::begin_write();
            bus::begin_transaction();
            while (yy < hh - !!async) {
                gfx::rect16 dr = {dstr.x1, uint16_t(dstr.y1 + yy), dstr.x2,
                                  uint16_t(dstr.y1 + yy)};
                set_window(dr);
                bus::write_raw(
                    src.begin() + 2 * (ww * (srcr.y1 + yy) + srcr.x1), pitch);
                ++yy;
            }
            if (async) {
                gfx::rect16 dr = {dstr.x1, uint16_t(dstr.y1 + yy), dstr.x2,
                                  uint16_t(dstr.y1 + yy)};
                set_window(dr);
                bus::write_raw_dma(
                    src.begin() + 2 * (ww * (srcr.y1 + yy) + srcr.x1), pitch);
            }
            bus::end_transaction();
            bus::end_write();
            return gfx::gfx_result::success;
        }
    };
    template <typename Source>
    gfx::gfx_result copy_from_impl(const gfx::rect16& src_rect,
                                   const Source& src, gfx::point16 location,
                                   bool async) {
        gfx::rect16 srcr = src_rect.normalize().crop(src.bounds());
        gfx::rect16 dstr(location, src_rect.dimensions());
        dstr = dstr.crop(bounds());
        if (srcr.width() > dstr.width()) {
            srcr.x2 = srcr.x1 + dstr.width() - 1;
        }
        if (srcr.height() > dstr.height()) {
            srcr.y2 = srcr.y1 + dstr.height() - 1;
        }
        return copy_from_helper < Source,
               gfx::helpers::is_same<pixel_type,
                                     typename Source::pixel_type>::value &&
                   Source::caps::blt > ::do_draw(this, dstr, src, srcr, async);
    }

    static void apply_rotation() {
        bus::begin_write();
        driver::send_command(0x36);
        switch (rotation) {
            case 0:
                // portrait
                driver::send_data8(0x40 | 0x08);
                break;
            case 1:
                // landscape
                driver::send_data8(0x20 | 0x08);
                break;
            case 2:
                // portrait
                driver::send_data8(0x80 | 0x08);
                break;
            case 3:
                // landscape
                driver::send_data8(0x20 | 0x40 | 0x80 | 0x08);
                break;
        }
        delayMicroseconds(10);

        bus::end_write();
    }
};
}  // namespace arduino

First, we include necessary of headers, mainly several GFX headers which we use in lieu of gfx_cpp14.hpp or gfx.hpp because it can shorten compile times and avoids dependencies on a particular C++ version.

We also include the header for the driver and bus code. The most difficult methods by far are the copy method helpers simply because of the myriad of different routes it must take depending on the copy options and target capabilities.

See Using GFX in Platform IO for a simple way to get the above code into your project.

GFX in Platform IO

History

  • 25th March, 2022 - Initial submission