diff options
author | Raymaekers Luca <luca@spacehb.net> | 2025-04-27 11:40:31 +0200 |
---|---|---|
committer | Raymaekers Luca <luca@spacehb.net> | 2025-04-27 11:43:30 +0200 |
commit | 7427ae1688e5933625a3609bcfba7e32988971fb (patch) | |
tree | 7476e6ebf0e317e00aec4ea63f5ea981a023b931 |
Initial commitmain
-rw-r--r-- | LICENSE | 7 | ||||
-rw-r--r-- | README.md | 18 | ||||
-rw-r--r-- | linux_handmade.cpp | 985 | ||||
-rw-r--r-- | linux_handmade.h | 71 |
4 files changed, 1081 insertions, 0 deletions
@@ -0,0 +1,7 @@ +Copyright (c) 2025 Luca Raymaekers + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
\ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..1145d0b --- /dev/null +++ b/README.md @@ -0,0 +1,18 @@ +# Poulbi's Handmade Hero Linux port + +## Overview +To make sure I internalize the Handmade Hero series, I am porting the game over to Linux. + +## Build +Run the build script. +```sh +./code/build.sh +``` + +## Running +```sh +./build/linux_handmade +``` + +# Resources +- [Episode Guide](https://guide.handmadehero.org/) diff --git a/linux_handmade.cpp b/linux_handmade.cpp new file mode 100644 index 0000000..1074685 --- /dev/null +++ b/linux_handmade.cpp @@ -0,0 +1,985 @@ +#include <stdio.h> +#include <time.h> +#include <x86intrin.h> +#include <string.h> +#include <sys/mman.h> +#include <sys/stat.h> +#include <dlfcn.h> +#include <fcntl.h> +#include <unistd.h> +#include <dirent.h> + +#include <linux/limits.h> +#include <linux/input.h> +#include <alsa/asoundlib.h> +#include <X11/Xlib.h> +#include <X11/Xutil.h> +#include <X11/keysymdef.h> +#include <X11/extensions/Xrandr.h> + +#include "handmade.h" +#include "linux_handmade.h" + +#define true 1 +#define false 0 + +#define MAX_PLAYER_COUNT 4 + +// NOTE(luca): Bits are layed out over multiple bytes. This macro checks which byte the bit will be set in. +#define IsEvdevBitSet(Bit, Array) (Array[(Bit) / 8] & (1 << ((Bit) % 8))) +#define BytesNeededForBits(Bits) ((Bits + 7) / 8) + +global_variable b32 GlobalRunning; +global_variable b32 GlobalPaused; + +void Memcpy(char *Dest, char *Source, size_t Count) +{ + while(Count--) *Dest++ = *Source++; +} + +int StrLen(char *String) +{ + size_t Result = 0; + + while(*String++) Result++; + + return Result; +} + +void CatStrings(size_t SourceACount, char *SourceA, + size_t SourceBCount, char *SourceB, + size_t DestCount, char *Dest) +{ + for(size_t Index = 0; + Index < SourceACount; + Index++) + { + *Dest++ = *SourceA++; + } + + for(size_t Index = 0; + Index < SourceBCount; + Index++) + { + *Dest++ = *SourceB++; + } +} + +struct linux_init_alsa_result +{ + snd_pcm_t *PCMHandle; + snd_pcm_hw_params_t *PCMParams; +}; + +internal linux_init_alsa_result LinuxInitALSA() +{ + linux_init_alsa_result Result = {}; + + int PCMResult = 0; + if((PCMResult = snd_pcm_open(&Result.PCMHandle, "default", + SND_PCM_STREAM_PLAYBACK, 0)) == 0) + { + snd_pcm_hw_params_alloca(&Result.PCMParams); + snd_pcm_hw_params_any(Result.PCMHandle, Result.PCMParams); + u32 ChannelCount = 2; + u32 SampleRate = 48000; + + if((PCMResult = snd_pcm_hw_params_set_access(Result.PCMHandle, Result.PCMParams, SND_PCM_ACCESS_RW_INTERLEAVED)) < 0) + { + // TODO(luca): Logging + // snd_strerror(pcm) + } + if((PCMResult = snd_pcm_hw_params_set_format(Result.PCMHandle, Result.PCMParams, SND_PCM_FORMAT_S16_LE)) < 0) + { + // TODO(luca): Logging + } + if((PCMResult = snd_pcm_hw_params_set_channels(Result.PCMHandle, Result.PCMParams, ChannelCount)) < 0) + { + // TODO(luca): Logging + } + if((PCMResult = snd_pcm_hw_params_set_rate_near(Result.PCMHandle, Result.PCMParams, &SampleRate, 0)) < 0) + { + // TODO(luca): Logging + } + if((PCMResult = snd_pcm_nonblock(Result.PCMHandle, 1)) < 0) + { + // TODO(luca): Logging + } + if((PCMResult = snd_pcm_reset(Result.PCMHandle)) < 0) + { + // TODO(luca): Logging + } + if((PCMResult = snd_pcm_hw_params(Result.PCMHandle, Result.PCMParams)) < 0) + { + // TODO(luca): Logging + } + + } + else + { + // TODO(luca): Logging + } + + return Result; +} + +internal void LinuxGetAxisInfo(linux_gamepad *GamePad, linux_gamepad_axes_enum Axis, int AbsAxis) +{ + input_absinfo AxesInfo = {}; + if(ioctl(GamePad->FileFD, EVIOCGABS(AbsAxis), &AxesInfo) != -1) + { + GamePad->Axes[Axis].Minimum = AxesInfo.minimum; + GamePad->Axes[Axis].Maximum = AxesInfo.maximum; + GamePad->Axes[Axis].Fuzz = AxesInfo.fuzz; + GamePad->Axes[Axis].Flat = AxesInfo.flat; + } +} + +internal void LinuxOpenGamePad(char *FilePath, linux_gamepad *GamePad) +{ + GamePad->FileFD = open(FilePath, O_RDWR|O_NONBLOCK); + + if(GamePad->FileFD != -1) + { + int Version = 0; + int IsCompatible = true; + + // TODO(luca): Check versions + ioctl(GamePad->FileFD, EVIOCGVERSION, &Version); + ioctl(GamePad->FileFD, EVIOCGNAME(sizeof(GamePad->Name)), GamePad->Name); + + char SupportedEventBits[BytesNeededForBits(EV_MAX)] = {}; + if(ioctl(GamePad->FileFD, EVIOCGBIT(0, sizeof(SupportedEventBits)), SupportedEventBits) != -1) + { + if(!IsEvdevBitSet(EV_ABS, SupportedEventBits)) + { + // TODO(luca): Logging + IsCompatible = false; + } + if(!IsEvdevBitSet(EV_KEY, SupportedEventBits)) + { + // TODO(luca): Logging + IsCompatible = false; + } + } + + char SupportedKeyBits[BytesNeededForBits(KEY_MAX)] = {}; + if(ioctl(GamePad->FileFD, EVIOCGBIT(EV_KEY , sizeof(SupportedKeyBits)), SupportedKeyBits) != -1) + { + if(!IsEvdevBitSet(BTN_GAMEPAD, SupportedKeyBits)) + { + // TODO(luca): Logging + IsCompatible = false; + } + } + + GamePad->SupportsRumble = IsEvdevBitSet(EV_FF, SupportedEventBits); + + if(IsCompatible) + { + // NOTE(luca): Map evdev axes to my enum. + LinuxGetAxisInfo(GamePad, LSTICKX, ABS_X); + LinuxGetAxisInfo(GamePad, LSTICKY, ABS_Y); + LinuxGetAxisInfo(GamePad, RSTICKX, ABS_RX); + LinuxGetAxisInfo(GamePad, RSTICKY, ABS_RY); + LinuxGetAxisInfo(GamePad, LSHOULDER, ABS_Z); + LinuxGetAxisInfo(GamePad, RSHOULDER, ABS_RZ); + LinuxGetAxisInfo(GamePad, DPADX, ABS_HAT0X); + LinuxGetAxisInfo(GamePad, DPADY, ABS_HAT0Y); + + Memcpy(GamePad->FilePath, FilePath, StrLen(FilePath)); + } + else + { + close(GamePad->FileFD); + *GamePad = {}; + GamePad->FileFD = -1; + } + } +} + +// TODO(luca): Make this work in the case of multiple displays. +internal r32 LinuxGetMonitorRefreshRate(Display *DisplayHandle, Window RootWindow) +{ + r32 Result = 0; + + void *LibraryHandle = dlopen("libXrandr.so", RTLD_NOW); + + if(LibraryHandle) + { + typedef XRRScreenResources *xrr_get_screen_resources(Display *Display, Window Window); + typedef XRRCrtcInfo *xrr_get_crtc_info(Display* Display, XRRScreenResources *Resources, RRCrtc Crtc); + + xrr_get_screen_resources *XRRGetScreenResources = (xrr_get_screen_resources *)dlsym(LibraryHandle, "XRRGetScreenResources"); + xrr_get_crtc_info *XRRGetCrtcInfo = (xrr_get_crtc_info *)dlsym(LibraryHandle, "XRRGetCrtcInfo"); + + XRRScreenResources *ScreenResources = XRRGetScreenResources(DisplayHandle, RootWindow); + + RRMode ActiveModeID = 0; + for(int CRTCIndex = 0; + CRTCIndex< ScreenResources->ncrtc; + CRTCIndex++) + { + XRRCrtcInfo *CRTCInfo = XRRGetCrtcInfo(DisplayHandle, ScreenResources, ScreenResources->crtcs[CRTCIndex]); + if(CRTCInfo->mode) + { + ActiveModeID = CRTCInfo->mode; + } + } + + r32 ActiveRate = 0; + for(int ModeIndex = 0; + ModeIndex < ScreenResources->nmode; + ModeIndex++) + { + XRRModeInfo ModeInfo = ScreenResources->modes[ModeIndex]; + if(ModeInfo.id == ActiveModeID) + { + Assert(ActiveRate == 0); + Result = (r32)ModeInfo.dotClock / ((r32)ModeInfo.hTotal * (r32)ModeInfo.vTotal); + } + } + + dlclose(LibraryHandle); + } + + return Result; +} + +internal void LinuxGetLibraryPath(char *Dest, char *ExecutablePath, char *LibraryFileName) +{ + size_t LastSlash = 0; + for(char *Scan = ExecutablePath; + *Scan; + Scan++) + { + if(*Scan == '/') + { + LastSlash = Scan - ExecutablePath; + } + } + + for(size_t Index = 0; + Index < LastSlash + 1; + Index++) + { + *Dest++ = *ExecutablePath++; + } + + while(*LibraryFileName) *Dest++ = *LibraryFileName++; +} + +GAME_UPDATE_AND_RENDER(LinuxGameUpdateAndRenderStub) {} +GAME_GET_SOUND_SAMPLES(LinuxGameGetSoundSamplesStub) {} + +internal linux_game_code LinuxLoadGameCode(char *LibraryPath) +{ + linux_game_code Result = {}; + + Result.LibraryHandle = dlopen(LibraryPath, RTLD_NOW); + if(Result.LibraryHandle) + { + Result.UpdateAndRender = (game_update_and_render *)dlsym(Result.LibraryHandle, "GameUpdateAndRender"); + Result.GetSoundSamples = (game_get_sound_samples *)dlsym(Result.LibraryHandle, "GameGetSoundSamples"); + } + else + { + Result.UpdateAndRender = (game_update_and_render *)LinuxGameUpdateAndRenderStub; + Result.GetSoundSamples = (game_get_sound_samples *)LinuxGameGetSoundSamplesStub; + } + + return Result; +} + +internal void LinuxUnloadGameCode(linux_game_code *Game) +{ + if(Game->LibraryHandle) + { + dlclose(Game->LibraryHandle); + } +} + +internal void LinuxProcessKeyPress(game_button_state *ButtonState, b32 IsDown) +{ + if(ButtonState->EndedDown != IsDown) + { + ButtonState->EndedDown = IsDown; + ButtonState->HalfTransitionCount++; + } +} + +internal void LinuxProcessPendingMessages(Display *DisplayHandle, Window WindowHandle, + Atom WM_DELETE_WINDOW, game_controller_input *KeyboardController) +{ + XEvent WindowEvent = {}; + while(XPending(DisplayHandle) > 0) + { + XNextEvent(DisplayHandle, &WindowEvent); + switch (WindowEvent.type) + { + case KeyPress: + case KeyRelease: + { + KeySym keysym = XLookupKeysym(&WindowEvent.xkey, 0); + + b32 IsDown = (WindowEvent.type == KeyPress); + + if(0) {} + else if(keysym == XK_w) + { + LinuxProcessKeyPress(&KeyboardController->MoveUp, IsDown); + } + else if(keysym == XK_a) + { + LinuxProcessKeyPress(&KeyboardController->MoveLeft, IsDown); + } + else if(keysym == XK_r) + { + LinuxProcessKeyPress(&KeyboardController->MoveDown, IsDown); + } + else if(keysym == XK_s) + { + LinuxProcessKeyPress(&KeyboardController->MoveRight, IsDown); + } + else if(keysym == XK_Up) + { + LinuxProcessKeyPress(&KeyboardController->ActionUp, IsDown); + } + else if(keysym == XK_Left) + { + LinuxProcessKeyPress(&KeyboardController->ActionLeft, IsDown); + } + else if(keysym == XK_Down) + { + LinuxProcessKeyPress(&KeyboardController->ActionDown, IsDown); + } + else if(keysym == XK_Right) + { + LinuxProcessKeyPress(&KeyboardController->ActionRight, IsDown); + } + else if(keysym == XK_p) + { + if(IsDown) + { + GlobalPaused = !GlobalPaused; + } + } + else if(keysym == XK_Escape || + keysym == XK_q) + { + GlobalRunning = false; + } + + } break; + case DestroyNotify: + { + XDestroyWindowEvent *Event = (XDestroyWindowEvent *)&WindowEvent; + if(Event->window == WindowHandle) + { + GlobalRunning = false; + } + } break; + + case ClientMessage: + { + XClientMessageEvent *Event = (XClientMessageEvent *)&WindowEvent; + if((Atom)Event->data.l[0] == WM_DELETE_WINDOW) + { + XDestroyWindow(DisplayHandle, WindowHandle); + GlobalRunning = false; + } + } break; + } + } + +} + +internal void LinuxSetSizeHint(Display *DisplayHandle, Window WindowHandle, + int MinWidth, int MinHeight, + int MaxWidth, int MaxHeight) +{ + XSizeHints Hints = {}; + if(MinWidth > 0 && MinHeight > 0) Hints.flags |= PMinSize; + if(MaxWidth > 0 && MaxHeight > 0) Hints.flags |= PMaxSize; + + Hints.min_width = MinWidth; + Hints.min_height = MinHeight; + Hints.max_width = MaxWidth; + Hints.max_height = MaxHeight; + + XSetWMNormalHints(DisplayHandle, WindowHandle, &Hints); +} + +DEBUG_PLATFORM_READ_ENTIRE_FILE(DEBUGPlatformReadEntireFile) +{ + debug_read_file_result Result = {}; + + int FD = open(FileName, O_RDONLY); + if(FD != -1) + { + struct stat FileStats = {}; + fstat(FD, &FileStats); + Result.ContentsSize = FileStats.st_size; + Result.Contents = mmap(0, FileStats.st_size, PROT_READ, MAP_PRIVATE, FD, 0); + + close(FD); + } + + return Result; +} + +DEBUG_PLATFORM_FREE_FILE_MEMORY(DEBUGPlatformFreeFileMemory) +{ + munmap(Memory, MemorySize); +} + +DEBUG_PLATFORM_WRITE_ENTIRE_FILE(DEBUGPlatformWriteEntireFile) +{ + b32 Result = false; + + int FD = open(FileName, O_CREAT|O_WRONLY|O_TRUNC, 00600); + if(FD != -1) + { + if(write(FD, Memory, MemorySize) != MemorySize) + { + Result = true; + } + + close(FD); + } + + return Result; +} + +internal struct timespec LinuxGetLastWriteTime(char *FilePath) +{ + struct timespec Result = {}; + + struct stat LibraryFileStats = {}; + if(!stat(FilePath, &LibraryFileStats)) + { + Result = LibraryFileStats.st_mtim; + } + + return Result; +} + +internal struct timespec LinuxGetWallClock() +{ + struct timespec Counter = {}; + clock_gettime(CLOCK_MONOTONIC, &Counter); + return Counter; +} + +internal s64 LinuxGetSecondsElapsed(struct timespec Start, struct timespec End) +{ + s64 Result = 0; + Result = ((s64)End.tv_sec*1000000000 + (s64)End.tv_nsec) - ((s64)Start.tv_sec*1000000000 + (s64)Start.tv_nsec); + return Result; +} + +internal r32 LinuxNormalizeAxisValue(s32 Value, linux_gamepad_axis Axis) +{ + r32 Result = 0; + if(Value) + { + // ((value - min / max) - 0.5) * 2 + r32 Normalized = ((r32)((r32)(Value - Axis.Minimum) / (r32)(Axis.Maximum - Axis.Minimum)) - 0.5f)*2; + Result = Normalized; + } + Assert(Result <= 1.0f && Result >= -1.0f); + + return Result; +} + +void LinuxDebugVerticalLine(game_offscreen_buffer *Buffer, int X, int Y, u32 Color) +{ + int Height = 32; + + if(X <= Buffer->Width && X >= 0 && + Y <= Buffer->Height - Height && Y <= 0) + { + u8 *Row = (u8 *)Buffer->Memory + Y*Buffer->Pitch + X*Buffer->BytesPerPixel; + while(Height--) + { + *(u32 *)Row = Color; + Row += Buffer->Pitch; + } + } +} + +int main(int ArgC, char *Args[]) +{ + Display *DisplayHandle = XOpenDisplay(0); + + if(DisplayHandle) + { + Window RootWindow = XDefaultRootWindow(DisplayHandle); + int Screen = XDefaultScreen(DisplayHandle); + int Width = 800; + int Height = 600; + int ScreenBitDepth = 24; + XVisualInfo WindowVisualInfo = {}; + if(XMatchVisualInfo(DisplayHandle, Screen, ScreenBitDepth, TrueColor, &WindowVisualInfo)) + { + XSetWindowAttributes WindowAttributes = {}; + WindowAttributes.bit_gravity = StaticGravity; + WindowAttributes.background_pixel = 0; + WindowAttributes.colormap = XCreateColormap(DisplayHandle, RootWindow, WindowVisualInfo.visual, AllocNone); + WindowAttributes.event_mask = StructureNotifyMask | KeyPressMask | KeyReleaseMask; + u64 WindowAttributeMask = CWBitGravity | CWBackPixel | CWColormap | CWEventMask; + + Window WindowHandle = XCreateWindow(DisplayHandle, RootWindow, + 1920 - Width - 2, 1080 - Height - 2, + Width, Height, + 0, + WindowVisualInfo.depth, InputOutput, + WindowVisualInfo.visual, WindowAttributeMask, &WindowAttributes); + if(WindowHandle) + { + XStoreName(DisplayHandle, WindowHandle, "Handmade Window"); + LinuxSetSizeHint(DisplayHandle, WindowHandle, Width, Height, Width, Height); + + Atom WM_DELETE_WINDOW = XInternAtom(DisplayHandle, "WM_DELETE_WINDOW", False); + if(!XSetWMProtocols(DisplayHandle, WindowHandle, &WM_DELETE_WINDOW, 1)) + { + printf("Couldn't register WM_DELETE_WINDOW property\n"); + } + + XClassHint ClassHint = {}; + ClassHint.res_name = "Handmade Window"; + ClassHint.res_class = "Handmade Window"; + XSetClassHint(DisplayHandle, WindowHandle, &ClassHint); + + int BitsPerPixel = 32; + int BytesPerPixel = BitsPerPixel/8; + int WindowBufferSize = Width*Height*BytesPerPixel; + char *WindowBufferMemory = (char *)malloc(WindowBufferSize); + + XImage *WindowBuffer = XCreateImage(DisplayHandle, WindowVisualInfo.visual, WindowVisualInfo.depth, ZPixmap, 0, WindowBufferMemory, Width, Height, BitsPerPixel, 0); + GC DefaultGC = DefaultGC(DisplayHandle, Screen); + + char LibraryFullPath[PATH_MAX] = {}; + char LibraryName[] = "handmade.so"; + LinuxGetLibraryPath(LibraryFullPath, Args[0], LibraryName); + linux_game_code Game = LinuxLoadGameCode(LibraryFullPath); + Game.LibraryLastWriteTime = LinuxGetLastWriteTime(LibraryFullPath); + + game_memory GameMemory = {}; + GameMemory.PermanentStorageSize = Megabytes(64); + GameMemory.TransientStorageSize = Gigabytes(1); + GameMemory.DEBUGPlatformReadEntireFile = DEBUGPlatformReadEntireFile; + GameMemory.DEBUGPlatformFreeFileMemory = DEBUGPlatformFreeFileMemory; + GameMemory.DEBUGPlatformWriteEntireFile = DEBUGPlatformWriteEntireFile; + +#if HANDMADE_INTERNAL + void *BaseAddress = (void *)Terabytes(2); +#else + void *BaseAddress = 0; +#endif + + u64 TotalSize = GameMemory.PermanentStorageSize + GameMemory.TransientStorageSize; + GameMemory.PermanentStorage = (char *)mmap(BaseAddress, TotalSize, PROT_READ|PROT_WRITE, MAP_ANONYMOUS|MAP_SHARED, -1, 0); + GameMemory.TransientStorage = (u8 *)GameMemory.PermanentStorage + GameMemory.PermanentStorageSize; + + game_input Input[2] = {}; + game_input *NewInput = &Input[0]; + game_input *OldInput = &Input[1]; + + linux_gamepad GamePads[MAX_PLAYER_COUNT] = {}; + for(int GamePadIndex = 0; + GamePadIndex < MAX_PLAYER_COUNT; + GamePadIndex++) + { + GamePads[GamePadIndex].FileFD = -1; + } + + // TODO(luca): Run this once on startup to detect connected controllers and later with inotify when controllers are added. + char EventDirectoryName[] = "/dev/input/"; + DIR *EventDirectory = opendir(EventDirectoryName); + struct dirent *Entry = 0; + int GamePadIndex = 0; + while((Entry = readdir(EventDirectory))) + { + if(!strncmp(Entry->d_name, "event", sizeof("event") - 1)) + { + char FilePath[PATH_MAX] = {}; + CatStrings(sizeof(EventDirectoryName) - 1, EventDirectoryName, + StrLen(Entry->d_name), Entry->d_name, + sizeof(FilePath) - 1, FilePath); + if(GamePadIndex < MAX_PLAYER_COUNT) + { + linux_gamepad *GamePadAt = &GamePads[GamePadIndex]; + LinuxOpenGamePad(FilePath, GamePadAt); + if(GamePadAt->FileFD != -1) + { +#if 0 + for(int AxesIndex = 0; + AxesIndex < AXES_COUNT; + AxesIndex++) + { + linux_gamepad_axis Axis = GamePadAt->Axes[AxesIndex]; + printf("min: %d, max: %d, fuzz: %d, flat: %d\n", Axis.Minimum, Axis.Maximum, Axis.Fuzz, Axis.Flat); + } +#endif + GamePadIndex++; + } + } + else + { + // TODO(luca): Logging + } + } + } + + game_offscreen_buffer OffscreenBuffer = {}; + OffscreenBuffer.Memory = WindowBufferMemory; + OffscreenBuffer.Width = Width; + OffscreenBuffer.Height = Height; + OffscreenBuffer.BytesPerPixel = BytesPerPixel; + OffscreenBuffer.Pitch = Width*BytesPerPixel; + + int LastFramesWritten = 0; + unsigned int SampleRate, ChannelCount, TimePeriod, SampleCount; + snd_pcm_status_t *PCMStatus = 0; + snd_pcm_t* PCMHandle = 0; + snd_pcm_hw_params_t *PCMParams = 0; + snd_pcm_uframes_t PeriodSize = 0; + snd_pcm_uframes_t PCMBufferSize = 0; + + linux_init_alsa_result ALSAInit = LinuxInitALSA(); + PCMHandle = ALSAInit.PCMHandle; + PCMParams = ALSAInit.PCMParams; + snd_pcm_hw_params_get_channels(PCMParams, &ChannelCount); + snd_pcm_hw_params_get_rate(PCMParams, &SampleRate, 0); + snd_pcm_hw_params_get_period_size(PCMParams, &PeriodSize, 0); + snd_pcm_hw_params_get_period_time(PCMParams, &TimePeriod, NULL); + snd_pcm_hw_params_get_buffer_size(PCMParams, &PCMBufferSize); + snd_pcm_status_malloc(&PCMStatus); + + char AudioSamples[PCMBufferSize]; + u64 Periods = 2; + u32 BytesPerSample = (sizeof(s16)*ChannelCount); + +#if 1 + r32 GameUpdateHz = 30; +#else + r32 GameUpdateHz = LinuxGetMonitorRefreshRate(DisplayHandle, RootWindow); +#endif + + thread_context ThreadContext = {}; + + XMapWindow(DisplayHandle, WindowHandle); + XFlush(DisplayHandle); + + struct timespec LastCounter = LinuxGetWallClock(); + struct timespec FlipWallClock = LinuxGetWallClock(); + r32 TargetSecondsPerFrame = 1.0f / GameUpdateHz; + + GlobalRunning = true; + + u64 LastCycleCount = __rdtsc(); + while(GlobalRunning) + { + +#if HANDMADE_INTERNAL + // NOTE(luca): Because gcc will first create an empty file and then write into it we skip trying to reload when the file is empty. + struct stat FileStats = {}; + stat(LibraryFullPath, &FileStats); + if(FileStats.st_size) + { + s64 SecondsElapsed = LinuxGetSecondsElapsed(Game.LibraryLastWriteTime, FileStats.st_mtim) / 1000/1000; + if(SecondsElapsed > 0) + { + LinuxUnloadGameCode(&Game); + Game = LinuxLoadGameCode(LibraryFullPath); + Game.LibraryLastWriteTime = FileStats.st_mtim; + } + } +#endif + + game_controller_input *OldKeyboardController = GetController(OldInput, 0); + game_controller_input *NewKeyboardController = GetController(NewInput, 0); + NewKeyboardController->IsConnected = true; + + LinuxProcessPendingMessages(DisplayHandle, WindowHandle, WM_DELETE_WINDOW, NewKeyboardController); + + // TODO(luca): Use buttonpress/release events instead so we query this less frequently. + s32 MouseX = 0, MouseY = 0, MouseZ = 0; // TODO(luca): Support mousewheel? + u32 MouseMask = 0; + u64 Ignored; + if(XQueryPointer(DisplayHandle, WindowHandle, + &Ignored, &Ignored, (int *)&Ignored, (int *)&Ignored, + &MouseX, &MouseY, &MouseMask)) + { + if(MouseX <= OffscreenBuffer.Width && + MouseX >= 0 && + MouseY <= OffscreenBuffer.Height && + MouseY >= 0) + { + NewInput->MouseY = MouseY; + NewInput->MouseX = MouseX; + NewInput->MouseButtons[0].EndedDown = ((MouseMask & Button1Mask) > 0); + NewInput->MouseButtons[1].EndedDown = ((MouseMask & Button2Mask) > 0); + NewInput->MouseButtons[2].EndedDown = ((MouseMask & Button3Mask) > 0); + } + } + + for(int GamePadIndex = 0; + GamePadIndex < MAX_PLAYER_COUNT; + GamePadIndex++) + { + linux_gamepad *GamePadAt = &GamePads[GamePadIndex]; + + if(GamePadAt->FileFD != -1) + { + game_controller_input *OldController = GetController(OldInput, 0); + game_controller_input *NewController = GetController(NewInput, 0); + + // TODO(luca): What we really want is the last event. (or haltransitions) + struct input_event InputEvents[64] = {}; + int BytesRead = 0; + + BytesRead = read(GamePadAt->FileFD, InputEvents, sizeof(InputEvents)); + if(BytesRead != -1) + { + for(int EventIndex = 0; + EventIndex < (int)ArrayCount(InputEvents); + EventIndex++) + { + struct input_event EventAt = InputEvents[EventIndex]; + + switch(EventAt.type) + { + case EV_KEY: + { + b32 IsDown = EventAt.value; + if(0) {} + else if(EventAt.code == BTN_A) + { + NewController->ActionDown.EndedDown = IsDown; + } + else if(EventAt.code == BTN_B) + { + NewController->ActionRight.EndedDown = IsDown; + } + else if(EventAt.code == BTN_X) + { + NewController->ActionLeft.EndedDown = IsDown; + } + else if(EventAt.code == BTN_Y) + { + NewController->ActionUp.EndedDown = IsDown; + } + else if(EventAt.code == BTN_START) + { + NewController->Start.EndedDown = IsDown; + } + else if(EventAt.code == BTN_BACK) + { + NewController->Back.EndedDown = IsDown; + } + } break; + + case EV_ABS: + { + if(0) {} + else if(EventAt.code == ABS_X) + { + NewController->IsAnalog = true; + + NewController->StickAverageX = LinuxNormalizeAxisValue(EventAt.value, GamePadAt->Axes[LSTICKX]); + } + else if(EventAt.code == ABS_Y) + { + NewController->StickAverageY = -1.0f * LinuxNormalizeAxisValue(EventAt.value, GamePadAt->Axes[LSTICKY]); + } + else if(EventAt.code == ABS_HAT0X) + { + NewController->StickAverageX = EventAt.value; + NewController->IsAnalog = false; + } + else if(EventAt.code == ABS_HAT0Y) + { + NewController->StickAverageY = -EventAt.value; + NewController->IsAnalog = false; + } + } break; + } +#if 0 + if(EventAt.type) printf("%d %d %d\n", EventAt.type, EventAt.code, EventAt.value); +#endif + } + + } + } + } + + + // NOTE(luca): Buffer is cleared just in case. +#if 1 + for(int X = 0; X < OffscreenBuffer.Width; X++) + { + for(int Y = 0; Y < OffscreenBuffer.Height; Y++) + { + ((u32 *)OffscreenBuffer.Memory)[X + Y*OffscreenBuffer.Width] = 0; + } + } +#endif + + if(!GlobalPaused) + { + if(Game.UpdateAndRender) + { + Game.UpdateAndRender(&ThreadContext, &GameMemory, NewInput, &OffscreenBuffer); + } + } + + r32 SamplesToWrite = TargetSecondsPerFrame*SampleRate; + local_persist b32 AudioFillFirstTime = true; + r32 SecondsSinceFlip = (LinuxGetSecondsElapsed(FlipWallClock, LinuxGetWallClock()))/1000000000.0f; + r32 SecondsLeftUntilFlip = TargetSecondsPerFrame - SecondsSinceFlip; + if(AudioFillFirstTime) + { + SamplesToWrite += SecondsLeftUntilFlip*SampleRate; + AudioFillFirstTime = false; + } + +#if HANDMADE_INTERNAL + snd_htimestamp_t AudioTimeStamp = {}; + snd_pcm_status(PCMHandle, PCMStatus); + sound_status *PCMSoundStatus = (sound_status *)PCMStatus; + u32 AvailableFrames = snd_pcm_status_get_avail(PCMStatus); + s32 DelayFrames = snd_pcm_status_get_delay(PCMStatus); + snd_pcm_status_get_htstamp(PCMStatus, &AudioTimeStamp); + struct timespec AudioWallClock = LinuxGetWallClock(); + +#if 0 + // TODO(luca): Why does this work? + if(DelayFrames > SamplesToWrite && DelayFrames > 0) + { + SamplesToWrite -= (DelayFrames - SamplesToWrite); + if(SamplesToWrite < 0) + { + SamplesToWrite = 0; + } + } +#endif + + u32 PadY = 16; + u32 LineHeight = 38; + u32 X = 0; + u32 Y = 0; + + u32 MapIntoWidth = (OffscreenBuffer.Width); + + X = MapIntoWidth*(r32)((r32)SamplesToWrite/(r32)SampleRate); + LinuxDebugVerticalLine(&OffscreenBuffer, X, Y*LineHeight + PadY, 0x0030FF00); + + X = MapIntoWidth*(r32)((r32)AvailableFrames/(r32)PCMBufferSize); + LinuxDebugVerticalLine(&OffscreenBuffer, X, Y*LineHeight + PadY, 0x0030FF00); + + X = MapIntoWidth*(r32)((r32)DelayFrames/(r32)PCMBufferSize); + LinuxDebugVerticalLine(&OffscreenBuffer, X, Y*LineHeight + PadY, 0x00FF00FF); + + Y++; + + X = MapIntoWidth*(r32)((r32)SecondsSinceFlip/TargetSecondsPerFrame); + LinuxDebugVerticalLine(&OffscreenBuffer, X, Y*LineHeight + PadY, 0x0000FF00); + +#endif + + game_sound_output_buffer SoundBuffer = {}; + SoundBuffer.SamplesPerSecond = SampleRate; + SoundBuffer.SampleCount = SamplesToWrite; + SoundBuffer.Samples = (s16 *)AudioSamples; + + if(Game.GetSoundSamples) + { + Game.GetSoundSamples(&ThreadContext, &GameMemory, &SoundBuffer); + } + + LastFramesWritten = snd_pcm_writei(PCMHandle, SoundBuffer.Samples, SoundBuffer.SampleCount); + + if(LastFramesWritten < 0) + { + // TODO(luca): Logging + // NOTE(luca): We might want to silence in case of overruns ahead of time. We also probably want to handle latency differently here. + snd_pcm_recover(PCMHandle, LastFramesWritten, 0); + } + + printf("Expected: %d, Delay: %4d, Being written: %5lu, Written: %d, Ptr: %lu\n", + (int)SamplesToWrite, DelayFrames, PCMBufferSize - AvailableFrames, LastFramesWritten, PCMSoundStatus->hw_ptr); + + struct timespec WorkCounter = LinuxGetWallClock(); + r32 WorkSecondsElapsed = LinuxGetSecondsElapsed(LastCounter, WorkCounter); + r32 SecondsElapsedForFrame = (WorkSecondsElapsed/1000000000.0f); + if(SecondsElapsedForFrame < TargetSecondsPerFrame) + { + + s64 SleepUS = (s64)((TargetSecondsPerFrame - 0.001 - SecondsElapsedForFrame)*1000000.0f); + if(SleepUS > 0) + { + usleep(SleepUS); + } + else + { + // TODO(luca): Logging + } + + r32 TestSecondsElapsedForFrame = (r32)(LinuxGetSecondsElapsed(LastCounter, LinuxGetWallClock())); + if(TestSecondsElapsedForFrame < TargetSecondsPerFrame) + { + // TODO(luca): Log missed sleep + } + + while(SecondsElapsedForFrame < TargetSecondsPerFrame) + { + SecondsElapsedForFrame = LinuxGetSecondsElapsed(LastCounter, LinuxGetWallClock())/1000000000.0f; + } + } + else + { + // TODO(luca): Log missed frame rate! + } + + struct timespec EndCounter = LinuxGetWallClock(); + r32 MSPerFrame = (r32)(LinuxGetSecondsElapsed(LastCounter, EndCounter)/1000000.f); + LastCounter = EndCounter; + + XPutImage(DisplayHandle, WindowHandle, DefaultGC, WindowBuffer, 0, 0, 0, 0, Width, Height); + FlipWallClock = LinuxGetWallClock(); + + game_input *TempInput = NewInput; + NewInput = OldInput; + TempInput = NewInput; + +#if 0 + u64 EndCycleCount = __rdtsc(); + u64 CyclesElapsed = EndCycleCount - LastCycleCount; + LastCycleCount = EndCycleCount; + + r64 FPS = 0; + r64 MCPF = (r64)(CyclesElapsed/(1000.0f*1000.0f)); + printf("%.2fms/f %.2ff/s %.2fmc/f\n", MSPerFrame, FPS, MCPF); +#endif + + } + + } + else + { + // TODO: Log this bad WindowHandle + } + } + else + { + // TODO: Log this could not match visual info + } + } + else + { + // TODO: Log could not get x connection + } + return 0; +} diff --git a/linux_handmade.h b/linux_handmade.h new file mode 100644 index 0000000..9e418b6 --- /dev/null +++ b/linux_handmade.h @@ -0,0 +1,71 @@ +/* date = April 15th 2025 4:50 pm */ + +#ifndef LINUX_HANDMADE_H +#define LINUX_HANDMADE_H + +struct linux_game_code +{ + game_update_and_render *UpdateAndRender; + game_get_sound_samples *GetSoundSamples; + + void *LibraryHandle; + struct timespec LibraryLastWriteTime; +}; + +enum linux_gamepad_axes_enum +{ + LSTICKX, + LSTICKY, + RSTICKX, + RSTICKY, + LSHOULDER, + RSHOULDER, + DPADX, + DPADY, + AXES_COUNT +}; + +struct linux_gamepad_axis +{ + s32 Minimum; + s32 Maximum; + s32 Fuzz; + s32 Flat; +}; + +struct linux_gamepad +{ + int FileFD; + char FilePath[PATH_MAX]; + + char Name[256]; + int SupportsRumble; + linux_gamepad_axis Axes[AXES_COUNT]; +}; + + + +// IMPORTANT(luca): Copy Pasted from alsa-lib; This is a hack to access the hw_ptr and to +// understand what alsa-lib functions are doing internally better through the debugger. +typedef struct { unsigned char pad[sizeof(time_t) - sizeof(int)]; } __time_pad; +typedef struct +{ + snd_pcm_state_t state; /* stream state */ + __time_pad pad1; /* align to timespec */ + struct timespec trigger_tstamp; /* time when stream was started/stopped/paused */ + struct timespec tstamp; /* reference timestamp */ + snd_pcm_uframes_t appl_ptr; /* appl ptr */ + snd_pcm_uframes_t hw_ptr; /* hw ptr */ + snd_pcm_sframes_t delay; /* current delay in frames */ + snd_pcm_uframes_t avail; /* number of frames available */ + snd_pcm_uframes_t avail_max; /* max frames available on hw since last status */ + snd_pcm_uframes_t overrange; /* count of ADC (capture) overrange detections from last status */ + snd_pcm_state_t suspended_state; /* suspended stream state */ + __u32 audio_tstamp_data; /* needed for 64-bit alignment, used for configs/report to/from userspace */ + struct timespec audio_tstamp; /* sample counter, wall clock, PHC or on-demand sync'ed */ + struct timespec driver_tstamp; /* useful in case reference system tstamp is reported with delay */ + __u32 audio_tstamp_accuracy; /* in ns units, only valid if indicated in audio_tstamp_data */ + unsigned char reserved[52-2*sizeof(struct timespec)]; /* must be filled with zero */ +} sound_status; + +#endif //LINUX_HANDMADE_H |