This commit is contained in:
Florin9doi 2025-04-20 01:40:11 +03:00 committed by GitHub
commit e8c9fa7003
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
53 changed files with 2124 additions and 9 deletions

View File

@ -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 {

View File

@ -13,6 +13,9 @@
<uses-feature
android:name="android.hardware.gamepad"
android:required="false"/>
<uses-feature
android:name="android.hardware.camera"
android:required="false"/>
<uses-feature
android:name="android.software.leanback"
android:required="false"/>
@ -28,6 +31,7 @@
<uses-permission
android:name="android.permission.VIBRATE"
android:required="false"/>
<uses-permission-sdk-23 android:name="android.permission.CAMERA"/>
<application
android:name=".DolphinApplication"

View File

@ -2,6 +2,7 @@
package org.dolphinemu.dolphinemu;
import android.app.Activity;
import android.app.Application;
import android.content.Context;
import android.hardware.usb.UsbManager;
@ -15,13 +16,15 @@ import org.dolphinemu.dolphinemu.utils.VolleyUtil;
public class DolphinApplication extends Application
{
private static DolphinApplication application;
private static ActivityTracker sActivityTracker;
@Override
public void onCreate()
{
super.onCreate();
application = this;
registerActivityLifecycleCallbacks(new ActivityTracker());
sActivityTracker = new ActivityTracker();
registerActivityLifecycleCallbacks(sActivityTracker);
VolleyUtil.init(getApplicationContext());
System.loadLibrary("main");
@ -36,4 +39,9 @@ public class DolphinApplication extends Application
{
return application.getApplicationContext();
}
public static Activity getAppActivity()
{
return sActivityTracker.getCurrentActivity();
}
}

View File

@ -459,6 +459,8 @@ public final class NativeLibrary
private static native String GetCurrentTitleDescriptionUnchecked();
public static native void CameraSetData(byte[] image);
@Keep
public static void displayToastMsg(final String text, final boolean long_length)
{

View File

@ -2,7 +2,6 @@
package org.dolphinemu.dolphinemu.activities
import android.annotation.SuppressLint
import android.content.DialogInterface
import android.content.Intent
import android.graphics.Rect
@ -60,6 +59,7 @@ import org.dolphinemu.dolphinemu.ui.main.ThemeProvider
import org.dolphinemu.dolphinemu.utils.AfterDirectoryInitializationRunner
import org.dolphinemu.dolphinemu.utils.DirectoryInitialization
import org.dolphinemu.dolphinemu.utils.FileBrowserHelper
import org.dolphinemu.dolphinemu.utils.PermissionsHandler
import org.dolphinemu.dolphinemu.utils.ThemeHelper
import kotlin.math.roundToInt
@ -994,6 +994,11 @@ class EmulationActivity : AppCompatActivity(), ThemeProvider {
this.themeId = themeId
}
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, 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"

View File

@ -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<String> = arrayOf()
private var cameraValues: Array<String> = 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<String> {
return cameraEntries
}
fun getCameraValues(): Array<String> {
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)
}
}
}
}

View File

@ -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

View File

@ -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),

View File

@ -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,

View File

@ -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<SettingsItem>) {
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,

View File

@ -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()

View File

@ -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()

View File

@ -7,12 +7,14 @@ import android.os.Bundle
class ActivityTracker : ActivityLifecycleCallbacks {
val resumedActivities = HashSet<Activity>()
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

View File

@ -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();
}
}
}
}

View File

@ -223,6 +223,18 @@
<item>5</item>
</integer-array>
<!-- Emulated camera -->
<string-array name="emulatedCameraEntries">
<item>@string/disabled</item>
<item>Duel Scanner</item>
<item>Motion Tracking Camera</item>
</string-array>
<integer-array name="emulatedCameraValues">
<item>0</item>
<item>1</item>
<item>2</item>
</integer-array>
<!-- Texture Cache Accuracy Preference -->
<string-array name="textureCacheAccuracyEntries">
<item>@string/accuracy_fast</item>

View File

@ -911,6 +911,8 @@ It can efficiently compress both junk data and encrypted Wii data.
<!-- Emulated USB Devices -->
<string name="emulated_usb_devices">Emulated USB Devices</string>
<string name="emulated_camera">Emulated Camera</string>
<string name="selected_camera">Selected Camera</string>
<string name="emulate_skylander_portal">Skylanders Portal</string>
<string name="skylanders_manager">Skylanders Manager</string>
<string name="create_skylander_title">Create Skylander</string>

View File

@ -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<jclass>(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<jclass>(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);
}
}

View File

@ -112,6 +112,11 @@ jclass GetCoreDeviceControlClass();
jfieldID GetCoreDeviceControlPointer();
jmethodID GetCoreDeviceControlConstructor();
jclass GetCameraClass();
jmethodID GetCameraStart();
jmethodID GetCameraResume();
jmethodID GetCameraStop();
jclass GetInputDetectorClass();
jfieldID GetInputDetectorPointer();

View File

@ -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<const u8 *>(buffer), size);
env->ReleaseByteArrayElements(image, buffer, 0);
}
}

View File

@ -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

View File

@ -585,6 +585,12 @@ void SetUSBDeviceWhitelist(const std::set<std::pair<u16, u16>>& devices)
// Main.EmulatedUSBDevices
const Info<int> MAIN_EMULATED_CAMERA{
{System::Main, "EmulatedUSBDevices", "EmulatedCamera"}, 0};
const Info<std::string> MAIN_SELECTED_CAMERA{
{System::Main, "EmulatedUSBDevices", "SelectedCamera"}, ""};
const Info<bool> MAIN_EMULATE_SKYLANDER_PORTAL{
{System::Main, "EmulatedUSBDevices", "EmulateSkylanderPortal"}, false};

View File

@ -358,6 +358,8 @@ void SetUSBDeviceWhitelist(const std::set<std::pair<u16, u16>>& devices);
// Main.EmulatedUSBDevices
extern const Info<int> MAIN_EMULATED_CAMERA;
extern const Info<std::string> MAIN_SELECTED_CAMERA;
extern const Info<bool> MAIN_EMULATE_SKYLANDER_PORTAL;
extern const Info<bool> MAIN_EMULATE_INFINITY_BASE;

View File

@ -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)

View File

@ -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 = {},

View File

@ -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,

View File

@ -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
```

View File

@ -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

View File

@ -0,0 +1,163 @@
// Copyright 2025 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <vector>
#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

View File

@ -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<ConfigDescriptor> DuelScanner::s_config_descriptor{
{
.bLength = 0x09,
.bDescriptorType = 0x02,
.wTotalLength = 0x021f,
.bNumInterfaces = 0x02,
.bConfigurationValue = 0x01,
.iConfiguration = 0x30,
.bmAttributes = 0x80,
.MaxPower = 0xfa,
}
};
std::vector<InterfaceDescriptor> 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<EndpointDescriptor> 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<ConfigDescriptor> DuelScanner::GetConfigurations() const
{
return s_config_descriptor;
}
std::vector<InterfaceDescriptor> DuelScanner::GetInterfaces(u8 config) const
{
return s_interface_descriptor;
}
std::vector<EndpointDescriptor> DuelScanner::GetEndpoints(u8 config, u8 interface, u8 alt) const
{
std::vector<EndpointDescriptor> 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<CtrlMessage> cmd)
{
switch ((cmd->request_type << 8) | cmd->request)
{
case USBHDR(DIR_DEVICE2HOST, TYPE_STANDARD, REC_DEVICE, REQUEST_GET_DESCRIPTOR): // 0x80 0x06
{
std::vector<u8> 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<u8> 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<u8> 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<u8> control_response = {};
ScheduleTransfer(std::move(cmd), control_response, 0);
}
}
return IPC_SUCCESS;
}
int DuelScanner::SubmitTransfer(std::unique_ptr<BulkMessage> 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<IntrMessage> 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<IsoMessage> 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<TransferCommand> command,
const std::vector<u8>& data, u64 expected_time_us)
{
command->FillBuffer(data.data(), static_cast<const size_t>(data.size()));
command->ScheduleTransferCompletion(static_cast<s32>(data.size()), expected_time_us);
}
} // namespace IOS::HLE::USB

View File

@ -0,0 +1,55 @@
// Copyright 2025 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <vector>
#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<ConfigDescriptor> GetConfigurations() const override;
std::vector<InterfaceDescriptor> GetInterfaces(u8 config) const override;
std::vector<EndpointDescriptor> 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<CtrlMessage> message) override;
int SubmitTransfer(std::unique_ptr<BulkMessage> message) override;
int SubmitTransfer(std::unique_ptr<IntrMessage> message) override;
int SubmitTransfer(std::unique_ptr<IsoMessage> message) override;
private:
void ScheduleTransfer(std::unique_ptr<TransferCommand> command, const std::vector<u8>& data,
u64 expected_time_us);
static DeviceDescriptor s_device_descriptor;
static std::vector<ConfigDescriptor> s_config_descriptor;
static std::vector<InterfaceDescriptor> s_interface_descriptor;
static std::vector<EndpointDescriptor> 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

View File

@ -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<ConfigDescriptor> MotionCamera::s_config_descriptor{
{
.bLength = 0x09,
.bDescriptorType = 0x02,
.wTotalLength = 0x0309,
.bNumInterfaces = 0x02,
.bConfigurationValue = 0x01,
.iConfiguration = 0x30,
.bmAttributes = 0x80,
.MaxPower = 0xfa,
}
};
std::vector<InterfaceDescriptor> 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<EndpointDescriptor> 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<ConfigDescriptor> MotionCamera::GetConfigurations() const
{
return s_config_descriptor;
}
std::vector<InterfaceDescriptor> MotionCamera::GetInterfaces(u8 config) const
{
return s_interface_descriptor;
}
std::vector<EndpointDescriptor> MotionCamera::GetEndpoints(u8 config, u8 interface, u8 alt) const
{
std::vector<EndpointDescriptor> 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<CtrlMessage> cmd)
{
switch ((cmd->request_type << 8) | cmd->request)
{
case USBHDR(DIR_DEVICE2HOST, TYPE_STANDARD, REC_DEVICE, REQUEST_GET_DESCRIPTOR): // 0x80 0x06
{
std::vector<u8> 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<u8> 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<u8> 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<u8> control_response = {};
ScheduleTransfer(std::move(cmd), control_response, 0);
}
}
return IPC_SUCCESS;
}
int MotionCamera::SubmitTransfer(std::unique_ptr<BulkMessage> 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<IntrMessage> 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<IsoMessage> 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<TransferCommand> command,
const std::vector<u8>& data, u64 expected_time_us)
{
command->FillBuffer(data.data(), static_cast<const size_t>(data.size()));
command->ScheduleTransferCompletion(static_cast<s32>(data.size()), expected_time_us);
}
} // namespace IOS::HLE::USB

View File

@ -0,0 +1,55 @@
// Copyright 2025 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <vector>
#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<ConfigDescriptor> GetConfigurations() const override;
std::vector<InterfaceDescriptor> GetInterfaces(u8 config) const override;
std::vector<EndpointDescriptor> 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<CtrlMessage> message) override;
int SubmitTransfer(std::unique_ptr<BulkMessage> message) override;
int SubmitTransfer(std::unique_ptr<IntrMessage> message) override;
int SubmitTransfer(std::unique_ptr<IsoMessage> message) override;
private:
void ScheduleTransfer(std::unique_ptr<TransferCommand> command, const std::vector<u8>& data,
u64 expected_time_us);
static DeviceDescriptor s_device_descriptor;
static std::vector<ConfigDescriptor> s_config_descriptor;
static std::vector<InterfaceDescriptor> s_interface_descriptor;
static std::vector<EndpointDescriptor> 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

View File

@ -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<u64>& new_devices, DeviceChangeHooks& hooks,
bool always_add_hooks)
{
if (Config::Get(Config::MAIN_EMULATED_CAMERA) == 1 && !NetPlay::IsNetPlayRunning())
{
auto duel_scanner = std::make_unique<USB::DuelScanner>(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<USB::MotionCamera>(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<USB::SkylanderUSB>(GetEmulationKernel());

View File

@ -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;

View File

@ -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;

View File

@ -400,7 +400,10 @@
<ClInclude Include="Core\IOS\USB\Bluetooth\WiimoteDevice.h" />
<ClInclude Include="Core\IOS\USB\Bluetooth\WiimoteHIDAttr.h" />
<ClInclude Include="Core\IOS\USB\Common.h" />
<ClInclude Include="Core\IOS\USB\Emulated\CameraBase.h" />
<ClInclude Include="Core\IOS\USB\Emulated\DuelScanner.h" />
<ClInclude Include="Core\IOS\USB\Emulated\Infinity.h" />
<ClInclude Include="Core\IOS\USB\Emulated\MotionCamera.h" />
<ClInclude Include="Core\IOS\USB\Emulated\Skylanders\Skylander.h" />
<ClInclude Include="Core\IOS\USB\Emulated\Skylanders\SkylanderCrypto.h" />
<ClInclude Include="Core\IOS\USB\Emulated\Skylanders\SkylanderFigure.h" />
@ -1063,7 +1066,10 @@
<ClCompile Include="Core\IOS\USB\Bluetooth\WiimoteDevice.cpp" />
<ClCompile Include="Core\IOS\USB\Bluetooth\WiimoteHIDAttr.cpp" />
<ClCompile Include="Core\IOS\USB\Common.cpp" />
<ClCompile Include="Core\IOS\USB\Emulated\CameraBase.cpp" />
<ClCompile Include="Core\IOS\USB\Emulated\DuelScanner.cpp" />
<ClCompile Include="Core\IOS\USB\Emulated\Infinity.cpp" />
<ClCompile Include="Core\IOS\USB\Emulated\MotionCamera.cpp" />
<ClCompile Include="Core\IOS\USB\Emulated\Skylanders\Skylander.cpp" />
<ClCompile Include="Core\IOS\USB\Emulated\Skylanders\SkylanderCrypto.cpp" />
<ClCompile Include="Core\IOS\USB\Emulated\Skylanders\SkylanderFigure.cpp" />

View File

@ -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

View File

@ -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

View File

@ -0,0 +1,191 @@
// Copyright 2025 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <string>
#include <QComboBox>
#include <QCameraDevice>
#include <QBoxLayout>
#include <QLabel>
#include <QMediaDevices>
#include <QMediaCaptureSession>
#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<QCameraDevice> 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();
}

View File

@ -0,0 +1,43 @@
// Copyright 2025 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <array>
#include <QCamera>
#include <QVideoFrame>
#include <QVideoSink>
#include <QWidget>
#include <QPushButton>
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;
};

View File

@ -54,6 +54,7 @@
<ClCompile Include="Achievements\AchievementProgressWidget.cpp" />
<ClCompile Include="Achievements\AchievementSettingsWidget.cpp" />
<ClCompile Include="Achievements\AchievementsWindow.cpp" />
<ClCompile Include="CameraQt\CameraQt.cpp" />
<ClCompile Include="Config\ARCodeWidget.cpp" />
<ClCompile Include="Config\CheatCodeEditor.cpp" />
<ClCompile Include="Config\CheatWarningWidget.cpp" />
@ -240,6 +241,7 @@
also be modified using the VS UI.
-->
<ItemGroup>
<ClInclude Include="CameraQt\CameraQt.h" />
<ClInclude Include="Config\CheatCodeEditor.h" />
<ClInclude Include="Config\ConfigControls\ConfigControl.h" />
<ClInclude Include="Config\GameConfigEdit.h" />
@ -278,6 +280,7 @@
<QtMoc Include="Achievements\AchievementProgressWidget.h" />
<QtMoc Include="Achievements\AchievementSettingsWidget.h" />
<QtMoc Include="Achievements\AchievementsWindow.h" />
<QtMoc Include="CameraQt\CameraQt.h" />
<QtMoc Include="Config\ARCodeWidget.h" />
<QtMoc Include="Config\CheatWarningWidget.h" />
<QtMoc Include="Config\CommonControllersWidget.h" />

View File

@ -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

View File

@ -44,6 +44,8 @@ signals:
void JitProfileDataWiped();
void PPCSymbolsChanged();
void PPCBreakpointsChanged();
void CameraStart(u16 width, u16 height);
void CameraStop();
private:
Host();

View File

@ -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<std::string> save_state_path;
if (options.is_set("save_state"))
{

View File

@ -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)

View File

@ -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;

View File

@ -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);

View File

@ -89,6 +89,7 @@ signals:
void ShowAboutDialog();
void ShowCheatsManager();
void ShowResourcePackManager();
void ShowCameraWindow();
void ShowSkylanderPortal();
void ShowInfinityBase();
void ConnectWiiRemote(int id);

View File

@ -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)
{
}

View File

@ -22,6 +22,7 @@ static const std::map<std::pair<u16, u16>, 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"},

View File

@ -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)
{
}

View File

@ -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)
{
}

View File

@ -26,6 +26,7 @@
<AdditionalIncludeDirectories>$(QtIncludeDir)QtCore;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<AdditionalIncludeDirectories>$(QtIncludeDir)QtGui;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<AdditionalIncludeDirectories>$(QtIncludeDir)QtWidgets;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<AdditionalIncludeDirectories>$(QtIncludeDir)QtMultimedia;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<!--
As of Qt6.3, Qt needs user code deriving from certain Qt types to have RTTI (AS WELL AS MOC, UGH).
Do NOT enable in dolphin outside of Qt-dependant code.
@ -34,7 +35,7 @@
</ClCompile>
<Link>
<AdditionalLibraryDirectories>$(QtLibDir);%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories>
<AdditionalDependencies>Qt6Core$(QtLibSuffix).lib;Qt6Gui$(QtLibSuffix).lib;Qt6Widgets$(QtLibSuffix).lib;%(AdditionalDependencies)</AdditionalDependencies>
<AdditionalDependencies>Qt6Core$(QtLibSuffix).lib;Qt6Gui$(QtLibSuffix).lib;Qt6Widgets$(QtLibSuffix).lib;Qt6Multimedia$(QtLibSuffix).lib;%(AdditionalDependencies)</AdditionalDependencies>
<SubSystem>Windows</SubSystem>
<!--
<AdditionalOptions>"/manifestdependency:type='Win32' name='Microsoft.Windows.Common-Controls' version='6.0.0.0' processorArchitecture='*' publicKeyToken='6595b64144ccf1df' language='*'\" %(AdditionalOptions)</AdditionalOptions>
@ -109,11 +110,11 @@
<!--Copy the needed dlls-->
<ItemGroup>
<QtDllNames_ Include="Qt6Core;Qt6Gui;Qt6Widgets;Qt6Svg" />
<QtDllNames_ Include="Qt6Core;Qt6Gui;Qt6Widgets;Qt6Svg;Qt6Multimedia;Qt6Network" />
<QtDllNames Include="@(QtDllNames_ -> '%(Identity)$(QtLibSuffix).dll')" />
<QtDllsSrc Include="@(QtDllNames -> '$(QtBinDir)%(Identity)')" />
<QtDllsDst Include="@(QtDllNames -> '$(BinaryOutputDir)%(Identity)')" />
<QtPluginNames_ Include="iconengines\qsvgicon;imageformats\qsvg;platforms\qdirect2d;platforms\qwindows;styles\qwindowsvistastyle"/>
<QtPluginNames_ Include="iconengines\qsvgicon;imageformats\qsvg;platforms\qdirect2d;platforms\qwindows;styles\qwindowsvistastyle;multimedia\windowsmediaplugin"/>
<QtPluginNames Include="@(QtPluginNames_ -> '%(Identity)$(QtLibSuffix).dll')" />
<QtPluginsSrc Include="@(QtPluginNames -> '$(QtPluginsDir)%(Identity)')" />
<QtPluginsDst Include="@(QtPluginNames -> '$(BinaryOutputDir)$(QtPluginFolder)\%(Identity)')" />