pw_graphics: 32blit particle and text demo

Change-Id: I082395d0efd3da8d226a8270b96a696ff4f274d4
Reviewed-on: https://pigweed-review.googlesource.com/c/pigweed/experimental/+/95500
Reviewed-by: Chris Mumford <cmumford@google.com>
Reviewed-by: Anthony DiGirolamo <tonymd@google.com>
Presubmit-Verified: CQ Bot Account <pigweed-scoped@luci-project-accounts.iam.gserviceaccount.com>
Commit-Queue: Anthony DiGirolamo <tonymd@google.com>
diff --git a/.gitmodules b/.gitmodules
index dcaa5dd..6c5f16c 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -26,3 +26,6 @@
 	path = infra/config
 	url = https://pigweed.googlesource.com/infra/config
 	branch = main
+[submodule "third_party/32blit/32blit-sdk"]
+	path = third_party/32blit/32blit-sdk
+	url = https://github.com/32blit/32blit-sdk
diff --git a/BUILD.gn b/BUILD.gn
index 9f6f648..aaf4f99 100644
--- a/BUILD.gn
+++ b/BUILD.gn
@@ -49,6 +49,7 @@
     "$dir_pw_framebuffer:tests.run(//targets/host:host_debug_tests)",
 
     # See //applications/pw_lcd_display_host_imgui/README.md for instructions.
+    "//applications/32blit_demo:all(//targets/host:host_debug)",
     "//applications/terminal_display:all(//targets/host:host_debug)",
   ]
 }
@@ -83,6 +84,7 @@
   if (PICO_SRC_DIR != "") {
     deps = [
       ":pico_tests(//targets/rp2040)",
+      "//applications/32blit_demo:all(//targets/rp2040)",
       "//applications/terminal_display:all(//targets/rp2040)",
     ]
   }
@@ -237,6 +239,7 @@
     # STMicroelectronics STM32F429I-DISC1 STM32Cube applications steps.
     deps += [
       ":applications_tests(//targets/stm32f429i_disc1_stm32cube:stm32f429i_disc1_stm32cube_debug)",
+      "//applications/32blit_demo:all(//targets/stm32f429i_disc1_stm32cube:stm32f429i_disc1_stm32cube_debug)",
       "//applications/blinky:blinky(//targets/stm32f429i_disc1_stm32cube:stm32f429i_disc1_stm32cube_debug)",
       "//applications/terminal_display:all(//targets/stm32f429i_disc1_stm32cube:stm32f429i_disc1_stm32cube_debug)",
     ]
diff --git a/applications/32blit_demo/BUILD.gn b/applications/32blit_demo/BUILD.gn
new file mode 100644
index 0000000..8293003
--- /dev/null
+++ b/applications/32blit_demo/BUILD.gn
@@ -0,0 +1,69 @@
+# Copyright 2023 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.
+
+import("//build_overrides/pigweed.gni")
+
+import("$dir_pw_arduino_build/arduino.gni")
+import("$dir_pw_build/target_types.gni")
+import("$dir_pw_tokenizer/database.gni")
+import("$dir_pw_unit_test/test.gni")
+
+group("all") {
+  deps = [ ":32blit_demo" ]
+}
+
+pw_executable("32blit_demo") {
+  sources = [
+    "main.cc",
+    "random.cc",
+    "random.h",
+  ]
+  deps = [
+    "$dir_app_common",
+    "$dir_pw_board_led",
+    "$dir_pw_color",
+    "$dir_pw_display",
+    "$dir_pw_draw",
+    "$dir_pw_framebuffer",
+    "$dir_pw_log",
+    "$dir_pw_random",
+    "$dir_pw_ring_buffer",
+    "$dir_pw_spin_delay",
+    "$dir_pw_string",
+    "$dir_pw_sys_io",
+    "//third_party/32blit:32blit",
+  ]
+  remove_configs = [ "$dir_pw_build:strict_warnings" ]
+
+  if (host_os == "linux") {
+    remove_configs += [ "$dir_pw_toolchain/host_clang:linux_sysroot" ]
+  }
+
+  # Hack for targets with no pre-init target implementation.
+  defines = []
+  if (pw_build_EXECUTABLE_TARGET_TYPE != "executable" &&
+      pw_build_EXECUTABLE_TARGET_TYPE != "arduino_executable") {
+    defines += [ "USE_FREERTOS" ]
+    deps += [ "$dir_pw_third_party/freertos" ]
+  }
+  if (pw_build_EXECUTABLE_TARGET_TYPE == "pico_executable") {
+    defines += [ "DEFINE_FREERTOS_MEMORY_FUNCTIONS=1" ]
+  }
+
+  if (pw_build_EXECUTABLE_TARGET_TYPE == "arduino_executable" ||
+      pw_build_EXECUTABLE_TARGET_TYPE == "pico_executable" ||
+      pw_build_EXECUTABLE_TARGET_TYPE == "mimxrt595_executable") {
+    ldflags = [ "-Wl,--print-memory-usage" ]
+  }
+}
diff --git a/applications/32blit_demo/main.cc b/applications/32blit_demo/main.cc
new file mode 100644
index 0000000..7d3e1f9
--- /dev/null
+++ b/applications/32blit_demo/main.cc
@@ -0,0 +1,273 @@
+// Copyright 2023 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 <cstdint>
+
+#define PW_LOG_LEVEL PW_LOG_LEVEL_DEBUG
+
+#include "app_common/common.h"
+#include "graphics/surface.hpp"
+#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_display/display.h"
+#include "pw_framebuffer/framebuffer.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 "random.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::colors_pico8_rgb565;
+using pw::display::Display;
+using pw::framebuffer::Framebuffer;
+using pw::ring_buffer::PrefixedEntryRingBuffer;
+
+// TODO(tonymd): 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 {
+
+#if defined(USE_FREERTOS)
+std::array<StackType_t, configMINIMAL_STACK_SIZE> s_freertos_stack;
+StaticTask_t s_freertos_tcb;
+#endif  // defined(USE_FREERTOS)
+
+struct test_particle {
+  blit::Vec2 pos;
+  blit::Vec2 vel;
+  int age;
+  bool generated = false;
+};
+
+void rain_generate(test_particle& p, blit::Surface screen) {
+  p.pos = blit::Vec2(GetRandomFloat(screen.bounds.w),
+                     GetRandomFloat(10) - (screen.bounds.h + 10));
+  p.vel = blit::Vec2(0, 150);
+  p.age = 0;
+  p.generated = true;
+};
+
+void rain(blit::Surface screen, uint32_t time_ms, blit::Rect floor_position) {
+  static test_particle s[300];
+  static int generate_index = 0;
+  static uint32_t last_time_ms = time_ms;
+
+  int elapsed_ms = time_ms - last_time_ms;
+  float td = (elapsed_ms) / 1000.0f;
+
+  rain_generate(s[generate_index++], screen);
+  if (generate_index >= 300)
+    generate_index = 0;
+
+  float w = sinf(time_ms / 1000.0f) * 0.05f;
+
+  blit::Vec2 gvec = blit::Vec2(0, 9.8 * 5);
+  blit::Vec2 gravity = gvec * td;
+
+  for (auto& p : s) {
+    if (p.generated) {
+      p.vel += gravity;
+      p.pos += p.vel * td;
+
+      int floor = -3;
+      if (p.pos.x > floor_position.x &&
+          p.pos.x < (floor_position.x + floor_position.w))
+        floor = -3 - (screen.bounds.h - floor_position.y);
+
+      if (p.pos.y >= floor) {
+        p.pos.y = floor;
+        float bounce = (GetRandomFloat(10)) / 80.0f;
+        p.vel.y *= -bounce;
+        p.vel.x = (GetRandomFloat(30) - 15);
+      }
+      p.age++;
+
+      int a = p.age / 2;
+      int r = 100 - (a / 2);
+      int g = 255 - (a / 2);
+      int b = 255;  // -(a * 4);
+
+      if (p.vel.length() > 20) {
+        screen.pen = blit::Pen(b, g, r, 100);
+        screen.pixel(p.pos + blit::Point(0, screen.bounds.h - 1));
+        screen.pen = blit::Pen(b, g, r, 160);
+        screen.pixel(p.pos + blit::Point(0, screen.bounds.h + 1));
+      }
+      screen.pen = blit::Pen(b, g, r, 180);
+      screen.pixel(p.pos + blit::Point(0, screen.bounds.h + 2));
+    }
+  }
+
+  last_time_ms = time_ms;
+};
+
+// 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::board_led::Init();
+  PW_CHECK_OK(Common::Init());
+
+  Display& display = Common::GetDisplay();
+  Framebuffer framebuffer = display.GetFramebuffer();
+  PW_ASSERT(framebuffer.is_valid());
+
+  blit::Surface screen = blit::Surface(
+      (uint8_t*)framebuffer.data(),
+      blit::PixelFormat::RGB565,
+      blit::Size(framebuffer.size().width, framebuffer.size().height));
+  screen.pen = blit::Pen(0, 0, 0, 255);
+  screen.clear();
+
+  display.ReleaseFramebuffer(std::move(framebuffer));
+
+  uint32_t delta_screen_draw = 0;
+
+  // The display loop.
+  while (1) {
+    uint32_t start = pw::spin_delay::Millis();
+    framebuffer = display.GetFramebuffer();
+    PW_ASSERT(framebuffer.is_valid());
+    screen.data = (uint8_t*)framebuffer.data();
+
+    // Draw Phase
+    // Clear the screen
+    screen.pen = blit::Pen(0, 0, 0);
+    screen.clear();
+
+    // Draw 32blit animation
+    std::string text = "Pigweed + 32blit";
+    auto text_size = screen.measure_text(text, blit::minimal_font, true);
+    blit::Rect text_rect(
+        blit::Point((screen.bounds.w / 2) - (text_size.w / 2),
+                    (screen.bounds.h * .75) - (text_size.h / 2)),
+        text_size);
+    rain(screen, start - delta_screen_draw, text_rect);
+    screen.pen = blit::Pen(0xFF, 0xFF, 0xFF);
+    screen.text(
+        text, blit::minimal_font, text_rect, true, blit::TextAlign::top_left);
+    delta_screen_draw = pw::spin_delay::Millis() - start;
+
+    // Update timers
+    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;
+}
diff --git a/applications/32blit_demo/random.cc b/applications/32blit_demo/random.cc
new file mode 100644
index 0000000..12d78ed
--- /dev/null
+++ b/applications/32blit_demo/random.cc
@@ -0,0 +1,87 @@
+#include "random.h"
+
+#include <stdint.h>
+
+#include "pw_random/random.h"
+#include "pw_random/xor_shift.h"
+
+#define RANDOM_TYPE_PRNG 0
+#define RANDOM_TYPE_PW_PRNG 1
+
+namespace {
+
+constexpr uint64_t kRandomSeed = 314159265358979;
+static pw::random::XorShiftStarRng64 rng(kRandomSeed);
+
+uint32_t random_seed_offset = 0;
+uint8_t current_random_source = RANDOM_TYPE_PW_PRNG;
+uint32_t current_random_seed = 0x64063701;
+uint32_t prng_lfsr = 0;
+const uint16_t prng_tap = 0x74b8;
+
+}  // namespace
+
+uint32_t GetCurrentSeed() { return current_random_seed; }
+
+void RestartSeed() {
+  prng_lfsr = current_random_seed;
+  rng = pw::random::XorShiftStarRng64(kRandomSeed + random_seed_offset);
+}
+
+void IncrementSeed(int diff) {
+  current_random_seed += diff;
+  random_seed_offset += diff;
+  RestartSeed();
+}
+
+void SetSeed(uint32_t seed) {
+  current_random_seed = seed;
+  RestartSeed();
+}
+
+uint32_t GetRandomNumber() {
+  if (current_random_source == RANDOM_TYPE_PRNG) {
+    uint8_t lsb = prng_lfsr & 1;
+    prng_lfsr >>= 1;
+
+    if (lsb) {
+      prng_lfsr ^= prng_tap;
+    }
+    return prng_lfsr;
+  } else if (current_random_source == RANDOM_TYPE_PW_PRNG) {
+    int random_value = 0;
+    rng.GetInt(random_value);
+    return (uint32_t)random_value;
+  }
+  return 0;
+}
+
+int GetRandomInteger(uint32_t max_value) {
+  return (int)(GetRandomNumber() % max_value);
+}
+
+int GetRandomInteger(uint32_t min_value, uint32_t max_value) {
+  int diff = max_value - min_value;
+  if (diff < 0)
+    diff *= -1;
+
+  int r = GetRandomNumber() % (uint32_t)(diff);
+  r += min_value;
+
+  return r;
+}
+
+float GetRandomFloat(float max_value) {
+  uint32_t r = GetRandomNumber() % (uint32_t)(max_value);
+  uint32_t d = GetRandomNumber() % 1000000;
+  float decimal_part = (float)d / 1000000.0f;
+  float x = (float)r + decimal_part;
+  return x;
+}
+
+float GetRandomFloat(float min_value, float max_value) {
+  float diff = max_value - min_value;
+  float r = GetRandomFloat(diff);
+  r += min_value;
+  return r;
+}
diff --git a/applications/32blit_demo/random.h b/applications/32blit_demo/random.h
new file mode 100644
index 0000000..0d45554
--- /dev/null
+++ b/applications/32blit_demo/random.h
@@ -0,0 +1,13 @@
+#pragma once
+
+#include <stdint.h>
+
+uint32_t GetCurrentSeed();
+void RestartSeed();
+void IncrementSeed(int diff);
+void SetSeed(uint32_t seed);
+uint32_t GetRandomNumber();
+int GetRandomInteger(uint32_t max_value);
+int GetRandomInteger(uint32_t min_value, uint32_t max_value);
+float GetRandomFloat(float max_value);
+float GetRandomFloat(float min_value, float max_value);
diff --git a/third_party/32blit/32blit-sdk b/third_party/32blit/32blit-sdk
new file mode 160000
index 0000000..7048b93
--- /dev/null
+++ b/third_party/32blit/32blit-sdk
@@ -0,0 +1 @@
+Subproject commit 7048b93680b72421fae7f93651f1802dcff18566
diff --git a/third_party/32blit/BUILD.gn b/third_party/32blit/BUILD.gn
new file mode 100644
index 0000000..3a9c2da
--- /dev/null
+++ b/third_party/32blit/BUILD.gn
@@ -0,0 +1,60 @@
+import("//build_overrides/pigweed.gni")
+
+import("$dir_pw_build/target_types.gni")
+
+declare_args() {
+  THIRTYTWO_BLIT_SDK = "//third_party/32blit/32blit-sdk"
+}
+
+config("32blit_config") {
+  include_dirs = [
+    "$THIRTYTWO_BLIT_SDK/32blit",
+    "$THIRTYTWO_BLIT_SDK/3rd-party",
+    "include",
+  ]
+}
+
+pw_source_set("32blit") {
+  public_configs = [ ":32blit_config" ]
+  sources = [
+    "$THIRTYTWO_BLIT_SDK/32blit/audio/audio.cpp",
+    "$THIRTYTWO_BLIT_SDK/32blit/audio/mp3-stream.cpp",
+    "$THIRTYTWO_BLIT_SDK/32blit/engine/api.cpp",
+    "$THIRTYTWO_BLIT_SDK/32blit/engine/engine.cpp",
+    "$THIRTYTWO_BLIT_SDK/32blit/engine/file.cpp",
+    "$THIRTYTWO_BLIT_SDK/32blit/engine/input.cpp",
+    "$THIRTYTWO_BLIT_SDK/32blit/engine/multiplayer.cpp",
+    "$THIRTYTWO_BLIT_SDK/32blit/engine/output.cpp",
+    "$THIRTYTWO_BLIT_SDK/32blit/engine/particle.cpp",
+    "$THIRTYTWO_BLIT_SDK/32blit/engine/profiler.cpp",
+    "$THIRTYTWO_BLIT_SDK/32blit/engine/running_average.cpp",
+    "$THIRTYTWO_BLIT_SDK/32blit/engine/save.cpp",
+    "$THIRTYTWO_BLIT_SDK/32blit/engine/timer.cpp",
+    "$THIRTYTWO_BLIT_SDK/32blit/engine/tweening.cpp",
+    "$THIRTYTWO_BLIT_SDK/32blit/engine/version.cpp",
+    "$THIRTYTWO_BLIT_SDK/32blit/graphics/blend.cpp",
+    "$THIRTYTWO_BLIT_SDK/32blit/graphics/color.cpp",
+    "$THIRTYTWO_BLIT_SDK/32blit/graphics/filter.cpp",
+    "$THIRTYTWO_BLIT_SDK/32blit/graphics/font.cpp",
+    "$THIRTYTWO_BLIT_SDK/32blit/graphics/jpeg.cpp",
+    "$THIRTYTWO_BLIT_SDK/32blit/graphics/mask.cpp",
+    "$THIRTYTWO_BLIT_SDK/32blit/graphics/mode7.cpp",
+    "$THIRTYTWO_BLIT_SDK/32blit/graphics/primitive.cpp",
+    "$THIRTYTWO_BLIT_SDK/32blit/graphics/sprite.cpp",
+    "$THIRTYTWO_BLIT_SDK/32blit/graphics/surface.cpp",
+    "$THIRTYTWO_BLIT_SDK/32blit/graphics/text.cpp",
+    "$THIRTYTWO_BLIT_SDK/32blit/graphics/tilemap.cpp",
+    "$THIRTYTWO_BLIT_SDK/32blit/math/geometry.cpp",
+    "$THIRTYTWO_BLIT_SDK/32blit/math/interpolation.cpp",
+    "$THIRTYTWO_BLIT_SDK/32blit/types/map.cpp",
+    "$THIRTYTWO_BLIT_SDK/32blit/types/mat3.cpp",
+    "$THIRTYTWO_BLIT_SDK/32blit/types/mat4.cpp",
+    "$THIRTYTWO_BLIT_SDK/32blit/types/vec2.cpp",
+    "$THIRTYTWO_BLIT_SDK/32blit/types/vec3.cpp",
+  ]
+  public = [
+    "$THIRTYTWO_BLIT_SDK/32blit/32blit.hpp",
+    "include/version_defs.hpp",
+  ]
+  remove_configs = [ "$dir_pw_build:strict_warnings" ]
+}
diff --git a/third_party/32blit/include/version_defs.hpp b/third_party/32blit/include/version_defs.hpp
new file mode 100644
index 0000000..bc63875
--- /dev/null
+++ b/third_party/32blit/include/version_defs.hpp
@@ -0,0 +1,4 @@
+#pragma once
+
+#define BLIT_BUILD_DATE "UNKNOWN"
+#define BLIT_BUILD_VER "VS-DEV"