1 //============================================================================
2 //
3 //   SSSS    tt          lll  lll
4 //  SS  SS   tt           ll   ll
5 //  SS     tttttt  eeee   ll   ll   aaaa
6 //   SSSS    tt   ee  ee  ll   ll      aa
7 //      SS   tt   eeeeee  ll   ll   aaaaa  --  "An Atari 2600 VCS Emulator"
8 //  SS  SS   tt   ee      ll   ll  aa  aa
9 //   SSSS     ttt  eeeee llll llll  aaaaa
10 //
11 // Copyright (c) 1995-2021 by Bradford W. Mott, Stephen Anthony
12 // and the Stella Team
13 //
14 // See the file "License.txt" for information on usage and redistribution of
15 // this file, and for a DISCLAIMER OF ALL WARRANTIES.
16 //============================================================================
17 
18 #include "bspf.hxx"
19 #include "Logger.hxx"
20 
21 #include "Console.hxx"
22 #include "EventHandler.hxx"
23 #include "Event.hxx"
24 #include "OSystem.hxx"
25 #include "Settings.hxx"
26 #include "TIA.hxx"
27 #include "Sound.hxx"
28 #include "AudioSettings.hxx"
29 #include "MediaFactory.hxx"
30 
31 #include "FBSurface.hxx"
32 #include "TIASurface.hxx"
33 #include "FrameBuffer.hxx"
34 #include "PaletteHandler.hxx"
35 #include "StateManager.hxx"
36 #include "RewindManager.hxx"
37 
38 #ifdef DEBUGGER_SUPPORT
39   #include "Debugger.hxx"
40 #endif
41 #ifdef GUI_SUPPORT
42   #include "Font.hxx"
43   #include "StellaFont.hxx"
44   #include "ConsoleMediumFont.hxx"
45   #include "ConsoleMediumBFont.hxx"
46   #include "StellaMediumFont.hxx"
47   #include "StellaLargeFont.hxx"
48   #include "Stella12x24tFont.hxx"
49   #include "Stella14x28tFont.hxx"
50   #include "Stella16x32tFont.hxx"
51   #include "ConsoleFont.hxx"
52   #include "ConsoleBFont.hxx"
53   #include "Launcher.hxx"
54   #include "OptionsMenu.hxx"
55   #include "CommandMenu.hxx"
56   #include "HighScoresMenu.hxx"
57   #include "MessageMenu.hxx"
58   #include "PlusRomsMenu.hxx"
59   #include "TimeMachine.hxx"
60 #endif
61 
62 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
FrameBuffer(OSystem & osystem)63 FrameBuffer::FrameBuffer(OSystem& osystem)
64   : myOSystem{osystem}
65 {
66 }
67 
68 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
~FrameBuffer()69 FrameBuffer::~FrameBuffer()
70 {
71 }
72 
73 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
initialize()74 void FrameBuffer::initialize()
75 {
76   // First create the platform-specific backend; it is needed before anything
77   // else can be used
78   myBackend = MediaFactory::createVideoBackend(myOSystem);
79 
80   // Get desktop resolution and supported renderers
81   myBackend->queryHardware(myFullscreenDisplays, myWindowedDisplays, myRenderers);
82 
83   int numDisplays = int(myWindowedDisplays.size());
84 
85   for(int display = 0; display < numDisplays; ++display)
86   {
87     uInt32 query_w = myWindowedDisplays[display].w, query_h = myWindowedDisplays[display].h;
88 
89     // Check the 'maxres' setting, which is an undocumented developer feature
90     // that specifies the desktop size (not normally set)
91     const Common::Size& s = myOSystem.settings().getSize("maxres");
92     if(s.valid())
93     {
94       query_w = s.w;
95       query_h = s.h;
96     }
97     // Various parts of the codebase assume a minimum screen size
98     Common::Size size(std::max(query_w, FBMinimum::Width), std::max(query_h, FBMinimum::Height));
99     myAbsDesktopSize.push_back(size);
100 
101     // Check for HiDPI mode (is it activated, and can we use it?)
102     myHiDPIAllowed.push_back(((size.w / 2) >= FBMinimum::Width) &&
103                              ((size.h / 2) >= FBMinimum::Height));
104     myHiDPIEnabled.push_back(myHiDPIAllowed.back() && myOSystem.settings().getBool("hidpi"));
105 
106     // In HiDPI mode, the desktop resolution is essentially halved
107     // Later, the output is scaled and rendered in 2x mode
108     if(myHiDPIEnabled.back())
109     {
110       size.w /= hidpiScaleFactor();
111       size.h /= hidpiScaleFactor();
112     }
113     myDesktopSize.push_back(size);
114   }
115 
116 #ifdef GUI_SUPPORT
117   setupFonts();
118 #endif
119 
120   setUIPalette();
121 
122   myGrabMouse = myOSystem.settings().getBool("grabmouse");
123 
124   // Create a TIA surface; we need it for rendering TIA images
125   myTIASurface = make_unique<TIASurface>(myOSystem);
126 }
127 
128 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
displayId(BufferType bufferType) const129 int FrameBuffer::displayId(BufferType bufferType) const
130 {
131   const int maxDisplay = int(myWindowedDisplays.size()) - 1;
132   int display;
133 
134   if(bufferType == myBufferType)
135     display = myBackend->getCurrentDisplayIndex();
136   else
137     display = myOSystem.settings().getInt(getDisplayKey(bufferType != BufferType::None
138                                           ? bufferType : myBufferType));
139 
140   return std::min(std::max(0, display), maxDisplay);
141 }
142 
143 #ifdef GUI_SUPPORT
144 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
setupFonts()145 void FrameBuffer::setupFonts()
146 {
147   ////////////////////////////////////////////////////////////////////
148   // Create fonts to draw text
149   // NOTE: the logic determining appropriate font sizes is done here,
150   //       so that the UI classes can just use the font they expect,
151   //       and not worry about it
152   //       This logic should also take into account the size of the
153   //       framebuffer, and try to be intelligent about font sizes
154   //       We can probably add ifdefs to take care of corner cases,
155   //       but that means we've failed to abstract it enough ...
156   ////////////////////////////////////////////////////////////////////
157 
158   // This font is used in a variety of situations when a really small
159   // font is needed; we let the specific widget/dialog decide when to
160   // use it
161   mySmallFont = make_unique<GUI::Font>(GUI::stellaDesc); // 6x10
162 
163   if(myOSystem.settings().getBool("minimal_ui"))
164   {
165     // The general font used in all UI elements
166     myFont = make_unique<GUI::Font>(GUI::stella12x24tDesc);           // 12x24
167     // The info font used in all UI elements
168     myInfoFont = make_unique<GUI::Font>(GUI::stellaLargeDesc);        // 10x20
169   }
170   else
171   {
172     const int NUM_FONTS = 7;
173     FontDesc FONT_DESC[NUM_FONTS] = {GUI::consoleDesc, GUI::consoleMediumDesc, GUI::stellaMediumDesc,
174       GUI::stellaLargeDesc, GUI::stella12x24tDesc, GUI::stella14x28tDesc, GUI::stella16x32tDesc};
175     const string& dialogFont = myOSystem.settings().getString("dialogfont");
176     FontDesc fd = getFontDesc(dialogFont);
177 
178     // The general font used in all UI elements
179     myFont = make_unique<GUI::Font>(fd);                                //  default: 9x18
180     // The info font used in all UI elements,
181     //  automatically determined aiming for 1 / 1.4 (~= 18 / 13) size
182     int fontIdx = 0;
183     for(int i = 0; i < NUM_FONTS; ++i)
184     {
185       if(fd.height <= FONT_DESC[i].height * 1.4)
186       {
187         fontIdx = i;
188         break;
189       }
190     }
191     myInfoFont = make_unique<GUI::Font>(FONT_DESC[fontIdx]);            //  default 8x13
192 
193     // Determine minimal zoom level based on the default font
194     //  So what fits with default font should fit for any font.
195     //  However, we have to make sure all Dialogs are sized using the fontsize.
196     int zoom_h = (fd.height * 4 * 2) / GUI::stellaMediumDesc.height;
197     int zoom_w = (fd.maxwidth * 4 * 2) / GUI::stellaMediumDesc.maxwidth;
198     // round to 25% steps, >= 200%
199     myTIAMinZoom = std::max(std::max(zoom_w, zoom_h) / 4.F, 2.F);
200   }
201 
202   // The font used by the ROM launcher
203   const string& lf = myOSystem.settings().getString("launcherfont");
204 
205   myLauncherFont = make_unique<GUI::Font>(getFontDesc(lf));       //  8x13
206 }
207 
208 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
getFontDesc(const string & name) const209 FontDesc FrameBuffer::getFontDesc(const string& name) const
210 {
211   if(name == "small")
212     return GUI::consoleDesc;        //  8x13
213   else if(name == "low_medium")
214     return GUI::consoleMediumBDesc; //  9x15
215   else if(name == "medium")
216     return GUI::stellaMediumDesc;   //  9x18
217   else if(name == "large" || name == "large10")
218     return GUI::stellaLargeDesc;    // 10x20
219   else if(name == "large12")
220     return GUI::stella12x24tDesc;   // 12x24
221   else if(name == "large14")
222     return GUI::stella14x28tDesc;   // 14x28
223   else // "large16"
224     return GUI::stella16x32tDesc;   // 16x32
225 }
226 #endif
227 
228 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
createDisplay(const string & title,BufferType type,Common::Size size,bool honourHiDPI)229 FBInitStatus FrameBuffer::createDisplay(const string& title, BufferType type,
230                                         Common::Size size, bool honourHiDPI)
231 {
232   ++myInitializedCount;
233   myBackend->setTitle(title);
234 
235   // Always save, maybe only the mode of the window has changed
236   saveCurrentWindowPosition();
237   myBufferType = type;
238 
239   // In HiDPI mode, all created displays must be scaled appropriately
240   if(honourHiDPI && hidpiEnabled())
241   {
242     size.w *= hidpiScaleFactor();
243     size.h *= hidpiScaleFactor();
244   }
245 
246   // A 'windowed' system is defined as one where the window size can be
247   // larger than the screen size, as there's some sort of window manager
248   // that takes care of it (all current desktop systems fall in this category)
249   // However, some systems have no concept of windowing, and have hard limits
250   // on how large a window can be (ie, the size of the 'desktop' is the
251   // absolute upper limit on window size)
252   //
253   // If the WINDOWED_SUPPORT macro is defined, we treat the system as the
254   // former type; if not, as the latter type
255 
256   int display = displayId();
257 #ifdef WINDOWED_SUPPORT
258   // We assume that a desktop of at least minimum acceptable size means that
259   // we're running on a 'large' system, and the window size requirements
260   // can be relaxed
261   // Otherwise, we treat the system as if WINDOWED_SUPPORT is not defined
262   if(myDesktopSize[display].w < FBMinimum::Width &&
263      myDesktopSize[display].h < FBMinimum::Height &&
264      size > myDesktopSize[display])
265     return FBInitStatus::FailTooLarge;
266 #else
267   // Make sure this mode is even possible
268   // We only really need to worry about it in non-windowed environments,
269   // where requesting a window that's too large will probably cause a crash
270   if(size > myDesktopSize[display])
271     return FBInitStatus::FailTooLarge;
272 #endif
273 
274   if(myBufferType == BufferType::Emulator)
275   {
276     // Determine possible TIA windowed zoom levels
277     float currentTIAZoom = myOSystem.settings().getFloat("tia.zoom");
278     myOSystem.settings().setValue("tia.zoom",
279                                   BSPF::clampw(currentTIAZoom, supportedTIAMinZoom(), supportedTIAMaxZoom()));
280   }
281 
282 #ifdef GUI_SUPPORT  // TODO: put message stuff in its own class
283   // Erase any messages from a previous run
284   myMsg.enabled = false;
285 
286   // Create surfaces for TIA statistics and general messages
287   const GUI::Font& f = hidpiEnabled() ? infoFont() : font();
288   myStatsMsg.color = kColorInfo;
289   myStatsMsg.w = f.getMaxCharWidth() * 40 + 3;
290   myStatsMsg.h = (f.getFontHeight() + 2) * 3;
291 
292   if(!myStatsMsg.surface)
293   {
294     myStatsMsg.surface = allocateSurface(myStatsMsg.w, myStatsMsg.h);
295     myStatsMsg.surface->attributes().blending = true;
296     myStatsMsg.surface->attributes().blendalpha = 92; //aligned with TimeMachineDialog
297     myStatsMsg.surface->applyAttributes();
298   }
299 
300   if(!myMsg.surface)
301   {
302     const int fontWidth = font().getMaxCharWidth(),
303               HBORDER = fontWidth * 1.25 / 2.0;
304     myMsg.surface = allocateSurface(fontWidth * MESSAGE_WIDTH + HBORDER * 2,
305                                     font().getFontHeight() * 1.5);
306   }
307 #endif
308 
309   // Initialize video mode handler, so it can know what video modes are
310   // appropriate for the requested image size
311   myVidModeHandler.setImageSize(size);
312 
313   // Initialize video subsystem
314   string pre_about = myBackend->about();
315   FBInitStatus status = applyVideoMode();
316   if(status != FBInitStatus::Success)
317     return status;
318 
319   // Print initial usage message, but only print it later if the status has changed
320   if(myInitializedCount == 1)
321   {
322     Logger::info(myBackend->about());
323   }
324   else
325   {
326     string post_about = myBackend->about();
327     if(post_about != pre_about)
328       Logger::info(post_about);
329   }
330 
331   return status;
332 }
333 
334 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
update(UpdateMode mode)335 void FrameBuffer::update(UpdateMode mode)
336 {
337   // Onscreen messages are a special case and require different handling than
338   // other objects; they aren't UI dialogs in the normal sense nor are they
339   // TIA images, and they need to be rendered on top of everything
340   // The logic is split in two pieces:
341   //  - at the top of ::update(), to determine whether underlying dialogs
342   //    need to be force-redrawn
343   //  - at the bottom of ::update(), to actually draw them (this must come
344   //    last, since they are always drawn on top of everything else).
345 
346   bool forceRedraw = mode & UpdateMode::REDRAW;
347   bool redraw = forceRedraw;
348 
349   // Forced render without draw required if messages or dialogs were closed
350   // Note: For dialogs only relevant when two or more dialogs were stacked
351   bool rerender = (mode & (UpdateMode::REDRAW | UpdateMode::RERENDER))
352     || myPendingRender;
353   myPendingRender = false;
354 
355   switch(myOSystem.eventHandler().state())
356   {
357     case EventHandlerState::NONE:
358     case EventHandlerState::EMULATION:
359       // Do nothing; emulation mode is handled separately (see below)
360       return;
361 
362     case EventHandlerState::PAUSE:
363     {
364       // Show a pause message immediately and then every 7 seconds
365       bool shade = myOSystem.settings().getBool("pausedim");
366 
367       if(myPausedCount-- <= 0)
368       {
369         myPausedCount = uInt32(7 * myOSystem.frameRate());
370         showTextMessage("Paused", MessagePosition::MiddleCenter);
371         myTIASurface->render(shade);
372       }
373       if(rerender)
374         myTIASurface->render(shade);
375       break;  // EventHandlerState::PAUSE
376     }
377 
378   #ifdef GUI_SUPPORT
379     case EventHandlerState::OPTIONSMENU:
380     {
381       myOSystem.optionsMenu().tick();
382       redraw |= myOSystem.optionsMenu().needsRedraw();
383       if(redraw)
384       {
385         clear();
386         myTIASurface->render(true);
387         myOSystem.optionsMenu().draw(forceRedraw);
388       }
389       else if(rerender)
390       {
391         clear();
392         myTIASurface->render(true);
393         myOSystem.optionsMenu().render();
394       }
395       break;  // EventHandlerState::OPTIONSMENU
396     }
397 
398     case EventHandlerState::CMDMENU:
399     {
400       myOSystem.commandMenu().tick();
401       redraw |= myOSystem.commandMenu().needsRedraw();
402       if(redraw)
403       {
404         clear();
405         myTIASurface->render(true);
406         myOSystem.commandMenu().draw(forceRedraw);
407       }
408       else if(rerender)
409       {
410         clear();
411         myTIASurface->render(true);
412         myOSystem.commandMenu().render();
413       }
414       break;  // EventHandlerState::CMDMENU
415     }
416 
417     case EventHandlerState::HIGHSCORESMENU:
418     {
419       myOSystem.highscoresMenu().tick();
420       redraw |= myOSystem.highscoresMenu().needsRedraw();
421       if(redraw)
422       {
423         clear();
424         myTIASurface->render(true);
425         myOSystem.highscoresMenu().draw(forceRedraw);
426       }
427       else if(rerender)
428       {
429         clear();
430         myTIASurface->render(true);
431         myOSystem.highscoresMenu().render();
432       }
433       break;  // EventHandlerState::HIGHSCORESMENU
434     }
435 
436     case EventHandlerState::MESSAGEMENU:
437     {
438       myOSystem.messageMenu().tick();
439       redraw |= myOSystem.messageMenu().needsRedraw();
440       if(redraw)
441       {
442         clear();
443         myTIASurface->render(true);
444         myOSystem.messageMenu().draw(forceRedraw);
445       }
446       break;  // EventHandlerState::MESSAGEMENU
447     }
448 
449     case EventHandlerState::PLUSROMSMENU:
450     {
451       myOSystem.plusRomsMenu().tick();
452       redraw |= myOSystem.plusRomsMenu().needsRedraw();
453       if(redraw)
454       {
455         clear();
456         myTIASurface->render(true);
457         myOSystem.plusRomsMenu().draw(forceRedraw);
458       }
459       break;  // EventHandlerState::PLUSROMSMENU
460     }
461 
462     case EventHandlerState::TIMEMACHINE:
463     {
464       myOSystem.timeMachine().tick();
465       redraw |= myOSystem.timeMachine().needsRedraw();
466       if(redraw)
467       {
468         clear();
469         myTIASurface->render();
470         myOSystem.timeMachine().draw(forceRedraw);
471       }
472       else if(rerender)
473       {
474         clear();
475         myTIASurface->render();
476         myOSystem.timeMachine().render();
477       }
478       break;  // EventHandlerState::TIMEMACHINE
479     }
480 
481     case EventHandlerState::PLAYBACK:
482     {
483       static Int32 frames = 0;
484       bool success = true;
485 
486       if(--frames <= 0)
487       {
488         RewindManager& r = myOSystem.state().rewindManager();
489         uInt64 prevCycles = r.getCurrentCycles();
490 
491         success = r.unwindStates(1);
492 
493         // Determine playback speed, the faster the more the states are apart
494         Int64 frameCycles = 76 * std::max<Int32>(myOSystem.console().tia().scanlinesLastFrame(), 240);
495         Int64 intervalFrames = r.getInterval() / frameCycles;
496         Int64 stateFrames = (r.getCurrentCycles() - prevCycles) / frameCycles;
497 
498         //frames = intervalFrames + std::sqrt(std::max(stateFrames - intervalFrames, 0));
499         frames = std::round(std::sqrt(stateFrames));
500 
501         // Mute sound if saved states were removed or states are too far apart
502         myOSystem.sound().mute(stateFrames > intervalFrames ||
503             frames > static_cast<Int32>(myOSystem.audioSettings().bufferSize() / 2 + 1));
504       }
505       redraw |= success;
506       if(redraw)
507         myTIASurface->render();
508 
509       // Stop playback mode at the end of the state buffer
510       // and switch to Time Machine or Pause mode
511       if(!success)
512       {
513         frames = 0;
514         myOSystem.sound().mute(true);
515         myOSystem.eventHandler().enterMenuMode(EventHandlerState::TIMEMACHINE);
516       }
517       break;  // EventHandlerState::PLAYBACK
518     }
519 
520     case EventHandlerState::LAUNCHER:
521     {
522       myOSystem.launcher().tick();
523       redraw |= myOSystem.launcher().needsRedraw();
524       if(redraw)
525         myOSystem.launcher().draw(forceRedraw);
526       else if(rerender)
527         myOSystem.launcher().render();
528       break;  // EventHandlerState::LAUNCHER
529     }
530   #endif
531 
532   #ifdef DEBUGGER_SUPPORT
533     case EventHandlerState::DEBUGGER:
534     {
535       myOSystem.debugger().tick();
536       redraw |= myOSystem.debugger().needsRedraw();
537       if(redraw)
538         myOSystem.debugger().draw(forceRedraw);
539       else if(rerender)
540         myOSystem.debugger().render();
541       break;  // EventHandlerState::DEBUGGER
542     }
543   #endif
544     default:
545       break;
546   }
547 
548   // Draw any pending messages
549   // The logic here determines whether to draw the message
550   // If the message is to be disabled, logic inside the draw method
551   // indicates that, and then the code at the top of this method sees
552   // the change and redraws everything
553   if(myMsg.enabled)
554     redraw |= drawMessage();
555 
556   // Push buffers to screen only when necessary
557   if(redraw || rerender)
558     myBackend->renderToScreen();
559 }
560 
561 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
updateInEmulationMode(float framesPerSecond)562 void FrameBuffer::updateInEmulationMode(float framesPerSecond)
563 {
564   // Update method that is specifically tailored to emulation mode
565   //
566   // We don't worry about selective rendering here; the rendering
567   // always happens at the full framerate
568 
569   clear();  // TODO - test this: it may cause slowdowns on older systems
570   myTIASurface->render();
571 
572   // Show frame statistics
573   if(myStatsMsg.enabled)
574     drawFrameStats(framesPerSecond);
575 
576   myLastScanlines = myOSystem.console().tia().frameBufferScanlinesLastFrame();
577   myPausedCount = 0;
578 
579   // Draw any pending messages
580   if(myMsg.enabled)
581     drawMessage();
582 
583   // Push buffers to screen
584   myBackend->renderToScreen();
585 }
586 
587 #ifdef GUI_SUPPORT
588 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
createMessage(const string & message,MessagePosition position,bool force)589 void FrameBuffer::createMessage(const string& message, MessagePosition position, bool force)
590 {
591   // Only show messages if they've been enabled
592   if(myMsg.surface == nullptr || !(force || myOSystem.settings().getBool("uimessages")))
593     return;
594 
595   const int fontHeight = font().getFontHeight();
596   const int VBORDER = fontHeight / 4;
597 
598   myMsg.counter = uInt32(myOSystem.frameRate()) * 2; // Show message for 2 seconds
599   if(myMsg.counter == 0)
600     myMsg.counter = 120;
601 
602   // Precompute the message coordinates
603   myMsg.text      = message;
604   myMsg.color     = kBtnTextColor;
605   myMsg.h         = fontHeight + VBORDER * 2;
606   myMsg.position  = position;
607   myMsg.enabled   = true;
608   myMsg.dirty     = true;
609 
610   myMsg.surface->setSrcSize(myMsg.w, myMsg.h);
611   myMsg.surface->setDstSize(myMsg.w * hidpiScaleFactor(), myMsg.h * hidpiScaleFactor());
612 }
613 #endif
614 
615 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
showTextMessage(const string & message,MessagePosition position,bool force)616 void FrameBuffer::showTextMessage(const string& message, MessagePosition position,
617                                   bool force)
618 {
619 #ifdef GUI_SUPPORT
620   const int fontWidth = font().getMaxCharWidth();
621   const int HBORDER = fontWidth * 1.25 / 2.0;
622 
623   myMsg.showGauge = false;
624   myMsg.w         = std::min(fontWidth * (MESSAGE_WIDTH) - HBORDER * 2,
625                              font().getStringWidth(message) + HBORDER * 2);
626 
627   createMessage(message, position, force);
628 #endif
629 }
630 
631 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
showGaugeMessage(const string & message,const string & valueText,float value,float minValue,float maxValue)632 void FrameBuffer::showGaugeMessage(const string& message, const string& valueText,
633                                    float value, float minValue, float maxValue)
634 {
635 #ifdef GUI_SUPPORT
636   const int fontWidth = font().getMaxCharWidth();
637   const int HBORDER = fontWidth * 1.25 / 2.0;
638 
639   myMsg.showGauge  = true;
640   if(maxValue - minValue != 0)
641     myMsg.value = (value - minValue) / (maxValue - minValue) * 100.F;
642   else
643     myMsg.value = 100.F;
644   myMsg.valueText  = valueText;
645   myMsg.w          = std::min(fontWidth * MESSAGE_WIDTH,
646                               font().getStringWidth(message)
647                               + fontWidth * (GAUGEBAR_WIDTH + 2)
648                               + font().getStringWidth(valueText))
649                               + HBORDER * 2;
650 
651   createMessage(message, MessagePosition::BottomCenter);
652 #endif
653 }
654 
655 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
messageShown() const656 bool FrameBuffer::messageShown() const
657 {
658 #ifdef GUI_SUPPORT
659   return myMsg.enabled;
660 #else
661   return false;
662 #endif
663 }
664 
665 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
drawFrameStats(float framesPerSecond)666 void FrameBuffer::drawFrameStats(float framesPerSecond)
667 {
668 #ifdef GUI_SUPPORT
669   const ConsoleInfo& info = myOSystem.console().about();
670   int xPos = 2, yPos = 0;
671   const GUI::Font& f = hidpiEnabled() ? infoFont() : font();
672   const int dy = f.getFontHeight() + 2;
673 
674   ostringstream ss;
675 
676   myStatsMsg.surface->invalidate();
677 
678   // draw scanlines
679   ColorId color = myOSystem.console().tia().frameBufferScanlinesLastFrame() !=
680     myLastScanlines ? kDbgColorRed : myStatsMsg.color;
681 
682   ss
683     << myOSystem.console().tia().frameBufferScanlinesLastFrame()
684     << " / "
685     << std::fixed << std::setprecision(1)
686     << myOSystem.console().currentFrameRate()
687     << "Hz => "
688     << info.DisplayFormat;
689 
690   myStatsMsg.surface->drawString(f, ss.str(), xPos, yPos,
691                                  myStatsMsg.w, color, TextAlign::Left, 0, true, kBGColor);
692 
693   yPos += dy;
694   ss.str("");
695 
696   ss
697     << std::fixed << std::setprecision(1) << framesPerSecond
698     << "fps @ "
699     << std::fixed << std::setprecision(0) << 100 *
700       (myOSystem.settings().getBool("turbo")
701         ? 20.0F
702         : myOSystem.settings().getFloat("speed"))
703     << "% speed";
704 
705   myStatsMsg.surface->drawString(f, ss.str(), xPos, yPos,
706       myStatsMsg.w, myStatsMsg.color, TextAlign::Left, 0, true, kBGColor);
707 
708   yPos += dy;
709   ss.str("");
710 
711   ss << info.BankSwitch;
712   if (myOSystem.settings().getBool("dev.settings")) ss << "| Developer";
713 
714   myStatsMsg.surface->drawString(f, ss.str(), xPos, yPos,
715       myStatsMsg.w, myStatsMsg.color, TextAlign::Left, 0, true, kBGColor);
716 
717   myStatsMsg.surface->setDstPos(imageRect().x() + 10, imageRect().y() + 8);
718   myStatsMsg.surface->setDstSize(myStatsMsg.w * hidpiScaleFactor(),
719                                  myStatsMsg.h * hidpiScaleFactor());
720   myStatsMsg.surface->render();
721 #endif
722 }
723 
724 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
toggleFrameStats(bool toggle)725 void FrameBuffer::toggleFrameStats(bool toggle)
726 {
727   if (toggle)
728     showFrameStats(!myStatsEnabled);
729   myOSystem.settings().setValue(
730     myOSystem.settings().getBool("dev.settings") ? "dev.stats" : "plr.stats", myStatsEnabled);
731 
732   myOSystem.frameBuffer().showTextMessage(string("Console info ") +
733                                           (myStatsEnabled ? "enabled" : "disabled"));
734 }
735 
736 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
showFrameStats(bool enable)737 void FrameBuffer::showFrameStats(bool enable)
738 {
739   myStatsEnabled = myStatsMsg.enabled = enable;
740 }
741 
742 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
enableMessages(bool enable)743 void FrameBuffer::enableMessages(bool enable)
744 {
745   if(enable)
746   {
747     // Only re-enable frame stats if they were already enabled before
748     myStatsMsg.enabled = myStatsEnabled;
749   }
750   else
751   {
752     // Temporarily disable frame stats
753     myStatsMsg.enabled = false;
754 
755     // Erase old messages on the screen
756     hideMessage();
757 
758     update();  // update immediately
759   }
760 }
761 
762 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
hideMessage()763 void FrameBuffer::hideMessage()
764 {
765   myPendingRender = myMsg.enabled;
766   myMsg.enabled = false;
767 }
768 
769 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
drawMessage()770 inline bool FrameBuffer::drawMessage()
771 {
772 #ifdef GUI_SUPPORT
773   // Either erase the entire message (when time is reached),
774   // or show again this frame
775   if(myMsg.counter == 0)
776   {
777     hideMessage();
778     return false;
779   }
780 
781   if(myMsg.dirty)
782   {
783   #ifdef DEBUG_BUILD
784     cerr << "m";
785     //cerr << "--- draw message ---" << endl;
786   #endif
787 
788     // Draw the bounded box and text
789     const Common::Rect& dst = myMsg.surface->dstRect();
790     const int fontWidth = font().getMaxCharWidth(),
791       fontHeight = font().getFontHeight();
792     const int VBORDER = fontHeight / 4;
793     const int HBORDER = fontWidth * 1.25 / 2.0;
794     constexpr int BORDER = 1;
795 
796     switch(myMsg.position)
797     {
798       case MessagePosition::TopLeft:
799         myMsg.x = 5;
800         myMsg.y = 5;
801         break;
802 
803       case MessagePosition::TopCenter:
804         myMsg.x = (imageRect().w() - dst.w()) >> 1;
805         myMsg.y = 5;
806         break;
807 
808       case MessagePosition::TopRight:
809         myMsg.x = imageRect().w() - dst.w() - 5;
810         myMsg.y = 5;
811         break;
812 
813       case MessagePosition::MiddleLeft:
814         myMsg.x = 5;
815         myMsg.y = (imageRect().h() - dst.h()) >> 1;
816         break;
817 
818       case MessagePosition::MiddleCenter:
819         myMsg.x = (imageRect().w() - dst.w()) >> 1;
820         myMsg.y = (imageRect().h() - dst.h()) >> 1;
821         break;
822 
823       case MessagePosition::MiddleRight:
824         myMsg.x = imageRect().w() - dst.w() - 5;
825         myMsg.y = (imageRect().h() - dst.h()) >> 1;
826         break;
827 
828       case MessagePosition::BottomLeft:
829         myMsg.x = 5;
830         myMsg.y = imageRect().h() - dst.h() - 5;
831         break;
832 
833       case MessagePosition::BottomCenter:
834         myMsg.x = (imageRect().w() - dst.w()) >> 1;
835         myMsg.y = imageRect().h() - dst.h() - 5;
836         break;
837 
838       case MessagePosition::BottomRight:
839         myMsg.x = imageRect().w() - dst.w() - 5;
840         myMsg.y = imageRect().h() - dst.h() - 5;
841         break;
842     }
843 
844     myMsg.surface->setDstPos(myMsg.x + imageRect().x(), myMsg.y + imageRect().y());
845     myMsg.surface->fillRect(0, 0, myMsg.w, myMsg.h, kColor);
846     myMsg.surface->fillRect(BORDER, BORDER, myMsg.w - BORDER * 2, myMsg.h - BORDER * 2, kBtnColor);
847     myMsg.surface->drawString(font(), myMsg.text, HBORDER, VBORDER,
848                               myMsg.w, myMsg.color);
849 
850     if(myMsg.showGauge)
851     {
852       constexpr int NUM_TICKMARKS = 4;
853       // limit gauge bar width if texts are too long
854       const int swidth = std::min(fontWidth * GAUGEBAR_WIDTH,
855                                   fontWidth * (MESSAGE_WIDTH - 2)
856                                   - font().getStringWidth(myMsg.text)
857                                   - font().getStringWidth(myMsg.valueText));
858       const int bwidth = swidth * myMsg.value / 100.F;
859       const int bheight = fontHeight >> 1;
860       const int x = HBORDER + font().getStringWidth(myMsg.text) + fontWidth;
861       // align bar with bottom of text
862       const int y = VBORDER + font().desc().ascent - bheight;
863 
864       // draw gauge bar
865       myMsg.surface->fillRect(x - BORDER, y, swidth + BORDER * 2, bheight, kSliderBGColor);
866       myMsg.surface->fillRect(x, y + BORDER, bwidth, bheight - BORDER * 2, kSliderColor);
867       // draw tickmark in the middle of the bar
868       for(int i = 1; i < NUM_TICKMARKS; ++i)
869       {
870         ColorId color;
871         int xt = x + swidth * i / NUM_TICKMARKS;
872         if(bwidth < xt - x)
873           color = kCheckColor; // kSliderColor;
874         else
875           color = kSliderBGColor;
876         myMsg.surface->vLine(xt, y + bheight / 2, y + bheight - 1, color);
877       }
878       // draw value text
879       myMsg.surface->drawString(font(), myMsg.valueText,
880                                 x + swidth + fontWidth, VBORDER,
881                                 myMsg.w, myMsg.color);
882     }
883     myMsg.dirty = false;
884     myMsg.surface->render();
885     return true;
886   }
887 
888   myMsg.counter--;
889   myMsg.surface->render();
890 #endif
891 
892   return false;
893 }
894 
895 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
setPauseDelay()896 void FrameBuffer::setPauseDelay()
897 {
898   myPausedCount = uInt32(2 * myOSystem.frameRate());
899 }
900 
901 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
allocateSurface(int w,int h,ScalingInterpolation inter,const uInt32 * data)902 shared_ptr<FBSurface> FrameBuffer::allocateSurface(
903     int w, int h, ScalingInterpolation inter, const uInt32* data)
904 {
905   mySurfaceList.push_back(myBackend->createSurface(w, h, inter, data));
906   return mySurfaceList.back();
907 }
908 
909 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
deallocateSurface(shared_ptr<FBSurface> surface)910 void FrameBuffer::deallocateSurface(shared_ptr<FBSurface> surface)
911 {
912   if(surface)
913     mySurfaceList.remove(surface);
914 }
915 
916 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
resetSurfaces()917 void FrameBuffer::resetSurfaces()
918 {
919   for(auto& surface: mySurfaceList)
920     surface->reload();
921 
922   update(UpdateMode::REDRAW); // force full update
923 }
924 
925 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
setTIAPalette(const PaletteArray & rgb_palette)926 void FrameBuffer::setTIAPalette(const PaletteArray& rgb_palette)
927 {
928   // Create a TIA palette from the raw RGB data
929   PaletteArray tia_palette;
930   for(int i = 0; i < 256; ++i)
931   {
932     uInt8 r = (rgb_palette[i] >> 16) & 0xff;
933     uInt8 g = (rgb_palette[i] >> 8) & 0xff;
934     uInt8 b = rgb_palette[i] & 0xff;
935 
936     tia_palette[i] = mapRGB(r, g, b);
937   }
938 
939   // Remember the TIA palette; place it at the beginning of the full palette
940   std::copy_n(tia_palette.begin(), tia_palette.size(), myFullPalette.begin());
941 
942   // Let the TIA surface know about the new palette
943   myTIASurface->setPalette(tia_palette, rgb_palette);
944 
945   // Since the UI palette shares the TIA palette, we need to update it too
946   setUIPalette();
947 }
948 
949 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
setUIPalette()950 void FrameBuffer::setUIPalette()
951 {
952   // Set palette for UI (upper area of full palette)
953   const UIPaletteArray& ui_palette =
954      (myOSystem.settings().getString("uipalette") == "classic") ? ourClassicUIPalette :
955      (myOSystem.settings().getString("uipalette") == "light")   ? ourLightUIPalette :
956      (myOSystem.settings().getString("uipalette") == "dark")    ? ourDarkUIPalette :
957       ourStandardUIPalette;
958 
959   for(size_t i = 0, j = myFullPalette.size() - ui_palette.size();
960       i < ui_palette.size(); ++i, ++j)
961   {
962     const uInt8 r = (ui_palette[i] >> 16) & 0xff,
963                 g = (ui_palette[i] >> 8) & 0xff,
964                 b =  ui_palette[i] & 0xff;
965 
966     myFullPalette[j] = mapRGB(r, g, b);
967   }
968   FBSurface::setPalette(myFullPalette);
969 }
970 
971 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
stateChanged(EventHandlerState state)972 void FrameBuffer::stateChanged(EventHandlerState state)
973 {
974   // Make sure any onscreen messages are removed
975   hideMessage();
976 
977   update(); // update immediately
978 }
979 
980 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
getDisplayKey(BufferType bufferType) const981 string FrameBuffer::getDisplayKey(BufferType bufferType) const
982 {
983   if(bufferType == BufferType::None)
984     bufferType = myBufferType;
985 
986   // save current window's display and position
987   switch(bufferType)
988   {
989     case BufferType::Launcher:
990       return "launcherdisplay";
991 
992     case BufferType::Emulator:
993       return "display";
994 
995     #ifdef DEBUGGER_SUPPORT
996     case BufferType::Debugger:
997       return "dbg.display";
998     #endif
999 
1000     default:
1001       return "";
1002   }
1003 }
1004 
1005 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
getPositionKey() const1006 string FrameBuffer::getPositionKey() const
1007 {
1008   // save current window's display and position
1009   switch(myBufferType)
1010   {
1011     case BufferType::Launcher:
1012       return "launcherpos";
1013 
1014     case BufferType::Emulator:
1015       return  "windowedpos";
1016 
1017     #ifdef DEBUGGER_SUPPORT
1018     case BufferType::Debugger:
1019       return "dbg.pos";
1020     #endif
1021 
1022     default:
1023       return "";
1024   }
1025 }
1026 
1027 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
saveCurrentWindowPosition() const1028 void FrameBuffer::saveCurrentWindowPosition() const
1029 {
1030   if(myBackend)
1031   {
1032     myOSystem.settings().setValue(
1033       getDisplayKey(), myBackend->getCurrentDisplayIndex());
1034     if(myBackend->isCurrentWindowPositioned())
1035       myOSystem.settings().setValue(
1036         getPositionKey(), myBackend->getCurrentWindowPos());
1037   }
1038 }
1039 
1040 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
saveConfig(Settings & settings) const1041 void FrameBuffer::saveConfig(Settings& settings) const
1042 {
1043   // Save the last windowed position and display on system shutdown
1044   saveCurrentWindowPosition();
1045 }
1046 
1047 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
setFullscreen(bool enable)1048 void FrameBuffer::setFullscreen(bool enable)
1049 {
1050 #ifdef WINDOWED_SUPPORT
1051   // Switching between fullscreen and windowed modes will invariably mean
1052   // that the 'window' resolution changes.  Currently, dialogs are not
1053   // able to resize themselves when they are actively being shown
1054   // (they would have to be closed and then re-opened, etc).
1055   // For now, we simply disallow screen switches in such modes
1056   switch(myOSystem.eventHandler().state())
1057   {
1058     case EventHandlerState::EMULATION:
1059     case EventHandlerState::PAUSE:
1060       break; // continue with processing (aka, allow a mode switch)
1061     case EventHandlerState::DEBUGGER:
1062     case EventHandlerState::LAUNCHER:
1063       if(myOSystem.eventHandler().overlay().baseDialogIsActive())
1064         break; // allow a mode switch when there is only one dialog
1065       [[fallthrough]];
1066     default:
1067       return;
1068   }
1069 
1070   myOSystem.settings().setValue("fullscreen", enable);
1071   saveCurrentWindowPosition();
1072   applyVideoMode();
1073 #endif
1074 }
1075 
1076 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
toggleFullscreen(bool toggle)1077 void FrameBuffer::toggleFullscreen(bool toggle)
1078 {
1079   EventHandlerState state = myOSystem.eventHandler().state();
1080 
1081   switch(state)
1082   {
1083     case EventHandlerState::LAUNCHER:
1084     case EventHandlerState::EMULATION:
1085     case EventHandlerState::PAUSE:
1086     case EventHandlerState::DEBUGGER:
1087     {
1088       const bool isFullscreen = toggle ? !fullScreen() : fullScreen();
1089       setFullscreen(isFullscreen);
1090 
1091       if(state != EventHandlerState::LAUNCHER)
1092       {
1093         ostringstream msg;
1094         msg << "Fullscreen ";
1095 
1096         if(state != EventHandlerState::DEBUGGER)
1097         {
1098           if(isFullscreen)
1099             msg << "enabled (" << myBackend->refreshRate() << " Hz, ";
1100           else
1101             msg << "disabled (";
1102           msg << "Zoom " << myActiveVidMode.zoom * 100 << "%)";
1103         }
1104         else
1105         {
1106           if(isFullscreen)
1107             msg << "enabled";
1108           else
1109             msg << "disabled";
1110         }
1111         showTextMessage(msg.str());
1112       }
1113       break;
1114     }
1115     default:
1116       break;
1117   }
1118 }
1119 
1120 #ifdef ADAPTABLE_REFRESH_SUPPORT
1121 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
toggleAdaptRefresh(bool toggle)1122 void FrameBuffer::toggleAdaptRefresh(bool toggle)
1123 {
1124   bool isAdaptRefresh = myOSystem.settings().getInt("tia.fs_refresh");
1125 
1126   if(toggle)
1127     isAdaptRefresh = !isAdaptRefresh;
1128 
1129   if(myBufferType == BufferType::Emulator)
1130   {
1131     if(toggle)
1132     {
1133       myOSystem.settings().setValue("tia.fs_refresh", isAdaptRefresh);
1134       // issue a complete framebuffer re-initialization
1135       myOSystem.createFrameBuffer();
1136     }
1137 
1138     ostringstream msg;
1139 
1140     msg << "Adapt refresh rate ";
1141     msg << (isAdaptRefresh ? "enabled" : "disabled");
1142     msg << " (" << myBackend->refreshRate() << " Hz)";
1143 
1144     showTextMessage(msg.str());
1145   }
1146 }
1147 #endif
1148 
1149 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
changeOverscan(int direction)1150 void FrameBuffer::changeOverscan(int direction)
1151 {
1152   if (fullScreen())
1153   {
1154     int oldOverscan = myOSystem.settings().getInt("tia.fs_overscan");
1155     int overscan = BSPF::clamp(oldOverscan + direction, 0, 10);
1156 
1157     if (overscan != oldOverscan)
1158     {
1159       myOSystem.settings().setValue("tia.fs_overscan", overscan);
1160 
1161       // issue a complete framebuffer re-initialization
1162       myOSystem.createFrameBuffer();
1163     }
1164 
1165     ostringstream val;
1166     if(overscan)
1167       val << (overscan > 0 ? "+" : "" ) << overscan << "%";
1168     else
1169       val << "Off";
1170     myOSystem.frameBuffer().showGaugeMessage("Overscan", val.str(), overscan, 0, 10);
1171   }
1172 }
1173 
1174 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
switchVideoMode(int direction)1175 void FrameBuffer::switchVideoMode(int direction)
1176 {
1177   // Only applicable when in TIA/emulation mode
1178   if(!myOSystem.eventHandler().inTIAMode())
1179     return;
1180 
1181   if(!fullScreen())
1182   {
1183     // Windowed TIA modes support variable zoom levels
1184     float zoom = myOSystem.settings().getFloat("tia.zoom");
1185     if(direction == +1)       zoom += ZOOM_STEPS;
1186     else if(direction == -1)  zoom -= ZOOM_STEPS;
1187 
1188     // Make sure the level is within the allowable desktop size
1189     zoom = BSPF::clampw(zoom, supportedTIAMinZoom(), supportedTIAMaxZoom());
1190     myOSystem.settings().setValue("tia.zoom", zoom);
1191   }
1192   else
1193   {
1194     // In fullscreen mode, there are only two modes, so direction
1195     // is irrelevant
1196     if(direction == +1 || direction == -1)
1197     {
1198       bool stretch = myOSystem.settings().getBool("tia.fs_stretch");
1199       myOSystem.settings().setValue("tia.fs_stretch", !stretch);
1200     }
1201   }
1202 
1203   saveCurrentWindowPosition();
1204   if(!direction || applyVideoMode() == FBInitStatus::Success)
1205   {
1206     if(fullScreen())
1207       showTextMessage(myActiveVidMode.description);
1208     else
1209       showGaugeMessage("Zoom", myActiveVidMode.description, myActiveVidMode.zoom,
1210                   supportedTIAMinZoom(), supportedTIAMaxZoom());
1211   }
1212 }
1213 
1214 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
applyVideoMode()1215 FBInitStatus FrameBuffer::applyVideoMode()
1216 {
1217   // Update display size, in case windowed/fullscreen mode has changed
1218   const Settings& s = myOSystem.settings();
1219   int display = displayId();
1220 
1221   if(s.getBool("fullscreen"))
1222     myVidModeHandler.setDisplaySize(myFullscreenDisplays[display], display);
1223   else
1224     myVidModeHandler.setDisplaySize(myAbsDesktopSize[display]);
1225 
1226   const bool inTIAMode = myOSystem.eventHandler().inTIAMode();
1227 
1228   // Build the new mode based on current settings
1229   const VideoModeHandler::Mode& mode = myVidModeHandler.buildMode(s, inTIAMode);
1230   if(mode.imageR.size() > mode.screenS)
1231     return FBInitStatus::FailTooLarge;
1232 
1233   // Changing the video mode can take some time, during which the last
1234   // sound played may get 'stuck'
1235   // So we mute the sound until the operation completes
1236   bool oldMuteState = myOSystem.sound().mute(true);
1237   FBInitStatus status = FBInitStatus::FailNotSupported;
1238 
1239   if(myBackend->setVideoMode(mode,
1240       myOSystem.settings().getInt(getDisplayKey()),
1241       myOSystem.settings().getPoint(getPositionKey()))
1242     )
1243   {
1244     myActiveVidMode = mode;
1245     status = FBInitStatus::Success;
1246 
1247     // Did we get the requested fullscreen state?
1248     myOSystem.settings().setValue("fullscreen", fullScreen());
1249 
1250     // Inform TIA surface about new mode, and update TIA settings
1251     if(inTIAMode)
1252     {
1253       myTIASurface->initialize(myOSystem.console(), myActiveVidMode);
1254       if(fullScreen())
1255         myOSystem.settings().setValue("tia.fs_stretch",
1256           myActiveVidMode.stretch == VideoModeHandler::Mode::Stretch::Fill);
1257       else
1258         myOSystem.settings().setValue("tia.zoom", myActiveVidMode.zoom);
1259     }
1260 
1261     resetSurfaces();
1262     setCursorState();
1263     myPendingRender = true;
1264   }
1265   else
1266     Logger::error("ERROR: Couldn't initialize video subsystem");
1267 
1268   // Restore sound settings
1269   myOSystem.sound().mute(oldMuteState);
1270 
1271   return status;
1272 }
1273 
1274 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
maxWindowZoom() const1275 float FrameBuffer::maxWindowZoom() const
1276 {
1277   int display = displayId(BufferType::Emulator);
1278   float multiplier = 1;
1279 
1280   for(;;)
1281   {
1282     // Figure out the zoomed size of the window
1283     uInt32 width  = TIAConstants::viewableWidth * multiplier;
1284     uInt32 height = TIAConstants::viewableHeight * multiplier;
1285 
1286     if((width > myAbsDesktopSize[display].w) || (height > myAbsDesktopSize[display].h))
1287       break;
1288 
1289     multiplier += ZOOM_STEPS;
1290   }
1291   return multiplier > 1 ? multiplier - ZOOM_STEPS : 1;
1292 }
1293 
1294 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
setCursorState()1295 void FrameBuffer::setCursorState()
1296 {
1297   myGrabMouse = myOSystem.settings().getBool("grabmouse");
1298   // Always grab mouse in emulation (if enabled) and emulating a controller
1299   // that always uses the mouse
1300   const bool emulation =
1301       myOSystem.eventHandler().state() == EventHandlerState::EMULATION;
1302   const bool usesLightgun = emulation && myOSystem.hasConsole() ?
1303     myOSystem.console().leftController().type() == Controller::Type::Lightgun ||
1304     myOSystem.console().rightController().type() == Controller::Type::Lightgun : false;
1305   // Show/hide cursor in UI/emulation mode based on 'cursor' setting
1306   int cursor = myOSystem.settings().getInt("cursor");
1307 
1308   // Always enable cursor in lightgun games
1309   if (usesLightgun && !myGrabMouse)
1310     cursor |= 1;  // +Emulation
1311 
1312   switch(cursor)
1313   {
1314     case 0:                   // -UI, -Emulation
1315       showCursor(false);
1316       break;
1317     case 1:
1318       showCursor(emulation);  // -UI, +Emulation
1319       break;
1320     case 2:                   // +UI, -Emulation
1321       showCursor(!emulation);
1322       break;
1323     case 3:
1324       showCursor(true);       // +UI, +Emulation
1325       break;
1326     default:
1327       break;
1328   }
1329 
1330   myGrabMouse &= grabMouseAllowed();
1331   myBackend->grabMouse(myGrabMouse);
1332 }
1333 
1334 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
grabMouseAllowed()1335 bool FrameBuffer::grabMouseAllowed()
1336 {
1337   // Allow grabbing mouse in emulation (if enabled) and emulating a controller
1338   // that always uses the mouse
1339   bool emulation =
1340     myOSystem.eventHandler().state() == EventHandlerState::EMULATION;
1341   bool analog = myOSystem.hasConsole() ?
1342     (myOSystem.console().leftController().isAnalog() ||
1343      myOSystem.console().rightController().isAnalog()) : false;
1344   bool usesLightgun = emulation && myOSystem.hasConsole() ?
1345     myOSystem.console().leftController().type() == Controller::Type::Lightgun ||
1346     myOSystem.console().rightController().type() == Controller::Type::Lightgun : false;
1347   bool alwaysUseMouse = BSPF::equalsIgnoreCase("always", myOSystem.settings().getString("usemouse"));
1348 
1349   // Disable grab while cursor is shown in emulation
1350   bool cursorHidden = !(myOSystem.settings().getInt("cursor") & 1);
1351 
1352   return emulation && (analog || usesLightgun || alwaysUseMouse) && cursorHidden;
1353 }
1354 
1355 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
enableGrabMouse(bool enable)1356 void FrameBuffer::enableGrabMouse(bool enable)
1357 {
1358   myGrabMouse = enable;
1359   setCursorState();
1360 }
1361 
1362 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
toggleGrabMouse(bool toggle)1363 void FrameBuffer::toggleGrabMouse(bool toggle)
1364 {
1365   bool oldState = myGrabMouse = myOSystem.settings().getBool("grabmouse");
1366 
1367   if(toggle)
1368   {
1369     if(grabMouseAllowed())
1370     {
1371       myGrabMouse = !myGrabMouse;
1372       myOSystem.settings().setValue("grabmouse", myGrabMouse);
1373       setCursorState();
1374     }
1375   }
1376   else
1377     oldState = !myGrabMouse; // display current state
1378 
1379   myOSystem.frameBuffer().showTextMessage(oldState != myGrabMouse ? myGrabMouse
1380                                           ? "Grab mouse enabled" : "Grab mouse disabled"
1381                                           : "Grab mouse not allowed");
1382 }
1383 
1384 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
1385 /*
1386   Palette is defined as follows:
1387     *** Base colors ***
1388     kColor            Normal foreground color (non-text)
1389     kBGColor          Normal background color (non-text)
1390     kBGColorLo        Disabled background color dark (non-text)
1391     kBGColorHi        Disabled background color light (non-text)
1392     kShadowColor      Item is disabled
1393     *** Text colors ***
1394     kTextColor        Normal text color
1395     kTextColorHi      Highlighted text color
1396     kTextColorEm      Emphasized text color
1397     kTextColorInv     Color for selected text
1398     kTextColorLink    Color for links
1399     *** UI elements (dialog and widgets) ***
1400     kDlgColor         Dialog background
1401     kWidColor         Widget background
1402     kWidColorHi       Widget highlight color
1403     kWidFrameColor    Border for currently selected widget
1404     *** Button colors ***
1405     kBtnColor         Normal button background
1406     kBtnColorHi       Highlighted button background
1407     kBtnBorderColor,
1408     kBtnBorderColorHi,
1409     kBtnTextColor     Normal button font color
1410     kBtnTextColorHi   Highlighted button font color
1411     *** Checkbox colors ***
1412     kCheckColor       Color of 'X' in checkbox
1413     *** Scrollbar colors ***
1414     kScrollColor      Normal scrollbar color
1415     kScrollColorHi    Highlighted scrollbar color
1416     *** Debugger colors ***
1417     kDbgChangedColor      Background color for changed cells
1418     kDbgChangedTextColor  Text color for changed cells
1419     kDbgColorHi           Highlighted color in debugger data cells
1420     kDbgColorRed          Red color in debugger
1421     *** Slider colors ***
1422     kSliderColor          Enabled slider
1423     kSliderColorHi        Focussed slider
1424     kSliderBGColor        Enabled slider background
1425     kSliderBGColorHi      Focussed slider background
1426     kSliderBGColorLo      Disabled slider background
1427     *** Other colors ***
1428     kColorInfo            TIA output position color
1429     kColorTitleBar        Title bar color
1430     kColorTitleText       Title text color
1431 */
1432 UIPaletteArray FrameBuffer::ourStandardUIPalette = {
1433   { 0x686868, 0x000000, 0xa38c61, 0xdccfa5, 0x404040,           // base
1434     0x000000, 0xac3410, 0x9f0000, 0xf0f0cf, 0xac3410,           // text
1435     0xc9af7c, 0xf0f0cf, 0xd55941, 0xc80000,                     // UI elements
1436     0xac3410, 0xd55941, 0x686868, 0xdccfa5, 0xf0f0cf, 0xf0f0cf, // buttons
1437     0xac3410,                                                   // checkbox
1438     0xac3410, 0xd55941,                                         // scrollbar
1439     0xc80000, 0xffff80, 0xc8c8ff, 0xc80000,                     // debugger
1440     0xac3410, 0xd55941, 0xdccfa5, 0xf0f0cf, 0xa38c61,           // slider
1441     0xffffff, 0xac3410, 0xf0f0cf                                // other
1442   }
1443 };
1444 
1445 UIPaletteArray FrameBuffer::ourClassicUIPalette = {
1446   { 0x686868, 0x000000, 0x404040, 0x404040, 0x404040,           // base
1447     0x20a020, 0x00ff00, 0xc80000, 0x000000, 0x00ff00,           // text
1448     0x000000, 0x000000, 0x00ff00, 0xc80000,                     // UI elements
1449     0x000000, 0x000000, 0x686868, 0x00ff00, 0x20a020, 0x00ff00, // buttons
1450     0x20a020,                                                   // checkbox
1451     0x20a020, 0x00ff00,                                         // scrollbar
1452     0xc80000, 0x00ff00, 0xc8c8ff, 0xc80000,                     // debugger
1453     0x20a020, 0x00ff00, 0x404040, 0x686868, 0x404040,           // slider
1454     0x00ff00, 0x20a020, 0x000000                                // other
1455   }
1456 };
1457 
1458 UIPaletteArray FrameBuffer::ourLightUIPalette = {
1459   { 0x808080, 0x000000, 0xc0c0c0, 0xe1e1e1, 0x333333,           // base
1460     0x000000, 0xBDDEF9, 0x0078d7, 0x000000, 0x005aa1,           // text
1461     0xf0f0f0, 0xffffff, 0x0078d7, 0x0f0f0f,                     // UI elements
1462     0xe1e1e1, 0xe5f1fb, 0x808080, 0x0078d7, 0x000000, 0x000000, // buttons
1463     0x333333,                                                   // checkbox
1464     0xc0c0c0, 0x808080,                                         // scrollbar
1465     0xffc0c0, 0x000000, 0xe00000, 0xc00000,                     // debugger
1466     0x333333, 0x0078d7, 0xc0c0c0, 0xffffff, 0xc0c0c0,           // slider 0xBDDEF9| 0xe1e1e1 | 0xffffff
1467     0xffffff, 0x333333, 0xf0f0f0                                // other
1468   }
1469 };
1470 
1471 UIPaletteArray FrameBuffer::ourDarkUIPalette = {
1472   { 0x646464, 0xc0c0c0, 0x3c3c3c, 0x282828, 0x989898,           // base
1473     0xc0c0c0, 0x1567a5, 0x0064b7, 0xc0c0c0, 0x1d92e0,           // text
1474     0x202020, 0x000000, 0x0059a3, 0xb0b0b0,                     // UI elements
1475     0x282828, 0x00467f, 0x646464, 0x0059a3, 0xc0c0c0, 0xc0c0c0, // buttons
1476     0x989898,                                                   // checkbox
1477     0x3c3c3c, 0x646464,                                         // scrollbar
1478     0x7f2020, 0xc0c0c0, 0xe00000, 0xc00000,                     // debugger
1479     0x989898, 0x0059a3, 0x3c3c3c, 0x000000, 0x3c3c3c,           // slider
1480     0x000000, 0x989898, 0x202020                                // other
1481   }
1482 };
1483