From 5e83958ac29f18555e1e7a45e904e53202b444dd Mon Sep 17 00:00:00 2001 From: Florin9doi Date: Thu, 9 Jan 2025 12:21:25 +0200 Subject: [PATCH 1/3] Add QtMultimedia --- Source/Core/DolphinQt/CMakeLists.txt | 3 ++- Source/VSProps/QtCompile.props | 7 ++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/Source/Core/DolphinQt/CMakeLists.txt b/Source/Core/DolphinQt/CMakeLists.txt index 38d481cdf9..9f676f0246 100644 --- a/Source/Core/DolphinQt/CMakeLists.txt +++ b/Source/Core/DolphinQt/CMakeLists.txt @@ -14,7 +14,7 @@ endif() set(CMAKE_AUTOMOC ON) set(CMAKE_AUTORCC ON) -find_package(Qt6 REQUIRED COMPONENTS Core Gui Widgets Svg) +find_package(Qt6 REQUIRED COMPONENTS Core Gui Widgets Svg Multimedia) message(STATUS "Found Qt version ${Qt6_VERSION}") set_property(TARGET Qt6::Core PROPERTY INTERFACE_COMPILE_FEATURES "") @@ -427,6 +427,7 @@ target_link_libraries(dolphin-emu PRIVATE core Qt6::Widgets + Qt6::Multimedia uicommon imgui implot diff --git a/Source/VSProps/QtCompile.props b/Source/VSProps/QtCompile.props index c42c823f88..78f598de56 100644 --- a/Source/VSProps/QtCompile.props +++ b/Source/VSProps/QtCompile.props @@ -26,6 +26,7 @@ $(QtIncludeDir)QtCore;%(AdditionalIncludeDirectories) $(QtIncludeDir)QtGui;%(AdditionalIncludeDirectories) $(QtIncludeDir)QtWidgets;%(AdditionalIncludeDirectories) + $(QtIncludeDir)QtMultimedia;%(AdditionalIncludeDirectories) - + - + From 43ce4585ed7d9dbdc9ce2251d728bff744d773da Mon Sep 17 00:00:00 2001 From: Florin9doi Date: Tue, 7 Jan 2025 18:55:24 +0200 Subject: [PATCH 2/3] USB: Duel Scanner / Motion Tracking camera emulation --- Source/Android/app/build.gradle.kts | 5 + .../Android/app/src/main/AndroidManifest.xml | 4 + .../dolphinemu/DolphinApplication.java | 10 +- .../dolphinemu/dolphinemu/NativeLibrary.java | 2 + .../activities/EmulationActivity.kt | 7 +- .../dolphinemu/features/camera/Camera.kt | 166 ++++++ .../input/ui/ProfileDialogPresenter.kt | 1 - .../features/settings/model/IntSetting.kt | 6 + .../features/settings/model/StringSetting.kt | 6 + .../settings/ui/SettingsFragmentPresenter.kt | 35 ++ .../dolphinemu/ui/main/MainActivity.kt | 3 + .../dolphinemu/ui/main/TvMainActivity.kt | 1 + .../dolphinemu/utils/ActivityTracker.kt | 5 + .../dolphinemu/utils/PermissionsHandler.java | 33 +- .../app/src/main/res/values/arrays.xml | 12 + .../app/src/main/res/values/strings.xml | 2 + Source/Android/jni/AndroidCommon/IDCache.cpp | 33 ++ Source/Android/jni/AndroidCommon/IDCache.h | 5 + Source/Android/jni/MainAndroid.cpp | 24 + Source/Core/Core/CMakeLists.txt | 6 + Source/Core/Core/Config/MainSettings.cpp | 6 + Source/Core/Core/Config/MainSettings.h | 2 + Source/Core/Core/Core.cpp | 2 + Source/Core/Core/Host.h | 3 + Source/Core/Core/IOS/USB/Common.h | 1 + .../Core/Core/IOS/USB/Emulated/CameraBase.cpp | 127 +++++ .../Core/Core/IOS/USB/Emulated/CameraBase.h | 163 ++++++ .../Core/IOS/USB/Emulated/DuelScanner.cpp | 472 +++++++++++++++++ .../Core/Core/IOS/USB/Emulated/DuelScanner.h | 55 ++ .../Core/IOS/USB/Emulated/MotionCamera.cpp | 487 ++++++++++++++++++ .../Core/Core/IOS/USB/Emulated/MotionCamera.h | 55 ++ Source/Core/Core/IOS/USB/Host.cpp | 12 + Source/Core/Core/System.cpp | 7 + Source/Core/Core/System.h | 2 + Source/Core/DolphinLib.props | 6 + Source/Core/DolphinNoGUI/MainNoGUI.cpp | 8 + Source/Core/DolphinQt/CMakeLists.txt | 2 + Source/Core/DolphinQt/CameraQt/CameraQt.cpp | 191 +++++++ Source/Core/DolphinQt/CameraQt/CameraQt.h | 43 ++ Source/Core/DolphinQt/DolphinQt.vcxproj | 3 + Source/Core/DolphinQt/Host.cpp | 10 + Source/Core/DolphinQt/Host.h | 2 + Source/Core/DolphinQt/Main.cpp | 3 + Source/Core/DolphinQt/MainWindow.cpp | 15 + Source/Core/DolphinQt/MainWindow.h | 3 + Source/Core/DolphinQt/MenuBar.cpp | 1 + Source/Core/DolphinQt/MenuBar.h | 1 + .../Core/DolphinTool/ToolHeadlessPlatform.cpp | 8 + Source/Core/UICommon/USBUtils.cpp | 1 + Source/DSPTool/StubHost.cpp | 6 + Source/UnitTests/StubHost.cpp | 6 + 51 files changed, 2064 insertions(+), 5 deletions(-) create mode 100644 Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/camera/Camera.kt create mode 100644 Source/Core/Core/IOS/USB/Emulated/CameraBase.cpp create mode 100644 Source/Core/Core/IOS/USB/Emulated/CameraBase.h create mode 100644 Source/Core/Core/IOS/USB/Emulated/DuelScanner.cpp create mode 100644 Source/Core/Core/IOS/USB/Emulated/DuelScanner.h create mode 100644 Source/Core/Core/IOS/USB/Emulated/MotionCamera.cpp create mode 100644 Source/Core/Core/IOS/USB/Emulated/MotionCamera.h create mode 100644 Source/Core/DolphinQt/CameraQt/CameraQt.cpp create mode 100644 Source/Core/DolphinQt/CameraQt/CameraQt.h diff --git a/Source/Android/app/build.gradle.kts b/Source/Android/app/build.gradle.kts index 0ed29fde4b..5c1ddae06b 100644 --- a/Source/Android/app/build.gradle.kts +++ b/Source/Android/app/build.gradle.kts @@ -154,6 +154,11 @@ dependencies { implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3") implementation("com.nononsenseapps:filepicker:4.2.1") + + // Motion Camera emulation + implementation("androidx.camera:camera-core:1.4.1") + implementation("androidx.camera:camera-camera2:1.4.1") + implementation("androidx.camera:camera-lifecycle:1.4.1") } fun getGitVersion(): String { diff --git a/Source/Android/app/src/main/AndroidManifest.xml b/Source/Android/app/src/main/AndroidManifest.xml index 6d33d6158a..fc14bfd075 100644 --- a/Source/Android/app/src/main/AndroidManifest.xml +++ b/Source/Android/app/src/main/AndroidManifest.xml @@ -13,6 +13,9 @@ + @@ -28,6 +31,7 @@ + , grantResults: IntArray) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults) + PermissionsHandler.onRequestPermissionsResult(requestCode, permissions, grantResults) + } + companion object { private const val BACKSTACK_NAME_MENU = "menu" private const val BACKSTACK_NAME_SUBMENU = "submenu" diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/camera/Camera.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/camera/Camera.kt new file mode 100644 index 0000000000..b22ed7637d --- /dev/null +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/camera/Camera.kt @@ -0,0 +1,166 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.dolphinemu.dolphinemu.features.camera + +import android.content.Context +import android.graphics.ImageFormat +import android.hardware.camera2.CameraMetadata +import android.util.Log +import android.util.Size +import androidx.camera.core.ImageAnalysis +import androidx.camera.core.ImageCapture +import androidx.camera.core.ImageProxy +import androidx.camera.core.Preview +import androidx.camera.lifecycle.ProcessCameraProvider +import androidx.core.content.ContextCompat +import androidx.lifecycle.LifecycleOwner +import org.dolphinemu.dolphinemu.DolphinApplication +import org.dolphinemu.dolphinemu.NativeLibrary +import org.dolphinemu.dolphinemu.features.settings.model.StringSetting +import org.dolphinemu.dolphinemu.utils.PermissionsHandler +import java.util.concurrent.ExecutorService +import java.util.concurrent.Executors + +class Camera { + companion object { + val TAG = "Camera" + private var instance: Camera? = null + + private var cameraEntries: Array = arrayOf() + private var cameraValues: Array = arrayOf() + + private var width = 0 + private var height = 0 + private var isRunning: Boolean = false + private lateinit var imageCapture: ImageCapture + private lateinit var cameraExecutor: ExecutorService + + fun getInstance(context: Context) = instance ?: synchronized(this) { + instance ?: Camera().also { + instance = it + val cameraProviderFuture = ProcessCameraProvider.getInstance(context) + cameraProviderFuture.addListener({ + val cameraProvider = cameraProviderFuture.get() + val cameraInfos = cameraProvider.getAvailableCameraInfos() + var index = 0; + fun getCameraDescription(type: Int) : String { + return when (type) { + CameraMetadata.LENS_FACING_BACK -> "Back" + CameraMetadata.LENS_FACING_FRONT -> "Front" + CameraMetadata.LENS_FACING_EXTERNAL -> "External" + else -> "Unknown" + } + } + for (camera in cameraInfos) { + cameraEntries += "${index}: ${getCameraDescription(camera.lensFacing)}" + cameraValues += index++.toString() + } + }, ContextCompat.getMainExecutor(context)) + cameraExecutor = Executors.newSingleThreadExecutor() + } + } + + fun getCameraEntries(): Array { + return cameraEntries + } + + fun getCameraValues(): Array { + return cameraValues + } + + @JvmStatic + fun resumeCamera() { + if (width == 0 || height == 0) + return + Log.i(TAG, "resumeCamera") + startCamera(width, height) + } + + @JvmStatic + fun startCamera(width: Int, height: Int) { + this.width = width + this.height = height + if (!PermissionsHandler.hasCameraAccess(DolphinApplication.getAppContext())) { + PermissionsHandler.requestCameraPermission(DolphinApplication.getAppActivity()) + return + } + + if (isRunning) + return + isRunning = true + Log.i(TAG, "startCamera: " + width + "x" + height) + Log.i(TAG, "Selected Camera: " + StringSetting.MAIN_SELECTED_CAMERA.string) + + val cameraProviderFuture = ProcessCameraProvider.getInstance(DolphinApplication.getAppContext()) + cameraProviderFuture.addListener({ + val cameraProvider = cameraProviderFuture.get() + + val cameraSelector = cameraProvider.getAvailableCameraInfos() + .get(Integer.parseInt(StringSetting.MAIN_SELECTED_CAMERA.string)) + .cameraSelector + val preview = Preview.Builder().build() + imageCapture = ImageCapture.Builder() + .setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY) + .build() + val imageAnalyzer = ImageAnalysis.Builder() + .setTargetResolution(Size(width, height)) + .setOutputImageFormat(ImageAnalysis.OUTPUT_IMAGE_FORMAT_YUV_420_888) + .build() + .also { + it.setAnalyzer(cameraExecutor, ImageProcessor()) + } + + cameraProvider.bindToLifecycle(DolphinApplication.getAppActivity() as LifecycleOwner, cameraSelector, preview, imageCapture, imageAnalyzer) + }, ContextCompat.getMainExecutor(DolphinApplication.getAppContext())) + } + + @JvmStatic + fun stopCamera() { + if (!isRunning) + return + isRunning = false + width = 0 + height = 0 + Log.i(TAG, "stopCamera") + + val cameraProviderFuture = ProcessCameraProvider.getInstance(DolphinApplication.getAppContext()) + cameraProviderFuture.addListener({ + val cameraProvider = cameraProviderFuture.get() + cameraProvider.unbindAll() + }, ContextCompat.getMainExecutor(DolphinApplication.getAppContext())) + } + + private class ImageProcessor : ImageAnalysis.Analyzer { + override fun analyze(image: ImageProxy) { + if (image.format != ImageFormat.YUV_420_888) { + Log.e(TAG, "Error: Unhandled image format: ${image.format}") + image.close() + stopCamera() + return + } + + Log.i(TAG, "analyze sz=${image.width}x${image.height} / fmt=${image.format} / " + + "rot=${image.imageInfo.rotationDegrees} / " +// + "px0=${image.planes[0].pixelStride} / px1=${image.planes[1].pixelStride} / px2=${image.planes[2].pixelStride} / " + + "row0=${image.planes[0].rowStride} / row1=${image.planes[1].rowStride} / row2=${image.planes[2].rowStride}" + ) + + // Convert YUV_420_888 to YUY2 + val yuy2Image = ByteArray(2 * width * height) + for (line in 0 until height) { + for (col in 0 until width) { + val yuy2Pos = 2 * (width * line + col) + val yPos = image.planes[0].rowStride * line + image.planes[0].pixelStride * col + var uPos = image.planes[1].rowStride * (line / 2) + image.planes[1].pixelStride * (col / 2) + var vPos = image.planes[2].rowStride * (line / 2) + image.planes[2].pixelStride * (col / 2) + yuy2Image.set(yuy2Pos, image.planes[0].buffer.get(yPos)) + yuy2Image.set(yuy2Pos + 1, if (col % 2 == 0) image.planes[1].buffer.get(uPos) + else image.planes[2].buffer.get(vPos)) + } + } + image.close() + NativeLibrary.CameraSetData(yuy2Image) + } + } + } +} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/ui/ProfileDialogPresenter.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/ui/ProfileDialogPresenter.kt index 5c0a50a8b8..9e108f3907 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/ui/ProfileDialogPresenter.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/ui/ProfileDialogPresenter.kt @@ -11,7 +11,6 @@ import org.dolphinemu.dolphinemu.R import org.dolphinemu.dolphinemu.databinding.DialogInputStringBinding import org.dolphinemu.dolphinemu.features.settings.ui.MenuTag import org.dolphinemu.dolphinemu.features.settings.ui.SettingsActivityView -import org.dolphinemu.dolphinemu.utils.DirectoryInitialization import java.io.File import java.util.Locale diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/IntSetting.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/IntSetting.kt index 946b267207..0a48a204aa 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/IntSetting.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/IntSetting.kt @@ -82,6 +82,12 @@ enum class IntSetting( "DoubleTapButton", NativeLibrary.ButtonType.WIIMOTE_BUTTON_A ), + MAIN_EMULATED_CAMERA( + Settings.FILE_DOLPHIN, + Settings.SECTION_EMULATED_USB_DEVICES, + "EmulatedCamera", + 0 + ), SYSCONF_LANGUAGE(Settings.FILE_SYSCONF, "IPL", "LNG", 0x01), SYSCONF_SOUND_MODE(Settings.FILE_SYSCONF, "IPL", "SND", 0x01), SYSCONF_SENSOR_BAR_POSITION(Settings.FILE_SYSCONF, "BT", "BAR", 0x01), diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/StringSetting.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/StringSetting.kt index b150c845e1..73a54a54c7 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/StringSetting.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/StringSetting.kt @@ -55,6 +55,12 @@ enum class StringSetting( "ResourcePackPath", "" ), + MAIN_SELECTED_CAMERA( + Settings.FILE_DOLPHIN, + Settings.SECTION_EMULATED_USB_DEVICES, + "SelectedCamera", + "" + ), MAIN_FS_PATH(Settings.FILE_DOLPHIN, Settings.SECTION_INI_GENERAL, "NANDRootPath", ""), MAIN_WII_SD_CARD_IMAGE_PATH( Settings.FILE_DOLPHIN, diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsFragmentPresenter.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsFragmentPresenter.kt index ff82f4c63c..10a45aaa75 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsFragmentPresenter.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsFragmentPresenter.kt @@ -17,6 +17,7 @@ import kotlinx.coroutines.withContext import org.dolphinemu.dolphinemu.NativeLibrary import org.dolphinemu.dolphinemu.R import org.dolphinemu.dolphinemu.activities.UserDataActivity +import org.dolphinemu.dolphinemu.features.camera.Camera import org.dolphinemu.dolphinemu.features.input.model.ControlGroupEnabledSetting import org.dolphinemu.dolphinemu.features.input.model.InputMappingBooleanSetting import org.dolphinemu.dolphinemu.features.input.model.InputMappingDoubleSetting @@ -186,6 +187,28 @@ class SettingsFragmentPresenter( } private fun addTopLevelSettings(sl: ArrayList) { + sl.add( + SingleChoiceSetting( + context, + IntSetting.MAIN_EMULATED_CAMERA, + R.string.emulated_camera, + 0, + R.array.emulatedCameraEntries, + R.array.emulatedCameraValues + ) + ) + var camerasEntries = Camera.getCameraEntries() + var cameraValues = Camera.getCameraValues() + sl.add( + StringSingleChoiceSetting( + context, + StringSetting.MAIN_SELECTED_CAMERA, + R.string.selected_camera, + 0, + camerasEntries, + cameraValues + ) + ) sl.add(SubmenuSetting(context, R.string.config, MenuTag.CONFIG)) sl.add(SubmenuSetting(context, R.string.graphics_settings, MenuTag.GRAPHICS)) @@ -876,6 +899,18 @@ class SettingsFragmentPresenter( ) sl.add(HeaderSetting(context, R.string.emulated_usb_devices, 0)) +// var camerasEntries = Camera.getCameraEntries() +// var cameraValues = Camera.getCameraValues() +// sl.add( +// StringSingleChoiceSetting( +// context, +// StringSetting.MAIN_EMULATE_CAMERA, +// R.string.emulate_camera, +// 0, +// camerasEntries, +// cameraValues +// ) +// ) sl.add( SwitchSetting( context, diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/MainActivity.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/MainActivity.kt index a4e5d3600a..b97b9d6fff 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/MainActivity.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/MainActivity.kt @@ -21,6 +21,7 @@ import org.dolphinemu.dolphinemu.R import org.dolphinemu.dolphinemu.activities.EmulationActivity import org.dolphinemu.dolphinemu.adapters.PlatformPagerAdapter import org.dolphinemu.dolphinemu.databinding.ActivityMainBinding +import org.dolphinemu.dolphinemu.features.camera.Camera import org.dolphinemu.dolphinemu.features.settings.model.IntSetting import org.dolphinemu.dolphinemu.features.settings.model.NativeConfig import org.dolphinemu.dolphinemu.features.settings.ui.MenuTag @@ -76,6 +77,7 @@ class MainActivity : AppCompatActivity(), MainView, OnRefreshListener, ThemeProv } presenter.onCreate() + Camera.getInstance(applicationContext) // Stuff in this block only happens when this activity is newly created (i.e. not a rotation) if (savedInstanceState == null) { @@ -216,6 +218,7 @@ class MainActivity : AppCompatActivity(), MainView, OnRefreshListener, ThemeProv grantResults: IntArray ) { super.onRequestPermissionsResult(requestCode, permissions, grantResults) + PermissionsHandler.onRequestPermissionsResult(requestCode, permissions, grantResults) if (requestCode == PermissionsHandler.REQUEST_CODE_WRITE_PERMISSION) { if (grantResults[0] == PackageManager.PERMISSION_DENIED) { PermissionsHandler.setWritePermissionDenied() diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/TvMainActivity.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/TvMainActivity.kt index 9beec74fc3..08ff64443a 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/TvMainActivity.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/TvMainActivity.kt @@ -220,6 +220,7 @@ class TvMainActivity : FragmentActivity(), MainView, OnRefreshListener { grantResults: IntArray ) { super.onRequestPermissionsResult(requestCode, permissions, grantResults) + PermissionsHandler.onRequestPermissionsResult(requestCode, permissions, grantResults) if (requestCode == PermissionsHandler.REQUEST_CODE_WRITE_PERMISSION) { if (grantResults[0] == PackageManager.PERMISSION_DENIED) { PermissionsHandler.setWritePermissionDenied() diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/ActivityTracker.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/ActivityTracker.kt index b3a6a5d91a..5bb08e45e6 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/ActivityTracker.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/ActivityTracker.kt @@ -7,12 +7,14 @@ import android.os.Bundle class ActivityTracker : ActivityLifecycleCallbacks { val resumedActivities = HashSet() var backgroundExecutionAllowed = false + var currentActivity : Activity? = null override fun onActivityCreated(activity: Activity, bundle: Bundle?) {} override fun onActivityStarted(activity: Activity) {} override fun onActivityResumed(activity: Activity) { + currentActivity = activity resumedActivities.add(activity) if (!backgroundExecutionAllowed && !resumedActivities.isEmpty()) { backgroundExecutionAllowed = true @@ -21,6 +23,9 @@ class ActivityTracker : ActivityLifecycleCallbacks { } override fun onActivityPaused(activity: Activity) { + if (currentActivity === activity) { + currentActivity = null + } resumedActivities.remove(activity) if (backgroundExecutionAllowed && resumedActivities.isEmpty()) { backgroundExecutionAllowed = false diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/PermissionsHandler.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/PermissionsHandler.java index a4c69281f2..7291872c1b 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/PermissionsHandler.java +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/PermissionsHandler.java @@ -2,6 +2,7 @@ package org.dolphinemu.dolphinemu.utils; +import android.app.Activity; import android.content.Context; import android.content.pm.PackageManager; import android.os.Build; @@ -10,16 +11,21 @@ import android.os.Environment; import androidx.core.content.ContextCompat; import androidx.fragment.app.FragmentActivity; +import org.dolphinemu.dolphinemu.features.camera.Camera; +import org.jetbrains.annotations.NotNull; + import static android.Manifest.permission.WRITE_EXTERNAL_STORAGE; +import static android.Manifest.permission.CAMERA; public class PermissionsHandler { public static final int REQUEST_CODE_WRITE_PERMISSION = 500; + public static final int REQUEST_CODE_CAMERA_PERMISSION = 502; private static boolean sWritePermissionDenied = false; public static void requestWritePermission(final FragmentActivity activity) { - if (android.os.Build.VERSION.SDK_INT < Build.VERSION_CODES.M) + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) return; activity.requestPermissions(new String[]{WRITE_EXTERNAL_STORAGE}, @@ -28,7 +34,7 @@ public class PermissionsHandler public static boolean hasWriteAccess(Context context) { - if (android.os.Build.VERSION.SDK_INT < Build.VERSION_CODES.M) + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) return true; if (!isExternalStorageLegacy()) @@ -52,4 +58,27 @@ public class PermissionsHandler { return sWritePermissionDenied; } + + public static boolean hasCameraAccess(Context context) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) + return true; + + int hasCameraPermission = ContextCompat.checkSelfPermission(context, CAMERA); + return hasCameraPermission == PackageManager.PERMISSION_GRANTED; + } + + public static void requestCameraPermission(Activity activity) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) + return; + + activity.requestPermissions(new String[]{CAMERA}, REQUEST_CODE_CAMERA_PERMISSION); + } + + public static void onRequestPermissionsResult(int requestCode, @NotNull String[] permissions, @NotNull int[] grantResults) { + if (requestCode == PermissionsHandler.REQUEST_CODE_CAMERA_PERMISSION) { + if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { + Camera.Companion.resumeCamera(); + } + } + } } diff --git a/Source/Android/app/src/main/res/values/arrays.xml b/Source/Android/app/src/main/res/values/arrays.xml index 8892f7f218..a369d5d50e 100644 --- a/Source/Android/app/src/main/res/values/arrays.xml +++ b/Source/Android/app/src/main/res/values/arrays.xml @@ -223,6 +223,18 @@ 5 + + + @string/disabled + Duel Scanner + Motion Tracking Camera + + + 0 + 1 + 2 + + @string/accuracy_fast diff --git a/Source/Android/app/src/main/res/values/strings.xml b/Source/Android/app/src/main/res/values/strings.xml index 1b08fc6475..6d5e46e102 100644 --- a/Source/Android/app/src/main/res/values/strings.xml +++ b/Source/Android/app/src/main/res/values/strings.xml @@ -911,6 +911,8 @@ It can efficiently compress both junk data and encrypted Wii data. Emulated USB Devices + Emulated Camera + Selected Camera Skylanders Portal Skylanders Manager Create Skylander diff --git a/Source/Android/jni/AndroidCommon/IDCache.cpp b/Source/Android/jni/AndroidCommon/IDCache.cpp index ed382745c0..1003d8e732 100644 --- a/Source/Android/jni/AndroidCommon/IDCache.cpp +++ b/Source/Android/jni/AndroidCommon/IDCache.cpp @@ -113,6 +113,11 @@ static jclass s_core_device_control_class; static jfieldID s_core_device_control_pointer; static jmethodID s_core_device_control_constructor; +static jclass s_camera_class; +static jmethodID s_camera_start; +static jmethodID s_camera_resume; +static jmethodID s_camera_stop; + static jclass s_input_detector_class; static jfieldID s_input_detector_pointer; @@ -528,6 +533,26 @@ jmethodID GetCoreDeviceControlConstructor() return s_core_device_control_constructor; } +jclass GetCameraClass() +{ + return s_camera_class; +} + +jmethodID GetCameraStart() +{ + return s_camera_start; +} + +jmethodID GetCameraResume() +{ + return s_camera_resume; +} + +jmethodID GetCameraStop() +{ + return s_camera_stop; +} + jclass GetInputDetectorClass() { return s_input_detector_class; @@ -759,6 +784,13 @@ JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) "(Lorg/dolphinemu/dolphinemu/features/input/model/CoreDevice;J)V"); env->DeleteLocalRef(core_device_control_class); + const jclass camera_class = env->FindClass("org/dolphinemu/dolphinemu/features/camera/Camera"); + s_camera_class = reinterpret_cast(env->NewGlobalRef(camera_class)); + s_camera_start = env->GetStaticMethodID(camera_class, "startCamera", "(II)V"); + s_camera_resume = env->GetStaticMethodID(camera_class, "resumeCamera", "()V"); + s_camera_stop = env->GetStaticMethodID(camera_class, "stopCamera", "()V"); + env->DeleteLocalRef(camera_class); + const jclass input_detector_class = env->FindClass("org/dolphinemu/dolphinemu/features/input/model/InputDetector"); s_input_detector_class = reinterpret_cast(env->NewGlobalRef(input_detector_class)); @@ -803,6 +835,7 @@ JNIEXPORT void JNI_OnUnload(JavaVM* vm, void* reserved) env->DeleteGlobalRef(s_numeric_setting_class); env->DeleteGlobalRef(s_core_device_class); env->DeleteGlobalRef(s_core_device_control_class); + env->DeleteGlobalRef(s_camera_class); env->DeleteGlobalRef(s_input_detector_class); } } diff --git a/Source/Android/jni/AndroidCommon/IDCache.h b/Source/Android/jni/AndroidCommon/IDCache.h index 0b01d14b42..eb075e0130 100644 --- a/Source/Android/jni/AndroidCommon/IDCache.h +++ b/Source/Android/jni/AndroidCommon/IDCache.h @@ -112,6 +112,11 @@ jclass GetCoreDeviceControlClass(); jfieldID GetCoreDeviceControlPointer(); jmethodID GetCoreDeviceControlConstructor(); +jclass GetCameraClass(); +jmethodID GetCameraStart(); +jmethodID GetCameraResume(); +jmethodID GetCameraStop(); + jclass GetInputDetectorClass(); jfieldID GetInputDetectorPointer(); diff --git a/Source/Android/jni/MainAndroid.cpp b/Source/Android/jni/MainAndroid.cpp index be1d168730..7b5989339f 100644 --- a/Source/Android/jni/MainAndroid.cpp +++ b/Source/Android/jni/MainAndroid.cpp @@ -45,6 +45,7 @@ #include "Core/HW/Wiimote.h" #include "Core/HW/WiimoteReal/WiimoteReal.h" #include "Core/Host.h" +#include "Core/IOS/USB/Emulated/MotionCamera.h" #include "Core/PowerPC/JitInterface.h" #include "Core/PowerPC/PowerPC.h" #include "Core/State.h" @@ -136,6 +137,18 @@ void Host_UpdateTitle(const std::string& title) __android_log_write(ANDROID_LOG_INFO, DOLPHIN_TAG, title.c_str()); } +void Host_CameraStart(u16 width, u16 height) +{ + JNIEnv* env = IDCache::GetEnvForThread(); + env->CallStaticVoidMethod(IDCache::GetCameraClass(), IDCache::GetCameraStart(), width, height); +} + +void Host_CameraStop() +{ + JNIEnv* env = IDCache::GetEnvForThread(); + env->CallStaticVoidMethod(IDCache::GetCameraClass(), IDCache::GetCameraStop()); +} + void Host_UpdateDiscordClientID(const std::string& client_id) { } @@ -488,6 +501,8 @@ JNIEXPORT void JNICALL Java_org_dolphinemu_dolphinemu_NativeLibrary_SurfaceChang if (g_presenter) g_presenter->ChangeSurface(s_surf); + env->CallStaticVoidMethod(IDCache::GetCameraClass(), IDCache::GetCameraResume()); + s_surface_cv.notify_all(); } @@ -832,4 +847,13 @@ Java_org_dolphinemu_dolphinemu_NativeLibrary_GetCurrentTitleDescriptionUnchecked return ToJString(env, description); } + +JNIEXPORT void JNICALL +Java_org_dolphinemu_dolphinemu_NativeLibrary_CameraSetData(JNIEnv* env, jclass, jbyteArray image) +{ + jlong size = env->GetArrayLength(image); + jbyte* buffer = env->GetByteArrayElements(image, nullptr); + Core::System::GetInstance().GetCameraBase().SetData(reinterpret_cast(buffer), size); + env->ReleaseByteArrayElements(image, buffer, 0); +} } diff --git a/Source/Core/Core/CMakeLists.txt b/Source/Core/Core/CMakeLists.txt index b4437747e8..fc5490365d 100644 --- a/Source/Core/Core/CMakeLists.txt +++ b/Source/Core/Core/CMakeLists.txt @@ -425,8 +425,14 @@ add_library(core IOS/USB/Bluetooth/WiimoteHIDAttr.h IOS/USB/Common.cpp IOS/USB/Common.h + IOS/USB/Emulated/CameraBase.cpp + IOS/USB/Emulated/CameraBase.h + IOS/USB/Emulated/DuelScanner.cpp + IOS/USB/Emulated/DuelScanner.h IOS/USB/Emulated/Infinity.cpp IOS/USB/Emulated/Infinity.h + IOS/USB/Emulated/MotionCamera.cpp + IOS/USB/Emulated/MotionCamera.h IOS/USB/Emulated/Skylanders/Skylander.cpp IOS/USB/Emulated/Skylanders/Skylander.h IOS/USB/Emulated/Skylanders/SkylanderCrypto.cpp diff --git a/Source/Core/Core/Config/MainSettings.cpp b/Source/Core/Core/Config/MainSettings.cpp index 37f2a6054e..9afc2f44f9 100644 --- a/Source/Core/Core/Config/MainSettings.cpp +++ b/Source/Core/Core/Config/MainSettings.cpp @@ -585,6 +585,12 @@ void SetUSBDeviceWhitelist(const std::set>& devices) // Main.EmulatedUSBDevices +const Info MAIN_EMULATED_CAMERA{ + {System::Main, "EmulatedUSBDevices", "EmulatedCamera"}, 0}; + +const Info MAIN_SELECTED_CAMERA{ + {System::Main, "EmulatedUSBDevices", "SelectedCamera"}, ""}; + const Info MAIN_EMULATE_SKYLANDER_PORTAL{ {System::Main, "EmulatedUSBDevices", "EmulateSkylanderPortal"}, false}; diff --git a/Source/Core/Core/Config/MainSettings.h b/Source/Core/Core/Config/MainSettings.h index 51aa7ec8aa..1440e002ef 100644 --- a/Source/Core/Core/Config/MainSettings.h +++ b/Source/Core/Core/Config/MainSettings.h @@ -358,6 +358,8 @@ void SetUSBDeviceWhitelist(const std::set>& devices); // Main.EmulatedUSBDevices +extern const Info MAIN_EMULATED_CAMERA; +extern const Info MAIN_SELECTED_CAMERA; extern const Info MAIN_EMULATE_SKYLANDER_PORTAL; extern const Info MAIN_EMULATE_INFINITY_BASE; diff --git a/Source/Core/Core/Core.cpp b/Source/Core/Core/Core.cpp index 5f90f93bae..1757859f80 100644 --- a/Source/Core/Core/Core.cpp +++ b/Source/Core/Core/Core.cpp @@ -900,6 +900,8 @@ void UpdateTitle(Core::System& system) } Host_UpdateTitle(message); + //Host_CameraStart(320, 240); + //Host_CameraStart(640, 480); } void Shutdown(Core::System& system) diff --git a/Source/Core/Core/Host.h b/Source/Core/Core/Host.h index 5fc1fc1ee9..cf41ef6757 100644 --- a/Source/Core/Core/Host.h +++ b/Source/Core/Core/Host.h @@ -68,6 +68,9 @@ void Host_UpdateTitle(const std::string& title); void Host_YieldToUI(); void Host_TitleChanged(); +void Host_CameraStart(u16 width, u16 height); +void Host_CameraStop(); + void Host_UpdateDiscordClientID(const std::string& client_id = {}); bool Host_UpdateDiscordPresenceRaw(const std::string& details = {}, const std::string& state = {}, const std::string& large_image_key = {}, diff --git a/Source/Core/Core/IOS/USB/Common.h b/Source/Core/Core/IOS/USB/Common.h index 1c5fd07670..12a48850ff 100644 --- a/Source/Core/Core/IOS/USB/Common.h +++ b/Source/Core/Core/IOS/USB/Common.h @@ -29,6 +29,7 @@ enum ControlRequestTypes DIR_HOST2DEVICE = 0, DIR_DEVICE2HOST = 1, TYPE_STANDARD = 0, + TYPE_CLASS = 1, TYPE_VENDOR = 2, REC_DEVICE = 0, REC_INTERFACE = 1, diff --git a/Source/Core/Core/IOS/USB/Emulated/CameraBase.cpp b/Source/Core/Core/IOS/USB/Emulated/CameraBase.cpp new file mode 100644 index 0000000000..8fd66b32d9 --- /dev/null +++ b/Source/Core/Core/IOS/USB/Emulated/CameraBase.cpp @@ -0,0 +1,127 @@ +// Copyright 2025 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "Core/Host.h" +#include "Core/HW/Memmap.h" +#include "Core/IOS/USB/Emulated/MotionCamera.h" +#include "Core/System.h" + +namespace IOS::HLE::USB +{ + +CameraBase::~CameraBase() +{ + if (m_image_data) + { + free(m_image_data); + m_image_data = nullptr; + } +} + +void CameraBase::CreateSample(const u16 width, const u16 height) +{ + NOTICE_LOG_FMT(IOS_USB, "CameraBase::CreateSample width={}, height={}", width, height); + u32 new_size = width * height * 2; + if (m_image_size != new_size) + { + m_image_size = new_size; + if (m_image_data) + { + free(m_image_data); + } + m_image_data = (u8*) calloc(1, m_image_size); + } + + for (int line = 0 ; line < height; line++) { + for (int col = 0; col < width; col++) { + u8 *pos = m_image_data + 2 * (width * line + col); + + u8 r = col * 255 / width; + u8 g = col * 255 / width; + u8 b = line * 255 / height; + + u8 y = (( 66 * r + 129 * g + 25 * b + 128) / 256) + 16; + u8 u = ((-38 * r - 74 * g + 112 * b + 128) / 256) + 128; + u8 v = ((112 * r - 94 * g - 18 * b + 128) / 256) + 128; + + pos[0] = y; + pos[1] = (col % 2 == 0) ? u : v; + } + } +} + +void CameraBase::SetData(const u8* data, u32 length) +{ + if (length > m_image_size) + { + NOTICE_LOG_FMT(IOS_USB, "CameraBase::SetData length({}) > m_image_size({})", length, m_image_size); + return; + } + m_image_size = length; + memcpy(m_image_data, data, length); + //ERROR_LOG_FMT(IOS_USB, "SetData length={:x}", length); + //static bool done = false; + //if (!done) + //{ + // FILE* f = fopen("yugioh_dualscanner.raw", "wb"); + // if (!f) + // { + // ERROR_LOG_FMT(IOS_USB, "yugioh_dualscanner null"); + // return; + // } + // fwrite(m_image_data, m_image_size, 1, f); + // fclose(f); + // done = true; + //} +} + +void CameraBase::GetData(const u8* data, u32 length) +{ + if (length > m_image_size) + { + NOTICE_LOG_FMT(IOS_USB, "CameraBase::GetData length({}) > m_image_size({})", length, m_image_size); + return; + } + memcpy((void*)data, m_image_data, length); +} + +std::string CameraBase::getUVCVideoStreamingControl(u8 value) +{ + std::string names[] = { "VS_CONTROL_UNDEFINED", "VS_PROBE", "VS_COMMIT" }; + if (value <= VS_COMMIT) + return names[value]; + return "Unknown"; +} + +std::string CameraBase::getUVCRequest(u8 value) +{ + if (value == SET_CUR) + return "SET_CUR"; + std::string names[] = { "GET_CUR", "GET_MIN", "GET_MAX", "GET_RES", "GET_LEN", "GET_INF", "GET_DEF" }; + if (GET_CUR <= value && value <= GET_DEF) + return names[value - GET_CUR]; + return "Unknown"; +} + +std::string CameraBase::getUVCTerminalControl(u8 value) +{ + std::string names[] = { "CONTROL_UNDEFINED", "SCANNING_MODE", "AE_MODE", "AE_PRIORITY", "EXPOSURE_TIME_ABSOLUTE", + "EXPOSURE_TIME_RELATIVE", "FOCUS_ABSOLUTE", "FOCUS_RELATIVE", "FOCUS_AUTO", "IRIS_ABSOLUTE", "IRIS_RELATIVE", + "ZOOM_ABSOLUTE", "ZOOM_RELATIVE", "PANTILT_ABSOLUTE", "PANTILT_RELATIVE", "ROLL_ABSOLUTE", "ROLL_RELATIVE", + "PRIVACY" }; + if (value <= CT_PRIVACY) + return names[value]; + return "Unknown"; +} + +std::string CameraBase::getUVCProcessingUnitControl(u8 value) +{ + std::string names[] = { "CONTROL_UNDEFINED", "BACKLIGHT_COMPENSATION", "BRIGHTNESS", "CONTRAST", "GAIN", + "POWER_LINE_FREQUENCY", "HUE", "SATURATION", "SHARPNESS", "GAMMA", "WHITE_BALANCE_TEMPERATURE", + "WHITE_BALANCE_TEMPERATURE_AUTO", "WHITE_BALANCE_COMPONENT", "WHITE_BALANCE_COMPONENT_AUTO", "DIGITAL_MULTIPLIER", + "DIGITAL_MULTIPLIER_LIMIT", "HUE_AUTO", "ANALOG_VIDEO_STANDARD", "ANALOG_LOCK_STATUS" }; + if (value <= PU_ANALOG_LOCK_STATUS) + return names[value]; + return "Unknown"; +} +} // namespace IOS::HLE::USB diff --git a/Source/Core/Core/IOS/USB/Emulated/CameraBase.h b/Source/Core/Core/IOS/USB/Emulated/CameraBase.h new file mode 100644 index 0000000000..1208f34351 --- /dev/null +++ b/Source/Core/Core/IOS/USB/Emulated/CameraBase.h @@ -0,0 +1,163 @@ +// Copyright 2025 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include "Core/IOS/USB/Common.h" + +namespace IOS::HLE::USB +{ + +enum UVCRequestCodes +{ + SET_CUR = 0x01, + GET_CUR = 0x81, + GET_MIN = 0x82, + GET_MAX = 0x83, + GET_RES = 0x84, + GET_LEN = 0x85, + GET_INF = 0x86, + GET_DEF = 0x87, +}; + +enum UVCVideoStreamingControl +{ + VS_CONTROL_UNDEFINED = 0x00, + VS_PROBE = 0x01, + VS_COMMIT = 0x02, + VS_STILL_PROBE = 0x03, + VS_STILL_COMMIT = 0x04, + VS_STILL_IMAGE_TRIGGER = 0x05, + VS_STREAM_ERROR_CODE = 0x06, + VS_GENERATE_KEY_FRAME = 0x07, + VS_UPDATE_FRAME_SEGMENT = 0x08, + VS_SYNCH_DELAY = 0x09, +}; + +enum UVCTerminalControl +{ + CT_CONTROL_UNDEFINED = 0x00, + CT_SCANNING_MODE = 0x01, + CT_AE_MODE = 0x02, + CT_AE_PRIORITY = 0x03, + CT_EXPOSURE_TIME_ABSOLUTE = 0x04, + CT_EXPOSURE_TIME_RELATIVE = 0x05, + CT_FOCUS_ABSOLUTE = 0x06, + CT_FOCUS_RELATIVE = 0x07, + CT_FOCUS_AUTO = 0x08, + CT_IRIS_ABSOLUTE = 0x09, + CT_IRIS_RELATIVE = 0x0A, + CT_ZOOM_ABSOLUTE = 0x0B, + CT_ZOOM_RELATIVE = 0x0C, + CT_PANTILT_ABSOLUTE = 0x0D, + CT_PANTILT_RELATIVE = 0x0E, + CT_ROLL_ABSOLUTE = 0x0F, + CT_ROLL_RELATIVE = 0x10, + CT_PRIVACY = 0x11, +}; + +enum UVCProcessingUnitControl +{ + PU_CONTROL_UNDEFINED = 0x00, + PU_BACKLIGHT_COMPENSATION = 0x01, + PU_BRIGHTNESS = 0x02, + PU_CONTRAST = 0x03, + PU_GAIN = 0x04, + PU_POWER_LINE_FREQUENCY = 0x05, + PU_HUE = 0x06, + PU_SATURATION = 0x07, + PU_SHARPNESS = 0x08, + PU_GAMMA = 0x09, + PU_WHITE_BALANCE_TEMPERATURE = 0x0A, + PU_WHITE_BALANCE_TEMPERATURE_AUTO = 0x0B, + PU_WHITE_BALANCE_COMPONENT = 0x0C, + PU_WHITE_BALANCE_COMPONENT_AUTO = 0x0D, + PU_DIGITAL_MULTIPLIER = 0x0E, + PU_DIGITAL_MULTIPLIER_LIMIT = 0x0F, + PU_HUE_AUTO = 0x10, + PU_ANALOG_VIDEO_STANDARD = 0x11, + PU_ANALOG_LOCK_STATUS = 0x12, +}; + +#pragma pack(push, 1) +struct UVCHeader +{ + u8 bHeaderLength; + union { + u8 bmHeaderInfo; + struct { + u8 frameId : 1; + u8 endOfFrame : 1; + u8 presentationTimeStamp : 1; + u8 sourceClockReference : 1; + u8 : 1; + u8 stillImage : 1; + u8 error : 1; + u8 endOfHeader : 1; + }; + }; + // let's skip the optional fiels for now and see how it goes + /* + u32 dwPresentationTime; + union { + u8 scrSourceClock[6]; + struct { + u32 sourceTimeClock; + u16 sofCounter : 11; + u16 : 5; + }; + }; + */ +}; + +struct UVCProbeCommitControl +{ + union { + u16 bmHint; + struct { + u16 frameInterval : 1; + u16 keyFrameRate : 1; + u16 frameRate : 1; + u16 compQuality : 1; + u16 compWindowSize : 1; + u16 : 11; + }; + }; + u8 bFormatIndex; + u8 bFrameIndex; + u32 dwFrameInterval; + u16 wKeyFrameRate; + u16 wPFrameRate; + u16 wCompQuality; + u16 wCompWindowSize; + u16 wDelay; + u32 dwMaxVideoFrameSize; + u32 dwMaxPayloadTransferSize; +}; + +struct UVCImageSize +{ + u16 width; + u16 height; +}; +#pragma pack(pop) + +class CameraBase final +{ +public: + CameraBase() {}; + ~CameraBase(); + void CreateSample(const u16 width, const u16 height); + void SetData(const u8* data, u32 length); + void GetData(const u8* data, u32 length); + static std::string getUVCVideoStreamingControl(u8 value); + static std::string getUVCRequest(u8 value); + static std::string getUVCTerminalControl(u8 value); + static std::string getUVCProcessingUnitControl(u8 value); + +private: + u32 m_image_size = 0; + u8 *m_image_data = nullptr; +}; +} // namespace IOS::HLE::USB diff --git a/Source/Core/Core/IOS/USB/Emulated/DuelScanner.cpp b/Source/Core/Core/IOS/USB/Emulated/DuelScanner.cpp new file mode 100644 index 0000000000..9b6889871a --- /dev/null +++ b/Source/Core/Core/IOS/USB/Emulated/DuelScanner.cpp @@ -0,0 +1,472 @@ +// Copyright 2025 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "Core/Host.h" +#include "Core/HW/Memmap.h" +#include "Core/IOS/USB/Emulated/DuelScanner.h" +#include "Core/System.h" + +namespace IOS::HLE::USB +{ + +const u8 usb_config_desc[] = { + 0x09, 0x02, 0x1f, 0x02, 0x02, 0x01, 0x30, 0x80, 0xfa, 0x08, 0x0b, 0x00, 0x02, 0x0e, 0x03, 0x00, + 0x60, 0x09, 0x04, 0x00, 0x00, 0x01, 0x0e, 0x01, 0x00, 0x60, 0x0d, 0x24, 0x01, 0x00, 0x01, 0x4d, + 0x00, 0xc0, 0xe1, 0xe4, 0x00, 0x01, 0x01, 0x09, 0x24, 0x03, 0x02, 0x01, 0x01, 0x00, 0x04, 0x00, + 0x1a, 0x24, 0x06, 0x04, 0xf0, 0x77, 0x35, 0xd1, 0x89, 0x8d, 0x00, 0x47, 0x81, 0x2e, 0x7d, 0xd5, + 0xe2, 0xfd, 0xb8, 0x98, 0x08, 0x01, 0x03, 0x01, 0xff, 0x00, 0x12, 0x24, 0x02, 0x01, 0x01, 0x02, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, + // 0x00, 0x02, 0x00, + 0x00, 0x00, 0x00, + 0x0b, 0x24, 0x05, 0x03, + 0x01, 0x00, 0x00, 0x02, + // 0x7f, 0x05, + 0x00, 0x00, + 0x00, 0x07, 0x05, 0x82, 0x03, 0x10, 0x00, 0x06, 0x05, 0x25, + 0x03, 0x10, 0x00, 0x09, 0x04, 0x01, 0x00, 0x00, 0x0e, 0x02, 0x00, 0x00, 0x0e, 0x24, 0x01, 0x01, + 0x43, 0x01, 0x81, 0x00, 0x02, 0x02, 0x01, 0x00, 0x01, 0x00, 0x1b, 0x24, 0x04, 0x01, 0x05, 0x59, + 0x55, 0x59, 0x32, 0x00, 0x00, 0x10, 0x00, 0x80, 0x00, 0x00, 0xaa, 0x00, 0x38, 0x9b, 0x71, 0x10, + 0x01, 0x00, 0x00, 0x00, 0x00, 0x32, 0x24, 0x05, 0x01, 0x00, 0x80, 0x02, 0xe0, 0x01, 0x00, 0x60, + 0x09, 0x00, 0x00, 0x40, 0x19, 0x01, 0x00, 0x60, 0x09, 0x00, 0x15, 0x16, 0x05, 0x00, 0x06, 0x15, + 0x16, 0x05, 0x00, 0x20, 0xa1, 0x07, 0x00, 0x2a, 0x2c, 0x0a, 0x00, 0x40, 0x42, 0x0f, 0x00, 0x80, + 0x84, 0x1e, 0x00, 0x80, 0x96, 0x98, 0x00, 0x32, 0x24, 0x05, 0x02, 0x00, 0x40, 0x01, 0xf0, 0x00, + 0x00, 0x58, 0x02, 0x00, 0x00, 0x50, 0x46, 0x00, 0x00, 0x58, 0x02, 0x00, 0x15, 0x16, 0x05, 0x00, + 0x06, 0x15, 0x16, 0x05, 0x00, 0x20, 0xa1, 0x07, 0x00, 0x2a, 0x2c, 0x0a, 0x00, 0x40, 0x42, 0x0f, + 0x00, 0x80, 0x84, 0x0f, 0x00, 0x80, 0x96, 0x98, 0x00, 0x32, 0x24, 0x05, 0x03, 0x00, 0xa0, 0x00, + 0x78, 0x00, 0x00, 0x96, 0x00, 0x00, 0x00, 0x94, 0x11, 0x00, 0x00, 0x96, 0x00, 0x00, 0x15, 0x16, + 0x05, 0x00, 0x06, 0x15, 0x16, 0x05, 0x00, 0x20, 0xa1, 0x07, 0x00, 0x2a, 0x2c, 0x0a, 0x00, 0x40, + 0x42, 0x0f, 0x00, 0x80, 0x84, 0x0f, 0x00, 0x80, 0x96, 0x98, 0x00, 0x32, 0x24, 0x05, 0x04, 0x00, + 0xb0, 0x00, 0x90, 0x00, 0x00, 0xc6, 0x00, 0x00, 0x00, 0x34, 0x17, 0x00, 0x00, 0xc6, 0x00, 0x00, + 0x15, 0x16, 0x05, 0x00, 0x06, 0x15, 0x16, 0x05, 0x00, 0x20, 0xa1, 0x07, 0x00, 0x2a, 0x2c, 0x0a, + 0x00, 0x40, 0x42, 0x0f, 0x00, 0x80, 0x84, 0x0f, 0x00, 0x80, 0x96, 0x98, 0x00, 0x32, 0x24, 0x05, + 0x05, 0x00, 0x60, 0x01, 0x20, 0x01, 0x00, 0x18, 0x03, 0x00, 0x00, 0xd0, 0x5c, 0x00, 0x00, 0x18, + 0x03, 0x00, 0x15, 0x16, 0x05, 0x00, 0x06, 0x15, 0x16, 0x05, 0x00, 0x20, 0xa1, 0x07, 0x00, 0x2a, + 0x2c, 0x0a, 0x00, 0x40, 0x42, 0x0f, 0x00, 0x80, 0x84, 0x0f, 0x00, 0x80, 0x96, 0x98, 0x00, 0x1a, + 0x24, 0x03, 0x00, 0x05, 0x80, 0x02, 0xe0, 0x01, 0x40, 0x01, 0xf0, 0x00, 0xa0, 0x00, 0x78, 0x00, + 0xb0, 0x00, 0x90, 0x00, 0x60, 0x01, 0x20, 0x01, 0x00, 0x06, 0x24, 0x0d, 0x01, 0x01, 0x04, 0x09, + 0x04, 0x01, 0x01, 0x01, 0x0e, 0x02, 0x00, 0x00, 0x07, 0x05, 0x81, 0x05, 0x60, 0x0a, 0x01, 0x09, + 0x04, 0x01, 0x02, 0x01, 0x0e, 0x02, 0x00, 0x00, 0x07, 0x05, 0x81, 0x05, 0x00, 0x0b, 0x01, 0x09, + 0x04, 0x01, 0x03, 0x01, 0x0e, 0x02, 0x00, 0x00, 0x07, 0x05, 0x81, 0x05, 0x20, 0x0b, 0x01, 0x09, + 0x04, 0x01, 0x04, 0x01, 0x0e, 0x02, 0x00, 0x00, 0x07, 0x05, 0x81, 0x05, 0x00, 0x13, 0x01, 0x09, + 0x04, 0x01, 0x05, 0x01, 0x0e, 0x02, 0x00, 0x00, 0x07, 0x05, 0x81, 0x05, 0x20, 0x13, 0x01, 0x09, + 0x04, 0x01, 0x06, 0x01, 0x0e, 0x02, 0x00, 0x00, 0x07, 0x05, 0x81, 0x05, 0xfc, 0x13, 0x01 +}; + +DeviceDescriptor DuelScanner::s_device_descriptor{ + .bLength = 0x12, + .bDescriptorType = 0x01, + .bcdUSB = 0x0200, + .bDeviceClass = 0xef, + .bDeviceSubClass = 0x02, + .bDeviceProtocol = 0x01, + .bMaxPacketSize0 = 0x40, + .idVendor = 0x057e, + .idProduct = 0x030d, + .bcdDevice = 0x0705, + .iManufacturer = 0x30, + .iProduct = 0x60, + .iSerialNumber = 0x00, + .bNumConfigurations = 0x01 +}; +std::vector DuelScanner::s_config_descriptor{ + { + .bLength = 0x09, + .bDescriptorType = 0x02, + .wTotalLength = 0x021f, + .bNumInterfaces = 0x02, + .bConfigurationValue = 0x01, + .iConfiguration = 0x30, + .bmAttributes = 0x80, + .MaxPower = 0xfa, + } +}; +std::vector DuelScanner::s_interface_descriptor{ + { + .bLength = 0x09, + .bDescriptorType = 0x04, + .bInterfaceNumber = 0x00, + .bAlternateSetting = 0x00, + .bNumEndpoints = 0x01, + .bInterfaceClass = 0x0e, + .bInterfaceSubClass = 0x01, + .bInterfaceProtocol = 0x00, + .iInterface = 0x60, + }, + { + .bLength = 0x09, + .bDescriptorType = 0x04, + .bInterfaceNumber = 0x01, + .bAlternateSetting = 0x00, + .bNumEndpoints = 0x00, + .bInterfaceClass = 0x0e, + .bInterfaceSubClass = 0x02, + .bInterfaceProtocol = 0x00, + .iInterface = 0x00, + }, + { + .bLength = 0x09, + .bDescriptorType = 0x04, + .bInterfaceNumber = 0x01, + .bAlternateSetting = 0x01, + .bNumEndpoints = 0x01, + .bInterfaceClass = 0x0e, + .bInterfaceSubClass = 0x02, + .bInterfaceProtocol = 0x00, + .iInterface = 0x00, + }, + { + .bLength = 0x09, + .bDescriptorType = 0x04, + .bInterfaceNumber = 0x01, + .bAlternateSetting = 0x02, + .bNumEndpoints = 0x01, + .bInterfaceClass = 0x0e, + .bInterfaceSubClass = 0x02, + .bInterfaceProtocol = 0x00, + .iInterface = 0x00, + }, + { + .bLength = 0x09, + .bDescriptorType = 0x04, + .bInterfaceNumber = 0x01, + .bAlternateSetting = 0x03, + .bNumEndpoints = 0x01, + .bInterfaceClass = 0x0e, + .bInterfaceSubClass = 0x02, + .bInterfaceProtocol = 0x00, + .iInterface = 0x00, + }, + { + .bLength = 0x09, + .bDescriptorType = 0x04, + .bInterfaceNumber = 0x01, + .bAlternateSetting = 0x04, + .bNumEndpoints = 0x01, + .bInterfaceClass = 0x0e, + .bInterfaceSubClass = 0x02, + .bInterfaceProtocol = 0x00, + .iInterface = 0x00, + }, + { + .bLength = 0x09, + .bDescriptorType = 0x04, + .bInterfaceNumber = 0x01, + .bAlternateSetting = 0x05, + .bNumEndpoints = 0x01, + .bInterfaceClass = 0x0e, + .bInterfaceSubClass = 0x02, + .bInterfaceProtocol = 0x00, + .iInterface = 0x00, + }, + { + .bLength = 0x09, + .bDescriptorType = 0x04, + .bInterfaceNumber = 0x01, + .bAlternateSetting = 0x06, + .bNumEndpoints = 0x01, + .bInterfaceClass = 0x0e, + .bInterfaceSubClass = 0x02, + .bInterfaceProtocol = 0x00, + .iInterface = 0x00, + } +}; +std::vector DuelScanner::s_endpoint_descriptor{ + { + .bLength = 0x07, + .bDescriptorType = 0x05, + .bEndpointAddress = 0x82, + .bmAttributes = 0x03, + .wMaxPacketSize = 0x0010, + .bInterval = 0x06, + }, + { + .bLength = 0x07, + .bDescriptorType = 0x05, + .bEndpointAddress = 0x81, + .bmAttributes = 0x05, + .wMaxPacketSize = 0x0a60, + .bInterval = 0x01, + }, + { + .bLength = 0x07, + .bDescriptorType = 0x05, + .bEndpointAddress = 0x81, + .bmAttributes = 0x05, + .wMaxPacketSize = 0x0b00, + .bInterval = 0x01, + }, + { + .bLength = 0x07, + .bDescriptorType = 0x05, + .bEndpointAddress = 0x81, + .bmAttributes = 0x05, + .wMaxPacketSize = 0x0b20, + .bInterval = 0x01, + }, + { + .bLength = 0x07, + .bDescriptorType = 0x05, + .bEndpointAddress = 0x81, + .bmAttributes = 0x05, + .wMaxPacketSize = 0x1300, + .bInterval = 0x01, + }, + { + .bLength = 0x07, + .bDescriptorType = 0x05, + .bEndpointAddress = 0x81, + .bmAttributes = 0x05, + .wMaxPacketSize = 0x1320, + .bInterval = 0x01, + }, + { + .bLength = 0x07, + .bDescriptorType = 0x05, + .bEndpointAddress = 0x81, + .bmAttributes = 0x05, + .wMaxPacketSize = 0x13fc, + .bInterval = 0x01, + } +}; + +DuelScanner::DuelScanner(EmulationKernel& ios) : m_ios(ios) +{ + m_id = (u64(m_vid) << 32 | u64(m_pid) << 16 | u64(9) << 8 | u64(1)); +} + +DuelScanner::~DuelScanner() { + if (m_active_altsetting) + { + NOTICE_LOG_FMT(IOS_USB, "Host_CameraStop"); + Host_CameraStop(); + } +} + +DeviceDescriptor DuelScanner::GetDeviceDescriptor() const +{ + return s_device_descriptor; +} + +std::vector DuelScanner::GetConfigurations() const +{ + return s_config_descriptor; +} + +std::vector DuelScanner::GetInterfaces(u8 config) const +{ + return s_interface_descriptor; +} + +std::vector DuelScanner::GetEndpoints(u8 config, u8 interface, u8 alt) const +{ + std::vector ret; + if (interface == 0) + ret.push_back(s_endpoint_descriptor[0]); + else if (interface == 1 && alt > 0) + ret.push_back(s_endpoint_descriptor[alt]); + return ret; +} + +bool DuelScanner::Attach() +{ + //NOTICE_LOG_FMT(IOS_USB, "[{:04x}:{:04x}] Opening device", m_vid, m_pid); + return true; +} + +bool DuelScanner::AttachAndChangeInterface(const u8 interface) +{ + if (!Attach()) + return false; + + if (interface != m_active_interface) + return ChangeInterface(interface) == 0; + + return true; +} + +int DuelScanner::CancelTransfer(const u8 endpoint) +{ + INFO_LOG_FMT(IOS_USB, "[{:04x}:{:04x} {}] Cancelling transfers (endpoint {:#x})", m_vid, m_pid, + m_active_interface, endpoint); + return IPC_SUCCESS; +} + +int DuelScanner::ChangeInterface(const u8 interface) +{ + NOTICE_LOG_FMT(IOS_USB, "[{:04x}:{:04x} {}] Changing interface to {}", m_vid, m_pid, m_active_interface, interface); + m_active_interface = interface; + return 0; +} + +int DuelScanner::GetNumberOfAltSettings(u8 interface) +{ + NOTICE_LOG_FMT(IOS_USB, "[{:04x}:{:04x}] GetNumberOfAltSettings: interface={:02x}", m_vid, m_pid, interface); + return (interface == 1) ? 7 : 1; +} + +int DuelScanner::SetAltSetting(u8 alt_setting) +{ + NOTICE_LOG_FMT(IOS_USB, "[{:04x}:{:04x}] SetAltSetting: alt_setting={:02x}", m_vid, m_pid, alt_setting); + m_active_altsetting = alt_setting; + if (alt_setting) + { + NOTICE_LOG_FMT(IOS_USB, "Host_CameraStart({}x{})", m_active_size.width, m_active_size.height); + auto& system = m_ios.GetSystem(); + system.GetCameraBase().CreateSample(m_active_size.width, m_active_size.height); + Host_CameraStart(m_active_size.width, m_active_size.height); + } + else + { + NOTICE_LOG_FMT(IOS_USB, "Host_CameraStop"); + Host_CameraStop(); + } + return 0; +} + +int DuelScanner::SubmitTransfer(std::unique_ptr cmd) +{ + switch ((cmd->request_type << 8) | cmd->request) + { + case USBHDR(DIR_DEVICE2HOST, TYPE_STANDARD, REC_DEVICE, REQUEST_GET_DESCRIPTOR): // 0x80 0x06 + { + std::vector control_response(usb_config_desc, usb_config_desc + sizeof(usb_config_desc)); + ScheduleTransfer(std::move(cmd), control_response, 0); + break; + } + case USBHDR(DIR_HOST2DEVICE, TYPE_CLASS, REC_INTERFACE, SET_CUR): // 0x21 0x01 + { + u8 unit = cmd->index >> 8; + u8 control = cmd->value >> 8; + NOTICE_LOG_FMT(IOS_USB, "[{:04x}:{:04x} {}] Control: bRequestType={:02x} bRequest={:02x} wValue={:04x} wIndex={:04x} wLength={:04x} // {} / {}", + m_vid, m_pid, m_active_interface, cmd->request_type, cmd->request, cmd->value, cmd->index, cmd->length, + CameraBase::getUVCRequest(cmd->request), + (unit == 0) ? CameraBase::getUVCVideoStreamingControl(control) + : (unit == 1) ? CameraBase::getUVCTerminalControl(control) + : (unit == 3) ? CameraBase::getUVCProcessingUnitControl(control) + : ""); + if (unit == 0 && control == VS_COMMIT) + { + auto& system = m_ios.GetSystem(); + auto& memory = system.GetMemory(); + UVCProbeCommitControl* commit = (UVCProbeCommitControl*) memory.GetPointerForRange(cmd->data_address, cmd->length); + m_active_size = m_supported_sizes[commit->bFrameIndex - 1]; + m_delay = commit->dwFrameInterval / 10; + u32 new_size = m_active_size.width * m_active_size.height * 2; + if (m_image_size != new_size) + { + m_image_size = new_size; + if (m_image_data) + { + free(m_image_data); + } + m_image_data = (u8*) calloc(1, m_image_size); + } + NOTICE_LOG_FMT(IOS_USB, "[{:04x}:{:04x}] VS_COMMIT: bFormatIndex={:02x} bFrameIndex={:02x} dwFrameInterval={:04x} / size={}x{} delay={}", + m_vid, m_pid, commit->bFormatIndex, commit->bFrameIndex, commit->dwFrameInterval, + m_active_size.width, m_active_size.height, + m_delay + ); + } + std::vector control_response = {}; + ScheduleTransfer(std::move(cmd), control_response, 0); + break; + } + case USBHDR(DIR_DEVICE2HOST, TYPE_CLASS, REC_INTERFACE, GET_CUR): // 0xa1 0x81 + case USBHDR(DIR_DEVICE2HOST, TYPE_CLASS, REC_INTERFACE, GET_MIN): // 0xa1 0x82 + case USBHDR(DIR_DEVICE2HOST, TYPE_CLASS, REC_INTERFACE, GET_MAX): // 0xa1 0x83 + case USBHDR(DIR_DEVICE2HOST, TYPE_CLASS, REC_INTERFACE, GET_RES): // 0xa1 0x84 + case USBHDR(DIR_DEVICE2HOST, TYPE_CLASS, REC_INTERFACE, GET_LEN): // 0xa1 0x85 + case USBHDR(DIR_DEVICE2HOST, TYPE_CLASS, REC_INTERFACE, GET_INF): // 0xa1 0x86 + case USBHDR(DIR_DEVICE2HOST, TYPE_CLASS, REC_INTERFACE, GET_DEF): // 0xa1 0x87 + { + u8 unit = cmd->index >> 8; + NOTICE_LOG_FMT(IOS_USB, "[{:04x}:{:04x} {}] Control: bRequestType={:02x} bRequest={:02x} wValue={:04x} wIndex={:04x} wLength={:04x} // {} / {}", + m_vid, m_pid, m_active_interface, cmd->request_type, cmd->request, cmd->value, cmd->index, cmd->length, + CameraBase::getUVCRequest(cmd->request), + (unit == 0) ? CameraBase::getUVCVideoStreamingControl(cmd->value >> 8) + : (unit == 1) ? CameraBase::getUVCTerminalControl(cmd->value >> 8) + : (unit == 3) ? CameraBase::getUVCProcessingUnitControl(cmd->value >> 8) + : ""); + std::vector control_response = {}; + ScheduleTransfer(std::move(cmd), control_response, 0); + break; + } + default: + { + NOTICE_LOG_FMT(IOS_USB, "[{:04x}:{:04x} {}] Control: bRequestType={:02x} bRequest={:02x} wValue={:04x} wIndex={:04x} wLength={:04x}", + m_vid, m_pid, m_active_interface, cmd->request_type, cmd->request, cmd->value, cmd->index, cmd->length); + std::vector control_response = {}; + ScheduleTransfer(std::move(cmd), control_response, 0); + } + } + return IPC_SUCCESS; +} + +int DuelScanner::SubmitTransfer(std::unique_ptr cmd) +{ + NOTICE_LOG_FMT(IOS_USB, "[{:04x}:{:04x} {}] Bulk: length={:04x} endpoint={:02x}", m_vid, m_pid, + m_active_interface, cmd->length, cmd->endpoint); + return IPC_SUCCESS; +} + +int DuelScanner::SubmitTransfer(std::unique_ptr cmd) +{ + NOTICE_LOG_FMT(IOS_USB, "[{:04x}:{:04x} {}] Interrupt: length={:04x} endpoint={:02x}", m_vid, + m_pid, m_active_interface, cmd->length, cmd->endpoint); + return IPC_SUCCESS; +} + +int DuelScanner::SubmitTransfer(std::unique_ptr cmd) +{ + auto& system = m_ios.GetSystem(); + auto& memory = system.GetMemory(); + u8* iso_buffer = memory.GetPointerForRange(cmd->data_address, cmd->length); + if (!iso_buffer) + { + ERROR_LOG_FMT(IOS_USB, "DuelScanner iso buf error"); + return IPC_EINVAL; + } + //ERROR_LOG_FMT(IOS_USB, "cmd->length = 0x{:02x} / 0x{:02x}", cmd->length, cmd->packet_sizes[0]); + + u8* iso_buffer_pos = iso_buffer; + + for (std::size_t i = 0; i < cmd->num_packets; i++) + { + UVCHeader uvc_header{}; + uvc_header.bHeaderLength = sizeof(UVCHeader); + uvc_header.endOfHeader = 1; + uvc_header.frameId = m_frame_id; + + u32 data_size = std::min(cmd->packet_sizes[i] - (u32)sizeof(uvc_header), m_image_size - m_image_pos); + if (data_size > 0 && m_image_pos + data_size == m_image_size) + { + m_frame_id ^= 1; + uvc_header.endOfFrame = 1; + } + std::memcpy(iso_buffer_pos, &uvc_header, sizeof(uvc_header)); + if (data_size > 0) + { + std::memcpy(iso_buffer_pos + sizeof(uvc_header), m_image_data + m_image_pos, data_size); + } + m_image_pos += data_size; + iso_buffer_pos += sizeof(uvc_header) + data_size; + cmd->SetPacketReturnValue(i, (u32)sizeof(uvc_header) + data_size); + } + + if (m_image_pos == m_image_size) + { + system.GetCameraBase().GetData(m_image_data, m_image_size); + m_image_pos = 0; + } + + cmd->ScheduleTransferCompletion(IPC_SUCCESS, m_delay); + return IPC_SUCCESS; +} + +void DuelScanner::ScheduleTransfer(std::unique_ptr command, + const std::vector& data, u64 expected_time_us) +{ + command->FillBuffer(data.data(), static_cast(data.size())); + command->ScheduleTransferCompletion(static_cast(data.size()), expected_time_us); +} +} // namespace IOS::HLE::USB diff --git a/Source/Core/Core/IOS/USB/Emulated/DuelScanner.h b/Source/Core/Core/IOS/USB/Emulated/DuelScanner.h new file mode 100644 index 0000000000..ec42704c92 --- /dev/null +++ b/Source/Core/Core/IOS/USB/Emulated/DuelScanner.h @@ -0,0 +1,55 @@ +// Copyright 2025 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include "Core/IOS/USB/Common.h" +#include "Core/IOS/USB/Emulated/CameraBase.h" + +namespace IOS::HLE::USB +{ + +class DuelScanner final : public Device +{ +public: + DuelScanner(EmulationKernel& ios); + ~DuelScanner() override; + DeviceDescriptor GetDeviceDescriptor() const override; + std::vector GetConfigurations() const override; + std::vector GetInterfaces(u8 config) const override; + std::vector GetEndpoints(u8 config, u8 interface, u8 alt) const override; + bool Attach() override; + bool AttachAndChangeInterface(u8 interface) override; + int CancelTransfer(u8 endpoint) override; + int ChangeInterface(u8 interface) override; + int GetNumberOfAltSettings(u8 interface) override; + int SetAltSetting(u8 alt_setting) override; + int SubmitTransfer(std::unique_ptr message) override; + int SubmitTransfer(std::unique_ptr message) override; + int SubmitTransfer(std::unique_ptr message) override; + int SubmitTransfer(std::unique_ptr message) override; + +private: + void ScheduleTransfer(std::unique_ptr command, const std::vector& data, + u64 expected_time_us); + + static DeviceDescriptor s_device_descriptor; + static std::vector s_config_descriptor; + static std::vector s_interface_descriptor; + static std::vector s_endpoint_descriptor; + + EmulationKernel& m_ios; + const u16 m_vid = 0x057e; + const u16 m_pid = 0x030d; + u8 m_active_interface = 0; + u8 m_active_altsetting = 0; + const struct UVCImageSize m_supported_sizes[5] = {{640, 480}, {320, 240}, {160, 120}, {176, 144}, {352, 288}}; + struct UVCImageSize m_active_size; + u32 m_delay = 0; + u32 m_image_size = 0; + u32 m_image_pos = 0; + u8 *m_image_data = nullptr; + bool m_frame_id = 0; +}; +} // namespace IOS::HLE::USB diff --git a/Source/Core/Core/IOS/USB/Emulated/MotionCamera.cpp b/Source/Core/Core/IOS/USB/Emulated/MotionCamera.cpp new file mode 100644 index 0000000000..c2c64a750d --- /dev/null +++ b/Source/Core/Core/IOS/USB/Emulated/MotionCamera.cpp @@ -0,0 +1,487 @@ +// Copyright 2025 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "Core/Host.h" +#include "Core/HW/Memmap.h" +#include "Core/IOS/USB/Emulated/MotionCamera.h" +#include "Core/System.h" + +namespace IOS::HLE::USB +{ + +const u8 usb_config_desc[] = { + 0x09, 0x02, 0x09, 0x03, 0x02, 0x01, 0x30, 0x80, 0xfa, 0x08, 0x0b, 0x00, 0x02, 0x0e, 0x03, 0x00, + 0x60, 0x09, 0x04, 0x00, 0x00, 0x01, 0x0e, 0x01, 0x00, 0x60, 0x0d, 0x24, 0x01, 0x00, 0x01, 0x4d, + 0x00, 0xc0, 0xe1, 0xe4, 0x00, 0x01, 0x01, 0x09, 0x24, 0x03, 0x02, 0x01, 0x01, 0x00, 0x04, 0x00, + 0x1a, 0x24, 0x06, 0x04, 0xf0, 0x77, 0x35, 0xd1, 0x89, 0x8d, 0x00, 0x47, 0x81, 0x2e, 0x7d, 0xd5, + 0xe2, 0xfd, 0xb8, 0x98, 0x08, 0x01, 0x03, 0x01, 0xff, 0x00, 0x12, 0x24, 0x02, 0x01, 0x01, 0x02, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, + // 0x0A, 0x02, 0x00, // patch bmControls to avoid unnecessary requests + 0x00, 0x00, 0x00, + 0x0b, 0x24, 0x05, 0x03, + 0x01, 0x00, 0x00, 0x02, + // 0x7F, 0x15, // patch bmControls to avoid unnecessary requests + 0x00, 0x00, + 0x00, 0x07, 0x05, 0x82, 0x03, 0x10, 0x00, 0x06, 0x05, 0x25, + 0x03, 0x10, 0x00, 0x09, 0x04, 0x01, 0x00, 0x00, 0x0e, 0x02, 0x00, 0x00, 0x0f, 0x24, 0x01, 0x02, + 0x2d, 0x02, 0x81, 0x00, 0x02, 0x02, 0x01, 0x00, 0x01, 0x00, 0x00, 0x0b, 0x24, 0x06, 0x01, 0x05, + 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x26, 0x24, 0x07, 0x01, 0x00, 0x80, 0x02, 0xe0, 0x01, 0x00, + 0xf4, 0x01, 0x00, 0x00, 0xc0, 0xa8, 0x00, 0x00, 0x08, 0x07, 0x00, 0x15, 0x16, 0x05, 0x00, 0x00, + 0x15, 0x16, 0x05, 0x00, 0x76, 0x96, 0x98, 0x00, 0x15, 0x16, 0x05, 0x00, 0x26, 0x24, 0x07, 0x02, + 0x00, 0x40, 0x01, 0xf0, 0x00, 0x00, 0xf4, 0x01, 0x00, 0x00, 0x30, 0x2a, 0x00, 0x00, 0xc2, 0x01, + 0x00, 0x15, 0x16, 0x05, 0x00, 0x00, 0x15, 0x16, 0x05, 0x00, 0x76, 0x96, 0x98, 0x00, 0x15, 0x16, + 0x05, 0x00, 0x26, 0x24, 0x07, 0x03, 0x00, 0xa0, 0x00, 0x78, 0x00, 0x00, 0xf4, 0x01, 0x00, 0x00, + 0x8c, 0x0a, 0x00, 0x80, 0x70, 0x00, 0x00, 0x15, 0x16, 0x05, 0x00, 0x00, 0x15, 0x16, 0x05, 0x00, + 0x76, 0x96, 0x98, 0x00, 0x15, 0x16, 0x05, 0x00, 0x26, 0x24, 0x07, 0x04, 0x00, 0xb0, 0x00, 0x90, + 0x00, 0x00, 0xf4, 0x01, 0x00, 0x00, 0xec, 0x0d, 0x00, 0x80, 0x94, 0x00, 0x00, 0x15, 0x16, 0x05, + 0x00, 0x00, 0x15, 0x16, 0x05, 0x00, 0x76, 0x96, 0x98, 0x00, 0x15, 0x16, 0x05, 0x00, 0x26, 0x24, + 0x07, 0x05, 0x00, 0x60, 0x01, 0x20, 0x01, 0x00, 0xf4, 0x01, 0x00, 0x00, 0xb0, 0x37, 0x00, 0x00, + 0x52, 0x02, 0x00, 0x15, 0x16, 0x05, 0x00, 0x00, 0x15, 0x16, 0x05, 0x00, 0x76, 0x96, 0x98, 0x00, + 0x15, 0x16, 0x05, 0x00, 0x1a, 0x24, 0x03, 0x00, 0x05, 0x80, 0x02, 0xe0, 0x01, 0x40, 0x01, 0xf0, + 0x00, 0xa0, 0x00, 0x78, 0x00, 0xb0, 0x00, 0x90, 0x00, 0x60, 0x01, 0x20, 0x01, 0x00, 0x06, 0x24, + 0x0d, 0x01, 0x01, 0x04, 0x1b, 0x24, 0x04, 0x02, 0x05, 0x59, 0x55, 0x59, 0x32, 0x00, 0x00, 0x10, + 0x00, 0x80, 0x00, 0x00, 0xaa, 0x00, 0x38, 0x9b, 0x71, 0x10, 0x01, 0x00, 0x00, 0x00, 0x00, 0x32, + 0x24, 0x05, 0x01, 0x00, 0x80, 0x02, 0xe0, 0x01, 0x00, 0x60, 0x09, 0x00, 0x00, 0x40, 0x19, 0x01, + 0x00, 0x60, 0x09, 0x00, 0x15, 0x16, 0x05, 0x00, 0x06, 0x15, 0x16, 0x05, 0x00, 0x20, 0xa1, 0x07, + 0x00, 0x2a, 0x2c, 0x0a, 0x00, 0x40, 0x42, 0x0f, 0x00, 0x80, 0x84, 0x1e, 0x00, 0x80, 0x96, 0x98, + 0x00, 0x32, 0x24, 0x05, 0x02, 0x00, 0x40, 0x01, 0xf0, 0x00, 0x00, 0x58, 0x02, 0x00, 0x00, 0x50, + 0x46, 0x00, 0x00, 0x58, 0x02, 0x00, 0x15, 0x16, 0x05, 0x00, 0x06, 0x15, 0x16, 0x05, 0x00, 0x20, + 0xa1, 0x07, 0x00, 0x2a, 0x2c, 0x0a, 0x00, 0x40, 0x42, 0x0f, 0x00, 0x80, 0x84, 0x0f, 0x00, 0x80, + 0x96, 0x98, 0x00, 0x32, 0x24, 0x05, 0x03, 0x00, 0xa0, 0x00, 0x78, 0x00, 0x00, 0x96, 0x00, 0x00, + 0x00, 0x94, 0x11, 0x00, 0x00, 0x96, 0x00, 0x00, 0x15, 0x16, 0x05, 0x00, 0x06, 0x15, 0x16, 0x05, + 0x00, 0x20, 0xa1, 0x07, 0x00, 0x2a, 0x2c, 0x0a, 0x00, 0x40, 0x42, 0x0f, 0x00, 0x80, 0x84, 0x0f, + 0x00, 0x80, 0x96, 0x98, 0x00, 0x32, 0x24, 0x05, 0x04, 0x00, 0xb0, 0x00, 0x90, 0x00, 0x00, 0xc6, + 0x00, 0x00, 0x00, 0x34, 0x17, 0x00, 0x00, 0xc6, 0x00, 0x00, 0x15, 0x16, 0x05, 0x00, 0x06, 0x15, + 0x16, 0x05, 0x00, 0x20, 0xa1, 0x07, 0x00, 0x2a, 0x2c, 0x0a, 0x00, 0x40, 0x42, 0x0f, 0x00, 0x80, + 0x84, 0x0f, 0x00, 0x80, 0x96, 0x98, 0x00, 0x32, 0x24, 0x05, 0x05, 0x00, 0x60, 0x01, 0x20, 0x01, + 0x00, 0x18, 0x03, 0x00, 0x00, 0xd0, 0x5c, 0x00, 0x00, 0x18, 0x03, 0x00, 0x15, 0x16, 0x05, 0x00, + 0x06, 0x15, 0x16, 0x05, 0x00, 0x20, 0xa1, 0x07, 0x00, 0x2a, 0x2c, 0x0a, 0x00, 0x40, 0x42, 0x0f, + 0x00, 0x80, 0x84, 0x0f, 0x00, 0x80, 0x96, 0x98, 0x00, 0x1a, 0x24, 0x03, 0x00, 0x05, 0x80, 0x02, + 0xe0, 0x01, 0x40, 0x01, 0xf0, 0x00, 0xa0, 0x00, 0x78, 0x00, 0xb0, 0x00, 0x90, 0x00, 0x60, 0x01, + 0x20, 0x01, 0x00, 0x06, 0x24, 0x0d, 0x01, 0x01, 0x04, 0x09, 0x04, 0x01, 0x01, 0x01, 0x0e, 0x02, + 0x00, 0x00, 0x07, 0x05, 0x81, 0x05, 0x60, 0x0a, 0x01, 0x09, 0x04, 0x01, 0x02, 0x01, 0x0e, 0x02, + 0x00, 0x00, 0x07, 0x05, 0x81, 0x05, 0x00, 0x0b, 0x01, 0x09, 0x04, 0x01, 0x03, 0x01, 0x0e, 0x02, + 0x00, 0x00, 0x07, 0x05, 0x81, 0x05, 0x20, 0x0b, 0x01, 0x09, 0x04, 0x01, 0x04, 0x01, 0x0e, 0x02, + 0x00, 0x00, 0x07, 0x05, 0x81, 0x05, 0x00, 0x13, 0x01, 0x09, 0x04, 0x01, 0x05, 0x01, 0x0e, 0x02, + 0x00, 0x00, 0x07, 0x05, 0x81, 0x05, 0x20, 0x13, 0x01, 0x09, 0x04, 0x01, 0x06, 0x01, 0x0e, 0x02, + 0x00, 0x00, 0x07, 0x05, 0x81, 0x05, 0xfc, 0x13, 0x01 +}; + +DeviceDescriptor MotionCamera::s_device_descriptor{ + .bLength = 0x12, + .bDescriptorType = 0x01, + .bcdUSB = 0x0200, + .bDeviceClass = 0xef, + .bDeviceSubClass = 0x02, + .bDeviceProtocol = 0x01, + .bMaxPacketSize0 = 0x40, + .idVendor = 0x057e, + .idProduct = 0x030a, + .bcdDevice = 0x0924, + .iManufacturer = 0x30, + .iProduct = 0x60, + .iSerialNumber = 0x00, + .bNumConfigurations = 0x01 +}; +std::vector MotionCamera::s_config_descriptor{ + { + .bLength = 0x09, + .bDescriptorType = 0x02, + .wTotalLength = 0x0309, + .bNumInterfaces = 0x02, + .bConfigurationValue = 0x01, + .iConfiguration = 0x30, + .bmAttributes = 0x80, + .MaxPower = 0xfa, + } +}; +std::vector MotionCamera::s_interface_descriptor{ + { + .bLength = 0x09, + .bDescriptorType = 0x04, + .bInterfaceNumber = 0x00, + .bAlternateSetting = 0x00, + .bNumEndpoints = 0x01, + .bInterfaceClass = 0x0e, + .bInterfaceSubClass = 0x01, + .bInterfaceProtocol = 0x00, + .iInterface = 0x60, + }, + { + .bLength = 0x09, + .bDescriptorType = 0x04, + .bInterfaceNumber = 0x01, + .bAlternateSetting = 0x00, + .bNumEndpoints = 0x00, + .bInterfaceClass = 0x0e, + .bInterfaceSubClass = 0x02, + .bInterfaceProtocol = 0x00, + .iInterface = 0x00, + }, + { + .bLength = 0x09, + .bDescriptorType = 0x04, + .bInterfaceNumber = 0x01, + .bAlternateSetting = 0x01, + .bNumEndpoints = 0x01, + .bInterfaceClass = 0x0e, + .bInterfaceSubClass = 0x02, + .bInterfaceProtocol = 0x00, + .iInterface = 0x00, + }, + { + .bLength = 0x09, + .bDescriptorType = 0x04, + .bInterfaceNumber = 0x01, + .bAlternateSetting = 0x02, + .bNumEndpoints = 0x01, + .bInterfaceClass = 0x0e, + .bInterfaceSubClass = 0x02, + .bInterfaceProtocol = 0x00, + .iInterface = 0x00, + }, + { + .bLength = 0x09, + .bDescriptorType = 0x04, + .bInterfaceNumber = 0x01, + .bAlternateSetting = 0x03, + .bNumEndpoints = 0x01, + .bInterfaceClass = 0x0e, + .bInterfaceSubClass = 0x02, + .bInterfaceProtocol = 0x00, + .iInterface = 0x00, + }, + { + .bLength = 0x09, + .bDescriptorType = 0x04, + .bInterfaceNumber = 0x01, + .bAlternateSetting = 0x04, + .bNumEndpoints = 0x01, + .bInterfaceClass = 0x0e, + .bInterfaceSubClass = 0x02, + .bInterfaceProtocol = 0x00, + .iInterface = 0x00, + }, + { + .bLength = 0x09, + .bDescriptorType = 0x04, + .bInterfaceNumber = 0x01, + .bAlternateSetting = 0x05, + .bNumEndpoints = 0x01, + .bInterfaceClass = 0x0e, + .bInterfaceSubClass = 0x02, + .bInterfaceProtocol = 0x00, + .iInterface = 0x00, + }, + { + .bLength = 0x09, + .bDescriptorType = 0x04, + .bInterfaceNumber = 0x01, + .bAlternateSetting = 0x06, + .bNumEndpoints = 0x01, + .bInterfaceClass = 0x0e, + .bInterfaceSubClass = 0x02, + .bInterfaceProtocol = 0x00, + .iInterface = 0x00, + } +}; +std::vector MotionCamera::s_endpoint_descriptor{ + { + .bLength = 0x07, + .bDescriptorType = 0x05, + .bEndpointAddress = 0x82, + .bmAttributes = 0x03, + .wMaxPacketSize = 0x0010, + .bInterval = 0x06, + }, + { + .bLength = 0x07, + .bDescriptorType = 0x05, + .bEndpointAddress = 0x81, + .bmAttributes = 0x05, + .wMaxPacketSize = 0x0a60, + .bInterval = 0x01, + }, + { + .bLength = 0x07, + .bDescriptorType = 0x05, + .bEndpointAddress = 0x81, + .bmAttributes = 0x05, + .wMaxPacketSize = 0x0b00, + .bInterval = 0x01, + }, + { + .bLength = 0x07, + .bDescriptorType = 0x05, + .bEndpointAddress = 0x81, + .bmAttributes = 0x05, + .wMaxPacketSize = 0x0b20, + .bInterval = 0x01, + }, + { + .bLength = 0x07, + .bDescriptorType = 0x05, + .bEndpointAddress = 0x81, + .bmAttributes = 0x05, + .wMaxPacketSize = 0x1300, + .bInterval = 0x01, + }, + { + .bLength = 0x07, + .bDescriptorType = 0x05, + .bEndpointAddress = 0x81, + .bmAttributes = 0x05, + .wMaxPacketSize = 0x1320, + .bInterval = 0x01, + }, + { + .bLength = 0x07, + .bDescriptorType = 0x05, + .bEndpointAddress = 0x81, + .bmAttributes = 0x05, + .wMaxPacketSize = 0x13fc, + .bInterval = 0x01, + } +}; + +MotionCamera::MotionCamera(EmulationKernel& ios) : m_ios(ios) +{ + m_id = (u64(m_vid) << 32 | u64(m_pid) << 16 | u64(9) << 8 | u64(1)); +} + +MotionCamera::~MotionCamera() { + if (m_active_altsetting) + { + NOTICE_LOG_FMT(IOS_USB, "Host_CameraStop"); + Host_CameraStop(); + } +} + +DeviceDescriptor MotionCamera::GetDeviceDescriptor() const +{ + return s_device_descriptor; +} + +std::vector MotionCamera::GetConfigurations() const +{ + return s_config_descriptor; +} + +std::vector MotionCamera::GetInterfaces(u8 config) const +{ + return s_interface_descriptor; +} + +std::vector MotionCamera::GetEndpoints(u8 config, u8 interface, u8 alt) const +{ + std::vector ret; + if (interface == 0) + ret.push_back(s_endpoint_descriptor[0]); + else if (interface == 1 && alt > 0) + ret.push_back(s_endpoint_descriptor[alt]); + return ret; +} + +bool MotionCamera::Attach() +{ + //NOTICE_LOG_FMT(IOS_USB, "[{:04x}:{:04x}] Opening device", m_vid, m_pid); + return true; +} + +bool MotionCamera::AttachAndChangeInterface(const u8 interface) +{ + if (!Attach()) + return false; + + if (interface != m_active_interface) + return ChangeInterface(interface) == 0; + + return true; +} + +int MotionCamera::CancelTransfer(const u8 endpoint) +{ + INFO_LOG_FMT(IOS_USB, "[{:04x}:{:04x} {}] Cancelling transfers (endpoint {:#x})", m_vid, m_pid, + m_active_interface, endpoint); + return IPC_SUCCESS; +} + +int MotionCamera::ChangeInterface(const u8 interface) +{ + NOTICE_LOG_FMT(IOS_USB, "[{:04x}:{:04x} {}] Changing interface to {}", m_vid, m_pid, m_active_interface, interface); + m_active_interface = interface; + return 0; +} + +int MotionCamera::GetNumberOfAltSettings(u8 interface) +{ + NOTICE_LOG_FMT(IOS_USB, "[{:04x}:{:04x}] GetNumberOfAltSettings: interface={:02x}", m_vid, m_pid, interface); + return (interface == 1) ? 7 : 1; +} + +int MotionCamera::SetAltSetting(u8 alt_setting) +{ + NOTICE_LOG_FMT(IOS_USB, "[{:04x}:{:04x}] SetAltSetting: alt_setting={:02x}", m_vid, m_pid, alt_setting); + m_active_altsetting = alt_setting; + if (alt_setting) + { + NOTICE_LOG_FMT(IOS_USB, "Host_CameraStart({}x{})", m_active_size.width, m_active_size.height); + auto& system = m_ios.GetSystem(); + system.GetCameraBase().CreateSample(m_active_size.width, m_active_size.height); + Host_CameraStart(m_active_size.width, m_active_size.height); + } + else + { + NOTICE_LOG_FMT(IOS_USB, "Host_CameraStop"); + Host_CameraStop(); + } + return 0; +} + +int MotionCamera::SubmitTransfer(std::unique_ptr cmd) +{ + switch ((cmd->request_type << 8) | cmd->request) + { + case USBHDR(DIR_DEVICE2HOST, TYPE_STANDARD, REC_DEVICE, REQUEST_GET_DESCRIPTOR): // 0x80 0x06 + { + std::vector control_response(usb_config_desc, usb_config_desc + sizeof(usb_config_desc)); + ScheduleTransfer(std::move(cmd), control_response, 0); + break; + } + case USBHDR(DIR_HOST2DEVICE, TYPE_CLASS, REC_INTERFACE, SET_CUR): // 0x21 0x01 + { + u8 unit = cmd->index >> 8; + u8 control = cmd->value >> 8; + NOTICE_LOG_FMT(IOS_USB, "[{:04x}:{:04x} {}] Control: bRequestType={:02x} bRequest={:02x} wValue={:04x} wIndex={:04x} wLength={:04x} // {} / {}", + m_vid, m_pid, m_active_interface, cmd->request_type, cmd->request, cmd->value, cmd->index, cmd->length, + CameraBase::getUVCRequest(cmd->request), + (unit == 0) ? CameraBase::getUVCVideoStreamingControl(control) + : (unit == 1) ? CameraBase::getUVCTerminalControl(control) + : (unit == 3) ? CameraBase::getUVCProcessingUnitControl(control) + : ""); + if (unit == 0 && control == VS_COMMIT) + { + auto& system = m_ios.GetSystem(); + auto& memory = system.GetMemory(); + UVCProbeCommitControl* commit = (UVCProbeCommitControl*) memory.GetPointerForRange(cmd->data_address, cmd->length); + m_active_size = m_supported_sizes[commit->bFrameIndex - 1]; + m_delay = commit->dwFrameInterval / 10; + u32 new_size = m_active_size.width * m_active_size.height * 2; + if (m_image_size != new_size) + { + m_image_size = new_size; + if (m_image_data) + { + free(m_image_data); + } + m_image_data = (u8*) calloc(1, m_image_size); + } + NOTICE_LOG_FMT(IOS_USB, "[{:04x}:{:04x}] VS_COMMIT: bFormatIndex={:02x} bFrameIndex={:02x} dwFrameInterval={:04x} / size={}x{} delay={}", + m_vid, m_pid, commit->bFormatIndex, commit->bFrameIndex, commit->dwFrameInterval, + m_active_size.width, m_active_size.height, + m_delay + ); + } + std::vector control_response = {}; + ScheduleTransfer(std::move(cmd), control_response, 0); + break; + } + case USBHDR(DIR_DEVICE2HOST, TYPE_CLASS, REC_INTERFACE, GET_CUR): // 0xa1 0x81 + case USBHDR(DIR_DEVICE2HOST, TYPE_CLASS, REC_INTERFACE, GET_MIN): // 0xa1 0x82 + case USBHDR(DIR_DEVICE2HOST, TYPE_CLASS, REC_INTERFACE, GET_MAX): // 0xa1 0x83 + case USBHDR(DIR_DEVICE2HOST, TYPE_CLASS, REC_INTERFACE, GET_RES): // 0xa1 0x84 + case USBHDR(DIR_DEVICE2HOST, TYPE_CLASS, REC_INTERFACE, GET_LEN): // 0xa1 0x85 + case USBHDR(DIR_DEVICE2HOST, TYPE_CLASS, REC_INTERFACE, GET_INF): // 0xa1 0x86 + case USBHDR(DIR_DEVICE2HOST, TYPE_CLASS, REC_INTERFACE, GET_DEF): // 0xa1 0x87 + { + u8 unit = cmd->index >> 8; + NOTICE_LOG_FMT(IOS_USB, "[{:04x}:{:04x} {}] Control: bRequestType={:02x} bRequest={:02x} wValue={:04x} wIndex={:04x} wLength={:04x} // {} / {}", + m_vid, m_pid, m_active_interface, cmd->request_type, cmd->request, cmd->value, cmd->index, cmd->length, + CameraBase::getUVCRequest(cmd->request), + (unit == 0) ? CameraBase::getUVCVideoStreamingControl(cmd->value >> 8) + : (unit == 1) ? CameraBase::getUVCTerminalControl(cmd->value >> 8) + : (unit == 3) ? CameraBase::getUVCProcessingUnitControl(cmd->value >> 8) + : ""); + std::vector control_response = {}; + ScheduleTransfer(std::move(cmd), control_response, 0); + break; + } + default: + { + NOTICE_LOG_FMT(IOS_USB, "[{:04x}:{:04x} {}] Control: bRequestType={:02x} bRequest={:02x} wValue={:04x} wIndex={:04x} wLength={:04x}", + m_vid, m_pid, m_active_interface, cmd->request_type, cmd->request, cmd->value, cmd->index, cmd->length); + std::vector control_response = {}; + ScheduleTransfer(std::move(cmd), control_response, 0); + } + } + return IPC_SUCCESS; +} + +int MotionCamera::SubmitTransfer(std::unique_ptr cmd) +{ + NOTICE_LOG_FMT(IOS_USB, "[{:04x}:{:04x} {}] Bulk: length={:04x} endpoint={:02x}", m_vid, m_pid, + m_active_interface, cmd->length, cmd->endpoint); + return IPC_SUCCESS; +} + +int MotionCamera::SubmitTransfer(std::unique_ptr cmd) +{ + NOTICE_LOG_FMT(IOS_USB, "[{:04x}:{:04x} {}] Interrupt: length={:04x} endpoint={:02x}", m_vid, + m_pid, m_active_interface, cmd->length, cmd->endpoint); + return IPC_SUCCESS; +} + +int MotionCamera::SubmitTransfer(std::unique_ptr cmd) +{ + auto& system = m_ios.GetSystem(); + auto& memory = system.GetMemory(); + u8* iso_buffer = memory.GetPointerForRange(cmd->data_address, cmd->length); + if (!iso_buffer) + { + ERROR_LOG_FMT(IOS_USB, "MotionCamera iso buf error"); + return IPC_EINVAL; + } + + u8* iso_buffer_pos = iso_buffer; + + for (std::size_t i = 0; i < cmd->num_packets; i++) + { + UVCHeader uvc_header{}; + uvc_header.bHeaderLength = sizeof(UVCHeader); + uvc_header.endOfHeader = 1; + uvc_header.frameId = m_frame_id; + + u32 data_size = std::min(cmd->packet_sizes[i] - (u32)sizeof(uvc_header), m_image_size - m_image_pos); + if (data_size > 0 && m_image_pos + data_size == m_image_size) + { + m_frame_id ^= 1; + uvc_header.endOfFrame = 1; + } + std::memcpy(iso_buffer_pos, &uvc_header, sizeof(uvc_header)); + if (data_size > 0) + { + std::memcpy(iso_buffer_pos + sizeof(uvc_header), m_image_data + m_image_pos, data_size); + } + m_image_pos += data_size; + iso_buffer_pos += sizeof(uvc_header) + data_size; + cmd->SetPacketReturnValue(i, (u32)sizeof(uvc_header) + data_size); + } + + if (m_image_pos == m_image_size) + { + system.GetCameraBase().GetData(m_image_data, m_image_size); + m_image_pos = 0; + } + + // 15 fps, one frame every 66ms, half a frame per transfer, one transfer every 33ms + cmd->ScheduleTransferCompletion(IPC_SUCCESS, m_delay); + return IPC_SUCCESS; +} + +void MotionCamera::ScheduleTransfer(std::unique_ptr command, + const std::vector& data, u64 expected_time_us) +{ + command->FillBuffer(data.data(), static_cast(data.size())); + command->ScheduleTransferCompletion(static_cast(data.size()), expected_time_us); +} +} // namespace IOS::HLE::USB diff --git a/Source/Core/Core/IOS/USB/Emulated/MotionCamera.h b/Source/Core/Core/IOS/USB/Emulated/MotionCamera.h new file mode 100644 index 0000000000..286a04799a --- /dev/null +++ b/Source/Core/Core/IOS/USB/Emulated/MotionCamera.h @@ -0,0 +1,55 @@ +// Copyright 2025 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include "Core/IOS/USB/Common.h" +#include "Core/IOS/USB/Emulated/CameraBase.h" + +namespace IOS::HLE::USB +{ + +class MotionCamera final : public Device +{ +public: + MotionCamera(EmulationKernel& ios); + ~MotionCamera() override; + DeviceDescriptor GetDeviceDescriptor() const override; + std::vector GetConfigurations() const override; + std::vector GetInterfaces(u8 config) const override; + std::vector GetEndpoints(u8 config, u8 interface, u8 alt) const override; + bool Attach() override; + bool AttachAndChangeInterface(u8 interface) override; + int CancelTransfer(u8 endpoint) override; + int ChangeInterface(u8 interface) override; + int GetNumberOfAltSettings(u8 interface) override; + int SetAltSetting(u8 alt_setting) override; + int SubmitTransfer(std::unique_ptr message) override; + int SubmitTransfer(std::unique_ptr message) override; + int SubmitTransfer(std::unique_ptr message) override; + int SubmitTransfer(std::unique_ptr message) override; + +private: + void ScheduleTransfer(std::unique_ptr command, const std::vector& data, + u64 expected_time_us); + + static DeviceDescriptor s_device_descriptor; + static std::vector s_config_descriptor; + static std::vector s_interface_descriptor; + static std::vector s_endpoint_descriptor; + + EmulationKernel& m_ios; + const u16 m_vid = 0x057e; + const u16 m_pid = 0x030a; + u8 m_active_interface = 0; + u8 m_active_altsetting = 0; + const struct UVCImageSize m_supported_sizes[5] = {{640, 480}, {320, 240}, {160, 120}, {176, 144}, {352, 288}}; + struct UVCImageSize m_active_size; + u32 m_delay = 0; + u32 m_image_size = 0; + u32 m_image_pos = 0; + u8 *m_image_data = nullptr; + bool m_frame_id = 0; +}; +} // namespace IOS::HLE::USB diff --git a/Source/Core/Core/IOS/USB/Host.cpp b/Source/Core/Core/IOS/USB/Host.cpp index c8d55a0bf5..77a8fd3a64 100644 --- a/Source/Core/Core/IOS/USB/Host.cpp +++ b/Source/Core/Core/IOS/USB/Host.cpp @@ -22,7 +22,9 @@ #include "Core/Config/MainSettings.h" #include "Core/Core.h" #include "Core/IOS/USB/Common.h" +#include "Core/IOS/USB/Emulated/DuelScanner.h" #include "Core/IOS/USB/Emulated/Infinity.h" +#include "Core/IOS/USB/Emulated/MotionCamera.h" #include "Core/IOS/USB/Emulated/Skylanders/Skylander.h" #include "Core/IOS/USB/LibusbDevice.h" #include "Core/NetPlayProto.h" @@ -185,6 +187,16 @@ void USBHost::DispatchHooks(const DeviceChangeHooks& hooks) void USBHost::AddEmulatedDevices(std::set& new_devices, DeviceChangeHooks& hooks, bool always_add_hooks) { + if (Config::Get(Config::MAIN_EMULATED_CAMERA) == 1 && !NetPlay::IsNetPlayRunning()) + { + auto duel_scanner = std::make_unique(GetEmulationKernel()); + CheckAndAddDevice(std::move(duel_scanner), new_devices, hooks, always_add_hooks); + } + if (Config::Get(Config::MAIN_EMULATED_CAMERA) == 2 && !NetPlay::IsNetPlayRunning()) + { + auto motion_camera = std::make_unique(GetEmulationKernel()); + CheckAndAddDevice(std::move(motion_camera), new_devices, hooks, always_add_hooks); + } if (Config::Get(Config::MAIN_EMULATE_SKYLANDER_PORTAL) && !NetPlay::IsNetPlayRunning()) { auto skylanderportal = std::make_unique(GetEmulationKernel()); diff --git a/Source/Core/Core/System.cpp b/Source/Core/Core/System.cpp index 695d5861fc..fcae1e9c36 100644 --- a/Source/Core/Core/System.cpp +++ b/Source/Core/Core/System.cpp @@ -31,6 +31,7 @@ #include "Core/PowerPC/JitInterface.h" #include "Core/PowerPC/PowerPC.h" #include "IOS/USB/Emulated/Infinity.h" +#include "IOS/USB/Emulated/MotionCamera.h" #include "IOS/USB/Emulated/Skylanders/Skylander.h" #include "VideoCommon/Assets/CustomAssetLoader.h" #include "VideoCommon/CommandProcessor.h" @@ -78,6 +79,7 @@ struct System::Impl HSP::HSPManager m_hsp; IOS::HLE::USB::InfinityBase m_infinity_base; IOS::HLE::USB::SkylanderPortal m_skylander_portal; + IOS::HLE::USB::CameraBase m_camera_data; IOS::WiiIPC m_wii_ipc; Memory::MemoryManager m_memory; MemoryInterface::MemoryInterfaceManager m_memory_interface; @@ -243,6 +245,11 @@ IOS::HLE::USB::InfinityBase& System::GetInfinityBase() const return m_impl->m_infinity_base; } +IOS::HLE::USB::CameraBase& System::GetCameraBase() const +{ + return m_impl->m_camera_data; +} + IOS::WiiIPC& System::GetWiiIPC() const { return m_impl->m_wii_ipc; diff --git a/Source/Core/Core/System.h b/Source/Core/Core/System.h index 9ec8391ff0..dec9e84aa9 100644 --- a/Source/Core/Core/System.h +++ b/Source/Core/Core/System.h @@ -69,6 +69,7 @@ namespace IOS::HLE::USB { class SkylanderPortal; class InfinityBase; +class CameraBase; } // namespace IOS::HLE::USB namespace Memory { @@ -176,6 +177,7 @@ public: JitInterface& GetJitInterface() const; IOS::HLE::USB::SkylanderPortal& GetSkylanderPortal() const; IOS::HLE::USB::InfinityBase& GetInfinityBase() const; + IOS::HLE::USB::CameraBase& GetCameraBase() const; IOS::WiiIPC& GetWiiIPC() const; Memory::MemoryManager& GetMemory() const; MemoryInterface::MemoryInterfaceManager& GetMemoryInterface() const; diff --git a/Source/Core/DolphinLib.props b/Source/Core/DolphinLib.props index 388367afd1..507a7df0a2 100644 --- a/Source/Core/DolphinLib.props +++ b/Source/Core/DolphinLib.props @@ -400,7 +400,10 @@ + + + @@ -1063,7 +1066,10 @@ + + + diff --git a/Source/Core/DolphinNoGUI/MainNoGUI.cpp b/Source/Core/DolphinNoGUI/MainNoGUI.cpp index 539bbe769f..9154615398 100644 --- a/Source/Core/DolphinNoGUI/MainNoGUI.cpp +++ b/Source/Core/DolphinNoGUI/MainNoGUI.cpp @@ -139,6 +139,14 @@ void Host_TitleChanged() #endif } +void Host_CameraStart(u16 width, u16 height) +{ +} + +void Host_CameraStop() +{ +} + void Host_UpdateDiscordClientID(const std::string& client_id) { #ifdef USE_DISCORD_PRESENCE diff --git a/Source/Core/DolphinQt/CMakeLists.txt b/Source/Core/DolphinQt/CMakeLists.txt index 9f676f0246..f9df6a4a07 100644 --- a/Source/Core/DolphinQt/CMakeLists.txt +++ b/Source/Core/DolphinQt/CMakeLists.txt @@ -40,6 +40,8 @@ add_executable(dolphin-emu Achievements/AchievementSettingsWidget.h Achievements/AchievementsWindow.cpp Achievements/AchievementsWindow.h + CameraQt/CameraQt.cpp + CameraQt/CameraQt.h Config/ARCodeWidget.cpp Config/ARCodeWidget.h Config/CheatCodeEditor.cpp diff --git a/Source/Core/DolphinQt/CameraQt/CameraQt.cpp b/Source/Core/DolphinQt/CameraQt/CameraQt.cpp new file mode 100644 index 0000000000..a8711e0373 --- /dev/null +++ b/Source/Core/DolphinQt/CameraQt/CameraQt.cpp @@ -0,0 +1,191 @@ +// Copyright 2025 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include + +#include +#include +#include +#include +#include +#include + +#include "Core/Config/MainSettings.h" +#include "Core/Core.h" +#include "Core/IOS/USB/Emulated/MotionCamera.h" +#include "Core/System.h" +#include "DolphinQt/CameraQt/CameraQt.h" +#include "DolphinQt/Host.h" +#include "DolphinQt/Resources.h" + +CameraWindow::CameraWindow(QWidget* parent) : QWidget(parent) +{ + setWindowTitle(tr("Duel Scanner / Motion Tracking Camera")); + setWindowIcon(Resources::GetAppIcon()); + auto* main_layout = new QVBoxLayout(); + auto* layout1 = new QHBoxLayout(); + auto* layout2 = new QHBoxLayout(); + + auto* label1 = new QLabel(tr("Emulated Camera:")); + label1->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); + layout1->addWidget(label1); + + m_emudevCombobox = new QComboBox(); + QString emuDevs[] = {tr("Disabled"), tr("Duel Scanner"), tr("Motion Tracking Camera")}; + for (auto& dev : emuDevs) + { + m_emudevCombobox->addItem(dev); + } + m_emudevCombobox->setCurrentIndex(Config::Get(Config::MAIN_EMULATED_CAMERA)); + connect(m_emudevCombobox, &QComboBox::currentIndexChanged, this, &CameraWindow::EmulatedDeviceSelected); + layout1->addWidget(m_emudevCombobox); + + auto* label2 = new QLabel(tr("Selected Camera:")); + label2->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); + layout2->addWidget(label2); + + m_hostdevCombobox = new QComboBox(); + m_hostdevCombobox->setMinimumWidth(200); + RefreshDeviceList(); + connect(m_hostdevCombobox, &QComboBox::currentIndexChanged, this, &CameraWindow::HostDeviceSelected); + m_hostdevCombobox->setDisabled(0 == Config::Get(Config::MAIN_EMULATED_CAMERA)); + layout2->addWidget(m_hostdevCombobox); + + m_refreshButton = new QPushButton(tr("Refresh")); + m_refreshButton->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); + connect(m_refreshButton, &QPushButton::pressed, this, &CameraWindow::RefreshDeviceList); + m_refreshButton->setDisabled(0 == Config::Get(Config::MAIN_EMULATED_CAMERA)); + layout2->addWidget(m_refreshButton); + + main_layout->addLayout(layout1); + main_layout->addLayout(layout2); + setLayout(main_layout); +} + +CameraWindow::~CameraWindow() = default; + +void CameraWindow::RefreshDeviceList() +{ + disconnect(m_hostdevCombobox, &QComboBox::currentIndexChanged, this, &CameraWindow::HostDeviceSelected); + m_hostdevCombobox->clear(); + m_hostdevCombobox->addItem(tr("Fake")); + auto selectedDevice = Config::Get(Config::MAIN_SELECTED_CAMERA); + const QList availableCameras = QMediaDevices::videoInputs(); + for (const QCameraDevice& camera : availableCameras) + { + m_hostdevCombobox->addItem(camera.description()); + if (camera.description().toStdString() == selectedDevice) + { + m_hostdevCombobox->setCurrentIndex(m_hostdevCombobox->count() - 1); + } + } + connect(m_hostdevCombobox, &QComboBox::currentIndexChanged, this, &CameraWindow::HostDeviceSelected); +} + +void CameraWindow::EmulatedDeviceSelected(int index) +{ + m_hostdevCombobox->setDisabled(0 == index); + m_refreshButton->setDisabled(0 == index); + Config::SetBaseOrCurrent(Config::MAIN_EMULATED_CAMERA, index); +} + +void CameraWindow::HostDeviceSelected(int index) +{ + std::string camera = m_hostdevCombobox->currentText().toStdString(); + Config::SetBaseOrCurrent(Config::MAIN_SELECTED_CAMERA, camera); +} + +////////////////////////////////////////// + +CameraManager::CameraManager() +{ + m_captureSession = new QMediaCaptureSession(); + m_videoSink = new QVideoSink; + connect(Host::GetInstance(), &Host::CameraStart, this, &CameraManager::Start); + connect(Host::GetInstance(), &Host::CameraStop, this, &CameraManager::Stop); +} + +CameraManager::~CameraManager() +{ + disconnect(Host::GetInstance(), &Host::CameraStart, this, &CameraManager::Start); + disconnect(Host::GetInstance(), &Host::CameraStop, this, &CameraManager::Stop); +} + +void CameraManager::Start(u16 width, u16 heigth) +{ + auto selectedCamera = Config::Get(Config::MAIN_SELECTED_CAMERA); + const auto videoInputs = QMediaDevices::videoInputs(); + for (const QCameraDevice &camera : videoInputs) + { + if (camera.description().toStdString() == selectedCamera) + { + m_camera = new QCamera(camera); + break; + } + } + if (!m_camera) + { + return; + } + + m_captureSession->setCamera(m_camera); + m_captureSession->setVideoSink(m_videoSink); + + const auto videoFormats = m_camera->cameraDevice().videoFormats(); + for (const auto &format : videoFormats) { + if (format.pixelFormat() == QVideoFrameFormat::Format_NV12 + && format.resolution().width() == width + && format.resolution().height() == heigth) { + m_camera->setCameraFormat(format); + break; + } + } + + connect(m_videoSink, &QVideoSink::videoFrameChanged, this, &CameraManager::VideoFrameChanged); + m_camera_active = true; + m_camera->start(); +} + +void CameraManager::Stop() +{ + if (m_camera_active) + { + disconnect(m_videoSink, &QVideoSink::videoFrameChanged, this, &CameraManager::VideoFrameChanged); + m_camera->stop(); + } + m_camera_active = false; +} + +void CameraManager::VideoFrameChanged(const QVideoFrame& frame) +{ + QVideoFrame rwFrame(frame); + if (rwFrame.pixelFormat() != QVideoFrameFormat::PixelFormat::Format_NV12) + { + NOTICE_LOG_FMT(IOS_USB, "VideoFrameChanged : Unhandled format:{}", (u16)rwFrame.pixelFormat()); + return; + } + rwFrame.map(QVideoFrame::MapMode::ReadOnly); + + // Convert NV12 to YUY2 + u32 yuy2Size = 2 * frame.width() * frame.height(); + u8 *yuy2Image = (u8*) calloc(1, yuy2Size); + if (!yuy2Image) + { + NOTICE_LOG_FMT(IOS_USB, "alloc faied"); + rwFrame.unmap(); + return; + } + for (int line = 0; line < frame.height(); line++) { + for (int col = 0; col < frame.width(); col++) { + u8 *yuyvPos = yuy2Image + 2 * (frame.width() * line + col); + const u8 *yPos = rwFrame.bits(0) + (frame.width() * line + col); + const u8 *uvPos = rwFrame.bits(1) + (frame.width() * (line & ~1U) / 2 + (col & ~1U)); + yuyvPos[0] = yPos[0]; + yuyvPos[1] = (col % 2 == 0) ? uvPos[0] : uvPos[1]; + } + } + + Core::System::GetInstance().GetCameraBase().SetData(yuy2Image, yuy2Size); + free(yuy2Image); + rwFrame.unmap(); +} diff --git a/Source/Core/DolphinQt/CameraQt/CameraQt.h b/Source/Core/DolphinQt/CameraQt/CameraQt.h new file mode 100644 index 0000000000..02ac7cfd50 --- /dev/null +++ b/Source/Core/DolphinQt/CameraQt/CameraQt.h @@ -0,0 +1,43 @@ +// Copyright 2025 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include +#include +#include +#include +#include + +class CameraWindow : public QWidget +{ + Q_OBJECT +public: + explicit CameraWindow(QWidget* parent = nullptr); + ~CameraWindow() override; + +private: + void RefreshDeviceList(); + void EmulatedDeviceSelected(int index); + void HostDeviceSelected(int index); + QComboBox *m_emudevCombobox = nullptr; + QComboBox *m_hostdevCombobox = nullptr; + QPushButton *m_refreshButton = nullptr; +}; + +class CameraManager : public QObject +{ +public: + CameraManager(); + ~CameraManager(); + void Start(u16 width, u16 heigth); + void Stop(); + void VideoFrameChanged(const QVideoFrame& frame); + +private: + QCamera *m_camera = nullptr; + QMediaCaptureSession *m_captureSession = nullptr; + QVideoSink *m_videoSink = nullptr; + bool m_camera_active = false; +}; diff --git a/Source/Core/DolphinQt/DolphinQt.vcxproj b/Source/Core/DolphinQt/DolphinQt.vcxproj index 41349a4cee..cf284ae258 100644 --- a/Source/Core/DolphinQt/DolphinQt.vcxproj +++ b/Source/Core/DolphinQt/DolphinQt.vcxproj @@ -54,6 +54,7 @@ + @@ -240,6 +241,7 @@ also be modified using the VS UI. --> + @@ -278,6 +280,7 @@ + diff --git a/Source/Core/DolphinQt/Host.cpp b/Source/Core/DolphinQt/Host.cpp index a81924cfd8..735e6df087 100644 --- a/Source/Core/DolphinQt/Host.cpp +++ b/Source/Core/DolphinQt/Host.cpp @@ -307,6 +307,16 @@ void Host_TitleChanged() #endif } +void Host_CameraStart(u16 width, u16 height) +{ + QueueOnObject(QApplication::instance(), [=] { emit Host::GetInstance()->CameraStart(width, height); }); +} + +void Host_CameraStop() +{ + QueueOnObject(QApplication::instance(), [] { emit Host::GetInstance()->CameraStop(); }); +} + void Host_UpdateDiscordClientID(const std::string& client_id) { #ifdef USE_DISCORD_PRESENCE diff --git a/Source/Core/DolphinQt/Host.h b/Source/Core/DolphinQt/Host.h index cee44a2e6a..59d891af9d 100644 --- a/Source/Core/DolphinQt/Host.h +++ b/Source/Core/DolphinQt/Host.h @@ -44,6 +44,8 @@ signals: void JitProfileDataWiped(); void PPCSymbolsChanged(); void PPCBreakpointsChanged(); + void CameraStart(u16 width, u16 height); + void CameraStop(); private: Host(); diff --git a/Source/Core/DolphinQt/Main.cpp b/Source/Core/DolphinQt/Main.cpp index 8baa6d7f08..2bae06aae5 100644 --- a/Source/Core/DolphinQt/Main.cpp +++ b/Source/Core/DolphinQt/Main.cpp @@ -30,6 +30,7 @@ #include "Core/DolphinAnalytics.h" #include "Core/System.h" +#include "DolphinQt/CameraQt/CameraQt.h" #include "DolphinQt/Host.h" #include "DolphinQt/MainWindow.h" #include "DolphinQt/QtUtils/ModalMessageBox.h" @@ -190,6 +191,8 @@ int main(int argc, char* argv[]) QObject::connect(QAbstractEventDispatcher::instance(), &QAbstractEventDispatcher::aboutToBlock, &app, [] { Core::HostDispatchJobs(Core::System::GetInstance()); }); + CameraManager camera; + std::optional save_state_path; if (options.is_set("save_state")) { diff --git a/Source/Core/DolphinQt/MainWindow.cpp b/Source/Core/DolphinQt/MainWindow.cpp index 93ad16a59e..9cba32d9fb 100644 --- a/Source/Core/DolphinQt/MainWindow.cpp +++ b/Source/Core/DolphinQt/MainWindow.cpp @@ -75,6 +75,7 @@ #include "DolphinQt/AboutDialog.h" #include "DolphinQt/Achievements/AchievementsWindow.h" #include "DolphinQt/CheatsManager.h" +#include "DolphinQt/CameraQt/CameraQt.h" #include "DolphinQt/Config/ControllersWindow.h" #include "DolphinQt/Config/FreeLookWindow.h" #include "DolphinQt/Config/Graphics/GraphicsWindow.h" @@ -570,6 +571,7 @@ void MainWindow::ConnectMenuBar() connect(m_menu_bar, &MenuBar::StartNetPlay, this, &MainWindow::ShowNetPlaySetupDialog); connect(m_menu_bar, &MenuBar::BrowseNetPlay, this, &MainWindow::ShowNetPlayBrowser); connect(m_menu_bar, &MenuBar::ShowFIFOPlayer, this, &MainWindow::ShowFIFOPlayer); + connect(m_menu_bar, &MenuBar::ShowCameraWindow, this, &MainWindow::ShowCameraWindow); connect(m_menu_bar, &MenuBar::ShowSkylanderPortal, this, &MainWindow::ShowSkylanderPortal); connect(m_menu_bar, &MenuBar::ShowInfinityBase, this, &MainWindow::ShowInfinityBase); connect(m_menu_bar, &MenuBar::ConnectWiiRemote, this, &MainWindow::OnConnectWiiRemote); @@ -1387,6 +1389,19 @@ void MainWindow::ShowFIFOPlayer() m_fifo_window->activateWindow(); } +void MainWindow::ShowCameraWindow() +{ + if (!m_camera_window) + { + m_camera_window = new CameraWindow(); + } + + SetQWidgetWindowDecorations(m_camera_window); + m_camera_window->show(); + m_camera_window->raise(); + m_camera_window->activateWindow(); +} + void MainWindow::ShowSkylanderPortal() { if (!m_skylander_window) diff --git a/Source/Core/DolphinQt/MainWindow.h b/Source/Core/DolphinQt/MainWindow.h index f9f0f1c95d..2691552df9 100644 --- a/Source/Core/DolphinQt/MainWindow.h +++ b/Source/Core/DolphinQt/MainWindow.h @@ -21,6 +21,7 @@ class AchievementsWindow; class AssemblerWidget; class BreakpointWidget; struct BootParameters; +class CameraWindow; class CheatsManager; class CodeWidget; class ControllersWindow; @@ -171,6 +172,7 @@ private: void ShowNetPlaySetupDialog(); void ShowNetPlayBrowser(); void ShowFIFOPlayer(); + void ShowCameraWindow(); void ShowSkylanderPortal(); void ShowInfinityBase(); void ShowMemcardManager(); @@ -243,6 +245,7 @@ private: SettingsWindow* m_settings_window = nullptr; GraphicsWindow* m_graphics_window = nullptr; FIFOPlayerWindow* m_fifo_window = nullptr; + CameraWindow* m_camera_window = nullptr; SkylanderPortalWindow* m_skylander_window = nullptr; InfinityBaseWindow* m_infinity_window = nullptr; MappingWindow* m_hotkey_window = nullptr; diff --git a/Source/Core/DolphinQt/MenuBar.cpp b/Source/Core/DolphinQt/MenuBar.cpp index 24c6cc2d04..37bb45386c 100644 --- a/Source/Core/DolphinQt/MenuBar.cpp +++ b/Source/Core/DolphinQt/MenuBar.cpp @@ -272,6 +272,7 @@ void MenuBar::AddToolsMenu() tools_menu->addAction(tr("FIFO Player"), this, &MenuBar::ShowFIFOPlayer); auto* usb_device_menu = new QMenu(tr("Emulated USB Devices"), tools_menu); + usb_device_menu->addAction(tr("&Duel Scanner / Motion Tracking Camera"), this, &MenuBar::ShowCameraWindow); usb_device_menu->addAction(tr("&Skylanders Portal"), this, &MenuBar::ShowSkylanderPortal); usb_device_menu->addAction(tr("&Infinity Base"), this, &MenuBar::ShowInfinityBase); tools_menu->addMenu(usb_device_menu); diff --git a/Source/Core/DolphinQt/MenuBar.h b/Source/Core/DolphinQt/MenuBar.h index 29457c15f7..e32c7e0d7a 100644 --- a/Source/Core/DolphinQt/MenuBar.h +++ b/Source/Core/DolphinQt/MenuBar.h @@ -89,6 +89,7 @@ signals: void ShowAboutDialog(); void ShowCheatsManager(); void ShowResourcePackManager(); + void ShowCameraWindow(); void ShowSkylanderPortal(); void ShowInfinityBase(); void ConnectWiiRemote(int id); diff --git a/Source/Core/DolphinTool/ToolHeadlessPlatform.cpp b/Source/Core/DolphinTool/ToolHeadlessPlatform.cpp index 59495a7d43..5d63ca5e91 100644 --- a/Source/Core/DolphinTool/ToolHeadlessPlatform.cpp +++ b/Source/Core/DolphinTool/ToolHeadlessPlatform.cpp @@ -46,6 +46,14 @@ void Host_UpdateTitle(const std::string& title) { } +void Host_CameraStart(u16 width, u16 height) +{ +} + +void Host_CameraStop() +{ +} + void Host_UpdateDiscordClientID(const std::string& client_id) { } diff --git a/Source/Core/UICommon/USBUtils.cpp b/Source/Core/UICommon/USBUtils.cpp index 2264afe82c..6acef447e7 100644 --- a/Source/Core/UICommon/USBUtils.cpp +++ b/Source/Core/UICommon/USBUtils.cpp @@ -22,6 +22,7 @@ static const std::map, std::string_view> s_wii_peripherals{{ {{0x057e, 0x0308}, "Wii Speak"}, {{0x057e, 0x0309}, "Nintendo USB Microphone"}, {{0x057e, 0x030a}, "Ubisoft Motion Tracking Camera"}, + {{0x057e, 0x030d}, "Duel Scanner"}, {{0x0e6f, 0x0129}, "Disney Infinity Reader (Portal Device)"}, {{0x1430, 0x0100}, "Tony Hawk Ride Skateboard"}, {{0x1430, 0x0150}, "Skylanders Portal"}, diff --git a/Source/DSPTool/StubHost.cpp b/Source/DSPTool/StubHost.cpp index eb263d904c..2634d72b72 100644 --- a/Source/DSPTool/StubHost.cpp +++ b/Source/DSPTool/StubHost.cpp @@ -28,6 +28,12 @@ void Host_Message(HostMessageID) void Host_UpdateTitle(const std::string&) { } +void Host_CameraStart(u16 width, u16 height) +{ +} +void Host_CameraStop() +{ +} void Host_UpdateDiscordClientID(const std::string& client_id) { } diff --git a/Source/UnitTests/StubHost.cpp b/Source/UnitTests/StubHost.cpp index ffbb0c41c2..b4141ce988 100644 --- a/Source/UnitTests/StubHost.cpp +++ b/Source/UnitTests/StubHost.cpp @@ -28,6 +28,12 @@ void Host_Message(HostMessageID) void Host_UpdateTitle(const std::string&) { } +void Host_CameraStart(u16 width, u16 height) +{ +} +void Host_CameraStop() +{ +} void Host_UpdateDiscordClientID(const std::string& client_id) { } From 65747631438b0fc6b14c9b952c4e9c6f7412a104 Mon Sep 17 00:00:00 2001 From: Florin9doi Date: Mon, 24 Feb 2025 23:05:36 +0200 Subject: [PATCH 3/3] USB: Create Camera.md - This commit should be removed once qsc repo is updated and eventually moved to wiki to help future QT updates --- Source/Core/Core/IOS/USB/Emulated/Camera.md | 54 +++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 Source/Core/Core/IOS/USB/Emulated/Camera.md diff --git a/Source/Core/Core/IOS/USB/Emulated/Camera.md b/Source/Core/Core/IOS/USB/Emulated/Camera.md new file mode 100644 index 0000000000..e27f697824 --- /dev/null +++ b/Source/Core/Core/IOS/USB/Emulated/Camera.md @@ -0,0 +1,54 @@ +#### Support: +- [x] Yu-Gi-Oh! 5D's: Duel Transer / 640x480 +- [x] Your Shape / 320x240 +- [x] Fit in Six / 320x240 +- [?] Racket Sports Party / 160x120 + +#### QTMultimedia: +``` +git clone https://github.com/dolphin-emu/qsc.git _qsc +cd _qsc +py -m pip install -r requirements.txt +$env:Path += ';C:\Program Files\Git\usr\bin\' +``` +```diff +diff --git a/examples/dolphin-x64.yml b/examples/dolphin-x64.yml +index 5486190..a895eaf 100644 +--- a/examples/dolphin-x64.yml ++++ b/examples/dolphin-x64.yml +@@ -29,7 +29,6 @@ configure: + - qtlocation + - qtlottie + - qtmqtt +- - qtmultimedia + - qtnetworkauth + - qtopcua + - qtpositioning +@@ -42,7 +41,6 @@ configure: + - qtsensors + - qtserialbus + - qtserialport +- - qtshadertools + - qtspeech + - qttools + - qttranslations +@@ -53,13 +51,13 @@ configure: + - qtwebsockets + - qtwebview + feature: +- concurrent: false ++ concurrent: true + dbus: false + gif: false + ico: false + imageformat_bmp: false + jpeg: false +- network: false ++ network: true + printsupport: false + qmake: false + sql: false +``` +``` +py -m qsc examples\dolphin-x64.yml +```