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/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
+```
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 38d481cdf9..f9df6a4a07 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 "")
@@ -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
@@ -427,6 +429,7 @@ target_link_libraries(dolphin-emu
PRIVATE
core
Qt6::Widgets
+ Qt6::Multimedia
uicommon
imgui
implot
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)
{
}
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)
-
+
-
+