diff --git a/.travis/linux-frozen/docker.sh b/.travis/linux-frozen/docker.sh index eef05d0b2..88db834c8 100755 --- a/.travis/linux-frozen/docker.sh +++ b/.travis/linux-frozen/docker.sh @@ -8,10 +8,14 @@ apt-get install -y build-essential wget git python-launchpadlib ccache # Install specific versions of packages with their dependencies # The apt repositories remove older versions regularly, so we can't use # apt-get and have to pull the packages directly from the archives. +# qt5-qmltooling-plugins and qtdeclarative5-dev are required for qtmultimedia5-dev /citra/.travis/linux-frozen/install_package.py \ libsdl2-dev 2.0.7+dfsg1-3ubuntu1 bionic \ qtbase5-dev 5.9.3+dfsg-0ubuntu2 bionic \ libqt5opengl5-dev 5.9.3+dfsg-0ubuntu2 bionic \ + qt5-qmltooling-plugins 5.9.3-0ubuntu1 bionic \ + qtdeclarative5-dev 5.9.3-0ubuntu1 bionic \ + qtmultimedia5-dev 5.9.3-0ubuntu3 bionic \ libicu57 57.1-6ubuntu0.2 bionic # Get a recent version of CMake diff --git a/.travis/linux/docker.sh b/.travis/linux/docker.sh index 703817d9f..a1942027d 100755 --- a/.travis/linux/docker.sh +++ b/.travis/linux/docker.sh @@ -3,7 +3,7 @@ cd /citra apt-get update -apt-get install -y build-essential libsdl2-dev qtbase5-dev libqt5opengl5-dev qttools5-dev qttools5-dev-tools wget git ccache +apt-get install -y build-essential libsdl2-dev qtbase5-dev libqt5opengl5-dev qtmultimedia5-dev qttools5-dev qttools5-dev-tools wget git ccache # Get a recent version of CMake wget https://cmake.org/files/v3.10/cmake-3.10.1-Linux-x86_64.sh diff --git a/CMakeLists.txt b/CMakeLists.txt index 9c78b70aa..195c52503 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -225,7 +225,7 @@ endif() if (ENABLE_QT) if (CITRA_USE_BUNDLED_QT) if (MSVC14 AND ARCHITECTURE_x86_64) - set(QT_VER qt-5.7-msvc2015_64) + set(QT_VER qt-5.10.0-msvc2017_64) else() message(FATAL_ERROR "No bundled Qt binaries for your toolchain. Disable CITRA_USE_BUNDLED_QT and provide your own.") endif() @@ -241,7 +241,7 @@ if (ENABLE_QT) set(QT_PREFIX_HINT) endif() - find_package(Qt5 REQUIRED COMPONENTS Widgets OpenGL ${QT_PREFIX_HINT}) + find_package(Qt5 REQUIRED COMPONENTS Widgets OpenGL Multimedia ${QT_PREFIX_HINT}) if (ENABLE_QT_TRANSLATION) find_package(Qt5 REQUIRED COMPONENTS LinguistTools ${QT_PREFIX_HINT}) diff --git a/CMakeModules/CopyCitraQt5Deps.cmake b/CMakeModules/CopyCitraQt5Deps.cmake index f7a6294f7..fcbf174ee 100644 --- a/CMakeModules/CopyCitraQt5Deps.cmake +++ b/CMakeModules/CopyCitraQt5Deps.cmake @@ -3,8 +3,10 @@ function(copy_citra_Qt5_deps target_dir) set(DLL_DEST "${CMAKE_BINARY_DIR}/bin/$/") set(Qt5_DLL_DIR "${Qt5_DIR}/../../../bin") set(Qt5_PLATFORMS_DIR "${Qt5_DIR}/../../../plugins/platforms/") + set(Qt5_MEDIASERVICE_DIR "${Qt5_DIR}/../../../plugins/mediaservice/") set(Qt5_STYLES_DIR "${Qt5_DIR}/../../../plugins/styles/") set(PLATFORMS ${DLL_DEST}platforms/) + set(MEDIASERVICE ${DLL_DEST}mediaservice/) set(STYLES ${DLL_DEST}styles/) windows_copy_files(${target_dir} ${Qt5_DLL_DIR} ${DLL_DEST} icudt*.dll @@ -14,7 +16,13 @@ function(copy_citra_Qt5_deps target_dir) Qt5Gui$<$:d>.* Qt5OpenGL$<$:d>.* Qt5Widgets$<$:d>.* + Qt5Multimedia$<$:d>.* + Qt5Network$<$:d>.* ) windows_copy_files(citra-qt ${Qt5_PLATFORMS_DIR} ${PLATFORMS} qwindows$<$:d>.*) + windows_copy_files(citra-qt ${Qt5_MEDIASERVICE_DIR} ${MEDIASERVICE} + dsengine$<$:d>.* + wmfengine$<$:d>.* + ) windows_copy_files(citra-qt ${Qt5_STYLES_DIR} ${STYLES} qwindowsvistastyle$<$:d>.*) endfunction(copy_citra_Qt5_deps) diff --git a/appveyor.yml b/appveyor.yml index 924e857e8..a6e66881d 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -110,6 +110,7 @@ after_build: mkdir $RELEASE_DIST mkdir $RELEASE_DIST/platforms + mkdir $RELEASE_DIST/mediaservice mkdir $RELEASE_DIST/styles # copy the compiled binaries and other release files to the release folder @@ -131,6 +132,9 @@ after_build: # copy the qt windows plugin dll to platforms Copy-Item -path "C:/msys64/mingw64/share/qt5/plugins/platforms/qwindows.dll" -force -destination "$RELEASE_DIST/platforms" + # copy the qt mediaservice plugin dll + Copy-Item -path "C:/msys64/mingw64/share/qt5/plugins/mediaservice/dsengine.dll" -force -destination "$RELEASE_DIST/mediaservice" + # copy the qt windows vista style dll to platforms Copy-Item -path "C:/msys64/mingw64/share/qt5/plugins/styles/qwindowsvistastyle.dll" -force -destination "$RELEASE_DIST/styles" diff --git a/src/citra_qt/CMakeLists.txt b/src/citra_qt/CMakeLists.txt index 7d83143ee..4e0fcdbeb 100644 --- a/src/citra_qt/CMakeLists.txt +++ b/src/citra_qt/CMakeLists.txt @@ -9,11 +9,21 @@ add_executable(citra-qt aboutdialog.h bootmanager.cpp bootmanager.h + camera/camera_util.cpp + camera/camera_util.h + camera/still_image_camera.cpp + camera/still_image_camera.h + camera/qt_camera_factory.cpp + camera/qt_camera_factory.h + camera/qt_multimedia_camera.cpp + camera/qt_multimedia_camera.h citra-qt.rc configuration/config.cpp configuration/config.h configuration/configure_audio.cpp configuration/configure_audio.h + configuration/configure_camera.cpp + configuration/configure_camera.h configuration/configure_debug.cpp configuration/configure_debug.h configuration/configure_dialog.cpp @@ -92,6 +102,7 @@ add_executable(citra-qt set(UIS configuration/configure.ui configuration/configure_audio.ui + configuration/configure_camera.ui configuration/configure_debug.ui configuration/configure_general.ui configuration/configure_graphics.ui @@ -180,7 +191,7 @@ endif() create_target_directory_groups(citra-qt) target_link_libraries(citra-qt PRIVATE audio_core common core input_common network video_core) -target_link_libraries(citra-qt PRIVATE Boost::boost glad nihstro-headers Qt5::OpenGL Qt5::Widgets) +target_link_libraries(citra-qt PRIVATE Boost::boost glad nihstro-headers Qt5::OpenGL Qt5::Widgets Qt5::Multimedia) target_link_libraries(citra-qt PRIVATE ${PLATFORM_LIBRARIES} Threads::Threads) if(UNIX AND NOT APPLE) diff --git a/src/citra_qt/camera/camera_util.cpp b/src/citra_qt/camera/camera_util.cpp new file mode 100644 index 000000000..1a3e3b1b0 --- /dev/null +++ b/src/citra_qt/camera/camera_util.cpp @@ -0,0 +1,228 @@ +// Copyright 2018 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include +#include +#include "citra_qt/camera/camera_util.h" +#include "common/math_util.h" +#include "core/frontend/camera/factory.h" +#include "core/frontend/camera/interface.h" + +namespace CameraUtil { + +// The following are data tables for RGB -> YUV conversions. +namespace YuvTable { + +constexpr std::array Y_R = { + 53, 53, 53, 54, 54, 54, 55, 55, 55, 56, 56, 56, 56, 57, 57, 57, 58, 58, 58, + 59, 59, 59, 59, 60, 60, 60, 61, 61, 61, 62, 62, 62, 62, 63, 63, 63, 64, 64, + 64, 65, 65, 65, 65, 66, 66, 66, 67, 67, 67, 67, 68, 68, 68, 69, 69, 69, 70, + 70, 70, 70, 71, 71, 71, 72, 72, 72, 73, 73, 73, 73, 74, 74, 74, 75, 75, 75, + 76, 76, 76, 76, 77, 77, 77, 78, 78, 78, 79, 79, 79, 79, 80, 80, 80, 81, 81, + 81, 82, 82, 82, 82, 83, 83, 83, 84, 84, 84, 85, 85, 85, 85, 86, 86, 86, 87, + 87, 87, 87, 88, 88, 88, 89, 89, 89, 90, 90, 90, 90, 91, 91, 91, 92, 92, 92, + 93, 93, 93, 93, 94, 94, 94, 95, 95, 95, 96, 96, 96, 96, 97, 97, 97, 98, 98, + 98, 99, 99, 99, 99, 100, 100, 100, 101, 101, 101, 102, 102, 102, 102, 103, 103, 103, 104, + 104, 104, 105, 105, 105, 105, 106, 106, 106, 107, 107, 107, 108, 108, 108, 108, 109, 109, 109, + 110, 110, 110, 110, 111, 111, 111, 112, 112, 112, 113, 113, 113, 113, 114, 114, 114, 115, 115, + 115, 116, 116, 116, 116, 117, 117, 117, 118, 118, 118, 119, 119, 119, 119, 120, 120, 120, 121, + 121, 121, 122, 122, 122, 122, 123, 123, 123, 124, 124, 124, 125, 125, 125, 125, 126, 126, 126, + 127, 127, 127, 128, 128, 128, 128, 129, 129, +}; + +constexpr std::array Y_G = { + -79, -79, -78, -78, -77, -77, -76, -75, -75, -74, -74, -73, -72, -72, -71, -71, -70, -70, -69, + -68, -68, -67, -67, -66, -65, -65, -64, -64, -63, -62, -62, -61, -61, -60, -60, -59, -58, -58, + -57, -57, -56, -55, -55, -54, -54, -53, -52, -52, -51, -51, -50, -50, -49, -48, -48, -47, -47, + -46, -45, -45, -44, -44, -43, -42, -42, -41, -41, -40, -40, -39, -38, -38, -37, -37, -36, -35, + -35, -34, -34, -33, -33, -32, -31, -31, -30, -30, -29, -28, -28, -27, -27, -26, -25, -25, -24, + -24, -23, -23, -22, -21, -21, -20, -20, -19, -18, -18, -17, -17, -16, -15, -15, -14, -14, -13, + -13, -12, -11, -11, -10, -10, -9, -8, -8, -7, -7, -6, -5, -5, -4, -4, -3, -3, -2, + -1, -1, 0, 0, 0, 1, 1, 2, 2, 3, 4, 4, 5, 5, 6, 6, 7, 8, 8, + 9, 9, 10, 11, 11, 12, 12, 13, 13, 14, 15, 15, 16, 16, 17, 18, 18, 19, 19, + 20, 21, 21, 22, 22, 23, 23, 24, 25, 25, 26, 26, 27, 28, 28, 29, 29, 30, 31, + 31, 32, 32, 33, 33, 34, 35, 35, 36, 36, 37, 38, 38, 39, 39, 40, 41, 41, 42, + 42, 43, 43, 44, 45, 45, 46, 46, 47, 48, 48, 49, 49, 50, 50, 51, 52, 52, 53, + 53, 54, 55, 55, 56, 56, 57, 58, 58, 59, 59, 60, 60, 61, 62, 62, 63, 63, 64, + 65, 65, 66, 66, 67, 68, 68, 69, 69, +}; + +constexpr std::array Y_B = { + 25, 25, 26, 26, 26, 26, 26, 26, 26, 26, 26, 27, 27, 27, 27, 27, 27, 27, 27, 27, 28, 28, 28, 28, + 28, 28, 28, 28, 28, 29, 29, 29, 29, 29, 29, 29, 29, 30, 30, 30, 30, 30, 30, 30, 30, 30, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 32, 32, 32, 32, 32, 32, 32, 32, 32, 33, 33, 33, 33, 33, 33, 33, 33, + 34, 34, 34, 34, 34, 34, 34, 34, 34, 35, 35, 35, 35, 35, 35, 35, 35, 35, 36, 36, 36, 36, 36, 36, + 36, 36, 36, 37, 37, 37, 37, 37, 37, 37, 37, 38, 38, 38, 38, 38, 38, 38, 38, 38, 39, 39, 39, 39, + 39, 39, 39, 39, 39, 40, 40, 40, 40, 40, 40, 40, 40, 40, 41, 41, 41, 41, 41, 41, 41, 41, 41, 42, + 42, 42, 42, 42, 42, 42, 42, 43, 43, 43, 43, 43, 43, 43, 43, 43, 44, 44, 44, 44, 44, 44, 44, 44, + 44, 45, 45, 45, 45, 45, 45, 45, 45, 45, 46, 46, 46, 46, 46, 46, 46, 46, 47, 47, 47, 47, 47, 47, + 47, 47, 47, 48, 48, 48, 48, 48, 48, 48, 48, 48, 49, 49, 49, 49, 49, 49, 49, 49, 49, 50, 50, 50, + 50, 50, 50, 50, 50, 51, 51, 51, 51, 51, 51, 51, 51, 51, 52, 52, 52, 52, 52, 52, 52, 52, 52, 53, + 53, 53, 53, 53, 53, 53, 53, 53, 54, 54, 54, 54, 54, 54, 54, 54, +}; + +static constexpr int Y(int r, int g, int b) { + return Y_R[r] + Y_G[g] + Y_B[b]; +} + +constexpr std::array U_R = { + 30, 30, 30, 30, 30, 30, 31, 31, 31, 31, 31, 32, 32, 32, 32, 32, 32, 33, 33, 33, 33, 33, 33, 34, + 34, 34, 34, 34, 34, 35, 35, 35, 35, 35, 35, 36, 36, 36, 36, 36, 36, 37, 37, 37, 37, 37, 37, 38, + 38, 38, 38, 38, 38, 39, 39, 39, 39, 39, 39, 40, 40, 40, 40, 40, 40, 41, 41, 41, 41, 41, 41, 42, + 42, 42, 42, 42, 42, 43, 43, 43, 43, 43, 43, 44, 44, 44, 44, 44, 45, 45, 45, 45, 45, 45, 46, 46, + 46, 46, 46, 46, 47, 47, 47, 47, 47, 47, 48, 48, 48, 48, 48, 48, 49, 49, 49, 49, 49, 49, 50, 50, + 50, 50, 50, 50, 51, 51, 51, 51, 51, 51, 52, 52, 52, 52, 52, 52, 53, 53, 53, 53, 53, 53, 54, 54, + 54, 54, 54, 54, 55, 55, 55, 55, 55, 55, 56, 56, 56, 56, 56, 56, 57, 57, 57, 57, 57, 57, 58, 58, + 58, 58, 58, 59, 59, 59, 59, 59, 59, 60, 60, 60, 60, 60, 60, 61, 61, 61, 61, 61, 61, 62, 62, 62, + 62, 62, 62, 63, 63, 63, 63, 63, 63, 64, 64, 64, 64, 64, 64, 65, 65, 65, 65, 65, 65, 66, 66, 66, + 66, 66, 66, 67, 67, 67, 67, 67, 67, 68, 68, 68, 68, 68, 68, 69, 69, 69, 69, 69, 69, 70, 70, 70, + 70, 70, 70, 71, 71, 71, 71, 71, 72, 72, 72, 72, 72, 72, 73, 73, +}; + +constexpr std::array U_G = { + -45, -44, -44, -44, -43, -43, -43, -42, -42, -42, -41, -41, -41, -40, -40, -40, -39, -39, -39, + -38, -38, -38, -37, -37, -37, -36, -36, -36, -35, -35, -35, -34, -34, -34, -33, -33, -33, -32, + -32, -32, -31, -31, -31, -30, -30, -30, -29, -29, -29, -28, -28, -28, -27, -27, -27, -26, -26, + -26, -25, -25, -25, -24, -24, -24, -23, -23, -23, -22, -22, -22, -21, -21, -21, -20, -20, -20, + -19, -19, -19, -18, -18, -18, -17, -17, -17, -16, -16, -16, -15, -15, -15, -14, -14, -14, -14, + -13, -13, -13, -12, -12, -12, -11, -11, -11, -10, -10, -10, -9, -9, -9, -8, -8, -8, -7, + -7, -7, -6, -6, -6, -5, -5, -5, -4, -4, -4, -3, -3, -3, -2, -2, -2, -1, -1, + -1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 2, 2, 2, 3, 3, 3, 4, 4, 4, + 5, 5, 5, 6, 6, 6, 7, 7, 7, 8, 8, 8, 9, 9, 9, 10, 10, 10, 11, + 11, 11, 12, 12, 12, 13, 13, 13, 14, 14, 14, 15, 15, 15, 16, 16, 16, 17, 17, + 17, 18, 18, 18, 19, 19, 19, 20, 20, 20, 21, 21, 21, 22, 22, 22, 23, 23, 23, + 24, 24, 24, 25, 25, 25, 26, 26, 26, 27, 27, 27, 28, 28, 28, 29, 29, 29, 30, + 30, 30, 31, 31, 31, 32, 32, 32, 33, 33, 33, 34, 34, 34, 35, 35, 35, 36, 36, + 36, 37, 37, 37, 38, 38, 38, 39, 39, +}; + +constexpr std::array U_B = { + 113, 113, 114, 114, 115, 115, 116, 116, 117, 117, 118, 118, 119, 119, 120, 120, 121, 121, 122, + 122, 123, 123, 124, 124, 125, 125, 126, 126, 127, 127, 128, 128, 129, 129, 130, 130, 131, 131, + 132, 132, 133, 133, 134, 134, 135, 135, 136, 136, 137, 137, 138, 138, 139, 139, 140, 140, 141, + 141, 142, 142, 143, 143, 144, 144, 145, 145, 146, 146, 147, 147, 148, 148, 149, 149, 150, 150, + 151, 151, 152, 152, 153, 153, 154, 154, 155, 155, 156, 156, 157, 157, 158, 158, 159, 159, 160, + 160, 161, 161, 162, 162, 163, 163, 164, 164, 165, 165, 166, 166, 167, 167, 168, 168, 169, 169, + 170, 170, 171, 171, 172, 172, 173, 173, 174, 174, 175, 175, 176, 176, 177, 177, 178, 178, 179, + 179, 180, 180, 181, 181, 182, 182, 183, 183, 184, 184, 185, 185, 186, 186, 187, 187, 188, 188, + 189, 189, 190, 190, 191, 191, 192, 192, 193, 193, 194, 194, 195, 195, 196, 196, 197, 197, 198, + 198, 199, 199, 200, 200, 201, 201, 202, 202, 203, 203, 204, 204, 205, 205, 206, 206, 207, 207, + 208, 208, 209, 209, 210, 210, 211, 211, 212, 212, 213, 213, 214, 214, 215, 215, 216, 216, 217, + 217, 218, 218, 219, 219, 220, 220, 221, 221, 222, 222, 223, 223, 224, 224, 225, 225, 226, 226, + 227, 227, 228, 228, 229, 229, 230, 230, 231, 231, 232, 232, 233, 233, 234, 234, 235, 235, 236, + 236, 237, 237, 238, 238, 239, 239, 240, 240, +}; + +static constexpr int U(int r, int g, int b) { + return -U_R[r] - U_G[g] + U_B[b]; +} + +constexpr std::array V_R = { + 89, 90, 90, 91, 91, 92, 92, 93, 93, 94, 94, 95, 95, 96, 96, 97, 97, 98, 98, + 99, 99, 100, 100, 101, 101, 102, 102, 103, 103, 104, 104, 105, 105, 106, 106, 107, 107, 108, + 108, 109, 109, 110, 110, 111, 111, 112, 112, 113, 113, 114, 114, 115, 115, 116, 116, 117, 117, + 118, 118, 119, 119, 120, 120, 121, 121, 122, 122, 123, 123, 124, 124, 125, 125, 126, 126, 127, + 127, 128, 128, 129, 129, 130, 130, 131, 131, 132, 132, 133, 133, 134, 134, 135, 135, 136, 136, + 137, 137, 138, 138, 139, 139, 140, 140, 141, 141, 142, 142, 143, 143, 144, 144, 145, 145, 146, + 146, 147, 147, 148, 148, 149, 149, 150, 150, 151, 151, 152, 152, 153, 153, 154, 154, 155, 155, + 156, 156, 157, 157, 158, 158, 159, 159, 160, 160, 161, 161, 162, 162, 163, 163, 164, 164, 165, + 165, 166, 166, 167, 167, 168, 168, 169, 169, 170, 170, 171, 171, 172, 172, 173, 173, 174, 174, + 175, 175, 176, 176, 177, 177, 178, 178, 179, 179, 180, 180, 181, 181, 182, 182, 183, 183, 184, + 184, 185, 185, 186, 186, 187, 187, 188, 188, 189, 189, 190, 190, 191, 191, 192, 192, 193, 193, + 194, 194, 195, 195, 196, 196, 197, 197, 198, 198, 199, 199, 200, 200, 201, 201, 202, 202, 203, + 203, 204, 205, 205, 206, 206, 207, 207, 208, 208, 209, 209, 210, 210, 211, 211, 212, 212, 213, + 213, 214, 214, 215, 215, 216, 216, 217, 217, +}; + +constexpr std::array V_G = { + -57, -56, -56, -55, -55, -55, -54, -54, -53, -53, -52, -52, -52, -51, -51, -50, -50, -50, -49, + -49, -48, -48, -47, -47, -47, -46, -46, -45, -45, -45, -44, -44, -43, -43, -42, -42, -42, -41, + -41, -40, -40, -39, -39, -39, -38, -38, -37, -37, -37, -36, -36, -35, -35, -34, -34, -34, -33, + -33, -32, -32, -31, -31, -31, -30, -30, -29, -29, -29, -28, -28, -27, -27, -26, -26, -26, -25, + -25, -24, -24, -24, -23, -23, -22, -22, -21, -21, -21, -20, -20, -19, -19, -18, -18, -18, -17, + -17, -16, -16, -16, -15, -15, -14, -14, -13, -13, -13, -12, -12, -11, -11, -10, -10, -10, -9, + -9, -8, -8, -8, -7, -7, -6, -6, -5, -5, -5, -4, -4, -3, -3, -3, -2, -2, -1, + -1, 0, 0, 0, 0, 0, 1, 1, 2, 2, 2, 3, 3, 4, 4, 4, 5, 5, 6, + 6, 7, 7, 7, 8, 8, 9, 9, 10, 10, 10, 11, 11, 12, 12, 12, 13, 13, 14, + 14, 15, 15, 15, 16, 16, 17, 17, 17, 18, 18, 19, 19, 20, 20, 20, 21, 21, 22, + 22, 23, 23, 23, 24, 24, 25, 25, 25, 26, 26, 27, 27, 28, 28, 28, 29, 29, 30, + 30, 31, 31, 31, 32, 32, 33, 33, 33, 34, 34, 35, 35, 36, 36, 36, 37, 37, 38, + 38, 38, 39, 39, 40, 40, 41, 41, 41, 42, 42, 43, 43, 44, 44, 44, 45, 45, 46, + 46, 46, 47, 47, 48, 48, 49, 49, 49, +}; + +constexpr std::array V_B = { + 18, 18, 18, 18, 18, 18, 18, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 20, 20, 20, 20, + 20, 20, 20, 20, 20, 20, 20, 20, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 22, 22, 22, 22, + 22, 22, 22, 22, 22, 22, 22, 22, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 24, 24, 24, + 24, 24, 24, 24, 24, 24, 24, 24, 24, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 26, 26, 26, + 26, 26, 26, 26, 26, 26, 26, 26, 26, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 28, 28, + 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 30, 30, + 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 33, 33, 33, 33, 33, 33, 33, 33, 33, 33, 33, 33, 34, + 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 35, 35, 35, 35, 35, 35, 35, 35, 35, 35, 35, 35, 35, + 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, + 38, 38, 38, 38, 38, 38, 38, 38, 38, 38, 38, 38, 39, 39, 39, 39, +}; + +static constexpr int V(int r, int g, int b) { + return V_R[r] - V_G[g] - V_B[b]; +} +} // namespace YuvTable + +std::vector Rgb2Yuv(const QImage& source, int width, int height) { + auto buffer = std::vector(width * height); + auto dest = buffer.begin(); + bool write = false; + int py, pu, pv; + for (int y = 0; y < height; ++y) { + for (int x = 0; x < width; ++x) { + QRgb rgb = source.pixel(x, y); + int r = qRed(rgb); + int g = qGreen(rgb); + int b = qBlue(rgb); + + // The following transformation is a reverse of the one in Y2R using ITU_Rec601 + int y = YuvTable::Y(r, g, b); + int u = YuvTable::U(r, g, b); + int v = YuvTable::V(r, g, b); + + if (write) { + pu = (pu + u) / 2; + pv = (pv + v) / 2; + using MathUtil::Clamp; + *(dest++) = static_cast(Clamp(py, 0, 0xFF) | (Clamp(pu, 0, 0xFF) << 8)); + *(dest++) = static_cast(Clamp(y, 0, 0xFF) | (Clamp(pv, 0, 0xFF) << 8)); + } else { + py = y; + pu = u; + pv = v; + } + write = !write; + } + } + return buffer; +} + +std::vector ProcessImage(const QImage& image, int width, int height, bool output_rgb = false, + bool flip_horizontal = false, bool flip_vertical = false) { + std::vector buffer(width * height); + if (image.isNull()) { + return buffer; + } + QImage scaled = + image.scaled(width, height, Qt::KeepAspectRatioByExpanding, Qt::SmoothTransformation); + QImage transformed = + scaled.copy((scaled.width() - width) / 2, (scaled.height() - height) / 2, width, height) + .mirrored(flip_horizontal, flip_vertical); + if (output_rgb) { + QImage converted = transformed.convertToFormat(QImage::Format_RGB16); + std::memcpy(buffer.data(), converted.bits(), width * height * sizeof(u16)); + } else { + return CameraUtil::Rgb2Yuv(transformed, width, height); + } + return buffer; +} + +} // namespace CameraUtil diff --git a/src/citra_qt/camera/camera_util.h b/src/citra_qt/camera/camera_util.h new file mode 100644 index 000000000..68ccc5584 --- /dev/null +++ b/src/citra_qt/camera/camera_util.h @@ -0,0 +1,21 @@ +// Copyright 2018 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include +#include "common/common_types.h" + +class QImage; + +namespace CameraUtil { + +/// Converts QImage to a yuv formatted std::vector +std::vector Rgb2Yuv(const QImage& source, int width, int height); + +/// Processes the QImage (resizing, flipping ...) and converts it to a std::vector +std::vector ProcessImage(const QImage& source, int width, int height, bool output_rgb, + bool flip_horizontal, bool flip_vertical); + +} // namespace CameraUtil diff --git a/src/citra_qt/camera/qt_camera_factory.cpp b/src/citra_qt/camera/qt_camera_factory.cpp new file mode 100644 index 000000000..d7eb49cd4 --- /dev/null +++ b/src/citra_qt/camera/qt_camera_factory.cpp @@ -0,0 +1,24 @@ +// Copyright 2018 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include +#include "citra_qt/camera/qt_camera_factory.h" + +namespace Camera { + +std::unique_ptr QtCameraFactory::CreatePreview(const std::string& config, + int width, int height) const { + std::unique_ptr camera = Create(config); + + if (camera->IsPreviewAvailable()) { + return camera; + } + QMessageBox::critical(nullptr, QObject::tr("Error"), + QObject::tr("Couldn't load ") + + (config.empty() ? QObject::tr("the camera") : "") + + QString::fromStdString(config)); + return nullptr; +} + +} // namespace Camera diff --git a/src/citra_qt/camera/qt_camera_factory.h b/src/citra_qt/camera/qt_camera_factory.h new file mode 100644 index 000000000..7efc21b58 --- /dev/null +++ b/src/citra_qt/camera/qt_camera_factory.h @@ -0,0 +1,18 @@ +// Copyright 2018 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include +#include "core/frontend/camera/factory.h" + +namespace Camera { + +// Base class for camera factories of citra_qt +class QtCameraFactory : public CameraFactory { + std::unique_ptr CreatePreview(const std::string& config, int width, + int height) const override; +}; + +} // namespace Camera diff --git a/src/citra_qt/camera/qt_multimedia_camera.cpp b/src/citra_qt/camera/qt_multimedia_camera.cpp new file mode 100644 index 000000000..6c2668df6 --- /dev/null +++ b/src/citra_qt/camera/qt_multimedia_camera.cpp @@ -0,0 +1,224 @@ +// Copyright 2018 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include +#include +#include +#include +#include +#include "citra_qt/camera/qt_multimedia_camera.h" +#include "citra_qt/main.h" + +namespace Camera { + +QList QtCameraSurface::supportedPixelFormats( + QAbstractVideoBuffer::HandleType handleType) const { + Q_UNUSED(handleType); + return QList() + << QVideoFrame::Format_ARGB32 << QVideoFrame::Format_ARGB32_Premultiplied + << QVideoFrame::Format_RGB32 << QVideoFrame::Format_RGB24 << QVideoFrame::Format_RGB565 + << QVideoFrame::Format_RGB555 << QVideoFrame::Format_ARGB8565_Premultiplied + << QVideoFrame::Format_BGRA32 << QVideoFrame::Format_BGRA32_Premultiplied + << QVideoFrame::Format_BGR32 << QVideoFrame::Format_BGR24 << QVideoFrame::Format_BGR565 + << QVideoFrame::Format_BGR555 << QVideoFrame::Format_BGRA5658_Premultiplied + << QVideoFrame::Format_AYUV444 << QVideoFrame::Format_AYUV444_Premultiplied + << QVideoFrame::Format_YUV444 << QVideoFrame::Format_YUV420P << QVideoFrame::Format_YV12 + << QVideoFrame::Format_UYVY << QVideoFrame::Format_YUYV << QVideoFrame::Format_NV12 + << QVideoFrame::Format_NV21 << QVideoFrame::Format_IMC1 << QVideoFrame::Format_IMC2 + << QVideoFrame::Format_IMC3 << QVideoFrame::Format_IMC4 << QVideoFrame::Format_Y8 + << QVideoFrame::Format_Y16 << QVideoFrame::Format_Jpeg << QVideoFrame::Format_CameraRaw + << QVideoFrame::Format_AdobeDng; // Supporting all the formats +} + +bool QtCameraSurface::present(const QVideoFrame& frame) { + if (!frame.isValid()) { + return false; + } + QVideoFrame cloneFrame(frame); + cloneFrame.map(QAbstractVideoBuffer::ReadOnly); + const QImage image(cloneFrame.bits(), cloneFrame.width(), cloneFrame.height(), + QVideoFrame::imageFormatFromPixelFormat(cloneFrame.pixelFormat())); + QMutexLocker locker(&mutex); + current_frame = image.mirrored(true, true); + locker.unlock(); + cloneFrame.unmap(); + return true; +} + +QtMultimediaCamera::QtMultimediaCamera(const std::string& camera_name) + : handler(QtMultimediaCameraHandler::GetHandler()) { + if (handler->thread() == QThread::currentThread()) { + handler->CreateCamera(camera_name); + } else { + QMetaObject::invokeMethod(handler.get(), "CreateCamera", Qt::BlockingQueuedConnection, + Q_ARG(const std::string&, camera_name)); + } +} + +QtMultimediaCamera::~QtMultimediaCamera() { + handler->StopCamera(); + QtMultimediaCameraHandler::ReleaseHandler(handler); +} + +void QtMultimediaCamera::StartCapture() { + if (handler->thread() == QThread::currentThread()) { + handler->StartCamera(); + } else { + QMetaObject::invokeMethod(handler.get(), "StartCamera", Qt::BlockingQueuedConnection); + } +} + +void QtMultimediaCamera::StopCapture() { + handler->StopCamera(); +} + +void QtMultimediaCamera::SetFormat(Service::CAM::OutputFormat output_format) { + output_rgb = output_format == Service::CAM::OutputFormat::RGB565; +} + +void QtMultimediaCamera::SetFrameRate(Service::CAM::FrameRate frame_rate) { + const std::array FrameRateList = { + /* Rate_15 */ QCamera::FrameRateRange(15, 15), + /* Rate_15_To_5 */ QCamera::FrameRateRange(5, 15), + /* Rate_15_To_2 */ QCamera::FrameRateRange(2, 15), + /* Rate_10 */ QCamera::FrameRateRange(10, 10), + /* Rate_8_5 */ QCamera::FrameRateRange(8.5, 8.5), + /* Rate_5 */ QCamera::FrameRateRange(5, 5), + /* Rate_20 */ QCamera::FrameRateRange(20, 20), + /* Rate_20_To_5 */ QCamera::FrameRateRange(5, 20), + /* Rate_30 */ QCamera::FrameRateRange(30, 30), + /* Rate_30_To_5 */ QCamera::FrameRateRange(5, 30), + /* Rate_15_To_10 */ QCamera::FrameRateRange(10, 15), + /* Rate_20_To_10 */ QCamera::FrameRateRange(10, 20), + /* Rate_30_To_10 */ QCamera::FrameRateRange(10, 30), + }; + + auto framerate = FrameRateList[static_cast(frame_rate)]; + + handler->settings.setMinimumFrameRate(framerate.minimumFrameRate); + handler->settings.setMinimumFrameRate(framerate.maximumFrameRate); +} + +void QtMultimediaCamera::SetResolution(const Service::CAM::Resolution& resolution) { + width = resolution.width; + height = resolution.height; +} + +void QtMultimediaCamera::SetFlip(Service::CAM::Flip flip) { + using namespace Service::CAM; + flip_horizontal = (flip == Flip::Horizontal) || (flip == Flip::Reverse); + flip_vertical = (flip == Flip::Vertical) || (flip == Flip::Reverse); +} + +void QtMultimediaCamera::SetEffect(Service::CAM::Effect effect) { + if (effect != Service::CAM::Effect::None) { + NGLOG_ERROR(Service_CAM, "Unimplemented effect {}", static_cast(effect)); + } +} + +std::vector QtMultimediaCamera::ReceiveFrame() { + QMutexLocker locker(&handler->camera_surface.mutex); + return CameraUtil::ProcessImage(handler->camera_surface.current_frame, width, height, + output_rgb, flip_horizontal, flip_vertical); +} + +bool QtMultimediaCamera::IsPreviewAvailable() { + return handler->CameraAvailable(); +} + +std::unique_ptr QtMultimediaCameraFactory::Create( + const std::string& config) const { + return std::make_unique(config); +} + +std::array, 3> QtMultimediaCameraHandler::handlers; + +std::array QtMultimediaCameraHandler::status; + +void QtMultimediaCameraHandler::Init() { + for (auto& handler : handlers) { + handler = std::make_shared(); + } +} + +std::shared_ptr QtMultimediaCameraHandler::GetHandler() { + for (int i = 0; i < handlers.size(); i++) { + if (!status[i]) { + NGLOG_INFO(Service_CAM, "Successfully got handler {}", i); + status[i] = true; + return handlers[i]; + } + } + NGLOG_CRITICAL(Service_CAM, "All handlers taken up"); + return nullptr; +} + +void QtMultimediaCameraHandler::ReleaseHandler( + const std::shared_ptr& handler) { + for (int i = 0; i < handlers.size(); i++) { + if (handlers[i] == handler) { + NGLOG_INFO(Service_CAM, "Successfully released handler {}", i); + status[i] = false; + handlers[i]->started = false; + break; + } + } +} + +void QtMultimediaCameraHandler::CreateCamera(const std::string& camera_name) { + QList cameras = QCameraInfo::availableCameras(); + for (const QCameraInfo& cameraInfo : cameras) { + if (cameraInfo.deviceName().toStdString() == camera_name) + camera = std::make_unique(cameraInfo); + } + if (!camera) { // no cameras found, using default camera + camera = std::make_unique(); + } + settings.setMinimumFrameRate(30); + settings.setMaximumFrameRate(30); + camera->setViewfinder(&camera_surface); +} + +void QtMultimediaCameraHandler::StopCamera() { + camera->stop(); + started = false; +} + +void QtMultimediaCameraHandler::StartCamera() { + camera->setViewfinderSettings(settings); + camera->start(); + started = true; +} + +bool QtMultimediaCameraHandler::CameraAvailable() const { + return camera && camera->isAvailable(); +} + +void QtMultimediaCameraHandler::StopCameras() { + NGLOG_INFO(Service_CAM, "Stopping all cameras"); + for (auto& handler : handlers) { + if (handler && handler->started) { + handler->StopCamera(); + } + } +} + +void QtMultimediaCameraHandler::ResumeCameras() { + for (auto& handler : handlers) { + if (handler && handler->started) { + handler->StartCamera(); + } + } +} + +void QtMultimediaCameraHandler::ReleaseHandlers() { + StopCameras(); + NGLOG_INFO(Service_CAM, "Releasing all handlers"); + for (int i = 0; i < handlers.size(); i++) { + status[i] = false; + handlers[i]->started = false; + } +} + +} // namespace Camera diff --git a/src/citra_qt/camera/qt_multimedia_camera.h b/src/citra_qt/camera/qt_multimedia_camera.h new file mode 100644 index 000000000..103aff8a0 --- /dev/null +++ b/src/citra_qt/camera/qt_multimedia_camera.h @@ -0,0 +1,105 @@ +// Copyright 2018 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include "citra_qt/camera/camera_util.h" +#include "citra_qt/camera/qt_camera_factory.h" +#include "core/frontend/camera/interface.h" + +class GMainWindow; + +namespace Camera { + +class QtCameraSurface final : public QAbstractVideoSurface { +public: + QList supportedPixelFormats( + QAbstractVideoBuffer::HandleType) const override; + bool present(const QVideoFrame&) override; + +private: + QMutex mutex; + QImage current_frame; + + friend class QtMultimediaCamera; // For access to current_frame +}; + +class QtMultimediaCameraHandler; + +/// This class is only an interface. It just calls QtMultimediaCameraHandler. +class QtMultimediaCamera final : public CameraInterface { +public: + QtMultimediaCamera(const std::string& camera_name); + ~QtMultimediaCamera(); + void StartCapture() override; + void StopCapture() override; + void SetResolution(const Service::CAM::Resolution&) override; + void SetFlip(Service::CAM::Flip) override; + void SetEffect(Service::CAM::Effect) override; + void SetFormat(Service::CAM::OutputFormat) override; + void SetFrameRate(Service::CAM::FrameRate frame_rate) override; + std::vector ReceiveFrame() override; + bool IsPreviewAvailable() override; + +private: + std::shared_ptr handler; + int width, height; + bool output_rgb; + bool flip_horizontal, flip_vertical; +}; + +class QtMultimediaCameraFactory final : public QtCameraFactory { +public: + std::unique_ptr Create(const std::string& config) const override; +}; + +class QtMultimediaCameraHandler final : public QObject { + Q_OBJECT + +public: + /// Creates the global handler. Must be called in UI thread. + static void Init(); + static std::shared_ptr GetHandler(); + static void ReleaseHandler(const std::shared_ptr& handler); + + /** + * Creates the camera. + * Note: This function must be called via QMetaObject::invokeMethod in UI thread. + */ + Q_INVOKABLE void CreateCamera(const std::string& camera_name); + + /** + * Starts the camera. + * Note: This function must be called via QMetaObject::invokeMethod in UI thread when + * starting the camera for the first time. 'Resume' calls can be in other threads. + */ + Q_INVOKABLE void StartCamera(); + + void StopCamera(); + bool CameraAvailable() const; + static void StopCameras(); + static void ResumeCameras(); + static void ReleaseHandlers(); + +private: + std::unique_ptr camera; + QtCameraSurface camera_surface{}; + QCameraViewfinderSettings settings; + bool started = false; + + static std::array, 3> handlers; + static std::array status; + + friend class QtMultimediaCamera; // For access to camera_surface (and camera) +}; + +} // namespace Camera diff --git a/src/citra_qt/camera/still_image_camera.cpp b/src/citra_qt/camera/still_image_camera.cpp new file mode 100644 index 000000000..c3929df4f --- /dev/null +++ b/src/citra_qt/camera/still_image_camera.cpp @@ -0,0 +1,73 @@ +// Copyright 2018 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include +#include +#include +#include "citra_qt/camera/still_image_camera.h" + +namespace Camera { + +StillImageCamera::StillImageCamera(QImage image_) : image(std::move(image_)) {} + +void StillImageCamera::StartCapture() {} + +void StillImageCamera::StopCapture() {} + +void StillImageCamera::SetFormat(Service::CAM::OutputFormat output_format) { + output_rgb = output_format == Service::CAM::OutputFormat::RGB565; +} + +void StillImageCamera::SetResolution(const Service::CAM::Resolution& resolution) { + width = resolution.width; + height = resolution.height; +} + +void StillImageCamera::SetFlip(Service::CAM::Flip flip) { + using namespace Service::CAM; + flip_horizontal = (flip == Flip::Horizontal) || (flip == Flip::Reverse); + flip_vertical = (flip == Flip::Vertical) || (flip == Flip::Reverse); +} + +void StillImageCamera::SetEffect(Service::CAM::Effect effect) { + if (effect != Service::CAM::Effect::None) { + NGLOG_ERROR(Service_CAM, "Unimplemented effect {}", static_cast(effect)); + } +} + +std::vector StillImageCamera::ReceiveFrame() { + return CameraUtil::ProcessImage(image, width, height, output_rgb, flip_horizontal, + flip_vertical); +} + +bool StillImageCamera::IsPreviewAvailable() { + return !image.isNull(); +} + +const std::string StillImageCameraFactory::getFilePath() { + QList types = QImageReader::supportedImageFormats(); + QList temp_filters; + for (QByteArray type : types) { + temp_filters << QString("*." + QString(type)); + } + + QString filter = QObject::tr("Supported image files (") + temp_filters.join(" ") + ")"; + + return QFileDialog::getOpenFileName(nullptr, QObject::tr("Open File"), ".", filter) + .toStdString(); +} + +std::unique_ptr StillImageCameraFactory::Create(const std::string& config) const { + std::string real_config = config; + if (config.empty()) { + real_config = getFilePath(); + } + QImage image(QString::fromStdString(real_config)); + if (image.isNull()) { + NGLOG_ERROR(Service_CAM, "Couldn't load image \"{}\"", real_config.c_str()); + } + return std::make_unique(image); +} + +} // namespace Camera diff --git a/src/citra_qt/camera/still_image_camera.h b/src/citra_qt/camera/still_image_camera.h new file mode 100644 index 000000000..c54d367e8 --- /dev/null +++ b/src/citra_qt/camera/still_image_camera.h @@ -0,0 +1,43 @@ +// Copyright 2018 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include +#include +#include "citra_qt/camera/camera_util.h" +#include "citra_qt/camera/qt_camera_factory.h" +#include "core/frontend/camera/interface.h" + +namespace Camera { + +class StillImageCamera final : public CameraInterface { +public: + StillImageCamera(QImage image); + void StartCapture() override; + void StopCapture() override; + void SetResolution(const Service::CAM::Resolution&) override; + void SetFlip(Service::CAM::Flip) override; + void SetEffect(Service::CAM::Effect) override; + void SetFormat(Service::CAM::OutputFormat) override; + void SetFrameRate(Service::CAM::FrameRate frame_rate) override {} + std::vector ReceiveFrame() override; + bool IsPreviewAvailable() override; + +private: + QImage image; + int width, height; + bool output_rgb; + bool flip_horizontal, flip_vertical; +}; + +class StillImageCameraFactory final : public QtCameraFactory { +public: + std::unique_ptr Create(const std::string& config) const override; + +private: + static const std::string getFilePath(); +}; + +} // namespace Camera diff --git a/src/citra_qt/configuration/configure.ui b/src/citra_qt/configuration/configure.ui index dce2b7739..21fa5fbc8 100644 --- a/src/citra_qt/configuration/configure.ui +++ b/src/citra_qt/configuration/configure.ui @@ -43,6 +43,11 @@ Audio + + + + Camera + @@ -83,6 +88,12 @@ QWidget
configuration/configure_audio.h
1 + + + ConfigureCamera + QWidget +
configuration/configure_camera.h
+ 1
ConfigureDebug diff --git a/src/citra_qt/configuration/configure_camera.cpp b/src/citra_qt/configuration/configure_camera.cpp new file mode 100644 index 000000000..ca6db4623 --- /dev/null +++ b/src/citra_qt/configuration/configure_camera.cpp @@ -0,0 +1,324 @@ +// Copyright 2018 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include +#include +#include +#include +#include +#include +#include +#include "citra_qt/configuration/configure_camera.h" +#include "citra_qt/ui_settings.h" +#include "core/core.h" +#include "core/settings.h" +#include "ui_configure_camera.h" + +const std::array ConfigureCamera::Implementations = { + "blank", /* Blank */ + "image", /* Image */ + "qt" /* System Camera */ +}; + +ConfigureCamera::ConfigureCamera(QWidget* parent) + : QWidget(parent), ui(std::make_unique()) { + ui->setupUi(this); + // Load settings + camera_name = Settings::values.camera_name; + camera_config = Settings::values.camera_config; + for (auto&& item : camera_name) { + if (item == "opencv") { + QMessageBox::critical(this, tr("Error"), + tr("Sorry, Citra has removed support for OpenCV cameras.\n\nYour " + "existing OpenCV cameras have been replaced with Blank.")); + item = "blank"; + } + } + QList cameras = QCameraInfo::availableCameras(); + for (const QCameraInfo& cameraInfo : cameras) { + ui->system_camera->addItem(cameraInfo.deviceName()); + } + updateCameraMode(); + setConfiguration(); + connectEvents(); + ui->preview_box->setHidden(true); +} + +ConfigureCamera::~ConfigureCamera() { + stopPreviewing(); +} + +void ConfigureCamera::connectEvents() { + connect(ui->image_source, + static_cast(&QComboBox::currentIndexChanged), this, [this] { + stopPreviewing(); + updateImageSourceUI(); + }); + connect(ui->camera_selection, + static_cast(&QComboBox::currentIndexChanged), this, [this] { + stopPreviewing(); + if (getCameraSelection() != current_selected) { + recordConfig(); + } + if (ui->camera_selection->currentIndex() == 1) { + ui->camera_mode->setCurrentIndex(1); // Double + if (camera_name[0] == camera_name[2] && camera_config[0] == camera_config[2]) { + ui->camera_mode->setCurrentIndex(0); // Single + } + } + updateCameraMode(); + setConfiguration(); + }); + connect(ui->camera_mode, static_cast(&QComboBox::currentIndexChanged), + this, [this] { + stopPreviewing(); + ui->camera_position_label->setVisible(ui->camera_mode->currentIndex() == 1); + ui->camera_position->setVisible(ui->camera_mode->currentIndex() == 1); + current_selected = getCameraSelection(); + }); + connect(ui->camera_position, + static_cast(&QComboBox::currentIndexChanged), this, [this] { + stopPreviewing(); + if (getCameraSelection() != current_selected) { + recordConfig(); + } + setConfiguration(); + }); + connect(ui->toolButton, &QToolButton::clicked, this, &ConfigureCamera::onToolButtonClicked); + connect(ui->preview_button, &QPushButton::clicked, this, [=] { startPreviewing(); }); + connect(ui->prompt_before_load, &QCheckBox::stateChanged, this, [this](int state) { + ui->camera_file->setDisabled(state == Qt::Checked); + ui->toolButton->setDisabled(state == Qt::Checked); + if (state == Qt::Checked) { + ui->camera_file->setText(""); + } + }); + connect(ui->camera_file, &QLineEdit::textChanged, this, [=] { stopPreviewing(); }); + connect(ui->system_camera, + static_cast(&QComboBox::currentIndexChanged), this, + [=] { stopPreviewing(); }); +} + +void ConfigureCamera::updateCameraMode() { + CameraPosition pos = getCameraSelection(); + // Set the visibility of the camera mode selection widgets + if (pos == CameraPosition::RearBoth) { + ui->camera_position->setHidden(true); + ui->camera_position_label->setHidden(true); + ui->camera_mode->setHidden(false); + ui->camera_mode_label->setHidden(false); + } else { + ui->camera_position->setHidden(pos == CameraPosition::Front); + ui->camera_position_label->setHidden(pos == CameraPosition::Front); + ui->camera_mode->setHidden(pos == CameraPosition::Front); + ui->camera_mode_label->setHidden(pos == CameraPosition::Front); + } +} + +void ConfigureCamera::updateImageSourceUI() { + int image_source = ui->image_source->currentIndex(); + switch (image_source) { + case 0: /* blank */ + case 2: /* system camera */ + ui->prompt_before_load->setHidden(true); + ui->prompt_before_load->setChecked(false); + ui->camera_file_label->setHidden(true); + ui->camera_file->setHidden(true); + ui->camera_file->setText(""); + ui->toolButton->setHidden(true); + break; + case 1: /* still image */ + ui->prompt_before_load->setHidden(false); + ui->camera_file_label->setHidden(false); + ui->camera_file->setHidden(false); + ui->toolButton->setHidden(false); + if (camera_config[getSelectedCameraIndex()].empty()) { + ui->prompt_before_load->setChecked(true); + ui->camera_file->setDisabled(true); + ui->toolButton->setDisabled(true); + ui->camera_file->setText(""); + } else { + ui->camera_file->setDisabled(false); + ui->toolButton->setDisabled(false); + } + break; + default: + NGLOG_ERROR(Service_CAM, "Unknown image source {}", image_source); + } + ui->system_camera_label->setHidden(image_source != 2); + ui->system_camera->setHidden(image_source != 2); +} + +void ConfigureCamera::recordConfig() { + std::string implementation = Implementations[ui->image_source->currentIndex()]; + int image_source = ui->image_source->currentIndex(); + std::string config; + if (image_source == 2) { /* system camera */ + if (ui->system_camera->currentIndex() == 0) { + config = ""; + } else { + config = ui->system_camera->currentText().toStdString(); + } + } else { + config = ui->camera_file->text().toStdString(); + } + if (current_selected == CameraPosition::RearBoth) { + camera_name[0] = camera_name[2] = implementation; + camera_config[0] = camera_config[2] = config; + } else if (current_selected != CameraPosition::Null) { + int index = static_cast(current_selected); + camera_name[index] = implementation; + camera_config[index] = config; + } + current_selected = getCameraSelection(); +} + +void ConfigureCamera::startPreviewing() { + current_selected = getCameraSelection(); + recordConfig(); + int camera_selection = getSelectedCameraIndex(); + stopPreviewing(); + // Init preview box + ui->preview_box->setHidden(false); + ui->preview_button->setHidden(true); + preview_width = ui->preview_box->size().width(); + preview_height = preview_width * 0.75; + ui->preview_box->setToolTip(tr("Resolution: ") + QString::number(preview_width) + "*" + + QString::number(preview_height)); + // Load previewing camera + previewing_camera = + Camera::CreateCameraPreview(camera_name[camera_selection], camera_config[camera_selection], + preview_width, preview_height); + if (!previewing_camera) { + stopPreviewing(); + return; + } + previewing_camera->SetResolution( + {static_cast(preview_width), static_cast(preview_height)}); + previewing_camera->SetEffect(Service::CAM::Effect::None); + previewing_camera->SetFlip(Service::CAM::Flip::None); + previewing_camera->SetFormat(Service::CAM::OutputFormat::RGB565); + previewing_camera->SetFrameRate(Service::CAM::FrameRate::Rate_30); + previewing_camera->StartCapture(); + + timer_id = startTimer(1000 / 30); +} + +void ConfigureCamera::stopPreviewing() { + ui->preview_box->setHidden(true); + ui->preview_button->setHidden(false); + + if (previewing_camera) { + previewing_camera->StopCapture(); + } + + if (timer_id != 0) { + killTimer(timer_id); + timer_id = 0; + } +} + +void ConfigureCamera::timerEvent(QTimerEvent* event) { + if (event->timerId() != timer_id) { + return; + } + if (!previewing_camera) { + killTimer(timer_id); + timer_id = 0; + return; + } + std::vector frame = previewing_camera->ReceiveFrame(); + int width = ui->preview_box->size().width(); + int height = width * 0.75; + if (width != preview_width || height != preview_height) { + stopPreviewing(); + return; + } + QImage image(width, height, QImage::Format::Format_RGB16); + std::memcpy(image.bits(), frame.data(), width * height * sizeof(u16)); + ui->preview_box->setPixmap(QPixmap::fromImage(image)); +} + +void ConfigureCamera::setConfiguration() { + int index = getSelectedCameraIndex(); + for (int i = 0; i < Implementations.size(); i++) { + if (Implementations[i] == camera_name[index]) { + ui->image_source->setCurrentIndex(i); + } + } + if (camera_name[index] == "image") { + ui->camera_file->setDisabled(camera_config[index].empty()); + ui->toolButton->setDisabled(camera_config[index].empty()); + if (camera_config[index].empty()) { + ui->camera_file->setText(""); + } + } + if (camera_name[index] == "qt") { + ui->system_camera->setCurrentIndex(0); + if (!camera_config[index].empty()) { + ui->system_camera->setCurrentText(QString::fromStdString(camera_config[index])); + } + } else { + ui->camera_file->setText(QString::fromStdString(camera_config[index])); + } + updateImageSourceUI(); +} + +void ConfigureCamera::onToolButtonClicked() { + stopPreviewing(); + int camera_selection = getSelectedCameraIndex(); + QString filter; + if (camera_name[camera_selection] == "image") { + QList types = QImageReader::supportedImageFormats(); + QList temp_filters; + for (const QByteArray& type : types) { + temp_filters << QString("*." + QString(type)); + } + filter = tr("Supported image files (%1)").arg(temp_filters.join(" ")); + } + QString path = QFileDialog::getOpenFileName(this, tr("Open File"), ".", filter); + if (!path.isEmpty()) { + ui->camera_file->setText(path); + } +} + +void ConfigureCamera::applyConfiguration() { + recordConfig(); + stopPreviewing(); + Settings::values.camera_name = camera_name; + Settings::values.camera_config = camera_config; + Settings::Apply(); +} + +ConfigureCamera::CameraPosition ConfigureCamera::getCameraSelection() { + switch (ui->camera_selection->currentIndex()) { + case 0: // Front + return CameraPosition::Front; + case 1: // Rear + if (ui->camera_mode->currentIndex() == 0) { + // Single (2D) mode + return CameraPosition::RearBoth; + } else { + // Double (3D) mode + return (ui->camera_position->currentIndex() == 0) ? CameraPosition::RearLeft + : CameraPosition::RearRight; + } + default: + NGLOG_ERROR(Frontend, "Unknown camera selection"); + return CameraPosition::Front; + } +} + +int ConfigureCamera::getSelectedCameraIndex() { + CameraPosition pos = getCameraSelection(); + int camera_selection = static_cast(pos); + if (pos == CameraPosition::RearBoth) { // Single Mode + camera_selection = 0; // Either camera is the same, so we return RearRight + } + return camera_selection; +} + +void ConfigureCamera::retranslateUi() { + ui->retranslateUi(this); +} diff --git a/src/citra_qt/configuration/configure_camera.h b/src/citra_qt/configuration/configure_camera.h new file mode 100644 index 000000000..5621de7ef --- /dev/null +++ b/src/citra_qt/configuration/configure_camera.h @@ -0,0 +1,56 @@ +// Copyright 2018 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include "core/frontend/camera/factory.h" +#include "core/frontend/camera/interface.h" + +namespace Ui { +class ConfigureCamera; +} + +class ConfigureCamera : public QWidget { + Q_OBJECT + +public: + explicit ConfigureCamera(QWidget* parent = nullptr); + ~ConfigureCamera(); + + void applyConfiguration(); + void retranslateUi(); + + void timerEvent(QTimerEvent*) override; + +public slots: + /// recordConfig() and updateUiDisplay() + void setConfiguration(); + void onToolButtonClicked(); + +private: + enum class CameraPosition { RearRight, Front, RearLeft, RearBoth, Null }; + static const std::array Implementations; + /// Record the current configuration + void recordConfig(); + /// Updates camera mode + void updateCameraMode(); + /// Updates image source + void updateImageSourceUI(); + void startPreviewing(); + void stopPreviewing(); + void connectEvents(); + CameraPosition getCameraSelection(); + int getSelectedCameraIndex(); + +private: + std::unique_ptr ui; + std::array camera_name; + std::array camera_config; + int timer_id = 0; + int preview_width = 0; + int preview_height = 0; + CameraPosition current_selected = CameraPosition::Front; + bool is_previewing = false; + std::unique_ptr previewing_camera; +}; diff --git a/src/citra_qt/configuration/configure_camera.ui b/src/citra_qt/configuration/configure_camera.ui new file mode 100644 index 000000000..ff6615498 --- /dev/null +++ b/src/citra_qt/configuration/configure_camera.ui @@ -0,0 +1,257 @@ + + + + ConfigureCamera + + + + + + Camera + + + + + + + + Select the camera to configure + + + Camera to configure: + + + + + + + Select the camera to configure + + + + Front + + + + + Rear + + + + + + + + + + + + Select the camera mode (single or double) + + + Camera mode: + + + + + + + Select the camera mode (single or double) + + + + Single (2D) + + + + + Double (3D) + + + + + + + + + + + + Select the position of camera to configure + + + Camera position: + + + + + + + Select the position of camera to configure + + + + Left + + + + + Right + + + + + + + + + + + + + Configuration + + + + + + + + Select where the image of the emulated camera come from. It may be an image or a real camera. + + + Camera Image Source: + + + + + + + Select where the image of the emulated camera come from. It may be an image or a real camera. + + + + Blank (blank) + + + + + Still Image (image) + + + + + System Camera (qt) + + + + + + + + + + + + QFrame::NoFrame + + + File: + + + + + + + + + + ... + + + + + + + + + + + QFrame::NoFrame + + + Camera: + + + + + + + + <Default> + + + + + + + + + + Prompt before load + + + + + + + + + + Preview + + + + + + + 512 + 384 + + + + Resolution: 512*384 + + + + + + + + + + Click to preview + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + diff --git a/src/citra_qt/configuration/configure_dialog.cpp b/src/citra_qt/configuration/configure_dialog.cpp index ca468bf81..b2aaeb0d6 100644 --- a/src/citra_qt/configuration/configure_dialog.cpp +++ b/src/citra_qt/configuration/configure_dialog.cpp @@ -24,6 +24,7 @@ void ConfigureDialog::applyConfiguration() { ui->inputTab->applyConfiguration(); ui->graphicsTab->applyConfiguration(); ui->audioTab->applyConfiguration(); + ui->cameraTab->applyConfiguration(); ui->debugTab->applyConfiguration(); ui->webTab->applyConfiguration(); Settings::Apply(); @@ -37,6 +38,7 @@ void ConfigureDialog::onLanguageChanged(const QString& locale) { ui->inputTab->retranslateUi(); ui->graphicsTab->retranslateUi(); ui->audioTab->retranslateUi(); + ui->cameraTab->applyConfiguration(); ui->debugTab->retranslateUi(); ui->webTab->retranslateUi(); } diff --git a/src/citra_qt/main.cpp b/src/citra_qt/main.cpp index be7046767..2167abef5 100644 --- a/src/citra_qt/main.cpp +++ b/src/citra_qt/main.cpp @@ -16,6 +16,8 @@ #include #include "citra_qt/aboutdialog.h" #include "citra_qt/bootmanager.h" +#include "citra_qt/camera/qt_multimedia_camera.h" +#include "citra_qt/camera/still_image_camera.h" #include "citra_qt/compatdb.h" #include "citra_qt/configuration/config.h" #include "citra_qt/configuration/configure_dialog.h" @@ -696,6 +698,8 @@ void GMainWindow::ShutdownGame() { emu_thread->wait(); emu_thread = nullptr; + Camera::QtMultimediaCameraHandler::ReleaseHandlers(); + // The emulation is stopped, so closing the window or not does not matter anymore disconnect(render_window, &GRenderWindow::Closed, this, &GMainWindow::OnStopGame); @@ -910,6 +914,7 @@ void GMainWindow::OnMenuRecentFile() { } void GMainWindow::OnStartGame() { + Camera::QtMultimediaCameraHandler::ResumeCameras(); emu_thread->SetRunning(true); qRegisterMetaType("Core::System::ResultStatus"); qRegisterMetaType("std::string"); @@ -925,7 +930,7 @@ void GMainWindow::OnStartGame() { void GMainWindow::OnPauseGame() { emu_thread->SetRunning(false); - + Camera::QtMultimediaCameraHandler::StopCameras(); ui.action_Start->setEnabled(true); ui.action_Pause->setEnabled(false); ui.action_Stop->setEnabled(true); @@ -1363,6 +1368,11 @@ int main(int argc, char* argv[]) { Log::AddBackend( std::make_unique(FileUtil::GetUserPath(D_LOGS_IDX) + LOG_FILE)); + // Register CameraFactory + Camera::RegisterFactory("image", std::make_unique()); + Camera::RegisterFactory("qt", std::make_unique()); + Camera::QtMultimediaCameraHandler::Init(); + main_window.show(); return app.exec(); } diff --git a/src/core/frontend/camera/blank_camera.cpp b/src/core/frontend/camera/blank_camera.cpp index 7995abcbd..a63ba780a 100644 --- a/src/core/frontend/camera/blank_camera.cpp +++ b/src/core/frontend/camera/blank_camera.cpp @@ -23,9 +23,13 @@ void BlankCamera::SetFlip(Service::CAM::Flip) {} void BlankCamera::SetEffect(Service::CAM::Effect) {} -std::vector BlankCamera::ReceiveFrame() const { +std::vector BlankCamera::ReceiveFrame() { // Note: 0x80008000 stands for two black pixels in YUV422 return std::vector(width * height, output_rgb ? 0 : 0x8000); } +bool BlankCamera::IsPreviewAvailable() { + return true; +} + } // namespace Camera diff --git a/src/core/frontend/camera/blank_camera.h b/src/core/frontend/camera/blank_camera.h index c6619bd88..039417a2a 100644 --- a/src/core/frontend/camera/blank_camera.h +++ b/src/core/frontend/camera/blank_camera.h @@ -17,7 +17,9 @@ public: void SetFlip(Service::CAM::Flip) override; void SetEffect(Service::CAM::Effect) override; void SetFormat(Service::CAM::OutputFormat) override; - std::vector ReceiveFrame() const override; + void SetFrameRate(Service::CAM::FrameRate frame_rate) override {} + std::vector ReceiveFrame() override; + bool IsPreviewAvailable() override; private: int width = 0; diff --git a/src/core/frontend/camera/factory.cpp b/src/core/frontend/camera/factory.cpp index 4b4da50dd..f0434cdad 100644 --- a/src/core/frontend/camera/factory.cpp +++ b/src/core/frontend/camera/factory.cpp @@ -29,4 +29,18 @@ std::unique_ptr CreateCamera(const std::string& name, const std return std::make_unique(); } +std::unique_ptr CreateCameraPreview(const std::string& name, + const std::string& config, int width, + int height) { + auto pair = factories.find(name); + if (pair != factories.end()) { + return pair->second->CreatePreview(config, width, height); + } + + if (name != "blank") { + LOG_ERROR(Service_CAM, "Unknown camera \"%s\"", name.c_str()); + } + return std::make_unique(); +} + } // namespace Camera diff --git a/src/core/frontend/camera/factory.h b/src/core/frontend/camera/factory.h index f46413fa7..840be7022 100644 --- a/src/core/frontend/camera/factory.h +++ b/src/core/frontend/camera/factory.h @@ -21,6 +21,20 @@ public: * @returns a unique_ptr to the created camera object. */ virtual std::unique_ptr Create(const std::string& config) const = 0; + + /** + * Creates a camera object for preview based on the configuration string. + * @param config Configuration string to create the camera. The implementation can decide the + * meaning of this string. + * @returns a unique_ptr to the created camera object. + * Note: The default implementation for this is to call Create(). Derived classes may have other + * Implementations. For example, A dialog may be used instead of LOG_ERROR when error + * occurs. + */ + virtual std::unique_ptr CreatePreview(const std::string& config, int width, + int height) const { + return Create(config); + } }; /** @@ -38,4 +52,14 @@ void RegisterFactory(const std::string& name, std::unique_ptr fac */ std::unique_ptr CreateCamera(const std::string& name, const std::string& config); +/** + * Creates a camera from the factory for previewing. + * @param name Identifier of the camera factory. + * @param config Configuration string to create the camera. The meaning of this string is + * defined by the factory. + */ +std::unique_ptr CreateCameraPreview(const std::string& name, + const std::string& config, int width, + int height); + } // namespace Camera diff --git a/src/core/frontend/camera/interface.h b/src/core/frontend/camera/interface.h index a55a495c9..6d8040ae2 100644 --- a/src/core/frontend/camera/interface.h +++ b/src/core/frontend/camera/interface.h @@ -49,13 +49,27 @@ public: */ virtual void SetFormat(Service::CAM::OutputFormat format) = 0; + /** + * Sets the recommended framerate of the camera. + * @param frame_rate Recommended framerate + */ + virtual void SetFrameRate(Service::CAM::FrameRate frame_rate) = 0; + /** * Receives a frame from the camera. * This function should be only called between a StartCapture call and a StopCapture call. * @returns A std::vector containing pixels. The total size of the vector is width * height * where width and height are set by a call to SetResolution. */ - virtual std::vector ReceiveFrame() const = 0; + virtual std::vector ReceiveFrame() = 0; + + /** + * Test if the camera is opened successfully and can receive a preview frame. Only used for + * preview. This function should be only called between a StartCapture call and a StopCapture + * call. + * @returns true if the camera is opened successfully and false otherwise + */ + virtual bool IsPreviewAvailable() = 0; }; } // namespace Camera diff --git a/src/core/hle/service/cam/cam.cpp b/src/core/hle/service/cam/cam.cpp index e82870909..3328dcf8e 100644 --- a/src/core/hle/service/cam/cam.cpp +++ b/src/core/hle/service/cam/cam.cpp @@ -22,6 +22,8 @@ namespace Service { namespace CAM { +static std::weak_ptr current_cam; + // built-in resolution parameters constexpr std::array PRESET_RESOLUTION{{ {640, 480, 0, 0, 639, 479}, // VGA @@ -138,9 +140,16 @@ void Module::StartReceiving(int port_id) { port.is_receiving = true; // launches a capture task asynchronously - const CameraConfig& camera = cameras[port.camera_id]; - port.capture_result = - std::async(std::launch::async, &Camera::CameraInterface::ReceiveFrame, camera.impl.get()); + CameraConfig& camera = cameras[port.camera_id]; + port.capture_result = std::async(std::launch::async, [&camera, &port, this] { + if (is_camera_reload_pending.exchange(false)) { + // reinitialize the camera according to new settings + camera.impl->StopCapture(); + LoadCameraImplementation(camera, port.camera_id); + camera.impl->StartCapture(); + } + return camera.impl->ReceiveFrame(); + }); // schedules a completion event according to the frame rate. The event will block on the // capture task if it is not finished within the expected time @@ -771,7 +780,7 @@ void Module::Interface::SetFrameRate(Kernel::HLERequestContext& ctx) { if (camera_select.IsValid()) { for (int camera : camera_select) { cam->cameras[camera].frame_rate = frame_rate; - // TODO(wwylele): consider hinting the actual camera with the expected frame rate + cam->cameras[camera].impl->SetFrameRate(frame_rate); } rb.Push(RESULT_SUCCESS); } else { @@ -980,12 +989,7 @@ void Module::Interface::DriverInitialize(Kernel::HLERequestContext& ctx) { context.resolution = context_id == 0 ? PRESET_RESOLUTION[5 /*DS_LCD*/] : PRESET_RESOLUTION[0 /*VGA*/]; } - camera.impl = Camera::CreateCamera(Settings::values.camera_name[camera_id], - Settings::values.camera_config[camera_id]); - camera.impl->SetFlip(camera.contexts[0].flip); - camera.impl->SetEffect(camera.contexts[0].effect); - camera.impl->SetFormat(camera.contexts[0].format); - camera.impl->SetResolution(camera.contexts[0].resolution); + cam->LoadCameraImplementation(camera, camera_id); } for (PortConfig& port : cam->ports) { @@ -1032,8 +1036,28 @@ Module::~Module() { CancelReceiving(1); } +void Module::ReloadCameraDevices() { + is_camera_reload_pending.store(true); +} + +void Module::LoadCameraImplementation(CameraConfig& camera, int camera_id) { + camera.impl = Camera::CreateCamera(Settings::values.camera_name[camera_id], + Settings::values.camera_config[camera_id]); + camera.impl->SetFlip(camera.contexts[0].flip); + camera.impl->SetEffect(camera.contexts[0].effect); + camera.impl->SetFormat(camera.contexts[0].format); + camera.impl->SetResolution(camera.contexts[0].resolution); +} + +void ReloadCameraDevices() { + if (auto cam = current_cam.lock()) + cam->ReloadCameraDevices(); +} + void InstallInterfaces(SM::ServiceManager& service_manager) { auto cam = std::make_shared(); + current_cam = cam; + std::make_shared(cam)->InstallAsService(service_manager); std::make_shared(cam)->InstallAsService(service_manager); std::make_shared(cam)->InstallAsService(service_manager); diff --git a/src/core/hle/service/cam/cam.h b/src/core/hle/service/cam/cam.h index 0d9d59dbc..45361a4ef 100644 --- a/src/core/hle/service/cam/cam.h +++ b/src/core/hle/service/cam/cam.h @@ -240,6 +240,7 @@ class Module final { public: Module(); ~Module(); + void ReloadCameraDevices(); class Interface : public ServiceFramework { public: @@ -771,11 +772,17 @@ private: void Clear(); }; + void LoadCameraImplementation(CameraConfig& camera, int camera_id); + std::array cameras; std::array ports; CoreTiming::EventType* completion_event_callback; + std::atomic is_camera_reload_pending{false}; }; +/// Reload camera devices. Used when input configuration changed +void ReloadCameraDevices(); + void InstallInterfaces(SM::ServiceManager& service_manager); } // namespace CAM diff --git a/src/core/settings.cpp b/src/core/settings.cpp index 770b7fda7..f7467e573 100644 --- a/src/core/settings.cpp +++ b/src/core/settings.cpp @@ -35,6 +35,7 @@ void Apply() { Service::HID::ReloadInputDevices(); Service::IR::ReloadInputDevices(); + Service::CAM::ReloadCameraDevices(); } } // namespace Settings