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"