pebble/devsite/source/_guides/graphics-and-animations/framebuffer-graphics.md
2025-02-24 18:58:29 -08:00

5.9 KiB

title description guide_group order related_docs
Framebuffer Graphics How to perform advanced drawing using direct framebuffer access. graphics-and-animations 2
Graphics Context
GBitmap
GBitmapDataRowInfo

In the context of a Pebble app, the framebuffer is the data region used to store the contents of the what is shown on the display. Using the Graphics Context API allows developers to draw primitive shapes and text, but at a slower speed and with a restricted set of drawing patterns. Getting direct access to the framebuffer allows arbitrary transforms, special effects, and other modifications to be applied to the display contents, and allows drawing at a much greater speed than standard SDK APIs.

Accessing the Framebuffer

Access to the framebuffer can only be obtained during a LayerUpdateProc, when redrawing is taking place. When the time comes to update the associated Layer, the framebuffer can be obtained as a GBitmap:

static void layer_update_proc(Layer *layer, GContext *ctx) {
  // Get the framebuffer
  GBitmap *fb = graphics_capture_frame_buffer(ctx);

  // Manipulate the image data...

  // Finally, release the framebuffer
  graphics_release_frame_buffer(ctx, fb);
}

Note: Once obtained, the framebuffer must be released back to the app so that it may continue drawing.

The format of the data returned will vary by platform, as will the representation of a single pixel, shown in the table below.

Platform Framebuffer Bitmap Format Pixel Format
Aplite GBitmapFormat1Bit One bit (black or white)
Basalt GBitmapFormat8Bit One byte (two bits per color)
Chalk GBitmapFormat8BitCircular One byte (two bits per color)

Modifying the Framebuffer Data

Once the framebuffer has been captured, the underlying data can be manipulated on a row-by-row or even pixel-by-pixel basis. This data region can be obtained using gbitmap_get_data(), but the recommended approach is to make use of gbitmap_get_data_row_info() objects to cater for platforms (such as Chalk), where not every row is of the same width. The GBitmapDataRowInfo object helps with this by providing a min_x and max_x value for each y used to build it.

To iterate over all rows and columns, safely avoiding those with irregular start and end indices, use two nested loops as shown below. The implementation of set_pixel_color() is shown in Getting and Setting Pixels:

Note: it is only necessary to call gbitmap_get_data_row_info() once per row. Calling it more often (such as for every pixel) will incur a sigificant speed penalty.

GRect bounds = layer_get_bounds(layer);

// Iterate over all rows
for(int y = 0; y < bounds.size.h; y++) {
  // Get this row's range and data
  GBitmapDataRowInfo info = gbitmap_get_data_row_info(fb, y);

  // Iterate over all visible columns
  for(int x = info.min_x; x <= info.max_x; x++) {
    // Manipulate the pixel at x,y...
    const GColor random_color = (GColor){ .argb = rand() % 255 };

    // ...to be a random color
    set_pixel_color(info, GPoint(x, y), random_color);
  }
}

Getting and Setting Pixels

To modify a pixel's value, simply set a new value at the appropriate position in the data field of that row's GBitmapDataRowInfo object. This will modify the underlying data, and update the display once the frame buffer is released.

This process will be different depending on the GBitmapFormat of the captured framebuffer. On a color platform, each pixel is stored as a single byte. However, on black and white platforms this will be one bit per byte. Using memset() to read or modify the correct pixel on a black and white display requires a bit more logic, shown below:

static GColor get_pixel_color(GBitmapDataRowInfo info, GPoint point) {
#if defined(PBL_COLOR)
  // Read the single byte color pixel
  return (GColor){ .argb = info.data[point.x] };
#elif defined(PBL_BW)
  // Read the single bit of the correct byte
  uint8_t byte = point.x / 8;
  uint8_t bit = point.x % 8; 
  return byte_get_bit(&info.data[byte], bit) ? GColorWhite : GColorBlack;
#endif
}

Setting a pixel value is achieved in much the same way, with different logic depending on the format of the framebuffer on each platform:

static void set_pixel_color(GBitmapDataRowInfo info, GPoint point, 
                                                                GColor color) {
#if defined(PBL_COLOR)
  // Write the pixel's byte color
  memset(&info.data[point.x], color.argb, 1);
#elif defined(PBL_BW)
  // Find the correct byte, then set the appropriate bit
  uint8_t byte = point.x / 8;
  uint8_t bit = point.x % 8; 
  byte_set_bit(&info.data[byte], bit, gcolor_equal(color, GColorWhite) ? 1 : 0);
#endif
}

The byte_get_bit() and byte_set_bit() implementations are shown here for convenience:

static bool byte_get_bit(uint8_t *byte, uint8_t bit) {
  return ((*byte) >> bit) & 1;
}

static void byte_set_bit(uint8_t *byte, uint8_t bit, uint8_t value) {
  *byte ^= (-value ^ *byte) & (1 << bit);
}

Learn More

To see an example of what can be achieved with direct access to the framebuffer and learn more about the underlying principles, watch the talk given at the 2014 Developer Retreat.

EMBED