Introduction
This is a C++ coding test. It is expected that this task should take a few hours to complete.
Task description
Overview
When designing graphics hardware it is useful to be able to display various test patterns and colour ramps. The task is to write a command-line application to generate some of these test patterns. In the simplest case this program will produce a colour ramp starting with one colour on one side of the display and changing smoothly to a second colour on the other side. In the most complex case there will be a different colour in each corner of the display and each pixel on the display will show the appropriate mix of these four colours.
Input
The program should be invoked by the following command line:
./ramp.exe display tl tr [bl] [br]
where
- display is the name of the display device
- tl is the top left colour value
- tr is the top right colour value
- bl is the bottom left colour value [optional, defaults to tl]
- br is the bottom right colour value [optional, defaults to tr]
The colour values are specified as 16-bit RGB565 pixels in hex or decimal. The bits for each colour are assigned as follows:
Bit 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0 Colour R4 R3 R2 R1 R0 G5 G4 G3 G2 G1 G0 B4 B3 B2 B1 B0
For example,
- White is 0xffff
- Black is 0x0000
- Pure blue is 0x001f
- Pure green is 0x07e0
- Pure red is 0xf800
The following inputs to the program are all valid:
- ramp.exe display 0x0 0x2
- ramp.exe display 65 255
- ramp.exe display 200 0 30
- ramp.exe display 0 0 3200 1800
- etc
Output
The program must fill the display with a colour ramp as defined by the input values. Each corner of the display must have the specified colour and the pixels in the middle must be a linear mix of those colours.
The image is output on the display using the Display class which is provided. The size method returns the size of the display and the draw method is used to draw a row of 16-bit RGB565 pixels on the display. Finally, the present method is called to display the pixels.
Assessment
The results of this task will be assessed in the following areas:
- Correct program operation for all input values
- Clarity of design
- Consistent coding style and demonstrated use of C++ features
- Testing strategy (Supplied unit tests, etc)
- Appropriate error handling
You must provide full source code and a brief description. The program will be tested with a variety of input values. The implementation of the Display class is provided below.
Notes
Correct behaviour is much more important than good performance.
This task is harder than it appears: there are a number of pitfalls and difficult cases to allow for. In particular, make sure that the colours are spread evenly when there are only a few different colours across the display. For example, a ramp from 0 to 0xf, over a width of 16 pixels, should attain each value exactly once.
Display Class implementation
Display.h
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | #ifndef DISPLAY_H_INCLUDED #define DISPLAY_H_INCLUDED #include <vector> #include <string> #include <cstdint> struct Point { int16_t x; int16_t y; }; struct Dimension { uint16_t width; uint16_t height; }; class Display { public: Display(const std::string& name); ~Display() = default; Dimension size() const; void draw(Point point, const uint16_t* pixels, uint16_t width); void present(); private: const std::string m_name; std::vector<uint16_t> m_frameBuffer; }; #endif |
#ifndef DISPLAY_H_INCLUDED #define DISPLAY_H_INCLUDED #include <vector> #include <string> #include <cstdint> struct Point { int16_t x; int16_t y; }; struct Dimension { uint16_t width; uint16_t height; }; class Display { public: Display(const std::string& name); ~Display() = default; Dimension size() const; void draw(Point point, const uint16_t* pixels, uint16_t width); void present(); private: const std::string m_name; std::vector<uint16_t> m_frameBuffer; }; #endif
Display.cpp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | #include "Display.h" #include <cstdio> #include <cstring> namespace { constexpr uint16_t Width = 16; constexpr uint16_t Height = 9; } Display::Display(const std::string& name) : m_name(name) , m_frameBuffer(Width * Height) { } Dimension Display::size() const { return { Width, Height }; } void Display::draw(const Point point, const uint16_t *pixels, const uint16_t width) { std::memcpy(&m_frameBuffer[point.y * Width + point.x], pixels, width * sizeof(uint16_t)); } void Display::present() { auto pixels = m_frameBuffer.data(); for (int y = 0; y < Height; ++y) { for (int x = 0; x < Width; ++x) { printf("%04X ", *pixels++); } printf("\n"); } } |
#include "Display.h" #include <cstdio> #include <cstring> namespace { constexpr uint16_t Width = 16; constexpr uint16_t Height = 9; } Display::Display(const std::string& name) : m_name(name) , m_frameBuffer(Width * Height) { } Dimension Display::size() const { return { Width, Height }; } void Display::draw(const Point point, const uint16_t *pixels, const uint16_t width) { std::memcpy(&m_frameBuffer[point.y * Width + point.x], pixels, width * sizeof(uint16_t)); } void Display::present() { auto pixels = m_frameBuffer.data(); for (int y = 0; y < Height; ++y) { for (int x = 0; x < Width; ++x) { printf("%04X ", *pixels++); } printf("\n"); } }
Colour Chart Ramp ScreenShot
The Colour Chart Generator
The main.cpp:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 | // command line utility: ./ramp // Compile: make ramp #include <iostream> #include <string> #include <cstdint> #include <memory> #include "utils.h" #include "ramp.h" using namespace std; void help() { cout << "The following inputs to the program are all valid:" << endl; cout << " ./ramp display 0x0 0x2" << endl; cout << " ./ramp display 65 255" << endl; cout << " ./ramp display 200 0 30" << endl; cout << " ./ramp display 0 0 3200 1800" << endl; } int main(int argc, char *argv[]) { if (argc < 4) { help(); return -1; } // first three parameters are required. string name = argv[1]; uint16_t tl, tr, bl, br; tl = convert(string(argv[2])); tr = convert(string(argv[3])); // next two are optional: bl and br // set default values bl = tl; br = tr; if (argc > 4) { bl = convert(string(argv[4])); if (argc > 5) { br = convert(string(argv[5])); } } shared_ptr<Ramp> ramp(new Ramp(name, tl, tr, bl, br)); // render it to display buffer ramp->render(); // print it to console ramp->getDisplayObject()->present(); return 0; } |
// command line utility: ./ramp // Compile: make ramp #include <iostream> #include <string> #include <cstdint> #include <memory> #include "utils.h" #include "ramp.h" using namespace std; void help() { cout << "The following inputs to the program are all valid:" << endl; cout << " ./ramp display 0x0 0x2" << endl; cout << " ./ramp display 65 255" << endl; cout << " ./ramp display 200 0 30" << endl; cout << " ./ramp display 0 0 3200 1800" << endl; } int main(int argc, char *argv[]) { if (argc < 4) { help(); return -1; } // first three parameters are required. string name = argv[1]; uint16_t tl, tr, bl, br; tl = convert(string(argv[2])); tr = convert(string(argv[3])); // next two are optional: bl and br // set default values bl = tl; br = tr; if (argc > 4) { bl = convert(string(argv[4])); if (argc > 5) { br = convert(string(argv[5])); } } shared_ptr<Ramp> ramp(new Ramp(name, tl, tr, bl, br)); // render it to display buffer ramp->render(); // print it to console ramp->getDisplayObject()->present(); return 0; }
The ramp.cpp:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 | // Ramp class implementation #include <string> #include <vector> #include "ramp.h" #include "utils.h" Ramp::Ramp(const std::string& name, color tl, color tr, color bl, color br) : m_tl(tl) , m_tr(tr) , m_bl(bl) , m_br(br) { display = new Display(name); } Ramp::Ramp(const std::string& name, color tl, color tr): Ramp(name, tl, tr, tl, tr) { } Ramp::Ramp(const std::string& name, color tl, color tr, color bl): Ramp(name, tl, tr, bl, tr) { } Ramp::~Ramp() { // TODO: use shared_ptr so that we don't need to explicitly free the pointer delete display; } void Ramp::render() { // get display size auto dim = display->size(); auto w = (int16_t)(dim.width - 1); auto h = (int16_t)(dim.height - 1); // fill the coners display->draw({0, 0}, &m_tl, 1); display->draw({w, 0}, &m_tr, 1); display->draw({0, h}, &m_bl, 1); display->draw({w, h}, &m_br, 1); // interpolate the rest // TODO: the following loop can be unrolled to improve the performance // another improvement is creating a local buffer and call display->draw when everything is done for (int16_t x = 0; x < dim.width; ++ x) { for (int16_t y = 0; y < dim.height; ++ y) { // exclude 4 corners, this could be unrolled if ( ((x != 0) && (x != w)) || ((y != 0) && (y != h)) ) { color c = BilinearInterpolation(m_tl, m_tr, m_bl, m_br, w, h, x, y); display->draw({x, y}, &c, 1); } } } } |
// Ramp class implementation #include <string> #include <vector> #include "ramp.h" #include "utils.h" Ramp::Ramp(const std::string& name, color tl, color tr, color bl, color br) : m_tl(tl) , m_tr(tr) , m_bl(bl) , m_br(br) { display = new Display(name); } Ramp::Ramp(const std::string& name, color tl, color tr): Ramp(name, tl, tr, tl, tr) { } Ramp::Ramp(const std::string& name, color tl, color tr, color bl): Ramp(name, tl, tr, bl, tr) { } Ramp::~Ramp() { // TODO: use shared_ptr so that we don't need to explicitly free the pointer delete display; } void Ramp::render() { // get display size auto dim = display->size(); auto w = (int16_t)(dim.width - 1); auto h = (int16_t)(dim.height - 1); // fill the coners display->draw({0, 0}, &m_tl, 1); display->draw({w, 0}, &m_tr, 1); display->draw({0, h}, &m_bl, 1); display->draw({w, h}, &m_br, 1); // interpolate the rest // TODO: the following loop can be unrolled to improve the performance // another improvement is creating a local buffer and call display->draw when everything is done for (int16_t x = 0; x < dim.width; ++ x) { for (int16_t y = 0; y < dim.height; ++ y) { // exclude 4 corners, this could be unrolled if ( ((x != 0) && (x != w)) || ((y != 0) && (y != h)) ) { color c = BilinearInterpolation(m_tl, m_tr, m_bl, m_br, w, h, x, y); display->draw({x, y}, &c, 1); } } } }
The ramp.h:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | // Ramp class declaration #ifndef RAMP_H_INCLUDED #define RAMP_H_INCLUDED #include <string> #include "display.h" #include "types.h" class Ramp { public: // overloaded constructors with 2, 3 or 4 parameters Ramp(const std::string& name, color tl, color tr, color bl, color br); Ramp(const std::string& name, color tl, color tr); Ramp(const std::string& name, color tl, color tr, color bl); ~Ramp(); // fill the ramp to the display void render(); inline Display* getDisplayObject() { return display; } private: Display *display; color m_tl, m_tr, m_bl, m_br; }; #endif |
// Ramp class declaration #ifndef RAMP_H_INCLUDED #define RAMP_H_INCLUDED #include <string> #include "display.h" #include "types.h" class Ramp { public: // overloaded constructors with 2, 3 or 4 parameters Ramp(const std::string& name, color tl, color tr, color bl, color br); Ramp(const std::string& name, color tl, color tr); Ramp(const std::string& name, color tl, color tr, color bl); ~Ramp(); // fill the ramp to the display void render(); inline Display* getDisplayObject() { return display; } private: Display *display; color m_tl, m_tr, m_bl, m_br; }; #endif
The test utilities, simpletest.h:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | // The Simplest Unit Test Framework for C++ #ifndef SIMPLETEST_H_INCLUDED #define SIMPLETEST_H_INCLUDED #include <iostream> #include <string> using namespace std; template <typename T> inline void AssertEquals(T a, T b, string msg = "") { if (a != b) { cout << "AssertEquals Fails: " << a << " != " << b << " " << msg << endl; throw; } } inline void AssertTrue(bool a, string msg = "") { if (!a) { cout << "AssertTrue Fails: " << a << " " << msg << endl; throw; } } inline void AssertFalse(bool a, string msg = "") { if (a) { cout << "AssertFalse Fails: " << a << " " << msg << endl; throw; } } #endif |
// The Simplest Unit Test Framework for C++ #ifndef SIMPLETEST_H_INCLUDED #define SIMPLETEST_H_INCLUDED #include <iostream> #include <string> using namespace std; template <typename T> inline void AssertEquals(T a, T b, string msg = "") { if (a != b) { cout << "AssertEquals Fails: " << a << " != " << b << " " << msg << endl; throw; } } inline void AssertTrue(bool a, string msg = "") { if (!a) { cout << "AssertTrue Fails: " << a << " " << msg << endl; throw; } } inline void AssertFalse(bool a, string msg = "") { if (a) { cout << "AssertFalse Fails: " << a << " " << msg << endl; throw; } } #endif
And some tests test.h:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 | // The test executable // Compile and run: make test #include <iostream> #include <string> #include <cstdint> #include <memory> #include "utils.h" #include "ramp.h" #include "simpletest.h" using namespace std; int main(int argc, char *argv[]) { // tests for getColorR, getColorG and getColorB AssertEquals<uint16_t>(getColorR(65535), (uint16_t)31); AssertEquals<uint16_t>(getColorG(65535), (uint16_t)63); AssertEquals<uint16_t>(getColorB(65535), (uint16_t)31); // tests for getColor AssertEquals<uint16_t>(getColor(31, 63, 31), (uint16_t)65535); // tests for isDecimalInteger AssertTrue(isDecimalInteger("1234"), "testing 1234 for isDecimalInteger"); AssertFalse(isDecimalInteger("xxx1234"), "testing xxxx1234 for isDecimalInteger"); // tests for convert AssertEquals<int>(convert("0x0A"), 10); AssertEquals<int>(convert("1234"), 1234); // tests for BilinearInterpolation auto c = BilinearInterpolation(1, 16, 16, 1, 16, 9, 7, 4); AssertEquals<uint16_t>(c, 8); // tests for display checksum // **** this requires changes to Display class shared_ptr<Ramp> ramp(new Ramp("display_test", 1, 2, 55, 66)); // render it to display buffer ramp->render(); // test the checksum AssertEquals<uint16_t>(ramp->getDisplayObject()->checksum(), 96); // if all tests pass, you should see the following message ^_^ cout << "All Tests OK." << endl; } |
// The test executable // Compile and run: make test #include <iostream> #include <string> #include <cstdint> #include <memory> #include "utils.h" #include "ramp.h" #include "simpletest.h" using namespace std; int main(int argc, char *argv[]) { // tests for getColorR, getColorG and getColorB AssertEquals<uint16_t>(getColorR(65535), (uint16_t)31); AssertEquals<uint16_t>(getColorG(65535), (uint16_t)63); AssertEquals<uint16_t>(getColorB(65535), (uint16_t)31); // tests for getColor AssertEquals<uint16_t>(getColor(31, 63, 31), (uint16_t)65535); // tests for isDecimalInteger AssertTrue(isDecimalInteger("1234"), "testing 1234 for isDecimalInteger"); AssertFalse(isDecimalInteger("xxx1234"), "testing xxxx1234 for isDecimalInteger"); // tests for convert AssertEquals<int>(convert("0x0A"), 10); AssertEquals<int>(convert("1234"), 1234); // tests for BilinearInterpolation auto c = BilinearInterpolation(1, 16, 16, 1, 16, 9, 7, 4); AssertEquals<uint16_t>(c, 8); // tests for display checksum // **** this requires changes to Display class shared_ptr<Ramp> ramp(new Ramp("display_test", 1, 2, 55, 66)); // render it to display buffer ramp->render(); // test the checksum AssertEquals<uint16_t>(ramp->getDisplayObject()->checksum(), 96); // if all tests pass, you should see the following message ^_^ cout << "All Tests OK." << endl; }
the types.h:
1 2 3 4 5 6 7 8 9 | #ifndef TYPES_H_INCLUDED #define TYPES_H_INCLUDED #include <cstdint> // color type typedef uint16_t color; #endif |
#ifndef TYPES_H_INCLUDED #define TYPES_H_INCLUDED #include <cstdint> // color type typedef uint16_t color; #endif
the utils.h:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 | // Provides a few useful functions #ifndef UTILS_H_INCLUDED #define UTILS_H_INCLUDED #include <sstream> #include <cstdlib> #include "types.h" float BilinearInterpolation(float q11, float q12, float q21, float q22, float x1, float x2, float y1, float y2, float x, float y); color BilinearInterpolation(color tl, color tr, color bl, color br, uint16_t w, uint16_t h, uint16_t x, uint16_t y); inline uint16_t getColorR(color c) { return c >> 11; } inline uint16_t getColorG(color c) { return (c >> 5) & 63; } inline uint16_t getColorB(color c) { return c & 31; } inline color getColor(uint16_t r, uint16_t g, uint16_t b) { return ((r & 31) << 11) | ((g & 63) << 5) | (b & 31); } // check if dec integer inline bool isDecimalInteger(std::string s) { int len = s.size(); if (0 == len) { return false; } for (int i = 0; i < len; ++ i) { if ( !(s[i] >= '0' && s[i] <= '9')) { return false; } } return true; } // convert string to dec or hex number inline int convert(std::string str) { if (isDecimalInteger(str)) { return std::stoi(str, nullptr, 10); } return std::stoi(str, nullptr, 16); } #endif |
// Provides a few useful functions #ifndef UTILS_H_INCLUDED #define UTILS_H_INCLUDED #include <sstream> #include <cstdlib> #include "types.h" float BilinearInterpolation(float q11, float q12, float q21, float q22, float x1, float x2, float y1, float y2, float x, float y); color BilinearInterpolation(color tl, color tr, color bl, color br, uint16_t w, uint16_t h, uint16_t x, uint16_t y); inline uint16_t getColorR(color c) { return c >> 11; } inline uint16_t getColorG(color c) { return (c >> 5) & 63; } inline uint16_t getColorB(color c) { return c & 31; } inline color getColor(uint16_t r, uint16_t g, uint16_t b) { return ((r & 31) << 11) | ((g & 63) << 5) | (b & 31); } // check if dec integer inline bool isDecimalInteger(std::string s) { int len = s.size(); if (0 == len) { return false; } for (int i = 0; i < len; ++ i) { if ( !(s[i] >= '0' && s[i] <= '9')) { return false; } } return true; } // convert string to dec or hex number inline int convert(std::string str) { if (isDecimalInteger(str)) { return std::stoi(str, nullptr, 10); } return std::stoi(str, nullptr, 16); } #endif
The BilinearInterpolation has been detailed here – however it might be refactored into a class so it doesn’t take so many parameters.
And finally the utils.cpp:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | // Utility implementation #include "utils.h" // based on: https://en.wikipedia.org/wiki/Bilinear_interpolation float BilinearInterpolation(float q11, float q12, float q21, float q22, float x1, float x2, float y1, float y2, float x, float y) { float x2x1, y2y1, x2x, y2y, yy1, xx1; x2x1 = x2 - x1; y2y1 = y2 - y1; x2x = x2 - x; y2y = y2 - y; yy1 = y - y1; xx1 = x - x1; return 1.0 / (x2x1 * y2y1) * ( q11 * x2x * y2y + q21 * xx1 * y2y + q12 * x2x * yy1 + q22 * xx1 * yy1 ); } color BilinearInterpolation(color tl, color tr, color bl, color br, uint16_t w, uint16_t h, uint16_t x, uint16_t y) { // interopolate three color components respectively color r = (uint16_t)BilinearInterpolation(getColorR(tl), getColorR(bl), getColorR(tr), getColorR(br), 0, w, 0, h, x, y); color g = (uint16_t)BilinearInterpolation(getColorG(tl), getColorG(bl), getColorG(tr), getColorG(br), 0, w, 0, h, x, y); color b = (uint16_t)BilinearInterpolation(getColorB(tl), getColorB(bl), getColorB(tr), getColorB(br), 0, w, 0, h, x, y); // then make a color based on the components return getColor(r, g, b); } |
// Utility implementation #include "utils.h" // based on: https://en.wikipedia.org/wiki/Bilinear_interpolation float BilinearInterpolation(float q11, float q12, float q21, float q22, float x1, float x2, float y1, float y2, float x, float y) { float x2x1, y2y1, x2x, y2y, yy1, xx1; x2x1 = x2 - x1; y2y1 = y2 - y1; x2x = x2 - x; y2y = y2 - y; yy1 = y - y1; xx1 = x - x1; return 1.0 / (x2x1 * y2y1) * ( q11 * x2x * y2y + q21 * xx1 * y2y + q12 * x2x * yy1 + q22 * xx1 * yy1 ); } color BilinearInterpolation(color tl, color tr, color bl, color br, uint16_t w, uint16_t h, uint16_t x, uint16_t y) { // interopolate three color components respectively color r = (uint16_t)BilinearInterpolation(getColorR(tl), getColorR(bl), getColorR(tr), getColorR(br), 0, w, 0, h, x, y); color g = (uint16_t)BilinearInterpolation(getColorG(tl), getColorG(bl), getColorG(tr), getColorG(br), 0, w, 0, h, x, y); color b = (uint16_t)BilinearInterpolation(getColorB(tl), getColorB(bl), getColorB(tr), getColorB(br), 0, w, 0, h, x, y); // then make a color based on the components return getColor(r, g, b); }
–EOF (The Ultimate Computing & Technology Blog) —
loading...
Last Post: Interview Coding Exercise - Nested String (Python)
Next Post: Coding Exercise - Sort Letters by Case (C++)