platform.cpp

cross platform rendering playground

src/core/platform.cpp

10.26 KB
#ifdef _WIN32
#define WIN32_LEAN_AND_MEAN
#define GLFW_EXPOSE_NATIVE_WIN32
#include <dwmapi.h>
#include <windows.h>
#endif

#include "platform.h"

#include <GLFW/glfw3.h>
#include <cassert>
#include <cstdlib>

#ifdef _WIN32
#include <GLFW/glfw3native.h>
#endif

#include "core/logger.h"

static GLFWwindow *as_glfw(PlatformWindow *w) {
    return reinterpret_cast<GLFWwindow *>(w);
}
static PlatformWindow *as_platform(GLFWwindow *w) {
    return reinterpret_cast<PlatformWindow *>(w);
}

#ifdef _WIN32
static void apply_dark_titlebar(GLFWwindow *window) {
    HWND hwnd = glfwGetWin32Window(window);
    BOOL dark = TRUE;
    if (FAILED(DwmSetWindowAttribute(hwnd, DWMWA_USE_IMMERSIVE_DARK_MODE, &dark, sizeof(dark)))) {
        DwmSetWindowAttribute(hwnd, 19, &dark, sizeof(dark));
    }
}
#endif

void toggle_capture(Platform &platform) {
    platform.captured = !platform.captured;

    platform.pending_mouse_dx = 0.0f;
    platform.pending_mouse_dy = 0.0f;
    platform.first_mouse_sample = true;

    if (platform.captured) {
        glfwGetCursorPos(as_glfw(platform.window), &platform.last_mouse_x, &platform.last_mouse_y);
        glfwSetInputMode(as_glfw(platform.window), GLFW_CURSOR, GLFW_CURSOR_DISABLED);

        // raw input, TODO: actually validate?
        if (glfwRawMouseMotionSupported()) {
            glfwSetInputMode(as_glfw(platform.window), GLFW_RAW_MOUSE_MOTION, GLFW_TRUE);
        }
    } else {
        glfwSetInputMode(as_glfw(platform.window), GLFW_CURSOR, GLFW_CURSOR_NORMAL);
        glfwSetInputMode(as_glfw(platform.window), GLFW_RAW_MOUSE_MOTION, GLFW_FALSE);
        platform.input = {};
    }
}

void cursor_position_callback(GLFWwindow *window, f64 xpos, f64 ypos) {
    Platform *platform = static_cast<Platform *>(glfwGetWindowUserPointer(window));
    if (!platform || !platform->captured) {
        return;
    }

    if (platform->first_mouse_sample) {
        platform->last_mouse_x = xpos;
        platform->last_mouse_y = ypos;
        platform->first_mouse_sample = false;
        return;
    }

    // accumulate ALL sub-frame mouse events from high-polling mice (1000Hz+)
    platform->pending_mouse_dx += static_cast<f32>(xpos - platform->last_mouse_x);
    platform->pending_mouse_dy += static_cast<f32>(ypos - platform->last_mouse_y);

    platform->last_mouse_x = xpos;
    platform->last_mouse_y = ypos;
}

void key_callback(GLFWwindow *window, int key, int scancode, int action, int mods) {
    Platform *platform = static_cast<Platform *>(glfwGetWindowUserPointer(window));
    if (!platform) {
        return;
    }

    if (key == GLFW_KEY_ESCAPE && action == GLFW_PRESS) {
        glfwSetWindowShouldClose(window, GLFW_TRUE);
        return;
    }

    if (key == GLFW_KEY_L && action == GLFW_PRESS) {
        toggle_capture(*platform);
        return;
    }

    if (!platform->captured) {
        return;
    }

    // ignore key repeat events sent by the OS
    if (action == GLFW_REPEAT) {
        return;
    }

    const bool pressed = (action == GLFW_PRESS);
    switch (key) {
    case GLFW_KEY_W:
        platform->input.move_forward = pressed;
        break;
    case GLFW_KEY_S:
        platform->input.move_backward = pressed;
        break;
    case GLFW_KEY_A:
        platform->input.move_left = pressed;
        break;
    case GLFW_KEY_D:
        platform->input.move_right = pressed;
        break;
    case GLFW_KEY_E:
        platform->input.move_up = pressed;
        break;
    case GLFW_KEY_Q:
        platform->input.move_down = pressed;
        break;
    case GLFW_KEY_LEFT_SHIFT:
    case GLFW_KEY_RIGHT_SHIFT:
        platform->input.fast = pressed;
        break;
    default:
        break;
    }
}

void mouse_button_callback(GLFWwindow *window, int button, int action, int mods) {
    Platform *platform = static_cast<Platform *>(glfwGetWindowUserPointer(window));
    if (!platform) {
        return;
    }

    if (button == GLFW_MOUSE_BUTTON_5 && action == GLFW_PRESS) {
        toggle_capture(*platform);
    }
}

void window_focus_callback(GLFWwindow *window, int focused) {
    Platform *platform = static_cast<Platform *>(glfwGetWindowUserPointer(window));
    if (!platform) {
        return;
    }

    if (!focused) {
        // clear inputs immediately on Alt-Tab so keys don't get stuck down
        platform->input = {};
        platform->pending_mouse_dx = 0.0f;
        platform->pending_mouse_dy = 0.0f;
        platform->first_mouse_sample = true;
    } else if (platform->captured) {
        platform->first_mouse_sample = true;
    }
}

void framebuffer_resize_callback(GLFWwindow *window, int width, int height) {
    Platform *platform = static_cast<Platform *>(glfwGetWindowUserPointer(window));
    if (platform) {
        platform->framebuffer_resized = true;
        platform->width = static_cast<u32>(width);
        platform->height = static_cast<u32>(height);
    }
}

void init_platform(std::vector<const char *> &extensions) {
    VEL_INFO("[GLFW] Initializing");
    glfwInit();
#ifndef RHI_BACKEND_METAL
    // Vulkan needs GLFW's required instance extensions; Metal needs none.
    if (!glfwVulkanSupported()) {
        std::abort();
    }
    u32 extension_count = 0;
    const char **glfw_extensions = glfwGetRequiredInstanceExtensions(&extension_count);
    for (u32 i = 0; i < extension_count; ++i) {
        extensions.push_back(glfw_extensions[i]);
    }
#endif
}

void create_window(rhi::Device &device, Platform &platform, const char *windowTitle, WindowMode mode, bool resizable) {
    VEL_INFO("[GLFW] Initializing window");
    glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API);
    glfwWindowHint(GLFW_VISIBLE, GLFW_FALSE);

    GLFWmonitor *monitor = glfwGetPrimaryMonitor();
    const GLFWvidmode *video = glfwGetVideoMode(monitor);
    GLFWmonitor *target_monitor = nullptr;

    int w = platform.width;
    int h = platform.height;
    switch (mode) {
    case WindowMode::Fullscreen: {
        target_monitor = monitor;
        w = video->width;
        h = video->height;
        glfwWindowHint(GLFW_DECORATED, GLFW_FALSE);
        glfwWindowHint(GLFW_RESIZABLE, GLFW_FALSE);
    } break;

    case WindowMode::Maximized: {
        w = video->width;
        h = video->height;
        glfwWindowHint(GLFW_DECORATED, GLFW_TRUE);
        glfwWindowHint(GLFW_RESIZABLE, GLFW_TRUE);
    } break;

    case WindowMode::Windowed:
    default: {
        assert(platform.width > 0 && platform.height > 0);
        glfwWindowHint(GLFW_DECORATED, GLFW_TRUE);
        glfwWindowHint(GLFW_RESIZABLE, resizable ? GLFW_TRUE : GLFW_FALSE);
    } break;
    }

    platform.window = as_platform(glfwCreateWindow(w, h, windowTitle, target_monitor, nullptr));
    if (!platform.window) {
        VEL_CRITICAL("[GLFW] Failed to create window");
        std::abort();
    }

#ifdef _WIN32
    apply_dark_titlebar(as_glfw(platform.window));
#endif

    if (mode == WindowMode::Maximized) {
        glfwMaximizeWindow(as_glfw(platform.window));
    } else if (mode == WindowMode::Windowed) {
        int mx, my;
        glfwGetMonitorPos(monitor, &mx, &my);
        glfwSetWindowPos(as_glfw(platform.window), mx + (video->width - w) / 2, my + (video->height - h) / 2);
    }

    glfwSetWindowUserPointer(as_glfw(platform.window), &platform);

    glfwSetFramebufferSizeCallback(as_glfw(platform.window), framebuffer_resize_callback);
    glfwSetKeyCallback(as_glfw(platform.window), key_callback);
    glfwSetMouseButtonCallback(as_glfw(platform.window), mouse_button_callback);
    glfwSetCursorPosCallback(as_glfw(platform.window), cursor_position_callback);
    glfwSetWindowFocusCallback(as_glfw(platform.window), window_focus_callback);

    if (platform.captured) {
        glfwSetInputMode(as_glfw(platform.window), GLFW_CURSOR, GLFW_CURSOR_DISABLED);
        if (glfwRawMouseMotionSupported()) {
            glfwSetInputMode(as_glfw(platform.window), GLFW_RAW_MOUSE_MOTION, GLFW_TRUE);
        }
    }

    glfwGetCursorPos(as_glfw(platform.window), &platform.last_mouse_x, &platform.last_mouse_y);
    platform.first_mouse_sample = true;

    glfwShowWindow(as_glfw(platform.window));
}

void destroy_window(rhi::Device &device, Platform &platform) {
    VEL_INFO("Destroying GLFW window...");
    glfwDestroyWindow(as_glfw(platform.window));
    platform.window = nullptr;
}

bool platform_update(Platform &platform) {
    if (glfwWindowShouldClose(as_glfw(platform.window))) {
        return false;
    }

    const f64 now = glfwGetTime();
    static f64 last_time = now;
    const f64 raw_dt = now - last_time;
    last_time = now;

    // unclamped delta for profiling:
    // hitches >50ms are preserved here so the
    // stats overlay shows real frame-time spikes.
    platform.raw_dt = raw_dt;

    // clamp (50ms)
    // to prevent camera jumps on hitching/pipeline reload
    platform.dt = (raw_dt > 0.05) ? 0.016667 : raw_dt;
    platform.input.dt = static_cast<f32>(platform.dt);

    // clear previous frame pending mouse deltas
    // pull this shit into a helper or something ffs
    platform.pending_mouse_dx = 0.0f;
    platform.pending_mouse_dy = 0.0f;

    glfwPollEvents();

    platform.input.mouse_dx = platform.pending_mouse_dx;
    platform.input.mouse_dy = platform.pending_mouse_dy;

    return true;
}

bool platform_should_close(Platform &platform) {
    return glfwWindowShouldClose(as_glfw(platform.window)) != 0;
}

void platform_request_close(Platform &platform) {
    glfwSetWindowShouldClose(as_glfw(platform.window), GLFW_TRUE);
}

void platform_framebuffer_size(Platform &platform, u32 &out_w, u32 &out_h) {
    int w = 0, h = 0;
    glfwGetFramebufferSize(as_glfw(platform.window), &w, &h);
    out_w = static_cast<u32>(w);
    out_h = static_cast<u32>(h);
}

f32 platform_content_scale(Platform &platform) {
    f32 scale = 1.0f;
    glfwGetWindowContentScale(as_glfw(platform.window), &scale, nullptr);
    return scale;
}

bool platform_wait_for_valid_framebuffer(Platform &platform) {
    int w = 0, h = 0;
    glfwGetFramebufferSize(as_glfw(platform.window), &w, &h);
    while ((w == 0 || h == 0) && !glfwWindowShouldClose(as_glfw(platform.window))) {
        glfwWaitEvents();
        glfwGetFramebufferSize(as_glfw(platform.window), &w, &h);
    }
    if (glfwWindowShouldClose(as_glfw(platform.window))) {
        return false;
    }
    platform.width = static_cast<u32>(w);
    platform.height = static_cast<u32>(h);
    return true;
}

void *platform_native_window(Platform &platform) {
    return as_glfw(platform.window);
}