/* * Copyright (C) 2021 Christopher J. Howard * * This file is part of Antkeeper source code. * * Antkeeper source code is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Antkeeper source code is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Antkeeper source code. If not, see . */ #include "application.hpp" #include "debug/logger.hpp" #include "event/event-dispatcher.hpp" #include "event/window-events.hpp" #include "input/scancode.hpp" #include "input/sdl-game-controller-tables.hpp" #include "input/sdl-scancode-table.hpp" #include "resources/image.hpp" #include #include #include #include #include #include application::application(): closed(false), fullscreen(true), v_sync(false), cursor_visible(true), display_dimensions({0, 0}), display_dpi(0.0f), window_dimensions({0, 0}), viewport_dimensions({0, 0}), mouse_position({0, 0}), logger(nullptr), sdl_window(nullptr), sdl_gl_context(nullptr) { // Setup logging logger = new debug::logger(); // Get SDL compiled version SDL_version sdl_compiled_version; SDL_VERSION(&sdl_compiled_version); std::string sdl_compiled_version_string = std::to_string(sdl_compiled_version.major) + "." + std::to_string(sdl_compiled_version.minor) + "." + std::to_string(sdl_compiled_version.patch); logger->log("Compiled against SDL " + sdl_compiled_version_string); // Get SDL linked version SDL_version sdl_linked_version; SDL_GetVersion(&sdl_linked_version); std::string sdl_linked_version_string = std::to_string(sdl_linked_version.major) + "." + std::to_string(sdl_linked_version.minor) + "." + std::to_string(sdl_linked_version.patch); logger->log("Linking against SDL " + sdl_linked_version_string); // Init SDL logger->push_task("Initializing SDL"); if (SDL_Init(SDL_INIT_VIDEO) != 0) { logger->pop_task(EXIT_FAILURE); throw std::runtime_error("Failed to initialize SDL"); } else { logger->pop_task(EXIT_SUCCESS); } // Load default OpenGL library logger->push_task("Loading OpenGL library"); if (SDL_GL_LoadLibrary(nullptr) != 0) { logger->pop_task(EXIT_FAILURE); } else { logger->pop_task(EXIT_SUCCESS); } // Set window creation hints SDL_GL_SetAttribute(SDL_GL_ACCELERATED_VISUAL, 1); SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3); SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 3); SDL_GL_SetAttribute(SDL_GL_CONTEXT_FLAGS, SDL_GL_CONTEXT_FORWARD_COMPATIBLE_FLAG); SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE); SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1); SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 24); // Get display dimensions SDL_DisplayMode sdl_desktop_display_mode; if (SDL_GetDesktopDisplayMode(0, &sdl_desktop_display_mode) != 0) { logger->error("Failed to detect desktop display mode: \"" + std::string(SDL_GetError()) + "\""); throw std::runtime_error("Failed to detect desktop display mode"); } else { display_dimensions = {sdl_desktop_display_mode.w, sdl_desktop_display_mode.h}; } // Get display DPI if (SDL_GetDisplayDPI(0, &display_dpi, nullptr, nullptr) != 0) { logger->error("Failed to detect display DPI: \"" + std::string(SDL_GetError()) + "\""); throw std::runtime_error("Failed to detect display DPI"); } else { logger->log("Detected " + std::to_string(sdl_desktop_display_mode.w) + "x" + std::to_string(sdl_desktop_display_mode.h) + " display with " + std::to_string(display_dpi) + " DPI"); } // Create a hidden fullscreen window logger->push_task("Creating " + std::to_string(display_dimensions[0]) + "x" + std::to_string(display_dimensions[1]) + " window"); sdl_window = SDL_CreateWindow ( "Antkeeper", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, display_dimensions[0], display_dimensions[1], SDL_WINDOW_OPENGL | SDL_WINDOW_FULLSCREEN_DESKTOP | SDL_WINDOW_ALLOW_HIGHDPI | SDL_WINDOW_HIDDEN ); if (!sdl_window) { logger->pop_task(EXIT_FAILURE); throw std::runtime_error("Failed to create SDL window"); } else { logger->pop_task(EXIT_SUCCESS); } // Create OpenGL context logger->push_task("Creating OpenGL 3.3 context"); sdl_gl_context = SDL_GL_CreateContext(sdl_window); if (!sdl_gl_context) { logger->pop_task(EXIT_FAILURE); throw std::runtime_error("Failed to create OpenGL context"); } else { logger->pop_task(EXIT_SUCCESS); } // Make OpenGL context current logger->push_task("Making OpenGL context current"); if (SDL_GL_MakeCurrent(sdl_window, sdl_gl_context) != 0) { logger->pop_task(EXIT_FAILURE); throw std::runtime_error("Failed to make OpenGL context current"); } else { logger->pop_task(EXIT_SUCCESS); } // Load OpenGL functions via GLAD logger->push_task("Loading OpenGL functions"); if (!gladLoadGLLoader((GLADloadproc)SDL_GL_GetProcAddress)) { logger->pop_task(EXIT_FAILURE); throw std::runtime_error("Failed to load OpenGL functions"); } else { logger->pop_task(EXIT_SUCCESS); } // Update window size and viewport size SDL_GetWindowSize(sdl_window, &window_dimensions[0], &window_dimensions[1]); SDL_GL_GetDrawableSize(sdl_window, &viewport_dimensions[0], &viewport_dimensions[1]); // Set v-sync mode set_v_sync(true); // Init SDL joystick and gamepad subsystems logger->push_task("Initializing SDL Joystick and Game Controller subsystems"); if (SDL_InitSubSystem(SDL_INIT_JOYSTICK | SDL_INIT_GAMECONTROLLER) != 0) { logger->pop_task(EXIT_FAILURE); } else { logger->pop_task(EXIT_SUCCESS); } // Setup rasterizer rasterizer = new gl::rasterizer(); // Setup events event_dispatcher = new ::event_dispatcher(); // Setup input keyboard = new input::keyboard(); keyboard->set_event_dispatcher(event_dispatcher); mouse = new input::mouse(); mouse->set_event_dispatcher(event_dispatcher); // Connect gamepads process_events(); } application::~application() { // Destroy the OpenGL context SDL_GL_DeleteContext(sdl_gl_context); // Destroy the SDL window SDL_DestroyWindow(sdl_window); // Shutdown SDL SDL_QuitSubSystem(SDL_INIT_VIDEO | SDL_INIT_JOYSTICK | SDL_INIT_GAMECONTROLLER); SDL_Quit(); } void application::close() { closed = true; } void application::set_title(const std::string& title) { SDL_SetWindowTitle(sdl_window, title.c_str()); } void application::set_cursor_visible(bool visible) { SDL_ShowCursor((visible) ? SDL_ENABLE : SDL_DISABLE); cursor_visible = visible; } void application::set_relative_mouse_mode(bool enabled) { if (enabled) { SDL_GetMouseState(&mouse_position[0], &mouse_position[1]); SDL_ShowCursor(SDL_DISABLE); SDL_SetRelativeMouseMode(SDL_TRUE); } else { SDL_SetRelativeMouseMode(SDL_FALSE); SDL_WarpMouseInWindow(sdl_window, mouse_position[0], mouse_position[1]); if (cursor_visible) SDL_ShowCursor(SDL_ENABLE); } } void application::resize_window(int width, int height) { int x = (display_dimensions[0] >> 1) - (width >> 1); int y = (display_dimensions[1] >> 1) - (height >> 1); // Resize and center window SDL_SetWindowPosition(sdl_window, x, y); SDL_SetWindowSize(sdl_window, width, height); window_resized(); } void application::set_fullscreen(bool fullscreen) { if (this->fullscreen != fullscreen) { this->fullscreen = fullscreen; if (fullscreen) { SDL_HideWindow(sdl_window); SDL_SetWindowFullscreen(sdl_window, SDL_WINDOW_FULLSCREEN_DESKTOP); SDL_ShowWindow(sdl_window); } else { SDL_SetWindowFullscreen(sdl_window, 0); SDL_SetWindowBordered(sdl_window, SDL_TRUE); SDL_SetWindowResizable(sdl_window, SDL_TRUE); } } } void application::set_v_sync(bool v_sync) { if (this->v_sync != v_sync) { if (v_sync) { logger->push_task("Enabling adaptive v-sync"); if (SDL_GL_SetSwapInterval(-1) != 0) { logger->pop_task(EXIT_FAILURE); logger->push_task("Enabling synchronized v-sync"); if (SDL_GL_SetSwapInterval(-1) != 0) { logger->pop_task(EXIT_FAILURE); } else { this->v_sync = v_sync; logger->pop_task(EXIT_SUCCESS); } } else { this->v_sync = v_sync; logger->pop_task(EXIT_SUCCESS); } } else { logger->push_task("Disabling v-sync"); if (SDL_GL_SetSwapInterval(0) != 0) { logger->pop_task(EXIT_FAILURE); } else { this->v_sync = v_sync; logger->pop_task(EXIT_SUCCESS); } } } } void application::set_window_opacity(float opacity) { SDL_SetWindowOpacity(sdl_window, opacity); } void application::swap_buffers() { SDL_GL_SwapWindow(sdl_window); } void application::show_window() { SDL_ShowWindow(sdl_window); //SDL_GL_MakeCurrent(sdl_window, sdl_gl_context); } void application::hide_window() { SDL_HideWindow(sdl_window); } void application::add_game_controller_mappings(const void* mappings, std::size_t size) { logger->push_task("Adding SDL game controller mappings"); int mapping_count = SDL_GameControllerAddMappingsFromRW(SDL_RWFromConstMem(mappings, size), 0); if (mapping_count == -1) { logger->pop_task(EXIT_FAILURE); } else { logger->log("Added " + std::to_string(mapping_count) + " SDL game controller mappings"); logger->pop_task(EXIT_SUCCESS); } } void application::process_events() { // Mouse motion event accumulators bool mouse_motion = false; int mouse_x; int mouse_y; int mouse_dx = 0; int mouse_dy = 0; SDL_Event sdl_event; while (SDL_PollEvent(&sdl_event)) { switch (sdl_event.type) { case SDL_KEYDOWN: case SDL_KEYUP: { if (sdl_event.key.repeat == 0) { input::scancode scancode = input::scancode::unknown; if (sdl_event.key.keysym.scancode <= SDL_SCANCODE_APP2) { scancode = input::sdl_scancode_table[sdl_event.key.keysym.scancode]; } if (sdl_event.type == SDL_KEYDOWN) keyboard->press(scancode); else keyboard->release(scancode); } break; } case SDL_MOUSEMOTION: { // More than one mouse motion event is often generated per frame, and may be a source of lag. // Mouse events are accumulated here to prevent excess function calls and allocations mouse_motion = true; mouse_x = sdl_event.motion.x; mouse_y = sdl_event.motion.y; mouse_dx += sdl_event.motion.xrel; mouse_dy += sdl_event.motion.yrel; break; } case SDL_MOUSEBUTTONDOWN: { mouse->press(sdl_event.button.button, sdl_event.button.x, sdl_event.button.y); break; } case SDL_MOUSEBUTTONUP: { mouse->release(sdl_event.button.button, sdl_event.button.x, sdl_event.button.y); break; } case SDL_MOUSEWHEEL: { int direction = (sdl_event.wheel.direction == SDL_MOUSEWHEEL_FLIPPED) ? -1 : 1; mouse->scroll(sdl_event.wheel.x * direction, sdl_event.wheel.y * direction); break; } case SDL_CONTROLLERBUTTONDOWN: { if (sdl_event.cbutton.button != SDL_CONTROLLER_BUTTON_INVALID) { if (auto it = gamepad_map.find(sdl_event.cdevice.which); it != gamepad_map.end()) { input::gamepad_button button = input::sdl_button_table[sdl_event.cbutton.button]; it->second->press(button); } } break; } case SDL_CONTROLLERBUTTONUP: { if (sdl_event.cbutton.button != SDL_CONTROLLER_BUTTON_INVALID) { if (auto it = gamepad_map.find(sdl_event.cdevice.which); it != gamepad_map.end()) { input::gamepad_button button = input::sdl_button_table[sdl_event.cbutton.button]; it->second->release(button); } } break; } case SDL_CONTROLLERAXISMOTION: { if (sdl_event.caxis.axis != SDL_CONTROLLER_AXIS_INVALID) { if (auto it = gamepad_map.find(sdl_event.cdevice.which); it != gamepad_map.end()) { input::gamepad_axis axis = input::sdl_axis_table[sdl_event.caxis.axis]; float value = sdl_event.caxis.value; static const float min = static_cast(std::numeric_limits::min()); static const float max = static_cast(std::numeric_limits::max()); value /= (value < 0.0f) ? -min : max; it->second->move(axis, value); } } break; } case SDL_CONTROLLERDEVICEADDED: { if (SDL_IsGameController(sdl_event.cdevice.which)) { SDL_GameController* sdl_controller = SDL_GameControllerOpen(sdl_event.cdevice.which); std::string controller_name = SDL_GameControllerNameForIndex(sdl_event.cdevice.which); if (sdl_controller) { if (auto it = gamepad_map.find(sdl_event.cdevice.which); it != gamepad_map.end()) { logger->log("Reconnected gamepad \"" + controller_name + "\""); it->second->connect(true); } else { // Get gamepad GUID SDL_Joystick* sdl_joystick = SDL_GameControllerGetJoystick(sdl_controller); SDL_JoystickGUID sdl_guid = SDL_JoystickGetGUID(sdl_joystick); char guid_buffer[33]; std::memset(guid_buffer, '\0', 33); SDL_JoystickGetGUIDString(sdl_guid, &guid_buffer[0], 33); std::string guid(guid_buffer); logger->log("Connected gamepad \"" + controller_name + "\" with GUID: " + guid + ""); // Create new gamepad input::gamepad* gamepad = new input::gamepad(); gamepad->set_event_dispatcher(event_dispatcher); gamepad->set_guid(guid); // Add gamepad to list and map gamepads.push_back(gamepad); gamepad_map[sdl_event.cdevice.which] = gamepad; // Send controller connected event gamepad->connect(false); } } else { logger->error("Failed to connected gamepad \"" + controller_name + "\""); } } break; } case SDL_CONTROLLERDEVICEREMOVED: { SDL_GameController* sdl_controller = SDL_GameControllerFromInstanceID(sdl_event.cdevice.which); if (sdl_controller) { SDL_GameControllerClose(sdl_controller); logger->log("Disconnected gamepad"); if (auto it = gamepad_map.find(sdl_event.cdevice.which); it != gamepad_map.end()) { it->second->disconnect(); } } break; } case SDL_WINDOWEVENT: { if (sdl_event.window.event == SDL_WINDOWEVENT_RESIZED) { window_resized(); } break; } case SDL_QUIT: { close(); break; } } } // Process accumulated mouse motion events if (mouse_motion) { mouse->move(mouse_x, mouse_y, mouse_dx, mouse_dy); } } void application::window_resized() { // Update window size and viewport size SDL_GetWindowSize(sdl_window, &window_dimensions[0], &window_dimensions[1]); SDL_GL_GetDrawableSize(sdl_window, &viewport_dimensions[0], &viewport_dimensions[1]); rasterizer->context_resized(viewport_dimensions[0], viewport_dimensions[1]); window_resized_event event; event.w = window_dimensions[0]; event.h = window_dimensions[1]; event_dispatcher->queue(event); }