top

Graphics 2D in C++

This tutorial performs a step by step decomposition of NaCl Graphics2D demo application in C++.

Introduction

This article will discuss the structure of the most basic NaCl application written in the C programming language. The source code can be found in Tizen Studio or you can click [here]graphics_2d.zip. The downloaded zip file can be imported into Tizen Studio or extracted and compiled using a separately downloaded NaCl toolchain. Build instructions are available in the demo package.

Important

Please note that you need at least pepper_42 toolchain version to compile this sample application.

We recommend to go through this tutorial along with analyzing the downloaded source code. This decomposition will only focus on the native part of the NaCl application, the web part (.html and .js code) will be omitted because it’s trivial and almost identical for every demo presented on this website. We also suggest to see Hello World in C++ and Input Events in C++ tutorials.

There is also a cheat sheet for this demo available in Tizen Studio that explains the application functionality by highlighting most important code fragments. Cheat sheets are available from the Help menu.



The application is a 2D graphics application that draws a 2D animation of fire in the NaCl embed element object on the web page. The animation can be controlled with a mouse.

Code Analysis

Initialization

In this article we will focus on specific functionalities you need to implement in the application to enable drawing in 2D.

In order to draw 2D graphics the application has to:

  • initialize the instance,

  • initialize Graphics2D context,

  • create the main loop routine (described in the Main drawing loop section).
    Let’s take a look at Graphics2DFlameInstance class member variables before going into code structure. These are as follows:

  • callback_factory_ - used for simplifying the creation of CompletionCallbacks which help to establish main drawing loop,

  • context_, flush_context_ - used for double buffering mechanism which is a part of the rendering routine,
    size_ - contains size of current viewport,

  • mouse_- contains information about current mouse position,

  • mouse_down_ - indicates if mouse button is pressed down,

  • buffer_ - contains palette entries of size equal to size of the context,

  • palette_ - color palette for drawing, stored as 32-bit values in RGBA or BGRA representation (depends on the host system),

  • device_scale_ - scale of the viewport.

Initialization of the instance (in this case Graphics2DFlameInstance) can be done in the class constructor and will look like this:

Graphics2DFlameInstance::Graphics2DFlameInstance(PP_Instance instance)
    : pp::Instance(instance),
      callback_factory_(this),
      mouse_down_(false),
      buffer_(NULL),
      device_scale_(1.0f) {
  RequestInputEvents(PP_INPUTEVENT_CLASS_MOUSE);
  Logger::InitializeInstance(this);
  unsigned int seed = 1;
  srand(seed);
  CreatePalette();
}

This piece of code performs initialization of members with basic values, initializes the Logger, initializes the random number generator, requests mouse input events and calls CreatePalette()function which is responsible for preparing palette of colors used in the demo.

Graphics2DFlameInstance::Graphics2DFlameInstance(PP_Instance instance)
    : pp::Instance(instance),
      callback_factory_(this),
      mouse_down_(false),
      buffer_(NULL),
      device_scale_(1.0f) {
  RequestInputEvents(PP_INPUTEVENT_CLASS_MOUSE);
  Logger::InitializeInstance(this);
  unsigned int seed = 1;
  srand(seed);
  CreatePalette();
}

This piece of code performs initialization of members with basic values, initializes the Logger, initializes the random number generator, requests mouse input events and calls CreatePalette() function which is responsible for preparing palette of colors used in the demo.

Palette Initialization

CreatePalette() populates the palette with colors ranging from: black -> red -> yellow -> white for smooth transition between different colors for the displayed flame. The code is given below:

void Graphics2DFlameInstance::CreatePalette() {
  for (int i = 0; i < 64; ++i) {
    // Black -> Red
    palette_[i] = MakeColor(i * 2, 0, 0);
    palette_[i + 64] = MakeColor(128 + i * 2, 0, 0);
    // Red -> Yellow
    palette_[i + 128] = MakeColor(255, i * 4, 0);
    // Yellow -> White
    palette_[i + 192] = MakeColor(255, 255, i * 4);
  }
}

CreatePalette() calls the MakeColor() function in order to create pixel value for given RGB color. This function polls ImageDataFormat of the platform and converts provided RGB values into 32-bit unsigned integer, which can be either RGBA or BGRA (depends on the host system). It’s implementation is quite simple and looks like this:

/// Returns a 4-byte color in proper image data format from R,G,B params.
uint32_t MakeColor(uint8_t r, uint8_t g, uint8_t b) {
  uint8_t a = 255;
  PP_ImageDataFormat format = pp::ImageData::GetNativeImageDataFormat();
  if (format == PP_IMAGEDATAFORMAT_BGRA_PREMUL) {
    return (a << 24) | (r << 16) | (g << 8) | b;
  } else {
    return (a << 24) | (b << 16) | (g << 8) | r;
  }
}

Drawing Context Initialization

Initialization of the drawing context takes place in the DidChangeView() method of pp::Instance. This method is called whenever a viewport is created or changed and provides information about its size, location, etc.

void Graphics2DFlameInstance::DidChangeView(const pp::View& view) {
  device_scale_ = view.GetDeviceScale();
  pp::Size new_size = pp::Size(view.GetRect().width() * device_scale_,
                               view.GetRect().height() * device_scale_);

  if (!CreateContext(new_size))
    return;

  Logger::Log("Successfully initialized module's view with resolution %dx%d",
              new_size.width(), new_size.height());

  // When flush_context_ is null, it means there is no Flush callback in
  // flight. This may have happened if the context was not created
  // successfully, or if this is the first call to DidChangeView (when the
  // module first starts). In either case, start the main loop.
  if (flush_context_.is_null())
    MainLoopIteration(0);
}

This piece of code is responsible for the initialization of stored device scale and size to match the viewport. Then it calls CreateContext() method which creates Graphics2D context. If necessary, it runs the first main drawing loop iteration. CreateContext() creates new instance of pp::Graphics2Dinterface and then binds the graphics context to it. Then, if the context creation succeeds, it creates new buffer of palette entries and updates the size field of the instance class. It returns true when context creation succeeds. The code of the method is presented below:

bool Graphics2DFlameInstance::CreateContext(const pp::Size& new_size) {
  const bool kIsAlwaysOpaque = true;
  context_ = pp::Graphics2D(this, new_size, kIsAlwaysOpaque);
  // Call SetScale before BindGraphics so the image is scaled correctly on
  // HiDPI displays.
  context_.SetScale(1.0f / device_scale_);
  if (!BindGraphics(context_)) {
    Logger::Error("Unable to bind 2D context!");
    context_ = pp::Graphics2D();
    return false;
  }

  // Allocate a new buffer of palette entries of the same size as the new
  // context.
  if (buffer_ != NULL)
    delete[] buffer_;
  buffer_ = new uint8_t[new_size.width() * new_size.height()];
  size_ = new_size;

  return true;
}

After above steps the instance is initialized, graphics context is created and the first main loop iteration is started, so we are ready to start drawing into the viewport.

Main Drawing Loop

The MainLoopIteration() method represents a single iteration of the drawing loop. A loop behavior is established with usage of completion callbacks in a way that the Flush() method is called before ending the MainLoopIteration() method. The Flush() method takes a completion callback as an argument. The MainLoopIteration()method is called again via this callback mechanism, which results in a non-recursive looping behavior.

The above application runs in a single thread and we can’t invoke an infinite loop in the main thread, which may lead to the application freezing. MainLoopIteration()looks like this:

void Graphics2DFlameInstance::MainLoopIteration(int32_t) {
  if (context_.is_null()) {
    // The current Graphics2D context is null, so updating and rendering is
    // pointless. Set flush_context_ to null as well, so if we get another
    // DidChangeView call, the main loop is started again.
    flush_context_ = context_;
    return;
  }

  Update();
  Paint();
  // Store a reference to the context that is being flushed; this ensures
  // the callback is called, even if context_ changes before the flush
  // completes.
  flush_context_ = context_;
  // When Flush() finishes, MainLoopIteration will be called continuously to
  // preserve animation running.
  context_.Flush(callback_factory_.NewCallback(
      &Graphics2DFlameInstance::MainLoopIteration));
}

Beside context management procedures, there are also Update() andPaint()functions called.

  • The Update() method is responsible for interpreting mouse input and updating the buffer field which represents the drawn flame.
  • The Paint() method interprets the buffer representation to specific pixel colors and updates the context framebuffer.

Finally, the Flush() method is called on the context which draws the pixels on the screen.

Implementation of the Update() method won’t be analyzed in this article as it’s specific for this particular application (flame drawing). All that is important is that after the Update() method finishes execution the buffer field, which is an array containing palette entries for each pixel of the viewport, is updated with the drawing information and ready for being drawn on the screen.

Implementation of the Paint() method is on the other hand important for understanding this demo. The Paint()method takes data from the buffer, converts it using the palette which was created earlier and copies it to the context framebuffer. Its implementation looks like this:

void Graphics2DFlameInstance::Paint() {
  // See the comment before the call to ReplaceContents below.
  PP_ImageDataFormat format = pp::ImageData::GetNativeImageDataFormat();
  const bool kDontInitToZero = false;
  pp::ImageData image_data(this, format, size_, kDontInitToZero);

  uint32_t* data = static_cast<uint32_t*>(image_data.data());
  if (!data)
    return;

  uint32_t num_pixels = size_.width() * size_.height();

  for (uint32_t offset = 0; offset < num_pixels; ++offset) {
    data[offset] = palette_[buffer_[offset]];
  }

  context_.ReplaceContents(&image_data);
}

The Paint() method creates an instance of pp::ImageDataobject, which is populated with the colors from the buffer_ field. The ReplaceContents()method is called from the context object which copies the pp::ImageData to itself for future drawing. When the Paint() method finishes execution, this data is flushed to the screen so user can see it. This concludes the drawing of 2D graphics.

Summary

This application can be compiled using Tizen Studio or manually with the make program. The application compiled manually can be run in Google Chrome. To run it on Smart TV Emulator you have to build it with Tizen Studio.

After a successful load you should be presented with a webpage that looks like the one below and displays nice interactive flame animation.