2016-06-17 19:59:42 +02:00
|
|
|
/* ResidualVM - A 3D game interpreter
|
|
|
|
*
|
|
|
|
* ResidualVM is the legal property of its developers, whose names
|
|
|
|
* are too numerous to list here. Please refer to the COPYRIGHT
|
|
|
|
* file distributed with this source distribution.
|
|
|
|
*
|
|
|
|
* This program 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 2
|
|
|
|
* of the License, or (at your option) any later version.
|
|
|
|
*
|
|
|
|
* This program 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 this program; if not, write to the Free Software
|
|
|
|
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
|
|
|
*
|
|
|
|
*/
|
|
|
|
|
|
|
|
#include "common/scummsys.h"
|
|
|
|
|
|
|
|
#if defined(SDL_BACKEND)
|
|
|
|
|
|
|
|
#include "backends/graphics/openglsdl/openglsdl-graphics.h"
|
2016-06-26 12:43:11 +02:00
|
|
|
|
2018-02-20 20:22:42 +01:00
|
|
|
#include "backends/events/sdl/sdl-events.h"
|
2016-06-17 19:59:42 +02:00
|
|
|
#include "common/config-manager.h"
|
2019-02-12 14:43:18 +08:00
|
|
|
#include "common/file.h"
|
2016-06-26 12:43:11 +02:00
|
|
|
#include "engines/engine.h"
|
2016-06-17 19:59:42 +02:00
|
|
|
#include "graphics/pixelbuffer.h"
|
|
|
|
#include "graphics/opengl/context.h"
|
2016-06-26 12:43:11 +02:00
|
|
|
#include "graphics/opengl/framebuffer.h"
|
|
|
|
#include "graphics/opengl/surfacerenderer.h"
|
|
|
|
#include "graphics/opengl/system_headers.h"
|
|
|
|
#include "graphics/opengl/texture.h"
|
|
|
|
#include "graphics/opengl/tiledsurface.h"
|
2019-02-12 14:43:18 +08:00
|
|
|
#include "image/png.h"
|
2016-06-17 19:59:42 +02:00
|
|
|
|
2016-06-19 10:13:01 +02:00
|
|
|
OpenGLSdlGraphicsManager::OpenGLSdlGraphicsManager(SdlEventSource *sdlEventSource, SdlWindow *window, const Capabilities &capabilities)
|
2016-06-17 19:59:42 +02:00
|
|
|
:
|
2016-06-19 10:13:01 +02:00
|
|
|
ResVmSdlGraphicsManager(sdlEventSource, window, capabilities),
|
2016-06-17 19:59:42 +02:00
|
|
|
#if SDL_VERSION_ATLEAST(2, 0, 0)
|
|
|
|
_glContext(nullptr),
|
|
|
|
#endif
|
2016-06-18 07:56:24 +02:00
|
|
|
_overlayScreen(nullptr),
|
|
|
|
_overlayBackground(nullptr),
|
2016-06-17 19:59:42 +02:00
|
|
|
_gameRect(),
|
|
|
|
_frameBuffer(nullptr),
|
|
|
|
_surfaceRenderer(nullptr) {
|
|
|
|
ConfMan.registerDefault("antialiasing", 0);
|
|
|
|
|
|
|
|
_sideTextures[0] = _sideTextures[1] = nullptr;
|
|
|
|
|
|
|
|
// Don't start at zero so that the value is never the same as the surface graphics manager
|
|
|
|
_screenChangeCount = 1 << (sizeof(int) * 8 - 2);
|
|
|
|
}
|
|
|
|
|
|
|
|
OpenGLSdlGraphicsManager::~OpenGLSdlGraphicsManager() {
|
|
|
|
closeOverlay();
|
2017-08-27 19:24:21 +02:00
|
|
|
#if SDL_VERSION_ATLEAST(2, 0, 0)
|
|
|
|
deinitializeRenderer();
|
|
|
|
#endif
|
2016-06-17 19:59:42 +02:00
|
|
|
}
|
|
|
|
|
2017-12-03 18:33:19 +01:00
|
|
|
bool OpenGLSdlGraphicsManager::hasFeature(OSystem::Feature f) const {
|
2016-06-17 19:59:42 +02:00
|
|
|
return
|
|
|
|
(f == OSystem::kFeatureFullscreenMode) ||
|
|
|
|
(f == OSystem::kFeatureOpenGL) ||
|
2017-08-26 22:22:31 +02:00
|
|
|
#if SDL_VERSION_ATLEAST(2, 0, 0)
|
|
|
|
(f == OSystem::kFeatureFullscreenToggleKeepsContext) ||
|
|
|
|
#endif
|
2016-11-20 13:12:14 +01:00
|
|
|
(f == OSystem::kFeatureVSync) ||
|
2016-06-18 07:56:24 +02:00
|
|
|
(f == OSystem::kFeatureAspectRatioCorrection) ||
|
|
|
|
(f == OSystem::kFeatureOverlaySupportsAlpha && _overlayFormat.aBits() > 3);
|
2016-06-17 19:59:42 +02:00
|
|
|
}
|
|
|
|
|
2017-12-03 18:33:19 +01:00
|
|
|
bool OpenGLSdlGraphicsManager::getFeatureState(OSystem::Feature f) const {
|
2016-11-20 13:12:14 +01:00
|
|
|
switch (f) {
|
|
|
|
case OSystem::kFeatureVSync:
|
|
|
|
return isVSyncEnabled();
|
|
|
|
default:
|
|
|
|
return ResVmSdlGraphicsManager::getFeatureState(f);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-08-26 22:22:31 +02:00
|
|
|
void OpenGLSdlGraphicsManager::setFeatureState(OSystem::Feature f, bool enable) {
|
|
|
|
switch (f) {
|
|
|
|
case OSystem::kFeatureFullscreenMode:
|
|
|
|
if (_fullscreen != enable) {
|
|
|
|
_fullscreen = enable;
|
|
|
|
createOrUpdateScreen();
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
ResVmSdlGraphicsManager::setFeatureState(f, enable);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-06-17 19:59:42 +02:00
|
|
|
void OpenGLSdlGraphicsManager::setupScreen(uint gameWidth, uint gameHeight, bool fullscreen, bool accel3d) {
|
|
|
|
assert(accel3d);
|
|
|
|
closeOverlay();
|
|
|
|
|
2017-08-27 19:24:21 +02:00
|
|
|
#if SDL_VERSION_ATLEAST(2, 0, 0)
|
|
|
|
// Clear the GL context when going from / to the launcher
|
|
|
|
SDL_GL_DeleteContext(_glContext);
|
|
|
|
_glContext = nullptr;
|
|
|
|
#endif
|
|
|
|
|
2017-08-26 22:22:31 +02:00
|
|
|
_engineRequestedWidth = gameWidth;
|
|
|
|
_engineRequestedHeight = gameHeight;
|
2016-06-17 19:59:42 +02:00
|
|
|
_antialiasing = ConfMan.getInt("antialiasing");
|
|
|
|
_fullscreen = fullscreen;
|
|
|
|
_lockAspectRatio = ConfMan.getBool("aspect_ratio");
|
2016-11-20 13:12:14 +01:00
|
|
|
_vsync = ConfMan.getBool("vsync");
|
2016-06-17 19:59:42 +02:00
|
|
|
|
2017-08-26 22:22:31 +02:00
|
|
|
createOrUpdateScreen();
|
2016-06-17 19:59:42 +02:00
|
|
|
|
|
|
|
int glflag;
|
|
|
|
const GLubyte *str;
|
|
|
|
|
|
|
|
str = glGetString(GL_VENDOR);
|
|
|
|
debug("INFO: OpenGL Vendor: %s", str);
|
|
|
|
str = glGetString(GL_RENDERER);
|
|
|
|
debug("INFO: OpenGL Renderer: %s", str);
|
|
|
|
str = glGetString(GL_VERSION);
|
|
|
|
debug("INFO: OpenGL Version: %s", str);
|
|
|
|
SDL_GL_GetAttribute(SDL_GL_RED_SIZE, &glflag);
|
|
|
|
debug("INFO: OpenGL Red bits: %d", glflag);
|
|
|
|
SDL_GL_GetAttribute(SDL_GL_GREEN_SIZE, &glflag);
|
|
|
|
debug("INFO: OpenGL Green bits: %d", glflag);
|
|
|
|
SDL_GL_GetAttribute(SDL_GL_BLUE_SIZE, &glflag);
|
|
|
|
debug("INFO: OpenGL Blue bits: %d", glflag);
|
|
|
|
SDL_GL_GetAttribute(SDL_GL_ALPHA_SIZE, &glflag);
|
|
|
|
debug("INFO: OpenGL Alpha bits: %d", glflag);
|
|
|
|
SDL_GL_GetAttribute(SDL_GL_DEPTH_SIZE, &glflag);
|
|
|
|
debug("INFO: OpenGL Z buffer depth bits: %d", glflag);
|
|
|
|
SDL_GL_GetAttribute(SDL_GL_DOUBLEBUFFER, &glflag);
|
|
|
|
debug("INFO: OpenGL Double Buffer: %d", glflag);
|
|
|
|
SDL_GL_GetAttribute(SDL_GL_STENCIL_SIZE, &glflag);
|
|
|
|
debug("INFO: OpenGL Stencil buffer bits: %d", glflag);
|
|
|
|
#ifdef USE_GLEW
|
|
|
|
debug("INFO: GLEW Version: %s", glewGetString(GLEW_VERSION));
|
2017-08-26 22:22:31 +02:00
|
|
|
#endif
|
|
|
|
#ifdef USE_OPENGL_SHADERS
|
|
|
|
debug("INFO: GLSL version: %s", glGetString(GL_SHADING_LANGUAGE_VERSION));
|
|
|
|
#endif
|
|
|
|
}
|
|
|
|
|
|
|
|
void OpenGLSdlGraphicsManager::createOrUpdateScreen() {
|
|
|
|
closeOverlay();
|
|
|
|
|
2018-07-27 20:17:18 +02:00
|
|
|
// If the game can't adapt to any resolution, render it to a framebuffer
|
|
|
|
// so it can be scaled to fill the available space.
|
|
|
|
bool engineSupportsArbitraryResolutions = !g_engine || g_engine->hasFeature(Engine::kSupportsArbitraryResolutions);
|
|
|
|
bool renderToFrameBuffer = !engineSupportsArbitraryResolutions && _capabilities.openGLFrameBuffer;
|
2017-08-26 22:22:31 +02:00
|
|
|
|
|
|
|
// Choose the effective window size or fullscreen mode
|
|
|
|
uint effectiveWidth;
|
|
|
|
uint effectiveHeight;
|
2018-12-20 06:20:51 +01:00
|
|
|
if (_fullscreen && (engineSupportsArbitraryResolutions || renderToFrameBuffer)) {
|
2017-08-26 22:22:31 +02:00
|
|
|
Common::Rect fullscreenResolution = getPreferredFullscreenResolution();
|
|
|
|
effectiveWidth = fullscreenResolution.width();
|
|
|
|
effectiveHeight = fullscreenResolution.height();
|
|
|
|
} else {
|
|
|
|
effectiveWidth = _engineRequestedWidth;
|
|
|
|
effectiveHeight = _engineRequestedHeight;
|
|
|
|
}
|
|
|
|
|
2018-07-27 20:17:18 +02:00
|
|
|
if (!createOrUpdateGLContext(_engineRequestedWidth, _engineRequestedHeight,
|
|
|
|
effectiveWidth, effectiveHeight, renderToFrameBuffer)) {
|
2018-07-25 09:15:55 +02:00
|
|
|
warning("SDL Error: %s", SDL_GetError());
|
2017-08-26 22:22:31 +02:00
|
|
|
g_system->quit();
|
|
|
|
}
|
|
|
|
|
|
|
|
#ifdef USE_GLEW
|
2016-06-17 19:59:42 +02:00
|
|
|
GLenum err = glewInit();
|
|
|
|
if (err != GLEW_OK) {
|
|
|
|
warning("Error: %s", glewGetErrorString(err));
|
|
|
|
g_system->quit();
|
|
|
|
}
|
|
|
|
#endif
|
|
|
|
|
2018-07-27 20:17:18 +02:00
|
|
|
#if SDL_VERSION_ATLEAST(2, 0, 1)
|
|
|
|
int obtainedWidth = 0, obtainedHeight = 0;
|
|
|
|
SDL_GL_GetDrawableSize(_window->getSDLWindow(), &obtainedWidth, &obtainedHeight);
|
|
|
|
#else
|
|
|
|
int obtainedWidth = effectiveWidth;
|
|
|
|
int obtainedHeight = effectiveHeight;
|
|
|
|
#endif
|
|
|
|
|
|
|
|
// Compute the rectangle where to draw the game inside the effective screen
|
|
|
|
_gameRect = computeGameRect(renderToFrameBuffer, _engineRequestedWidth, _engineRequestedHeight,
|
|
|
|
obtainedWidth, obtainedHeight);
|
|
|
|
|
2016-06-17 19:59:42 +02:00
|
|
|
initializeOpenGLContext();
|
|
|
|
_surfaceRenderer = OpenGL::createBestSurfaceRenderer();
|
|
|
|
|
2017-08-25 06:44:24 +02:00
|
|
|
_overlayFormat = OpenGL::Texture::getRGBAPixelFormat();
|
2018-07-27 20:17:18 +02:00
|
|
|
_overlayScreen = new OpenGL::TiledSurface(obtainedWidth, obtainedHeight, _overlayFormat);
|
2016-06-17 19:59:42 +02:00
|
|
|
|
|
|
|
_screenFormat = _overlayFormat;
|
|
|
|
|
|
|
|
_screenChangeCount++;
|
|
|
|
|
2018-07-27 20:17:18 +02:00
|
|
|
_eventSource->resetKeyboardEmulation(obtainedWidth - 1, obtainedHeight - 1);
|
2018-02-20 20:22:42 +01:00
|
|
|
|
2016-06-17 19:59:42 +02:00
|
|
|
#if !defined(AMIGAOS)
|
2018-07-27 20:17:18 +02:00
|
|
|
if (renderToFrameBuffer) {
|
2016-06-17 19:59:42 +02:00
|
|
|
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
|
2017-08-26 22:22:31 +02:00
|
|
|
_frameBuffer = createFramebuffer(_engineRequestedWidth, _engineRequestedHeight);
|
2016-06-17 19:59:42 +02:00
|
|
|
_frameBuffer->attach();
|
|
|
|
}
|
|
|
|
#endif
|
|
|
|
}
|
|
|
|
|
2018-07-27 20:17:18 +02:00
|
|
|
Math::Rect2d OpenGLSdlGraphicsManager::computeGameRect(bool renderToFrameBuffer, uint gameWidth, uint gameHeight,
|
|
|
|
uint screenWidth, uint screenHeight) {
|
|
|
|
if (renderToFrameBuffer) {
|
|
|
|
if (_lockAspectRatio) {
|
|
|
|
// The game is scaled to fit the screen, keeping the same aspect ratio
|
|
|
|
float scale = MIN(screenHeight / float(gameHeight), screenWidth / float(gameWidth));
|
|
|
|
float scaledW = scale * (gameWidth / float(screenWidth));
|
|
|
|
float scaledH = scale * (gameHeight / float(screenHeight));
|
|
|
|
return Math::Rect2d(
|
|
|
|
Math::Vector2d(0.5 - (0.5 * scaledW), 0.5 - (0.5 * scaledH)),
|
|
|
|
Math::Vector2d(0.5 + (0.5 * scaledW), 0.5 + (0.5 * scaledH))
|
|
|
|
);
|
|
|
|
} else {
|
|
|
|
// The game occupies the whole screen
|
|
|
|
return Math::Rect2d(Math::Vector2d(0, 0), Math::Vector2d(1, 1));
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
return Math::Rect2d(Math::Vector2d(0, 0), Math::Vector2d(1, 1));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void OpenGLSdlGraphicsManager::notifyResize(const uint width, const uint height) {
|
|
|
|
#if SDL_VERSION_ATLEAST(2, 0, 0)
|
|
|
|
// Get the updated size directly from SDL, in case there are multiple
|
|
|
|
// resize events in the message queue.
|
|
|
|
int newWidth = 0, newHeight = 0;
|
|
|
|
SDL_GL_GetDrawableSize(_window->getSDLWindow(), &newWidth, &newHeight);
|
|
|
|
|
|
|
|
if (newWidth == _overlayScreen->getWidth() && newHeight == _overlayScreen->getHeight()) {
|
|
|
|
return; // nothing to do
|
|
|
|
}
|
|
|
|
|
|
|
|
// Compute the rectangle where to draw the game inside the effective screen
|
|
|
|
_gameRect = computeGameRect(_frameBuffer != nullptr,
|
|
|
|
_engineRequestedWidth, _engineRequestedHeight,
|
|
|
|
newWidth, newHeight);
|
|
|
|
|
|
|
|
// Update the overlay
|
|
|
|
delete _overlayScreen;
|
|
|
|
_overlayScreen = new OpenGL::TiledSurface(newWidth, newHeight, _overlayFormat);
|
|
|
|
|
|
|
|
// Clear the overlay background so it is not displayed distorted while resizing
|
|
|
|
delete _overlayBackground;
|
|
|
|
_overlayBackground = nullptr;
|
|
|
|
|
|
|
|
_screenChangeCount++;
|
|
|
|
|
|
|
|
_eventSource->resetKeyboardEmulation(newWidth - 1, newHeight- 1);
|
|
|
|
#endif
|
|
|
|
}
|
|
|
|
|
2016-06-17 19:59:42 +02:00
|
|
|
Graphics::PixelBuffer OpenGLSdlGraphicsManager::getScreenPixelBuffer() {
|
|
|
|
error("Direct screen buffer access is not allowed when using OpenGL");
|
|
|
|
}
|
|
|
|
|
|
|
|
void OpenGLSdlGraphicsManager::initializeOpenGLContext() const {
|
|
|
|
OpenGL::ContextType type;
|
|
|
|
|
|
|
|
#ifdef USE_GLES2
|
|
|
|
type = OpenGL::kContextGLES2;
|
|
|
|
#else
|
|
|
|
type = OpenGL::kContextGL;
|
|
|
|
#endif
|
|
|
|
|
|
|
|
OpenGLContext.initialize(type);
|
2016-11-20 13:12:14 +01:00
|
|
|
|
|
|
|
#if SDL_VERSION_ATLEAST(2, 0, 0)
|
|
|
|
if (SDL_GL_SetSwapInterval(_vsync ? 1 : 0)) {
|
|
|
|
warning("Unable to %s VSync: %s", _vsync ? "enable" : "disable", SDL_GetError());
|
|
|
|
}
|
|
|
|
#endif
|
2016-06-17 19:59:42 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
OpenGLSdlGraphicsManager::OpenGLPixelFormat::OpenGLPixelFormat(uint screenBytesPerPixel, uint red, uint blue, uint green, uint alpha, int samples) :
|
|
|
|
bytesPerPixel(screenBytesPerPixel),
|
|
|
|
redSize(red),
|
|
|
|
blueSize(blue),
|
|
|
|
greenSize(green),
|
|
|
|
alphaSize(alpha),
|
|
|
|
multisampleSamples(samples) {
|
|
|
|
|
|
|
|
}
|
|
|
|
|
2018-07-27 20:17:18 +02:00
|
|
|
bool OpenGLSdlGraphicsManager::createOrUpdateGLContext(uint gameWidth, uint gameHeight,
|
|
|
|
uint effectiveWidth, uint effectiveHeight,
|
|
|
|
bool renderToFramebuffer) {
|
2016-06-17 19:59:42 +02:00
|
|
|
// Build a list of OpenGL pixel formats usable by ResidualVM
|
|
|
|
Common::Array<OpenGLPixelFormat> pixelFormats;
|
2018-07-27 20:17:18 +02:00
|
|
|
if (_antialiasing > 0 && !renderToFramebuffer) {
|
2016-06-17 19:59:42 +02:00
|
|
|
// Don't enable screen level multisampling when rendering to a framebuffer
|
|
|
|
pixelFormats.push_back(OpenGLPixelFormat(32, 8, 8, 8, 8, _antialiasing));
|
|
|
|
pixelFormats.push_back(OpenGLPixelFormat(16, 5, 5, 5, 1, _antialiasing));
|
|
|
|
pixelFormats.push_back(OpenGLPixelFormat(16, 5, 6, 5, 0, _antialiasing));
|
|
|
|
}
|
|
|
|
pixelFormats.push_back(OpenGLPixelFormat(32, 8, 8, 8, 8, 0));
|
|
|
|
pixelFormats.push_back(OpenGLPixelFormat(16, 5, 5, 5, 1, 0));
|
|
|
|
pixelFormats.push_back(OpenGLPixelFormat(16, 5, 6, 5, 0, 0));
|
|
|
|
|
|
|
|
// Unfortunatly, SDL does not provide a list of valid pixel formats
|
|
|
|
// for the current OpenGL implementation and hardware.
|
|
|
|
// SDL may not be able to create a screen with the preferred pixel format.
|
|
|
|
// Try all the pixel formats in the list until SDL returns a valid screen.
|
|
|
|
Common::Array<OpenGLPixelFormat>::const_iterator it = pixelFormats.begin();
|
|
|
|
for (; it != pixelFormats.end(); it++) {
|
|
|
|
SDL_GL_SetAttribute(SDL_GL_RED_SIZE, it->redSize);
|
|
|
|
SDL_GL_SetAttribute(SDL_GL_GREEN_SIZE, it->greenSize);
|
|
|
|
SDL_GL_SetAttribute(SDL_GL_BLUE_SIZE, it->blueSize);
|
|
|
|
SDL_GL_SetAttribute(SDL_GL_ALPHA_SIZE, it->alphaSize);
|
|
|
|
SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 24);
|
|
|
|
SDL_GL_SetAttribute(SDL_GL_STENCIL_SIZE, 8);
|
|
|
|
SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1);
|
|
|
|
SDL_GL_SetAttribute(SDL_GL_MULTISAMPLEBUFFERS, it->multisampleSamples > 0);
|
|
|
|
SDL_GL_SetAttribute(SDL_GL_MULTISAMPLESAMPLES, it->multisampleSamples);
|
2016-11-20 13:12:14 +01:00
|
|
|
#if !SDL_VERSION_ATLEAST(2, 0, 0)
|
|
|
|
SDL_GL_SetAttribute(SDL_GL_SWAP_CONTROL, _vsync ? 1 : 0);
|
|
|
|
#endif
|
2016-06-17 19:59:42 +02:00
|
|
|
#if SDL_VERSION_ATLEAST(2, 0, 0)
|
|
|
|
#ifdef USE_GLES2
|
|
|
|
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 2);
|
|
|
|
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 0);
|
|
|
|
SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_ES);
|
|
|
|
#else
|
2018-06-24 15:05:58 +02:00
|
|
|
// AmigaOS4's OpenGL implementation is close to 1.3. Until that changes we need
|
|
|
|
// to use 1.3 as version or residualvm will cease working at all on that platform.
|
2018-07-25 08:34:04 +02:00
|
|
|
// Profile Mask has to be 0 as well.
|
|
|
|
// This will be revised and removed once AmigaOS4 supports 2.x or OpenGLES2.
|
2018-06-24 15:05:58 +02:00
|
|
|
#ifdef __amigaos4__
|
|
|
|
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 1);
|
|
|
|
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 3);
|
|
|
|
#else
|
|
|
|
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 2);
|
|
|
|
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 1);
|
|
|
|
#endif
|
2016-06-17 19:59:42 +02:00
|
|
|
#endif
|
|
|
|
#endif
|
|
|
|
|
|
|
|
#if SDL_VERSION_ATLEAST(2, 0, 0)
|
2018-07-27 20:17:18 +02:00
|
|
|
uint32 sdlflags = SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE;
|
2017-08-26 22:22:31 +02:00
|
|
|
if (_fullscreen) {
|
|
|
|
// On Linux/X11, when toggling to fullscreen, the window manager saves
|
|
|
|
// the window size to be able to restore it when going back to windowed mode.
|
|
|
|
// If the user configured ResidualVM to start in fullscreen mode, we first
|
|
|
|
// create a window and then toggle it to fullscreen to give the window manager
|
|
|
|
// a chance to save the window size. That way if the user switches back
|
|
|
|
// to windowed mode, the window manager has a window size to apply instead
|
|
|
|
// of leaving the window at the fullscreen resolution size.
|
|
|
|
if (!_window->getSDLWindow()) {
|
2018-07-27 20:17:18 +02:00
|
|
|
_window->createOrUpdateWindow(gameWidth, gameHeight, sdlflags);
|
2017-08-26 22:22:31 +02:00
|
|
|
}
|
|
|
|
|
2016-06-17 19:59:42 +02:00
|
|
|
sdlflags |= SDL_WINDOW_FULLSCREEN;
|
2017-08-26 22:22:31 +02:00
|
|
|
}
|
2016-06-17 19:59:42 +02:00
|
|
|
|
2017-08-26 22:16:40 +02:00
|
|
|
if (_window->createOrUpdateWindow(effectiveWidth, effectiveHeight, sdlflags)) {
|
2017-08-26 22:22:31 +02:00
|
|
|
// Get the current GL context from SDL in case the previous one
|
|
|
|
// was destroyed because the window was recreated.
|
|
|
|
_glContext = SDL_GL_GetCurrentContext();
|
|
|
|
if (!_glContext) {
|
|
|
|
_glContext = SDL_GL_CreateContext(_window->getSDLWindow());
|
2019-05-18 15:25:51 +02:00
|
|
|
if (_glContext) {
|
|
|
|
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
|
|
|
|
}
|
2017-08-26 22:22:31 +02:00
|
|
|
}
|
|
|
|
|
2016-06-17 19:59:42 +02:00
|
|
|
if (_glContext) {
|
2017-08-26 22:22:31 +02:00
|
|
|
assert(SDL_GL_GetCurrentWindow() == _window->getSDLWindow());
|
2016-06-17 19:59:42 +02:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
_window->destroyWindow();
|
|
|
|
#else
|
|
|
|
uint32 sdlflags = SDL_OPENGL;
|
|
|
|
if (_fullscreen)
|
|
|
|
sdlflags |= SDL_FULLSCREEN;
|
|
|
|
|
|
|
|
SDL_Surface *screen = SDL_SetVideoMode(effectiveWidth, effectiveHeight, it->bytesPerPixel, sdlflags);
|
|
|
|
if (screen) {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
#endif
|
|
|
|
}
|
|
|
|
|
|
|
|
// Display a warning if the effective pixel format is not the preferred one
|
|
|
|
if (it != pixelFormats.begin() && it != pixelFormats.end()) {
|
|
|
|
bool wantsAA = pixelFormats.front().multisampleSamples > 0;
|
|
|
|
bool gotAA = it->multisampleSamples > 0;
|
|
|
|
|
|
|
|
warning("Couldn't create a %d-bit visual%s, using to %d-bit%s instead",
|
|
|
|
pixelFormats.front().bytesPerPixel,
|
|
|
|
wantsAA && !gotAA ? " with AA" : "",
|
|
|
|
it->bytesPerPixel,
|
|
|
|
wantsAA && !gotAA ? " without AA" : "");
|
|
|
|
}
|
|
|
|
|
|
|
|
return it != pixelFormats.end();
|
|
|
|
}
|
|
|
|
|
2017-12-03 18:33:19 +01:00
|
|
|
bool OpenGLSdlGraphicsManager::isVSyncEnabled() const {
|
2016-11-20 13:12:14 +01:00
|
|
|
#if SDL_VERSION_ATLEAST(2, 0, 0)
|
|
|
|
return SDL_GL_GetSwapInterval() != 0;
|
|
|
|
#else
|
|
|
|
int swapControl = 0;
|
|
|
|
SDL_GL_GetAttribute(SDL_GL_SWAP_CONTROL, &swapControl);
|
|
|
|
return swapControl != 0;
|
|
|
|
#endif
|
|
|
|
}
|
|
|
|
|
2016-06-18 07:41:09 +02:00
|
|
|
void OpenGLSdlGraphicsManager::drawOverlay() {
|
2018-07-27 20:17:18 +02:00
|
|
|
glViewport(0, 0, _overlayScreen->getWidth(), _overlayScreen->getHeight());
|
2016-06-17 19:59:42 +02:00
|
|
|
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
|
|
|
|
|
|
|
|
_surfaceRenderer->prepareState();
|
|
|
|
|
2016-06-18 07:56:24 +02:00
|
|
|
if (_overlayBackground) {
|
|
|
|
_overlayBackground->draw(_surfaceRenderer);
|
2016-06-17 19:59:42 +02:00
|
|
|
}
|
|
|
|
|
2016-06-18 07:56:24 +02:00
|
|
|
_surfaceRenderer->enableAlphaBlending(true);
|
|
|
|
_surfaceRenderer->setFlipY(true);
|
|
|
|
_overlayScreen->draw(_surfaceRenderer);
|
|
|
|
|
2016-06-17 19:59:42 +02:00
|
|
|
_surfaceRenderer->restorePreviousState();
|
|
|
|
}
|
|
|
|
|
2016-06-18 07:41:09 +02:00
|
|
|
void OpenGLSdlGraphicsManager::drawSideTextures() {
|
2016-06-17 19:59:42 +02:00
|
|
|
if (_fullscreen && _lockAspectRatio) {
|
2016-06-18 07:56:24 +02:00
|
|
|
_surfaceRenderer->setFlipY(true);
|
|
|
|
|
2018-07-27 20:17:18 +02:00
|
|
|
const Math::Vector2d nudge(1.0 / float(_overlayScreen->getWidth()), 0);
|
2016-06-17 19:59:42 +02:00
|
|
|
if (_sideTextures[0] != nullptr) {
|
2018-07-27 20:17:18 +02:00
|
|
|
float left = _gameRect.getBottomLeft().getX() - (float(_overlayScreen->getHeight()) / float(_sideTextures[0]->getHeight())) * _sideTextures[0]->getWidth() / float(_overlayScreen->getWidth());
|
2016-06-17 19:59:42 +02:00
|
|
|
Math::Rect2d leftRect(Math::Vector2d(left, 0.0), _gameRect.getBottomLeft() + nudge);
|
2016-06-18 07:56:24 +02:00
|
|
|
_surfaceRenderer->render(_sideTextures[0], leftRect);
|
2016-06-17 19:59:42 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
if (_sideTextures[1] != nullptr) {
|
2018-07-27 20:17:18 +02:00
|
|
|
float right = _gameRect.getTopRight().getX() + (float(_overlayScreen->getHeight()) / float(_sideTextures[1]->getHeight())) * _sideTextures[1]->getWidth() / float(_overlayScreen->getWidth());
|
2016-06-17 19:59:42 +02:00
|
|
|
Math::Rect2d rightRect(_gameRect.getTopRight() - nudge, Math::Vector2d(right, 1.0));
|
2016-06-18 07:56:24 +02:00
|
|
|
_surfaceRenderer->render(_sideTextures[1], rightRect);
|
2016-06-17 19:59:42 +02:00
|
|
|
}
|
2016-06-18 07:56:24 +02:00
|
|
|
|
|
|
|
_surfaceRenderer->setFlipY(false);
|
2016-06-17 19:59:42 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#ifndef AMIGAOS
|
|
|
|
OpenGL::FrameBuffer *OpenGLSdlGraphicsManager::createFramebuffer(uint width, uint height) {
|
|
|
|
#if !defined(USE_GLES2)
|
|
|
|
if (_antialiasing && OpenGLContext.framebufferObjectMultisampleSupported) {
|
|
|
|
return new OpenGL::MultiSampleFrameBuffer(width, height, _antialiasing);
|
|
|
|
} else
|
|
|
|
#endif
|
|
|
|
{
|
|
|
|
return new OpenGL::FrameBuffer(width, height);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
#endif // AMIGAOS
|
|
|
|
|
|
|
|
void OpenGLSdlGraphicsManager::updateScreen() {
|
|
|
|
if (_frameBuffer) {
|
|
|
|
_frameBuffer->detach();
|
2018-07-27 20:17:18 +02:00
|
|
|
glViewport(0, 0, _overlayScreen->getWidth(), _overlayScreen->getHeight());
|
2016-06-17 19:59:42 +02:00
|
|
|
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
|
|
|
|
_surfaceRenderer->prepareState();
|
2016-06-18 07:41:09 +02:00
|
|
|
drawSideTextures();
|
2016-06-17 19:59:42 +02:00
|
|
|
_surfaceRenderer->render(_frameBuffer, _gameRect);
|
|
|
|
_surfaceRenderer->restorePreviousState();
|
|
|
|
}
|
|
|
|
|
|
|
|
if (_overlayVisible) {
|
2016-06-18 07:56:24 +02:00
|
|
|
_overlayScreen->update();
|
|
|
|
|
|
|
|
if (_overlayBackground) {
|
|
|
|
_overlayBackground->update();
|
2016-06-17 19:59:42 +02:00
|
|
|
}
|
|
|
|
|
2016-06-18 07:41:09 +02:00
|
|
|
drawOverlay();
|
2016-06-17 19:59:42 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
#if SDL_VERSION_ATLEAST(2, 0, 0)
|
|
|
|
SDL_GL_SwapWindow(_window->getSDLWindow());
|
|
|
|
#else
|
|
|
|
SDL_GL_SwapBuffers();
|
|
|
|
#endif
|
|
|
|
|
|
|
|
if (_frameBuffer) {
|
|
|
|
_frameBuffer->attach();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-12-03 18:33:19 +01:00
|
|
|
int16 OpenGLSdlGraphicsManager::getHeight() const {
|
2016-06-17 19:59:42 +02:00
|
|
|
// ResidualVM specific
|
|
|
|
if (_frameBuffer)
|
|
|
|
return _frameBuffer->getHeight();
|
|
|
|
else
|
2018-07-27 20:17:18 +02:00
|
|
|
return _overlayScreen->getHeight();
|
2016-06-17 19:59:42 +02:00
|
|
|
}
|
|
|
|
|
2017-12-03 18:33:19 +01:00
|
|
|
int16 OpenGLSdlGraphicsManager::getWidth() const {
|
2016-06-17 19:59:42 +02:00
|
|
|
// ResidualVM specific
|
|
|
|
if (_frameBuffer)
|
|
|
|
return _frameBuffer->getWidth();
|
|
|
|
else
|
2018-07-27 20:17:18 +02:00
|
|
|
return _overlayScreen->getWidth();
|
2016-06-17 19:59:42 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
#pragma mark -
|
|
|
|
#pragma mark --- Overlays ---
|
|
|
|
#pragma mark -
|
|
|
|
|
2016-06-18 08:14:40 +02:00
|
|
|
void OpenGLSdlGraphicsManager::suggestSideTextures(Graphics::Surface *left, Graphics::Surface *right) {
|
2016-06-17 19:59:42 +02:00
|
|
|
delete _sideTextures[0];
|
|
|
|
_sideTextures[0] = nullptr;
|
|
|
|
delete _sideTextures[1];
|
|
|
|
_sideTextures[1] = nullptr;
|
|
|
|
if (left) {
|
|
|
|
_sideTextures[0] = new OpenGL::Texture(*left);
|
|
|
|
}
|
|
|
|
if (right) {
|
|
|
|
_sideTextures[1] = new OpenGL::Texture(*right);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-06-18 07:56:24 +02:00
|
|
|
void OpenGLSdlGraphicsManager::showOverlay() {
|
|
|
|
if (_overlayVisible) {
|
2016-06-17 19:59:42 +02:00
|
|
|
return;
|
2016-06-18 07:56:24 +02:00
|
|
|
}
|
|
|
|
_overlayVisible = true;
|
2016-06-17 19:59:42 +02:00
|
|
|
|
2016-06-18 07:56:24 +02:00
|
|
|
delete _overlayBackground;
|
|
|
|
_overlayBackground = nullptr;
|
2016-06-17 19:59:42 +02:00
|
|
|
|
2016-06-18 07:56:24 +02:00
|
|
|
if (g_engine) {
|
2018-02-20 06:31:46 +01:00
|
|
|
if (_frameBuffer)
|
|
|
|
_frameBuffer->detach();
|
2016-06-18 07:56:24 +02:00
|
|
|
// If there is a game running capture the screen, so that it can be shown "below" the overlay.
|
2018-07-27 20:17:18 +02:00
|
|
|
_overlayBackground = new OpenGL::TiledSurface(_overlayScreen->getWidth(), _overlayScreen->getHeight(), _overlayFormat);
|
2016-06-18 07:56:24 +02:00
|
|
|
Graphics::Surface *background = _overlayBackground->getBackingSurface();
|
|
|
|
glReadPixels(0, 0, background->w, background->h, GL_RGBA, GL_UNSIGNED_BYTE, background->getPixels());
|
2018-02-20 06:31:46 +01:00
|
|
|
if (_frameBuffer)
|
|
|
|
_frameBuffer->attach();
|
2016-06-18 07:56:24 +02:00
|
|
|
}
|
2016-06-17 19:59:42 +02:00
|
|
|
}
|
|
|
|
|
2016-06-18 07:56:24 +02:00
|
|
|
void OpenGLSdlGraphicsManager::hideOverlay() {
|
|
|
|
if (!_overlayVisible) {
|
2016-06-17 19:59:42 +02:00
|
|
|
return;
|
|
|
|
}
|
2016-06-18 07:56:24 +02:00
|
|
|
_overlayVisible = false;
|
2016-06-17 19:59:42 +02:00
|
|
|
|
2016-06-18 07:56:24 +02:00
|
|
|
delete _overlayBackground;
|
|
|
|
_overlayBackground = nullptr;
|
|
|
|
}
|
2016-06-17 19:59:42 +02:00
|
|
|
|
2016-06-18 07:56:24 +02:00
|
|
|
void OpenGLSdlGraphicsManager::copyRectToOverlay(const void *buf, int pitch, int x, int y, int w, int h) {
|
|
|
|
_overlayScreen->copyRectToSurface(buf, pitch, x, y, w, h);
|
|
|
|
}
|
2016-06-17 19:59:42 +02:00
|
|
|
|
2016-06-18 07:56:24 +02:00
|
|
|
void OpenGLSdlGraphicsManager::clearOverlay() {
|
|
|
|
_overlayScreen->fill(0);
|
|
|
|
}
|
2016-06-17 19:59:42 +02:00
|
|
|
|
2017-12-03 18:33:19 +01:00
|
|
|
void OpenGLSdlGraphicsManager::grabOverlay(void *buf, int pitch) const {
|
2016-06-18 07:56:24 +02:00
|
|
|
const Graphics::Surface *overlayData = _overlayScreen->getBackingSurface();
|
2016-06-17 19:59:42 +02:00
|
|
|
|
2016-06-18 07:56:24 +02:00
|
|
|
const byte *src = (const byte *)overlayData->getPixels();
|
|
|
|
byte *dst = (byte *)buf;
|
2016-06-17 19:59:42 +02:00
|
|
|
|
2016-06-18 07:56:24 +02:00
|
|
|
for (uint h = overlayData->h; h > 0; --h) {
|
|
|
|
memcpy(dst, src, overlayData->w * overlayData->format.bytesPerPixel);
|
|
|
|
dst += pitch;
|
|
|
|
src += overlayData->pitch;
|
|
|
|
}
|
2016-06-17 19:59:42 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
void OpenGLSdlGraphicsManager::closeOverlay() {
|
|
|
|
delete _sideTextures[0];
|
|
|
|
delete _sideTextures[1];
|
|
|
|
_sideTextures[0] = _sideTextures[1] = nullptr;
|
|
|
|
|
2016-06-18 07:56:24 +02:00
|
|
|
if (_overlayScreen) {
|
|
|
|
delete _overlayScreen;
|
|
|
|
_overlayScreen = nullptr;
|
2016-06-17 19:59:42 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
delete _surfaceRenderer;
|
|
|
|
_surfaceRenderer = nullptr;
|
|
|
|
|
|
|
|
delete _frameBuffer;
|
|
|
|
_frameBuffer = nullptr;
|
|
|
|
|
|
|
|
OpenGL::Context::destroy();
|
|
|
|
}
|
|
|
|
|
2018-07-27 20:17:18 +02:00
|
|
|
int16 OpenGLSdlGraphicsManager::getOverlayHeight() const {
|
|
|
|
return _overlayScreen->getHeight();
|
|
|
|
}
|
|
|
|
|
|
|
|
int16 OpenGLSdlGraphicsManager::getOverlayWidth() const {
|
|
|
|
return _overlayScreen->getWidth();
|
|
|
|
}
|
|
|
|
|
2016-06-17 19:59:42 +02:00
|
|
|
void OpenGLSdlGraphicsManager::warpMouse(int x, int y) {
|
|
|
|
//ResidualVM specific
|
|
|
|
if (_frameBuffer) {
|
|
|
|
// Scale from game coordinates to screen coordinates
|
2018-07-27 20:17:18 +02:00
|
|
|
x = (x * _gameRect.getWidth() * _overlayScreen->getWidth()) / _frameBuffer->getWidth();
|
|
|
|
y = (y * _gameRect.getHeight() * _overlayScreen->getHeight()) / _frameBuffer->getHeight();
|
2016-06-17 19:59:42 +02:00
|
|
|
|
2018-07-27 20:17:18 +02:00
|
|
|
x += _gameRect.getTopLeft().getX() * _overlayScreen->getWidth();
|
|
|
|
y += _gameRect.getTopLeft().getY() * _overlayScreen->getHeight();
|
2016-06-17 19:59:42 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
_window->warpMouseInWindow(x, y);
|
|
|
|
}
|
|
|
|
|
|
|
|
void OpenGLSdlGraphicsManager::transformMouseCoordinates(Common::Point &point) {
|
|
|
|
if (_overlayVisible || !_frameBuffer)
|
|
|
|
return;
|
|
|
|
|
|
|
|
// Scale from screen coordinates to game coordinates
|
2018-07-27 20:17:18 +02:00
|
|
|
point.x -= _gameRect.getTopLeft().getX() * _overlayScreen->getWidth();
|
|
|
|
point.y -= _gameRect.getTopLeft().getY() * _overlayScreen->getHeight();
|
2016-06-17 19:59:42 +02:00
|
|
|
|
2018-07-27 20:17:18 +02:00
|
|
|
point.x = (point.x * _frameBuffer->getWidth()) / (_gameRect.getWidth() * _overlayScreen->getWidth());
|
|
|
|
point.y = (point.y * _frameBuffer->getHeight()) / (_gameRect.getHeight() * _overlayScreen->getHeight());
|
2016-06-17 19:59:42 +02:00
|
|
|
|
|
|
|
// Make sure we only supply valid coordinates.
|
|
|
|
point.x = CLIP<int16>(point.x, 0, _frameBuffer->getWidth() - 1);
|
|
|
|
point.y = CLIP<int16>(point.y, 0, _frameBuffer->getHeight() - 1);
|
|
|
|
}
|
|
|
|
|
|
|
|
#if SDL_VERSION_ATLEAST(2, 0, 0)
|
|
|
|
void OpenGLSdlGraphicsManager::deinitializeRenderer() {
|
|
|
|
SDL_GL_DeleteContext(_glContext);
|
|
|
|
_glContext = nullptr;
|
|
|
|
}
|
|
|
|
#endif // SDL_VERSION_ATLEAST(2, 0, 0)
|
|
|
|
|
2019-02-12 14:43:18 +08:00
|
|
|
bool OpenGLSdlGraphicsManager::saveScreenshot(const Common::String &file) const {
|
|
|
|
// Largely based on the implementation from ScummVM
|
|
|
|
uint width = _overlayScreen->getWidth();
|
|
|
|
uint height = _overlayScreen->getHeight();
|
|
|
|
|
|
|
|
uint linePaddingSize = width % 4;
|
|
|
|
uint lineSize = width * 3 + linePaddingSize;
|
|
|
|
|
|
|
|
Common::DumpFile out;
|
|
|
|
if (!out.open(file)) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
Common::Array<uint8> pixels;
|
|
|
|
pixels.resize(lineSize * height);
|
|
|
|
|
|
|
|
if (_frameBuffer) {
|
|
|
|
_frameBuffer->detach();
|
|
|
|
}
|
|
|
|
glReadPixels(0, 0, width, height, GL_RGB, GL_UNSIGNED_BYTE, &pixels.front());
|
|
|
|
if (_frameBuffer) {
|
|
|
|
_frameBuffer->attach();
|
|
|
|
}
|
|
|
|
|
|
|
|
#ifdef USE_PNG
|
|
|
|
Graphics::PixelFormat format(3, 8, 8, 8, 0, 16, 8, 0, 0);
|
|
|
|
Graphics::Surface data;
|
|
|
|
data.init(width, height, lineSize, &pixels.front(), format);
|
|
|
|
return Image::writePNG(out, data, true);
|
|
|
|
#else
|
|
|
|
for (uint y = height; y-- > 0;) {
|
|
|
|
uint8 *line = &pixels.front() + y * lineSize;
|
|
|
|
|
|
|
|
for (uint x = width; x > 0; --x, line += 3) {
|
|
|
|
SWAP(line[0], line[2]);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
out.writeByte('B');
|
|
|
|
out.writeByte('M');
|
|
|
|
out.writeUint32LE(height * lineSize + 54);
|
|
|
|
out.writeUint32LE(0);
|
|
|
|
out.writeUint32LE(54);
|
|
|
|
out.writeUint32LE(40);
|
|
|
|
out.writeUint32LE(width);
|
|
|
|
out.writeUint32LE(height);
|
|
|
|
out.writeUint16LE(1);
|
|
|
|
out.writeUint16LE(24);
|
|
|
|
out.writeUint32LE(0);
|
|
|
|
out.writeUint32LE(0);
|
|
|
|
out.writeUint32LE(0);
|
|
|
|
out.writeUint32LE(0);
|
|
|
|
out.writeUint32LE(0);
|
|
|
|
out.writeUint32LE(0);
|
|
|
|
out.write(&pixels.front(), pixels.size());
|
|
|
|
|
|
|
|
return true;
|
|
|
|
#endif
|
|
|
|
}
|
|
|
|
|
2016-06-17 19:59:42 +02:00
|
|
|
#endif
|