Animation Playlists, lots of cleanup, settings file stuff, etc.

This commit is contained in:
Peter Slattery 2021-03-27 21:41:47 -07:00
parent 5ddca7fbac
commit 6b137154bc
10 changed files with 298 additions and 74 deletions

1
.gitignore vendored
View File

@ -3,3 +3,4 @@ meta_run_tree/
process/
reference/
working_data/
nssm_log.log

View File

@ -160,6 +160,9 @@ struct animation_system
animation_array Animations;
animation_repeat_mode RepeatMode;
animation_handle_array Playlist;
u32 PlaylistAt;
r32 PlaylistFadeTime;
// NOTE(Peter): The frame currently being displayed/processed. you
// can see which frame you're on by looking at the time slider on the timeline
@ -621,14 +624,21 @@ AnimationFadeGroup_Update(animation_fade_group* Group, r32 DeltaTime)
internal void
AnimationFadeGroup_FadeTo(animation_fade_group* Group, animation_handle To, r32 Duration)
{
// complete current fade if there is one in progress
if (IsValid(Group->To))
if (IsValid(Group->From))
{
AnimationFadeGroup_Advance(Group);
}
// complete current fade if there is one in progress
if (IsValid(Group->To))
{
AnimationFadeGroup_Advance(Group);
}
Group->To = To;
Group->FadeDuration = Duration;
Group->To = To;
Group->FadeDuration = Duration;
}
else
{
Group->From = To;
}
}
// System
@ -750,7 +760,15 @@ AnimationSystem_Update(animation_system* System, r32 DeltaTime)
case AnimationRepeat_Loop:
{
// TODO(pjs):
Assert(System->Playlist.Count > 0);
u32 NextIndex = System->PlaylistAt;
System->PlaylistAt = (System->PlaylistAt + 1) % System->Playlist.Count;
animation_handle Next = System->Playlist.Handles[NextIndex];
AnimationFadeGroup_FadeTo(&System->ActiveFadeGroup,
Next,
System->PlaylistFadeTime);
System->CurrentFrame = 0;
}break;
InvalidDefaultCase;
@ -759,6 +777,15 @@ AnimationSystem_Update(animation_system* System, r32 DeltaTime)
}
}
internal void
AnimationSystem_FadeToPlaylist(animation_system* System, animation_handle_array Playlist)
{
System->Playlist = Playlist;
System->PlaylistAt = 0;
AnimationFadeGroup_FadeTo(&System->ActiveFadeGroup, Playlist.Handles[0], System->PlaylistFadeTime);
}
inline bool
AnimationSystem_NeedsRender(animation_system System)
{

View File

@ -120,6 +120,7 @@ UPDATE_AND_RENDER(UpdateAndRender)
// zero the Transient arena when we clear it so it wouldn't be a problem, but it is technically
// incorrect to clear the arena, and then access the memory later.
ClearArena(State->Transient);
Assert(State->UserSpaceDesc.UserData.Memory != 0);
if (State->RunEditor)
{
@ -129,6 +130,7 @@ UPDATE_AND_RENDER(UpdateAndRender)
AnimationSystem_Update(&State->AnimationSystem, Context->DeltaTime);
if (AnimationSystem_NeedsRender(State->AnimationSystem))
{
Assert(State->UserSpaceDesc.UserData.Memory != 0);
AnimationSystem_RenderToLedBuffers(&State->AnimationSystem,
State->Assemblies,
&State->LedSystem,
@ -138,7 +140,9 @@ UPDATE_AND_RENDER(UpdateAndRender)
State->UserSpaceDesc.UserData.Memory);
}
Assert(State->UserSpaceDesc.UserData.Memory != 0);
US_CustomUpdate(&State->UserSpaceDesc, State, Context);
Assert(State->UserSpaceDesc.UserData.Memory != 0);
AssemblyDebug_OverrideOutput(State->AssemblyDebugState,
State->Assemblies,
@ -149,6 +153,7 @@ UPDATE_AND_RENDER(UpdateAndRender)
Editor_Render(State, Context, RenderBuffer);
}
Assert(State->UserSpaceDesc.UserData.Memory != 0);
BuildAssemblyData(State, *Context, OutputData);
}

View File

@ -1065,7 +1065,7 @@ Pattern_Patchy(led_buffer* Leds, led_buffer_range Range, assembly Assembly, r32
DEBUG_TRACK_FUNCTION;
blumen_lumen_state* BLState = (blumen_lumen_state*)UserData;
phrase_hue Hue = BLState->AssemblyColors[Assembly.AssemblyIndex % 3];
phrase_hue Hue = BLState->AssemblyColors[Assembly.AssemblyIndex % BL_FLOWER_COUNT];
v4 C0 = HSVToRGB({Hue.Hue0, 1, 1, 1});
v4 C1 = HSVToRGB({Hue.Hue1, 1, 1, 1});

View File

@ -556,6 +556,41 @@ Win32_SendOutputData(gs_thread_context ThreadContext, addressed_data_buffer_list
}
// Time
internal system_time
Win32GetSystemTime()
{
system_time Result = {};
SYSTEMTIME WinLocalTime;
GetLocalTime(&WinLocalTime);
SYSTEMTIME WinSysTime;
FILETIME WinSysFileTime;
GetSystemTime(&WinSysTime);
if (SystemTimeToFileTime((const SYSTEMTIME*)&WinSysTime, &WinSysFileTime))
{
ULARGE_INTEGER SysTime = {};
SysTime.LowPart = WinSysFileTime.dwLowDateTime;
SysTime.HighPart = WinSysFileTime.dwHighDateTime;
Result.NanosSinceEpoch = SysTime.QuadPart;
Result.Year = WinLocalTime.wYear;
Result.Month = WinLocalTime.wMonth;
Result.Day = WinLocalTime.wDay;
Result.Hour = WinLocalTime.wHour;
Result.Minute = WinLocalTime.wMinute;
Result.Second = WinLocalTime.wSecond;
}
else
{
u32 Error = GetLastError();
InvalidCodePath;
}
return Result;
}
int WINAPI
WinMain (
HINSTANCE HInstance,
@ -636,6 +671,8 @@ WinMain (
Context.InitializeApplication(Context);
system_time StartTime = Win32GetSystemTime();
Running = true;
Context.WindowIsVisible = true;
while (Running)
@ -647,31 +684,8 @@ WinMain (
DEBUG_TRACK_SCOPE(MainLoop);
{
// update system time
SYSTEMTIME WinLocalTime;
GetLocalTime(&WinLocalTime);
SYSTEMTIME WinSysTime;
FILETIME WinSysFileTime;
GetSystemTime(&WinSysTime);
if (!SystemTimeToFileTime((const SYSTEMTIME*)&WinSysTime, &WinSysFileTime))
{
u32 Error = GetLastError();
InvalidCodePath;
}
ULARGE_INTEGER SysTime = {};
SysTime.LowPart = WinSysFileTime.dwLowDateTime;
SysTime.HighPart = WinSysFileTime.dwHighDateTime;
Context.SystemTime_Last = Context.SystemTime_Current;
Context.SystemTime_Current.NanosSinceEpoch = SysTime.QuadPart;
Context.SystemTime_Current.Year = WinLocalTime.wYear;
Context.SystemTime_Current.Month = WinLocalTime.wMonth;
Context.SystemTime_Current.Day = WinLocalTime.wDay;
Context.SystemTime_Current.Hour = WinLocalTime.wHour;
Context.SystemTime_Current.Minute = WinLocalTime.wMinute;
Context.SystemTime_Current.Second = WinLocalTime.wSecond;
Context.SystemTime_Current = Win32GetSystemTime();
#define PRINT_SYSTEM_TIME 0
#if PRINT_SYSTEM_TIME
@ -682,6 +696,11 @@ WinMain (
Context.SystemTime_Current.Minute,
Context.SystemTime_Current.Second,
Context.SystemTime_Current.NanosSinceEpoch);
u64 NanosElapsed = Context.SystemTime_Current.NanosSinceEpoch - StartTime.NanosSinceEpoch;
r64 SecondsElapsed = (r64)NanosElapsed * NanosToSeconds;
PrintF(&T, "%lld %f Seconds\n", NanosElapsed, SecondsElapsed);
NullTerminate(&T);
OutputDebugStringA(T.Str);
#endif

View File

@ -174,6 +174,16 @@ BlumenLumen_MicListenJob(gs_thread_context* Ctx, u8* UserData)
CloseSocket(Data->SocketManager, ListenSocket);
}
internal void
BlumenLumen_SetPatternMode(bl_pattern_mode Mode, r32 FadeDuration, animation_system* System, blumen_lumen_state* BLState)
{
BLState->PatternMode = Mode;
animation_handle_array Playlist = BLState->ModeAnimations[Mode];
System->RepeatMode = AnimationRepeat_Loop;
System->PlaylistFadeTime = FadeDuration;
AnimationSystem_FadeToPlaylist(System, Playlist);
}
internal void
BlumenLumen_LoadPatterns(app_state* State)
{
@ -298,7 +308,7 @@ BlumenLumen_CustomInit(app_state* State, context Context)
BLState->ModeAnimations[BlumenPattern_Standard] = LoadAllAnimationsInDir(AmbientPatternFolder, BLState, State, Context);
BLState->ModeAnimations[BlumenPattern_VoiceCommand] = LoadAllAnimationsInDir(VoicePatternFolder, BLState, State, Context);
State->AnimationSystem.ActiveFadeGroup.From = BLState->ModeAnimations[BlumenPattern_Standard].Handles[0];
BlumenLumen_SetPatternMode(BlumenPattern_Standard, 5, &State->AnimationSystem, BLState);
#endif
State->AnimationSystem.TimelineShouldAdvance = true;
@ -333,21 +343,16 @@ BlumenLumen_CustomUpdate(gs_data UserData, app_state* State, context* Context)
BLState->AssemblyColors[0] = NewHue;
BLState->AssemblyColors[1] = NewHue;
BLState->AssemblyColors[2] = NewHue;
animation_handle NewAnim = BLState->ModeAnimations[BlumenPattern_VoiceCommand].Handles[0];
AnimationFadeGroup_FadeTo(&State->AnimationSystem.ActiveFadeGroup,
NewAnim,
VoiceCommandFadeDuration);
}
else
{
u32 AssemblyIdx = BLState->LastAssemblyColorSet;
BLState->AssemblyColors[AssemblyIdx] = NewHue;
BLState->LastAssemblyColorSet = (BLState->LastAssemblyColorSet + 1) % 3;
}
BLState->PatternMode = BlumenPattern_VoiceCommand;
// TODO(PS): get current time so we can fade back after
// a while
BlumenLumen_SetPatternMode(BlumenPattern_VoiceCommand, 5, &State->AnimationSystem, BLState);
BLState->TimeLastSetToVoiceMode = Context->SystemTime_Current;
}
}break;
@ -361,8 +366,18 @@ BlumenLumen_CustomUpdate(gs_data UserData, app_state* State, context* Context)
Motor.Temperature = (T[0] << 8 |
T[1] << 0);
motor_packet CurrPos = Motor.Pos;
motor_packet LastPos = BLState->LastKnownMotorState;
DEBUG_ReceivedMotorPositions(LastPos, Motor.Pos, Context->ThreadContext);
for (u32 i = 0; i < BL_FLOWER_COUNT; i++)
{
if (LastPos.FlowerPositions[i] != CurrPos.FlowerPositions[i])
{
BLState->LastTimeMotorStateChanged[i] = Context->SystemTime_Current.NanosSinceEpoch;
}
}
BLState->LastKnownMotorState = Motor.Pos;
}break;
@ -373,11 +388,11 @@ BlumenLumen_CustomUpdate(gs_data UserData, app_state* State, context* Context)
if (Temp.Temperature > 0)
{
BLState->BrightnessPercent = .25f;
BLState->BrightnessPercent = HighTemperatureBrightnessPercent;
}
else
{
BLState->BrightnessPercent = 1.f;
BLState->BrightnessPercent = FullBrightnessPercent;
}
DEBUG_ReceivedTemperature(Temp, Context->ThreadContext);
@ -387,6 +402,23 @@ BlumenLumen_CustomUpdate(gs_data UserData, app_state* State, context* Context)
}
}
// Transition back to standard mode after some time
if (BLState->PatternMode == BlumenPattern_VoiceCommand)
{
u64 LastChangeClock = BLState->TimeLastSetToVoiceMode.NanosSinceEpoch;
u64 NowClocks = Context->SystemTime_Current.NanosSinceEpoch;
s64 NanosSinceChange = NowClocks - LastChangeClock;
r64 SecondsSinceChange = (r64)NanosSinceChange * NanosToSeconds;
if (SecondsSinceChange > VoiceCommandSustainDuration)
{
BLState->PatternMode = BlumenPattern_Standard;
animation_handle NewAnim = BLState->ModeAnimations[BlumenPattern_Standard].Handles[0];
AnimationFadeGroup_FadeTo(&State->AnimationSystem.ActiveFadeGroup,
NewAnim,
VoiceCommandFadeDuration);
}
}
// Open / Close the Motor
if (MessageQueue_CanWrite(BLState->OutgoingMsgQueue))
@ -441,38 +473,42 @@ BlumenLumen_CustomUpdate(gs_data UserData, app_state* State, context* Context)
DEBUG_SentMotorCommand(MotorCommand.MotorPacket, Context->ThreadContext);
}
}
// Dim the leds based on temp data
for (u32 i = 0; i < State->LedSystem.BuffersCount; i++)
// When a motor state changes to being open, wait to turn Upper Leds on
// in order to hide the fact that they are turning off
motor_packet CurrMotorPos = BLState->LastKnownMotorState;
u64 NowNanos = Context->SystemTime_Current.NanosSinceEpoch;
for (u32 i = 0; i < BL_FLOWER_COUNT; i++)
{
led_buffer Buffer = State->LedSystem.Buffers[i];
for (u32 j = 0; j < Buffer.LedCount; j++)
// have to map from "assembly load order" to
// the order that the clear core is referencing the
// motors by
assembly Assembly = State->Assemblies.Values[i];
u64 AssemblyCCIndex = GetCCIndex(Assembly, BLState);
u8 MotorPos = CurrMotorPos.FlowerPositions[AssemblyCCIndex];
if ((MotorPos == MotorState_Open || MotorPos == MotorState_MostlyOpen) &&
!BLState->ShouldDimUpperLeds[i])
{
pixel* Color = Buffer.Colors + j;
Color->R = Color->R * BLState->BrightnessPercent;
Color->G = Color->G * BLState->BrightnessPercent;
Color->B = Color->B * BLState->BrightnessPercent;
u64 ChangedNanos = BLState->LastTimeMotorStateChanged[i];
u64 NanosSinceChanged = NowNanos - ChangedNanos;
r64 SecondsSinceChanged = (r64)NanosSinceChanged * NanosToSeconds;
if (SecondsSinceChanged > TurnUpperLedsOffAfterMotorCloseCommandDelay)
{
BLState->ShouldDimUpperLeds[i] = true;
}
}
}
// NOTE(PS): If the flowers are mostly open or full open
// we mask off the top leds to prevent them from overheating
// while telescoped inside the flower
motor_packet CurrMotorPos = BLState->LastKnownMotorState;
for (u32 a = 0; a < State->Assemblies.Count; a++)
for (u32 a = 0; a < BL_FLOWER_COUNT; a++)
{
assembly Assembly = State->Assemblies.Values[a];
u64 AssemblyCCIndex = GetCCIndex(Assembly, BLState);
u8 MotorPos = CurrMotorPos.FlowerPositions[AssemblyCCIndex];
if (MotorPos == MotorState_Closed ||
MotorPos == MotorState_HalfOpen)
{
continue;
}
if (!BLState->ShouldDimUpperLeds[a]) continue;
led_buffer Buffer = State->LedSystem.Buffers[Assembly.LedBufferIndex];
led_strip_list TopStrips = AssemblyStripsGetWithTagValue(Assembly, ConstString("section"), ConstString("inner_bloom"), State->Transient);
for (u32 s = 0; s < TopStrips.Count; s++)
{
@ -486,6 +522,22 @@ BlumenLumen_CustomUpdate(gs_data UserData, app_state* State, context* Context)
}
}
// Dim the leds based on temp data
if (!BLState->DEBUG_IgnoreWeatherDimmingLeds)
{
for (u32 i = 0; i < State->LedSystem.BuffersCount; i++)
{
led_buffer Buffer = State->LedSystem.Buffers[i];
for (u32 j = 0; j < Buffer.LedCount; j++)
{
pixel* Color = Buffer.Colors + j;
Color->R = Color->R * BLState->BrightnessPercent;
Color->G = Color->G * BLState->BrightnessPercent;
Color->B = Color->B * BLState->BrightnessPercent;
}
}
}
// Send Status Packet
{
system_time LastSendTime = BLState->LastStatusUpdateTime;
@ -524,7 +576,7 @@ US_CUSTOM_DEBUG_UI(BlumenLumen_DebugUI)
{
motor_packet PendingPacket = BLState->DEBUG_PendingMotorPacket;
for (u32 MotorIndex = 0; MotorIndex < 3; MotorIndex++)
for (u32 MotorIndex = 0; MotorIndex < BL_FLOWER_COUNT; MotorIndex++)
{
gs_string Label = PushStringF(State->Transient, 32, "Motor %d", MotorIndex);
ui_BeginRow(I, 5);
@ -567,6 +619,40 @@ US_CUSTOM_DEBUG_UI(BlumenLumen_DebugUI)
DEBUG_SentMotorCommand(Packet.MotorPacket, Context.ThreadContext);
}
motor_packet MotorPos = BLState->LastKnownMotorState;
ui_Label(I, MakeString("Current Motor Positions"));
{
for (u32 i = 0; i < BL_FLOWER_COUNT; i++)
{
ui_BeginRow(I, 2);
gs_string MotorStr = PushStringF(State->Transient, 32,
"Motor %d",
i);
ui_Label(I, MotorStr);
gs_string StateStr = {};
switch (MotorPos.FlowerPositions[i])
{
case MotorState_Closed: {
StateStr = MakeString("Closed");
} break;
case MotorState_HalfOpen: {
StateStr = MakeString("Half Open");
} break;
case MotorState_MostlyOpen: {
StateStr = MakeString("Mostly Open");
} break;
case MotorState_Open: {
StateStr = MakeString("Open");
} break;
}
ui_Label(I, StateStr);
ui_EndRow(I);
}
}
BLState->DEBUG_IgnoreWeatherDimmingLeds = ui_LabeledToggle(I, MakeString("Ignore Weather Dimming Leds"), BLState->DEBUG_IgnoreWeatherDimmingLeds);
}
}

View File

@ -145,11 +145,8 @@ struct blumen_lumen_state
mic_listen_job_data MicListenJobData;
motor_packet LastKnownMotorState;
r64 TimeElapsed;
animation_handle AnimHandles[3];
u32 CurrAnim;
u64 LastTimeMotorStateChanged[BL_FLOWER_COUNT];
b8 ShouldDimUpperLeds[BL_FLOWER_COUNT];
// NOTE(pjs): Based on temperature data from weatherman
// dim the leds.
@ -158,7 +155,7 @@ struct blumen_lumen_state
system_time LastSendTime;
phrase_hue AssemblyColors[3];
phrase_hue AssemblyColors[BL_FLOWER_COUNT];
u32 LastAssemblyColorSet;
// The indices of this array are the index the clear core uses to
@ -171,12 +168,13 @@ struct blumen_lumen_state
bl_pattern_mode PatternMode;
animation_handle_array ModeAnimations[BlumenPattern_Count];
u32 CurrentAnimation;
phrase_hue_map PhraseHueMap;
system_time TimeLastSetToVoiceMode;
// Debug
motor_packet DEBUG_PendingMotorPacket;
bool DEBUG_IgnoreWeatherDimmingLeds;
};
#include "message_queue.cpp"

View File

@ -3,16 +3,35 @@
#ifndef BLUMEN_LUMEN_SETTINGS_H
#define BLUMEN_LUMEN_SETTINGS_H
// Hey you never know, might need to change this some day lololol
// The number of flowers in the sculpture. Used to size all sorts of
// arrays. Maybe don't touch this unless you really know what you're doing?
#define BL_FLOWER_COUNT 3
// The path to the three flower assembly files
// PS is 90% sure you don't need to touch these ever
gs_const_string Flower0AssemblyPath = ConstString("data/ss_blumen_one.fold");
gs_const_string Flower1AssemblyPath = ConstString("data/ss_blumen_two.fold");
gs_const_string Flower2AssemblyPath = ConstString("data/ss_blumen_three.fold");
// The path to the phrase map CSV. Can be an absolute path, or relative
// to the app_run_tree folder
gs_const_string PhraseMapCSVPath = ConstString("data/flower_codes.csv");
char PhraseMapCSVSeparator = ',';
// Search Strings for which folders to find ambient animation files and
// voice animation files in.
// these search patterns should always end in *.foldanim so they only
// return valid animation files
gs_const_string AmbientPatternFolder = ConstString("data/blumen_animations/ambient_patterns/*.foldanim");
gs_const_string VoicePatternFolder = ConstString("data/blumen_animations/audio_responses/*.foldanim");
// The times of day when the motors should be open.
// these are in the format { Start_Hour, Start_Minute, End_Hour, End_Minute }
// Hours are in the range 0-23 inclusive
// Minutes are in the range 0-59 inclusive
// NOTE: There is no need to modify the MotorOpenTimesCount variable -
// it is a compile time constant that gets calculated automatically
global time_range MotorOpenTimes[] = {
{ 00, 30, 00, 40 },
{ 00, 50, 01, 00 },
@ -59,8 +78,52 @@ global time_range MotorOpenTimes[] = {
{ 14, 30, 14, 40 },
{ 14, 50, 15, 00 },
};
global u32 MotorOpenTimesCount = CArrayLength(MotorOpenTimes);
global u32 MotorOpenTimesCount = CArrayLength(MotorOpenTimes); // do not edit
// How long it takes to fade from the default pattern to the
// voice activated pattern
r32 VoiceCommandFadeDuration = 1.0f; // in seconds
// How long the voice activated pattern will remain active
// without additional voice commands, before fading back to
// default behaviour.
// ie.
// if this is set to 30 seconds, upon receiving a voice command
// lumenarium will fade to the requested pattern/color palette
// and then wait 30 seconds before fading back to the original
// pattern. If, in that 30 second window, another voice command
// is issued, lumenarium will reset the 30 second counter.
r64 VoiceCommandSustainDuration = 30.0; // in seconds
// When we send a Motor Close command, we don't want the upper leds to
// immediately turn off. Instead, we want to wait until the flower is
// at least some of the way closed. This variable dictates how long
// we wait for.
// For example:
// 1. We send a 'motor close' command to the clear core
// 2. the clear core sends back a 'motor closed' state packet
// 3. We begin a timer
// 4. When the timer reaches the value set in this variable,
// we turn the upper leds off.
//
// NOTE: This is not a symmetric operation. When we send a 'motor open'
// command, we want to immediately turn the upper leds on so they appear
// to have been on the whole time.
r64 TurnUpperLedsOffAfterMotorCloseCommandDelay = 5.0; // in seconds
// NOTE: Temperature & Time of Day Based Led Brightness Settings
// The percent brightness we set leds to during high temperatures.
// A value in the range 0:1 inclusive
// This is multiplied by each pixels R, G, & B channels before being
// sent. So if it is set to .1f, then the maximum brightness value sent
// to any channel of any pixel will be 25 (255 * .1 = 25).
r32 HighTemperatureBrightnessPercent = .25f;
// The percent brightness we set leds to when no other conditions apply
// A value in the range 0:1 inclusive.
// Probably wants to be something high like 1 but we might want to
// lower it for heat reasons?
r32 FullBrightnessPercent = 1.0f;
#endif //BLUMEN_LUMEN_SETTINGS_H

View File

@ -2709,8 +2709,30 @@ PushSize_(gs_memory_arena* Arena, u64 Size, char* Location)
{
CursorEntry = MemoryArenaNewCursor(Arena, Size, Location);
}
Assert(CursorEntry);
Assert(CursorHasRoom(CursorEntry->Cursor, Size));
if (!CursorEntry || !CursorHasRoom(CursorEntry->Cursor, Size))
{
__debugbreak();
CursorEntry = 0;
for (u64 i = 0;
i < Arena->CursorsCount;
i++)
{
gs_memory_cursor_list* At = Arena->Cursors + i;
if (CursorHasRoom(At->Cursor, Size))
{
CursorEntry = At;
break;
}
}
if (!CursorEntry)
{
CursorEntry = MemoryArenaNewCursor(Arena, Size, Location);
}
}
//Assert(CursorEntry);
//Assert(CursorHasRoom(CursorEntry->Cursor, Size));
#else
gs_memory_cursor_list* CursorEntry = Arena->CursorList;

View File

@ -137,6 +137,9 @@ global_const r64 MinR64 = -MaxR64;
global_const r64 SmallestPositiveR64 = 4.94065645841247e-324;
global_const r64 EpsilonR64 = 1.11022302462515650e-16;
global_const r64 NanosToSeconds = 1 / 10000000.0;
global_const r64 SecondsToNanos = 10000000.0;
// TODO: va_start and va_arg replacements
internal r32