#include "base_core.h" #include "sdl_handmade.h" #include "handmade.cpp" #include #include // mmap #include // malloc, calloc #include #include // fstat #include // open #include // close, read // NOTE: This is so it compiles on ARM. #if __x86_64__ || __i386__ #include #endif global bool GlobalRunning; global U64 GlobalPerfCountFrequency; #define CONTROLLER_LEFT_THUMB_DEADZONE 8000 #define MAX_CONTROLLERS 4 SDL_GameController *ControllerHandles[MAX_CONTROLLERS]; global offscreen_buffer GlobalBackbuffer; internal U32 SafeTruncateUInt64(U64 Value) { Assert(Value <= 0xFFFFFFFF); U32 Result = (U32)Value; return Result; } internal debug_read_file_result DEBUGPlatformReadEntireFile(const char *Filename) { debug_read_file_result Result = {0}; S32 FileHandle = open(Filename, O_RDONLY); if(FileHandle == -1) { return Result; } struct stat FileStatus; if(fstat(FileHandle, &FileStatus) == -1) { close(FileHandle); return Result; } Result.ContentsSize = SafeTruncateUInt64(FileStatus.st_size); Result.Contents = malloc(Result.ContentsSize); if(!Result.Contents) { Result.ContentsSize = 0; close(FileHandle); return Result; } U32 BytesToRead = Result.ContentsSize; U8 *NextByteLocation = (U8 *)Result.Contents; while(BytesToRead) { U32 BytesRead = read(FileHandle, NextByteLocation, BytesToRead); if(BytesRead == (U32)-1) { free(Result.Contents); Result.Contents = 0; Result.ContentsSize = 0; close(FileHandle); return Result; } BytesToRead -= BytesRead; NextByteLocation += BytesRead; } close(FileHandle); return Result; } internal B32 DEBUGPlatformWriteEntireFile(const char *Filename, U32 MemorySize, void *Memory) { S32 FileHandle = open(Filename, O_WRONLY | O_CREAT, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH); if(FileHandle == -1) return false; U32 BytesToWrite = MemorySize; U8 *NextByteLocation = (U8*)Memory; while(BytesToWrite) { U32 BytesWritten = write(FileHandle, NextByteLocation, BytesToWrite); if(BytesWritten == (U32)-1) { close(FileHandle); return false; } BytesToWrite -= BytesWritten; NextByteLocation += BytesWritten; } close(FileHandle); return true; } internal void DEBUGPlatformFreeFileMemory(void *Memory) { if(Memory) free(Memory); } sdl_window_dimension SDLGetWindowDimension(SDL_Window *Window) { sdl_window_dimension Dimension; SDL_GetWindowSize(Window, &Dimension.Width, &Dimension.Height); return Dimension; } internal void SDLResizeTexture(offscreen_buffer *Buffer, SDL_Renderer *Renderer, int Width, int Height) { if(Buffer->Texture) SDL_DestroyTexture(Buffer->Texture); if(Buffer->Memory) free(Buffer->Memory); S32 BytesPerPixel = 4; Buffer->Texture = SDL_CreateTexture(Renderer, SDL_PIXELFORMAT_ARGB8888, SDL_TEXTUREACCESS_STREAMING, Width, Height); Buffer->Memory = malloc(Width * Height * BytesPerPixel); Buffer->Height = Height; Buffer->Width = Width; Buffer->Pitch = Width * BytesPerPixel; } internal void SDLDisplayBufferInWindow(offscreen_buffer Buffer, SDL_Window *Window, SDL_Renderer *Renderer) { if(SDL_UpdateTexture(Buffer.Texture, 0, Buffer.Memory, Buffer.Pitch)) { // TODO: Do something about this error! } SDL_RenderCopy(Renderer, Buffer.Texture, 0, 0); SDL_RenderPresent(Renderer); } internal void SDLInitControllers() { int MaxJoysticks = SDL_NumJoysticks(); int ControllerIndex = 0; for(int JoystickIndex = 0; JoystickIndex < MaxJoysticks; JoystickIndex++) { if(!SDL_IsGameController(JoystickIndex)) continue; if(ControllerIndex >= MAX_CONTROLLERS) break; ControllerHandles[ControllerIndex] = SDL_GameControllerOpen(JoystickIndex); ControllerIndex++; } } /* internal void SDLDeinitControllers() { for(int ControllerIndex = 0; ControllerIndex < MAX_CONTROLLERS; ControllerIndex++) { if (ControllerHandles[ControllerIndex]) SDL_GameControllerClose(ControllerHandles[ControllerIndex]); } } */ internal void SDLInitSound(S32 SamplesPerSecond, S32 BufferSize) { SDL_AudioSpec AudioSettings = {0}; AudioSettings.freq = SamplesPerSecond; AudioSettings.format = AUDIO_S16LSB; AudioSettings.channels = 2; AudioSettings.samples = BufferSize; SDL_OpenAudio(&AudioSettings, 0); if(AudioSettings.format != AUDIO_S16LSB) { SDL_CloseAudio(); } } internal void SDLFillSoundBuffer(sdl_sound_output *SoundOutput, S32 BytesToWrite) { SDL_QueueAudio(1, SoundOutput->Samples, BytesToWrite); } internal void SDLClearSoundBuffer(sdl_sound_output *SoundOutput) { memset(SoundOutput->Samples, 0, SoundOutput->SecondaryBufferSize); } internal void SDLProcessKeyboardMessage(game_button_state *NewState, bool IsDown) { Assert(NewState->EndedDown != IsDown); NewState->EndedDown = IsDown; ++NewState->HalfTransitionCount; } internal void SDLProcessInputDigitalButton(SDL_GameController *Controller, game_button_state *OldState, SDL_GameControllerButton Button, game_button_state *NewState) { NewState->EndedDown = SDL_GameControllerGetButton(Controller, Button); NewState->HalfTransitionCount = (OldState->EndedDown != NewState->EndedDown) ? 1 : 0; } internal void HandleEvent(SDL_Event *Event) { switch(Event->type) { case SDL_QUIT: { GlobalRunning = false; } break; case SDL_WINDOWEVENT: { switch(Event->window.event) { case SDL_WINDOWEVENT_SIZE_CHANGED: { SDL_Window *Window = SDL_GetWindowFromID(Event->window.windowID); SDL_Renderer *Renderer = SDL_GetRenderer(Window); sdl_window_dimension Dimension = SDLGetWindowDimension(Window); SDLResizeTexture(&GlobalBackbuffer, Renderer, Dimension.Width, Dimension.Height); } break; case SDL_WINDOWEVENT_EXPOSED: { } break; } } break; } } internal void SDLProcessMessages(game_controller_input *KeyboardController) { SDL_Event Event; while(SDL_PollEvent(&Event)) { switch(Event.type) { case SDL_KEYDOWN: case SDL_KEYUP: { SDL_Keycode KeyCode = Event.key.keysym.sym; bool IsDown = (Event.key.state == SDL_PRESSED); bool WasDown = false; if(Event.key.state == SDL_RELEASED) { WasDown = true; } else if(Event.key.repeat != 0) { WasDown = true; } if(Event.key.repeat == 0) { if(KeyCode == SDLK_w) { SDLProcessKeyboardMessage(&KeyboardController->MoveUp, IsDown); } else if(KeyCode == SDLK_s) { SDLProcessKeyboardMessage(&KeyboardController->MoveDown, IsDown); } else if(KeyCode == SDLK_a) { SDLProcessKeyboardMessage(&KeyboardController->MoveLeft, IsDown); } else if(KeyCode == SDLK_d) { SDLProcessKeyboardMessage(&KeyboardController->MoveRight, IsDown); } else if(KeyCode == SDLK_q) { SDLProcessKeyboardMessage(&KeyboardController->LeftShoulder, IsDown); } else if(KeyCode == SDLK_e) { SDLProcessKeyboardMessage(&KeyboardController->RightShoulder, IsDown); } else if(KeyCode == SDLK_UP) { SDLProcessKeyboardMessage(&KeyboardController->ActionUp, IsDown); } else if(KeyCode == SDLK_DOWN) { SDLProcessKeyboardMessage(&KeyboardController->ActionDown, IsDown); } else if(KeyCode == SDLK_LEFT) { SDLProcessKeyboardMessage(&KeyboardController->ActionLeft, IsDown); } else if(KeyCode == SDLK_RIGHT) { SDLProcessKeyboardMessage(&KeyboardController->ActionRight, IsDown); } else if(KeyCode == SDLK_ESCAPE) { SDLProcessKeyboardMessage(&KeyboardController->Start, IsDown); } else if(KeyCode == SDLK_SPACE) { SDLProcessKeyboardMessage(&KeyboardController->Back, IsDown); } } // NOTE: If your window manager already uses an Alt+F4 keybind to close programs, then we are likely to see SDL_QUIT event occur before our keybind. bool AltKeyWasDown = (Event.key.keysym.mod & KMOD_ALT); if(KeyCode == SDLK_F4 && AltKeyWasDown) GlobalRunning = false; } break; default: { HandleEvent(&Event); } break; } } } internal F32 SDLProcessInputStickValue(F32 Value, S32 DeadZoneThreshold) { F32 Result = 0.0f; if(Value < -DeadZoneThreshold) Result = (F32)((Value + DeadZoneThreshold) / (32768.0f - DeadZoneThreshold)); else if(Value > DeadZoneThreshold) Result = (F32)((Value + DeadZoneThreshold) / (32767.0f - DeadZoneThreshold)); return Result; } #define DEFAULT_REFRESH_RATE 60 internal S32 SDLGetWindowRefreshRate(SDL_Window *Window) { int DisplayIndex = SDL_GetWindowDisplayIndex(Window); SDL_DisplayMode Mode; if(SDL_GetDesktopDisplayMode(DisplayIndex, &Mode) != 0) return DEFAULT_REFRESH_RATE; if(Mode.refresh_rate == 0) return DEFAULT_REFRESH_RATE; return Mode.refresh_rate; } internal U64 SDLGetWallClock() { return SDL_GetPerformanceCounter(); } internal F32 SDLGetSecondsElapsed(U64 Start, U64 End) { return (F32)(End - Start) / (F32)GlobalPerfCountFrequency; } S32 main(int argc, char *argv[]) { if(SDL_Init(SDL_INIT_VIDEO|SDL_INIT_GAMECONTROLLER|SDL_INIT_AUDIO)) { /* TODO: This didn't work . . . */ } SDLInitControllers(); SDL_Window *Window = SDL_CreateWindow("Handmade Hero", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, 700, 500, SDL_WINDOW_RESIZABLE); if(Window) { //SDL_Renderer *Renderer = SDL_CreateRenderer(Window, -1, 0); SDL_Renderer *Renderer = SDL_CreateRenderer(Window, -1, SDL_RENDERER_PRESENTVSYNC); // "Wayland needs an event loop and rendering or it won't function." https://github.com/libsdl-org/SDL/issues/7699#issuecomment-1545684792 // NOTE: This means that we need to readraw to even resize the window, otherwise the window frame will appear outdated to its actual size. // SDL_RenderPresent(Renderer) is enough to display the window. if(Renderer) { GlobalRunning = true; sdl_window_dimension Dimension = SDLGetWindowDimension(Window); SDLResizeTexture(&GlobalBackbuffer, Renderer, Dimension.Width, Dimension.Height); sdl_sound_output SoundOutput = {0}; SoundOutput.SamplesPerSecond = 48000; SoundOutput.BytesPerSample = sizeof(S16) * 2; SoundOutput.SecondaryBufferSize = SoundOutput.SamplesPerSecond * SoundOutput.BytesPerSample; SoundOutput.tSine = 0; SoundOutput.LatencySampleCount = SoundOutput.SamplesPerSecond / 15; SDLInitSound(SoundOutput.SamplesPerSecond, SoundOutput.SamplesPerSecond * SoundOutput.BytesPerSample / 60); SDL_PauseAudio(0); SoundOutput.Samples = calloc(SoundOutput.SamplesPerSecond, SoundOutput.BytesPerSample); // SoundOutput.Samples = malloc(SoundOutput.SecondaryBufferSize); // SDLClearSoundBuffer(&SoundOutput); // NOTE: calloc auto clears to zero #if BUILD_DEBUG void *BaseAddress = (void *)TB(2); #else void *BaseAddress = (void *)(0); #endif game_memory GameMemory = {0}; GameMemory.PermanentStorageSize = MB(64); GameMemory.TransientStorageSize = GB(4); U64 TotalStorageSize = GameMemory.PermanentStorageSize + GameMemory.TransientStorageSize; // NOTE: On MacOS and Linux, mmap seems to zero-fill anonymous // memory as a side-effect of security. GameMemory.PermanentStorage = mmap(BaseAddress, TotalStorageSize, PROT_READ | PROT_WRITE, MAP_ANON | MAP_PRIVATE, -1, 0); GameMemory.TransientStorage = (U8 *)(GameMemory.PermanentStorage) + GameMemory.PermanentStorageSize; if(SoundOutput.Samples && GameMemory.PermanentStorage && GameMemory.TransientStorage) { game_input Input[2]; game_input *NewInput = &Input[0]; game_input *OldInput = &Input[1]; memset(&Input, 0, sizeof(Input)); GlobalPerfCountFrequency = SDL_GetPerformanceFrequency(); U64 LastCounter; #if __x86_64__ || __i386__ U64 LastCycleCount; #endif while(GlobalRunning) { LastCounter = SDLGetWallClock(); #if __x86_64__ || __i386__ LastCycleCount = __rdtsc(); // NOTE: rdtsc reports clock cylces, however it is not meant for really precise profiler work as the value returned is varied. #endif // Also, __rdtsc is not available on MacOS ARM; I have not checked MacOS x64. S32 MonitorRefreshHz = SDLGetWindowRefreshRate(Window); S32 GameUpdateHz = MonitorRefreshHz / 2; F32 TargetSecondsPerFrame = 1.0f / (F32)GameUpdateHz; game_controller_input *OldKeyboardController = GetController(OldInput, 0); game_controller_input *NewKeyboardController = GetController(NewInput, 0); game_controller_input ZeroController = {0}; *NewKeyboardController = ZeroController; NewKeyboardController->IsConnected = true; for(U32 ButtonIndex = 0; ButtonIndex < ArrayCount(NewKeyboardController->Buttons); ButtonIndex++) { NewKeyboardController->Buttons[ButtonIndex].EndedDown = OldKeyboardController->Buttons[ButtonIndex].EndedDown; } SDLProcessMessages(NewKeyboardController); for(S32 ControllerIndex = 0; ControllerIndex < MAX_CONTROLLERS; ControllerIndex++) { game_controller_input *OldController = GetController(OldInput, ControllerIndex+1); game_controller_input *NewController = GetController(NewInput, ControllerIndex+1); if(ControllerHandles[ControllerIndex] != 0 && SDL_GameControllerGetAttached(ControllerHandles[ControllerIndex])) { NewController->IsConnected = true; S16 StickX = SDL_GameControllerGetAxis(ControllerHandles[ControllerIndex], SDL_CONTROLLER_AXIS_LEFTX); S16 StickY = SDL_GameControllerGetAxis(ControllerHandles[ControllerIndex], SDL_CONTROLLER_AXIS_LEFTY); NewController->IsAnalog = true; NewController->StickAverageX = SDLProcessInputStickValue((F32)StickX, CONTROLLER_LEFT_THUMB_DEADZONE); NewController->StickAverageY = SDLProcessInputStickValue((F32)StickY, CONTROLLER_LEFT_THUMB_DEADZONE); if((NewController->StickAverageX != 0.0f) || (NewController->StickAverageY != 0.0f)) { NewController->IsAnalog = true; } if(SDL_GameControllerGetButton(ControllerHandles[ControllerIndex], SDL_CONTROLLER_BUTTON_DPAD_UP)) { NewController->StickAverageY = -1.0f; NewController->IsAnalog = false; } if(SDL_GameControllerGetButton(ControllerHandles[ControllerIndex], SDL_CONTROLLER_BUTTON_DPAD_DOWN)) { NewController->StickAverageY = 1.0f; NewController->IsAnalog = false; } if(SDL_GameControllerGetButton(ControllerHandles[ControllerIndex], SDL_CONTROLLER_BUTTON_DPAD_LEFT)) { NewController->StickAverageX = -1.0f; NewController->IsAnalog = false; } if(SDL_GameControllerGetButton(ControllerHandles[ControllerIndex], SDL_CONTROLLER_BUTTON_DPAD_RIGHT)) { NewController->StickAverageX = 1.0f; NewController->IsAnalog = false; } F32 Threshold = 0.5f; SDLProcessInputDigitalButton(ControllerHandles[(NewController->StickAverageY < -Threshold) ? 1 : 0], &OldController->MoveUp, SDL_CONTROLLER_BUTTON_A, &NewController->MoveUp); SDLProcessInputDigitalButton(ControllerHandles[(NewController->StickAverageY > Threshold) ? 1 : 0], &OldController->MoveDown, SDL_CONTROLLER_BUTTON_A, &NewController->MoveDown); SDLProcessInputDigitalButton(ControllerHandles[(NewController->StickAverageX < -Threshold) ? 1 : 0], &OldController->MoveLeft, SDL_CONTROLLER_BUTTON_A, &NewController->MoveLeft); SDLProcessInputDigitalButton(ControllerHandles[(NewController->StickAverageX > Threshold) ? 1 : 0], &OldController->MoveRight, SDL_CONTROLLER_BUTTON_A, &NewController->MoveRight); SDLProcessInputDigitalButton(ControllerHandles[ControllerIndex], &OldController->ActionDown, SDL_CONTROLLER_BUTTON_A, &NewController->ActionDown); SDLProcessInputDigitalButton(ControllerHandles[ControllerIndex], &OldController->ActionRight, SDL_CONTROLLER_BUTTON_B, &NewController->ActionRight); SDLProcessInputDigitalButton(ControllerHandles[ControllerIndex], &OldController->ActionLeft, SDL_CONTROLLER_BUTTON_X, &NewController->ActionLeft); SDLProcessInputDigitalButton(ControllerHandles[ControllerIndex], &OldController->ActionUp, SDL_CONTROLLER_BUTTON_Y, &NewController->ActionUp); SDLProcessInputDigitalButton(ControllerHandles[ControllerIndex], &OldController->LeftShoulder, SDL_CONTROLLER_BUTTON_LEFTSHOULDER, &NewController->LeftShoulder); SDLProcessInputDigitalButton(ControllerHandles[ControllerIndex], &OldController->RightShoulder, SDL_CONTROLLER_BUTTON_RIGHTSHOULDER, &NewController->RightShoulder); SDLProcessInputDigitalButton(ControllerHandles[ControllerIndex], &OldController->Start, SDL_CONTROLLER_BUTTON_START, &NewController->Start); SDLProcessInputDigitalButton(ControllerHandles[ControllerIndex], &OldController->Back, SDL_CONTROLLER_BUTTON_BACK, &NewController->Back); } else { // NOTE: This controller is note plugged in. NewController->IsConnected = false; } } S32 TargetQueueBytes = SoundOutput.LatencySampleCount * SoundOutput.BytesPerSample; S32 BytesToWrite = TargetQueueBytes - SDL_GetQueuedAudioSize(1); game_sound_output_buffer SoundBuffer; SoundBuffer.SamplesPerSecond = SoundOutput.SamplesPerSecond; SoundBuffer.SampleCount = BytesToWrite / SoundOutput.BytesPerSample; SoundBuffer.Samples = (S16 *)SoundOutput.Samples; game_offscreen_buffer Buffer; Buffer.Memory = GlobalBackbuffer.Memory; Buffer.Width = GlobalBackbuffer.Width; Buffer.Height = GlobalBackbuffer.Height; Buffer.Pitch = GlobalBackbuffer.Pitch; GameUpdateAndRender(&GameMemory, NewInput, &Buffer, &SoundBuffer); SDLFillSoundBuffer(&SoundOutput, BytesToWrite); U64 WorkCounter = SDLGetWallClock(); F32 WorkSecondsElapsed = SDLGetSecondsElapsed(LastCounter, WorkCounter); F32 SecondsElapsedForFrame = WorkSecondsElapsed; if(SecondsElapsedForFrame < TargetSecondsPerFrame) { while(SecondsElapsedForFrame < TargetSecondsPerFrame) { SecondsElapsedForFrame = SDLGetSecondsElapsed(LastCounter, SDLGetWallClock()); SDL_Delay((U32)((TargetSecondsPerFrame - SecondsElapsedForFrame) * 1000.0f)); } } else { // TODO: Missed frame rate! // TODO: Logging. } U64 EndCounter = SDLGetWallClock(); SDLDisplayBufferInWindow(GlobalBackbuffer, Window, Renderer); F64 MSPerFrame = 1000.0f * SDLGetSecondsElapsed(LastCounter, EndCounter); F64 FPS = 0.0f; LastCounter = EndCounter; #if __x86_64__ || __i386__ U64 EndCycleCount = __rdtsc(); U64 CyclesElapsed = EndCycleCount - LastCycleCount; F64 MCPF = ((F64)CyclesElapsed / (1000.0f * 1000.0f)); fprintf(stderr, "%.02f ms/f, %.02f/s, %.02f mc/f\n", MSPerFrame, FPS, MCPF); LastCycleCount = EndCycleCount; #else fprintf(stderr, "%.02f ms/f, %.02f/s\n", MSPerFrame, FPS); #endif Swap(game_input *, NewInput, OldInput); } } else { // TODO: Failed to allocate Samples and Sound memory . . . } } else { // TODO: This didn't work . . . } } else { // TODO: This didn't work . . . } // NOTE: Let the OS clean things up. // SDLDeinitControllers(); // NOTE: This, however, might need to be closed? Is there maybe some clean up state that close communicates to the controller? // SDL_CloseAudio(); // SDL_DestroyRenderer(Renderer); // SDL_DestroyWindow(Window); // SDL_Quit(); return 0; }