diff --git a/CMakeLists.txt b/CMakeLists.txt index bba242e4..e50af1b8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -328,7 +328,7 @@ set(SOURCE_FILES src/emulator.cpp src/io_file.cpp src/config.cpp src/core/memory.cpp src/renderer.cpp src/core/renderer_null/renderer_null.cpp src/http_server.cpp src/stb_image_write.c src/core/cheats.cpp src/core/action_replay.cpp src/discord_rpc.cpp src/lua.cpp src/memory_mapped_file.cpp src/renderdoc.cpp - src/frontend_settings.cpp src/miniaudio/miniaudio.cpp + src/frontend_settings.cpp src/miniaudio/miniaudio.cpp src/core/screen_layout.cpp ) set(CRYPTO_SOURCE_FILES src/core/crypto/aes_engine.cpp) set(KERNEL_SOURCE_FILES src/core/kernel/kernel.cpp src/core/kernel/resource_limits.cpp @@ -416,7 +416,7 @@ set(HEADER_FILES include/emulator.hpp include/helpers.hpp include/termcolor.hpp include/fs/archive_twl_sound.hpp include/fs/archive_card_spi.hpp include/services/ns.hpp include/audio/audio_device.hpp include/audio/audio_device_interface.hpp include/audio/libretro_audio_device.hpp include/services/ir/ir_types.hpp include/services/ir/ir_device.hpp include/services/ir/circlepad_pro.hpp include/services/service_intercept.hpp - include/PICA/screen_layout.hpp + include/screen_layout.hpp ) if(IOS) diff --git a/include/PICA/gpu.hpp b/include/PICA/gpu.hpp index c168a9bf..77bfb8ed 100644 --- a/include/PICA/gpu.hpp +++ b/include/PICA/gpu.hpp @@ -89,6 +89,7 @@ class GPU { PICA::Vertex getImmediateModeVertex(); void getAcceleratedDrawInfo(PICA::DrawAcceleration& accel, bool indexed); + public: // 256 entries per LUT with each LUT as its own row forming a 2D image 256 * LUT_COUNT // Encoded in PICA native format @@ -134,6 +135,8 @@ class GPU { // Used for setting the size of the window we'll be outputting graphics to void setOutputSize(u32 width, u32 height) { renderer->setOutputSize(width, height); } + // Used for notifying the renderer the screen layout has changed + void reloadScreenLayout() { renderer->reloadScreenLayout(); } // TODO: Emulate the transfer engine & its registers // Then this can be emulated by just writing the appropriate values there @@ -181,6 +184,7 @@ class GPU { } Renderer* getRenderer() { return renderer.get(); } + private: // GPU external registers // We have them in the end of the struct for cache locality reasons. Tl;dr we want the more commonly used things to be packed in the start @@ -189,8 +193,8 @@ class GPU { ALWAYS_INLINE void setVsOutputMask(u32 val) { val &= 0xffff; - - // Avoid recomputing this if not necessary + + // Avoid recomputing this if not necessary if (oldVsOutputMask != val) [[unlikely]] { oldVsOutputMask = val; diff --git a/include/PICA/screen_layout.hpp b/include/PICA/screen_layout.hpp deleted file mode 100644 index 8a42aba9..00000000 --- a/include/PICA/screen_layout.hpp +++ /dev/null @@ -1,161 +0,0 @@ -#pragma once -#include - -#include "helpers.hpp" - -namespace ScreenLayout { - static constexpr u32 TOP_SCREEN_WIDTH = 400; - static constexpr u32 BOTTOM_SCREEN_WIDTH = 320; - static constexpr u32 TOP_SCREEN_HEIGHT = 240; - static constexpr u32 BOTTOM_SCREEN_HEIGHT = 240; - - // The bottom screen is less wide by 80 pixels, so we center it by offsetting it 40 pixels right - static constexpr u32 BOTTOM_SCREEN_X_OFFSET = 40; - static constexpr u32 CONSOLE_HEIGHT = TOP_SCREEN_HEIGHT + BOTTOM_SCREEN_HEIGHT; - - enum class Layout { - Default, // Top screen up, bottom screen down - DefaultFlipped, // Top screen down, bottom screen up - SideBySide, // Top screen left, bottom screen right, - SideBySideFlipped, // Top screen right, bottom screen left, - }; - - // For properly handling touchscreen, we have to remember what window coordinates our screens map to - // We also remember some more information that is useful to our renderers, particularly for the final screen blit. - struct WindowCoordinates { - u32 topScreenX = 0; - u32 topScreenY = 0; - u32 topScreenWidth = TOP_SCREEN_WIDTH; - u32 topScreenHeight = TOP_SCREEN_HEIGHT; - - u32 bottomScreenX = BOTTOM_SCREEN_X_OFFSET; - u32 bottomScreenY = TOP_SCREEN_HEIGHT; - u32 bottomScreenWidth = BOTTOM_SCREEN_WIDTH; - u32 bottomScreenHeight = BOTTOM_SCREEN_HEIGHT; - - u32 windowWidth = topScreenWidth; - u32 windowHeight = topScreenHeight + bottomScreenHeight; - - // Information used when optimizing the final screen blit into a single blit - struct { - int destX = 0, destY = 0; - int destWidth = TOP_SCREEN_WIDTH; - int destHeight = CONSOLE_HEIGHT; - } singleBlitInfo; - }; - - // Calculate screen coordinates on the screen for a given layout & a given size for the output window - // Used in both the renderers and in the frontends (To eg calculate touch screen boundaries) - static void calculateCoordinates(WindowCoordinates& coordinates, u32 outputWindowWidth, u32 outputWindowHeight, Layout layout) { - layout = Layout::SideBySideFlipped; - const float topScreenPercentage = 0.5f; - - const float destAspect = float(outputWindowWidth) / float(outputWindowHeight); - - if (layout == Layout::Default || layout == Layout::DefaultFlipped) { - // Calculate available height for each screen based on split - int availableTopHeight = int(outputWindowHeight * topScreenPercentage + 0.5f); - int availableBottomHeight = outputWindowHeight - availableTopHeight; - - // Calculate scales for top and bottom screens, and then the actual sizes - float scaleTopX = float(outputWindowWidth) / float(TOP_SCREEN_WIDTH); - float scaleTopY = float(availableTopHeight) / float(TOP_SCREEN_HEIGHT); - float scaleTop = std::min(scaleTopX, scaleTopY); - - float scaleBottomX = float(outputWindowWidth) / float(BOTTOM_SCREEN_WIDTH); - float scaleBottomY = float(availableBottomHeight) / float(BOTTOM_SCREEN_HEIGHT); - float scaleBottom = std::min(scaleBottomX, scaleBottomY); - - int topScreenWidth = int(TOP_SCREEN_WIDTH * scaleTop + 0.5f); - int topScreenHeight = int(TOP_SCREEN_HEIGHT * scaleTop + 0.5f); - - int bottomScreenWidth = int(BOTTOM_SCREEN_WIDTH * scaleBottom + 0.5f); - int bottomScreenHeight = int(BOTTOM_SCREEN_HEIGHT * scaleBottom + 0.5f); - - // Center screens horizontally - int topScreenX = (outputWindowWidth - topScreenWidth) / 2; - int bottomScreenX = (outputWindowWidth - bottomScreenWidth) / 2; - - coordinates.topScreenWidth = topScreenWidth; - coordinates.topScreenHeight = topScreenHeight; - coordinates.bottomScreenWidth = bottomScreenWidth; - coordinates.bottomScreenHeight = bottomScreenHeight; - - coordinates.topScreenX = topScreenX; - coordinates.bottomScreenX = bottomScreenX; - - if (layout == Layout::Default) { - coordinates.topScreenY = 0; - coordinates.bottomScreenY = topScreenHeight; - } else { - coordinates.bottomScreenY = 0; - coordinates.topScreenY = bottomScreenHeight; - } - - coordinates.windowWidth = outputWindowWidth; - coordinates.windowHeight = outputWindowHeight; - - // Default layout can be rendered using a single blit, flipped layout can't - if (layout == Layout::Default) { - coordinates.singleBlitInfo.destX = coordinates.topScreenX; - coordinates.singleBlitInfo.destY = coordinates.topScreenY; - coordinates.singleBlitInfo.destWidth = coordinates.topScreenWidth; - coordinates.singleBlitInfo.destHeight = coordinates.topScreenHeight + coordinates.bottomScreenHeight; - } else { - // Dummy data for the single blit info, as we can't render the screen in 1 blit when the screens are side-by-sid - coordinates.singleBlitInfo.destX = 0; - coordinates.singleBlitInfo.destY = 0; - coordinates.singleBlitInfo.destWidth = 0; - coordinates.singleBlitInfo.destHeight = 0; - } - } else if (layout == Layout::SideBySide || layout == Layout::SideBySideFlipped) { - // Calculate available width for each screen based on split - int availableTopWidth = int(outputWindowWidth * topScreenPercentage + 0.5f); - int availableBottomWidth = outputWindowWidth - availableTopWidth; - - // Calculate scales for top and bottom screens, and then the actual sizes - float scaleTop = std::min(float(availableTopWidth) / float(TOP_SCREEN_WIDTH), float(outputWindowHeight) / float(TOP_SCREEN_HEIGHT)); - float scaleBottom = - std::min(float(availableBottomWidth) / float(BOTTOM_SCREEN_WIDTH), float(outputWindowHeight) / float(BOTTOM_SCREEN_HEIGHT)); - - int topScreenWidth = int(TOP_SCREEN_WIDTH * scaleTop + 0.5f); - int topScreenHeight = int(TOP_SCREEN_HEIGHT * scaleTop + 0.5f); - int bottomScreenWidth = int(BOTTOM_SCREEN_WIDTH * scaleBottom + 0.5f); - int bottomScreenHeight = int(BOTTOM_SCREEN_HEIGHT * scaleBottom + 0.5f); - - // Vertically center the tallest screen - int maxHeight = std::max(topScreenHeight, bottomScreenHeight); - int baseY = (outputWindowHeight - maxHeight) / 2; - - int topScreenY = baseY + (maxHeight - topScreenHeight) / 2; - int bottomScreenY = baseY + (maxHeight - bottomScreenHeight) / 2; - - if (layout == Layout::SideBySide) { - coordinates.topScreenX = (outputWindowWidth - (topScreenWidth + bottomScreenWidth)) / 2; - coordinates.bottomScreenX = coordinates.topScreenX + topScreenWidth; - } else { - coordinates.bottomScreenX = (outputWindowWidth - (topScreenWidth + bottomScreenWidth)) / 2; - coordinates.topScreenX = coordinates.bottomScreenX + bottomScreenWidth; - } - - coordinates.topScreenY = topScreenY; - coordinates.topScreenWidth = topScreenWidth; - coordinates.topScreenHeight = topScreenHeight; - - coordinates.bottomScreenY = bottomScreenY; - coordinates.bottomScreenWidth = bottomScreenWidth; - coordinates.bottomScreenHeight = bottomScreenHeight; - - coordinates.windowWidth = outputWindowWidth; - coordinates.windowHeight = outputWindowHeight; - - // Dummy data for the single blit info, as we can't render the screen in 1 blit when the screens are side-by-side - coordinates.singleBlitInfo.destX = 0; - coordinates.singleBlitInfo.destY = 0; - coordinates.singleBlitInfo.destWidth = 0; - coordinates.singleBlitInfo.destHeight = 0; - } else { - Helpers::panic("Unimplemented screen layout"); - } - } -} // namespace ScreenLayout diff --git a/include/config.hpp b/include/config.hpp index bc18f287..b890e251 100644 --- a/include/config.hpp +++ b/include/config.hpp @@ -2,6 +2,7 @@ #include #include +#include "screen_layout.hpp" #include "audio/dsp_core.hpp" #include "frontend_settings.hpp" #include "renderer.hpp" @@ -69,6 +70,9 @@ struct EmulatorConfig { bool accelerateShaders = accelerateShadersDefault; bool hashTextures = hashTexturesDefault; + ScreenLayout::Layout screenLayout = ScreenLayout::Layout::Default; + float topScreenSize = 0.5; + bool accurateShaderMul = false; bool discordRpcEnabled = false; diff --git a/include/emulator.hpp b/include/emulator.hpp index 6b7f97fc..6b63a211 100644 --- a/include/emulator.hpp +++ b/include/emulator.hpp @@ -115,6 +115,8 @@ class Emulator { RomFS::DumpingResult dumpRomFS(const std::filesystem::path& path); void setOutputSize(u32 width, u32 height) { gpu.setOutputSize(width, height); } + void reloadScreenLayout() { gpu.reloadScreenLayout(); } + void deinitGraphicsContext() { gpu.deinitGraphicsContext(); } // Reloads some settings that require special handling, such as audio enable diff --git a/include/panda_qt/screen.hpp b/include/panda_qt/screen.hpp index f785ee57..4c1fc1a5 100644 --- a/include/panda_qt/screen.hpp +++ b/include/panda_qt/screen.hpp @@ -3,7 +3,7 @@ #include #include -#include "PICA/screen_layout.hpp" +#include "screen_layout.hpp" #include "gl/context.h" #include "window_info.h" @@ -33,6 +33,11 @@ class ScreenWidget : public QWidget { // Coordinates (x/y/width/height) for the two screens in window space, used for properly handling touchscreen regardless // of layout or resizing ScreenLayout::WindowCoordinates screenCoordinates; + // Screen layouts and sizes + ScreenLayout::Layout screenLayout = ScreenLayout::Layout::Default; + float topScreenSize = 0.5f; + + void reloadScreenLayout(ScreenLayout::Layout newLayout, float newTopScreenSize); private: std::unique_ptr glContext = nullptr; @@ -44,4 +49,6 @@ class ScreenWidget : public QWidget { int scaledWindowWidth() const; int scaledWindowHeight() const; std::optional getWindowInfo(); + + void reloadScreenCoordinates(); }; diff --git a/include/panda_sdl/frontend_sdl.hpp b/include/panda_sdl/frontend_sdl.hpp index af5b88ab..7bd9892f 100644 --- a/include/panda_sdl/frontend_sdl.hpp +++ b/include/panda_sdl/frontend_sdl.hpp @@ -4,7 +4,7 @@ #include -#include "PICA/screen_layout.hpp" +#include "screen_layout.hpp" #include "emulator.hpp" #include "input_mappings.hpp" diff --git a/include/renderer.hpp b/include/renderer.hpp index 753bc319..40f244db 100644 --- a/include/renderer.hpp +++ b/include/renderer.hpp @@ -130,4 +130,5 @@ class Renderer { void setConfig(EmulatorConfig* config) { emulatorConfig = config; } void setHashTextures(bool setting) { hashTextures = setting; } + void reloadScreenLayout() { outputSizeChanged = true; } }; diff --git a/include/screen_layout.hpp b/include/screen_layout.hpp new file mode 100644 index 00000000..fc3f1c40 --- /dev/null +++ b/include/screen_layout.hpp @@ -0,0 +1,58 @@ +#pragma once +#include + +#include "helpers.hpp" + +namespace ScreenLayout { + static constexpr u32 TOP_SCREEN_WIDTH = 400; + static constexpr u32 BOTTOM_SCREEN_WIDTH = 320; + static constexpr u32 TOP_SCREEN_HEIGHT = 240; + static constexpr u32 BOTTOM_SCREEN_HEIGHT = 240; + + // The bottom screen is less wide by 80 pixels, so we center it by offsetting it 40 pixels right + static constexpr u32 BOTTOM_SCREEN_X_OFFSET = 40; + static constexpr u32 CONSOLE_HEIGHT = TOP_SCREEN_HEIGHT + BOTTOM_SCREEN_HEIGHT; + + enum class Layout { + Default = 0, // Top screen up, bottom screen down + DefaultFlipped, // Top screen down, bottom screen up + SideBySide, // Top screen left, bottom screen right, + SideBySideFlipped, // Top screen right, bottom screen left, + }; + + // For properly handling touchscreen, we have to remember what window coordinates our screens map to + // We also remember some more information that is useful to our renderers, particularly for the final screen blit. + struct WindowCoordinates { + u32 topScreenX = 0; + u32 topScreenY = 0; + u32 topScreenWidth = TOP_SCREEN_WIDTH; + u32 topScreenHeight = TOP_SCREEN_HEIGHT; + + u32 bottomScreenX = BOTTOM_SCREEN_X_OFFSET; + u32 bottomScreenY = TOP_SCREEN_HEIGHT; + u32 bottomScreenWidth = BOTTOM_SCREEN_WIDTH; + u32 bottomScreenHeight = BOTTOM_SCREEN_HEIGHT; + + u32 windowWidth = topScreenWidth; + u32 windowHeight = topScreenHeight + bottomScreenHeight; + + // Information used when optimizing the final screen blit into a single blit + struct { + // Can we actually render both of the screens in a single blit? + bool canDoSingleBlit = false; + // Blit information used if we can + int destX = 0, destY = 0; + int destWidth = TOP_SCREEN_WIDTH; + int destHeight = CONSOLE_HEIGHT; + } singleBlitInfo; + }; + + // Calculate screen coordinates on the screen for a given layout & a given size for the output window + // Used in both the renderers and in the frontends (To eg calculate touch screen boundaries) + void calculateCoordinates( + WindowCoordinates& coordinates, u32 outputWindowWidth, u32 outputWindowHeight, float topScreenPercentage, Layout layout + ); + + Layout layoutFromString(std::string inString); + const char* layoutToString(Layout layout); +} // namespace ScreenLayout diff --git a/src/config.cpp b/src/config.cpp index 28fd7df0..f2cddaba 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -93,6 +93,10 @@ void EmulatorConfig::load() { lightShadergenThreshold = toml::find_or(gpu, "ShadergenLightThreshold", 1); hashTextures = toml::find_or(gpu, "HashTextures", hashTexturesDefault); enableRenderdoc = toml::find_or(gpu, "EnableRenderdoc", false); + + auto screenLayoutName = toml::find_or(gpu, "ScreenLayout", "Default"); + screenLayout = ScreenLayout::layoutFromString(screenLayoutName); + topScreenSize = float(std::clamp(toml::find_or(gpu, "TopScreenSize", 0.5), 0.0, 1.0)); } } @@ -194,6 +198,8 @@ void EmulatorConfig::save() { data["GPU"]["AccelerateShaders"] = accelerateShaders; data["GPU"]["EnableRenderdoc"] = enableRenderdoc; data["GPU"]["HashTextures"] = hashTextures; + data["GPU"]["ScreenLayout"] = std::string(ScreenLayout::layoutToString(screenLayout)); + data["GPU"]["TopScreenSize"] = double(topScreenSize); data["Audio"]["DSPEmulation"] = std::string(Audio::DSPCore::typeToString(dspType)); data["Audio"]["EnableAudio"] = audioEnabled; diff --git a/src/core/renderer_gl/renderer_gl.cpp b/src/core/renderer_gl/renderer_gl.cpp index 8a189aed..79417976 100644 --- a/src/core/renderer_gl/renderer_gl.cpp +++ b/src/core/renderer_gl/renderer_gl.cpp @@ -11,7 +11,7 @@ #include "PICA/pica_hash.hpp" #include "PICA/pica_simd.hpp" #include "PICA/regs.hpp" -#include "PICA/screen_layout.hpp" +#include "screen_layout.hpp" #include "PICA/shader_decompiler.hpp" #include "config.hpp" #include "math_util.hpp" @@ -570,15 +570,18 @@ void RendererGL::display() { if constexpr (!Helpers::isHydraCore()) { glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0); + OpenGL::clearColor(); + screenFramebuffer.bind(OpenGL::ReadFramebuffer); - constexpr auto layout = ScreenLayout::Layout::Default; if (outputSizeChanged) { outputSizeChanged = false; + auto layout = emulatorConfig->screenLayout; + // Get information about our new screen layout to use for blitting the output ScreenLayout::WindowCoordinates windowCoords; - ScreenLayout::calculateCoordinates(windowCoords, outputWindowWidth, outputWindowHeight, layout); + ScreenLayout::calculateCoordinates(windowCoords, outputWindowWidth, outputWindowHeight, emulatorConfig->topScreenSize, layout); blitInfo.topScreenX = windowCoords.topScreenX; blitInfo.topScreenY = windowCoords.topScreenY; @@ -590,20 +593,16 @@ void RendererGL::display() { blitInfo.bottomScreenWidth = windowCoords.bottomScreenWidth; blitInfo.bottomScreenHeight = windowCoords.bottomScreenHeight; - // Flip topScreenY and bottomScreenY because glBlitFramebuffer uses bottom-left origin + // Flip topScreenY and bottomScreenY because glBlitFramebuffer uses bottom-left origin blitInfo.topScreenY = outputWindowHeight - (blitInfo.topScreenY + blitInfo.topScreenHeight); blitInfo.bottomScreenY = outputWindowHeight - (blitInfo.bottomScreenY + blitInfo.bottomScreenHeight); // Used for optimizing the screen blit into a single blit + blitInfo.canDoSingleBlit = windowCoords.singleBlitInfo.canDoSingleBlit; blitInfo.destX = windowCoords.singleBlitInfo.destX; blitInfo.destY = windowCoords.singleBlitInfo.destY; blitInfo.destWidth = windowCoords.singleBlitInfo.destWidth; blitInfo.destHeight = windowCoords.singleBlitInfo.destHeight; - - // Check if we can blit the screens in 1 blit. If not, we'll break it into two. - // TODO: Maybe add some size-related checks too. - blitInfo.canDoSingleBlit = - windowCoords.topScreenY + windowCoords.topScreenHeight == windowCoords.bottomScreenY && layout == ScreenLayout::Layout::Default; } if (blitInfo.canDoSingleBlit) { diff --git a/src/core/renderer_mtl/renderer_mtl.cpp b/src/core/renderer_mtl/renderer_mtl.cpp index 7014b943..71cdd616 100644 --- a/src/core/renderer_mtl/renderer_mtl.cpp +++ b/src/core/renderer_mtl/renderer_mtl.cpp @@ -10,7 +10,7 @@ #include "PICA/gpu.hpp" #include "PICA/pica_hash.hpp" -#include "PICA/screen_layout.hpp" +#include "screen_layout.hpp" #include "SDL_metal.h" using namespace PICA; @@ -107,7 +107,9 @@ void RendererMTL::display() { if (outputSizeChanged) { outputSizeChanged = false; ScreenLayout::WindowCoordinates windowCoords; - ScreenLayout::calculateCoordinates(windowCoords, outputWindowWidth, outputWindowHeight, ScreenLayout::Layout::Default); + ScreenLayout::calculateCoordinates( + windowCoords, outputWindowWidth, outputWindowHeight, emulatorConfig->topScreenSize, emulatorConfig->screenLayout + ); blitInfo.topScreenX = float(windowCoords.topScreenX); blitInfo.topScreenY = float(windowCoords.topScreenY); diff --git a/src/core/screen_layout.cpp b/src/core/screen_layout.cpp new file mode 100644 index 00000000..ba1a5318 --- /dev/null +++ b/src/core/screen_layout.cpp @@ -0,0 +1,155 @@ +#include "screen_layout.hpp" + +#include +#include +#include + +using namespace ScreenLayout; + +// Calculate screen coordinates on the screen for a given layout & a given size for the output window +// Used in both the renderers and in the frontends (To eg calculate touch screen boundaries) +void ScreenLayout::calculateCoordinates( + WindowCoordinates& coordinates, u32 outputWindowWidth, u32 outputWindowHeight, float topScreenPercentage, Layout layout +) { + const float destAspect = float(outputWindowWidth) / float(outputWindowHeight); + + if (layout == Layout::Default || layout == Layout::DefaultFlipped) { + // Calculate available height for each screen based on split + int availableTopHeight = int(outputWindowHeight * topScreenPercentage + 0.5f); + int availableBottomHeight = outputWindowHeight - availableTopHeight; + + // Calculate scales for top and bottom screens, and then the actual sizes + float scaleTopX = float(outputWindowWidth) / float(TOP_SCREEN_WIDTH); + float scaleTopY = float(availableTopHeight) / float(TOP_SCREEN_HEIGHT); + float scaleTop = std::min(scaleTopX, scaleTopY); + + float scaleBottomX = float(outputWindowWidth) / float(BOTTOM_SCREEN_WIDTH); + float scaleBottomY = float(availableBottomHeight) / float(BOTTOM_SCREEN_HEIGHT); + float scaleBottom = std::min(scaleBottomX, scaleBottomY); + + int topScreenWidth = int(TOP_SCREEN_WIDTH * scaleTop + 0.5f); + int topScreenHeight = int(TOP_SCREEN_HEIGHT * scaleTop + 0.5f); + int bottomScreenWidth = int(BOTTOM_SCREEN_WIDTH * scaleBottom + 0.5f); + int bottomScreenHeight = int(BOTTOM_SCREEN_HEIGHT * scaleBottom + 0.5f); + + // Center screens horizontally + int topScreenX = (outputWindowWidth - topScreenWidth) / 2; + int bottomScreenX = (outputWindowWidth - bottomScreenWidth) / 2; + + coordinates.topScreenWidth = topScreenWidth; + coordinates.topScreenHeight = topScreenHeight; + coordinates.bottomScreenWidth = bottomScreenWidth; + coordinates.bottomScreenHeight = bottomScreenHeight; + + coordinates.topScreenX = topScreenX; + coordinates.bottomScreenX = bottomScreenX; + + if (layout == Layout::Default) { + coordinates.topScreenY = 0; + coordinates.bottomScreenY = topScreenHeight; + } else { + coordinates.bottomScreenY = 0; + coordinates.topScreenY = bottomScreenHeight; + } + + coordinates.windowWidth = outputWindowWidth; + coordinates.windowHeight = outputWindowHeight; + + // Default layout can be rendered using a single blit, flipped layout can't + if (layout == Layout::Default) { + coordinates.singleBlitInfo.destX = coordinates.topScreenX; + coordinates.singleBlitInfo.destY = coordinates.topScreenY; + coordinates.singleBlitInfo.destWidth = coordinates.topScreenWidth; + coordinates.singleBlitInfo.destHeight = coordinates.topScreenHeight + coordinates.bottomScreenHeight; + } else { + // Dummy data for the single blit info, as we can't render the screen in 1 blit when the screens are side-by-sid + coordinates.singleBlitInfo.destX = 0; + coordinates.singleBlitInfo.destY = 0; + coordinates.singleBlitInfo.destWidth = 0; + coordinates.singleBlitInfo.destHeight = 0; + } + + // Check if we can blit the screens in 1 blit. If not, we'll break it into two. + // TODO: Maybe add some more size-related checks too. + coordinates.singleBlitInfo.canDoSingleBlit = layout == Layout::Default && topScreenPercentage == 0.5 && + coordinates.topScreenY + coordinates.topScreenHeight == coordinates.bottomScreenY; + } else if (layout == Layout::SideBySide || layout == Layout::SideBySideFlipped) { + // Calculate available width for each screen based on split + int availableTopWidth = int(outputWindowWidth * topScreenPercentage + 0.5f); + int availableBottomWidth = outputWindowWidth - availableTopWidth; + + // Calculate scales for top and bottom screens, and then the actual sizes + float scaleTop = std::min(float(availableTopWidth) / float(TOP_SCREEN_WIDTH), float(outputWindowHeight) / float(TOP_SCREEN_HEIGHT)); + float scaleBottom = + std::min(float(availableBottomWidth) / float(BOTTOM_SCREEN_WIDTH), float(outputWindowHeight) / float(BOTTOM_SCREEN_HEIGHT)); + + int topScreenWidth = int(TOP_SCREEN_WIDTH * scaleTop + 0.5f); + int topScreenHeight = int(TOP_SCREEN_HEIGHT * scaleTop + 0.5f); + int bottomScreenWidth = int(BOTTOM_SCREEN_WIDTH * scaleBottom + 0.5f); + int bottomScreenHeight = int(BOTTOM_SCREEN_HEIGHT * scaleBottom + 0.5f); + + // Vertically center the tallest screen + int maxHeight = std::max(topScreenHeight, bottomScreenHeight); + int baseY = (outputWindowHeight - maxHeight) / 2; + + int topScreenY = baseY + (maxHeight - topScreenHeight) / 2; + int bottomScreenY = baseY + (maxHeight - bottomScreenHeight) / 2; + + if (layout == Layout::SideBySide) { + coordinates.topScreenX = (outputWindowWidth - (topScreenWidth + bottomScreenWidth)) / 2; + coordinates.bottomScreenX = coordinates.topScreenX + topScreenWidth; + } else { + coordinates.bottomScreenX = (outputWindowWidth - (topScreenWidth + bottomScreenWidth)) / 2; + coordinates.topScreenX = coordinates.bottomScreenX + bottomScreenWidth; + } + + coordinates.topScreenY = topScreenY; + coordinates.topScreenWidth = topScreenWidth; + coordinates.topScreenHeight = topScreenHeight; + + coordinates.bottomScreenY = bottomScreenY; + coordinates.bottomScreenWidth = bottomScreenWidth; + coordinates.bottomScreenHeight = bottomScreenHeight; + + coordinates.windowWidth = outputWindowWidth; + coordinates.windowHeight = outputWindowHeight; + + // Dummy data for the single blit info, as we can't render the screen in 1 blit when the screens are side-by-side + coordinates.singleBlitInfo.canDoSingleBlit = false; + coordinates.singleBlitInfo.destX = 0; + coordinates.singleBlitInfo.destY = 0; + coordinates.singleBlitInfo.destWidth = 0; + coordinates.singleBlitInfo.destHeight = 0; + } else { + Helpers::panic("Unimplemented screen layout"); + } +} + +Layout ScreenLayout::layoutFromString(std::string inString) { + // Transform to lower-case to make the setting case-insensitive + std::transform(inString.begin(), inString.end(), inString.begin(), [](unsigned char c) { return std::tolower(c); }); + + static const std::unordered_map map = { + {"default", Layout::Default}, + {"defaultflipped", Layout::DefaultFlipped}, + {"sidebyside", Layout::SideBySide}, + {"sidebysideflipped", Layout::SideBySideFlipped}, + }; + + if (auto search = map.find(inString); search != map.end()) { + return search->second; + } + + printf("Invalid screen layout. Defaulting to Default\n"); + return Layout::Default; +} + +const char* ScreenLayout::layoutToString(Layout layout) { + switch (layout) { + case Layout::Default: return "default"; + case Layout::DefaultFlipped: return "defaultFlipped"; + case Layout::SideBySide: return "sideBySide"; + case Layout::SideBySideFlipped: return "sideBySideFlipped"; + default: return "invalid"; + } +} diff --git a/src/panda_qt/config_window.cpp b/src/panda_qt/config_window.cpp index 5360b12c..aecefbc5 100644 --- a/src/panda_qt/config_window.cpp +++ b/src/panda_qt/config_window.cpp @@ -240,6 +240,36 @@ ConfigWindow::ConfigWindow(ConfigCallback configCallback, MainWindowCallback win connectCheckbox(hashTextures, config.hashTextures); gpuLayout->addRow(hashTextures); + QComboBox* screenLayout = new QComboBox(); + screenLayout->addItem(tr("Default")); + screenLayout->addItem(tr("Default (Flipped)")); + screenLayout->addItem(tr("Side-by-Side")); + screenLayout->addItem(tr("Side-by-Side (Flipped)")); + screenLayout->setCurrentIndex(static_cast(config.screenLayout)); + connect(screenLayout, &QComboBox::currentIndexChanged, this, [&](int index) { + config.screenLayout = static_cast(index); + updateConfig(); + }); + gpuLayout->addRow(tr("Screen Layout"), screenLayout); + + // Screen size slider widgets + QLabel* topScreenSizeLabel = new QLabel(QString::number(int(config.topScreenSize * 100))); + QSlider* topScreenSizeSlider = new QSlider(Qt::Horizontal); + + topScreenSizeSlider->setRange(0, 100); + topScreenSizeSlider->setValue(int(config.topScreenSize * 100)); + connect(topScreenSizeSlider, &QSlider::valueChanged, this, [this, topScreenSizeLabel](int value) { + config.topScreenSize = float(value) / 100.0f; + topScreenSizeLabel->setText(QString::number(value)); + updateConfig(); + }); + + QHBoxLayout* screenSizeLayout = new QHBoxLayout(); + screenSizeLayout->setSpacing(4); + screenSizeLayout->addWidget(topScreenSizeSlider); + screenSizeLayout->addWidget(topScreenSizeLabel); + gpuLayout->addRow(tr("Top screen size (%)"), screenSizeLayout); + QCheckBox* forceShadergenForLights = new QCheckBox(tr("Force shadergen when rendering lights")); connectCheckbox(forceShadergenForLights, config.forceShadergenForLights); gpuLayout->addRow(forceShadergenForLights); @@ -302,7 +332,7 @@ ConfigWindow::ConfigWindow(ConfigCallback configCallback, MainWindowCallback win volumeSlider->setRange(0, 200); volumeSlider->setValue(int(config.audioDeviceConfig.volumeRaw * 100)); connect(volumeSlider, &QSlider::valueChanged, this, [this, volumeLabel](int value) { - config.audioDeviceConfig.volumeRaw = static_cast(value) / 100.0f; + config.audioDeviceConfig.volumeRaw = float(value) / 100.0f; volumeLabel->setText(QString::number(value)); updateConfig(); @@ -467,12 +497,14 @@ void ConfigWindow::setTheme(Theme theme) { p.setColor(QPalette::Highlight, QColor(42, 130, 218)); p.setColor(QPalette::HighlightedText, Qt::black); qApp->setPalette(p); - qApp->setStyleSheet("QLineEdit {" + qApp->setStyleSheet( + "QLineEdit {" "background-color: #000000; color: #ffffff; border: 1px solid #a0a0a0; " "border-radius: 4px; padding: 5px; }" "QCheckBox::indicator:unchecked {" - "border: 1px solid #808080; border-radius: 4px; }"); + "border: 1px solid #808080; border-radius: 4px; }" + ); break; } diff --git a/src/panda_qt/main_window.cpp b/src/panda_qt/main_window.cpp index aa162e4f..d29b9875 100644 --- a/src/panda_qt/main_window.cpp +++ b/src/panda_qt/main_window.cpp @@ -129,6 +129,8 @@ MainWindow::MainWindow(QApplication* app, QWidget* parent) : QMainWindow(parent) if (config.printAppVersion) { printf("Welcome to Panda3DS v%s!\n", PANDA3DS_VERSION); } + + screen->reloadScreenLayout(config.screenLayout, config.topScreenSize); } // The emulator graphics context for the thread should be initialized in the emulator thread due to how GL contexts work @@ -435,13 +437,24 @@ void MainWindow::dispatchMessage(const EmulatorMessage& message) { break; } - case MessageType::UpdateConfig: - emu->getConfig() = configWindow->getConfig(); + case MessageType::UpdateConfig: { + auto& emuConfig = emu->getConfig(); + auto& newConfig = configWindow->getConfig(); + // If the screen layout changed, we have to notify the emulator & the screen widget + bool reloadScreenLayout = (emuConfig.screenLayout != newConfig.screenLayout || emuConfig.topScreenSize != newConfig.topScreenSize); + + emuConfig = newConfig; emu->reloadSettings(); + if (reloadScreenLayout) { + emu->reloadScreenLayout(); + screen->reloadScreenLayout(newConfig.screenLayout, newConfig.topScreenSize); + } + // Save new settings to disk - emu->getConfig().save(); + emuConfig.save(); break; + } } } diff --git a/src/panda_qt/screen.cpp b/src/panda_qt/screen.cpp index 14869ef2..0876bb71 100644 --- a/src/panda_qt/screen.cpp +++ b/src/panda_qt/screen.cpp @@ -47,7 +47,7 @@ void ScreenWidget::resizeEvent(QResizeEvent* event) { this->windowInfo = *windowInfo; } - ScreenLayout::calculateCoordinates(screenCoordinates, u32(width()), u32(height()), ScreenLayout::Layout::Default); + reloadScreenCoordinates(); // This will call take care of calling resizeSurface from the emulator thread resizeCallback(surfaceWidth, surfaceHeight); @@ -62,6 +62,17 @@ void ScreenWidget::resizeSurface(u32 width, u32 height) { } } +void ScreenWidget::reloadScreenCoordinates() { + ScreenLayout::calculateCoordinates(screenCoordinates, u32(width()), u32(height()), topScreenSize, screenLayout); +} + +void ScreenWidget::reloadScreenLayout(ScreenLayout::Layout newLayout, float newTopScreenSize) { + screenLayout = newLayout; + topScreenSize = newTopScreenSize; + + reloadScreenCoordinates(); +} + bool ScreenWidget::createGLContext() { // List of GL context versions we will try. Anything 4.1+ is good for desktop OpenGL, and 3.1+ for OpenGL ES static constexpr std::array versionsToTry = { diff --git a/src/panda_sdl/frontend_sdl.cpp b/src/panda_sdl/frontend_sdl.cpp index de4d1dc6..8033165a 100644 --- a/src/panda_sdl/frontend_sdl.cpp +++ b/src/panda_sdl/frontend_sdl.cpp @@ -55,7 +55,12 @@ FrontendSDL::FrontendSDL() : keyboardMappings(InputMappings::defaultKeyboardMapp windowWidth = 400; windowHeight = 480; } + + // Initialize output size and screen layout emu.setOutputSize(windowWidth, windowHeight); + ScreenLayout::calculateCoordinates( + screenCoordinates, u32(windowWidth), u32(windowHeight), emu.getConfig().topScreenSize, emu.getConfig().screenLayout + ); if (needOpenGL) { // Demand 4.1 core for OpenGL renderer (max available on MacOS), 3.3 for the software & null renderers @@ -365,7 +370,11 @@ void FrontendSDL::run() { if (type == SDL_WINDOWEVENT_RESIZED) { windowWidth = event.window.data1; windowHeight = event.window.data2; - ScreenLayout::calculateCoordinates(screenCoordinates, u32(windowWidth), u32(windowHeight), ScreenLayout::Layout::Default); + + const auto& config = emu.getConfig(); + ScreenLayout::calculateCoordinates( + screenCoordinates, u32(windowWidth), u32(windowHeight), emu.getConfig().topScreenSize, emu.getConfig().screenLayout + ); emu.setOutputSize(windowWidth, windowHeight); }