/* ScummVM - Graphic Adventure Engine * * ScummVM 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 "backends/graphics/opengl/opengl-graphics.h" #include "backends/graphics/opengl/texture.h" #include "backends/graphics/opengl/pipelines/pipeline.h" #include "backends/graphics/opengl/pipelines/fixed.h" #include "backends/graphics/opengl/pipelines/shader.h" #include "backends/graphics/opengl/shader.h" #include "common/array.h" #include "common/textconsole.h" #include "common/translation.h" #include "common/algorithm.h" #include "common/file.h" #ifdef USE_OSD #include "common/tokenizer.h" #include "common/rect.h" #endif #include "graphics/conversion.h" #ifdef USE_OSD #include "graphics/fontman.h" #include "graphics/font.h" #endif #ifdef USE_PNG #include "image/png.h" #else #include "image/bmp.h" #endif #ifdef USE_TTS #include "common/text-to-speech.h" #endif namespace OpenGL { OpenGLGraphicsManager::OpenGLGraphicsManager() : _currentState(), _oldState(), _transactionMode(kTransactionNone), _screenChangeID(1 << (sizeof(int) * 8 - 2)), _pipeline(nullptr), _stretchMode(STRETCH_FIT), _defaultFormat(), _defaultFormatAlpha(), _gameScreen(nullptr), _overlay(nullptr), _cursor(nullptr), _cursorHotspotX(0), _cursorHotspotY(0), _cursorHotspotXScaled(0), _cursorHotspotYScaled(0), _cursorWidthScaled(0), _cursorHeightScaled(0), _cursorKeyColor(0), _cursorDontScale(false), _cursorPaletteEnabled(false) #ifdef USE_OSD , _osdMessageChangeRequest(false), _osdMessageAlpha(0), _osdMessageFadeStartTime(0), _osdMessageSurface(nullptr), _osdIconSurface(nullptr) #endif { memset(_gamePalette, 0, sizeof(_gamePalette)); g_context.reset(); } OpenGLGraphicsManager::~OpenGLGraphicsManager() { delete _gameScreen; delete _overlay; delete _cursor; #ifdef USE_OSD delete _osdMessageSurface; delete _osdIconSurface; #endif #if !USE_FORCED_GLES ShaderManager::destroy(); #endif } bool OpenGLGraphicsManager::hasFeature(OSystem::Feature f) const { switch (f) { case OSystem::kFeatureAspectRatioCorrection: case OSystem::kFeatureCursorPalette: case OSystem::kFeatureFilteringMode: case OSystem::kFeatureStretchMode: return true; case OSystem::kFeatureOverlaySupportsAlpha: return _defaultFormatAlpha.aBits() > 3; default: return false; } } void OpenGLGraphicsManager::setFeatureState(OSystem::Feature f, bool enable) { switch (f) { case OSystem::kFeatureAspectRatioCorrection: assert(_transactionMode != kTransactionNone); _currentState.aspectRatioCorrection = enable; break; case OSystem::kFeatureFilteringMode: assert(_transactionMode != kTransactionNone); _currentState.filtering = enable; if (_gameScreen) { _gameScreen->enableLinearFiltering(enable); } if (_cursor) { _cursor->enableLinearFiltering(enable); } break; case OSystem::kFeatureCursorPalette: _cursorPaletteEnabled = enable; updateCursorPalette(); break; default: break; } } bool OpenGLGraphicsManager::getFeatureState(OSystem::Feature f) const { switch (f) { case OSystem::kFeatureAspectRatioCorrection: return _currentState.aspectRatioCorrection; case OSystem::kFeatureFilteringMode: return _currentState.filtering; case OSystem::kFeatureCursorPalette: return _cursorPaletteEnabled; default: return false; } } namespace { const OSystem::GraphicsMode glGraphicsModes[] = { { "opengl", _s("OpenGL"), GFX_OPENGL }, { nullptr, nullptr, 0 } }; } // End of anonymous namespace const OSystem::GraphicsMode *OpenGLGraphicsManager::getSupportedGraphicsModes() const { return glGraphicsModes; } int OpenGLGraphicsManager::getDefaultGraphicsMode() const { return GFX_OPENGL; } bool OpenGLGraphicsManager::setGraphicsMode(int mode) { assert(_transactionMode != kTransactionNone); switch (mode) { case GFX_OPENGL: _currentState.graphicsMode = mode; return true; default: warning("OpenGLGraphicsManager::setGraphicsMode(%d): Unknown graphics mode", mode); return false; } } int OpenGLGraphicsManager::getGraphicsMode() const { return _currentState.graphicsMode; } #ifdef USE_RGB_COLOR Graphics::PixelFormat OpenGLGraphicsManager::getScreenFormat() const { return _currentState.gameFormat; } Common::List OpenGLGraphicsManager::getSupportedFormats() const { Common::List formats; // Our default mode is (memory layout wise) RGBA8888 which is a different // logical layout depending on the endianness. We chose this mode because // it is the only 32bit color mode we can safely assume to be present in // OpenGL and OpenGL ES implementations. Thus, we need to supply different // logical formats based on endianness. #ifdef SCUMM_LITTLE_ENDIAN // ABGR8888 formats.push_back(Graphics::PixelFormat(4, 8, 8, 8, 8, 0, 8, 16, 24)); #else // RGBA8888 formats.push_back(Graphics::PixelFormat(4, 8, 8, 8, 8, 24, 16, 8, 0)); #endif // RGB565 formats.push_back(Graphics::PixelFormat(2, 5, 6, 5, 0, 11, 5, 0, 0)); // RGBA5551 formats.push_back(Graphics::PixelFormat(2, 5, 5, 5, 1, 11, 6, 1, 0)); // RGBA4444 formats.push_back(Graphics::PixelFormat(2, 4, 4, 4, 4, 12, 8, 4, 0)); // These formats are not natively supported by OpenGL ES implementations, // we convert the pixel format internally. #ifdef SCUMM_LITTLE_ENDIAN // RGBA8888 formats.push_back(Graphics::PixelFormat(4, 8, 8, 8, 8, 24, 16, 8, 0)); #else // ABGR8888 formats.push_back(Graphics::PixelFormat(4, 8, 8, 8, 8, 0, 8, 16, 24)); #endif // RGB555, this is used by SCUMM HE 16 bit games. formats.push_back(Graphics::PixelFormat(2, 5, 5, 5, 0, 10, 5, 0, 0)); formats.push_back(Graphics::PixelFormat::createFormatCLUT8()); return formats; } #endif namespace { const OSystem::GraphicsMode glStretchModes[] = { {"center", _s("Center"), STRETCH_CENTER}, {"pixel-perfect", _s("Pixel-perfect scaling"), STRETCH_INTEGRAL}, {"fit", _s("Fit to window"), STRETCH_FIT}, {"stretch", _s("Stretch to window"), STRETCH_STRETCH}, {nullptr, nullptr, 0} }; } // End of anonymous namespace const OSystem::GraphicsMode *OpenGLGraphicsManager::getSupportedStretchModes() const { return glStretchModes; } int OpenGLGraphicsManager::getDefaultStretchMode() const { return STRETCH_FIT; } bool OpenGLGraphicsManager::setStretchMode(int mode) { assert(getTransactionMode() != kTransactionNone); if (mode == _stretchMode) return true; // Check this is a valid mode const OSystem::GraphicsMode *sm = getSupportedStretchModes(); bool found = false; while (sm->name) { if (sm->id == mode) { found = true; break; } sm++; } if (!found) { warning("unknown stretch mode %d", mode); return false; } _stretchMode = mode; return true; } int OpenGLGraphicsManager::getStretchMode() const { return _stretchMode; } void OpenGLGraphicsManager::beginGFXTransaction() { assert(_transactionMode == kTransactionNone); // Start a transaction. _oldState = _currentState; _transactionMode = kTransactionActive; } OSystem::TransactionError OpenGLGraphicsManager::endGFXTransaction() { assert(_transactionMode == kTransactionActive); uint transactionError = OSystem::kTransactionSuccess; bool setupNewGameScreen = false; if ( _oldState.gameWidth != _currentState.gameWidth || _oldState.gameHeight != _currentState.gameHeight) { setupNewGameScreen = true; } #ifdef USE_RGB_COLOR if (_oldState.gameFormat != _currentState.gameFormat) { setupNewGameScreen = true; } // Check whether the requested format can actually be used. Common::List supportedFormats = getSupportedFormats(); // In case the requested format is not usable we will fall back to CLUT8. if (Common::find(supportedFormats.begin(), supportedFormats.end(), _currentState.gameFormat) == supportedFormats.end()) { _currentState.gameFormat = Graphics::PixelFormat::createFormatCLUT8(); transactionError |= OSystem::kTransactionFormatNotSupported; } #endif do { const uint desiredAspect = getDesiredGameAspectRatio(); const uint requestedWidth = _currentState.gameWidth; const uint requestedHeight = intToFrac(requestedWidth) / desiredAspect; if (!loadVideoMode(requestedWidth, requestedHeight, #ifdef USE_RGB_COLOR _currentState.gameFormat #else Graphics::PixelFormat::createFormatCLUT8() #endif ) // HACK: This is really nasty but we don't have any guarantees of // a context existing before, which means we don't know the maximum // supported texture size before this. Thus, we check whether the // requested game resolution is supported over here. || ( _currentState.gameWidth > (uint)g_context.maxTextureSize || _currentState.gameHeight > (uint)g_context.maxTextureSize)) { if (_transactionMode == kTransactionActive) { // Try to setup the old state in case its valid and is // actually different from the new one. if (_oldState.valid && _oldState != _currentState) { // Give some hints on what failed to set up. if ( _oldState.gameWidth != _currentState.gameWidth || _oldState.gameHeight != _currentState.gameHeight) { transactionError |= OSystem::kTransactionSizeChangeFailed; } #ifdef USE_RGB_COLOR if (_oldState.gameFormat != _currentState.gameFormat) { transactionError |= OSystem::kTransactionFormatNotSupported; } #endif if (_oldState.aspectRatioCorrection != _currentState.aspectRatioCorrection) { transactionError |= OSystem::kTransactionAspectRatioFailed; } if (_oldState.graphicsMode != _currentState.graphicsMode) { transactionError |= OSystem::kTransactionModeSwitchFailed; } if (_oldState.filtering != _currentState.filtering) { transactionError |= OSystem::kTransactionFilteringFailed; } // Roll back to the old state. _currentState = _oldState; _transactionMode = kTransactionRollback; // Try to set up the old state. continue; } } // DON'T use error(), as this tries to bring up the debug // console, which WON'T WORK now that we might no have a // proper screen. warning("OpenGLGraphicsManager::endGFXTransaction: Could not load any graphics mode!"); g_system->quit(); } // In case we reach this we have a valid state, yay. _transactionMode = kTransactionNone; _currentState.valid = true; } while (_transactionMode == kTransactionRollback); if (setupNewGameScreen) { delete _gameScreen; _gameScreen = nullptr; #ifdef USE_RGB_COLOR _gameScreen = createSurface(_currentState.gameFormat); #else _gameScreen = createSurface(Graphics::PixelFormat::createFormatCLUT8()); #endif assert(_gameScreen); if (_gameScreen->hasPalette()) { _gameScreen->setPalette(0, 256, _gamePalette); } _gameScreen->allocate(_currentState.gameWidth, _currentState.gameHeight); _gameScreen->enableLinearFiltering(_currentState.filtering); // We fill the screen to all black or index 0 for CLUT8. #ifdef USE_RGB_COLOR if (_currentState.gameFormat.bytesPerPixel == 1) { _gameScreen->fill(0); } else { _gameScreen->fill(_gameScreen->getSurface()->format.RGBToColor(0, 0, 0)); } #else _gameScreen->fill(0); #endif } // Update our display area and cursor scaling. This makes sure we pick up // aspect ratio correction and game screen changes correctly. recalculateDisplayAreas(); recalculateCursorScaling(); // Something changed, so update the screen change ID. ++_screenChangeID; // Since transactionError is a ORd list of TransactionErrors this is // clearly wrong. But our API is simply broken. return (OSystem::TransactionError)transactionError; } int OpenGLGraphicsManager::getScreenChangeID() const { return _screenChangeID; } void OpenGLGraphicsManager::initSize(uint width, uint height, const Graphics::PixelFormat *format) { Graphics::PixelFormat requestedFormat; #ifdef USE_RGB_COLOR if (!format) { requestedFormat = Graphics::PixelFormat::createFormatCLUT8(); } else { requestedFormat = *format; } _currentState.gameFormat = requestedFormat; #endif _currentState.gameWidth = width; _currentState.gameHeight = height; _gameScreenShakeOffset = 0; } int16 OpenGLGraphicsManager::getWidth() const { return _currentState.gameWidth; } int16 OpenGLGraphicsManager::getHeight() const { return _currentState.gameHeight; } void OpenGLGraphicsManager::copyRectToScreen(const void *buf, int pitch, int x, int y, int w, int h) { _gameScreen->copyRectToTexture(x, y, w, h, buf, pitch); } void OpenGLGraphicsManager::fillScreen(uint32 col) { _gameScreen->fill(col); } void OpenGLGraphicsManager::updateScreen() { if (!_gameScreen) { return; } #ifdef USE_OSD if (_osdMessageChangeRequest) { osdMessageUpdateSurface(); } if (_osdIconSurface) { _osdIconSurface->updateGLTexture(); } #endif // We only update the screen when there actually have been any changes. if ( !_forceRedraw && !_cursorNeedsRedraw && !_gameScreen->isDirty() && !(_overlayVisible && _overlay->isDirty()) && !(_cursorVisible && _cursor && _cursor->isDirty()) #ifdef USE_OSD && !_osdMessageSurface && !_osdIconSurface #endif ) { return; } // Update changes to textures. _gameScreen->updateGLTexture(); if (_cursorVisible && _cursor) { _cursor->updateGLTexture(); } _overlay->updateGLTexture(); // Clear the screen buffer. GL_CALL(glClear(GL_COLOR_BUFFER_BIT)); if (!_overlayVisible) { // The scissor test is enabled to: // - Clip the cursor to the game screen // - Clip the game screen when the shake offset is non-zero _backBuffer.enableScissorTest(true); } // Alpha blending is disabled when drawing the screen _backBuffer.enableBlend(Framebuffer::kBlendModeDisabled); // First step: Draw the (virtual) game screen. g_context.getActivePipeline()->drawTexture(_gameScreen->getGLTexture(), _gameDrawRect.left, _gameDrawRect.top, _gameDrawRect.width(), _gameDrawRect.height()); // Second step: Draw the overlay if visible. if (_overlayVisible) { _backBuffer.enableBlend(Framebuffer::kBlendModeTraditionalTransparency); g_context.getActivePipeline()->drawTexture(_overlay->getGLTexture(), 0, 0, _overlayDrawRect.width(), _overlayDrawRect.height()); } // Third step: Draw the cursor if visible. if (_cursorVisible && _cursor) { _backBuffer.enableBlend(Framebuffer::kBlendModePremultipliedTransparency); g_context.getActivePipeline()->drawTexture(_cursor->getGLTexture(), _cursorX - _cursorHotspotXScaled, _cursorY - _cursorHotspotYScaled, _cursorWidthScaled, _cursorHeightScaled); } if (!_overlayVisible) { _backBuffer.enableScissorTest(false); } #ifdef USE_OSD // Fourth step: Draw the OSD. if (_osdMessageSurface || _osdIconSurface) { _backBuffer.enableBlend(Framebuffer::kBlendModeTraditionalTransparency); } if (_osdMessageSurface) { // Update alpha value. const int diff = g_system->getMillis(false) - _osdMessageFadeStartTime; if (diff > 0) { if (diff >= kOSDMessageFadeOutDuration) { // Back to full transparency. _osdMessageAlpha = 0; } else { // Do a fade out. _osdMessageAlpha = kOSDMessageInitialAlpha - diff * kOSDMessageInitialAlpha / kOSDMessageFadeOutDuration; } } // Set the OSD transparency. g_context.getActivePipeline()->setColor(1.0f, 1.0f, 1.0f, _osdMessageAlpha / 100.0f); int dstX = (_windowWidth - _osdMessageSurface->getWidth()) / 2; int dstY = (_windowHeight - _osdMessageSurface->getHeight()) / 2; // Draw the OSD texture. g_context.getActivePipeline()->drawTexture(_osdMessageSurface->getGLTexture(), dstX, dstY, _osdMessageSurface->getWidth(), _osdMessageSurface->getHeight()); // Reset color. g_context.getActivePipeline()->setColor(1.0f, 1.0f, 1.0f, 1.0f); if (_osdMessageAlpha <= 0) { delete _osdMessageSurface; _osdMessageSurface = nullptr; } } if (_osdIconSurface) { int dstX = _windowWidth - _osdIconSurface->getWidth() - kOSDIconRightMargin; int dstY = kOSDIconTopMargin; // Draw the OSD icon texture. g_context.getActivePipeline()->drawTexture(_osdIconSurface->getGLTexture(), dstX, dstY, _osdIconSurface->getWidth(), _osdIconSurface->getHeight()); } #endif _cursorNeedsRedraw = false; _forceRedraw = false; refreshScreen(); } Graphics::Surface *OpenGLGraphicsManager::lockScreen() { return _gameScreen->getSurface(); } void OpenGLGraphicsManager::unlockScreen() { _gameScreen->flagDirty(); } void OpenGLGraphicsManager::setFocusRectangle(const Common::Rect& rect) { } void OpenGLGraphicsManager::clearFocusRectangle() { } int16 OpenGLGraphicsManager::getOverlayWidth() const { if (_overlay) { return _overlay->getWidth(); } else { return 0; } } int16 OpenGLGraphicsManager::getOverlayHeight() const { if (_overlay) { return _overlay->getHeight(); } else { return 0; } } Graphics::PixelFormat OpenGLGraphicsManager::getOverlayFormat() const { return _overlay->getFormat(); } void OpenGLGraphicsManager::copyRectToOverlay(const void *buf, int pitch, int x, int y, int w, int h) { _overlay->copyRectToTexture(x, y, w, h, buf, pitch); } void OpenGLGraphicsManager::clearOverlay() { _overlay->fill(0); } void OpenGLGraphicsManager::grabOverlay(void *buf, int pitch) const { const Graphics::Surface *overlayData = _overlay->getSurface(); const byte *src = (const byte *)overlayData->getPixels(); byte *dst = (byte *)buf; for (uint h = overlayData->h; h > 0; --h) { memcpy(dst, src, overlayData->w * overlayData->format.bytesPerPixel); dst += pitch; src += overlayData->pitch; } } namespace { template void multiplyColorWithAlpha(const byte *src, byte *dst, const uint w, const uint h, const Graphics::PixelFormat &srcFmt, const Graphics::PixelFormat &dstFmt, const uint srcPitch, const uint dstPitch, const SrcColor keyColor) { for (uint y = 0; y < h; ++y) { for (uint x = 0; x < w; ++x) { const uint32 color = *(const SrcColor *)src; if (color == keyColor) { *(DstColor *)dst = 0; } else { byte a, r, g, b; srcFmt.colorToARGB(color, a, r, g, b); if (a != 0xFF) { r = (int) r * a / 255; g = (int) g * a / 255; b = (int) b * a / 255; } *(DstColor *)dst = dstFmt.ARGBToColor(a, r, g, b); } src += sizeof(SrcColor); dst += sizeof(DstColor); } src += srcPitch - w * srcFmt.bytesPerPixel; dst += dstPitch - w * dstFmt.bytesPerPixel; } } } // End of anonymous namespace void OpenGLGraphicsManager::setMouseCursor(const void *buf, uint w, uint h, int hotspotX, int hotspotY, uint32 keycolor, bool dontScale, const Graphics::PixelFormat *format) { _cursorKeyColor = keycolor; _cursorHotspotX = hotspotX; _cursorHotspotY = hotspotY; _cursorDontScale = dontScale; if (!w || !h) { delete _cursor; _cursor = nullptr; return; } Graphics::PixelFormat inputFormat; #ifdef USE_RGB_COLOR if (format) { inputFormat = *format; } else { inputFormat = Graphics::PixelFormat::createFormatCLUT8(); } #else inputFormat = Graphics::PixelFormat::createFormatCLUT8(); #endif // In case the color format has changed we will need to create the texture. if (!_cursor || _cursor->getFormat() != inputFormat) { delete _cursor; _cursor = nullptr; GLenum glIntFormat, glFormat, glType; Graphics::PixelFormat textureFormat; if (inputFormat.bytesPerPixel == 1 || (inputFormat.aBits() && getGLPixelFormat(inputFormat, glIntFormat, glFormat, glType))) { // There is two cases when we can use the cursor format directly. // The first is when it's CLUT8, here color key handling can // always be applied because we use the alpha channel of // _defaultFormatAlpha for that. // The other is when the input format has alpha bits and // furthermore is directly supported. textureFormat = inputFormat; } else { textureFormat = _defaultFormatAlpha; } _cursor = createSurface(textureFormat, true); assert(_cursor); _cursor->enableLinearFiltering(_currentState.filtering); } _cursor->allocate(w, h); if (inputFormat.bytesPerPixel == 1) { // For CLUT8 cursors we can simply copy the input data into the // texture. _cursor->copyRectToTexture(0, 0, w, h, buf, w * inputFormat.bytesPerPixel); } else { // Otherwise it is a bit more ugly because we have to handle a key // color properly. Graphics::Surface *dst = _cursor->getSurface(); const uint srcPitch = w * inputFormat.bytesPerPixel; // Copy the cursor data to the actual texture surface. This will make // sure that the data is also converted to the expected format. // Also multiply the color values with the alpha channel. // The pre-multiplication allows using a blend mode that prevents // color fringes due to filtering. if (dst->format.bytesPerPixel == 2) { if (inputFormat.bytesPerPixel == 2) { multiplyColorWithAlpha((const byte *) buf, (byte *) dst->getPixels(), w, h, inputFormat, dst->format, srcPitch, dst->pitch, keycolor); } else if (inputFormat.bytesPerPixel == 4) { multiplyColorWithAlpha((const byte *) buf, (byte *) dst->getPixels(), w, h, inputFormat, dst->format, srcPitch, dst->pitch, keycolor); } } else { if (inputFormat.bytesPerPixel == 2) { multiplyColorWithAlpha((const byte *) buf, (byte *) dst->getPixels(), w, h, inputFormat, dst->format, srcPitch, dst->pitch, keycolor); } else if (inputFormat.bytesPerPixel == 4) { multiplyColorWithAlpha((const byte *) buf, (byte *) dst->getPixels(), w, h, inputFormat, dst->format, srcPitch, dst->pitch, keycolor); } } // Flag the texture as dirty. _cursor->flagDirty(); } // In case we actually use a palette set that up properly. if (inputFormat.bytesPerPixel == 1) { updateCursorPalette(); } recalculateCursorScaling(); } void OpenGLGraphicsManager::setCursorPalette(const byte *colors, uint start, uint num) { // FIXME: For some reason client code assumes that usage of this function // automatically enables the cursor palette. _cursorPaletteEnabled = true; memcpy(_cursorPalette + start * 3, colors, num * 3); updateCursorPalette(); } void OpenGLGraphicsManager::displayMessageOnOSD(const char *msg) { #ifdef USE_OSD _osdMessageChangeRequest = true; _osdMessageNextData = msg; #endif // USE_OSD } #ifdef USE_OSD void OpenGLGraphicsManager::osdMessageUpdateSurface() { // Split up the lines. Common::Array osdLines; Common::StringTokenizer tokenizer(_osdMessageNextData, "\n"); while (!tokenizer.empty()) { osdLines.push_back(tokenizer.nextToken()); } // Do the actual drawing like the SDL backend. const Graphics::Font *font = getFontOSD(); // Determine a rect which would contain the message string (clipped to the // screen dimensions). const int vOffset = 6; const int lineSpacing = 1; const int lineHeight = font->getFontHeight() + 2 * lineSpacing; uint width = 0; uint height = lineHeight * osdLines.size() + 2 * vOffset; for (uint i = 0; i < osdLines.size(); i++) { width = MAX(width, font->getStringWidth(osdLines[i]) + 14); } // Clip the rect width = MIN(width, _gameDrawRect.width()); height = MIN(height, _gameDrawRect.height()); delete _osdMessageSurface; _osdMessageSurface = nullptr; _osdMessageSurface = createSurface(_defaultFormatAlpha); assert(_osdMessageSurface); // We always filter the osd with GL_LINEAR. This assures it's // readable in case it needs to be scaled and does not affect it // otherwise. _osdMessageSurface->enableLinearFiltering(true); _osdMessageSurface->allocate(width, height); Graphics::Surface *dst = _osdMessageSurface->getSurface(); // Draw a dark gray rect. const uint32 color = dst->format.RGBToColor(40, 40, 40); dst->fillRect(Common::Rect(0, 0, width, height), color); // Render the message in white const uint32 white = dst->format.RGBToColor(255, 255, 255); for (uint i = 0; i < osdLines.size(); ++i) { font->drawString(dst, osdLines[i], 0, i * lineHeight + vOffset + lineSpacing, width, white, Graphics::kTextAlignCenter); } _osdMessageSurface->updateGLTexture(); // Init the OSD display parameters. _osdMessageAlpha = kOSDMessageInitialAlpha; _osdMessageFadeStartTime = g_system->getMillis() + kOSDMessageFadeOutDelay; #ifdef USE_TTS if (ConfMan.hasKey("tts_enabled", "scummvm") && ConfMan.getBool("tts_enabled", "scummvm")) { Common::TextToSpeechManager *ttsMan = g_system->getTextToSpeechManager(); if (ttsMan) ttsMan->say(_osdMessageNextData); } #endif // USE_TTS // Clear the text update request _osdMessageNextData.clear(); _osdMessageChangeRequest = false; } #endif void OpenGLGraphicsManager::displayActivityIconOnOSD(const Graphics::Surface *icon) { #ifdef USE_OSD if (_osdIconSurface) { delete _osdIconSurface; _osdIconSurface = nullptr; // Make sure the icon is cleared on the next update _forceRedraw = true; } if (icon) { Graphics::Surface *converted = icon->convertTo(_defaultFormatAlpha); _osdIconSurface = createSurface(_defaultFormatAlpha); assert(_osdIconSurface); // We always filter the osd with GL_LINEAR. This assures it's // readable in case it needs to be scaled and does not affect it // otherwise. _osdIconSurface->enableLinearFiltering(true); _osdIconSurface->allocate(converted->w, converted->h); Graphics::Surface *dst = _osdIconSurface->getSurface(); // Copy the icon to the texture dst->copyRectToSurface(*converted, 0, 0, Common::Rect(0, 0, converted->w, converted->h)); converted->free(); delete converted; } #endif } void OpenGLGraphicsManager::setPalette(const byte *colors, uint start, uint num) { assert(_gameScreen->hasPalette()); memcpy(_gamePalette + start * 3, colors, num * 3); _gameScreen->setPalette(start, num, colors); // We might need to update the cursor palette here. updateCursorPalette(); } void OpenGLGraphicsManager::grabPalette(byte *colors, uint start, uint num) const { assert(_gameScreen->hasPalette()); memcpy(colors, _gamePalette + start * 3, num * 3); } void OpenGLGraphicsManager::handleResizeImpl(const int width, const int height, const int xdpi, const int ydpi) { // Setup backbuffer size. _backBuffer.setDimensions(width, height); uint overlayWidth = width; uint overlayHeight = height; // WORKAROUND: We can only support surfaces up to the maximum supported // texture size. Thus, in case we encounter a physical size bigger than // this maximum texture size we will simply use an overlay as big as // possible and then scale it to the physical display size. This sounds // bad but actually all recent chips should support full HD resolution // anyway. Thus, it should not be a real issue for modern hardware. if ( overlayWidth > (uint)g_context.maxTextureSize || overlayHeight > (uint)g_context.maxTextureSize) { const frac_t outputAspect = intToFrac(_windowWidth) / _windowHeight; if (outputAspect > (frac_t)FRAC_ONE) { overlayWidth = g_context.maxTextureSize; overlayHeight = intToFrac(overlayWidth) / outputAspect; } else { overlayHeight = g_context.maxTextureSize; overlayWidth = fracToInt(overlayHeight * outputAspect); } } // HACK: We limit the minimal overlay size to 256x200, which is the // minimum of the dimensions of the two resolutions 256x240 (NES) and // 320x200 (many DOS games use this). This hopefully assure that our // GUI has working layouts. overlayWidth = MAX(overlayWidth, 256); overlayHeight = MAX(overlayHeight, 200); // HACK: Reduce the size of the overlay on high DPI screens. overlayWidth = fracToInt(overlayWidth * (intToFrac(90) / xdpi)); overlayHeight = fracToInt(overlayHeight * (intToFrac(90) / ydpi)); if (!_overlay || _overlay->getFormat() != _defaultFormatAlpha) { delete _overlay; _overlay = nullptr; _overlay = createSurface(_defaultFormatAlpha); assert(_overlay); // We always filter the overlay with GL_LINEAR. This assures it's // readable in case it needs to be scaled and does not affect it // otherwise. _overlay->enableLinearFiltering(true); } _overlay->allocate(overlayWidth, overlayHeight); _overlay->fill(0); // Re-setup the scaling for the screen and cursor recalculateDisplayAreas(); recalculateCursorScaling(); // Something changed, so update the screen change ID. ++_screenChangeID; } void OpenGLGraphicsManager::notifyContextCreate(const Graphics::PixelFormat &defaultFormat, const Graphics::PixelFormat &defaultFormatAlpha) { // Initialize context for use. initializeGLContext(); // Initialize pipeline. delete _pipeline; _pipeline = nullptr; #if !USE_FORCED_GLES if (g_context.shadersSupported) { ShaderMan.notifyCreate(); _pipeline = new ShaderPipeline(ShaderMan.query(ShaderManager::kDefault)); } #endif #if !USE_FORCED_GLES2 if (_pipeline == nullptr) { _pipeline = new FixedPipeline(); } #endif g_context.setPipeline(_pipeline); // Disable 3D properties. GL_CALL(glDisable(GL_CULL_FACE)); GL_CALL(glDisable(GL_DEPTH_TEST)); GL_CALL(glDisable(GL_DITHER)); g_context.getActivePipeline()->setColor(1.0f, 1.0f, 1.0f, 1.0f); // Setup backbuffer state. // Default to black as clear color. _backBuffer.setClearColor(0.0f, 0.0f, 0.0f, 0.0f); g_context.getActivePipeline()->setFramebuffer(&_backBuffer); // We use a "pack" alignment (when reading from textures) to 4 here, // since the only place where we really use it is the BMP screenshot // code and that requires the same alignment too. GL_CALL(glPixelStorei(GL_PACK_ALIGNMENT, 4)); // Refresh the output screen dimensions if some are set up. if (_windowWidth != 0 && _windowHeight != 0) { handleResize(_windowWidth, _windowHeight, _xdpi, _ydpi); } // TODO: Should we try to convert textures into one of those formats if // possible? For example, when _gameScreen is CLUT8 we might want to use // defaultFormat now. _defaultFormat = defaultFormat; _defaultFormatAlpha = defaultFormatAlpha; if (_gameScreen) { _gameScreen->recreate(); } if (_overlay) { _overlay->recreate(); } if (_cursor) { _cursor->recreate(); } #ifdef USE_OSD if (_osdMessageSurface) { _osdMessageSurface->recreate(); } if (_osdIconSurface) { _osdIconSurface->recreate(); } #endif } void OpenGLGraphicsManager::notifyContextDestroy() { if (_gameScreen) { _gameScreen->destroy(); } if (_overlay) { _overlay->destroy(); } if (_cursor) { _cursor->destroy(); } #ifdef USE_OSD if (_osdMessageSurface) { _osdMessageSurface->destroy(); } if (_osdIconSurface) { _osdIconSurface->destroy(); } #endif #if !USE_FORCED_GLES if (g_context.shadersSupported) { ShaderMan.notifyDestroy(); } #endif // Destroy rendering pipeline. g_context.setPipeline(nullptr); delete _pipeline; _pipeline = nullptr; // Rest our context description since the context is gone soon. g_context.reset(); } Surface *OpenGLGraphicsManager::createSurface(const Graphics::PixelFormat &format, bool wantAlpha) { GLenum glIntFormat, glFormat, glType; if (format.bytesPerPixel == 1) { #if !USE_FORCED_GLES if (TextureCLUT8GPU::isSupportedByContext()) { return new TextureCLUT8GPU(); } #endif const Graphics::PixelFormat &virtFormat = wantAlpha ? _defaultFormatAlpha : _defaultFormat; const bool supported = getGLPixelFormat(virtFormat, glIntFormat, glFormat, glType); if (!supported) { return nullptr; } else { return new TextureCLUT8(glIntFormat, glFormat, glType, virtFormat); } #if !USE_FORCED_GL } else if (isGLESContext() && format == Graphics::PixelFormat(2, 5, 5, 5, 0, 10, 5, 0, 0)) { // OpenGL ES does not support a texture format usable for RGB555. // Since SCUMM uses this pixel format for some games (and there is no // hope for this to change anytime soon) we use pixel format // conversion to a supported texture format. return new TextureRGB555(); #ifdef SCUMM_LITTLE_ENDIAN } else if (isGLESContext() && format == Graphics::PixelFormat(4, 8, 8, 8, 8, 24, 16, 8, 0)) { // RGBA8888 #else } else if (isGLESContext() && format == Graphics::PixelFormat(4, 8, 8, 8, 8, 0, 8, 16, 24)) { // ABGR8888 #endif return new TextureRGBA8888Swap(); #endif // !USE_FORCED_GL } else { const bool supported = getGLPixelFormat(format, glIntFormat, glFormat, glType); if (!supported) { return nullptr; } else { return new Texture(glIntFormat, glFormat, glType, format); } } } bool OpenGLGraphicsManager::getGLPixelFormat(const Graphics::PixelFormat &pixelFormat, GLenum &glIntFormat, GLenum &glFormat, GLenum &glType) const { #ifdef SCUMM_LITTLE_ENDIAN if (pixelFormat == Graphics::PixelFormat(4, 8, 8, 8, 8, 0, 8, 16, 24)) { // ABGR8888 #else if (pixelFormat == Graphics::PixelFormat(4, 8, 8, 8, 8, 24, 16, 8, 0)) { // RGBA8888 #endif glIntFormat = GL_RGBA; glFormat = GL_RGBA; glType = GL_UNSIGNED_BYTE; return true; } else if (pixelFormat == Graphics::PixelFormat(2, 5, 6, 5, 0, 11, 5, 0, 0)) { // RGB565 glIntFormat = GL_RGB; glFormat = GL_RGB; glType = GL_UNSIGNED_SHORT_5_6_5; return true; } else if (pixelFormat == Graphics::PixelFormat(2, 5, 5, 5, 1, 11, 6, 1, 0)) { // RGBA5551 glIntFormat = GL_RGBA; glFormat = GL_RGBA; glType = GL_UNSIGNED_SHORT_5_5_5_1; return true; } else if (pixelFormat == Graphics::PixelFormat(2, 4, 4, 4, 4, 12, 8, 4, 0)) { // RGBA4444 glIntFormat = GL_RGBA; glFormat = GL_RGBA; glType = GL_UNSIGNED_SHORT_4_4_4_4; return true; #if !USE_FORCED_GLES && !USE_FORCED_GLES2 // The formats below are not supported by every GLES implementation. // Thus, we do not mark them as supported when a GLES context is setup. } else if (isGLESContext()) { return false; #ifdef SCUMM_LITTLE_ENDIAN } else if (pixelFormat == Graphics::PixelFormat(4, 8, 8, 8, 8, 24, 16, 8, 0)) { // RGBA8888 glIntFormat = GL_RGBA; glFormat = GL_RGBA; glType = GL_UNSIGNED_INT_8_8_8_8; return true; #endif } else if (pixelFormat == Graphics::PixelFormat(2, 5, 5, 5, 0, 10, 5, 0, 0)) { // RGB555 glIntFormat = GL_RGB; glFormat = GL_BGRA; glType = GL_UNSIGNED_SHORT_1_5_5_5_REV; return true; } else if (pixelFormat == Graphics::PixelFormat(2, 4, 4, 4, 4, 8, 4, 0, 12)) { // ARGB4444 glIntFormat = GL_RGBA; glFormat = GL_BGRA; glType = GL_UNSIGNED_SHORT_4_4_4_4_REV; return true; #ifdef SCUMM_BIG_ENDIAN } else if (pixelFormat == Graphics::PixelFormat(4, 8, 8, 8, 8, 0, 8, 16, 24)) { // ABGR8888 glIntFormat = GL_RGBA; glFormat = GL_RGBA; glType = GL_UNSIGNED_INT_8_8_8_8_REV; return true; #endif } else if (pixelFormat == Graphics::PixelFormat(4, 8, 8, 8, 8, 8, 16, 24, 0)) { // BGRA8888 glIntFormat = GL_RGBA; glFormat = GL_BGRA; glType = GL_UNSIGNED_INT_8_8_8_8; return true; } else if (pixelFormat == Graphics::PixelFormat(2, 5, 6, 5, 0, 0, 5, 11, 0)) { // BGR565 glIntFormat = GL_RGB; glFormat = GL_RGB; glType = GL_UNSIGNED_SHORT_5_6_5_REV; return true; } else if (pixelFormat == Graphics::PixelFormat(2, 5, 5, 5, 1, 1, 6, 11, 0)) { // BGRA5551 glIntFormat = GL_RGBA; glFormat = GL_BGRA; glType = GL_UNSIGNED_SHORT_5_5_5_1; return true; } else if (pixelFormat == Graphics::PixelFormat(2, 4, 4, 4, 4, 0, 4, 8, 12)) { // ABGR4444 glIntFormat = GL_RGBA; glFormat = GL_RGBA; glType = GL_UNSIGNED_SHORT_4_4_4_4_REV; return true; } else if (pixelFormat == Graphics::PixelFormat(2, 4, 4, 4, 4, 4, 8, 12, 0)) { // BGRA4444 glIntFormat = GL_RGBA; glFormat = GL_BGRA; glType = GL_UNSIGNED_SHORT_4_4_4_4; return true; #endif // !USE_FORCED_GLES && !USE_FORCED_GLES2 } else { return false; } } bool OpenGLGraphicsManager::gameNeedsAspectRatioCorrection() const { if (_currentState.aspectRatioCorrection) { const uint width = getWidth(); const uint height = getHeight(); // In case we enable aspect ratio correction we force a 4/3 ratio. // But just for 320x200 and 640x400 games, since other games do not need // this. return (width == 320 && height == 200) || (width == 640 && height == 400); } return false; } void OpenGLGraphicsManager::recalculateDisplayAreas() { if (!_gameScreen) { return; } WindowedGraphicsManager::recalculateDisplayAreas(); // Setup drawing limitation for game graphics. // This involves some trickery because OpenGL's viewport coordinate system // is upside down compared to ours. _backBuffer.setScissorBox(_gameDrawRect.left, _windowHeight - _gameDrawRect.height() - _gameDrawRect.top, _gameDrawRect.width(), _gameDrawRect.height()); // Update the cursor position to adjust for new display area. setMousePosition(_cursorX, _cursorY); // Force a redraw to assure screen is properly redrawn. _forceRedraw = true; } void OpenGLGraphicsManager::updateCursorPalette() { if (!_cursor || !_cursor->hasPalette()) { return; } if (_cursorPaletteEnabled) { _cursor->setPalette(0, 256, _cursorPalette); } else { _cursor->setPalette(0, 256, _gamePalette); } _cursor->setColorKey(_cursorKeyColor); } void OpenGLGraphicsManager::recalculateCursorScaling() { if (!_cursor || !_gameScreen) { return; } // By default we use the unscaled versions. _cursorHotspotXScaled = _cursorHotspotX; _cursorHotspotYScaled = _cursorHotspotY; _cursorWidthScaled = _cursor->getWidth(); _cursorHeightScaled = _cursor->getHeight(); // In case scaling is actually enabled we will scale the cursor according // to the game screen. if (!_cursorDontScale) { const frac_t screenScaleFactorX = intToFrac(_gameDrawRect.width()) / _gameScreen->getWidth(); const frac_t screenScaleFactorY = intToFrac(_gameDrawRect.height()) / _gameScreen->getHeight(); _cursorHotspotXScaled = fracToInt(_cursorHotspotXScaled * screenScaleFactorX); _cursorWidthScaled = fracToInt(_cursorWidthScaled * screenScaleFactorX); _cursorHotspotYScaled = fracToInt(_cursorHotspotYScaled * screenScaleFactorY); _cursorHeightScaled = fracToInt(_cursorHeightScaled * screenScaleFactorY); } else { const frac_t screenScaleFactorX = intToFrac(90) / _xdpi; const frac_t screenScaleFactorY = intToFrac(90) / _ydpi; // FIXME: Replace this with integer maths _cursorHotspotXScaled /= fracToDouble(screenScaleFactorX); _cursorWidthScaled /= fracToDouble(screenScaleFactorX); _cursorHotspotYScaled /= fracToDouble(screenScaleFactorY); _cursorHeightScaled /= fracToDouble(screenScaleFactorY); } } #ifdef USE_OSD const Graphics::Font *OpenGLGraphicsManager::getFontOSD() const { return FontMan.getFontByUsage(Graphics::FontManager::kLocalizedFont); } #endif bool OpenGLGraphicsManager::saveScreenshot(const Common::String &filename) const { const uint width = _windowWidth; const uint height = _windowHeight; // A line of a BMP image must have a size divisible by 4. // We calculate the padding bytes needed here. // Since we use a 3 byte per pixel mode, we can use width % 4 here, since // it is equal to 4 - (width * 3) % 4. (4 - (width * Bpp) % 4, is the // usual way of computing the padding bytes required). // GL_PACK_ALIGNMENT is 4, so this line padding is required for PNG too const uint linePaddingSize = width % 4; const uint lineSize = width * 3 + linePaddingSize; Common::DumpFile out; if (!out.open(filename)) { return false; } Common::Array pixels; pixels.resize(lineSize * height); GL_CALL(glReadPixels(0, 0, width, height, GL_RGB, GL_UNSIGNED_BYTE, &pixels.front())); #ifdef SCUMM_LITTLE_ENDIAN const Graphics::PixelFormat format(3, 8, 8, 8, 0, 0, 8, 16, 0); #else const Graphics::PixelFormat format(3, 8, 8, 8, 0, 16, 8, 0, 0); #endif Graphics::Surface data; data.init(width, height, lineSize, &pixels.front(), format); #ifdef USE_PNG return Image::writePNG(out, data, true); #else return Image::writeBMP(out, data, true); #endif } } // End of namespace OpenGL