/*
* Copyright (C) 2020 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 "animation/frame-scheduler.hpp"
#include "application.hpp"
#include "debug/logger.hpp"
#include "debug/performance-sampler.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 "input/keyboard.hpp"
#include "input/mouse.hpp"
#include "input/game-controller.hpp"
#include "rasterizer/rasterizer.hpp"
#include "resources/image.hpp"
#include
#include
#include
#include
#include
#include
#include
#include
application::application():
closed(false),
exit_status(EXIT_SUCCESS),
state({nullptr, nullptr}),
update_callback(nullptr),
render_callback(nullptr),
fullscreen(true),
vsync(true),
cursor_visible(true),
display_dimensions({0, 0}),
window_dimensions({0, 0}),
viewport_dimensions({0, 0}),
mouse_position({0, 0}),
update_rate(60.0),
logger(nullptr),
sdl_window(nullptr),
sdl_gl_context(nullptr)
{
// Setup logging
logger = new ::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_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 get desktop display mode: \"" + std::string(SDL_GetError()) + "\"");
throw std::runtime_error("Failed to detect desktop display mode");
}
else
{
logger->log("Detected " + std::to_string(sdl_desktop_display_mode.w) + "x" + std::to_string(sdl_desktop_display_mode.h) + " display");
display_dimensions = {sdl_desktop_display_mode.w, sdl_desktop_display_mode.h};
}
// 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
(
"",
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);
}
// 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]);
// 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);
}
// Set v-sync mode
int swap_interval = (vsync) ? 1 : 0;
logger->push_task((swap_interval) ? "Enabling v-sync" : "Disabling v-sync");
if (SDL_GL_SetSwapInterval(swap_interval) != 0)
{
logger->pop_task(EXIT_FAILURE);
}
else
{
logger->pop_task(EXIT_SUCCESS);
}
// Init SDL joystick and game controller 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);
}
// Load SDL game controller mappings
/*
logger->push_task("Loading SDL game controller mappings from database");
std::string gamecontrollerdb_path = data_path + "controls/gamecontrollerdb.txt";
if (SDL_GameControllerAddMappingsFromFile(gamecontrollerdb_path.c_str()) == -1)
{
logger->pop_task(EXIT_FAILURE);
}
else
{
logger->pop_task(EXIT_SUCCESS);
}
*/
// Setup rasterizer
rasterizer = new ::rasterizer();
// Setup events
event_dispatcher = new ::event_dispatcher();
// Setup input
keyboard = new ::keyboard();
keyboard->set_event_dispatcher(event_dispatcher);
mouse = new ::mouse();
mouse->set_event_dispatcher(event_dispatcher);
// Setup frame scheduler
frame_scheduler = new ::frame_scheduler();
frame_scheduler->set_update_callback(std::bind(&application::update, this, std::placeholders::_1, std::placeholders::_2));
frame_scheduler->set_render_callback(std::bind(&application::render, this, std::placeholders::_1));
frame_scheduler->set_update_rate(update_rate);
frame_scheduler->set_max_frame_duration(0.25);
// Setup performance sampling
performance_sampler = new ::performance_sampler();
performance_sampler->set_sample_size(15);
}
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(int status)
{
closed = true;
exit_status = status;
}
int application::execute(bootloader_type bootloader)
{
// Execute bootloader
if (bootloader)
{
exit_status = bootloader(this);
if (exit_status != EXIT_SUCCESS)
{
return exit_status;
}
}
// Show window
SDL_ShowWindow(sdl_window);
// Clear window
rasterizer->clear_framebuffer(true, false, false);
SDL_GL_SwapWindow(sdl_window);
// Perform initial update
update(0.0, 0.0);
// Reset frame scheduler
frame_scheduler->reset();
// Schedule frames until closed
while (!closed)
{
// Tick frame scheduler
frame_scheduler->tick();
// Sample frame duration
performance_sampler->sample(frame_scheduler->get_frame_duration());
}
// Exit current state
change_state({nullptr, nullptr});
return exit_status;
}
void application::change_state(const state_type& next_state)
{
// Exit current state
if (state[1])
{
state[1]();
}
// Enter next state
state = next_state;
if (state[0])
{
state[0]();
}
}
std::shared_ptr application::capture_frame() const
{
int w = viewport_dimensions[0];
int h = viewport_dimensions[1];
std::shared_ptr frame = std::make_shared();
frame->format(3, false);
frame->resize(w, h);
// Read pixel data from framebuffer into image
glReadBuffer(GL_BACK);
glReadPixels(0, 0, w, h, GL_RGB, GL_UNSIGNED_BYTE, frame->get_pixels());
return std::move(frame);
}
void application::save_frame(const std::string& path) const
{
logger->push_task("Saving screenshot to \"" + path + "\"");
auto frame = capture_frame();
std::thread
(
[frame, path]
{
stbi_flip_vertically_on_write(1);
stbi_write_png(path.c_str(), frame->get_width(), frame->get_height(), frame->get_channels(), frame->get_pixels(), frame->get_width() * frame->get_channels());
}
).detach();
logger->pop_task(EXIT_SUCCESS);
}
void application::set_update_callback(const update_callback_type& callback)
{
update_callback = callback;
}
void application::set_render_callback(const render_callback_type& callback)
{
render_callback = callback;
}
void application::set_update_rate(double frequency)
{
update_rate = frequency;
frame_scheduler->set_update_rate(update_rate);
}
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_vsync(bool vsync)
{
if (this->vsync != vsync)
{
this->vsync = vsync;
SDL_GL_SetSwapInterval((vsync) ? 1 : 0);
}
}
void application::set_window_opacity(float opacity)
{
SDL_SetWindowOpacity(sdl_window, opacity);
}
void application::swap_buffers()
{
SDL_GL_SwapWindow(sdl_window);
}
void application::update(double t, double dt)
{
translate_sdl_events();
event_dispatcher->update(t);
if (update_callback)
{
update_callback(t, dt);
}
/*
static int frame = 0;
if (frame % 60 == 0)
{
std::cout << std::fixed;
std::cout << std::setprecision(2);
std::cout << performance_sampler->mean_frame_duration() * 1000.0 << "\n";
}
++frame;
*/
}
void application::render(double alpha)
{
/*
std::cout << std::fixed;
std::cout << std::setprecision(2);
std::cout << performance_sampler->mean_frame_duration() * 1000.0 << std::endl;
*/
if (render_callback)
{
render_callback(alpha);
}
SDL_GL_SwapWindow(sdl_window);
}
void application::translate_sdl_events()
{
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)
{
scancode scancode = scancode::unknown;
if (sdl_event.key.keysym.scancode <= SDL_SCANCODE_APP2)
{
scancode = 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:
{
mouse->move(sdl_event.motion.x, sdl_event.motion.y, sdl_event.motion.xrel, 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 = game_controller_map.find(sdl_event.cdevice.which); it != game_controller_map.end())
{
game_controller_button button = 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 = game_controller_map.find(sdl_event.cdevice.which); it != game_controller_map.end())
{
game_controller_button button = 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 = game_controller_map.find(sdl_event.cdevice.which); it != game_controller_map.end())
{
game_controller_axis axis = sdl_axis_table[sdl_event.caxis.axis];
float value = sdl_event.caxis.value;
value /= (value < 0.0f) ? 32768.0f : 32767.0f;
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 = game_controller_map.find(sdl_event.cdevice.which); it != game_controller_map.end())
{
logger->log("Reconnected game controller \"" + controller_name + "\"");
it->second->connect(true);
}
else
{
logger->log("Connected game controller \"" + controller_name + "\"");
game_controller* controller = new game_controller();
controller->set_event_dispatcher(event_dispatcher);
game_controllers.push_back(controller);
game_controller_map[sdl_event.cdevice.which] = controller;
controller->connect(false);
}
}
else
{
logger->error("Failed to connected game controller \"" + 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 game controller");
if (auto it = game_controller_map.find(sdl_event.cdevice.which); it != game_controller_map.end())
{
it->second->disconnect();
}
}
break;
}
case SDL_WINDOWEVENT:
{
if (sdl_event.window.event == SDL_WINDOWEVENT_RESIZED)
{
window_resized();
}
break;
}
case SDL_QUIT:
{
close(EXIT_SUCCESS);
break;
}
}
}
}
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);
/*
rasterizer->window_resized(width, height);
overworld_camera.set_perspective(overworld_camera.get_fov(), aspect_ratio, overworld_camera.get_clip_near(), overworld_camera.get_clip_far());
underworld_camera.set_perspective(underworld_camera.get_fov(), aspect_ratio, underworld_camera.get_clip_near(), underworld_camera.get_clip_far());
control_system->set_viewport(viewport);
camera_system->set_viewport(viewport);
tool_system->set_viewport(viewport);
ui_system->set_viewport(viewport);
*/
}