diff --git a/Changes.txt b/Changes.txt index 1f5156f84..e5718a38e 100644 --- a/Changes.txt +++ b/Changes.txt @@ -24,7 +24,7 @@ * Added 2nd UI theme and hotkey for toggling UI theme. - * Adde bezel support. + * Added bezel support. (TODO: Doc) * Added optional type format detection based on colors used. diff --git a/src/common/FBBackendSDL2.hxx b/src/common/FBBackendSDL2.hxx index acc3952c7..7ab01def2 100644 --- a/src/common/FBBackendSDL2.hxx +++ b/src/common/FBBackendSDL2.hxx @@ -101,6 +101,18 @@ class FBBackendSDL2 : public FBBackend FORCE_INLINE void getRGB(uInt32 pixel, uInt8* r, uInt8* g, uInt8* b) const override { SDL_GetRGB(pixel, myPixelFormat, r, g, b); } + /** + This method is called to retrieve the R/G/B/A data from the given pixel. + + @param pixel The pixel containing R/G/B data + @param r The red component of the color + @param g The green component of the color + @param b The blue component of the color + @param a The alpha component of the color. + */ + FORCE_INLINE void getRGBA(uInt32 pixel, uInt8* r, uInt8* g, uInt8* b, uInt8* a) const override + { SDL_GetRGBA(pixel, myPixelFormat, r, g, b, a); } + /** This method is called to map a given R/G/B triple to the screen palette. @@ -111,6 +123,14 @@ class FBBackendSDL2 : public FBBackend inline uInt32 mapRGB(uInt8 r, uInt8 g, uInt8 b) const override { return SDL_MapRGB(myPixelFormat, r, g, b); } + /** + This method is called to map a given R/G/B/A triple to the screen palette. + + @param r The red component of the color. + @param g The green component of the color. + @param b The blue component of the color. + @param a The alpha component of the color. + */ inline uInt32 mapRGBA(uInt8 r, uInt8 g, uInt8 b, uInt8 a) const override { return SDL_MapRGBA(myPixelFormat, r, g, b, a); } diff --git a/src/common/VideoModeHandler.cxx b/src/common/VideoModeHandler.cxx index e26ac4760..df343c0a2 100644 --- a/src/common/VideoModeHandler.cxx +++ b/src/common/VideoModeHandler.cxx @@ -16,6 +16,8 @@ //============================================================================ #include "Settings.hxx" +#include "Bezel.hxx" + #include "VideoModeHandler.hxx" // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -33,7 +35,7 @@ void VideoModeHandler::setDisplaySize(const Common::Size& display, Int32 fsIndex // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - const VideoModeHandler::Mode& - VideoModeHandler::buildMode(const Settings& settings, bool inTIAMode, bool showBezel) + VideoModeHandler::buildMode(const Settings& settings, bool inTIAMode, Bezel::Info bezelInfo) { const bool windowedRequested = myFSIndex == -1; @@ -42,25 +44,24 @@ const VideoModeHandler::Mode& { if(windowedRequested) { - const float zoom = settings.getFloat("tia.zoom"); + const double zoom = settings.getFloat("tia.zoom"); ostringstream desc; desc << (zoom * 100) << "%"; // Image and screen (aka window) dimensions are the same // Overscan is not applicable in this mode - myMode = Mode(myImage.w * zoom, myImage.h * zoom, Mode::Stretch::Fill, - myFSIndex, desc.str(), zoom, showBezel); + myMode = Mode(myImage.w, myImage.h, + Mode::Stretch::Fill, myFSIndex, + desc.str(), zoom, bezelInfo); } else { - const float overscan = 1 - settings.getInt("tia.fs_overscan") / 100.0; + const double overscan = 1 - settings.getInt("tia.fs_overscan") / 100.0; // First calculate maximum zoom that keeps aspect ratio - // Note: We are assuming a 16:9 bezel image here - const float bezelScaleW = showBezel ? (16.F / 9.F) / (4.F / 3.F) : 1; - const float scaleX = myImage.w / (myDisplay.w / bezelScaleW), - scaleY = static_cast(myImage.h) / myDisplay.h; - float zoom = 1.F / std::max(scaleX, scaleY); + const double scaleX = static_cast(myImage.w) / (myDisplay.w / bezelInfo.ratioW()), + scaleY = static_cast(myImage.h) / (myDisplay.h / bezelInfo.ratioH()); + double zoom = 1. / std::max(scaleX, scaleY); // When aspect ratio correction is off, we want pixel-exact images, // so we default to integer zooming @@ -69,20 +70,19 @@ const VideoModeHandler::Mode& if(!settings.getBool("tia.fs_stretch")) // preserve aspect, use all space { - myMode = Mode(myImage.w * zoom, myImage.h * zoom, + myMode = Mode(myImage.w, myImage.h, myDisplay.w, myDisplay.h, Mode::Stretch::Preserve, myFSIndex, "Fullscreen: Preserve aspect, no stretch", - zoom, overscan, - showBezel, showBezel ? settings.getInt("bezel.border") : 0); + zoom, overscan, bezelInfo); } else // ignore aspect, use all space { - myMode = Mode(myImage.w * zoom, myImage.h * zoom, + myMode = Mode(myImage.w, myImage.h, myDisplay.w, myDisplay.h, Mode::Stretch::Fill, myFSIndex, "Fullscreen: Ignore aspect, full stretch", - zoom, overscan, showBezel); + zoom, overscan, bezelInfo); } } } @@ -94,54 +94,48 @@ const VideoModeHandler::Mode& myMode = Mode(myImage.w, myImage.h, myDisplay.w, myDisplay.h, Mode::Stretch::None, myFSIndex); } - return myMode; } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - VideoModeHandler::Mode::Mode(uInt32 iw, uInt32 ih, Stretch smode, Int32 fsindex, string_view desc, - float zoomLevel, - bool showBezel, Int32 bezelBorder) - : Mode(iw, ih, iw, ih, smode, fsindex, desc, zoomLevel, 1.F, showBezel, bezelBorder) + double zoomLevel, Bezel::Info bezelInfo) + : Mode(iw, ih, iw, ih, smode, fsindex, desc, zoomLevel, 1., bezelInfo) { } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - VideoModeHandler::Mode::Mode(uInt32 iw, uInt32 ih, uInt32 sw, uInt32 sh, Stretch smode, Int32 fsindex, string_view desc, - float zoomLevel, float overscan, - bool showBezel, Int32 bezelBorder) + double zoomLevel, double overscan, Bezel::Info bezelInfo) : screenS{sw, sh}, stretch{smode}, description{desc}, - zoom{zoomLevel}, + zoom{zoomLevel}, //hZoom{zoomLevel}, vZoom{zoomLevel}, fsIndex{fsindex} { - // Note: We are assuming a 16:9 bezel image here - const float bezelScaleW = showBezel ? (16.F / 9.F) / (4.F / 3.F) : 1; // Now resize based on windowed/fullscreen mode and stretch factor if(fsIndex != -1) // fullscreen mode { switch(stretch) { case Stretch::Preserve: - iw = (iw - bezelBorder * 4.F / 3.F * zoomLevel) * overscan; - ih = (ih - bezelBorder * zoomLevel) * overscan; - //iw *= overscan; - //ih *= overscan; + iw = std::round(iw * overscan * zoomLevel); + ih = std::round(ih * overscan * zoomLevel); break; case Stretch::Fill: + { // Scale to all available space - iw = screenS.w * (overscan / bezelScaleW); - ih = screenS.h * overscan; + iw = std::round(screenS.w * overscan / bezelInfo.ratioW()); + ih = std::round(screenS.h * overscan / bezelInfo.ratioH()); break; - + } case Stretch::None: // UI Mode // Don't do any scaling at all - iw = std::min(iw, screenS.w) * overscan; - ih = std::min(ih, screenS.h) * overscan; + iw = std::min(static_cast(iw * zoomLevel), screenS.w) * overscan; + ih = std::min(static_cast(ih * zoomLevel), screenS.h) * overscan; break; } } @@ -153,8 +147,10 @@ VideoModeHandler::Mode::Mode(uInt32 iw, uInt32 ih, uInt32 sw, uInt32 sh, { case Stretch::Preserve: case Stretch::Fill: - screenS.w = iw * bezelScaleW; - screenS.h = ih; + iw *= zoomLevel; + ih *= zoomLevel; + screenS.w = std::round(iw * bezelInfo.ratioW()); + screenS.h = std::round(ih * bezelInfo.ratioH()); break; case Stretch::None: // UI Mode @@ -166,7 +162,17 @@ VideoModeHandler::Mode::Mode(uInt32 iw, uInt32 ih, uInt32 sw, uInt32 sh, iw = std::min(iw, screenS.w); ih = std::min(ih, screenS.h); - imageR.moveTo((screenS.w - iw) >> 1, (screenS.h - ih) >> 1); + // Allow variable image positions in asymmetric bezels + // (works in case of no bezel too) + const uInt32 wx = bezelInfo.window().x() * iw / bezelInfo.window().w(); + const uInt32 wy = bezelInfo.window().y() * ih / bezelInfo.window().h(); + const uInt32 bezelW = std::min(screenS.w, + static_cast(std::round(iw * bezelInfo.ratioW()))); + const uInt32 bezelH = std::min(screenS.h, + static_cast(std::round(ih * bezelInfo.ratioH()))); + // Center image (no bezel) or move image relative to centered bezel + imageR.moveTo(((screenS.w - bezelW) >> 1) + wx, ((screenS.h - bezelH) >> 1) + wy); + imageR.setWidth(iw); imageR.setHeight(ih); diff --git a/src/common/VideoModeHandler.hxx b/src/common/VideoModeHandler.hxx index a13b159f0..544f9ae8d 100644 --- a/src/common/VideoModeHandler.hxx +++ b/src/common/VideoModeHandler.hxx @@ -22,6 +22,7 @@ class Settings; #include "Rect.hxx" #include "bspf.hxx" +#include "Bezel.hxx" class VideoModeHandler { @@ -38,35 +39,23 @@ class VideoModeHandler Fill, // Stretch to fill all available space None // No stretching (1x zoom) }; - struct BezelInfo - { - bool enabled{false}; - bool windowedMode{false}; - uInt32 topBorder{0}; - uInt32 bottomBorder{0}; - - BezelInfo() = default; - BezelInfo(bool _enabled, bool _windowedMode, uInt32 _topBorder, uInt32 _bottomBorder) - : enabled{_enabled}, windowedMode{_windowedMode}, - topBorder{_topBorder}, bottomBorder(_bottomBorder) { } - }; Common::Rect imageR; Common::Rect screenR; Common::Size screenS; Stretch stretch{Mode::Stretch::None}; string description; - float zoom{1.F}; + double zoom{1.}; Int32 fsIndex{-1}; // -1 indicates windowed mode Mode() = default; Mode(uInt32 iw, uInt32 ih, uInt32 sw, uInt32 sh, Stretch smode, Int32 fsindex = -1, string_view desc = "", - float zoomLevel = 1.F, float overscan = 1.F, - bool showBezel = false, Int32 bezelBorder = 0); + double zoomLevel = 1., double overscan = 1., + Bezel::Info bezelInfo = Bezel::Info()); Mode(uInt32 iw, uInt32 ih, Stretch smode, Int32 fsindex = -1, - string_view desc = "", float zoomLevel = 1.F, - bool showBezel = false, Int32 bezelBorder = 0); + string_view desc = "", double zoomLevel = 1., + Bezel::Info bezelInfo = Bezel::Info()); friend ostream& operator<<(ostream& os, const Mode& vm) { @@ -108,8 +97,8 @@ class VideoModeHandler @return A video mode based on the given criteria */ - const VideoModeHandler::Mode& buildMode(const Settings& settings, - bool inTIAMode, bool showBezel); + const VideoModeHandler::Mode& buildMode(const Settings& settings, bool inTIAMode, + Bezel::Info bezelInfo = Bezel::Info()); private: Common::Size myImage, myDisplay; diff --git a/src/emucore/Bezel.cxx b/src/emucore/Bezel.cxx new file mode 100644 index 000000000..809fcd46b --- /dev/null +++ b/src/emucore/Bezel.cxx @@ -0,0 +1,198 @@ +//============================================================================ +// +// SSSS tt lll lll +// SS SS tt ll ll +// SS tttttt eeee ll ll aaaa +// SSSS tt ee ee ll ll aa +// SS tt eeeeee ll ll aaaaa -- "An Atari 2600 VCS Emulator" +// SS SS tt ee ll ll aa aa +// SSSS ttt eeeee llll llll aaaaa +// +// Copyright (c) 1995-2023 by Bradford W. Mott, Stephen Anthony +// and the Stella Team +// +// See the file "License.txt" for information on usage and redistribution of +// this file, and for a DISCLAIMER OF ALL WARRANTIES. +//============================================================================ + +#include "OSystem.hxx" +#include "Console.hxx" +#include "EventHandler.hxx" +#include "FBSurface.hxx" +#include "PNGLibrary.hxx" + +#include "Bezel.hxx" + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +Bezel::Bezel(OSystem& osystem) + : myOSystem{osystem}, + myFB{osystem.frameBuffer()} +{ +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +const string Bezel::getName(int& index) const +{ + if(++index == 1) + return myOSystem.console().properties().get(PropType::Bezel_Name); + + // Try to generate bezel name from cart name + const string& cartName = myOSystem.console().properties().get(PropType::Cart_Name); + const size_t pos = cartName.find_first_of("("); + if(index < 10 && pos != std::string::npos && pos > 0) + { + // The following suffixes are from "The Official No-Intro Convention", + // covering all used combinations by "The Bezel Project" (except single ones) + // (Unl) = unlicensed (Homebrews) + const std::array suffixes = { + " (USA)", " (USA) (Proto)", " (USA) (Unl)", " (USA) (Hack)", + " (Europe)", " (Germany)", " (France) (Unl)", " (Australia)" + }; + return cartName.substr(0, pos - 1) + suffixes[index - 2]; + } + + if(index == 10) + { + index = -1; + return "default"; + } + return ""; +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +uInt32 Bezel::borderSize(uInt32 x, uInt32 y, uInt32 size, Int32 step) const +{ + uInt32 *pixels{nullptr}, pitch; + uInt32 i; + + mySurface->basePtr(pixels, pitch); + pixels += x + y * pitch; + + for(i = 0; i < size; ++i, pixels += step) + { + uInt8 r, g, b, a; + + myFB.getRGBA(*pixels, &r, &g, &b, &a); + if(a < 255) + return i; + } + return size; +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +bool Bezel::load() +{ + bool isValid = false; + +#ifdef IMAGE_SUPPORT + const bool isShown = myOSystem.eventHandler().inTIAMode() && + myOSystem.settings().getBool("bezel.show") && + (myFB.fullScreen() || myOSystem.settings().getBool("bezel.windowed")); + + if(mySurface) + myFB.deallocateSurface(mySurface); + mySurface = nullptr; + + if(isShown) + { + double aspectRatio = 1; + + mySurface = myFB.allocateSurface(1, 1); // dummy size + try + { + const string& path = myOSystem.bezelDir().getPath(); + string imageName; + VariantList metaData; + int index = 0; + + do + { + const string& name = getName(index); + if(name != EmptyString) + { + imageName = path + name + ".png"; + FSNode node(imageName); + if(node.exists()) + { + isValid = true; + break; + } + } + } while(index != -1); + if(isValid) + myOSystem.png().loadImage(imageName, *mySurface, &aspectRatio, metaData); + } + catch(const runtime_error&) + { + isValid = false; + } + } +#endif + if(isValid) + { + const Settings& settings = myOSystem.settings(); + const Int32 w = mySurface->width(); + const Int32 h = mySurface->height(); + uInt32 top, bottom, left, right; + + if(settings.getBool("bezel.autoborders")) + { + // Determine transparent window inside bezel image + top = borderSize(w >> 1, 0, h, w); + bottom = h - 1 - borderSize(w >> 1, h - 1, h, -w); + left = borderSize(0, (bottom + top) >> 1, w, 1); + right = w - 1 - borderSize(w - 1, (bottom + top) >> 1, w, -1); + } + else + { + left = std::min(w, settings.getInt("bezel.leftborder")); + right = w - 1 - std::min(w, settings.getInt("bezel.rightborder")); + top = std::min(h, settings.getInt("bezel.topborder")); + bottom = h - 1 - std::min(h, settings.getInt("bezel.bottomborder")); + } + //cerr << right - left + 1 << " x " << bottom - top + 1 << " = " + // << double(right - left + 1) / double(bottom - top + 1); + myInfo = Info(Common::Size(w, h), Common::Rect(left, top, right, bottom)); + } + else + myInfo = Info(); + + return isValid; +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +void Bezel::apply() +{ + if(isShown()) + { + const uInt32 bezelW = + std::min(myFB.screenSize().w, + static_cast(std::round(myFB.imageRect().w() * myInfo.ratioW()))); + const uInt32 bezelH = + std::min(myFB.screenSize().h, + static_cast(std::round(myFB.imageRect().h() * myInfo.ratioH()))); + + // Position and scale bezel + mySurface->setDstSize(bezelW, bezelH); + mySurface->setDstPos((myFB.screenSize().w - bezelW) / 2, // center + (myFB.screenSize().h - bezelH) / 2); + mySurface->setScalingInterpolation(ScalingInterpolation::sharp); + // Note: Variable bezel window positions are handled in VideoModeHandler::Mode + + // Enable blending to allow overlaying the bezel over the TIA output + mySurface->attributes().blending = true; + mySurface->attributes().blendalpha = 100; + mySurface->applyAttributes(); + mySurface->setVisible(true); + } + else + if(mySurface) + mySurface->setVisible(false); +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +void Bezel::render() +{ + if(mySurface) + mySurface->render(); +} diff --git a/src/emucore/Bezel.hxx b/src/emucore/Bezel.hxx new file mode 100644 index 000000000..8bdeec952 --- /dev/null +++ b/src/emucore/Bezel.hxx @@ -0,0 +1,140 @@ +//============================================================================ +// +// SSSS tt lll lll +// SS SS tt ll ll +// SS tttttt eeee ll ll aaaa +// SSSS tt ee ee ll ll aa +// SS tt eeeeee ll ll aaaaa -- "An Atari 2600 VCS Emulator" +// SS SS tt ee ll ll aa aa +// SSSS ttt eeeee llll llll aaaaa +// +// Copyright (c) 1995-2023 by Bradford W. Mott, Stephen Anthony +// and the Stella Team +// +// See the file "License.txt" for information on usage and redistribution of +// this file, and for a DISCLAIMER OF ALL WARRANTIES. +//============================================================================ + +#ifndef BEZEL_HXX +#define BEZEL_HXX + +class OSystem; +class FBSurface; +class FrameBuffer; + +#include "Rect.hxx" + +/** + This class handles the bezels. + + Bezels are loaded using a file name which is either a bezel name property or + is autogenerated from the cart name property. The bezels can be any size and + their transparent emulation window can be at any position. The position of + the window can be determined automatically. + + +--------------------------------------+ + | | display.h + +--------------------------------------+ + | | + | +---------------+ | + | | window | | + | | | | + | | tia.h * zoom | | + | | | | bezel.h * zoom + | | | | + | +---------------+ | + | | + +--------------------------------------+ size + | | + +--------------------------------------+ + + The bezel and window sizes and their ratios are used for correct scaling. + + @author Thomas Jentzsch +*/ + +class Bezel +{ + public: + explicit Bezel(OSystem& osystem); + ~Bezel() = default; + + struct Info + { + private: + bool _isShown{false}; // Is bezel shown? + Common::Size _size{1, 1}; // Bezel size + Common::Rect _window{1, 1}; // Area of transparent TIA window inside bezel + + public: + explicit Info() = default; + explicit Info(Common::Size size, Common::Rect window) + : _isShown{true}, _size{size}, _window{window} { } + + bool isShown() const { return _isShown; } + Common::Size size() const { return _size; } + Common::Rect window() const { return _window; } + + // Ratios between bezel sizes and TIA window sizes + double ratioW() const { return static_cast(size().w) / window().w(); } + double ratioH() const { return static_cast(size().h) / window().h(); } + }; + + // Structure access methods + const Info& info() const { return myInfo; } + bool isShown() const { return myInfo.isShown(); } + Common::Size size() const { return myInfo.size(); } + Common::Rect window() const { return myInfo.window(); } + // Ratio between bezel size and TIA window size + double ratioW() const { return myInfo.ratioW(); } + double ratioH() const { return myInfo.ratioH(); } + + /* + Calculate size of a bezel border. + */ + uInt32 borderSize(uInt32 x, uInt32 y, uInt32 size, Int32 step) const; + + /* + Load the bezel. + */ + bool load(); + + /* + Display scaled bezel. + */ + void apply(); + + /* + Render bezel surface + */ + void render(); + + private: + /* + Generate bezel file name. + */ + const string getName(int& index) const; + + private: + // The parent system for the bezel + OSystem& myOSystem; + + // Pointer to the FrameBuffer object + FrameBuffer& myFB; + + // The bezel surface which blends over the TIA surface + shared_ptr mySurface; + + // Bezel info structure + Info myInfo; + + private: + // Following constructors and assignment operators not supported + Bezel() = delete; + Bezel(const Bezel&) = delete; + Bezel(Bezel&&) = delete; + Bezel& operator=(const Bezel&) = delete; + Bezel& operator=(Bezel&&) = delete; +}; + +#endif diff --git a/src/emucore/FBBackend.hxx b/src/emucore/FBBackend.hxx index 7ad3455c1..a430ba6a2 100644 --- a/src/emucore/FBBackend.hxx +++ b/src/emucore/FBBackend.hxx @@ -128,6 +128,17 @@ class FBBackend */ virtual void getRGB(uInt32 pixel, uInt8* r, uInt8* g, uInt8* b) const = 0; + /** + This method is called to retrieve the R/G/B/A data from the given pixel. + + @param pixel The pixel containing R/G/B data + @param r The red component of the color + @param g The green component of the color + @param b The blue component of the color + @param a The alpha component of the color. + */ + virtual void getRGBA(uInt32 pixel, uInt8* r, uInt8* g, uInt8* b, uInt8* a) const = 0; + /** This method is called to map a given R/G/B triple to the screen palette. @@ -143,6 +154,7 @@ class FBBackend @param r The red component of the color. @param g The green component of the color. @param b The blue component of the color. + @param a The alpha component of the color. */ virtual uInt32 mapRGBA(uInt8 r, uInt8 g, uInt8 b, uInt8 a) const = 0; diff --git a/src/emucore/FrameBuffer.cxx b/src/emucore/FrameBuffer.cxx index 1729293e3..fb4421c67 100644 --- a/src/emucore/FrameBuffer.cxx +++ b/src/emucore/FrameBuffer.cxx @@ -31,6 +31,7 @@ #include "FBSurface.hxx" #include "TIASurface.hxx" +#include "Bezel.hxx" #include "FrameBuffer.hxx" #include "PaletteHandler.hxx" #include "StateManager.hxx" @@ -126,6 +127,8 @@ void FrameBuffer::initialize() // Create a TIA surface; we need it for rendering TIA images myTIASurface = make_unique(myOSystem); + // Create a bezel surface for TIA overlays + myBezel = make_unique(myOSystem); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -278,8 +281,10 @@ FBInitStatus FrameBuffer::createDisplay(string_view title, BufferType type, if(myBufferType == BufferType::Emulator) { + myBezel->load(); + // Determine possible TIA windowed zoom levels - const float currentTIAZoom = myOSystem.settings().getFloat("tia.zoom"); + const double currentTIAZoom = myOSystem.settings().getFloat("tia.zoom"); myOSystem.settings().setValue("tia.zoom", BSPF::clampw(currentTIAZoom, supportedTIAMinZoom(), supportedTIAMaxZoom())); } @@ -960,8 +965,8 @@ void FrameBuffer::renderTIA(bool shade, bool doClear) clear(); // TODO - test this: it may cause slowdowns on older systems myTIASurface->render(shade); - if(myBezelSurface) - myBezelSurface->render(); + if(myBezel) + myBezel->render(); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -1146,7 +1151,7 @@ void FrameBuffer::toggleFullscreen(bool toggle) msg << "enabled (" << myBackend->refreshRate() << " Hz, "; else msg << "disabled ("; - msg << "Zoom " << myActiveVidMode.zoom * 100 << "%)"; + msg << "Zoom " << round(myActiveVidMode.zoom * 100) << "%)"; } else { @@ -1228,7 +1233,7 @@ void FrameBuffer::switchVideoMode(int direction) if(!fullScreen()) { // Windowed TIA modes support variable zoom levels - float zoom = myOSystem.settings().getFloat("tia.zoom"); + double zoom = myOSystem.settings().getFloat("tia.zoom"); if(direction == +1) zoom += ZOOM_STEPS; else if(direction == -1) zoom -= ZOOM_STEPS; @@ -1271,17 +1276,10 @@ FBInitStatus FrameBuffer::applyVideoMode() myVidModeHandler.setDisplaySize(myAbsDesktopSize[display]); const bool inTIAMode = myOSystem.eventHandler().inTIAMode(); -#ifdef IMAGE_SUPPORT - const bool showBezel = inTIAMode && - myOSystem.settings().getBool("bezel.show") && - (fullScreen() || myOSystem.settings().getBool("bezel.windowed")) && - checkBezel(); -#else - const bool showBezel = false; -#endif // Build the new mode based on current settings - const VideoModeHandler::Mode& mode = myVidModeHandler.buildMode(s, inTIAMode, showBezel); + const VideoModeHandler::Mode& mode + = myVidModeHandler.buildMode(s, inTIAMode, myBezel->info()); if(mode.imageR.size() > mode.screenS) return FBInitStatus::FailTooLarge; @@ -1306,11 +1304,7 @@ FBInitStatus FrameBuffer::applyVideoMode() if(inTIAMode) { #ifdef IMAGE_SUPPORT - if(myBezelSurface) - deallocateSurface(myBezelSurface); - myBezelSurface = nullptr; - if(showBezel) - loadBezel(); + myBezel->apply(); #endif myTIASurface->initialize(myOSystem.console(), myActiveVidMode); @@ -1334,147 +1328,18 @@ FBInitStatus FrameBuffer::applyVideoMode() return status; } -#ifdef IMAGE_SUPPORT -// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -const string FrameBuffer::getBezelName(int& index) const -{ - if(++index == 1) - return myOSystem.console().properties().get(PropType::Bezel_Name); - - // Try to generate bezel name from cart name - const string& cartName = myOSystem.console().properties().get(PropType::Cart_Name); - const size_t pos = cartName.find_first_of("("); - if(index < 10 && pos != std::string::npos && pos > 0) - { - // The following suffixes are from "The Official No-Intro Convention", - // covering all used combinations by "The Bezel Project" (except single ones) - // (Unl) = unlicensed (Homebrews) - const std::array suffixes = { - " (USA)", " (USA) (Proto)", " (USA) (Unl)", " (USA) (Hack)", - " (Europe)", " (Germany)", " (France) (Unl)", " (Australia)" - }; - return cartName.substr(0, pos - 1) + suffixes[index - 2]; - } - - if(index == 10) - { - index = -1; - return "default"; - } - return ""; -} // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -bool FrameBuffer::checkBezel() -{ - const string& path = myOSystem.bezelDir().getPath(); - int index = 0; - - do - { - const string& name = getBezelName(index); - - if(name != EmptyString) - { - FSNode node(path + name + ".png"); - if(node.exists()) - return true; - } - } while (index != -1); - return false; -} - -// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -bool FrameBuffer::loadBezel() -{ - bool isValid = false; - double aspectRatio = 1; - - myBezelSurface = allocateSurface(myActiveVidMode.screenS.w, myActiveVidMode.screenS.h); - try - { - const string& path = myOSystem.bezelDir().getPath(); - string imageName; - VariantList metaData; - int index = 0; - - do - { - const string& name = getBezelName(index); - if(name != EmptyString) - { - imageName = path + name + ".png"; - FSNode node(imageName); - if(node.exists()) - { - isValid = true; - break; - } - } - } while (index != -1); - if(isValid) - myOSystem.png().loadImage(imageName, *myBezelSurface, &aspectRatio, metaData); - } - catch(const runtime_error&) - { - isValid = false; - } - - if(isValid) - { - const float overscan = 1 - myOSystem.settings().getInt("tia.fs_overscan") / 100.F; - - uInt32 imageW, imageH; - if(fullScreen()) - { - const float bezelBorder = myOSystem.settings().getInt("bezel.border") * overscan * myActiveVidMode.zoom; - imageW = (myActiveVidMode.imageR.w() + static_cast(bezelBorder * 4.F / 3.F)) * (16.F / 9.F) / (4.F / 3.F); - imageH = myActiveVidMode.imageR.h() + static_cast(bezelBorder); - } - else - { - imageW = static_cast(myActiveVidMode.imageR.w() * (16.F / 9.F) / (4.F / 3.F)); - imageH = myActiveVidMode.imageR.h(); - } - - // Scale bezel to fullscreen (preserve or stretch) or window size - const uInt32 bezelW = std::min( - myActiveVidMode.screenS.w, imageW); - //static_cast(myActiveVidMode.imageR.w() * (16.F / 9.F) / (4.F / 3.F)) + static_cast(40 * myActiveVidMode.zoom)); - const uInt32 bezelH = std::min( - myActiveVidMode.screenS.h, imageH); - //myActiveVidMode.imageR.h() + static_cast(30 * myActiveVidMode.zoom)); - //cerr << bezelW << " x " << bezelH << endl; - myBezelSurface->setDstSize(bezelW, bezelH); - myBezelSurface->setDstPos((myActiveVidMode.screenS.w - bezelW) / 2, - (myActiveVidMode.screenS.h - bezelH) / 2); // center - myBezelSurface->setScalingInterpolation(ScalingInterpolation::sharp); - - // Enable blending to allow overlaying the bezel over the TIA output - myBezelSurface->attributes().blending = true; - myBezelSurface->attributes().blendalpha = 100; - myBezelSurface->applyAttributes(); - } - if(myBezelSurface) - myBezelSurface->setVisible(isValid); - return isValid; -} -#endif - -// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -float FrameBuffer::maxWindowZoom() const +double FrameBuffer::maxWindowZoom() const { const int display = displayId(BufferType::Emulator); - float multiplier = 1; - - const bool showBezel = myOSystem.settings().getBool("bezel.show"); - const float scaleW = showBezel ? (16.F / 9.F) / (4.F / 3.F) : 1.F; // = 1.333 + double multiplier = 1; for(;;) { - // Figure out the zoomed size of the window - const uInt32 width = TIAConstants::viewableWidth * multiplier * scaleW; - const uInt32 height = TIAConstants::viewableHeight * multiplier; + // Figure out the zoomed size of the window (incl. the bezel) + const uInt32 width = static_cast(TIAConstants::viewableWidth) * myBezel->ratioW() * multiplier; + const uInt32 height = static_cast(TIAConstants::viewableHeight) * myBezel->ratioH() * multiplier; if((width > myAbsDesktopSize[display].w) || (height > myAbsDesktopSize[display].h)) diff --git a/src/emucore/FrameBuffer.hxx b/src/emucore/FrameBuffer.hxx index a28b09195..54930d2f9 100644 --- a/src/emucore/FrameBuffer.hxx +++ b/src/emucore/FrameBuffer.hxx @@ -25,6 +25,7 @@ class Console; class Settings; class FBSurface; class TIASurface; +class Bezel; #ifdef GUI_SUPPORT #include "Font.hxx" @@ -53,7 +54,7 @@ class FrameBuffer { public: // Zoom level step interval - static constexpr float ZOOM_STEPS = 0.25; + static constexpr double ZOOM_STEPS = 0.25; enum UpdateMode { NONE = 0, @@ -219,8 +220,8 @@ class FrameBuffer Get the minimum/maximum supported TIA zoom level (windowed mode) for the framebuffer. */ - float supportedTIAMinZoom() const { return myTIAMinZoom * hidpiScaleFactor(); } - float supportedTIAMaxZoom() const { return maxWindowZoom(); } + double supportedTIAMinZoom() const { return myTIAMinZoom * hidpiScaleFactor(); } + double supportedTIAMaxZoom() const { return maxWindowZoom(); } /** Get the TIA surface associated with the framebuffer. @@ -349,6 +350,19 @@ class FrameBuffer myBackend->getRGB(pixel, r, g, b); } + /** + This method is called to retrieve the R/G/B/A data from the given pixel. + + @param pixel The pixel containing R/G/B data + @param r The red component of the color + @param g The green component of the color + @param b The blue component of the color + @param a The alpha component of the color. + */ + void getRGBA(uInt32 pixel, uInt8* r, uInt8* g, uInt8* b, uInt8* a) const { + myBackend->getRGBA(pixel, r, g, b, a); + } + /** This method is called to map a given R/G/B triple to the screen palette. @@ -361,11 +375,12 @@ class FrameBuffer } /** - This method is called to map a given R/G/B triple to the screen palette. + This method is called to map a given R/G/B/A triple to the screen palette. @param r The red component of the color. @param g The green component of the color. @param b The blue component of the color. + @param a The alpha component of the color. */ uInt32 mapRGBA(uInt8 r, uInt8 g, uInt8 b, uInt8 a) const { return myBackend->mapRGBA(r, g, b, a); @@ -462,37 +477,11 @@ class FrameBuffer */ FBInitStatus applyVideoMode(); - #ifdef IMAGE_SUPPORT - /** - Return bezel names, which are either read from the properties - or generated from the cart name. - - @param index The index of the returned bezel name - - @return The bezel name for the given index - */ - const string getBezelName(int& index) const; - - /** - Check if a bezel for the current ROM name exists. - - @return Whether the bezel was found or not - */ - bool checkBezel(); - - /** - Load the bezel for the current ROM. - - @return Whether the bezel was loaded or not - */ - bool loadBezel(); - #endif - /** Calculate the maximum level by which the base window can be zoomed and still fit in the desktop screen. */ - float maxWindowZoom() const; + double maxWindowZoom() const; /** Enables/disables fullscreen mode. @@ -567,7 +556,7 @@ class FrameBuffer shared_ptr myTIASurface; // The BezelSurface which blends over the TIA surface - shared_ptr myBezelSurface; + unique_ptr myBezel; // Used for onscreen messages and frame statistics // (scanline count and framerate) @@ -594,7 +583,7 @@ class FrameBuffer vector myHiDPIEnabled; // Minimum TIA zoom level that can be used for this framebuffer - float myTIAMinZoom{2.F}; + double myTIAMinZoom{2.F}; // Holds a reference to all the surfaces that have been created std::list> mySurfaceList; diff --git a/src/emucore/Settings.cxx b/src/emucore/Settings.cxx index 9382f8ba6..ad951ccb3 100644 --- a/src/emucore/Settings.cxx +++ b/src/emucore/Settings.cxx @@ -64,7 +64,11 @@ Settings::Settings() setPermanent("pausedim", "true"); setPermanent("bezel.show", "true"); setPermanent("bezel.windowed", "false"); - setPermanent("bezel.border", "30"); + setPermanent("bezel.autoborders", "true"); + setPermanent("bezel.leftborder", "0"); + setPermanent("bezel.rightborder", "0"); + setPermanent("bezel.topborder", "0"); + setPermanent("bezel.bottomborder", "0"); // TIA specific options setPermanent("tia.inter", "false"); setPermanent("tia.zoom", "3"); @@ -542,8 +546,13 @@ void Settings::usage() << " -turbo <1|0> Enable 'Turbo' mode for maximum emulation speed\n" << " -uimessages <1|0> Show onscreen UI messages for different events\n" << " -pausedim <1|0> Enable emulation dimming in pause mode\n" - << " -bezel.show <1|0> Show bezel left and right of emulation\n" - << " -bezel.windowed <1|0> Show bezel in windowed modes\n" + << " -bezel.show <1|0> Show bezel left and right of emulation\n" + << " -bezel.windowed <1|0> Show bezel in windowed modes\n" + << " -bezel.autoborders <1|0> Automatically set bezel window borders\n" + << " -bezel.leftborder Set left bezel window border\n" + << " -bezel.rightborder Set right bezel window border\n" + << " -bezel.topborder Set top bezel window border\n" + << " -bezel.bottomborder Set bottom bezel window border\n" << endl #ifdef SOUND_SUPPORT << " -audio.enabled <1|0> Enable audio\n" diff --git a/src/emucore/module.mk b/src/emucore/module.mk index 38e53273d..f9e791af4 100644 --- a/src/emucore/module.mk +++ b/src/emucore/module.mk @@ -2,6 +2,7 @@ MODULE := src/emucore MODULE_OBJS := \ src/emucore/AtariVox.o \ + src/emucore/Bezel.o src/emucore/Bankswitch.o \ src/emucore/Booster.o \ src/emucore/Cart.o \ diff --git a/src/gui/DeveloperDialog.cxx b/src/gui/DeveloperDialog.cxx index 368ab8ea9..f162447cc 100644 --- a/src/gui/DeveloperDialog.cxx +++ b/src/gui/DeveloperDialog.cxx @@ -1410,7 +1410,7 @@ void DeveloperDialog::handleDebugColours(int idx, int color) // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - void DeveloperDialog::handleDebugColours(string_view colors) { - for(int i = 0; i < DEBUG_COLORS; ++i) + for(int i = 0; i < DEBUG_COLORS && i < colors.length(); ++i) { switch(colors[i]) { diff --git a/src/gui/VideoAudioDialog.cxx b/src/gui/VideoAudioDialog.cxx index f0a40c8cf..714570df6 100644 --- a/src/gui/VideoAudioDialog.cxx +++ b/src/gui/VideoAudioDialog.cxx @@ -478,21 +478,47 @@ void VideoAudioDialog::addBezelTab() ypos += lineHeight + VGAP * 3; myBezelShowWindowed = new CheckboxWidget(myTab, _font, xpos, ypos, "Show in windowed modes"); - //myBezelEnableCheckbox->setToolTip(Event::BezelToggle); + //myBezelShowWindowed->setToolTip(Event::BezelToggle); wid.push_back(myBezelShowWindowed); + // Disable auto borders ypos += lineHeight + VGAP * 1; - myTopBorderSlider = new SliderWidget(myTab, _font, xpos, ypos, - "Top border ", 0, 0, 6 * fontWidth, "px"); - myTopBorderSlider->setMinValue(0); myTopBorderSlider->setMaxValue(50); + myManualBorders = new CheckboxWidget(myTab, _font, xpos, ypos, + "Manual borders", kAutoBordersChanged); + myManualBorders->setToolTip("Enable if automatic border detection fails."); + wid.push_back(myManualBorders); + xpos += INDENT; + + const int lWidth = _font.getStringWidth("Bottom "); + const int sWidth = myBezelPath->getRight() - xpos - lWidth - 5.5 * fontWidth; // _w - HBORDER - xpos - lwidth; + ypos += lineHeight + VGAP * 1; + myLeftBorderSlider = new SliderWidget(myTab, _font, xpos, ypos, sWidth, lineHeight, + "Left ", 0, 0, 5 * fontWidth, "px"); + myLeftBorderSlider->setMinValue(0); myLeftBorderSlider->setMaxValue(500); + myLeftBorderSlider->setTickmarkIntervals(10); + //myLeftBorderSlider->setToolTip(Event::VolumeDecrease, Event::VolumeIncrease); + wid.push_back(myLeftBorderSlider); + + ypos += lineHeight + VGAP * 1; + myRightBorderSlider = new SliderWidget(myTab, _font, xpos, ypos, sWidth, lineHeight, + "Right ", 0, 0, 5 * fontWidth, "px"); + myRightBorderSlider->setMinValue(0); myRightBorderSlider->setMaxValue(500); + myRightBorderSlider->setTickmarkIntervals(10); + //myRightBorderSlider->setToolTip(Event::VolumeDecrease, Event::VolumeIncrease); + wid.push_back(myRightBorderSlider); + + ypos += lineHeight + VGAP * 1; + myTopBorderSlider = new SliderWidget(myTab, _font, xpos, ypos, sWidth, lineHeight, + "Top ", 0, 0, 5 * fontWidth, "px"); + myTopBorderSlider->setMinValue(0); myTopBorderSlider->setMaxValue(250); myTopBorderSlider->setTickmarkIntervals(5); //myTopBorderSlider->setToolTip(Event::VolumeDecrease, Event::VolumeIncrease); wid.push_back(myTopBorderSlider); ypos += lineHeight + VGAP; - myBtmBorderSlider = new SliderWidget(myTab, _font, xpos, ypos, - "Bottom border ", 0, 0, 6 * fontWidth, "px"); - myBtmBorderSlider->setMinValue(0); myBtmBorderSlider->setMaxValue(50); + myBtmBorderSlider = new SliderWidget(myTab, _font, xpos, ypos, sWidth, lineHeight, + "Bottom ", 0, 0, 5 * fontWidth, "px"); + myBtmBorderSlider->setMinValue(0); myBtmBorderSlider->setMaxValue(250); myBtmBorderSlider->setTickmarkIntervals(5); //myBtmBorderSlider->setToolTip(Event::VolumeDecrease, Event::VolumeIncrease); wid.push_back(myBtmBorderSlider); @@ -748,6 +774,9 @@ void VideoAudioDialog::loadConfig() myBezelEnableCheckbox->setState(settings.getBool("bezel.show")); myBezelPath->setText(settings.getString("bezel.dir")); myBezelShowWindowed->setState(settings.getBool("bezel.windowed")); + myManualBorders->setState(!settings.getBool("bezel.autoborders")); + myLeftBorderSlider->setValue(settings.getInt("bezel.leftborder")); + myRightBorderSlider->setValue(settings.getInt("bezel.rightborder")); myTopBorderSlider->setValue(settings.getInt("bezel.topborder")); myBtmBorderSlider->setValue(settings.getInt("bezel.bottomborder")); handleBezelChange(); @@ -880,9 +909,11 @@ void VideoAudioDialog::saveConfig() settings.setValue("bezel.show", myBezelEnableCheckbox->getState()); settings.setValue("bezel.dir", myBezelPath->getText()); settings.setValue("bezel.windowed", myBezelShowWindowed->getState()); + settings.setValue("bezel.autoborders", !myManualBorders->getState()); + settings.setValue("bezel.leftborder", myLeftBorderSlider->getValueLabel()); + settings.setValue("bezel.rightborder", myRightBorderSlider->getValueLabel()); settings.setValue("bezel.topborder", myTopBorderSlider->getValueLabel()); settings.setValue("bezel.bottomborder", myBtmBorderSlider->getValueLabel()); - cerr << myTopBorderSlider << endl; // Note: The following has to happen after all video related setting have been saved if(instance().hasConsole()) @@ -1022,8 +1053,7 @@ void VideoAudioDialog::setDefaults() myBezelEnableCheckbox->setState(true); myBezelPath->setText(instance().userDir().getShortPath()); myBezelShowWindowed->setState(false); - myTopBorderSlider->setValue(0); - myBtmBorderSlider->setValue(0); + myManualBorders->setState(false); handleBezelChange(); break; @@ -1193,12 +1223,15 @@ void VideoAudioDialog::handlePhosphorChange() void VideoAudioDialog::handleBezelChange() { const bool enable = myBezelEnableCheckbox->getState(); + const bool nonAuto = myManualBorders->getState(); myOpenBrowserButton->setEnabled(enable); myBezelPath->setEnabled(enable); myBezelShowWindowed->setEnabled(enable); - myTopBorderSlider->setEnabled(enable); - myBtmBorderSlider->setEnabled(enable); + myLeftBorderSlider->setEnabled(enable && nonAuto); + myRightBorderSlider->setEnabled(enable && nonAuto); + myTopBorderSlider->setEnabled(enable && nonAuto); + myBtmBorderSlider->setEnabled(enable && nonAuto); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -1317,6 +1350,7 @@ void VideoAudioDialog::handleCommand(CommandSender* sender, int cmd, break; case kBezelEnableChanged: + case kAutoBordersChanged: handleBezelChange(); break; diff --git a/src/gui/VideoAudioDialog.hxx b/src/gui/VideoAudioDialog.hxx index af7053483..4b3be0d4c 100644 --- a/src/gui/VideoAudioDialog.hxx +++ b/src/gui/VideoAudioDialog.hxx @@ -135,6 +135,9 @@ class VideoAudioDialog : public Dialog ButtonWidget* myOpenBrowserButton{nullptr}; EditTextWidget* myBezelPath{nullptr}; CheckboxWidget* myBezelShowWindowed{nullptr}; + CheckboxWidget* myManualBorders{nullptr}; + SliderWidget* myLeftBorderSlider{nullptr}; + SliderWidget* myRightBorderSlider{nullptr}; SliderWidget* myTopBorderSlider{nullptr}; SliderWidget* myBtmBorderSlider{nullptr}; @@ -180,6 +183,7 @@ class VideoAudioDialog : public Dialog kBezelEnableChanged = 'BZen', kChooseBezelDirCmd = 'BZsl', + kAutoBordersChanged = 'BZab', kSoundEnableChanged = 'ADse', kDeviceChanged = 'ADdc', diff --git a/src/os/windows/Stella.vcxproj b/src/os/windows/Stella.vcxproj index 26ce3db5e..39524a5c3 100755 --- a/src/os/windows/Stella.vcxproj +++ b/src/os/windows/Stella.vcxproj @@ -993,6 +993,7 @@ true + @@ -2320,6 +2321,7 @@ + diff --git a/src/os/windows/Stella.vcxproj.filters b/src/os/windows/Stella.vcxproj.filters index d75b91f01..3874d2089 100644 --- a/src/os/windows/Stella.vcxproj.filters +++ b/src/os/windows/Stella.vcxproj.filters @@ -1209,6 +1209,9 @@ Source Files\debugger\gui + + Source Files\emucore + @@ -2465,6 +2468,9 @@ Header Files\debugger\gui + + Header Files\emucore + diff --git a/test/bezels/default.png b/test/bezels/default.png index e9b52df45..07ac6a757 100644 Binary files a/test/bezels/default.png and b/test/bezels/default.png differ