stella/src/emucore/FrameBuffer.cxx

1229 lines
39 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-2019 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 "bspf.hxx"
#include "Logger.hxx"
#include "Console.hxx"
#include "EventHandler.hxx"
#include "Event.hxx"
#include "OSystem.hxx"
#include "Settings.hxx"
#include "TIA.hxx"
#include "Sound.hxx"
#include "FBSurface.hxx"
#include "TIASurface.hxx"
#include "FrameBuffer.hxx"
#ifdef DEBUGGER_SUPPORT
#include "Debugger.hxx"
#endif
#ifdef GUI_SUPPORT
#include "Font.hxx"
#include "StellaFont.hxx"
#include "StellaMediumFont.hxx"
#include "StellaLargeFont.hxx"
#include "ConsoleFont.hxx"
#include "Launcher.hxx"
#include "Menu.hxx"
#include "CommandMenu.hxx"
#include "TimeMachine.hxx"
#endif
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
FrameBuffer::FrameBuffer(OSystem& osystem)
: myOSystem(osystem),
myInitializedCount(0),
myPausedCount(0),
myStatsEnabled(false),
myLastScanlines(0),
myGrabMouse(false),
myHiDPIEnabled(false),
myCurrentModeList(nullptr)
{
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
FrameBuffer::~FrameBuffer()
{
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
bool FrameBuffer::initialize()
{
// Get desktop resolution and supported renderers
vector<Common::Size> windowedDisplays;
queryHardware(myFullscreenDisplays, windowedDisplays, myRenderers);
uInt32 query_w = windowedDisplays[0].w, query_h = windowedDisplays[0].h;
// Check the 'maxres' setting, which is an undocumented developer feature
// that specifies the desktop size (not normally set)
const Common::Size& s = myOSystem.settings().getSize("maxres");
if(s.valid())
{
query_w = s.w;
query_h = s.h;
}
// Various parts of the codebase assume a minimum screen size
myAbsDesktopSize.w = std::max(query_w, FBMinimum::Width);
myAbsDesktopSize.h = std::max(query_h, FBMinimum::Height);
myDesktopSize = myAbsDesktopSize;
// Check for HiDPI mode (is it activated, and can we use it?)
myHiDPIEnabled = myOSystem.settings().getBool("hidpi") &&
((myAbsDesktopSize.w / 2) >= FBMinimum::Width) &&
((myAbsDesktopSize.h / 2) >= FBMinimum::Height);
// In HiDPI mode, the desktop resolution is essentially halved
// Later, the output is scaled and rendered in 2x mode
if(hidpiEnabled())
{
myDesktopSize.w = myAbsDesktopSize.w / hidpiScaleFactor();
myDesktopSize.h = myAbsDesktopSize.h / hidpiScaleFactor();
}
#ifdef GUI_SUPPORT
////////////////////////////////////////////////////////////////////
// Create fonts to draw text
// NOTE: the logic determining appropriate font sizes is done here,
// so that the UI classes can just use the font they expect,
// and not worry about it
// This logic should also take into account the size of the
// framebuffer, and try to be intelligent about font sizes
// We can probably add ifdefs to take care of corner cases,
// but that means we've failed to abstract it enough ...
////////////////////////////////////////////////////////////////////
// This font is used in a variety of situations when a really small
// font is needed; we let the specific widget/dialog decide when to
// use it
mySmallFont = make_unique<GUI::Font>(GUI::stellaDesc);
// The general font used in all UI elements
// This is determined by the size of the framebuffer
if(myOSystem.settings().getBool("minimal_ui"))
myFont = make_unique<GUI::Font>(GUI::stellaLargeDesc);
else
myFont = make_unique<GUI::Font>(GUI::stellaMediumDesc);
// The info font used in all UI elements
// This is determined by the size of the framebuffer
myInfoFont = make_unique<GUI::Font>(GUI::consoleDesc);
// The font used by the ROM launcher
const string& lf = myOSystem.settings().getString("launcherfont");
if(lf == "small")
myLauncherFont = make_unique<GUI::Font>(GUI::consoleDesc);
else if(lf == "medium")
myLauncherFont = make_unique<GUI::Font>(GUI::stellaMediumDesc);
else
myLauncherFont = make_unique<GUI::Font>(GUI::stellaLargeDesc);
#endif
// Determine possible TIA windowed zoom levels
uInt32 minZoom = 2 * hidpiScaleFactor();
uInt32 maxZoom = maxWindowSizeForScreen(
TIAConstants::viewableWidth, TIAConstants::viewableHeight,
myAbsDesktopSize.w, myAbsDesktopSize.h);
for(uInt32 zoom = minZoom; zoom <= maxZoom; ++zoom)
{
ostringstream desc;
desc << "Zoom " << zoom << "x";
VarList::push_back(myTIAZoomLevels, desc.str(), zoom);
}
setUIPalette();
myGrabMouse = myOSystem.settings().getBool("grabmouse");
// Create a TIA surface; we need it for rendering TIA images
myTIASurface = make_unique<TIASurface>(myOSystem);
return true;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void FrameBuffer::setUIPalette()
{
// Set palette for GUI (upper area of array)
int palID = 0;
if(myOSystem.settings().getString("uipalette") == "classic")
palID = 1;
else if(myOSystem.settings().getString("uipalette") == "light")
palID = 2;
for(uInt32 i = 0, j = 256; i < kNumColors - 256; ++i, ++j)
{
uInt8 r = (ourGUIColors[palID][i] >> 16) & 0xff;
uInt8 g = (ourGUIColors[palID][i] >> 8) & 0xff;
uInt8 b = ourGUIColors[palID][i] & 0xff;
myPalette[j] = mapRGB(r, g, b);
}
FBSurface::setPalette(myPalette);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
FBInitStatus FrameBuffer::createDisplay(const string& title,
uInt32 width, uInt32 height,
bool honourHiDPI)
{
++myInitializedCount;
myScreenTitle = title;
// In HiDPI mode, all created displays must be scaled by 2x
if(honourHiDPI && hidpiEnabled())
{
width *= hidpiScaleFactor();
height *= hidpiScaleFactor();
}
// A 'windowed' system is defined as one where the window size can be
// larger than the screen size, as there's some sort of window manager
// that takes care of it (all current desktop systems fall in this category)
// However, some systems have no concept of windowing, and have hard limits
// on how large a window can be (ie, the size of the 'desktop' is the
// absolute upper limit on window size)
//
// If the WINDOWED_SUPPORT macro is defined, we treat the system as the
// former type; if not, as the latter type
bool useFullscreen = false;
#ifdef WINDOWED_SUPPORT
// We assume that a desktop of at least minimum acceptable size means that
// we're running on a 'large' system, and the window size requirements
// can be relaxed
// Otherwise, we treat the system as if WINDOWED_SUPPORT is not defined
if(myDesktopSize.w < FBMinimum::Width && myDesktopSize.h < FBMinimum::Height &&
(myDesktopSize.w < width || myDesktopSize.h < height))
return FBInitStatus::FailTooLarge;
useFullscreen = myOSystem.settings().getBool("fullscreen");
#else
// Make sure this mode is even possible
// We only really need to worry about it in non-windowed environments,
// where requesting a window that's too large will probably cause a crash
if(myDesktopSize.w < width || myDesktopSize.h < height)
return FBInitStatus::FailTooLarge;
useFullscreen = true;
#endif
// Set the available video modes for this framebuffer
setAvailableVidModes(width, height);
// Initialize video subsystem (make sure we get a valid mode)
string pre_about = about();
const FrameBuffer::VideoMode& mode = getSavedVidMode(useFullscreen);
if(width <= mode.screen.w && height <= mode.screen.h)
{
// Changing the video mode can take some time, during which the last
// sound played may get 'stuck'
// So we mute the sound until the operation completes
bool oldMuteState = myOSystem.sound().mute(true);
if(setVideoMode(myScreenTitle, mode))
{
myImageRect = mode.image;
myScreenSize = mode.screen;
// Inform TIA surface about new mode
if(myOSystem.eventHandler().state() != EventHandlerState::LAUNCHER &&
myOSystem.eventHandler().state() != EventHandlerState::DEBUGGER)
myTIASurface->initialize(myOSystem.console(), mode);
// Did we get the requested fullscreen state?
myOSystem.settings().setValue("fullscreen", fullScreen());
resetSurfaces();
setCursorState();
myOSystem.sound().mute(oldMuteState);
}
else
{
Logger::log("ERROR: Couldn't initialize video subsystem", 0);
return FBInitStatus::FailNotSupported;
}
}
else
return FBInitStatus::FailTooLarge;
#ifdef GUI_SUPPORT
// Erase any messages from a previous run
myMsg.counter = 0;
// Create surfaces for TIA statistics and general messages
const GUI::Font& f = hidpiEnabled() ? infoFont() : font();
myStatsMsg.color = kColorInfo;
myStatsMsg.w = f.getMaxCharWidth() * 40 + 3;
myStatsMsg.h = (f.getFontHeight() + 2) * 3;
if(!myStatsMsg.surface)
{
myStatsMsg.surface = allocateSurface(myStatsMsg.w, myStatsMsg.h);
myStatsMsg.surface->attributes().blending = true;
myStatsMsg.surface->attributes().blendalpha = 92; //aligned with TimeMachineDialog
myStatsMsg.surface->applyAttributes();
}
if(!myMsg.surface)
myMsg.surface = allocateSurface(FBMinimum::Width, font().getFontHeight()+10);
#endif
// Print initial usage message, but only print it later if the status has changed
if(myInitializedCount == 1)
{
Logger::log(about(), 1);
}
else
{
string post_about = about();
if(post_about != pre_about)
Logger::log(post_about, 1);
}
return FBInitStatus::Success;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void FrameBuffer::update(bool force)
{
// Onscreen messages are a special case and require different handling than
// other objects; they aren't UI dialogs in the normal sense nor are they
// TIA images, and they need to be rendered on top of everything
// The logic is split in two pieces:
// - at the top of ::update(), to determine whether underlying dialogs
// need to be force-redrawn
// - at the bottom of ::update(), to actually draw them (this must come
// last, since they are always drawn on top of everything else).
// Full rendering is required when messages are enabled
force |= myMsg.counter >= 0;
// Detect when a message has been turned off; one last redraw is required
// in this case, to draw over the area that the message occupied
if(myMsg.counter == 0)
myMsg.counter = -1;
switch(myOSystem.eventHandler().state())
{
case EventHandlerState::NONE:
case EventHandlerState::EMULATION:
// Do nothing; emulation mode is handled separately (see below)
return;
case EventHandlerState::PAUSE:
{
// Show a pause message immediately and then every 7 seconds
if(myPausedCount-- <= 0)
{
myPausedCount = uInt32(7 * myOSystem.frameRate());
showMessage("Paused", MessagePosition::MiddleCenter);
}
if(force)
myTIASurface->render();
break; // EventHandlerState::PAUSE
}
#ifdef GUI_SUPPORT
case EventHandlerState::OPTIONSMENU:
{
force |= myOSystem.menu().needsRedraw();
if(force)
{
myTIASurface->render();
myOSystem.menu().draw(force);
}
break; // EventHandlerState::OPTIONSMENU
}
case EventHandlerState::CMDMENU:
{
force |= myOSystem.commandMenu().needsRedraw();
if(force)
{
myTIASurface->render();
myOSystem.commandMenu().draw(force);
}
break; // EventHandlerState::CMDMENU
}
case EventHandlerState::TIMEMACHINE:
{
force |= myOSystem.timeMachine().needsRedraw();
if(force)
{
myTIASurface->render();
myOSystem.timeMachine().draw(force);
}
break; // EventHandlerState::TIMEMACHINE
}
case EventHandlerState::LAUNCHER:
{
force |= myOSystem.launcher().draw(force);
break; // EventHandlerState::LAUNCHER
}
#endif
#ifdef DEBUGGER_SUPPORT
case EventHandlerState::DEBUGGER:
{
force |= myOSystem.debugger().draw(force);
break; // EventHandlerState::DEBUGGER
}
#endif
default:
break;
}
// Draw any pending messages
// The logic here determines whether to draw the message
// If the message is to be disabled, logic inside the draw method
// indicates that, and then the code at the top of this method sees
// the change and redraws everything
if(myMsg.enabled)
force |= drawMessage();
// Push buffers to screen only when necessary
if(force)
renderToScreen();
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void FrameBuffer::updateInEmulationMode(float framesPerSecond)
{
// Update method that is specifically tailored to emulation mode
// Typically called from a thread, so it needs to be separate from
// the normal update() method
//
// We don't worry about selective rendering here; the rendering
// always happens at the full framerate
myTIASurface->render();
// Show frame statistics
if(myStatsMsg.enabled)
drawFrameStats(framesPerSecond);
myLastScanlines = myOSystem.console().tia().frameBufferScanlinesLastFrame();
myPausedCount = 0;
// Draw any pending messages
if(myMsg.enabled)
drawMessage();
// Push buffers to screen
renderToScreen();
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void FrameBuffer::showMessage(const string& message, MessagePosition position,
bool force)
{
#ifdef GUI_SUPPORT
// Only show messages if they've been enabled
if(myMsg.surface == nullptr || !(force || myOSystem.settings().getBool("uimessages")))
return;
// Precompute the message coordinates
myMsg.text = message;
myMsg.counter = uInt32(myOSystem.frameRate()) << 1; // Show message for 2 seconds
if(myMsg.counter == 0) myMsg.counter = 60;
myMsg.color = kBtnTextColor;
myMsg.w = font().getStringWidth(myMsg.text) + 10;
myMsg.h = font().getFontHeight() + 8;
myMsg.surface->setSrcSize(myMsg.w, myMsg.h);
myMsg.surface->setDstSize(myMsg.w * hidpiScaleFactor(), myMsg.h * hidpiScaleFactor());
myMsg.position = position;
myMsg.enabled = true;
#endif
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void FrameBuffer::drawFrameStats(float framesPerSecond)
{
#ifdef GUI_SUPPORT
const ConsoleInfo& info = myOSystem.console().about();
int xPos = 2, yPos = 0;
const GUI::Font& f = hidpiEnabled() ? infoFont() : font();
const int dy = f.getFontHeight() + 2;
ostringstream ss;
myStatsMsg.surface->invalidate();
// draw scanlines
ColorId color = myOSystem.console().tia().frameBufferScanlinesLastFrame() != myLastScanlines ?
kDbgColorRed : myStatsMsg.color;
ss
<< myOSystem.console().tia().frameBufferScanlinesLastFrame()
<< " / "
<< std::fixed << std::setprecision(1) << myOSystem.console().getFramerate()
<< "Hz => "
<< info.DisplayFormat;
myStatsMsg.surface->drawString(f, ss.str(), xPos, yPos,
myStatsMsg.w, color, TextAlign::Left, 0, true, kBGColor);
yPos += dy;
ss.str("");
ss
<< std::fixed << std::setprecision(1) << framesPerSecond
<< "fps @ "
<< std::fixed << std::setprecision(0) << 100 * myOSystem.settings().getFloat("speed")
<< "% speed";
myStatsMsg.surface->drawString(f, ss.str(), xPos, yPos,
myStatsMsg.w, myStatsMsg.color, TextAlign::Left, 0, true, kBGColor);
yPos += dy;
ss.str("");
ss << info.BankSwitch;
if (myOSystem.settings().getBool("dev.settings")) ss << "| Developer";
myStatsMsg.surface->drawString(f, ss.str(), xPos, yPos,
myStatsMsg.w, myStatsMsg.color, TextAlign::Left, 0, true, kBGColor);
myStatsMsg.surface->setDstPos(myImageRect.x() + 10, myImageRect.y() + 8);
myStatsMsg.surface->setDstSize(myStatsMsg.w * hidpiScaleFactor(),
myStatsMsg.h * hidpiScaleFactor());
myStatsMsg.surface->render();
#endif
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void FrameBuffer::toggleFrameStats()
{
showFrameStats(!myStatsEnabled);
myOSystem.settings().setValue(
myOSystem.settings().getBool("dev.settings") ? "dev.stats" : "plr.stats", myStatsEnabled);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void FrameBuffer::showFrameStats(bool enable)
{
myStatsEnabled = myStatsMsg.enabled = enable;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void FrameBuffer::enableMessages(bool enable)
{
if(enable)
{
// Only re-enable frame stats if they were already enabled before
myStatsMsg.enabled = myStatsEnabled;
}
else
{
// Temporarily disable frame stats
myStatsMsg.enabled = false;
// Erase old messages on the screen
myMsg.enabled = false;
myMsg.counter = 0;
update(true); // Force update immediately
}
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
inline bool FrameBuffer::drawMessage()
{
#ifdef GUI_SUPPORT
// Either erase the entire message (when time is reached),
// or show again this frame
if(myMsg.counter == 0)
{
myMsg.enabled = false;
return true;
}
else if(myMsg.counter < 0)
{
myMsg.enabled = false;
return false;
}
// Draw the bounded box and text
const Common::Rect& dst = myMsg.surface->dstRect();
switch(myMsg.position)
{
case MessagePosition::TopLeft:
myMsg.x = 5;
myMsg.y = 5;
break;
case MessagePosition::TopCenter:
myMsg.x = (myImageRect.width() - dst.width()) >> 1;
myMsg.y = 5;
break;
case MessagePosition::TopRight:
myMsg.x = myImageRect.width() - dst.width() - 5;
myMsg.y = 5;
break;
case MessagePosition::MiddleLeft:
myMsg.x = 5;
myMsg.y = (myImageRect.height() - dst.height()) >> 1;
break;
case MessagePosition::MiddleCenter:
myMsg.x = (myImageRect.width() - dst.width()) >> 1;
myMsg.y = (myImageRect.height() - dst.height()) >> 1;
break;
case MessagePosition::MiddleRight:
myMsg.x = myImageRect.width() - dst.width() - 5;
myMsg.y = (myImageRect.height() - dst.height()) >> 1;
break;
case MessagePosition::BottomLeft:
myMsg.x = 5;
myMsg.y = myImageRect.height() - dst.height() - 5;
break;
case MessagePosition::BottomCenter:
myMsg.x = (myImageRect.width() - dst.width()) >> 1;
myMsg.y = myImageRect.height() - dst.height() - 5;
break;
case MessagePosition::BottomRight:
myMsg.x = myImageRect.width() - dst.width() - 5;
myMsg.y = myImageRect.height() - dst.height() - 5;
break;
}
myMsg.surface->setDstPos(myMsg.x + myImageRect.x(), myMsg.y + myImageRect.y());
myMsg.surface->fillRect(1, 1, myMsg.w-2, myMsg.h-2, kBtnColor);
myMsg.surface->frameRect(0, 0, myMsg.w, myMsg.h, kColor);
myMsg.surface->drawString(font(), myMsg.text, 5, 4,
myMsg.w, myMsg.color, TextAlign::Left);
myMsg.surface->render();
myMsg.counter--;
#endif
return true;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void FrameBuffer::setPauseDelay()
{
myPausedCount = uInt32(2 * myOSystem.frameRate());
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
shared_ptr<FBSurface> FrameBuffer::allocateSurface(int w, int h, const uInt32* data)
{
// Add new surface to the list
mySurfaceList.push_back(createSurface(w, h, data));
// And return a pointer to it (pointer should be treated read-only)
return mySurfaceList.at(mySurfaceList.size() - 1);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void FrameBuffer::freeSurfaces()
{
for(auto& s: mySurfaceList)
s->free();
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void FrameBuffer::reloadSurfaces()
{
for(auto& s: mySurfaceList)
s->reload();
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void FrameBuffer::resetSurfaces()
{
// Free all resources for each surface, then reload them
// Due to possible timing and/or synchronization issues, all free()'s
// are done first, then all reload()'s
// Any derived FrameBuffer classes that call this method should be
// aware of these restrictions, and act accordingly
freeSurfaces();
reloadSurfaces();
update(true); // force full update
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void FrameBuffer::setPalette(const uInt32* raw_palette)
{
// Set palette for normal fill
for(int i = 0; i < 256; ++i)
{
uInt8 r = (raw_palette[i] >> 16) & 0xff;
uInt8 g = (raw_palette[i] >> 8) & 0xff;
uInt8 b = raw_palette[i] & 0xff;
myPalette[i] = mapRGB(r, g, b);
}
// Let the TIA surface know about the new palette
myTIASurface->setPalette(myPalette, raw_palette);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void FrameBuffer::stateChanged(EventHandlerState state)
{
// Make sure any onscreen messages are removed
myMsg.enabled = false;
myMsg.counter = 0;
update(true); // force full update
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void FrameBuffer::setFullscreen(bool enable)
{
#ifdef WINDOWED_SUPPORT
// Switching between fullscreen and windowed modes will invariably mean
// that the 'window' resolution changes. Currently, dialogs are not
// able to resize themselves when they are actively being shown
// (they would have to be closed and then re-opened, etc).
// For now, we simply disallow screen switches in such modes
switch(myOSystem.eventHandler().state())
{
case EventHandlerState::EMULATION:
case EventHandlerState::PAUSE:
break; // continue with processing (aka, allow a mode switch)
case EventHandlerState::DEBUGGER:
case EventHandlerState::LAUNCHER:
if(myOSystem.eventHandler().overlay().baseDialogIsActive())
break; // allow a mode switch when there is only one dialog
[[fallthrough]];
default:
return;
}
// Changing the video mode can take some time, during which the last
// sound played may get 'stuck'
// So we mute the sound until the operation completes
bool oldMuteState = myOSystem.sound().mute(true);
const VideoMode& mode = getSavedVidMode(enable);
if(setVideoMode(myScreenTitle, mode))
{
myImageRect = mode.image;
myScreenSize = mode.screen;
// Inform TIA surface about new mode
if(myOSystem.eventHandler().state() != EventHandlerState::LAUNCHER &&
myOSystem.eventHandler().state() != EventHandlerState::DEBUGGER)
myTIASurface->initialize(myOSystem.console(), mode);
// Did we get the requested fullscreen state?
myOSystem.settings().setValue("fullscreen", fullScreen());
resetSurfaces();
setCursorState();
}
myOSystem.sound().mute(oldMuteState);
#endif
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void FrameBuffer::toggleFullscreen()
{
setFullscreen(!fullScreen());
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
bool FrameBuffer::changeVidMode(int direction)
{
EventHandlerState state = myOSystem.eventHandler().state();
bool tiaMode = (state != EventHandlerState::DEBUGGER &&
state != EventHandlerState::LAUNCHER);
// Only applicable when in TIA/emulation mode
if(!tiaMode)
return false;
if(direction == +1)
myCurrentModeList->next();
else if(direction == -1)
myCurrentModeList->previous();
else
return false;
// Changing the video mode can take some time, during which the last
// sound played may get 'stuck'
// So we mute the sound until the operation completes
bool oldMuteState = myOSystem.sound().mute(true);
const VideoMode& mode = myCurrentModeList->current();
if(setVideoMode(myScreenTitle, mode))
{
myImageRect = mode.image;
myScreenSize = mode.screen;
// Inform TIA surface about new mode
myTIASurface->initialize(myOSystem.console(), mode);
resetSurfaces();
showMessage(mode.description);
myOSystem.sound().mute(oldMuteState);
if(fullScreen())
myOSystem.settings().setValue("tia.fs_stretch",
mode.stretch == VideoMode::Stretch::Fill);
else
myOSystem.settings().setValue("tia.zoom", mode.zoom);
return true;
}
myOSystem.sound().mute(oldMuteState);
return false;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void FrameBuffer::setCursorState()
{
// Always grab mouse in emulation (if enabled) and emulating a controller
// that always uses the mouse
bool emulation =
myOSystem.eventHandler().state() == EventHandlerState::EMULATION;
bool analog = myOSystem.hasConsole() ?
(myOSystem.console().leftController().isAnalog() ||
myOSystem.console().rightController().isAnalog()) : false;
bool alwaysUseMouse = BSPF::equalsIgnoreCase("always", myOSystem.settings().getString("usemouse"));
grabMouse(emulation && (analog || alwaysUseMouse) && myGrabMouse);
// Show/hide cursor in UI/emulation mode based on 'cursor' setting
switch(myOSystem.settings().getInt("cursor"))
{
case 0: showCursor(false); break;
case 1: showCursor(emulation); break;
case 2: showCursor(!emulation); break;
case 3: showCursor(true); break;
}
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void FrameBuffer::enableGrabMouse(bool enable)
{
myGrabMouse = enable;
setCursorState();
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void FrameBuffer::toggleGrabMouse()
{
myGrabMouse = !myGrabMouse;
setCursorState();
myOSystem.settings().setValue("grabmouse", myGrabMouse);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
uInt32 FrameBuffer::maxWindowSizeForScreen(uInt32 baseWidth, uInt32 baseHeight,
uInt32 screenWidth, uInt32 screenHeight) const
{
uInt32 multiplier = 1;
for(;;)
{
// Figure out the zoomed size of the window
uInt32 width = baseWidth * multiplier;
uInt32 height = baseHeight * multiplier;
if((width > screenWidth) || (height > screenHeight))
break;
++multiplier;
}
return multiplier > 1 ? multiplier - 1 : 1;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void FrameBuffer::setAvailableVidModes(uInt32 baseWidth, uInt32 baseHeight)
{
myWindowedModeList.clear();
for(auto& mode: myFullscreenModeLists)
mode.clear();
for(size_t i = myFullscreenModeLists.size(); i < myFullscreenDisplays.size(); ++i)
myFullscreenModeLists.push_back(VideoModeList());
// Check if zooming is allowed for this state (currently only allowed
// for TIA screens)
EventHandlerState state = myOSystem.eventHandler().state();
bool tiaMode = (state != EventHandlerState::DEBUGGER &&
state != EventHandlerState::LAUNCHER);
// TIA mode allows zooming at integral factors in windowed modes,
// and also non-integral factors in fullscreen mode
if(tiaMode)
{
// TIA windowed modes
uInt32 minZoom = 2 * hidpiScaleFactor();
uInt32 maxZoom = maxWindowSizeForScreen(baseWidth, baseHeight,
myAbsDesktopSize.w, myAbsDesktopSize.h);
#if 0 // FIXME - does this apply any longer??
// Aspect ratio
uInt32 aspect = myOSystem.settings().getInt(
myOSystem.console().tia().frameLayout() == FrameLayout::ntsc ?
"tia.aspectn" : "tia.aspectp");
#endif
// Determine all zoom levels
for(uInt32 zoom = minZoom; zoom <= maxZoom; ++zoom)
{
ostringstream desc;
desc << "Zoom " << zoom << "x";
VideoMode mode(baseWidth*zoom, baseHeight*zoom, baseWidth*zoom, baseHeight*zoom,
VideoMode::Stretch::Fill, desc.str(), zoom);
myWindowedModeList.add(mode);
}
// TIA fullscreen mode
for(uInt32 i = 0; i < myFullscreenDisplays.size(); ++i)
{
maxZoom = maxWindowSizeForScreen(baseWidth, baseHeight,
myFullscreenDisplays[i].w, myFullscreenDisplays[i].h);
// Add both normal aspect and filled modes
// It's easier to define them both now, and simply switch between
// them when necessary
VideoMode mode1(baseWidth*maxZoom, baseHeight*maxZoom,
myFullscreenDisplays[i].w, myFullscreenDisplays[i].h,
VideoMode::Stretch::Preserve,
"Preserve aspect, no stretch", maxZoom, i);
myFullscreenModeLists[i].add(mode1);
VideoMode mode2(baseWidth*maxZoom, baseHeight*maxZoom,
myFullscreenDisplays[i].w, myFullscreenDisplays[i].h,
VideoMode::Stretch::Fill,
"Ignore aspect, full stretch", maxZoom, i);
myFullscreenModeLists[i].add(mode2);
}
}
else // UI mode
{
// Windowed and fullscreen mode differ only in screen size
myWindowedModeList.add(
VideoMode(baseWidth, baseHeight, baseWidth, baseHeight, VideoMode::Stretch::Fill)
);
for(uInt32 i = 0; i < myFullscreenDisplays.size(); ++i)
{
myFullscreenModeLists[i].add(
VideoMode(baseWidth, baseHeight, myFullscreenDisplays[i].w, myFullscreenDisplays[i].h,
VideoMode::Stretch::Fill, "", 1, i)
);
}
}
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
const FrameBuffer::VideoMode& FrameBuffer::getSavedVidMode(bool fullscreen)
{
EventHandlerState state = myOSystem.eventHandler().state();
if(fullscreen)
{
Int32 i = getCurrentDisplayIndex();
if(i < 0)
{
// default to the first display
i = 0;
}
myCurrentModeList = &myFullscreenModeLists[i];
}
else
myCurrentModeList = &myWindowedModeList;
// Now select the best resolution depending on the state
// UI modes (launcher and debugger) have only one supported resolution
// so the 'current' one is the only valid one
if(state == EventHandlerState::DEBUGGER || state == EventHandlerState::LAUNCHER)
myCurrentModeList->setByZoom(1);
else // TIA mode
{
if(fullscreen)
myCurrentModeList->setByStretch(myOSystem.settings().getBool("tia.fs_stretch")
? VideoMode::Stretch::Fill : VideoMode::Stretch::Preserve);
else
myCurrentModeList->setByZoom(myOSystem.settings().getInt("tia.zoom"));
}
return myCurrentModeList->current();
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
//
// VideoMode implementation
//
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
FrameBuffer::VideoMode::VideoMode()
: stretch(VideoMode::Stretch::Fill),
description(""),
zoom(1),
fsIndex(-1)
{
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
FrameBuffer::VideoMode::VideoMode(uInt32 iw, uInt32 ih, uInt32 sw, uInt32 sh,
Stretch smode, const string& desc,
uInt32 zoomLevel, Int32 fsindex)
: stretch(smode),
description(desc),
zoom(zoomLevel),
fsIndex(fsindex)
{
// First set default size and positioning
sw = std::max(sw, TIAConstants::viewableWidth);
sh = std::max(sh, TIAConstants::viewableHeight);
iw = std::min(iw, sw);
ih = std::min(ih, sh);
int ix = (sw - iw) >> 1;
int iy = (sh - ih) >> 1;
image = Common::Rect(ix, iy, ix+iw, iy+ih);
screen = Common::Size(sw, sh);
// Now resize based on windowed/fullscreen mode and stretch factor
iw = image.width();
ih = image.height();
if(fsIndex != -1)
{
switch(stretch)
{
case Stretch::Preserve:
{
float stretchFactor = 1.0;
float scaleX = float(iw) / screen.w;
float scaleY = float(ih) / screen.h;
// Scale to all available space, keep aspect correct
if(scaleX > scaleY)
stretchFactor = float(screen.w) / iw;
else
stretchFactor = float(screen.h) / ih;
iw = uInt32(stretchFactor * iw);
ih = uInt32(stretchFactor * ih);
break;
}
case Stretch::Fill:
// Scale to all available space
iw = screen.w;
ih = screen.h;
break;
}
}
else
{
// In windowed mode, currently the size is scaled to the screen
// TODO - this may be updated if/when we allow variable-sized windows
switch(stretch)
{
case Stretch::Preserve:
case Stretch::Fill:
screen.w = iw;
screen.h = ih;
break;
}
}
// Now re-calculate the dimensions
iw = std::min(iw, screen.w);
ih = std::min(ih, screen.h);
image.moveTo((screen.w - iw) >> 1, (screen.h - ih) >> 1);
image.setWidth(iw);
image.setHeight(ih);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
//
// VideoModeList implementation
//
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
FrameBuffer::VideoModeList::VideoModeList()
: myIdx(-1)
{
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void FrameBuffer::VideoModeList::add(const VideoMode& mode)
{
myModeList.emplace_back(mode);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void FrameBuffer::VideoModeList::clear()
{
myModeList.clear();
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
bool FrameBuffer::VideoModeList::empty() const
{
return myModeList.empty();
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
uInt32 FrameBuffer::VideoModeList::size() const
{
return uInt32(myModeList.size());
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void FrameBuffer::VideoModeList::previous()
{
--myIdx;
if(myIdx < 0) myIdx = int(myModeList.size()) - 1;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
const FrameBuffer::VideoMode& FrameBuffer::VideoModeList::current() const
{
return myModeList[myIdx];
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void FrameBuffer::VideoModeList::next()
{
myIdx = (myIdx + 1) % myModeList.size();
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void FrameBuffer::VideoModeList::setByZoom(uInt32 zoom)
{
for(uInt32 i = 0; i < myModeList.size(); ++i)
{
if(myModeList[i].zoom == zoom)
{
myIdx = i;
return;
}
}
myIdx = 0;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void FrameBuffer::VideoModeList::setByStretch(FrameBuffer::VideoMode::Stretch stretch)
{
for(uInt32 i = 0; i < myModeList.size(); ++i)
{
if(myModeList[i].stretch == stretch)
{
myIdx = i;
return;
}
}
myIdx = 0;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
/*
Palette is defined as follows:
*** Base colors ***
kColor Normal foreground color (non-text)
kBGColor Normal background color (non-text)
kBGColorLo Disabled background color dark (non-text)
kBGColorHi Disabled background color light (non-text)
kShadowColor Item is disabled
*** Text colors ***
kTextColor Normal text color
kTextColorHi Highlighted text color
kTextColorEm Emphasized text color
kTextColorInv Color for selected text
*** UI elements (dialog and widgets) ***
kDlgColor Dialog background
kWidColor Widget background
kWidColorHi Widget highlight color
kWidFrameColor Border for currently selected widget
*** Button colors ***
kBtnColor Normal button background
kBtnColorHi Highlighted button background
kBtnBorderColor,
kBtnBorderColorHi,
kBtnTextColor Normal button font color
kBtnTextColorHi Highlighted button font color
*** Checkbox colors ***
kCheckColor Color of 'X' in checkbox
*** Scrollbar colors ***
kScrollColor Normal scrollbar color
kScrollColorHi Highlighted scrollbar color
*** Debugger colors ***
kDbgChangedColor Background color for changed cells
kDbgChangedTextColor Text color for changed cells
kDbgColorHi Highlighted color in debugger data cells
kDbgColorRed Red color in debugger
*** Slider colors ***
kSliderColor Enabled slider
kSliderColorHi Focussed slider
kSliderBGColor Enabled slider background
kSliderBGColorHi Focussed slider background
kSliderBGColorLo Disabled slider background
*** Other colors ***
kColorInfo TIA output position color
kColorTitleBar Title bar color
kColorTitleText Title text color
kColorTitleBarLo Disabled title bar color
kColorTitleTextLo Disabled title text color
*/
uInt32 FrameBuffer::ourGUIColors[3][kNumColors-256] = {
// Standard
{ 0x686868, 0x000000, 0xa38c61, 0xdccfa5, 0x404040, // base
0x000000, 0xac3410, 0x9f0000, 0xf0f0cf, // text
0xc9af7c, 0xf0f0cf, 0xd55941, 0xc80000, // UI elements
0xac3410, 0xd55941, 0x686868, 0xdccfa5, 0xf0f0cf, 0xf0f0cf, // buttons
0xac3410, // checkbox
0xac3410, 0xd55941, // scrollbar
0xc80000, 0x00ff00, 0xc8c8ff, 0xc80000, // debugger
0xac3410, 0xd55941, 0xdccfa5, 0xf0f0cf, 0xa38c61, // slider
0xffffff, 0xac3410, 0xf0f0cf, 0x686868, 0xdccfa5 // other
},
// Classic
{ 0x686868, 0x000000, 0x404040, 0x404040, 0x404040, // base
0x20a020, 0x00ff00, 0xc80000, 0x000000, // text
0x000000, 0x000000, 0x00ff00, 0xc80000, // UI elements
0x000000, 0x000000, 0x686868, 0x00ff00, 0x20a020, 0x00ff00, // buttons
0x20a020, // checkbox
0x20a020, 0x00ff00, // scrollbar
0xc80000, 0x00ff00, 0xc8c8ff, 0xc80000, // debugger
0x20a020, 0x00ff00, 0x404040, 0x686868, 0x404040, // slider
0x00ff00, 0x20a020, 0x000000, 0x686868, 0x404040 // other
},
// Light
{ 0x808080, 0x000000, 0xc0c0c0, 0xe1e1e1, 0x333333, // base
0x000000, 0xBDDEF9, 0x0078d7, 0x000000, // text
0xf0f0f0, 0xffffff, 0x0078d7, 0x0f0f0f, // UI elements
0xe1e1e1, 0xe5f1fb, 0x808080, 0x0078d7, 0x000000, 0x000000, // buttons
0x333333, // checkbox
0xc0c0c0, 0x808080, // scrollbar
0xffc0c0, 0x000000, 0xe00000, 0xc00000, // debugger
0x333333, 0x0078d7, 0xc0c0c0, 0xffffff, 0xc0c0c0, // slider 0xBDDEF9| 0xe1e1e1 | 0xffffff
0xffffff, 0x333333, 0xf0f0f0, 0x808080, 0xc0c0c0 // other
}
};