diff --git a/BUILD.md b/BUILD.md index 6e1a2be9..276a5263 100644 --- a/BUILD.md +++ b/BUILD.md @@ -242,11 +242,12 @@ Example usage: `cmake -S . -B build -DCMAKE_BUILD_TYPE=release -DENABLE_SDL=ON - |--------------------|:--|-----------------------------------------------------------------------------|---------|--------------------| | ALLOW_PORTABLE | | Allow Cemu to use the `portable` directory to store configs and data | ON | | | CEMU_CXX_FLAGS | | Flags passed straight to the compiler, e.g. `-march=native`, `-Wall`, `/W3` | "" | | +| ENABLE_CAMERA | | Enable camera support | ON | | | ENABLE_CUBEB | | Enable cubeb audio backend | ON | | | ENABLE_DISCORD_RPC | | Enable Discord Rich presence support | ON | | | ENABLE_OPENGL | | Enable OpenGL graphics backend | ON | | | ENABLE_HIDAPI | | Enable HIDAPI (used for Wiimote controller API) | ON | | -| ENABLE_SDL | | Enable SDLController controller API | ON | | +| ENABLE_SDL | | Enable SDLController controller API/SDLCamera implementation | ON | | | ENABLE_VCPKG | | Use VCPKG package manager to obtain dependencies | ON | | | ENABLE_VULKAN | | Enable the Vulkan graphics backend | ON | | | ENABLE_WXWIDGETS | | Enable wxWidgets UI | ON | Currently required | diff --git a/CMakeLists.txt b/CMakeLists.txt index 644032ce..7738daff 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -135,6 +135,7 @@ endif() option(ENABLE_HIDAPI "Build with HIDAPI" ON) option(ENABLE_SDL "Enables the SDLController backend" ON) option(ENABLE_LIBUSB "Build with libusb support" ON) +option(ENABLE_CAMERA "Build with support for camera access" ON) if (ENABLE_LIBUSB) add_compile_definitions(HAS_LIBUSB) diff --git a/src/Cafe/OS/common/OSUtil.h b/src/Cafe/OS/common/OSUtil.h index 31784882..6f0e9449 100644 --- a/src/Cafe/OS/common/OSUtil.h +++ b/src/Cafe/OS/common/OSUtil.h @@ -242,3 +242,14 @@ MPTR makeCallableExport() } void osLib_addVirtualPointer(const char* libraryName, const char* functionName, uint32 vPtr); + +class CafeLockGuard +{ +public: + explicit CafeLockGuard(coreinit::OSMutex* mutex) : m_mutex(mutex) { coreinit::OSLockMutex(m_mutex); } + ~CafeLockGuard() { coreinit::OSUnlockMutex(m_mutex); } + CafeLockGuard(const CafeLockGuard&) = delete; + CafeLockGuard& operator=(const CafeLockGuard&) = delete; +private: + coreinit::OSMutex* m_mutex; +}; diff --git a/src/Cafe/OS/libs/camera/camera.cpp b/src/Cafe/OS/libs/camera/camera.cpp index 6812de7b..5ac18969 100644 --- a/src/Cafe/OS/libs/camera/camera.cpp +++ b/src/Cafe/OS/libs/camera/camera.cpp @@ -5,278 +5,370 @@ #include "Cafe/OS/libs/coreinit/coreinit_Alarm.h" #include "Cafe/OS/libs/coreinit/coreinit_Time.h" #include "Cafe/HW/Espresso/PPCCallback.h" +#include "input/camera/CameraManager.h" +#include "Common/CafeString.h" +#include "OS/common/OSUtil.h" +#include "OS/libs/coreinit/coreinit_Misc.h" +#include "util/helpers/ringbuffer.h" namespace camera { + enum CAMStatus : sint32 + { + CAM_STATUS_SUCCESS = 0, + CAM_STATUS_INVALID_ARG = -1, + CAM_STATUS_INVALID_HANDLE = -2, + CAM_STATUS_SURFACE_QUEUE_FULL = -4, + CAM_STATUS_INSUFFICIENT_MEMORY = -5, + CAM_STATUS_NOT_READY = -6, + CAM_STATUS_UNINITIALIZED = -8, + CAM_STATUS_UVC_ERROR = -9, + CAM_STATUS_DECODER_INIT_INIT_FAILED = -10, + CAM_STATUS_DEVICE_IN_USE = -12, + CAM_STATUS_DECODER_SESSION_FAILED = -13, + CAM_STATUS_INVALID_PROPERTY = -14, + CAM_STATUS_SEGMENT_VIOLATION = -15 + }; - struct CAMInitInfo_t - { - /* +0x00 */ uint32be ukn00; - /* +0x04 */ uint32be width; - /* +0x08 */ uint32be height; - - /* +0x0C */ uint32be workMemorySize; - /* +0x10 */ MEMPTR workMemory; + enum class CAMFps : uint32 + { + FPS_15 = 0, + FPS_30 = 1 + }; - /* +0x14 */ uint32be handlerFuncPtr; + enum class CAMEventType : uint32 + { + Decode = 0, + // Implement when AVM*DRC* functions are added + Detached [[maybe_unused]] = 1 + }; - /* +0x18 */ uint32be ukn18; - /* +0x1C */ uint32be fps; + enum class CAMForceDisplay + { + None = 0, + DRC = 1 + }; - /* +0x20 */ uint32be ukn20; - }; + enum class CAMImageType : uint32 + { + Default = 0 + }; - struct CAMTargetSurface - { - /* +0x00 */ uint32be surfaceSize; - /* +0x04 */ MEMPTR surfacePtr; - /* +0x08 */ uint32be ukn08; - /* +0x0C */ uint32be ukn0C; - /* +0x10 */ uint32be ukn10; - /* +0x14 */ uint32be ukn14; - /* +0x18 */ uint32be ukn18; - /* +0x1C */ uint32be ukn1C; - }; + struct CAMImageInfo + { + betype type; + uint32be height; + uint32be width; + }; - struct CAMCallbackParam - { - // type 0 - frame decoded | field1 - imagePtr, field2 - imageSize, field3 - ukn (0) - // type 1 - ??? + static_assert(sizeof(CAMImageInfo) == 0x0C); + struct CAMInitInfo_t + { + CAMImageInfo imageInfo; + uint32be workMemorySize; + MEMPTR workMemoryData; + MEMPTR callback; + betype forceDisplay; + betype fps; + uint32be threadFlags; + uint8 unk[0x10]; + }; - /* +0x0 */ uint32be type; // 0 -> Frame decoded - /* +0x4 */ uint32be field1; - /* +0x8 */ uint32be field2; - /* +0xC */ uint32be field3; - }; + static_assert(sizeof(CAMInitInfo_t) == 0x34); + struct CAMTargetSurface + { + sint32be size; + MEMPTR data; + uint8 unused[0x18]; + }; - #define CAM_ERROR_SUCCESS 0 - #define CAM_ERROR_INVALID_HANDLE -8 + static_assert(sizeof(CAMTargetSurface) == 0x20); - std::vector g_table_cameraHandles; - std::vector g_activeCameraInstances; - std::recursive_mutex g_mutex_camera; - std::atomic_int g_cameraCounter{ 0 }; - SysAllocator g_alarm_camera; - SysAllocator g_cameraHandlerParam; + struct CAMDecodeEventParam + { + betype type; + MEMPTR data; + uint32be channel; + uint32be errored; + }; - CameraInstance* GetCameraInstanceByHandle(sint32 camHandle) - { - std::unique_lock _lock(g_mutex_camera); - if (camHandle <= 0) - return nullptr; - camHandle -= 1; - if (camHandle >= g_table_cameraHandles.size()) - return nullptr; - return g_table_cameraHandles[camHandle]; - } + static_assert(sizeof(CAMDecodeEventParam) == 0x10); - struct CameraInstance - { - CameraInstance(uint32 frameWidth, uint32 frameHeight, MPTR handlerFunc) : width(frameWidth), height(frameHeight), handlerFunc(handlerFunc) { AcquireHandle(); }; - ~CameraInstance() { if (isOpen) { CloseCam(); } ReleaseHandle(); }; + constexpr static int32_t CAM_HANDLE = 0; - sint32 handle{ 0 }; - uint32 width; - uint32 height; - bool isOpen{false}; - std::queue queue_targetSurfaces; - MPTR handlerFunc; + struct + { + bool previouslyInitialized = false; + bool initialized = false; + std::atomic_bool isOpen = false; + std::atomic_bool isExiting = false; + std::atomic_bool isWorkerExiting = false; + unsigned fps = 30; + MEMPTR eventCallback = nullptr; + RingBuffer, 20> inTargetBuffers{}; + } s_instance; - bool OpenCam() - { - if (isOpen) - return false; - isOpen = true; - g_activeCameraInstances.push_back(this); - return true; - } + SysAllocator s_cameraMutex; + SysAllocator s_cameraEventData; + SysAllocator s_cameraWorkerThread; + SysAllocator s_cameraWorkerThreadStack; + SysAllocator s_cameraWorkerThreadName("CameraWorkerThread"); + SysAllocator s_cameraOpenEvent; + SysAllocator s_driverInterface; + SysAllocator s_driverName("CAM"); - bool CloseCam() - { - if (!isOpen) - return false; - isOpen = false; - vectorRemoveByValue(g_activeCameraInstances, this); - return true; - } + static void WorkerThreadFunc(PPCInterpreter_t*) + { + s_cameraEventData->type = CAMEventType::Decode; + s_cameraEventData->channel = 0; + s_cameraEventData->data = nullptr; + s_cameraEventData->errored = false; + PPCCoreCallback(s_instance.eventCallback, s_cameraEventData.GetMPTR()); - void QueueTargetSurface(CAMTargetSurface* targetSurface) - { - std::unique_lock _lock(g_mutex_camera); - cemu_assert_debug(queue_targetSurfaces.size() < 100); // check for sane queue length - queue_targetSurfaces.push(*targetSurface); - } + while (!s_instance.isExiting) + { + coreinit::OSWaitEvent(s_cameraOpenEvent); + while (true) + { + const auto surfaceBuffer = s_instance.inTargetBuffers.Pop(); + if (!surfaceBuffer) + { + // Only exit when no buffers are left + if (!s_instance.isOpen || s_instance.isExiting) + break; + s_cameraEventData->data = nullptr; + s_cameraEventData->errored = true; + } + else + { + CameraManager::FillNV12Buffer( + std::span( + surfaceBuffer.GetPtr(), CameraManager::CAMERA_NV12_BUFFER_SIZE)); + s_cameraEventData->data = surfaceBuffer; + s_cameraEventData->errored = false; + } + s_cameraEventData->type = CAMEventType::Decode; + s_cameraEventData->channel = 0; + PPCCoreCallback(s_instance.eventCallback, s_cameraEventData.GetMPTR()); + coreinit::OSSleepTicks(coreinit::EspressoTime::ConvertMsToTimerTicks(1000 / s_instance.fps)); + } + } + s_instance.isWorkerExiting = false; + coreinit::OSExitThread(0); + } - private: - void AcquireHandle() - { - std::unique_lock _lock(g_mutex_camera); - for (uint32 i = 0; i < g_table_cameraHandles.size(); i++) - { - if (g_table_cameraHandles[i] == nullptr) - { - g_table_cameraHandles[i] = this; - this->handle = i + 1; - return; - } - } - this->handle = (sint32)(g_table_cameraHandles.size() + 1); - g_table_cameraHandles.push_back(this); - } + sint32 CAMGetMemReq(const CAMImageInfo* info) + { + if (!info) + return CAM_STATUS_INVALID_ARG; + return 1 * 1024; // always return 1KB + } - void ReleaseHandle() - { - for (uint32 i = 0; i < g_table_cameraHandles.size(); i++) - { - if (g_table_cameraHandles[i] == this) - { - g_table_cameraHandles[i] = nullptr; - return; - } - } - cemu_assert_debug(false); - } - }; + CAMStatus CAMCheckMemSegmentation(const void* startAddr, uint32 size) + { + if (!startAddr || size == 0) + return CAM_STATUS_INVALID_ARG; + return CAM_STATUS_SUCCESS; + } - sint32 CAMGetMemReq(void* ukn) - { - return 1 * 1024; // always return 1KB - } + sint32 CAMInit(sint32 channel, const CAMInitInfo_t* initInfo, betype* error) + { + *error = CAM_STATUS_SUCCESS; + CafeLockGuard lock(s_cameraMutex); + if (s_instance.initialized) + { + *error = CAM_STATUS_DEVICE_IN_USE; + return -1; + } - sint32 CAMCheckMemSegmentation(void* base, uint32 size) - { - return CAM_ERROR_SUCCESS; // always return success - } + if (channel != 0 || !initInfo || !initInfo->workMemoryData || + !match_any_of(initInfo->forceDisplay, CAMForceDisplay::None, CAMForceDisplay::DRC) || + !match_any_of(initInfo->fps, CAMFps::FPS_15, CAMFps::FPS_30) || + initInfo->imageInfo.type != CAMImageType::Default) + { + *error = CAM_STATUS_INVALID_ARG; + return -1; + } - void ppcCAMUpdate60(PPCInterpreter_t* hCPU) - { - // update all open camera instances - size_t numCamInstances = g_activeCameraInstances.size(); - //for (auto& itr : g_activeCameraInstances) - for(size_t i=0; i _lock(g_mutex_camera); - if (i >= g_activeCameraInstances.size()) - break; - CameraInstance* camInstance = g_activeCameraInstances[i]; - // todo - handle 30 / 60 FPS - if (camInstance->queue_targetSurfaces.empty()) - continue; - auto& targetSurface = camInstance->queue_targetSurfaces.front(); - g_cameraHandlerParam->type = 0; - g_cameraHandlerParam->field1 = targetSurface.surfacePtr.GetMPTR(); - g_cameraHandlerParam->field2 = targetSurface.surfaceSize; - g_cameraHandlerParam->field3 = 0; - cemu_assert_debug(camInstance->handlerFunc != MPTR_NULL); - camInstance->queue_targetSurfaces.pop(); - _lock.unlock(); - PPCCoreCallback(camInstance->handlerFunc, g_cameraHandlerParam.GetPtr()); - } - osLib_returnFromFunction(hCPU, 0); - } + if (s_instance.previouslyInitialized) + { + while (s_instance.isWorkerExiting) + { + coreinit::OSSleepTicks(coreinit::EspressoTime::ConvertMsToTimerTicks(1000)); + } + } + cemu_assert_debug(initInfo->forceDisplay != CAMForceDisplay::DRC); + cemu_assert_debug(initInfo->workMemorySize != 0); + cemu_assert_debug(initInfo->imageInfo.type == CAMImageType::Default); - sint32 CAMInit(uint32 cameraId, CAMInitInfo_t* camInitInfo, uint32be* error) - { - CameraInstance* camInstance = new CameraInstance(camInitInfo->width, camInitInfo->height, camInitInfo->handlerFuncPtr); - *error = 0; // Hunter's Trophy 2 will fail to boot if we don't set this - std::unique_lock _lock(g_mutex_camera); - if (g_cameraCounter == 0) - { - coreinit::OSCreateAlarm(g_alarm_camera.GetPtr()); - coreinit::OSSetPeriodicAlarm(g_alarm_camera.GetPtr(), coreinit::OSGetTime(), (uint64)ESPRESSO_TIMER_CLOCK / 60ull, RPLLoader_MakePPCCallable(ppcCAMUpdate60)); - } - g_cameraCounter++; + s_instance.isExiting = false; + s_instance.fps = initInfo->fps == CAMFps::FPS_15 ? 15 : 30; + s_instance.initialized = true; + s_instance.eventCallback = initInfo->callback; - return camInstance->handle; - } + coreinit::OSInitEvent(s_cameraOpenEvent, coreinit::OSEvent::EVENT_STATE::STATE_NOT_SIGNALED, + coreinit::OSEvent::EVENT_MODE::MODE_AUTO); - sint32 CAMExit(sint32 camHandle) - { - CameraInstance* camInstance = GetCameraInstanceByHandle(camHandle); - if (!camInstance) - return CAM_ERROR_INVALID_HANDLE; - CAMClose(camHandle); - delete camInstance; + coreinit::__OSCreateThreadType( + s_cameraWorkerThread, RPLLoader_MakePPCCallable(WorkerThreadFunc), 0, nullptr, + s_cameraWorkerThreadStack.GetPtr() + s_cameraWorkerThreadStack.GetByteSize(), + s_cameraWorkerThreadStack.GetByteSize(), + 0x10, initInfo->threadFlags & 7, OSThread_t::THREAD_TYPE::TYPE_DRIVER); + coreinit::OSSetThreadName(s_cameraWorkerThread.GetPtr(), s_cameraWorkerThreadName.GetPtr()); + coreinit::OSResumeThread(s_cameraWorkerThread.GetPtr()); - std::unique_lock _lock(g_mutex_camera); - g_cameraCounter--; - if (g_cameraCounter == 0) - coreinit::OSCancelAlarm(g_alarm_camera.GetPtr()); - return CAM_ERROR_SUCCESS; - } + s_instance.previouslyInitialized = true; - sint32 CAMOpen(sint32 camHandle) - { - CameraInstance* camInstance = GetCameraInstanceByHandle(camHandle); - if (!camInstance) - return CAM_ERROR_INVALID_HANDLE; - camInstance->OpenCam(); - return CAM_ERROR_SUCCESS; - } + return channel; + } - sint32 CAMClose(sint32 camHandle) - { - CameraInstance* camInstance = GetCameraInstanceByHandle(camHandle); - if (!camInstance) - return CAM_ERROR_INVALID_HANDLE; - camInstance->CloseCam(); - return CAM_ERROR_SUCCESS; - } + CAMStatus CAMClose(sint32 camHandle) + { + if (camHandle != CAM_HANDLE) + return CAM_STATUS_INVALID_HANDLE; - sint32 CAMSubmitTargetSurface(sint32 camHandle, CAMTargetSurface* targetSurface) - { - CameraInstance* camInstance = GetCameraInstanceByHandle(camHandle); - if (!camInstance) - return CAM_ERROR_INVALID_HANDLE; - - camInstance->QueueTargetSurface(targetSurface); + // Check necessary because this function is called on unload, and OSMutex needs an active scheduler + const auto schedulerActive = coreinit::OSIsSchedulerActive(); + if (schedulerActive) + coreinit::OSLockMutex(s_cameraMutex); - return CAM_ERROR_SUCCESS; - } + if (!s_instance.initialized || !s_instance.isOpen) + return CAM_STATUS_UNINITIALIZED; + s_instance.isOpen = false; + CameraManager::Close(); + if (schedulerActive) + coreinit::OSUnlockMutex(s_cameraMutex); + return CAM_STATUS_SUCCESS; + } - void reset() - { - g_cameraCounter = 0; - } + CAMStatus CAMOpen(sint32 camHandle) + { + if (camHandle != CAM_HANDLE) + return CAM_STATUS_INVALID_HANDLE; + CafeLockGuard lock(s_cameraMutex); + if (!s_instance.initialized) + return CAM_STATUS_UNINITIALIZED; + if (s_instance.isOpen) + return CAM_STATUS_DEVICE_IN_USE; + CameraManager::Open(); + s_instance.isOpen = true; + coreinit::OSSignalEvent(s_cameraOpenEvent); + s_instance.inTargetBuffers.Clear(); + return CAM_STATUS_SUCCESS; + } - class : public COSModule - { - public: - std::string_view GetName() override - { - return "camera"; - } + CAMStatus CAMSubmitTargetSurface(sint32 camHandle, CAMTargetSurface* targetSurface) + { + if (camHandle != CAM_HANDLE) + return CAM_STATUS_INVALID_HANDLE; + if (!targetSurface || targetSurface->data.IsNull() || targetSurface->size < 1) + return CAM_STATUS_INVALID_ARG; + cemu_assert_debug(targetSurface->size >= CameraManager::CAMERA_NV12_BUFFER_SIZE); + CafeLockGuard lock(s_cameraMutex); + if (!s_instance.initialized) + return CAM_STATUS_UNINITIALIZED; + if (!s_instance.inTargetBuffers.Push(targetSurface->data)) + return CAM_STATUS_SURFACE_QUEUE_FULL; + return CAM_STATUS_SUCCESS; + } - void RPLMapped() override - { - cafeExportRegister("camera", CAMGetMemReq, LogType::Placeholder); - cafeExportRegister("camera", CAMCheckMemSegmentation, LogType::Placeholder); - cafeExportRegister("camera", CAMInit, LogType::Placeholder); - cafeExportRegister("camera", CAMExit, LogType::Placeholder); - cafeExportRegister("camera", CAMOpen, LogType::Placeholder); - cafeExportRegister("camera", CAMClose, LogType::Placeholder); - cafeExportRegister("camera", CAMSubmitTargetSurface, LogType::Placeholder); - }; + void CAMExit(sint32 camHandle) + { + if (camHandle != CAM_HANDLE || !s_instance.previouslyInitialized) + return; + CafeLockGuard lock(s_cameraMutex); + if (!s_instance.initialized) + return; + s_instance.isExiting = true; + s_instance.isWorkerExiting = true; + if (s_instance.isOpen) + CAMClose(camHandle); + coreinit::OSSignalEvent(s_cameraOpenEvent.GetPtr()); + s_instance.initialized = false; + } - void rpl_entry(uint32 moduleHandle, coreinit::RplEntryReason reason) override - { - if (reason == coreinit::RplEntryReason::Loaded) - { - reset(); - } - else if (reason == coreinit::RplEntryReason::Unloaded) - { - // todo - } - } - }s_COScameraModule; + namespace Driver + { + MEMPTR Name() + { + return s_driverName.GetPtr(); + } + void Init() {} + void Acquire() {} + void Release() + { + CAMClose(CAM_HANDLE); + } + void Done() + { + CAMClose(CAM_HANDLE); + } + } - COSModule* GetModule() - { - return &s_COScameraModule; - } + class : public COSModule + { + public: + std::string_view GetName() override + { + return "camera"; + } + + void RPLMapped() override + { + cafeExportRegister("camera", CAMGetMemReq, LogType::InputAPI); + cafeExportRegister("camera", CAMCheckMemSegmentation, LogType::InputAPI); + cafeExportRegister("camera", CAMInit, LogType::InputAPI); + cafeExportRegister("camera", CAMExit, LogType::InputAPI); + cafeExportRegister("camera", CAMOpen, LogType::InputAPI); + cafeExportRegister("camera", CAMClose, LogType::InputAPI); + cafeExportRegister("camera", CAMSubmitTargetSurface, LogType::InputAPI); + }; + + void rpl_entry(uint32 moduleHandle, coreinit::RplEntryReason reason) override + { + if (reason == coreinit::RplEntryReason::Loaded) + { + s_driverInterface->getDriverName = RPLLoader_MakePPCCallable(+[](PPCInterpreter_t* hCPU) + { + osLib_returnFromFunction(hCPU, Driver::Name().GetMPTR()); + }); + + s_driverInterface->init = RPLLoader_MakePPCCallable(+[](PPCInterpreter_t* hCPU) + { + Driver::Init(); + osLib_returnFromFunction(hCPU, 0); + }); + s_driverInterface->onAcquireForeground = RPLLoader_MakePPCCallable(+[](PPCInterpreter_t* hCPU) + { + Driver::Acquire(); + osLib_returnFromFunction(hCPU, 0); + }); + s_driverInterface->onReleaseForeground = RPLLoader_MakePPCCallable(+[](PPCInterpreter_t* hCPU) + { + Driver::Release(); + osLib_returnFromFunction(hCPU, 0); + }); + s_driverInterface->done = RPLLoader_MakePPCCallable(+[](PPCInterpreter_t* hCPU) + { + Driver::Done(); + osLib_returnFromFunction(hCPU, 0); + }); + coreinit::OSDriver_Register(moduleHandle, 0x226, s_driverInterface.GetPtr(), 0, nullptr, nullptr, + nullptr); + } + else if (reason == coreinit::RplEntryReason::Unloaded) + { + Driver::Done(); + coreinit::OSDriver_Deregister(moduleHandle, 0); + } + } + } s_COScameraModule; + + COSModule* GetModule() + { + return &s_COScameraModule; + } } - diff --git a/src/Cafe/OS/libs/camera/camera.h b/src/Cafe/OS/libs/camera/camera.h index 7171470a..3761a978 100644 --- a/src/Cafe/OS/libs/camera/camera.h +++ b/src/Cafe/OS/libs/camera/camera.h @@ -3,8 +3,5 @@ namespace camera { - sint32 CAMOpen(sint32 camHandle); - sint32 CAMClose(sint32 camHandle); - COSModule* GetModule(); }; \ No newline at end of file diff --git a/src/config/CemuConfig.cpp b/src/config/CemuConfig.cpp index 139c290c..b5e8720b 100644 --- a/src/config/CemuConfig.cpp +++ b/src/config/CemuConfig.cpp @@ -284,12 +284,17 @@ XMLConfigParser CemuConfig::Load(XMLConfigParser& parser) dsu_client.host = dsuc.get_attribute("host", dsu_client.host); dsu_client.port = dsuc.get_attribute("port", dsu_client.port); + // emulatedusbdevices auto usbdevices = parser.get("EmulatedUsbDevices"); emulated_usb_devices.emulate_skylander_portal = usbdevices.get("EmulateSkylanderPortal", emulated_usb_devices.emulate_skylander_portal); emulated_usb_devices.emulate_infinity_base = usbdevices.get("EmulateInfinityBase", emulated_usb_devices.emulate_infinity_base); emulated_usb_devices.emulate_dimensions_toypad = usbdevices.get("EmulateDimensionsToypad", emulated_usb_devices.emulate_dimensions_toypad); + + auto camera = parser.get("Camera"); + camera_id = camera.get("DeviceId", ""); + return parser; } @@ -454,6 +459,9 @@ XMLConfigParser CemuConfig::Save(XMLConfigParser& parser) usbdevices.set("EmulateInfinityBase", emulated_usb_devices.emulate_infinity_base.GetValue()); usbdevices.set("EmulateDimensionsToypad", emulated_usb_devices.emulate_dimensions_toypad.GetValue()); + auto camera = config.set("Camera"); + camera.set("DeviceId", camera_id.GetValue()); + return config; } diff --git a/src/config/CemuConfig.h b/src/config/CemuConfig.h index 48c396f2..1ad32fa5 100644 --- a/src/config/CemuConfig.h +++ b/src/config/CemuConfig.h @@ -512,6 +512,10 @@ struct CemuConfig // debug ConfigValueBounds crash_dump{ CrashDump::Disabled }; ConfigValue gdb_port{ 1337 }; + + // camera + ConfigValue camera_id; + #ifdef ENABLE_METAL ConfigValue gpu_capture_dir{ "" }; ConfigValue framebuffer_fetch{ true }; diff --git a/src/gui/wxgui/CMakeLists.txt b/src/gui/wxgui/CMakeLists.txt index 079a78cd..bd74faf9 100644 --- a/src/gui/wxgui/CMakeLists.txt +++ b/src/gui/wxgui/CMakeLists.txt @@ -1,4 +1,6 @@ add_library(CemuWxGui STATIC + CameraSettingsWindow.cpp + CameraSettingsWindow.h canvas/IRenderCanvas.h CemuApp.cpp CemuApp.h diff --git a/src/gui/wxgui/CameraSettingsWindow.cpp b/src/gui/wxgui/CameraSettingsWindow.cpp new file mode 100644 index 00000000..42f85cfc --- /dev/null +++ b/src/gui/wxgui/CameraSettingsWindow.cpp @@ -0,0 +1,179 @@ +#include "CameraSettingsWindow.h" + +#include "input/camera/CameraManager.h" +#include +#include +#include +#include +#include +#include + +struct wxCameraDeviceUniqueId : public wxClientData +{ + std::string id; + + explicit wxCameraDeviceUniqueId(std::string id) : id(std::move(id)) + { + } +}; + +CameraSettingsWindow::CameraSettingsWindow(wxWindow* parent) + : wxDialog(parent, wxID_ANY, _("Camera settings"), wxDefaultPosition), + m_imageBitmap(CameraManager::CAMERA_WIDTH, CameraManager::CAMERA_HEIGHT, 24), + m_imageBuffer(CameraManager::CAMERA_RGB_BUFFER_SIZE) +{ + CameraManager::Open(); + auto* rootSizer = new wxBoxSizer(wxVERTICAL); + { + auto* topSizer = new wxBoxSizer(wxHORIZONTAL); + { + m_cameraComboBox = new wxComboBox(this, wxID_ANY); + m_cameraComboBox->Bind(wxEVT_COMBOBOX, &CameraSettingsWindow::OnSelectCameraChoice, this); + m_cameraComboBox->SetToolTip(_("Cameras are only listed if they support 640x480")); + + m_refreshButton = new wxButton(this, wxID_ANY, _("Refresh")); + m_refreshButton->Bind(wxEVT_BUTTON, &CameraSettingsWindow::OnRefreshPressed, this); + wxQueueEvent(m_refreshButton, new wxCommandEvent{wxEVT_BUTTON}); + + topSizer->Add(m_cameraComboBox); + topSizer->Add(m_refreshButton); + } + + m_imageWindow = new wxWindow(this, wxID_ANY, wxDefaultPosition, + {CameraManager::CAMERA_WIDTH, CameraManager::CAMERA_HEIGHT}); + m_imageWindow->SetBackgroundStyle(wxBG_STYLE_PAINT); + + m_statusInfoText = new wxStaticText(this, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, + wxTE_READONLY); + + rootSizer->Add(topSizer); + rootSizer->Add(m_imageWindow, wxEXPAND); + rootSizer->Add(m_statusInfoText); + } + SetSizerAndFit(rootSizer); + m_imageUpdateTimer.Bind(wxEVT_TIMER, &CameraSettingsWindow::Update, this); + m_imageWindow->Bind(wxEVT_PAINT, &CameraSettingsWindow::DrawImage, this); + this->Bind(wxEVT_CLOSE_WINDOW, &CameraSettingsWindow::OnClose, this); + + m_imageUpdateTimer.Start(33, wxTIMER_CONTINUOUS); +} + +void CameraSettingsWindow::OnSelectCameraChoice(wxCommandEvent&) +{ + const auto selection = m_cameraComboBox->GetSelection(); + if (selection < 0) + { + m_cameraComboBox->Select(0); + return; + } + if (selection == 0) + CameraManager::ResetDevice(); + else + { + auto id = static_cast(m_cameraComboBox->GetClientData(selection))->id; + CameraManager::SetDevice(id); + } +} + +void CameraSettingsWindow::OnRefreshPressed(wxCommandEvent&) +{ + wxArrayString choices = {_("None")}; + const auto devices = CameraManager::EnumerateDevices(); + const auto currentDevice = CameraManager::GetCurrentDevice(); + auto currentIndex = 0; + for (auto i = 0; i < devices.size(); ++i) + { + auto& device = devices[i]; + choices.push_back(fmt::format("{} ({})", device.name, device.uniqueId)); + if (currentDevice && device.uniqueId == *currentDevice) + currentIndex = i + 1; + } + + m_cameraComboBox->Set(choices); + m_cameraComboBox->SetSelection(currentIndex); + // Client data can only be set when there are entries + for (auto i = 0; i < devices.size(); ++i) + { + auto& device = devices[i]; + const auto choiceIndex = i + 1; + m_cameraComboBox->SetClientData(choiceIndex, new wxCameraDeviceUniqueId{device.uniqueId}); + } +} + +void CameraSettingsWindow::Update(const wxTimerEvent&) +{ + switch (CameraManager::GetState()) + { + using enum CameraManager::State; + case NoSupport: + { + m_statusInfoText->SetLabel(_("This build does not support camera capture")); + return; + } + case NoDevice: + { + m_statusInfoText->SetLabel(_("No camera selected")); + return; + } + case UnsupportedFormat: + { + m_statusInfoText->SetLabel(_("Camera does not support the required format")); + } + case NotOpen: + { + m_statusInfoText->SetLabel(_("Camera is unavailable, or was closed due to an error")); + return; + } + case NeedsPermission: + { + m_statusInfoText->SetLabel(_("Waiting for permission to access camea")); + return; + } + case NoPermission: + { + m_statusInfoText->SetLabel(_("Cemu was denied permission to access camera")); + return; + } + case Capturing: + { + m_statusInfoText->SetLabel(_("Capturing")); + break; + } + default: + cemu_assert(false); + } + CameraManager::FillRGBBuffer(std::span(m_imageBuffer)); + wxNativePixelData data{m_imageBitmap}; + if (!data) + return; + wxNativePixelData::Iterator p{data}; + for (auto row = 0u; row < CameraManager::CAMERA_HEIGHT; ++row) + { + const auto* rowPtr = m_imageBuffer.data() + row * CameraManager::CAMERA_WIDTH * 3; + wxNativePixelData::Iterator rowStart = p; + for (auto col = 0u; col < CameraManager::CAMERA_WIDTH; ++col, ++p) + { + auto* colour = rowPtr + col * 3; + p.Red() = colour[0]; + p.Green() = colour[1]; + p.Blue() = colour[2]; + } + p = rowStart; + p.OffsetY(data, 1); + } + m_imageWindow->Refresh(); +} + +void CameraSettingsWindow::DrawImage(const wxPaintEvent&) +{ + wxAutoBufferedPaintDC dc{m_imageWindow}; + dc.DrawBitmap(m_imageBitmap, 0, 0); +} + +void CameraSettingsWindow::OnClose(wxCloseEvent& event) +{ + m_imageUpdateTimer.Stop(); + CameraManager::SaveDevice(); + CameraManager::Close(); + event.Skip(); +} diff --git a/src/gui/wxgui/CameraSettingsWindow.h b/src/gui/wxgui/CameraSettingsWindow.h new file mode 100644 index 00000000..d87893e5 --- /dev/null +++ b/src/gui/wxgui/CameraSettingsWindow.h @@ -0,0 +1,27 @@ +#pragma once +#include +#include +#include + +class wxButton; +class wxComboBox; +class wxStaticText; + +class CameraSettingsWindow : public wxDialog +{ + wxComboBox* m_cameraComboBox; + wxButton* m_refreshButton; + wxWindow* m_imageWindow; + wxBitmap m_imageBitmap; + wxStaticText* m_statusInfoText; + wxTimer m_imageUpdateTimer; + std::vector m_imageBuffer; + +public: + explicit CameraSettingsWindow(wxWindow* parent); + void OnSelectCameraChoice(wxCommandEvent&); + void OnRefreshPressed(wxCommandEvent&); + void Update(const wxTimerEvent&); + void DrawImage(const wxPaintEvent&); + void OnClose(wxCloseEvent& event); +}; diff --git a/src/gui/wxgui/MainWindow.cpp b/src/gui/wxgui/MainWindow.cpp index 698bf65a..587d50c3 100644 --- a/src/gui/wxgui/MainWindow.cpp +++ b/src/gui/wxgui/MainWindow.cpp @@ -7,6 +7,7 @@ #include "CemuUpdateWindow.h" #include "GraphicPacksWindow2.h" #include "AudioDebuggerWindow.h" +#include "CameraSettingsWindow.h" #include "input/InputSettings2.h" #include "input/HotkeySettings.h" #include "debugger/DebuggerWindow2.h" @@ -91,6 +92,7 @@ enum MAINFRAME_MENU_ID_OPTIONS_AUDIO, MAINFRAME_MENU_ID_OPTIONS_INPUT, MAINFRAME_MENU_ID_OPTIONS_HOTKEY, + MAINFRAME_MENU_ID_OPTIONS_CAMERA, MAINFRAME_MENU_ID_OPTIONS_MAC_SETTINGS, // options -> account MAINFRAME_MENU_ID_OPTIONS_ACCOUNT_1 = 20350, @@ -193,6 +195,7 @@ EVT_MENU(MAINFRAME_MENU_ID_OPTIONS_GENERAL2, MainWindow::OnOptionsInput) EVT_MENU(MAINFRAME_MENU_ID_OPTIONS_AUDIO, MainWindow::OnOptionsInput) EVT_MENU(MAINFRAME_MENU_ID_OPTIONS_INPUT, MainWindow::OnOptionsInput) EVT_MENU(MAINFRAME_MENU_ID_OPTIONS_HOTKEY, MainWindow::OnOptionsInput) +EVT_MENU(MAINFRAME_MENU_ID_OPTIONS_CAMERA, MainWindow::OnOptionsInput) EVT_MENU(MAINFRAME_MENU_ID_OPTIONS_MAC_SETTINGS, MainWindow::OnOptionsInput) // tools menu EVT_MENU(MAINFRAME_MENU_ID_TOOLS_MEMORY_SEARCHER, MainWindow::OnToolsInput) @@ -935,6 +938,14 @@ void MainWindow::OnOptionsInput(wxCommandEvent& event) frame->Show(); break; } + case MAINFRAME_MENU_ID_OPTIONS_CAMERA: + { + auto* frame = new CameraSettingsWindow(this); + frame->ShowModal(); + frame->Destroy(); + break; + } + } } @@ -2248,6 +2259,7 @@ void MainWindow::RecreateMenu() optionsMenu->Append(MAINFRAME_MENU_ID_OPTIONS_GENERAL2, _("&General settings")); optionsMenu->Append(MAINFRAME_MENU_ID_OPTIONS_INPUT, _("&Input settings")); optionsMenu->Append(MAINFRAME_MENU_ID_OPTIONS_HOTKEY, _("&Hotkey settings")); + optionsMenu->Append(MAINFRAME_MENU_ID_OPTIONS_CAMERA, _("&Camera settings")); optionsMenu->AppendSeparator(); optionsMenu->AppendSubMenu(m_optionsAccountMenu, _("&Active account")); diff --git a/src/input/CMakeLists.txt b/src/input/CMakeLists.txt index 4978ef0a..6670069b 100644 --- a/src/input/CMakeLists.txt +++ b/src/input/CMakeLists.txt @@ -19,6 +19,7 @@ add_library(CemuInput api/Keyboard/KeyboardControllerProvider.cpp api/Keyboard/KeyboardController.cpp api/Keyboard/KeyboardController.h + camera/CameraManager.h emulated/ProController.cpp emulated/EmulatedController.h emulated/EmulatedController.cpp @@ -98,6 +99,16 @@ if(ENABLE_SDL) target_link_libraries(CemuInput PRIVATE SDL3::SDL3) endif() +if (NOT ENABLE_CAMERA) + target_sources(CemuInput PRIVATE + camera/DummyCameraManager.cpp + ) +elseif (ENABLE_SDL) + target_sources(CemuInput PRIVATE + camera/SDLCameraManager.cpp + ) +endif () + target_include_directories(CemuInput PUBLIC "../") target_link_libraries(CemuInput PRIVATE diff --git a/src/input/camera/CameraManager.h b/src/input/camera/CameraManager.h new file mode 100644 index 00000000..40403414 --- /dev/null +++ b/src/input/camera/CameraManager.h @@ -0,0 +1,40 @@ +#pragma once + +namespace CameraManager +{ + enum class State + { + Capturing, + NoSupport, + NoDevice, + UnsupportedFormat, + NotOpen, + NeedsPermission, + NoPermission + }; + constexpr uint32 CAMERA_WIDTH = 640; + constexpr uint32 CAMERA_HEIGHT = 480; + constexpr uint32 CAMERA_PITCH = 768; + + constexpr uint32 CAMERA_NV12_BUFFER_SIZE = (CAMERA_HEIGHT * CAMERA_PITCH * 3) >> 1; + constexpr uint32 CAMERA_RGB_BUFFER_SIZE = CAMERA_HEIGHT * CAMERA_WIDTH * 3; + + struct DeviceInfo + { + std::string uniqueId; + std::string name; + }; + + void Open(); + void Close(); + + void FillNV12Buffer(std::span nv12Buffer); + void FillRGBBuffer(std::span rgbBuffer); + + void SetDevice(std::string_view deviceId); + void ResetDevice(); + std::vector EnumerateDevices(); + void SaveDevice(); + std::optional GetCurrentDevice(); + State GetState(); +} // namespace CameraManager diff --git a/src/input/camera/DummyCameraManager.cpp b/src/input/camera/DummyCameraManager.cpp new file mode 100644 index 00000000..cdc8eb51 --- /dev/null +++ b/src/input/camera/DummyCameraManager.cpp @@ -0,0 +1,51 @@ +#include "CameraManager.h" + +namespace CameraManager +{ + void FillNV12Buffer(std::span nv12Buffer) + { + constexpr static auto PIXEL_COUNT = CAMERA_HEIGHT * CAMERA_PITCH; + std::ranges::fill_n(nv12Buffer.data(), PIXEL_COUNT, 16); + std::ranges::fill_n(nv12Buffer.data() + PIXEL_COUNT, (PIXEL_COUNT / 2), 128); + } + + void FillRGBBuffer(std::span rgbBuffer) + { + std::ranges::fill(rgbBuffer, 0); + } + + void SetDevice(std::string_view deviceId) + { + } + + void ResetDevice() + { + } + + void Open() + { + } + + void Close() + { + } + + std::vector EnumerateDevices() + { + return {}; + } + + void SaveDevice() + { + } + + std::optional GetCurrentDevice() + { + return std::nullopt; + } + + State GetState() + { + return State::NoSupport; + } +} // namespace CameraManager diff --git a/src/input/camera/SDLCameraManager.cpp b/src/input/camera/SDLCameraManager.cpp new file mode 100644 index 00000000..d1f9c5bb --- /dev/null +++ b/src/input/camera/SDLCameraManager.cpp @@ -0,0 +1,315 @@ +#include "CameraManager.h" + +#include "config/CemuConfig.h" +#include "util/helpers/helpers.h" + +#include +#include +#include +#include + +#include +#include + +namespace CameraManager +{ + namespace + { + struct InternalDeviceInfo + { + SDL_CameraID id; + std::string name; + }; + + std::mutex s_deviceMutex; + std::mutex s_bufferMutex; + std::optional s_deviceId; + std::atomic s_camera; + std::unique_ptr s_rgbBufferOut; + std::unique_ptr s_nv12BufferOut; + int s_refCount = 0; + std::thread s_captureThread; + std::atomic_bool s_running = false; + std::atomic_bool s_permissionWasDenied = false; + std::atomic_bool s_unsupportedFormat = false; + } + + static void OpenStream() + { + s_unsupportedFormat = false; + s_permissionWasDenied = false; + SDL_CameraSpec cameraSpec; + cameraSpec.format = SDL_PIXELFORMAT_NV12; + cameraSpec.colorspace = SDL_COLORSPACE_BT601_LIMITED; + cameraSpec.framerate_numerator = 30; + cameraSpec.framerate_denominator = 1; + cameraSpec.width = CAMERA_WIDTH; + cameraSpec.height = CAMERA_HEIGHT; + const auto camera = SDL_OpenCamera(*s_deviceId, &cameraSpec); + if (camera == nullptr) + { + cemuLog_log(LogType::Force, "Failed to open camera: {}", SDL_GetError()); + return; + } + const auto permission = SDL_GetCameraPermissionState(camera); + if (permission == SDL_CAMERA_PERMISSION_STATE_PENDING) + cemuLog_log(LogType::Force, "Cemu is waiting for permission to access camera"); + s_camera = camera; + } + + static void CloseStream() + { + if (s_camera) + { + SDL_CloseCamera(s_camera); + s_camera = nullptr; + } + } + + static void CaptureWorkerFunc() + { + SetThreadName("CameraManager"); + auto nv12Buffer = std::unique_ptr(new uint8[CAMERA_NV12_BUFFER_SIZE]); + while (s_running) + { + if (!s_camera) + { + std::this_thread::sleep_for(std::chrono::seconds(1)); + std::this_thread::yield(); + continue; + } + if (auto deviceLock = std::unique_lock(s_deviceMutex, std::try_to_lock)) + { + const auto camera = s_camera.load(); + if (!camera) + continue; + if (auto frame = SDL_AcquireCameraFrame(camera, nullptr)) + { + if (frame->format != SDL_PIXELFORMAT_NV12 || frame->w != CAMERA_WIDTH || frame->h != CAMERA_HEIGHT) + { + cemuLog_log(LogType::Force, "Camera format is not NV12 {}x{}", CAMERA_WIDTH, CAMERA_HEIGHT); + s_unsupportedFormat = true; + CloseStream(); + continue; + } + if (SDL_MUSTLOCK(frame)) + SDL_LockSurface(frame); + const auto byteRowCount = (frame->h * 3) >> 1; + for (auto row = 0; row < byteRowCount; ++row) + { + const auto lineIn = static_cast(frame->pixels) + frame->pitch * row; + const auto lineOut = nv12Buffer.get() + CAMERA_PITCH * row; + std::memcpy(lineOut, lineIn, frame->w); + } + if (SDL_MUSTLOCK(frame)) + SDL_UnlockSurface(frame); + SDL_ReleaseCameraFrame(s_camera, frame); + std::scoped_lock lock(s_bufferMutex); + std::swap(s_nv12BufferOut, nv12Buffer); + } + else if (auto permission = SDL_GetCameraPermissionState(s_camera); permission == SDL_CAMERA_PERMISSION_STATE_DENIED) + { + s_permissionWasDenied = true; + CloseStream(); + } + } + else + { + std::this_thread::sleep_for(std::chrono::milliseconds(30)); + } + } + } + + static void ResetBuffers() + { + std::scoped_lock lock(s_bufferMutex); + std::fill_n(s_rgbBufferOut.get(), CAMERA_RGB_BUFFER_SIZE, 0); + constexpr static auto PIXEL_COUNT = CAMERA_HEIGHT * CAMERA_PITCH; + std::ranges::fill_n(s_nv12BufferOut.get(), PIXEL_COUNT, 16); + std::ranges::fill_n(s_nv12BufferOut.get() + PIXEL_COUNT, (PIXEL_COUNT / 2), 128); + } + + static std::vector InternalEnumerateDevices() + { + int deviceCount = 0; + const auto devices = SDL_GetCameras(&deviceCount); + if (devices == nullptr) + { + cemuLog_log(LogType::Force, "Failed to list cameras: {}", SDL_GetError()); + return {}; + } + if (deviceCount == 0) + return {}; + std::vector infos; + cemuLog_log(LogType::Force, "Available video capture devices:"); + for (auto cameraId : std::span(devices, deviceCount)) + { + const auto name = SDL_GetCameraName(cameraId); + std::string strName = name ? name : ""; + cemuLog_log(LogType::Force, "\t{}", strName); + infos.push_back(InternalDeviceInfo{.id = cameraId, .name = std::move(strName)}); + } + SDL_free(devices); + return infos; + } + + static void Init() + { + SDL_InitSubSystem(SDL_INIT_CAMERA); + s_running = true; + s_rgbBufferOut.reset(new uint8[CAMERA_RGB_BUFFER_SIZE]); + s_nv12BufferOut.reset(new uint8[CAMERA_NV12_BUFFER_SIZE]); + + s_captureThread = std::thread(&CaptureWorkerFunc); + + const auto deviceName = GetConfig().camera_id.GetValue(); + if (!deviceName.empty()) + { + for (auto device : InternalEnumerateDevices()) + { + if (device.name == deviceName) + { + s_deviceId = device.id; + } + } + } + ResetBuffers(); + } + + static void Deinit() + { + CloseStream(); + s_running = false; + s_captureThread.join(); + s_nv12BufferOut.reset(); + s_rgbBufferOut.reset(); + SDL_QuitSubSystem(SDL_INIT_CAMERA); + } + + void FillNV12Buffer(std::span nv12Buffer) + { + std::scoped_lock lock(s_bufferMutex); + std::ranges::copy_n(s_nv12BufferOut.get(), CAMERA_NV12_BUFFER_SIZE, nv12Buffer.data()); + } + + static uint8 ClampU8(int v) + { + v = (v > 255) ? 255 : v; + v = (v < 0) ? 0 : v; + return v; + } + + void FillRGBBuffer(std::span rgbBuffer) + { + std::scoped_lock lock(s_bufferMutex); + const auto* yPtr = s_nv12BufferOut.get(); + const auto* uvPtr = s_nv12BufferOut.get() + CAMERA_PITCH * CAMERA_HEIGHT; + auto* rgbPtr = rgbBuffer.data(); + for (auto row = 0; row < CAMERA_HEIGHT; ++row) + { + const auto yRow = yPtr + row * CAMERA_PITCH; + const auto uvRow = uvPtr + (row >> 1) * CAMERA_PITCH; + const auto rgbRow = rgbPtr + row * CAMERA_WIDTH * 3; + for (auto col = 0; col < CAMERA_WIDTH; ++col) + { + const auto _y = 19 * (yRow[col] - 16); + const auto uvCol = col >> 1; + const auto u = uvRow[(uvCol << 1) + 0]; + const auto v = uvRow[(uvCol << 1) + 1]; + rgbRow[col * 3 + 0] = ClampU8((_y + 26 * (v - 128)) >> 4); + rgbRow[col * 3 + 1] = ClampU8((_y - 13 * (v - 128) - 6 * (u - 128)) >> 4); + rgbRow[col * 3 + 2] = ClampU8((_y + 32 * (u - 128)) >> 4);; + } + } + } + + void SetDevice(std::string_view deviceId) + { + std::scoped_lock lock(s_deviceMutex); + CloseStream(); + SDL_CameraID id; + auto [it, ec] = std::from_chars(deviceId.data(), deviceId.data() + deviceId.size(), id); + if (ec != std::errc{}) + return; + s_deviceId = id; + if (s_refCount != 0) + OpenStream(); + } + + void ResetDevice() + { + std::scoped_lock lock(s_deviceMutex); + CloseStream(); + s_deviceId = std::nullopt; + ResetBuffers(); + } + + + void Open() + { + std::scoped_lock lock(s_deviceMutex); + if (!s_running) + { + Init(); + } + if (s_deviceId && s_refCount == 0) + { + OpenStream(); + } + s_refCount += 1; + } + + void Close() + { + std::scoped_lock lock(s_deviceMutex); + if (s_refCount == 0) + return; + s_refCount -= 1; + if (s_refCount != 0) + return; + CloseStream(); + Deinit(); + } + + std::vector EnumerateDevices() + { + std::scoped_lock lock(s_deviceMutex); + std::vector deviceInfos; + for (const auto& [id, name] : InternalEnumerateDevices()) + { + deviceInfos.push_back(DeviceInfo{.uniqueId = fmt::to_string(id), .name = name}); + } + return deviceInfos; + } + + void SaveDevice() + { + std::scoped_lock lock(s_deviceMutex); + const std::string cameraId = s_deviceId ? SDL_GetCameraName(*s_deviceId) : ""; + GetConfig().camera_id.SetValue(cameraId); + GetConfigHandle().Save(); + } + + std::optional GetCurrentDevice() + { + if (s_deviceId) + return std::to_string(*s_deviceId); + return std::nullopt; + } + + State GetState() + { + if (!s_deviceId) + return State::NoDevice; + if (s_permissionWasDenied) + return State::NoPermission; + if (s_unsupportedFormat) + return State::UnsupportedFormat; + if (!s_camera) + return State::NotOpen; + const auto permission = SDL_GetCameraPermissionState(s_camera); + if (permission == SDL_CAMERA_PERMISSION_STATE_PENDING) + return State::NeedsPermission; + return State::Capturing; + } +} // namespace CameraManager diff --git a/src/resource/MacOSXBundleInfo.plist.in b/src/resource/MacOSXBundleInfo.plist.in index ccd1c922..403fedb0 100644 --- a/src/resource/MacOSXBundleInfo.plist.in +++ b/src/resource/MacOSXBundleInfo.plist.in @@ -65,5 +65,7 @@ Viewer + NSCameraUsageDescription + For Wii U Gamepad camera emulation diff --git a/src/resource/cemu.macos.entitlements b/src/resource/cemu.macos.entitlements index 0017e1a9..4c1edbfd 100644 --- a/src/resource/cemu.macos.entitlements +++ b/src/resource/cemu.macos.entitlements @@ -9,5 +9,7 @@ com.apple.security.cs.allow-unsigned-executable-memory + com.apple.security.device.camera +