소스 검색

Initial commit from base

Kajetan Johannes Hammerle 2 달 전
커밋
2ae2b61839

+ 3 - 0
.clangd

@@ -0,0 +1,3 @@
+CompileFlags:
+  Add: [-ferror-limit=0]
+  CompilationDatabase: ./build_debug/

+ 12 - 0
.gitignore

@@ -0,0 +1,12 @@
+build_debug
+build_profile
+build_release
+install
+profile
+compiler
+.cache
+*.swp
+*.swo
+*.cpp
+*.hpp
+*.spv

+ 86 - 0
CMakeLists.txt

@@ -0,0 +1,86 @@
+cmake_minimum_required(VERSION 3.25)
+project(gaming_core C)
+
+add_subdirectory(glfw SYSTEM)
+
+set(CMAKE_C_STANDARD 23)
+
+set(SRC
+    "src/Image.c"
+    "src/Network.c"
+    "src/VulkanUtils.c"
+    "src/VulkanWrapper.c"
+    "src/WindowManager.c"
+)
+
+set(SRC_TESTS
+    "test/Main.c"
+    "test/modules/ImageTests.c"
+    "test/modules/NetworkTests.c"
+    "test/modules/WindowManagerTests.c"
+)
+
+if("${CMAKE_BUILD_TYPE}" STREQUAL "Release")
+    set(COMPILE_OPTIONS "")
+    set(LINK_OPTIONS "")
+    set(LOG_LEVEL 2)
+    set(DEFINITIONS CHECK_MEMORY)
+else()
+    set(DEFINITIONS ERROR_SIMULATOR CHECK_MEMORY DEBUG_VULKAN)
+    if(CMAKE_C_COMPILER_ID STREQUAL "GNU")
+        set(COMPILE_OPTIONS --coverage)
+        set(LINK_OPTIONS gcov)
+    elseif(CMAKE_C_COMPILER_ID STREQUAL "Clang")
+        set(COMPILE_OPTIONS -fprofile-instr-generate -fcoverage-mapping)
+        set(LINK_OPTIONS ${COMPILE_OPTIONS})
+    endif()
+    set(LOG_LEVEL 4)
+    list(APPEND SRC "src/ErrorSimulator.c")
+endif()
+
+if(CMAKE_C_COMPILER_ID STREQUAL "GNU")
+    include("cmake/gcc_warnings.cmake")
+elseif(CMAKE_C_COMPILER_ID STREQUAL "Clang")
+    include("cmake/clang_warnings.cmake")
+endif()
+
+add_library(gaming_core STATIC ${SRC})
+target_compile_options(gaming_core PUBLIC
+    ${COMPILE_OPTIONS}
+    ${WARNINGS}
+    -fdiagnostics-color=always
+)
+target_compile_definitions(gaming_core
+    PUBLIC LOG_LEVEL=${LOG_LEVEL}
+    PUBLIC ${DEFINITIONS}
+)
+target_link_libraries(gaming_core
+    PRIVATE m core glfw vulkan ${LINK_OPTIONS}
+)
+target_include_directories(gaming_core SYSTEM
+    PUBLIC ${CMAKE_INSTALL_PREFIX}/include
+    PUBLIC enet/include
+    PUBLIC stb
+    PUBLIC glfw/include
+)
+target_link_directories(gaming_core
+    PUBLIC ${CMAKE_INSTALL_PREFIX}/lib
+)
+target_sources(gaming_core PUBLIC
+    FILE_SET HEADERS
+    BASE_DIRS include
+    FILES
+        ./include/core/Image.h
+        ./include/core/Network.h
+        ./include/core/VulkanWrapper.h
+        ./include/core/WindowManager.h
+)
+install(TARGETS gaming_core FILE_SET HEADERS)
+
+include("cmake/add_shader.cmake")
+
+add_shader(NAME vertex SOURCES shaders/vertex.vert.glsl)
+add_shader(NAME fragment SOURCES shaders/fragment.frag.glsl)
+
+add_executable(test ${SRC_TESTS})
+target_link_libraries(test PRIVATE gaming_core)

+ 18 - 0
cmake/add_shader.cmake

@@ -0,0 +1,18 @@
+function(add_shader)
+    cmake_parse_arguments(args
+        "" "NAME" "SOURCES" ${ARGN}
+    )
+    if("${args_NAME}" STREQUAL "")
+        message( FATAL_ERROR "add_shader misses NAME" )
+    endif()
+
+    set(output ${CMAKE_SOURCE_DIR}/shaders/${args_NAME}.spv)
+
+    add_custom_command(
+        OUTPUT ${output}
+        COMMAND glslangValidator -V -o ${output} ${args_SOURCES}
+        WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
+        DEPENDS ${args_SOURCES}
+    )
+    add_custom_target(${args_NAME} ALL DEPENDS ${output})
+endfunction()

+ 59 - 0
cmake/clang_warnings.cmake

@@ -0,0 +1,59 @@
+set(WARNINGS
+    -Wall
+    -Walloca
+    -Warray-parameter
+    -Wbad-function-cast
+    -Wcast-qual
+    -Wconditional-uninitialized
+    -Wconversion
+    -Wdate-time
+    -Wdisabled-optimization
+    -Wdouble-promotion
+    -Wenum-compare
+    -Wenum-conversion
+    -Werror
+    -Wextra
+    -Wextra-semi-stmt
+    -Wfloat-equal
+    -Wformat=2
+    -Wframe-larger-than=8388608
+    -Winfinite-recursion
+    -Winit-self
+    -Winvalid-pch
+    -Wlarger-than=1073741824
+    -Wmissing-braces
+    -Wmissing-declarations
+    -Wmissing-include-dirs
+    -Wmissing-noreturn 
+    -Wmissing-prototypes
+    -Wmissing-variable-declarations
+    -Wmultichar
+    -Wnarrowing
+    -Wnested-externs
+    -Wnull-dereference
+    -Wold-style-definition
+    -Woverlength-strings
+    -Wredundant-decls
+    -Wshadow
+    -Wsign-conversion
+    -Wstack-protector
+    -Wstrict-overflow=2
+    -Wstrict-prototypes
+    -Wundef
+    -Wunreachable-code
+    -Wvla
+    -Wwrite-strings
+    -pedantic
+    -pedantic-errors
+)
+
+if(0)
+    set(WARNINGS ${WARNINGS} 
+        -Weverything
+        -Wno-unsafe-buffer-usage
+        -Wno-c++98-compat
+        -Wno-declaration-after-statement
+        -Wno-pre-c2x-compat
+        -Wno-padded
+    )
+endif()

+ 69 - 0
cmake/gcc_warnings.cmake

@@ -0,0 +1,69 @@
+set(WARNINGS
+    -Wno-attributes
+
+    -Wall
+    -Walloc-zero
+    -Walloca
+    -Wanalyzer-too-complex
+    -Warith-conversion
+    -Warray-bounds=2
+    -Warray-parameter
+    -Wattribute-alias=2
+    -Wbad-function-cast
+    -Wbidi-chars=any
+    -Wcast-align=strict
+    -Wcast-qual
+    -Wconversion
+    -Wdate-time
+    -Wdisabled-optimization
+    -Wdouble-promotion
+    -Wduplicated-branches
+    -Wduplicated-cond
+    -Wenum-compare
+    -Wenum-conversion
+    -Werror
+    -Wextra
+    -Wfloat-equal
+    -Wformat-overflow=2
+    -Wformat-signedness
+    -Wformat-truncation=2
+    -Wformat=2
+    -Wframe-larger-than=8388608
+    -Wimplicit-fallthrough=5
+    -Winfinite-recursion
+    -Winit-self
+    -Winvalid-pch
+    -Wjump-misses-init
+    -Wlarger-than=1073741824
+    -Wlogical-op
+    -Wmissing-braces
+    -Wmissing-declarations
+    -Wmissing-include-dirs
+    -Wmissing-prototypes
+    -Wmultichar
+    -Wnarrowing
+    -Wnested-externs
+    -Wnormalized=nfkc
+    -Wnull-dereference
+    -Wold-style-definition
+    -Woverlength-strings
+    -Wredundant-decls
+    -Wshadow
+    -Wshift-overflow=2
+    -Wsign-conversion
+    -Wstack-protector
+    -Wstack-usage=8388608
+    -Wstrict-overflow=2
+    -Wstrict-prototypes
+    -Wstringop-overflow=4
+    -Wtrampolines
+    -Wtrivial-auto-var-init
+    -Wundef
+    -Wunreachable-code
+    -Wunused-const-variable=2
+    -Wuse-after-free=3
+    -Wvla
+    -Wwrite-strings
+    -pedantic
+    -pedantic-errors
+)

+ 25 - 0
include/core/Image.h

@@ -0,0 +1,25 @@
+#ifndef CORE_IMAGE_H
+#define CORE_IMAGE_H
+
+#include <core/Types.h>
+
+typedef struct {
+    u8* data;
+    int width;
+    int height;
+    int channels;
+} Image8;
+
+typedef struct {
+    u16* data;
+    int width;
+    int height;
+    int channels;
+} Image16;
+
+bool initImage8(Image8* image, const char* path);
+void destroyImage8(Image8* image);
+bool initImage16(Image16* image, const char* path);
+void destroyImage16(Image16* image);
+
+#endif

+ 83 - 0
include/core/Network.h

@@ -0,0 +1,83 @@
+#ifndef CORE_NETWORK_H
+#define CORE_NETWORK_H
+
+#include <core/Buffer.h>
+#include <core/Types.h>
+
+typedef enum {
+    PACKET_RELIABLE,
+    PACKET_SEQUENCED,
+    PACKET_UNSEQUENCED
+} PacketSendMode;
+
+typedef struct {
+    const char* data;
+    size_t size;
+    size_t index;
+} InPacket;
+
+void initInPacket(InPacket* in, const void* data, size_t n);
+bool readInPacketU8(InPacket* in, u8* u);
+bool readInPacketU16(InPacket* in, u16* u);
+bool readInPacketU32(InPacket* in, u32* u);
+bool readInPacketI8(InPacket* in, i8* i);
+bool readInPacketI16(InPacket* in, i16* i);
+bool readInPacketI32(InPacket* in, i32* i);
+bool readInPacketFloat(InPacket* in, float* f);
+size_t readInPacketString(InPacket* in, char* buffer, size_t n);
+bool readInPacket(InPacket* in, void* buffer, size_t n);
+
+typedef struct {
+    Buffer data;
+} OutPacket;
+
+void initOutPacket(OutPacket* out);
+void destroyOutPacket(OutPacket* out);
+void writeOutPacketU8(OutPacket* out, u8 u);
+void writeOutPacketU16(OutPacket* out, u16 u);
+void writeOutPacketU32(OutPacket* out, u32 u);
+void writeOutPacketI8(OutPacket* out, i8 i);
+void writeOutPacketI16(OutPacket* out, i16 i);
+void writeOutPacketI32(OutPacket* out, i32 i);
+void writeOutPacketFloat(OutPacket* out, float f);
+void writeOutPacketString(OutPacket* out, const char* buffer);
+void writeOutPacket(OutPacket* out, const void* buffer, size_t n);
+
+typedef u16 Port;
+typedef void (*OnServerConnect)(void);
+typedef void (*OnServerDisconnect)(void);
+typedef void (*OnServerPacket)(InPacket*);
+
+bool startClient(void);
+void stopClient(void);
+bool connectClient(const char* server, Port port, int timeoutTicks);
+void setClientTimeout(u32 timeout, u32 timeoutMin, u32 timeoutMax);
+void disconnectClient(int timeoutTicks);
+void sendClientPacket(const OutPacket* p, PacketSendMode mode);
+void tickClient(void);
+void setClientConnectHandler(OnServerConnect oc);
+void setClientDisconnectHandler(OnServerDisconnect od);
+void setClientPacketHandler(OnServerPacket op);
+void resetClientHandler(void);
+bool isClientConnecting(void);
+bool isClientConnected(void);
+
+typedef int Client;
+typedef void (*OnClientConnect)(Client);
+typedef void (*OnClientDisconnect)(Client);
+typedef void (*OnClientPacket)(Client, InPacket*);
+
+bool startServer(Port port, size_t maxClients);
+void stopServer(void);
+void tickServer(void);
+void sendServerPacketBroadcast(const OutPacket* p, PacketSendMode mode);
+void sendServerPacket(Client client, const OutPacket* p, PacketSendMode mode);
+void setServerTimeout(
+    Client client, u32 timeout, u32 timeoutMin, u32 timeoutMax);
+void disconnectServerClient(Client client);
+void setServerConnectHandler(OnClientConnect oc);
+void setServerDisconnectHandler(OnClientDisconnect od);
+void setServerPacketHandler(OnClientPacket op);
+void resetServerHandler(void);
+
+#endif

+ 8 - 0
include/core/VulkanWrapper.h

@@ -0,0 +1,8 @@
+#ifndef CORE_VULKAN_WRAPPER_H
+#define CORE_VULKAN_WRAPPER_H
+
+bool initVulkan(void);
+void renderVulkan(void);
+void destroyVulkan(void);
+
+#endif

+ 63 - 0
include/core/WindowManager.h

@@ -0,0 +1,63 @@
+#ifndef CORE_WINDOW_MANAGER_H
+#define CORE_WINDOW_MANAGER_H
+
+#include <core/Vector.h>
+
+typedef bool (*WindowRunHandler)(void* data);
+typedef void (*WindowTickHandler)(void* data);
+typedef void (*WindowRenderHandler)(void* data, float lag);
+
+typedef struct {
+    IntVector2 size;
+    bool fullscreen;
+    const char* name;
+} WindowOptions;
+
+bool openWindow(const WindowOptions* options);
+void closeWindow(void);
+void showWindow(void);
+void trapWindowCursor(void);
+void freeWindowCursor(void);
+bool isWindowCursorTrapped(void);
+const IntVector2* getWindowSize(void);
+bool hasWindowSizeChanged(void);
+bool shouldWindowClose(void);
+
+void setWindowRunHandler(WindowRunHandler wr, void* data);
+void setWindowTickHandler(WindowTickHandler t, void* data);
+void setWindowRenderHandler(WindowRenderHandler r, void* data);
+void setWindowNanosPerTick(i64 nanos);
+void runWindow(void);
+float getWindowTicksPerSecond(void);
+float getWindowFramesPerSecond(void);
+
+void setInputLimit(size_t limit);
+void resetInput(void);
+void enableInput(void);
+void disableInput(void);
+bool isInputEnabled(void);
+void fillInput(const char* s);
+size_t getInputCursor(void);
+void setInputCursor(size_t index);
+const char* getInputString(void);
+
+typedef size_t Button;
+
+Button addButton(const char* name);
+void bindKeyToButton(Button b, int key);
+void bindGamepadToButton(Button b, int gamepadButton);
+void bindMouseToButton(Button b, int mouseButton);
+
+Vector2 getLastMousePosition(void);
+Vector2 getMousePosition(void);
+Vector2 getLeftGamepadAxis(void);
+Vector2 getRightGamepadAxis(void);
+float getLeftGamepadTrigger(void);
+float getRightGamepadTrigger(void);
+
+bool isButtonDown(Button b);
+int getButtonDownTime(Button b);
+bool wasButtonReleased(Button b);
+const char* getButtonName(Button b);
+
+#endif

+ 8 - 0
shaders/fragment.frag.glsl

@@ -0,0 +1,8 @@
+#version 450
+
+layout(location = 0) out vec4 outColor;
+layout(location = 0) in vec3 fragColor;
+
+void main() {
+    outColor = vec4(fragColor, 1.0);
+}

+ 20 - 0
shaders/vertex.vert.glsl

@@ -0,0 +1,20 @@
+#version 450
+
+layout(location = 0) out vec3 fragColor;
+
+vec2 positions[3] = vec2[](
+    vec2(0.0, -0.5),
+    vec2(0.5, 0.5),
+    vec2(-0.5, 0.5)
+);
+
+vec3 colors[3] = vec3[](
+    vec3(1.0, 0.0, 0.0),
+    vec3(0.0, 1.0, 0.0),
+    vec3(0.0, 0.0, 1.0)
+);
+
+void main() {
+    gl_Position = vec4(positions[gl_VertexIndex], 0.0, 1.0);
+    fragColor = colors[gl_VertexIndex];
+}

+ 14 - 0
src/ErrorSimulator.c

@@ -0,0 +1,14 @@
+#ifdef ERROR_SIMULATOR
+
+#include "ErrorSimulator.h"
+
+int failCounter = -1;
+
+bool checkFail(void) {
+    if(failCounter < 0) {
+        return false;
+    }
+    return failCounter == 0 ? true : failCounter-- == 0;
+}
+
+#endif

+ 12 - 0
src/ErrorSimulator.h

@@ -0,0 +1,12 @@
+#ifndef CORE_ERROR_SIMULATOR_H
+#define CORE_ERROR_SIMULATOR_H
+
+#ifdef ERROR_SIMULATOR
+extern int failCounter;
+bool checkFail(void);
+#define FAIL(x, e) (checkFail() ? e : (x))
+#else
+#define FAIL(x, e) x
+#endif
+
+#endif

+ 9 - 0
src/GLFW.h

@@ -0,0 +1,9 @@
+#ifndef CORE_GLFW_H
+#define CORE_GLFW_H
+
+#define GLFW_INCLUDE_VULKAN
+#include <GLFW/glfw3.h>
+
+GLFWwindow* getWindow(void);
+
+#endif

+ 46 - 0
src/Image.c

@@ -0,0 +1,46 @@
+#include "core/Image.h"
+
+#define STB_IMAGE_IMPLEMENTATION
+#define STBI_ONLY_PNG
+#define STBI_MALLOC(n) coreAllocate(n)
+#define STBI_REALLOC(p, n) coreReallocate(p, n)
+#define STBI_FREE(p) coreFree(p)
+#include <core/Logger.h>
+#include <core/Utility.h>
+#include <errno.h>
+#include <stb_image.h>
+#include <string.h>
+
+static void printError(const char* path) {
+    LOG_ERROR("Cannot read image '%s': %s", path, strerror(errno));
+}
+
+bool initImage8(Image8* i, const char* path) {
+    *i = (Image8){0};
+    i->data = stbi_load(path, &i->width, &i->height, &i->channels, 0);
+    if(i->data == nullptr) {
+        printError(path);
+        return true;
+    }
+    return false;
+}
+
+void destroyImage8(Image8* i) {
+    stbi_image_free(i->data);
+    *i = (Image8){0};
+}
+
+bool initImage16(Image16* i, const char* path) {
+    *i = (Image16){0};
+    i->data = stbi_load_16(path, &i->width, &i->height, &i->channels, 0);
+    if(i->data == nullptr) {
+        printError(path);
+        return true;
+    }
+    return false;
+}
+
+void destroyImage16(Image16* i) {
+    stbi_image_free(i->data);
+    *i = (Image16){0};
+}

+ 559 - 0
src/Network.c

@@ -0,0 +1,559 @@
+#include "core/Network.h"
+
+#define ENET_IMPLEMENTATION
+#include <assert.h>
+#include <core/HashMap.h>
+#include <core/Logger.h>
+#include <core/Utility.h>
+#include <enet.h>
+#include <string.h>
+
+#include "ErrorSimulator.h"
+
+// HashMap clients; // Client -> ENetPeer*
+HASHMAP(Client, ENetPeer*, Client)
+#define equalClient equalInt
+#define isInvalidKeyClient isInvalidKeyInt
+#define hashClient hashInt
+HASHMAP_SOURCE(Client, ENetPeer*, Client)
+
+void initInPacket(InPacket* in, const void* data, size_t n) {
+    in->data = data;
+    in->size = n;
+    in->index = 0;
+}
+
+bool readInPacketU8(InPacket* in, u8* u) {
+    return readInPacket(in, u, sizeof(*u));
+}
+
+bool readInPacketU16(InPacket* in, u16* u) {
+    if(readInPacket(in, u, sizeof(*u))) {
+        return true;
+    }
+    *u = ntohs(*u);
+    return false;
+}
+
+bool readInPacketU32(InPacket* in, u32* u) {
+    if(readInPacket(in, u, sizeof(*u))) {
+        return true;
+    }
+    *u = ntohl(*u);
+    return false;
+}
+
+bool readInPacketI8(InPacket* in, i8* i) {
+    u8 u;
+    if(readInPacketU8(in, &u)) {
+        return true;
+    }
+    *i = (i8)((i32)u - (i32)128);
+    return false;
+}
+
+bool readInPacketI16(InPacket* in, i16* i) {
+    u16 u;
+    if(readInPacketU16(in, &u)) {
+        return true;
+    }
+    *i = (i16)((i32)u - (i32)32'768);
+    return false;
+}
+
+bool readInPacketI32(InPacket* in, i32* i) {
+    u32 u;
+    if(readInPacketU32(in, &u)) {
+        return true;
+    }
+    if(u < 2'147'483'648) {
+        *i = (i32)((i32)u - (i32)2'147'483'648);
+    } else {
+        *i = (i32)(u - (u32)2'147'483'648);
+    }
+    return false;
+}
+
+bool readInPacketFloat(InPacket* in, float* f) {
+    u32 u;
+    static_assert(sizeof(u) == sizeof(*f), "float and u32 size do not match");
+    if(readInPacketU32(in, &u)) {
+        return true;
+    }
+    memcpy(f, &u, sizeof(float));
+    return false;
+}
+
+size_t readInPacketString(InPacket* in, char* buffer, size_t n) {
+    if(n == 0) {
+        return 0;
+    }
+    u16 size;
+    if(readInPacketU16(in, &size)) {
+        return 0;
+    }
+    size_t end = size;
+    char* bufferStart = buffer;
+    n--;
+    while(n-- > 0 && end > 0) {
+        end--;
+        u8 u;
+        if(readInPacketU8(in, &u)) {
+            *bufferStart = '\0';
+            break;
+        }
+        *(buffer++) = (char)u;
+    }
+    while(end-- > 0 && !readInPacketU8(in, &(u8){0})) {}
+    *buffer = '\0';
+    return size;
+}
+
+bool readInPacket(InPacket* in, void* buffer, size_t n) {
+    if(in->index + n > in->size) {
+        return true;
+    }
+    memcpy(buffer, in->data + in->index, n);
+    in->index += n;
+    return false;
+}
+
+void initOutPacket(OutPacket* out) {
+    initBuffer(&out->data);
+}
+
+void destroyOutPacket(OutPacket* out) {
+    destroyBuffer(&out->data);
+}
+
+void writeOutPacketU8(OutPacket* out, u8 u) {
+    addSizedBufferData(&out->data, &u, sizeof(u));
+}
+
+void writeOutPacketU16(OutPacket* out, u16 u) {
+    u = htons(u);
+    addSizedBufferData(&out->data, &u, sizeof(u));
+}
+
+void writeOutPacketU32(OutPacket* out, u32 u) {
+    u = htonl(u);
+    addSizedBufferData(&out->data, &u, sizeof(u));
+}
+
+void writeOutPacketI8(OutPacket* out, i8 i) {
+    writeOutPacketU8(out, (u8)((i32)i + (i32)128));
+}
+
+void writeOutPacketI16(OutPacket* out, i16 i) {
+    writeOutPacketU16(out, (u16)((i32)i + (i32)32'768));
+}
+
+void writeOutPacketI32(OutPacket* out, i32 i) {
+    if(i < 0) {
+        writeOutPacketU32(out, (u32)((i + (i32)2'147'483'647) + (i32)1));
+    } else {
+        writeOutPacketU32(out, (u32)((u32)i + (u32)2'147'483'648));
+    }
+}
+
+void writeOutPacketFloat(OutPacket* out, float f) {
+    u32 u;
+    static_assert(sizeof(u) == sizeof(f), "float and u32 size do not match");
+    memcpy(&u, &f, sizeof(float));
+    writeOutPacketU32(out, u);
+}
+
+void writeOutPacketString(OutPacket* out, const char* s) {
+    size_t marker = out->data.size;
+    writeOutPacketU16(out, 0);
+    size_t end = 0;
+    while(end < 65'534 && *s != '\0') {
+        writeOutPacketU8(out, (u8)(*(s++)));
+        end++;
+    }
+    writeOutPacketU8(out, 0);
+    end++;
+    size_t endMarker = out->data.size;
+    out->data.size = marker;
+    writeOutPacketU16(out, (u16)end);
+    out->data.size = endMarker;
+}
+
+void writeOutPacket(OutPacket* out, const void* buffer, size_t n) {
+    addSizedBufferData(&out->data, buffer, n);
+}
+
+static int enetCounter = 0;
+
+static bool addENet(void) {
+    if(enetCounter == 0 && FAIL(enet_initialize() != 0, true)) {
+        return true;
+    }
+    enetCounter++;
+    return false;
+}
+
+static void removeENet(void) {
+    if(enetCounter > 0 && --enetCounter == 0) {
+        enet_deinitialize();
+    }
+}
+
+static_assert(sizeof(enet_uint16) == sizeof(Port), "port has wrong type");
+
+static void voidVoidDummy(void) {
+}
+
+static void voidInPacketDummy(InPacket*) {
+}
+
+typedef struct {
+    ENetHost* client;
+    ENetPeer* connection;
+    OnServerConnect onConnect;
+    OnServerDisconnect onDisconnect;
+    OnServerPacket onPacket;
+    int connectTicks;
+    int connectTimeoutTicks;
+    int disconnectTicks;
+    int disconnectTimeoutTicks;
+} ClientData;
+
+static ClientData client = {
+    nullptr, nullptr, voidVoidDummy, voidVoidDummy, voidInPacketDummy, 0, 0,
+    0,       0};
+
+bool startClient(void) {
+    if(client.client != nullptr) {
+        LOG_WARNING("Client already started");
+        return true;
+    } else if(addENet()) {
+        LOG_ERROR("Client cannot initialize enet");
+        return true;
+    }
+    client.client = FAIL(enet_host_create(nullptr, 1, 2, 0, 0), nullptr);
+    if(client.client == nullptr) {
+        stopClient();
+        LOG_ERROR("Cannot create enet client host");
+        return true;
+    }
+    return false;
+}
+
+void stopClient(void) {
+    if(client.connection != nullptr) {
+        client.onDisconnect();
+        FAIL(
+            enet_peer_disconnect_now(client.connection, 0),
+            enet_peer_reset(client.connection));
+        client.connection = nullptr;
+    }
+    if(client.client != nullptr) {
+        enet_host_destroy(client.client);
+        client.client = nullptr;
+    }
+    removeENet();
+    client.connectTicks = 0;
+    client.disconnectTicks = 0;
+}
+
+bool connectClient(const char* server, Port port, int timeoutTicks) {
+    if(client.client == nullptr) {
+        LOG_WARNING("Client not started");
+        return true;
+    } else if(client.connection != nullptr) {
+        LOG_WARNING("Connection already exists");
+        return true;
+    }
+
+    ENetAddress address = {0};
+    enet_address_set_host(&address, server);
+    address.port = port;
+
+    client.connection =
+        FAIL(enet_host_connect(client.client, &address, 3, 0), nullptr);
+    if(client.connection == nullptr) {
+        LOG_ERROR("Cannot create connection");
+        return true;
+    }
+    client.connectTicks = 1;
+    client.connectTimeoutTicks = timeoutTicks;
+    return false;
+}
+
+void setClientTimeout(u32 timeout, u32 timeoutMin, u32 timeoutMax) {
+    if(client.connection != nullptr) {
+        enet_peer_timeout(client.connection, timeout, timeoutMin, timeoutMax);
+    }
+}
+
+void disconnectClient(int timeoutTicks) {
+    if(client.connection == nullptr) {
+        return;
+    }
+    client.connectTicks = 0;
+    enet_peer_disconnect(client.connection, 0);
+    client.disconnectTicks = 1;
+    client.disconnectTimeoutTicks = timeoutTicks;
+}
+
+void sendClientPacket(const OutPacket* p, PacketSendMode mode) {
+    if(client.client == nullptr || client.connection == nullptr ||
+       client.connectTicks >= 0) {
+        return;
+    }
+    static const enet_uint32 flags[] = {
+        ENET_PACKET_FLAG_RELIABLE, 0, ENET_PACKET_FLAG_UNSEQUENCED};
+    enet_uint8 i = (enet_uint8)mode;
+    enet_peer_send(
+        client.connection, i,
+        enet_packet_create(p->data.buffer, p->data.size, flags[i]));
+}
+
+static void tickClientEvents(void) {
+    ENetEvent e;
+    while(enet_host_service(client.client, &e, 0) >= 0) {
+        switch(e.type) {
+            case ENET_EVENT_TYPE_CONNECT:
+                client.connectTicks = -1;
+                client.onConnect();
+                break;
+            case ENET_EVENT_TYPE_DISCONNECT_TIMEOUT:
+            case ENET_EVENT_TYPE_DISCONNECT:
+                client.disconnectTicks = 0;
+                client.connectTicks = 0;
+                client.onDisconnect();
+                client.connection = nullptr;
+                break;
+            case ENET_EVENT_TYPE_RECEIVE: {
+                InPacket in;
+                initInPacket(&in, e.packet->data, e.packet->dataLength);
+                client.onPacket(&in);
+                enet_packet_destroy(e.packet);
+                break;
+            }
+            case ENET_EVENT_TYPE_NONE: return;
+        }
+    }
+}
+
+void tickClient(void) {
+    if(client.client == nullptr) {
+        return;
+    }
+    tickClientEvents();
+    if(client.connectTicks >= 1 &&
+       ++client.connectTicks > client.connectTimeoutTicks) {
+        client.connectTicks = 0;
+        disconnectClient(client.connectTimeoutTicks);
+    }
+    if(client.disconnectTicks >= 1 &&
+       ++client.disconnectTicks > client.disconnectTimeoutTicks) {
+        client.disconnectTicks = 0;
+        client.onDisconnect();
+        if(client.connection != nullptr) {
+            enet_peer_reset(client.connection);
+            client.connection = nullptr;
+        }
+    }
+}
+
+void setClientConnectHandler(OnServerConnect oc) {
+    client.onConnect = oc == nullptr ? voidVoidDummy : oc;
+}
+
+void setClientDisconnectHandler(OnServerDisconnect od) {
+    client.onDisconnect = od == nullptr ? voidVoidDummy : od;
+}
+
+void setClientPacketHandler(OnServerPacket op) {
+    client.onPacket = op == nullptr ? voidInPacketDummy : op;
+}
+
+void resetClientHandler(void) {
+    client.onConnect = voidVoidDummy;
+    client.onDisconnect = voidVoidDummy;
+    client.onPacket = voidInPacketDummy;
+}
+
+bool isClientConnecting(void) {
+    return client.connectTicks >= 1;
+}
+
+bool isClientConnected(void) {
+    return client.connectTicks < 0;
+}
+
+static void voidClientDummy(Client) {
+}
+
+static void voidClientInPacketDummy(Client, InPacket*) {
+}
+
+typedef struct {
+    ENetHost* server;
+    HashMapClient clients;
+    Client idCounter;
+    OnClientConnect onConnect;
+    OnClientDisconnect onDisconnect;
+    OnClientPacket onPacket;
+} ServerData;
+
+static ServerData server = {
+    nullptr, {0}, 1, voidClientDummy, voidClientDummy, voidClientInPacketDummy};
+
+bool startServer(Port port, size_t maxClients) {
+    if(maxClients <= 0) {
+        LOG_ERROR("Invalid max client amount");
+        return true;
+    } else if(server.server != nullptr) {
+        LOG_WARNING("Server already started");
+        return true;
+    } else if(addENet()) {
+        LOG_ERROR("Server cannot initialize enet");
+        return true;
+    }
+
+    ENetAddress address = {.host = ENET_HOST_ANY, .port = port};
+    server.server =
+        FAIL(enet_host_create(&address, maxClients, 3, 0, 0), nullptr);
+    if(server.server == nullptr) {
+        stopServer();
+        LOG_ERROR("Cannot create enet server host");
+        return true;
+    }
+    initHashMapClient(&server.clients);
+    return false;
+}
+
+void stopServer(void) {
+    if(server.server != nullptr) {
+        HashMapIteratorClient i;
+        initHashMapIteratorClient(&i, &server.clients);
+        while(hasNextHashMapNodeClient(&i)) {
+            HashMapNodeClient* n = nextHashMapNodeClient(&i);
+            enet_peer_reset(*n->value);
+        }
+        enet_host_destroy(server.server);
+        server.server = nullptr;
+        destroyHashMapClient(&server.clients);
+    }
+    removeENet();
+}
+
+static void writeId(ENetPeer* peer, Client id) {
+    static_assert(
+        sizeof(peer->data) >= sizeof(id), "private data not big enough for id");
+    memcpy(&(peer->data), &id, sizeof(id));
+}
+
+static Client getId(ENetPeer* peer) {
+    assert(peer->data != nullptr);
+    Client id = -1;
+    memcpy(&id, &(peer->data), sizeof(id));
+    return id;
+}
+
+static void handleConnect(ENetEvent* e) {
+    Client id = server.idCounter++;
+    assert(searchHashMapKeyClient(&server.clients, id) == nullptr);
+    *putHashMapKeyClient(&server.clients, id) = e->peer;
+    writeId(e->peer, id);
+    server.onConnect(id);
+}
+
+static void handlePacket(ENetEvent* e) {
+    Client id = getId(e->peer);
+    InPacket in;
+    initInPacket(&in, e->packet->data, e->packet->dataLength);
+    server.onPacket(id, &in);
+}
+
+static void handleDisconnect(ENetEvent* e) {
+    Client id = getId(e->peer);
+    server.onDisconnect(id);
+    removeHashMapKeyClient(&server.clients, id);
+}
+
+void tickServer(void) {
+    if(server.server == nullptr) {
+        return;
+    }
+    ENetEvent e;
+    while(enet_host_service(server.server, &e, 0) >= 0) {
+        switch(e.type) {
+            case ENET_EVENT_TYPE_CONNECT: handleConnect(&e); break;
+            case ENET_EVENT_TYPE_RECEIVE:
+                handlePacket(&e);
+                enet_packet_destroy(e.packet);
+                break;
+            case ENET_EVENT_TYPE_DISCONNECT_TIMEOUT:
+            case ENET_EVENT_TYPE_DISCONNECT: handleDisconnect(&e); break;
+            case ENET_EVENT_TYPE_NONE: return;
+        }
+    }
+}
+
+static ENetPacket* fromBuffer(const Buffer* buffer, enet_uint8 index) {
+    static const enet_uint32 flags[] = {
+        ENET_PACKET_FLAG_RELIABLE, 0, ENET_PACKET_FLAG_UNSEQUENCED};
+    return enet_packet_create(buffer->buffer, buffer->size, flags[index]);
+}
+
+void sendServerPacketBroadcast(const OutPacket* p, PacketSendMode mode) {
+    if(server.server != nullptr) {
+        enet_uint8 index = (enet_uint8)mode;
+        enet_host_broadcast(server.server, index, fromBuffer(&p->data, index));
+    }
+}
+
+void sendServerPacket(
+    Client clientId, const OutPacket* p, PacketSendMode mode) {
+    if(server.server == nullptr) {
+        return;
+    }
+    ENetPeer** peer = searchHashMapKeyClient(&server.clients, clientId);
+    if(peer != nullptr) {
+        enet_uint8 index = (enet_uint8)mode;
+        enet_peer_send(*peer, index, fromBuffer(&p->data, index));
+    }
+}
+
+void setServerTimeout(
+    Client clientId, u32 timeout, u32 timeoutMin, u32 timeoutMax) {
+    if(server.server == nullptr) {
+        return;
+    }
+    ENetPeer** peer = searchHashMapKeyClient(&server.clients, clientId);
+    if(peer != nullptr) {
+        enet_peer_timeout(*peer, timeout, timeoutMin, timeoutMax);
+    }
+}
+
+void disconnectServerClient(Client clientId) {
+    if(server.server == nullptr) {
+        return;
+    }
+    ENetPeer** peer = searchHashMapKeyClient(&server.clients, clientId);
+    if(peer != nullptr) {
+        enet_peer_disconnect(*peer, 0);
+    }
+}
+
+void setServerConnectHandler(OnClientConnect oc) {
+    server.onConnect = oc == nullptr ? voidClientDummy : oc;
+}
+
+void setServerDisconnectHandler(OnClientDisconnect od) {
+    server.onDisconnect = od == nullptr ? voidClientDummy : od;
+}
+
+void setServerPacketHandler(OnClientPacket op) {
+    server.onPacket = op == nullptr ? voidClientInPacketDummy : op;
+}
+
+void resetServerHandler(void) {
+    server.onConnect = voidClientDummy;
+    server.onDisconnect = voidClientDummy;
+    server.onPacket = voidClientInPacketDummy;
+}

+ 552 - 0
src/VulkanUtils.c

@@ -0,0 +1,552 @@
+#include "VulkanUtils.h"
+
+#include <core/File.h>
+#include <core/Logger.h>
+
+static VkInstance instance;
+#ifdef DEBUG_VULKAN
+static VkDebugUtilsMessengerEXT debugMessenger;
+static VkDebugReportCallbackEXT debugReportCallback;
+#endif
+
+#define VK_ERROR_CASE(error)  \
+    case error: return #error
+
+const char* getVulkanResultString(VkResult r) {
+    switch(r) {
+        VK_ERROR_CASE(VK_SUCCESS);
+        VK_ERROR_CASE(VK_NOT_READY);
+        VK_ERROR_CASE(VK_TIMEOUT);
+        VK_ERROR_CASE(VK_EVENT_SET);
+        VK_ERROR_CASE(VK_EVENT_RESET);
+        VK_ERROR_CASE(VK_INCOMPLETE);
+        VK_ERROR_CASE(VK_ERROR_OUT_OF_HOST_MEMORY);
+        VK_ERROR_CASE(VK_ERROR_OUT_OF_DEVICE_MEMORY);
+        VK_ERROR_CASE(VK_ERROR_INITIALIZATION_FAILED);
+        VK_ERROR_CASE(VK_ERROR_DEVICE_LOST);
+        VK_ERROR_CASE(VK_ERROR_MEMORY_MAP_FAILED);
+        VK_ERROR_CASE(VK_ERROR_LAYER_NOT_PRESENT);
+        VK_ERROR_CASE(VK_ERROR_EXTENSION_NOT_PRESENT);
+        VK_ERROR_CASE(VK_ERROR_FEATURE_NOT_PRESENT);
+        VK_ERROR_CASE(VK_ERROR_INCOMPATIBLE_DRIVER);
+        VK_ERROR_CASE(VK_ERROR_TOO_MANY_OBJECTS);
+        VK_ERROR_CASE(VK_ERROR_FORMAT_NOT_SUPPORTED);
+        VK_ERROR_CASE(VK_ERROR_FRAGMENTED_POOL);
+        VK_ERROR_CASE(VK_ERROR_UNKNOWN);
+        VK_ERROR_CASE(VK_ERROR_OUT_OF_POOL_MEMORY);
+        VK_ERROR_CASE(VK_ERROR_INVALID_EXTERNAL_HANDLE);
+        VK_ERROR_CASE(VK_ERROR_FRAGMENTATION);
+        VK_ERROR_CASE(VK_ERROR_INVALID_OPAQUE_CAPTURE_ADDRESS);
+        VK_ERROR_CASE(VK_PIPELINE_COMPILE_REQUIRED);
+        VK_ERROR_CASE(VK_ERROR_SURFACE_LOST_KHR);
+        VK_ERROR_CASE(VK_ERROR_NATIVE_WINDOW_IN_USE_KHR);
+        VK_ERROR_CASE(VK_SUBOPTIMAL_KHR);
+        VK_ERROR_CASE(VK_ERROR_OUT_OF_DATE_KHR);
+        VK_ERROR_CASE(VK_ERROR_INCOMPATIBLE_DISPLAY_KHR);
+        VK_ERROR_CASE(VK_ERROR_VALIDATION_FAILED_EXT);
+        VK_ERROR_CASE(VK_ERROR_INVALID_SHADER_NV);
+        VK_ERROR_CASE(VK_ERROR_IMAGE_USAGE_NOT_SUPPORTED_KHR);
+        VK_ERROR_CASE(VK_ERROR_VIDEO_PICTURE_LAYOUT_NOT_SUPPORTED_KHR);
+        VK_ERROR_CASE(VK_ERROR_VIDEO_PROFILE_OPERATION_NOT_SUPPORTED_KHR);
+        VK_ERROR_CASE(VK_ERROR_VIDEO_PROFILE_FORMAT_NOT_SUPPORTED_KHR);
+        VK_ERROR_CASE(VK_ERROR_VIDEO_PROFILE_CODEC_NOT_SUPPORTED_KHR);
+        VK_ERROR_CASE(VK_ERROR_VIDEO_STD_VERSION_NOT_SUPPORTED_KHR);
+        VK_ERROR_CASE(VK_ERROR_INVALID_DRM_FORMAT_MODIFIER_PLANE_LAYOUT_EXT);
+        VK_ERROR_CASE(VK_ERROR_NOT_PERMITTED_KHR);
+        VK_ERROR_CASE(VK_ERROR_FULL_SCREEN_EXCLUSIVE_MODE_LOST_EXT);
+        VK_ERROR_CASE(VK_THREAD_IDLE_KHR);
+        VK_ERROR_CASE(VK_THREAD_DONE_KHR);
+        VK_ERROR_CASE(VK_OPERATION_DEFERRED_KHR);
+        VK_ERROR_CASE(VK_OPERATION_NOT_DEFERRED_KHR);
+        VK_ERROR_CASE(VK_ERROR_INVALID_VIDEO_STD_PARAMETERS_KHR);
+        VK_ERROR_CASE(VK_ERROR_COMPRESSION_EXHAUSTED_EXT);
+        VK_ERROR_CASE(VK_INCOMPATIBLE_SHADER_BINARY_EXT);
+        VK_ERROR_CASE(VK_PIPELINE_BINARY_MISSING_KHR);
+        VK_ERROR_CASE(VK_ERROR_NOT_ENOUGH_SPACE_KHR);
+        default: break;
+    }
+    return "unknown";
+}
+
+static PFN_vkVoidFunction getVulkanFunction(const char* name) {
+    return vkGetInstanceProcAddr(instance, name);
+}
+
+#define GET_VULKAN_FUNCTION(name) ((PFN_##name)getVulkanFunction(#name))
+
+bool initVulkanInstance() {
+    u32 baseCount = 0;
+    const char** baseExtensions = glfwGetRequiredInstanceExtensions(&baseCount);
+    if(baseExtensions == nullptr) {
+        LOG_ERROR("Could not get required extensions from GLFW");
+        return true;
+    }
+#ifdef DEBUG_VULKAN
+    u32 count = baseCount + 2;
+    const char* extensions[32];
+    if(count > ARRAY_LENGTH(extensions)) {
+        LOG_ERROR("Extension buffer is too small");
+        return true;
+    }
+    for(u32 i = 0; i < baseCount; i++) {
+        extensions[i] = baseExtensions[i];
+    }
+    extensions[baseCount] = VK_EXT_DEBUG_UTILS_EXTENSION_NAME;
+    extensions[baseCount + 1] = VK_EXT_DEBUG_REPORT_EXTENSION_NAME;
+#else
+    u32 count = baseCount;
+    const char** extensions = baseExtensions;
+#endif
+    VkApplicationInfo appInfo = {
+        .sType = VK_STRUCTURE_TYPE_APPLICATION_INFO,
+        .pApplicationName = "Vulkan",
+        .applicationVersion = VK_MAKE_VERSION(1, 0, 0),
+        .pEngineName = "Kajetan",
+        .engineVersion = VK_MAKE_VERSION(0, 0, 1),
+        .apiVersion = VK_API_VERSION_1_1};
+    VkInstanceCreateInfo info = {
+        .sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO,
+        .pApplicationInfo = &appInfo,
+#ifdef DEBUG_VULKAN
+        .enabledLayerCount = 1,
+        .ppEnabledLayerNames = (const char*[]){"VK_LAYER_KHRONOS_validation"},
+#endif
+        .enabledExtensionCount = count,
+        .ppEnabledExtensionNames = extensions};
+    VK_ASSERT(vkCreateInstance(&info, nullptr, &instance));
+    return false;
+}
+
+void destroyVulkanInstance() {
+    vkDestroyInstance(instance, nullptr);
+    instance = VK_NULL_HANDLE;
+}
+
+#ifdef DEBUG_VULKAN
+static VKAPI_ATTR VkBool32 onVulkanDebugMessenger VKAPI_CALL(
+    VkDebugUtilsMessageSeverityFlagBitsEXT, VkDebugUtilsMessageTypeFlagsEXT,
+    const VkDebugUtilsMessengerCallbackDataEXT* data, void*) {
+    LOG_WARNING("Vulkan validation layer message: %s", data->pMessage);
+    return false;
+}
+
+static bool initDebugMessenger() {
+    VkDebugUtilsMessengerCreateInfoEXT info = {
+        .sType = VK_STRUCTURE_TYPE_DEBUG_UTILS_MESSENGER_CREATE_INFO_EXT,
+        .messageSeverity = VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT |
+                           VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT,
+        .messageType = VK_DEBUG_UTILS_MESSAGE_TYPE_GENERAL_BIT_EXT |
+                       VK_DEBUG_UTILS_MESSAGE_TYPE_VALIDATION_BIT_EXT |
+                       VK_DEBUG_UTILS_MESSAGE_TYPE_PERFORMANCE_BIT_EXT,
+        .pfnUserCallback = onVulkanDebugMessenger};
+    auto f = GET_VULKAN_FUNCTION(vkCreateDebugUtilsMessengerEXT);
+    if(f == nullptr) {
+        LOG_WARNING("Could not find debug util messenger function");
+        return false;
+    }
+    VK_ASSERT(f(instance, &info, nullptr, &debugMessenger));
+    return false;
+}
+
+static VKAPI_ATTR VkBool32 onVulkanDebugReport VKAPI_CALL(
+    VkDebugReportFlagsEXT, VkDebugReportObjectTypeEXT, uint64_t, size_t,
+    int32_t, const char* pLayerPrefix, const char* pMessage, void*) {
+    LOG_WARNING("Vulkan debug message '%s': %s", pLayerPrefix, pMessage);
+    return false;
+}
+
+static bool initDebugReportCallback() {
+    VkDebugReportCallbackCreateInfoEXT info = {
+        .sType = VK_STRUCTURE_TYPE_DEBUG_REPORT_CREATE_INFO_EXT,
+        .flags = VK_DEBUG_REPORT_WARNING_BIT_EXT |
+                 VK_DEBUG_REPORT_PERFORMANCE_WARNING_BIT_EXT |
+                 VK_DEBUG_REPORT_ERROR_BIT_EXT,
+        .pfnCallback = onVulkanDebugReport};
+    auto f = GET_VULKAN_FUNCTION(vkCreateDebugReportCallbackEXT);
+    if(f == nullptr) {
+        LOG_WARNING("Could not find debug report function");
+        return false;
+    }
+    VK_ASSERT(f(instance, &info, nullptr, &debugReportCallback));
+    return false;
+}
+
+bool initVulkanDebugging() {
+    return initDebugMessenger() || initDebugReportCallback();
+}
+
+static void destroyDebugMessenger() {
+    auto f = GET_VULKAN_FUNCTION(vkDestroyDebugUtilsMessengerEXT);
+    if(f == nullptr) {
+        LOG_WARNING("Could not find debug util messenger destroy function");
+        return;
+    }
+    f(instance, debugMessenger, nullptr);
+}
+
+static void destroyDebugReportCallback() {
+    auto f = GET_VULKAN_FUNCTION(vkDestroyDebugReportCallbackEXT);
+    if(f == nullptr) {
+        LOG_WARNING("Could not find debug report destroy function");
+        return;
+    }
+    f(instance, debugReportCallback, nullptr);
+}
+
+void destroyVulkanDebugging() {
+    if(instance != VK_NULL_HANDLE) {
+        destroyDebugMessenger();
+        destroyDebugReportCallback();
+    }
+}
+#else
+bool initVulkanDebugging() {
+    (void)getVulkanFunction;
+    return false;
+}
+
+void destroyVulkanDebugging() {
+}
+#endif
+
+u32 findVulkanQueueFamily(VkPhysicalDevice pd, VkQueueFlags flags) {
+    VkQueueFamilyProperties properties[32];
+    u32 count = ARRAY_LENGTH(properties);
+    vkGetPhysicalDeviceQueueFamilyProperties(pd, &count, properties);
+    for(u32 i = 0; i < count; i++) {
+        if((properties[i].queueFlags & flags) == flags) {
+            return i;
+        }
+    }
+    return INVALID_VULKAN_QUEUE_FAMILY;
+}
+
+static bool noPresentationSupport(
+    VkPhysicalDevice pd, VkSurfaceKHR s, u32 index) {
+    VkBool32 b = false;
+    VK_ASSERT(vkGetPhysicalDeviceSurfaceSupportKHR(pd, index, s, &b));
+    return !b;
+}
+
+u32 findVulkanSurfaceQueueFamily(VkPhysicalDevice pd, VkSurfaceKHR s) {
+    u32 count = 0;
+    vkGetPhysicalDeviceQueueFamilyProperties(pd, &count, nullptr);
+    for(u32 i = 0; i < count; i++) {
+        if(!noPresentationSupport(pd, s, i)) {
+            return i;
+        }
+    }
+    return INVALID_VULKAN_QUEUE_FAMILY;
+}
+
+bool findVulkanSurfaceFormat(
+    VkSurfaceFormatKHR* sf, VkPhysicalDevice pd, VkSurfaceKHR s,
+    VulkanSurfaceFormatSelector sfs) {
+    VkSurfaceFormatKHR formats[64];
+    u32 c = ARRAY_LENGTH(formats);
+    VK_ASSERT(vkGetPhysicalDeviceSurfaceFormatsKHR(pd, s, &c, formats));
+    int bestPoints = 0;
+    for(u32 i = 0; i < c; i++) {
+        int points = sfs(formats + i);
+        if(points > bestPoints) {
+            bestPoints = points;
+            *sf = formats[i];
+        }
+    }
+    return bestPoints == 0;
+}
+
+bool findVulkanSurfacePresentMode(
+    VkPresentModeKHR* m, VkPhysicalDevice pd, VkSurfaceKHR s,
+    VulkanSurfacePresentModeSelector spms) {
+    VkPresentModeKHR modes[64];
+    u32 c = ARRAY_LENGTH(modes);
+    VK_ASSERT(vkGetPhysicalDeviceSurfacePresentModesKHR(pd, s, &c, modes));
+    int bestPoints = 0;
+    for(u32 i = 0; i < c; i++) {
+        int points = spms(modes[i]);
+        if(points > bestPoints) {
+            bestPoints = points;
+            *m = modes[i];
+        }
+    }
+    return bestPoints == 0;
+}
+
+bool findVulkanPhysicalDevice(
+    VkPhysicalDevice* pd, VulkanPhysicalDeviceSelector s) {
+    VkPhysicalDevice devices[32];
+    u32 c = ARRAY_LENGTH(devices);
+    VK_ASSERT(vkEnumeratePhysicalDevices(instance, &c, devices));
+    int bestPoints = 0;
+    for(u32 i = 0; i < c; i++) {
+        int points = s(devices[i]);
+        if(points > bestPoints) {
+            bestPoints = points;
+            *pd = devices[i];
+        }
+    }
+    return bestPoints == 0;
+}
+
+bool hasVulkanExtension(VkPhysicalDevice pd, const char* extension) {
+    VkExtensionProperties e[1024];
+    u32 c = ARRAY_LENGTH(e);
+    VkResult r = vkEnumerateDeviceExtensionProperties(pd, nullptr, &c, e);
+    if(r != VK_SUCCESS) {
+        LOG_ERROR(
+            "Cannot get physical device extension properties: %s",
+            getVulkanResultString(r));
+        return false;
+    }
+    for(u32 i = 0; i < c; i++) {
+        if(strcmp(e[i].extensionName, extension) == 0) {
+            return true;
+        }
+    }
+    return false;
+}
+
+bool initVulkanDevice(
+    VkDevice* d, VkPhysicalDevice pd, const VulkanDeviceQueueData* data,
+    size_t n, const char** extensions, size_t nExtensions) {
+    constexpr size_t LENGTH = 32;
+    if(n >= LENGTH) {
+        LOG_ERROR("Vulkan device queue overload");
+        return true;
+    }
+    VkDeviceQueueCreateInfo deviceQueueInfo[LENGTH] = {0};
+    for(size_t i = 0; i < n; i++) {
+        VkDeviceQueueCreateInfo* qInfo = deviceQueueInfo + i;
+        qInfo->sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO;
+        qInfo->queueFamilyIndex = data[i].queueFamilyIndex;
+        qInfo->queueCount = 1;
+        qInfo->pQueuePriorities = &data[i].priority;
+    }
+    VkPhysicalDeviceFeatures deviceFeatures = {0};
+    vkGetPhysicalDeviceFeatures(pd, &deviceFeatures);
+    VkDeviceCreateInfo info = {
+        .sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO,
+        .queueCreateInfoCount = (u32)n,
+        .pQueueCreateInfos = deviceQueueInfo,
+        .enabledExtensionCount = (u32)nExtensions,
+        .ppEnabledExtensionNames = extensions,
+        .pEnabledFeatures = &deviceFeatures};
+    VK_ASSERT(vkCreateDevice(pd, &info, nullptr, d));
+    return false;
+}
+
+void destroyVulkanDevice(VkDevice d) {
+    if(instance != VK_NULL_HANDLE) {
+        vkDestroyDevice(d, nullptr);
+    }
+}
+
+bool initVulkanSurface(VkSurfaceKHR* s, GLFWwindow* w) {
+    VK_ASSERT(glfwCreateWindowSurface(instance, w, nullptr, s));
+    return false;
+}
+
+void destroyVulkanSurface(VkSurfaceKHR s) {
+    if(instance != VK_NULL_HANDLE) {
+        vkDestroySurfaceKHR(instance, s, nullptr);
+    }
+}
+
+static u32 getSwapImageCount(const VkSurfaceCapabilitiesKHR* caps) {
+    u32 c = caps->minImageCount + 1;
+    // according to VkSurfaceCapabilitiesKHR doc:
+    // maxImageCount is 0 when there is no strict limit
+    if(caps->maxImageCount != 0) {
+        return minU32(c, caps->maxImageCount);
+    }
+    return c;
+}
+
+bool initVulkanSwapchain(VkSwapchainKHR* sc, VulkanSwapchainData* d) {
+    VkSurfaceCapabilitiesKHR caps = {0};
+    VK_ASSERT(vkGetPhysicalDeviceSurfaceCapabilitiesKHR(
+        d->physicalDevice, d->surface, &caps));
+    VkSwapchainCreateInfoKHR ci = {
+        .sType = VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR,
+        .surface = d->surface,
+        .minImageCount = getSwapImageCount(&caps),
+        .imageFormat = d->surfaceFormat.format,
+        .imageColorSpace = d->surfaceFormat.colorSpace,
+        .imageExtent = d->size,
+        .imageArrayLayers = 1,
+        .imageUsage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT,
+        .imageSharingMode = d->sharingMode,
+        .queueFamilyIndexCount = d->queueFamilyIndexCount,
+        .pQueueFamilyIndices = d->queueFamilyIndices,
+        .preTransform = caps.currentTransform,
+        .compositeAlpha = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR,
+        .presentMode = d->presentMode,
+        .clipped = VK_TRUE,
+        .oldSwapchain = VK_NULL_HANDLE};
+    VK_ASSERT(vkCreateSwapchainKHR(d->device, &ci, nullptr, sc));
+    return false;
+}
+
+void destroyVulkanSwapchain(VkSwapchainKHR s, VkDevice d) {
+    if(d != VK_NULL_HANDLE) {
+        vkDestroySwapchainKHR(d, s, nullptr);
+    }
+}
+
+static bool createImageView(
+    VkImageView* view, VkDevice d, VkImage image, VkFormat format) {
+    VkImageViewCreateInfo info = {
+        .sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO,
+        .image = image,
+        .viewType = VK_IMAGE_VIEW_TYPE_2D,
+        .format = format,
+        .subresourceRange = {
+            .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT,
+            .baseMipLevel = 0,
+            .levelCount = 1,
+            .baseArrayLayer = 0,
+            .layerCount = 1}};
+    VK_ASSERT(vkCreateImageView(d, &info, nullptr, view));
+    return false;
+}
+
+bool initVulkanSwapchainImages(
+    VulkanSwapchainImages* si, VkDevice d, VkSwapchainKHR sc, VkFormat format) {
+    VK_ASSERT(vkGetSwapchainImagesKHR(d, sc, &si->amount, nullptr));
+    si->images = coreZeroAllocate(sizeof(VkImage) * si->amount);
+    si->imageViews = coreZeroAllocate(sizeof(VkImageView) * si->amount);
+    VK_ASSERT(vkGetSwapchainImagesKHR(d, sc, &si->amount, si->images));
+    for(u32 x = 0; x < si->amount; x++) {
+        if(createImageView(si->imageViews + x, d, si->images[x], format)) {
+            return true;
+        }
+    }
+    return false;
+}
+
+void destroyVulkanSwapchainImages(VulkanSwapchainImages* si, VkDevice d) {
+    if(d == VK_NULL_HANDLE || si == nullptr) {
+        return;
+    }
+    for(size_t x = 0; x < si->amount; x++) {
+        vkDestroyImageView(d, si->imageViews[x], nullptr);
+    }
+    coreFree(si->images);
+    coreFree(si->imageViews);
+    *si = (VulkanSwapchainImages){0};
+}
+
+static bool initShaderModule(FileContent* f, VkDevice d, VkShaderModule* sm) {
+    if((f->length % 4) != 0) {
+        LOG_ERROR("Shader size is not a multiple of 4");
+        return true;
+    }
+    VkShaderModuleCreateInfo info = {
+        .sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO,
+        .codeSize = f->length,
+        .pCode = (u32*)f->data};
+    VK_ASSERT(vkCreateShaderModule(d, &info, nullptr, sm));
+    return false;
+}
+
+bool initVulkanShaderModule(VkShaderModule* sm, VkDevice d, const char* path) {
+    FileContent f = {0};
+    if(readFile(&f, path)) {
+        return true;
+    }
+    bool r = initShaderModule(&f, d, sm);
+    destroyFileContent(&f);
+    return r;
+}
+
+void destroyVulkanShaderModule(VkShaderModule sm, VkDevice d) {
+    if(d != VK_NULL_HANDLE) {
+        vkDestroyShaderModule(d, sm, nullptr);
+    }
+}
+
+bool initVulkanPipelineLayout(VkPipelineLayout* pl, VkDevice d) {
+    VkPipelineLayoutCreateInfo info = {
+        .sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO,
+        .setLayoutCount = 0,
+        .pSetLayouts = nullptr,
+        .pushConstantRangeCount = 0,
+        .pPushConstantRanges = nullptr};
+    VK_ASSERT(vkCreatePipelineLayout(d, &info, nullptr, pl));
+    return false;
+}
+
+void destroyVulkanPipelineLayout(VkPipelineLayout pl, VkDevice d) {
+    if(d != VK_NULL_HANDLE) {
+        vkDestroyPipelineLayout(d, pl, nullptr);
+    }
+}
+
+bool initVulkanFramebuffers(
+    VkFramebuffer** f, VulkanSwapchainImages* si, VkDevice d, VkRenderPass rp,
+    u32 width, u32 height) {
+    *f = coreZeroAllocate(sizeof(VkFramebuffer) * si->amount);
+    for(u32 i = 0; i < si->amount; i++) {
+        VkFramebufferCreateInfo info = {
+            .sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO,
+            .renderPass = rp,
+            .attachmentCount = 1,
+            .pAttachments = si->imageViews + i,
+            .width = width,
+            .height = height,
+            .layers = 1};
+        VK_ASSERT(vkCreateFramebuffer(d, &info, nullptr, (*f) + i));
+    }
+    return false;
+}
+
+void destroyVulkanFramebuffers(VkFramebuffer** f, u32 amount, VkDevice d) {
+    if(f != nullptr) {
+        if(d != VK_NULL_HANDLE && *f != nullptr) {
+            for(u32 i = 0; i < amount; i++) {
+                vkDestroyFramebuffer(d, (*f)[i], nullptr);
+            }
+        }
+        coreFree(*f);
+        *f = nullptr;
+    }
+}
+
+bool initCommandVulkanBuffer(
+    VkCommandBuffer* cb, VkDevice d, VkCommandPool cp) {
+    VkCommandBufferAllocateInfo info = {
+        .sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO,
+        .commandPool = cp,
+        .level = VK_COMMAND_BUFFER_LEVEL_PRIMARY,
+        .commandBufferCount = 1};
+    VK_ASSERT(vkAllocateCommandBuffers(d, &info, cb));
+    return false;
+}
+
+bool initVulkanSemaphore(VkSemaphore* s, VkDevice d) {
+    VkSemaphoreCreateInfo info = {
+        .sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO};
+    VK_ASSERT(vkCreateSemaphore(d, &info, nullptr, s));
+    return false;
+}
+
+void destroyVulkanSemaphore(VkSemaphore s, VkDevice d) {
+    if(d != VK_NULL_HANDLE) {
+        vkDestroySemaphore(d, s, nullptr);
+    }
+}
+
+bool initVulkanFence(VkFence* f, VkDevice d) {
+    VkFenceCreateInfo info = {
+        .sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO,
+        .flags = VK_FENCE_CREATE_SIGNALED_BIT};
+    VK_ASSERT(vkCreateFence(d, &info, nullptr, f));
+    return false;
+}
+
+void destroyVulkanFence(VkFence f, VkDevice d) {
+    if(d != VK_NULL_HANDLE) {
+        vkDestroyFence(d, f, nullptr);
+    }
+}

+ 99 - 0
src/VulkanUtils.h

@@ -0,0 +1,99 @@
+#ifndef CORE_VULKAN_UTILS_H
+#define CORE_VULKAN_UTILS_H
+
+#define GLFW_INCLUDE_VULKAN
+#include <GLFW/glfw3.h>
+#include <core/Utility.h>
+
+const char* getVulkanResultString(VkResult r);
+
+#define VK_ASSERT(a)                                                          \
+    do {                                                                      \
+        VkResult vkResult = (a);                                              \
+        if(vkResult != VK_SUCCESS) {                                          \
+            LOG_ERROR("Vulkan error: %s\n", getVulkanResultString(vkResult)); \
+            return true;                                                      \
+        }                                                                     \
+    } while(false)
+
+bool initVulkanInstance();
+void destroyVulkanInstance();
+
+bool initVulkanDebugging();
+void destroyVulkanDebugging();
+
+#define INVALID_VULKAN_QUEUE_FAMILY ((u32) - 1)
+u32 findVulkanQueueFamily(VkPhysicalDevice pd, VkQueueFlags flags);
+u32 findVulkanSurfaceQueueFamily(VkPhysicalDevice pd, VkSurfaceKHR s);
+typedef int (*VulkanSurfaceFormatSelector)(const VkSurfaceFormatKHR* sf);
+bool findVulkanSurfaceFormat(
+    VkSurfaceFormatKHR* sf, VkPhysicalDevice pd, VkSurfaceKHR s,
+    VulkanSurfaceFormatSelector sfs);
+typedef int (*VulkanSurfacePresentModeSelector)(VkPresentModeKHR m);
+bool findVulkanSurfacePresentMode(
+    VkPresentModeKHR* m, VkPhysicalDevice pd, VkSurfaceKHR s,
+    VulkanSurfacePresentModeSelector spms);
+
+typedef int (*VulkanPhysicalDeviceSelector)(VkPhysicalDevice pd);
+bool findVulkanPhysicalDevice(
+    VkPhysicalDevice* pd, VulkanPhysicalDeviceSelector s);
+bool hasVulkanExtension(VkPhysicalDevice pd, const char* extension);
+
+typedef struct {
+    u32 queueFamilyIndex;
+    float priority;
+} VulkanDeviceQueueData;
+
+bool initVulkanDevice(
+    VkDevice* d, VkPhysicalDevice pd, const VulkanDeviceQueueData* data,
+    size_t nData, const char** extensions, size_t nExtensions);
+void destroyVulkanDevice(VkDevice d);
+
+bool initVulkanSurface(VkSurfaceKHR* s, GLFWwindow* w);
+void destroyVulkanSurface(VkSurfaceKHR s);
+
+typedef struct {
+    VkPhysicalDevice physicalDevice;
+    VkDevice device;
+    VkSurfaceKHR surface;
+    VkExtent2D size;
+    VkSurfaceFormatKHR surfaceFormat;
+    VkPresentModeKHR presentMode;
+    VkSharingMode sharingMode;
+    u32 queueFamilyIndexCount;
+    u32* queueFamilyIndices;
+} VulkanSwapchainData;
+
+bool initVulkanSwapchain(VkSwapchainKHR* sc, VulkanSwapchainData* d);
+void destroyVulkanSwapchain(VkSwapchainKHR s, VkDevice d);
+
+typedef struct {
+    u32 amount;
+    VkImage* images;
+    VkImageView* imageViews;
+} VulkanSwapchainImages;
+
+bool initVulkanSwapchainImages(
+    VulkanSwapchainImages* si, VkDevice d, VkSwapchainKHR sc, VkFormat format);
+void destroyVulkanSwapchainImages(VulkanSwapchainImages* si, VkDevice d);
+
+bool initVulkanShaderModule(VkShaderModule* sm, VkDevice d, const char* path);
+void destroyVulkanShaderModule(VkShaderModule sm, VkDevice d);
+
+bool initVulkanPipelineLayout(VkPipelineLayout* pl, VkDevice d);
+void destroyVulkanPipelineLayout(VkPipelineLayout pl, VkDevice d);
+
+bool initVulkanFramebuffers(
+    VkFramebuffer** f, VulkanSwapchainImages* si, VkDevice d, VkRenderPass rp,
+    u32 width, u32 height);
+void destroyVulkanFramebuffers(VkFramebuffer** f, u32 amount, VkDevice d);
+
+bool initCommandVulkanBuffer(VkCommandBuffer* cb, VkDevice d, VkCommandPool cp);
+
+bool initVulkanSemaphore(VkSemaphore* s, VkDevice d);
+void destroyVulkanSemaphore(VkSemaphore s, VkDevice d);
+
+bool initVulkanFence(VkFence* f, VkDevice d);
+void destroyVulkanFence(VkFence f, VkDevice d);
+
+#endif

+ 495 - 0
src/VulkanWrapper.c

@@ -0,0 +1,495 @@
+#include "core/VulkanWrapper.h"
+
+#include <core/Logger.h>
+#include <core/Utility.h>
+
+#include "GLFW.h"
+#include "VulkanUtils.h"
+
+static VkPhysicalDevice physicalDevice;
+static u32 graphicsFamily = 0;
+static u32 presentFamily = 0;
+static VkDevice device;
+static VkQueue graphicsQueue;
+static VkQueue presentQueue;
+static VkSurfaceFormatKHR surfaceFormat;
+static VkSurfaceKHR surface;
+static VkExtent2D swapchainSize;
+static VkSwapchainKHR swapchain;
+static VulkanSwapchainImages images;
+static VkShaderModule vertexShaderModule;
+static VkShaderModule fragmentShaderModule;
+static VkPipelineLayout pipelineLayout;
+static VkRenderPass renderPass;
+static VkViewport viewport;
+static VkRect2D scissor;
+static VkPipeline pipeline;
+static VkFramebuffer* framebuffers;
+static VkCommandPool commandPool;
+static constexpr size_t MAX_FRAMES = 2;
+static size_t currentFrame = 0;
+
+typedef struct {
+    VkCommandBuffer commandBuffer;
+    VkSemaphore imageAvailableSemaphore;
+    VkSemaphore renderFinishedSemaphore;
+    VkFence inFlightFence;
+} Frame;
+
+static Frame frames[MAX_FRAMES];
+
+static int getSurfaceFormatPoints(const VkSurfaceFormatKHR* sf) {
+    if(sf->format == VK_FORMAT_B8G8R8A8_UNORM &&
+       sf->colorSpace == VK_COLOR_SPACE_SRGB_NONLINEAR_KHR) {
+        return 10;
+    }
+    return 1;
+}
+
+static int getSurfacePresentModePoints(VkPresentModeKHR m) {
+    if(m == VK_PRESENT_MODE_MAILBOX_KHR) {
+        return 5;
+    } else if(m == VK_PRESENT_MODE_FIFO_KHR) {
+        return 2;
+    }
+    return 0;
+}
+
+static bool getSwapchainSize(VkExtent2D* size) {
+    VkSurfaceCapabilitiesKHR c = {0};
+    VK_ASSERT(
+        vkGetPhysicalDeviceSurfaceCapabilitiesKHR(physicalDevice, surface, &c));
+    if(c.currentExtent.width != 0xFFFF'FFFFu &&
+       c.currentExtent.height != 0xFFFF'FFFFu) {
+        *size = c.currentExtent;
+        LOG_INFO("Swapchain size: %ux%u", size->width, size->height);
+        return false;
+    }
+    int w = 0;
+    int h = 0;
+    glfwGetFramebufferSize(getWindow(), &w, &h);
+    if(w <= 0 || h <= 0) {
+        LOG_ERROR("Could not get framebuffer size");
+        return true;
+    }
+    LOG_INFO("Framebuffer size: %dx%d", w, h);
+    size->width =
+        clampU32((u32)w, c.minImageExtent.width, c.maxImageExtent.width);
+    size->height =
+        clampU32((u32)h, c.minImageExtent.height, c.maxImageExtent.height);
+    LOG_INFO("Swapchain size: %ux%u", size->width, size->height);
+    return false;
+}
+
+static bool initSwapchain() {
+    VulkanSwapchainData d = {
+        .physicalDevice = physicalDevice, .device = device, .surface = surface};
+    if(getSwapchainSize(&d.size)) {
+        LOG_ERROR("Could not retrieve any swapchain size");
+        return true;
+    }
+    swapchainSize = d.size;
+    if(findVulkanSurfaceFormat(
+           &d.surfaceFormat, physicalDevice, surface, getSurfaceFormatPoints)) {
+        LOG_ERROR("Could not find surface format");
+        return true;
+    }
+    surfaceFormat = d.surfaceFormat;
+    if(findVulkanSurfacePresentMode(
+           &d.presentMode, physicalDevice, surface,
+           getSurfacePresentModePoints)) {
+        LOG_ERROR("Could not find present mode");
+        return true;
+    }
+    u32 queueFamilyIndices[] = {graphicsFamily, presentFamily};
+    if(graphicsFamily != presentFamily) {
+        d.sharingMode = VK_SHARING_MODE_CONCURRENT;
+        d.queueFamilyIndexCount = 2;
+        d.queueFamilyIndices = queueFamilyIndices;
+    } else {
+        d.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
+    }
+    return initVulkanSwapchain(&swapchain, &d);
+}
+
+static bool initDevice() {
+    graphicsFamily =
+        findVulkanQueueFamily(physicalDevice, VK_QUEUE_GRAPHICS_BIT);
+    presentFamily = findVulkanSurfaceQueueFamily(physicalDevice, surface);
+    if(graphicsFamily == INVALID_VULKAN_QUEUE_FAMILY ||
+       presentFamily == INVALID_VULKAN_QUEUE_FAMILY) {
+        return true;
+    }
+    bool same = graphicsFamily == presentFamily;
+    VulkanDeviceQueueData data[] = {
+        {graphicsFamily, 1.0f}, {presentFamily, 1.0f}};
+    const char* extensions[] = {VK_KHR_SWAPCHAIN_EXTENSION_NAME};
+    if(initVulkanDevice(
+           &device, physicalDevice, data, same ? 1 : 2, extensions,
+           ARRAY_LENGTH(extensions))) {
+        return true;
+    }
+    vkGetDeviceQueue(device, graphicsFamily, 0, &graphicsQueue);
+    if(graphicsQueue == VK_NULL_HANDLE) {
+        LOG_ERROR("Cannot get device graphics queue");
+        return true;
+    }
+    if(same) {
+        presentQueue = graphicsQueue;
+        return false;
+    }
+    vkGetDeviceQueue(device, presentFamily, 0, &presentQueue);
+    if(presentQueue == VK_NULL_HANDLE) {
+        LOG_ERROR("Cannot get device present queue");
+        return true;
+    }
+    return false;
+}
+
+static int getDevicePoints(VkPhysicalDevice pd) {
+    int points = 0;
+    VkPhysicalDeviceProperties p;
+    vkGetPhysicalDeviceProperties(pd, &p);
+    LOG_INFO("Checking '%s'", p.deviceName);
+    switch(p.deviceType) {
+        case VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU: points += 100; break;
+        case VK_PHYSICAL_DEVICE_TYPE_INTEGRATED_GPU: points += 50; break;
+        case VK_PHYSICAL_DEVICE_TYPE_VIRTUAL_GPU: points += 20; break;
+        default: break;
+    }
+    u32 gf = findVulkanQueueFamily(pd, VK_QUEUE_GRAPHICS_BIT);
+    if(gf == INVALID_VULKAN_QUEUE_FAMILY) {
+        LOG_INFO("> ... has no graphics family");
+        points = -1;
+    }
+    u32 pf = findVulkanSurfaceQueueFamily(pd, surface);
+    if(pf == INVALID_VULKAN_QUEUE_FAMILY) {
+        LOG_INFO("> ... has no present family");
+        points = -1;
+    }
+    if(!hasVulkanExtension(pd, VK_KHR_SWAPCHAIN_EXTENSION_NAME)) {
+        LOG_INFO("> ... has no swapchain support");
+        points = -1;
+    }
+    VkSurfaceFormatKHR sf = {0};
+    if(findVulkanSurfaceFormat(&sf, pd, surface, getSurfaceFormatPoints)) {
+        LOG_INFO("> ... has no matching surface format");
+        points = -1;
+    } else {
+        points += getSurfaceFormatPoints(&sf);
+    }
+    VkPresentModeKHR m = 0;
+    if(findVulkanSurfacePresentMode(
+           &m, pd, surface, getSurfacePresentModePoints)) {
+        LOG_INFO("> ... has no matching present mode");
+        points = -1;
+    } else {
+        points += getSurfacePresentModePoints(m);
+    }
+    LOG_INFO("> Final points: %d", points);
+    return points;
+}
+
+static bool initPhysicalDevice() {
+    LOG_INFO("Searching for physical devices ...");
+    if(findVulkanPhysicalDevice(&physicalDevice, getDevicePoints)) {
+        LOG_ERROR("No matching physical device was found");
+        return true;
+    }
+    VkPhysicalDeviceProperties p;
+    vkGetPhysicalDeviceProperties(physicalDevice, &p);
+    LOG_INFO("Best Device: %s", p.deviceName);
+    return false;
+}
+
+static bool initSwapchainImages() {
+    if(initVulkanSwapchainImages(
+           &images, device, swapchain, surfaceFormat.format)) {
+        LOG_ERROR("Could not get swapchain images");
+        return true;
+    }
+    LOG_INFO("Found %u images", images.amount);
+    return false;
+}
+
+static bool initShaders() {
+    return initVulkanShaderModule(
+               &vertexShaderModule, device, "shaders/vertex.spv") ||
+           initVulkanShaderModule(
+               &fragmentShaderModule, device, "shaders/fragment.spv");
+}
+
+static bool initPipeline() {
+    VkPipelineShaderStageCreateInfo stages[2] = {
+        {.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO,
+         .stage = VK_SHADER_STAGE_VERTEX_BIT,
+         .module = vertexShaderModule,
+         .pName = "main"},
+        {.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO,
+         .stage = VK_SHADER_STAGE_FRAGMENT_BIT,
+         .module = fragmentShaderModule,
+         .pName = "main"}};
+
+    VkPipelineVertexInputStateCreateInfo vertexInputState = {
+        .sType = VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO};
+
+    VkPipelineInputAssemblyStateCreateInfo inputAssemblyState = {
+        .sType = VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO,
+        .topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST,
+        .primitiveRestartEnable = false};
+
+    viewport = (VkViewport){.x = 0.0f,
+                            .y = 0.0f,
+                            .width = (float)swapchainSize.width,
+                            .height = (float)swapchainSize.height,
+                            .minDepth = 0.0f,
+                            .maxDepth = 1.0f};
+    scissor = (VkRect2D){.offset = {0, 0}, .extent = swapchainSize};
+    VkPipelineViewportStateCreateInfo viewportState = {
+        .sType = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO,
+        .viewportCount = 1,
+        .pViewports = &viewport,
+        .scissorCount = 1,
+        .pScissors = &scissor};
+
+    VkPipelineRasterizationStateCreateInfo rasterizationState = {
+        .sType = VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO,
+        .depthClampEnable = false,
+        .rasterizerDiscardEnable = false,
+        .polygonMode = VK_POLYGON_MODE_FILL,
+        .cullMode = VK_CULL_MODE_BACK_BIT,
+        .frontFace = VK_FRONT_FACE_CLOCKWISE,
+        .depthBiasEnable = false,
+        .depthBiasConstantFactor = 0.0f,
+        .depthBiasClamp = 0.0f,
+        .depthBiasSlopeFactor = 0.0f,
+        .lineWidth = 1.0f};
+
+    VkPipelineMultisampleStateCreateInfo multisampleState = {
+        .sType = VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO,
+        .rasterizationSamples = VK_SAMPLE_COUNT_1_BIT,
+        .sampleShadingEnable = false,
+        .minSampleShading = 1.0f,
+        .pSampleMask = nullptr,
+        .alphaToCoverageEnable = false,
+        .alphaToOneEnable = false};
+
+    VkPipelineColorBlendAttachmentState colorBlendAttachmentState = {
+        .blendEnable = false,
+        .srcColorBlendFactor = VK_BLEND_FACTOR_ONE,
+        .dstColorBlendFactor = VK_BLEND_FACTOR_ZERO,
+        .colorBlendOp = VK_BLEND_OP_ADD,
+        .srcAlphaBlendFactor = VK_BLEND_FACTOR_ONE,
+        .dstAlphaBlendFactor = VK_BLEND_FACTOR_ZERO,
+        .alphaBlendOp = VK_BLEND_OP_ADD,
+        .colorWriteMask = VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT |
+                          VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT};
+
+    VkPipelineColorBlendStateCreateInfo colorBlendState = {
+        .sType = VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO,
+        .logicOpEnable = false,
+        .logicOp = VK_LOGIC_OP_COPY,
+        .attachmentCount = 1,
+        .pAttachments = &colorBlendAttachmentState,
+        .blendConstants = {0.0f, 0.0f, 0.0f, 0.0f}};
+
+    VkDynamicState dynamicStates[] = {
+        VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR};
+
+    VkPipelineDynamicStateCreateInfo dynamicState = {
+        .sType = VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO,
+        .dynamicStateCount = ARRAY_LENGTH(dynamicStates),
+        .pDynamicStates = dynamicStates};
+
+    VkGraphicsPipelineCreateInfo info = {
+        .sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO,
+        .stageCount = 2,
+        .pStages = stages,
+        .pVertexInputState = &vertexInputState,
+        .pInputAssemblyState = &inputAssemblyState,
+        .pTessellationState = nullptr,
+        .pViewportState = &viewportState,
+        .pRasterizationState = &rasterizationState,
+        .pMultisampleState = &multisampleState,
+        .pDepthStencilState = nullptr,
+        .pColorBlendState = &colorBlendState,
+        .pDynamicState = &dynamicState,
+        .layout = pipelineLayout,
+        .renderPass = renderPass,
+        .subpass = 0,
+        .basePipelineHandle = VK_NULL_HANDLE,
+        .basePipelineIndex = -1};
+
+    VK_ASSERT(vkCreateGraphicsPipelines(
+        device, VK_NULL_HANDLE, 1, &info, nullptr, &pipeline));
+    return false;
+}
+
+static bool initRenderPass() {
+    VkAttachmentDescription c = {
+        .format = surfaceFormat.format,
+        .samples = VK_SAMPLE_COUNT_1_BIT,
+        .loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR,
+        .storeOp = VK_ATTACHMENT_STORE_OP_STORE,
+        .stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE,
+        .stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE,
+        .initialLayout = VK_IMAGE_LAYOUT_UNDEFINED,
+        .finalLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR};
+    VkAttachmentReference ca = {
+        .attachment = 0, .layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL};
+    VkSubpassDescription subpass = {
+        .pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS,
+        .colorAttachmentCount = 1,
+        .pColorAttachments = &ca};
+    VkSubpassDependency dependency = {
+        .srcSubpass = VK_SUBPASS_EXTERNAL,
+        .dstSubpass = 0,
+        .srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT,
+        .dstStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT,
+        .srcAccessMask = 0,
+        .dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT,
+    };
+    VkRenderPassCreateInfo info = {
+        .sType = VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO,
+        .attachmentCount = 1,
+        .pAttachments = &c,
+        .subpassCount = 1,
+        .pSubpasses = &subpass,
+        .dependencyCount = 1,
+        .pDependencies = &dependency};
+    VK_ASSERT(vkCreateRenderPass(device, &info, nullptr, &renderPass));
+    return false;
+}
+
+static bool initCommandPool() {
+    VkCommandPoolCreateInfo info = {
+        .sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO,
+        .flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT,
+        .queueFamilyIndex = graphicsFamily};
+    VK_ASSERT(vkCreateCommandPool(device, &info, nullptr, &commandPool));
+    return false;
+}
+
+static bool fillCommandBuffer(VkCommandBuffer cb, u32 index) {
+    VkCommandBufferBeginInfo info = {
+        .sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO};
+    VK_ASSERT(vkBeginCommandBuffer(cb, &info));
+    VkClearValue v = {.color = {.float32 = {0.0f, 0.5f, 0.0f, 1.0f}}};
+    VkRenderPassBeginInfo rInfo = {
+        .sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO,
+        .renderPass = renderPass,
+        .framebuffer = framebuffers[index],
+        .renderArea = {.offset = {0, 0}, .extent = swapchainSize},
+        .clearValueCount = 1,
+        .pClearValues = &v};
+    vkCmdBeginRenderPass(cb, &rInfo, VK_SUBPASS_CONTENTS_INLINE);
+    vkCmdBindPipeline(cb, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline);
+    vkCmdSetViewport(cb, 0, 1, &viewport);
+    vkCmdSetScissor(cb, 0, 1, &scissor);
+    vkCmdDraw(cb, 3, 1, 0, 0);
+    vkCmdEndRenderPass(cb);
+    VK_ASSERT(vkEndCommandBuffer(cb));
+    return false;
+}
+
+static bool initFrames() {
+    for(size_t i = 0; i < MAX_FRAMES; i++) {
+        if(initCommandVulkanBuffer(
+               &frames[i].commandBuffer, device, commandPool) ||
+           initVulkanSemaphore(&frames[i].imageAvailableSemaphore, device) ||
+           initVulkanSemaphore(&frames[i].renderFinishedSemaphore, device) ||
+           initVulkanFence(&frames[i].inFlightFence, device)) {
+            return true;
+        }
+    }
+    return false;
+}
+
+bool initVulkan() {
+    return initVulkanInstance() || initVulkanDebugging() ||
+           initVulkanSurface(&surface, getWindow()) || initPhysicalDevice() ||
+           initDevice() || initSwapchain() || initSwapchainImages() ||
+           initShaders() || initVulkanPipelineLayout(&pipelineLayout, device) ||
+           initRenderPass() || initPipeline() ||
+           initVulkanFramebuffers(
+               &framebuffers, &images, device, renderPass, swapchainSize.width,
+               swapchainSize.height) ||
+           initCommandPool() || initFrames();
+}
+
+static bool shouldWait = false;
+
+static bool render() {
+    if(shouldWait) {
+        return false;
+    }
+    Frame* f = frames + currentFrame;
+    VK_ASSERT(vkWaitForFences(device, 1, &f->inFlightFence, true, UINT64_MAX));
+    VK_ASSERT(vkResetFences(device, 1, &f->inFlightFence));
+
+    uint32_t imageIndex = 0;
+    VK_ASSERT(vkAcquireNextImageKHR(
+        device, swapchain, UINT64_MAX, f->imageAvailableSemaphore,
+        VK_NULL_HANDLE, &imageIndex));
+
+    vkResetCommandBuffer(f->commandBuffer, 0);
+    fillCommandBuffer(f->commandBuffer, imageIndex);
+
+    VkSemaphore waitSemaphores[] = {f->imageAvailableSemaphore};
+    VkPipelineStageFlags waitStages[] = {
+        VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT};
+    VkSemaphore signalSemaphores[] = {f->renderFinishedSemaphore};
+    VkSubmitInfo info = {
+        .sType = VK_STRUCTURE_TYPE_SUBMIT_INFO,
+        .waitSemaphoreCount = 1,
+        .pWaitSemaphores = waitSemaphores,
+        .pWaitDstStageMask = waitStages,
+        .commandBufferCount = 1,
+        .pCommandBuffers = &f->commandBuffer,
+        .signalSemaphoreCount = 1,
+        .pSignalSemaphores = signalSemaphores};
+    VK_ASSERT(vkQueueSubmit(graphicsQueue, 1, &info, f->inFlightFence));
+
+    VkPresentInfoKHR presentInfo = {
+        .sType = VK_STRUCTURE_TYPE_PRESENT_INFO_KHR,
+        .waitSemaphoreCount = 1,
+        .pWaitSemaphores = signalSemaphores,
+        .swapchainCount = 1,
+        .pSwapchains = &swapchain,
+        .pImageIndices = &imageIndex};
+
+    VK_ASSERT(vkQueuePresentKHR(presentQueue, &presentInfo));
+    currentFrame = (currentFrame + 1) % MAX_FRAMES;
+    return false;
+}
+
+void renderVulkan() {
+    if(render()) {
+        shouldWait = true;
+    }
+}
+
+void destroyVulkan(void) {
+    if(device != VK_NULL_HANDLE) {
+        vkDeviceWaitIdle(device);
+        for(size_t i = 0; i < MAX_FRAMES; i++) {
+            Frame* f = frames + i;
+            destroyVulkanFence(f->inFlightFence, device);
+            destroyVulkanSemaphore(f->imageAvailableSemaphore, device);
+            destroyVulkanSemaphore(f->renderFinishedSemaphore, device);
+        }
+        vkDestroyCommandPool(device, commandPool, nullptr);
+        destroyVulkanFramebuffers(&framebuffers, images.amount, device);
+        vkDestroyPipeline(device, pipeline, nullptr);
+        vkDestroyRenderPass(device, renderPass, nullptr);
+    }
+    destroyVulkanPipelineLayout(pipelineLayout, device);
+    destroyVulkanShaderModule(vertexShaderModule, device);
+    destroyVulkanShaderModule(fragmentShaderModule, device);
+    destroyVulkanSwapchainImages(&images, device);
+    destroyVulkanSwapchain(swapchain, device);
+    destroyVulkanDevice(device);
+    destroyVulkanSurface(surface);
+    destroyVulkanDebugging();
+    destroyVulkanInstance();
+}

+ 505 - 0
src/WindowManager.c

@@ -0,0 +1,505 @@
+#include "core/WindowManager.h"
+
+#include <core/Logger.h>
+#include <core/Utility.h>
+#include <limits.h>
+#include <stdio.h>
+#include <uchar.h>
+
+#include "GLFW.h"
+#include "core/VulkanWrapper.h"
+
+static bool dummyWindowRunHandler(void*) {
+    return !shouldWindowClose();
+}
+
+static void dummyWindowTickHandler(void*) {
+}
+
+static void dummyWindowRenderHandler(void*, float) {
+}
+
+static GLFWwindow* window = nullptr;
+static IntVector2 size = {0};
+static bool sizeChanged = false;
+static WindowRunHandler runHandler = dummyWindowRunHandler;
+static WindowTickHandler tickHandler = dummyWindowTickHandler;
+static WindowRenderHandler renderHandler = dummyWindowRenderHandler;
+static void* runHandlerData = nullptr;
+static void* tickHandlerData = nullptr;
+static void* renderHandlerData = nullptr;
+static i64 nanosPerTick = 1;
+
+#define CLOCK_SIZE 16
+
+typedef struct {
+    i64 last;
+    i64 sum;
+    i64 values[CLOCK_SIZE];
+    size_t index;
+} Clock;
+
+static Clock fps = {0};
+static Clock tps = {0};
+
+#define INPUT_SIZE 2048
+static char input[INPUT_SIZE] = {0};
+static size_t inputLength = 0;
+static size_t inputCursor = 0;
+static size_t inputLimit = INPUT_SIZE - 1;
+static bool inputActive = false;
+
+typedef struct {
+    char name[31];
+    bool released;
+    int key;
+    int downTime;
+    int upEvents;
+    int downEvents;
+    int controllerUp;
+    int controllerDown;
+} ButtonData;
+
+#define BUTTONS 100
+static ButtonData buttons[BUTTONS] = {{.name = "unknown"}};
+static size_t buttonIndex = 1;
+#define KEYS (GLFW_KEY_LAST + 1)
+static Button keyToButton[KEYS] = {0};
+#define GAMEPAD_BUTTONS (GLFW_GAMEPAD_BUTTON_LAST + 1)
+static Button gamepadToButton[GAMEPAD_BUTTONS] = {0};
+#define MOUSE_BUTTONS (GLFW_MOUSE_BUTTON_LAST + 1)
+static Button mouseToButton[MOUSE_BUTTONS] = {0};
+static Vector2 lastMousePosition = {0};
+static Vector2 mousePosition = {0};
+static int activeController = -1;
+static GLFWgamepadstate lastControllerState = {0};
+
+static void onButton(Button* map, int n, int key, int action) {
+    if(key < 0 || key >= n) {
+        return;
+    }
+    Button b = map[key];
+    if(b == 0 || b >= buttonIndex) {
+        return;
+    }
+    if(action == GLFW_RELEASE) {
+        buttons[b].upEvents++;
+    } else if(action == GLFW_PRESS) {
+        buttons[b].downEvents++;
+    }
+}
+
+static bool isCharSequence(size_t i) {
+    return (input[i] & 0xC0) == 0x80;
+}
+
+static void handleInputKey(int key, int action) {
+    if(action == GLFW_RELEASE) {
+        return;
+    }
+    switch(key) {
+        case GLFW_KEY_BACKSPACE:
+            if(inputLength >= inputCursor && inputCursor > 0) {
+                size_t w = 1;
+                while(inputCursor - w > 0 && isCharSequence(inputCursor - w)) {
+                    w++;
+                }
+                for(size_t i = inputCursor - w; i < inputLength; i++) {
+                    input[i] = input[i + w];
+                }
+                inputLength -= w;
+                inputCursor -= w;
+            }
+            break;
+        case GLFW_KEY_LEFT: {
+            inputCursor -= inputCursor > 0;
+            while(inputCursor > 0 && isCharSequence(inputCursor)) {
+                inputCursor--;
+            }
+            break;
+        }
+        case GLFW_KEY_RIGHT: {
+            inputCursor += inputCursor < inputLength;
+            while(inputCursor < inputLength && isCharSequence(inputCursor)) {
+                inputCursor++;
+            }
+            break;
+        }
+    }
+}
+
+static void onKey(GLFWwindow*, int key, int scancode, int action, int mods) {
+    (void)scancode;
+    (void)mods;
+    if(inputActive) {
+        handleInputKey(key, action);
+    }
+    onButton(keyToButton, KEYS, key, action);
+}
+
+static void addUnicode(u32 c) {
+    char buffer[MB_LEN_MAX + 1] = {0};
+    mbstate_t state = {0};
+    size_t w = c32rtomb(buffer, c, &state);
+    if(w >= MB_LEN_MAX || inputLength + w >= inputLimit) {
+        return;
+    }
+    if(inputLength > 0) {
+        for(size_t i = inputLength - 1; i >= inputCursor; i--) {
+            swap(input + (i + w), input + i);
+        }
+    }
+    memcpy(input + inputCursor, buffer, w);
+    inputLength += w;
+    inputCursor += w;
+}
+
+static void onChar(GLFWwindow*, u32 codepoint) {
+    if(inputActive) {
+        addUnicode(codepoint);
+    }
+}
+
+static void onResize(GLFWwindow*, int width, int height) {
+    sizeChanged = true;
+    size.data[0] = width;
+    size.data[1] = height;
+}
+
+static void onMouse(GLFWwindow*, int button, int action, int mods) {
+    (void)mods;
+    onButton(mouseToButton, MOUSE_BUTTONS, button, action);
+}
+
+static void onMouseMove(GLFWwindow*, double x, double y) {
+    mousePosition.data[0] = (float)x;
+    mousePosition.data[1] = (float)y;
+}
+
+static bool openWindowI(const WindowOptions* o) {
+    if(!glfwInit()) {
+        LOG_ERROR("could not initialize GLFW");
+        return true;
+    }
+
+    glfwDefaultWindowHints();
+    glfwWindowHint(GLFW_VISIBLE, false);
+    glfwWindowHint(GLFW_RESIZABLE, true);
+    glfwWindowHint(GLFW_DECORATED, !o->fullscreen);
+    glfwWindowHint(GLFW_DOUBLEBUFFER, true);
+    glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API);
+
+    GLFWmonitor* m = o->fullscreen ? glfwGetPrimaryMonitor() : nullptr;
+    window =
+        glfwCreateWindow(o->size.data[0], o->size.data[1], o->name, m, nullptr);
+    if(window == nullptr) {
+        LOG_ERROR("could not create window");
+        return true;
+    }
+    size = o->size;
+    glfwSetKeyCallback(window, onKey);
+    glfwSetCharCallback(window, onChar);
+    glfwSetFramebufferSizeCallback(window, onResize);
+    glfwSetMouseButtonCallback(window, onMouse);
+    glfwSetCursorPosCallback(window, onMouseMove);
+    return initVulkan();
+}
+
+bool openWindow(const WindowOptions* o) {
+    if(openWindowI(o)) {
+        closeWindow();
+        return true;
+    }
+    return false;
+}
+
+void closeWindow(void) {
+    if(window != nullptr) {
+        destroyVulkan();
+        glfwDestroyWindow(window);
+        window = nullptr;
+    }
+    glfwTerminate();
+}
+
+void showWindow(void) {
+    glfwShowWindow(window);
+}
+
+void trapWindowCursor(void) {
+    glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_DISABLED);
+}
+
+void freeWindowCursor(void) {
+    glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_NORMAL);
+}
+
+bool isWindowCursorTrapped(void) {
+    return glfwGetInputMode(window, GLFW_CURSOR) == GLFW_CURSOR_DISABLED;
+}
+
+const IntVector2* getWindowSize(void) {
+    return &size;
+}
+
+bool hasWindowSizeChanged(void) {
+    return sizeChanged;
+}
+
+bool shouldWindowClose(void) {
+    return glfwWindowShouldClose(window);
+}
+
+void setWindowRunHandler(WindowRunHandler wr, void* data) {
+    runHandlerData = data;
+    runHandler = wr == nullptr ? dummyWindowRunHandler : wr;
+}
+
+void setWindowTickHandler(WindowTickHandler t, void* data) {
+    tickHandlerData = data;
+    tickHandler = t == nullptr ? dummyWindowTickHandler : t;
+}
+
+void setWindowRenderHandler(WindowRenderHandler r, void* data) {
+    renderHandlerData = data;
+    renderHandler = r == nullptr ? dummyWindowRenderHandler : r;
+}
+
+void setWindowNanosPerTick(i64 nanos) {
+    nanosPerTick = nanos <= 0 ? 1 : nanos;
+}
+
+static i64 updateClock(Clock* c) {
+    i64 nanos = getNanos();
+    if(nanos < 0) {
+        LOG_WARNING("Cannot get nanos, using default");
+        nanos = 10'000'000 + c->last;
+    }
+    i64 diff = nanos - c->last;
+    c->last = nanos;
+    c->sum -= c->values[c->index];
+    c->values[c->index] = diff;
+    c->sum += diff;
+    c->index = (c->index + 1) % CLOCK_SIZE;
+    return diff;
+}
+
+static bool searchForGamepad() {
+    if(activeController != -1) {
+        return true;
+    }
+    for(int i = GLFW_JOYSTICK_1; i <= GLFW_JOYSTICK_LAST; i++) {
+        if(glfwJoystickIsGamepad(i)) {
+            activeController = i;
+            return true;
+        }
+    }
+    return false;
+}
+
+static void checkGamepad() {
+    GLFWgamepadstate state;
+    if(!glfwGetGamepadState(activeController, &state)) {
+        activeController = -1;
+        return;
+    }
+    for(int i = 0; i <= GLFW_GAMEPAD_BUTTON_LAST; i++) {
+        Button b = gamepadToButton[i];
+        if(b == 0 || b >= buttonIndex) {
+            return;
+        }
+        if(!lastControllerState.buttons[i] && state.buttons[i]) {
+            buttons[b].controllerDown++;
+        } else if(lastControllerState.buttons[i] && !state.buttons[i]) {
+            buttons[b].controllerUp++;
+        }
+    }
+    lastControllerState = state;
+}
+
+static void endFrame(void) {
+    sizeChanged = false;
+    glfwPollEvents();
+    if(searchForGamepad()) {
+        checkGamepad();
+    }
+}
+
+static void tickButton(ButtonData* b) {
+    bool down = (b->downEvents > 0) || (b->controllerDown > 0);
+    bool up = (b->upEvents == b->downEvents) &&
+              (b->controllerUp == b->controllerDown);
+    if(b->released) {
+        b->downTime = 0;
+    }
+    b->downTime += down;
+    b->released = down && up;
+
+    b->downEvents -= b->upEvents;
+    b->upEvents = 0;
+
+    b->controllerDown -= b->controllerUp;
+    b->controllerUp = 0;
+}
+
+static void tick() {
+    updateClock(&tps);
+    ButtonData* b = buttons;
+    for(size_t i = 0; i < buttonIndex; i++) {
+        tickButton(b++);
+    }
+    tickHandler(tickHandlerData);
+    lastMousePosition = mousePosition;
+}
+
+void runWindow(void) {
+    searchForGamepad(); // this is slow the first time
+    tps.last = getNanos();
+    fps.last = getNanos();
+    i64 lag = 0;
+    while(runHandler(runHandlerData)) {
+        lag += updateClock(&fps);
+        while(lag >= nanosPerTick) {
+            lag -= nanosPerTick;
+            tick();
+        }
+        renderVulkan();
+        renderHandler(renderHandlerData, (float)lag / (float)nanosPerTick);
+        endFrame();
+    }
+}
+
+float getWindowTicksPerSecond(void) {
+    return (1'000'000'000.0f * CLOCK_SIZE) / (float)tps.sum;
+}
+
+float getWindowFramesPerSecond(void) {
+    return (1'000'000'000.0f * CLOCK_SIZE) / (float)fps.sum;
+}
+
+void setInputLimit(size_t limit) {
+    inputLimit = limit >= INPUT_SIZE ? INPUT_SIZE - 1 : limit;
+    if(inputLength > inputLimit) {
+        inputLength = inputLimit;
+    }
+    if(inputCursor > inputLimit) {
+        inputCursor = inputLimit;
+    }
+}
+
+void resetInput() {
+    inputLength = 0;
+    inputCursor = 0;
+}
+
+void enableInput() {
+    inputActive = true;
+}
+
+void disableInput() {
+    inputActive = false;
+}
+
+bool isInputEnabled() {
+    return inputActive;
+}
+
+void fillInput(const char* s) {
+    resetInput();
+    snprintf(input, inputLength, "%s", s);
+}
+
+size_t getInputCursor() {
+    return inputCursor;
+}
+
+void setInputCursor(size_t index) {
+    if(index > inputLength) {
+        inputCursor = inputLength;
+    } else {
+        inputCursor = index;
+    }
+}
+
+const char* getInputString(void) {
+    return input;
+}
+
+Button addButton(const char* name) {
+    if(buttonIndex >= BUTTONS) {
+        return 0;
+    }
+    Button b = buttonIndex++;
+    snprintf(buttons[b].name, sizeof(buttons[b].name), "%s", name);
+    return b;
+}
+
+void bindKeyToButton(Button b, int key) {
+    if(key >= 0 && key < KEYS) {
+        keyToButton[key] = b;
+    }
+}
+
+void bindGamepadToButton(Button b, int gamepadButton) {
+    if(gamepadButton >= 0 && gamepadButton < GAMEPAD_BUTTONS) {
+        gamepadToButton[gamepadButton] = b;
+    }
+}
+
+void bindMouseToButton(Button b, int mouseButton) {
+    if(mouseButton >= 0 && mouseButton < MOUSE_BUTTONS) {
+        mouseToButton[mouseButton] = b;
+    }
+}
+
+Vector2 getLastMousePosition(void) {
+    return lastMousePosition;
+}
+
+Vector2 getMousePosition(void) {
+    return mousePosition;
+}
+
+Vector2 getLeftGamepadAxis(void) {
+    return (Vector2){
+        {lastControllerState.axes[GLFW_GAMEPAD_AXIS_LEFT_X],
+         lastControllerState.axes[GLFW_GAMEPAD_AXIS_LEFT_Y]}};
+}
+
+Vector2 getRightGamepadAxis(void) {
+    return (Vector2){
+        {lastControllerState.axes[GLFW_GAMEPAD_AXIS_RIGHT_X],
+         lastControllerState.axes[GLFW_GAMEPAD_AXIS_RIGHT_Y]}};
+}
+
+float getLeftGamepadTrigger(void) {
+    return lastControllerState.axes[GLFW_GAMEPAD_AXIS_LEFT_TRIGGER];
+}
+
+float getRightGamepadTrigger(void) {
+    return lastControllerState.axes[GLFW_GAMEPAD_AXIS_RIGHT_TRIGGER];
+}
+
+static const ButtonData* getButton(Button id) {
+    return buttons + (id >= buttonIndex ? 0 : id);
+}
+
+bool isButtonDown(Button b) {
+    return getButton(b)->downTime > 0;
+}
+
+int getButtonDownTime(Button b) {
+    return getButton(b)->downTime;
+}
+
+bool wasButtonReleased(Button b) {
+    return getButton(b)->released;
+}
+
+const char* getButtonName(Button b) {
+    return getButton(b)->name;
+}
+
+GLFWwindow* getWindow(void) {
+    return window;
+}

+ 28 - 0
test/Main.c

@@ -0,0 +1,28 @@
+#include <core/Logger.h>
+#include <core/Utility.h>
+#include <locale.h>
+#include <stdio.h>
+#include <string.h>
+
+#include "Tests.h"
+
+int main(int argAmount, char** args) {
+    if(argAmount >= 2 && strcmp(args[1], "help") == 0) {
+        // puts("test");
+        puts("window");
+        return 0;
+    }
+    setlocale(LC_ALL, "en_US.utf8");
+    if(argAmount < 2) {
+        LOG_ERROR("missing mode");
+        return 0;
+    } else if(strcmp("test", args[1]) == 0) {
+        testImageReader("test/resources");
+        testNetwork();
+    } else if(strcmp("window", args[1]) == 0) {
+        testWindow();
+    }
+    finalizeTests();
+    printMemoryReport();
+    return 0;
+}

+ 10 - 0
test/Tests.h

@@ -0,0 +1,10 @@
+#ifndef CORE_TESTS_H
+#define CORE_TESTS_H
+
+#include <core/Test.h>
+
+void testImageReader(const char* path);
+void testNetwork(void);
+void testWindow(void);
+
+#endif

+ 60 - 0
test/modules/ImageTests.c

@@ -0,0 +1,60 @@
+#include <core/Logger.h>
+#include <stdio.h>
+
+#include "../Tests.h"
+#include "core/Image.h"
+
+static void testReadPNG8(const char* path, const char* name, int width,
+                         int height, int channels) {
+    char fullPath[512];
+    snprintf(fullPath, sizeof(fullPath), "%s/%s.png", path, name);
+    Image8 image;
+    TEST_BOOL(width == 0, initImage8(&image, fullPath));
+    if(width != 0) {
+        TEST_NOT_NULL(image.data);
+    }
+    TEST_INT(width, image.width);
+    TEST_INT(height, image.height);
+    TEST_INT(channels, image.channels);
+    destroyImage8(&image);
+}
+
+static void testReadPNG16(const char* path, const char* name, int width,
+                          int height, int channels) {
+    char fullPath[512];
+    snprintf(fullPath, sizeof(fullPath), "%s/%s.png", path, name);
+    Image16 image;
+    TEST_BOOL(width == 0, initImage16(&image, fullPath));
+    if(width != 0) {
+        TEST_NOT_NULL(image.data);
+    }
+    TEST_INT(width, image.width);
+    TEST_INT(height, image.height);
+    TEST_INT(channels, image.channels);
+    destroyImage16(&image);
+}
+
+void testImageReader(const char* path) {
+    testReadPNG8(path, "rgb8", 32, 64, 3);
+    testReadPNG8(path, "rgb16", 32, 64, 3);
+    testReadPNG8(path, "rgba8", 32, 64, 4);
+    testReadPNG8(path, "rgba16", 32, 64, 4);
+    testReadPNG8(path, "gray8", 32, 64, 1);
+    testReadPNG8(path, "gray16", 32, 64, 1);
+    testReadPNG8(path, "graya8", 32, 64, 2);
+    testReadPNG8(path, "graya16", 32, 64, 2);
+    logLevel = LOG_NONE;
+    testReadPNG8(path, "nope", 0, 0, 0);
+    logLevel = LOG_DEBUG;
+    testReadPNG16(path, "rgb8", 32, 64, 3);
+    testReadPNG16(path, "rgb16", 32, 64, 3);
+    testReadPNG16(path, "rgba8", 32, 64, 4);
+    testReadPNG16(path, "rgba16", 32, 64, 4);
+    testReadPNG16(path, "gray8", 32, 64, 1);
+    testReadPNG16(path, "gray16", 32, 64, 1);
+    testReadPNG16(path, "graya8", 32, 64, 2);
+    testReadPNG16(path, "graya16", 32, 64, 2);
+    logLevel = LOG_NONE;
+    testReadPNG16(path, "nope", 0, 0, 0);
+    logLevel = LOG_DEBUG;
+}

+ 503 - 0
test/modules/NetworkTests.c

@@ -0,0 +1,503 @@
+#include <core/Logger.h>
+#include <core/Utility.h>
+#include <string.h>
+
+#include "../Tests.h"
+#include "../src/ErrorSimulator.h"
+#include "core/Network.h"
+
+#define TEST_READ(Type, type, value)                                           \
+    {                                                                          \
+        type u;                                                                \
+        TEST_FALSE(readInPacket##Type(&in, &u));                               \
+        TEST_TRUE(u == value);                                                 \
+    }
+
+static void testWriteRead(void) {
+    OutPacket out;
+    initOutPacket(&out);
+    writeOutPacketU8(&out, 200);
+    writeOutPacketU16(&out, 6656);
+    writeOutPacketU32(&out, 348923689);
+    writeOutPacketI8(&out, 90);
+    writeOutPacketI8(&out, -35);
+    writeOutPacketI16(&out, 843);
+    writeOutPacketI16(&out, -8961);
+    writeOutPacketI32(&out, 100430199);
+    writeOutPacketI32(&out, -534534);
+    writeOutPacketFloat(&out, 64564.5346f);
+    const char s[] = "This is Great";
+    writeOutPacketString(&out, s);
+
+    InPacket in;
+    initInPacket(&in, out.data.buffer, out.data.size);
+    TEST_READ(U8, u8, 200)
+    TEST_READ(U16, u16, 6656)
+    TEST_READ(U32, u32, 348923689)
+    TEST_READ(I8, i8, 90)
+    TEST_READ(I8, i8, -35)
+    TEST_READ(I16, i16, 843)
+    TEST_READ(I16, i16, -8961)
+    TEST_READ(I32, i32, 100430199)
+    TEST_READ(I32, i32, -534534)
+    float f;
+    TEST_FALSE(readInPacketFloat(&in, &f));
+    TEST_FLOAT(64564.5346f, f, 0.00001f);
+    char buffer[256];
+    size_t n = readInPacketString(&in, buffer, sizeof(buffer));
+    TEST_SIZE(14, n);
+    TEST_STRING(s, buffer);
+
+    TEST_TRUE(readInPacketU8(&in, &(u8){0}));
+    TEST_TRUE(readInPacketU16(&in, &(u16){0}));
+    TEST_TRUE(readInPacketU32(&in, &(u32){0}));
+    TEST_TRUE(readInPacketI8(&in, &(i8){0}));
+    TEST_TRUE(readInPacketI16(&in, &(i16){0}));
+    TEST_TRUE(readInPacketI32(&in, &(i32){0}));
+    TEST_TRUE(readInPacketFloat(&in, &(float){0}));
+    TEST_SIZE(0, readInPacketString(&in, nullptr, 0));
+    TEST_SIZE(0, readInPacketString(&in, buffer, sizeof(buffer)));
+
+    destroyOutPacket(&out);
+}
+
+static void testTooShortBuffer(void) {
+    OutPacket out;
+    initOutPacket(&out);
+    writeOutPacketString(&out, "This is Great");
+    writeOutPacketString(&out, "Well hoho");
+
+    InPacket in;
+    initInPacket(&in, out.data.buffer, out.data.size);
+    char buffer[8];
+    size_t n = readInPacketString(&in, buffer, sizeof(buffer));
+    TEST_SIZE(14, n);
+    TEST_STRING("This is", buffer);
+    char buffer2[6];
+    size_t n2 = readInPacketString(&in, buffer2, sizeof(buffer2));
+    TEST_SIZE(10, n2);
+    TEST_STRING("Well ", buffer2);
+
+    destroyOutPacket(&out);
+}
+
+typedef struct {
+    u8 a;
+    u8 b;
+} PacketTest;
+
+static void testBinaryData(void) {
+    OutPacket out;
+    initOutPacket(&out);
+    PacketTest data = {56, 3};
+    writeOutPacket(&out, &data, sizeof(data));
+
+    InPacket in;
+    initInPacket(&in, out.data.buffer, out.data.size);
+
+    PacketTest inData;
+    TEST_FALSE(readInPacket(&in, &inData, sizeof(inData)));
+    TEST_TRUE(memcmp(&inData, &data, sizeof(inData)) == 0);
+
+    destroyOutPacket(&out);
+}
+
+static void testShortString(void) {
+    OutPacket out;
+    initOutPacket(&out);
+    writeOutPacketU16(&out, 200);
+    writeOutPacketU16(&out, 65535);
+
+    InPacket in;
+    initInPacket(&in, out.data.buffer, out.data.size);
+
+    char buffer[256];
+    size_t n = readInPacketString(&in, buffer, sizeof(buffer));
+    TEST_SIZE(200, n);
+    TEST_STRING("", buffer);
+
+    destroyOutPacket(&out);
+}
+
+static void tickClientN(int ticks) {
+    for(int i = 0; i < ticks; i++) {
+        tickClient();
+    }
+}
+
+static void tick(int ticks) {
+    for(int i = 0; i < ticks; i++) {
+        tickClient();
+        tickServer();
+    }
+}
+
+static bool clientConnected = false;
+static bool clientDisconnected = false;
+static bool clientPackage = false;
+static int packageCounter = 0;
+static bool serverConnected = false;
+static bool serverDisconnect = false;
+static u8 data1 = 0;
+static u16 data2 = 0;
+static u32 data3 = 0;
+static i8 data4 = 0;
+static i16 data5 = 0;
+static i32 data6 = 0;
+static i8 data7 = 0;
+static i16 data8 = 0;
+static i32 data9 = 0;
+static char data10[20];
+static float data11 = 0.0f;
+
+static void onServerConnect(Client) {
+    serverConnected = true;
+}
+
+static void onServerDisconnect(Client) {
+    serverDisconnect = true;
+}
+
+static void onServerPacket(Client client, InPacket* in) {
+    TEST_FALSE(readInPacketU8(in, &data1));
+    TEST_FALSE(readInPacketU16(in, &data2));
+    TEST_FALSE(readInPacketU32(in, &data3));
+    TEST_FALSE(readInPacketI8(in, &data4));
+    TEST_FALSE(readInPacketI16(in, &data5));
+    TEST_FALSE(readInPacketI32(in, &data6));
+    TEST_FALSE(readInPacketI8(in, &data7));
+    TEST_FALSE(readInPacketI16(in, &data8));
+    TEST_FALSE(readInPacketI32(in, &data9));
+    TEST_SIZE(9, readInPacketString(in, data10, sizeof(data10)));
+    TEST_FALSE(readInPacketFloat(in, &data11));
+
+    OutPacket out;
+    initOutPacket(&out);
+    if(packageCounter == 0) {
+        sendServerPacket(client, &out, PACKET_RELIABLE);
+    } else if(packageCounter == 1) {
+        sendServerPacket(client, &out, PACKET_SEQUENCED);
+    } else if(packageCounter == 2) {
+        sendServerPacket(client, &out, PACKET_UNSEQUENCED);
+    }
+    destroyOutPacket(&out);
+    packageCounter++;
+}
+
+static void onClientConnect() {
+    clientConnected = true;
+}
+
+static void onClientDisconnect() {
+    clientDisconnected = true;
+}
+
+static void onClientPacket(InPacket*) {
+    clientPackage = true;
+}
+
+static void testConnect(PacketSendMode mode) {
+    clientConnected = false;
+    clientDisconnected = false;
+    clientPackage = false;
+    serverConnected = false;
+    serverDisconnect = false;
+    data1 = 0;
+    data2 = 0;
+    data3 = 0;
+    data4 = 0;
+    data5 = 0;
+    data6 = 0;
+    data7 = 0;
+    data8 = 0;
+    data9 = 0;
+    *data10 = '\0';
+    data11 = 0.0f;
+
+    resetServerHandler();
+    setServerConnectHandler(onServerConnect);
+    setServerDisconnectHandler(onServerDisconnect);
+    setServerPacketHandler(onServerPacket);
+
+    resetClientHandler();
+    setClientConnectHandler(onClientConnect);
+    setClientDisconnectHandler(onClientDisconnect);
+    setClientPacketHandler(onClientPacket);
+
+    if(!TEST_FALSE(startServer(54321, 5))) {
+        return;
+    } else if(!TEST_FALSE(startClient())) {
+        return;
+    } else if(!TEST_FALSE(connectClient("127.0.0.1", 54321, 90))) {
+        return;
+    }
+    TEST_FALSE(isClientConnected());
+    TEST_TRUE(isClientConnecting());
+    tick(100);
+    TEST_TRUE(clientConnected);
+    TEST_TRUE(isClientConnected());
+    TEST_FALSE(isClientConnecting());
+
+    OutPacket out;
+    initOutPacket(&out);
+    writeOutPacketU8(&out, 0xF1);
+    writeOutPacketU16(&out, 0xF123);
+    writeOutPacketU32(&out, 0xF1234567);
+    writeOutPacketI8(&out, -0x71);
+    writeOutPacketI16(&out, -0x7123);
+    writeOutPacketI32(&out, -0x71234567);
+    writeOutPacketI8(&out, 0x71);
+    writeOutPacketI16(&out, 0x7123);
+    writeOutPacketI32(&out, 0x71234567);
+    const char s[] = "Hi there";
+    writeOutPacketString(&out, s);
+    writeOutPacketFloat(&out, 252345.983f);
+    sendClientPacket(&out, mode);
+    destroyOutPacket(&out);
+
+    tick(100);
+
+    TEST_TRUE(clientPackage);
+    TEST_TRUE(serverConnected);
+
+    TEST_U64(0xF1, data1);
+    TEST_U64(0xF123, data2);
+    TEST_U64(0xF1234567, data3);
+    TEST_I64(-0x71, data4);
+    TEST_I64(-0x7123, data5);
+    TEST_I64(-0x71234567, data6);
+    TEST_I64(0x71, data7);
+    TEST_I64(0x7123, data8);
+    TEST_I64(0x71234567, data9);
+    TEST_STRING(s, data10);
+    TEST_FLOAT(252345.983f, data11, 0.01f);
+
+    disconnectClient(90);
+    TEST_FALSE(isClientConnected());
+    TEST_FALSE(isClientConnecting());
+    tick(100);
+    TEST_TRUE(clientDisconnected);
+    TEST_TRUE(serverDisconnect);
+
+    stopClient();
+    stopServer();
+}
+
+static bool disconnected = false;
+
+static void testStopDisconnect(void) {
+    disconnected = true;
+}
+
+static void testDisconnect(void) {
+    disconnected = false;
+    resetClientHandler();
+    setClientDisconnectHandler(testStopDisconnect);
+    if(!TEST_FALSE(startClient())) {
+        return;
+    } else if(!TEST_FALSE(connectClient("127.0.0.1", 54321, 90))) {
+        return;
+    }
+    TEST_FALSE(isClientConnected());
+    TEST_TRUE(isClientConnecting());
+    disconnectClient(50);
+    tickClientN(100);
+    TEST_FALSE(isClientConnected());
+    TEST_FALSE(isClientConnecting());
+    TEST_TRUE(disconnected);
+    stopClient();
+}
+
+static void testStop(void) {
+    disconnected = false;
+    resetClientHandler();
+    setClientDisconnectHandler(testStopDisconnect);
+    if(!TEST_FALSE(startClient())) {
+        return;
+    } else if(!TEST_FALSE(connectClient("127.0.0.1", 54321, 90))) {
+        return;
+    }
+    TEST_FALSE(isClientConnected());
+    TEST_TRUE(isClientConnecting());
+    stopClient();
+    TEST_FALSE(isClientConnected());
+    TEST_FALSE(isClientConnecting());
+    TEST_TRUE(disconnected);
+}
+
+static void testClientStartFails(void) {
+    TEST_FALSE(startClient());
+    logLevel = LOG_NONE;
+    TEST_TRUE(startClient());
+    stopClient();
+#ifdef ERROR_SIMULATOR
+    failCounter = 0;
+    TEST_TRUE(startClient());
+    failCounter = 1;
+    TEST_TRUE(startClient());
+    failCounter = -1;
+#endif
+    logLevel = LOG_DEBUG;
+}
+
+static void testClientConnectionFails(void) {
+    resetClientHandler();
+    logLevel = LOG_NONE;
+    TEST_TRUE(connectClient("", 54321, 100));
+    TEST_FALSE(startClient());
+#ifdef ERROR_SIMULATOR
+    failCounter = 0;
+    TEST_TRUE(connectClient("", 54321, 100));
+    failCounter = -1;
+#endif
+    TEST_FALSE(connectClient("", 54321, 100));
+    TEST_TRUE(connectClient("", 54321, 100));
+    logLevel = LOG_DEBUG;
+    tickClientN(100);
+    stopClient();
+}
+
+static void testInvalidClientAccess(void) {
+    disconnectClient(0);
+    sendClientPacket(nullptr, 0);
+    tickClient();
+}
+
+static void testServerStartFails(void) {
+    logLevel = LOG_NONE;
+    TEST_TRUE(startServer(54321, 0));
+#ifdef ERROR_SIMULATOR
+    failCounter = 0;
+    TEST_TRUE(startServer(54321, 5));
+    failCounter = 1;
+    TEST_TRUE(startServer(54321, 5));
+    failCounter = -1;
+#endif
+    TEST_FALSE(startServer(54321, 5));
+    TEST_TRUE(startServer(54321, 5));
+    logLevel = LOG_DEBUG;
+    stopServer();
+}
+
+static void testServerClosesOnConnected(void) {
+    clientDisconnected = false;
+    TEST_FALSE(startServer(54321, 5));
+    TEST_FALSE(startClient());
+    setClientDisconnectHandler(onClientDisconnect);
+    TEST_FALSE(connectClient("127.0.0.1", 54321, 50));
+    tick(100);
+    TEST_TRUE(isClientConnected());
+    stopServer();
+
+    setClientTimeout(500, 500, 500);
+    for(int i = 0; i < 500 && isClientConnected(); i++) {
+        tickClient();
+        sleepNanos(10000000);
+    }
+    TEST_FALSE(isClientConnected());
+    TEST_TRUE(clientDisconnected);
+    stopClient();
+}
+
+static Client clientId = 0;
+
+static void onConnectSetClient(Client client) {
+    clientId = client;
+}
+
+static void testServerDisconnectsClient(void) {
+    clientDisconnected = false;
+    TEST_FALSE(startServer(54321, 5));
+    TEST_FALSE(startClient());
+    setClientDisconnectHandler(onClientDisconnect);
+    setServerConnectHandler(onConnectSetClient);
+    TEST_FALSE(connectClient("127.0.0.1", 54321, 50));
+    tick(100);
+    TEST_TRUE(isClientConnected());
+
+    disconnectServerClient(clientId);
+
+    for(int i = 0; i < 500 && isClientConnected(); i++) {
+        tickClient();
+        tickServer();
+        sleepNanos(10000000);
+    }
+    TEST_FALSE(isClientConnected());
+    TEST_TRUE(clientDisconnected);
+    stopClient();
+    stopServer();
+}
+
+static void onConnectSetTimeout(Client client) {
+    setServerTimeout(client, 500, 500, 500);
+}
+
+static void testClientClosesOnConnected(void) {
+    serverDisconnect = false;
+    TEST_FALSE(startServer(54321, 5));
+    TEST_FALSE(startClient());
+    setServerDisconnectHandler(onServerDisconnect);
+    setServerConnectHandler(onConnectSetTimeout);
+    TEST_FALSE(connectClient("127.0.0.1", 54321, 50));
+    tick(100);
+    TEST_TRUE(isClientConnected());
+#ifdef ERROR_SIMULATOR
+    failCounter = 0;
+#endif
+    stopClient();
+#ifdef ERROR_SIMULATOR
+    failCounter = -1;
+#endif
+    for(int i = 0; i < 500 && !serverDisconnect; i++) {
+        tickServer();
+        sleepNanos(10000000);
+    }
+    TEST_TRUE(serverDisconnect);
+    stopServer();
+}
+
+static void testInvalidServerAccess(void) {
+    tickServer();
+    sendServerPacket(0, nullptr, 0);
+    setServerTimeout(0, 500, 500, 500);
+    disconnectServerClient(0);
+}
+
+static void testDummyCallbacks(void) {
+    resetClientHandler();
+    resetServerHandler();
+    TEST_FALSE(startServer(54321, 5));
+    TEST_FALSE(startClient());
+    TEST_FALSE(connectClient("127.0.0.1", 54321, 50));
+    tick(100);
+    TEST_TRUE(isClientConnected());
+    OutPacket out;
+    initOutPacket(&out);
+    sendServerPacketBroadcast(&out, PACKET_RELIABLE);
+    sendClientPacket(&out, PACKET_RELIABLE);
+    tick(100);
+    destroyOutPacket(&out);
+    stopClient();
+    stopServer();
+}
+
+void testNetwork(void) {
+    testWriteRead();
+    testTooShortBuffer();
+    testBinaryData();
+    testShortString();
+    testConnect(PACKET_UNSEQUENCED);
+    testConnect(PACKET_SEQUENCED);
+    testConnect(PACKET_RELIABLE);
+    testDisconnect();
+    testStop();
+    testClientStartFails();
+    testClientConnectionFails();
+    testInvalidClientAccess();
+    testServerStartFails();
+    testServerClosesOnConnected();
+    testServerDisconnectsClient();
+    testClientClosesOnConnected();
+    testInvalidServerAccess();
+    testDummyCallbacks();
+}

+ 78 - 0
test/modules/WindowManagerTests.c

@@ -0,0 +1,78 @@
+#include <GLFW/glfw3.h>
+#include <core/Logger.h>
+#include <stdio.h>
+
+#include "../Tests.h"
+#include "core/WindowManager.h"
+
+static int ticks = 2;
+static Button closeButton = 0;
+static Button testButton = 0;
+static Button textButton = 0;
+
+static bool isRunning(void*) {
+    return !shouldWindowClose() && /*ticks > 0 &&*/ !isButtonDown(closeButton);
+}
+
+static void tick(void*) {
+    ticks -= ticks > 0;
+    if(!isInputEnabled()) {
+        printf(
+            "TPS: %.3f\nFPS: %.3f\n", (double)getWindowTicksPerSecond(),
+            (double)getWindowFramesPerSecond());
+        printf(
+            "%12s | Down: %d | DownTime: %3d | Released: %d\n",
+            getButtonName(closeButton), isButtonDown(closeButton),
+            getButtonDownTime(closeButton), wasButtonReleased(closeButton));
+        printf(
+            "%12s | Down: %d | DownTime: %3d | Released: %d\n",
+            getButtonName(testButton), isButtonDown(testButton),
+            getButtonDownTime(testButton), wasButtonReleased(testButton));
+        Vector2 mouse = getLastMousePosition();
+        printf(
+            "Mouse: %.2f %.2f\n", (double)mouse.data[0], (double)mouse.data[1]);
+    }
+    if(getButtonDownTime(textButton) == 1) {
+        if(isInputEnabled()) {
+            disableInput();
+        } else {
+            enableInput();
+        }
+    }
+    if(isInputEnabled()) {
+        printf("Input: '%s'\n", getInputString());
+        printf("Cursor: %zu\n", getInputCursor());
+    }
+}
+
+static void render(void*, float) {
+}
+
+static void printReport(
+    LogLevel l, const char* file, int line, void*, const char* message) {
+    printLog(l, file, line, "", TERMINAL_RED, "%s", message);
+}
+
+void testWindow(void) {
+    setReportHandler(printReport, nullptr);
+
+    WindowOptions options = {{{800, 480}}, false, "Test"};
+    if(openWindow(&options)) {
+        return;
+    }
+
+    closeButton = addButton("Close Button");
+    bindKeyToButton(closeButton, GLFW_KEY_Q);
+    testButton = addButton("Test Button");
+    bindKeyToButton(testButton, GLFW_KEY_T);
+    textButton = addButton("Text Button");
+    bindKeyToButton(textButton, GLFW_KEY_C);
+
+    showWindow();
+    setWindowRunHandler(isRunning, nullptr);
+    setWindowTickHandler(tick, nullptr);
+    setWindowRenderHandler(render, nullptr);
+    setWindowNanosPerTick(50'000'000);
+    runWindow();
+    closeWindow();
+}

BIN
test/resources/gray16.png


BIN
test/resources/gray8.png


BIN
test/resources/graya16.png


BIN
test/resources/graya8.png


BIN
test/resources/rgb16.png


BIN
test/resources/rgb8.png


BIN
test/resources/rgba16.png


BIN
test/resources/rgba8.png


+ 181 - 0
valgrind.supp

@@ -0,0 +1,181 @@
+{
+  LLVM
+  Memcheck:Leak
+  match-leak-kinds: reachable
+  fun:malloc
+  fun:strdup
+  fun:parseAndSetFilename
+  fun:__llvm_profile_initialize
+  ...
+}
+{
+  glfwInit 1
+  Memcheck:Leak
+  match-leak-kinds: reachable
+  fun:calloc
+  fun:calloc
+  fun:_dl_new_object
+}
+{
+  glfwInit 2
+  Memcheck:Leak
+  match-leak-kinds: reachable
+  fun:calloc
+  ...
+  fun:_dl_catch_exception
+}
+{
+  glfwInit 3
+  Memcheck:Leak
+  match-leak-kinds: reachable
+  fun:malloc
+  ...
+  fun:_dl_catch_exception
+}
+{
+  glfwInit 4
+  Memcheck:Leak
+  match-leak-kinds: reachable
+  fun:realloc
+  ...
+  fun:_dl_catch_exception
+}
+{
+  glfwInit 5
+  Memcheck:Leak
+  match-leak-kinds: reachable
+  ...
+  obj:/usr/lib/x86_64-linux-gnu/libdbus-1.so.3.32.4
+}
+{
+  glfwInit 6
+  Memcheck:Leak
+  match-leak-kinds: reachable
+  ...
+  fun:_dl_init
+  ...
+  fun:_dl_catch_exception
+}
+{
+  glfwinit 7
+  Memcheck:Leak
+  match-leak-kinds: reachable
+  fun:realloc
+  ...
+  fun:g_hash_table_insert
+  ...
+  fun:call_init
+}
+{
+  glfwShowWindow
+  Memcheck:Leak
+  match-leak-kinds: definite
+  fun:calloc
+  ...
+  fun:createLibdecorFrame
+  fun:createShellObjects
+}
+{
+  libcairo font stuff
+  Memcheck:Leak
+  match-leak-kinds: reachable
+  ...
+  obj:/usr/lib/x86_64-linux-gnu/libcairo.so.2.11802.2
+}
+{
+  libpangocairo font stuff
+  Memcheck:Leak
+  match-leak-kinds: reachable,possible
+  ...
+  obj:/usr/lib/x86_64-linux-gnu/libpangocairo-1.0.so.0.5400.0
+}
+{
+  libpango font stuff
+  Memcheck:Leak
+  ...
+  obj:/usr/lib/x86_64-linux-gnu/libpango-1.0.so.0.5400.0
+}
+{
+  libexpat font stuff
+  Memcheck:Leak
+  match-leak-kinds: reachable
+  ...
+  obj:/usr/lib/x86_64-linux-gnu/libexpat.so.1.9.2
+}
+{
+  libfontconfig font stuff
+  Memcheck:Leak
+  ...
+  obj:/usr/lib/x86_64-linux-gnu/libfontconfig.so.1.12.1
+}
+{
+  libharfbuzz font stuff
+  Memcheck:Leak
+  ...
+  obj:/usr/lib/x86_64-linux-gnu/libharfbuzz.so.0.61001.0
+}
+{
+  libfreetype font stuff
+  Memcheck:Leak
+  ...
+  obj:/usr/lib/x86_64-linux-gnu/libfreetype.so.6.20.2
+}
+{
+  libgobject 1
+  Memcheck:Leak
+  match-leak-kinds: reachable
+  fun:realloc
+  ...
+  fun:g_hash_table_insert
+  ...
+  fun:g_type_class_ref
+}
+{
+  libgobject 2
+  Memcheck:Leak
+  match-leak-kinds: reachable
+  fun:malloc
+  ...
+  fun:g_type_class_ref
+}
+{
+  libgobject 3
+  Memcheck:Leak
+  match-leak-kinds: reachable
+  fun:realloc
+  ...
+  fun:g_type_class_ref
+}
+{
+  libgobject 4
+  Memcheck:Leak
+  match-leak-kinds: reachable
+  fun:calloc
+  ...
+  fun:g_type_class_ref
+}
+{
+  libglib
+  Memcheck:Leak
+  match-leak-kinds: reachable
+  fun:malloc
+  ...
+  fun:start_thread
+  fun:clone
+}
+{
+  libglib 2
+  Memcheck:Leak
+  match-leak-kinds: reachable
+  fun:calloc
+  ...
+  fun:start_thread
+  fun:clone
+}
+{
+  dl_init
+  Memcheck:Leak
+  match-leak-kinds: reachable
+  ...
+  fun:_dl_init
+}