|  | // Copyright 2022 The Pigweed Authors | 
|  | // | 
|  | // Licensed under the Apache License, Version 2.0 (the "License"); you may not | 
|  | // use this file except in compliance with the License. You may obtain a copy of | 
|  | // the License at | 
|  | // | 
|  | //     https://www.apache.org/licenses/LICENSE-2.0 | 
|  | // | 
|  | // Unless required by applicable law or agreed to in writing, software | 
|  | // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | 
|  | // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | 
|  | // License for the specific language governing permissions and limitations under | 
|  | // the License. | 
|  | #include <array> | 
|  | #include <cstdint> | 
|  | #include <cwchar> | 
|  | #include <forward_list> | 
|  | #include <memory> | 
|  | #include <string_view> | 
|  | #include <utility> | 
|  |  | 
|  | #define PW_LOG_LEVEL PW_LOG_LEVEL_DEBUG | 
|  |  | 
|  | #include "ansi.h" | 
|  | #include "app_common/common.h" | 
|  | #include "pw_assert/assert.h" | 
|  | #include "pw_assert/check.h" | 
|  | #include "pw_board_led/led.h" | 
|  | #include "pw_color/color.h" | 
|  | #include "pw_color/colors_endesga32.h" | 
|  | #include "pw_color/colors_pico8.h" | 
|  | #include "pw_draw/draw.h" | 
|  | #include "pw_draw/font6x8.h" | 
|  | #include "pw_draw/font_set.h" | 
|  | #include "pw_draw/pigweed_farm.h" | 
|  | #include "pw_framebuffer/framebuffer.h" | 
|  | #include "pw_geometry/vector2.h" | 
|  | #include "pw_geometry/vector3.h" | 
|  | #include "pw_log/log.h" | 
|  | #include "pw_ring_buffer/prefixed_entry_ring_buffer.h" | 
|  | #include "pw_spin_delay/delay.h" | 
|  | #include "pw_string/string_builder.h" | 
|  | #include "pw_sys_io/sys_io.h" | 
|  | #include "text_buffer.h" | 
|  |  | 
|  | #if defined(USE_FREERTOS) | 
|  | #include "FreeRTOS.h" | 
|  | #include "task.h" | 
|  | #endif  // if defined(USE_FREERTOS) | 
|  |  | 
|  | using pw::color::color_rgb565_t; | 
|  | using pw::color::kColorsPico8Rgb565; | 
|  | using pw::display::Display; | 
|  | using pw::draw::FontSet; | 
|  | using pw::framebuffer::Framebuffer; | 
|  | using pw::geometry::Size; | 
|  | using pw::geometry::Vector2; | 
|  | using pw::ring_buffer::PrefixedEntryRingBuffer; | 
|  |  | 
|  | // TODO(cmumford): move this code into a pre_init section (i.e. boot.cc) which | 
|  | //                 is part of the target. Not all targets currently have this. | 
|  | #if defined(DEFINE_FREERTOS_MEMORY_FUNCTIONS) | 
|  | std::array<StackType_t, 100 /*configMINIMAL_STACK_SIZE*/> freertos_idle_stack; | 
|  | StaticTask_t freertos_idle_tcb; | 
|  |  | 
|  | std::array<StackType_t, configTIMER_TASK_STACK_DEPTH> freertos_timer_stack; | 
|  | StaticTask_t freertos_timer_tcb; | 
|  |  | 
|  | extern "C" { | 
|  | // Required for configUSE_TIMERS. | 
|  | void vApplicationGetTimerTaskMemory(StaticTask_t** ppxTimerTaskTCBBuffer, | 
|  | StackType_t** ppxTimerTaskStackBuffer, | 
|  | uint32_t* pulTimerTaskStackSize) { | 
|  | *ppxTimerTaskTCBBuffer = &freertos_timer_tcb; | 
|  | *ppxTimerTaskStackBuffer = freertos_timer_stack.data(); | 
|  | *pulTimerTaskStackSize = freertos_timer_stack.size(); | 
|  | } | 
|  |  | 
|  | void vApplicationGetIdleTaskMemory(StaticTask_t** ppxIdleTaskTCBBuffer, | 
|  | StackType_t** ppxIdleTaskStackBuffer, | 
|  | uint32_t* pulIdleTaskStackSize) { | 
|  | *ppxIdleTaskTCBBuffer = &freertos_idle_tcb; | 
|  | *ppxIdleTaskStackBuffer = freertos_idle_stack.data(); | 
|  | *pulIdleTaskStackSize = freertos_idle_stack.size(); | 
|  | } | 
|  | }  // extern "C" | 
|  | #endif  // defined(DEFINE_FREERTOS_MEMORY_FUNCTIONS) | 
|  |  | 
|  | namespace { | 
|  |  | 
|  | constexpr color_rgb565_t kBlack = 0U; | 
|  | constexpr color_rgb565_t kWhite = 0xffff; | 
|  |  | 
|  | class DemoDecoder : public AnsiDecoder { | 
|  | public: | 
|  | DemoDecoder(TextBuffer& log_text_buffer) | 
|  | : log_text_buffer_(log_text_buffer) {} | 
|  |  | 
|  | protected: | 
|  | void SetFgColor(uint8_t r, uint8_t g, uint8_t b) override { | 
|  | fg_color_ = pw::color::ColorRgba(r, g, b).ToRgb565(); | 
|  | } | 
|  | void SetBgColor(uint8_t r, uint8_t g, uint8_t b) override { | 
|  | bg_color_ = pw::color::ColorRgba(r, g, b).ToRgb565(); | 
|  | } | 
|  | void EmitChar(char c) override { | 
|  | log_text_buffer_.DrawCharacter(TextBuffer::Char{c, fg_color_, bg_color_}); | 
|  | } | 
|  |  | 
|  | private: | 
|  | color_rgb565_t fg_color_ = kWhite; | 
|  | color_rgb565_t bg_color_ = kBlack; | 
|  | TextBuffer& log_text_buffer_; | 
|  | }; | 
|  |  | 
|  | // A simple implementation of a UI button. | 
|  | class Button { | 
|  | public: | 
|  | // The label ptr must be valid throughout the lifetime of this object. | 
|  | Button(const wchar_t* label, const Vector2<int>& tl, const Size<int>& size) | 
|  | : label_(label), tl_(tl), size_(size) {} | 
|  |  | 
|  | bool Contains(Vector2<int> pt) const { | 
|  | return pt.x >= tl_.x && pt.x < (tl_.x + size_.width) && pt.y >= tl_.y && | 
|  | pt.y < (tl_.y + size_.height); | 
|  | } | 
|  | const wchar_t* label_; | 
|  | const Vector2<int> tl_; | 
|  | const Size<int> size_; | 
|  | }; | 
|  |  | 
|  | constexpr const wchar_t* kButtonLabel = L"Click to add logs"; | 
|  | constexpr int kButtonWidth = 108; | 
|  | constexpr Vector2<int> kButtonTL = {320 - kButtonWidth, 0}; | 
|  | constexpr Size<int> kButtonSize = {kButtonWidth, 12}; | 
|  |  | 
|  | TextBuffer s_log_text_buffer; | 
|  | DemoDecoder s_demo_decoder(s_log_text_buffer); | 
|  | Button g_button(kButtonLabel, kButtonTL, kButtonSize); | 
|  | #if defined(USE_FREERTOS) | 
|  | std::array<StackType_t, configMINIMAL_STACK_SIZE> s_freertos_stack; | 
|  | StaticTask_t s_freertos_tcb; | 
|  | #endif  // defined(USE_FREERTOS) | 
|  |  | 
|  | void DrawButton(const Button& button, | 
|  | color_rgb565_t bg_color, | 
|  | Framebuffer& framebuffer) { | 
|  | pw::draw::DrawRectWH(framebuffer, | 
|  | button.tl_.x, | 
|  | button.tl_.y, | 
|  | button.size_.width, | 
|  | button.size_.height, | 
|  | bg_color, | 
|  | /*filled=*/true); | 
|  | constexpr int kMargin = 2; | 
|  | Vector2<int> tl{button.tl_.x + kMargin, button.tl_.y + kMargin}; | 
|  | DrawString( | 
|  | button.label_, tl, kBlack, bg_color, pw::draw::GetFont6x8(), framebuffer); | 
|  | } | 
|  |  | 
|  | // Draw a font sheet starting at the given top-left screen coordinates. | 
|  | Vector2<int> DrawTestFontSheet(Vector2<int> tl, | 
|  | int num_columns, | 
|  | color_rgb565_t fg_color, | 
|  | color_rgb565_t bg_color, | 
|  | const FontSet& font, | 
|  | Framebuffer& framebuffer) { | 
|  | Vector2<int> max_extents = tl; | 
|  | const int initial_x = tl.x; | 
|  | for (int c = font.starting_character; c <= font.ending_character; c++) { | 
|  | int char_idx = c - font.starting_character; | 
|  | if (char_idx % num_columns == 0) { | 
|  | tl.x = initial_x; | 
|  | tl.y += font.height; | 
|  | } | 
|  | auto char_size = | 
|  | DrawCharacter(c, tl, fg_color, bg_color, font, framebuffer); | 
|  | tl.x += char_size.width; | 
|  | max_extents.x = std::max(tl.x, max_extents.x); | 
|  | max_extents.y = std::max(tl.y, max_extents.y); | 
|  | } | 
|  | max_extents.y += font.height; | 
|  | return max_extents; | 
|  | } | 
|  |  | 
|  | Vector2<int> DrawColorFontSheet(Vector2<int> tl, | 
|  | int num_columns, | 
|  | color_rgb565_t fg_color, | 
|  | const FontSet& font, | 
|  | Framebuffer& framebuffer) { | 
|  | constexpr int kNumColors = sizeof(pw::color::colors_endesga32_rgb565) / | 
|  | sizeof(pw::color::colors_endesga32_rgb565[0]); | 
|  | const int initial_x = tl.x; | 
|  | Vector2<int> max_extents = tl; | 
|  | for (int c = font.starting_character; c <= font.ending_character; c++) { | 
|  | int char_idx = c - font.starting_character; | 
|  | if (char_idx % num_columns == 0) { | 
|  | tl.x = initial_x; | 
|  | tl.y += font.height; | 
|  | } | 
|  | auto char_size = | 
|  | DrawCharacter(c, | 
|  | tl, | 
|  | fg_color, | 
|  | pw::color::colors_endesga32_rgb565[char_idx % kNumColors], | 
|  | font, | 
|  | framebuffer); | 
|  | tl.x += char_size.width; | 
|  | max_extents.x = std::max(tl.x, max_extents.x); | 
|  | max_extents.y = std::max(tl.y, max_extents.y); | 
|  | } | 
|  | max_extents.y += font.height; | 
|  | return max_extents; | 
|  | } | 
|  |  | 
|  | // The logging callback used to capture log messages sent to pw_log. | 
|  | void LogCallback(std::string_view log) { | 
|  | for (auto c : log) { | 
|  | s_demo_decoder.ProcessChar(c); | 
|  | } | 
|  | s_demo_decoder.ProcessChar('\n'); | 
|  |  | 
|  | pw::sys_io::WriteLine(log).IgnoreError(); | 
|  | }; | 
|  |  | 
|  | // Draw the Pigweed sprite and artwork at the top of the display. | 
|  | // Returns the bottom Y coordinate drawn. | 
|  | int DrawPigweedSprite(Framebuffer& framebuffer) { | 
|  | int sprite_pos_x = 10; | 
|  | int sprite_pos_y = 24; | 
|  | int sprite_scale = 4; | 
|  | int border_size = 8; | 
|  |  | 
|  | // Draw the dark blue border | 
|  | pw::draw::DrawRectWH( | 
|  | framebuffer, | 
|  | sprite_pos_x - border_size, | 
|  | sprite_pos_y - border_size, | 
|  | pigweed_farm_sprite_sheet.width * sprite_scale + (border_size * 2), | 
|  | pigweed_farm_sprite_sheet.height * sprite_scale + (border_size * 2), | 
|  | kColorsPico8Rgb565[pw::color::kColorDarkBlue], | 
|  | true); | 
|  |  | 
|  | // Shrink the border | 
|  | border_size = 4; | 
|  |  | 
|  | // Draw the light blue background | 
|  | pw::draw::DrawRectWH( | 
|  | framebuffer, | 
|  | sprite_pos_x - border_size, | 
|  | sprite_pos_y - border_size, | 
|  | pigweed_farm_sprite_sheet.width * sprite_scale + (border_size * 2), | 
|  | pigweed_farm_sprite_sheet.height * sprite_scale + (border_size * 2), | 
|  | kColorsPico8Rgb565[pw::color::kColorBlue], | 
|  | true); | 
|  |  | 
|  | static Vector2<int> sun_offset; | 
|  | static int motion_dir = -1; | 
|  | static int frame_num = 0; | 
|  | frame_num++; | 
|  | if ((frame_num % 5) == 0) | 
|  | sun_offset.x += motion_dir; | 
|  | if ((frame_num % 15) == 0) | 
|  | sun_offset.y -= motion_dir; | 
|  | if (sun_offset.x < -60) | 
|  | motion_dir = 1; | 
|  | else if (sun_offset.x > 10) | 
|  | motion_dir = -1; | 
|  |  | 
|  | // Draw the Sun | 
|  | pw::draw::DrawCircle(framebuffer, | 
|  | sun_offset.x + sprite_pos_x + | 
|  | (pigweed_farm_sprite_sheet.width * sprite_scale) - | 
|  | 32, | 
|  | sun_offset.y + sprite_pos_y, | 
|  | 20, | 
|  | kColorsPico8Rgb565[pw::color::kColorOrange], | 
|  | true); | 
|  | pw::draw::DrawCircle(framebuffer, | 
|  | sun_offset.x + sprite_pos_x + | 
|  | (pigweed_farm_sprite_sheet.width * sprite_scale) - | 
|  | 32, | 
|  | sun_offset.y + sprite_pos_y, | 
|  | 18, | 
|  | kColorsPico8Rgb565[pw::color::kColorYellow], | 
|  | true); | 
|  |  | 
|  | // Draw the farm sprite's shadow | 
|  | pigweed_farm_sprite_sheet.current_index = 1; | 
|  | pw::draw::DrawSprite(framebuffer, | 
|  | sprite_pos_x + 2, | 
|  | sprite_pos_y + 2, | 
|  | &pigweed_farm_sprite_sheet, | 
|  | 4); | 
|  |  | 
|  | // Draw the farm sprite | 
|  | pigweed_farm_sprite_sheet.current_index = 0; | 
|  | pw::draw::DrawSprite( | 
|  | framebuffer, sprite_pos_x, sprite_pos_y, &pigweed_farm_sprite_sheet, 4); | 
|  |  | 
|  | return 76; | 
|  | } | 
|  |  | 
|  | void DrawFPS(Vector2<int> tl, | 
|  | Framebuffer& framebuffer, | 
|  | std::wstring_view fps_msg) { | 
|  | if (fps_msg.empty()) | 
|  | return; | 
|  |  | 
|  | DrawString(fps_msg, | 
|  | tl, | 
|  | kColorsPico8Rgb565[pw::color::kColorPeach], | 
|  | kBlack, | 
|  | pw::draw::GetFont6x8(), | 
|  | framebuffer); | 
|  | } | 
|  |  | 
|  | // Draw the pigweed text banner. | 
|  | // Returns the bottom Y coordinate of the bottommost pixel set. | 
|  | int DrawPigweedBanner(Vector2<int> tl, Framebuffer& framebuffer) { | 
|  | constexpr std::array<std::wstring_view, 5> pigweed_banner = { | 
|  | L"▒█████▄   █▓  ▄███▒  ▒█    ▒█ ░▓████▒ ░▓████▒ ▒▓████▄", | 
|  | L" ▒█░  █░ ░█▒ ██▒ ▀█▒ ▒█░ █ ▒█  ▒█   ▀  ▒█   ▀  ▒█  ▀█▌", | 
|  | L" ▒█▄▄▄█░ ░█▒ █▓░ ▄▄░ ▒█░ █ ▒█  ▒███    ▒███    ░█   █▌", | 
|  | L" ▒█▀     ░█░ ▓█   █▓ ░█░ █ ▒█  ▒█   ▄  ▒█   ▄  ░█  ▄█▌", | 
|  | L" ▒█      ░█░ ░▓███▀   ▒█▓▀▓█░ ░▓████▒ ░▓████▒ ▒▓████▀"}; | 
|  |  | 
|  | auto font = pw::draw::GetFont6x8BoxChars(); | 
|  | // Draw the Pigweed "ASCII" banner. | 
|  | for (auto text_row : pigweed_banner) { | 
|  | Size<int> string_dims = | 
|  | DrawString(text_row, | 
|  | tl, | 
|  | kColorsPico8Rgb565[pw::color::kColorPink], | 
|  | kBlack, | 
|  | font, | 
|  | framebuffer); | 
|  | tl.y += string_dims.height; | 
|  | } | 
|  | return tl.y - font.height; | 
|  | } | 
|  |  | 
|  | // Draw the font sheets. | 
|  | // Returns the bottom Y coordinate drawn. | 
|  | int DrawFontSheets(Vector2<int> tl, Framebuffer& framebuffer) { | 
|  | constexpr int kFontSheetVerticalPadding = 4; | 
|  | constexpr int kFontSheetNumColumns = 48; | 
|  |  | 
|  | auto font = pw::draw::GetFont6x8(); | 
|  | int initial_x = tl.x; | 
|  | tl = DrawColorFontSheet(tl, | 
|  | kFontSheetNumColumns, | 
|  | /*fg_color=*/kBlack, | 
|  | font, | 
|  | framebuffer); | 
|  |  | 
|  | tl.x = initial_x; | 
|  | tl.y -= font.height; | 
|  | tl.y += kFontSheetVerticalPadding; | 
|  |  | 
|  | tl = DrawTestFontSheet(tl, | 
|  | kFontSheetNumColumns, | 
|  | /*fg_color=*/kWhite, | 
|  | /*bg_color=*/kBlack, | 
|  | font, | 
|  | framebuffer); | 
|  |  | 
|  | tl.x = initial_x; | 
|  | tl.y += kFontSheetVerticalPadding; | 
|  |  | 
|  | Size<int> string_dims = DrawString(L"Box Characters:", | 
|  | tl, | 
|  | /*fg_color=*/kWhite, | 
|  | /*bg_color=*/kBlack, | 
|  | font, | 
|  | framebuffer); | 
|  | tl.x += string_dims.width + font.width; | 
|  | tl.y -= font.height; | 
|  |  | 
|  | tl = DrawTestFontSheet(tl, | 
|  | /*num_columns=*/32, | 
|  | /*fg_color=*/kWhite, | 
|  | /*bg_color=*/kBlack, | 
|  | pw::draw::GetFont6x8BoxChars(), | 
|  | framebuffer); | 
|  | return tl.y; | 
|  | } | 
|  |  | 
|  | // Draw the application header section which is mostly static text/graphics. | 
|  | // Return the height (in pixels) of the header. | 
|  | int DrawHeader(Framebuffer& framebuffer, std::wstring_view fps_msg) { | 
|  | DrawButton(g_button, | 
|  | /*bg_color=*/kColorsPico8Rgb565[pw::color::kColorBlue], | 
|  | framebuffer); | 
|  | Vector2<int> tl = {0, 0}; | 
|  | tl.y = DrawPigweedSprite(framebuffer); | 
|  |  | 
|  | tl.y = DrawPigweedBanner(tl, framebuffer); | 
|  | constexpr int kFontSheetMargin = 4; | 
|  | tl.y += kFontSheetMargin; | 
|  |  | 
|  | DrawFPS({1, 2}, framebuffer, fps_msg); | 
|  |  | 
|  | return DrawFontSheets(tl, framebuffer); | 
|  | } | 
|  |  | 
|  | void DrawLogTextBuffer(int top, const FontSet& font, Framebuffer& framebuffer) { | 
|  | constexpr int kLeft = 0; | 
|  | Vector2<int> loc; | 
|  | Vector2<int> pos{kLeft, top}; | 
|  | Size<int> buffer_size = s_log_text_buffer.GetSize(); | 
|  | for (loc.y = 0; loc.y < buffer_size.height; loc.y++) { | 
|  | for (loc.x = 0; loc.x < buffer_size.width; loc.x++) { | 
|  | auto ch = s_log_text_buffer.GetChar(loc); | 
|  | if (!ch.ok()) | 
|  | continue; | 
|  | Size<int> char_size = DrawCharacter(ch->ch, | 
|  | pos, | 
|  | ch->foreground_color, | 
|  | ch->background_color, | 
|  | font, | 
|  | framebuffer); | 
|  | pos.x += char_size.width; | 
|  | } | 
|  | pos.y += font.height; | 
|  | pos.x = kLeft; | 
|  | } | 
|  | } | 
|  |  | 
|  | void DrawFrame(Framebuffer& framebuffer, std::wstring_view fps_msg) { | 
|  | constexpr int kHeaderMargin = 4; | 
|  | int header_bottom = DrawHeader(framebuffer, fps_msg); | 
|  | DrawLogTextBuffer( | 
|  | header_bottom + kHeaderMargin, pw::draw::GetFont6x8(), framebuffer); | 
|  | } | 
|  |  | 
|  | void CreateDemoLogMessages() { | 
|  | PW_LOG_CRITICAL("An irrecoverable error has occurred!"); | 
|  | PW_LOG_ERROR("There was an error on our last operation"); | 
|  | PW_LOG_WARN("Looks like something is amiss; consider investigating"); | 
|  | PW_LOG_INFO("The operation went as expected"); | 
|  | PW_LOG_DEBUG("Debug output"); | 
|  | } | 
|  |  | 
|  | // Given a ring buffer full of uint32_t values, return the average value | 
|  | // or zero if empty (or iteration error). | 
|  | uint32_t CalcAverageUint32Value(PrefixedEntryRingBuffer& ring_buffer) { | 
|  | uint64_t sum = 0; | 
|  | uint32_t count = 0; | 
|  | for (const auto& entry_info : ring_buffer) { | 
|  | PW_ASSERT(entry_info.buffer.size() == sizeof(uint32_t)); | 
|  | uint32_t val; | 
|  | std::memcpy(&val, entry_info.buffer.data(), sizeof(val)); | 
|  | sum += val; | 
|  | count++; | 
|  | } | 
|  | return count == 0 ? 0 : sum / count; | 
|  | } | 
|  |  | 
|  | }  // namespace | 
|  |  | 
|  | void MainTask(void* pvParameters) { | 
|  | // Timing variables | 
|  | uint32_t frame_start_millis = pw::spin_delay::Millis(); | 
|  | uint32_t frames = 0; | 
|  | int frames_per_second = 0; | 
|  | std::array<wchar_t, 40> fps_buffer = {0}; | 
|  | std::wstring_view fps_view(fps_buffer.data(), 0); | 
|  | std::byte draw_buffer[30 * sizeof(uint32_t)]; | 
|  | std::byte flush_buffer[30 * sizeof(uint32_t)]; | 
|  | PrefixedEntryRingBuffer draw_times; | 
|  | PrefixedEntryRingBuffer flush_times; | 
|  |  | 
|  | draw_times.SetBuffer(draw_buffer); | 
|  | flush_times.SetBuffer(flush_buffer); | 
|  |  | 
|  | pw::log_basic::SetOutput(LogCallback); | 
|  |  | 
|  | pw::board_led::Init(); | 
|  | PW_CHECK_OK(Common::Init()); | 
|  |  | 
|  | Display& display = Common::GetDisplay(); | 
|  | Framebuffer framebuffer = display.GetFramebuffer(); | 
|  | PW_ASSERT(framebuffer.is_valid()); | 
|  |  | 
|  | pw::draw::Fill(framebuffer, kBlack); | 
|  |  | 
|  | DrawFrame(framebuffer, fps_view); | 
|  | // Push the frame buffer to the screen. | 
|  | display.ReleaseFramebuffer(std::move(framebuffer)); | 
|  |  | 
|  | // The display loop. | 
|  | while (1) { | 
|  | uint32_t start = pw::spin_delay::Millis(); | 
|  | framebuffer = display.GetFramebuffer(); | 
|  | PW_ASSERT(framebuffer.is_valid()); | 
|  | pw::draw::Fill(framebuffer, kBlack); | 
|  | DrawFrame(framebuffer, fps_view); | 
|  | uint32_t end = pw::spin_delay::Millis(); | 
|  | uint32_t time = end - start; | 
|  | draw_times.PushBack(pw::as_bytes(pw::span{std::addressof(time), 1})); | 
|  | start = end; | 
|  |  | 
|  | display.ReleaseFramebuffer(std::move(framebuffer)); | 
|  | time = pw::spin_delay::Millis() - start; | 
|  | flush_times.PushBack(pw::as_bytes(pw::span{std::addressof(time), 1})); | 
|  |  | 
|  | // Every second make a log message. | 
|  | frames++; | 
|  | if (pw::spin_delay::Millis() > frame_start_millis + 1000) { | 
|  | frames_per_second = frames; | 
|  | frames = 0; | 
|  | PW_LOG_INFO("FPS:%d, Draw:%dms, Flush:%dms", | 
|  | frames_per_second, | 
|  | CalcAverageUint32Value(draw_times), | 
|  | CalcAverageUint32Value(flush_times)); | 
|  | int len = std::swprintf(fps_buffer.data(), | 
|  | fps_buffer.size(), | 
|  | L"FPS:%d, Draw:%dms, Flush:%dms", | 
|  | frames_per_second, | 
|  | CalcAverageUint32Value(draw_times), | 
|  | CalcAverageUint32Value(flush_times)); | 
|  | fps_view = std::wstring_view(fps_buffer.data(), len); | 
|  |  | 
|  | frame_start_millis = pw::spin_delay::Millis(); | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | int main(void) { | 
|  | #if defined(USE_FREERTOS) | 
|  | TaskHandle_t task_handle = xTaskCreateStatic(MainTask, | 
|  | "main", | 
|  | s_freertos_stack.size(), | 
|  | /*pvParameters=*/nullptr, | 
|  | tskIDLE_PRIORITY, | 
|  | s_freertos_stack.data(), | 
|  | &s_freertos_tcb); | 
|  | PW_CHECK_NOTNULL(task_handle);  // Ensure it succeeded. | 
|  | vTaskStartScheduler(); | 
|  | #else | 
|  | MainTask(/*pvParameters=*/nullptr); | 
|  | #endif | 
|  | return 0; | 
|  | } |