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