diff --git a/CMakeLists.txt b/CMakeLists.txt index d8fc54a9..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,6 +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/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/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 1ed4966b..4c1fc1a5 100644 --- a/include/panda_qt/screen.hpp +++ b/include/panda_qt/screen.hpp @@ -3,6 +3,7 @@ #include #include +#include "screen_layout.hpp" #include "gl/context.h" #include "window_info.h" @@ -29,6 +30,15 @@ class ScreenWidget : public QWidget { u32 previousWidth = 0; u32 previousHeight = 0; + // 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; ResizeCallback resizeCallback; @@ -39,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 cbd0b88e..7bd9892f 100644 --- a/include/panda_sdl/frontend_sdl.hpp +++ b/include/panda_sdl/frontend_sdl.hpp @@ -4,6 +4,7 @@ #include +#include "screen_layout.hpp" #include "emulator.hpp" #include "input_mappings.hpp" @@ -27,7 +28,11 @@ class FrontendSDL { u32 windowHeight = 480; int gameControllerID; bool programRunning = true; - + + // 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; + // For tracking whether to update gyroscope // We bind gyro to right click + mouse movement bool holdingRightClick = false; @@ -38,5 +43,7 @@ class FrontendSDL { bool keyboardAnalogX = false; bool keyboardAnalogY = false; + private: void setupControllerSensors(SDL_GameController* controller); + void handleLeftClick(int mouseX, int mouseY); }; \ No newline at end of file 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/renderer_gl/renderer_gl.hpp b/include/renderer_gl/renderer_gl.hpp index 5476843f..b105f3e9 100644 --- a/include/renderer_gl/renderer_gl.hpp +++ b/include/renderer_gl/renderer_gl.hpp @@ -147,12 +147,24 @@ class RendererGL final : public Renderer { OpenGL::Driver driverInfo; // Information about the final 3DS screen -> Window blit, accounting for things like scaling and shifting the output based on - // the window's dimensions. + // the window's dimensions. Updated whenever the screen size or layout changes. struct { + int topScreenX = 0; + int topScreenY = 0; + int topScreenWidth = 400; + int topScreenHeight = 240; + + int bottomScreenX = 40; + int bottomScreenY = 240; + int bottomScreenWidth = 320; + int bottomScreenHeight = 240; + + // For optimizing the final screen blit into a single blit instead of 2 when possible: int destX = 0; int destY = 0; int destWidth = 400; int destHeight = 480; + bool canDoSingleBlit = true; } blitInfo; MAKE_LOG_FUNCTION(log, rendererLogger) diff --git a/include/renderer_mtl/renderer_mtl.hpp b/include/renderer_mtl/renderer_mtl.hpp index add02d52..c9b4b9b6 100644 --- a/include/renderer_mtl/renderer_mtl.hpp +++ b/include/renderer_mtl/renderer_mtl.hpp @@ -89,13 +89,17 @@ class RendererMTL final : public Renderer { MTL::Texture* lastDepthTexture = nullptr; // Information about the final 3DS screen -> Window blit, accounting for things like scaling and shifting the output based on - // the window's dimensions. + // the window's dimensions. Updated whenever the screen size or layout changes. struct { float topScreenX = 0; float topScreenY = 0; + float topScreenWidth = 400; + float topScreenHeight = 240; + float bottomScreenX = 40; float bottomScreenY = 240; - float scale = 1.0; + float bottomScreenWidth = 320; + float bottomScreenHeight = 240; } blitInfo; // Debug 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 9dbc0434..57ccca18 100644 --- a/src/core/renderer_gl/renderer_gl.cpp +++ b/src/core/renderer_gl/renderer_gl.cpp @@ -11,6 +11,7 @@ #include "PICA/pica_hash.hpp" #include "PICA/pica_simd.hpp" #include "PICA/regs.hpp" +#include "screen_layout.hpp" #include "PICA/shader_decompiler.hpp" #include "config.hpp" #include "math_util.hpp" @@ -134,8 +135,8 @@ void RendererGL::initGraphicsContextInternal() { auto prevTexture = OpenGL::getTex2D(); - // Create a plain black texture for when a game reads an invalid texture. It is common for games to configure the PICA to read texture info from NULL. - // Some games that do this are Pokemon X, Cars 2, Tomodachi Life, and more. We bind the texture to an FBO, clear it, and free the FBO + // Create a plain black texture for when a game reads an invalid texture. It is common for games to configure the PICA to read texture info from + // NULL. Some games that do this are Pokemon X, Cars 2, Tomodachi Life, and more. We bind the texture to an FBO, clear it, and free the FBO blankTexture.create(8, 8, GL_RGBA8); blankTexture.bind(); blankTexture.setMinFilter(OpenGL::Linear); @@ -228,7 +229,7 @@ void RendererGL::setupBlending() { // Shows if blending is enabled. If it is not enabled, then logic ops are enabled instead const bool blendingEnabled = (regs[PICA::InternalRegs::ColourOperation] & (1 << 8)) != 0; - if (!blendingEnabled) { // Logic ops are enabled + if (!blendingEnabled) { // Logic ops are enabled const u32 logicOp = getBits<0, 4>(regs[PICA::InternalRegs::LogicOp]); gl.setLogicOp(logicOps[logicOp]); @@ -269,20 +270,13 @@ void RendererGL::setupStencilTest(bool stencilEnable) { } static constexpr std::array stencilFuncs = { - GL_NEVER, - GL_ALWAYS, - GL_EQUAL, - GL_NOTEQUAL, - GL_LESS, - GL_LEQUAL, - GL_GREATER, - GL_GEQUAL + GL_NEVER, GL_ALWAYS, GL_EQUAL, GL_NOTEQUAL, GL_LESS, GL_LEQUAL, GL_GREATER, GL_GEQUAL, }; gl.enableStencil(); const u32 stencilConfig = regs[PICA::InternalRegs::StencilTest]; const u32 stencilFunc = getBits<4, 3>(stencilConfig); - const s32 reference = s8(getBits<16, 8>(stencilConfig)); // Signed reference value + const s32 reference = s8(getBits<16, 8>(stencilConfig)); // Signed reference value const u32 stencilRefMask = getBits<24, 8>(stencilConfig); const bool stencilWrite = regs[PICA::InternalRegs::DepthBufferWrite]; @@ -293,15 +287,9 @@ void RendererGL::setupStencilTest(bool stencilEnable) { gl.setStencilMask(stencilBufferMask); static constexpr std::array stencilOps = { - GL_KEEP, - GL_ZERO, - GL_REPLACE, - GL_INCR, - GL_DECR, - GL_INVERT, - GL_INCR_WRAP, - GL_DECR_WRAP + GL_KEEP, GL_ZERO, GL_REPLACE, GL_INCR, GL_DECR, GL_INVERT, GL_INCR_WRAP, GL_DECR_WRAP, }; + const u32 stencilOpConfig = regs[PICA::InternalRegs::StencilOp]; const u32 stencilFailOp = getBits<0, 3>(stencilOpConfig); const u32 depthFailOp = getBits<4, 3>(stencilOpConfig); @@ -468,7 +456,10 @@ void RendererGL::drawVertices(PICA::PrimType primType, std::span v const int depthFunc = getBits<4, 3>(depthControl); const int colourMask = getBits<8, 4>(depthControl); gl.setColourMask(colourMask & 1, colourMask & 2, colourMask & 4, colourMask & 8); - static constexpr std::array depthModes = {GL_NEVER, GL_ALWAYS, GL_EQUAL, GL_NOTEQUAL, GL_LESS, GL_LEQUAL, GL_GREATER, GL_GEQUAL}; + + static constexpr std::array depthModes = { + GL_NEVER, GL_ALWAYS, GL_EQUAL, GL_NOTEQUAL, GL_LESS, GL_LEQUAL, GL_GREATER, GL_GEQUAL, + }; bindTexturesToSlots(); if (gpu.fogLUTDirty) { @@ -565,14 +556,14 @@ void RendererGL::display() { if (topScreen) { topScreen->get().texture.bind(); - OpenGL::setViewport(0, 240, 400, 240); // Top screen viewport - OpenGL::draw(OpenGL::TriangleStrip, 4); // Actually draw our 3DS screen + OpenGL::setViewport(0, 240, 400, 240); // Top screen viewport + OpenGL::draw(OpenGL::TriangleStrip, 4); // Actually draw our 3DS screen } const u32 bottomActiveFb = externalRegs[Framebuffer1Select] & 1; const u32 bottomScreenAddr = externalRegs[bottomActiveFb == 0 ? Framebuffer1AFirstAddr : Framebuffer1ASecondAddr]; auto bottomScreen = colourBufferCache.findFromAddress(bottomScreenAddr); - + if (bottomScreen) { bottomScreen->get().texture.bind(); OpenGL::setViewport(40, 0, 320, 240); @@ -581,34 +572,59 @@ void RendererGL::display() { if constexpr (!Helpers::isHydraCore()) { glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0); + OpenGL::clearColor(); + screenFramebuffer.bind(OpenGL::ReadFramebuffer); if (outputSizeChanged) { outputSizeChanged = false; - const float srcAspect = 400.0f / 480.0f; // 3DS aspect ratio - const float dstAspect = float(outputWindowWidth) / float(outputWindowHeight); + auto layout = emulatorConfig->screenLayout; - blitInfo.destWidth = outputWindowWidth; - blitInfo.destHeight = outputWindowHeight; - blitInfo.destX = 0; - blitInfo.destY = 0; + // Get information about our new screen layout to use for blitting the output + ScreenLayout::WindowCoordinates windowCoords; + ScreenLayout::calculateCoordinates(windowCoords, outputWindowWidth, outputWindowHeight, emulatorConfig->topScreenSize, layout); - if (dstAspect > srcAspect) { - // Window is wider than source - blitInfo.destWidth = int(outputWindowHeight * srcAspect + 0.5f); - blitInfo.destX = (outputWindowWidth - blitInfo.destWidth) / 2; - } else { - // Window is taller than source - blitInfo.destHeight = int(outputWindowWidth / srcAspect + 0.5f); - blitInfo.destY = (outputWindowHeight - blitInfo.destHeight) / 2; - } + blitInfo.topScreenX = windowCoords.topScreenX; + blitInfo.topScreenY = windowCoords.topScreenY; + blitInfo.topScreenWidth = windowCoords.topScreenWidth; + blitInfo.topScreenHeight = windowCoords.topScreenHeight; + + blitInfo.bottomScreenX = windowCoords.bottomScreenX; + blitInfo.bottomScreenY = windowCoords.bottomScreenY; + blitInfo.bottomScreenWidth = windowCoords.bottomScreenWidth; + blitInfo.bottomScreenHeight = windowCoords.bottomScreenHeight; + + // 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; } - glBlitFramebuffer( - 0, 0, 400, 480, blitInfo.destX, blitInfo.destY, blitInfo.destX + blitInfo.destWidth, blitInfo.destY + blitInfo.destHeight, - GL_COLOR_BUFFER_BIT, GL_LINEAR - ); + if (blitInfo.canDoSingleBlit) { + glBlitFramebuffer( + 0, 0, 400, 480, blitInfo.destX, blitInfo.destY, blitInfo.destX + blitInfo.destWidth, blitInfo.destY + blitInfo.destHeight, + GL_COLOR_BUFFER_BIT, GL_LINEAR + ); + } else { + // Blit top screen + glBlitFramebuffer( + 0, 240, 400, 480, blitInfo.topScreenX, blitInfo.topScreenY, blitInfo.topScreenX + blitInfo.topScreenWidth, + blitInfo.topScreenY + blitInfo.topScreenHeight, GL_COLOR_BUFFER_BIT, GL_LINEAR + ); + + // Blit bottom screen + glBlitFramebuffer( + 40, 0, 360, 240, blitInfo.bottomScreenX, blitInfo.bottomScreenY, blitInfo.bottomScreenX + blitInfo.bottomScreenWidth, + blitInfo.bottomScreenY + blitInfo.bottomScreenHeight, GL_COLOR_BUFFER_BIT, GL_LINEAR + ); + } } } @@ -735,8 +751,10 @@ void RendererGL::displayTransfer(u32 inputAddr, u32 outputAddr, u32 inputSize, u u32 outputWidth = outputSize & 0xffff; u32 outputHeight = outputSize >> 16; - OpenGL::DebugScope scope("DisplayTransfer inputAddr 0x%08X outputAddr 0x%08X inputWidth %d outputWidth %d inputHeight %d outputHeight %d", - inputAddr, outputAddr, inputWidth, outputWidth, inputHeight, outputHeight); + OpenGL::DebugScope scope( + "DisplayTransfer inputAddr 0x%08X outputAddr 0x%08X inputWidth %d outputWidth %d inputHeight %d outputHeight %d", inputAddr, outputAddr, + inputWidth, outputWidth, inputHeight, outputHeight + ); auto srcFramebuffer = getColourBuffer(inputAddr, inputFormat, inputWidth, outputHeight); Math::Rect srcRect = srcFramebuffer->getSubRect(inputAddr, outputWidth, outputHeight); @@ -786,8 +804,10 @@ void RendererGL::textureCopy(u32 inputAddr, u32 outputAddr, u32 totalBytes, u32 const u32 outputWidth = (outputSize & 0xffff) << 4; const u32 outputGap = (outputSize >> 16) << 4; - OpenGL::DebugScope scope("TextureCopy inputAddr 0x%08X outputAddr 0x%08X totalBytes %d inputWidth %d inputGap %d outputWidth %d outputGap %d", - inputAddr, outputAddr, totalBytes, inputWidth, inputGap, outputWidth, outputGap); + OpenGL::DebugScope scope( + "TextureCopy inputAddr 0x%08X outputAddr 0x%08X totalBytes %d inputWidth %d inputGap %d outputWidth %d outputGap %d", inputAddr, outputAddr, + totalBytes, inputWidth, inputGap, outputWidth, outputGap + ); if (inputGap != 0 || outputGap != 0) { // Helpers::warn("Strided texture copy\n"); @@ -825,7 +845,7 @@ void RendererGL::textureCopy(u32 inputAddr, u32 outputAddr, u32 totalBytes, u32 // Find the source surface. auto srcFramebuffer = getColourBuffer(inputAddr, PICA::ColorFmt::RGBA8, copyStride, copyHeight, false); if (!srcFramebuffer) { - static int shutUpCounter = 0; // Don't want to spam the console too much, so shut up after 5 times + static int shutUpCounter = 0; // Don't want to spam the console too much, so shut up after 5 times if (shutUpCounter < 5) { shutUpCounter++; @@ -1041,8 +1061,8 @@ bool RendererGL::prepareForDraw(ShaderUnit& shaderUnit, PICA::DrawAcceleration* driverInfo.usingGLES ? PICA::ShaderGen::API::GLES : PICA::ShaderGen::API::GL, PICA::ShaderGen::Language::GLSL ); - // Empty source means compilation error, if the source is not empty then we convert the recompiled PICA code into a valid shader and upload - // it to the GPU + // Empty source means compilation error, if the source is not empty then we convert the recompiled PICA code into a valid shader and + // upload it to the GPU if (!picaShaderSource.empty()) { std::string vertexShaderSource = fragShaderGen.getVertexShaderAccelerated(picaShaderSource, vertexConfig, usingUbershader); shader->create({vertexShaderSource}, OpenGL::Vertex); @@ -1073,7 +1093,7 @@ bool RendererGL::prepareForDraw(ShaderUnit& shaderUnit, PICA::DrawAcceleration* if (!usingUbershader) { OpenGL::Program& program = getSpecializedShader(); gl.useProgram(program); - } else { // Bind ubershader & load ubershader uniforms + } else { // Bind ubershader & load ubershader uniforms gl.useProgram(triangleProgram); const float depthScale = f24::fromRaw(regs[PICA::InternalRegs::DepthScale] & 0xffffff).toFloat32(); @@ -1243,7 +1263,7 @@ void RendererGL::accelerateVertexUpload(ShaderUnit& shaderUnit, PICA::DrawAccele const u32 currentAttributeMask = accel->enabledAttributeMask; // Use bitwise xor to calculate which attributes changed u32 attributeMaskDiff = currentAttributeMask ^ previousAttributeMask; - + while (attributeMaskDiff != 0) { // Get index of next different attribute and turn it off const u32 index = 31 - std::countl_zero(attributeMaskDiff); diff --git a/src/core/renderer_mtl/renderer_mtl.cpp b/src/core/renderer_mtl/renderer_mtl.cpp index bbc8212a..71cdd616 100644 --- a/src/core/renderer_mtl/renderer_mtl.cpp +++ b/src/core/renderer_mtl/renderer_mtl.cpp @@ -10,6 +10,7 @@ #include "PICA/gpu.hpp" #include "PICA/pica_hash.hpp" +#include "screen_layout.hpp" #include "SDL_metal.h" using namespace PICA; @@ -105,32 +106,26 @@ void RendererMTL::display() { if (outputSizeChanged) { outputSizeChanged = false; + ScreenLayout::WindowCoordinates windowCoords; + ScreenLayout::calculateCoordinates( + windowCoords, outputWindowWidth, outputWindowHeight, emulatorConfig->topScreenSize, emulatorConfig->screenLayout + ); - const float srcAspect = 400.0 / 480.0; - const float destAspect = float(outputWindowWidth) / float(outputWindowHeight); - int destX = 0, destY = 0, destWidth = outputWindowWidth, destHeight = outputWindowHeight; + blitInfo.topScreenX = float(windowCoords.topScreenX); + blitInfo.topScreenY = float(windowCoords.topScreenY); + blitInfo.bottomScreenX = float(windowCoords.bottomScreenX); + blitInfo.bottomScreenY = float(windowCoords.bottomScreenY); - if (destAspect > srcAspect) { - // Window is wider than source - destWidth = int(outputWindowHeight * srcAspect + 0.5f); - destX = (outputWindowWidth - destWidth) / 2; - } else { - // Window is taller than source - destHeight = int(outputWindowWidth / srcAspect + 0.5f); - destY = (outputWindowHeight - destHeight) / 2; - } - - blitInfo.scale = float(destWidth) / 400.0f; - blitInfo.topScreenX = float(destX); - blitInfo.topScreenY = float(destY + (destHeight - int(480 * blitInfo.scale)) / 2); - blitInfo.bottomScreenX = float(destX) + 40 * blitInfo.scale; - blitInfo.bottomScreenY = blitInfo.topScreenY + 240 * blitInfo.scale; + blitInfo.topScreenWidth = float(windowCoords.topScreenWidth); + blitInfo.topScreenHeight = float(windowCoords.topScreenHeight); + blitInfo.bottomScreenWidth = float(windowCoords.bottomScreenWidth); + blitInfo.bottomScreenHeight = float(windowCoords.bottomScreenHeight); } // Top screen if (topScreen) { renderCommandEncoder->setViewport( - MTL::Viewport{blitInfo.topScreenX, blitInfo.topScreenY, 400 * blitInfo.scale, 240 * blitInfo.scale, 0.0f, 1.0f} + MTL::Viewport{blitInfo.topScreenX, blitInfo.topScreenY, blitInfo.topScreenWidth, blitInfo.topScreenHeight, 0.0f, 1.0f} ); renderCommandEncoder->setFragmentTexture(topScreen->get().texture, 0); renderCommandEncoder->drawPrimitives(MTL::PrimitiveTypeTriangleStrip, NS::UInteger(0), NS::UInteger(4)); @@ -139,7 +134,7 @@ void RendererMTL::display() { // Bottom screen if (bottomScreen) { renderCommandEncoder->setViewport( - MTL::Viewport{blitInfo.bottomScreenX, blitInfo.bottomScreenY, 320 * blitInfo.scale, 240 * blitInfo.scale, 0.0f, 1.0f} + MTL::Viewport{blitInfo.bottomScreenX, blitInfo.bottomScreenY, blitInfo.bottomScreenWidth, blitInfo.bottomScreenHeight, 0.0f, 1.0f} ); renderCommandEncoder->setFragmentTexture(bottomScreen->get().texture, 0); renderCommandEncoder->drawPrimitives(MTL::PrimitiveTypeTriangleStrip, NS::UInteger(0), NS::UInteger(4)); @@ -800,7 +795,7 @@ void RendererMTL::updateLightingLUT(MTL::RenderCommandEncoder* encoder) { void RendererMTL::updateFogLUT(MTL::RenderCommandEncoder* encoder) { gpu.fogLUTDirty = false; - std::array fogLut = {0.0f}; + std::array fogLut = {0.0f}; for (int i = 0; i < fogLut.size(); i += 2) { const uint32_t value = gpu.fogLUT[i >> 1]; @@ -835,8 +830,11 @@ void RendererMTL::textureCopyImpl( commandEncoder.setRenderPipelineState(blitPipeline); // Viewport - renderCommandEncoder->setViewport(MTL::Viewport{ - double(destRect.left), double(destRect.bottom), double(destRect.right - destRect.left), double(destRect.top - destRect.bottom), 0.0, 1.0}); + renderCommandEncoder->setViewport( + MTL::Viewport{ + double(destRect.left), double(destRect.bottom), double(destRect.right - destRect.left), double(destRect.top - destRect.bottom), 0.0, 1.0 + } + ); float srcRectNDC[4] = { srcRect.left / (float)srcFramebuffer.size.u(), diff --git a/src/core/screen_layout.cpp b/src/core/screen_layout.cpp new file mode 100644 index 00000000..77318f17 --- /dev/null +++ b/src/core/screen_layout.cpp @@ -0,0 +1,152 @@ +#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 scaleTop = std::min(float(outputWindowWidth) / float(TOP_SCREEN_WIDTH), float(availableTopHeight) / float(TOP_SCREEN_HEIGHT)); + float scaleBottom = + std::min(float(outputWindowWidth) / float(BOTTOM_SCREEN_WIDTH), float(availableBottomHeight) / 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); + + // 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 centerY = (outputWindowHeight - maxHeight) / 2; + + int topScreenY = centerY + (maxHeight - topScreenHeight) / 2; + int bottomScreenY = centerY + (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"; + } +} + \ No newline at end of file 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 a55684e0..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; + } } } @@ -525,25 +538,28 @@ void MainWindow::handleTouchscreenPress(QMouseEvent* event) { const QPointF clickPos = event->globalPosition(); const QPointF widgetPos = screen->mapFromGlobal(clickPos); + const auto& coords = screen->screenCoordinates; + const float bottomScreenX = float(coords.bottomScreenX); + const float bottomScreenY = float(coords.bottomScreenY); + const float bottomScreenWidth = float(coords.bottomScreenWidth); + const float bottomScreenHeight = float(coords.bottomScreenHeight); + // Press is inside the screen area - if (widgetPos.x() >= 0 && widgetPos.x() < screen->width() && widgetPos.y() >= 0 && widgetPos.y() < screen->height()) { - // Go from widget positions to [0, 400) for x and [0, 480) for y - uint x = (uint)std::round(widgetPos.x() / screen->width() * 400.f); - uint y = (uint)std::round(widgetPos.y() / screen->height() * 480.f); + if (widgetPos.x() >= bottomScreenX && widgetPos.x() < bottomScreenX + bottomScreenWidth && widgetPos.y() >= bottomScreenY && + widgetPos.y() < bottomScreenY + bottomScreenHeight) { + // Map widget position to 3DS touchscreen coordinates, with (0, 0) = top left of touchscreen + float relX = (widgetPos.x() - bottomScreenX) / bottomScreenWidth; + float relY = (widgetPos.y() - bottomScreenY) / bottomScreenHeight; - // Check if touch falls in the touch screen area - if (y >= 240 && y <= 480 && x >= 40 && x < 40 + 320) { - // Convert to 3DS coordinates - u16 x_converted = static_cast(x) - 40; - u16 y_converted = static_cast(y) - 240; + u16 x_converted = u16(std::clamp(relX * ScreenLayout::BOTTOM_SCREEN_WIDTH, 0.f, float(ScreenLayout::BOTTOM_SCREEN_WIDTH - 1))); + u16 y_converted = u16(std::clamp(relY * ScreenLayout::BOTTOM_SCREEN_HEIGHT, 0.f, float(ScreenLayout::BOTTOM_SCREEN_HEIGHT - 1))); - EmulatorMessage message{.type = MessageType::PressTouchscreen}; - message.touchscreen.x = x_converted; - message.touchscreen.y = y_converted; - sendMessage(message); - } else { - sendMessage(EmulatorMessage{.type = MessageType::ReleaseTouchscreen}); - } + EmulatorMessage message{.type = MessageType::PressTouchscreen}; + message.touchscreen.x = x_converted; + message.touchscreen.y = y_converted; + sendMessage(message); + } else { + sendMessage(EmulatorMessage{.type = MessageType::ReleaseTouchscreen}); } } diff --git a/src/panda_qt/screen.cpp b/src/panda_qt/screen.cpp index fc783683..0876bb71 100644 --- a/src/panda_qt/screen.cpp +++ b/src/panda_qt/screen.cpp @@ -21,7 +21,7 @@ ScreenWidget::ScreenWidget(ResizeCallback resizeCallback, QWidget* parent) : QWidget(parent), resizeCallback(resizeCallback) { // Create a native window for use with our graphics API of choice resize(800, 240 * 4); - + setAutoFillBackground(false); setAttribute(Qt::WA_NativeWindow, true); setAttribute(Qt::WA_NoSystemBackground, true); @@ -47,6 +47,8 @@ void ScreenWidget::resizeEvent(QResizeEvent* event) { this->windowInfo = *windowInfo; } + reloadScreenCoordinates(); + // This will call take care of calling resizeSurface from the emulator thread resizeCallback(surfaceWidth, surfaceHeight); } @@ -60,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 665b2512..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 @@ -230,24 +235,7 @@ void FrontendSDL::run() { if (emu.romType == ROMType::None) break; if (event.button.button == SDL_BUTTON_LEFT) { - if (windowWidth == 0 || windowHeight == 0) [[unlikely]] { - break; - } - - // Go from window positions to [0, 400) for x and [0, 480) for y - const s32 x = (s32)std::round(event.button.x * 400.f / windowWidth); - const s32 y = (s32)std::round(event.button.y * 480.f / windowHeight); - - // Check if touch falls in the touch screen area - if (y >= 240 && y <= 480 && x >= 40 && x < 40 + 320) { - // Convert to 3DS coordinates - u16 x_converted = static_cast(x) - 40; - u16 y_converted = static_cast(y) - 240; - - hid.setTouchScreenPress(x_converted, y_converted); - } else { - hid.releaseTouchScreen(); - } + handleLeftClick(event.button.x, event.button.y); } else if (event.button.button == SDL_BUTTON_RIGHT) { holdingRightClick = true; } @@ -321,18 +309,7 @@ void FrontendSDL::run() { break; } - // Go from window positions to [0, 400) for x and [0, 480) for y - const s32 x = (s32)std::round(event.motion.x * 400.f / windowWidth); - const s32 y = (s32)std::round(event.motion.y * 480.f / windowHeight); - - // Check if touch falls in the touch screen area and register the new touch screen position - if (y >= 240 && y <= 480 && x >= 40 && x < 40 + 320) { - // Convert to 3DS coordinates - u16 x_converted = static_cast(x) - 40; - u16 y_converted = static_cast(y) - 240; - - hid.setTouchScreenPress(x_converted, y_converted); - } + handleLeftClick(event.motion.x, event.motion.y); } // We use right click to indicate we want to rotate the console. If right click is not held, then this is not a gyroscope rotation @@ -393,6 +370,12 @@ void FrontendSDL::run() { if (type == SDL_WINDOWEVENT_RESIZED) { windowWidth = event.window.data1; windowHeight = event.window.data2; + + const auto& config = emu.getConfig(); + ScreenLayout::calculateCoordinates( + screenCoordinates, u32(windowWidth), u32(windowHeight), emu.getConfig().topScreenSize, emu.getConfig().screenLayout + ); + emu.setOutputSize(windowWidth, windowHeight); } } @@ -471,3 +454,31 @@ void FrontendSDL::setupControllerSensors(SDL_GameController* controller) { SDL_GameControllerSetSensorEnabled(controller, SDL_SENSOR_ACCEL, SDL_TRUE); } } + +void FrontendSDL::handleLeftClick(int mouseX, int mouseY) { + if (windowWidth == 0 || windowHeight == 0) [[unlikely]] { + return; + } + + const auto& coords = screenCoordinates; + const int bottomScreenX = int(coords.bottomScreenX); + const int bottomScreenY = int(coords.bottomScreenY); + const int bottomScreenWidth = int(coords.bottomScreenWidth); + const int bottomScreenHeight = int(coords.bottomScreenHeight); + auto& hid = emu.getServiceManager().getHID(); + + // Check if the mouse is inside the bottom screen area + if (mouseX >= int(bottomScreenX) && mouseX < int(bottomScreenX + bottomScreenWidth) && mouseY >= int(bottomScreenY) && + mouseY < int(bottomScreenY + bottomScreenHeight)) { + // Map to 3DS touchscreen coordinates + float relX = float(mouseX - bottomScreenX) / float(bottomScreenWidth); + float relY = float(mouseY - bottomScreenY) / float(bottomScreenHeight); + + u16 x_converted = static_cast(std::clamp(relX * ScreenLayout::BOTTOM_SCREEN_WIDTH, 0.f, float(ScreenLayout::BOTTOM_SCREEN_WIDTH - 1))); + u16 y_converted = static_cast(std::clamp(relY * ScreenLayout::BOTTOM_SCREEN_HEIGHT, 0.f, float(ScreenLayout::BOTTOM_SCREEN_HEIGHT - 1))); + + hid.setTouchScreenPress(x_converted, y_converted); + } else { + hid.releaseTouchScreen(); + } +} \ No newline at end of file