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-2019 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
29#include "FBSurface.hxx"
30#include "TIASurface.hxx"
31#include "FrameBuffer.hxx"
32
33#ifdef DEBUGGER_SUPPORT
34 #include "Debugger.hxx"
35#endif
36#ifdef GUI_SUPPORT
37 #include "Font.hxx"
38 #include "StellaFont.hxx"
39 #include "StellaMediumFont.hxx"
40 #include "StellaLargeFont.hxx"
41 #include "ConsoleFont.hxx"
42 #include "Launcher.hxx"
43 #include "Menu.hxx"
44 #include "CommandMenu.hxx"
45 #include "TimeMachine.hxx"
46#endif
47
48// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
49FrameBuffer::FrameBuffer(OSystem& osystem)
50 : myOSystem(osystem),
51 myNumDisplays(1),
52 myInitializedCount(0),
53 myPausedCount(0),
54 myStatsEnabled(false),
55 myLastScanlines(0),
56 myGrabMouse(false),
57 myHiDPIAllowed(false),
58 myHiDPIEnabled(false),
59 myCurrentModeList(nullptr)
60{
61}
62
63// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
64FrameBuffer::~FrameBuffer()
65{
66}
67
68// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
69bool FrameBuffer::initialize()
70{
71 // Get desktop resolution and supported renderers
72 vector<Common::Size> windowedDisplays;
73 queryHardware(myFullscreenDisplays, windowedDisplays, myRenderers);
74 uInt32 query_w = windowedDisplays[0].w, query_h = windowedDisplays[0].h;
75
76 // Check the 'maxres' setting, which is an undocumented developer feature
77 // that specifies the desktop size (not normally set)
78 const Common::Size& s = myOSystem.settings().getSize("maxres");
79 if(s.valid())
80 {
81 query_w = s.w;
82 query_h = s.h;
83 }
84 // Various parts of the codebase assume a minimum screen size
85 myAbsDesktopSize.w = std::max(query_w, FBMinimum::Width);
86 myAbsDesktopSize.h = std::max(query_h, FBMinimum::Height);
87 myDesktopSize = myAbsDesktopSize;
88
89 // Check for HiDPI mode (is it activated, and can we use it?)
90 myHiDPIAllowed = ((myAbsDesktopSize.w / 2) >= FBMinimum::Width) &&
91 ((myAbsDesktopSize.h / 2) >= FBMinimum::Height);
92 myHiDPIEnabled = myHiDPIAllowed && myOSystem.settings().getBool("hidpi");
93
94 // In HiDPI mode, the desktop resolution is essentially halved
95 // Later, the output is scaled and rendered in 2x mode
96 if(hidpiEnabled())
97 {
98 myDesktopSize.w = myAbsDesktopSize.w / hidpiScaleFactor();
99 myDesktopSize.h = myAbsDesktopSize.h / hidpiScaleFactor();
100 }
101
102#ifdef GUI_SUPPORT
103 ////////////////////////////////////////////////////////////////////
104 // Create fonts to draw text
105 // NOTE: the logic determining appropriate font sizes is done here,
106 // so that the UI classes can just use the font they expect,
107 // and not worry about it
108 // This logic should also take into account the size of the
109 // framebuffer, and try to be intelligent about font sizes
110 // We can probably add ifdefs to take care of corner cases,
111 // but that means we've failed to abstract it enough ...
112 ////////////////////////////////////////////////////////////////////
113
114 // This font is used in a variety of situations when a really small
115 // font is needed; we let the specific widget/dialog decide when to
116 // use it
117 mySmallFont = make_unique<GUI::Font>(GUI::stellaDesc);
118
119 // The general font used in all UI elements
120 // This is determined by the size of the framebuffer
121 if(myOSystem.settings().getBool("minimal_ui"))
122 myFont = make_unique<GUI::Font>(GUI::stellaLargeDesc);
123 else
124 myFont = make_unique<GUI::Font>(GUI::stellaMediumDesc);
125
126 // The info font used in all UI elements
127 // This is determined by the size of the framebuffer
128 myInfoFont = make_unique<GUI::Font>(GUI::consoleDesc);
129
130 // The font used by the ROM launcher
131 const string& lf = myOSystem.settings().getString("launcherfont");
132 if(lf == "small")
133 myLauncherFont = make_unique<GUI::Font>(GUI::consoleDesc);
134 else if(lf == "medium")
135 myLauncherFont = make_unique<GUI::Font>(GUI::stellaMediumDesc);
136 else
137 myLauncherFont = make_unique<GUI::Font>(GUI::stellaLargeDesc);
138#endif
139
140 // Determine possible TIA windowed zoom levels
141 myTIAMaxZoom = maxZoomForScreen(
142 TIAConstants::viewableWidth, TIAConstants::viewableHeight,
143 myAbsDesktopSize.w, myAbsDesktopSize.h);
144
145 setUIPalette();
146
147 myGrabMouse = myOSystem.settings().getBool("grabmouse");
148
149 // Create a TIA surface; we need it for rendering TIA images
150 myTIASurface = make_unique<TIASurface>(myOSystem);
151
152 return true;
153}
154
155// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
156void FrameBuffer::setUIPalette()
157{
158 // Set palette for GUI (upper area of array)
159 int palID = 0;
160 if(myOSystem.settings().getString("uipalette") == "classic")
161 palID = 1;
162 else if(myOSystem.settings().getString("uipalette") == "light")
163 palID = 2;
164
165 for(uInt32 i = 0, j = 256; i < kNumColors - 256; ++i, ++j)
166 {
167 uInt8 r = (ourGUIColors[palID][i] >> 16) & 0xff;
168 uInt8 g = (ourGUIColors[palID][i] >> 8) & 0xff;
169 uInt8 b = ourGUIColors[palID][i] & 0xff;
170
171 myPalette[j] = mapRGB(r, g, b);
172 }
173 FBSurface::setPalette(myPalette);
174}
175
176// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
177FBInitStatus FrameBuffer::createDisplay(const string& title,
178 uInt32 width, uInt32 height,
179 bool honourHiDPI)
180{
181 ++myInitializedCount;
182 myScreenTitle = title;
183
184 // In HiDPI mode, all created displays must be scaled by 2x
185 if(honourHiDPI && hidpiEnabled())
186 {
187 width *= hidpiScaleFactor();
188 height *= hidpiScaleFactor();
189 }
190
191 // A 'windowed' system is defined as one where the window size can be
192 // larger than the screen size, as there's some sort of window manager
193 // that takes care of it (all current desktop systems fall in this category)
194 // However, some systems have no concept of windowing, and have hard limits
195 // on how large a window can be (ie, the size of the 'desktop' is the
196 // absolute upper limit on window size)
197 //
198 // If the WINDOWED_SUPPORT macro is defined, we treat the system as the
199 // former type; if not, as the latter type
200
201 bool useFullscreen = false;
202#ifdef WINDOWED_SUPPORT
203 // We assume that a desktop of at least minimum acceptable size means that
204 // we're running on a 'large' system, and the window size requirements
205 // can be relaxed
206 // Otherwise, we treat the system as if WINDOWED_SUPPORT is not defined
207 if(myDesktopSize.w < FBMinimum::Width && myDesktopSize.h < FBMinimum::Height &&
208 (myDesktopSize.w < width || myDesktopSize.h < height))
209 return FBInitStatus::FailTooLarge;
210
211 useFullscreen = myOSystem.settings().getBool("fullscreen");
212#else
213 // Make sure this mode is even possible
214 // We only really need to worry about it in non-windowed environments,
215 // where requesting a window that's too large will probably cause a crash
216 if(myDesktopSize.w < width || myDesktopSize.h < height)
217 return FBInitStatus::FailTooLarge;
218
219 useFullscreen = true;
220#endif
221
222 // Set the available video modes for this framebuffer
223 setAvailableVidModes(width, height);
224
225 // Initialize video subsystem (make sure we get a valid mode)
226 string pre_about = about();
227 const FrameBuffer::VideoMode& mode = getSavedVidMode(useFullscreen);
228 if(width <= mode.screen.w && height <= mode.screen.h)
229 {
230 // Changing the video mode can take some time, during which the last
231 // sound played may get 'stuck'
232 // So we mute the sound until the operation completes
233 bool oldMuteState = myOSystem.sound().mute(true);
234 if(setVideoMode(myScreenTitle, mode))
235 {
236 myImageRect = mode.image;
237 myScreenSize = mode.screen;
238 myScreenRect = Common::Rect(mode.screen);
239
240 // Inform TIA surface about new mode
241 if(myOSystem.eventHandler().state() != EventHandlerState::LAUNCHER &&
242 myOSystem.eventHandler().state() != EventHandlerState::DEBUGGER)
243 myTIASurface->initialize(myOSystem.console(), mode);
244
245 // Did we get the requested fullscreen state?
246 myOSystem.settings().setValue("fullscreen", fullScreen());
247 resetSurfaces();
248 setCursorState();
249
250 myOSystem.sound().mute(oldMuteState);
251 }
252 else
253 {
254 Logger::error("ERROR: Couldn't initialize video subsystem");
255 return FBInitStatus::FailNotSupported;
256 }
257 }
258 else
259 return FBInitStatus::FailTooLarge;
260
261#ifdef GUI_SUPPORT
262 // Erase any messages from a previous run
263 myMsg.counter = 0;
264
265 // Create surfaces for TIA statistics and general messages
266 const GUI::Font& f = hidpiEnabled() ? infoFont() : font();
267 myStatsMsg.color = kColorInfo;
268 myStatsMsg.w = f.getMaxCharWidth() * 40 + 3;
269 myStatsMsg.h = (f.getFontHeight() + 2) * 3;
270
271 if(!myStatsMsg.surface)
272 {
273 myStatsMsg.surface = allocateSurface(myStatsMsg.w, myStatsMsg.h);
274 myStatsMsg.surface->attributes().blending = true;
275 myStatsMsg.surface->attributes().blendalpha = 92; //aligned with TimeMachineDialog
276 myStatsMsg.surface->applyAttributes();
277 }
278
279 if(!myMsg.surface)
280 myMsg.surface = allocateSurface(FBMinimum::Width, font().getFontHeight()+10);
281#endif
282
283 // Print initial usage message, but only print it later if the status has changed
284 if(myInitializedCount == 1)
285 {
286 Logger::info(about());
287 }
288 else
289 {
290 string post_about = about();
291 if(post_about != pre_about)
292 Logger::info(post_about);
293 }
294
295 return FBInitStatus::Success;
296}
297
298// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
299void FrameBuffer::update(bool force)
300{
301 // Onscreen messages are a special case and require different handling than
302 // other objects; they aren't UI dialogs in the normal sense nor are they
303 // TIA images, and they need to be rendered on top of everything
304 // The logic is split in two pieces:
305 // - at the top of ::update(), to determine whether underlying dialogs
306 // need to be force-redrawn
307 // - at the bottom of ::update(), to actually draw them (this must come
308 // last, since they are always drawn on top of everything else).
309
310 // Full rendering is required when messages are enabled
311 force |= myMsg.counter >= 0;
312
313 // Detect when a message has been turned off; one last redraw is required
314 // in this case, to draw over the area that the message occupied
315 if(myMsg.counter == 0)
316 myMsg.counter = -1;
317
318 switch(myOSystem.eventHandler().state())
319 {
320 case EventHandlerState::NONE:
321 case EventHandlerState::EMULATION:
322 // Do nothing; emulation mode is handled separately (see below)
323 return;
324
325 case EventHandlerState::PAUSE:
326 {
327 // Show a pause message immediately and then every 7 seconds
328 if(myPausedCount-- <= 0)
329 {
330 myPausedCount = uInt32(7 * myOSystem.frameRate());
331 showMessage("Paused", MessagePosition::MiddleCenter);
332 }
333 if(force)
334 myTIASurface->render();
335
336 break; // EventHandlerState::PAUSE
337 }
338
339 #ifdef GUI_SUPPORT
340 case EventHandlerState::OPTIONSMENU:
341 {
342 force |= myOSystem.menu().needsRedraw();
343 if(force)
344 {
345 clear();
346 myTIASurface->render();
347 myOSystem.menu().draw(force);
348 }
349 break; // EventHandlerState::OPTIONSMENU
350 }
351
352 case EventHandlerState::CMDMENU:
353 {
354 force |= myOSystem.commandMenu().needsRedraw();
355 if(force)
356 {
357 clear();
358 myTIASurface->render();
359 myOSystem.commandMenu().draw(force);
360 }
361 break; // EventHandlerState::CMDMENU
362 }
363
364 case EventHandlerState::TIMEMACHINE:
365 {
366 force |= myOSystem.timeMachine().needsRedraw();
367 if(force)
368 {
369 clear();
370 myTIASurface->render();
371 myOSystem.timeMachine().draw(force);
372 }
373 break; // EventHandlerState::TIMEMACHINE
374 }
375
376 case EventHandlerState::LAUNCHER:
377 {
378 force |= myOSystem.launcher().needsRedraw();
379 if(force)
380 {
381 clear();
382 myOSystem.launcher().draw(force);
383 }
384 break; // EventHandlerState::LAUNCHER
385 }
386 #endif
387
388 #ifdef DEBUGGER_SUPPORT
389 case EventHandlerState::DEBUGGER:
390 {
391 force |= myOSystem.debugger().needsRedraw();
392 if(force)
393 {
394 clear();
395 myOSystem.debugger().draw(force);
396 }
397 break; // EventHandlerState::DEBUGGER
398 }
399 #endif
400 default:
401 break;
402 }
403
404 // Draw any pending messages
405 // The logic here determines whether to draw the message
406 // If the message is to be disabled, logic inside the draw method
407 // indicates that, and then the code at the top of this method sees
408 // the change and redraws everything
409 if(myMsg.enabled)
410 force |= drawMessage();
411
412 // Push buffers to screen only when necessary
413 if(force)
414 renderToScreen();
415}
416
417// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
418void FrameBuffer::updateInEmulationMode(float framesPerSecond)
419{
420 // Update method that is specifically tailored to emulation mode
421 // Typically called from a thread, so it needs to be separate from
422 // the normal update() method
423 //
424 // We don't worry about selective rendering here; the rendering
425 // always happens at the full framerate
426
427 clear(); // TODO - test this: it may cause slowdowns on older systems
428 myTIASurface->render();
429
430 // Show frame statistics
431 if(myStatsMsg.enabled)
432 drawFrameStats(framesPerSecond);
433
434 myLastScanlines = myOSystem.console().tia().frameBufferScanlinesLastFrame();
435 myPausedCount = 0;
436
437 // Draw any pending messages
438 if(myMsg.enabled)
439 drawMessage();
440
441 // Push buffers to screen
442 renderToScreen();
443}
444
445// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
446void FrameBuffer::showMessage(const string& message, MessagePosition position,
447 bool force)
448{
449#ifdef GUI_SUPPORT
450 // Only show messages if they've been enabled
451 if(myMsg.surface == nullptr || !(force || myOSystem.settings().getBool("uimessages")))
452 return;
453
454 // Precompute the message coordinates
455 myMsg.text = message;
456 myMsg.counter = uInt32(myOSystem.frameRate()) << 1; // Show message for 2 seconds
457 if(myMsg.counter == 0) myMsg.counter = 60;
458 myMsg.color = kBtnTextColor;
459
460 myMsg.w = font().getStringWidth(myMsg.text) + 10;
461 myMsg.h = font().getFontHeight() + 8;
462 myMsg.surface->setSrcSize(myMsg.w, myMsg.h);
463 myMsg.surface->setDstSize(myMsg.w * hidpiScaleFactor(), myMsg.h * hidpiScaleFactor());
464 myMsg.position = position;
465 myMsg.enabled = true;
466#endif
467}
468
469// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
470void FrameBuffer::drawFrameStats(float framesPerSecond)
471{
472#ifdef GUI_SUPPORT
473 const ConsoleInfo& info = myOSystem.console().about();
474 int xPos = 2, yPos = 0;
475 const GUI::Font& f = hidpiEnabled() ? infoFont() : font();
476 const int dy = f.getFontHeight() + 2;
477
478 ostringstream ss;
479
480 myStatsMsg.surface->invalidate();
481
482 // draw scanlines
483 ColorId color = myOSystem.console().tia().frameBufferScanlinesLastFrame() != myLastScanlines ?
484 kDbgColorRed : myStatsMsg.color;
485
486 ss
487 << myOSystem.console().tia().frameBufferScanlinesLastFrame()
488 << " / "
489 << std::fixed << std::setprecision(1) << myOSystem.console().getFramerate()
490 << "Hz => "
491 << info.DisplayFormat;
492
493 myStatsMsg.surface->drawString(f, ss.str(), xPos, yPos,
494 myStatsMsg.w, color, TextAlign::Left, 0, true, kBGColor);
495
496 yPos += dy;
497 ss.str("");
498
499 ss
500 << std::fixed << std::setprecision(1) << framesPerSecond
501 << "fps @ "
502 << std::fixed << std::setprecision(0) << 100 * myOSystem.settings().getFloat("speed")
503 << "% speed";
504
505 myStatsMsg.surface->drawString(f, ss.str(), xPos, yPos,
506 myStatsMsg.w, myStatsMsg.color, TextAlign::Left, 0, true, kBGColor);
507
508 yPos += dy;
509 ss.str("");
510
511 ss << info.BankSwitch;
512 if (myOSystem.settings().getBool("dev.settings")) ss << "| Developer";
513
514 myStatsMsg.surface->drawString(f, ss.str(), xPos, yPos,
515 myStatsMsg.w, myStatsMsg.color, TextAlign::Left, 0, true, kBGColor);
516
517 myStatsMsg.surface->setDstPos(myImageRect.x() + 10, myImageRect.y() + 8);
518 myStatsMsg.surface->setDstSize(myStatsMsg.w * hidpiScaleFactor(),
519 myStatsMsg.h * hidpiScaleFactor());
520 myStatsMsg.surface->render();
521#endif
522}
523
524// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
525void FrameBuffer::toggleFrameStats()
526{
527 showFrameStats(!myStatsEnabled);
528 myOSystem.settings().setValue(
529 myOSystem.settings().getBool("dev.settings") ? "dev.stats" : "plr.stats", myStatsEnabled);
530}
531
532// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
533void FrameBuffer::showFrameStats(bool enable)
534{
535 myStatsEnabled = myStatsMsg.enabled = enable;
536}
537
538// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
539void FrameBuffer::enableMessages(bool enable)
540{
541 if(enable)
542 {
543 // Only re-enable frame stats if they were already enabled before
544 myStatsMsg.enabled = myStatsEnabled;
545 }
546 else
547 {
548 // Temporarily disable frame stats
549 myStatsMsg.enabled = false;
550
551 // Erase old messages on the screen
552 myMsg.enabled = false;
553 myMsg.counter = 0;
554 update(true); // Force update immediately
555 }
556}
557
558// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
559inline bool FrameBuffer::drawMessage()
560{
561#ifdef GUI_SUPPORT
562 // Either erase the entire message (when time is reached),
563 // or show again this frame
564 if(myMsg.counter == 0)
565 {
566 myMsg.enabled = false;
567 return true;
568 }
569 else if(myMsg.counter < 0)
570 {
571 myMsg.enabled = false;
572 return false;
573 }
574
575 // Draw the bounded box and text
576 const Common::Rect& dst = myMsg.surface->dstRect();
577
578 switch(myMsg.position)
579 {
580 case MessagePosition::TopLeft:
581 myMsg.x = 5;
582 myMsg.y = 5;
583 break;
584
585 case MessagePosition::TopCenter:
586 myMsg.x = (myImageRect.w() - dst.w()) >> 1;
587 myMsg.y = 5;
588 break;
589
590 case MessagePosition::TopRight:
591 myMsg.x = myImageRect.w() - dst.w() - 5;
592 myMsg.y = 5;
593 break;
594
595 case MessagePosition::MiddleLeft:
596 myMsg.x = 5;
597 myMsg.y = (myImageRect.h() - dst.h()) >> 1;
598 break;
599
600 case MessagePosition::MiddleCenter:
601 myMsg.x = (myImageRect.w() - dst.w()) >> 1;
602 myMsg.y = (myImageRect.h() - dst.h()) >> 1;
603 break;
604
605 case MessagePosition::MiddleRight:
606 myMsg.x = myImageRect.w() - dst.w() - 5;
607 myMsg.y = (myImageRect.h() - dst.h()) >> 1;
608 break;
609
610 case MessagePosition::BottomLeft:
611 myMsg.x = 5;
612 myMsg.y = myImageRect.h() - dst.h() - 5;
613 break;
614
615 case MessagePosition::BottomCenter:
616 myMsg.x = (myImageRect.w() - dst.w()) >> 1;
617 myMsg.y = myImageRect.h() - dst.h() - 5;
618 break;
619
620 case MessagePosition::BottomRight:
621 myMsg.x = myImageRect.w() - dst.w() - 5;
622 myMsg.y = myImageRect.h() - dst.h() - 5;
623 break;
624 }
625
626 myMsg.surface->setDstPos(myMsg.x + myImageRect.x(), myMsg.y + myImageRect.y());
627 myMsg.surface->fillRect(1, 1, myMsg.w-2, myMsg.h-2, kBtnColor);
628 myMsg.surface->frameRect(0, 0, myMsg.w, myMsg.h, kColor);
629 myMsg.surface->drawString(font(), myMsg.text, 5, 4,
630 myMsg.w, myMsg.color, TextAlign::Left);
631 myMsg.surface->render();
632 myMsg.counter--;
633#endif
634
635 return true;
636}
637
638// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
639void FrameBuffer::setPauseDelay()
640{
641 myPausedCount = uInt32(2 * myOSystem.frameRate());
642}
643
644// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
645shared_ptr<FBSurface> FrameBuffer::allocateSurface(int w, int h, const uInt32* data)
646{
647 // Add new surface to the list
648 mySurfaceList.push_back(createSurface(w, h, data));
649
650 // And return a pointer to it (pointer should be treated read-only)
651 return mySurfaceList.at(mySurfaceList.size() - 1);
652}
653
654// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
655void FrameBuffer::freeSurfaces()
656{
657 for(auto& s: mySurfaceList)
658 s->free();
659}
660
661// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
662void FrameBuffer::reloadSurfaces()
663{
664 for(auto& s: mySurfaceList)
665 s->reload();
666}
667
668// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
669void FrameBuffer::resetSurfaces()
670{
671 // Free all resources for each surface, then reload them
672 // Due to possible timing and/or synchronization issues, all free()'s
673 // are done first, then all reload()'s
674 // Any derived FrameBuffer classes that call this method should be
675 // aware of these restrictions, and act accordingly
676
677 freeSurfaces();
678 reloadSurfaces();
679
680 update(true); // force full update
681}
682
683// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
684void FrameBuffer::setPalette(const uInt32* raw_palette)
685{
686 // Set palette for normal fill
687 for(int i = 0; i < 256; ++i)
688 {
689 uInt8 r = (raw_palette[i] >> 16) & 0xff;
690 uInt8 g = (raw_palette[i] >> 8) & 0xff;
691 uInt8 b = raw_palette[i] & 0xff;
692
693 myPalette[i] = mapRGB(r, g, b);
694 }
695
696 // Let the TIA surface know about the new palette
697 myTIASurface->setPalette(myPalette, raw_palette);
698}
699
700// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
701void FrameBuffer::stateChanged(EventHandlerState state)
702{
703 // Make sure any onscreen messages are removed
704 myMsg.enabled = false;
705 myMsg.counter = 0;
706
707 update(true); // force full update
708}
709
710// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
711void FrameBuffer::setFullscreen(bool enable)
712{
713#ifdef WINDOWED_SUPPORT
714 // Switching between fullscreen and windowed modes will invariably mean
715 // that the 'window' resolution changes. Currently, dialogs are not
716 // able to resize themselves when they are actively being shown
717 // (they would have to be closed and then re-opened, etc).
718 // For now, we simply disallow screen switches in such modes
719 switch(myOSystem.eventHandler().state())
720 {
721 case EventHandlerState::EMULATION:
722 case EventHandlerState::PAUSE:
723 break; // continue with processing (aka, allow a mode switch)
724 case EventHandlerState::DEBUGGER:
725 case EventHandlerState::LAUNCHER:
726 if(myOSystem.eventHandler().overlay().baseDialogIsActive())
727 break; // allow a mode switch when there is only one dialog
728 [[fallthrough]];
729 default:
730 return;
731 }
732
733 // Changing the video mode can take some time, during which the last
734 // sound played may get 'stuck'
735 // So we mute the sound until the operation completes
736 bool oldMuteState = myOSystem.sound().mute(true);
737
738 const VideoMode& mode = getSavedVidMode(enable);
739 if(setVideoMode(myScreenTitle, mode))
740 {
741 myImageRect = mode.image;
742 myScreenSize = mode.screen;
743 myScreenRect = Common::Rect(mode.screen);
744
745 // Inform TIA surface about new mode
746 if(myOSystem.eventHandler().state() != EventHandlerState::LAUNCHER &&
747 myOSystem.eventHandler().state() != EventHandlerState::DEBUGGER)
748 myTIASurface->initialize(myOSystem.console(), mode);
749
750 // Did we get the requested fullscreen state?
751 myOSystem.settings().setValue("fullscreen", fullScreen());
752 resetSurfaces();
753 setCursorState();
754 }
755 myOSystem.sound().mute(oldMuteState);
756#endif
757}
758
759// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
760void FrameBuffer::toggleFullscreen()
761{
762 setFullscreen(!fullScreen());
763}
764
765// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
766void FrameBuffer::changeOverscan(int direction)
767{
768 if (fullScreen())
769 {
770 int oldOverscan = myOSystem.settings().getInt("tia.fs_overscan");
771 int overscan = BSPF::clamp(oldOverscan + direction, 0, 10);
772
773 if (overscan != oldOverscan)
774 {
775 myOSystem.settings().setValue("tia.fs_overscan", overscan);
776
777 // issue a complete framebuffer re-initialization
778 myOSystem.createFrameBuffer();
779 }
780 ostringstream msg;
781 msg << "Overscan at " << overscan << "%";
782 showMessage(msg.str());
783 }
784}
785
786// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
787bool FrameBuffer::changeVidMode(int direction)
788{
789 EventHandlerState state = myOSystem.eventHandler().state();
790 bool tiaMode = (state != EventHandlerState::DEBUGGER &&
791 state != EventHandlerState::LAUNCHER);
792
793 // Only applicable when in TIA/emulation mode
794 if(!tiaMode)
795 return false;
796
797 if(direction == +1)
798 myCurrentModeList->next();
799 else if(direction == -1)
800 myCurrentModeList->previous();
801 else
802 return false;
803
804 // Changing the video mode can take some time, during which the last
805 // sound played may get 'stuck'
806 // So we mute the sound until the operation completes
807 bool oldMuteState = myOSystem.sound().mute(true);
808
809 const VideoMode& mode = myCurrentModeList->current();
810 if(setVideoMode(myScreenTitle, mode))
811 {
812 myImageRect = mode.image;
813 myScreenSize = mode.screen;
814 myScreenRect = Common::Rect(mode.screen);
815
816 // Inform TIA surface about new mode
817 myTIASurface->initialize(myOSystem.console(), mode);
818
819 resetSurfaces();
820 showMessage(mode.description);
821 myOSystem.sound().mute(oldMuteState);
822
823 if(fullScreen())
824 myOSystem.settings().setValue("tia.fs_stretch",
825 mode.stretch == VideoMode::Stretch::Fill);
826 else
827 myOSystem.settings().setValue("tia.zoom", mode.zoom);
828
829 return true;
830 }
831 myOSystem.sound().mute(oldMuteState);
832
833 return false;
834}
835
836// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
837void FrameBuffer::setCursorState()
838{
839 // Always grab mouse in emulation (if enabled) and emulating a controller
840 // that always uses the mouse
841 bool emulation =
842 myOSystem.eventHandler().state() == EventHandlerState::EMULATION;
843 bool analog = myOSystem.hasConsole() ?
844 (myOSystem.console().leftController().isAnalog() ||
845 myOSystem.console().rightController().isAnalog()) : false;
846 bool alwaysUseMouse = BSPF::equalsIgnoreCase("always", myOSystem.settings().getString("usemouse"));
847
848 grabMouse(emulation && (analog || alwaysUseMouse) && myGrabMouse);
849
850 // Show/hide cursor in UI/emulation mode based on 'cursor' setting
851 switch(myOSystem.settings().getInt("cursor"))
852 {
853 case 0: showCursor(false); break;
854 case 1: showCursor(emulation); break;
855 case 2: showCursor(!emulation); break;
856 case 3: showCursor(true); break;
857 }
858}
859
860// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
861void FrameBuffer::enableGrabMouse(bool enable)
862{
863 myGrabMouse = enable;
864 setCursorState();
865}
866
867// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
868void FrameBuffer::toggleGrabMouse()
869{
870 myGrabMouse = !myGrabMouse;
871 setCursorState();
872 myOSystem.settings().setValue("grabmouse", myGrabMouse);
873}
874
875// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
876float FrameBuffer::maxZoomForScreen(uInt32 baseWidth, uInt32 baseHeight,
877 uInt32 screenWidth, uInt32 screenHeight) const
878{
879 float multiplier = 1;
880 for(;;)
881 {
882 // Figure out the zoomed size of the window
883 uInt32 width = baseWidth * multiplier;
884 uInt32 height = baseHeight * multiplier;
885
886 if((width > screenWidth) || (height > screenHeight))
887 break;
888
889 multiplier += ZOOM_STEPS;
890 }
891 return multiplier > 1 ? multiplier - ZOOM_STEPS : 1;
892}
893
894// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
895void FrameBuffer::setAvailableVidModes(uInt32 baseWidth, uInt32 baseHeight)
896{
897 myWindowedModeList.clear();
898
899 for(auto& mode: myFullscreenModeLists)
900 mode.clear();
901 for(size_t i = myFullscreenModeLists.size(); i < myFullscreenDisplays.size(); ++i)
902 myFullscreenModeLists.push_back(VideoModeList());
903
904 // Check if zooming is allowed for this state (currently only allowed
905 // for TIA screens)
906 EventHandlerState state = myOSystem.eventHandler().state();
907 bool tiaMode = (state != EventHandlerState::DEBUGGER &&
908 state != EventHandlerState::LAUNCHER);
909 float overscan = 1 - myOSystem.settings().getInt("tia.fs_overscan") / 100.0;
910
911 // TIA mode allows zooming at integral factors in windowed modes,
912 // and also non-integral factors in fullscreen mode
913 if(tiaMode)
914 {
915 // TIA windowed modes
916 uInt32 minZoom = supportedTIAMinZoom();
917 myTIAMaxZoom = maxZoomForScreen(baseWidth, baseHeight,
918 myAbsDesktopSize.w, myAbsDesktopSize.h);
919
920 #if 0 // FIXME - does this apply any longer??
921 // Aspect ratio
922 uInt32 aspect = myOSystem.settings().getInt(
923 myOSystem.console().tia().frameLayout() == FrameLayout::ntsc ?
924 "tia.aspectn" : "tia.aspectp");
925 #endif
926
927 // Determine all zoom levels
928 for(float zoom = minZoom; zoom <= myTIAMaxZoom; zoom += ZOOM_STEPS)
929 {
930 ostringstream desc;
931 desc << "Zoom " << zoom << "x";
932
933 VideoMode mode(baseWidth*zoom, baseHeight*zoom, baseWidth*zoom, baseHeight*zoom,
934 VideoMode::Stretch::Fill, 1.0, desc.str(), zoom);
935 myWindowedModeList.add(mode);
936 }
937
938 // TIA fullscreen mode
939 for(uInt32 i = 0; i < myFullscreenDisplays.size(); ++i)
940 {
941 myTIAMaxZoom = maxZoomForScreen(baseWidth, baseHeight,
942 myFullscreenDisplays[i].w * overscan,
943 myFullscreenDisplays[i].h * overscan);
944
945 // Add both normal aspect and filled modes
946 // It's easier to define them both now, and simply switch between
947 // them when necessary
948 VideoMode mode1(baseWidth * myTIAMaxZoom, baseHeight * myTIAMaxZoom,
949 myFullscreenDisplays[i].w, myFullscreenDisplays[i].h,
950 VideoMode::Stretch::Preserve, overscan,
951 "Preserve aspect, no stretch", myTIAMaxZoom, i);
952 myFullscreenModeLists[i].add(mode1);
953 VideoMode mode2(baseWidth * myTIAMaxZoom, baseHeight * myTIAMaxZoom,
954 myFullscreenDisplays[i].w, myFullscreenDisplays[i].h,
955 VideoMode::Stretch::Fill, overscan,
956 "Ignore aspect, full stretch", myTIAMaxZoom, i);
957 myFullscreenModeLists[i].add(mode2);
958 }
959 }
960 else // UI mode
961 {
962 // Windowed and fullscreen mode differ only in screen size
963 myWindowedModeList.add(
964 VideoMode(baseWidth, baseHeight, baseWidth, baseHeight,
965 VideoMode::Stretch::None)
966 );
967 for(uInt32 i = 0; i < myFullscreenDisplays.size(); ++i)
968 {
969 myFullscreenModeLists[i].add(
970 VideoMode(baseWidth, baseHeight,
971 myFullscreenDisplays[i].w, myFullscreenDisplays[i].h,
972 VideoMode::Stretch::None, 1.0, "", 1, i)
973 );
974 }
975 }
976}
977
978// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
979const FrameBuffer::VideoMode& FrameBuffer::getSavedVidMode(bool fullscreen)
980{
981 if(fullscreen)
982 {
983 Int32 i = getCurrentDisplayIndex();
984 if(i < 0)
985 {
986 // default to the first display
987 i = 0;
988 }
989 myCurrentModeList = &myFullscreenModeLists[i];
990 }
991 else
992 myCurrentModeList = &myWindowedModeList;
993
994 // Now select the best resolution depending on the state
995 // UI modes (launcher and debugger) have only one supported resolution
996 // so the 'current' one is the only valid one
997 EventHandlerState state = myOSystem.eventHandler().state();
998 if(state == EventHandlerState::DEBUGGER || state == EventHandlerState::LAUNCHER)
999 myCurrentModeList->setByZoom(1);
1000 else // TIA mode
1001 {
1002 if(fullscreen)
1003 myCurrentModeList->setByStretch(myOSystem.settings().getBool("tia.fs_stretch")
1004 ? VideoMode::Stretch::Fill : VideoMode::Stretch::Preserve);
1005 else
1006 myCurrentModeList->setByZoom(myOSystem.settings().getFloat("tia.zoom"));
1007 }
1008
1009 return myCurrentModeList->current();
1010}
1011
1012// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
1013//
1014// VideoMode implementation
1015//
1016// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
1017FrameBuffer::VideoMode::VideoMode()
1018 : stretch(VideoMode::Stretch::None),
1019 description(""),
1020 zoom(1),
1021 fsIndex(-1)
1022{
1023}
1024
1025// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
1026FrameBuffer::VideoMode::VideoMode(uInt32 iw, uInt32 ih, uInt32 sw, uInt32 sh,
1027 Stretch smode, float overscan, const string& desc,
1028 float zoomLevel, Int32 fsindex)
1029 : stretch(smode),
1030 description(desc),
1031 zoom(zoomLevel),
1032 fsIndex(fsindex)
1033{
1034 // First set default size and positioning
1035 sw = std::max(sw, TIAConstants::viewableWidth);
1036 sh = std::max(sh, TIAConstants::viewableHeight);
1037 iw = std::min(iw, sw);
1038 ih = std::min(ih, sh);
1039 int ix = (sw - iw) >> 1;
1040 int iy = (sh - ih) >> 1;
1041 image = Common::Rect(ix, iy, ix+iw, iy+ih);
1042 screen = Common::Size(sw, sh);
1043
1044 // Now resize based on windowed/fullscreen mode and stretch factor
1045 iw = image.w();
1046 ih = image.h();
1047
1048 if(fsIndex != -1)
1049 {
1050 switch(stretch)
1051 {
1052 case Stretch::Preserve:
1053 {
1054 float stretchFactor = 1.0;
1055 float scaleX = float(iw) / screen.w;
1056 float scaleY = float(ih) / screen.h;
1057
1058 // Scale to all available space, keep aspect correct
1059 if(scaleX > scaleY)
1060 stretchFactor = float(screen.w) / iw;
1061 else
1062 stretchFactor = float(screen.h) / ih;
1063
1064 iw = uInt32(stretchFactor * iw) * overscan;
1065 ih = uInt32(stretchFactor * ih) * overscan;
1066 break;
1067 }
1068
1069 case Stretch::Fill:
1070 // Scale to all available space
1071 iw = screen.w * overscan;
1072 ih = screen.h * overscan;
1073 break;
1074
1075 case Stretch::None:
1076 // Don't do any scaling at all, but obey overscan
1077 iw = std::min(iw, screen.w) * overscan;
1078 ih = std::min(ih, screen.h) * overscan;
1079 break;
1080 }
1081 }
1082 else
1083 {
1084 // In windowed mode, currently the size is scaled to the screen
1085 // TODO - this may be updated if/when we allow variable-sized windows
1086 switch(stretch)
1087 {
1088 case Stretch::Preserve:
1089 case Stretch::Fill:
1090 screen.w = iw;
1091 screen.h = ih;
1092 break;
1093 case Stretch::None:
1094 break; // Do not change image or screen rects whatsoever
1095 }
1096 }
1097
1098 // Now re-calculate the dimensions
1099 iw = std::min(iw, screen.w);
1100 ih = std::min(ih, screen.h);
1101
1102 image.moveTo((screen.w - iw) >> 1, (screen.h - ih) >> 1);
1103 image.setWidth(iw);
1104 image.setHeight(ih);
1105}
1106
1107// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
1108//
1109// VideoModeList implementation
1110//
1111// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
1112FrameBuffer::VideoModeList::VideoModeList()
1113 : myIdx(-1)
1114{
1115}
1116
1117// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
1118void FrameBuffer::VideoModeList::add(const VideoMode& mode)
1119{
1120 myModeList.emplace_back(mode);
1121}
1122
1123// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
1124void FrameBuffer::VideoModeList::clear()
1125{
1126 myModeList.clear();
1127}
1128
1129// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
1130bool FrameBuffer::VideoModeList::empty() const
1131{
1132 return myModeList.empty();
1133}
1134
1135// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
1136uInt32 FrameBuffer::VideoModeList::size() const
1137{
1138 return uInt32(myModeList.size());
1139}
1140
1141// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
1142void FrameBuffer::VideoModeList::previous()
1143{
1144 --myIdx;
1145 if(myIdx < 0) myIdx = int(myModeList.size()) - 1;
1146}
1147
1148// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
1149const FrameBuffer::VideoMode& FrameBuffer::VideoModeList::current() const
1150{
1151 return myModeList[myIdx];
1152}
1153
1154// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
1155void FrameBuffer::VideoModeList::next()
1156{
1157 myIdx = (myIdx + 1) % myModeList.size();
1158}
1159
1160// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
1161void FrameBuffer::VideoModeList::setByZoom(float zoom)
1162{
1163 for(uInt32 i = 0; i < myModeList.size(); ++i)
1164 {
1165 if(myModeList[i].zoom == zoom)
1166 {
1167 myIdx = i;
1168 return;
1169 }
1170 }
1171 myIdx = 0;
1172}
1173
1174// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
1175void FrameBuffer::VideoModeList::setByStretch(FrameBuffer::VideoMode::Stretch stretch)
1176{
1177 for(uInt32 i = 0; i < myModeList.size(); ++i)
1178 {
1179 if(myModeList[i].stretch == stretch)
1180 {
1181 myIdx = i;
1182 return;
1183 }
1184 }
1185 myIdx = 0;
1186}
1187
1188// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
1189/*
1190 Palette is defined as follows:
1191 *** Base colors ***
1192 kColor Normal foreground color (non-text)
1193 kBGColor Normal background color (non-text)
1194 kBGColorLo Disabled background color dark (non-text)
1195 kBGColorHi Disabled background color light (non-text)
1196 kShadowColor Item is disabled
1197 *** Text colors ***
1198 kTextColor Normal text color
1199 kTextColorHi Highlighted text color
1200 kTextColorEm Emphasized text color
1201 kTextColorInv Color for selected text
1202 *** UI elements (dialog and widgets) ***
1203 kDlgColor Dialog background
1204 kWidColor Widget background
1205 kWidColorHi Widget highlight color
1206 kWidFrameColor Border for currently selected widget
1207 *** Button colors ***
1208 kBtnColor Normal button background
1209 kBtnColorHi Highlighted button background
1210 kBtnBorderColor,
1211 kBtnBorderColorHi,
1212 kBtnTextColor Normal button font color
1213 kBtnTextColorHi Highlighted button font color
1214 *** Checkbox colors ***
1215 kCheckColor Color of 'X' in checkbox
1216 *** Scrollbar colors ***
1217 kScrollColor Normal scrollbar color
1218 kScrollColorHi Highlighted scrollbar color
1219 *** Debugger colors ***
1220 kDbgChangedColor Background color for changed cells
1221 kDbgChangedTextColor Text color for changed cells
1222 kDbgColorHi Highlighted color in debugger data cells
1223 kDbgColorRed Red color in debugger
1224 *** Slider colors ***
1225 kSliderColor Enabled slider
1226 kSliderColorHi Focussed slider
1227 kSliderBGColor Enabled slider background
1228 kSliderBGColorHi Focussed slider background
1229 kSliderBGColorLo Disabled slider background
1230 *** Other colors ***
1231 kColorInfo TIA output position color
1232 kColorTitleBar Title bar color
1233 kColorTitleText Title text color
1234 kColorTitleBarLo Disabled title bar color
1235 kColorTitleTextLo Disabled title text color
1236*/
1237uInt32 FrameBuffer::ourGUIColors[3][kNumColors-256] = {
1238 // Standard
1239 { 0x686868, 0x000000, 0xa38c61, 0xdccfa5, 0x404040, // base
1240 0x000000, 0xac3410, 0x9f0000, 0xf0f0cf, // text
1241 0xc9af7c, 0xf0f0cf, 0xd55941, 0xc80000, // UI elements
1242 0xac3410, 0xd55941, 0x686868, 0xdccfa5, 0xf0f0cf, 0xf0f0cf, // buttons
1243 0xac3410, // checkbox
1244 0xac3410, 0xd55941, // scrollbar
1245 0xc80000, 0xffff80, 0xc8c8ff, 0xc80000, // debugger
1246 0xac3410, 0xd55941, 0xdccfa5, 0xf0f0cf, 0xa38c61, // slider
1247 0xffffff, 0xac3410, 0xf0f0cf, 0x686868, 0xdccfa5 // other
1248 },
1249 // Classic
1250 { 0x686868, 0x000000, 0x404040, 0x404040, 0x404040, // base
1251 0x20a020, 0x00ff00, 0xc80000, 0x000000, // text
1252 0x000000, 0x000000, 0x00ff00, 0xc80000, // UI elements
1253 0x000000, 0x000000, 0x686868, 0x00ff00, 0x20a020, 0x00ff00, // buttons
1254 0x20a020, // checkbox
1255 0x20a020, 0x00ff00, // scrollbar
1256 0xc80000, 0x00ff00, 0xc8c8ff, 0xc80000, // debugger
1257 0x20a020, 0x00ff00, 0x404040, 0x686868, 0x404040, // slider
1258 0x00ff00, 0x20a020, 0x000000, 0x686868, 0x404040 // other
1259 },
1260 // Light
1261 { 0x808080, 0x000000, 0xc0c0c0, 0xe1e1e1, 0x333333, // base
1262 0x000000, 0xBDDEF9, 0x0078d7, 0x000000, // text
1263 0xf0f0f0, 0xffffff, 0x0078d7, 0x0f0f0f, // UI elements
1264 0xe1e1e1, 0xe5f1fb, 0x808080, 0x0078d7, 0x000000, 0x000000, // buttons
1265 0x333333, // checkbox
1266 0xc0c0c0, 0x808080, // scrollbar
1267 0xffc0c0, 0x000000, 0xe00000, 0xc00000, // debugger
1268 0x333333, 0x0078d7, 0xc0c0c0, 0xffffff, 0xc0c0c0, // slider 0xBDDEF9| 0xe1e1e1 | 0xffffff
1269 0xffffff, 0x333333, 0xf0f0f0, 0x808080, 0xc0c0c0 // other
1270 }
1271};
1272