Dev Log #1 - First Projects
It's been a while, and longer than I'd like, but I have completed the first two projects in the list. The first one I tackled was a basic framebuffer utility, and the second was a validating UTF-8 iterator. Neither of these is particularly complicated, but they are both interesting in their own right.
Framebuffer
The basic premise of this one is trivial. Given a struct RGB { std::uint8_t r, g, b; };,
store a std::vector<RGB> of them in memory, and provide a wrapper above it to make
image operations a bit easier to deal with. To make further tooling easier, implement
a simple non-owning view into a part of the image with a similar interface
(modulo some of the raw data access). To round off the feature list, add some simple functions
to serialize the image in PPM format.
The whole thing was fairly easy, apart from properly loading PPM images into memory. I was a bit stumped with the mixed text/binary encoding, but it clicked after a while. The implementation is less elegant than I'd like, but it works, and that's what was important to me right now. In the end, I consider the project "finished" with the code below working properly.
#include <Graphics/Framebuffer.hpp>
int main() {
using namespace Graphics;
Framebuffer fb(640, 480, { 255, 255, 255 });
for (std::uint32_t i = 1; i < 639; ++i) {
fb[i, 1] = RGB{ 20, 65, 128 };
}
FillRect(fb, 20, 20, 600, 440, { 128, 50, 45 });
Save("image.ppm", fb);
Framebuffer fb2 = Load("image.ppm");
FillRect(fb2, 40, 40, 560, 400, { 30, 123, 55 });
Save("image2.ppm", fb2);
}
The one thing I don't like so far is the pile of integers in the FillRect function,
but I have some future plans that will fix this problem nicely. Overall, this was a 1~2 hour project
when ignoring the Load function. When that is included, it roughly another 2 hours
to get it right. The estimate may feel a bit high, but don't underestimate it.
One final note. As you might have noticed, the interface is built with exceptions in mind to signal errors. I will continue using this error handling approach, unless stated otherwise, because I find it makes the resulting code a bit easier to follow.
UTF-8 Iterator
Here, the basic idea was to write a validating iterator (and a range adaptor), such that a snippet like below works without problems. This one is also quite easy in the grand scheme of things, but there are definitely some rough edges worth testing properly.
#include <Unicode/Iterators.hpp>
std::string_view sv("吾輩は猫である。名前はまだ無い。どこで生れたかとんと見当がつかぬ。");
for (char32_t ch: Unicode::Utf8::Range(sv)) {
std::println("Codepoint: U+{:6X}", ch);
}
The whole thing is fairly easy in terms of the interface, iterators are a very well-trodden primitive
in C++. In terms of this specific instance, I decided to cache the current codepoint (char32_t) in
the iterator itself. This forced me to add a bool to be able to properly distinguish if the whole
input has been consumed. I'm not sure if I really like this solution, but it seemed like the least
wasteful option, given the alternative would be to decode on demand each time in operator*, and
then again in operator++ to actually advance to the next codepoint. This choice removes some
const-correctness, since operator* is const, but it needs to set the "finished" flag when the last
codepoint is actually visited. The alternative is forgoing the flag, and not consuming the final
codepoint.
The validation isn't complicated, but the checks are a little tricky, and it's easy to overlook
minor things. Firstly, codepoint values above 0x10'FF'FF are not allowed, neither are surrogates
0xD8'00 ~ 0xDF'FF, nor are overlong encodings (e.g. storing an ASCII value in a 3-byte encoding).
The remaining bits are just a matter of course when it comes to the encoding (checking for bit patterns,
ensuring that codepoints are not cut off, etc.).
In the end, the pattern I showed above is compilable, and it works just fine with the text I've been able to throw at it. I wrote some tests for the edge cases I mentioned, and it managed to deal with all of them. Overall, the implementation took me about 2~3 hours to get to the current state. I struggled on the edge cases a bit more than I expected, but nothing too major.
Extra notes
I completed fewer projects than I wanted between the last dev log and this one. There were other commitments I had to prioritize, but that's not a big deal. I also considered quitting the project list idea, and I don't think I'll really follow it blindly in the future. The problem is that wishing up a project list out of thin air will inevitably lead to mistakes and errors. That said, the basic pattern of picking a project from the list, and spending a few hours on it is quite nice, and I won't throw it away completely, even if I won't follow it absolutely as-written.