diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 56d871b8..1a39d59d 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -30,6 +30,7 @@ add_executable(cursor cursor.c ${GLAD}) add_executable(empty WIN32 MACOSX_BUNDLE empty.c ${TINYCTHREAD} ${GLAD}) add_executable(gamma WIN32 MACOSX_BUNDLE gamma.c ${GLAD}) add_executable(icon WIN32 MACOSX_BUNDLE icon.c ${GLAD}) +add_executable(inputlag WIN32 MACOSX_BUNDLE inputlag.c ${GETOPT} ${GLAD}) add_executable(joysticks WIN32 MACOSX_BUNDLE joysticks.c ${GLAD}) add_executable(sharing WIN32 MACOSX_BUNDLE sharing.c ${GETOPT} ${GLAD}) add_executable(tearing WIN32 MACOSX_BUNDLE tearing.c ${GETOPT} ${GLAD}) @@ -45,8 +46,8 @@ if (RT_LIBRARY) target_link_libraries(threads "${RT_LIBRARY}") endif() -set(WINDOWS_BINARIES empty gamma icon joysticks sharing tearing threads timeout - title windows) +set(WINDOWS_BINARIES empty gamma icon inputlag joysticks sharing tearing threads + timeout title windows) set(CONSOLE_BINARIES clipboard events msaa glfwinfo iconify monitors reopen cursor) @@ -73,6 +74,7 @@ endif() if (APPLE) set_target_properties(empty PROPERTIES MACOSX_BUNDLE_BUNDLE_NAME "Empty Event") set_target_properties(gamma PROPERTIES MACOSX_BUNDLE_BUNDLE_NAME "Gamma") + set_target_properties(inputlag PROPERTIES MACOSX_BUNDLE_BUNDLE_NAME "Input Lag") set_target_properties(joysticks PROPERTIES MACOSX_BUNDLE_BUNDLE_NAME "Joysticks") set_target_properties(sharing PROPERTIES MACOSX_BUNDLE_BUNDLE_NAME "Sharing") set_target_properties(tearing PROPERTIES MACOSX_BUNDLE_BUNDLE_NAME "Tearing") diff --git a/tests/inputlag.c b/tests/inputlag.c new file mode 100644 index 00000000..eee4544c --- /dev/null +++ b/tests/inputlag.c @@ -0,0 +1,305 @@ +//======================================================================== +// Input lag test +// Copyright (c) Camilla Löwy +// +// This software is provided 'as-is', without any express or implied +// warranty. In no event will the authors be held liable for any damages +// arising from the use of this software. +// +// Permission is granted to anyone to use this software for any purpose, +// including commercial applications, and to alter it and redistribute it +// freely, subject to the following restrictions: +// +// 1. The origin of this software must not be misrepresented; you must not +// claim that you wrote the original software. If you use this software +// in a product, an acknowledgment in the product documentation would +// be appreciated but is not required. +// +// 2. Altered source versions must be plainly marked as such, and must not +// be misrepresented as being the original software. +// +// 3. This notice may not be removed or altered from any source +// distribution. +// +//======================================================================== +// +// This test renders a marker at the cursor position reported by GLFW to +// check how much it lags behind the hardware mouse cursor +// +//======================================================================== + +#include +#include + +#define NK_IMPLEMENTATION +#define NK_INCLUDE_FIXED_TYPES +#define NK_INCLUDE_FONT_BAKING +#define NK_INCLUDE_DEFAULT_FONT +#define NK_INCLUDE_DEFAULT_ALLOCATOR +#define NK_INCLUDE_VERTEX_BUFFER_OUTPUT +#define NK_INCLUDE_STANDARD_VARARGS +#include + +#define NK_GLFW_GL2_IMPLEMENTATION +#include + +#include +#include +#include + +#include "getopt.h" + +void usage(void) +{ + printf("Usage: inputlag [-h] [-f]\n"); + printf("Options:\n"); + printf(" -f create full screen window\n"); + printf(" -h show this help\n"); +} + +struct nk_vec2 cursor_new, cursor_pos, cursor_vel; +enum { cursor_sync_query, cursor_input_message } cursor_method = cursor_sync_query; + +void sample_input(GLFWwindow* window) +{ + float a = .25; // exponential smoothing factor + + if (cursor_method == cursor_sync_query) { + double x, y; + glfwGetCursorPos(window, &x, &y); + cursor_new.x = (float) x; + cursor_new.y = (float) y; + } + + cursor_vel.x = (cursor_new.x - cursor_pos.x) * a + cursor_vel.x * (1 - a); + cursor_vel.y = (cursor_new.y - cursor_pos.y) * a + cursor_vel.y * (1 - a); + cursor_pos = cursor_new; +} + +void cursor_pos_callback(GLFWwindow* window, double xpos, double ypos) +{ + cursor_new.x = (float) xpos; + cursor_new.y = (float) ypos; +} + +int enable_vsync = nk_true; + +void update_vsync() +{ + glfwSwapInterval(enable_vsync == nk_true ? 1 : 0); +} + +int swap_clear = nk_false; +int swap_finish = nk_true; +int swap_occlusion_query = nk_false; +int swap_read_pixels = nk_false; +GLuint occlusion_query; + +void swap_buffers(GLFWwindow* window) +{ + glfwSwapBuffers(window); + + if (swap_clear) + glClear(GL_COLOR_BUFFER_BIT); + + if (swap_finish) + glFinish(); + + if (swap_occlusion_query) { + GLint occlusion_result; + if (!occlusion_query) + glGenQueries(1, &occlusion_query); + glBeginQuery(GL_SAMPLES_PASSED, occlusion_query); + glBegin(GL_POINTS); + glVertex2f(0, 0); + glEnd(); + glEndQuery(GL_SAMPLES_PASSED); + glGetQueryObjectiv(occlusion_query, GL_QUERY_RESULT, &occlusion_result); + } + + if (swap_read_pixels) { + unsigned char rgba[4]; + glReadPixels(0, 0, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, rgba); + } +} + +void error_callback(int error, const char* description) +{ + fprintf(stderr, "Error: %s\n", description); +} + +void key_callback(GLFWwindow* window, int key, int scancode, int action, int mods) +{ + if (action != GLFW_PRESS) + return; + + switch (key) + { + case GLFW_KEY_ESCAPE: + glfwSetWindowShouldClose(window, 1); + break; + } +} + +void draw_marker(struct nk_command_buffer* canvas, int lead, struct nk_vec2 pos) +{ + struct nk_color colors[4] = { nk_rgb(255,0,0), nk_rgb(255,255,0), nk_rgb(0,255,0), nk_rgb(0,96,255) }; + struct nk_rect rect = { -5 + pos.x, -5 + pos.y, 10, 10 }; + nk_fill_circle(canvas, rect, colors[lead]); +} + +int main(int argc, char** argv) +{ + int ch, width, height; + unsigned long frame_count = 0; + double last_time, current_time; + double frame_rate = 0; + int fullscreen = GLFW_FALSE; + GLFWmonitor* monitor = NULL; + GLFWwindow* window; + struct nk_context* nk; + struct nk_font_atlas* atlas; + + int show_forecasts = nk_true; + + while ((ch = getopt(argc, argv, "fh")) != -1) + { + switch (ch) + { + case 'h': + usage(); + exit(EXIT_SUCCESS); + + case 'f': + fullscreen = GLFW_TRUE; + break; + } + } + + glfwSetErrorCallback(error_callback); + + if (!glfwInit()) + exit(EXIT_FAILURE); + + if (fullscreen) + { + const GLFWvidmode* mode; + + monitor = glfwGetPrimaryMonitor(); + mode = glfwGetVideoMode(monitor); + + width = mode->width; + height = mode->height; + } + else + { + width = 640; + height = 480; + } + + glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 2); + glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 0); + + window = glfwCreateWindow(width, height, "Input lag test", monitor, NULL); + if (!window) + { + glfwTerminate(); + exit(EXIT_FAILURE); + } + + glfwMakeContextCurrent(window); + gladLoadGLLoader((GLADloadproc) glfwGetProcAddress); + update_vsync(); + + last_time = glfwGetTime(); + + nk = nk_glfw3_init(window, NK_GLFW3_INSTALL_CALLBACKS); + nk_glfw3_font_stash_begin(&atlas); + nk_glfw3_font_stash_end(); + + glfwSetKeyCallback(window, key_callback); + glfwSetCursorPosCallback(window, cursor_pos_callback); + + while (!glfwWindowShouldClose(window)) + { + int width, height; + struct nk_rect area; + + glfwPollEvents(); + sample_input(window); + + glfwGetWindowSize(window, &width, &height); + area = nk_rect(0.f, 0.f, (float) width, (float) height); + + glClear(GL_COLOR_BUFFER_BIT); + nk_glfw3_new_frame(); + if (nk_begin(nk, "", area, 0)) + { + nk_flags align_left = NK_TEXT_ALIGN_LEFT | NK_TEXT_ALIGN_MIDDLE; + struct nk_command_buffer *canvas = nk_window_get_canvas(nk); + int lead; + + for (lead = show_forecasts ? 3 : 0; lead >= 0; lead--) + draw_marker(canvas, lead, nk_vec2(cursor_pos.x + cursor_vel.x * lead, + cursor_pos.y + cursor_vel.y * lead)); + + // print instructions + nk_layout_row_dynamic(nk, 20, 1); + nk_label(nk, "Move mouse uniformly and check marker under cursor:", align_left); + for (lead = 0; lead <= 3; lead++) { + nk_layout_row_begin(nk, NK_STATIC, 12, 2); + nk_layout_row_push(nk, 25); + draw_marker(canvas, lead, nk_layout_space_to_screen(nk, nk_vec2(20, 5))); + nk_label(nk, "", 0); + nk_layout_row_push(nk, 500); + if (lead == 0) + nk_label(nk, "- current cursor position (no input lag)", align_left); + else + nk_labelf(nk, align_left, "- %d-frame forecast (input lag is %d frame)", lead, lead); + nk_layout_row_end(nk); + } + + nk_layout_row_dynamic(nk, 20, 1); + + nk_checkbox_label(nk, "Show forecasts", &show_forecasts); + nk_label(nk, "Input method:", align_left); + if (nk_option_label(nk, "glfwGetCursorPos (sync query)", cursor_method == cursor_sync_query)) + cursor_method = cursor_sync_query; + if (nk_option_label(nk, "glfwSetCursorPosCallback (latest input message)", cursor_method == cursor_input_message)) + cursor_method = cursor_input_message; + + nk_label(nk, "", 0); // separator + + nk_value_float(nk, "FPS", (float) frame_rate); + if (nk_checkbox_label(nk, "Enable vsync", &enable_vsync)) + update_vsync(); + + nk_label(nk, "", 0); // separator + + nk_label(nk, "After swap:", align_left); + nk_checkbox_label(nk, "glClear", &swap_clear); + nk_checkbox_label(nk, "glFinish", &swap_finish); + nk_checkbox_label(nk, "draw with occlusion query", &swap_occlusion_query); + nk_checkbox_label(nk, "glReadPixels", &swap_read_pixels); + } + + nk_end(nk); + nk_glfw3_render(NK_ANTI_ALIASING_ON); + + swap_buffers(window); + + frame_count++; + + current_time = glfwGetTime(); + if (current_time - last_time > 1.0) + { + frame_rate = frame_count / (current_time - last_time); + frame_count = 0; + last_time = current_time; + } + } + + glfwTerminate(); + exit(EXIT_SUCCESS); +} +