Skip to content

Instantly share code, notes, and snippets.

@lefticus
Last active April 18, 2024 11:25
Show Gist options
  • Star 49 You must be signed in to star a gist
  • Fork 4 You must be signed in to fork a gist
  • Save lefticus/cd8ca664619f0ec43c818d70f01a7797 to your computer and use it in GitHub Desktop.
Save lefticus/cd8ca664619f0ec43c818d70f01a7797 to your computer and use it in GitHub Desktop.

QR Code for URL

Split a Line of Text Into Multiple Lines and Draw Them Centered on the Screen

This is part of the "Your New Mental Model For constexpr" talk by Jason Turner, copyright 2021. It'll make less sense on its own without the intro which is not here.

Note: all helper functions are static to prevent them from mudying up the assembly output after they have been inlined.

100% Without constexpr

#include <algorithm>
#include <array>
#include <cstdint>
#include <span>
#include <string_view>
#include <utility>
#include <cstring>

[[nodiscard]] static char charToPETSCII2(char c) noexcept {
  if (c >= 'a' && c <= 'z') {
    return c - 'a' + 1;
  }
  if (c >= 'A' && c <= 'Z') {
    return c - 'A' + 65;
  }
  if (c == '@') {
    return 0;
  }
  if (c == '_') {
    return 100;
  }
  return c;
}

template <std::size_t Size>
[[nodiscard]] static auto PETSCII2(const char (&value)[Size]) noexcept {
  std::array<char, Size - 1> result{};
  std::transform(std::begin(value), std::prev(std::end(value)),
                 std::begin(result), charToPETSCII2);
  return result;
}

template <typename Contained, std::size_t MaxLines = 10> struct FixedVector {
  std::array<Contained, MaxLines> data;
  std::size_t count{0};

  constexpr auto size() const noexcept { return count; }

  constexpr auto &push_back(Contained &&item) noexcept {
    data[count++] = std::move(item);
    return data[count - 1];
  }

  constexpr auto &push_back(const Contained &item) noexcept {
    data[count++] = item;
    return data[count - 1];
  }

  [[nodiscard]] constexpr auto begin() noexcept { return std::begin(data); }

  [[nodiscard]] constexpr auto end() noexcept {
    return std::next(begin(), count);
  }

  [[nodiscard]] constexpr auto begin() const noexcept {
    return std::begin(data);
  }

  [[nodiscard]] constexpr auto end() const noexcept {
    return std::next(begin(), count);
  }
};

static constexpr std::size_t MaxLines = 10;
using SplitLines = FixedVector<std::string_view, MaxLines>;

using CenteredLines =
    FixedVector<std::pair<std::string_view, std::size_t>, MaxLines>;

[[nodiscard]] static CenteredLines center_lines(const SplitLines &lines,
                                                   std::size_t line_length) {
  CenteredLines results;

  for (const auto &line : lines) {
    results.push_back({line, (line_length - line.size()) / 2});
  }

  return results;
}

[[nodiscard]] static std::size_t
vertical_center_offset(const CenteredLines &lines, std::size_t display_height) {
  return (display_height - lines.size()) / 2;
}

[[nodiscard]] static SplitLines
split_line(std::string_view string, const std::size_t line_length) noexcept {
  SplitLines lines;

  auto begin = string.begin();
  const auto end = string.end();

  while (begin != end) {
    auto word_end = [end](auto start) {
      while (start != end && *start != ' ') {
        ++start;
      }
      return start;
    };

    auto eat_spaces = [end](auto start) {
      while (start != end && *start == ' ') {
        ++start;
      }
      return start;
    };

    begin = eat_spaces(begin);

    auto next_word = word_end(begin);

    while (next_word != end) {

      auto next_begin = eat_spaces(next_word);

      if (next_begin == end) {
        break;
      }

      auto possible_next = word_end(next_begin);

      if (std::cmp_less_equal(std::distance(begin, possible_next),
                              line_length)) {
        next_word = possible_next;
        if (possible_next == end) {
          break;
        }
      } else {
        break;
      }
    }

    lines.push_back(std::string_view(begin, next_word));
    begin = next_word;
  }

  return lines;
}

struct Size {
  std::size_t width;
  std::size_t height;
};

struct Point {
  std::size_t x;
  std::size_t y;
};

static void centered_line_drawer(std::string_view line, Size screen_size, const auto &draw) {
    const auto split = split_line(line, screen_size.width);
    const auto centered = center_lines(split, screen_size.width);
    const auto y_start = vertical_center_offset(centered, screen_size.height);

    for (std::size_t y = y_start; const auto &line : centered) {
      draw(Point{line.second, y}, line.first);
      ++y;
    }
}


void draw(Point, std::string_view);

void do_work() {
  // we want to take this string and center it on the screen (40x25),
  // split to appropriate line lengths, with each line centered and the whole
  // block centered on the screen

  // can we make `str` constexpr? That is, is the conversion known at compile time?
  const auto str = PETSCII2("Hello world, let's write a long string that needs to be split up. After it's split, we can draw it!");
  centered_line_drawer(std::string_view{str.begin(), str.end()}, {40, 25}, draw);
}

https://compiler-explorer.com/z/M1n1xjnTz

With constexpr Local Encoding to PETSCII (C64 Screen Codes)

/* snip */

[[nodiscard]] constexpr static char charToPETSCII2(char c) noexcept {
  if (c >= 'a' && c <= 'z') {
    return c - 'a' + 1;
  }
  if (c >= 'A' && c <= 'Z') {
    return c - 'A' + 65;
  }
  if (c == '@') {
    return 0;
  }
  if (c == '_') {
    return 100;
  }
  return c;
}

template <std::size_t Size>
[[nodiscard]] constexpr static auto
PETSCII2(const char (&value)[Size]) noexcept {
  std::array<char, Size - 1> result{};
  std::transform(std::begin(value), std::prev(std::end(value)),
                 std::begin(result), charToPETSCII2);
  return result;
}

void draw(Point, std::string_view);

/* snip */

void do_work() {
  constexpr static auto str =
      PETSCII2("Hello world, let's write a long string that needs to be split "
               "up. After it's split, we can draw it!");
  centered_line_drawer(std::string_view{str.begin(), str.end()}, {40, 25},
                       draw);
}

https://compiler-explorer.com/z/1ceP34Tnb

Let's Invert Some Dependencies

/* snip */
// auto return type deduction to return a lambda
static auto centered_line_drawer(std::string_view line, Size screen_size) {
  // all params captured
  return [=](const auto &draw) {
    const auto split = split_line(line, screen_size.width);
    const auto centered = center_lines(split, screen_size.width);
    const auto y_start = vertical_center_offset(centered, screen_size.height);

    for (std::size_t y = y_start; const auto &line : centered) {
      draw(Point{line.second, y}, line.first);
      ++y;
    }
  };
}

// intentionally unimplemented so we can see the results in CE
void draw(Point, std::string_view);

void do_work() {
  constexpr static auto str =
      PETSCII2("Hello world, let's write a long string that needs to be split "
               "up. After it's split, we can draw it!");
  // now we return a lambda that is reusable that will draw this
  // given block of text whenever it is called.
  const auto drawer =
      centered_line_drawer(std::string_view{str.begin(), str.end()}, {40, 25});
  drawer(draw);
}

https://compiler-explorer.com/z/ExEvcGT14

We Are Now Inside "The Continuum"

Moving Work From compile-time To Runtime

Units of work

  • Convert ASCII to PETSCII
  • Split 1 string into several lines
  • Center those lines
  • Center paragraph
  • Draw paragraph

We have gone from 0% of the work being done at compile time (optimizations excluded) to 20% done at compile time.

  • Convert ASCII to PETSCII - Compile Time
  • Split 1 string into several lines
  • Center those lines
  • Center paragraph
  • Draw paragraph

Using a Lambda For "Save And Continue"

Currently, the lambda returned by centered_line_drawer does no up-front work, but we can change that. To start, we need to make the lambda returned also constexpr. This is easy to accomplish because the function isn't doing anything other than creating a lambda.

/* snip */
// Just made it constexpr
constexpr static auto centered_line_drawer(std::string_view line, Size screen_size) {
  return [=](const auto &draw) {
    const auto split = split_line(line, screen_size.width);
    const auto centered = center_lines(split, screen_size.width);
    const auto y_start = vertical_center_offset(centered, screen_size.height);

    for (std::size_t y = y_start; const auto &line : centered) {
      draw(Point{line.second, y}, line.first);
      ++y;
    }
  };
}

void draw(Point, std::string_view);

void do_work() {
  constexpr static auto str =
      PETSCII2("Hello world, let's write a long string that needs to be split "
               "up. After it's split, we can draw it!");
  constexpr static auto drawer =
      centered_line_drawer(std::string_view{str.begin(), str.end()}, {40, 25});
  drawer(draw);
}

https://compiler-explorer.com/z/9369565T3

Move split To constexpr Time

/* snip */
// This is the key for the "moving along the continuum"
// idea
constexpr static auto centered_line_drawer(std::string_view line, Size screen_size) {
  // now "split_line" is run at compile-time
  const auto split = split_line(line, screen_size.width);
  return [=](const auto &draw) {
    const auto centered = center_lines(split, screen_size.width);
    const auto y_start = vertical_center_offset(centered, screen_size.height);

    for (std::size_t y = y_start; const auto &line : centered) {
      draw(Point{line.second, y}, line.first);
      ++y;
    }
  };
}

void draw(Point, std::string_view);

void do_work() {
  constexpr static auto str =
      PETSCII2("Hello world, let's write a long string that needs to be split "
               "up. After it's split, we can draw it!");
  constexpr static auto drawer =
      centered_line_drawer(std::string_view{str.begin(), str.end()}, {40, 25});
  drawer(draw);
}

https://compiler-explorer.com/z/rMK5r8r8x

  • Convert ASCII to PETSCII - Compile Time
  • Split 1 string into several lines - Compile Time
  • Center those lines
  • Center paragraph
  • Draw paragraph

40% done at compile-time

Move centered to constexpr

/* snip */
// one more step to constexpr
constexpr static auto centered_line_drawer(std::string_view line, Size screen_size) {
  const auto split = split_line(line, screen_size.width);
  const auto centered = center_lines(split, screen_size.width);
  return [=](const auto &draw) {
    const auto y_start = vertical_center_offset(centered, screen_size.height);

    for (std::size_t y = y_start; const auto &line : centered) {
      draw(Point{line.second, y}, line.first);
      ++y;
    }
  };
}

void draw(Point, std::string_view);

void do_work() {
  constexpr static auto str =
      PETSCII2("Hello world, let's write a long string that needs to be split "
               "up. After it's split, we can draw it!");
  constexpr static auto drawer =
      centered_line_drawer(std::string_view{str.begin(), str.end()}, {40, 25});
  drawer(draw);
}

https://compiler-explorer.com/z/YzsMoh3Gb

  • Convert ASCII to PETSCII - Compile Time
  • Split 1 string into several lines - Compile Time
  • Center those lines - Compile Time
  • Center paragraph
  • Draw paragraph

60% done at compile-time

Move Vertical Centering to constexpr

/* snip */
// now everything is calculated at compile time
constexpr static auto centered_line_drawer(std::string_view line, Size screen_size) {
  const auto split = split_line(line, screen_size.width);
  const auto centered = center_lines(split, screen_size.width);
  const auto y_start = vertical_center_offset(centered, screen_size.height);

  return [=](const auto &draw) {
    for (std::size_t y = y_start; const auto &line : centered) {
      draw(Point{line.second, y}, line.first);
      ++y;
    }
  };
}

void draw(Point, std::string_view);

void do_work() {
  constexpr static auto str =
      PETSCII2("Hello world, let's write a long string that needs to be split "
               "up. After it's split, we can draw it!");
  constexpr static auto drawer =
      centered_line_drawer(std::string_view{str.begin(), str.end()}, {40, 25});
  drawer(draw);
}

https://compiler-explorer.com/z/68of8s5Gd

  • Convert ASCII to PETSCII - Compile Time
  • Split 1 string into several lines - Compile Time
  • Center those lines - Compile Time
  • Center paragraph - Compile Time
  • Draw paragraph

80% done at compile-time

Can We Get To 100% At compile-time constexpr?

Moving the draw Function To constexpr

... Simply requires making a compile-time allocated buffer to store the resulting screen image.

/* snip */
constexpr auto centered_line_drawer(std::string_view line, Size screen_size) {
  return [=](const auto &draw) {
    const auto split = split_line(line, screen_size.width);
    const auto centered = center_lines(split, screen_size.width);
    const auto y_start = vertical_center_offset(centered, screen_size.height);

    for (std::size_t y = y_start; const auto &line : centered) {
      draw(Point{line.second, y}, line.first);
      ++y;
    }
  };
}

template <typename T>
constexpr std::array<char, 40 * 25> run_worker(const T &worker) {
  std::array<char, 40 * 25> results{};

  auto draw = [&](Point p, std::string_view data) {
    const std::size_t start = p.y * 40 + p.x;
    std::copy(data.begin(), data.end(), std::next(results.begin(), start));
  };

  worker(draw);
  return results;
}

static void blit(const std::array<char, 40 * 25> &data) {
  std::copy(data.begin(), data.end(), reinterpret_cast<char *>(0x0400));
}

void do_work() {
  void draw(Point, std::string_view);

  static constexpr auto str =
      PETSCII2("Hello world, let's write a long string that needs to be split "
               "up. After it's split, we can draw it!");

  static constexpr auto worker =
      centered_line_drawer(std::string_view{str.begin(), str.end()}, {40, 25});
  static constexpr auto data = run_worker(worker);
  // now all we do is blit at runtime to the display
  blit(data);
}

https://compiler-explorer.com/z/YhjKqah5E

  • Convert ASCII to PETSCII - Compile Time
  • Split 1 string into several lines - Compile Time
  • Center those lines - Compile Time
  • Center paragraph - Compile Time
  • Draw paragraph - Compile Time

100% done at compile-time, with only a memcpy required at runtime.

The Point Of This Talk

...is to give you a new way of thinking about constexpr and reconsider what things can be moved and what techniques can be used when moving work from runtime to compile-time.

But

... this "save and continue" technique for using lambdas can have many other applications for pre-calculating values you need more than once!

constexpr static Data Is Not The End

Consider my ARM Emulator

You could run a series of instructions up to a certain point, then at runtime copy the compile-time generated state of the system and continue calculates at runtime.

Runtime Manipulation of constexpr Screen Data

/* snip */

static void blit(const std::array<char, 40 * 25> &data) {
  std::copy(data.begin(), data.end(), reinterpret_cast<char *>(0x0400));
}

constexpr auto rotate_left(const std::array<char, 40 * 25> &data) {
  auto result = data;

  for (std::size_t row = 0; row < 25; ++row) {
    auto begin = std::next(std::begin(result), 40 * row);
    // #thatsarotate
    std::rotate(begin, std::next(begin), std::next(begin, 40));
  }
  return result;
}

void do_work() {
  void draw(Point, std::string_view);

  static constexpr auto str =
      PETSCII2("Hello world, let's write a long string that needs to be split "
               "up. After it's split, we can draw it!");

  static constexpr auto worker =
      centered_line_drawer(std::string_view{str.begin(), str.end()}, {40, 25});
  static constexpr auto data = run_worker(worker);
  // take compile-time generated data and run it through a runtime processor
  blit(rotate_left(data));
}

https://compiler-explorer.com/z/bae84d7Tb

Consider

  • What can you move to compile time?
  • Where can you end up on this continuum?
  • What might you gain from generating some portion at compile time, then copying and modifying that data at runtime?

Also note that you can move these things behind a compilation firewall if you want to.

// hpp file:
const std::array<std::uint8_t, 40*25> &get_screen();

// cpp file:
const std::array<std::uint8_t, 40*25> &get_screen()
{
  static constexpr auto str =
      PETSCII2("Hello world, let's write a long string that needs to be split "
               "up. After it's split, we can draw it!");

  static constexpr auto worker =
      centered_line_drawer(std::string_view{str.begin(), str.end()}, {40, 25});
  static constexpr auto data = run_worker(worker);
  return data;
}

// another cpp file:
void do_work() {
  // Now you only pay for the data constexpr code in one place,
  // and get all of the runtime advantages!
  blit(rotate_left(get_screen()));
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment