diff --git a/.github/workflows/Qt_Build.yml b/.github/workflows/Qt_Build.yml index 27d7d5ca..0cb007f4 100644 --- a/.github/workflows/Qt_Build.yml +++ b/.github/workflows/Qt_Build.yml @@ -108,7 +108,7 @@ jobs: sudo apt-get update && sudo apt install libx11-dev libgl1-mesa-glx mesa-common-dev libfuse2 libwayland-dev sudo add-apt-repository -y ppa:savoury1/qt-6-2 sudo apt update - sudo apt install qt6-base-dev + sudo apt install qt6-base-dev qt6-base-private-dev - name: Install newer Clang run: | diff --git a/CMakeLists.txt b/CMakeLists.txt index 28fba674..22294821 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -63,7 +63,7 @@ if(ENABLE_DISCORD_RPC AND NOT ANDROID) include_directories(third_party/discord-rpc/include) endif() -if (ENABLE_QT_GUI) +if(ENABLE_QT_GUI) find_package(Qt6 REQUIRED COMPONENTS Widgets) # We can't use qt_standard_project_setup since it's Qt 6.3+ and we don't need to set the minimum that high @@ -179,9 +179,15 @@ set(RENDERER_SW_SOURCE_FILES src/core/renderer_sw/renderer_sw.cpp) # Frontend source files if(ENABLE_QT_GUI) - set(FRONTEND_SOURCE_FILES src/panda_qt/main.cpp) + set(FRONTEND_SOURCE_FILES src/panda_qt/main.cpp src/panda_qt/screen.cpp) + set(FRONTEND_HEADER_FILES include/panda_qt/screen.hpp) + + source_group("Source Files\\Qt" FILES ${FRONTEND_SOURCE_FILES}) + source_group("Header Files\\Qt" FILES ${FRONTEND_HEADER_FILES}) + include_directories(${Qt6Gui_PRIVATE_INCLUDE_DIRS}) else() set(FRONTEND_SOURCE_FILES src/panda_sdl/main.cpp) + set(FRONTEND_HEADER_FILES "") endif() set(HEADER_FILES include/emulator.hpp include/helpers.hpp include/termcolor.hpp @@ -353,7 +359,8 @@ endif() source_group("Header Files\\Core" FILES ${HEADER_FILES}) set(ALL_SOURCES ${SOURCE_FILES} ${FRONTEND_SOURCE_FILES} ${FS_SOURCE_FILES} ${CRYPTO_SOURCE_FILES} ${KERNEL_SOURCE_FILES} - ${LOADER_SOURCE_FILES} ${SERVICE_SOURCE_FILES} ${APPLET_SOURCE_FILES} ${RENDERER_SW_SOURCE_FILES} ${PICA_SOURCE_FILES} ${THIRD_PARTY_SOURCE_FILES} ${HEADER_FILES}) + ${LOADER_SOURCE_FILES} ${SERVICE_SOURCE_FILES} ${APPLET_SOURCE_FILES} ${RENDERER_SW_SOURCE_FILES} ${PICA_SOURCE_FILES} ${THIRD_PARTY_SOURCE_FILES} + ${HEADER_FILES} ${FRONTEND_HEADER_FILES}) if(ENABLE_OPENGL) # Add the OpenGL source files to ALL_SOURCES @@ -400,6 +407,11 @@ if(ENABLE_QT_GUI) if(LINUX OR FREEBSD) find_package(X11 REQUIRED) target_link_libraries(Alber PRIVATE ${X11_LIBRARIES}) + + if(ENABLE_OPENGL) + find_package(OpenGL REQUIRED COMPONENTS OpenGL EGL GLX) + target_link_libraries(Alber PRIVATE OpenGL::OpenGL OpenGL::EGL OpenGL::GLX) + endif() endif() else() target_compile_definitions(Alber PUBLIC "PANDA3DS_FRONTEND_SDL=1") diff --git a/include/panda_qt/screen.hpp b/include/panda_qt/screen.hpp new file mode 100644 index 00000000..1555e463 --- /dev/null +++ b/include/panda_qt/screen.hpp @@ -0,0 +1,23 @@ +#pragma once +#include +#include + +#include "gl/context.h" +#include "window_info.h" + +// OpenGL widget for drawing the 3DS screen +class ScreenWidget : public QWidget { + Q_OBJECT + + public: + ScreenWidget(QWidget* parent = nullptr); + + private: + std::unique_ptr glContext = nullptr; + bool createGLContext(); + + qreal devicePixelRatioFromScreen() const; + int scaledWindowWidth() const; + int scaledWindowHeight() const; + std::optional getWindowInfo(); +}; diff --git a/src/panda_qt/main.cpp b/src/panda_qt/main.cpp index e8757f1a..34e71915 100644 --- a/src/panda_qt/main.cpp +++ b/src/panda_qt/main.cpp @@ -1,12 +1,18 @@ #include #include +#include "panda_qt/screen.hpp" + int main(int argc, char *argv[]) { - QApplication app(argc, argv); - QWidget window; - - window.resize(320, 240); - window.show(); - window.setWindowTitle("Alber"); - return app.exec(); + QApplication app(argc, argv); + QWidget window; + + window.resize(320, 240); + window.show(); + window.setWindowTitle("Alber"); + ScreenWidget screen(&window); + screen.show(); + screen.resize(320, 240); + + return app.exec(); } \ No newline at end of file diff --git a/src/panda_qt/screen.cpp b/src/panda_qt/screen.cpp new file mode 100644 index 00000000..62cac106 --- /dev/null +++ b/src/panda_qt/screen.cpp @@ -0,0 +1,120 @@ +#include "opengl.hpp" +// opengl.hpp must be included at the very top. This comment exists to make clang-format not reorder it :p +#include +#include +#include +#include +#include +#include +#include + +#if !defined(_WIN32) && !defined(APPLE) +#include +#endif + +#include "panda_qt/screen.hpp" + +// OpenGL screen widget, based on https://github.com/stenzek/duckstation/blob/master/src/duckstation-qt/displaywidget.cpp +// and https://github.com/melonDS-emu/melonDS/blob/master/src/frontend/qt_sdl/main.cpp + +#ifdef PANDA3DS_ENABLE_OPENGL +ScreenWidget::ScreenWidget(QWidget* parent) : QWidget(parent) { + // Create a native window for use with our graphics API of choice + setAutoFillBackground(false); + setAttribute(Qt::WA_NativeWindow, true); + setAttribute(Qt::WA_NoSystemBackground, true); + setAttribute(Qt::WA_PaintOnScreen, true); + setAttribute(Qt::WA_KeyCompression, false); + setFocusPolicy(Qt::StrongFocus); + setMouseTracking(true); + + if (!createGLContext()) { + Helpers::panic("Failed to create GL context for display"); + } + + // Make our context current to use it + glContext->MakeCurrent(); + // Enable VSync for now + glContext->SetSwapInterval(1); + + OpenGL::setViewport(320, 240); + OpenGL::setClearColor(1.0, 0.0, 0.0, 1.0); + OpenGL::clearColor(); + + // Swap buffers to display our red as a test + glContext->SwapBuffers(); +} + +bool ScreenWidget::createGLContext() { + // List of GL context versions we will try. Anything 4.1+ is good + static constexpr std::array versionsToTry = { + GL::Context::Version{GL::Context::Profile::Core, 4, 6}, GL::Context::Version{GL::Context::Profile::Core, 4, 5}, + GL::Context::Version{GL::Context::Profile::Core, 4, 4}, GL::Context::Version{GL::Context::Profile::Core, 4, 3}, + GL::Context::Version{GL::Context::Profile::Core, 4, 2}, GL::Context::Version{GL::Context::Profile::Core, 4, 1}, + }; + + std::optional windowInfo = getWindowInfo(); + if (windowInfo.has_value()) { + glContext = GL::Context::Create(*getWindowInfo(), versionsToTry); + glContext->DoneCurrent(); + } + + return glContext != nullptr; +} + +qreal ScreenWidget::devicePixelRatioFromScreen() const { + const QScreen* screenForRatio = window()->windowHandle()->screen(); + if (!screenForRatio) { + screenForRatio = QGuiApplication::primaryScreen(); + } + + return screenForRatio ? screenForRatio->devicePixelRatio() : static_cast(1); +} + +int ScreenWidget::scaledWindowWidth() const { + return std::max(static_cast(std::ceil(static_cast(width()) * devicePixelRatioFromScreen())), 1); +} + +int ScreenWidget::scaledWindowHeight() const { + return std::max(static_cast(std::ceil(static_cast(height()) * devicePixelRatioFromScreen())), 1); +} + +std::optional ScreenWidget::getWindowInfo() { + WindowInfo wi; + +// Windows and Apple are easy here since there's no display connection. +#if defined(_WIN32) + wi.type = WindowInfo::Type::Win32; + wi.window_handle = reinterpret_cast(winId()); +#elif defined(__APPLE__) + wi.type = WindowInfo::Type::MacOS; + wi.window_handle = reinterpret_cast(winId()); +#else + QPlatformNativeInterface* pni = QGuiApplication::platformNativeInterface(); + const QString platform_name = QGuiApplication::platformName(); + if (platform_name == QStringLiteral("xcb")) { + wi.type = WindowInfo::Type::X11; + wi.display_connection = pni->nativeResourceForWindow("display", windowHandle()); + wi.window_handle = reinterpret_cast(winId()); + } else if (platform_name == QStringLiteral("wayland")) { + wi.type = WindowInfo::Type::Wayland; + QWindow* handle = windowHandle(); + if (handle == nullptr) { + return std::nullopt; + } + + wi.display_connection = pni->nativeResourceForWindow("display", handle); + wi.window_handle = pni->nativeResourceForWindow("surface", handle); + } else { + qCritical() << "Unknown PNI platform " << platform_name; + return std::nullopt; + } +#endif + + wi.surface_width = static_cast(scaledWindowWidth()); + wi.surface_height = static_cast(scaledWindowHeight()); + wi.surface_scale = static_cast(devicePixelRatioFromScreen()); + + return wi; +} +#endif \ No newline at end of file