Updated on 2022-03-25
A more in-depth guide to creating drivers and other custom draw targets for GFX
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 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.
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.
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.
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:
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.