stella/src/common/FBBackendSDL2.cxx

643 lines
18 KiB
C++

//============================================================================
//
// SSSS tt lll lll
// SS SS tt ll ll
// SS tttttt eeee ll ll aaaa
// SSSS tt ee ee ll ll aa
// SS tt eeeeee ll ll aaaaa -- "An Atari 2600 VCS Emulator"
// SS SS tt ee ll ll aa aa
// SSSS ttt eeeee llll llll aaaaa
//
// Copyright (c) 1995-2020 by Bradford W. Mott, Stephen Anthony
// and the Stella Team
//
// See the file "License.txt" for information on usage and redistribution of
// this file, and for a DISCLAIMER OF ALL WARRANTIES.
//============================================================================
#include <cmath>
#include "SDL_lib.hxx"
#include "bspf.hxx"
#include "Logger.hxx"
#include "Console.hxx"
#include "OSystem.hxx"
#include "Settings.hxx"
#include "ThreadDebugging.hxx"
#include "FBSurfaceSDL2.hxx"
#include "FBBackendSDL2.hxx"
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
FBBackendSDL2::FBBackendSDL2(OSystem& osystem)
: myOSystem(osystem)
{
ASSERT_MAIN_THREAD;
// Initialize SDL2 context
if(SDL_InitSubSystem(SDL_INIT_VIDEO | SDL_INIT_TIMER) < 0)
{
ostringstream buf;
buf << "ERROR: Couldn't initialize SDL: " << SDL_GetError() << endl;
Logger::error(buf.str());
throw runtime_error("FATAL ERROR");
}
Logger::debug("FBBackendSDL2::FBBackendSDL2 SDL_Init()");
// We need a pixel format for palette value calculations
// It's done this way (vs directly accessing a FBSurfaceSDL2 object)
// since the structure may be needed before any FBSurface's have
// been created
myPixelFormat = SDL_AllocFormat(SDL_PIXELFORMAT_ARGB8888);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
FBBackendSDL2::~FBBackendSDL2()
{
ASSERT_MAIN_THREAD;
SDL_FreeFormat(myPixelFormat);
if(myRenderer)
{
SDL_DestroyRenderer(myRenderer);
myRenderer = nullptr;
}
if(myWindow)
{
SDL_SetWindowFullscreen(myWindow, 0); // on some systems, a crash occurs
// when destroying fullscreen window
SDL_DestroyWindow(myWindow);
myWindow = nullptr;
}
SDL_QuitSubSystem(SDL_INIT_VIDEO | SDL_INIT_TIMER);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void FBBackendSDL2::queryHardware(vector<Common::Size>& fullscreenRes,
vector<Common::Size>& windowedRes,
VariantList& renderers)
{
ASSERT_MAIN_THREAD;
// Get number of displays (for most systems, this will be '1')
myNumDisplays = SDL_GetNumVideoDisplays();
// First get the maximum fullscreen desktop resolution
SDL_DisplayMode display;
for(int i = 0; i < myNumDisplays; ++i)
{
SDL_GetDesktopDisplayMode(i, &display);
fullscreenRes.emplace_back(display.w, display.h);
// evaluate fullscreen display modes (debug only for now)
int numModes = SDL_GetNumDisplayModes(i);
ostringstream s;
s << "Supported video modes (" << numModes << ") for display " << i << ":";
string lastRes = "";
for(int m = 0; m < numModes; ++m)
{
SDL_DisplayMode mode;
ostringstream res;
SDL_GetDisplayMode(i, m, &mode);
res << std::setw(4) << mode.w << "x" << std::setw(4) << mode.h;
if(lastRes != res.str())
{
Logger::debug(s.str());
s.str("");
lastRes = res.str();
s << " " << lastRes << ": ";
}
s << mode.refresh_rate << "Hz";
if(mode.w == display.w && mode.h == display.h && mode.refresh_rate == display.refresh_rate)
s << "* ";
else
s << " ";
}
Logger::debug(s.str());
}
// Now get the maximum windowed desktop resolution
// Try to take into account taskbars, etc, if available
#if SDL_VERSION_ATLEAST(2,0,5)
// Take window title-bar into account; SDL_GetDisplayUsableBounds doesn't do that
int wTop = 0, wLeft = 0, wBottom = 0, wRight = 0;
SDL_Window* tmpWindow = SDL_CreateWindow("", 0, 0, 0, 0, SDL_WINDOW_HIDDEN);
if(tmpWindow != nullptr)
{
SDL_GetWindowBordersSize(tmpWindow, &wTop, &wLeft, &wBottom, &wRight);
SDL_DestroyWindow(tmpWindow);
}
SDL_Rect r;
for(int i = 0; i < myNumDisplays; ++i)
{
// Display bounds minus dock
SDL_GetDisplayUsableBounds(i, &r); // Requires SDL-2.0.5 or higher
r.h -= (wTop + wBottom);
windowedRes.emplace_back(r.w, r.h);
}
#else
for(int i = 0; i < myNumDisplays; ++i)
{
SDL_GetDesktopDisplayMode(i, &display);
windowedRes.emplace_back(display.w, display.h);
}
#endif
struct RenderName
{
string sdlName;
string stellaName;
};
// Create name map for all currently known SDL renderers
static const std::array<RenderName, 6> RENDERER_NAMES = {{
{ "direct3d", "Direct3D" },
{ "metal", "Metal" },
{ "opengl", "OpenGL" },
{ "opengles", "OpenGLES" },
{ "opengles2", "OpenGLES2" },
{ "software", "Software" }
}};
int numDrivers = SDL_GetNumRenderDrivers();
for(int i = 0; i < numDrivers; ++i)
{
SDL_RendererInfo info;
if(SDL_GetRenderDriverInfo(i, &info) == 0)
{
// Map SDL names into nicer Stella names (if available)
bool found = false;
for(size_t j = 0; j < RENDERER_NAMES.size(); ++j)
{
if(RENDERER_NAMES[j].sdlName == info.name)
{
VarList::push_back(renderers, RENDERER_NAMES[j].stellaName, info.name);
found = true;
break;
}
}
if(!found)
VarList::push_back(renderers, info.name, info.name);
}
}
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
bool FBBackendSDL2::isCurrentWindowPositioned() const
{
ASSERT_MAIN_THREAD;
return !myCenter
&& myWindow && !(SDL_GetWindowFlags(myWindow) & SDL_WINDOW_FULLSCREEN_DESKTOP);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Common::Point FBBackendSDL2::getCurrentWindowPos() const
{
ASSERT_MAIN_THREAD;
Common::Point pos;
SDL_GetWindowPosition(myWindow, &pos.x, &pos.y);
return pos;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Int32 FBBackendSDL2::getCurrentDisplayIndex() const
{
ASSERT_MAIN_THREAD;
return SDL_GetWindowDisplayIndex(myWindow);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
bool FBBackendSDL2::setVideoMode(const VideoModeHandler::Mode& mode,
int winIdx, const Common::Point& winPos)
{
ASSERT_MAIN_THREAD;
// If not initialized by this point, then immediately fail
if(SDL_WasInit(SDL_INIT_VIDEO) == 0)
return false;
const bool fullScreen = mode.fsIndex != -1;
bool forceCreateRenderer = false;
Int32 displayIndex = std::min(myNumDisplays, winIdx);
int posX, posY;
myCenter = myOSystem.settings().getBool("center");
if(myCenter)
posX = posY = SDL_WINDOWPOS_CENTERED_DISPLAY(displayIndex);
else
{
posX = winPos.x;
posY = winPos.y;
// Make sure the window is at least partially visibile
int x0 = 0, y0 = 0, x1 = 0, y1 = 0;
for(int display = SDL_GetNumVideoDisplays() - 1; display >= 0; --display)
{
SDL_Rect rect;
if (!SDL_GetDisplayUsableBounds(display, &rect))
{
x0 = std::min(x0, rect.x);
y0 = std::min(y0, rect.y);
x1 = std::max(x1, rect.x + rect.w);
y1 = std::max(y1, rect.y + rect.h);
}
}
posX = BSPF::clamp(posX, x0 - Int32(mode.screenS.w) + 50, x1 - 50);
posY = BSPF::clamp(posY, y0 + 50, y1 - 50);
}
#ifdef ADAPTABLE_REFRESH_SUPPORT
SDL_DisplayMode adaptedSdlMode;
const int gameRefreshRate =
myOSystem.hasConsole() ? myOSystem.console().gameRefreshRate() : 0;
const bool shouldAdapt = fullScreen
&& myOSystem.settings().getBool("tia.fs_refresh")
&& gameRefreshRate
// take care of 59.94 Hz
&& refreshRate() % gameRefreshRate != 0
&& refreshRate() % (gameRefreshRate - 1) != 0;
const bool adaptRefresh = shouldAdapt &&
adaptRefreshRate(displayIndex, adaptedSdlMode);
#else
const bool adaptRefresh = false;
#endif
const uInt32 flags = SDL_WINDOW_ALLOW_HIGHDPI
| (fullScreen ? adaptRefresh ? SDL_WINDOW_FULLSCREEN :
SDL_WINDOW_FULLSCREEN_DESKTOP : 0);
// Don't re-create the window if its display and size hasn't changed,
// as it's not necessary, and causes flashing in fullscreen mode
if(myWindow)
{
const int d = SDL_GetWindowDisplayIndex(myWindow);
int w, h;
SDL_GetWindowSize(myWindow, &w, &h);
if(d != displayIndex || uInt32(w) != mode.screenS.w ||
uInt32(h) != mode.screenS.h || adaptRefresh)
{
SDL_DestroyWindow(myWindow);
myWindow = nullptr;
}
}
if(myWindow)
{
// Even though window size stayed the same, the title may have changed
SDL_SetWindowTitle(myWindow, myScreenTitle.c_str());
SDL_SetWindowPosition(myWindow, posX, posY);
}
else
{
forceCreateRenderer = true;
myWindow = SDL_CreateWindow(myScreenTitle.c_str(), posX, posY,
mode.screenS.w, mode.screenS.h, flags);
if(myWindow == nullptr)
{
string msg = "ERROR: Unable to open SDL window: " + string(SDL_GetError());
Logger::error(msg);
return false;
}
setWindowIcon();
}
#ifdef ADAPTABLE_REFRESH_SUPPORT
if(adaptRefresh)
{
// Switch to mode for adapted refresh rate
if(SDL_SetWindowDisplayMode(myWindow, &adaptedSdlMode) != 0)
{
Logger::error("ERROR: Display refresh rate change failed");
}
else
{
ostringstream msg;
msg << "Display refresh rate changed to "
<< adaptedSdlMode.refresh_rate << " Hz";
Logger::info(msg.str());
}
}
#endif
return createRenderer(forceCreateRenderer);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
bool FBBackendSDL2::adaptRefreshRate(Int32 displayIndex,
SDL_DisplayMode& adaptedSdlMode)
{
ASSERT_MAIN_THREAD;
SDL_DisplayMode sdlMode;
if(SDL_GetCurrentDisplayMode(displayIndex, &sdlMode) != 0)
{
Logger::error("ERROR: Display mode could not be retrieved");
return false;
}
const int currentRefreshRate = sdlMode.refresh_rate;
const int wantedRefreshRate =
myOSystem.hasConsole() ? myOSystem.console().gameRefreshRate() : 0;
// Take care of rounded refresh rates (e.g. 59.94 Hz)
float factor = std::min(float(currentRefreshRate) / wantedRefreshRate,
float(currentRefreshRate) / (wantedRefreshRate - 1));
// Calculate difference taking care of integer factors (e.g. 100/120)
float bestDiff = std::abs(factor - std::round(factor)) / factor;
bool adapt = false;
// Display refresh rate should be an integer factor of the game's refresh rate
// Note: Modes are scanned with size being first priority,
// therefore the size will never change.
// Check for integer factors 1 (60/50 Hz) and 2 (120/100 Hz)
for(int m = 1; m <= 2; ++m)
{
SDL_DisplayMode closestSdlMode;
sdlMode.refresh_rate = wantedRefreshRate * m;
if(SDL_GetClosestDisplayMode(displayIndex, &sdlMode, &closestSdlMode) == nullptr)
{
Logger::error("ERROR: Closest display mode could not be retrieved");
return adapt;
}
factor = std::min(float(sdlMode.refresh_rate) / sdlMode.refresh_rate,
float(sdlMode.refresh_rate) / (sdlMode.refresh_rate - 1));
const float diff = std::abs(factor - std::round(factor)) / factor;
if(diff < bestDiff)
{
bestDiff = diff;
adaptedSdlMode = closestSdlMode;
adapt = true;
}
}
//cerr << "refresh rate adapt ";
//if(adapt)
// cerr << "required (" << currentRefreshRate << " Hz -> " << adaptedSdlMode.refresh_rate << " Hz)";
//else
// cerr << "not required/possible";
//cerr << endl;
// Only change if the display supports a better refresh rate
return adapt;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
bool FBBackendSDL2::createRenderer(bool force)
{
ASSERT_MAIN_THREAD;
// A new renderer is only created when necessary:
// - new myWindow (force = true)
// - no renderer existing
// - different renderer flags
// - different renderer name
bool recreate = force || myRenderer == nullptr;
uInt32 renderFlags = SDL_RENDERER_ACCELERATED;
const string& video = myOSystem.settings().getString("video"); // Render hint
SDL_RendererInfo renderInfo;
if(myOSystem.settings().getBool("vsync")
&& !myOSystem.settings().getBool("turbo")) // V'synced blits option
renderFlags |= SDL_RENDERER_PRESENTVSYNC;
// check renderer flags and name
recreate |= (SDL_GetRendererInfo(myRenderer, &renderInfo) != 0)
|| ((renderInfo.flags & (SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC)) != renderFlags
|| (video != renderInfo.name));
if(recreate)
{
//cerr << "Create new renderer for buffer type #" << int(myBufferType) << endl;
if(myRenderer)
SDL_DestroyRenderer(myRenderer);
if(video != "")
SDL_SetHint(SDL_HINT_RENDER_DRIVER, video.c_str());
myRenderer = SDL_CreateRenderer(myWindow, -1, renderFlags);
detectFeatures();
determineDimensions();
if(myRenderer == nullptr)
{
string msg = "ERROR: Unable to create SDL renderer: " + string(SDL_GetError());
Logger::error(msg);
return false;
}
}
clear();
SDL_RendererInfo renderinfo;
if(SDL_GetRendererInfo(myRenderer, &renderinfo) >= 0)
myOSystem.settings().setValue("video", renderinfo.name);
return true;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void FBBackendSDL2::setTitle(const string& title)
{
ASSERT_MAIN_THREAD;
myScreenTitle = title;
if(myWindow)
SDL_SetWindowTitle(myWindow, title.c_str());
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
string FBBackendSDL2::about() const
{
ASSERT_MAIN_THREAD;
ostringstream out;
out << "Video system: " << SDL_GetCurrentVideoDriver() << endl;
SDL_RendererInfo info;
if(SDL_GetRendererInfo(myRenderer, &info) >= 0)
{
out << " Renderer: " << info.name << endl;
if(info.max_texture_width > 0 && info.max_texture_height > 0)
out << " Max texture: " << info.max_texture_width << "x"
<< info.max_texture_height << endl;
out << " Flags: "
<< ((info.flags & SDL_RENDERER_PRESENTVSYNC) ? "+" : "-") << "vsync, "
<< ((info.flags & SDL_RENDERER_ACCELERATED) ? "+" : "-") << "accel"
<< endl;
}
return out.str();
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void FBBackendSDL2::showCursor(bool show)
{
ASSERT_MAIN_THREAD;
SDL_ShowCursor(show ? SDL_ENABLE : SDL_DISABLE);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void FBBackendSDL2::grabMouse(bool grab)
{
ASSERT_MAIN_THREAD;
SDL_SetRelativeMouseMode(grab ? SDL_TRUE : SDL_FALSE);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
bool FBBackendSDL2::fullScreen() const
{
ASSERT_MAIN_THREAD;
#ifdef WINDOWED_SUPPORT
return SDL_GetWindowFlags(myWindow) & SDL_WINDOW_FULLSCREEN_DESKTOP;
#else
return true;
#endif
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
int FBBackendSDL2::refreshRate() const
{
ASSERT_MAIN_THREAD;
const uInt32 displayIndex = SDL_GetWindowDisplayIndex(myWindow);
SDL_DisplayMode sdlMode;
if(SDL_GetCurrentDisplayMode(displayIndex, &sdlMode) == 0)
return sdlMode.refresh_rate;
if(myWindow != nullptr)
Logger::error("Could not retrieve current display mode");
return 0;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void FBBackendSDL2::renderToScreen()
{
ASSERT_MAIN_THREAD;
// Show all changes made to the renderer
SDL_RenderPresent(myRenderer);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void FBBackendSDL2::setWindowIcon()
{
#if !defined(BSPF_MACOS) && !defined(RETRON77)
#include "stella_icon.hxx"
ASSERT_MAIN_THREAD;
SDL_Surface* surface = SDL_CreateRGBSurfaceFrom(stella_icon, 32, 32, 32,
32 * 4, 0xFF0000, 0x00FF00, 0x0000FF, 0xFF000000);
SDL_SetWindowIcon(myWindow, surface);
SDL_FreeSurface(surface);
#endif
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
unique_ptr<FBSurface> FBBackendSDL2::createSurface(
uInt32 w,
uInt32 h,
ScalingInterpolation inter,
const uInt32* data
) const
{
return make_unique<FBSurfaceSDL2>
(const_cast<FBBackendSDL2&>(*this), w, h, inter, data);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void FBBackendSDL2::readPixels(uInt8* pixels, uInt32 pitch,
const Common::Rect& rect) const
{
ASSERT_MAIN_THREAD;
SDL_Rect r;
r.x = rect.x(); r.y = rect.y();
r.w = rect.w(); r.h = rect.h();
SDL_RenderReadPixels(myRenderer, &r, 0, pixels, pitch);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void FBBackendSDL2::clear()
{
ASSERT_MAIN_THREAD;
SDL_RenderClear(myRenderer);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void FBBackendSDL2::detectFeatures()
{
myRenderTargetSupport = detectRenderTargetSupport();
if(myRenderer && !myRenderTargetSupport)
Logger::info("Render targets are not supported --- QIS not available");
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
bool FBBackendSDL2::detectRenderTargetSupport()
{
ASSERT_MAIN_THREAD;
if(myRenderer == nullptr)
return false;
SDL_RendererInfo info;
SDL_GetRendererInfo(myRenderer, &info);
if(!(info.flags & SDL_RENDERER_TARGETTEXTURE))
return false;
SDL_Texture* tex =
SDL_CreateTexture(myRenderer, myPixelFormat->format,
SDL_TEXTUREACCESS_TARGET, 16, 16);
if(!tex)
return false;
int sdlError = SDL_SetRenderTarget(myRenderer, tex);
SDL_SetRenderTarget(myRenderer, nullptr);
SDL_DestroyTexture(tex);
return sdlError == 0;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void FBBackendSDL2::determineDimensions()
{
ASSERT_MAIN_THREAD;
SDL_GetWindowSize(myWindow, &myWindowW, &myWindowH);
if(myRenderer == nullptr)
{
myRenderW = myWindowW;
myRenderH = myWindowH;
}
else
SDL_GetRendererOutputSize(myRenderer, &myRenderW, &myRenderH);
}