From 10c5eaf9eaac3d550be2a05ab8a89157276bc336 Mon Sep 17 00:00:00 2001 From: Raymaekers Luca Date: Thu, 21 Aug 2025 20:58:21 +0200 Subject: checkpoint --- code/build.bat | 19 + code/libs/linuxhmh | 2 +- code/win32_handmade.cpp | 1580 +++++++++++++++++++++++++++++++++++++++++++++++ code/win32_handmade.h | 89 +++ 4 files changed, 1689 insertions(+), 1 deletion(-) create mode 100755 code/build.bat create mode 100755 code/win32_handmade.cpp create mode 100755 code/win32_handmade.h (limited to 'code') diff --git a/code/build.bat b/code/build.bat new file mode 100755 index 0000000..2f9ac95 --- /dev/null +++ b/code/build.bat @@ -0,0 +1,19 @@ +@echo off + +call cache.bat + +set CommonCompilerFlags=-MTd -Gm- -nologo -GR- -EHa- -Oi -WX -W4 -wd4459 -wd4456 -wd4201 -wd4100 -wd4189 -wd4505 -wd4996 -wd4389 -DHANDMADE_SLOW=1 -DHANDMADE_INTERNAL=1 -FC -Z7 +set CommonLinkerFlags=-opt:ref -incremental:no user32.lib Gdi32.lib winmm.lib + +IF NOT EXIST ..\build mkdir ..\build +pushd ..\build + +REM 32-Bit +REM cl %CommonCompilerFlags% ..\code\win32_handmade.cpp /link -subsystem:windows,5.1 %CommonLinkerFlags% + +REM 64-Bit +cl %CommonCompilerFlags% ..\code\handmade.cpp -Fmhandmade.map -LD /link /DLL /EXPORT:GameGetSoundSamples /EXPORT:GameUpdateAndRender + +cl %CommonCompilerFlags% ..\code\win32_handmade.cpp -Fmwin32_handmade.map /link %CommonLinkerFlags% + +popd \ No newline at end of file diff --git a/code/libs/linuxhmh b/code/libs/linuxhmh index 7f5300d..54f819b 160000 --- a/code/libs/linuxhmh +++ b/code/libs/linuxhmh @@ -1 +1 @@ -Subproject commit 7f5300d93eceb05e0dc5c666081d5644cbec495f +Subproject commit 54f819bb81feb686f93a736139bc24639f4efd95 diff --git a/code/win32_handmade.cpp b/code/win32_handmade.cpp new file mode 100755 index 0000000..0474848 --- /dev/null +++ b/code/win32_handmade.cpp @@ -0,0 +1,1580 @@ +/* ======================================================================== + $File: $ + $Date: $ + $Revision: $ + $Creator: Casey Muratori $ + $Notice: (C) Copyright 2014 by Molly Rocket, Inc. All Rights Reserved. $ + ======================================================================== */ + +/* + TODO(casey): THIS IS NOT A FINAL PLATFORM LAYER!!! + + - Saved game locations + - Getting a handle to our own executable file + - Asset loading path + - Threading (launch a thread) + - Raw Input (support for multiple keyboards) + - Sleep/timeBeginPeriod + - ClipCursor() (for multimonitor support) + - Fullscreen support + - WM_SETCURSOR (control cursor visibility) + - QueryCancelAutoplay + - WM_ACTIVATEAPP (for when we are not the active application) + - Blit speed improvements (BitBlt) + - Hardware acceleration (OpenGL or Direct3D or BOTH??) + - GetKeyboardLayout (for French keyboards, international WASD support) + + Just a partial list of stuff!! +*/ + +#include "handmade.h" + +#include +#include +#include +#include +#include + +#include "win32_handmade.h" + +// TODO(casey): This is a global for now. +global_variable b32 GlobalRunning; +global_variable b32 GlobalPause; +global_variable win32_offscreen_buffer GlobalBackbuffer; +global_variable LPDIRECTSOUNDBUFFER GlobalSecondaryBuffer; +global_variable s64 GlobalPerfCountFrequency; + +// NOTE(casey): XInputGetState +#define X_INPUT_GET_STATE(name) DWORD WINAPI name(DWORD dwUserIndex, XINPUT_STATE *pState) +typedef X_INPUT_GET_STATE(x_input_get_state); +X_INPUT_GET_STATE(XInputGetStateStub) +{ + return(ERROR_DEVICE_NOT_CONNECTED); +} +global_variable x_input_get_state *XInputGetState_ = XInputGetStateStub; +#define XInputGetState XInputGetState_ + +// NOTE(casey): XInputSetState +#define X_INPUT_SET_STATE(name) DWORD WINAPI name(DWORD dwUserIndex, XINPUT_VIBRATION *pVibration) +typedef X_INPUT_SET_STATE(x_input_set_state); +X_INPUT_SET_STATE(XInputSetStateStub) +{ + return(ERROR_DEVICE_NOT_CONNECTED); +} +global_variable x_input_set_state *XInputSetState_ = XInputSetStateStub; +#define XInputSetState XInputSetState_ + +#define DIRECT_SOUND_CREATE(name) HRESULT WINAPI name(LPCGUID pcGuidDevice, LPDIRECTSOUND *ppDS, LPUNKNOWN pUnkOuter) +typedef DIRECT_SOUND_CREATE(direct_sound_create); + +internal void +CatStrings(size_t SourceACount, char *SourceA, + size_t SourceBCount, char *SourceB, + size_t DestCount, char *Dest) +{ + // TODO(casey): Dest bounds checking! + + for(int Index = 0; + Index < SourceACount; + ++Index) + { + *Dest++ = *SourceA++; + } + + for(int Index = 0; + Index < SourceBCount; + ++Index) + { + *Dest++ = *SourceB++; + } + + *Dest++ = 0; +} + +internal void +Win32GetEXEFileName(win32_state *State) +{ + // NOTE(casey): Never use MAX_PATH in code that is user-facing, because it + // can be dangerous and lead to bad results. + DWORD SizeOfFilename = GetModuleFileNameA(0, State->EXEFileName, sizeof(State->EXEFileName)); + State->OnePastLastEXEFileNameSlash = State->EXEFileName; + for(char *Scan = State->EXEFileName; + *Scan; + ++Scan) + { + if(*Scan == '\\') + { + State->OnePastLastEXEFileNameSlash = Scan + 1; + } + } +} + +internal int +StringLength(char *String) +{ + int Count = 0; + while(*String++) + { + ++Count; + } + return(Count); +} + +internal void +Win32BuildEXEPathFileName(win32_state *State, char *FileName, + int DestCount, char *Dest) +{ + CatStrings(State->OnePastLastEXEFileNameSlash - State->EXEFileName, State->EXEFileName, + StringLength(FileName), FileName, + DestCount, Dest); +} + +DEBUG_PLATFORM_FREE_FILE_MEMORY(DEBUGPlatformFreeFileMemory) +{ + if(Memory) + { + VirtualFree(Memory, 0, MEM_RELEASE); + } +} + +DEBUG_PLATFORM_READ_ENTIRE_FILE(DEBUGPlatformReadEntireFile) +{ + debug_read_file_result Result = {}; + + HANDLE FileHandle = CreateFileA(FileName, GENERIC_READ, FILE_SHARE_READ, 0, OPEN_EXISTING, 0, 0); + if(FileHandle != INVALID_HANDLE_VALUE) + { + LARGE_INTEGER FileSize; + if(GetFileSizeEx(FileHandle, &FileSize)) + { + u32 FileSize32 = SafeTruncateUInt64(FileSize.QuadPart); + Result.Contents = VirtualAlloc(0, FileSize32, MEM_RESERVE|MEM_COMMIT, PAGE_READWRITE); + if(Result.Contents) + { + DWORD BytesRead; + if(ReadFile(FileHandle, Result.Contents, FileSize32, &BytesRead, 0) && + (FileSize32 == BytesRead)) + { + // NOTE(casey): File read successfully + Result.ContentsSize = FileSize32; + } + else + { + // TODO(casey): Logging + DEBUGPlatformFreeFileMemory(Thread, Result.Contents, Result.ContentsSize); + Result.Contents = 0; + } + } + else + { + // TODO(casey): Logging + } + } + else + { + // TODO(casey): Logging + } + + CloseHandle(FileHandle); + } + else + { + // TODO(casey): Logging + } + + return(Result); +} + +DEBUG_PLATFORM_WRITE_ENTIRE_FILE(DEBUGPlatformWriteEntireFile) +{ + b32 Result = false; + + HANDLE FileHandle = CreateFileA(FileName, GENERIC_WRITE, 0, 0, CREATE_ALWAYS, 0, 0); + if(FileHandle != INVALID_HANDLE_VALUE) + { + DWORD BytesWritten; + if(WriteFile(FileHandle, Memory, MemorySize, &BytesWritten, 0)) + { + // NOTE(casey): File read successfully + Result = (BytesWritten == MemorySize); + } + else + { + // TODO(casey): Logging + } + + CloseHandle(FileHandle); + } + else + { + // TODO(casey): Logging + } + + return(Result); +} + +inline FILETIME +Win32GetLastWriteTime(char *Filename) +{ + FILETIME LastWriteTime = {}; + + WIN32_FILE_ATTRIBUTE_DATA Data; + if(GetFileAttributesEx(Filename, GetFileExInfoStandard, &Data)) + { + LastWriteTime = Data.ftLastWriteTime; + } + + return(LastWriteTime); +} + +internal win32_game_code +Win32LoadGameCode(char *SourceDLLName, char *TempDLLName) +{ + win32_game_code Result = {}; + + // TODO(casey): Need to get the proper path here! + // TODO(casey): Automatic determination of when updates are necessary. + + Result.DLLLastWriteTime = Win32GetLastWriteTime(SourceDLLName); + + CopyFile(SourceDLLName, TempDLLName, FALSE); + + Result.GameCodeDLL = LoadLibraryA(TempDLLName); + if(Result.GameCodeDLL) + { + Result.UpdateAndRender = (game_update_and_render *) + GetProcAddress(Result.GameCodeDLL, "GameUpdateAndRender"); + + Result.GetSoundSamples = (game_get_sound_samples *) + GetProcAddress(Result.GameCodeDLL, "GameGetSoundSamples"); + + Result.IsValid = (Result.UpdateAndRender && + Result.GetSoundSamples); + } + + if(!Result.IsValid) + { + Result.UpdateAndRender = 0; + Result.GetSoundSamples = 0; + } + + return(Result); +} + +internal void +Win32UnloadGameCode(win32_game_code *GameCode) +{ + if(GameCode->GameCodeDLL) + { + FreeLibrary(GameCode->GameCodeDLL); + GameCode->GameCodeDLL = 0; + } + + GameCode->IsValid = false; + GameCode->UpdateAndRender = 0; + GameCode->GetSoundSamples = 0; +} + +internal void +Win32LoadXInput(void) +{ + // TODO(casey): Test this on Windows 8 + HMODULE XInputLibrary = LoadLibraryA("xinput1_4.dll"); + if(!XInputLibrary) + { + // TODO(casey): Diagnostic + XInputLibrary = LoadLibraryA("xinput9_1_0.dll"); + } + + if(!XInputLibrary) + { + // TODO(casey): Diagnostic + XInputLibrary = LoadLibraryA("xinput1_3.dll"); + } + + if(XInputLibrary) + { + XInputGetState = (x_input_get_state *)GetProcAddress(XInputLibrary, "XInputGetState"); + if(!XInputGetState) {XInputGetState = XInputGetStateStub;} + + XInputSetState = (x_input_set_state *)GetProcAddress(XInputLibrary, "XInputSetState"); + if(!XInputSetState) {XInputSetState = XInputSetStateStub;} + + // TODO(casey): Diagnostic + + } + else + { + // TODO(casey): Diagnostic + } +} + +internal void +Win32InitDSound(HWND Window, s32 SamplesPerSecond, s32 BufferSize) +{ + // NOTE(casey): Load the library + HMODULE DSoundLibrary = LoadLibraryA("dsound.dll"); + if(DSoundLibrary) + { + // NOTE(casey): Get a DirectSound object! - cooperative + direct_sound_create *DirectSoundCreate = (direct_sound_create *) + GetProcAddress(DSoundLibrary, "DirectSoundCreate"); + + // TODO(casey): Double-check that this works on XP - DirectSound8 or 7?? + LPDIRECTSOUND DirectSound; + if(DirectSoundCreate && SUCCEEDED(DirectSoundCreate(0, &DirectSound, 0))) + { + WAVEFORMATEX WaveFormat = {}; + WaveFormat.wFormatTag = WAVE_FORMAT_PCM; + WaveFormat.nChannels = 2; + WaveFormat.nSamplesPerSec = SamplesPerSecond; + WaveFormat.wBitsPerSample = 16; + WaveFormat.nBlockAlign = (WaveFormat.nChannels*WaveFormat.wBitsPerSample) / 8; + WaveFormat.nAvgBytesPerSec = WaveFormat.nSamplesPerSec*WaveFormat.nBlockAlign; + WaveFormat.cbSize = 0; + + if(SUCCEEDED(DirectSound->SetCooperativeLevel(Window, DSSCL_PRIORITY))) + { + DSBUFFERDESC BufferDescription = {}; + BufferDescription.dwSize = sizeof(BufferDescription); + BufferDescription.dwFlags = DSBCAPS_PRIMARYBUFFER; + + // NOTE(casey): "Create" a primary buffer + // TODO(casey): DSBCAPS_GLOBALFOCUS? + LPDIRECTSOUNDBUFFER PrimaryBuffer; + if(SUCCEEDED(DirectSound->CreateSoundBuffer(&BufferDescription, &PrimaryBuffer, 0))) + { + HRESULT Error = PrimaryBuffer->SetFormat(&WaveFormat); + if(SUCCEEDED(Error)) + { + // NOTE(casey): We have finally set the format! + OutputDebugStringA("Primary buffer format was set.\n"); + } + else + { + // TODO(casey): Diagnostic + } + } + else + { + // TODO(casey): Diagnostic + } + } + else + { + // TODO(casey): Diagnostic + } + + // TODO(casey): DSBCAPS_GETCURRENTPOSITION2 + DSBUFFERDESC BufferDescription = {}; + BufferDescription.dwSize = sizeof(BufferDescription); + BufferDescription.dwFlags = DSBCAPS_GETCURRENTPOSITION2; + BufferDescription.dwBufferBytes = BufferSize; + BufferDescription.lpwfxFormat = &WaveFormat; + HRESULT Error = DirectSound->CreateSoundBuffer(&BufferDescription, &GlobalSecondaryBuffer, 0); + if(SUCCEEDED(Error)) + { + OutputDebugStringA("Secondary buffer created successfully.\n"); + } + } + else + { + // TODO(casey): Diagnostic + } + } + else + { + // TODO(casey): Diagnostic + } +} + +internal win32_window_dimension +Win32GetWindowDimension(HWND Window) +{ + win32_window_dimension Result; + + RECT ClientRect; + GetClientRect(Window, &ClientRect); + Result.Width = ClientRect.right - ClientRect.left; + Result.Height = ClientRect.bottom - ClientRect.top; + + return(Result); +} + +internal void +Win32ResizeDIBSection(win32_offscreen_buffer *Buffer, int Width, int Height) +{ + // TODO(casey): Bulletproof this. + // Maybe don't free first, free after, then free first if that fails. + + if(Buffer->Memory) + { + VirtualFree(Buffer->Memory, 0, MEM_RELEASE); + } + + Buffer->Width = Width; + Buffer->Height = Height; + + int BytesPerPixel = 4; + Buffer->BytesPerPixel = BytesPerPixel; + + // NOTE(casey): When the biHeight field is negative, this is the clue to + // Windows to treat this bitmap as top-down, not bottom-up, meaning that + // the first three bytes of the image are the color for the top left pixel + // in the bitmap, not the bottom left! + Buffer->Info.bmiHeader.biSize = sizeof(Buffer->Info.bmiHeader); + Buffer->Info.bmiHeader.biWidth = Buffer->Width; + Buffer->Info.bmiHeader.biHeight = -Buffer->Height; + Buffer->Info.bmiHeader.biPlanes = 1; + Buffer->Info.bmiHeader.biBitCount = 32; + Buffer->Info.bmiHeader.biCompression = BI_RGB; + + // NOTE(casey): Thank you to Chris Hecker of Spy Party fame + // for clarifying the deal with StretchDIBits and BitBlt! + // No more DC for us. + int BitmapMemorySize = (Buffer->Width*Buffer->Height)*BytesPerPixel; + Buffer->Memory = VirtualAlloc(0, BitmapMemorySize, MEM_RESERVE|MEM_COMMIT, PAGE_READWRITE); + Buffer->Pitch = Width*BytesPerPixel; + + // TODO(casey): Probably clear this to black +} + +internal void +Win32DisplayBufferInWindow(win32_offscreen_buffer *Buffer, + HDC DeviceContext, int WindowWidth, int WindowHeight) +{ + // NOTE(casey): For prototyping purposes, we're going to always blit + // 1-to-1 pixels to make sure we don't introduce artifacts with + // stretching while we are learning to code the renderer! + StretchDIBits(DeviceContext, + 0, 0, Buffer->Width, Buffer->Height, + 0, 0, Buffer->Width, Buffer->Height, + Buffer->Memory, + &Buffer->Info, + DIB_RGB_COLORS, SRCCOPY); +} + +internal LRESULT CALLBACK +Win32MainWindowCallback(HWND Window, + UINT Message, + WPARAM WParam, + LPARAM LParam) +{ + LRESULT Result = 0; + + switch(Message) + { + case WM_CLOSE: + { + // TODO(casey): Handle this with a message to the user? + GlobalRunning = false; + } break; + + case WM_ACTIVATEAPP: + { +#if 0 + if(WParam == TRUE) + { + SetLayeredWindowAttributes(Window, RGB(0, 0, 0), 255, LWA_ALPHA); + } + else + { + SetLayeredWindowAttributes(Window, RGB(0, 0, 0), 64, LWA_ALPHA); + } +#endif + } break; + + case WM_DESTROY: + { + // TODO(casey): Handle this as an error - recreate window? + GlobalRunning = false; + } break; + + case WM_SYSKEYDOWN: + case WM_SYSKEYUP: + case WM_KEYDOWN: + case WM_KEYUP: + { + Assert(!"Keyboard input came in through a non-dispatch message!"); + } break; + + case WM_PAINT: + { + PAINTSTRUCT Paint; + HDC DeviceContext = BeginPaint(Window, &Paint); + win32_window_dimension Dimension = Win32GetWindowDimension(Window); + Win32DisplayBufferInWindow(&GlobalBackbuffer, DeviceContext, + Dimension.Width, Dimension.Height); + EndPaint(Window, &Paint); + } break; + + default: + { + // OutputDebugStringA("default\n"); + Result = DefWindowProcA(Window, Message, WParam, LParam); + } break; + } + + return(Result); +} + +internal void +Win32ClearBuffer(win32_sound_output *SoundOutput) +{ + VOID *Region1; + DWORD Region1Size; + VOID *Region2; + DWORD Region2Size; + if(SUCCEEDED(GlobalSecondaryBuffer->Lock(0, SoundOutput->SecondaryBufferSize, + &Region1, &Region1Size, + &Region2, &Region2Size, + 0))) + { + // TODO(casey): assert that Region1Size/Region2Size is valid + u8 *DestSample = (u8 *)Region1; + for(DWORD ByteIndex = 0; + ByteIndex < Region1Size; + ++ByteIndex) + { + *DestSample++ = 0; + } + + DestSample = (u8 *)Region2; + for(DWORD ByteIndex = 0; + ByteIndex < Region2Size; + ++ByteIndex) + { + *DestSample++ = 0; + } + + GlobalSecondaryBuffer->Unlock(Region1, Region1Size, Region2, Region2Size); + } +} + +internal void +Win32FillSoundBuffer(win32_sound_output *SoundOutput, DWORD ByteToLock, DWORD BytesToWrite, + game_sound_output_buffer *SourceBuffer) +{ + // TODO(casey): More strenuous test! + VOID *Region1; + DWORD Region1Size; + VOID *Region2; + DWORD Region2Size; + if(SUCCEEDED(GlobalSecondaryBuffer->Lock(ByteToLock, BytesToWrite, + &Region1, &Region1Size, + &Region2, &Region2Size, + 0))) + { + // TODO(casey): assert that Region1Size/Region2Size is valid + + // TODO(casey): Collapse these two loops + DWORD Region1SampleCount = Region1Size/SoundOutput->BytesPerSample; + s16 *DestSample = (s16 *)Region1; + s16 *SourceSample = SourceBuffer->Samples; + for(DWORD SampleIndex = 0; + SampleIndex < Region1SampleCount; + ++SampleIndex) + { + *DestSample++ = *SourceSample++; + *DestSample++ = *SourceSample++; + ++SoundOutput->RunningSampleIndex; + } + + DWORD Region2SampleCount = Region2Size/SoundOutput->BytesPerSample; + DestSample = (s16 *)Region2; + for(DWORD SampleIndex = 0; + SampleIndex < Region2SampleCount; + ++SampleIndex) + { + *DestSample++ = *SourceSample++; + *DestSample++ = *SourceSample++; + ++SoundOutput->RunningSampleIndex; + } + + GlobalSecondaryBuffer->Unlock(Region1, Region1Size, Region2, Region2Size); + } +} + +internal void +Win32ProcessKeyboardMessage(game_button_state *NewState, b32 IsDown) +{ + if(NewState->EndedDown != IsDown) + { + NewState->EndedDown = IsDown; + ++NewState->HalfTransitionCount; + } +} + +internal void +Win32ProcessXInputDigitalButton(DWORD XInputButtonState, + game_button_state *OldState, DWORD ButtonBit, + game_button_state *NewState) +{ + NewState->EndedDown = ((XInputButtonState & ButtonBit) == ButtonBit); + NewState->HalfTransitionCount = (OldState->EndedDown != NewState->EndedDown) ? 1 : 0; +} + +internal r32 +Win32ProcessXInputStickValue(SHORT Value, SHORT DeadZoneThreshold) +{ + r32 Result = 0; + + if(Value < -DeadZoneThreshold) + { + Result = (r32)((Value + DeadZoneThreshold) / (32768.0f - DeadZoneThreshold)); + } + else if(Value > DeadZoneThreshold) + { + Result = (r32)((Value - DeadZoneThreshold) / (32767.0f - DeadZoneThreshold)); + } + + return(Result); +} + +internal void +Win32GetInputFileLocation(win32_state *State, b32 InputStream, + int SlotIndex, int DestCount, char *Dest) +{ + char Temp[64]; + wsprintf(Temp, "loop_edit_%d_%s.hmi", SlotIndex, InputStream ? "input" : "state"); + Win32BuildEXEPathFileName(State, Temp, DestCount, Dest); +} + +internal win32_replay_buffer * +Win32GetReplayBuffer(win32_state *State, int unsigned Index) +{ + Assert(Index < ArrayCount(State->ReplayBuffers)); + win32_replay_buffer *Result = &State->ReplayBuffers[Index]; + return(Result); +} + +internal void +Win32BeginRecordingInput(win32_state *State, int InputRecordingIndex) +{ + win32_replay_buffer *ReplayBuffer = Win32GetReplayBuffer(State, InputRecordingIndex); + if(ReplayBuffer->MemoryBlock) + { + State->InputRecordingIndex = InputRecordingIndex; + + char FileName[WIN32_STATE_FILE_NAME_COUNT]; + Win32GetInputFileLocation(State, true, InputRecordingIndex, sizeof(FileName), FileName); + State->RecordingHandle = CreateFileA(FileName, GENERIC_WRITE, 0, 0, CREATE_ALWAYS, 0, 0); + +#if 0 + LARGE_INTEGER FilePosition; + FilePosition.QuadPart = State->TotalSize; + SetFilePointerEx(State->RecordingHandle, FilePosition, 0, FILE_BEGIN); +#endif + + CopyMemory(ReplayBuffer->MemoryBlock, State->GameMemoryBlock, State->TotalSize); + } +} + +internal void +Win32EndRecordingInput(win32_state *State) +{ + CloseHandle(State->RecordingHandle); + State->InputRecordingIndex = 0; +} + +internal void +Win32BeginInputPlayBack(win32_state *State, int InputPlayingIndex) +{ + win32_replay_buffer *ReplayBuffer = Win32GetReplayBuffer(State, InputPlayingIndex); + if(ReplayBuffer->MemoryBlock) + { + State->InputPlayingIndex = InputPlayingIndex; + + char FileName[WIN32_STATE_FILE_NAME_COUNT]; + Win32GetInputFileLocation(State, true, InputPlayingIndex, sizeof(FileName), FileName); + State->PlaybackHandle = CreateFileA(FileName, GENERIC_READ, 0, 0, OPEN_EXISTING, 0, 0); + +#if 0 + LARGE_INTEGER FilePosition; + FilePosition.QuadPart = State->TotalSize; + SetFilePointerEx(State->PlaybackHandle, FilePosition, 0, FILE_BEGIN); +#endif + + CopyMemory(State->GameMemoryBlock, ReplayBuffer->MemoryBlock, State->TotalSize); + } +} + +internal void +Win32EndInputPlayBack(win32_state *State) +{ + CloseHandle(State->PlaybackHandle); + State->InputPlayingIndex = 0; +} + +internal void +Win32RecordInput(win32_state *State, game_input *NewInput) +{ + DWORD BytesWritten; + WriteFile(State->RecordingHandle, NewInput, sizeof(*NewInput), &BytesWritten, 0); +} + +internal void +Win32PlayBackInput(win32_state *State, game_input *NewInput) +{ + DWORD BytesRead = 0; + if(ReadFile(State->PlaybackHandle, NewInput, sizeof(*NewInput), &BytesRead, 0)) + { + if(BytesRead == 0) + { + // NOTE(casey): We've hit the end of the stream, go back to the beginning + int PlayingIndex = State->InputPlayingIndex; + Win32EndInputPlayBack(State); + Win32BeginInputPlayBack(State, PlayingIndex); + ReadFile(State->PlaybackHandle, NewInput, sizeof(*NewInput), &BytesRead, 0); + } + } +} + +internal void +Win32ProcessPendingMessages(win32_state *State, game_controller_input *KeyboardController) +{ + MSG Message; + while(PeekMessage(&Message, 0, 0, 0, PM_REMOVE)) + { + switch(Message.message) + { + case WM_QUIT: + { + GlobalRunning = false; + } break; + + case WM_SYSKEYDOWN: + case WM_SYSKEYUP: + case WM_KEYDOWN: + case WM_KEYUP: + { + u32 VKCode = (u32)Message.wParam; + + // NOTE(casey): Since we are comparing WasDown to IsDown, + // we MUST use == and != to convert these bit tests to actual + // 0 or 1 values. + b32 WasDown = ((Message.lParam & (1 << 30)) != 0); + b32 IsDown = ((Message.lParam & (1 << 31)) == 0); + if(WasDown != IsDown) + { + if(VKCode == 'W') + { + Win32ProcessKeyboardMessage(&KeyboardController->MoveUp, IsDown); + } + else if(VKCode == 'A') + { + Win32ProcessKeyboardMessage(&KeyboardController->MoveLeft, IsDown); + } + else if(VKCode == 'R') + { + Win32ProcessKeyboardMessage(&KeyboardController->MoveDown, IsDown); + } + else if(VKCode == 'S') + { + Win32ProcessKeyboardMessage(&KeyboardController->MoveRight, IsDown); + } + else if(VKCode == 'Q') + { + Win32ProcessKeyboardMessage(&KeyboardController->LeftShoulder, IsDown); + } + else if(VKCode == 'E') + { + Win32ProcessKeyboardMessage(&KeyboardController->RightShoulder, IsDown); + } + else if(VKCode == VK_UP) + { + Win32ProcessKeyboardMessage(&KeyboardController->ActionUp, IsDown); + } + else if(VKCode == VK_LEFT) + { + Win32ProcessKeyboardMessage(&KeyboardController->ActionLeft, IsDown); + } + else if(VKCode == VK_DOWN) + { + Win32ProcessKeyboardMessage(&KeyboardController->ActionDown, IsDown); + } + else if(VKCode == VK_RIGHT) + { + Win32ProcessKeyboardMessage(&KeyboardController->ActionRight, IsDown); + } + else if(VKCode == VK_ESCAPE) + { + Win32ProcessKeyboardMessage(&KeyboardController->Start, IsDown); + } + else if(VKCode == VK_SPACE) + { + Win32ProcessKeyboardMessage(&KeyboardController->Back, IsDown); + } +#if HANDMADE_INTERNAL + else if(VKCode == 'P') + { + if(IsDown) + { + GlobalPause = !GlobalPause; + } + } + else if(VKCode == 'L') + { + if(IsDown) + { + if(State->InputPlayingIndex == 0) + { + if(State->InputRecordingIndex == 0) + { + Win32BeginRecordingInput(State, 1); + } + else + { + Win32EndRecordingInput(State); + Win32BeginInputPlayBack(State, 1); + } + } + else + { + Win32EndInputPlayBack(State); + } + } + } +#endif + } + + b32 AltKeyWasDown = (Message.lParam & (1 << 29)); + if((VKCode == VK_F4) && AltKeyWasDown) + { + GlobalRunning = false; + } + } break; + + default: + { + TranslateMessage(&Message); + DispatchMessageA(&Message); + } break; + } + } +} + +inline LARGE_INTEGER +Win32GetWallClock(void) +{ + LARGE_INTEGER Result; + QueryPerformanceCounter(&Result); + return(Result); +} + +inline r32 +Win32GetSecondsElapsed(LARGE_INTEGER Start, LARGE_INTEGER End) +{ + r32 Result = ((r32)(End.QuadPart - Start.QuadPart) / + (r32)GlobalPerfCountFrequency); + return(Result); +} + +#if 0 + +internal void +Win32DebugDrawVertical(win32_offscreen_buffer *Backbuffer, + int X, int Top, int Bottom, u32 Color) +{ + if(Top <= 0) + { + Top = 0; + } + + if(Bottom > Backbuffer->Height) + { + Bottom = Backbuffer->Height; + } + + if((X >= 0) && (X < Backbuffer->Width)) + { + u8 *Pixel = ((u8 *)Backbuffer->Memory + + X*Backbuffer->BytesPerPixel + + Top*Backbuffer->Pitch); + for(int Y = Top; + Y < Bottom; + ++Y) + { + *(u32 *)Pixel = Color; + Pixel += Backbuffer->Pitch; + } + } +} + +inline void +Win32DrawSoundBufferMarker(win32_offscreen_buffer *Backbuffer, + win32_sound_output *SoundOutput, + r32 C, int PadX, int Top, int Bottom, + DWORD Value, u32 Color) +{ + r32 XReal32 = (C * (r32)Value); + int X = PadX + (int)XReal32; + Win32DebugDrawVertical(Backbuffer, X, Top, Bottom, Color); +} + +internal void +Win32DebugSyncDisplay(win32_offscreen_buffer *Backbuffer, + int MarkerCount, win32_debug_time_marker *Markers, + int CurrentMarkerIndex, + win32_sound_output *SoundOutput, r32 TargetSecondsPerFrame) +{ + int PadX = 16; + int PadY = 16; + + int LineHeight = 64; + + r32 C = (r32)(Backbuffer->Width - 2*PadX) / (r32)SoundOutput->SecondaryBufferSize; + for(int MarkerIndex = 0; + MarkerIndex < MarkerCount; + ++MarkerIndex) + { + win32_debug_time_marker *ThisMarker = &Markers[MarkerIndex]; + Assert(ThisMarker->OutputPlayCursor < SoundOutput->SecondaryBufferSize); + Assert(ThisMarker->OutputWriteCursor < SoundOutput->SecondaryBufferSize); + Assert(ThisMarker->OutputLocation < SoundOutput->SecondaryBufferSize); + Assert(ThisMarker->OutputByteCount < SoundOutput->SecondaryBufferSize); + Assert(ThisMarker->FlipPlayCursor < SoundOutput->SecondaryBufferSize); + Assert(ThisMarker->FlipWriteCursor < SoundOutput->SecondaryBufferSize); + + DWORD PlayColor = 0xFFFFFFFF; + DWORD WriteColor = 0xFFFF0000; + DWORD ExpectedFlipColor = 0xFFFFFF00; + DWORD PlayWindowColor = 0xFFFF00FF; + + int Top = PadY; + int Bottom = PadY + LineHeight; + if(MarkerIndex == CurrentMarkerIndex) + { + Top += LineHeight+PadY; + Bottom += LineHeight+PadY; + + int FirstTop = Top; + + Win32DrawSoundBufferMarker(Backbuffer, SoundOutput, C, PadX, Top, Bottom, ThisMarker->OutputPlayCursor, PlayColor); + Win32DrawSoundBufferMarker(Backbuffer, SoundOutput, C, PadX, Top, Bottom, ThisMarker->OutputWriteCursor, WriteColor); + + Top += LineHeight+PadY; + Bottom += LineHeight+PadY; + + Win32DrawSoundBufferMarker(Backbuffer, SoundOutput, C, PadX, Top, Bottom, ThisMarker->OutputLocation, PlayColor); + Win32DrawSoundBufferMarker(Backbuffer, SoundOutput, C, PadX, Top, Bottom, ThisMarker->OutputLocation + ThisMarker->OutputByteCount, WriteColor); + + Top += LineHeight+PadY; + Bottom += LineHeight+PadY; + + Win32DrawSoundBufferMarker(Backbuffer, SoundOutput, C, PadX, FirstTop, Bottom, ThisMarker->ExpectedFlipPlayCursor, ExpectedFlipColor); + } + + Win32DrawSoundBufferMarker(Backbuffer, SoundOutput, C, PadX, Top, Bottom, ThisMarker->FlipPlayCursor, PlayColor); + Win32DrawSoundBufferMarker(Backbuffer, SoundOutput, C, PadX, Top, Bottom, ThisMarker->FlipPlayCursor + 480*SoundOutput->BytesPerSample, PlayWindowColor); + Win32DrawSoundBufferMarker(Backbuffer, SoundOutput, C, PadX, Top, Bottom, ThisMarker->FlipWriteCursor, WriteColor); + } +} + +#endif + +int CALLBACK +WinMain(HINSTANCE Instance, + HINSTANCE PrevInstance, + LPSTR CommandLine, + int ShowCode) +{ + win32_state Win32State = {}; + + LARGE_INTEGER PerfCountFrequencyResult; + QueryPerformanceFrequency(&PerfCountFrequencyResult); + GlobalPerfCountFrequency = PerfCountFrequencyResult.QuadPart; + + Win32GetEXEFileName(&Win32State); + + char SourceGameCodeDLLFullPath[WIN32_STATE_FILE_NAME_COUNT]; + Win32BuildEXEPathFileName(&Win32State, "handmade.dll", + sizeof(SourceGameCodeDLLFullPath), SourceGameCodeDLLFullPath); + + char TempGameCodeDLLFullPath[WIN32_STATE_FILE_NAME_COUNT]; + Win32BuildEXEPathFileName(&Win32State, "handmade_temp.dll", + sizeof(TempGameCodeDLLFullPath), TempGameCodeDLLFullPath); + + // NOTE(casey): Set the Windows scheduler granularity to 1ms + // so that our Sleep() can be more granular. + UINT DesiredSchedulerMS = 1; + b32 SleepIsGranular = (timeBeginPeriod(DesiredSchedulerMS) == TIMERR_NOERROR); + + Win32LoadXInput(); + + WNDCLASSA WindowClass = {}; + + Win32ResizeDIBSection(&GlobalBackbuffer, 1280, 720); + + WindowClass.style = CS_HREDRAW|CS_VREDRAW; + WindowClass.lpfnWndProc = Win32MainWindowCallback; + WindowClass.hInstance = Instance; + // WindowClass.hIcon; + WindowClass.lpszClassName = "HandmadeHeroWindowClass"; + + if(RegisterClassA(&WindowClass)) + { + HWND Window = + CreateWindowExA( + 0, // WS_EX_TOPMOST|WS_EX_LAYERED, + WindowClass.lpszClassName, + "Handmade Hero", + WS_OVERLAPPEDWINDOW|WS_VISIBLE, + CW_USEDEFAULT, + CW_USEDEFAULT, + CW_USEDEFAULT, + CW_USEDEFAULT, + 0, + 0, + Instance, + 0); + if(Window) + { + win32_sound_output SoundOutput = {}; + + // TODO(casey): How do we reliably query on this on Windows? + int MonitorRefreshHz = 60; + HDC RefreshDC = GetDC(Window); + int Win32RefreshRate = GetDeviceCaps(RefreshDC, VREFRESH); + ReleaseDC(Window, RefreshDC); + if(Win32RefreshRate > 1) + { + MonitorRefreshHz = Win32RefreshRate; + } + r32 GameUpdateHz = (MonitorRefreshHz / 2.0f); + r32 TargetSecondsPerFrame = 1.0f / (r32)GameUpdateHz; + + // TODO(casey): Make this like sixty seconds? + SoundOutput.SamplesPerSecond = 48000; + SoundOutput.BytesPerSample = sizeof(s16)*2; + SoundOutput.SecondaryBufferSize = SoundOutput.SamplesPerSecond*SoundOutput.BytesPerSample; + // TODO(casey): Actually compute this variance and see + // what the lowest reasonable value is. + SoundOutput.SafetyBytes = (int)(((r32)SoundOutput.SamplesPerSecond*(r32)SoundOutput.BytesPerSample / GameUpdateHz)/3.0f); + Win32InitDSound(Window, SoundOutput.SamplesPerSecond, SoundOutput.SecondaryBufferSize); + Win32ClearBuffer(&SoundOutput); + GlobalSecondaryBuffer->Play(0, 0, DSBPLAY_LOOPING); + + GlobalRunning = true; + +#if 0 + // NOTE(casey): This tests the PlayCursor/WriteCursor update frequency + // On the Handmade Hero machine, it was 480 samples. + while(GlobalRunning) + { + DWORD PlayCursor; + DWORD WriteCursor; + GlobalSecondaryBuffer->GetCurrentPosition(&PlayCursor, &WriteCursor); + + char TextBuffer[256]; + _snprintf_s(TextBuffer, sizeof(TextBuffer), + "PC:%u WC:%u\n", PlayCursor, WriteCursor); + OutputDebugStringA(TextBuffer); + } +#endif + + // TODO(casey): Pool with bitmap VirtualAlloc + s16 *Samples = (s16 *)VirtualAlloc(0, SoundOutput.SecondaryBufferSize, + MEM_RESERVE|MEM_COMMIT, PAGE_READWRITE); + + +#if HANDMADE_INTERNAL + LPVOID BaseAddress = (LPVOID)Terabytes(2); +#else + LPVOID BaseAddress = 0; +#endif + + game_memory GameMemory = {}; + GameMemory.PermanentStorageSize = Megabytes(64); + GameMemory.TransientStorageSize = Gigabytes(1); + GameMemory.DEBUGPlatformFreeFileMemory = DEBUGPlatformFreeFileMemory; + GameMemory.DEBUGPlatformReadEntireFile = DEBUGPlatformReadEntireFile; + GameMemory.DEBUGPlatformWriteEntireFile = DEBUGPlatformWriteEntireFile; + + + // TODO(casey): Handle various memory footprints (USING SYSTEM METRICS) + // TODO(casey): Use MEM_LARGE_PAGES and call adjust token + // privileges when not on Windows XP? + Win32State.TotalSize = GameMemory.PermanentStorageSize + GameMemory.TransientStorageSize; + Win32State.GameMemoryBlock = VirtualAlloc(BaseAddress, (size_t)Win32State.TotalSize, + MEM_RESERVE|MEM_COMMIT, + PAGE_READWRITE); + GameMemory.PermanentStorage = Win32State.GameMemoryBlock; + GameMemory.TransientStorage = ((u8 *)GameMemory.PermanentStorage + + GameMemory.PermanentStorageSize); + + for(int ReplayIndex = 0; + ReplayIndex < ArrayCount(Win32State.ReplayBuffers); + ++ReplayIndex) + { + win32_replay_buffer *ReplayBuffer = &Win32State.ReplayBuffers[ReplayIndex]; + + // TODO(casey): Recording system still seems to take too long + // on record start - find out what Windows is doing and if + // we can speed up / defer some of that processing. + + Win32GetInputFileLocation(&Win32State, false, ReplayIndex, + sizeof(ReplayBuffer->FileName), ReplayBuffer->FileName); + + ReplayBuffer->FileHandle = + CreateFileA(ReplayBuffer->FileName, + GENERIC_WRITE|GENERIC_READ, 0, 0, CREATE_ALWAYS, 0, 0); + + LARGE_INTEGER MaxSize; + MaxSize.QuadPart = Win32State.TotalSize; + ReplayBuffer->MemoryMap = CreateFileMapping( + ReplayBuffer->FileHandle, 0, PAGE_READWRITE, + MaxSize.HighPart, MaxSize.LowPart, 0); + + ReplayBuffer->MemoryBlock = MapViewOfFile( + ReplayBuffer->MemoryMap, FILE_MAP_ALL_ACCESS, 0, 0, Win32State.TotalSize); + if(ReplayBuffer->MemoryBlock) + { + } + else + { + // TODO(casey): Diagnostic + } + } + + if(Samples && GameMemory.PermanentStorage && GameMemory.TransientStorage) + { + game_input Input[2] = {}; + game_input *NewInput = &Input[0]; + game_input *OldInput = &Input[1]; + + LARGE_INTEGER LastCounter = Win32GetWallClock(); + LARGE_INTEGER FlipWallClock = Win32GetWallClock(); + + int DebugTimeMarkerIndex = 0; + win32_debug_time_marker DebugTimeMarkers[30] = {0}; + + DWORD AudioLatencyBytes = 0; + r32 AudioLatencySeconds = 0; + b32 SoundIsValid = false; + + win32_game_code Game = Win32LoadGameCode(SourceGameCodeDLLFullPath, + TempGameCodeDLLFullPath); + u32 LoadCounter = 0; + + s64 LastCycleCount = __rdtsc(); + while(GlobalRunning) + { + FILETIME NewDLLWriteTime = Win32GetLastWriteTime(SourceGameCodeDLLFullPath); + if(CompareFileTime(&NewDLLWriteTime, &Game.DLLLastWriteTime) != 0) + { + Win32UnloadGameCode(&Game); + Game = Win32LoadGameCode(SourceGameCodeDLLFullPath, + TempGameCodeDLLFullPath); + LoadCounter = 0; + } + + // TODO(casey): Zeroing macro + // TODO(casey): We can't zero everything because the up/down state will + // be wrong!!! + game_controller_input *OldKeyboardController = GetController(OldInput, 0); + game_controller_input *NewKeyboardController = GetController(NewInput, 0); + *NewKeyboardController = {}; + NewKeyboardController->IsConnected = true; + for(int ButtonIndex = 0; + ButtonIndex < ArrayCount(NewKeyboardController->Buttons); + ++ButtonIndex) + { + NewKeyboardController->Buttons[ButtonIndex].EndedDown = + OldKeyboardController->Buttons[ButtonIndex].EndedDown; + } + + Win32ProcessPendingMessages(&Win32State, NewKeyboardController); + + if(!GlobalPause) + { + POINT MouseP; + GetCursorPos(&MouseP); + ScreenToClient(Window, &MouseP); + NewInput->MouseX = MouseP.x; + NewInput->MouseY = MouseP.y; + NewInput->MouseZ = 0; // TODO(casey): Support mousewheel? + Win32ProcessKeyboardMessage(&NewInput->MouseButtons[0], + GetKeyState(VK_LBUTTON) & (1 << 15)); + Win32ProcessKeyboardMessage(&NewInput->MouseButtons[1], + GetKeyState(VK_MBUTTON) & (1 << 15)); + Win32ProcessKeyboardMessage(&NewInput->MouseButtons[2], + GetKeyState(VK_RBUTTON) & (1 << 15)); + Win32ProcessKeyboardMessage(&NewInput->MouseButtons[3], + GetKeyState(VK_XBUTTON1) & (1 << 15)); + Win32ProcessKeyboardMessage(&NewInput->MouseButtons[4], + GetKeyState(VK_XBUTTON2) & (1 << 15)); + + // TODO(casey): Need to not poll disconnected controllers to avoid + // xinput frame rate hit on older libraries... + // TODO(casey): Should we poll this more frequently + DWORD MaxControllerCount = XUSER_MAX_COUNT; + if(MaxControllerCount > (ArrayCount(NewInput->Controllers) - 1)) + { + MaxControllerCount = (ArrayCount(NewInput->Controllers) - 1); + } + + for (DWORD ControllerIndex = 0; + ControllerIndex < MaxControllerCount; + ++ControllerIndex) + { + DWORD OurControllerIndex = ControllerIndex + 1; + game_controller_input *OldController = GetController(OldInput, OurControllerIndex); + game_controller_input *NewController = GetController(NewInput, OurControllerIndex); + + XINPUT_STATE ControllerState; + if(XInputGetState(ControllerIndex, &ControllerState) == ERROR_SUCCESS) + { + NewController->IsConnected = true; + NewController->IsAnalog = OldController->IsAnalog; + + // NOTE(casey): This controller is plugged in + // TODO(casey): See if ControllerState.dwPacketNumber increments too rapidly + XINPUT_GAMEPAD *Pad = &ControllerState.Gamepad; + + // TODO(casey): This is a square deadzone, check XInput to + // verify that the deadzone is "round" and show how to do + // round deadzone processing. + NewController->StickAverageX = Win32ProcessXInputStickValue( + Pad->sThumbLX, XINPUT_GAMEPAD_LEFT_THUMB_DEADZONE); + NewController->StickAverageY = Win32ProcessXInputStickValue( + Pad->sThumbLY, XINPUT_GAMEPAD_LEFT_THUMB_DEADZONE); + if((NewController->StickAverageX != 0.0f) || + (NewController->StickAverageY != 0.0f)) + { + NewController->IsAnalog = true; + } + + if(Pad->wButtons & XINPUT_GAMEPAD_DPAD_UP) + { + NewController->StickAverageY = 1.0f; + NewController->IsAnalog = false; + } + + if(Pad->wButtons & XINPUT_GAMEPAD_DPAD_DOWN) + { + NewController->StickAverageY = -1.0f; + NewController->IsAnalog = false; + } + + if(Pad->wButtons & XINPUT_GAMEPAD_DPAD_LEFT) + { + NewController->StickAverageX = -1.0f; + NewController->IsAnalog = false; + } + + if(Pad->wButtons & XINPUT_GAMEPAD_DPAD_RIGHT) + { + NewController->StickAverageX = 1.0f; + NewController->IsAnalog = false; + } + + r32 Threshold = 0.5f; + Win32ProcessXInputDigitalButton( + (NewController->StickAverageX < -Threshold) ? 1 : 0, + &OldController->MoveLeft, 1, + &NewController->MoveLeft); + Win32ProcessXInputDigitalButton( + (NewController->StickAverageX > Threshold) ? 1 : 0, + &OldController->MoveRight, 1, + &NewController->MoveRight); + Win32ProcessXInputDigitalButton( + (NewController->StickAverageY < -Threshold) ? 1 : 0, + &OldController->MoveDown, 1, + &NewController->MoveDown); + Win32ProcessXInputDigitalButton( + (NewController->StickAverageY > Threshold) ? 1 : 0, + &OldController->MoveUp, 1, + &NewController->MoveUp); + + Win32ProcessXInputDigitalButton(Pad->wButtons, + &OldController->ActionDown, XINPUT_GAMEPAD_A, + &NewController->ActionDown); + Win32ProcessXInputDigitalButton(Pad->wButtons, + &OldController->ActionRight, XINPUT_GAMEPAD_B, + &NewController->ActionRight); + Win32ProcessXInputDigitalButton(Pad->wButtons, + &OldController->ActionLeft, XINPUT_GAMEPAD_X, + &NewController->ActionLeft); + Win32ProcessXInputDigitalButton(Pad->wButtons, + &OldController->ActionUp, XINPUT_GAMEPAD_Y, + &NewController->ActionUp); + Win32ProcessXInputDigitalButton(Pad->wButtons, + &OldController->LeftShoulder, XINPUT_GAMEPAD_LEFT_SHOULDER, + &NewController->LeftShoulder); + Win32ProcessXInputDigitalButton(Pad->wButtons, + &OldController->RightShoulder, XINPUT_GAMEPAD_RIGHT_SHOULDER, + &NewController->RightShoulder); + + Win32ProcessXInputDigitalButton(Pad->wButtons, + &OldController->Start, XINPUT_GAMEPAD_START, + &NewController->Start); + Win32ProcessXInputDigitalButton(Pad->wButtons, + &OldController->Back, XINPUT_GAMEPAD_BACK, + &NewController->Back); + } + else + { + // NOTE(casey): The controller is not available + NewController->IsConnected = false; + } + } + + thread_context Thread = {}; + + game_offscreen_buffer Buffer = {}; + Buffer.Memory = GlobalBackbuffer.Memory; + Buffer.Width = GlobalBackbuffer.Width; + Buffer.Height = GlobalBackbuffer.Height; + Buffer.Pitch = GlobalBackbuffer.Pitch; + Buffer.BytesPerPixel = GlobalBackbuffer.BytesPerPixel; + + if(Win32State.InputRecordingIndex) + { + Win32RecordInput(&Win32State, NewInput); + } + + if(Win32State.InputPlayingIndex) + { + Win32PlayBackInput(&Win32State, NewInput); + } + if(Game.UpdateAndRender) + { + Game.UpdateAndRender(&Thread, &GameMemory, NewInput, &Buffer); + } + + LARGE_INTEGER AudioWallClock = Win32GetWallClock(); + r32 FromBeginToAudioSeconds = Win32GetSecondsElapsed(FlipWallClock, AudioWallClock); + + DWORD PlayCursor; + DWORD WriteCursor; + if(GlobalSecondaryBuffer->GetCurrentPosition(&PlayCursor, &WriteCursor) == DS_OK) + { + /* NOTE(casey): + + Here is how sound output computation works. + + We define a safety value that is the number + of samples we think our game update loop + may vary by (let's say up to 2ms) + + When we wake up to write audio, we will look + and see what the play cursor position is and we + will forecast ahead where we think the play + cursor will be on the next frame boundary. + + We will then look to see if the write cursor is + before that by at least our safety value. If + it is, the target fill position is that frame + boundary plus one frame. This gives us perfect + audio sync in the case of a card that has low + enough latency. + + If the write cursor is _after_ that safety + margin, then we assume we can never sync the + audio perfectly, so we will write one frame's + worth of audio plus the safety margin's worth + of guard samples. + */ + if(!SoundIsValid) + { + SoundOutput.RunningSampleIndex = WriteCursor / SoundOutput.BytesPerSample; + SoundIsValid = true; + } + + DWORD ByteToLock = ((SoundOutput.RunningSampleIndex*SoundOutput.BytesPerSample) % + SoundOutput.SecondaryBufferSize); + + DWORD ExpectedSoundBytesPerFrame = + (int)((r32)(SoundOutput.SamplesPerSecond*SoundOutput.BytesPerSample) / + GameUpdateHz); + r32 SecondsLeftUntilFlip = (TargetSecondsPerFrame - FromBeginToAudioSeconds); + DWORD ExpectedBytesUntilFlip = (DWORD)((SecondsLeftUntilFlip/TargetSecondsPerFrame)*(r32)ExpectedSoundBytesPerFrame); + + DWORD ExpectedFrameBoundaryByte = PlayCursor + ExpectedBytesUntilFlip; + + DWORD SafeWriteCursor = WriteCursor; + if(SafeWriteCursor < PlayCursor) + { + SafeWriteCursor += SoundOutput.SecondaryBufferSize; + } + Assert(SafeWriteCursor >= PlayCursor); + SafeWriteCursor += SoundOutput.SafetyBytes; + + b32 AudioCardIsLowLatency = (SafeWriteCursor < ExpectedFrameBoundaryByte); + + DWORD TargetCursor = 0; + if(AudioCardIsLowLatency) + { + TargetCursor = (ExpectedFrameBoundaryByte + ExpectedSoundBytesPerFrame); + } + else + { + TargetCursor = (WriteCursor + ExpectedSoundBytesPerFrame + + SoundOutput.SafetyBytes); + } + TargetCursor = (TargetCursor % SoundOutput.SecondaryBufferSize); + + DWORD BytesToWrite = 0; + if(ByteToLock > TargetCursor) + { + BytesToWrite = (SoundOutput.SecondaryBufferSize - ByteToLock); + BytesToWrite += TargetCursor; + } + else + { + BytesToWrite = TargetCursor - ByteToLock; + } + + game_sound_output_buffer SoundBuffer = {}; + SoundBuffer.SamplesPerSecond = SoundOutput.SamplesPerSecond; + SoundBuffer.SampleCount = BytesToWrite / SoundOutput.BytesPerSample; + SoundBuffer.Samples = Samples; + if(Game.GetSoundSamples) + { + Game.GetSoundSamples(&Thread, &GameMemory, &SoundBuffer); + } + +#if HANDMADE_INTERNAL + win32_debug_time_marker *Marker = &DebugTimeMarkers[DebugTimeMarkerIndex]; + Marker->OutputPlayCursor = PlayCursor; + Marker->OutputWriteCursor = WriteCursor; + Marker->OutputLocation = ByteToLock; + Marker->OutputByteCount = BytesToWrite; + Marker->ExpectedFlipPlayCursor = ExpectedFrameBoundaryByte; + + DWORD UnwrappedWriteCursor = WriteCursor; + if(UnwrappedWriteCursor < PlayCursor) + { + UnwrappedWriteCursor += SoundOutput.SecondaryBufferSize; + } + AudioLatencyBytes = UnwrappedWriteCursor - PlayCursor; + AudioLatencySeconds = + (((r32)AudioLatencyBytes / (r32)SoundOutput.BytesPerSample) / + (r32)SoundOutput.SamplesPerSecond); + +#if 0 + char TextBuffer[256]; + _snprintf_s(TextBuffer, sizeof(TextBuffer), + "BTL:%u TC:%u BTW:%u - PC:%u WC:%u DELTA:%u (%fs)\n", + ByteToLock, TargetCursor, BytesToWrite, + PlayCursor, WriteCursor, AudioLatencyBytes, AudioLatencySeconds); + OutputDebugStringA(TextBuffer); +#endif +#endif + Win32FillSoundBuffer(&SoundOutput, ByteToLock, BytesToWrite, &SoundBuffer); + } + else + { + SoundIsValid = false; + } + + LARGE_INTEGER WorkCounter = Win32GetWallClock(); + r32 WorkSecondsElapsed = Win32GetSecondsElapsed(LastCounter, WorkCounter); + + // TODO(casey): NOT TESTED YET! PROBABLY BUGGY!!!!! + r32 SecondsElapsedForFrame = WorkSecondsElapsed; + if(SecondsElapsedForFrame < TargetSecondsPerFrame) + { + if(SleepIsGranular) + { + DWORD SleepMS = (DWORD)(1000.0f * (TargetSecondsPerFrame - + SecondsElapsedForFrame)); + if(SleepMS > 0) + { + Sleep(SleepMS); + } + } + + r32 TestSecondsElapsedForFrame = Win32GetSecondsElapsed(LastCounter, + Win32GetWallClock()); + if(TestSecondsElapsedForFrame < TargetSecondsPerFrame) + { + // TODO(casey): LOG MISSED SLEEP HERE + } + + while(SecondsElapsedForFrame < TargetSecondsPerFrame) + { + SecondsElapsedForFrame = Win32GetSecondsElapsed(LastCounter, + Win32GetWallClock()); + } + } + else + { + // TODO(casey): MISSED FRAME RATE! + // TODO(casey): Logging + } + + LARGE_INTEGER EndCounter = Win32GetWallClock(); + r32 MSPerFrame = 1000.0f*Win32GetSecondsElapsed(LastCounter, EndCounter); + LastCounter = EndCounter; + + win32_window_dimension Dimension = Win32GetWindowDimension(Window); + HDC DeviceContext = GetDC(Window); + Win32DisplayBufferInWindow(&GlobalBackbuffer, DeviceContext, + Dimension.Width, Dimension.Height); + ReleaseDC(Window, DeviceContext); + + FlipWallClock = Win32GetWallClock(); +#if HANDMADE_INTERNAL + // NOTE(casey): This is debug code + { + DWORD PlayCursor; + DWORD WriteCursor; + if(GlobalSecondaryBuffer->GetCurrentPosition(&PlayCursor, &WriteCursor) == DS_OK) + { + Assert(DebugTimeMarkerIndex < ArrayCount(DebugTimeMarkers)); + win32_debug_time_marker *Marker = &DebugTimeMarkers[DebugTimeMarkerIndex]; + Marker->FlipPlayCursor = PlayCursor; + Marker->FlipWriteCursor = WriteCursor; + } + + } +#endif + + game_input *Temp = NewInput; + NewInput = OldInput; + OldInput = Temp; + // TODO(casey): Should I clear these here? + +#if 1 + u64 EndCycleCount = __rdtsc(); + u64 CyclesElapsed = EndCycleCount - LastCycleCount; + LastCycleCount = EndCycleCount; + + r64 FPS = 0.0f; + r64 MCPF = ((r64)CyclesElapsed / (1000.0f * 1000.0f)); + + char FPSBuffer[256]; + _snprintf_s(FPSBuffer, sizeof(FPSBuffer), + "%.02fms/f, %.02ff/s, %.02fmc/f\n", MSPerFrame, FPS, MCPF); + OutputDebugStringA(FPSBuffer); +#endif + +#if HANDMADE_INTERNAL + ++DebugTimeMarkerIndex; + if(DebugTimeMarkerIndex == ArrayCount(DebugTimeMarkers)) + { + DebugTimeMarkerIndex = 0; + } +#endif + } + } + } + else + { + // TODO(casey): Logging + } + } + else + { + // TODO(casey): Logging + } + } + else + { + // TODO(casey): Logging + } + + return(0); +} diff --git a/code/win32_handmade.h b/code/win32_handmade.h new file mode 100755 index 0000000..ba39332 --- /dev/null +++ b/code/win32_handmade.h @@ -0,0 +1,89 @@ +#if !defined(WIN32_HANDMADE_H) +/* ======================================================================== + $File: $ + $Date: $ + $Revision: $ + $Creator: Casey Muratori $ + $Notice: (C) Copyright 2014 by Molly Rocket, Inc. All Rights Reserved. $ + ======================================================================== */ + +struct win32_offscreen_buffer +{ + // NOTE(casey): Pixels are alwasy 32-bits wide, Memory Order BB GG RR XX + BITMAPINFO Info; + void *Memory; + int Width; + int Height; + int Pitch; + int BytesPerPixel; +}; + +struct win32_window_dimension +{ + int Width; + int Height; +}; + +struct win32_sound_output +{ + int SamplesPerSecond; + u32 RunningSampleIndex; + int BytesPerSample; + DWORD SecondaryBufferSize; + DWORD SafetyBytes; + r32 tSine; + // TODO(casey): Should running sample index be in bytes as well + // TODO(casey): Math gets simpler if we add a "bytes per second" field? +}; + +struct win32_debug_time_marker +{ + DWORD OutputPlayCursor; + DWORD OutputWriteCursor; + DWORD OutputLocation; + DWORD OutputByteCount; + DWORD ExpectedFlipPlayCursor; + + DWORD FlipPlayCursor; + DWORD FlipWriteCursor; +}; + +struct win32_game_code +{ + HMODULE GameCodeDLL; + FILETIME DLLLastWriteTime; + + // IMPORTANT(casey): Either of the callbacks can be 0! You must + // check before calling. + game_update_and_render *UpdateAndRender; + game_get_sound_samples *GetSoundSamples; + + b32 IsValid; +}; + +#define WIN32_STATE_FILE_NAME_COUNT MAX_PATH +struct win32_replay_buffer +{ + HANDLE FileHandle; + HANDLE MemoryMap; + char FileName[WIN32_STATE_FILE_NAME_COUNT]; + void *MemoryBlock; +}; +struct win32_state +{ + u64 TotalSize; + void *GameMemoryBlock; + win32_replay_buffer ReplayBuffers[4]; + + HANDLE RecordingHandle; + int InputRecordingIndex; + + HANDLE PlaybackHandle; + int InputPlayingIndex; + + char EXEFileName[WIN32_STATE_FILE_NAME_COUNT]; + char *OnePastLastEXEFileNameSlash; +}; + +#define WIN32_HANDMADE_H +#endif -- cgit v1.2.3