diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index 090e35ed3943..3c20713537e8 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -138,7 +138,7 @@ jobs: working-directory: build env: # Render analyzer waveform tests to an offscreen buffer - QT_QPA_PLATFORM: ${{ matrix.qt_qpa_platform }} + QT_QPA_PLATFORM: offscreen GTEST_COLOR: 1 # Only use single thread to prevent *.gcna files from overwriting each other CTEST_PARALLEL_LEVEL: 1 diff --git a/CMakeLists.txt b/CMakeLists.txt index f37af18b2244..033cb530671f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2978,23 +2978,15 @@ if(BUILD_TESTING) include(CTest) include(GoogleTest) enable_testing() - gtest_add_tests( - TARGET mixxx-test + gtest_discover_tests( + mixxx-test EXTRA_ARGS --logLevel info WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}" + PROPERTIES ENVIRONMENT "QT_QPA_PLATFORM=offscreen" TEST_LIST testsuite + DISCOVERY_MODE PRE_TEST ) - if(NOT WIN32) - # Default to offscreen rendering during tests. - # This is required if the build system like Fedora koji/mock does not - # allow to pass environment variables into the ctest macro expansion. - set_tests_properties( - ${testsuite} - PROPERTIES ENVIRONMENT "QT_QPA_PLATFORM=offscreen" - ) - endif() - if(BUILD_BENCH) # Benchmarking add_custom_target( diff --git a/src/controllers/rendering/controllerrenderingengine.cpp b/src/controllers/rendering/controllerrenderingengine.cpp index c1a2767d5f77..0a1ecf79e54b 100644 --- a/src/controllers/rendering/controllerrenderingengine.cpp +++ b/src/controllers/rendering/controllerrenderingengine.cpp @@ -48,16 +48,15 @@ ControllerRenderingEngine::ControllerRenderingEngine( switch (m_screenInfo.pixelFormat) { case QImage::Format_RGB16: m_GLDataFormat = GL_RGB; +#ifndef QT_OPENGL_ES_2 if (m_screenInfo.reversedColor) { -#ifdef QT_OPENGL_ES_2 - m_isValid = false; - kLogger.critical() << "Reversed RGB16 format is not supported in OpenGL ES"; -#else m_GLDataType = GL_UNSIGNED_SHORT_5_6_5_REV; -#endif } else { +#endif m_GLDataType = GL_UNSIGNED_SHORT_5_6_5; +#ifndef QT_OPENGL_ES_2 } +#endif break; case QImage::Format_RGB888: if (m_screenInfo.reversedColor) { @@ -220,6 +219,7 @@ void ControllerRenderingEngine::setup(std::shared_ptr qmlEngine) { } m_renderControl = std::make_unique(this); + m_renderControl->setSamples(format.samples()); m_quickWindow = std::make_unique(m_renderControl.get()); if (!qmlEngine->incubationController()) { @@ -268,10 +268,10 @@ void ControllerRenderingEngine::renderFrame() { VERIFY_OR_TERMINATE(m_offscreenSurface->isValid(), "OffscreenSurface isn't valid anymore."); VERIFY_OR_TERMINATE(m_context->isValid(), "GLContext isn't valid anymore."); - VERIFY_OR_TERMINATE(m_context->makeCurrent(m_offscreenSurface.get()), - "Couldn't make the GLContext current to the OffscreenSurface."); if (!m_fbo) { + VERIFY_OR_TERMINATE(m_context->makeCurrent(m_offscreenSurface.get()), + "Couldn't make the GLContext current to the OffscreenSurface."); ScopedTimer t(QStringLiteral("ControllerRenderingEngine::renderFrame::initFBO")); VERIFY_OR_TERMINATE( QOpenGLFramebufferObject::hasOpenGLFramebufferObjects(), @@ -295,18 +295,24 @@ void ControllerRenderingEngine::renderFrame() { m_screenInfo.size)); m_quickWindow->setGeometry(0, 0, m_screenInfo.size.width(), m_screenInfo.size.height()); + + m_context->doneCurrent(); } m_nextFrameStart = Clock::now(); - m_renderControl->beginFrame(); - if (m_pEngineThreadControl) { - m_pEngineThreadControl->pause(); + if (!m_pEngineThreadControl->pause()) { + kLogger.debug() << "Couldn't pause the GUI thread. Rescheduling frame rendering"; + QCoreApplication::postEvent(this, new QEvent(QEvent::UpdateRequest)); + return; + } } m_renderControl->polishItems(); + m_renderControl->beginFrame(); + { ScopedTimer t(QStringLiteral("ControllerRenderingEngine::renderFrame::sync")); VERIFY_OR_DEBUG_ASSERT(m_renderControl->sync()) { @@ -317,27 +323,38 @@ void ControllerRenderingEngine::renderFrame() { if (m_pEngineThreadControl) { m_pEngineThreadControl->resume(); } + + VERIFY_OR_TERMINATE(m_offscreenSurface->isValid(), "OffscreenSurface isn't valid anymore."); + VERIFY_OR_TERMINATE(m_context->makeCurrent(m_offscreenSurface.get()), + "Couldn't make the GLContext current to the OffscreenSurface."); + +#ifdef QT_OPENGL_ES_2 + // OpenGL ES doesn't support extended format and type when reading pixel and + // only support GL_RGBA/GL_UNSIGNED_BYTE On this platform, we fallback to Qt + // for the pixel transformation, using QImage conversion capabilities + QImage fboImage(m_screenInfo.size, QImage::Format_RGBA8888); +#else QImage fboImage(m_screenInfo.size, m_screenInfo.pixelFormat); +#endif VERIFY_OR_DEBUG_ASSERT(m_fbo->bind()) { kLogger.warning() << "Couldn't bind the FBO."; } GLenum glError; + // Flush any remaining GL errors. + while ((glError = m_context->functions()->glGetError()) != GL_NO_ERROR) { + kLogger.debug() << "Retrieved a previously unhandled GL error: " << glError; + } +#ifndef QT_OPENGL_ES_2 m_context->functions()->glFlush(); glError = m_context->functions()->glGetError(); - VERIFY_OR_TERMINATE(glError == GL_NO_ERROR, "GLError: " << glError); + VERIFY_OR_TERMINATE(glError == GL_NO_ERROR, "GLError after glFlush: " << glError); if (static_cast(m_screenInfo.endian) != std::endian::native) { -#ifdef QT_OPENGL_ES_2 - kLogger.critical() - << "Screen endianness mismatches native endianness, but OpenGL " - "ES does not let us specify a reverse pixel store order. " - "This will likely lead to invalid colors."; -#else m_context->functions()->glPixelStorei(GL_PACK_SWAP_BYTES, GL_TRUE); -#endif } glError = m_context->functions()->glGetError(); - VERIFY_OR_TERMINATE(glError == GL_NO_ERROR, "GLError: " << glError); + VERIFY_OR_TERMINATE(glError == GL_NO_ERROR, "GLError after glPixelStorei: " << glError); +#endif QDateTime timestamp = QDateTime::currentDateTime(); m_renderControl->render(); @@ -353,12 +370,17 @@ void ControllerRenderingEngine::renderFrame() { 0, m_screenInfo.size.width(), m_screenInfo.size.height(), +#ifndef QT_OPENGL_ES_2 m_GLDataFormat, m_GLDataType, +#else + GL_RGBA, + GL_UNSIGNED_BYTE, +#endif fboImage.bits()); } glError = m_context->functions()->glGetError(); - VERIFY_OR_TERMINATE(glError == GL_NO_ERROR, "GLError: " << glError); + VERIFY_OR_TERMINATE(glError == GL_NO_ERROR, "GLError after glReadPixels: " << glError); VERIFY_OR_DEBUG_ASSERT(!fboImage.isNull()) { kLogger.warning() << "Screen frame is null!"; } @@ -366,6 +388,54 @@ void ControllerRenderingEngine::renderFrame() { kLogger.debug() << "Couldn't release the FBO."; } + m_context->doneCurrent(); + +#ifdef QT_OPENGL_ES_2 + fboImage.convertTo(m_screenInfo.pixelFormat); + + // OpenGL ES doesn't support extended reverse format (suffixed with _REV) so + // we use QImage function for this + if (static_cast(m_screenInfo.endian) != std::endian::native) { + fboImage.rgbSwap(); + } + + // OpenGL ES doesn't support explicit endianness (GL_PACK_SWAP_BYTES) se we + // use Qt helper function to convert the pixel buffer. Only 16 and 32 bit + // pixel format are supported currently. + switch (static_cast(m_screenInfo.endian)) { + case std::endian::big: + switch (m_screenInfo.pixelFormat) { + case QImage::Format_RGB16: + qToBigEndian(fboImage.bits(), fboImage.sizeInBytes() / 2, fboImage.bits()); + break; + case QImage::Format_RGBA8888: + qToBigEndian(fboImage.bits(), fboImage.sizeInBytes() / 4, fboImage.bits()); + break; + default: + kLogger.critical() + << "Screen endianness mismatches native endianness, but OpenGL " + "ES does not let us specify a reverse pixel store order. " + "This will likely lead to invalid colors."; + } + break; + case std::endian::little: + switch (m_screenInfo.pixelFormat) { + case QImage::Format_RGB16: + qToLittleEndian(fboImage.bits(), fboImage.sizeInBytes(), fboImage.bits()); + break; + case QImage::Format_RGBA8888: + qToLittleEndian(fboImage.bits(), fboImage.sizeInBytes(), fboImage.bits()); + break; + default: + kLogger.critical() + << "Screen endianness mismatches native endianness, but OpenGL " + "ES does not let us specify a reverse pixel store order. " + "This will likely lead to invalid colors."; + } + break; + } +#endif + #if QT_VERSION >= QT_VERSION_CHECK(6, 9, 0) fboImage.flip(Qt::Vertical); #else @@ -373,8 +443,6 @@ void ControllerRenderingEngine::renderFrame() { #endif emit frameRendered(m_screenInfo, fboImage.copy(), timestamp); - - m_context->doneCurrent(); } bool ControllerRenderingEngine::stop() { diff --git a/src/test/stemcontrolobjecttest.cpp b/src/test/stemcontrolobjecttest.cpp index eb5becc81f3c..f26c43e7fbbe 100644 --- a/src/test/stemcontrolobjecttest.cpp +++ b/src/test/stemcontrolobjecttest.cpp @@ -5,11 +5,23 @@ #include #include "control/pollingcontrolproxy.h" +#include "gtest/gtest.h" #include "mixxxtest.h" #include "test/signalpathtest.h" -class StemControlTest : public BaseSignalPathTest { - protected: +#define STEM_FILE QStringLiteral("stems/sin_%1.stem.mp4").arg(QString::fromStdString(GetParam())) + +namespace { +const std::vector supportedCodecs = { +#if !defined(Q_OS_WIN) + "AAC_256kbps_VBR", +#endif + "ALAC_24bit"}; +} // namespace + +class StemControlFixture : public BaseSignalPathTest, + public ::testing::WithParamInterface { + public: QString getGroupForStem(QStringView deckGroup, int stemNr) { DEBUG_ASSERT(deckGroup.endsWith(QChar(']')) && stemNr <= 4); return deckGroup.chopped(1) + QStringLiteral("_Stem") + QChar('0' + stemNr) + QChar(']'); @@ -41,7 +53,7 @@ class StemControlTest : public BaseSignalPathTest { m_pEffectsManager->addStem(stemHandleGroup); } - const QString kStemFileLocationTest = getTestDir().filePath("stems/test.stem.mp4"); + const QString kStemFileLocationTest = getTestDir().filePath(STEM_FILE); TrackPointer pStemFile(Track::newTemporary(kStemFileLocationTest)); loadTrack(m_pMixerDeck1, pStemFile); @@ -142,7 +154,7 @@ class StemControlTest : public BaseSignalPathTest { std::unique_ptr m_pStemCount; }; -TEST_F(StemControlTest, StemCount) { +TEST_P(StemControlFixture, StemCount) { EXPECT_EQ(m_pStemCount->get(), 4.0); QString kTrackLocationTest = getTestDir().filePath(QStringLiteral("sine-30.wav")); @@ -151,14 +163,14 @@ TEST_F(StemControlTest, StemCount) { EXPECT_EQ(m_pStemCount->get(), 0.0); - kTrackLocationTest = getTestDir().filePath("stems/test.stem.mp4"); + kTrackLocationTest = getTestDir().filePath(STEM_FILE); pTrack = Track::newTemporary(kTrackLocationTest); loadTrack(m_pMixerDeck1, pTrack); EXPECT_EQ(m_pStemCount->get(), 4.0); } -TEST_F(StemControlTest, StemColor) { +TEST_P(StemControlFixture, StemColor) { EXPECT_EQ(m_pStem1Color->get(), 0xfd << 16 | 0x4a << 8 | 0x4a); EXPECT_EQ(m_pStem2Color->get(), 0xff << 16 | 0xff << 8 | 0x00); EXPECT_EQ(m_pStem3Color->get(), 0x00 << 16 | 0xe8 << 8 | 0xe8); @@ -173,7 +185,7 @@ TEST_F(StemControlTest, StemColor) { EXPECT_EQ(m_pStem3Color->get(), -1.0); EXPECT_EQ(m_pStem4Color->get(), -1.0); - kTrackLocationTest = getTestDir().filePath("stems/test.stem.mp4"); + kTrackLocationTest = getTestDir().filePath(STEM_FILE); pTrack = Track::newTemporary(kTrackLocationTest); loadTrack(m_pMixerDeck1, pTrack); @@ -183,7 +195,7 @@ TEST_F(StemControlTest, StemColor) { EXPECT_EQ(m_pStem4Color->get(), 0xad << 16 | 0x65 << 8 | 0xff); } -TEST_F(StemControlTest, DISABLED_Volume) { +TEST_P(StemControlFixture, Volume) { m_pChannel1->getEngineBuffer()->queueNewPlaypos( mixxx::audio::FramePos{0}, EngineBuffer::SEEK_STANDARD); m_pPlay->set(1.0); @@ -231,7 +243,7 @@ TEST_F(StemControlTest, DISABLED_Volume) { QStringLiteral("StemVolumeControlFull")); } -TEST_F(StemControlTest, VolumeResetOnLoad) { +TEST_P(StemControlFixture, VolumeResetOnLoad) { m_pStem1Volume->set(0.1); m_pStem2Volume->set(0.2); m_pStem3Volume->set(0.3); @@ -270,7 +282,7 @@ TEST_F(StemControlTest, VolumeResetOnLoad) { EXPECT_EQ(m_pStem4Mute->get(), 0.0); } -TEST_F(StemControlTest, DISABLED_Mute) { +TEST_P(StemControlFixture, Mute) { m_pChannel1->getEngineBuffer()->queueNewPlaypos( mixxx::audio::FramePos{0}, EngineBuffer::SEEK_STANDARD); m_pPlay->set(1.0); @@ -316,3 +328,11 @@ TEST_F(StemControlTest, DISABLED_Mute) { assertBufferMatchesReference(m_pEngineMixer->getMainBuffer(), QStringLiteral("StemMuteControlFull")); } + +INSTANTIATE_TEST_SUITE_P( + StemControlTest, + StemControlFixture, + ::testing::ValuesIn(supportedCodecs), + [](const testing::TestParamInfo& info) { + return info.param; + }); diff --git a/src/test/stems/test.stem.mp4 b/src/test/stems/sin_AAC_256kbps_VBR.stem.mp4 similarity index 100% rename from src/test/stems/test.stem.mp4 rename to src/test/stems/sin_AAC_256kbps_VBR.stem.mp4 diff --git a/src/test/stems/sin_ALAC_24bit.stem.mp4 b/src/test/stems/sin_ALAC_24bit.stem.mp4 new file mode 100644 index 000000000000..288418a40943 Binary files /dev/null and b/src/test/stems/sin_ALAC_24bit.stem.mp4 differ diff --git a/src/test/stemtest.cpp b/src/test/stemtest.cpp index ae7d2bf79d0f..3179941b7c4f 100644 --- a/src/test/stemtest.cpp +++ b/src/test/stemtest.cpp @@ -9,8 +9,16 @@ using namespace mixxx; +#define STEM_FILE QStringLiteral("stems/sin_%1.stem.mp4").arg(QString::fromStdString(GetParam())) + namespace { +const std::vector supportedCodecs = { +#if !defined(Q_OS_WIN) + "AAC_256kbps_VBR", +#endif + "ALAC_24bit"}; + const QList kStemFiles = { "01-drum.wav", "02-bass.wav", @@ -18,7 +26,7 @@ const QList kStemFiles = { "04-vocal.wav", }; -class StemTest : public MixxxTest { +class StemFixture : public MixxxTest, public ::testing::WithParamInterface { protected: void SetUp() override { ASSERT_TRUE(SoundSourceProxy::isFileTypeSupported("stem.mp4") || @@ -26,8 +34,8 @@ class StemTest : public MixxxTest { } }; -TEST_F(StemTest, FetchStemInfo) { - TrackPointer pTrack(Track::newTemporary(getTestDir().filePath("stems/test.stem.mp4"))); +TEST_P(StemFixture, FetchStemInfo) { + TrackPointer pTrack(Track::newTemporary(getTestDir().filePath(STEM_FILE))); mixxx::AudioSource::OpenParams config; config.setChannelCount(mixxx::audio::ChannelCount(2)); @@ -42,7 +50,7 @@ TEST_F(StemTest, FetchStemInfo) { ASSERT_EQ(stemInfo.at(3), StemInfo("Vox", QColor(0xad, 0x65, 0xff))); // #ad65ff } -TEST_F(StemTest, FetchStemEmptyInfo) { +TEST_P(StemFixture, FetchStemEmptyInfo) { TrackPointer pTrack(Track::newTemporary( getTestDir().filePath("stems/test_missing_stem_details.stem.mp4"))); @@ -59,10 +67,10 @@ TEST_F(StemTest, FetchStemEmptyInfo) { ASSERT_EQ(stemInfo.at(3), StemInfo("Stem #4", QColor(0x56, 0xB4, 0xE9))); } -TEST_F(StemTest, ReadMainMix) { +TEST_P(StemFixture, ReadMainMix) { SoundSourceFFmpeg sourceMainMix( QUrl::fromLocalFile(getTestDir().filePath("stems/mainmix.wav"))); - SoundSourceSTEM sourceStem(QUrl::fromLocalFile(getTestDir().filePath("stems/test.stem.mp4"))); + SoundSourceSTEM sourceStem(QUrl::fromLocalFile(getTestDir().filePath(STEM_FILE))); mixxx::AudioSource::OpenParams config; config.setChannelCount(mixxx::audio::ChannelCount(2)); @@ -96,14 +104,14 @@ TEST_F(StemTest, ReadMainMix) { EXPECT_TRUE(0 == std::memcmp(buffer1.data(), buffer1.data(), sizeof(buffer1))); } -TEST_F(StemTest, ReadEachStem) { +TEST_P(StemFixture, ReadEachStem) { int stemIdx = 0; for (auto& stem : kStemFiles) { SoundSourceFFmpeg sourceStandaloneStem( QUrl::fromLocalFile(getTestDir().filePath("stems/" + stem))); SoundSourceSingleSTEM sourceStem( QUrl::fromLocalFile( - getTestDir().filePath("stems/test.stem.mp4")), + getTestDir().filePath(STEM_FILE)), stemIdx++); mixxx::AudioSource::OpenParams config; @@ -139,8 +147,8 @@ TEST_F(StemTest, ReadEachStem) { } } -TEST_F(StemTest, OpenStem) { - SoundSourceSTEM sourceStem(QUrl::fromLocalFile(getTestDir().filePath("stems/test.stem.mp4"))); +TEST_P(StemFixture, OpenStem) { + SoundSourceSTEM sourceStem(QUrl::fromLocalFile(getTestDir().filePath(STEM_FILE))); mixxx::AudioSource::OpenParams config; config.setChannelCount(mixxx::audio::ChannelCount(8)); @@ -152,4 +160,12 @@ TEST_F(StemTest, OpenStem) { sourceStem.getSignalInfo()); } +INSTANTIATE_TEST_SUITE_P( + StemTest, + StemFixture, + ::testing::ValuesIn(supportedCodecs), + [](const testing::TestParamInfo& info) { + return info.param; + }); + } // namespace