diff --git a/.github/workflows/create-test-plan.py b/.github/workflows/create-test-plan.py index c4d10524ad5a6..24505ab41962d 100755 --- a/.github/workflows/create-test-plan.py +++ b/.github/workflows/create-test-plan.py @@ -378,6 +378,7 @@ def spec_to_job(spec: JobSpec, key: str, trackmem_symbol_names: bool, ctest_args job.clang_tidy = False case _: raise ValueError(f"Invalid intel={spec.intel}") + job.apt_packages.append("intel-oneapi-compiler-dpcpp-cpp-and-cpp-classic") job.source_cmd = f"source /opt/intel/oneapi/setvars.sh;" job.intel = True job.shell = "bash" @@ -494,6 +495,8 @@ def spec_to_job(spec: JobSpec, key: str, trackmem_symbol_names: bool, ctest_args "libavutil-dev", "libswresample-dev", "libswscale-dev", + # testqt6 + "qt6-base-dev", )) match = re.match(r"ubuntu-(?P[0-9]+)\.(?P[0-9]+|latest).*", spec.os.value) ubuntu_ge_24 = True @@ -790,6 +793,7 @@ def spec_to_job(spec: JobSpec, key: str, trackmem_symbol_names: bool, ctest_args if spec.msys2_platform not in (Msys2Platform.Mingw32, ): job.msys2_packages.append(f"{msys2_env}-perl") job.msys2_packages.append(f"{msys2_env}-clang-tools-extra") + job.msys2_packages.append(f"{msys2_env}-qt6") if job.ccache: job.msys2_packages.append(f"{msys2_env}-ccache") job.microsoft_gameinput = True diff --git a/.github/workflows/generic.yml b/.github/workflows/generic.yml index e4313ef99e9bb..4111d07191fcb 100644 --- a/.github/workflows/generic.yml +++ b/.github/workflows/generic.yml @@ -119,12 +119,6 @@ jobs: # Add signed entry to apt sources and configure the APT client to use Intel repository: echo "deb [signed-by=/usr/share/keyrings/oneapi-archive-keyring.gpg] https://apt.repos.intel.com/oneapi all main" | sudo tee /etc/apt/sources.list.d/oneAPI.list - - # Update package list - sudo apt-get update -y - - # Install oneAPI - sudo apt-get install -y intel-oneapi-compiler-dpcpp-cpp-and-cpp-classic - name: 'Install apk packages' if: ${{ matrix.platform.apk-packages != '' }} run: | @@ -133,7 +127,7 @@ jobs: - name: 'Install apt packages' if: ${{ matrix.platform.apt-packages != '' }} run: | - ${{ matrix.platform.sudo }} apt-get update + ${{ matrix.platform.sudo }} apt-get update -y ${{ matrix.platform.sudo }} apt-get install -y ${{ matrix.platform.apt-packages }} - name: 'Install brew packages' if: ${{ matrix.platform.brew-packages != '' }} diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 074c21035da96..928b29dde0a31 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -21,6 +21,14 @@ include(sdlcompilers) check_language(CXX) if(CMAKE_CXX_COMPILER) enable_language(CXX) + + find_package(Qt6 QUIET COMPONENTS Widgets) + if(Qt6_FOUND) + + if(cxx_std_17 IN_LIST CMAKE_CXX_COMPILE_FEATURES) + add_subdirectory(qt6) + endif() + endif() endif() find_package(Python3 COMPONENTS Interpreter) @@ -710,8 +718,8 @@ function(add_sdl_test TEST TARGET) set(installedtestsdir "${CMAKE_INSTALL_FULL_LIBEXECDIR}/installed-tests/SDL3") configure_file(template.test.in "${exe}.test" @ONLY) install( - FILES "${CMAKE_CURRENT_BINARY_DIR}/${exe}.test" - DESTINATION ${CMAKE_INSTALL_DATADIR}/installed-tests/SDL3 + FILES "${CMAKE_CURRENT_BINARY_DIR}/${exe}.test" + DESTINATION ${CMAKE_INSTALL_DATADIR}/installed-tests/SDL3 ) endif() if(TARGET pretest AND NOT "${TARGET}" MATCHES "pretest") diff --git a/test/qt6/CMakeLists.txt b/test/qt6/CMakeLists.txt new file mode 100644 index 0000000000000..04f94488b1c22 --- /dev/null +++ b/test/qt6/CMakeLists.txt @@ -0,0 +1,10 @@ +qt_add_executable(testqt6 WIN32 + main.cpp + mainwindow.h + mainwindow.cpp + QSDLCanvas.h + QSDLCanvas.cpp +) +target_compile_features(testqt6 PRIVATE cxx_std_17) +target_link_libraries(testqt6 PRIVATE Qt6::Widgets SDL3::SDL3) +set_property(TARGET testqt6 PROPERTY AUTOMOC TRUE) diff --git a/test/qt6/QSDLCanvas.cpp b/test/qt6/QSDLCanvas.cpp new file mode 100644 index 0000000000000..01ddb3d3674ab --- /dev/null +++ b/test/qt6/QSDLCanvas.cpp @@ -0,0 +1,158 @@ +/* + Copyright (C) 1997-2026 Sam Lantinga + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely. +*/ +#include "QSDLCanvas.h" + +#include + +QSDLCanvas::QSDLCanvas(QWidget *parent, bool updateCanvasSize) : QWidget(parent), m_drawTimer(this) +{ + // Allow SDL to draw to the window + setAttribute(Qt::WA_PaintOnScreen); + setAttribute(Qt::WA_OpaquePaintEvent); + setAttribute(Qt::WA_NoSystemBackground); + + m_updateCanvasSize = updateCanvasSize; +} + +QSDLCanvas::~QSDLCanvas() +{ + if (m_renderer) { + SDL_DestroyRenderer(m_renderer); + } + if (m_window) { + SDL_DestroyWindow(m_window); + } +} + +void QSDLCanvas::onUpdate() +{ + SDL_Event event; + while (SDL_PollEvent(&event)) { + onEvent(&event); + } + + onDraw(); +} + +void QSDLCanvas::changeRenderer(const QString &name) +{ + m_rendererName = name; + m_rendererNameChanged = true; + + recreateRenderer(); +} + +void QSDLCanvas::drawRendererName(bool draw) +{ + m_draw_renderer_name = draw; +} + +void QSDLCanvas::onDraw() +{ + char text_buffer[256]; + SDL_SetRenderDrawColor(m_renderer, 0, 255, 0, SDL_ALPHA_OPAQUE); + SDL_RenderClear(m_renderer); + + // Do your drawing here + + if (m_draw_renderer_name) { + const char *renderer_name = SDL_GetRendererName(m_renderer); + if (renderer_name == NULL) { + SDL_snprintf(text_buffer, sizeof(text_buffer), "(null) [%s]", SDL_GetError()); + renderer_name = text_buffer; + } + SDL_Point render_size; + SDL_GetRenderOutputSize(m_renderer, &render_size.x, &render_size.y); + SDL_SetRenderDrawColor(m_renderer, 0, 0, 0, SDL_ALPHA_OPAQUE); + SDL_RenderDebugText(m_renderer, render_size.x - SDL_DEBUG_TEXT_FONT_CHARACTER_SIZE * SDL_strlen(renderer_name), render_size.y - 1 * SDL_DEBUG_TEXT_FONT_CHARACTER_SIZE, renderer_name); + } + + SDL_RenderPresent(m_renderer); +} + +void QSDLCanvas::onEvent(SDL_Event *event) +{ + +} + +void QSDLCanvas::resizeEvent(QResizeEvent *event) +{ + if (m_updateCanvasSize && m_window) { + const QSize canvasSize = event->size(); + SDL_SetWindowSize(m_window, canvasSize.width(), canvasSize.height()); + } +} + +void QSDLCanvas::showEvent(QShowEvent *event) +{ + QWidget::showEvent(event); + + if (!m_window) { + SDL_PropertiesID props = SDL_CreateProperties(); + QString video_driver = SDL_GetCurrentVideoDriver(); + if (video_driver == "cocoa") { + SDL_SetPointerProperty(props, SDL_PROP_WINDOW_CREATE_COCOA_WINDOW_POINTER, (void *)winId()); + } else if (video_driver == "x11") { + SDL_SetNumberProperty(props, SDL_PROP_WINDOW_CREATE_X11_WINDOW_NUMBER, (int)winId()); + } else if (video_driver == "wayland") { + SDL_SetPointerProperty(props, SDL_PROP_WINDOW_CREATE_WAYLAND_WL_SURFACE_POINTER, (void *)winId()); + } else if (video_driver == "windows") { + SDL_SetPointerProperty(props, SDL_PROP_WINDOW_CREATE_WIN32_HWND_POINTER, (void *)winId()); + } else { + SDL_Log("QSDLCanvas does not support this video driver: %s", SDL_GetCurrentVideoDriver()); + QMessageBox::critical(nullptr, "Unknown SDL video driver", QString{"Don't know how to create SDL_Window from native handle for '%1' SDL video driver"}.arg(SDL_GetCurrentVideoDriver())); + return; + } + SDL_SetBooleanProperty(props, SDL_PROP_WINDOW_CREATE_HIDDEN_BOOLEAN, false); + SDL_SetBooleanProperty(props, SDL_PROP_WINDOW_CREATE_OPENGL_BOOLEAN, true); + SDL_SetBooleanProperty(props, SDL_PROP_WINDOW_CREATE_BORDERLESS_BOOLEAN, true); + if (m_updateCanvasSize) { + QSize canvasSize = size(); + SDL_SetNumberProperty(props, SDL_PROP_WINDOW_CREATE_WIDTH_NUMBER, canvasSize.width()); + SDL_SetNumberProperty(props, SDL_PROP_WINDOW_CREATE_HEIGHT_NUMBER, canvasSize.height()); + } + m_window = SDL_CreateWindowWithProperties(props); + SDL_DestroyProperties(props); + } + if (!m_window) { + SDL_Log("Failed to create SDL window (%s)", SDL_GetError()); + return; + } + + recreateRenderer(); + + int ms = (int)(1000.0f / framerate()); + connect(&m_drawTimer, SIGNAL(timeout()), this, SLOT(onUpdate())); + m_drawTimer.start(ms); +} + +void QSDLCanvas::recreateRenderer() +{ + if (m_rendererNameChanged) { + m_rendererNameChanged = false; + QByteArray name_byte_array = m_rendererName.toUtf8(); + const char *name_bytes = name_byte_array.data(); + if (m_rendererName.length() == 0) { + name_bytes = nullptr; + } + if (m_renderer) { + SDL_DestroyRenderer(m_renderer); + m_renderer = nullptr; + } + m_renderer = SDL_CreateRenderer(m_window, name_bytes); + if (!m_renderer) { + SDL_Log("Couldn't create SDL renderer(%s): %s", name_bytes, SDL_GetError()); + return; + } + SDL_Log("Render driver changed to %s", name_bytes); + } +} diff --git a/test/qt6/QSDLCanvas.h b/test/qt6/QSDLCanvas.h new file mode 100644 index 0000000000000..4ae843a3ff461 --- /dev/null +++ b/test/qt6/QSDLCanvas.h @@ -0,0 +1,64 @@ +/* + Copyright (C) 1997-2026 Sam Lantinga + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely. +*/ +#include +#include +#include + +#include + +class QSDLCanvas : public QWidget +{ + Q_OBJECT + +public: + QSDLCanvas(QWidget *parent = nullptr, bool updateCanvasSize = false); + virtual ~QSDLCanvas(); + +public Q_SLOTS: + void changeRenderer(const QString &name); + void drawRendererName(bool draw); + +protected Q_SLOTS: + virtual void onUpdate(); + +protected: + + void recreateRenderer(); + + // Override this to set the update framerate, this is called when the widget is shown + virtual float framerate() { return 60.0f; } + + // Override this to draw to the canvas + virtual void onDraw(); + + // Override this to handle SDL events + // This example assumes there is only one canvas and it should process all SDL events + virtual void onEvent(SDL_Event *event); + + virtual void resizeEvent(QResizeEvent *event) override; + + // Handle the Qt show event + virtual void showEvent(QShowEvent *event) override; + + // Let Qt know that we're going to do our own drawing + virtual QPaintEngine *paintEngine() const override { return nullptr; } + +protected: + SDL_Window *m_window = nullptr; + SDL_Renderer *m_renderer = nullptr; + QTimer m_drawTimer; + + bool m_updateCanvasSize = false; + bool m_draw_renderer_name = true; + QString m_rendererName; + bool m_rendererNameChanged = true; +}; diff --git a/test/qt6/main.cpp b/test/qt6/main.cpp new file mode 100644 index 0000000000000..1e0ca28e8ae5a --- /dev/null +++ b/test/qt6/main.cpp @@ -0,0 +1,74 @@ +/* + Copyright (C) 1997-2026 Sam Lantinga + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely. +*/ + +#include "mainwindow.h" + +#include +#include +#include + +#include + +int main(int argc, char *argv[]) +{ + QApplication app(argc, argv); + QCoreApplication::setApplicationName("sdl3-qt-demo"); + QCoreApplication::setApplicationVersion(SDL_GetRevision()); + + QCommandLineParser parser; + parser.setApplicationDescription("SDL3 example demonstrating Qt6 integration"); + parser.addHelpOption(); + parser.addVersionOption(); + + parser.process(app); + + QString app_platform_name = app.platformName(); + bool updateCanvasSize = false; + + if (app_platform_name == "xcb") { + SDL_SetHintWithPriority(SDL_HINT_VIDEO_DRIVER, "x11", SDL_HINT_OVERRIDE); +#if defined(QT_FEATURE_wayland) && QT_CONFIG(wayland) == 1 + } else if (app_platform_name == "wayland" || app_platform_name == "wayland-egl") { + /* Get the wl_display object from Qt */ + QNativeInterface::QWaylandApplication *qtWlApp = app.nativeInterface(); + SDL_SetPointerProperty(SDL_GetGlobalProperties(), SDL_PROP_GLOBAL_VIDEO_WAYLAND_WL_DISPLAY_POINTER, qtWlApp->display()); + SDL_SetHintWithPriority(SDL_HINT_VIDEO_DRIVER, "wayland", SDL_HINT_OVERRIDE); + + // Wayland requires explicitly setting and updating the canvas size. + updateCanvasSize = true; +#endif + } else if (app_platform_name == "windows") { + SDL_SetHintWithPriority(SDL_HINT_VIDEO_DRIVER, "windows", SDL_HINT_OVERRIDE); + } else if (app_platform_name == "qnx") { + SDL_SetHintWithPriority(SDL_HINT_VIDEO_DRIVER, "qnx", SDL_HINT_OVERRIDE); + } else if (app_platform_name == "cocoa") { + SDL_SetHintWithPriority(SDL_HINT_VIDEO_DRIVER, "cocoa", SDL_HINT_OVERRIDE); + } else if (app_platform_name == "ios") { + SDL_SetHintWithPriority(SDL_HINT_VIDEO_DRIVER, "ios", SDL_HINT_OVERRIDE); + } else { + QErrorMessage error_message; + error_message.showMessage(QString("Don't know SDL platform equivalent for qt platform: ") + app.platformName()); + app.exec(); + return 1; + } + if (!SDL_Init(SDL_INIT_VIDEO)) { + SDL_Log("SDL_Init failed (%s)", SDL_GetError()); + return 1; + } + + MainWindow window(updateCanvasSize); + window.show(); + + const int ret = app.exec(); + SDL_Quit(); + return ret; +} diff --git a/test/qt6/mainwindow.cpp b/test/qt6/mainwindow.cpp new file mode 100644 index 0000000000000..e27bd928b086b --- /dev/null +++ b/test/qt6/mainwindow.cpp @@ -0,0 +1,82 @@ +/* + Copyright (C) 1997-2026 Sam Lantinga + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely. +*/ +#include + +#include "mainwindow.h" +#include "QSDLCanvas.h" + +MainWindow::MainWindow(bool updateCanvasSize) +{ + QWidget *widget = new QWidget; + setCentralWidget(widget); + + m_canvas = new QSDLCanvas(this, updateCanvasSize); + + QVBoxLayout *layout = new QVBoxLayout; + layout->setContentsMargins(0, 4, 0, 0); + layout->addWidget(m_canvas); + widget->setLayout(layout); + + createMenus(); + + setWindowTitle(tr("SDL Qt Demo")); + setMinimumSize(160, 160); + resize(640, 480); +} + +void MainWindow::createMenus() +{ + // Create "File" menu + QMenu *fileMenu = menuBar()->addMenu(tr("&File")); +#if QT_VERSION >= QT_VERSION_CHECK(6, 7, 0) + QAction *exitAct = new QAction(QIcon::fromTheme(QIcon::ThemeIcon::ApplicationExit), tr("E&xit"), this); +#else + QAction *exitAct = new QAction(tr("E&xit"), this); +#endif + exitAct->setShortcuts(QKeySequence::Quit); + connect(exitAct, &QAction::triggered, this, &QWidget::close); + fileMenu->addAction(exitAct); + + // Create Renderer menu + QMenu *rendererMenu = menuBar()->addMenu(tr("&Renderer")); + QActionGroup* renderGroup = new QActionGroup(this); + renderGroup->setExclusive(true); + { + QAction *defaultDriverAction = new QAction(tr("(default)"), this); + defaultDriverAction->setCheckable(true); + defaultDriverAction->setChecked(true); + renderGroup->addAction(defaultDriverAction); + rendererMenu->addAction(defaultDriverAction); + connect(defaultDriverAction, &QAction::triggered, [this] { m_canvas->changeRenderer(""); }); + } + int count_render_drivers = SDL_GetNumRenderDrivers(); + for (int i = 0; i < count_render_drivers; i++) { + QString driver_name = SDL_GetRenderDriver(i); + QAction *driverAction = new QAction(driver_name, this); + driverAction->setCheckable(true); + driverAction->setChecked(false); + renderGroup->addAction(driverAction); + rendererMenu->addAction(driverAction); + connect(driverAction, &QAction::triggered, [this, driver_name] { m_canvas->changeRenderer(driver_name); }); + } + rendererMenu->addSeparator(); + QAction *drawNameAction = rendererMenu->addAction(tr("Draw name")); + drawNameAction->setCheckable(true); + connect(drawNameAction, &QAction::toggled, m_canvas, &QSDLCanvas::drawRendererName); + drawNameAction->setChecked(true); + + // Create Help menu + QMenu *helpMenu = menuBar()->addMenu(tr("&Help")); + QAction *aboutQtAction = new QAction(tr("About &Qt"), this); + connect(aboutQtAction, &QAction::triggered, qApp, &QApplication::aboutQt); + helpMenu->addAction(aboutQtAction); +} diff --git a/test/qt6/mainwindow.h b/test/qt6/mainwindow.h new file mode 100644 index 0000000000000..62457fb97c98b --- /dev/null +++ b/test/qt6/mainwindow.h @@ -0,0 +1,25 @@ +/* + Copyright (C) 1997-2026 Sam Lantinga + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely. +*/ +#include + +class MainWindow : public QMainWindow +{ + Q_OBJECT + +public: + MainWindow(bool updateCanvasSize = false); + +private: + void createMenus(); + + class QSDLCanvas *m_canvas; +};